昔開発したTwitter BotがどこぞやのVPSで動いていて、毎月ムダに数百円払っているので、モダンな環境に移行して節約したい。今どきはServerlessで出来るものはServerlessでやるのがスマート。移行先としてCloud Functionsにしようと思っているので、Twitter Botを動かせるか試してみます。

Cloud Functionを使うなら本家GCPの方だけでも出来るんだけど、プロジェクトの初期化と生成がCLIで完結するのでfirebaseを使います。

Cloud FunctionsでHello World

久しぶりにCloud Functionsをいじるのでやり方を忘れてしまった。このエントリを参考にやりました。

Firebase で Cloud Functions を簡単にはじめよう

プロジェクトを初期化・生成。

$ mkdir cloud-function-twitter-bot
$ firebase init

Cloud Functionsを選択します(スペースで選択)。

Functions: Configure and deploy Cloud Functions
? Please select an option: Create a new project
i  If you want to create a project in a Google Cloud organization or folder, please use "firebase projects:create" instead, and return to this command when you've created the project.
? Please specify a unique project id (warning: cannot be modified afterward) [6-30 characters]:
 () cloud-function-twitter-bot

TypeScript使ったことなかったのでTypeScriptを選択した。

生成されるディレクトリはこんな感じ。

$ tree . -L 2
.
├── firebase.json
└── functions
    ├── node_modules
    ├── package-lock.json
    ├── package.json
    ├── src
    ├── tsconfig.json
    └── tslint.json

生成されたコードをとりあえず実行してみる。

index.ts

import * as functions from 'firebase-functions';

// // Start writing Firebase Functions
// // https://firebase.google.com/docs/functions/typescript
//
export const helloWorld = functions.https.onRequest((request, response) => {
 response.send("Hello from Firebase!");
});

どうやらfirebase serveしてみると、index.jsがねえぞと怒られる。

$ cd functions
$ firebase serve --only functions

=== Serving from '/Users/atsushiharada/source/cloud-function-twitter-bot'...

⚠  Your requested "node" version "8" doesn't match your global version "12"
✔  functions: functions emulator started at http://localhost:5000
i  functions: Watching "/Users/atsushiharada/source/cloud-function-twitter-bot/functions" for Cloud Functions...
⚠  Error: Cannot find module '/Users/atsushiharada/source/cloud-function-twitter-bot/functions/lib/index.js'. Please verify that the package.json has a valid "main" entry
    at tryPackage (internal/modules/cjs/loader.js:228:19)
    at Function.Module._findPath (internal/modules/cjs/loader.js:365:18)
    at Function.Module._resolveFilename (internal/modules/cjs/loader.js:610:27)
    at Function.Module._load (internal/modules/cjs/loader.js:527:27)
    at Module.require (internal/modules/cjs/loader.js:681:19)
    at require (internal/modules/cjs/helpers.js:16:16)
    at /Users/atsushiharada/.nodenv/versions/12.4.0/lib/node_modules/firebase-tools/lib/emulator/functionsEmulatorRuntime.js:661:33
    at Generator.next (<anonymous>)
    at fulfilled (/Users/atsushiharada/.nodenv/versions/12.4.0/lib/node_modules/firebase-tools/lib/emulator/functionsEmulatorRuntime.js:5:58)
    at processTicksAndRejections (internal/process/task_queues.js:89:5)
⚠  We were unable to load your functions code. (see above)
   - It appears your code is written in Typescript, which must be compiled before emulation.
   - You may be able to run "npm run build" in your functions directory to resolve this.

まあTypeScriptだからコンパイル(ビルド?)しなきゃいかんのだろう。

コンパイルとローカル起動を同時にやってくれるコマンドを実行してみる。

$ npm run serve

http://localhost:5001/project_name/region_name/helloWorld にアクセスしたら無事にHello World出来た(serveしたらコンソールに出力されるURL)。

HelloWorld

Cloud FunctionsからTwitterに投稿する

必要なパッケージをインストールする。

$ npm install twitter

HTTPリクエストしたらツイートするコード。

index.ts

import * as functions from 'firebase-functions';
import * as Twitter from 'twitter';

const client = new Twitter({
    consumer_key: '<consumer_key>',
    consumer_secret: '<consumer_secret>',
    access_token_key: '<access_token_key>',
    access_token_secret: '<access_token_secret>'
});

export const doTweet = functions.https.onRequest((request, response) => {
    client.post('statuses/update', {status: 'テストだよ'},  function(error: any, tweet: any, response: any) {
        if(error) throw error;
        console.log(tweet);
        console.log(response);
      });
});

実行してみるもエラー。

$ npm run serve

> functions@ serve /Users/atsushiharada/source/cloud-function-twitter-bot/functions
> npm run build && firebase emulators:start --only functions


> functions@ build /Users/atsushiharada/source/cloud-function-twitter-bot/functions
> tsc

src/index.ts:2:26 - error TS7016: Could not find a declaration file for module 'twitter'. '/Users/atsushiharada/source/cloud-function-twitter-bot/functions/node_modules/twitter/lib/twitter.js' implicitly has an 'any' type.
  Try `npm install @types/twitter` if it exists or add a new declaration (.d.ts) file containing `declare module 'twitter';`

2 import * as Twitter from 'twitter';
                           ~~~~~~~~~


Found 1 error.

雰囲気から察するに、TypeScriptで書かれていない既存ライブラリを呼び出そうとして怒られているのでは。

$ npm install @types/twitter

詳しくは追っていないけど、@typesで始まるモジュールは、既存ライブラリ 型定義やメソッドのインタフェース定義を追加するものっぽい。 投再び起動。

$ npm run serve

無事投稿されていた。

tweet

最終的にはリプライに反応するボットにしたいので、リプライをトリガーに動く仕組みを考える。

そういうケースでは通常はSteaming APIを使うんだけど、まあCloud Functionではムリだよねやっぱり。

https://stackoverflow.com/questions/53334832/how-can-i-have-a-continuous-firebase-cloud-function-for-a-continuous-stream-of-d

なので、1分毎に関数を実行して、新しいリプライを取得する仕組みを想定する。ひとまず、今回は1分毎にツイートするところまでやってみる。

FirebaseのCloud Functionsは、標準の仕組みでCloud Schedulerというものを使ってジョブ実行出来る。関数の定義の仕方を変えるだけで出来る優れもの。

Twitter APIは文面が全く同じツイートを繰り返し出来ないので、1分ごとに適当にランダムな小数を付与したツイートをする。

index.ts

export const doTweet = functions.pubsub.schedule('every 1 minutes').onRun((context) => {
    const ran = Math.random()
    client.post('statuses/update', {status: 'テストだよーーん ' + ran},  function(error: any, tweet: any, response: any) {
        if(error) throw error;
        console.log(tweet);  // Tweet body.
        console.log(response);  // Raw response object.
      });
});

デプロイするとエラーになった。

$ npm run deploy

Error: HTTP Error: 400, Billing must be enabled for activation of service '[cloudscheduler.googleapis.com]' in project '121629411851' to proceed.

Cloud Schedulerを使うには、FirebaseのプランをBlazeに変更して支払情報を設定しておかなければならないので、Firebaseのコンソールから設定しておきます。

もう一回デプロイ。

$ npm run deploy

Error: Cloud resource location is not set for this project but scheduled functions requires it. Please see this documentation for more details: https://firebase.google.com/docs/projects/locations.

関数を置くロケーション(AWSで言うリージョン)を設定しなければならないらしい。これもFirebaseのコンソールから設定出来るので、どこでもいいのだけどasia-northesat1(東京)にしておいた。

Cloud Functionsのロケーション

再びデプロイ。

$ npm run deploy

scheduled tweet

出来たあああ!