slack API + GASでリアクションがあったら指定チャンネルにメンション通知する

まえがき

Slackでリアクションされたら、メンション&リアクションに一覧表示されますが、リアルタイムでリアクションに気づきたくて通知されるようにSlack API+GASで実装してみました。

というのも夫婦間でSlackを使っていて、家事育児をしていると仕事中のように常にSlackを開いているわけではないのでリアクション通知があったらいいなという気持ちで作ってみました。

こんな通知がくるようにしました。

通知イメージ

ちなみにメンバーが多いと通知がきまくってうざいので、メンバー2人ならではの機能だとは思います。

構成

構成図

  • Slackのリアクションイベントをトリガー
  • GASでPOSTリクエストを受け取って処理
  • GASからIncoming WebhooksでSlackの対象チャンネルにメッセージ投稿

*GASとGitHub連携はGoogle Apps Script GitHub アシスタントを使っています。

実装

GAS作成(公開)

サーバ側はGASで用意しました。 「ウェブアプリ」としてデプロイして、公開URLをメモしておきます。

Promise.allを使ってチャンネル名とユーザIDを取得しておいて、Webhookで「リアクション+メンション+メッセージURL」を対象チャンネルに投稿するようにしています。

const props = PropertiesService.getScriptProperties().getProperties();
const oAuthAccessToken = props.OAUTH_ACCESS_TOKEN;
const slackApiBase = 'https://slack.com/api/';

function doPost(e){
  const json = JSON.parse(e.postData.getDataAsString());
  const itemChannel = json.event.item.channel;
  const itemTs = json.event.item.ts;
  const reaction = json.event.reaction;

  Promise.all([
    fetchChannel(itemChannel),
    fetchMessage(itemChannel, itemTs)
  ])
  .then(function(data) {
    const [channelName, messageData] = data;
    const message = "<@" + messageData["userId"] + ">" + "[" + channelName + "]にリアクションがつきました。\n\n" + messageData["linkUrl"];
    const jsonData = { "text": ":" +reaction + ":" + message };
    const payload = JSON.stringify(jsonData);
    const options = {
      "method" : "post",
      "contentType" : "application/json",
      "payload" : payload
    };

    UrlFetchApp.fetch(props.WEBHOOK_URL, options);
  })
  .then(() => {
    return ContentService.createTextOutput(params.challenge);
  })
}

// チャンネルリストを取得してチャンネルIDから対象のチャンネル名を取得
function fetchChannel(channelId) {
  return new Promise((resolve) => {
    const channelsUrl = slackApiBase + 'conversations.list?token=' + oAuthAccessToken + '&exclude_archived=1&pretty=1';
    const channels = JSON.parse(UrlFetchApp.fetch(channelsUrl)).channels;
    const targetChannel = channels.find(channel => channel.id === channelId);
    const channelName = (targetChannel !== undefined) ? targetChannel.name : "チャンネル以外";
    resolve(channelName);
  })
}

// メンション用に対象メッセージ投稿者のユーザIDを取得
function fetchMessage(channel, ts) {
  return new Promise((resolve) => {
    const url = slackApiBase + "conversations.replies?token=" + oAuthAccessToken + "&channel=" + channel + "&ts=" + ts + "&latest=" + ts + "&limit=1&inclusive=true" 
    const replies = JSON.parse(UrlFetchApp.fetch(url));
    const targetMessage = replies.messages[0];
    const isThread = (targetMessage.thread_ts !== undefined && targetMessage.ts !== targetMessage.thread_ts);
    let linkUrl = props.SLACK_WORKSPACE_URL + "archives/" + channel +"/p" + String(targetMessage.ts).replace(/\./g, '');
    // スレッドの場合
    if(isThread) {
      linkUrl += "?thread_ts=" + String(targetMessage.thread_ts).replace(/\./g, '') + "&cid=" + channel;
    }
    resolve({linkUrl: linkUrl, userId: targetMessage.user});
  })
}

参考

qiita.com

環境変数

GASのプロパティストアを利用。

  • OAUTH_ACCESS_TOKENはSlackアプリ作成時のOAuth & Permissions > Scopes設定後に表示されるものを定義します。

  • WEBHOOK_URLはSlackアプリ作成時にIncoming WebhooksONにしてURLを取得します。投稿先は#reactionチャンネルを作成して設定しました。

IDEではなぜかプロパティ編集画面がなくなっていてかなりつまづきました。 仕方ないのでプロパティストアの設定時だけ旧IDEに戻して設定後にまた新IDEに戻しました。

プロパティストア

ちなみに旧IDEに戻す際に以下のようなアンケートが出たのでMissing script property editingにチェックしておきましたw

f:id:mashym91:20210220175144p:plain:w300

Slackアプリ作成

Slackアプリの作成はこちらを参考にさせていただきました。

www.pnkts.net

適当にリアクション通知という名前で作成。 Slackアプリ作成

Event Subscriptions>Enable Eventsを有効にして先ほどメモしておいたGASのURLを貼り付けてVerified表示されることを確認します。

Event Subscriptions

Subscribe to events on behalf of usersreaction_addedを追加します。

Subscribe to events on behalf of users

OAuth & Permissions>Scopesで以下を設定してアプリをインストール。(App Installから)表示されるOAuth Access TokenをGASのプロパティ(OAUTH_ACCESS_TOKEN)に定義します。

User Token Scopes

参考

www.lancard.com

Incoming Webhooks設定

GASからメッセージ投稿するためにIncoming Webhooks>Activate Incoming Webhooks を有効にします。

Webhook URLs for Your Workspaceワークスペースのチャンネル(作成した#reaction)に追加します。

*以前はicon_emojiも投稿できたみたいですが、できなくなったようなのでtextに絵文字リアクションを追加して投稿する実装にしています。

Reference: Message payloads | Slack

Slack での Incoming Webhook の利用 | Slack

つまづいた点

スレッド対応

元メッセージURLの生成とユーザID取得用にslack APIconversations.historyを使っていましたが、スレッドの場合thread_tsも必要だったのでそれがが取れるconversations.repliesを使うように変更しました。 スレッド時のURL構成やAPIのresponseなどドキュメントを調べたりけっこう時間かかりました。

conversations.replies method | Slack

非同期処理の制御(Promise→Promise.all)

そのままだとAPIでのデータ取得前にWebhook処理が走ってしまうので、Promiseを使って順次処理していましたが、APIでのデータ取得は順番を気にしなくていいのでPromise.allを使うようにリファクタリングしました。

Promise.allでコードがかなりスッキリしたと思います。

参考

qiita.com

google github アシスタントの連携

Chrome拡張機能をインストールしてもGASの画面に全然反映されず困りましたが、こちらにある「v4.0.7を手動でインストール」で解決しました。

Google Apps Script GitHub アシスタントが表示されない&Google認証でエラー This app is blocked - Qiita

Google Apps Script GitHub アシスタント - Chrome ウェブストア

運用

最初に書いたとおり夫婦間用Slackでメンバーは2人なのでメッセージにメンションはつけず通知設定をすべての新しいメッセージにしていて、#reactionチャンネルだけメンションのみ設定で自分の投稿に対するリアクションだけ通知がくるようにしています。

最後に

実装から記事をまとめるまでちょっと時間が空いているので設定の流れなど違ってる部分があるかもしれませんm(__)m

#reactionチャンネルに常にすべてのリアクション情報が投稿される点はかなり微妙ですが、Slack APIもGASも初めて触ったのでいろいろ勉強になりました。

何より、今までは「了解」などの返事 をいちいちメッセージでスタンプ投稿していたのが、リアクションでいけるようになったのは個人的にかなり良かったです。

追記(2021/2/22)

昨日から夫のAndroidにSlackのリアクション通知がくるようになったらしく...。iPhoneには来ていないのでAndroid版のみ実装されたのか、ベータ版で一部ユーザに公開されているのか、どちらにしてもリアクション通知が公式で導入されたらアプリ不要ですw