+defmodule Notifier.FBGraph.Worker do
+ use GenServer
+ require Logger
+
+ alias Notifier.SMTP
+
+ @graph_url "https://www.facebook.com/api/graphql"
+
+ def child_spec(args) do
+ [_id, name] = Keyword.get(args, :id)
+
+ %{
+ id: :"FBWorker-#{name}",
+ start: {__MODULE__, :start_link, [args]}
+ }
+ end
+
+ def start_link(args) do
+ GenServer.start_link(__MODULE__, args)
+ end
+
+ def init(id: [id, name], notifier: notifier, http: http, from: from, notify: notify) do
+ {:ok,
+ %{
+ id: id,
+ name: name,
+ notifier: notifier,
+ from: from,
+ http: http,
+ notify: notify,
+ last_post: %{id: nil}
+ }, {:continue, :get_latest_post}}
+ end
+
+ def handle_continue(:get_latest_post, %{http: http, id: id} = state) do
+ last_post =
+ :post
+ |> Finch.build(@graph_url, headers(), body(id))
+ |> Finch.request(http)
+ |> handle_response()
+ |> handle_post()
+ |> maybe_notify(state)
+
+ poll_interval = 60_000 * (15..25 |> Enum.random())
+ Logger.info("Will poll #{state.name} again in #{poll_interval}ms")
+ Process.send_after(self(), :get_posts, poll_interval)
+ {:noreply, %{state | last_post: last_post}}
+ end
+
+ def handle_info(:get_posts, state), do: {:noreply, state, {:continue, :get_latest_post}}
+
+ defp maybe_notify({:error, _}, %{last_post: last_post}), do: last_post
+
+ defp maybe_notify(%{id: id}, %{last_post: %{id: id} = last_post}),
+ do: last_post
+
+ defp maybe_notify(
+ %{message: message, images: images} = post,
+ %{notifier: notifier, from: from, notify: notify, name: name}
+ ) do
+ SMTP.send_html(
+ notifier,
+ from,
+ notify,
+ "FB post from #{name}",
+ create_notification(message, images)
+ )
+
+ Logger.info("Notification sent to: #{inspect(notify)} for #{name} post #{post.id}")
+ post
+ end
+
+ defp create_notification(message, images) do
+ imgs =
+ Enum.map(images, fn i ->
+ "<a href=\"#{get_in(i, ["photo_image", "uri"])}\">#{get_in(i, ["accessibility_caption"])}"
+ end)
+
+ "<p>#{message}</p>" <> Enum.join(imgs, " ")
+ end
+
+ defp handle_post([
+ %{"data" => %{"node" => %{"timeline_list_feed_units" => %{"edges" => [node | _edges]}}}}
+ | _
+ ]) do
+ id = get_in(node, ["node", "post_id"])
+ text = get_in(node, ["node", "comet_sections", "content", "story", "message", "text"])
+
+ images =
+ get_in(node, [
+ "node",
+ "comet_sections",
+ "content",
+ "story",
+ "attachments",
+ Access.all(),
+ "styles",
+ "attachment",
+ "media"
+ ])
+
+ %{id: id, message: text, images: images}
+ end
+
+ defp handle_post({:error, status} = error) do
+ Logger.error("FB Graph returned error: #{status}")
+ error
+ end
+
+ defp handle_response({:ok, %Finch.Response{status: 200, body: body}}) do
+ body
+ |> String.split("\r\n")
+ |> Enum.map(&Jason.decode!/1)
+ end
+
+ defp handle_response({:ok, %Finch.Response{status: status}}) do
+ {:error, status}
+ end
+
+ defp body(id) do
+ %{
+ doc_id: "27119308971007907",
+ variables:
+ Jason.encode!(%{
+ "feedLocation" => "TIMELINE",
+ "feedbackSource" => 0,
+ "omitPinnedPost" => true,
+ "id" => id,
+ "count" => 1
+ })
+ }
+ |> URI.encode_query(:www_form)
+ end
+
+ defp headers,
+ do: [
+ {"Content-Type", "application/x-www-form-urlencoded"},
+ {"User-Agent", "Mozilla/5.0 (X11; Linux x86_64; rv:149.0) Gecko/20100101 Firefox/149.0"},
+ {"DNT", "1"},
+ {"Accept", "*/*"},
+ {"Accept-Language", "en-US,en;q=0.9"},
+ {"Connection", "close"},
+ {"Host", "www.facebook.com"},
+ {"Sec-Fetch-Dest", "empty"},
+ {"Sec-Fetch-Mode", "cors"},
+ {"Sec-Fetch-Site", "same-origin"},
+ {"Sec-GPC", "1"}
+ ]
+end