こんにちは、GROOVE X Advent Calendar 2023 9日目の記事です。
普段はスクラムマスターをしている niwano が、久しぶりにプログラムコードを書いことを紹介します。
プログラミング楽しい♫ 。
きっかけ
Slack user groupの仕様が想定と違った
2022年の春に弊社は、Slack を EnterpriseGrid版に移行しました。
ワークスペースを組織単位でいくつも作れることや、Slack connectが無料とのことで、メリットを感じて採用したのですが、EnterpiseGrid版 にしてがっかりだった仕様の第1位が、user groupのスコープがワークスペースの範囲だということです。
どういうことかというと、以下のようなイメージです。
EnterpriseGridになると、Organaizationという概念がうまれて、その中に複数のワークスペースを作成できます。
チーム単位だったり、組織単位だったり、大きなチャンネルグループのイメージです。
弊社では、基本、1ユーザーの所属は1つのワークスペースになるように管理されています。
user groupがワークスペースの中で閉じているので、ワークスペースにまたがるメンバーを同じグループにいれたり、異なるワークスペースの user groupを呼び出したりすることができません。
おまけに、異なるワークスペースの user group はマスクされてしまう。
マスクされるというのはこんな感じです。
トーフっぽい。(トーフで年齢がばれますね!)
以前はマスクされてなかったんですけどね....
設定で変えられるようになるのを期待してます!Slackさんお願いします!
自作している人をみつけた
インターネット上では同じように困っている人がいて、自作していたので、エンジニア魂に火がついたわたしは、「自分でつくってみるかー」となりました。
仕様はこんな感じ "GroupMentionBot"
メッセージ上のグループメンションを検出して、
他のワークスペースに同じグループ名が存在したら、そのグループ名をリプライします。
その時に、メッセージの抜粋をちょっとつけます。
その名も、"GroupMentionBot" です。
こんなイメージです。 <画像はる>
作ってみる
システム構成
最初はGAS上で動かしていましたが、postMessageのACKを5秒以内(今の仕様は分からない)に返さないと、Slackが再通知を行う制限に頻繁にかかり、おなじリプライが何個も発生する...みたいなことが置きてました。
(これもやってみないとわからないですよね〜)
ですので、今は大好きなGO言語でcloud Pub/Sub と Cloud Functionsで実装しています。
Slack appから EventSubscriptionsで、メッセージイベントを一度、Cloud Functionsで受け取ります。ACKを返したあと、Pub/Subのキューにメッセージを入力することで、前述のタイムアウトを回避します。
Pub/Subから取り出す側のCloud Functionsで、メッセージ内のgroupメンションを解析し、リプライメッセージを postMessage( ) します
ポイント
作成するにあたっての、ポイントは3点でした。
(最初からこの形になったのではなく、試行錯誤をしてこういう形になったんです〜)
- Slack connectにも対応したい
- なるべくメンテナンスレスにしたい
- リプライにはメッセージの抜粋をつける
Slack connectにも対応したい
弊社は、Slack connectをつかって沢山の外部ワークスペースの人とコラボレーションをしています。
その方々とも、あたかも、同じ user group を使っているような動きがほしかったのと、その方々からも、グループメンションで呼び出されたい!
前者はスプレッドシートにて、個別IDをメンション候補に追加することで対応しています。
シートの内容は以下のような感じです。
合致する user group があった場合には、members id に定義された個別IDをリプライします。
後者は、平文での user groupの入力にもGroupMentionbotがリプライができるようにしました。
なるべくメンテナンスレスにしたい
最初は、GroupMentionBotが検出する user group をスプレッドシートに記載していたのですが、追加の要望が頻繁にあるので、検出用のシートは廃止し、user groupの接頭置で実施するかどうかを判断することにしました。
弊社では、group 名のあたまに、 * @group * @team がついている user group にのみ、GroupMentionBotが反応します。
リプライにはメッセージの抜粋をつける
当初は GroupMentionBotからのリプライは、group mentionのみの味気ないものでした。
この方法でも通知はできるのですが、Slack の Activityリストにはメッセージのない GroupMentionBotからの通知が貯まるんですね。
そこで、GroupMentionBotからの通知に本メッセージの文章を先頭から3行抜粋することにしました。 また、抜粋しているメッセージの中に user group が含まれると二重で通知されることになるので、user group を削除する処理をつけてます。
こんな感じです。
ところが... rate limit にかかる
ある時、メンバーから、BOTが動いてないみたいです!という連絡が。
Cloud Functionsのログをみてみると...
slack rate limit exceeded, retry after 30s
と出てました。むむ....
リプライする user group を決定するために、usergoup.listのAPIを使っているのですが、このAPIが、Tier2
という定義で、毎分20リクエストに制限がかかっていました。
よくよく処理をみると、全てのメッセージに対して、不要に usergroup.list
をコールする処理になっていました。
メッセージのなかに、user group があったときにのみ usergroup.list
をコールして処理すればいいですし、なんなら、Pub/Sub のまえにチェックしたらいいので、チェック処理の場所を変えて対応しました。
でも、心配なので、Tier3
に変えて欲しい気もする...。
(そんなことできるのかな)
1分に20個の group mention しか処理できないです!笑
まとめ
いかがでしたでしょうか!
みなさんの Slack 生活の参考になれば幸いです!
おまけ
slackメッセージの中の group mentionを抽出する処理サンプル
package p import ( "regexp" ) // Find and extract '@group_xxxx' and '@team_yyyy' from the message, // capturing @group_xxxx and @team_yyyy. func extractGroupID(text string) (groupIDs []GroupID) { // <!subteam^AAAA|@BBBB> があったら検出しないように消す subRegex, err := regexp.Compile(`<!subteam\^[A-Z0-9]+\|@[^\>]+>`) if err != nil { return nil } text = subRegex.ReplaceAllString(text, "") regex, err := regexp.Compile(`@(group_[^\s>]+|team_[^\s>]+)([ >\n]|$)`) if err != nil { return nil } matches := regex.FindAllStringSubmatch(text, -1) for _, match := range matches { if len(match) > 1 { groupID := GroupID{ handleName: "@" + match[1], subTeamID: "", } groupIDs = append(groupIDs, groupID) } } return groupIDs } // Find and extract <!subteam^AAAAAA|@bbbbbb> // capturing @bbbbbb func extractSubTeam(text string) (groupIDs []GroupID) { regex, err := regexp.Compile(`<!subteam\^([A-Z0-9]+)\|@(group_[^\>]+|team_[^\>]+)>`) if err != nil { return nil } matches := regex.FindAllStringSubmatch(text, -1) for _, match := range matches { if len(match) > 2 { groupID := GroupID{ handleName: "@" + match[2], subTeamID: match[1], } groupIDs = append(groupIDs, groupID) } } return groupIDs } func extractGroupIDAndSubTeam(text string) []GroupID { groupIDs := extractGroupID(text) groupIDs = append(groupIDs, extractSubTeam(text)...) return groupIDs }