Slackのメッセージをメール配信する
これからやりたいこと
- メンションがトリガーになっているので、メール側にもメンションのidが含まれている→メンションを削るかbotの名前に置換する
- スラッシュコマンドで登録・登録解除できるようにする
- 他の機能(予算登録・報告&リクエスト)も入れられるよう改造する
経緯
部活にSlackを導入したいですって言ったら、ガラケーに対応してる?って言われた。
もちろん対応していないわけで、じゃあメッセージを届けるためにメールで配信すれば良いんじゃね?ってことになった
一応、Classroomは入っているので、必要なときにパソコンからSlackを見るくらいのことはできるはず。
作る物
とりあえずは、メンション(特に@channel)などがとんだ際に自動でメーリングリストにメールを送信する。
Slack APIとGASを使えばできるんじゃないか?って思ってる。
Gmailの扱いが楽そうなのでGASを使いたい(Herokuは制限が厳しい気がする)
とりあえず基盤はGASで作りたいので新規プロジェクトを作成。学校のアカウントだとメールが使えない(これも問題だと思うが)ので、個人アカウント。
Claspでやろうとして失敗
Claspと言うのを使うとコマンドラインで操作できるらしい(すごい)
npm install @google/clasp -g #claspのインストール(node.jsを使ってる)
clasp login #ログイン(勝手にブラウザーが立ち上がった)
mkdir slack-mail-notification #ディレクトリを作成
cd slack-mail-notification #ディレクトリに移動
clasp create slack-mail-notification #slack-mail-notificationと言うapp scriptを作成
? Create which script? #スクリプトの種類を聞かれる
> api #どれを選べば良いか分からなかったが雰囲気でAPIを選択
Nested clasp projects are not supported.
よくわかんないですが、上手くいかないらしい
学校のアカウントはgmailは使えないのに、GASは使える謎仕様
新しいパソコンをもらったのに、未だに古いパソコンでやっていることに気づく。
GASプロジェクトの作成
気を取り直してWEB上で作った物をローカルに持ってくる作戦
昔より公式サイトがきれいになった印象
既にあるプロジェクトをCloneする(Gitっぽい)
設定のProject Settings
からScript IDをコピーしておく
clasp clone [Script ID]
後から気づくが、GAS側の設定でAPIをオンにしなければいけないらしい
スクラップ作ってて思うのは、コマンドラインでスクラップをかければ便利だなぁ、、、などと
GitとTypescriptをセットアップ
Typescriptがサポートされているらしいのでとりあえず
git init #Gitのセットアップ
npm i -S @types/google-apps-script #型の設定
mv Code.js Code.ts #tsに変更
touch .gitignore
node_modules
因みに、clasp push -watch
で何秒かに1回pushしてくれる。pushの時に自動でtsをgsに変換してくれる。
pushするときにエラーが出た。よく調べたらGASの設定でAPIをオンにする必要がある。
設定を変えたら、上手くいった。
スクラップに限らないがどれくらいの粒度で情報を書けば良いのか分からぬ
Gmailでメール配信
最後の部分から作ることにした。(一般的にどのような順番で作るのが正解なのか知らない)
GmailAppのsendEmailを使えば良さげ。
制限があるらしいがEmailは相当なこと(1つのメッセージに250も添付ファイルを付けるなど)をしない限り超えなさそう。
const recipient: string = "kaibatsu35.7m45@gmail.com";
const subject: string = "Hallo World";
const body: string = "This is a test message from GAS";
GmailApp.sendEmail(recipient, subject, body);
スクラップに概要欄みたいなの欲しいなって思ったけど、固定すれば良いんだって気づいた。
Gmailで一斉配信
複数のメールアドレスに一斉配信する。
スプレッドシートで、メールアドレスと名前を管理しているとする。
これらをGASから取得してメールを配信する。
と思って、ドキュメントを見てみたのだが長すぎて読む気を失う。
いろいろ調べた結果openById
を使えば良いらしい。(idは編集画面のURLでhttps://docs.google.com/spreadsheets/d/[id]/edit#gid=0
となっている)
SpreadsheetIDを環境変数として保管するか迷ったが、スプレッドシート自体を公開しなければ見られないと言うことを考えて、普通に定数として扱うことにした。
idからデータを取得して最初の項目(ラベル)を削除。名前とメールアドレスを返してくれるようになった。
function getEmailList(
SpreadSheetID: string
): { address: string; name: string }[] {
let mailData = [];
const sheet = SpreadsheetApp.openById(SpreadSheetID);
const data = sheet.getDataRange().getValues();
data.shift();
data.forEach((data) => {
mailData.push({
address: data[1],
name: data[0],
});
});
return mailData;
}
(初めて、dataは単複同形だと言うことを知った)
ご飯を食べた
Slackのメッセージを検出してGASにリクエストを送れるようにするが、、、
とりあえずGAS側でPOSTリクエストを受け付けるよう設定を変更。doPost()
の関数を作る’。
function doPost(e) {
const emailList = getEmailList(SpreadSheetID);
const slackData = JSON.parse(e);
emailList.forEach((emailData) => {
SendEmail(
emailData.address,
`A message from ${slackData.event.user}`,
`${slackData.event.user}: ${slackData.event.text}`
);
Logger.log(`Sent an email to ${emailData.name}`);
});
return ContentService.createTextOutput(JSON.stringify(e));
}
これをDeployしてURLを取得する。本来はテスト用のデプロイができるはずなのだが自分の環境では上手くいかなかった。
仕方が無いので普通にデプロイ。
Slack側の設定
Slackのアプリを作ったらOAuth&PermissionのBot Token Scopesにapp_mentions:read
を追加。
Eventのmentionに先ほどのGASをデプロイしたURLを貼り付けるとchallenge
パラメーターにtoken入れて返してねと書いてあったので、入力をそのまま返したら上手くいった。
そんなこんなで、最終的にはこんな感じのコードになった。
function doPost(e) {
const emailList = getEmailList(SpreadSheetID);
const slackData = JSON.parse(e.postData.getDataAsString());
emailList.forEach((emailData) => {
SendEmail(
emailData.address,
`A message from ${slackData.event.user}`,
`${slackData.event.user}: ${slackData.event.text}`
);
Logger.log(`Sent an email to ${emailData.name}`);
});
const response = {
challenge: e,
};
return ContentService.createTextOutput(JSON.stringify(response)).setMimeType(
ContentService.MimeType.JSON
);
}
userのデータが欲しいわけだが、Slack APIと言うのを使うっぽい。
というわけでfunction getUserInfo(id: string) {
const slackUsersApi: string = "https://slack.com/api/users.profile.get";
const params: object = {
method: "post",
headers: {
Authorization: `Bearer ${PropertiesService.getScriptProperties().getProperty(
"slackOAthToken"
)}`,
},
payload: {
user: id,
},
};
const response = JSON.parse(
UrlFetchApp.fetch(slackUsersApi, params).getContentText()
);
return response;
}
で任意のユーザーデータがとれるようになった。
メールにユーザー名を含める
先ほどの関数で取得したデータを元に、ユーザー名をメールに含める。
見たところSlack用の方定義が見当たらないので全部any
にしてる(良くない)
function doPost(e) {
const emailList = getEmailList(SpreadSheetID);
const slackData = JSON.parse(e.postData.getDataAsString());
+ const postedUser: string = getUserInfo(slackData.event.user).ok
+ ? "display_name_normalized" in getUserInfo(slackData.event.user).profile
+ ? getUserInfo(slackData.event.user).profile.display_name_normalized
+ : getUserInfo(slackData.event.user).profile.real_name_normalized
+ : "unknown";
emailList.forEach((emailData) => {
SendEmail(
emailData.address,
- `A message from ${slackData.event.user}`,
- `${slackData.event.user}: ${slackData.event.text}`
+ `A message from ${postedUser}`,
+ `${postedUser}: ${slackData.event.text}`
);
Logger.log(`Sent an email to ${emailData.name}`);
});
const response = {
challenge: e,
上手くいく
diff
を覚えた
上手くいってないです
修正
const postedUser: string = getUserInfo(slackData.event.user).ok
? getUserInfo(slackData.event.user).profile.display_name_normalized != ""
? getUserInfo(slackData.event.user).profile.display_name_normalized
: getUserInfo(slackData.event.user).profile.real_name_normalized
: "unknown";
別にdisplay nameが無いからってその項目がないわけではないと言うこと
GmailAppからMailAppに書き換える
リファレンスによれば認証の回数が減るらしい(あまり意味ない気もするが)
Changes to scripts written using GmailApp are more likely to trigger a re-authorization request from a user than MailApp scripts.
と言うことで
function SendEmail(recipient: string, subject: string, body: string) {
- GmailApp.sendEmail(recipient, subject, body, {
+ MailApp.sendEmail(recipient, subject, body, {
name: "Slack Mail Notification Bot",
});
}
新規登録をできるようにする
スプレッドシートに登録する関数を作る
appenRow()
と言う関数を使うっぽい。結構簡単。
検索にはTextfinder
とfindAllで配列を返してくれるらしい。
function addEmail(
sheet: GoogleAppsScript.Spreadsheet.Sheet,
name: string,
address: string
): { error: boolean; message?: string } {
let error = false;
let message = "";
const textFinder: GoogleAppsScript.Spreadsheet.TextFinder = sheet.createTextFinder(
address
);
if (!!sheet.createTextFinder(address).findAll().length) {
error = true;
message += "既に登録済みです。";
}
if (!error) {
sheet.appendRow([name, address, new Date().toLocaleDateString("ja-jp")]);
}
return {
error: error,
message: message,
};
}
address
がメールアドレスかどうか見る必要がある気がします。メールアドレスの判定は何を使えば良いんですかね?
textFinder
使ってなかった
Slackからメールアドレスを取得する
Slack側でGASにリクエストを送るよう設定する。
これをEvent APIと同じファイルでやろうとすると工夫が必要。PostリクエストのヘッダーがContent-Type: application/json
の場合にはイベント、Content-type: application/x-www-form-urlencoded
の場合はスラッシュコマンドとなる。
これは班別が簡単なのだが、ここから、メールアドレスを取得するのが難易度が高かった。これは自分とは関係ないが、GASがリクエストの実行結果のログが通常だと参照できない仕様なのはかなり困った。
例えば、先ほどユーザー名を取得するのに使った、Web APIだとメールアドレスは取得できない。(項目にメールアドレスは設定されているが、これは''
が帰ってくる。これにかなりはまった。
よくリファレンスを見たところ、users:read.email
の権限が必要らしい。(気づかなかった)→これをオンにしても上手くいかない。
いろいろいじっていると、管理者権限でメールアドレスの表示をオンにしないといけないらしい!この設定項目の所には、「アプリ、テストトークン、API経由の場合引き続きメールアドレスにアクセスできます。」と書いてあるんです!!!何もしなくてもアクセスできると思うじゃないですか!!!
スラッシュコマンドに対応する
そんなこんなでスラッシュコマンドに対応します。Slackによれば、リクエストヘッダーでtokenを認証しなければならないらしいのですが、GASではリクエストヘッダーを取得できないそうなので仕方なく本体の方で確認することにしました。
function doPost(e) {
if (e.postData.type == "application/json") {
const slackData: slackEventResponse = JSON.parse(
e.postData.getDataAsString()
);
- if (slackData.token == slackAuthToken)
+ if (slackData.token == slackVerificationToken)
if (slackData.event.type == "app_mention") {
Logger.log("App mentioned");
appMentioned(slackData);
}
+ } else if (
+ e.postData.type == "application/x-www-form-urlencoded" &&
+ e.parameter.token == slackVerificationToken
+ ) {
+ return slashCommand(e);
}
}
function subscribe(e) {
const slackData: Readonly<slashCommandResponse> = e.parameter;
console.info(e);
console.info(e.parameter);
const userInfo = getUserInfo(slackData.user_id);
const name: string =
userInfo.profile.display_name_normalized == ""
? userInfo.profile.display_name_normalized
: userInfo.profile.real_name_normalized;
const address: string = userInfo.profile.email;
const addEmailStatus = addEmail(sheet, name, address);
const message: string = addEmailStatus.error
? addEmailStatus.message
: `${address} を登録しました`;
const response = { text: message };
return ContentService.createTextOutput(JSON.stringify(response)).setMimeType(
ContentService.MimeType.JSON
);
}
登録解除コマンドを設定
function deleteEmail(
sheet: GoogleAppsScript.Spreadsheet.Sheet,
address: string
): { error: boolean; message?: string } {
let error = false;
let message = "";
const matchCell: GoogleAppsScript.Spreadsheet.Range[] = sheet
.createTextFinder(address)
.findAll();
if (!matchCell.length) {
error = true;
message += "このメールアドレスは登録されていません";
}
if (!error) {
sheet.deleteRow(matchCell[0].getRow());
SendEmail(
address,
"メールアドレス登録解除完了",
`登録解除が完了しました。`
);
}
return {
error: error,
message: message,
};
}
登録の関数を改造しました。
Google フォームで申請して自動でリンクを送信
Google フォームでSlackへの参加を申請して、自動的にメールを送信する仕組みを作りたかった。しかし、招待を作成するには、botに管理者権限が必要となり、これをインストールするためにはワークスペースを有料プランに変更する必要があるそうだ。
そんなお金は無いので、仕方なくGoogle フォームのアドオンから自動的に招待リンクを送信するようにした。
新規参加者にDMを送信する
新しい参加者に対して、ルールなどを説明したDMを送信したい。とりあえずDMを送信する処理だけ書く。chat.postMessage
を使えば良さそうだ。channleには、user IDを使えば良いらしい。
If you simply want your app's bot user to start a 1:1 conversation with another user in a workspace, provide the user's user ID as the channel value and a direct message conversation will be opened, if it hasn't already. Resultant messages and associated direct message objects will have a direct message ID you can use from that point forward, if you'd rather.
送信するデータはMarkdownとBlock Kitでできるそうだ。Block Kitは、GUIで組み立てられる新しいメッセージのフォーマットだそうだ。
引っかかったのは、Block Kitを使うときにはJSONに変更しないといけなかった点だ。
const params: GoogleAppsScript.URL_Fetch.URLFetchRequestOptions = {
method: "post",
headers: {
Authorization: `Bearer ${slackAuthToken}`
},
payload: {
channel: id,
blocks: JSON.stringify(blocks)
}
}
UrlFetchApp.fetch(slackCreateMessageAPI, params).getContentText()
const returnObj = { blocks: blocks }
return ContentService.createTextOutput(
JSON.stringify(returnObj)
).setMimeType(ContentService.MimeType.JSON)
Slackの仕組み上、1度作成したアカウントを消すことができないので、テストができない。仕方が無いので入ってきてくれた人に聞くしかなかったのは辛かった。