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

# はじめに

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

@torifukukaiou さんの『[NHK番組表API叩いてSlackで通知する[With Elixir]](https://qiita.com/torifukukaiou/items/56373e0bab428ae51a83)』をとてもとても参考にさせていただきました。



## 前提

- `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`します。

```elixir: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 個人などにメッセージを投げます。

```elixir: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`という名前で宣言しました。

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


## lib/slackchime/application.ex
Supervisor TreeにQuantumを登録します。

```elixir: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")`を実行


```elixir: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アドレスです。

```shell:fish
~> mix firmware
~> ./upload.sh 192.168.5.55
```


# 結果
Nervesが起動すると定時にSlackにメッセージが投げられていることが確認できました＼(^o^)／
![スクリーンショット 2020-05-10 0.08.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/51020/431917df-a94f-4dad-fac5-74b0dc1ed606.png)


## うまくいっていないこと

<font color="red">解決しました！！</font>

- [Nervesで出続けるtzdataのエラーを止めた](https://qiita.com/nishiuchikazuma/items/3a7dd012193f59423831)

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

```shell: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 さんの記事にとても助けていただきました！！ありがとうございました！