SaaS

Slackの各ゲストへ一発で一斉メッセージを送信する

はじめに

どーもばるすです。
ごぶさたしてますね。お元気ですか?
僕は元気にやっています。

突然ですが

Slackのゲストにまとめて連絡したいとき、どうしてます?
シングルチャンネルゲストだけにとか、マルチチャンネルゲストだけにとか、ゲスト全員とか、まとめて連絡したい時ってあると思うんです。

各ゲストが参加しているチャンネルでそれぞれ連絡する?
ゲストを検索してリストにしてDM送る?
SlackのAPIでユーザーの一覧出してメールで連絡?

これ、けっこう面倒だと思うんすよ。

定型文を打ったら一発で連絡できる何かそういうのあったら便利だと思いません?

これを解決する機能を作成してみたのでご紹介します。
もちろんWorkatoです。

GASやAWSLambdaでも実装できますが、Workatoが簡単すぎるので今回は省略しました。
気になる方はお問い合わせください。

動作イメージ

こんな感じで動くやつです。
もちろん、コマンドを発行できるチャンネルやユーザを限定できます。

使い方

  1. ユーザーがスラッシュコマンドで起動
  2. モーダルウィンドウが立ち上がるので入力する
  3. 対象ゲストにDMが飛ぶ
  4. 完了

各処理のロジックについて

ざっくり書くとこうです。

  1. スラッシュコマンドでSlackBot呼び出し
  2. SlackBot→Workatoへリクエスト送信
  3. InteractiveMessageを用いてモーダルウィンドウを表示
  4. ユーザーが入力→送信
  5. Workatoレシピ起動
  6. 入力された値をCallableレシピへ引数として渡して起動
  7. Slackユーザーリストを取得
  8. 通常メンバー,マルチチャンネルゲスト,シングルチャンネルゲストのリストを作成
  9. モーダルで入力された値で分岐し、対象ユーザーへDMを送信
  10. 完了

Workatoのレシピ構造は

管理しやすさと使い回しの可能性を考慮してレシピを2つに分けていますが、1つでもできます。

レシピ1:トリガー&ハンドラーの役割

スラッシュコマンドで起動→入力用モーダルを表示
入力を受け付けたのち、レシピ2を起動して値を渡す

レシピ2:ヘルパーの役割

  1. ユーザーリストを取得する
  2. メンバー/シングルチャンネルゲスト/マルチチャンネルゲスト別のリストを作成
  3. 入力された値で対象にDMを送る

レシピの作り方

1. (Slack)SlackBotを追加する

  • Interactivity & Shortcutsを有効化
    • RequestURL:https://www.workato.com/slack_webhooks/actions?coak_id=<CustomAuthProfileのid>
    • Options Load URL:https://www.workato.com/slack_webhooks/data_source?coak_id=<CustomAuthProfileのid>
  • スラッシュコマンドを有効化(後行程で対応する)
    • URLはWorkatoレシピのトリガーに表示されるURLを利用
  • OAuth & Permissionsを追加
    • im:write
    • users:read
    • commands
    • users:read.email

2. 起動用レシピを作成

  • トリガーを「Workbot for Slack」で設定
  • 起動Wordを設定
  • Slackから受信するリクエストのJsonを追加する

※設定するJson

[
	{
		"control_type": "text",
		"label": "Parameters string",
		"name": "parameters_string",
		"type": "string",
		"optional": true
	},
	{
		"control_type": "text",
		"label": "Command part",
		"name": "command_part",
		"type": "string",
		"optional": true
	},
	{
		"control_type": "text",
		"label": "Type",
		"name": "type",
		"type": "string",
		"optional": true
	},
	{
		"control_type": "text",
		"label": "Bot command ID",
		"parse_output": "float_conversion",
		"name": "bot_command_id",
		"type": "number",
		"optional": true
	},
	{
		"name": "Context",
		"type": "array",
		"of": "object",
		"label": "Context",
		"optional": true,
		"properties": []
	},
	{
		"control_type": "text",
		"label": "Usertype",
		"name": "user_type",
		"type": "string",
		"optional": true
	},
	{
		"control_type": "text",
		"label": "Notifybody",
		"name": "notify_body",
		"type": "string",
		"optional": true
	},
	{
		"control_type": "text",
		"label": "Text",
		"name": "text",
		"type": "string",
		"optional": true,
		"prompt": "false"
	}
]

※ user_type,notify_bodyは後述の行程で設定します。

  • スラッシュコマンドを設定

3. モーダルウィンドウを表示するアクションを設定

  • アクション追加 → Workbot for Slackを選択
  • Open/update or push を選択
  • Modal action type,Trigger IDを設定
  • Viewを設定してモーダルウィンドウの入力欄等をカスタムする

※ ここでuser_type,notify_bodyを設定しています。

  • Modal submissionを設定して、モーダルの入力値を送信する際の挙動を設定する

※ Enter custom valueに当該レシピの起動Wordを設定すると再帰的に設定ができる

4. モーダルの入力値受信後の処理を設定

このままでは無限にモーダルが出てくるので、モーダル送信前/後で処理を分岐させる

  • IF分岐を追加して条件を設定
  • IF分岐のYes配下に以下を配置
    • 先ほど作成したモーダルウィンドウ表示アクション
    • Stop Job

5. 一旦レシピを保存する

こうなっていればOK

6. DM送信用レシピを作成

  • トリガーをCallableレシピで作成
  • Input schemaに以下Jsonを定義
[
	{
		"name": "user_type",
		"type": "string",
		"optional": false,
		"control_type": "text"
	},
	{
		"name": "notify_body",
		"type": "string",
		"optional": false,
		"control_type": "text"
	},
	{
		"name": "call_channel",
		"type": "string",
		"optional": false,
		"control_type": "text"
	},
	{
		"name": "call_user_id",
		"type": "string",
		"optional": false,
		"control_type": "text"
	}
]

7. Slackユーザーリストを取得するアクションを追加

  • App → Slack、Action → Custom action
  • 以下パラメータを設定
    • Action name:適当に(使うAPI名を設定するのが個人的にオススメ)
    • Method:GET
    • Path:users.list
    • ResponseBody:以下Jsonを設定
      • 参考:https://api.slack.com/methods/users.list
[
	{
		"control_type": "text",
		"label": "Ok",
		"render_input": {},
		"parse_output": {},
		"toggle_hint": "Select from option list",
		"toggle_field": {
			"label": "Ok",
			"control_type": "text",
			"toggle_hint": "Use custom value",
			"type": "boolean",
			"name": "ok"
		},
		"type": "boolean",
		"name": "ok"
	},
	{
		"name": "members",
		"type": "array",
		"of": "object",
		"label": "Members",
		"properties": [
			{
				"control_type": "text",
				"label": "ID",
				"type": "string",
				"name": "id"
			},
			{
				"control_type": "text",
				"label": "Team ID",
				"type": "string",
				"name": "team_id"
			},
			{
				"control_type": "text",
				"label": "Name",
				"type": "string",
				"name": "name"
			},
			{
				"control_type": "text",
				"label": "Deleted",
				"render_input": {},
				"parse_output": {},
				"toggle_hint": "Select from option list",
				"toggle_field": {
					"label": "Deleted",
					"control_type": "text",
					"toggle_hint": "Use custom value",
					"type": "boolean",
					"name": "deleted"
				},
				"type": "boolean",
				"name": "deleted"
			},
			{
				"control_type": "text",
				"label": "Color",
				"type": "string",
				"name": "color"
			},
			{
				"control_type": "text",
				"label": "Real name",
				"type": "string",
				"name": "real_name"
			},
			{
				"control_type": "text",
				"label": "Tz",
				"type": "string",
				"name": "tz"
			},
			{
				"control_type": "text",
				"label": "Tz label",
				"type": "string",
				"name": "tz_label"
			},
			{
				"control_type": "number",
				"label": "Tz offset",
				"parse_output": "float_conversion",
				"type": "number",
				"name": "tz_offset"
			},
			{
				"properties": [
					{
						"control_type": "text",
						"label": "Avatar hash",
						"type": "string",
						"name": "avatar_hash"
					},
					{
						"control_type": "text",
						"label": "Status text",
						"type": "string",
						"name": "status_text"
					},
					{
						"control_type": "text",
						"label": "Status emoji",
						"type": "string",
						"name": "status_emoji"
					},
					{
						"control_type": "text",
						"label": "Real name",
						"type": "string",
						"name": "real_name"
					},
					{
						"control_type": "text",
						"label": "Display name",
						"type": "string",
						"name": "display_name"
					},
					{
						"control_type": "text",
						"label": "Real name normalized",
						"type": "string",
						"name": "real_name_normalized"
					},
					{
						"control_type": "text",
						"label": "Display name normalized",
						"type": "string",
						"name": "display_name_normalized"
					},
					{
						"control_type": "text",
						"label": "Email",
						"type": "string",
						"name": "email"
					},
					{
						"control_type": "text",
						"label": "Image 24",
						"type": "string",
						"name": "image_24"
					},
					{
						"control_type": "text",
						"label": "Image 32",
						"type": "string",
						"name": "image_32"
					},
					{
						"control_type": "text",
						"label": "Image 48",
						"type": "string",
						"name": "image_48"
					},
					{
						"control_type": "text",
						"label": "Image 72",
						"type": "string",
						"name": "image_72"
					},
					{
						"control_type": "text",
						"label": "Image 192",
						"type": "string",
						"name": "image_192"
					},
					{
						"control_type": "text",
						"label": "Image 512",
						"type": "string",
						"name": "image_512"
					},
					{
						"control_type": "text",
						"label": "Team",
						"type": "string",
						"name": "team"
					}
				],
				"label": "Profile",
				"type": "object",
				"name": "profile"
			},
			{
				"control_type": "text",
				"label": "Is admin",
				"render_input": {},
				"parse_output": {},
				"toggle_hint": "Select from option list",
				"toggle_field": {
					"label": "Is admin",
					"control_type": "text",
					"toggle_hint": "Use custom value",
					"type": "boolean",
					"name": "is_admin"
				},
				"type": "boolean",
				"name": "is_admin"
			},
			{
				"control_type": "text",
				"label": "Is owner",
				"render_input": {},
				"parse_output": {},
				"toggle_hint": "Select from option list",
				"toggle_field": {
					"label": "Is owner",
					"control_type": "text",
					"toggle_hint": "Use custom value",
					"type": "boolean",
					"name": "is_owner"
				},
				"type": "boolean",
				"name": "is_owner"
			},
			{
				"control_type": "text",
				"label": "Is primary owner",
				"render_input": {},
				"parse_output": {},
				"toggle_hint": "Select from option list",
				"toggle_field": {
					"label": "Is primary owner",
					"control_type": "text",
					"toggle_hint": "Use custom value",
					"type": "boolean",
					"name": "is_primary_owner"
				},
				"type": "boolean",
				"name": "is_primary_owner"
			},
			{
				"control_type": "text",
				"label": "Is restricted",
				"render_input": {},
				"parse_output": {},
				"toggle_hint": "Select from option list",
				"toggle_field": {
					"label": "Is restricted",
					"control_type": "text",
					"toggle_hint": "Use custom value",
					"type": "boolean",
					"name": "is_restricted"
				},
				"type": "boolean",
				"name": "is_restricted"
			},
			{
				"control_type": "text",
				"label": "Is ultra restricted",
				"render_input": {},
				"parse_output": {},
				"toggle_hint": "Select from option list",
				"toggle_field": {
					"label": "Is ultra restricted",
					"control_type": "text",
					"toggle_hint": "Use custom value",
					"type": "boolean",
					"name": "is_ultra_restricted"
				},
				"type": "boolean",
				"name": "is_ultra_restricted"
			},
			{
				"control_type": "text",
				"label": "Is bot",
				"render_input": {},
				"parse_output": {},
				"toggle_hint": "Select from option list",
				"toggle_field": {
					"label": "Is bot",
					"control_type": "text",
					"toggle_hint": "Use custom value",
					"type": "boolean",
					"name": "is_bot"
				},
				"type": "boolean",
				"name": "is_bot"
			},
			{
				"control_type": "number",
				"label": "Updated",
				"parse_output": "float_conversion",
				"type": "number",
				"name": "updated"
			},
			{
				"control_type": "text",
				"label": "Is app user",
				"render_input": {},
				"parse_output": {},
				"toggle_hint": "Select from option list",
				"toggle_field": {
					"label": "Is app user",
					"control_type": "text",
					"toggle_hint": "Use custom value",
					"type": "boolean",
					"name": "is_app_user"
				},
				"type": "boolean",
				"name": "is_app_user"
			},
			{
				"control_type": "text",
				"label": "Has 2 fa",
				"render_input": {},
				"parse_output": {},
				"toggle_hint": "Select from option list",
				"toggle_field": {
					"label": "Has 2 fa",
					"control_type": "text",
					"toggle_hint": "Use custom value",
					"type": "boolean",
					"name": "has_2fa"
				},
				"type": "boolean",
				"name": "has_2fa"
			}
		]
	},
	{
		"control_type": "number",
		"label": "Cache ts",
		"parse_output": "float_conversion",
		"type": "number",
		"name": "cache_ts"
	},
	{
		"properties": [
			{
				"control_type": "text",
				"label": "Next cursor",
				"type": "string",
				"name": "next_cursor"
			}
		],
		"label": "Response metadata",
		"type": "object",
		"name": "response_metadata"
	}
]

8. 全体,通常メンバー,マルチチャンネルゲスト,シングルチャンネルゲストの値を入れるリストを作成

  • App → Variables by Workatoを選択
  • Action → Create listを選択
  • Setup → List name,List item schemaを設定
  • 全体,通常メンバー,マルチチャンネルゲスト,シングルチャンネルゲストのリストをそれぞれ作成する

9. 取得したユーザーリストを元にループ処理を設定する

  • Repeat action を選択
  • 以下のように設定
  • 作成したループ内にユーザーリストの値を評価するIF分岐をそれぞれ(3つ)設定する
  • 以下の通り条件を設定する
    • 通常メンバー
      • Deleted:is not true
      • ID:does not equal:USLACKBOT
      • is bot:is not true
      • is restricted:is not true
      • is ultra restricted:is not true
    • マルチチャンネルゲスト
      • Deleted:is not true
      • ID:does not equal:USLACKBOT
      • is bot:is not true
      • is restricted:true
      • is ultra restricted:is not true
    • シングルチャンネルゲスト
      • Deleted:is not true
      • ID:does not equal:USLACKBOT
      • is bot:is not true
      • is restricted:true
      • is ultra restricted:true
  • 分岐先のアクションにて、先に定義した各リストへ値を追加する
  • こうなっていれば完成

分岐する条件について解説

  • 通常メンバー/マルチチャンネルゲスト/シングルチャンネルゲストはis restrictedとis ultra restrictedで判別できる
  • is restricted/is ultra restrictedのみではbot/通常メンバーの判別ができないのでis botを使う
  • is botではデフォルトのSlackbotがFALSEのためIDを指定して判別する

10. モーダルで入力された値で分岐し、対象ユーザーへDMを送信

  • 送る対象別にIF分岐を作成

参考)分岐条件早見表

  • IF分岐の配下に、対応するリストでループする処理を配置
  • DM送信処理を設定
    • App → SlackWorkbotを選択
    • Action → Post message
    • Setup → 以下画像参照

11. 作成したレシピを有効にすれば完了

以上です。

お疲れ様でした。

注意事項とか言い訳とか

注意!! 誤操作したらあちちだぞ

このレシピはSlackに存在するユーザーにまとめてDM飛ばすので、誤操作したらわりと大変なことになります。
(経験者は語る)
SlackEGを契約している方はテスト用Workspaceを立ててから作ることを強くオススメします。

(言い訳)ユーザー分岐とDMとでループ処理を分けているのはなぜか

ユーザ-取得→DM送信でループを二回まわしていますが、実は一つのループに統合できちゃいます。
以下の理由により今回はループを分けました。

  • ユーザー取得→条件に従ってリストへ代入、の処理を同一レシピ内で別途使う予定があった
  • DM送信前にデバッグ目的でLogを出力する、ドライランを仕掛ける等の目的があった

(言い訳)なぜレシピを分けたのか

レシピも一つに統合できます。じつは。
これはレシピ管理の都合上、処理とトリガー部分を分けた方が良いなと判断したためです。
Workbotトリガー→InteractiveMessageの値受け取りのレシピに処理も記載すれば良いのですが、
後日スラッシュコマンド起動の機能を起動用レシピに追加する予定だったのであえて分けています。

おわりに

ここまでお付き合いありがとうございました。
一斉連絡のツールは強力です。
実装してすぐに弊社内で使う機会があったので早速役に立ちました。
すっごく便利。ええもん作ったわぁ。

取扱注意なので気をつけてくださいね!

以上、ばるすでした。

ばるす

パチンコ屋→焼き肉屋→情シスを経てクラウドネイティブへ入社。
趣味はギター,キーボード,アウトプット,散歩,読書など。
苦手なものは朝と事務作業。得意分野は眠ること。