LoginSignup
6
2

More than 3 years have passed since last update.

Nervesでcronっぽい動作ができるQuantumを使って定期的にSlackにメッセージを投げた

Last updated at Posted at 2020-06-15

はじめに

定時にSlackのチャンネルに始業・終業のチャイムを投げるNervesをこしらえました。

@torifukukaiou さんの『NHK番組表API叩いてSlackで通知する[With Elixir]』をとてもとても参考にさせていただきました。

前提

  • mix nerves.new slackchime --nerves-packコマンドでプロジェクトを作成
  • ネットワークの設定は完了している
  • SlackのIncoming WebhookのURLを取得(生成)している

こんなのこしらえたよ

動きはきっとこう

Nerves起動時にlib/slackchime/application.exが呼び出されてSupervisor Treeにモジュールを登録 →  {Slackchime.Scheduler, []}

Slackchime.Scheduler(lib/slackchime/scheduler.exで定義)でQuantumで利用するotp_appを読み込み → use Quantum, otp_app: :slackchime

config/config.exsに設定しているjobsに書かれたモジュールを定期的に実行 → config :slackchime, Slackchime.Scheduler, jobs: [...]

対象ファイル

slackchime/
  config/
    config.exs
  lib/
    slackchime/
      application.ex
      scheduler.ex
    slackchime.ex
  mix.exs

mix.exs

depsに以下を追加して、mix deps.getします。

mix.exs ※関係関数のみ抜粋
defp deps do
  {:httpoison, "~> 1.6"},
  {:jason, "~> 1.2"},
  {:quantum, "~> 3.0-rc"},
  {:timex, "~> 3.5"}
end

lib/slackchime.ex

実際のプログラムはこのような感じにしています。
config/config.exsに設定した値をとってきてみたかったのでApplication.get_env()を使ってます :wink:

Slackchime.ring()を呼ぶとSlackで設定したチャンネル or 個人などにメッセージを投げます。

lib/slackchime.ex
defmodule Slackchime do
  @slack_incoming_hook_urls Application.get_env(:slackchime, :slack_incoming_hook_urls)
  @chime_01 Application.get_env(:slackchime, :chime_01)

  def ring(greeting) do
    message =
      case greeting do
        "start_of_work" -> "おはようございます!"
        "start_of_lunch" -> "お昼休みです!ご飯をもりもり食べましょう(^¬^)"
        "end_of_lunch" -> "お昼休み終わりました!"
        "end_of_work" -> "今日も一日おつかれさまでした!"
        nil -> "..."
        _ -> greeting
      end

    slack_post(message)
  end

  defp slack_post(message) do
    body =
      slack_post_body(message)
      |> Jason.encode!()

    header = slack_post_header()

    for slack_incoming_hook_url <- @slack_incoming_hook_urls do
      HTTPoison.post!(slack_incoming_hook_url, body, header)
    end
  end

  defp slack_post_body(message) do
    %{
      text: @chime_01 <> "\n" <> message
    }
  end

  defp slack_post_header() do
    [{"Content-type", "application/json"}]
  end
end

lib/slackchime/scheduler.ex

cron的な動きをするQuantumの利用を宣言します。
これはSupervisor Treeから呼ばれるだけのモジュールで、実際のcrontab的な設定はconfig/config.exに記載します。

今回は:slackchimeという名前で宣言しました。

lib/slackchime/scheduler.ex
defmodule Slackchime.Scheduler do
  use Quantum, otp_app: :slackchime
end

lib/slackchime/application.ex

Supervisor TreeにQuantumを登録します。

lib/slackchime/application.ex ※コメントを除く。関係関数のみ抜粋
def start(_type, _args) do
  opts = [strategy: :one_for_one, name: Slackchime.Supervisor]

  children =
    [
      {Slackchime.Scheduler, []}
    ] ++ children(target())

  Supervisor.start_link(children, opts)
end

config/config.ex

変数の設定とQuantumのジョブの設定をします。

  • 営業日は月曜日から金曜日
  • timezone "Asia/Tokyo"の設定をするとエラーだったのでUTCで記載
  • 営業日の日本時間9時にSlackchime.ring("start_of_work")を実行
  • 営業日の日本時間12時にSlackchime.ring("start_of_lunch")を実行
  • 営業日の日本時間13時にSlackchime.ring("end_of_lunch")を実行
  • 営業日の日本時間18時にSlackchime.ring("end_of_work")を実行
config/config.exs ※関係する設定のみ抜粋
#
# 変数設定
#
config :slackchime,
  slack_incoming_hook_urls:
    ["https://hooks.slack.com/services/*********/***********/************************"],    # リストで書くと複数にポスト
  chime_01: "キーンコーンカーンコーン♪"


#
# cron的に使うjobs設定
# UTCで記載する(timezoneの設定をするとエラー)
#
config :slackchime, Slackchime.Scheduler,
  jobs: [
    # {"* * * * *", {Slackchime, :ring, ["start_of_lunch"]}},   # テスト用 毎分実行
    # {"* * * * Mon-Fri *", {Slackchime, :ring, ["てすとー"]}},   # テスト用 毎分実行 月曜から金曜の間
    {"0 0 * * Mon-Fri *", {Slackchime, :ring, ["start_of_work"]}},
    {"0 3 * * Mon-Fri *", {Slackchime, :ring, ["start_of_lunch"]}},
    {"0 4 * * Mon-Fri *", {Slackchime, :ring, ["end_of_lunch"]}},
    {"0 9 * * Mon-Fri *", {Slackchime, :ring, ["end_of_work"]}}
  ]

Nervesにアップロード

upload.shを使ってイメージをアップロードします。
※192.168.5.55はNervesのIPアドレスです。

fish
~> mix firmware
~> ./upload.sh 192.168.5.55

結果

Nervesが起動すると定時にSlackにメッセージが投げられていることが確認できました\(^o^)/
スクリーンショット 2020-05-10 0.08.10.png

うまくいっていないこと

解決しました!!

Nervesのパーミッションの問題だと思うんですが"/srv/erlang/lib/tzdata-1.0.3/priv/latest_remote_poll.txt"に書き込みができず約3秒に1回エラーが出てしまってます。シェルに落ちてパーミッション追加しようとしてもchmodコマンドがなくお手上げでした(´・ω・`)

Nerves(RinLogger.attach)
エラーの例。

07:44:10.633 [error] GenServer :tzdata_release_updater terminating
** (File.Error) could not write to file "/srv/erlang/lib/tzdata-1.0.3/priv/latest_remote_poll.txt": read-only file system
    (elixir 1.10.2) lib/file.ex:1050: File.write!/3
    (tzdata 1.0.3) lib/tzdata/data_loader.ex:44: Tzdata.DataLoader.last_modified_of_latest_available/1
    (tzdata 1.0.3) lib/tzdata/release_updater.ex:81: Tzdata.ReleaseUpdater.loaded_tzdata_matches_remote_last_modified?/0
    (tzdata 1.0.3) lib/tzdata/release_updater.ex:42: Tzdata.ReleaseUpdater.poll_for_update/0
    (tzdata 1.0.3) lib/tzdata/release_updater.ex:19: Tzdata.ReleaseUpdater.handle_info/2
    (stdlib 3.12.1) gen_server.erl:637: :gen_server.try_dispatch/4
    (stdlib 3.12.1) gen_server.erl:711: :gen_server.handle_msg/6
    (stdlib 3.12.1) proc_lib.erl:249: :proc_lib.init_p_do_apply/3
Last message: :check_if_time_to_update
State: []

まとめ

Quantumを使ってNervesでcron的な定期的にモジュールを実行することができました。

@torifukukaiou さんの記事にとても助けていただきました!!ありがとうございました!

6
2
3

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
6
2