🔥

Slack APIで投票機能っぽいものを作った

2022/11/27に公開

Slackで定期的にメッセージを送るくらいのことはBotを使ってやってみたことがあったが、投票機能っぽいものが欲しくなったのでSlack API(Bot)とGASで作ってみた。
(簡単なものなのでリッチなUIとかは全く考えてないです)

作りたいもの

欲しい機能

  • 特定チャンネルに自動でメッセージを投稿
  • 投稿したメッセージに対する返信を取得(投票の選択肢とする)
  • それらの返信に対してリアクション数を取得し、最もリアクションの多いものを結果として送信

実際の画面

スレッド元のメッセージを自動送信

一番おいしいお茶を決める戦い。

投稿したメッセージに対する返信を取得

おーいお茶、綾鷹などを「投票の選択肢」として取得する。
(取得するといっても実際にはリアクション数くらいしかとってないけど)

リアクションの集計&結果の送信

選ばれたのは綾鷹でした。
(結局綾鷹が一番おいしい)

使いどころ

  • 選択肢を誰でも簡単に増やせる投票がしたいとき
  • 定期的な投票が必要なとき

定期的で、尚且つ、誰でも選択肢を追加できるような投票がある場合は毎回誰かがスレッドを立てなくていいし、リアクションを数えて引用しなくても良くなる。

Slack API

リファレンス
https://api.slack.com/methods

今回使うメソッド

  • chat.postMessage
  • conversations.history
  • conversations.replies
  • reactions.get
  • chat.getPermalink

必要な権限

  • chat:write
  • channels:history
  • reactions:read

今回はチャンネルのみだが、自分のDMなのでこういったことをしたい人はim:historyとかもいるかも。

GAS

コード全体

postThread, postPickedReplyを実行することでそれぞれ投票スレッド作成と結果送信をすることができる。そのため、これらを何かしらのイベント(週一回など)で発火させることで定期的に投票の管理をしてくれる。

const token = "<TOKEN>";
const channel = "<CHANNEL_ID>";

// スレッドの元となるメッセージを送信
function postThread() {
  const message = [{
    "type": "section",
    "text": {
      "type": "mrkdwn",
      "text": (
        "【投票】一番おいしいお茶は?"
      ),
    },
  }];
  UrlFetchApp.fetch(
    "https://slack.com/api/chat.postMessage",
    {
      "method": "post",
      "content-type": "application/json",
      "payload": {
        "token": token,
        "channel": channel,
        "blocks": JSON.stringify(message),
      }
    }
  )
}

// 最も投票が多いメッセージのリンクを送信
function postPickedReply() {
  var res = UrlFetchApp.fetch(
    "https://slack.com/api/conversations.replies",
    {
      "method": "get",
      "content-type": "application/x-www-form-urlencoded",
      "payload": {
        "token": token,
        "channel": channel,
        "ts": getLatestThreadTs()
      }
    }
  );
  replies = JSON.parse(res).messages.filter(message => !("reply_count" in message));
  reactions = [];
  for (let i=0; i<replies.length; i++) {
    reactions.push({
      "ts": replies[i].ts,
      "reactions": getNumReactions(replies[i].ts)
    });
  }

  pickedReplyTs = reactions.sort((a, b) => b.reactions - a.reactions)[0].ts;
  const message = [{
    "type": "section",
    "text": {
      "type": "mrkdwn",
      "text": "選ばれたのはこちらです\n" + getChatPermalink(pickedReplyTs)
    },
  }];
  UrlFetchApp.fetch(
    "https://slack.com/api/chat.postMessage",
    {
      "method": "post",
      "content-type": "application/json",
      "payload": {
        "token": token,
        "channel": channel,
        "blocks": JSON.stringify(message),
      }
    }
  )
}

// 8日前までのスレッドで最新のタイムスタンプ取得
function getLatestThreadTs() {
  const oldest = Date.now() / 1000 - 691200 // 8日前まで
  var res = UrlFetchApp.fetch(
    "https://slack.com/api/conversations.history",
    {
      "method": "get",
      "content-type": "application/x-www-form-urlencoded",
      "payload": {
        "token": token,
        "channel": channel,
        "oldest": String(oldest) + "000"
      }
    }
  )
  res = JSON.parse(res);
  var ts = 0.0;
  for (let i=0; i<res.messages.length; i++) {
    if (res.messages[i].text.includes("投票") && parseFloat(res.messages[i].ts) > ts) {
      ts = res.messages[i].ts;
    }
  }
  return ts;
}

// 指定したタイムスタンプのメッセージに対するリアクション数を取得
function getNumReactions(ts) {
  var res = UrlFetchApp.fetch(
    "https://slack.com/api/reactions.get",
    {
      "method": "get",
      "content-type": "application/x-www-form-urlencoded",
      "payload": {
        "token": token,
        "channel": channel,
        "timestamp": ts
      }
    }
  );
  res = JSON.parse(res);
  if (!("reactions" in res.message)) return 0;
  const reactions = res.message.reactions;
  var count = 0;
  for (let i=0; i<reactions.length; i++) {
    count += reactions[i].count;
  }
  return count;
}

function getChatPermalink(ts) {
  var res = UrlFetchApp.fetch(
    "https://slack.com/api/chat.getPermalink",
    {
      "method": "get",
      "content-type": "application/x-www-form-urlencoded",
      "payload": {
        "token": token,
        "channel": channel,
        "message_ts": ts
      }
    }
  );
  return JSON.parse(res).permalink;
}

特徴的なところ

Slack APIはタイムスタンプが識別子

Slack APIのメソッドのほとんどはメッセージの特定にタイムスタンプを使用している。(メッセージIDのようなものは作られていない)そのため、とりあえずタイムスタンプ(とチャンネルID)を取得しておけば後からほかの情報も取得できる。

Slack APIを介した自動送信では一部のmarkdownしか使えない

これはリファレンスのどこかに書いてあったが、Slack APIを介したメッセージの送信では一部のmarkdown機能しか使うことが出来ないらしい。例えば、箇条書きのリストなどは確か使えなかったはずで、逆に、太字や斜体、引用などは普段と同様に使える。
(ただ、もしかしたら知らないだけで何かしら使える方法があるかもしれない)

Discussion