aboutsummaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/mix/tasks/pleroma/digest.ex33
-rw-r--r--lib/mix/tasks/pleroma/instance.ex2
-rw-r--r--lib/pleroma/application.ex17
-rw-r--r--lib/pleroma/digest_email_worker.ex35
-rw-r--r--lib/pleroma/emails/user_email.ex71
-rw-r--r--lib/pleroma/jwt.ex9
-rw-r--r--lib/pleroma/notification.ex28
-rw-r--r--lib/pleroma/user.ex75
-rw-r--r--lib/pleroma/user/info.ex25
-rw-r--r--lib/pleroma/web/mailer/subscription_controller.ex20
-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
17 files changed, 353 insertions, 4 deletions
diff --git a/lib/mix/tasks/pleroma/digest.ex b/lib/mix/tasks/pleroma/digest.ex
new file mode 100644
index 000000000..81c207e10
--- /dev/null
+++ b/lib/mix/tasks/pleroma/digest.ex
@@ -0,0 +1,33 @@
+defmodule Mix.Tasks.Pleroma.Digest do
+ use Mix.Task
+
+ @shortdoc "Manages digest emails"
+ @moduledoc """
+ Manages digest emails
+
+ ## Send digest email since given date (user registration date by default)
+ ignoring user activity status.
+
+ ``mix pleroma.digest test <nickname> <since_date>``
+
+ Example: ``mix pleroma.digest test donaldtheduck 2019-05-20``
+ """
+ def run(["test", nickname | opts]) do
+ Mix.Pleroma.start_pleroma()
+
+ user = Pleroma.User.get_by_nickname(nickname)
+
+ last_digest_emailed_at =
+ with [date] <- opts,
+ {:ok, datetime} <- Timex.parse(date, "{YYYY}-{0M}-{0D}") do
+ datetime
+ else
+ _ -> user.inserted_at
+ end
+
+ patched_user = %{user | last_digest_emailed_at: last_digest_emailed_at}
+
+ _user = Pleroma.DigestEmailWorker.perform(patched_user)
+ Mix.shell().info("Digest email have been sent to #{nickname} (#{user.email})")
+ end
+end
diff --git a/lib/mix/tasks/pleroma/instance.ex b/lib/mix/tasks/pleroma/instance.ex
index 9080adb52..b9b1991c2 100644
--- a/lib/mix/tasks/pleroma/instance.ex
+++ b/lib/mix/tasks/pleroma/instance.ex
@@ -183,6 +183,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)
template_dir = Application.app_dir(:pleroma, "priv") <> "/templates"
@@ -200,6 +201,7 @@ defmodule Mix.Tasks.Pleroma.Instance do
dbuser: dbuser,
dbpass: dbpass,
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/pleroma/application.ex b/lib/pleroma/application.ex
index 035331491..00b06f723 100644
--- a/lib/pleroma/application.ex
+++ b/lib/pleroma/application.ex
@@ -162,7 +162,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
@@ -227,4 +229,17 @@ 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] do
+ PleromaJobQueue.schedule(
+ digest_config[:schedule],
+ :digest_emails,
+ Pleroma.DigestEmailWorker
+ )
+ 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..18e67d39b
--- /dev/null
+++ b/lib/pleroma/digest_email_worker.ex
@@ -0,0 +1,35 @@
+defmodule Pleroma.DigestEmailWorker do
+ import Ecto.Query
+
+ @queue_name :digest_emails
+
+ def perform do
+ config = Pleroma.Config.get([: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(~s(? #> '{"email_notifications","digest"}' @> 'true'), u.info),
+ where: u.last_digest_emailed_at < datetime_add(^now, ^negative_interval, "day"),
+ select: u
+ )
+ |> Pleroma.Repo.all()
+ |> Enum.each(&PleromaJobQueue.enqueue(@queue_name, __MODULE__, [&1]))
+ end
+
+ @doc """
+ Send digest email to the given user.
+ Updates `last_digest_emailed_at` field for the user and returns the updated user.
+ """
+ @spec perform(Pleroma.User.t()) :: Pleroma.User.t()
+ def perform(user) do
+ with %Swoosh.Email{} = email <- Pleroma.Emails.UserEmail.digest_email(user) do
+ Pleroma.Emails.Mailer.deliver_async(email)
+ end
+
+ Pleroma.User.touch_last_digest_emailed_at(user)
+ end
+end
diff --git a/lib/pleroma/emails/user_email.ex b/lib/pleroma/emails/user_email.ex
index 934620765..49046bb8b 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
@@ -87,4 +87,73 @@ 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} = activity} = notification,
+ acc ->
+ new_mention = %{
+ data: notification,
+ object: Pleroma.Object.normalize(activity),
+ from: Pleroma.User.get_by_ap_id(actor)
+ }
+
+ %{acc | mentions: [new_mention | acc.mentions]}
+
+ %{activity: %{data: %{"type" => "Follow"}, actor: actor} = activity} = notification,
+ acc ->
+ new_follower = %{
+ data: notification,
+ object: Pleroma.Object.normalize(activity),
+ 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/notification.ex b/lib/pleroma/notification.ex
index d47229258..5d29af853 100644
--- a/lib/pleroma/notification.ex
+++ b/lib/pleroma/notification.ex
@@ -18,6 +18,8 @@ defmodule Pleroma.Notification do
import Ecto.Query
import Ecto.Changeset
+ @type t :: %__MODULE__{}
+
schema "notifications" do
field(:seen, :boolean, default: false)
belongs_to(:user, User, type: Pleroma.FlakeId)
@@ -31,7 +33,7 @@ defmodule Pleroma.Notification do
|> cast(attrs, [:seen])
end
- def for_user_query(user, opts) do
+ def for_user_query(user, opts \\ []) do
query =
Notification
|> where(user_id: ^user.id)
@@ -75,6 +77,25 @@ defmodule Pleroma.Notification do
|> Pagination.fetch_paginated(opts)
end
+ @doc """
+ Returns notifications for user received since given date.
+
+ ## Examples
+
+ iex> Pleroma.Notification.for_user_since(%Pleroma.User{}, ~N[2019-04-13 11:22:33])
+ [%Pleroma.Notification{}, %Pleroma.Notification{}]
+
+ iex> Pleroma.Notification.for_user_since(%Pleroma.User{}, ~N[2019-04-15 11:22:33])
+ []
+ """
+ @spec for_user_since(Pleroma.User.t(), NaiveDateTime.t()) :: [t()]
+ def for_user_since(user, date) do
+ from(n in for_user_query(user),
+ where: n.updated_at > ^date
+ )
+ |> Repo.all()
+ end
+
def set_read_up_to(%{id: user_id} = _user, id) do
query =
from(
@@ -82,7 +103,10 @@ defmodule Pleroma.Notification do
where: n.user_id == ^user_id,
where: n.id <= ^id,
update: [
- set: [seen: true]
+ set: [
+ seen: true,
+ updated_at: ^NaiveDateTime.utc_now()
+ ]
]
)
diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex
index 974f96852..7d18f099e 100644
--- a/lib/pleroma/user.ex
+++ b/lib/pleroma/user.ex
@@ -57,6 +57,7 @@ defmodule Pleroma.User do
field(:search_type, :integer, virtual: true)
field(:tags, {:array, :string}, default: [])
field(:last_refreshed_at, :naive_datetime_usec)
+ field(:last_digest_emailed_at, :naive_datetime)
has_many(:notifications, Notification)
has_many(:registrations, Registration)
embeds_one(:info, User.Info)
@@ -1419,6 +1420,80 @@ defmodule Pleroma.User do
target.ap_id not in user.info.muted_reblogs
end
+ @doc """
+ The function returns a query to get users with no activity for given interval of days.
+ Inactive users are those who didn't read any notification, or had any activity where
+ the user is the activity's actor, during `inactivity_threshold` days.
+ Deactivated users will not appear in this list.
+
+ ## Examples
+
+ iex> Pleroma.User.list_inactive_users()
+ %Ecto.Query{}
+ """
+ @spec list_inactive_users_query(integer()) :: Ecto.Query.t()
+ def list_inactive_users_query(inactivity_threshold \\ 7) do
+ negative_inactivity_threshold = -inactivity_threshold
+ now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
+ # Subqueries are not supported in `where` clauses, join gets too complicated.
+ has_read_notifications =
+ from(n in Pleroma.Notification,
+ where: n.seen == true,
+ group_by: n.id,
+ having: max(n.updated_at) > datetime_add(^now, ^negative_inactivity_threshold, "day"),
+ select: n.user_id
+ )
+ |> Pleroma.Repo.all()
+
+ from(u in Pleroma.User,
+ left_join: a in Pleroma.Activity,
+ on: u.ap_id == a.actor,
+ where: not is_nil(u.nickname),
+ where: fragment("not (?->'deactivated' @> 'true')", u.info),
+ where: u.id not in ^has_read_notifications,
+ group_by: u.id,
+ having:
+ max(a.inserted_at) < datetime_add(^now, ^negative_inactivity_threshold, "day") or
+ 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
+
@spec toggle_confirmation(User.t()) :: {:ok, User.t()} | {:error, Changeset.t()}
def toggle_confirmation(%User{} = user) do
need_confirmation? = !user.info.confirmation_pending
diff --git a/lib/pleroma/user/info.ex b/lib/pleroma/user/info.ex
index b03e705c3..22eb9a182 100644
--- a/lib/pleroma/user/info.ex
+++ b/lib/pleroma/user/info.ex
@@ -45,6 +45,7 @@ defmodule Pleroma.User.Info do
field(:hide_follows, :boolean, default: false)
field(:hide_favorites, :boolean, default: true)
field(:pinned_activities, {:array, :string}, default: [])
+ field(:email_notifications, :map, default: %{"digest" => false})
field(:mascot, :map, default: nil)
field(:emoji, {:array, :map}, default: [])
field(:pleroma_settings_store, :map, default: %{})
@@ -95,6 +96,30 @@ defmodule Pleroma.User.Info do
|> validate_required([:notification_settings])
end
+ @doc """
+ Update email notifications in the given User.Info struct.
+
+ Examples:
+
+ iex> update_email_notifications(%Pleroma.User.Info{email_notifications: %{"digest" => false}}, %{"digest" => true})
+ %Pleroma.User.Info{email_notifications: %{"digest" => true}}
+
+ """
+ @spec update_email_notifications(t(), map()) :: Ecto.Changeset.t()
+ def update_email_notifications(info, settings) do
+ email_notifications =
+ info.email_notifications
+ |> Map.merge(settings)
+ |> Map.take(["digest"])
+
+ params = %{email_notifications: email_notifications}
+ fields = [:email_notifications]
+
+ info
+ |> cast(params, fields)
+ |> validate_required(fields)
+ end
+
def add_to_note_count(info, number) do
set_note_count(info, info.note_count + number)
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..478a83518
--- /dev/null
+++ b/lib/pleroma/web/mailer/subscription_controller.ex
@@ -0,0 +1,20 @@
+defmodule Pleroma.Web.Mailer.SubscriptionController do
+ use Pleroma.Web, :controller
+
+ alias Pleroma.JWT
+ alias Pleroma.Repo
+ alias Pleroma.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 d475fc973..c8c1c22dd 100644
--- a/lib/pleroma/web/router.ex
+++ b/lib/pleroma/web/router.ex
@@ -608,6 +608,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
pipeline :activitypub 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..c9dd699fd
--- /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, object: object, from: from} <- @mentions do %>
+ <li><%= link from.nickname, to: mention.activity.actor %>: <%= raw 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