📑

[Slack]ランダム指定でデイリースクラムのファシリテーター決定を楽にした話

2025/01/06に公開

はじめに

レセプトチームでは、下記の記事の通り、スクラム開発への移行の真っ只中。

https://zenn.dev/rehabforjapan/articles/db07e0f1586be8

まだまだ課題は散積しているが、その中の一つに会議体のファシリテーション誰がやるか問題があった。(とくにデイリースクラム)

もともとウォーターフォール型の開発方式で進めていたときは、ファシリテーション(以下、ファシリと略)は、日次の進捗確認会から全てリーダークラスで巻き取って進めていた。

スクラム開発へ移行し自己組織化されたチームを目指すにあたって、会議体のファシリは誰しもができるようになるべき。

また誰でもできるという運用面を考えて、できるだけSlackで運用が完結することを目指した。

本記事では、Slack上でファシリを回す方法をいくつか調査、実装を行い、実際に運用してみた感想まで記載してみた。

ターゲット

  • Slack上でファシリを回したいと考えている人
  • Slack上のランダム関数みたいなものを探している人

この記事で伝えたいこと

  • お手軽にやるならSlackbotのカスタムレスポンス
  • カスタムレスポンスじゃメンテがめんどいと思うなら、自作スラッシュコマンド
  • 定期通知したいなら、自作bot

slackbotのカスタムレスポンスはお手軽

最初に思いついた案は以前社内ですでに活用されていたSlackのカスタムレスポンス案。

以下のようにファシリを回すメンバーを複数人指定して、Slack上で決定した文字列を発言するだけ。
alt text

デメリットは、私基本ずぼらなので絶対カスタムレスポンスのメンテ忘れる & 有識者がいなくなるとメンテの運用が忘れられる。ということで別案を考えてみた。

対応方針

Slackで作成しているユーザーグループのなかで誰か一人をランダムに指定するサイコロみたいなツールがあれば、会議の前にサイコロふってファシリ決めればいいんじゃね?と思いつく。

けど、こんな誰でも思いつくようなツールはすでにSlack公式で作ってるはず、、と思いきや全くヒットせず、みんな自作しているらしい。

じゃあ作るか、ということで実装。

実装内容

案1:ユーザグループからランダムで指定するスラッシュコマンド実装

構成は、AWS api gateway + lambda。
コードは以下の通り。

app.py

from flask import Flask, request, jsonify, make_response
import random
import os
from slack_sdk import WebClient
from slack_sdk.errors import SlackApiError
from werkzeug.middleware.proxy_fix import ProxyFix
from mangum import Mangum
from asgiref.wsgi import WsgiToAsgi
import re

# 環境変数を設定
SLACK_BOT_TOKEN = "XXX"
# Slackクライアントの初期化
client = WebClient(token=SLACK_BOT_TOKEN)
app = Flask(__name__)
# AWS Lambda用にFlaskアプリを変換
app.wsgi_app = ProxyFix(app.wsgi_app)
# FlaskアプリをASGI準拠に変換
asgi_app = WsgiToAsgi(app)

# Mangumハンドラーの設定
handler = Mangum(asgi_app)

@app.route('/pick_random', methods=['POST'])
def pick_random():
    data = request.form
    
    channel_id = data.get('channel_id')
    print(channel_id)
    
    subteam_string = data.get('text')  # 引数を取得
    usergroup_id = extract_usergroup_id(subteam_string)
    print(usergroup_id)
    
    if not usergroup_id:
        return jsonify({"text": "Invalid user group mention format"})


    # ユーザーグループのメンバーを取得
    members = get_usergroup_members(usergroup_id)
    if not members:
        print("No members found in the user group.")
        return jsonify({"text": "No members found in the user group."})
    
    # メンバーをランダムに選択
    random_member = random.choice(members)
    
    # メンション用メッセージを作成
    message = f"🎉 ランダムに選ばれたメンバーは <@{random_member}> さんです!"
    
    # メッセージをチャンネルに送信
    send_message(channel_id, message)
    return make_response('', 204)  # 空のレスポンスを返す


def get_usergroup_members(usergroup_id):
    """
    ユーザーグループのメンバーリストを取得
    """
    try:
        response = client.usergroups_users_list(usergroup=usergroup_id)
        members = response["users"]
        return members
    except SlackApiError as e:
        print(f"Error fetching user group members: {e.response['error']}")
        return []

def send_message(channel_id, message):
    """
    メッセージを特定のチャンネルに送信
    """
    try:
        client.chat_postMessage(channel=channel_id, text=message)
    except SlackApiError as e:
        print(f"Error sending message: {e.response['error']}")

def extract_usergroup_id(subteam_string):
    """
    Slackのユーザグループメンション文字列からユーザグループIDを抽出する
    """
    match = re.search(r'<!subteam\^([A-Z0-9]+)\|', subteam_string)
    if match:
        return match.group(1)
    return None

ポイントは以下の通り。

  • スラッシュコマンドの引数で、ユーザグループの情報を受け取る
  • slack apiを実行して、ユーザグループの中でだれが所属しているかを取得
  • ランダム関数でその中の1名を指定して、slack apiで投稿

実装して、スラッシュコマンドは叩ける!あとは運用するだけ!というところで思わぬ想定外が2つあった。

Slackリマインダーでは、スラッシュコマンドは実行できない

定期実行するには、標準のSlackリマインダー ではスラッシュコマンドは文字列として認識されてしまうようで実行できなかった。

スラッシュコマンドは、スレッド内では実行できない

Slackリマインダーで、文字列だけしか投稿できないなら、「次のコマンドを叩いてね!」っていう依頼とともにスレッド内でスラッシュコマンドを実行してもらおうと思ったが、現状スレッド内では実行できないらしい。

案2:定期的にプログラムを実行するbotで通知

構成は、AWS lambda + EventBridge。
コードは以下の通り。

lambda_function.py
import os
from slack_sdk import WebClient
from slack_sdk.errors import SlackApiError
import random
import json

def get_usergroup_members(client, usergroup_id):
    """
    ユーザーグループのメンバーリストを取得
    """
    try:
        response = client.usergroups_users_list(usergroup=usergroup_id)
        members = response["users"]
        return members
    except SlackApiError as e:
        print(f"Error fetching user group members: {e.response['error']}")
        return []

def get_usergroup_name(client, usergroup_id):
    """
    ユーザーグループIDから表示名を取得
    """
    try:
        response = client.usergroups_list()
        usergroups = response["usergroups"]
        for usergroup in usergroups:
            if usergroup["id"] == usergroup_id:
                return usergroup["name"]
        return None
    except SlackApiError as e:
        print(f"Error fetching user group name: {e.response['error']}")
        return None

def send_message(client, channel_id, message):
    """
    メッセージを特定のチャンネルに送信
    """
    try:
        client.chat_postMessage(channel=channel_id, text=message)
    except SlackApiError as e:
        print(f"Error sending message: {e.response['error']}")

def execute(usergroup_id, SLACK_BOT_TOKEN, SLACK_CHANNEL_ID):
    # .envファイルを読み込む
    load_dotenv()
    # 環境変数を設定
    SLACK_BOT_TOKEN = os.getenv("SLACK_BOT_TOKEN")  # Botユーザーのトークン 
    SLACK_CHANNEL_ID = os.getenv("SLACK_CHANNEL_ID")  # 通知を送るチャンネルID 


    # Slackクライアントの初期化
    client = WebClient(token=SLACK_BOT_TOKEN)

    # ユーザーグループの表示名を取得
    usergroup_name = get_usergroup_name(client, usergroup_id)
    if not usergroup_name:
        print("User group name not found.")
        return

    # ユーザーグループのメンバーを取得
    members = get_usergroup_members(client, usergroup_id)
    if not members:
        print("No members found in the user group.")
        return
    
    # メンバーをランダムに選択
    random_member = random.choice(members)
    
    # メンション用メッセージを作成
    break_message = f""" :ice_cube: <!subteam^{usergroup_id}> 本日のデイリースクラムのアイスブレイクネタ募集です!この投稿のスレッドに記載お願いします。 :ice_cube: 
    
    ### デイリースクラムの流れ 
    ・ アイスブレイク5minほど
    ・ ボードを見ながら進捗と今日やること課題の報告・相談
    ・ 全体共有事項(あれば)

    本日のファシリ担当は <@{random_member}> さんです! :memo:
    都合合わなさそうであれば、以下のコマンドで再抽選を行ってください。

    /pick_random <@{usergroup_name}>
    """    
    # メッセージをチャンネルに送信
    send_message(client, SLACK_CHANNEL_ID, break_message)
 

def lambda_handler(event, context):
    usergroup_id = event["usergroup_id"]  # ユーザグループのID ローカルは固定値を渡す、lambdaではeventから取得、EventBridgeから取得
    # 環境変数を設定
    SLACK_BOT_TOKEN = os.getenv("SLACK_BOT_TOKEN")  # Botユーザーのトークン 
    SLACK_CHANNEL_ID = os.getenv("SLACK_CHANNEL_ID")  # 通知を送るチャンネルID 

    execute(usergroup_id, SLACK_BOT_TOKEN, SLACK_CHANNEL_ID)
    return {
        'statusCode': 200,
        'body': json.dumps('Hello from Lambda!')
    }

ポイントは以下の通り。

  • EventBridgeで日時指定、定刻になったら仕込んでる引数(ユーザグループID)でlambdaを実行
  • 引数で渡されたユーザグループIDで、slack api実行、ユーザグループの中でだれが所属しているかを取得
  • ランダム関数でその中の1名を指定して、指定のチャンネルへslack apiで投稿

以下が投稿文。ファシリ担当者不在時のリカバリーにせっかく作ったスラッシュコマンドは利用させてもらっている。

alt text

運用してみての感想

ファシリを回したいという本来の目的は達成できている。専用通知botにしたことで、通知文は自由に変更できるので、上記のようにデイリースクラムのファシリの手順や、再選抜のスラッシュコマンドも書いたりして、スムーズにファシリできるようにしてみたり。

最後に

レセプト開発チームでは、上記以外でも日々改善を繰り返して、ほんとうの意味で自己組織化された強いチームになるように取り組んでいます。

追伸:Slackさん、ユーザーグループを引数にランダムに指定できるスラッシュコマンドの標準実装をお願いします 🤲

Rehab Tech Blog

Discussion