Digest emails
This commit is contained in:
parent
73407f4eea
commit
64a2c6a041
19 changed files with 243 additions and 4 deletions
|
|
@ -125,6 +125,7 @@ defmodule Mix.Tasks.Pleroma.Instance 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 @@ defmodule Mix.Tasks.Pleroma.Instance 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 @@ defmodule Pleroma.Application do
|
|||
id: :cachex_idem
|
||||
),
|
||||
worker(Pleroma.FlakeId, []),
|
||||
worker(Pleroma.ScheduledActivityWorker, [])
|
||||
worker(Pleroma.ScheduledActivityWorker, []),
|
||||
worker(Pleroma.QuantumScheduler, [])
|
||||
] ++
|
||||
hackney_pool_children() ++
|
||||
[
|
||||
|
|
@ -125,7 +126,9 @@ defmodule Pleroma.Application 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 @@ defmodule Pleroma.Application 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 @@ defmodule Pleroma.Emails.UserEmail 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 @@ defmodule Pleroma.User 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
|
||||
Loading…
Add table
Add a link
Reference in a new issue