👍

slackでリアクションしたら、チャンネルにピン留めするAPIを作る

2024/08/16に公開

slackのリアクションイベントをフックに何か便利なことを出来ないかと触っていたら、案外知見が溜まったのでメモ。

やりたいこと

slackを使う時に特定のアクションをしたら、そのチャンネルにピン留めする。
今回は「👍リアクション」をしたらピン留めするようにしてみた。
👍リアクションを外したらピン留めからも削除される。

動作はこんな感じ↓

環境はAWSでAPI Gateway + Lambdaで作成しました。

事前準備

  • slackアカウントの作成
  • API Gateway + Lambdaの作成(Lambdaの中身はなんでもよし)

やり方

まずはslack apiの画面からAppを作成します。

  • App Name
    • 「英語」 で作成するAppの名前を入れます
  • Pick a workspace to develop your app in:
    • 追加するワークスペースを選択します

この時、App Nameを日本語で作成すると詰む可能性があります。(私は詰みました)
具体的にはこの後の作業である「Install your app」で、ワークスペースにインストールができません。(エラーも出ない)
そのため英語で設定しましょう。

OAuth & Permissions設定

APIのトークンを取得します。先にScopeを選択しろと言われるので、下にスクロールします。

Scopesでは、リアクションを取得する権限とピン留めをするために以下を付与しておきます。

  • reactions:read
  • pins:write

スコープを設定したら、トークンを生成します。
「Install to {WorkSpace名}」ボタンを選択して、Appとワークスペースを連携します。

作成できました。これはLambdaの環境変数として置いておきます。
セキュリティに懸念がある場合は、適宜パラメータストアやSecrets Managerなどを使ってください。

Event Subscriptions設定

Appが作成できたら、event Subscriptionsへ移動します。
事前に作成しておいたAPI Gatewayのエンドポイントをここにいれます。

URLを入力した際に、チャレンジリクエストが勝手に試され、有効なURLかのチェックが走ります。
そのためLambda側でこのリクエスト用のレスポンスを用意してあげます。

import json


def lambda_handler(event, context):

    body = json.loads(event['body'])

    # slackのチャレンジリクエスト用
    if 'challenge' in body:
        return {
            'statusCode': 200,
            'body': body['challenge']
        }

チャレンジリクエストが問題なければ、「Subscribe to bot events」を設定します。
reaction_addedreaction_removed の2つを選択します。

API実装

さて、設定ができたのでLambdaの実装を行なっていきます。
とりあえずは👍という、特定の絵文字が押されたことを取得します。

event_data = body['event']
reaction = event_data['reaction']
event_type = event_data['type']

# 👍リアクションが追加された場合
if reaction == '+1' and event_type == 'reaction_added':
    logger.info('👍リアクションが追加されました')

# 👍リアクションが削除された場合
if reaction == '+1' and event_type == 'reaction_removed':
    logger.info('👍リアクションが削除されました')

ピン留めしたり、消したりする処理を入れます。

event_data = body['event']

reaction = event_data['reaction']
event_type = event_data['type']
channel_id = event_data['item']['channel']
timestamp = event_data['item']['ts']

# 👍リアクションが追加された場合
if reaction == '+1' and event_type == 'reaction_added':
    logger.info('👍リアクションが追加されました')
    add_pins(channel_id, timestamp)

# 👍リアクションが削除された場合
if reaction == '+1' and event_type == 'reaction_removed':
    logger.info('👍リアクションが削除されました')
    remove_pins(channel_id, timestamp)
def add_pins(channel_id: str, timestamp: str):
    try:
        _ = client.pins_add(
            channel=channel_id,
            timestamp=timestamp
        )
    except SlackApiError:
        logger.exception("Error processing add_pins")


def remove_pins(channel_id: str, timestamp: str):
    try:
        _ = client.pins_remove(
            channel=channel_id,
            timestamp=timestamp
        )

    except SlackApiError:
        logger.exception("Error processing remove_pins")

これで完成です。
全体像↓

全体像
import json
import os
from slack_sdk import WebClient
from slack_sdk.errors import SlackApiError

# Slack設定
slack_token = os.environ['SLACK_BOT_TOKEN']
client = WebClient(token=slack_token)

def lambda_handler(event, context):

    try:
        body = json.loads(event['body'])

        # slackのチャレンジリクエスト用
        if 'challenge' in body:
            return {
                'statusCode': 200,
                'body': body['challenge']
            }

        if 'event' in body:
            event_data = body['event']
            
            reaction = event_data['reaction']
            event_type = event_data['type']
            channel_id = event_data['item']['channel']
            timestamp = event_data['item']['ts']

            # リアクションが追加された場合
            if reaction == '+1' and event_type == 'reaction_added':
                logger.info('👍リアクションが追加されました')
                add_pins(channel_id, timestamp)

            # リアクションが削除された場合
            if reaction == '+1' and event_type == 'reaction_removed':
                logger.info('👍リアクションが削除されました')
                remove_pins(channel_id, timestamp)
        
    except Exception:
        logger.exception("Error processing event")

def add_pins(channel_id: str, timestamp: str):

    try:
        _ = client.pins_add(
            channel=channel_id,
            timestamp=timestamp
        )
    except SlackApiError:
        logger.exception("Error processing add_pins")


def remove_pins(channel_id: str, timestamp: str):

    try:
        _ = client.pins_remove(
            channel=channel_id,
            timestamp=timestamp
        )

    except SlackApiError:
        logger.exception("Error processing remove_pins")

おまけ

ここまででやりたいことは実現できたのですが、問題が1つ。
ログを見てるうちに、このコードが2回動作している( = APIが2回叩かれている)ことが分かりました。

よく見ると2回実行され、2回目は「Error pinning message: already_pinned」になっている↓

後から動作されても困る(どうせもう一回そのアクションすればいいだけ)ので、リトライが走らないように強制的にHTTPステータスを200にして返すことにした。
よって最終系は以下のようになりました。

最終版
import json
import os
from slack_sdk import WebClient
from slack_sdk.errors import SlackApiError

# Slack設定
slack_token = os.environ['SLACK_BOT_TOKEN']
client = WebClient(token=slack_token)

def lambda_handler(event, context):

    try:
        body = json.loads(event['body'])

        # slackのチャレンジリクエスト用
        if 'challenge' in body:
            return {
                'statusCode': 200,
                'body': body['challenge']
            }

        if 'event' in body:
            event_data = body['event']
            
            reaction = event_data['reaction']
            event_type = event_data['type']
            channel_id = event_data['item']['channel']
            timestamp = event_data['item']['ts']

            # リアクションが追加された場合
            if reaction == '+1' and event_type == 'reaction_added':
                logger.info('👍リアクションが追加されました')
                add_pins(channel_id, timestamp)

            # リアクションが削除された場合
            if reaction == '+1' and event_type == 'reaction_removed':
                logger.info('👍リアクションが削除されました')
                remove_pins(channel_id, timestamp)
        
    except Exception:
        logger.exception("Error processing event")
    
    finally:
        # 常に200 OKを返すことで、Slackによるリトライを防止
        return {
            'statusCode': 200,
            'body': ''
        }


def add_pins(channel_id: str, timestamp: str):

    try:
        _ = client.pins_add(
            channel=channel_id,
            timestamp=timestamp
        )
    except SlackApiError:
        logger.exception("Error processing add_pins")


def remove_pins(channel_id: str, timestamp: str):

    try:
        _ = client.pins_remove(
            channel=channel_id,
            timestamp=timestamp
        )

    except SlackApiError:
        logger.exception("Error processing remove_pins")

おしまい。

アイレット株式会社

Discussion