目次
Connector

Slack

Slack Connectorは、ワークスペースのチャンネル、メッセージ、ユーザー、ファイルを読み、メッセージを投稿・編集し、ファイルを共有し、チャンネルをアーカイブします。プログラムが見つけたことや行ったことを人に伝える手段であり(リスクの高い取引先のまとめをチャンネルに、LLMの答えをスレッドの返信に)、人が書いたことを読む手段でもあります。

読み取り元

  • slack.channels — アプリから見えるチャンネル。id、name、topic、purpose、member_count、is_private、is_archived、created。キーは id です。
  • slack.messages — メッセージ。id(Connectorが作る channel:ts の識別子。キー)、channel、ts(チャンネルの中でのメッセージの識別子)、user、text、thread_ts、reply_count、reactions(名前と数の配列)、edited、files(ファイルidの配列)、そして posted_at(timestamp)。キーは id です。チャンネルと期間で絞り込んでください。ワークスペース全体の履歴を読むのは、プログラムの規模に収まる読み取りではありません。
  • slack.users — メンバー。id、name、real_name、display_name、email(スコープが許す場合)、tz、is_bot、deleted。キーは id です。
  • slack.files — ワークスペースで共有されたファイル。resource、version(ファイルid)、name、media_type、size、user、channels(配列)、created。チャンネルかユーザーで絞り込みます。
  • slack.contents — ファイルのバイト列を content_ref として読みます。resource、version、media_type、content。slack.files から resource と version でJOINしてください。ConnectorはJOINで指定されたファイルだけをダウンロードします。

型

Slackの値ActQL備考
ts(メッセージのタイムスタンプ)stringSlackが識別子として使う値。文字列のまま保つ。posted_at は同じ時点を timestamp にしたもの
id(C…、U…、F…)string
textstring送られたままのSlackのmrkdwn。ユーザーとチャンネルへのメンションは <@U…> の形のまま
reactionsarray<object>name、count、users
filesarray<string>ファイルid。slack.files とJOINする
created、edited.tstimestamp
真偽値(is_private、is_bot など)bool
member_count、reply_countint64

Effectと操作

メッセージは、存在した時点でレコードです。編集と削除は channel と ts をキーにしたレコードのEffectになります。

  • update — EFFECT UPDATE slack.messages ID id SET (text = …) は、アプリが投稿したメッセージを編集します(chat.update)。
  • delete — EFFECT DELETE slack.messages ID id は、メッセージを削除します(chat.delete)。

キーはちょうど1件のメッセージを識別しなければなりません。channel と ts の組は常にそうです。Slackでは、トークンに管理者のスコープがなければ、アプリは自分のメッセージしか編集・削除できません。

コンテンツ:

  • store — EFFECT STORE slack.files SET (channel = …, name = …, content = content [, thread_ts = …]) は、レコードの content(SERIALIZE または contents の読み取り元から来た content_ref)をアップロードし、チャンネルで共有します。

操作。投稿はメッセージを作って人に通知するので、レコードの書き込みではなく操作です。

  • post — EFFECT COMMAND slack.post TARGET channel ID key INPUT (text = …)。
  • reply — EFFECT COMMAND slack.reply TARGET channel ID key INPUT (thread_ts = …, text = …) は、スレッドに投稿します。
  • react — EFFECT COMMAND slack.react TARGET channel ID key INPUT (ts = …, name = 'white_check_mark')。
  • archive_channel — EFFECT COMMAND slack.archive_channel TARGET channel ID key。

TARGET は操作の対象となるチャンネルで、ID はレコードのキーです。ID はプレビューとステータスで各操作を識別するもので、メッセージの本文やチャンネルではありません。本文はプレーンなmrkdwnとして送ります。プログラムがBlock Kitのペイロードを組み立てることはありません。

セットアップ

"slack": {
  "connector": "slack",
  "credentials": {"bot_token": {"env": "SLACK_BOT_TOKEN"}}
}

ワークスペースにSlackアプリをインストールし、そのbotトークンを使います。読み取りには channels:read、channels:history(プライベートチャンネルには加えて groups:*、ダイレクトメッセージには im:* と mpim:*)、users:read(メールアドレスには users:read.email)、ファイルには files:read が必要です。投稿・編集・削除には chat:write、ファイルの共有には files:write、リアクションには reactions:write、アーカイブには channels:manage(または groups:write)が必要です。アプリは、読んだり投稿したりするチャンネルのメンバーである必要があります。ActQL CloudではSlackアプリをホスト型OAuthでインストールし、ローカルのHostではbotトークンを直接使います。

Capabilityとプッシュダウン

演算プッシュダウンされる条件
列の選択エンジンが保持する量を減らす。Slackはオブジェクト全体を返す
WHEREメッセージに対する channel = … と、ts / posted_at の期間(conversations.history の oldest/latest)。チャンネルとユーザーに対する id = …。ファイルに対する channel か user。テキストの述語はHostで実行する。Slackの検索APIは、ランキングとトークン化がSQLの意味と一致しないので使わない
ORDER BYメッセージの ts の昇順・降順。それ以外はHostで
LIMIT常に。ページサイズとカーソルで実現する
GROUP BY、JOINされない。Hostで実行する

プレビューと突き合わせ

プレビュー。 更新と削除は full_diff です。Connectorは channel と ts でメッセージを読み、現在の本文と edited のタイムスタンプを記録し、プレビューには現在の本文と新しい本文が出ます。適用時にメッセージをもう一度読み、本文か edited のタイムスタンプが変わっていれば、項目は failed(conflict)で終わり、何も送りません。投稿、返信、リアクション、アーカイブ、アップロードは targeted です。プレビューには、チャンネル、本文そのもの(またはファイル名とサイズ、リアクション)、返信ならそのスレッドが出ます。

突き合わせ。 投稿はSlack側では冪等ではないので、すべての投稿と返信はメッセージの metadata にリクエストのマーカーを持ちます(event_type: actql_effect と、Effectの項目のid)。応答が失われたときは、チャンネルの最近の履歴からそのマーカーを持つメッセージを探して突き合わせます。同じ本文でもマーカーのないメッセージは証跡になりません。編集でも、更新後のメッセージの metadata にマーカーを入れます。そのため編集は、メッセージを読んで、承認された本文の隣にマーカーがあることで突き合わせます。本文だけ、あるいはリクエストの時間帯にある edited のタイムスタンプだけでは証跡になりません。削除、リアクション、アーカイブにはSlackが返すマーカーがないので、応答が失われた項目は unknown のままです。メッセージの不在、付いているリアクション、チャンネルのアーカイブ状態は証跡になりません。ファイルのアップロードは、files.list でリクエストのファイル名と、ファイルの title にあるマーカーを探して突き合わせます。証跡がなければ、項目は unknown のままで、何も再送しません。操作は、Slackが ok: true を返した時点で accepted、読み返して確認できた時点で applied になります。

上限と費用

  • Slackはワークスペースごとにメソッド単位でレート制限をかけます(conversations.history はおおよそ毎秒1リクエスト、chat.postMessage はチャンネルごとにおおよそ毎秒1件)。Connectorはその範囲内でペースを調整するので、長い期間を読むプログラムは相応に時間がかかり、件数の上限で抑えられます。
  • 履歴は1回に200件(1リクエストの最大は1,000件)でページングします。プログラムの件数の上限を超えそうな読み取りは、途中までの結果を返すのではなくエラーで止まります。
  • ファイルはワークスペースのプランのストレージの範囲で、ダウンロードにはプログラムのバイト数の上限が適用されます。
  • SlackのAPIに利用料はありません。制約になるのはレート制限と、プランの保持期間です。

例

今週のチャンネル別メッセージ数です。

FROM slack.messages
|> WHERE posted_at >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 7 DAY)
|> AGGREGATE COUNT(*) AS messages GROUP BY channel
|> ORDER BY messages DESC;

リスクの高い取引先のまとめを1件のメッセージとして投稿します。

SELECT a.name, risk.value.reason AS reason
FROM salesforce.accounts AS a
JOIN snowflake.usage_30d AS u ON a.id = u.account_id
|> INFER `model-task:churn-risk.v1`
     INPUT (notes = a.description, active_users = u.active_users, trend = u.trend) AS risk
|> WHERE risk.outcome = 'ok' AND risk.value.level = 'high'
|> AGGREGATE STRING_AGG(CONCAT('• ', name, ': ', reason), '\n') AS body
|> EXTEND CURRENT_DATE() AS day
|> EFFECT COMMAND slack.post TARGET 'C0123456789' ID day
     INPUT (text = CONCAT('High churn risk today:\n', body));

サポートチャンネルの未回答の質問に、LLMの返事をスレッドで返します。

FROM slack.messages AS m
|> WHERE m.channel = 'C0SUPPORT' AND m.thread_ts IS NULL AND m.reply_count = 0
     AND m.posted_at >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 1 DAY)
|> INFER `model-task:support-reply.v1` INPUT (question = m.text) AS d
|> WHERE d.outcome = 'ok' AND d.value.confidence = 'high'
|> EFFECT COMMAND slack.reply TARGET m.channel ID m.id
     INPUT (thread_ts = m.ts, text = d.value.text);

Source: actql/actql/docs/connectors/slack.md at d904416674e4 · Edit on GitHub