Merge branch 'develop' into feature/compat/push-subscriptions
authorEgor Kislitsyn <egor@kislitsyn.com>
Thu, 6 Dec 2018 13:15:16 +0000 (20:15 +0700)
committerEgor Kislitsyn <egor@kislitsyn.com>
Thu, 6 Dec 2018 13:15:16 +0000 (20:15 +0700)
# Conflicts:
# lib/pleroma/application.ex
# lib/pleroma/plugs/oauth_plug.ex

14 files changed:
lib/mix/tasks/generate_config.ex
lib/mix/tasks/sample_config.eex
lib/pleroma/application.ex
lib/pleroma/notification.ex
lib/pleroma/plugs/oauth_plug.ex
lib/pleroma/web/mastodon_api/mastodon_api_controller.ex
lib/pleroma/web/mastodon_api/views/push_subscription_view.ex [new file with mode: 0644]
lib/pleroma/web/push/push.ex [new file with mode: 0644]
lib/pleroma/web/push/subscription.ex [new file with mode: 0644]
lib/pleroma/web/router.ex
lib/pleroma/web/twitter_api/controllers/util_controller.ex
mix.exs
mix.lock
priv/repo/migrations/20180918182427_create_push_subscriptions.exs [new file with mode: 0644]

index e3cbbf131ad688cc33ac62b39206e482f14ba9cf..be085d187cb6c356f7bbe15047e289a6b4b7662f 100644 (file)
@@ -22,6 +22,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",
@@ -29,7 +31,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 462c3463645c98a247d5a6ec3d0d1783150cd09d..47b6be729f16c4a99789bba85d7cd36c80edf30f 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 %>"
+
 # Enable Strict-Transport-Security once SSL is working:
 # config :pleroma, :http_security,
 #   sts: true
@@ -50,9 +56,9 @@ config :pleroma, Pleroma.Repo,
 
 
 # Configure Openstack Swift support if desired.
-# 
-# Many openstack deployments are different, so config is left very open with 
-# no assumptions made on which provider you're using. This should allow very 
+#
+# Many openstack deployments are different, so config is left very open with
+# no assumptions made on which provider you're using. This should allow very
 # wide support without needing separate handlers for OVH, Rackspace, etc.
 #
 # config :pleroma, Pleroma.Uploaders.Swift,
index cc68d9669fe7a22cba9de9185e8d1b8f1f28d758..0b0ec01972a2cdb497b46e2bc49277b6b39030cb 100644 (file)
@@ -66,7 +66,8 @@ defmodule Pleroma.Application do
         ),
         worker(Pleroma.Web.Federator.RetryQueue, []),
         worker(Pleroma.Web.Federator, []),
-        worker(Pleroma.Stats, [])
+        worker(Pleroma.Stats, []),
+        worker(Pleroma.Web.Push, [])
       ] ++
         streamer_child() ++
         chat_child() ++
index a3aeb1221f3216d5d6206c0d1308dba95073a6db..a40b8f8c936a6844afba2773763a61864cc538ff 100644 (file)
@@ -110,6 +110,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 75f9209c20cedc32227b35835d42bccfa5e7ba20..8b99a74d1f49f2c0d197990c86634adb797d86e0 100644 (file)
@@ -17,7 +17,9 @@ defmodule Pleroma.Plugs.OAuthPlug do
   def call(conn, _) do
     with {:ok, token} <- fetch_token(conn),
          {:ok, user} <- fetch_user(token) do
-      assign(conn, :user, user)
+      conn
+      |> assign(:token, token)
+      |> assign(:user, user)
     else
       _ -> conn
     end
index eecfc742b2096cd3b0395684401cb6b5d2d41d04..dd6b0a3619231cec580cc56dfd1578204f26baa4 100644 (file)
@@ -1160,6 +1160,35 @@ 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)
+    view = PushSubscriptionView.render("push_subscription.json", subscription: subscription)
+    json(conn, view)
+  end
+
+  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)
+  end
+
+  def update_push_subscription(
+        %{assigns: %{user: user, token: token}} = conn,
+        params
+      ) do
+    {:ok, subscription} = Pleroma.Web.Push.Subscription.update(user, token, params)
+    view = PushSubscriptionView.render("push_subscription.json", subscription: subscription)
+    json(conn, view)
+  end
+
+  def delete_push_subscription(%{assigns: %{user: user, token: token}} = conn, _params) do
+    {:ok, _response} = Pleroma.Web.Push.Subscription.delete(user, token)
+    json(conn, %{})
+  end
+
   def errors(conn, _) do
     conn
     |> put_status(500)
diff --git a/lib/pleroma/web/mastodon_api/views/push_subscription_view.ex b/lib/pleroma/web/mastodon_api/views/push_subscription_view.ex
new file mode 100644 (file)
index 0000000..a910bb4
--- /dev/null
@@ -0,0 +1,14 @@
+defmodule Pleroma.Web.MastodonAPI.PushSubscriptionView do
+  use Pleroma.Web, :view
+  alias Pleroma.Web.MastodonAPI.PushSubscriptionView
+
+  def render("push_subscription.json", %{subscription: subscription}) do
+    %{
+      id: to_string(subscription.id),
+      endpoint: subscription.endpoint,
+      alerts: Map.get(subscription.data, "alerts"),
+      # TODO: generate VAPID server key
+      server_key: "N/A"
+    }
+  end
+end
diff --git a/lib/pleroma/web/push/push.ex b/lib/pleroma/web/push/push.ex
new file mode 100644 (file)
index 0000000..4ac3be8
--- /dev/null
@@ -0,0 +1,128 @@
+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.warn(
+          "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"
+        )
+
+        :ignore
+
+      _ ->
+        {:ok, %{}}
+    end
+  end
+
+  def send(notification) do
+    if Application.get_env(:web_push_encryption, :vapid_details) do
+      GenServer.cast(Pleroma.Web.Push, {:send, notification})
+    end
+  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
diff --git a/lib/pleroma/web/push/subscription.ex b/lib/pleroma/web/push/subscription.ex
new file mode 100644 (file)
index 0000000..cfab7a9
--- /dev/null
@@ -0,0 +1,66 @@
+defmodule Pleroma.Web.Push.Subscription do
+  use Ecto.Schema
+  import Ecto.Changeset
+  alias Pleroma.{Repo, User}
+  alias Pleroma.Web.OAuth.Token
+  alias Pleroma.Web.Push.Subscription
+
+  schema "push_subscriptions" do
+    belongs_to(:user, User)
+    belongs_to(:token, Token)
+    field(:endpoint, :string)
+    field(:key_p256dh, :string)
+    field(:key_auth, :string)
+    field(:data, :map, default: %{})
+
+    timestamps()
+  end
+
+  @supported_alert_types ~w[follow favourite mention reblog]
+
+  defp alerts(%{"data" => %{"alerts" => alerts}}) do
+    alerts = Map.take(alerts, @supported_alert_types)
+    %{"alerts" => alerts}
+  end
+
+  def create(
+        %User{} = user,
+        %Token{} = token,
+        %{
+          "subscription" => %{
+            "endpoint" => endpoint,
+            "keys" => %{"auth" => key_auth, "p256dh" => key_p256dh}
+          }
+        } = params
+      ) do
+    Repo.insert(%Subscription{
+      user_id: user.id,
+      token_id: token.id,
+      endpoint: endpoint,
+      key_auth: key_auth,
+      key_p256dh: key_p256dh,
+      data: alerts(params)
+    })
+  end
+
+  def get(%User{id: user_id}, %Token{id: token_id}) do
+    Repo.get_by(Subscription, user_id: user_id, token_id: token_id)
+  end
+
+  def update(user, token, params) do
+    get(user, token)
+    |> change(data: alerts(params))
+    |> Repo.update()
+  end
+
+  def delete(user, token) do
+    Repo.delete(get(user, token))
+  end
+
+  def delete_if_exists(user, token) do
+    case get(user, token) do
+      nil -> {:ok, nil}
+      sub -> Repo.delete(sub)
+    end
+  end
+end
index b7c79d2eb06d1668b8b2fa54937b64371e5853b4..75d965c6d4f5d9f2e5a87d5cdeaa7121ec536c0d 100644 (file)
@@ -198,6 +198,11 @@ defmodule Pleroma.Web.Router do
     put("/filters/:id", MastodonAPIController, :update_filter)
     delete("/filters/:id", MastodonAPIController, :delete_filter)
 
+    post("/push/subscription", MastodonAPIController, :create_push_subscription)
+    get("/push/subscription", MastodonAPIController, :get_push_subscription)
+    put("/push/subscription", MastodonAPIController, :update_push_subscription)
+    delete("/push/subscription", MastodonAPIController, :delete_push_subscription)
+
     get("/suggestions", MastodonAPIController, :suggestions)
 
     get("/endorsements", MastodonAPIController, :empty_array)
index b0ed8387e904c5e2083cac6accd3af4c60e6d089..09277901063161df6299720a7efbc4642be719fb 100644 (file)
@@ -157,13 +157,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 1a28b6710e67a161ba365ecb9f4c460d788be6e5..bd9bce7665c7b99fce4adf9cb98d109f1011bb47 100644 (file)
--- a/mix.exs
+++ b/mix.exs
@@ -68,7 +68,8 @@ defmodule Pleroma.Mixfile do
       {:crypt,
        git: "https://github.com/msantos/crypt", ref: "1f2b58927ab57e72910191a7ebaeff984382a1d3"},
       {:cors_plug, "~> 1.5"},
-      {:ex_doc, "> 0.18.3 and < 0.20.0", only: :dev, runtime: false}
+      {:ex_doc, "> 0.18.3 and < 0.20.0", only: :dev, runtime: false},
+      {:web_push_encryption, "~> 0.2.1"}
     ]
   end
 
index 4c70061d33e61d38ef61f98b63661c517954a96e..ff8e9fdcaabf50eeeefb593c9442e0030358b6b1 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"},
@@ -25,6 +26,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"},
   "makeup": {:hex, :makeup, "0.5.5", "9e08dfc45280c5684d771ad58159f718a7b5788596099bdfb0284597d368a882", [:mix], [{:nimble_parsec, "~> 0.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm"},
   "makeup_elixir": {:hex, :makeup_elixir, "0.10.0", "0f09c2ddf352887a956d84f8f7e702111122ca32fbbc84c2f0569b8b65cbf7fa", [:mix], [{:makeup, "~> 0.5.5", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm"},
   "meck": {:hex, :meck, "0.8.9", "64c5c0bd8bcca3a180b44196265c8ed7594e16bcc845d0698ec6b4e577f48188", [:rebar3], [], "hexpm"},
@@ -52,4 +54,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"},
 }
diff --git a/priv/repo/migrations/20180918182427_create_push_subscriptions.exs b/priv/repo/migrations/20180918182427_create_push_subscriptions.exs
new file mode 100644 (file)
index 0000000..0cc7afa
--- /dev/null
@@ -0,0 +1,18 @@
+defmodule Pleroma.Repo.Migrations.CreatePushSubscriptions do
+  use Ecto.Migration
+
+  def change do
+    create table("push_subscriptions") do
+      add :user_id, references("users", on_delete: :delete_all)
+      add :token_id, references("oauth_tokens", on_delete: :delete_all)
+      add :endpoint, :string
+      add :key_p256dh, :string
+      add :key_auth, :string
+      add :data, :map
+
+      timestamps()
+    end
+
+    create index("push_subscriptions", [:user_id, :token_id], unique: true)
+  end
+end