突然ですがこんな経験はないですか?
Slack上でのやりとりでは限界があってビデオ通話で会話がしたいときに
- Slackで相手にMeetしましょうと声を掛ける
- ブラウザで新しいタブ開く
- MeetのURLを作成する
- Slackに戻ってMeetのURLを共有してルームに入る
という流れ
画面移動とか地味にめんどくさくないですか?
僕はめんどくさいです
なのでSlackBotに自分専用のMeetのURLを出してもらうことにしました
実際に動いてるもの
塗りつぶし多くてちょっと見辛いですがslackから移動する必要がなくMeetのURLを共有できます
どうやっているのか
Slack
GAS
-
Sheets API
/Calendar API
Slack
という流れでGAS経由でスプレッドシートで管理している個人ごとのURLを返します
スプレッドシートにURLがない場合はカレンダーAPIを使ってMeetのURLを生成してスプレッドシートに保存して返します
サーバレスで、GASは無料ホスティングできてとってもお手軽
設定していく
さっそくやってみたい!と思ってくれた人がいるかもしれないと信じてなるべく丁寧に手順書いていきます
1. Slackからの認証のためにGASを仮置きする
1-1. GoogleDriveで適当に右クリックしてGoogle Apps Scriptを作る
function doPost(e) {
return ContentService.createTextOutput(JSON.stringify(e));
}
1-2. GASをアプリとして公開する
ここで最後に発行されたURLをメモっておく
2. SlackBotを作る
2-1. https://api.slack.com/apps にアクセスしCreate New App
からBotを作る
2-2. 左のメニューの OAuth & Permissions
から Scopeを設定する
Bot Token Scopes
で
app_mentions:read
chat:write
を追加する
2-3. 続けて、アクセストークンを発行する
-
Install To Workspace
を押す - Botのアクセストークンが発行されるのでメモっておく
2-4. 左のメニューの Event Subscriptions
から イベントの設定とGASエンドポイントの認証をする
-
Enable Events
をOnにする - 1-2で発行したGASアプリのURLを
Request URL
へ入力する(成功すればVerified
になる) -
Subscribe to bot events
でapp_mention
を追加する -
Save Changes
で保存
3. スプレッドシートを作る
3-1. GAS同様に適当にドライブにスプレッドシートを作成する
3-2. URLからスプレッドシートのIDをメモる
https://docs.google.com/spreadsheets/d/*************/edit#gid=0
の
*************の部分がIDとなる
GASで使うのでメモっておく
4. Google CalendarでAPI経由でMeetのURLを作るために適当にカレンダーを作る
4-1. 新しいカレンダーを作成
から適当にカレンダーを作る
https://calendar.google.com/calendar/u/0/r/settings/createcalendar にアクセスして作る
4-2. カレンダーに登録
からさっき作ったカレンダーIDを確認する
https://calendar.google.com/calendar/u/0/r/settings/addcalendar にアクセスして確認
- さっき入力したカレンダー名でを入力して選択するとカレンダーの選択画面が出る
- 下にスクロールするとカレンダーIDが確認できるのでメモしておく
5. GASを修正
5-1. スクリプトを修正 ※1〜4を適宜置換
const slackBotAccessToken = '※1 2-3で発行したSlackBotのアクセストークン';
const spreadSheetId = '※2 3-2でメモっておいたスプレッドシートのID';
const calendarId = '※3 4で作った作ったカレンダーのID@group.calendar.google.com';
const attendees = [ { email: '※4 適当に自分のGsuiteのメアド' } ];
function doPost(e) {
const slackApp = SlackApp.create(slackBotAccessToken);
const contents = JSON.parse(e.postData.contents);
// slackの3秒タイムアウトリトライ対策
let cache = CacheService.getScriptCache();
if (cache.get(contents.event.client_msg_id) == 'done') {
return ContentService.createTextOutput();
} else {
cache.put(contents.event.client_msg_id, 'done', 600);
}
// スレッドの中からだったらスレッドに返信する
const opt = contents.event.thread_ts ? { thread_ts: contents.event.thread_ts } : {};
slackApp.postMessage(contents.event.channel, buildResponseMessage(contents.event.user), opt);
// 最初の認証でchallengeを返すのに必要だった
return ContentService.createTextOutput(JSON.stringify(e));
}
// select or upsert してメッセージ返す
function buildResponseMessage(userId) {
const sheet = SpreadsheetApp.openById(spreadSheetId).getSheets()[0];
let url = '';
// selectして
url = selectUrl(sheet, userId);
// なければ作ってinsert(upsertなのは名残)
if (!url) {
url = generateMeetUrl();
upsertUrl(sheet, userId, url);
}
return `${mention(userId)} ${url}`;
}
function getUserIdRange(sheet, userId) {
let matchedRange = sheet.createTextFinder(userId).matchCase(true).matchEntireCell(true).findNext();
return matchedRange;
}
// select
function selectUrl(sheet, userId) {
// userIdで探す
let matchedRange = getUserIdRange(sheet, userId)
// userIdヒットしなければnull返す
if (!matchedRange) return null;
// userIdヒットしてもURLが空文字ならnull返す
let url = matchedRange.offset(0, 1).getValue();
if (!url) return null;
// 前回のアクセス日が空 || 180日以上経過したら無効判定としてURL更新させる
// meetのURLの有効期限は365日
let lastDate = matchedRange.offset(0, 2).getValue();
if (!lastDate || dayDiff(todayYmd(), lastDate) >= 180) return null;
// 今日の日付で更新
matchedRange.offset(0, 2).setValue(todayYmd());
return url;
}
function mention(userId) {
return `<@${userId}>`;
}
// upsert
function upsertUrl(sheet, userId, url) {
let matchedRange = getUserIdRange(sheet, userId)
if (!matchedRange) {
// なければinsert
sheet.appendRow([userId, url, todayYmd()]);
return;
}
// あればupdate
matchedRange.offset(0, 1, 1, 2).setValues([[url, todayYmd()]]);
}
// カレンダー作ってMeetUrlとって消す
function generateMeetUrl() {
let event = Calendar.Events.insert(
{
start: { dateTime: new Date('2020/10/17 10:00:00').toISOString() },
end: { dateTime: new Date('2020/10/17 12:00:00').toISOString() },
attendees,
conferenceData: {
createRequest: {
conferenceSolutionKey: {
type: "hangoutsMeet"
},
requestId: 'リクエストID',
},
},
},
calendarId,
{ conferenceDataVersion: 1 },
);
const meetsUrl = event.hangoutLink;
Calendar.Events.remove(calendarId, event.id)
return meetsUrl;
}
function todayYmd() {
return toYmd(new Date());
}
function toYmd(date) {
let y = date.getFullYear();
let m = ('00' + (date.getMonth()+1)).slice(-2);
let d = ('00' + date.getDate()).slice(-2);
return (y + '-' + m + '-' + d);
}
function dayDiff(date1, date2) {
return ((new Date(date1) - new Date(date2)) / 86400000);
}
5-2. SlackAppライブラリを入れる
Slack BotをGASでいい感じで書くためのライブラリを作った
M3W5Ut3Q39AaIwLquryEPMwV62A3znfOO
5-3. Google Calendar のAPIを叩けるようにする
5-4. デプロイする
6. あとはslackでbotをチャンネルに追加してメンション飛ばせばurlをくれる!
社内でもユーザがだいぶ増えてスプレッドシートはこんな感じになってます
TODO
- GASの権限などが実装者(自分)依存になっているので自分が退職してGSuiteアカウント消えても動き続けるようにする必要がある
- がんばればTerraformでコード管理できそうなきがする