目的
まとめ
- 英語のみの人、日本語のみの人がストレスなく共存できるSlack環境が欲しい
- 自動翻訳botを無料で作りたい
- 必要なときに動作して、うるさくない形で表示できるといい
- 必要なときに動作→メッセージに対する絵文字リアクションで実装
- うるさくない形で表示→メッセージに対する返信で実装
- Google App ScriptとSlack Appを組み合わせて作る
あらまし
Slack超便利だからこれからも使いたい、けど4月から英語のみの外国人研究者が何人かラボに来る、どうしよう・・というのがきっかけ。いろいろやればSlackとGoogle翻訳を自動連携できそうだ、ということはわかった。最初はbotアプリとか自動連携のシステム化を助けてくれるウェブサービスを使ってみたけど、無料期間が終わったら使えなくなってしまった。ウェブアプリやウェブサービスを研究費で買うのは結構面倒ぽい。ああもう自分でやろう・・ということでやってみた。Google App Script(GAS)とSlack Appを組み合わせて実装。
いろいろ調べたけど、結局ここの手順をちょっとだけ変えただけ。こういうの公開してくれるのありがたすぎる・・ということで、自分も公開しようと思い立った。この先似たようなことをやりたくなったときのメモも兼ねる。
僕の知識レベル
- Slack使用歴は3年くらい
- プログラムはいろいろ書くけど、研究に使う程度で、大規模なシステム開発とかはしない
- GASは存在さえ知らなかった
- 開発期間は、勉強含め1日
全体の構成
動作
(日英翻訳の例)
- 翻訳したいメッセージに対し、ENという絵文字を付けると、Slack Appが起動される
- Slack Appはトークンを発行し、公開されたGASを起動、絵文字と文字列を渡す
- GASはENという絵文字に応じて文字列を英語に翻訳、Slack botとしてもとのメッセージに返信する形で翻訳文字列を返す
開発手順
- カスタム絵文字をSlackに導入
- GASの作成
- Slack Appの作成
- Slack Appの発行したトークンをGASに埋め込む
- テスト
1. カスタム絵文字の導入
Q:わざわざカスタム絵文字使わなくてもよくない?実際もとのページはアメリカ国旗と日本国旗使ってるよ?
A:英語使うのはアメリカ人だけじゃないし・・今度来るのは実際カナダ人とオーストラリア人とフランス人だし・・
そんなわけで、日英翻訳用の絵文字には「EN」を、英日翻訳用の絵文字には「JP」を使うことにした。あとついでに、botのアイコン用の絵文字も導入。(ダウンロード EN JP アイコン)
以下の手順でカスタム絵文字を追加。「EN」は:english:、「JP」は:japanese:、botアイコン用の絵文字は:gtranslate:にしてみた。
とりあえず、絵文字は押せるようになった。当然これだけでは何も起こらない。
2. GASの作成
Google Driveから辿ってGASを立ち上げる。(いつの間にか機能めっちゃ増えてるやん・・)
GASの初期画面が立ち上がる。うお、Java Script・・(←悪夢が蘇る人)と思いきや、.gs = Google Script。ほぼ一緒みたいだけど?(←何も知らない人)
ここに以下のコードをコピペ。var TOKENのところだけ、Slack Appができてから更新する必要がある。ここではひとまず置いておく。
var TOKEN = "xoxb-YOUR_TOKEN_HERE"; // Slack Appで発行されたトークンをここに埋め込む
var icon_emoji = ":gtranslate:"; // 翻訳botのアイコンを指定。ここを変えれば別のアイコンにもできる
// Slack Appから入力を受け取ってちゃんと正しい返事ができるかのチェック
// 関数名はdoPostである必要がある
function doPost(e) {
try {
var json = JSON.parse(e.postData.getDataAsString());
if (json.type == "url_verification") {
return ContentService.createTextOutput(json.challenge);
}
// Slack Appから「Slackで絵文字が追加されたよ」というイベントが来たら、処理を開始
// https://api.slack.com/events/reaction_added
// scope: "reactions:read"
if (json.type == "event_callback" && json.event.type == "reaction_added") {
// onReactionAddedという関数(下記参照)に飛ぶ
return ContentService.createTextOutput(onReactionAdded(json.event));
}
} catch (ex) {
}
}
// デバッグ用関数
function postDebugMessage(json) {
channel = "#__debug";
text = JSON.stringify(json);
UrlFetchApp.fetch("https://slack.com/api/chat.postMessage?token=" + TOKEN + "&channel=" + encodeURIComponent(channel) + "&text=" + encodeURIComponent(text));
}
// https://api.slack.com/methods/chat.postMessage
// scope: "chat:write:user" or "chat:write:bot"
// Slackにメッセージをポストする関数
function postThreadMessage(channel, ts, text) {
if (channel && ts && text) {
var payload = {
"channel" : channel, // チャネルを指定
"text" : text, // messageの中身を指定
"thread_ts" : ts, //タイムスタンプを指定
"icon_emoji" : icon_emoji, // botのアイコンを指定
};
var option = {
"method" : "POST", //POST送信
"payload" : payload //POSTデータ
};
// トークンに紐付いたアイテム(この場合翻訳前のmessage)に対する返信という形でbotがmessageをSlackに投稿
UrlFetchApp.fetch("https://slack.com/api/chat.postMessage?token=" + TOKEN, option);
}
}
// https://api.slack.com/methods/conversations.replies
// "channels:history" or "groups:history"
// Slackからmessageのデータを取得する関数
function getMessages(channel, ts) {
var response = UrlFetchApp.fetch("https://slack.com/api/conversations.replies?token=" + TOKEN + "&channel=" + channel + "&ts=" + ts);
var json = JSON.parse(response.getContentText());
return json.messages;
}
// すでに翻訳済みかチェックする関数
function isTranslated(messages, lang) {
for (var i in messages) {
var message = messages[i];
if (message.text.substring(0, lang.length) == lang) {
return true;
}
}
return false;
}
function onReactionAdded(json) {
// postDebugMessage(json);
var channel = json.item.channel; // イベントの起こったチャネルの名前
var type = json.item.type; // イベントの起こったアイテムの種類(ここではmessageであることが前提)
var ts = json.item.ts; // アイテムのタイムスタンプ
var reaction = json.reaction; // 絵文字の種類
if (type == "message") {
// Slackからmessageのデータを取得する関数(上記参照)
var messages = getMessages(channel, ts);
// postDebugMessage(messages);
if (messages) {
var message = messages[0].text;
var languagePrefix;
var translateTo;
// ENが押された場合
// すでに翻訳済みかもチェック
if ((reaction == "english") && !isTranslated(messages, ":english:")) {
languagePrefix = ":english:";
translateTo = "en";
}
//JPが押された場合
// すでに翻訳済みかもチェック
if ((reaction == "japanese") && !isTranslated(messages, ":japanese:")) {
languagePrefix = ":japanese:";
translateTo = "ja";
}
if (translateTo) {
// 翻訳
var translatedMessage = languagePrefix + " " + LanguageApp.translate(message, "", translateTo);
// Slackにmessageをポストする関数(上記参照)
postThreadMessage(channel, ts, translatedMessage);
}
}
}
return "OK";
}
それらしい名前をつけて保存。
GASは作っただけでは外部から叩くことができない。外から参照できるように、Publishする必要がある。
最初はセキュリティの確認が出る。以下の手順で許可を出す。
これでGASを外から参照できるようになった。web app URLは、あとでSlack Appで参照するので、取っておく。Slack Appを作った後、トークンを取得して、再度GASを編集→Publishの手順がある。
3. Slack Appの作成
以下の手順で新しいSlack Appを作成。名前をslack_translateとかにしておく。これがBotのユーザ名になる。名前はあとで変えられる。
作ったSlack Appの設定を行う。まず、Event Subscriptionsにて、絵文字が新たに使用→先ほど公開したGASを起動、という設定。URLの部分は、先ほど取得したGASのURLを入れる。
次に、OAuth & Permissionsにて、GASに与える読み書き権限を決める。この読み書き権限はトークンと紐付いているので、GAS側で読み書きのコマンドを実行しても、トークンによる権限が与えられていないと、実際は何もできない。ここでは、以下の権限を与える。そしてこのSlack Appをひとまずインストール。
- Bot Token Scopes
- chat:write ボットがslackにメッセージを送る権限
- chat:write:customize その際にユーザ名とアイコンを指定する権限
- User Token Scopes
- channels:history 公開チャネルのメッセージや関連情報を参照する権限
- reactions:read 絵文字リアクションや関連情報を参照する権限
4. トークンをGASに埋め込む
Slack AppのトークンをGASのTOKEN変数に入れて、再度Publishする。Project VersionはNewを指定する必要がある。Slack AppのEvent Subscriptionsにて再Publish後のURLを貼り付け、Save Changes(この手順必要ないかも?)。そしてOAuth…に移動し再度インストール。
5. テスト
ちなみに、Google Translationの上限は、普通に使っていたらよっぽどのことがない限り超えないはず。
参考にしたページ
- Slackに翻訳botを足す
- [Google Apps Script で Slack からの投稿などを受け取る] (https://qiita.com/negito6/items/7a457453ff875700d312)
- 【初心者向け】GASを使ってSlackへ自動通知
- Slackで特定のスタンプ(リアクション)に反応するサーバレスボットをGASで作成してみた