Qiita
QiitaAPI
Elixir
hatenabookmark
Phoenix

Qiitaの記事のはてブ増加を通知するサービスを作った

経緯

Qiitaの記事はQiitaで投稿されQiita上で表示されるため、自分の記事がはてなブックマークされても特に通知などが来ない。

ずっとはてなブログで記事を書いていたため通知が来るのがあたりまえだと思っていたが、基本的にその他のサービスだと通知が来ないのだな、ということに気づいた。

ということで、QiitaもはてなブックマークもAPIがあるので、連携させることではてブ追加された時に通知してくれるサービスを作った。

はてなブックマーク増加チェッカー

仕様

  • 基本的にはどんなURLでも追加してチェックできる。
  • サービスサイトにログインし、QiitaのAPI連携を承認することで自動的にQiitaの記事URLを取ってくるようになる。
  • WEB PUSH通知を許可しておくとブックマーク数が増えた時にPUSH通知してくれる。PCのChromeで許可したらPCのChromeを起動している時に通知が来るし、スマホのChromeで許可したら他のアプリと同様ChromeアプリからPUSH通知が来る。(端末毎に許可が必要)

Qiitaの記事取得

APIのマニュアル通り。

    url = "https://qiita.com/api/v2/authenticated_user/items?page=#{page}&per_page=100"
    response = get_response!(token, url)
    link = get_header(response.headers, "Link")
    last_page = get_last_page(link)
    result = Poison.decode!(response.body)
  defp get_last_page(raw) do
    String.split(raw, ",")
    |> Enum.map(fn row ->
      Regex.named_captures(~r/page=(?<page>\d+)[^"]+"(?<rel>first|last|prev|next)/, row)
    end)
    |> Enum.find(fn row -> row["rel"] == "last" end)
    |> Map.get("page")
  end

  defp get_response!(token, url) do
    headers = [
      {"Authorization", "Bearer #{token}"},
      {"Content-Type", "application/json"}
    ]

    HTTPoison.get!(url, headers)
  end

はてなブックマーク件数取得

APIのマニュアル通り。

  def get_entries_counts(urls) do
    try do
      query =
        urls
        |> Enum.map(fn url -> "url=" <> url end)
        |> Enum.join("&")

      URI.encode("http://api.b.st-hatena.com/entry.counts?" <> query)
      |> HTTPoison.get!()
      |> Map.get(:body)
      |> Poison.decode!()
    rescue
      error ->
        Logger.error(error)
        {}
    end
  end

チェック処理

アプリケーションにworkerをぶら下げてGenServerで回している。サーバーを起動すると勝手に起動するので便利。

application.ex
    children = [
      # Start the Ecto repository
      supervisor(BookmarkChecker.Repo, []),
      # Start the endpoint when the application starts
      supervisor(BookmarkCheckerWeb.Endpoint, []),
      # Start your own worker by calling: BookmarkChecker.Worker.start_link(arg1, arg2, arg3)
      worker(BookmarkChecker.Checker.Worker, [])
    ]

多分ループにすると処理が長くなったり依存しあったりしてメンテナンス不能に陥ると思うので、全ての処理はactionにしてそれを呼び出してもらう形にした。

詳しくは知らないので違うかもしれないが何かで落ちた時に、最後のstateで復活させてくれると嬉しいな…と。まあ何にしろバックグラウンドで自動で動く処理なので、ログを適宜残して全てエラー処理を入れている(抜けてなければ)。

とはいえAPI連携のため実際全部テストできるわけではないので量が増えたら何かしらは問題は起こるのではないかと思う。

  @actions [:start, :qiita, :web, :check]

  def start_link do
    GenServer.start_link(__MODULE__, %{
      action: :start,
      user: nil,
      page: 1,
      last_page: 1,
      bookmarks: [],
      sleep_time: nil,
      next_action: nil
    })
  end

  def init(state) do
    schedule_work(0)
    {:ok, state}
  end

  # アプリケーションから呼ばれるとこ
  def handle_info(:work, state) do
    state = %{state | sleep_time: @base_sleep}
    state = do_action(state)
    schedule_work(state.sleep_time)
    {:noreply, state}
  end

  # 各アクション(省略)
  defp action(:start, state) do
  defp action(:qiita, state) do
  defp action(:web, state) do
  defp action(:check, state) do

  defp schedule_work(sleep_time) do
    sleep_time = if sleep_time, do: sleep_time, else: @base_sleep
    Process.send_after(self(), :work, sleep_time)
  end

PUSH通知

FCMというのを使っている。

  def fcm_send(user, title, body) do
    data = %{
      notification: %{
        title: title,
        body: body,
        click_action: System.get_env("FCM_CLICK_ACTION")
      },
      to: user.fcm_notification_key
    }

    case Poison.encode(data) do
      {:ok, json} ->
        headers = get_headers()
        HTTPoison.post("https://fcm.googleapis.com/fcm/send", json, headers)

      error ->
        error
    end
  end

一人のユーザーが複数の端末の通知を設定する可能性があるので、マニュアルにあるとおり、ユーザー毎にnotificationを作ってそれに通知リクエストを送っている。

  def create_notification!(user) do
    fcm_tokens = Enum.map(user.fcm_tokens, fn fcm_token -> fcm_token.token end)
    headers = get_headers_with_sender_id()

    data = %{
      operation: "create",
      notification_key_name: Integer.to_string(user.id, 10),
      registration_ids: fcm_tokens
    }

    json = Poison.encode!(data)
    response = HTTPoison.post!("https://android.googleapis.com/gcm/notification", json, headers)
    body = Poison.decode!(response.body)
    body["notification_key"]
  end

実際にはaddやremoveもある。ちなみにマニュアルには書いてなさそうだが、notification_keyを紛失してしまった時はこれで取れるらしい。(一応作成前に取ってみてあったらそれを使うようにしている)

  def get_notification_key(user) do
    headers = get_headers_with_sender_id()
    user_id = Integer.to_string(user.id, 10)
    url = "https://android.googleapis.com/gcm/notification?notification_key_name=#{user_id}"

    try do
      response = HTTPoison.get!(url, headers)
      json = Poison.decode!(response.body)
      Map.get(json, "notification_key")
    rescue
      error ->
        Logger.error(error)
        nil
    end
  end

以上

もしQiitaを使ってる方がいれば是非お試し下さい。

はてなブックマーク増加チェッカー