Digest emails
This commit is contained in:
parent
73407f4eea
commit
64a2c6a041
19 changed files with 243 additions and 4 deletions
|
@ -468,6 +468,8 @@
|
|||
|
||||
config :pleroma, :email_notifications,
|
||||
digest: %{
|
||||
# Globally enable or disable digest emails
|
||||
active: true,
|
||||
# When to send digest email, in crontab format (https://en.wikipedia.org/wiki/Cron)
|
||||
# 0 0 * * 0 - once a week at midnight on Sunday morning
|
||||
schedule: "0 0 * * 0",
|
||||
|
|
|
@ -125,6 +125,7 @@ def run(["gen" | rest]) do
|
|||
)
|
||||
|
||||
secret = :crypto.strong_rand_bytes(64) |> Base.encode64() |> binary_part(0, 64)
|
||||
jwt_secret = :crypto.strong_rand_bytes(64) |> Base.encode64() |> binary_part(0, 64)
|
||||
signing_salt = :crypto.strong_rand_bytes(8) |> Base.encode64() |> binary_part(0, 8)
|
||||
{web_push_public_key, web_push_private_key} = :crypto.generate_key(:ecdh, :prime256v1)
|
||||
|
||||
|
@ -142,6 +143,7 @@ def run(["gen" | rest]) do
|
|||
dbpass: dbpass,
|
||||
version: Pleroma.Mixfile.project() |> Keyword.get(:version),
|
||||
secret: secret,
|
||||
jwt_secret: jwt_secret,
|
||||
signing_salt: signing_salt,
|
||||
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)
|
||||
|
|
|
@ -76,3 +76,5 @@ config :web_push_encryption, :vapid_details,
|
|||
# storage_url: "https://swift-endpoint.prodider.com/v1/AUTH_<tenant>/<container>",
|
||||
# object_url: "https://cdn-endpoint.provider.com/<container>"
|
||||
#
|
||||
|
||||
config :joken, default_signer: "<%= jwt_secret %>"
|
||||
|
|
|
@ -105,7 +105,8 @@ def start(_type, _args) do
|
|||
id: :cachex_idem
|
||||
),
|
||||
worker(Pleroma.FlakeId, []),
|
||||
worker(Pleroma.ScheduledActivityWorker, [])
|
||||
worker(Pleroma.ScheduledActivityWorker, []),
|
||||
worker(Pleroma.QuantumScheduler, [])
|
||||
] ++
|
||||
hackney_pool_children() ++
|
||||
[
|
||||
|
@ -125,7 +126,9 @@ def start(_type, _args) do
|
|||
# See http://elixir-lang.org/docs/stable/elixir/Supervisor.html
|
||||
# for other strategies and supported options
|
||||
opts = [strategy: :one_for_one, name: Pleroma.Supervisor]
|
||||
Supervisor.start_link(children, opts)
|
||||
result = Supervisor.start_link(children, opts)
|
||||
:ok = after_supervisor_start()
|
||||
result
|
||||
end
|
||||
|
||||
defp setup_instrumenters do
|
||||
|
@ -183,4 +186,19 @@ defp hackney_pool_children do
|
|||
:hackney_pool.child_spec(pool, options)
|
||||
end
|
||||
end
|
||||
|
||||
defp after_supervisor_start() do
|
||||
with digest_config <- Application.get_env(:pleroma, :email_notifications)[:digest],
|
||||
true <- digest_config[:active],
|
||||
%Crontab.CronExpression{} = schedule <-
|
||||
Crontab.CronExpression.Parser.parse!(digest_config[:schedule]) do
|
||||
Pleroma.QuantumScheduler.new_job()
|
||||
|> Quantum.Job.set_name(:digest_emails)
|
||||
|> Quantum.Job.set_schedule(schedule)
|
||||
|> Quantum.Job.set_task(&Pleroma.DigestEmailWorker.run/0)
|
||||
|> Pleroma.QuantumScheduler.add_job()
|
||||
end
|
||||
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
|
45
lib/pleroma/digest_email_worker.ex
Normal file
45
lib/pleroma/digest_email_worker.ex
Normal file
|
@ -0,0 +1,45 @@
|
|||
defmodule Pleroma.DigestEmailWorker do
|
||||
import Ecto.Query
|
||||
require Logger
|
||||
|
||||
# alias Pleroma.User
|
||||
|
||||
def run() do
|
||||
Logger.warn("Running digester")
|
||||
config = Application.get_env(:pleroma, :email_notifications)[:digest]
|
||||
negative_interval = -Map.fetch!(config, :interval)
|
||||
inactivity_threshold = Map.fetch!(config, :inactivity_threshold)
|
||||
inactive_users_query = Pleroma.User.list_inactive_users_query(inactivity_threshold)
|
||||
|
||||
now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
|
||||
|
||||
from(u in inactive_users_query,
|
||||
where: fragment("? #> '{\"email_notifications\",\"digest\"}' @> 'true'", u.info),
|
||||
where: u.last_digest_emailed_at < datetime_add(^now, ^negative_interval, "day"),
|
||||
select: u
|
||||
)
|
||||
|> Pleroma.Repo.all()
|
||||
|> run(:pre)
|
||||
end
|
||||
|
||||
defp run(v, :pre) do
|
||||
Logger.warn("Running for #{length(v)} users")
|
||||
run(v)
|
||||
end
|
||||
|
||||
defp run([]), do: :ok
|
||||
|
||||
defp run([user | users]) do
|
||||
with %Swoosh.Email{} = email <- Pleroma.Emails.UserEmail.digest_email(user) do
|
||||
Logger.warn("Sending to #{user.nickname}")
|
||||
Pleroma.Emails.Mailer.deliver_async(email)
|
||||
else
|
||||
_ ->
|
||||
Logger.warn("Skipping #{user.nickname}")
|
||||
end
|
||||
|
||||
Pleroma.User.touch_last_digest_emailed_at(user)
|
||||
|
||||
run(users)
|
||||
end
|
||||
end
|
|
@ -5,7 +5,7 @@
|
|||
defmodule Pleroma.Emails.UserEmail do
|
||||
@moduledoc "User emails"
|
||||
|
||||
import Swoosh.Email
|
||||
use Phoenix.Swoosh, view: Pleroma.Web.EmailView, layout: {Pleroma.Web.LayoutView, :email}
|
||||
|
||||
alias Pleroma.Web.Endpoint
|
||||
alias Pleroma.Web.Router
|
||||
|
@ -92,4 +92,61 @@ def account_confirmation_email(user) do
|
|||
|> subject("#{instance_name()} account confirmation")
|
||||
|> html_body(html_body)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Email used in digest email notifications
|
||||
Includes Mentions and New Followers data
|
||||
If there are no mentions (even when new followers exist), the function will return nil
|
||||
"""
|
||||
@spec digest_email(Pleroma.User.t()) :: Swoosh.Email.t() | nil
|
||||
def digest_email(user) do
|
||||
new_notifications =
|
||||
Pleroma.Notification.for_user_since(user, user.last_digest_emailed_at)
|
||||
|> Enum.reduce(%{followers: [], mentions: []}, fn
|
||||
%{activity: %{data: %{"type" => "Create"}, actor: actor}} = notification, acc ->
|
||||
new_mention = %{data: notification, from: Pleroma.User.get_by_ap_id(actor)}
|
||||
%{acc | mentions: [new_mention | acc.mentions]}
|
||||
|
||||
%{activity: %{data: %{"type" => "Follow"}, actor: actor}} = notification, acc ->
|
||||
new_follower = %{data: notification, from: Pleroma.User.get_by_ap_id(actor)}
|
||||
%{acc | followers: [new_follower | acc.followers]}
|
||||
|
||||
_, acc ->
|
||||
acc
|
||||
end)
|
||||
|
||||
with [_ | _] = mentions <- new_notifications.mentions do
|
||||
html_data = %{
|
||||
instance: instance_name(),
|
||||
user: user,
|
||||
mentions: mentions,
|
||||
followers: new_notifications.followers,
|
||||
unsubscribe_link: unsubscribe_url(user, "digest")
|
||||
}
|
||||
|
||||
new()
|
||||
|> to(recipient(user))
|
||||
|> from(sender())
|
||||
|> subject("Your digest from #{instance_name()}")
|
||||
|> render_body("digest.html", html_data)
|
||||
else
|
||||
_ ->
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Generate unsubscribe link for given user and notifications type.
|
||||
The link contains JWT token with the data, and subscription can be modified without
|
||||
authorization.
|
||||
"""
|
||||
@spec unsubscribe_url(Pleroma.User.t(), String.t()) :: String.t()
|
||||
def unsubscribe_url(user, notifications_type) do
|
||||
token =
|
||||
%{"sub" => user.id, "act" => %{"unsubscribe" => notifications_type}, "exp" => false}
|
||||
|> Pleroma.JWT.generate_and_sign!()
|
||||
|> Base.encode64()
|
||||
|
||||
Router.Helpers.subscription_url(Pleroma.Web.Endpoint, :unsubscribe, token)
|
||||
end
|
||||
end
|
||||
|
|
9
lib/pleroma/jwt.ex
Normal file
9
lib/pleroma/jwt.ex
Normal file
|
@ -0,0 +1,9 @@
|
|||
defmodule Pleroma.JWT do
|
||||
use Joken.Config
|
||||
|
||||
@impl true
|
||||
def token_config do
|
||||
default_claims(skip: [:aud])
|
||||
|> add_claim("aud", &Pleroma.Web.Endpoint.url/0, &(&1 == Pleroma.Web.Endpoint.url()))
|
||||
end
|
||||
end
|
4
lib/pleroma/quantum_scheduler.ex
Normal file
4
lib/pleroma/quantum_scheduler.ex
Normal file
|
@ -0,0 +1,4 @@
|
|||
defmodule Pleroma.QuantumScheduler do
|
||||
use Quantum.Scheduler,
|
||||
otp_app: :pleroma
|
||||
end
|
|
@ -1484,4 +1484,40 @@ def list_inactive_users_query(inactivity_threshold \\ 7) do
|
|||
is_nil(max(a.inserted_at))
|
||||
)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Enable or disable email notifications for user
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Pleroma.User.switch_email_notifications(Pleroma.User{info: %{email_notifications: %{"digest" => false}}}, "digest", true)
|
||||
Pleroma.User{info: %{email_notifications: %{"digest" => true}}}
|
||||
|
||||
iex> Pleroma.User.switch_email_notifications(Pleroma.User{info: %{email_notifications: %{"digest" => true}}}, "digest", false)
|
||||
Pleroma.User{info: %{email_notifications: %{"digest" => false}}}
|
||||
"""
|
||||
@spec switch_email_notifications(t(), String.t(), boolean()) ::
|
||||
{:ok, t()} | {:error, Ecto.Changeset.t()}
|
||||
def switch_email_notifications(user, type, status) do
|
||||
info = Pleroma.User.Info.update_email_notifications(user.info, %{type => status})
|
||||
|
||||
change(user)
|
||||
|> put_embed(:info, info)
|
||||
|> update_and_set_cache()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Set `last_digest_emailed_at` value for the user to current time
|
||||
"""
|
||||
@spec touch_last_digest_emailed_at(t()) :: t()
|
||||
def touch_last_digest_emailed_at(user) do
|
||||
now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
|
||||
|
||||
{:ok, updated_user} =
|
||||
user
|
||||
|> change(%{last_digest_emailed_at: now})
|
||||
|> update_and_set_cache()
|
||||
|
||||
updated_user
|
||||
end
|
||||
end
|
||||
|
|
18
lib/pleroma/web/mailer/subscription_controller.ex
Normal file
18
lib/pleroma/web/mailer/subscription_controller.ex
Normal file
|
@ -0,0 +1,18 @@
|
|||
defmodule Pleroma.Web.Mailer.SubscriptionController do
|
||||
use Pleroma.Web, :controller
|
||||
|
||||
alias Pleroma.{JWT, Repo, User}
|
||||
|
||||
def unsubscribe(conn, %{"token" => encoded_token}) do
|
||||
with {:ok, token} <- Base.decode64(encoded_token),
|
||||
{:ok, claims} <- JWT.verify_and_validate(token),
|
||||
%{"act" => %{"unsubscribe" => type}, "sub" => uid} <- claims,
|
||||
%User{} = user <- Repo.get(User, uid),
|
||||
{:ok, _user} <- User.switch_email_notifications(user, type, false) do
|
||||
render(conn, "unsubscribe_success.html", email: user.email)
|
||||
else
|
||||
_err ->
|
||||
render(conn, "unsubscribe_failure.html")
|
||||
end
|
||||
end
|
||||
end
|
|
@ -562,6 +562,8 @@ defmodule Pleroma.Web.Router do
|
|||
post("/push/hub/:nickname", Websub.WebsubController, :websub_subscription_request)
|
||||
get("/push/subscriptions/:id", Websub.WebsubController, :websub_subscription_confirmation)
|
||||
post("/push/subscriptions/:id", Websub.WebsubController, :websub_incoming)
|
||||
|
||||
get("/mailer/unsubscribe/:token", Mailer.SubscriptionController, :unsubscribe)
|
||||
end
|
||||
|
||||
scope "/", Pleroma.Web do
|
||||
|
|
20
lib/pleroma/web/templates/email/digest.html.eex
Normal file
20
lib/pleroma/web/templates/email/digest.html.eex
Normal file
|
@ -0,0 +1,20 @@
|
|||
<h1>Hey <%= @user.nickname %>, here is what you've missed!</h1>
|
||||
|
||||
<h2>New Mentions:</h2>
|
||||
<ul>
|
||||
<%= for %{data: mention, from: from} <- @mentions do %>
|
||||
<li><%= link from.nickname, to: mention.activity.actor %>: <%= raw mention.activity.object.data["content"] %></li>
|
||||
<% end %>
|
||||
</ul>
|
||||
|
||||
<%= if @followers != [] do %>
|
||||
<h2><%= length(@followers) %> New Followers:</h2>
|
||||
<ul>
|
||||
<%= for %{data: follow, from: from} <- @followers do %>
|
||||
<li><%= link from.nickname, to: follow.activity.actor %></li>
|
||||
<% end %>
|
||||
</ul>
|
||||
<% end %>
|
||||
|
||||
<p>You have received this email because you have signed up to receive digest emails from <b><%= @instance %></b> Pleroma instance.</p>
|
||||
<p>The email address you are subscribed as is <%= @user.email %>. To unsubscribe, please go <%= link "here", to: @unsubscribe_link %>.</p>
|
10
lib/pleroma/web/templates/layout/email.html.eex
Normal file
10
lib/pleroma/web/templates/layout/email.html.eex
Normal file
|
@ -0,0 +1,10 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title><%= @email.subject %></title>
|
||||
</head>
|
||||
<body>
|
||||
<%= render @view_module, @view_template, assigns %>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1 @@
|
|||
<h1>UNSUBSCRIBE FAILURE</h1>
|
|
@ -0,0 +1 @@
|
|||
<h1>UNSUBSCRIBE SUCCESSFUL</h1>
|
5
lib/pleroma/web/views/email_view.ex
Normal file
5
lib/pleroma/web/views/email_view.ex
Normal file
|
@ -0,0 +1,5 @@
|
|||
defmodule Pleroma.Web.EmailView do
|
||||
use Pleroma.Web, :view
|
||||
import Phoenix.HTML
|
||||
import Phoenix.HTML.Link
|
||||
end
|
3
lib/pleroma/web/views/mailer/subscription_view.ex
Normal file
3
lib/pleroma/web/views/mailer/subscription_view.ex
Normal file
|
@ -0,0 +1,3 @@
|
|||
defmodule Pleroma.Web.Mailer.SubscriptionView do
|
||||
use Pleroma.Web, :view
|
||||
end
|
4
mix.exs
4
mix.exs
|
@ -93,6 +93,7 @@ defp deps do
|
|||
{:ex_doc, "~> 0.20.2", only: :dev, runtime: false},
|
||||
{:web_push_encryption, "~> 0.2.1"},
|
||||
{:swoosh, "~> 0.20"},
|
||||
{:phoenix_swoosh, "~> 0.2"},
|
||||
{:gen_smtp, "~> 0.13"},
|
||||
{:websocket_client, git: "https://github.com/jeremyong/websocket_client.git", only: :test},
|
||||
{:floki, "~> 0.20.0"},
|
||||
|
@ -111,7 +112,8 @@ defp deps do
|
|||
{:prometheus_process_collector, "~> 1.4"},
|
||||
{:recon, github: "ferd/recon", tag: "2.4.0"},
|
||||
{:quack, "~> 0.1.1"},
|
||||
{:quantum, "~> 2.3"}
|
||||
{:quantum, "~> 2.3"},
|
||||
{:joken, "~> 2.0"}
|
||||
] ++ oauth_deps
|
||||
end
|
||||
|
||||
|
|
2
mix.lock
2
mix.lock
|
@ -37,6 +37,7 @@
|
|||
"httpoison": {:hex, :httpoison, "1.2.0", "2702ed3da5fd7a8130fc34b11965c8cfa21ade2f232c00b42d96d4967c39a3a3", [:mix], [{:hackney, "~> 1.8", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"},
|
||||
"idna": {:hex, :idna, "6.0.0", "689c46cbcdf3524c44d5f3dde8001f364cd7608a99556d8fbd8239a5798d4c10", [:rebar3], [{:unicode_util_compat, "0.4.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm"},
|
||||
"jason": {:hex, :jason, "1.1.2", "b03dedea67a99223a2eaf9f1264ce37154564de899fd3d8b9a21b1a6fd64afe7", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm"},
|
||||
"joken": {:hex, :joken, "2.0.1", "ec9ab31bf660f343380da033b3316855197c8d4c6ef597fa3fcb451b326beb14", [:mix], [{:jose, "~> 1.9", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm"},
|
||||
"jose": {:hex, :jose, "1.9.0", "4167c5f6d06ffaebffd15cdb8da61a108445ef5e85ab8f5a7ad926fdf3ada154", [:mix, :rebar3], [{:base64url, "~> 0.0.1", [hex: :base64url, repo: "hexpm", optional: false]}], "hexpm"},
|
||||
"libring": {:hex, :libring, "1.4.0", "41246ba2f3fbc76b3971f6bce83119dfec1eee17e977a48d8a9cfaaf58c2a8d6", [:mix], [], "hexpm"},
|
||||
"makeup": {:hex, :makeup, "0.8.0", "9cf32aea71c7fe0a4b2e9246c2c4978f9070257e5c9ce6d4a28ec450a839b55f", [:mix], [{:nimble_parsec, "~> 0.5.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm"},
|
||||
|
@ -55,6 +56,7 @@
|
|||
"phoenix_ecto": {:hex, :phoenix_ecto, "4.0.0", "c43117a136e7399ea04ecaac73f8f23ee0ffe3e07acfcb8062fe5f4c9f0f6531", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.9", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},
|
||||
"phoenix_html": {:hex, :phoenix_html, "2.13.2", "f5d27c9b10ce881a60177d2b5227314fc60881e6b66b41dfe3349db6ed06cf57", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},
|
||||
"phoenix_pubsub": {:hex, :phoenix_pubsub, "1.1.2", "496c303bdf1b2e98a9d26e89af5bba3ab487ba3a3735f74bf1f4064d2a845a3e", [:mix], [], "hexpm"},
|
||||
"phoenix_swoosh": {:hex, :phoenix_swoosh, "0.2.0", "a7e0b32077cd6d2323ae15198839b05d9caddfa20663fd85787479e81f89520e", [:mix], [{:phoenix, "~> 1.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.2", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:swoosh, "~> 0.1", [hex: :swoosh, repo: "hexpm", optional: false]}], "hexpm"},
|
||||
"pleroma_job_queue": {:hex, :pleroma_job_queue, "0.2.0", "879e660aa1cebe8dc6f0aaaa6aa48b4875e89cd961d4a585fd128e0773b31a18", [:mix], [], "hexpm"},
|
||||
"plug": {:hex, :plug, "1.7.2", "d7b7db7fbd755e8283b6c0a50be71ec0a3d67d9213d74422d9372effc8e87fd1", [:mix], [{:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}], "hexpm"},
|
||||
"plug_cowboy": {:hex, :plug_cowboy, "2.0.2", "6055f16868cc4882b24b6e1d63d2bada94fb4978413377a3b32ac16c18dffba2", [:mix], [{:cowboy, "~> 2.5", [hex: :cowboy, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},
|
||||
|
|
Loading…
Reference in a new issue