Add web push support

This commit is contained in:
Egor Kislitsyn 2018-12-06 19:29:04 +07:00
parent d94ee5cd50
commit 04a48286e6
10 changed files with 155 additions and 7 deletions

View file

@ -14,6 +14,8 @@ def run(_) do
resultSql = EEx.eval_file("lib/mix/tasks/sample_psql.eex", dbpass: dbpass) 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 = result =
EEx.eval_file( EEx.eval_file(
"lib/mix/tasks/sample_config.eex", "lib/mix/tasks/sample_config.eex",
@ -21,7 +23,9 @@ def run(_) do
email: email, email: email,
name: name, name: name,
secret: secret, 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( IO.puts(

View file

@ -25,6 +25,12 @@ config :pleroma, Pleroma.Repo,
hostname: "localhost", hostname: "localhost",
pool_size: 10 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. # Configure S3 support if desired.
# The public S3 endpoint is different depending on region and provider, # The public S3 endpoint is different depending on region and provider,
# consult your S3 provider's documentation for details on what to use. # consult your S3 provider's documentation for details on what to use.

View file

@ -41,7 +41,8 @@ def start(_type, _args) do
), ),
worker(Pleroma.Web.Federator, []), worker(Pleroma.Web.Federator, []),
worker(Pleroma.Gopher.Server, []), worker(Pleroma.Gopher.Server, []),
worker(Pleroma.Stats, []) worker(Pleroma.Stats, []),
worker(Pleroma.Web.Push, [])
] ++ ] ++
if Mix.env() == :test, if Mix.env() == :test,
do: [], do: [],

View file

@ -96,6 +96,7 @@ def create_notification(%Activity{} = activity, %User{} = user) do
notification = %Notification{user_id: user.id, activity: activity} notification = %Notification{user_id: user.id, activity: activity}
{:ok, notification} = Repo.insert(notification) {:ok, notification} = Repo.insert(notification)
Pleroma.Web.Streamer.stream("user", notification) Pleroma.Web.Streamer.stream("user", notification)
Pleroma.Web.Push.send(notification)
notification notification
end end
end end

View file

@ -1138,6 +1138,8 @@ def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
json(conn, %{}) json(conn, %{})
end end
alias Pleroma.Web.MastodonAPI.PushSubscriptionView
def create_push_subscription(%{assigns: %{user: user, token: token}} = conn, params) do def create_push_subscription(%{assigns: %{user: user, token: token}} = conn, params) do
Pleroma.Web.Push.Subscription.delete_if_exists(user, token) Pleroma.Web.Push.Subscription.delete_if_exists(user, token)
{:ok, subscription} = Pleroma.Web.Push.Subscription.create(user, token, params) {:ok, subscription} = Pleroma.Web.Push.Subscription.create(user, token, params)
@ -1145,7 +1147,7 @@ def create_push_subscription(%{assigns: %{user: user, token: token}} = conn, par
json(conn, view) json(conn, view)
end 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) subscription = Pleroma.Web.Push.Subscription.get(user, token)
view = PushSubscriptionView.render("push_subscription.json", subscription: subscription) view = PushSubscriptionView.render("push_subscription.json", subscription: subscription)
json(conn, view) json(conn, view)
@ -1160,7 +1162,7 @@ def update_push_subscription(
json(conn, view) json(conn, view)
end 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) {:ok, _response} = Pleroma.Web.Push.Subscription.delete(user, token)
json(conn, %{}) json(conn, %{})
end end

View file

@ -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

View file

@ -1,6 +1,6 @@
defmodule Pleroma.Web.Push.Subscription do defmodule Pleroma.Web.Push.Subscription do
use Ecto.Schema use Ecto.Schema
import Ecto.{Changeset, Query} import Ecto.Changeset
alias Pleroma.{Repo, User} alias Pleroma.{Repo, User}
alias Pleroma.Web.OAuth.Token alias Pleroma.Web.OAuth.Token
alias Pleroma.Web.Push.Subscription alias Pleroma.Web.Push.Subscription

View file

@ -156,13 +156,17 @@ def config(conn, _params) do
|> send_resp(200, response) |> send_resp(200, response)
_ -> _ ->
vapid_public_key =
Keyword.get(Application.get_env(:web_push_encryption, :vapid_details), :public_key)
data = %{ data = %{
name: Keyword.get(@instance, :name), name: Keyword.get(@instance, :name),
description: Keyword.get(@instance, :description), description: Keyword.get(@instance, :description),
server: Web.base_url(), server: Web.base_url(),
textlimit: to_string(Keyword.get(@instance, :limit)), textlimit: to_string(Keyword.get(@instance, :limit)),
closed: if(Keyword.get(@instance, :registrations_open), do: "0", else: "1"), 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 = %{ pleroma_fe = %{

View file

@ -52,7 +52,8 @@ defp deps do
{:credo, "~> 0.9.3", only: [:dev, :test]}, {:credo, "~> 0.9.3", only: [:dev, :test]},
{:mock, "~> 0.3.1", only: :test}, {:mock, "~> 0.3.1", only: :test},
{:crypt, {:crypt,
git: "https://github.com/msantos/crypt", ref: "1f2b58927ab57e72910191a7ebaeff984382a1d3"} git: "https://github.com/msantos/crypt", ref: "1f2b58927ab57e72910191a7ebaeff984382a1d3"},
{:web_push_encryption, "~> 0.2.1"}
] ]
end end

View file

@ -1,4 +1,5 @@
%{ %{
"base64url": {:hex, :base64url, "0.0.1", "36a90125f5948e3afd7be97662a1504b934dd5dac78451ca6e9abf85a10286be", [:rebar], [], "hexpm"},
"bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "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"}, "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"}, "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"}, "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"}, "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"}, "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"}, "meck": {:hex, :meck, "0.8.9", "64c5c0bd8bcca3a180b44196265c8ed7594e16bcc845d0698ec6b4e577f48188", [:rebar3], [], "hexpm"},
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm"}, "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm"},
"mime": {:hex, :mime, "1.3.0", "5e8d45a39e95c650900d03f897fbf99ae04f60ab1daa4a34c7a20a5151b7a5fe", [:mix], [], "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"}, "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"}, "unicode_util_compat": {:hex, :unicode_util_compat, "0.3.1", "a1f612a7b512638634a603c8f401892afbf99b8ce93a45041f8aaca99cadb85e", [:rebar3], [], "hexpm"},
"unsafe": {:hex, :unsafe, "1.0.0", "7c21742cd05380c7875546b023481d3a26f52df8e5dfedcb9f958f322baae305", [:mix], [], "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"},
} }