IBM Cloud Functionsとwebpackを使って定期的につぶやくTwitter botを作成する

# きっかけ

※興味ない人は読み飛ばしてもらって結構です。

先日、とある社会実験に使用するプログラムの最終チェックをしている際、バックエンド部分に重大な不備を発見し、バグ特定作業と修正作業で1日中対応に追われたことがありました。

夜遅く、ようやくチェックが終わり帰れることになったのですが、その頃には研究室は私一人、ただ黙々と帰るだけ。ねぎらいの言葉を欠けてくれる人もいません（そもそも研究室は内気な人が多いのでほとんど会話しないのですが）。

憂鬱な気分に浸ったまま、帰宅途中にTwitterのTLを眺めていて、ふと思いました。

「1日の活動レポートを自動でツイートしてくれる機能があればいいなあ」

こうして、ねぎらいの言葉をかけてくれる人がいないなら、それを自動で行うプログラムを作ればよいという発想に至ったのです。

# 作るもの

GitHubのプロフィールページから1日ごとのContribution数を取得して、それをレポートにまとめて毎日午後9時に自動でツイートします。

FaaSはIBM Cloud Functions, 言語はNode.jsを使用します。

IBM Cloudは無料トライアル期間がなく、代わりに機能が制限された無料アカウントを作成することにより無料で利用を続けることが出来ます。
クラウドサービスを気軽に利用したい方にオススメです。

# 必要なもの

- Node.js (手元でテストする用)
- IBM Cloudのアカウント
- IBM Cloud CLI
- IBM Cloud Function Plugin for CLI (https://cloud.ibm.com/functions/learn/cli)

# 手順

## 1. Twitter API 登録

↓このあたりの記事を参考にしながら、Twitter APIの認証コードを取得します。

https://qiita.com/kngsym2018/items/2524d21455aac111cdee

昨年度からTwitter APIの申請が厳しくなったという話を聞いていたのですが、私の場合は申請したら一瞬で認可されました。使用条件によるのでしょうか？

## 2. プロジェクトを作成

普通のNode.jsのプログラムを作成する手順と同じです。

```console
$ mkdir <package name>
$ cd <package name>
$ git init
$ yarn init -y
```

Package.jsonは特に書き換える必要はありません。

## 3. プログラムを記述する

IBM Cloud Functionsでは、指定されたtriggerが発動された際、main関数をエントリポイントとしてプログラムを実行するので、main関数をexportしておきます。

```javascript:main.js
const https = require('https');
const parser = require('node-html-parser');
const Twitter = require('twitter');

/**
 * GitHubのプロフィールページから直近num日のContribution数を取得する
 * @param {Number} num Contribution数を取得する日数
 * @return {Array} 直近num日のContribution数。日付降順
 */
function fetch(num) {
  const url = "https://github.com/<GitHub アカウント名>";

  return new Promise((resolve, reject) => {
    https.get(url, (res) => {
      let body = "";
      res.setEncoding("utf8");

      res.on("data", (chunk) => {
        body += chunk;
      });

      res.on("end", () => {
        const html = parser.parse(body);
        const rects = html.querySelectorAll("svg.js-calendar-graph-svg g rect").slice(-num);
        const counts = Array.prototype.map.call(rects, (v) => {
          return v.attributes['data-count'];
        });
        resolve(counts.reverse());
      });
    }).on("error", (e) => {
      reject(null);
    });
  });
}

/**
 * 指定された文字列をツイートする
 * @param {String} content ツイートする内容
 * @param {Object} settings Twitter API の認証情報
 */
function post(content, settings) {
  const client = new Twitter(settings);
  const params = {"status": content};
  console.log(params);
  return new Promise((resolve, reject) => {
    client.post('statuses/update', params, (err, tweet, response) => {
      if (err) {
        reject(err);
      } else {
        resolve(response);
      }
    });
  });
}

/**
 * エントリポイント
 */
async function main(params) {
  try {
    const counts = await fetch(2);

    const content = [
      "*** Contribution Report ***",
      "",
      "Today's contribution: " + counts[0],
      "Yesterday: " + counts[1]
    ].join("\n");
    console.log(content);

    const api_settings = {
      consumer_key: params.consumer_key,
      consumer_secret: params.consumer_secret,
      access_token_key: params.access_token_key,
      access_token_secret: params.access_token_secret
    };
    const response = await post(content, api_settings);
    console.log(response);
  } catch (err) {
    console.log(err);
    return err;
  }
}

module.exports = main;
```

ここで、main関数の引数paramsに注目します。paramsはIBM Cloud Functionsによって渡される値で、actionの設定画面などで値を設定することが出来ます。

paramsの値の設定方法は後述します。

## 4. webpack の設定を書く

```webpack.config.js
var path = require('path');
var HardSourceWebpackPlugin = require('hard-source-webpack-plugin');

module.exports = {
  mode: 'production',
  entry: './main.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'bundle.js',
    library: 'main',
    libraryTarget: 'global'
  },
  target: 'node',
  plugins: [
    new HardSourceWebpackPlugin()
  ]
};
```

outputの項目が肝です。

IBM Cloud Functionsではソースを読み込んだ後、global.mainを実行します。そのため、libraryTargetを`global`にしてexportされた関数をglobal.mainに代入するようにしています。

## 5. bundle, deployする。

```console
$ yarn webpack --config webpack.config.js
$ ibmcloud fn action update <action名> dist/bundle.js --kind nodejs:10
```

## 6. Actionの設定をする

[Actionsの管理画面](https://cloud.ibm.com/functions/actions)を開き、Actionがアップロードされていることを確認します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/171615/116859b0-83ef-1f08-4495-a53937b0131e.png)

該当するaction名をクリックし、左側のパネルから「Parameters」という項目をクリックして、必要なパラメータを追加します。

![ibm_cloud_functions_params.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/171615/0954e3e3-8e49-60e0-f7b6-76046aff69e7.png)

必要最低限の設定はこれだけです。ちなみに、parametersはコマンドラインからも設定することが出来ます。

```console
ibmcloud fn action update <action名> dist/bundle.js --kind nodejs:10 --param-file params.json
```

## 7. Triggerを設定する

[Triggersの管理画面](https://cloud.ibm.com/functions/triggers)を開き、CreateボタンでTriggerを新規作成します。

Create Trigger > Periodicと進み、Triggerの設定に移ります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/171615/b7bd64f8-1c87-ca7b-8d30-845f61cdd13a.png)

UTC時刻で記述してくださいとあるので、それに従います。

例えば日本時間で毎日午後9時にTriggerを発動させたい場合は、以下のようにCron Patternを設定します。

```c
00 12 * *
```

Createボタンをクリックすると、以下のような画面が表示されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/171615/1cd59a7b-6b55-f5ad-676a-9463f4a64d10.png)

現在はTriggerを設定しただけなので、実際の処理(Action)と結びつけます。

右上のAddというボタンをクリックして、Select Existingから既存のActionを選択します。

# さいごに

これで、一日のContributionレポートが自動でツイートされるようになりました。

これにより、苦労した日はその分のContribution数がツイートされ、「今日は頑張ったんだな」という実感が湧くようになりました。

さらに1日のContribution数がゼロだとフォロワーから冷たい視線を浴びせられるので、毎日コミットせざるを得ない環境に自分を追い込むことが出来ます。

今後はContribution数の推移グラなども作ってみたいと思います。

皆さんも毎日コミット頑張りましょう！

今回作成したプログラムのソースコードは[こちら](https://github.com/Kogia-sima/twicon)に置いてあります。