フューチャー技術ブログ

議事録をサッと準備する

この記事の内容

  • GASにWebサーバを立て、エンドポイントを公開する
  • ユーザのform入力を受け取る
  • 議事録を自動作成してSlackに投稿する

完成版コードはこちら

https://github.com/r-ryu/gas2slack-memo

はじめに

フューチャーTIGグループのDXユニットの棚井龍之介です。

ミーティング(以下MTG)の度に議事録を準備するのが大変だったので、作業を簡易化するWebサービスをGASで作成しました。

WebサイトにアクセスしてMTG名を入力すれば、議事録の準備が完了します。

ページにアクセスして、MTG名を入力すると...

⬇️

SlackにGoogleドライブとGoogleドキュメント(議事録)のURLが投稿されます。

議事録の準備

リモートワークで出社しないのが当たり前となり、お客様とのMTGもオンラインが基本となりました。
オンラインで済む分、MTG開催のコストが大幅に低下して、MTGの数は増加したような気がします。

それにともない、議事録を準備する機会も増えました。

この準備作業、単純ですが割と面倒です。

私の所属するチームでは、おおよそ以下の作業をMTG前後に実施しています。

  • 資料を格納するGoogleドライブを新規作成
  • MTGに向けた議事録(Googleドキュメント)を新規作成
  • ドライブとドキュメントのURLを他メンバーに共有
  • ドキュメントにMTG名、日付、参加メンバーを記入
  • ヘッダー、フッターにページ番号やCopyrightを記入

面倒な定常作業 → 自動化

これらの作業は面倒+予め決まっているので、GASで自動スクリプトを作成しました。

また、議事録の準備は私以外のPJメンバーも実施するため、「非エンジニアを含めて、誰にでもサッと使っていただける」ことを目指して設計しました。

本記事に沿ってGASを設定することにより、以下2つの操作のみで「MTG用ドライブの準備、必要事項が記載された議事録の準備、各URLをSlackで共有する」が完了します。

  1. 議事録生成ページにアクセス
  2. FormにMTG名を入力して作成ボタンをクリック

また、本記事はwebフォーム入力という手動操作が含まれますが、トリガーも含めて完全自動化したい場合は以下記事をご参照ください。GoogleカレンダードリブンでGASを起動する方法が掲載されています。

事前準備

GASコードを書き始める前に、以下の準備をお願いします

  • 議事録のテンプレート本体とそのid
  • アップロード先ドライブのid
  • Slackの Incoming WebHooks

議事録のテンプレート本体とid

本記事でGASがSlackに投稿する議事録は、予め用意した「テンプレートのドキュメント」をコピーしたものです。
まず初めに議事録テンプレートの作成をお願いします。

議事録にはお作法的に毎回記入される項目があると思いますので、テンプレートの充実度が、実運用での負荷軽減に直結します。

例えば、私が作成したテンプレートには以下項目を記入しています。

  • 会議名
  • 日時
  • 場所
  • 社内外の関係者名全て
  • ヘッダーにConfidentialマーク
  • フッターにCopyrightとページ番号

テンプレートのサンプル

テンプレートにMTG関係者の名前全てを記入しておけば、実際に議事をとる場面では「いない人を削除する」だけで済みます。

こういった少しづつの工夫が生産性向上に繋がるため、まずはテンプレートを充実させて、いらない項目は議事を取りながら削除する方法がオススメです。

また、ドキュメントの id(document) はページURLの document/d と /edit の間にある文字列が対応します。
後の作業で利用するため、必要に応じてご確認お願いします。

# Google documentのURLサンプル
https://docs.google.com/document/d/9************Z/edit

# id
9************Z

アップロード先ドライブのid

議事録だけでなく、それを格納するドライブも自動生成します。

本記事では以下のGoogle Drive構成を想定しています。
月単位でフォルダを分け、その配下に日時+MTG名単位でフォルダを生成+議事録を格納します。

┣ 2020年08月
┣ 2020年09月
┣ 2020年10月
┃ ┣ 20201005_月次定例 ━ 20201005_月次定例_議事
┃ ┗ 20201020_運用保守 ━ 20201020_運用保守_議事
┗ 2020年11月
┗ 20201105_月次定例 ━ 20201105_月次定例_議事

このとき、月単位のフォルダが表示されている画面での、URLの folders/ 以下にある文字列が、アップロード先ドライブの id(drive) です。

# Google driveのURLサンプル
https://drive.google.com/drive/folders/6************Q

# id
6************Q

Slackの Incoming WebHooks

Slackに外部システムからメッセージを投稿するため、Incoming WebHooks を用意します。
トークンは Slack API の Your Apps から作成可能です。

Webサーバの実装

サーバ側スクリプトを実装します

  1. 環境変数の設定
  2. GET,POSTのハンドリング
  3. HTMLテンプレートの作成
  4. handlerの実装
  5. Webサーバの公開

1.環境変数の設定

サーバのコードを実装します。

まずはGoogle App Scriptを新規に生成して、環境変数を設定しましょう。

環境変数は
File → Project Properties → Script Properties
で登録可能です。

| Property | Value |
|———- |——————— |
| FOLDERID | “id(document)” |
| FILEID | “id(drive)” |
| POSTURL | “Incoming WebHooks” |

同時に、環境変数を取得する処理を追加します。

Code.gs
var FOLDERID = PropertiesService.getScriptProperties().getProperty("FOLDERID");
var FILEID = PropertiesService.getScriptProperties().getProperty("FILEID");
var POSTURL = PropertiesService.getScriptProperties().getProperty("POSTURL");

2.GET,POSTのハンドリング

GASのWebサーバは、GETリクエストの場合はdoGet(), POSTリクエストの場合はdoPost()が呼び出されます。

本記事では

  1. URLを踏んで(GETリクエストで)Webサーバにアクセス
  2. Webサーバからformを取得
  3. formをsubmitし、POSTリクエストでWebサーバにアクセス

というフローのため、doGetとdoPostの両方を追記します。
一部、Indexe.parameter.name といった初出の部分は記事の後半で説明しますので、今の段階ではdoGetとdoPostでハンドリングできるんだな〜という感覚で問題ありません。

Code.gs
function doGet(e) {
return HtmlService.createHtmlOutputFromFile("Index");
}

function doPost(e) {
var name = e.parameter.name;
var mtg = handler(name);

var message = "完了ページ\n" + "作成した議事録:" + mtg;
return ContentService.createTextOutput(message);
}

3.HTMLテンプレートの作成

GASの画面から、HTMLテンプレートを作成します。

File → New → HTML file → “Index”を入力してOK
により、新たに Index.html が作成されます。

Index.htmlには、以下のhtml文を入力します。

Index.html
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<base target="_top">
<title>議事録生成フォーム</title>
</head>
<body>
<h1>議事録作成フォーム</h1>
<form action="" method="post" onsubmit="return double()">
<p>
<label>会議名 <input type="text" name="name" placeholder="例:月次定例" required></label>
</p>

<input type="submit" value="作成">
<input type="reset" value="リセット">
</form>
</body>
</html>

4.handlerの実装

formでPOSTリクエストを受けた後のハンドリングを実装します。
各処理の詳細は、コード内のコメント箇所を参照お願いします。

Code.gs
function handler(name) {

// 議事録テンプレートとフォルダ情報を取得する
var topFolderPath = DriveApp.getFolderById(FOLDERID);
var formatFilePath = DriveApp.getFileById(FILEID);

// 現在時刻からファイル名を生成
date = nowDate();
var yyyymmdd = date.year + date.month + date.day;
var mtgname = yyyymmdd + '_' + name;

// 「yyyy年mm月」フォルダを確認
var monthFolder = prepareMonthFolder(topFolderPath, date.year, date.month);

// 「yyyyddmm_(会議名)」 フォルダを新規作成する
var dayFolder = monthFolder.createFolder(mtgname);
var dayFolderUrl = dayFolder.getUrl();

// 議事録テンプレートをコピーして、適切なフォルダに移動する
var newFile = formatFilePath.makeCopy(mtgname, dayFolder);
var newFileUrl = newFile.getUrl();

// Slackに投稿
post2slack(mtgname, newFileUrl, dayFolderUrl);

return mtgname;
}

handler関数の複雑化を避けるために、3つの関数を作成しています。

  • nowDate()
  • prepareMonthFolder(path, year, month)
  • post2slack(name, fileUrl, folderUrl)
Code.gs
function nowDate() {
var today = new Date();
var year = today.getFullYear();
var month = '' + (today.getMonth() + 1);
var day = '' + today.getDate();
if (month.length < 2)
month = '0' + month;
if (day.length < 2)
day = '0' + day;

return {
year:year,
month:month,
day:day,
};
}

function prepareMonthFolder(path, year, month) {
var yyyymm = year + '年' + month + '月';

var folders = DriveApp.getFolders();
var monthFolderExist = false;
var monthFolder;

// 「yyyy年mm月」のフォルダが存在しない場合は新規作成
while (folders.hasNext()) {
var folder = folders.next();
if (folder.getName() == yyyymm) {
monthFolderExist = true;
monthFolder = folder;
}
}
if (!monthFolderExist) {
monthFolder = path.createFolder(yyyymm);
}

return monthFolder
}

function post2slack(name, fileUrl, folderUrl) {
// Slackに投稿されるメッセージ文
var message = '議事録を準備しました\n\n' + '会議名:' + name + '\n' + '議事:' + fileUrl + "\n" + "フォルダ:" + folderUrl;

var jsonData = {
'text':message
};
var payload = JSON.stringify(jsonData);
var options = {
'method':'post',
'contentType':'application/json',
'payload':payload
};

UrlFetchApp.fetch(POSTURL, options);
}

以上により、POSTリクエストのハンドリングは完了です。

5.Webサーバの公開

サーバ側コードの実装が完了したので、Webサーバのエンドポイントを公開します。

Publish → Deploy as web app… を入力し、以下項目の入力後 “Deploy” を選択します。

Project version: new
Execute the app as: me
Who has access to the app: anyone with "組織名" (会社内部のみでの利用を想定するため、anyone, even anonymous には設定しない)

初回Deploy時には認証画面が表示されます。
その承認が完了すると、Current web app URL が表示されます。

Current web app URL:
https://script.google.com/*****/macros/T**********U/exec

このURLがWebサーバのエンドポイントです。
データを受け渡しするために、Index.htmlのformへ追加します。

Index.html
<form action="" method="post">

<form action="https://script.google.com/*****/macros/T**********U/exec" method="post">

以上で、Webサーバの公開は完了です。

エンドポイントのURLをブラウザに打ち込むことで、議事録準備のフォーム画面が表示されます。
MTG名を入力すれば、GASが裏で動いて、Slackに議事が投稿されます。

より使いやすくするために

本システムはURLへのアクセスをトリガーとして処理が開始されます。
「早く議事を用意したいのに、議事準備のURLってなんだっけ?」となっては自動化した旨味がありません。

Slackの ワークスペースのカスタマイズ で、指定のURLを返してくれるように Slackbot を設定すればサッとURLを取得できます

(PJ名)+memo の文字列に反応して、SlackbotがURLを投稿してくれるように設定)

これで

  • 必要になったらパッとURLを取得して
  • 会議名を入力して
  • GASが頑張っているのを待つ

だけで、議事録の用意が完了します。

まとめ

本記事では、GASでWebサーバを公開して、議事録の準備をほぼ自動化する方法をご紹介しました。
普段の業務を自動化することにより、作業忘れや作業ミスを減らしつつ、別の業務に集中する時間が得られます。

また、Webサービスとして公開することで自分以外のPJメンバーも利用できるため、初期構築に若干の手間がかかるとしても、結果的にはPJ全体の作業効率向上につながると思います。

「定常業務の自動化・簡易化」で得られる恩恵はかなり大きいので、議事録を毎回手動作成している方がいられたら、ぜひとも本記事の手法を利用してみてください。

完成版のコード全文はこちらです。
https://github.com/r-ryu/gas2slack-memo