議事録作成＆Slack通知を自動化したい人へ捧げるGoogle Apps Script

# はじめに 
次回の会議のために事前に議事録ファイルを作って、Slackで「次回のアジェンダ記入してくださ〜い」なんて呼びかけたりしますよね。数分の作業ではあるものの、人間とは時に簡単なことまで煩わしく感じてしまう悲しい生き物です。主に私のことです。
煩わしいことは全て人任せ・・・というわけにもいかないこんな世の中じゃ、自動化するしかありません。時ハ金ナリ、さあ自動化だ！

# 対象読者
- Google Driveで管理しているファイル作成を自動化したい人
- Google Driveのファイル更新をSlackで通知したい人
- 基本的なJavaScriptの構文がわかる人
- 毎週の資料作成を後輩に頼みづらい人

# 今回作るもの
Google Apps Scriptで実装する議事録自動作成＆通知プログラム。
週一回決まった曜日に自動で次回の議事録ファイルを作成し、Slackで通知してくれるスクリプトを仕込みます。
今回はGoogle Driveのスプレッドシートで議事録を作成している想定で進めます。

# 使用技術
- Google Apps Script
- JavaScript
- Incoming Webhook （Slackとの連携用）
- 未来の自分に楽させたい気持ち

# Google Apps Scriptとは
>Google Apps Script は JavaScript ベースのスクリプト言語で、ドキュメント、スプレッドシート、スライド、フォームなどの G Suite サービスをカスタマイズ、拡張できます。インストール作業は不要です。ブラウザ内で動作するコードエディタが用意されており、スクリプトは Google のサーバーで実行されます。

引用元: https://developers.google.com/gsuite/aspects/appsscript?hl=ja

要するに、Googleの各種サービスを拡張したり、連携して何かを行うプログラムを作成できるJavaScriptベースの言語です。頭文字をとってGASと略されることが多いです。なんか強そう。
Gmailアカウントをもっていれば誰でも利用でき、ローカルでの環境構築ナシですぐに実装を開始できます。
Chrome V8ランタイムに対応しているため、ES6の構文の利用も可能です。嬉しい〜。

# さあ、自動化のお時間です
早速実装していきましょう。
今回作成する自動作成ツールの具体的な処理は以下の通り。

1. テンプレートファイルをコピー
1. ファイル名の末尾を次回の開催日に変更
1. ファイル内の開催日の日付を次回の開催日に変更
1. Slackで特定のチャンネルでファイルへのURL付リマインドを投下

## テンプレートファイルの用意
議事録を管理しているGoogle Driveのフォルダ配下にテンプレートファイルを用意します。
今回はファイル名を「うさぎさんグループ定例会_template」としました。楽しそうな会議ですね。
![template.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/269370/1bc8f753-6d0f-9fc9-c073-bc1413510393.gif)

## GASの導入
お待ちかね、GASの導入です。
先ほどと同じGoogle Driveのフォルダ配下で、「新規作成」 → 「その他」 → 「Google Apps Script」をクリックします。
![gas.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/269370/89e694f9-0eeb-7a59-27f0-d226894c424f.png)

すると、別タブでGASのエディタが立ち上がるので、あとはここに実装したい処理を書いていくだけ。お、お手軽すぎる〜。
![gas2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/269370/ecef2b82-0445-f4b5-e715-611b52bea7eb.png)

一旦ファイルを保存しておきましょう。
保存はCommand + s （Windowsの場合はCtrl + s）で可能です。
今回は「ぎじろく勝手につくるくん」として保存しました。お節介そうに聞こえる名前ですが実は結構いい奴です。
![gas3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/269370/f6b6f528-d194-194e-0efa-ea68f44ca092.png)

## 次回の議事録ファイル作成処理の実装

新しいファイル名でテンプレートファイルのコピーを作成する処理を実装します。
GASで定義されている`File`クラスの`makeCopy`メソッドを利用します。

```javascript
コピー元ファイル.makeCopy(新しいファイル名, コピー先フォルダ)
```

### テンプレートファイルとコピー先フォルダの取得

上記の関数を利用するために、コピー元ファイルとコピー先フォルダをそれぞれのIDによって取得します。
IDは以下のように各ファイル・フォルダのURLから取得ができます。

スプレッドシートの場合、以下の[ファイルID]の部分
`https://docs.google.com/spreadsheets/d/[ファイルID]/edit`

フォルダの場合、以下の[フォルダID]の部分
`https://drive.google.com/drive/folders/[フォルダID]`

上記のIDを利用して、テンプレートファイルとコピー先フォルダを取得します。

```javascript
  // テンプレートファイルのID
  const templateFileId = 'テンプレートファイルのID'
  // 議事録を管理しているフォルダのID
  const destFolderId = '議事録フォルダのID'
  // テンプレートファイルの取得
  const templateFile = DriveApp.getFileById(templateFileId);
  // 議事録を管理しているフォルダの取得
  const destFolder = DriveApp.getFolderById(destFolderId);
```

### 次回の議事録のファイル名の生成
新しい議事録のファイル名も渡す必要があるので、予め生成します。

今回は会議の一週間前に議事録ファイルを作成する想定のため、作成日から1週間後の日付を取得します。ちょっと泥臭いですがご容赦ください。

```javascript
  // 翌週の開催日の取得
  const today = new Date();
  const nextDate = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 7);
```

議事録のファイル名は`うさぎさんグループ定例会_yyyyMMdd`としたいので、日付のフォーマットを変更してファイル名を生成します。
GASで定義されている`Utilites`クラスの`formatDate`メソッドを利用します。

```javascript
  // 次回の議事録のファイル名用の日付を生成
  const nextDateForFileName = Utilities.formatDate(nextDate, 'JST', 'yyyyMMdd');

  // 次回の議事録のファイル名を生成
  const templateName = templateFile.getName();
  const newFileName = templateName.replace('template', nextDateForFileName); // 「うさぎさんグループ定例会_yyyyMMdd」になる
```

### 新しいファイル名でテンプレートファイルのコピーを作成
テンプレートファイル、コピー先フォルダ、新ファイル名が出揃ったので、先ほどの関数に渡してコピーを作成します。
`makeCopy`メソッドは返り値としてコピーしてできた新しいファイルを返すので、後続の処理のために予め変数にいれておきましょう。

```javascript
  // テンプレートファイルを新ファイル名でコピー
  const newFile = templateFile.makeCopy(newFileName, destFolder);
```

これで新しいファイルの作成ができました。

### 新しい議事録内の開催日の日付を変更
大抵、以下のように議事録ファイル内にも開催日の記載がありますよね。ここも変更してしまいましょう。
![template2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/269370/23e6a70e-3d0b-7acf-4967-7bf93e621c3d.png)

ファイル内の開催日は`yyyy/MM/dd`の形式にしたいので、先ほど取得した`nextDate`を用いてフォーマットを変換します。

先ほど`newFile`変数に格納した新しいファイルを編集するのですが、GASでセル操作を行うためにはこのファイルをさらにSpreadSheetオブジェクトに変換する必要があります。
変換は`newFile`を`SpreadsheetApp.open`で開くことによって可能です。

```javascript
  // 次回の議事録のタイトル用の日付を生成
  const nextDateForFileTitle = Utilities.formatDate(nextDate, 'JST', 'yyyy/MM/dd');
  // 後述の処理でセル操作を可能にするためにSpreadSheetオブジェクトにする
  const newSpreadSheet = SpreadsheetApp.open(newFile);
```

変換できたら、該当のシートを取得し、さらにそのシート上の該当のセルを取得します。
今回は「アジェンダ・議事録」シートのB3のセルに開催日を入力する想定です。

```javascript
  // 次回の議事録の「アジェンダ・議事録」シートを取得
  const targetSheet = newSpreadSheet.getSheetByName('アジェンダ・議事録');
  // 「アジェンダ・議事録」シートB3セルを取得
  const targetCell = targetSheet.getRange('B3');
```

あとは、取得したセルの内容を開催日に変更するだけです。

```javascript
  //B3セルの内容を次回の開催日(yyyy/MM/dd)に変更
  targetCell.setValue(nextDateForFileTitle);
```

以上で、以下の3ステップが完了しました。やったぜ！

>1. テンプレートファイルをコピー
1. ファイル名の末尾を次回の開催日に変更
1. ファイル内の開催日の日付を次回の開催日に変更

### ここまでのコード

以上のコードをまとめると、以下のようになります。

```javascript
/* createNewFile 議事録自動作成スクリプト
 * @return {void}
 */
function createNewFile() {
  
  // テンプレートファイルのID
  const templateFileId = 'テンプレートファイルのID'
  // 議事録を管理しているフォルダのID
  const destFolderId = '議事録フォルダのID'
  // テンプレートファイルの取得
  const templateFile = DriveApp.getFileById(templateFileId);
  // 議事録を管理しているフォルダの取得
  const destFolder = DriveApp.getFolderById(destFolderId);
 
  
  // 翌週の開催日の取得
  const today = new Date();
  const nextDate = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 7);
    
  // 次回の議事録のファイル名用の日付を生成
  const nextDateForFileName = Utilities.formatDate(nextDate, 'JST', 'yyyyMMdd');
  // 次回の議事録のファイル名を生成
  const templateName = templateFile.getName();
  const newFileName = templateName.replace('template', nextDateForFileName); // 「うさぎさんグループ定例会_yyyyMMdd」になる

  // テンプレートファイルを新ファイル名でコピー
  const newFile = templateFile.makeCopy(newFileName, destFolder);

  // 後述の処理でセル操作を可能にするためにSpreadSheetオブジェクトにする
  const newSpreadSheet = SpreadsheetApp.open(newFile);
  
  // 次回の議事録のタイトル用の日付を生成
  const nextDateForFileTitle = Utilities.formatDate(nextDate, 'JST', 'yyyy/MM/dd');
  // 次回の議事録の「アジェンダ・議事録」シートを取得
  const targetSheet = newSpreadSheet.getSheetByName('アジェンダ・議事録');
  // 「アジェンダ・議事録」シートB3セルを取得
  const targetCell = targetSheet.getRange('B3');
  //B3セルの内容を次回の開催日(yyyy/MM/dd)に変更
  targetCell.setValue(nextDateForFileTitle);
  
}

```

## Slack通知処理の実装

さて、次はファイルが作成されたことをSlackで通知する処理を実装しましょう。
今回はファイルへのURLも含めた通知を投下します。どんなに小さいことでもおもてなしは大切です。

SlackとGASの連携には[Incoming Webhook](https://slack.dev/java-slack-sdk/guides/ja/incoming-webhooks)を使います。

### ※Incoming Webhook 導入の際の注意点
本記事の後述のセクションではSlackのカスタムインテグレーションから設定する手順での導入方法を記載しておりますが、この方法は現在非推奨でSlack Appを作成して設定する方法が推奨されているようです。（ごめんなさい）
その場合の導入手順は以下の2記事がわかりやすかったのでこちらを見ていただく方が良さそうです。

- [SlackのIncoming WebhookをカスタムインテグレーションではなくSlack Appから設定してみる](https://dev.classmethod.jp/articles/slack-incoming-webhook-by-slack-app/)
- [【2020年度版】Slack通知はSlack AppのIncoming Webhooksを使おう！やり方を解説](https://keyama.hatenablog.com/entry/2020/04/21/181451)

カスタムインテグレーションから設定する方法で問題ないのだ、という方はどうぞ読み進めていただければ幸いです。

### Incoming Webhookの追加
Slackのスペース名右側にある三角矢印 → 「設定と管理」 → 「以下をカスタマイズ: ワークスペース名」をクリックすると、設定ページへ遷移します。
![incomingwebhook.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/269370/e9977d8a-b784-55dc-ed54-a2ddc81c58bb.png)

サイドバーの「App管理」をクリックするとAppの管理ページへ遷移します。
SlackにIncoming Webhookを導入していない場合は、この画面でIncoming Webhookを導入します。
![app_setting.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/269370/183535f2-8d60-d2b6-390d-97d794f142a9.png)

すでに導入済みの場合は、「カスタムインテグレーション」からIncoming Webhookをクリックします。
![custom_integration.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/269370/9eaf52a1-b2bf-5b53-3b9f-4ca57ce5995b.png)

遷移した画面内の以下の「Slackに追加」ボタンを押すと追加画面へ移動します。
![incomingwebhook_add.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/269370/24644d34-1ff0-80de-456e-2b3eb4288818.png)

ここで、投稿したいSlackチャンネルを指定し、「Incoming Webhook インテグレーションの追加」を押すと設定が追加されます。
![incomingwebhook_setting.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/269370/5e3e9b07-bdbb-0735-85bf-75d5f6c68ed6.png)

詳細設定ページに遷移するので、ここでbotの名前やアイコンのカスタマイズができます。
アイコンはSlackのワークスペースに登録されている絵文字からも選択可能です。
![incomingwebhook_setting2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/269370/b30d911a-43cd-cfd2-10fd-52cde93a18e5.png)

そして一番大切なのが上記で払い出されたWebhook URL。
このURLに送りたいメッセージのデータを送信すれば、Slackへ通知を飛ばすことができます。
今回はこの送信処理もGASで実装して連携させていきます。

### Webhook URLに送信するデータを作成

送信したいデータはjson形式でPOSTリクエストのpayloadパラメータとして送信します。
botで通知したいテキストだけを指定する、一番シンプルなpayloadは以下のようになります。

```json
  {
    "text": "今日のぎじろくを作ったぴょん",
  }
```

GASでPOSTリクエストをするためには、`UrlFetchApp`クラスの`fetch`メソッドを利用します。

```javascript
  UrlFetchApp.fetch([送信先URL], [オプション]);
```

オプションには`method`、`contentType`、`payload`をまとめたものを渡します。
今回は以下のように`options`としてまとめて定義しました。
また、`payload`の`text`には次回の議事録のURLも含めた文言を指定しました。

```javascript
  // 作成した議事録のURLを取得
  const newFileUrl = newFile.getUrl();
  // Webhook URLに送信するデータを作成
  const jsonData = { "text": "次回のぎじろくはこっちだぴょん！！: " + newFileUrl };
  const payload = JSON.stringify(jsonData);
  const options = {
    "method": "post",
    "contentType": "application/json",
    "payload": payload
  };
```
`options`も定義できたので、あとはWebhook URL当てに送信して、ミッションコンプリートです。

```javascript
  const targetUrl = 'Webhook URLのフルパス'
  // Slackで通知する
  UrlFetchApp.fetch(targetUrl, options);
```

### 実行してみる
さあ、Slackとの連携処理の実装が完了したので実行してみます。
以下のように、指定したチャンネル宛に通知が届くはずです。
![slack_test.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/269370/4e900ce9-c43c-9f23-e9a7-0ad414cceac3.png)

るんるんなうさぎさんに議事録を用意してもらえたら、毎週の会議も楽しみになりそうですね。

## 完成形のコード
これまでのコードをまとめると以下になります。これでスクリプトは完成。やったぜ！

```javascript
/* createNewFile 議事録自動作成スクリプト
 * @return {void}
 */
function createNewFile() {
  
  // テンプレートファイルのID
  const templateFileId = 'テンプレートファイルのID'
  // 議事録を管理しているフォルダのID
  const destFolderId = '議事録フォルダのID'
  // テンプレートファイルの取得
  const templateFile = DriveApp.getFileById(templateFileId);
  // 議事録を管理しているフォルダの取得
  const destFolder = DriveApp.getFolderById(destFolderId);
 
  
  // 翌週の開催日の取得
  const today = new Date();
  const nextDate = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 7);
    
  // 次回の議事録のファイル名用の日付を生成
  const nextDateForFileName = Utilities.formatDate(nextDate, 'JST', 'yyyyMMdd');
  // 次回の議事録のファイル名を生成
  const templateName = templateFile.getName();
  const newFileName = templateName.replace('template', nextDateForFileName); // 「うさぎさんグループ定例会_yyyyMMdd」になる

  // テンプレートファイルを新ファイル名でコピー
  const newFile = templateFile.makeCopy(newFileName, destFolder);

  // 後述の処理でセル操作を可能にするためにSpreadSheetオブジェクトにする
  const newSpreadSheet = SpreadsheetApp.open(newFile);
  
  // 次回の議事録のタイトル用の日付を生成
  const nextDateForFileTitle = Utilities.formatDate(nextDate, 'JST', 'yyyy/MM/dd');
  // 次回の議事録の「アジェンダ・議事録」シートを取得
  const targetSheet = newSpreadSheet.getSheetByName('アジェンダ・議事録');
  // 「アジェンダ・議事録」シートB3セルを取得
  const targetCell = targetSheet.getRange('B3');
  //B3セルの内容を次回の開催日(yyyy/MM/dd)に変更
  targetCell.setValue(nextDateForFileTitle);

  // 作成した議事録のURLを取得
  const newFileUrl = newFile.getUrl();
  // Webhook URLに送信するデータを作成
  const jsonData = { "text": "次回のぎじろくはこっちだぴょん！！: " + newFileUrl };
  const payload = JSON.stringify(jsonData);
  const options = {
    "method": "post",
    "contentType": "application/json",
    "payload": payload
  };

  const targetUrl = 'Webhook URLのフルパス'
  // Slackで通知する
  UrlFetchApp.fetch(targetUrl, options);

}
  
```

## 決まった時間に処理を実行するようにする
さあ、自動化の最終仕上げです。
実装した処理を毎週決まった時間に自動で実行するように設定します。

### トリガーを設定
GASのエディタ内のメニューバーの「編集」 → 「現在のプロジェクトのトリガー」から、トリガーの設定画面に遷移します。
![trigger.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/269370/dcb5f719-63f9-cb99-e4a6-e76004d6d897.png)

画面右下の「トリガーを追加」ボタンを押下するとダイアログが出てくるので、実行タイミングを設定します。
![trigger_config.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/269370/c3ab4a91-fb82-715d-0867-55cfa69ae732.png)

今回は毎週月曜日の午後4〜5時の間に実行するように以下の通りに設定しました。
![trigger_setting.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/269370/c205769a-34b4-4d92-684d-b03a22da8bc4.png)

これで「保存」ボタンを押したら、スクリプトの実行タイミングの設定は完了。
あとは実行されるその時をただ待つだけです。
議事録を作成し忘れて叱られることのない、平和な世界が訪れました。

# あとがき
以上、Google Apps Scriptで議事録を自動生成し、Slackで通知を飛ばす自動処理の実装方法でした。
小さな自動化は未来の自分やチームメイトを救います。
皆様も良き自動化ライフをお送りください。