Slackでサクッとアプリを作る話

はじめまして、TechAcademy開発チームのmです。最近、Slack上で動く簡単なアプリを作ったのでそのお話をします。

Slackについて

KiRAMEXではコミュニケーションツールとして、Slackを活用しています。

  • エンジニア <=> 非エンジニア
  • 社員 <=> 業務委託

などなど、全てのやりとりがSlack上でシームレスに行われております。 他のチャットツールは用いておらず、業務に関することは全てSlackに集約されるのが良いですね。 また、TechAcademyのサービス内でもチャットサポート等にSlackが活用されております。

さて、Slackの良いところは、個人的には、下記3点挙げられるかと思っているのですが

  • UI/操作性が良い
  • 検索機能が充実している
    • 「あの時あんなこと言ってた気がする...」という曖昧な記憶でも辿り着ける
  • カスタマイズ性が高い
    • 外部ツールとの連携、自作アプリなど、アイデアがあれば自由自在

その中でも、カスタマイズ性が高いというのは特に嬉しいポイント。APIが豊富、開発ツールも豊富で、エンジニアにとっては遊びがいあるツールですね。

実際にアプリを作ってみた

完成したもの

今回は以下のような、毎日業務終了時に送信している日報を簡単に書けるアプリを作りました。

ショートカットをクリック
前日分日報を取得し、フォームに入っている状態でモーダルが開く
日報用チャンネルに投稿する

バックエンド

APIGateway + Lambda
APIリクエストをフックにLambdaを発動させます。この組み合わせは素早く作れるので個人的に好きです。 APIGatewayとLambdaそれぞれの設定に関しては今回は割愛します。

ライブラリ

公式に提供されているライブラリがたくさんあります。

api.slack.com

今回は python-slack-sdk を使いました。*1 *2

github.com

メッセージの見た目

Block Kit BuilderでSlackに送信するメッセージのUIを決めます。 自分でjsonを組み立てずとも、ブロックを動かすだけでメッセージがいい感じにできます。 ここで作ったjsonは後ほど使います。

Block Kit Builder

実装

以下コードは一部改変&省略しています。

Slackからのリクエスト受け取り(handler部分)

import json
import base64
from slack_sdk import WebClient
from slack_sdk.errors import SlackApiError
from urllib.parse import parse_qs

def lambda_handler(event, context):
    body = base64.b64decode(event['body'])
    param = parse_qs(body.decode('utf-8'))

    payload = json.loads(param['payload'][0])
    user_id = payload['user']['id']

    if payload['type'] == 'shortcut':
        values = search_messages(user_id)
        open_modal(payload['trigger_id'])
    elif payload['type'] == 'view_submission':
        post_report(payload['view']['state']['values'])

    return {
        'statusCode': 200
    }

Slackでショートカットをクリックした時に最初に受け取る部分です。 event['body'] のなかにSlackから送られてくるpayloadが入っています。*3

payloadの中身はこんな感じ。

{
    "type": "shortcut",
    "token": "XXXXXXX",
    "action_ts": "1620296309.086441",
    "team": {
        "id": "XXXXXXX",
        "domain": "XXXXXXX"
    },
    "user": {
        "id": "XXXXXXX",
        "username": "XXXXXXX",
        "team_id": "XXXXXXX"
    },
    "is_enterprise_install": false,
    "enterprise": null,
    "callback_id": "daily-report",
    "trigger_id": "XXXXXXX"
}

以下APIを利用するには、ここに入っている trigger_id が必要です。

メッセージを検索して前日分を取得する

def search_messages(user_id):
    client = WebClient(token=os.environ['SLACK_USER_TOKEN'])
    query = f'日報 {user_id} in:#channel'
    response = client.search_messages(query=query, sort='timestamp')

APIsearch.messages メソッドを使って、前日分のメッセージを取得します。

api.slack.com

ここのクエリは、Slack上で command + F した時と同じ検索方法が使えるのでとても便利です。 返ってくるpayloadの中身はこんな感じ。

{
    "ok": "True",
    "query": "の日報 {user_id} in:#channel",
    "messages": {
        "total": 18,
        "pagination": {
            "total_count": 18,
            "page": 1,
            "per_page": 20,
            "page_count": 1,
            "first": 1,
            "last": 18
        },
        "paging": {
            "count": 20,
            "total": 18,
            "page": 1,
            "pages": 1
        },
        "matches": [
            〜〜取得したメッセージの中身〜〜
        ]
    }
}

モーダルを開く

# trigger_idはSlack側からのリクエストで送られてきます
def open_modal(trigger_id):
    client = WebClient(token=os.environ['SLACK_BOT_TOKEN'])
    data = { "Block Kit Builderで作ったjson" }
    client.views_open(view=data, trigger_id=trigger_id)

APIviews.open メソッドを使って、日報を入力するモーダルを開きます。

api.slack.com

ユーザ情報を取得する

def get_user_info(user_id):
    client = WebClient(token=os.environ['SLACK_BOT_TOKEN'])
    response = client.users_info(user=user_id)
    return response['user']

APIusers.info メソッドを使って、日報送信したユーザ情報を取得します。

api.slack.com

返ってくるpayloadの中身はこんな感じ。

{
    "ok": "True",
    "user": {
        "id": "XXXXXX",
        "team_id": "XXXXXX",
        "name": "XXXXXX",
        "deleted": "False",
        "color": "bc3663",
        "real_name": "XXXXXX",
        "tz": "Asia/Tokyo",
        "tz_label": "Japan Standard Time",
        "tz_offset": 32400,
        "profile": {
            "title": "",
            "phone": "",
            "skype": "",
            "real_name": "XXXXXX",
            "real_name_normalized": "XXXXXX",
            "display_name": "",
            "display_name_normalized": "",
            "fields": "",
            "status_text": "",
            "status_emoji": "",
            "status_expiration": 0,
            "avatar_hash": "XXXXXX",
            "image_original": "https://XXXXXX",
            "is_custom_image": "True",
            "first_name": "XXXXXX",
            "last_name": "XXXXXX",
            "image_24": "https://XXXXXX",
            "image_32": "https://XXXXXX",
            "image_48": "https://XXXXXX",
            "image_72": "https://XXXXXX",
            "image_192": "https://XXXXXX",
            "image_512": "https://XXXXXX",
            "image_1024": "https://XXXXXX",
            "status_text_canonical": "",
            "team": "XXXXXX"
        },
        "is_admin": "False",
        "is_owner": "False",
        "is_primary_owner": "False",
        "is_restricted": "False",
        "is_ultra_restricted": "False",
        "is_bot": "False",
        "is_app_user": "False",
        "updated": 1620288636,
        "is_email_confirmed": "True"
    }
}

フォーム内容を投稿する

def post_report(data):
    ## 中略
    blocks = { "Block Kit Builderで作ったjson" }
    client = WebClient(token=os.environ['SLACK_BOT_TOKEN'])
    response = client.chat_postMessage(
        channel="#daily-report",
        blocks=blocks,
        icon_url="投稿者のアイコンurl",
        username="username"
    )

APIchat.postMessage メソッドを使って、日報を送信します。

api.slack.com

ここでは、投稿者が誰かわかるように、user_nameicon_url を指定しました。

Slack側での設定

アプリの作成

api.slack.com

使用するAPIに必要なScopeをつける

Scopeを設定している例
APIに必要なScopeはドキュメントに書いてあります

ちなみに各APIに必要なScopeは以下の通りでした。

APIメソッド名 TOKENタイプ 必要なScope
search.messages user search:read
views.open bot -
users.info bot users:read
chat.postMessage bot chat:write chat:write.customize

Slackからリクエストを送るエンドポイントを設定する

エンドポイント設定箇所

アプリをインストール *4

インストールボタンがあります

おわりに

今回は、フォームの入力値をオートフィルしたかった&メッセージの見た目をいい感じにしたかったためアプリを作成しましたが、そこまでの機能が必要でなければ、ワークフロー *5を用いても作成することができます。Slackの活用方法は、無限大ですね。

それでは皆様もよきSlackライフを!

*1:Python以外にもNode.jsやJavaのものがあるのでお好みで使い分けてください

*2:BoltというRTMサーバも含めたライブラリもありますが、今回はサーバレスにしたかったので使用しませんでした。

*3:Reference: Shortcuts payloads | Slack

*4:ちなみにScopeを変更するたびに再インストールが必要です

*5:ワークフローは有料プランのみ作成可能です