aboutsummaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/mix/tasks/pleroma/instance.ex2
-rw-r--r--lib/mix/tasks/pleroma/sample_config.eex2
-rw-r--r--lib/pleroma/application.ex22
-rw-r--r--lib/pleroma/digest_email_worker.ex45
-rw-r--r--lib/pleroma/emails/user_email.ex59
-rw-r--r--lib/pleroma/jwt.ex9
-rw-r--r--lib/pleroma/quantum_scheduler.ex4
-rw-r--r--lib/pleroma/user.ex36
-rw-r--r--lib/pleroma/web/mailer/subscription_controller.ex18
-rw-r--r--lib/pleroma/web/router.ex2
-rw-r--r--lib/pleroma/web/templates/email/digest.html.eex20
-rw-r--r--lib/pleroma/web/templates/layout/email.html.eex10
-rw-r--r--lib/pleroma/web/templates/mailer/subscription/unsubscribe_failure.html.eex1
-rw-r--r--lib/pleroma/web/templates/mailer/subscription/unsubscribe_success.html.eex1
-rw-r--r--lib/pleroma/web/views/email_view.ex5
-rw-r--r--lib/pleroma/web/views/mailer/subscription_view.ex3
16 files changed, 236 insertions, 3 deletions
diff --git a/lib/mix/tasks/pleroma/instance.ex b/lib/mix/tasks/pleroma/instance.ex
index 6cee8d630..d276df93a 100644
--- a/lib/mix/tasks/pleroma/instance.ex
+++ b/lib/mix/tasks/pleroma/instance.ex
@@ -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)
diff --git a/lib/mix/tasks/pleroma/sample_config.eex b/lib/mix/tasks/pleroma/sample_config.eex
index 52bd57cb7..ec7d8821e 100644
--- a/lib/mix/tasks/pleroma/sample_config.eex
+++ b/lib/mix/tasks/pleroma/sample_config.eex
@@ -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 %>"
diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex
index eeb415084..76f8d9bcd 100644
--- a/lib/pleroma/application.ex
+++ b/lib/pleroma/application.ex
@@ -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
diff --git a/lib/pleroma/digest_email_worker.ex b/lib/pleroma/digest_email_worker.ex
new file mode 100644
index 000000000..fa6067a03
--- /dev/null
+++ b/lib/pleroma/digest_email_worker.ex
@@ -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
diff --git a/lib/pleroma/emails/user_email.ex b/lib/pleroma/emails/user_email.ex
index 8502a0d0c..64f855112 100644
--- a/lib/pleroma/emails/user_email.ex
+++ b/lib/pleroma/emails/user_email.ex
@@ -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
diff --git a/lib/pleroma/jwt.ex b/lib/pleroma/jwt.ex
new file mode 100644
index 000000000..10102ff5d
--- /dev/null
+++ b/lib/pleroma/jwt.ex
@@ -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
diff --git a/lib/pleroma/quantum_scheduler.ex b/lib/pleroma/quantum_scheduler.ex
new file mode 100644
index 000000000..9a3df81f6
--- /dev/null
+++ b/lib/pleroma/quantum_scheduler.ex
@@ -0,0 +1,4 @@
+defmodule Pleroma.QuantumScheduler do
+ use Quantum.Scheduler,
+ otp_app: :pleroma
+end
diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex
index 7053dfaf3..2509d2366 100644
--- a/lib/pleroma/user.ex
+++ b/lib/pleroma/user.ex
@@ -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
diff --git a/lib/pleroma/web/mailer/subscription_controller.ex b/lib/pleroma/web/mailer/subscription_controller.ex
new file mode 100644
index 000000000..2334ebacb
--- /dev/null
+++ b/lib/pleroma/web/mailer/subscription_controller.ex
@@ -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
diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex
index 8b665d61b..09e51e602 100644
--- a/lib/pleroma/web/router.ex
+++ b/lib/pleroma/web/router.ex
@@ -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
diff --git a/lib/pleroma/web/templates/email/digest.html.eex b/lib/pleroma/web/templates/email/digest.html.eex
new file mode 100644
index 000000000..93c9c884f
--- /dev/null
+++ b/lib/pleroma/web/templates/email/digest.html.eex
@@ -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> \ No newline at end of file
diff --git a/lib/pleroma/web/templates/layout/email.html.eex b/lib/pleroma/web/templates/layout/email.html.eex
new file mode 100644
index 000000000..f6dcd7f0f
--- /dev/null
+++ b/lib/pleroma/web/templates/layout/email.html.eex
@@ -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> \ No newline at end of file
diff --git a/lib/pleroma/web/templates/mailer/subscription/unsubscribe_failure.html.eex b/lib/pleroma/web/templates/mailer/subscription/unsubscribe_failure.html.eex
new file mode 100644
index 000000000..7b476f02d
--- /dev/null
+++ b/lib/pleroma/web/templates/mailer/subscription/unsubscribe_failure.html.eex
@@ -0,0 +1 @@
+<h1>UNSUBSCRIBE FAILURE</h1>
diff --git a/lib/pleroma/web/templates/mailer/subscription/unsubscribe_success.html.eex b/lib/pleroma/web/templates/mailer/subscription/unsubscribe_success.html.eex
new file mode 100644
index 000000000..6dfa2c185
--- /dev/null
+++ b/lib/pleroma/web/templates/mailer/subscription/unsubscribe_success.html.eex
@@ -0,0 +1 @@
+<h1>UNSUBSCRIBE SUCCESSFUL</h1>
diff --git a/lib/pleroma/web/views/email_view.ex b/lib/pleroma/web/views/email_view.ex
new file mode 100644
index 000000000..b63eb162c
--- /dev/null
+++ b/lib/pleroma/web/views/email_view.ex
@@ -0,0 +1,5 @@
+defmodule Pleroma.Web.EmailView do
+ use Pleroma.Web, :view
+ import Phoenix.HTML
+ import Phoenix.HTML.Link
+end
diff --git a/lib/pleroma/web/views/mailer/subscription_view.ex b/lib/pleroma/web/views/mailer/subscription_view.ex
new file mode 100644
index 000000000..fc3d20816
--- /dev/null
+++ b/lib/pleroma/web/views/mailer/subscription_view.ex
@@ -0,0 +1,3 @@
+defmodule Pleroma.Web.Mailer.SubscriptionView do
+ use Pleroma.Web, :view
+end