Discord用に誕生日通知botを作る

# 何のために生まれて
特定の日付にDiscordで通知してほしいな、ってことがよくある。ソシャゲのキャラの誕生日とか、TRPGセッションの予定日とか。
サーバー立ててプログラム書いてBotトークン貰ってサーバーに招待して... 面倒くさい。そこまでしたくない。
でも誕生日限定ボイスは聞きたい。キャラ設定は念入りにしたい派。そういう人のための。
# 何をして生きるのか
0. Googleスプレッドシートに日付と予定を入れておく。
0. 毎朝GoogleAppsScriptを自動起動、その日の予定を確認する。
0. 予定の内容をwebhook経由でDiscordへ送信

# 各サービスの詳細
##### Googleスプレッドシート
Google版Excelみたいなやつ。ブラウザで編集、Googleドライブに保存される。無料だよ。
##### GoogleAppsScript
略してGAS。自動処理を書くための言語で、ExcelでいうところのVBA、つまりマクロ。
一日一回実行、みたいな設定ができるので今回はそれを使う。
##### DiscordのWebhook
Discordの用意してくれるURLに引数付きでアクセスするとbotの投稿として処理してくれる。（正確に言うとPOSTでJSONを投げるのだけど）
`"ピザを食べるために生産ラインなんて必要ない お電話一本ですぐにお届け"` 的な最高のサービス

# さて、やってみようか
以下はソシャゲ(ブルアカ)の誕生日botを例にやってます。
### 1. DiscordのWebhookを作る
Discordで「ウェブフックの管理」って権限がいるので注意。
`サーバー設定 > 連携サービス > ウェブフック` で新しいWebhookを追加できる。urlは後で使う。
（Discordの設定画面は頻繁に変わるから正しいことは[公式](https://support.discord.com/hc/ja/articles/228383668)で）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1667893/f74926a2-33ce-e870-ca00-9ba341f55e73.png)

### 2. Googleスプレッドシートを作る
Googleドライブの適当な場所で右クリ→Googleスプレッドシート
そうしたら名前と誕生日を記入する。
後のために、日付を三列目に、名前はその左隣、シート名はbirthday_list、みたいな感じに決めておく。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1667893/a22fa6a0-8c81-887a-2b87-5fc6f9dd9f19.png)

### 3. GASを書く
Googleスプレッドシートのメニューバーから、`ツール > スクリプトエディタ`でGASの編集画面が出てくる。
コードはこんな感じ。

```javascript
// ウェブフックのURLリスト(さっきコピーした奴)
const Birthday_Notice_Webhook_List = [
  "https://discord.com/api/webhooks/???????????/?????????_?????????????????????????????????"
];
// botメッセージの設定
const Birthday_Notice_Webhook_Payload = {
  "username": "誕生日bot", // メッセージの送信者名
  "content": "今日%sは%sの誕生日です！" // メッセージ本文
  // "avatar_url": でアイコン設定とか、他にもいろいろできる。
  // (https://discord.com/developers/docs/resources/webhook#execute-webhook)
};


// この関数を毎日0-1時(JST)に起動
function main_everyday() {
  checkBirthday();
}


function checkBirthday() {
  // 検索対象の列番号(3列目=C列)
  const Day_Column = 3;

  // "birthday_list"シート
  var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("birthday_list");

  // 今日の日付
  today = Utilities.formatDate(new Date(), "JST", "M/d");
  Logger.log(`today: ${today}`);

  // 日付を検索
  var textFinder = sheet.createTextFinder(today);
  textFinder.matchEntireCell(true) // 全体一致
  var ranges = textFinder.findAll();
  Logger.log(`検索結果: ${ranges.length}件`);

  // 該当セルをひとつづつ
  for (var i = 0; i < ranges.length; i++) {
    if (ranges[i].getColumn() == Day_Column) {
      var webhook_payload = Object.assign({}, Birthday_Notice_Webhook_Payload); // オブジェクトの参照渡しを回避
      // メッセージに日付と名前を埋め込み
      webhook_payload["content"] = Utilities.formatString(
        Birthday_Notice_Webhook_Payload["content"],
        today, ranges[i].offset(0, -1).getValue() // 左隣の値=名前
      );
      // メッセージを送信
      postWebhook(Birthday_Notice_Webhook_List, webhook_payload);
    }
  }
}

function postWebhook(url_list, payload) {
  var options = {
    "method": "POST",
    'contentType': 'application/json', // これ忘れがち
    "payload": JSON.stringify(payload)
  };
  Logger.log(`Payload for webhook: ${options["payload"]}`);
  for (var i = 0; i < url_list.length; i++) {
    // HTTPリクエストを送る
    var r = UrlFetchApp.fetch(url_list[i], options);
    Logger.log(`Response from webhook: (${r.getResponseCode()})${r.getContentText("UTF-8")}`);
  }
}

```
たぶん途中でアクセス許可みたいな画面が出るので、スクリプトが信用できそうなら許可。
コードができたら左の時計マークからトリガーを設定する。
実行はmain_everyday、イベントのソースを時間主導型、日付ベースの午前0時～1時で保存。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1667893/93a8c05c-2aff-a36e-d677-071c5e091473.png)

これでもう、推しの誕生日ボイスを聞き逃す心配はない。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1667893/5aa29ba4-be38-b2fa-5274-df2062d0ba73.png)

# あとがき
Webhookは送信のみの一方通行なので、できることは限られる。でもそっちのほうが、セキュリティ的にも安心だよね。
コードが汚いのは手軽さ重視ってことで、言い訳にならない？無理か。
(2021-07-01) 検索が部分一致になっていたので全体一致検索に修正しました