経緯
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で回している。サーバーを起動すると勝手に起動するので便利。
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を使ってる方がいれば是非お試し下さい。