(この記事は、「fukuoka.ex(その2) Elixir Advent Calendar 2017」の14日目、Slack Advent Calendar 2017の12日目です)
昨日は@koga1020さんの「Phoenix + Vue.js入門」でした!
はじめに
|> ElixirでSlack Botを作った with Qiita API
前回に引き続きSlack Botの作成を行います。前回ではSlack Botの基本的な実装を進めていきましたが、今回はスケジューリング実行をするようにします。
スケジューリング実行でどうする?
気になってるタグのもので最新のものを定期的に取得、それぞれのタグに合わせて通知先のchannelを分けて通知するような仕組みを作りました。
概要
スケジューリング実行するためにライブラリでQuantumを利用して実行しました。
Quantumはcronみたいに定期実行するためのスケジューリングライブラリです。
あとはタグ情報の保存のためにETSを使いました。
実装
1. Quantumでスケジューリングの準備
まずは、スケジューリングのための準備をします。
defmodule ExAviso.Scheduler do
use Quantum.Scheduler, otp_app: :ex_aviso_scheduler
end
実装自体はシンプルでスケジューリング用のモジュールを作いuse Quantum.Scheduler
を使うだけです。
次にconfig.ex
にスケジューリング用のジョブを追加します。
config :ex_aviso_scheduler, ExAviso.Scheduler,
jobs: [
{"* * * * *", {ExAviso.Scheduler.Task, :tag_items, []}}
]
上記の設定でExAviso.Scheduler.Task
モジュールのtag_items
関数が毎分実行されます。
"* * * * *"
はcronではおなじみの設定ですね。わかりやすい。
他にも実行のスケジューリング方法として以下のようなものがあります。
# 1秒ごと
{{:extended, "* * * * *"},{ExAviso.Scheduler.Task, :tag_items, []}}
# 15分ごと
{"*/15 * * * *", {ExAviso.Scheduler.Task, :tag_items, []}}
# 18時、20時、22時、0時、2時、4時、6時で実行
{"0 18-6/2 * * *", {ExAviso.Scheduler.Task, :tag_items, []}}
# 毎日実行("0 0 * * *")
{"@daily", {ExAviso.Scheduler.Task, :tag_items, []}}
# 関数を直接実行
{"*/15 * * * *", fn -> System.cmd("rm", ["/tmp/tmp_"]) end}
さらに設定をもう少し細かくしジョブに名前をつけることもできます。
config :ex_aviso_scheduler, ExAviso.Scheduler,
jobs: [
notification_tag_items: [
schedule: {:cron, "* * * * *"},
task: {ExAviso.Scheduler.Task, :tag_items, []}
]
]
2. タグ情報を保存する
タグ情報を保存します。まずは
- どのタグを
- どのchannelに対して
通知するのかを設定していきます。
っとその前にETSの記述がいろんなところにあるのは少々気持ちが悪いのと、ある程度管理しやすくしたいのでデータストア用のモジュールを作りました。(雑です、特に複雑なことはせずただただETSを薄くラップした程度です)
defmodule ExAviso.DataStorage do
alias :ets, as: Ets
@tags :tags
def start_link() do
GenServer.start_link(__MODULE__, [], name: __MODULE__)
end
def init([]) do
Ets.new(@tags, [:set, :named_table, :private])
{:ok, %{@tags => 1}}
end
def handle_call({:fetch, table, id}, _from, t) do
res = Ets.lookup(table, id)
{:reply, res, t}
end
def handle_call({:first, table}, _from, t) do
id = Ets.first(table)
res = Ets.lookup(table, id)
{:reply, res, t}
end
def handle_call({:next, table, id}, _from, t) do
res_id = Ets.next(table, id)
res = Ets.lookup(table, res_id)
{:reply, res, t}
end
def handle_call({:all, table}, _from, t) do
res =
case Ets.first(table) do
:"$end_of_table" ->
[]
item_id ->
all_list(table, item_id, Ets.lookup(table, item_id))
end
{:reply, res, t}
end
defp all_list(table, id, lists) do
case Ets.next(table, id) do
:"$end_of_table" ->
lists
item_id ->
all_list(table, item_id, lists ++ Ets.lookup(table, item_id))
end
end
def handle_call({:insert, table, obj}, _from, t) do
id = t[@tags]
res_id = Ets.insert(table, Tuple.insert_at(obj, 0, id))
res = Ets.lookup(table, res_id)
{:reply, res, Map.replace!(t, @tags, id + 1)}
end
def get!(table, id) do
GenServer.call(__MODULE__, {:fetch, table, id})
end
def first(table) do
GenServer.call(__MODULE__, {:fetch, table, 1})
end
def next(table, id) do
GenServer.call(__MODULE__, {:next, table, id})
end
def all(table) do
GenServer.call(__MODULE__, {:all, table})
end
def insert!(table, obj) when is_tuple(obj) do
GenServer.call(__MODULE__, {:insert, table, obj})
end
end
ではETSにタグ情報を保存します。ETSにはschemaがないらしいのでアプリケーションで制御する必要があります (めんどい)。
今回は{タグ名、通知先のchannel、表示件数}
で保存します。
alias ExAviso.DataStorage
DataStorage.insert!(:tags, {"ptyhon", "python", 1})
DataStorage.insert!(:tags, {"elixir", "elixir", 5})
DataStorage.insert!(:tags, {"ruby", "ruby", 1})
試しに、python
、elixir
、ruby
それぞれのタグをそれぞれのchannelに通知するようにします。
3. Channel情報を取得する。
ここから実際の通知機能を実装していきます。
defmodule ExAviso.Scheduler.Task do
def tag_items() do
[token: token] = Application.get_all_env(:qiita)
headers = [{"Authorization", "Bearer #{token}"}]
channels = GenServer.call(ExAviso.Slack, {:channels})
message = fetch_tag_items(Storage.all(:tags), headers, channels)
end
channels = GenServer.call(ExAviso.Slack, {:channels})
まずはchannel名からchannel idに変換するためchannelの一覧を取得します。
(ExAviso.Slack
はGenServer
で起動させてます)
defmodule ExAviso.Slack do
def handle_call({:channels}, _from, t) do
[token: token] = Application.get_all_env(:slack)
url = "https://slack.com/api/channels.list?token=#{token}"
%{channels: channles} = body =
case HTTPoison.get!(url) do
%{status_code: 200, body: body} ->
Poison.Parser.parse!(body, keys: :atoms)
end
{:reply, channles, t}
end
https://slack.com/api/channels.list
にリクエストし正常にレスポンスが帰って来ればchannel一覧が取得できます。
取得したchannel一覧を元に通知をするchannel idを取得します。
4. 通知を行う
def fetch_tag_items([{id, tag, channel, per_page} | tail], headers, channels) do
c = Enum.find(channels, fn c -> c.name == channel end)
url = "https://qiita.com/api/v2/tags/#{tag}/items?page=1&per_page=#{per_page}"
body =
case HTTPoison.get!(url, headers) do
%{status_code: 200, body: body} ->
Poison.Parser.parse!(body, keys: :atoms)
end
e = for %{title: title, url: url} <- body, do: "#{title}[#{url}]"
m = %Request{
type: "message",
channel: c.id,
text: Enum.join([" ---- tag is [#{tag}] --- - "] ++ e, "\n"),
ts: DateTime.utc_now() |> DateTime.to_unix()
}
GenServer.cast(ExAviso.Slack, {:notification, m})
fetch_tag_items(tail, headers, channels)
end
Enum.find(channels, fn c -> c.name == channel end)
channel一覧からchannel名を元に特定のchannel情報を取得します。
https://qiita.com/api/v2/tags/:tag_id/items
でQiitaからタグの割り当てた最新の記事を取得します。(per_pageオプションで何件取得するか決めてます)
e = for %{title: title, url: url} <- body, do: "#{title}[#{url}]"
取得した一覧からタイトルとURLのみを抽出し、送信するためのメッセージを作成します。
GenServer.cast(ExAviso.Slack, {:notification, m})
最後にslack通知をするためのモジュールに受けたわし通知をします。
5. 実行結果
それぞれのchannelに送ることができました!!
最後に
- Quantumがcron likeなので楽にスケジューリング実行が実装できました。
- 秒単位での実行をcronではちょこっとした小技を使わないといけないですが、Quantumだと楽に実装できました。
- データの保存をMnesiaにしようとしたのですが、スキーマファイルを用意したりと、めんどかったのでETSにしました。ただやっぱりスキーマがないのでMnesiaにしとけばよかったと少し後悔してます(作り直そうかな、、)。
- 今はバリデーションも何もしてないので今後実装していきたいです。
明日は@piacere_exさんの「Elixirでデータ分析入門#2:インプットしたデータを変換する」になります!
満員御礼!Elixir MeetUpを6月末に開催します
※応募多数により、増枠しました!!
「fukuoka.ex#11:DB/データサイエンスにコネクトするElixir」を6/22(金)19時に開催します。
自分もLTで発表させていただきますので、ぜひ興味ある方はご参加ください!