🦁

【Slack】Event Subscriptionsを使って記事要約アプリを作ろう(GAS × Slack × Chat GPT)

2023/08/18に公開

はじめに

以前、業務委託先で使用する業務委託用の勤怠管理アプリをGASとSlackとスプシで作りました。

その際に開発でハマった箇所の記事を作ったのですが、今回は前回の開発を踏まえて新しくGASとSlackとChat GPTで記事要約アプリを作ったので、その振り返り記事になります。

https://zenn.dev/shuuuuuun/articles/9de830ec37de30

完成品

まずは今回作った記事要約アプリを具体的に掘り下げていきたいと思います。

仕組み自体は簡単でRSS Botの投稿に対してスレッドに要約内容を投稿するだけです。要約内容はChat GPTにRSS Botの投稿を食わせています。細かい箇所に触れると「要約して」スタンプを押下したらスレッドに要約内容を投稿させたり、RSS Botの内容が海外の記事であることも考慮して「日本語に訳して欲しい」とプロンプトを調整したりしています。

今回紹介するコードの全体像は最後に載せているので、もし説明不要でしたら下部の「まとめ」までスキップしてください。

RSSインテグレーションを追加する

具体的なRSSインテグレーションの追加に関しては別途紹介記事がありましたのでそちらをご覧ください。とりあえずは下記を登録してみると良いかもしれません。

https://vercel.com/atom
https://nextjs.org/feed.xml
https://status.firebase.google.com/en/feed.atom

https://zenn.dev/kenghaya/articles/6ff68c70235d49

必要なBot Eventを追加する

今回はRSS Botの投稿(message)と「要約する」スタンプ(reaction_added)をトリガーにスレッド投稿するので添付画像のBot Eventを追加します。

Bot Eventを追加することで様々なイベントに対しての情報を取得することができます。例えばreaction_addedを追加した場合、押下したスタンプの名前や誰が押下したのかなどの情報を取得することが可能になります。

Bot Event追加後は下記のように条件分岐させることが可能になります。クラスメソッドさんがBot Eventの一覧をまとめてくださっているので添付します。

const main = (e: any) => {
  const params = JSON.parse(e.postData.getDataAsString());
  const event = params.event;
  const type = event.type;

  if (["message"].includes(type)) {} // message.channels
  if (["reaction_added"].includes(type)) {} // reaction_added
};

https://dev.classmethod.jp/articles/slack-bot-event-reference/

メッセージ履歴を取得する仕組みを知る

「メッセージA」に対して何かしらのトリガー(メッセージ投稿やスタンプ押下)をきっかけに「メッセージA」のスレッドへ投稿できる仕組みが必要になります。

ここではどのように「メッセージA」を取得できるか触れます。

メッセージ取得にはSlack APIのconversations.repliesを使用します。(必須パラメータはchannel, ts, tokenになります。)

記事によってはconversations.historyが使用されていますがSlack APIのアップデートに伴いconversations.repliesを使用するのがベターのようです。

https://api.slack.com/methods/conversations.replies

export const getConversationsReplies = (channelId: string, ts: string) => {
  const payload = {
    token: process.env.BOT_USER_OAUTH_TOKEN,
    channel: channelId,
    ts: ts,
    limit: 1,
    inclusive: true,
  };

  const response = UrlFetchApp.fetch(
    "https://slack.com/api/conversations.replies",
    {
      method: "post",
      contentType: "application/x-www-form-urlencoded",
      payload: payload,
    }
  );

  const conversationsHistory = JSON.parse(response as any);
  return conversationsHistory.messages[0];
};

スレッド投稿の仕組みを知る

「メッセージA」に対して何かしらのトリガー(メッセージ投稿やスタンプ押下)をきっかけに「メッセージA」のスレッドへ投稿できる仕組みが必要になります

ここではどのように「メッセージA」のスレッドへ投稿できるか触れます。Incoming Webhooksについては過去に触れているので記事を載せておきます。

https://zenn.dev/shuuuuuun/articles/36a980f97c4c34

また、スレッドへの投稿はpayloadにthread_tsを追加すれば実現できるようです。

export const sendToSlack = (
  channelId?: string,
  thread_ts?: string,
  message?: any
) => {
  const url = process.env.SLACK_INCOMING_WEBHOOK;

  if (!url) return;

  const payload = JSON.stringify({
    token: process.env.BOT_USER_OAUTH_TOKEN,
    channel: channelId,
    text: JSON.stringify(message),
    thread_ts: thread_ts,
  });

  UrlFetchApp.fetch(url, {
    method: "post",
    contentType: "application/json",
    payload: payload,
  });
};

スレッド投稿する際に必要なchannelとtsの取得方法

ここまででRSS Botの投稿(messege)とスタンプ(reaction_added)をトリガーに情報を取得できるようになり、メッセージの取得からスレッド投稿の仕組みを知ることができました。

次にどうすれば必須パラメータであるchannelとtsを取得できるかですが、ここで気を付けて欲しいのはmessegeとreaction_addedではスレッド投稿に必要なchannelとtsの取得方法が異なることです。

具体的には下記になります。

const main = (e: any) => {
  const params = JSON.parse(e.postData.getDataAsString());
  const event = params.event;
  const type = event.type;

  if (["message"].includes(type)) {
    const channelId = event.channel;
    const thread_ts = event.thread_ts || event.ts;
  }
  if (["reaction_added"].includes(type)) {
    const channelId = event.item.channel;
    const thread_ts = event.item.ts;
  }
};

Slackの3秒ルールで複数回レスポンスする

以下、補足内容になります。

前回の記事で「キャッシュすることでSlackの3秒ルールよる複数回レスポンスを避ける方法」について取り上げましたが今回の実装でここを改修する必要がありました。

改修前
const params = JSON.parse(e.postData.getDataAsString());

const cache = CacheService.getScriptCache();
if (cache.get(params.event.client_msg_id) == "done") return;
cache.put(params.event.client_msg_id, "done", 600);

https://zenn.dev/shuuuuuun/articles/9de830ec37de30#slackの3秒ルールで複数回レスポンスする

具体的にはcacheの対象をparams.event.client_msg_idからparams.event_idへ変更しました。このように変更することでreaction_addedに対してもキャッシュを効かせます。

改修後
const params = JSON.parse(e.postData.getDataAsString());

const cache = CacheService.getScriptCache();
if (cache.get(params.event_id) == "done") return;
cache.put(params.event_id, "done", 600);

まとめ

ここまでくれば、RSS Botの投稿(messege)とスタンプ(reaction_added)をトリガーにメッセージを取得してそのメッセージのスレッドに投稿することができるようになっているはずなので、あとは好みに合わせてスレッド投稿する条件をカスタマイズしたりChat GPTの処理を書くのみになります。

今回紹介したコードの全体像は次になります。筆者の場合は前述している通りRSS Botの投稿と「要約して」スタンプの時だけ反応させたいので、そのバリデーションを追加しています。

const main = (e: any) => {
  const params = JSON.parse(e.postData.getDataAsString());

  // NOTE:SlackのEvent SubscriptionsのRequest Verification用
  if (params.type === "url_verification") {
    return ContentService.createTextOutput(params.challenge);
  }

  const event = params.event;
  const type = event.type;
  const bot_id = event.bot_id;

  // NOTE:Slack Botによるメンションを無視する(無限ループを回避する)
  // NOTE:自分自身(mosukun Bot)だけは無視して他のbotとの会話は可能にする
  if (bot_id === "B05CW8PF316") return;

  // NOTE:Slackの3秒ルールで発生するリトライをキャッシュする
  const cache = CacheService.getScriptCache();
  if (cache.get(params.event_id) == "done") return;
  cache.put(params.event_id, "done", 600);

  // NOTE:以下からメインの処理

  if (["message"].includes(type)) {
    // NOTE:RSS Bot以外は早期リターンする
    if (bot_id !== "B03GXHC7BMF") return;

    const channelId = event.channel;
    const thread_ts = event.thread_ts || event.ts;

    const message = getConversationsReplies(channelId, thread_ts);
    const content = messageFormatter(message);
    const text = getChatGptMessage(`日本語で要約してください。${content}`);

    sendToSlack(channelId, thread_ts, text);
  }

  if (["reaction_added"].includes(type)) {
    // NOTE:「要約して」スタンプだけに反応させる
    if (event.reaction !== "youyaku") return;

    const channelId = event.item.channel;
    const thread_ts = event.item.ts;

    const message = getConversationsReplies(channelId, thread_ts);
    const content = messageFormatter(message);
    const text = getChatGptMessage(`日本語で要約してください。${content}`);

    sendToSlack(channelId, thread_ts, text);
  }

  return;
};

(global as any).doPost = main;

最後に

前回の記事をベースに効率よく記事要約アプリを作ることができました。また、メッセージの取得、スレッド投稿、Bot Eventを追加した際のスタンプの挙動を知ることができ、前に比べて実装できる幅が広がったと思います。

本アプリを作った背景はSlackの記事共有チャンネル(RSSあり)でメンバーが共有してくれた記事を他のメンバーが呼んでくれるハードルを下げるために作りました。

今回の反省点としてはChat GPTを無料枠で使用しているので制限がかかってRSS Botの投稿に対するリアクションが止まってしまうことと、思ったより要約の精度が悪かったことです。これに関してはChat GPTに食わせるプロンプトの向上と、そもそも有料枠(4.0)にすることで解決しそうです。がお金がないので未検証です...

駆け足になりましたが、この記事がどなたかの参考になりましたら幸いです。最後までご覧いただきありがとうございました。

参考記事

https://zenn.dev/ozushi/articles/ebe3f47bf50a86

Discussion