17
14

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

SlackAdvent Calendar 2017

Day 12

定期的にSlack Botで記事を通知する with Qiita API

Last updated at Posted at 2018-06-02

(この記事は、「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

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})

試しに、pythonelixirrubyそれぞれのタグをそれぞれのchannelに通知するようにします。

3. Channel情報を取得する。

ここから実際の通知機能を実装していきます。

scheduler_task.ex
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.SlackGenServerで起動させてます)

slack.ex
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. 実行結果

python_slack_bot.png elixir_slack_bot.png ruby_slack_bot.png

それぞれのchannelに送ることができました!!

最後に

  • Quantumがcron likeなので楽にスケジューリング実行が実装できました。
  • 秒単位での実行をcronではちょこっとした小技を使わないといけないですが、Quantumだと楽に実装できました。
  • データの保存をMnesiaにしようとしたのですが、スキーマファイルを用意したりと、めんどかったのでETSにしました。ただやっぱりスキーマがないのでMnesiaにしとけばよかったと少し後悔してます(作り直そうかな、、)。
  • 今はバリデーションも何もしてないので今後実装していきたいです。

明日は@piacere_exさんの「Elixirでデータ分析入門#2:インプットしたデータを変換する」になります!

:stars::stars::stars::stars::stars: 満員御礼!Elixir MeetUpを6月末に開催します :stars::stars::stars::stars::stars:
※応募多数により、増枠しました!!

「fukuoka.ex#11:DB/データサイエンスにコネクトするElixir」を6/22(金)19時に開催します。
自分もLTで発表させていただきますので、ぜひ興味ある方はご参加ください!

fukuokaex.png

17
14
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
17
14

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?