2021/12/09 の回です。
前日は、@75asaさんによる「Block Kit でゲシュタルト崩壊しないために JSX でブロックを記述する」でした。
ゲシュタルト崩壊(ゲシュタルトほうかい、独: Gestaltzerfall)とは、知覚における現象のひとつ。 全体性を持ったまとまりのある構造(Gestalt, 形態)から全体性が失われてしまい、個々の構成部分にバラバラに切り離して認識し直されてしまう現象をいう。幾何学図形、文字、顔など、視覚的なものがよく知られているが、聴覚や皮膚感覚、味覚、嗅覚においても生じうる。
JSON で冗長な Block Kit に辟易している方はぜひお試しを!とのまとめです
はじめに
ボットをつくります
仕様は以下の通りです。
- チャンネルに新しいユーザが入ってきたら、歓迎メッセージを送る
- チャンネルからユーザが退出したら泣く、悲しむ
- ボット宛にメッセージが飛んできたらオウム返しする
- ボット宛に
"ping"
メッセージが飛んできたら、"pong"
を返す - Elixirでつくる
Bolt for Python
Bolt for Pythonで作るとこんな感じですぐにできあがります。
import os
from slack_bolt import App
from slack_bolt.adapter.socket_mode import SocketModeHandler
# ボットトークンとソケットモードハンドラーを使ってアプリを初期化します
app = App(token=os.environ.get("SLACK_BOT_TOKEN"))
# 'hello' を含むメッセージをリッスンします
# 指定可能なリスナーのメソッド引数の一覧は以下のモジュールドキュメントを参考にしてください:
# https://slack.dev/bolt-python/api-docs/slack_bolt/kwargs_injection/args.html
@app.message("hello")
def message_hello(message, say):
# イベントがトリガーされたチャンネルへ say() でメッセージを送信します
say(f"Hey there <@{message['user']}>!")
@app.event("app_mention")
def handle_app_mention_events(body, logger, say):
logger.info(body)
bot_user_id = body['authorizations'][0]['user_id']
text = body['event']['text']
user = body['event']['user']
text = text.replace(f"<@{bot_user_id}>", "").strip()
if text == "ping":
say(f"<@{user}> pong :robot_face:")
else:
say(f"<@{user}> {text} (to parrot :parrot:)")
@app.event("member_joined_channel")
def handle_member_joined_channel_events(body, logger, say):
logger.info(body)
user = body['event']['user']
msg = f"<@{user}> Welcome! We are the alchemists, my friends!\n(https://github.com/TORIFUKUKaiou/slack_doorman)"
say(msg)
@app.event("member_left_channel")
def handle_member_left_channel_events(body, logger, say):
logger.info(body)
user = body['event']['user']
say(f"<@{user}> left :sob:")
# アプリを起動します
if __name__ == "__main__":
SocketModeHandler(app, os.environ["SLACK_APP_TOKEN"]).start()
詳しくは、「Bolt 入門ガイド」をご参照ください。
まず公式のSDKを使って作ることの自信を得ました。
ガイドの通りにまずはイゴかし1てみることをオススメします。
Tips
import os
from slack_bolt import App
from slack_bolt.adapter.socket_mode import SocketModeHandler
# ボットトークンとソケットモードハンドラーを使ってアプリを初期化します
app = App(token=os.environ.get("SLACK_BOT_TOKEN"))
# アプリを起動します
if __name__ == "__main__":
SocketModeHandler(app, os.environ["SLACK_APP_TOKEN"]).start()
この雛形がとても役にたちます。
たとえば、Slackアプリの設定で、Event Subscriptionsにて、"app_mention"を購読したとします。
そうして
$ python3 app.py
⚡️ Bolt app is running!
と起動しておくわけですよ。
そこからボットを参加させたチャンネルにて、@first-bolt-app hello
なんて話かけるわけです。
雛形にはメンション宛に飛んできたメッセージを処理する関数を書いていないからなんにも反応できないのですけれどもログのほうに
Unhandled request ({'type': 'event_callback', 'event': {'type': 'app_mention'}})
---
[Suggestion] You can handle this type of event with the following listener function:
@app.event("app_mention")
def handle_app_mention_events(body, logger):
logger.info(body)
Unhandled request ({'type': 'event_callback', 'event': {'type': 'app_mention'}})
と、ばっちり[Suggestion]をいただけてこれを組み込めばいいわけですよ。
関数の引数は
https://slack.dev/bolt-python/api-docs/slack_bolt/kwargs_injection/args.html
に書いてあるものが使えます。
すばらしい
Contribution
ちなみにいくつか日本語訳の誤りをみつけたのでプルリクをだしてみたところ、マージしていただけました
ありがとうございます
- https://github.com/slackapi/bolt-python/pull/507
- https://github.com/slackapi/bolt-python/pull/508
- https://github.com/slackapi/bolt-python/pull/519
何がすごかったかって、マージされたとおもったらもう次の瞬間には、日本語訳のページに反映されていました
世界を相手にしているサービスのスピード感に驚きました!
$\huge{Awesome!!!}$
Elixirで書く
ここからはElixirで書きます。
HTTP通信で作ります。
WebSocket(ソケットモード)は、WebSockexを使うことでなんとなくは動いたのですが、再接続あたりがうまくいかず一旦置くことにしました。
これはこれでうまくいったところ、うまくいかなかったところをまとめたいとおもってはいます2。
ここではできたことを景気よく書いておきたいとおもいます
elixir 1.12.3-otp-24
erlang 24.1.4
を使っています。
大まかな仕組み(HTTP)
- Slackアプリの設定で購読したいEventを申し出ておく(設定)
- Slackで該当の操作(bot宛のメッセージや新しいメンバーの入場)が行われる
- Slackから
/slack/events
宛にHTTP POSTがボット(アプリ)に飛んでくる(https) - 飛んできたEventをボット(アプリ)は本当にSlackが送ってきたものかどうかを確かめる
- あとは、ボット(アプリ)のほうで煮るなり焼くなり好きにする
ドキュメント
- https://api.slack.com/apis/connections/events-api
- https://api.slack.com/authentication/verifying-requests-from-slack
- https://api.slack.com/methods/chat.postMessage
公式のSDKなり、非公式のライブラリなりを使わない場合はこのへんのことを理解して実装をする必要があります。
この記事では、PureなPhoenixプロジェクトを新規作成して実装を進めてみました。
完成品
どこでイゴいている1の?
elixir.jp Slack workspaceの#autoracex
チャンネルにaweseome-bot君がいます。
Elixirの純粋なもくもく会です。
Phoenixインストール
上記をご参照ください。
完成品ではPostgreSQLなどのデータベースは使用していません。
プロジェクトのnew
$ mix phx.new slack_doorman --no-ecto
$ cd slack_doorman
HTTPoisonの追加
- {:plug_cowboy, "~> 2.5"}
+ {:plug_cowboy, "~> 2.5"},
+ {:httpoison, "~> 1.8"}
]
$ mix setup
ソースコードを書く
Verifying requests from Slack
リンク先の通りに実装すると、本当にSlackから送られてきたHTTP POSTであることを確かめることができます。
SlackからボットにHTTP Postが送られてくる時に、"v0:{timestamp}:{body}"
をSlackとボットの作成者しか知り得ないSigning Secretで署名して、X-Slack-Signature
にセットして送ってくれます。
ボット側では、"v0:{timestamp}:{body}"
を組み立てSigning Secretでハッシュを計算してちょっとゴニョって、ヘッダのX-Slack-Signature
と一致していればSlackから送られてきたものであると判断するわけです。
以下、Elixirでの実装です。
一点注意点としては、:crypto.mac/4
関数はOTP-24以上です。OTP-23では:crypto.hmac/3
関数を使うと同じように実装できるはずです。
defmodule SlackDoorman.Slack do
require Logger
def validate_request(conn) do
# https://api.slack.com/authentication/verifying-requests-from-slack
Logger.info("validate_request")
timestamp =
conn.req_headers
|> Enum.find(fn {key, _} -> key == "x-slack-request-timestamp" end)
|> elem(1)
|> tap(&Logger.info/1)
|> String.to_integer()
request_body = conn.assigns.raw_body |> Enum.at(0) |> tap(&Logger.info/1)
slack_signature =
conn.req_headers
|> Enum.find(fn {key, _} -> key == "x-slack-signature" end)
|> elem(1)
|> tap(&Logger.info/1)
validate_request(timestamp, request_body, slack_signature)
end
defp validate_request(timestamp, request_body, slack_signature) do
DateTime.diff(DateTime.now!("Etc/UTC"), DateTime.from_unix!(timestamp))
|> validate_request(timestamp, request_body, slack_signature)
end
defp validate_request(diff, _timestamp, _request_body, _slack_signature)
when abs(diff) > 5 * 60 do
false
end
defp validate_request(_diff, timestamp, request_body, slack_signature) do
slack_signing_secret = System.get_env("SLACK_SIGNING_SECRET")
sig_basestring = "v0:" <> Integer.to_string(timestamp) <> ":" <> request_body
my_signature =
:crypto.mac(:hmac, :sha256, slack_signing_secret, sig_basestring)
|> Base.encode16()
|> String.downcase()
my_signature = "v0=" <> my_signature
my_signature == slack_signature
end
end
自分で書いたかと言われると、ググり力がものを言うわけでして、以下のページをとても参考にしました。
raw bodyの取り出し
defmodule SlackDoormanWeb.CacheBodyReader do
def read_body(conn, opts) do
{:ok, body, conn} = Plug.Conn.read_body(conn, opts)
conn = update_in(conn.assigns[:raw_body], &[body | &1 || []])
{:ok, body, conn}
end
end
plug Plug.Parsers,
parsers: [:urlencoded, :multipart, :json],
pass: ["*/*"],
+ body_reader: {SlackDoormanWeb.CacheBodyReader, :read_body, []},
json_decoder: Phoenix.json_library()
plug Plug.MethodOverride
Thanks!
chat.postMessage
defmodule SlackDoorman.Slack.Api do
def post_message(json) do
# https://api.slack.com/methods/chat.postMessage
url = "https://slack.com/api/chat.postMessage"
headers = [
"Content-type": "application/json",
Authorization: "Bearer #{slack_bot_token()}"
]
{:ok, _response} = HTTPoison.post(url, json, headers)
end
defp slack_bot_token do
System.get_env("SLACK_BOT_TOKEN")
end
end
Using the Slack Events API
SlackからHTTP Postで投げ込まれるEventを処理します。
ボットの振る舞いは、SlackDoorman.Handler.handle_event/1
を別プロセスで動作させることにしました。
defmodule SlackDoormanWeb.EventController do
use SlackDoormanWeb, :controller
require Logger
def create(
conn,
%{"challenge" => challenge, "token" => _token, "type" => "url_verification"} = params
) do
Logger.info(conn)
Logger.info(params)
if SlackDoorman.Slack.validate_request(conn) do
conn
|> put_status(:ok)
|> render("challenge.json", challenge: challenge)
end
end
def create(conn, params) do
Logger.info(conn)
Logger.info(params)
if SlackDoorman.Slack.validate_request(conn), do: do_something(params)
ok(conn)
end
defp ok(conn) do
conn
|> put_status(:ok)
|> render("ok.json", ok: :ok)
end
defp do_something(params) do
Logger.info("do_something")
spawn(fn -> SlackDoorman.Handler.handle_event(params) end)
end
end
defmodule SlackDoormanWeb.EventView do
use SlackDoormanWeb, :view
def render("challenge.json", %{challenge: challenge}) do
%{challenge: challenge}
end
def render("ok.json", %{ok: :ok}) do
%{}
end
end
# Other scopes may use custom stacks.
- # scope "/api", SlackDoormanWeb do
- # pipe_through :api
- # end
+ scope "/slack/events", SlackDoormanWeb do
+ pipe_through :api
+
+ post "/", EventController, :create
+ end
SlackDoorman.Handler
パターンマッチング大活躍です。
ボットの振る舞いを書いています。
defmodule SlackDoorman.Handler do
require Logger
# https://api.slack.com/events/member_joined_channel
def handle_event(
%{
"event" => %{"channel" => channel, "type" => "member_joined_channel", "user" => user},
"type" => "event_callback"
} = params
) do
Logger.info("member_joined_channel")
Logger.info(params)
say(
channel,
"<@#{user}> Welcome! We are the alchemists, my friends!\n(https://github.com/TORIFUKUKaiou/slack_doorman)"
)
end
# https://api.slack.com/events/member_left_channel
def handle_event(
%{
"event" => %{"channel" => channel, "type" => "member_left_channel", "user" => user},
"type" => "event_callback"
} = params
) do
Logger.info("member_left_channel")
Logger.info(params)
say(
channel,
"<@#{user}> left :sob:"
)
end
# https://api.slack.com/events/app_mention
def handle_event(
%{
"event" => %{
"channel" => channel,
"type" => "app_mention",
"user" => user,
"text" => text
},
"authorizations" => [%{"user_id" => bot_user_id}],
"type" => "event_callback"
} = params
) do
Logger.info("app_mention")
Logger.info(params)
text =
String.replace(text, "<@#{bot_user_id}>", "")
|> String.trim()
|> reply()
say(
channel,
"<@#{user}> #{text}"
)
end
def handle_event(params) do
Logger.info("no handle")
Logger.info(params)
IO.inspect(params)
end
defp reply("ping"), do: "pong :robot_face:"
defp reply(text), do: "#{text} (to parrot :parrot:)"
defp say(channel, text) do
%{
channel: channel,
text: text,
link_names: true,
username: "awesome-bot",
icon_url: "https://ca.slack-edge.com/TL799TXED-UL27SRN3V-ffb245030052-512"
}
|> Jason.encode!()
|> SlackDoorman.Slack.Api.post_message()
end
end
Slackアプリの設定
Event Subscriptions
OAuth & Permissions
環境変数
- Basic Information ページの署名シークレットをコピー => SLACK_SIGNING_SECRET
- OAuth & Permissions ページのボットトークン (xoxb) をコピー => SLACK_BOT_TOKEN
Run
$ mix phx.server
https
httpsじゃないといかんとですよ。
ローカルマシンでイゴかす1場合には、
を使うと便利です。
ngrokは、Bolt 入門ガイド(HTTP)で知りました。
$ ./ngrok authtoken <token>
$ ./ngrok http 4000
ngrok by @inconshreveable (Ctrl+C to quit)
Session Status online
Account TORIFUKUKaiou (Plan: Free)
Version 2.3.40
Region United States (us)
Web Interface http://127.0.0.1:4040
Forwarding https://95fa-163-49-206-28.ngrok.io -> http://localhost:4000
Forwarding https://95fa-163-49-206-28.ngrok.io -> http://localhost:4000
こげな感じで、たとえば上の実行例ですとhttps://95fa-163-49-206-28.ngrok.io
へのアクセスが、ローカルマシンの4000番(Phoenix)とつながります。
これをSlackアプリのほうに設定するとよかです。
Wrapping up
-
Elixirを使って、Bolt 入門ガイド(HTTP) 相当のことを楽しめました
-
elixir.jp Slack workspaceの
#autoracex
チャンネルにaweseome-bot君がいます
-
elixir.jp Slack workspaceの
- ちなみにローカルマシンで動かし続けるというのはいろいろつらいのでそうするとサーバのようなものが必要になります
- それには、さくらインターネットさんのHacobune(はこぶね)を使っています
- この話についてはまた記事をわけて別のカレンダーで書いてみたいとおもっています2
- うまく設計すると、Bolt for Elixirが誕生するかも
- Enjoy Elixir
以下、Elixirのお役立ち情報です
オススメの書籍
- プログラミングElixir(第2版) -- オーム社
- Elixir実践ガイド -- インプレス
Webアプリケーションを楽しむなら
IoTを楽しむなら
AIを楽しむなら
コミュニティ
-
elixir.jp Slack workspaceに参加してみてください
- マヂ、やさしい人ばっかりのコミュニティ
- あなたの困ったをきっと解決してくれるでしょう
- NervesJP Slack workspaceでは、NervesやIoTが好きな愉快なfolksたちがあなたの訪れを歓迎します
- たくさんのコミュニティがあります
- @kn339264 さん作の素敵な資料をご紹介します
- Elixirコミュニティ の歩き方〜国内オンライン編〜
@piacerex さん作
-
動かすの意。たぶん西日本の方言、おそらく。NervesJPではおなじみの表現。少し古いオートレースの映像ですが、実況アナウンサーが「針3イゴきます」とはっきり言っています。https://autorace.jp/netstadium/SearchMovie/Movie/iizuka?date=2017-01-04&p=5&race_number=11&pg= ↩
-
おもっています。あくまでもおもっています。 ↩
-
大時計の針のこと。針がイゴいてある地点まで到達すると選手はスタートを切って良い発走の合図。針がイゴきはじめると(おそらく)選手は緊張するし、スタートはその後のレース展開に大きく影響するので、車券を握りしめている観客たちがもっとも緊張する瞬間であるため、先の尖った鋭いものを連想させる針は緊張の暗喩としても言い得て妙。 ↩