超初心者が作ったSlackで動作する日英自動翻訳bot


# 目的
### まとめ
- 英語のみの人、日本語のみの人がストレスなく共存できるSlack環境が欲しい
- 自動翻訳botを無料で作りたい
- 必要なときに動作して、うるさくない形で表示できるといい
    - 必要なときに動作→メッセージに対する絵文字リアクションで実装
    - うるさくない形で表示→メッセージに対する返信で実装
- Google App ScriptとSlack Appを組み合わせて作る

### あらまし
Slack超便利だからこれからも使いたい、けど4月から英語のみの外国人研究者が何人かラボに来る、どうしよう・・というのがきっかけ。いろいろやればSlackとGoogle翻訳を自動連携できそうだ、ということはわかった。最初は[botアプリ](https://www.kiara-app.com/)とか[自動連携のシステム化を助けてくれるウェブサービス](https://zapier.com/)を使ってみたけど、無料期間が終わったら使えなくなってしまった。ウェブアプリやウェブサービスを研究費で買うのは結構面倒ぽい。ああもう自分でやろう・・ということでやってみた。Google App Script（GAS）とSlack Appを組み合わせて実装。

いろいろ調べたけど、結局[ここ](https://qiita.com/hotpepsi/items/3862618b38b463d37b53)の手順をちょっとだけ変えただけ。こういうの公開してくれるのありがたすぎる・・ということで、自分も公開しようと思い立った。この先似たようなことをやりたくなったときのメモも兼ねる。

### 僕の知識レベル
- Slack使用歴は3年くらい
- プログラムはいろいろ書くけど、研究に使う程度で、大規模なシステム開発とかはしない
- GASは存在さえ知らなかった
- 開発期間は、勉強含め1日

# 全体の構成
![t5cxcr8t.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/604441/2ef0ae7e-8c78-d77b-cc9e-224d3fb93da3.jpeg)


### 動作
（日英翻訳の例）
1. 翻訳したいメッセージに対し、ENという絵文字を付けると、Slack Appが起動される
2. Slack Appはトークンを発行し、公開されたGASを起動、絵文字と文字列を渡す
3. GASはENという絵文字に応じて文字列を英語に翻訳、Slack botとしてもとのメッセージに返信する形で翻訳文字列を返す

### 開発手順
1. カスタム絵文字をSlackに導入
2. GASの作成
3. Slack Appの作成
4. Slack Appの発行したトークンをGASに埋め込む
5. テスト


# 1. カスタム絵文字の導入

**Q**：わざわざカスタム絵文字使わなくてもよくない？実際[もと](https://qiita.com/hotpepsi/items/3862618b38b463d37b53)のページはアメリカ国旗と日本国旗使ってるよ？
**A**：英語使うのはアメリカ人だけじゃないし・・今度来るのは実際カナダ人とオーストラリア人とフランス人だし・・

そんなわけで、日英翻訳用の絵文字には「EN」を、英日翻訳用の絵文字には「JP」を使うことにした。あとついでに、botのアイコン用の絵文字も導入。（ダウンロード　[EN](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/604441/c87f58ce-91fc-600c-a3dc-52ae2d6c6c7d.png) [JP](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/604441/80eb4b94-6009-7cfa-4f5b-bf851d354e85.png) [アイコン](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/604441/fe6decb7-d4c5-ed96-fad5-8e968f972d34.png)）

以下の手順でカスタム絵文字を追加。「EN」は:english:、「JP」は:japanese:、botアイコン用の絵文字は:gtranslate:にしてみた。
![Ej8a9dlR.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/604441/cd96a2fe-cc84-c7f0-ba7c-8d3e2efa6e7c.jpeg)

とりあえず、絵文字は押せるようになった。当然これだけでは何も起こらない。
![x_HzAke-.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/604441/26c2aedd-94c7-6cb6-e4dc-93b2734fb2ec.jpeg)


# 2. GASの作成
Google Driveから辿ってGASを立ち上げる。（いつの間にか機能めっちゃ増えてるやん・・）
![JyqWC2t8.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/604441/69f02b0e-104e-bf88-9965-6fafd9404c6a.jpeg)

GASの初期画面が立ち上がる。うお、Java Script・・（←悪夢が蘇る人）と思いきや、.gs = Google Script。ほぼ一緒みたいだけど？（←何も知らない人）
![96uM79Wr.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/604441/e82fd870-4b02-a4ad-7e94-1a32f1765653.jpeg)

ここに以下のコードをコピペ。var TOKENのところだけ、Slack Appができてから更新する必要がある。ここではひとまず置いておく。

```javascript:slack_translate.gb

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";
}
```

それらしい名前をつけて保存。

![patlSDmQ.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/604441/1b297435-97a3-6b7d-91cf-bd7c9d36a8e0.jpeg)

GASは作っただけでは外部から叩くことができない。外から参照できるように、Publishする必要がある。

![fVJ4m6iC.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/604441/c717965f-777f-d143-f5ee-11730beec916.jpeg)

最初はセキュリティの確認が出る。以下の手順で許可を出す。

![sqrrS7tV.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/604441/dfe86543-600c-7639-d4a5-7f423073462f.jpeg)

これでGASを外から参照できるようになった。web app URLは、あとでSlack Appで参照するので、取っておく。Slack Appを作った後、トークンを取得して、再度GASを編集→Publishの手順がある。

![BB_4RXMe.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/604441/6825a59a-c19d-8bc5-da66-e5f0dd205e36.jpeg)



# 3. Slack Appの作成

以下の手順で新しいSlack Appを作成。名前をslack_translateとかにしておく。これがBotのユーザ名になる。名前はあとで変えられる。

![fLAQVWpz.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/604441/788d7e21-4e0e-9e8e-e233-5ad0961eb4b9.jpeg)

作ったSlack Appの設定を行う。まず、Event Subscriptionsにて、絵文字が新たに使用→先ほど公開したGASを起動、という設定。URLの部分は、先ほど取得したGASのURLを入れる。

![8RMqNucx.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/604441/97eba615-d417-49ed-a493-9e251f6b90f4.jpeg)

次に、OAuth & Permissionsにて、GASに与える読み書き権限を決める。この読み書き権限はトークンと紐付いているので、GAS側で読み書きのコマンドを実行しても、トークンによる権限が与えられていないと、実際は何もできない。ここでは、以下の権限を与える。そしてこのSlack Appをひとまずインストール。

- Bot Token Scopes
    - chat:write　ボットがslackにメッセージを送る権限
    - chat:write:customize　その際にユーザ名とアイコンを指定する権限
- User Token Scopes
    - channels:history　公開チャネルのメッセージや関連情報を参照する権限
    - reactions:read　絵文字リアクションや関連情報を参照する権限

![meh4scoy.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/604441/c8922c11-65e2-33c0-cc12-a85a77446700.jpeg)

# 4. トークンをGASに埋め込む

Slack AppのトークンをGASのTOKEN変数に入れて、再度Publishする。Project VersionはNewを指定する必要がある。Slack AppのEvent Subscriptionsにて再Publish後のURLを貼り付け、Save Changes（この手順必要ないかも？）。そしてOAuth…に移動し再度インストール。

![N0vj_ZOm.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/604441/5cea099e-e63a-dad0-fd0a-c0ca2ed32f0a.jpeg)


# 5. テスト

正常に動作するかのテスト。
![R1schOQF.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/604441/4cee0a68-a744-34d3-87cb-885310065755.jpeg)

ちなみに、[Google Translationの上限](https://cloud.google.com/translate/quotas?hl=ja)は、普通に使っていたらよっぽどのことがない限り超えないはず。
![iHE15Kdh.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/604441/d165ba84-3f83-9379-f72f-a27acec4870c.jpeg)


# 参考にしたページ
- [Slackに翻訳botを足す](https://qiita.com/hotpepsi/items/3862618b38b463d37b53)
- [Google Apps Script で Slack からの投稿などを受け取る] (https://qiita.com/negito6/items/7a457453ff875700d312)
- [【初心者向け】GASを使ってSlackへ自動通知](https://qiita.com/chihiro/items/c7b11abc78f5d806c3a8)
- [Slackで特定のスタンプ(リアクション)に反応するサーバレスボットをGASで作成してみた](https://qiita.com/tez/items/babf47f94c7d3f6f2ce6)