Githubに草を生やすだけのLineBotを作る

# なぜ作ったのか

- Githubの草（コントリビューション）を生やして一面緑にしてみたくなった。
- 毎日コミット大変なので、まずは手軽に草生やせるようにしたい。
- じゃあLineBot経由で草生やせるようにしよう。
- ~~結果、LineBotでメッセージ送るのも忘れていた今日この頃。~~

## 作ったもの

- bot画面
    - シンプルに「草生える」ボタンを用意しただけです。

<img width="200" alt="bot1" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/203410/606c72f2-1c34-3f3f-8239-760c16e727ae.jpeg">

- この時のコントリビューション
    - 生えてなさすぎぃ＼(^o^)／

![github_contribute_prev.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/203410/c49c20d8-354d-4749-f50d-155e91de348f.png)

- 生やすためにBotの「草生える」ボタンを押すと「草生える」とメッセージを送信します。
    - Botから返事がきます。

<img width="200" alt="bot2" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/203410/834c0dc4-4968-b17f-eb2b-a19763fb43c1.jpeg">

- 30秒～1分ほど待つとBotから連絡がきます。

<img width="200" alt="bot3" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/203410/cc854e81-62a7-d422-e0a5-853ff6be1a09.jpeg">

- 再度コントリビューションを確認してみます。
    - コミットされたことが確認できます（草生えました！）
    
![github_contribute_next.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/203410/9a915c61-7bb0-6171-4dde-7c3567c56654.png)

- コミット差分はこんな感じです。
    - ファイル内の日付をBotが実行された時間に更新しているだけです。
    
![github_commit.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/203410/a4ff2366-a4dd-127c-bc09-bb8f5724cc91.png)


- ちなみに「草生える」以外のメッセージを送ると草生やしてオウム返ししてきます。
    - メッセージ以外を送った場合は何もしません。
    
<img width="200" alt="bot4" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/203410/eec43792-c1e0-041b-64c9-42222f3fe0a0.jpeg">





# 構成

- LineBot + Cloud Functions for Firebase + Github API
    - LineBotはフリープラン
    - FirebaseはBlazeプラン（従量制）

構成自体はシンプルです。
Cloud Functions for Firebase上でBotの処理を実装しています。
特定のワード「草生やす」を含むメッセージをを受け取った場合にGithubApiを利用して、特定のリポジトリのファイルを上書きしています。

LineBot、Firebaseの環境や設定については特に大したことをしていないので割愛します。
そのあたりについては実装時に参照した記事のリンクを載せておくのでそちらを参照してください。

## Cloud Functions for Firebase

今回Botの実装はFirebase Functionを利用しました。Firebase FunctionはデプロイするとAPIのURLが発行されるので、LineBotのWebHookにも利用できます。

### コード

<details><summary>main.ts</summary><div>

expressを使用してHTTPリクエストを受け取れるようにしています。
今回は`webhook`エンドポイントでLineBotのイベントを受け取るように実装しています。

LineBotのイベントの処理は`handleEvent`関数で定義しています。今回はメッセージイベントかつ文字列メッセージの場合にのみ反応するコードしか記載していません。スタンプや画像でも何かしら処理したい場合はメッセージタイプ毎に処理を実装する必要があります。

```typescript
"use strict";
import * as functions from "firebase-functions";
import * as express from "express";
import * as line from "@line/bot-sdk";
import { gitCommitPush } from "./github";

const midllewareConfig: line.MiddlewareConfig = {
  channelSecret: functions.config().line.channel_secret, // トークン系はFirebase上で管理する環境変数に設定しています。
  channelAccessToken: functions.config().line.channel_token
};
const clientConfig: line.ClientConfig = {
  channelSecret: functions.config().line.channel_secret,
  channelAccessToken: functions.config().line.channel_token
};

const client = new line.Client(clientConfig);
const app = express();

// LineBotのWebHook
app.post("/webhook", line.middleware(midllewareConfig), (req, res) => {
  console.log(req.body.events);
  res.send("receive event");
  Promise.all(req.body.events.map(handleEvent)).catch(e => {
    console.error(e);
  });
});

app.get("/", (req, res) => {
  res.send("ok");
});

// LineBotのeventを処理する関数
async function handleEvent(event: line.MessageEvent) {
  if (event.type !== "message" || event.message.type !== "text") {
    return Promise.resolve(null);
  }

  if (event.message.text.includes("草生やす")) {
    // github に草生やす

    // リプライでメッセージを返す
    await client.replyMessage(event.replyToken, {
      type: "text",
      text: `草はやしたるわｗｗ`
    });

    // github上のリポジトリにコミットする
    const message = await growGrassToGithub();

    // コミット成功時はPushメッセージで送信する
    return client.pushMessage(event.source.userId!, {
      type: "text",
      text: message
    });
  } else {
    // その他のメッセージはオウム返し
    return client.replyMessage(event.replyToken, [
      {
        type: "text",
        text: "よくわかんねぇからオウム返しとくわｗｗ"
      },
      {
        type: "text",
        text: event.message.text + "www"
      }
    ]);
  }
}

// githubに草を生やす関数
async function growGrassToGithub(): Promise<string> {
  try {
    await gitCommitPush({
      token: functions.config().github.api_token,
      owner: "menom018",
      repo: "growgrass",
      file: {
        path: "GrowGlass.md",
        content: `草生やしたったwww ${new Date().toLocaleString()}`
      },
      branch: "master",
      commitMessage: "grow grass for bot"
    });

    console.log("success grow glass");
    return `草生やしたったwww`;
  } catch (error) {
    console.error(error);
    return `草生やすの失敗したったwww`;
  }
}
exports.app = functions.https.onRequest(app);

```
</div></details>

<details><summary>github.ts</summary><div>

githubのリポジトリにコミットする処理を記載しています。
githubAPIでコミットするには以下の手順を踏む必要があります。

1. Refを取得する。
1. Commitを取得する。
1. Blobを作成する。
1. Treeを作成する。
1. Commitを作成する。
1. Refを更新する。


```typescript
"use strict";
interface GithubPushConfig {
  token: string;
  owner: string;
  repo: string;
  file: {
    path: string;
    content: string;
  };
  branch: string;
  commitMessage: string;
}

import * as GitHubApi from "@octokit/rest";

const gitCommitPush = async (config: GithubPushConfig) => {
  const gh = new GitHubApi({
    auth: config.token
  });

  const ref = await gh.git.getRef({
    owner: config.owner,
    repo: config.repo,
    ref: `heads/${config.branch}`
  });
  const parentSha: string = ref.data.object.sha;
  const parentCommit = await gh.git.getCommit({
    owner: config.owner,
    repo: config.repo,
    commit_sha: parentSha
  });
  const createBlob = await gh.git.createBlob({
    owner: config.owner,
    repo: config.repo,
    content: config.file.content,
    encoding: "utf-8"
  });
  const createTree = await gh.git.createTree({
    owner: config.owner,
    repo: config.repo,
    base_tree: parentCommit.data.tree.sha,
    tree: [
      {
        path: config.file.path,
        sha: createBlob.data.sha,
        mode: `100644`,
        type: `blob`
      }
    ]
  });

  const createCommit = await gh.git.createCommit({
    message: config.commitMessage,
    owner: config.owner,
    repo: config.repo,
    parents: [parentSha],
    tree: createTree.data.sha
  });

  const updateRef = await gh.git.updateRef({
    owner: config.owner,
    repo: config.repo,
    ref: `heads/${config.branch}`,
    sha: createCommit.data.sha
  });

  console.log("commit success:", updateRef.data);
};

export { gitCommitPush };


```
</div></details>

# 開発時につまづいた点

- LineBotのReplyTokenの有効期限が短い
    - GithubAPIでコミットするまでに40～1分前後かかるため、コミット完了後にリプライメッセージを送るとすでにトークンの有効期限が切れているため、メッセージ送信に失敗していました。
    - そのため、Event発火時にすぐにリプライメッセージを返すように変更し、コミット完了後にPushメッセージで完了メッセージを送信するようにしました。
    - ただし、Pushメッセージは1000通/月の制限があるため、毎月草生やしすぎると、コミット完了メッセージが送信できなくなります。
    - ReplyTokenの有効期限は公式には名言されていないようですが、有志の方の調べによると30秒程度のようです。
- Firebaseは無料プランでは外部APIを実行できない
    - Firebaseは無料プランの場合、アプリから外部APIを実行できないため、無料プラン以外にする必要があります。
    - 従量制であるBlazeプランは無料利用枠もあるため、個人で利用する場合には十分無料枠の範囲で利用できると思います。
    
# まとめ

- ただただGithubのコントリビューションに草を生やすだけの~~クソ~~Botをを作りました
    - 調べ物含め、3～4時間程度で作成できました。
- ~~モチベーションがあれば~~今後機能追加もしていき、TIL(Today I Learned)をLineBotからでも出来るようにしていきたいと考えています。
    - 固定メッセージのコミットではなく、LineBotに送信したメッセージをコミットする。
    - コミット先のリポジトリやブランチの変更
    - コミットするファイルパスやファイル名の変更
- Firebaseは個人的なアプリをサクッと作る分には大変便利。
    - 環境構築からデプロイまで Firebase CLIでお手軽にできます。
   
# 参考文献

- [FirebaseでLINE botを作ってみた](https://qiita.com/vedokoy/items/e996d7e2d5e8baa93dac)
- [FirebaseでNode.jsを動かしてLINE BOTを作る](https://qiita.com/vedokoy/items/e996d7e2d5e8baa93dac)
- [【草はやしてる？】意外と知らないGitHubで草を生やす条件とは](https://findy-code.io/engineer-lab/github-contributions-rule)
- [GitHubのGit Data APIでコミットを作成する](https://int128.hatenablog.com/entry/2017/09/05/161641)
- [LINE Messaging API : Reply API におけるリプライトークンの有効期限は30秒 (独自調べ)](http://neos21.hatenablog.com/entry/2019/02/28/080000)