TwitterでTILしたらGitHubに草が生える

## はじめに

TIL(Today I Learned): 今日学んだこと

をtwitterのつぶやきで行おうと思いました

1. `#til` というハッシュタグで学びをつぶやく
2. 1日1回自分の投稿の `#til` ハッシュタグを拾いに行く
3. GitHubにコミット

というのがいけないかな〜と思ったのがきっかけです

AWSで `CloudWatch Events` + `Lambda Functions` あたりでできそうな気がしたのでやってみます

## 参考

* [AWS Lambda を Amazon CloudWatch Events に使用する](https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/with-scheduled-events.html)
* [Twitter API Documents](https://developer.twitter.com/en/docs)
* [GitHub REST API v3](https://developer.github.com/v3/)

## 準備

* AWSアカウント
* **Twitter開発者アカウント**
* TILをコミットしていくGitHubリポジトリ

Twitter APIを利用するためには開発者アカウントの登録が必要です

※ 利用目的とか審査とかあってやや面倒です…

## 手順

![til-twitter.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138022/135a71f8-34ce-f069-beec-4a7d6a3d0456.png)

1. Amazon CloudWatch Eventsで定期的にAWS Lambdaを起動
2. LambdaでTwitter APIを叩いて `#til` ツイートを取得
3. `#til` ツイートがあればGitHub APIを叩いてコミットする

アプリケーションの動きとしては上記の流れを想定します

なのでやらないといけないことは、

1. Lambda Functionsの作成
    1. Twitter APIを叩いて `#til` ツイートを取得するjsを作る
    2. GitHub APIを叩いてコミットするjsを作る
    3. Lambdaに登録する
2. 定期的にLambda Functionsを実行するCloudWatch Eventsの作成

という感じになります

### 1. Lambda Functionsの作成

最終的にはこんなjsができあがりました

https://github.com/halnique/til-twitter/blob/master/index.js

色々詰め込み過ぎだし改善の余地は大いにありそうですが、中身を見ていきます

#### 1-1. Twitter APIを叩いて `#til` ツイートを取得する

Twitter開発者アカウントを登録してアプリ作成を行うと、以下の値が得られます

* Consumer API key
* Consumer API secret key
* Access token
* Access token secret

これらを環境変数から設定し、[twitterモジュール](https://github.com/desmondmorris/node-twitter)を利用します

```js
const Twitter = require('twitter');

const twitterClient = new Twitter({
  consumer_key: process.env.API_KEY,
  consumer_secret: process.env.API_SECRET,
  access_token_key: process.env.ACCESS_TOKEN,
  access_token_secret: process.env.ACCESS_TOKEN_SECRET,
});
```

あとはTwitter APIのドキュメントを見ながら、特定のツイートを取得するように実装します

```js
  const nowString = `${now.getFullYear()}-${now.getMonth() + 1}-${now.getDate() - 1}`;
  const params = {
    q: `(from:${process.env.ACCOUNT_NAME}) since:${nowString} ${TARGET_HASHTAG}`,
    count: process.env.MAX_COUNT || 5,
  };

  const tweets = await twitterClient.get('search/tweets', params).catch(() => []);
```

`ACCOUNT_NAME` は自分のTwitterアカウントを環境変数で設定します

最終的には毎日日付が変わったタイミングぐらいで実行させるので、前日以降のツイートに絞っています

※ jsの日付処理貧弱すぎない？みんなmoment.jsとか使うの？

実際はこのあとに各ツイートのハッシュタグを厳密にチェックしてますが、だいたい↑ぐらいでお目当てのツイートは取得できるはずです

#### 1-2. GitHub APIを叩いてPRを作成する

これが地味に大変だった…

CLIなら `git add` して `git commit` して `git push` するだけのかんたんなお仕事ですが、APIでやろうとするとある程度踏み入った理解が必要になります

流れとしては `blob` を作って `tree` を作って `commit` を作って `HEADのSHAを書き換える` ということになります

```js
    const prevRefsHead = await getRefsHead();
    const commitSha = prevRefsHead.object.sha;
    const prevCommit = await getCommit(commitSha);
    const blob = await postBlob(data[i]);
    const tree = await postTree(prevCommit.tree.sha, blob.sha, i + 1);
    const commit = await postCommit(commitSha, tree.sha, i + 1);
    await patchRefsHead(commit.sha);
    await sleep(1);
```

中身はそれぞれ対応したAPIを実行しているだけですが、こちらもドキュメントとにらめっこしながらパラメータと流れを調節しました

※ 連続して実行したときにコミットがうまくいかないことがあったので、1件ずつsleepするようにしてます


#### 1-3. Lambdaに登録する

jsは何度もお試し実行すると思うので、ローカルでDockerとかで書くのがよいです

jsができあがったらLambdaに登録していきます

注意点として、Lambda上で外部モジュールは基本的にそのまま `require` することはできません

今回でいうと

```js
const Twitter = require('twitter');
```
ですね

これは事前に `Lambda Layers` として作成しておくことで解決できます

<img width="676" alt="image-lambda-layers1.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138022/2e9aa15b-def7-c91d-4594-9f3998f6e920.png">

zipファイルを直接アップロードするか、 `S3` に上げておいてそれを利用することができます

zipファイルの中身は注意が必要で、例えば `modules.zip` を解凍したときに以下の構成になっている必要があります

```
$ ls -1 modules/
node_modules/
```

`node_modules` の中に利用したいモジュールが入っているイメージです

この構成になっていないと、Lambda Functionsの方で利用するのにうまくいきません

先にLambda Layersを作ったら、Lambda Functionsを作成していきます

<img width="1298" alt="image-lambda1.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138022/b8dd29db-e21d-c983-3d7c-7f6bb753059d.png">

ランタイムはLambda Layersと同じになるようにします

Lambda FunctionsもzipファイルをアップロードしたりS3のファイルを利用することができますが、今回はそのまま作成したコードを貼り付けます

<img width="1316" alt="image-lambda2.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138022/6f382aa2-985a-f525-3aa6-cd5f247c1789.png">

環境変数をたくさん使うので、ぽちぽち登録します

<img width="1312" alt="image-lambda3.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138022/0dea0f56-5815-2003-3084-f3863e24b9bf.png">

タイムアウト設定をデフォルトの3秒 -> 30秒に変更しておきます

<img width="637" alt="image-lambda4.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138022/13e3e1c0-fdf0-6b56-4072-5a15deac123c.png">

最後にLambda FunctionsにLambda Layersを追加すればOKです

<img width="712" alt="image-lambda5.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138022/0c59cb12-7503-4b35-aa99-5e4440970444.png">

右上から適当なテストイベントを作ってテストしてみて、無事に動けばLambdaとしては完成です

### 2. 定期的にLambda Functionsを実行するCloudWatch Eventsの作成

<img width="1208" alt="image-cloudwatch-events3.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138022/78eb1cbe-f97e-19aa-3046-1d5d2bad6dff.png">

<img width="1214" alt="image-cloudwatch-events2.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138022/67f7d0b4-5b44-2162-791e-7d47c45826e8.png">

以上

簡単に設定できました

Cron式については、日付が変わったころに前日分のツイートを取得するような感じで実行されるようにします

CloudWatch EventsのCron式で実行されるイベントは、UTCで誤差1分以内だそうです

前日に `#til` ツイートをしていれば、日本時間でだいたい翌日の朝9時頃にコミットができあがる想定ですね

## 実行結果

<img width="600" alt="image-tweet1.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138022/ba194752-a7a1-6063-2fba-e99b47f04fe0.png">

<img width="998" alt="image-commit1.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138022/f8b60885-94a4-5e84-499d-27842d21a2fe.png">

コミットされました

これで無事にツイートするだけで草が生える環境ができあがりました

## 学び

* node_modulesを `Lambda Layers` に追加しておくことで、Lambda Functionsで使えるようになって便利
* Gitのコミットができるまでの流れ [10.2 Git Internals - Git Objects](https://git-scm.com/book/en/v2/Git-Internals-Git-Objects)
* `CloudWatch Events` のスケジュールでCron式を使う場合、日と曜日のどちらかは `?` にする必要がある

## Todo

* Lambda FunctionsとLambda Layersへのデプロイを `GitHub Actions` で自動化したい人生だった…
* GCPで `Cloud Functions` と `Cloud Scheduler` でも似たようなことができそうなのでやってみたい

## まとめ

GitHubの草が生えているからといって活発に開発をしているとは限らないぞ