Add web push support
authorEgor Kislitsyn <egor@kislitsyn.com>
Thu, 6 Dec 2018 12:29:04 +0000 (19:29 +0700)
committerEgor Kislitsyn <egor@kislitsyn.com>
Thu, 6 Dec 2018 12:29:04 +0000 (19:29 +0700)
lib/mix/tasks/generate_config.ex
lib/mix/tasks/sample_config.eex
lib/pleroma/application.ex
lib/pleroma/notification.ex
lib/pleroma/web/mastodon_api/mastodon_api_controller.ex
lib/pleroma/web/push/push.ex [new file with mode: 0644]
lib/pleroma/web/push/subscription.ex
lib/pleroma/web/twitter_api/controllers/util_controller.ex
mix.exs
mix.lock

index 70a11056114c23c2d43211d465f7bdc4f35265c2..58ce3113b147ec63d0d597dbf8faf81565fab5a6 100644 (file)
@@ -14,6 +14,8 @@ defmodule Mix.Tasks.GenerateConfig do
 
     resultSql = EEx.eval_file("lib/mix/tasks/sample_psql.eex", dbpass: dbpass)
 
+    {web_push_public_key, web_push_private_key} = :crypto.generate_key(:ecdh, :prime256v1)
+
     result =
       EEx.eval_file(
         "lib/mix/tasks/sample_config.eex",
@@ -21,7 +23,9 @@ defmodule Mix.Tasks.GenerateConfig do
         email: email,
         name: name,
         secret: secret,
-        dbpass: dbpass
+        dbpass: dbpass,
+        web_push_public_key: Base.url_encode64(web_push_public_key, padding: false),
+        web_push_private_key: Base.url_encode64(web_push_private_key, padding: false)
       )
 
     IO.puts(
index 3881ead26dacad84a5c5f8d50af6809203ab9d1c..f2272b10a76214022c18d1d035d8a80fc60178eb 100644 (file)
@@ -25,6 +25,12 @@ config :pleroma, Pleroma.Repo,
   hostname: "localhost",
   pool_size: 10
 
+# Configure web push notifications
+config :web_push_encryption, :vapid_details,
+  subject: "mailto:<%= email %>",
+  public_key: "<%= web_push_public_key %>",
+  private_key: "<%= web_push_private_key %>"
+
 # Configure S3 support if desired.
 # The public S3 endpoint is different depending on region and provider,
 # consult your S3 provider's documentation for details on what to use.
index a89728471f756eaa746fe0282f626bf3fc94845a..565e938fd521b5a6674dfa4822c4089f0afd3f1d 100644 (file)
@@ -41,7 +41,8 @@ defmodule Pleroma.Application do
         ),
         worker(Pleroma.Web.Federator, []),
         worker(Pleroma.Gopher.Server, []),
-        worker(Pleroma.Stats, [])
+        worker(Pleroma.Stats, []),
+        worker(Pleroma.Web.Push, [])
       ] ++
         if Mix.env() == :test,
           do: [],
index e0dcd98236eb8984a4b2b36ae13b15b65d4d97b4..6163413c81be56977457dcf0e7d003bd4e320fdb 100644 (file)
@@ -96,6 +96,7 @@ defmodule Pleroma.Notification do
       notification = %Notification{user_id: user.id, activity: activity}
       {:ok, notification} = Repo.insert(notification)
       Pleroma.Web.Streamer.stream("user", notification)
+      Pleroma.Web.Push.send(notification)
       notification
     end
   end
index 7f06ee607939a32b00763982ba0610b11a7091ba..de5b2696f7cce678a85828da5fa2641cf4e6db6f 100644 (file)
@@ -1138,6 +1138,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
     json(conn, %{})
   end
 
+  alias Pleroma.Web.MastodonAPI.PushSubscriptionView
+
   def create_push_subscription(%{assigns: %{user: user, token: token}} = conn, params) do
     Pleroma.Web.Push.Subscription.delete_if_exists(user, token)
     {:ok, subscription} = Pleroma.Web.Push.Subscription.create(user, token, params)
@@ -1145,7 +1147,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
     json(conn, view)
   end
 
-  def get_push_subscription(%{assigns: %{user: user, token: token}} = conn, params) do
+  def get_push_subscription(%{assigns: %{user: user, token: token}} = conn, _params) do
     subscription = Pleroma.Web.Push.Subscription.get(user, token)
     view = PushSubscriptionView.render("push_subscription.json", subscription: subscription)
     json(conn, view)
@@ -1160,7 +1162,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
     json(conn, view)
   end
 
-  def delete_push_subscription(%{assigns: %{user: user, token: token}} = conn, params) do
+  def delete_push_subscription(%{assigns: %{user: user, token: token}} = conn, _params) do
     {:ok, _response} = Pleroma.Web.Push.Subscription.delete(user, token)
     json(conn, %{})
   end
diff --git a/lib/pleroma/web/push/push.ex b/lib/pleroma/web/push/push.ex
new file mode 100644 (file)
index 0000000..d27750a
--- /dev/null
@@ -0,0 +1,126 @@
+defmodule Pleroma.Web.Push do
+  use GenServer
+
+  alias Pleroma.{Repo, User}
+  alias Pleroma.Web.Push.Subscription
+
+  require Logger
+  import Ecto.Query
+
+  @types ["Create", "Follow", "Announce", "Like"]
+
+  @gcm_api_key nil
+
+  def start_link() do
+    GenServer.start_link(__MODULE__, :ok, name: __MODULE__)
+  end
+
+  def init(:ok) do
+    case Application.get_env(:web_push_encryption, :vapid_details) do
+      nil ->
+        Logger.error(
+          "VAPID key pair is not found. Please, add VAPID configuration to config. Run `mix web_push.gen.keypair` mix task to create a key pair"
+        )
+
+        {:error, %{}}
+
+      _ ->
+        {:ok, %{}}
+    end
+  end
+
+  def send(notification) do
+    GenServer.cast(Pleroma.Web.Push, {:send, notification})
+  end
+
+  def handle_cast(
+        {:send, %{activity: %{data: %{"type" => type}}, user_id: user_id} = notification},
+        state
+      )
+      when type in @types do
+    actor = User.get_cached_by_ap_id(notification.activity.data["actor"])
+    body = notification |> format(actor) |> Jason.encode!()
+
+    Subscription
+    |> where(user_id: ^user_id)
+    |> Repo.all()
+    |> Enum.each(fn record ->
+      subscription = %{
+        keys: %{
+          p256dh: record.key_p256dh,
+          auth: record.key_auth
+        },
+        endpoint: record.endpoint
+      }
+
+      case WebPushEncryption.send_web_push(body, subscription, @gcm_api_key) do
+        {:ok, %{status_code: code}} when 400 <= code and code < 500 ->
+          Logger.debug("Removing subscription record")
+          Repo.delete!(record)
+          :ok
+
+        {:ok, %{status_code: code}} when 200 <= code and code < 300 ->
+          :ok
+
+        {:ok, %{status_code: code}} ->
+          Logger.error("Web Push Nonification failed with code: #{code}")
+          :error
+
+        data ->
+          Logger.error("Web Push Nonification failed with unknown error")
+          IO.inspect(data)
+          :error
+      end
+    end)
+
+    {:noreply, state}
+  end
+
+  def handle_cast({:send, _}, state) do
+    Logger.warn("Unknown notification type")
+    {:noreply, state}
+  end
+
+  def format(%{activity: %{data: %{"type" => "Create"}}}, actor) do
+    %{
+      title: "New Mention",
+      body: "@#{actor.nickname} has mentiond you",
+      icon: get_avatar_url(actor)
+    }
+  end
+
+  def format(%{activity: %{data: %{"type" => "Follow"}}}, actor) do
+    %{
+      title: "New Follower",
+      body: "@#{actor.nickname} has followed you",
+      icon: get_avatar_url(actor)
+    }
+  end
+
+  def format(%{activity: %{data: %{"type" => "Announce"}}}, actor) do
+    %{
+      title: "New Announce",
+      body: "@#{actor.nickname} has announced your post",
+      icon: get_avatar_url(actor)
+    }
+  end
+
+  def format(%{activity: %{data: %{"type" => "Like"}}}, actor) do
+    %{
+      title: "New Like",
+      body: "@#{actor.nickname} has liked your post",
+      icon: get_avatar_url(actor)
+    }
+  end
+
+  def get_avatar_url(%{avatar: %{"type" => "Image", "url" => urls}}) do
+    case List.first(urls) do
+      %{"href" => url} -> url
+      _ -> get_avatar_url(nil)
+    end
+  end
+
+  def get_avatar_url(_) do
+    Pleroma.Web.Endpoint.static_url() <> "/images/avi.png"
+  end
+end
index dc8fe9f33b7a9e29acbae5b4b1e08025f7639fde..cfab7a98e0a172867ced5205487bffff89a77823 100644 (file)
@@ -1,6 +1,6 @@
 defmodule Pleroma.Web.Push.Subscription do
   use Ecto.Schema
-  import Ecto.{Changeset, Query}
+  import Ecto.Changeset
   alias Pleroma.{Repo, User}
   alias Pleroma.Web.OAuth.Token
   alias Pleroma.Web.Push.Subscription
index 886b70f5f2b24d892b350fd4f42a66f3d822348b..f06020a3e02afd1460b6aeed30440cf78a109754 100644 (file)
@@ -156,13 +156,17 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do
         |> send_resp(200, response)
 
       _ ->
+        vapid_public_key =
+          Keyword.get(Application.get_env(:web_push_encryption, :vapid_details), :public_key)
+
         data = %{
           name: Keyword.get(@instance, :name),
           description: Keyword.get(@instance, :description),
           server: Web.base_url(),
           textlimit: to_string(Keyword.get(@instance, :limit)),
           closed: if(Keyword.get(@instance, :registrations_open), do: "0", else: "1"),
-          private: if(Keyword.get(@instance, :public, true), do: "0", else: "1")
+          private: if(Keyword.get(@instance, :public, true), do: "0", else: "1"),
+          vapidPublicKey: vapid_public_key
         }
 
         pleroma_fe = %{
diff --git a/mix.exs b/mix.exs
index 24c7108a0abd95119f4572d8063cf0e8a59d9911..2f2f1398ce8a1cce37b3bd40db9d56b47a36e120 100644 (file)
--- a/mix.exs
+++ b/mix.exs
@@ -52,7 +52,8 @@ defmodule Pleroma.Mixfile do
       {:credo, "~> 0.9.3", only: [:dev, :test]},
       {:mock, "~> 0.3.1", only: :test},
       {:crypt,
-       git: "https://github.com/msantos/crypt", ref: "1f2b58927ab57e72910191a7ebaeff984382a1d3"}
+       git: "https://github.com/msantos/crypt", ref: "1f2b58927ab57e72910191a7ebaeff984382a1d3"},
+      {:web_push_encryption, "~> 0.2.1"}
     ]
   end
 
index 1da8e7b0cfc21e2e64c775f3423dca4154854d45..4bc5d1bb2c226c6d89399d33341de7c7a573e700 100644 (file)
--- a/mix.lock
+++ b/mix.lock
@@ -1,4 +1,5 @@
 %{
+  "base64url": {:hex, :base64url, "0.0.1", "36a90125f5948e3afd7be97662a1504b934dd5dac78451ca6e9abf85a10286be", [:rebar], [], "hexpm"},
   "bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm"},
   "cachex": {:hex, :cachex, "3.0.2", "1351caa4e26e29f7d7ec1d29b53d6013f0447630bbf382b4fb5d5bad0209f203", [:mix], [{:eternal, "~> 1.2", [hex: :eternal, repo: "hexpm", optional: false]}, {:unsafe, "~> 1.0", [hex: :unsafe, repo: "hexpm", optional: false]}], "hexpm"},
   "calendar": {:hex, :calendar, "0.17.4", "22c5e8d98a4db9494396e5727108dffb820ee0d18fed4b0aa8ab76e4f5bc32f1", [:mix], [{:tzdata, "~> 0.5.8 or ~> 0.1.201603", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm"},
@@ -22,6 +23,7 @@
   "httpoison": {:hex, :httpoison, "1.2.0", "2702ed3da5fd7a8130fc34b11965c8cfa21ade2f232c00b42d96d4967c39a3a3", [:mix], [{:hackney, "~> 1.8", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"},
   "idna": {:hex, :idna, "5.1.2", "e21cb58a09f0228a9e0b95eaa1217f1bcfc31a1aaa6e1fdf2f53a33f7dbd9494", [:rebar3], [{:unicode_util_compat, "0.3.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm"},
   "jason": {:hex, :jason, "1.0.0", "0f7cfa9bdb23fed721ec05419bcee2b2c21a77e926bce0deda029b5adc716fe2", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm"},
+  "jose": {:hex, :jose, "1.8.4", "7946d1e5c03a76ac9ef42a6e6a20001d35987afd68c2107bcd8f01a84e75aa73", [:mix, :rebar3], [{:base64url, "~> 0.0.1", [hex: :base64url, repo: "hexpm", optional: false]}], "hexpm"},
   "meck": {:hex, :meck, "0.8.9", "64c5c0bd8bcca3a180b44196265c8ed7594e16bcc845d0698ec6b4e577f48188", [:rebar3], [], "hexpm"},
   "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm"},
   "mime": {:hex, :mime, "1.3.0", "5e8d45a39e95c650900d03f897fbf99ae04f60ab1daa4a34c7a20a5151b7a5fe", [:mix], [], "hexpm"},
@@ -45,4 +47,5 @@
   "tzdata": {:hex, :tzdata, "0.5.17", "50793e3d85af49736701da1a040c415c97dc1caf6464112fd9bd18f425d3053b", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"},
   "unicode_util_compat": {:hex, :unicode_util_compat, "0.3.1", "a1f612a7b512638634a603c8f401892afbf99b8ce93a45041f8aaca99cadb85e", [:rebar3], [], "hexpm"},
   "unsafe": {:hex, :unsafe, "1.0.0", "7c21742cd05380c7875546b023481d3a26f52df8e5dfedcb9f958f322baae305", [:mix], [], "hexpm"},
+  "web_push_encryption": {:hex, :web_push_encryption, "0.2.1", "d42cecf73420d9dc0053ba3299cc8c8d6ff2be2487d67ca2a57265868e4d9a98", [:mix], [{:httpoison, "~> 1.0", [hex: :httpoison, repo: "hexpm", optional: false]}, {:jose, "~> 1.8", [hex: :jose, repo: "hexpm", optional: false]}, {:poison, "~> 3.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"},
 }