承認依頼が来たらGoogleChatに通知するGoogleAppsScriptを作成する

## はじめに

みなさんこんにちは
株式会社BTMでグループ長をしている島田です
突然ですがみなさんの会社でも勤怠管理やワークフロー、経費精算等々で申請を行い、上長に確認してもらう機会は多いのではないでしょうか
確認する上長からするとメンバーから来た申請類は遅れなく承認したいのですが、未承認の申請があるかどうかを複数サービスに頻繁に見に行けるわけではありません
タイミングを逃してしまうと承認が遅れてしまう可能性があるため、**複数サービスの承認依頼**を**そこそこ迅速**に気づける仕組みをGoogleAppsScriptを使って構築しようと思います

## 処理の大枠の流れ

大体のサービスで申請が来るとメールが飛んでくるかと思います
このメールを元に、GoogleChatに通知する構想です

1. Gmailの機能でフィルターを設定し、メール件名を見て承認依頼メールであれば自動でラベルを付与・重要マークを付ける
2. 1時間に1回Google Apps Scriptを走らせ、特定のラベルが付与されており重要マークがついているメールを検索
3. 条件に合致するメールがあればその件名をGoogleChatに投稿、重要マークを外す

上記の流れで実現させました。それぞれ実施した詳細を記載していきます

## Gmailのフィルター機能設定

Gmailの設定で特定の条件に合致するメールに自動的にラベルと重要マークをつけるフィルターを作成していきます
このフィルターやラベルは今回に限らず結構便利なのでよく使いますね

1. Gmailにてラベルを作成しておく(今回は「承認依頼」といったラベルを作成しました)
2. Gmailの右上の設定→「すべての設定を表示」からフィルター設定を行う
3. フィルタとブロック中のアドレスを開き、新しいフィルターを作成を押下する
4. ここのフィルター設定でシステムから送信される承認依頼メールがヒットするような設定を行います
私の場合、件名を以下のように設定しました

    ```
    "[サービスA] 承認依頼" OR "[サービスB] 承認依頼" OR "【サービスC】経費精算"
    ```

5. ラベルを付けるにチェックを入れ、1で作成したラベルを選択
6. 常に重要マークを付けるにもチェックを入れ、フィルタを作成を押下

これでひとまず承認依頼系のメールにラベルと重要マークが付くようになりました

## 通知先のスペース作成・webhook発行

GoogleChatで自分のみのスペースを作成し、アプリと統合からWebhookを追加を押下します
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3824500/3eb5651b-b48b-4086-b8e3-991d010aa824.png)

webhookの名前は何でも良いのですが、ここで入れた名前でGoogleChatにメッセージが投下されます
保存するとURLが発行されます。このURLに対してHTTPリクエストを送るとメッセージが投下される仕組みとなっています
今回は通知先をGoogleChatにしましたが、Slackでも同様の仕組みはあるため実現可能です

## Google Apps Scriptの作成

そもそも[Google Apps Script](https://developers.google.com/apps-script?hl=ja)(GAS)を知らない方もいらっしゃるかもしれませんが
Googleが提供する無料のスクリプトプラットフォームで、JavaScriptのような言語を用いて処理を記載・実行することが出来ます

ブラウザ上で使えるエディターが備わっており環境構築も不要です
強みはGmailやスプレッドシートなどのGoogleサービスとの連携でしょうかね
スクリプトは処理時間制限があったりパフォーマンスは良いものではなかったりするのですが、ちょっとこういう機能欲しいな…みたいな時に役に立ちます
そんなわけで、今回はGASを使って処理を記載していきます

1. Googleドライブの適当な場所で右クリック→その他→Google Apps Scriptを押下でファイルを作成する
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3824500/3291aa7d-74a8-4344-b652-997a1584c843.png)
2. 作成したファイルを開くとWebエディターが出てくるのでコードを書いていきましょう

```javascript
const WEBHOOK_URL = 'https://chat.googleapis.com/v1/spaces/・・・・・'; // ここにWebhookURLを貼り付け

function checkMail() {
  const query = 'label:承認依頼 is:important'; // Gmailで作成したラベル名を設定
  const threads = GmailApp.search(query, 0, 100); // 指定したラベルと重要マークがついているスレッドを100件検索

  if (threads.length === 0) {
    Logger.log('通知対象無し');
    return;
  }

  // 承認依頼のメール件名一覧を保持
  const notificationSubjects = threads.map((thread) => thread.getFirstMessageSubject());

  // webhookへの送信データを作成していく
  const data = {
    'text': '承認依頼が来ています。\n' + notificationSubjects.join('\n')
  };
  const options = {
    'method': 'post',
    'headers' : {
      'Content-Type': 'application/json; charset=UTF-8'
    },
    'payload': JSON.stringify(data)
  };
  const response = UrlFetchApp.fetch(WEBHOOK_URL, options);
  const statusCode = response.getResponseCode();
  if (statusCode === 200) {
    Logger.log(`${notificationSubjects.length}件通知成功`);

    // 重要マークを外す
    threads.forEach((thread) => {
      GmailApp.markThreadUnimportant(thread);
    });
  } else {
    Logger.log(`送信失敗 HTTP Status Code: ${statusCode}`);
    Logger.log(response.getContentText());
    Logger.log(notificationSubjects);
  }
}
```

↑のコードをコピペし、ご自身のWebhookURLとラベル名だけ変更すればokなはずので、保存し動作確認に入ります
Gmailで適当なメールに承認依頼のラベルを付与し、重要マークをつけます
その後、Webエディターの「実行」を押下すると処理が実行され
うまく行っていればGoogleChatにメッセージが投下されるはずです

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3824500/d922b4f8-2bda-4570-900a-e084136a80e0.png)

3. 定期実行化
この処理実行を定期実行設定していきます
Webエディターのサイドメニューのトリガーを押下します
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3824500/a2b24929-ccf1-4e28-9e43-8646156aea04.png)
何をきっかけに処理を実行するか？を設定する画面になります
トリガーを追加を押下して以下を設定すると、1時間に1回処理が実行されるようになります
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3824500/05a84654-6c47-40e1-870a-a481f22ff46b.png)

以上で快適な承認ライフを送ることができるようになりましたね

## 処理詳細

上から順に軽く処理を解説して行ければと思います

### Gmail検索処理

```javascript
  const query = 'label:承認依頼 is:important';
  const threads = GmailApp.search(query, 0, 100);
```

GASでGoogleのサービスと連携したい場合、サービス毎にAPIが用意されているため利用していきます
今回はGmailのため、GmailAppに存在するメソッドを呼び出して検索をかけています
やっていることは単純で、queryに記載の「ラベルが承認依頼」「重要マーク有り」の条件で100件メールのスレッドを検索しています
1時間で100件以上の承認依頼が来る人気者の上長は上限を変えたほうが良さそうです
GmailAppのリファレンスは以下にあります
https://developers.google.com/apps-script/reference/gmail/gmail-app?hl=ja

### 通知対象件名取得処理

```javascript
  const notificationSubjects = threads.map((thread) => thread.getFirstMessageSubject());
```

条件に引っかかるメールのスレッドがthreads変数にあるため、そのスレッド情報から件名を取得し配列に保持しています
この件名をGoogleChatに投稿していきます

### GoogleChat投稿処理

```javascript
  const data = {
    'text': '承認依頼が来ています。\n' + notificationSubjects.join('\n')
  };
  const options = {
    'method': 'post',
    'headers' : {
      'Content-Type': 'application/json; charset=UTF-8'
    },
    'payload': JSON.stringify(data)
  };
  const response = UrlFetchApp.fetch(WEBHOOK_URL, options);
```

GoogleChatで発行したWebhook URLに対してPOSTすることによりメッセージの投下を実現させています
data変数にはGoogleChatにどのようなメッセージを投下したいかを設定しており、指定できるペイロードは以下に記載があります。結構色々ありますね
https://developers.google.com/workspace/chat/api/reference/rest/v1/spaces.messages?hl=ja

### ステータスコード確認・重要マークを外す処理

```javascript
  const statusCode = response.getResponseCode();
  if (statusCode === 200) {
    Logger.log(`${notificationSubjects.length}件通知成功`);

    // 重要マークを外す
    threads.forEach((thread) => {
      GmailApp.markThreadUnimportant(thread);
    });
  } else {
```

お察しかもしれませんが、この重要マークはGoogleChatに通知済みかどうかの判定に使用しています
これが無いと以前来たメールを永遠に通知し続けることとなってしまいます
重要マークではなく未読/既読で判定する方法もあるんですが、スクリプトに勝手に既読にされるのは気に入らないので、重要マークを使った形としました

GoogleChatの投稿には成功したが、重要マークを外す処理で失敗した場合には1時間後にも再度同じ通知が飛んでしまいますが
重要マークを外す処理の後にGoogleChat投稿処理を入れてしまうと、重要マークを外す処理には成功しGoogleChat投稿に失敗した場合、再度リトライしても重要マークが外れていることから永遠に投稿されなくなってしまいます
そのため安全なほうのGoogleChat投稿→重要マークを外すといった流れとしております
まぁ万が一申請に気づかなくてもメンバーが優しくリマインドしてくれるとは思いますけどね

処理詳細は以上となりますが、この程度の記述量で承認依頼メールが来たらGoogleChatに投稿する機能が実現できました

GASはGmailやGoogleChat以外にも色々できることが多く、日常のちょっとした面倒を解消するのには向いていると思うので
みなさんもこの記事をきっかけに是非活用してみていただければ幸いです

:::note info
株式会社BTMではエンジニアの採用をしております。
ご興味がある方はぜひ[コチラ](https://www.b-tm.co.jp/recruit/mid-career.html)をご覧ください。
:::