はじめに
- Slackのワークフロービルダー使っていますか!
- 申請内容を一覧化してみました!
- conversations.history APIを使わせていただいて、メッセージの内容をパースして、CSVに保存します
- 使用した言語は、もちろんElixirです

% uname -a
Darwin 20-02noMacBook-Pro.local 19.2.0 Darwin Kernel Version 19.2.0: Sat Nov 9 03:47:04 PST 2019; root:xnu-6153.61.1~20/RELEASE_X86_64 x86_64
% elixir -v
Erlang/OTP 22 [erts-10.5.3] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:1] [hipe]
Elixir 1.9.4 (compiled with Erlang/OTP 22)
なぜ一覧化するの?
- そもそもなぜ一覧化が必要なのでしょうか
- 私が所属している会社では、有給休暇の申請はSlackのワークフローで行うことになっています
- 他の各種申請(経費精算、始業時間変更等)もとにかく申請が必要だったものはあるときから、すべてSlackに移行しています
- 各人の有給休暇の取得日数を集計したりする管理部門の人は一覧で欲しかったりするのではないでしょうか
- 他の良い方法はたくさんあるかもしれません
- あれ? あの人、今日いないなあ、休みだっけ? とワークフローのチャネルをスクロールするのが面倒だったりしませんか?
- カレンダー共有するとかモダンなやりかたがあるかもしれません
- 詰まるところ、私が古臭い人間だから一覧が欲しいのです
- 私は「承認」ボタンを押したあと、スプレッドシートに手動でコピペしていました
- 最初のうちはそれでもよかったのですが、2ヶ月くらいそうやって続けてきてだんだん面倒になってきました
- 一覧は、Phoenixで! とおもわないわけではなかったのですが、まだPhoenixは修行中なので、まずは肝の部分をCSVに書き込むことをやってみました
ソースコード
- TORIFUKUKaiou/slack_workflow
- ワークフローは多種多様だとおもうので、私が所属しているワークスペースのチャネルの申請内容はギリギリ、パースできている気がしますが、みなさまのところのチャネルで同じようにうまく動くかどうかはわかりません
- 私が所属しているワークスペースでも、まあそれなりにというところくらいなので、みなさまのところではそのままでは動かない可能性は大です
- 以下、一覧化したいあなたに向けて、ポイントになりそうなところを説明します
- なにはともあれElixirをインストールしましょう!
- Installing Elixir
- Windowsの方は上記ページからインストーラをダウンロードできます
- macOSの方はasdf-vmでインストールするのがオススメですが、手軽に始めるならHomebrewでインストールするでもいいかもしれません
- 上のページはHomebrewでのインストール方法が最初に書いてありますし
SlackWorkflow.Applications.run/3
lib/slack_workflow/slack_applications.ex
defmodule SlackWorkflow.Applications do
def run(channel, oldest, latest) do
members = fetch_members()
channel_id = fetch_channels() |> Map.fetch!(channel)
SlackWorkflow.SlackConversationsHistory.fetch(channel_id, oldest, latest)
|> Enum.filter(&Map.get(&1, "blocks"))
|> Enum.map(&{Map.get(&1, "blocks"), Map.get(&1, "ts"), Map.get(&1, "username")})
|> Enum.map(fn {blocks, ts, name} -> {Enum.at(blocks, 0), ts, name} end)
|> Enum.filter(fn {blocks, _, _} -> Map.get(blocks, "text") end)
|> Enum.map(fn {blocks, ts, name} -> {Map.get(blocks, "text"), ts, name} end)
|> Enum.filter(fn {text_map, _, _} -> Map.get(text_map, "type") == "mrkdwn" end)
|> Enum.map(fn {text_map, ts, name} -> {Map.get(text_map, "text"), ts, name} end)
|> Enum.map(fn {text, ts, name} -> {parse(text, members), ts, name} end)
|> Enum.map(fn {keywords, ts, name} ->
{Keyword.put(keywords, :created_at, date(ts)), ts, name}
end)
|> Enum.map(fn {keywords, ts, name} ->
{Keyword.merge(keywords, [{"url" |> String.to_atom(), url(ts, channel_id)}]), name}
end)
|> Enum.reduce(%{}, fn {keywords, name}, acc_map ->
list = acc_map |> Map.get(name, [])
acc_map |> Map.merge(%{name => [keywords] ++ list})
end)
end
-
Applications
は申請たちの意味です -
conversations.history APIを呼び出した結果、
blocks
というキーをもつメッセージだけが、私の所属しているワークスペースのチャネルでは申請内容を表しているようにみえました- 最初にこれで絞りこんでいるので、もうこの時点でうちは違うよの場合は、書き換えが必要です
-
iex -S mix
してSlackWorkflow.SlackConversationsHistory.fetch(channel_id, oldest, latest)
を呼び出してください - きれいな形でJSONがみえるのであなたが所属しているワークスペースでの申請内容の傾向をつかんでください
- SlackへのリンクURLを作りたくて、
ts
キーの値をとってなんとなくURLをつくっていますが正しいのかどうかわかっていません
SlackWorkflow.Applications.parse/2
lib/slack_workflow/slack_conversations_history.ex
defp parse(s, members) do
first = s |> String.split("\n") |> Enum.find_index(&(&1 =~ ~r/\*.+\*/))
second =
s
|> String.split("\n")
|> Enum.slice((first + 1)..-1)
|> Enum.find_index(&(&1 =~ ~r/\*.+\*/))
|> Kernel.+(first)
|> Kernel.+(1)
index = if second - first > 1, do: first, else: second
s
|> String.split("\n")
|> Enum.slice(index..-1)
|> Enum.reject(&(&1 == ">"))
|> Enum.reject(&(&1 == ""))
|> Enum.chunk_every(2)
|> Enum.filter(&(length(&1) == 2))
|> Enum.reduce(Keyword.new(), fn [k, v], keywords ->
keywords
|> Keyword.merge([{k |> key(), v |> value(members)}])
end)
end
- 一番の難関はここでしょう
-
私の所属しているワークスペースのチャネルでは申請内容では、いろいろ試行錯誤して、いまのところ実行時エラーは起きないくらいの実装内容となっております
- 出力結果に、Slackへのリンクが含まれているから、一覧としては、まあいっかなあくらいの感じです
-
*申請内容*
のように*
で囲まれている文字列が含まれているところがキーでその次にくるやつが値だろうという実装をしています- ここもみなさまのワークフローメッセージとは違いがある可能性が高いところだとおもいます
- 申請内容文字列を改行でsplitしたListのここから先が申請内容キーの始まりですよというものを
index
にいれているつもりです-
first
とかsecond
とか書いているのは私の所属しているワークスペースでは次の2種類があったためです - これがみなさまに伝わるのは自分でも疑わしくおもっています
- とにかく一筋縄ではいかないことをお伝えしておきます
-
:information_source: *申請内容*
> :neutral_face: *申請者* ← ここからキーとして解析したい
> <@UC1234567>\n> ← 申請者の値
> :date: *日付* ← 日付キー
> 2020/01/07 ← 日付の値
...
> *申請者* ← ここからキーとして解析したい
> <@UC1234567>\n> ← 申請者の値
> *経費精算フォーム* ← 経費精算フォーム キー
> <https://drive.google.com/open?id=hogehogehoge> ← 経費精算フォームの値
動かし方
ソースコード取得と依存関係の解決
% git clone https://github.com/TORIFUKUKaiou/slack_workflow.git
% cd slack_workflow
% mix deps.get
Slack APIのtokenを取得して環境変数SLACK_TOKENに設定
-
channels:history
、channels:read
、users:read
のScopesをもったトークンを取得してください - Slackアプリの作成方法は下記の記事がとても参考になるとおもいます!

Team domainを環境変数SLACK_TEAM_DOMAINに設定
- たとえば、あなたが所属しているワークスペースが
elixir.slack.com
だったら、elixir
をSLACK_TEAM_DOMAIN
に設定してください
iex 編
% iex -S mix
Erlang/OTP 22 [erts-10.5.3] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:1] [hipe]
Interactive Elixir (1.9.4) - press Ctrl+C to exit (type h() ENTER for help)
iex> channel = "workflow"
"workflow"
iex> oldest = 0
0
iex> latest = Timex.now |> Timex.local |> Timex.end_of_day |> DateTime.to_unix
1578754799
iex> SlackWorkflow.create_csvs(channel, oldest, latest)
:ok
-
channel
はワークフローがながれているチャネル名 -
oldest
とlatest
はunix timeで指定します -
oldest
が0のときは、一番最初のメッセージから取得します -
oldest
〜latest
の期間を指定して習得することができます
escript編
% iex
iex> Tzdata.Util.data_dir
"/Users/torifukukaiou/slack_workflow/_build/dev/lib/tzdata/priv"
- なんてのがでますから、これを
config/config.exs
に設定してください
config/config.exs
config :tzdata,
data_dir: "/Users/torifukukaiou/slack_workflow/_build/dev/lib/tzdata/priv",
autoupdate: :disabled
- この設定をしたあとに
% mix escript.build
- してください
-
slack_workflow
というバイナリができてるはずです
% ./slack_workflow --channel workflow --oldest 0 --latest 1578754799
- てな感じでコマンドで動かせるはずです