大ちゃんの駆け出し技術ブログ

RUNTEQ受講生のかわいいといわれるアウトプットブログ

【Slack API】Block Kitで投稿する方法

はじめに

こんにちは!大ちゃんの駆け出し技術ブログです。

slackのBlock Kitを知っていますか?

Block Kit

Block KitはJSON形式でメッセージの装飾を表したものになります。

slackのメッセージでは他のチャットツールよりもかなり複雑なメッセージを送ることができます。

https://i.gyazo.com/2103ba7cc9c94e7371741e4d0576b1c6.png

これをJSONで表すと、、、

{
    "blocks": [
        {
            "type": "header",
            "text": {
                "type": "plain_text",
                "text": "New request",
                "emoji": true
            }
        },
        {
            "type": "section",
            "fields": [
                {
                    "type": "mrkdwn",
                    "text": "*Type:*\nPaid Time Off"
                },
                {
                    "type": "mrkdwn",
                    "text": "*Created by:*\n<example.com|Fred Enriquez>"
                }
            ]
        },
        {
            "type": "section",
            "fields": [
                {
                    "type": "mrkdwn",
                    "text": "*When:*\nAug 10 - Aug 13"
                },
                {
                    "type": "mrkdwn",
                    "text": "*Type:*\nPaid time off"
                }
            ]
        },
        {
            "type": "section",
            "fields": [
                {
                    "type": "mrkdwn",
                    "text": "*Hours:*\n16.0 (2 days)"
                },
                {
                    "type": "mrkdwn",
                    "text": "*Remaining balance:*\n32.0 hours (4 days)"
                }
            ]
        },
        {
            "type": "section",
            "text": {
                "type": "mrkdwn",
                "text": "<https://example.com|View request>"
            }
        }
    ]
}

このようにかなり複雑な見た目となっています。しかし、自分で装飾するのはとても簡単です!

slackが専用で直感的に作れるようにBlock Kit Builderというものが用意されています。

https://i.gyazo.com/ceecefc671b7453802a8e2870d200142.png

Sign in | Slack

これを使えば、めちゃくちゃ簡単に装飾できます。しかし、実はこれを自分のslackチャンネルにRailsアプリを使用して投稿するのに少し苦労したのでここに記録しておきます。

読者対象

  • omniauthを使用してsign in with slackが実装できているアプリをお持ちの方

以下のgemを使用すればできます。

ginjo/omniauth-slack

star数すごく少ないですが、omniauth-slackで1番使用されています。ちなみにTwitterのomniauthのstar数は569でした。悲しい、、、

※ omniauth-slackを使用した認証方法は結構時間かかると思いますので、別記事にて今度紹介したいと思います!

実装方法

①アクセストークンの取得

ではまず認証が通ってリダイレクトされる時に呼び出されるslackメソッドを空にした状態から始めます。

module Users
  class OmniauthCallbacksController < Devise::OmniauthCallbacksController
    skip_before_action :authenticate_user!, only: %i[slack failure]

    def slack

    end

まず、slack認証が通るとrequest.env['omniauth.strategy']というものが返ってきます。これを使用してslack用のアクセストークンを作ります。request.env['omniauth.strategy'].access_tokenbot_tokenというものを取得できます。

bot_token = request.env['omniauth.strategy'].access_token

bot_tokenは簡単に説明すると、slackアプリのBot専用のアクセストークンになります。ユーザー専用のトークンはbot_token.user_tokenで取り出すことができます。(bot_token, user_tokenの説明に関してはomniauth-slackを理解していることが前提ですので大幅に省きます。詳しくは今後の記事で。)

user_token = bot_token.user_token

その後、slackアプリを作る上で定番となる「SLACK CLIENT ID」、「SLACK CLIENT SECRET」を使用してBotのアクセストークンを生成することができます。第三引数にはbot_tokenをハッシュ化したものを指定します。

access_token = OmniAuth::Slack.build_access_token(ENV['SLACK_CLIENT_ID'], ENV['SLACK_CLIENT_SECRET'], bot_token.to_hash

使用しているメソッド(OmniAuth::Slack.build_access_token)については下記リンクを参照ください。

ginjo/omniauth-slack

access_tokenを使用することでslackのAPIを叩くことができます。ちなみに、機能するかどうかを確認する方法があります。access_token.get("/api/auth.test")を使用し、以下のようなレスポンスが返ってこれば正常に動作しています。

{
    "ok": true,
    "url": "https://subarachnoid.slack.com/",
    "team": "Subarachnoid Workspace",
    "user": "grace",
    "team_id": "T12345678",
    "user_id": "W12345678"
}

auth.testメソッドについてはこちら

auth.test Slack API Method

これまでのコード

module Users
  class OmniauthCallbacksController < Devise::OmniauthCallbacksController
    skip_before_action :authenticate_user!, only: %i[slack failure]

    def slack
      bot_token = request.env['omniauth.strategy'].access_token
            user_token = bot_token.user_token
      access_token = OmniAuth::Slack.build_access_token(ENV['SLACK_CLIENT_ID'], ENV['SLACK_CLIENT_SECRET'], bot_token.to_hash
    end

②ユーザ情報の取得

現状user_tokenを取得できているのでscopeを設定できていれば情報にアクセスすることができます。といっても今回はユーザーのID情報を取得できればいいのでidentity.basicのscopeだけあれば問題ないです。

https://i.gyazo.com/71eb89bfcbd81727c22ca85b5a49383d.png

ちなみにuser_token.scopesを使用することでscopeの中身を確認できます。identity.basicがあればokです!

user_token.scopes
=> {"classic"=>["identity.basic", "identity.email", "identity.avatar", "identity.team"]}

scopeを確認したらusers.identityメソッドを使用してユーザ情報を取得します。

user_info = user_token.get('/api/users.identity').parsed

返却値の例としては下記のような見た目です。

{
    "ok": true,
    "user": {
        "name": "Sonny Whether",
        "id": "U0G9QF9C6"
    },
    "team": {
        "id": "T0G9PQBBK"
    }
}

user_info.dig("user", "id")でidを取得することができます。

user_id = user_info.dig("user", "id")

users.identityメソッドについてはこちら

users.identity Slack API Method

これまでのコード

module Users
  class OmniauthCallbacksController < Devise::OmniauthCallbacksController
    skip_before_action :authenticate_user!, only: %i[slack failure]

    def slack
      bot_token = request.env['omniauth.strategy'].access_token
            user_token = bot_token.user_token
      access_token = OmniAuth::Slack.build_access_token(ENV['SLACK_CLIENT_ID'], ENV['SLACK_CLIENT_SECRET'], bot_token.to_hash
            user_info = user_token.get('/api/users.identity').parsed
            user_id = user_info.dig("user", "id")
    end

③ブロックメッセージの準備

それでは実際にBolt Kitのメッセージを変数に代入しておきます。代入するメッセージは最初にお見せしたJSONをそのまま使用してみます。

[再掲]

{
    "blocks": [
        {
            "type": "header",
            "text": {
                "type": "plain_text",
                "text": "New request",
                "emoji": true
            }
        },
        {
            "type": "section",
            "fields": [
                {
                    "type": "mrkdwn",
                    "text": "*Type:*\nPaid Time Off"
                },
                {
                    "type": "mrkdwn",
                    "text": "*Created by:*\n<example.com|Fred Enriquez>"
                }
            ]
        },
        {
            "type": "section",
            "fields": [
                {
                    "type": "mrkdwn",
                    "text": "*When:*\nAug 10 - Aug 13"
                },
                {
                    "type": "mrkdwn",
                    "text": "*Type:*\nPaid time off"
                }
            ]
        },
        {
            "type": "section",
            "fields": [
                {
                    "type": "mrkdwn",
                    "text": "*Hours:*\n16.0 (2 days)"
                },
                {
                    "type": "mrkdwn",
                    "text": "*Remaining balance:*\n32.0 hours (4 days)"
                }
            ]
        },
        {
            "type": "section",
            "text": {
                "type": "mrkdwn",
                "text": "<https://example.com|View request>"
            }
        }
    ]
}

しかし、ここで問題点がありまして、それがBolt KitのJSONが長すぎ問題です。これをコントローラに置いておいたらとんでもなくFat controllerになってしまいます。そのためBolt Kit用のmoduleを作成し、そこにメソッドとしてBolt KitのJSONを返すメソッドを定義しておきます。

SlackApiBlockKitをモジュールとして作成し、それを認証コントローラでincludeします。メッセージを返すメソッドはget_block_kit_msgとしましょう。

module SlackApiBlockKit

    def get_block_kit_msg
      message = '[        {           "type": "header",           "text": {                 "type": "plain_text",               "text": "New request",              "emoji": true             }       },      {           "type": "section",          "fields": [               {                   "type": "mrkdwn",                   "text": "*Type:*\nPaid Time Off"                },              {                   "type": "mrkdwn",                   "text": "*Created by:*\n<example.com|Fred Enriquez>"              }           ]       },      {           "type": "section",          "fields": [               {                   "type": "mrkdwn",                   "text": "*When:*\nAug 10 - Aug 13"              },              {                   "type": "mrkdwn",                   "text": "*Type:*\nPaid time off"                }           ]       },      {           "type": "section",          "fields": [               {                   "type": "mrkdwn",                   "text": "*Hours:*\n16.0 (2 days)"               },              {                   "type": "mrkdwn",                   "text": "*Remaining balance:*\n32.0 hours (4 days)"                 }           ]       },      {           "type": "section",          "text": {                 "type": "mrkdwn",               "text": "<https://example.com|View request>"          }       }   ]'
      return ERB::Util.url_encode(message)
    end
end
module Users
  class OmniauthCallbacksController < Devise::OmniauthCallbacksController
    skip_before_action :authenticate_user!, only: %i[slack failure]
    include SlackApiBlockKit

それではメソッドの中にメッセージを仕込みます。注意点としてはJSONの"blocks"の中の配列を使用してください。また、その配列を文字列として認識するために、``で囲いましょう。

message = '[         {           "type": "header",           "text": {                 "type": "plain_text",               "text": "New request",              "emoji": true             }       },      {           "type": "section",          "fields": [               {                   "type": "mrkdwn",                   "text": "*Type:*\nPaid Time Off"                },              {                   "type": "mrkdwn",                   "text": "*Created by:*\n<example.com|Fred Enriquez>"              }           ]       },      {           "type": "section",          "fields": [               {                   "type": "mrkdwn",                   "text": "*When:*\nAug 10 - Aug 13"              },              {                   "type": "mrkdwn",                   "text": "*Type:*\nPaid time off"                }           ]       },      {           "type": "section",          "fields": [               {                   "type": "mrkdwn",                   "text": "*Hours:*\n16.0 (2 days)"               },              {                   "type": "mrkdwn",                   "text": "*Remaining balance:*\n32.0 hours (4 days)"                 }           ]       },      {           "type": "section",          "text": {                 "type": "mrkdwn",               "text": "<https://example.com|View request>"          }       }   ]'

APIを叩くためにencodeする必要があるため、このメッセージをencodeします。slackのencode方法に対応したERB::Util.url_encodeメソッドを選定しました。他のencode方法も試しましたがうまく動かなかったため、Block Kitをencodeする際はERB::Util.url_encodeを使用しましょう。

return ERB::Util.url_encode(message)

よってメッセージを取得するメソッドは以下のようになります。

def get_block_kit_msg
  message = '[        {           "type": "header",           "text": {                 "type": "plain_text",               "text": "New request",              "emoji": true             }       },      {           "type": "section",          "fields": [               {                   "type": "mrkdwn",                   "text": "*Type:*\nPaid Time Off"                },              {                   "type": "mrkdwn",                   "text": "*Created by:*\n<example.com|Fred Enriquez>"              }           ]       },      {           "type": "section",          "fields": [               {                   "type": "mrkdwn",                   "text": "*When:*\nAug 10 - Aug 13"              },              {                   "type": "mrkdwn",                   "text": "*Type:*\nPaid time off"                }           ]       },      {           "type": "section",          "fields": [               {                   "type": "mrkdwn",                   "text": "*Hours:*\n16.0 (2 days)"               },              {                   "type": "mrkdwn",                   "text": "*Remaining balance:*\n32.0 hours (4 days)"                 }           ]       },      {           "type": "section",          "text": {                 "type": "mrkdwn",               "text": "<https://example.com|View request>"          }       }   ]'
  return ERB::Util.url_encode(message)
end

これをコントローラ側で取り出します。

encoded_mgs = get_block_kit_msg

これまでのコード

module Users
  class OmniauthCallbacksController < Devise::OmniauthCallbacksController
    skip_before_action :authenticate_user!, only: %i[slack failure]
        include SlackApiBlockKit

    def slack
      bot_token = request.env['omniauth.strategy'].access_token
            user_token = bot_token.user_token
      access_token = OmniAuth::Slack.build_access_token(ENV['SLACK_CLIENT_ID'], ENV['SLACK_CLIENT_SECRET'], bot_token.to_hash
            user_info = user_token.get('/api/users.identity').parsed
            user_id = user_info.dig("user", "id")
            encoded_mgs = get_block_kit_msg
    end
module SlackApiBlockKit
    def get_block_kit_msg
      message = '[        {           "type": "header",           "text": {                 "type": "plain_text",               "text": "New request",              "emoji": true             }       },      {           "type": "section",          "fields": [               {                   "type": "mrkdwn",                   "text": "*Type:*\nPaid Time Off"                },              {                   "type": "mrkdwn",                   "text": "*Created by:*\n<example.com|Fred Enriquez>"              }           ]       },      {           "type": "section",          "fields": [               {                   "type": "mrkdwn",                   "text": "*When:*\nAug 10 - Aug 13"              },              {                   "type": "mrkdwn",                   "text": "*Type:*\nPaid time off"                }           ]       },      {           "type": "section",          "fields": [               {                   "type": "mrkdwn",                   "text": "*Hours:*\n16.0 (2 days)"               },              {                   "type": "mrkdwn",                   "text": "*Remaining balance:*\n32.0 hours (4 days)"                 }           ]       },      {           "type": "section",          "text": {                 "type": "mrkdwn",               "text": "<https://example.com|View request>"          }       }   ]'
      return ERB::Util.url_encode(message)
    end
end

④メッセージを投稿

最後にメッセージを投稿するメソッドとして、chat.postMessageメソッドを使用します。本来であればチャンネルに投稿するためのメソッドですが、userのIDを使用することでDMを送ることができます。

以下公式の内容です。

If you simply want your app's bot user to start a 1:1 conversation with another user in a workspace, provide the user's user ID as the channel value and a direct message conversation will be opened, if it hasn't already. Resultant messages and associated direct message objects will have a direct message ID you can use from that point forward, if you'd rather.

ここではaccess_tokenを使用するため、access_tokenにchat:writeのscopeがあることも確認してください。user_tokenと同様にaccess_token.scopesで確認できます。

access_token.scopes
=> {"classic"=>["chat:write"]}

それでは実際にメソッドを使用しAPI を叩きます。

access_token.post("api/chat.postMessage?channel=#{user_id}&blocks=#{encoded_mgs}").parsed

するとDMが実際に飛んできたかと思います。

https://i.gyazo.com/d93b9840d06217f36970b114064f91f2.png

chat.postMessageメソッドについてはこちら

chat.postMessage Slack API Method

最終コード

module Users
  class OmniauthCallbacksController < Devise::OmniauthCallbacksController
    skip_before_action :authenticate_user!, only: %i[slack failure]
        include SlackApiBlockKit

    def slack
      bot_token = request.env['omniauth.strategy'].access_token
            user_token = bot_token.user_token
      access_token = OmniAuth::Slack.build_access_token(ENV['SLACK_CLIENT_ID'], ENV['SLACK_CLIENT_SECRET'], bot_token.to_hash
            user_info = user_token.get('/api/users.identity').parsed
            user_id = user_info.dig("user", "id")
            encoded_mgs = get_block_kit_msg
            access_token.post("api/chat.postMessage?channel=#{user_id}&blocks=#{encoded_mgs}").parsed
    end
module SlackApiBlockKit
    def get_block_kit_msg
      message = '[        {           "type": "header",           "text": {                 "type": "plain_text",               "text": "New request",              "emoji": true             }       },      {           "type": "section",          "fields": [               {                   "type": "mrkdwn",                   "text": "*Type:*\nPaid Time Off"                },              {                   "type": "mrkdwn",                   "text": "*Created by:*\n<example.com|Fred Enriquez>"              }           ]       },      {           "type": "section",          "fields": [               {                   "type": "mrkdwn",                   "text": "*When:*\nAug 10 - Aug 13"              },              {                   "type": "mrkdwn",                   "text": "*Type:*\nPaid time off"                }           ]       },      {           "type": "section",          "fields": [               {                   "type": "mrkdwn",                   "text": "*Hours:*\n16.0 (2 days)"               },              {                   "type": "mrkdwn",                   "text": "*Remaining balance:*\n32.0 hours (4 days)"                 }           ]       },      {           "type": "section",          "text": {                 "type": "mrkdwn",               "text": "<https://example.com|View request>"          }       }   ]'
      return ERB::Util.url_encode(message)
    end
end

注意点としては、Block Kitのメッセージは必ずERB::Util.url_encodeメソッドを使用してください。ここでだいぶ時間がかかりました。一応公式にもSlackが使用しているencode方法が載っているのですが、実際にRubyでBlock Kitをアクセストークンから飛ばしている記事はなかったので注意が必要です。