ElixirでTwitterのbotを作る


この記事はElixir Advent Calendar 2020の15日目の投稿です。

はじめに

こんにちは!今年から渋谷のITエンジニアになりましたringoと言います。
私のいる部署では10%ルールと言って、業務の10%の時間を自由な研究開発等に使うことができます。上司からの説明だと、「何か流行ってることとか面白そうなことをテーマに選んでやってみてね!よろしく!!」ってな感じだった気がします。そこで前々から気になっていたElixirに手を出してみることにしました。

Elixir始めた理由

一番最初のきっかけを辿るととても長く、学生5人でレンタカーを借りて24hで高知福岡往復LT会参戦の話になるのですが、それは置いといて、、、
Elixirを勉強しようと思った理由として

  • そろそろ新しい言語を勉強したいなと思った
  • Rust, go, Elixirと比較した時にElixirがカバー範囲が広く、習得が比較的学習コストが低そうだった
  • IoTで使えそう
  • コミュニティが活発で楽しそう

などの理由があります。
特にIoTで使えそうは理由として大きいです。IoTのサービスを考えたときにハードウェアからソフトウェア、さらにアプリケーションまでをElixirで作成できるのはとても夢があっていいなと思いました。

この記事で書くこと

前置きが長くなりましたが、新しい言語を勉強するといっても文法を学んだだけでバリバリ書けるようになるかと言われると、私には難しいです。というわけでElixir使って何か作るか。と考えた結果Twitterのbotを作ろう!となりました。
じゃあ何をネタにつぶやくかを考えると、以前作ったGrafanaを使った部屋の温湿度モニタリングを発展させて、その日の温度変化や最高最低気温などをTwitterで確認出来たら便利そうだし、勉強がてら作るのにちょうど良さそうな気がします。

ということでこの記事で扱うことは以下のことになります。

  • Grafanaのパネルイメージの取得
  • ExTwitterの使い方
  • quantum-elixirの使い方(間に合えば)

下調べ

ざっくりとした構想として、時間になったらGrafanaからグラフ画像の取得 -> メッセージを付け加えてツイートする。という流れは想像できます。どうやったらできるのか、実装可能かについて調べていきます。

Grafanaの画像取得

やりたいこととしては、Grafanaのダッシュボードに表示されるグラフをtweetできるように画像として取得することです。恐らくやりたいこと用のAPIがGrafanaに用意されているだろうと思っていたのですが、見つけられませんでした。
ここでちょっとやり方が分からずやる気が下がっていたのですが、考え方を変えて普通にGrafanaの画面を開いて画像を取得する方法を試してみます。
Grafanaでパネル単体の画像を取得するにはダッシュボードを開きパネルのタイトルからShare -> Direct link rendered imageをクリックするとパネルの画像を表示することができます。この時のURLを元にリクエストを変更してGETすると画像を取得することが分かりました。

ツイート部分

以前Twitterで遊んだ時にTwitterのトークンの取得は済ませていたので、方法としては素のTwitterAPIを叩くかElixirのTwitterライブラリを使用するかになります。
調べてみるとElixirにはExTwitterというライブラリがありました。どうやらこのライブラリを使うと簡単につぶやくことができそうです。
試しにTwitter公式のAPIドキュメントとExTwitterのREADME.mdと睨めっこしながら
iex(1)> ExTwitter.update_with_media("sample post", "test.png")
とすると何の問題もなく画像付きツイートが投稿されました。これでツイート部分はできそうです。

定期実行

botなら定期的につぶやいたり、何かしらアクションがあったときに実行されなければなりません。Elixirで定期実行を行うならQuantumというライブラリがcronのように使えそうです。
ドキュメントを読んだ感じ、スケジューラーに実行したい処理を書いてconfigにタイミングを指定して、jobを登録すれば実行されるっぽいです。

ここまでで必要そうな材料はそろいました。あとはこれをイイ感じに組み合わせて期待通りの動きができるようソースコードに落としていくだけです。

全体の設計

ひとまず毎日0時になったらその日の温湿度のグラフを取得してつぶやくことを目標にします。
コードの設計と言ってもだいたい1人で何か作るときはコードベタ書きで動いたらヨシされることが多いのですが、せっかくなのでプログラミングElixirの第13章プロジェクトを構成するを参考にしながらプロジェクトを作ります。
Grafanaから画像を取得する部分、Tweetする部分、定期実行に分けてプログラムを書いていきます。

Grafanaから画像の取得

リクエストを作ってGETすれば画像が取得できることは分かりました。
grafanaから得られるURLは次のようなURLになります。
http://grafana_url:3000/render/d-solo/uid/dashbord_name?orgId=1&from=1607900494937&to=1607922094938&panelId=2&width=1000&height=500&tz=Asia%2FTokyo

これにAuthorization: Bearer ヘッダでAPI Keyを送ればパネルの画像が取得できます。API KeyはGrafanaにログインし、ConfigurationのAPI Keyから発行することができます。

curlコマンドだとこんな感じです。

$ curl -s -k -H "Authorization: Bearer  Your Grafana API Key" "http://grafana_url:3000/render/d-solo/uid/dashbord?orgId=1&from=1604968011549&to=1605076016594&panelId=2&width=1000&height=500&tz=Asia%2FTokyo" > panel.png

このパラメータを紐解いてElixirで記述します。

grafana.ex
defmodule TweetGrafanaImg.Grafana do

  use Application

  @url ""
  @token ""

  def get_panel() do
    headers = make_headers(@token)
    from = System.os_time - 86400 * 1000000000 |> div(1000000)
    params = make_params(from, div(System.os_time, 1000000))
    options = make_options(5000)
    request = make_request(:get, headers, options, params, @url)
    HTTPoison.request(request)
  end

  def make_request(method, headers, options, params, url) do
    %HTTPoison.Request{
      method: method,
      headers: headers,
      options: options,
      params: params,
      url: url
    }
  end

  def make_headers(token) do
    ["Authorization": "Bearer #{token}"]
  end
  # 86400 = 24 * 60 * 60
  # params = make_params(System.os_time - 86400 * 1000000000, System.os_time)

  def make_params(from, to) do
    [
      {~s|orgId|, ~s|1|},
      {~s|from|, ~s|#{from}|},
      {~s|to|, ~s|#{to}|},
      {~s|panelId|, ~s|2|},
      {~s|width|, ~s|1000|},
      {~s|height|, ~s|500|},
      {~s|tz|, ~s|Asia/Tokyo|},
    ]
  end

  def make_options(timeout) do
    [hackney: [:insecure], recv_timeout: timeout]
  end

end

timeoutの時間を設定しているのは、Grafanaの画像レンダリングが重くてレスポンスに時間がかかるためです。
Qiita書いてるときに気づきましたが、時間の計算はしなくとも、&from=now-24h&to=nowでパラメータ設定したら今から過去24hのグラフ生成してくれるみたいです。

Tweet部分

tweet.ex
defmodule TweetGrafanaImg.Tweet do

  use Application

  def tweet(text) do
    ExTwitter.configure(
      consumer_key: Application.get_env(:extwitter, :oauth)[:consumer_key],
      consumer_secret: Application.get_env(:extwitter, :oauth)[:consumer_secret],
      access_token: Application.get_env(:extwitter, :oauth)[:access_token],
      access_token_secret: Application.get_env(:extwitter, :oauth)[:access_token_secret],
    )
    resp = ExTwitter.update(text)
  end

  def tweet(text, media) do
    ExTwitter.configure(
      consumer_key: Application.get_env(:extwitter, :oauth)[:consumer_key],
      consumer_secret: Application.get_env(:extwitter, :oauth)[:consumer_secret],
      access_token: Application.get_env(:extwitter, :oauth)[:access_token],
      access_token_secret: Application.get_env(:extwitter, :oauth)[:access_token_secret],
    )
    resp = ExTwitter.update_with_media(text, media)
  end
end

ExTwitterのドキュメント通りのプログラムです。
ここまで書けたら実際に動かしてみます。

$ iex -S mix
Erlang/OTP 23 [erts-11.1] [source] [64-bit] [smp:1:1] [ds:1:1:10] [async-threads:1]

Interactive Elixir (1.11.2) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> {:ok, resp} = TweetGrafanaImg.Grafana.get_panel()
iex(2)> TweetGrafanaImg.Tweet.tweet("test dashbord img", resp.body)

Twitterを確認すると、

ちゃんと投稿されています。あとは今までの処理をスケジューラーに登録すればとりあえずやりたいことは実現できるはずです。

定期実行する

Quantumを使ったらできる!らしい
間に合わなかったので、後々追記します。

終わりに

今回こそ余裕をもって記事書くぞ!って思ってたけどギリギリになってしまった。
Elixirでプロジェクト作ること自体初めてで分かんないことだらけでしたが、なんとか形にはなったと思います。
完成したプログラムはこちらになります。
https://github.com/ringo156/tweet_grafana_img

やってみた系の記事は言語問わずよく見ますが、どのように設計を組み立てていったとか、実装するまでの経緯(?)みたいなのはあまり見ない気がしたので、できるだけ書いてみました。
Elixirらしいかは分かんないけど当初の目的は達成できたと思います。
読んでくださってありがとうございました。

参考