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

### 経緯

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

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

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

[はてなブックマーク増加チェッカー](https://hatena.alphabrend.com)

### 仕様

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

### Qiitaの記事取得

APIのマニュアル通り。

```elixir
    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)
```

```elixir
  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のマニュアル通り。

```elixir
  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で回している。サーバーを起動すると勝手に起動するので便利。

```elixir: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連携のため実際全部テストできるわけではないので量が増えたら何かしらは問題は起こるのではないかと思う。

```elixir
  @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というのを使っている。

```elixir
  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を作ってそれに通知リクエストを送っている。

```elixir
  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を紛失してしまった時はこれで取れるらしい。（一応作成前に取ってみてあったらそれを使うようにしている）

```elixir
  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を使ってる方がいれば是非お試し下さい。

[はてなブックマーク増加チェッカー](https://hatena.alphabrend.com)