batch mention email notifications in timeframe

This commit is contained in:
Alexander Strizhakov 2020-11-29 14:45:16 +03:00
parent b050adb5e2
commit 69f8f9446e
No known key found for this signature in database
GPG key ID: 022896A53AEF1381
28 changed files with 1165 additions and 58 deletions

View file

@ -85,6 +85,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Ability to define custom HTTP headers per each frontend
- MRF (`NoEmptyPolicy`): New MRF Policy which will deny empty statuses or statuses of only mentions from being created by local users
- New users will receive a simple email confirming their registration if no other emails will be dispatched. (e.g., Welcome, Confirmation, or Approval Required)
- Email with missed mentions in a specific period.
<details>
<summary>API Changes</summary>

View file

@ -563,10 +563,12 @@ config :pleroma, Oban,
remote_fetcher: 2,
attachments_cleanup: 1,
new_users_digest: 1,
mute_expire: 5
mute_expire: 5,
email_mentions: 1
],
plugins: [Oban.Plugins.Pruner],
crontab: [
{"*/15 * * * *", Pleroma.Workers.Cron.EmailMentionsWorker},
{"0 0 * * 0", Pleroma.Workers.Cron.DigestEmailsWorker},
{"0 0 * * *", Pleroma.Workers.Cron.NewUsersDigestWorker}
]
@ -851,6 +853,10 @@ config :pleroma, ConcurrentLimiter, [
{Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy, [max_running: 5, max_waiting: 5]}
]
config :pleroma, Pleroma.Workers.Cron.EmailMentionsWorker,
enabled: false,
timeframe: 30
# Import environment specific config. This must remain at the bottom
# of this file so it overrides the configuration defined above.
import_config "#{Mix.env()}.exs"

View file

@ -300,3 +300,36 @@
```sh
mix pleroma.user unconfirm_all
```
## Update email notifications settings for user
=== "OTP"
```sh
./bin/pleroma_ctl user email_notifications <nickname> [option ...]
```
=== "From Source"
```sh
mix pleroma.user email_notifications <nickname> [option ...]
```
### Options
- `--digest`/`--no-digest` - whether the user should receive digest emails
- `--notifications` - what types of email notifications user should receive (can be aliased with `-n`). To disable all types pass `off` value.
Example:
=== "OTP"
```sh
./bin/pleroma_ctl user email_notifications lain --digest -n mention
```
=== "From Source"
```sh
mix pleroma.user email_notifications lain --digest -n mention
```

View file

@ -715,6 +715,7 @@ Pleroma has these periodic job workers:
* `Pleroma.Workers.Cron.DigestEmailsWorker` - digest emails for users with new mentions and follows
* `Pleroma.Workers.Cron.NewUsersDigestWorker` - digest emails for admins with new registrations
* `Pleroma.Workers.Cron.EmailMentionsWorker` - email with missed mentions notifications in special timeframe
```elixir
config :pleroma, Oban,
@ -726,6 +727,7 @@ config :pleroma, Oban,
federator_outgoing: 50
],
crontab: [
{"*/15 * * * *", Pleroma.Workers.Cron.EmailMentionsWorker},
{"0 0 * * 0", Pleroma.Workers.Cron.DigestEmailsWorker},
{"0 0 * * *", Pleroma.Workers.Cron.NewUsersDigestWorker}
]
@ -1154,3 +1156,10 @@ Each job has these settings:
* `:max_running` - max concurrently runnings jobs
* `:max_waiting` - max waiting jobs
## Mention emails (Pleroma.Workers.Cron.EmailMentionsWorker)
The worker sends email notifications not read in a certain timeframe.
* `:enabled` - enables email notifications for missed mentions & chat mentions
* `:timeframe` - the period after which the sending of emails begins for missed mentions (in minutes)

View file

@ -107,6 +107,7 @@ Has these additional fields under the `pleroma` object:
- `notification_settings`: object, can be absent. See `/api/v1/pleroma/notification_settings` for the parameters/keys returned.
- `accepts_chat_messages`: boolean, but can be null if we don't have that information about a user
- `favicon`: nullable URL string, Favicon image of the user's instance
- `email_notifications`: map with settings for `digest` emails (boolean) and `notifications` setting (list with notification types).
### Source

View file

@ -21,7 +21,7 @@ defmodule Mix.Tasks.Pleroma.App do
scopes =
if opts[:scopes] do
String.split(opts[:scopes], ",")
String.split(opts[:scopes], ",", trim: true)
else
["read", "write", "follow", "push"]
end

View file

@ -433,6 +433,35 @@ defmodule Mix.Tasks.Pleroma.User do
|> Stream.run()
end
def run(["email_notifications", nickname | options]) do
start_pleroma()
{opts, _} =
OptionParser.parse!(options,
strict: [digest: :boolean, notifications: :string],
aliases: [n: :notifications]
)
params =
Map.new(opts, fn
{:digest, v} ->
{"digest", v}
{:notifications, v} ->
types = if v == "off", do: [], else: String.split(v, ",", trim: true)
{"notifications", types}
end)
with keys when keys != [] <- Map.keys(params),
%User{local: true} = user <- User.get_cached_by_nickname(nickname) do
{:ok, user} = User.update_email_notifications(user, params)
shell_info("Email notifications for user #{user.nickname} were successfully updated.")
else
[] -> shell_error("No changes passed")
_ -> shell_error("No local user #{nickname}")
end
end
defp set_moderator(user, value) do
{:ok, user} =
user

View file

@ -8,6 +8,7 @@ defmodule Pleroma.Emails.UserEmail do
use Phoenix.Swoosh, view: Pleroma.Web.EmailView, layout: {Pleroma.Web.LayoutView, :email}
alias Pleroma.Config
alias Pleroma.Notification
alias Pleroma.User
alias Pleroma.Web.Endpoint
alias Pleroma.Web.Router
@ -120,6 +121,27 @@ defmodule Pleroma.Emails.UserEmail do
|> html_body(html_body)
end
defp prepare_mention(%Notification{type: type} = notification, acc)
when type in ["mention", "pleroma:chat_mention"] do
object = Pleroma.Object.normalize(notification.activity, fetch: false)
if object do
object = update_in(object.data["content"], &format_links/1)
mention = %{
data: notification,
object: object,
from: User.get_by_ap_id(notification.activity.actor)
}
[mention | acc]
else
acc
end
end
defp prepare_mention(_, acc), do: acc
@doc """
Email used in digest email notifications
Includes Mentions and New Followers data
@ -127,25 +149,12 @@ defmodule Pleroma.Emails.UserEmail do
"""
@spec digest_email(User.t()) :: Swoosh.Email.t() | nil
def digest_email(user) do
notifications = Pleroma.Notification.for_user_since(user, user.last_digest_emailed_at)
notifications = Notification.for_user_since(user, user.last_digest_emailed_at)
mentions =
notifications
|> Enum.filter(&(&1.activity.data["type"] == "Create"))
|> Enum.map(fn notification ->
object = Pleroma.Object.normalize(notification.activity, fetch: false)
if not is_nil(object) do
object = update_in(object.data["content"], &format_links/1)
%{
data: notification,
object: object,
from: User.get_by_ap_id(notification.activity.actor)
}
end
end)
|> Enum.filter(& &1)
|> Enum.reduce([], &prepare_mention/2)
followers =
notifications
@ -165,7 +174,6 @@ defmodule Pleroma.Emails.UserEmail do
unless Enum.empty?(mentions) do
styling = Config.get([__MODULE__, :styling])
logo = Config.get([__MODULE__, :logo])
html_data = %{
instance: instance_name(),
@ -176,20 +184,15 @@ defmodule Pleroma.Emails.UserEmail do
styling: styling
}
logo_path =
if is_nil(logo) do
Path.join(:code.priv_dir(:pleroma), "static/static/logo.svg")
else
Path.join(Config.get([:instance, :static_dir]), logo)
end
{logo_path, logo} = logo_path()
new()
|> to(recipient(user))
|> from(sender())
|> subject("Your digest from #{instance_name()}")
|> put_layout(false)
|> render_body("digest.html", html_data)
|> attachment(Swoosh.Attachment.new(logo_path, filename: "logo.svg", type: :inline))
|> render_body("digest.html", Map.put(html_data, :logo, logo))
|> attachment(Swoosh.Attachment.new(logo_path, filename: logo, type: :inline))
end
end
@ -242,4 +245,42 @@ defmodule Pleroma.Emails.UserEmail do
|> subject("Your account archive is ready")
|> html_body(html_body)
end
@spec mentions_notification_email(User.t(), [Notification.t()]) :: Swoosh.Email.t()
def mentions_notification_email(user, mentions) do
html_data = %{
instance: instance_name(),
user: user,
mentions: Enum.reduce(mentions, [], &prepare_mention/2),
unsubscribe_link: unsubscribe_url(user, "mentions_email"),
styling: Config.get([__MODULE__, :styling])
}
now = NaiveDateTime.utc_now()
{logo_path, logo} = logo_path()
new()
|> to(recipient(user))
|> from(sender())
|> subject(
"[Pleroma] New mentions from #{instance_name()} for #{
Timex.format!(now, "{Mfull} {D}, {YYYY} at {h12}:{m} {AM}")
}"
)
|> put_layout(false)
|> render_body("mentions.html", Map.put(html_data, :logo, logo))
|> attachment(Swoosh.Attachment.new(logo_path, filename: logo, type: :inline))
end
defp logo_path do
logo_path =
if logo = Config.get([__MODULE__, :logo]) do
Path.join(Config.get([:instance, :static_dir]), logo)
else
Path.join(:code.priv_dir(:pleroma), "static/static/logo.svg")
end
{logo_path, Path.basename(logo_path)}
end
end

View file

@ -11,9 +11,11 @@ defmodule Pleroma.MigrationHelper.NotificationBackfill do
def fill_in_notification_types do
query =
from(n in Pleroma.Notification,
from(n in "notifications",
where: is_nil(n.type),
preload: :activity
join: a in "activities",
on: n.activity_id == a.id,
select: %{id: n.id, activity: %{id: a.id, data: a.data}}
)
query
@ -22,9 +24,8 @@ defmodule Pleroma.MigrationHelper.NotificationBackfill do
if notification.activity do
type = type_from_activity(notification.activity)
notification
|> Ecto.Changeset.change(%{type: type})
|> Repo.update()
from(n in "notifications", where: n.id == ^notification.id)
|> Repo.update_all(set: [type: type])
end
end)
end

View file

@ -37,6 +37,7 @@ defmodule Pleroma.Notification do
field(:type, :string)
belongs_to(:user, User, type: FlakeId.Ecto.CompatType)
belongs_to(:activity, Activity, type: FlakeId.Ecto.CompatType)
field(:notified_at, :naive_datetime)
timestamps()
end
@ -249,7 +250,7 @@ defmodule Pleroma.Notification do
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()]
@spec for_user_since(User.t(), NaiveDateTime.t()) :: [t()]
def for_user_since(user, date) do
from(n in for_user_query(user),
where: n.updated_at > ^date
@ -664,4 +665,48 @@ defmodule Pleroma.Notification do
)
|> Repo.update_all(set: [seen: true])
end
defp unread_mentions_in_timeframe_query(query \\ __MODULE__, args) do
types = args[:types] || ["mention", "pleroma:chat_mention"]
max_at = args[:max_at]
from(n in query,
where: n.seen == false,
where: is_nil(n.notified_at),
where: n.type in ^types,
where: n.inserted_at <= ^max_at
)
end
@spec users_ids_with_unread_mentions(NaiveDateTime.t()) :: [String.t()]
def users_ids_with_unread_mentions(max_at) do
from(n in unread_mentions_in_timeframe_query(%{max_at: max_at}),
join: u in assoc(n, :user),
where: not is_nil(u.email),
distinct: n.user_id,
select: n.user_id
)
|> Repo.all()
end
@spec for_user_unread_mentions(User.t(), NaiveDateTime.t()) :: [t()]
def for_user_unread_mentions(%User{} = user, max_at) do
args = %{
max_at: max_at,
types: user.email_notifications["notifications"]
}
user
|> for_user_query()
|> unread_mentions_in_timeframe_query(args)
|> Repo.all()
end
@spec update_notified_at([pos_integer()]) :: {non_neg_integer(), nil}
def update_notified_at(ids \\ []) do
from(n in __MODULE__,
where: n.id in ^ids
)
|> Repo.update_all(set: [notified_at: NaiveDateTime.utc_now()])
end
end

View file

@ -131,7 +131,11 @@ defmodule Pleroma.User do
field(:hide_followers, :boolean, default: false)
field(:hide_follows, :boolean, default: false)
field(:hide_favorites, :boolean, default: true)
field(:email_notifications, :map, default: %{"digest" => false})
field(:email_notifications, :map,
default: %{"digest" => false, "notifications" => ["mention", "pleroma:chat_mention"]}
)
field(:mascot, :map, default: nil)
field(:emoji, :map, default: %{})
field(:pleroma_settings_store, :map, default: %{})
@ -525,7 +529,8 @@ defmodule Pleroma.User do
:is_discoverable,
:actor_type,
:accepts_chat_messages,
:disclose_client
:disclose_client,
:email_notifications
]
)
|> unique_constraint(:nickname)
@ -2390,17 +2395,14 @@ defmodule Pleroma.User do
|> update_and_set_cache()
end
@spec update_email_notifications(t(), map()) :: {:ok, t()} | {:error, Ecto.Changeset.t()}
def update_email_notifications(user, settings) do
email_notifications =
user.email_notifications
|> Map.merge(settings)
|> Map.take(["digest"])
email_notifications = Map.merge(user.email_notifications, settings)
params = %{email_notifications: email_notifications}
fields = [:email_notifications]
user
|> cast(params, fields)
|> cast(%{email_notifications: email_notifications}, fields)
|> validate_required(fields)
|> update_and_set_cache()
end
@ -2431,8 +2433,8 @@ defmodule Pleroma.User do
end
end
@spec add_to_block(User.t(), User.t()) ::
{:ok, UserRelationship.t()} | {:ok, nil} | {:error, Ecto.Changeset.t()}
@spec remove_from_block(User.t(), User.t()) ::
{:ok, UserRelationship.t() | nil} | {:error, Ecto.Changeset.t()}
defp remove_from_block(%User{} = user, %User{} = blocked) do
with {:ok, relationship} <- UserRelationship.delete_block(user, blocked) do
@cachex.del(:user_cache, "blocked_users_ap_ids:#{user.ap_id}")

View file

@ -635,7 +635,8 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
description:
"Discovery (listing, indexing) of this account by external services (search bots etc.) is allowed."
},
actor_type: ActorType
actor_type: ActorType,
email_notifications: email_notifications()
},
example: %{
bot: false,
@ -760,6 +761,31 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
}
end
defp email_notifications do
%Schema{
title: "EmailNotificationsObject",
description: "User Email notification settings",
type: :object,
properties: %{
digest: %Schema{
allOf: [BooleanLike],
nullable: true,
description: "Whether the account receives digest email"
},
notifications: %Schema{
type: :array,
nullable: true,
description: "List of notification types to receive by Email",
items: %Schema{type: :string}
}
},
example: %{
"digest" => true,
"notifications" => ["mention", "pleroma:chat_mention"]
}
}
end
defp array_of_lists do
%Schema{
title: "ArrayOfLists",

View file

@ -213,6 +213,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
|> Maps.put_if_present(:is_locked, params[:locked])
# Note: param name is indeed :discoverable (not an error)
|> Maps.put_if_present(:is_discoverable, params[:discoverable])
|> Maps.put_if_present(:email_notifications, params[:email_notifications])
# What happens here:
#

View file

@ -279,7 +279,8 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
skip_thread_containment: user.skip_thread_containment,
background_image: image_url(user.background) |> MediaProxy.url(),
accepts_chat_messages: user.accepts_chat_messages,
favicon: favicon
favicon: favicon,
email_notifications: user.email_notifications
}
}
|> maybe_put_role(user, opts[:for])

View file

@ -126,7 +126,7 @@
<div align="center" class="img-container center"
style="padding-right: 0px;padding-left: 0px;">
<!--[if mso]><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr style="line-height:0px"><td style="padding-right: 0px;padding-left: 0px;" align="center"><![endif]--><img
align="center" alt="Image" border="0" class="center" src="cid:logo.svg"
align="center" alt="Image" border="0" class="center" src="cid:<%= @logo %>"
style="text-decoration: none; -ms-interpolation-mode: bicubic; border: 0; height: 80px; width: auto; max-height: 80px; display: block;"
title="Image" height="80" />
<!--[if mso]></td></tr></table><![endif]-->

View file

@ -0,0 +1,439 @@
<!DOCTYPE html
PUBLIC "-//W3C//DTD XHTML 1.0 Transitional //EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:o="urn:schemas-microsoft-com:office:office"
xmlns:v="urn:schemas-microsoft-com:vml">
<head>
<!--[if gte mso 9]><xml><o:OfficeDocumentSettings><o:AllowPNG/><o:PixelsPerInch>96</o:PixelsPerInch></o:OfficeDocumentSettings></xml><![endif]-->
<meta content="text/html; charset=utf-8" http-equiv="Content-Type" />
<meta content="width=device-width" name="viewport" />
<!--[if !mso]><!-->
<meta content="IE=edge" http-equiv="X-UA-Compatible" />
<!--<![endif]-->
<title><%= @email.subject %><</title>
<!--[if !mso]><!-->
<!--<![endif]-->
<style type="text/css">
body {
margin: 0;
padding: 0;
}
a {
color: <%= @styling.link_color %>;
text-decoration: none;
}
table,
td,
tr {
vertical-align: top;
border-collapse: collapse;
}
* {
line-height: inherit;
}
a[x-apple-data-detectors=true] {
color: inherit !important;
text-decoration: none !important;
}
</style>
<style id="media-query" type="text/css">
@media (max-width: 610px) {
.block-grid,
.col {
min-width: 320px !important;
max-width: 100% !important;
display: block !important;
}
.block-grid {
width: 100% !important;
}
.col {
width: 100% !important;
}
.col>div {
margin: 0 auto;
}
.no-stack .col {
min-width: 0 !important;
display: table-cell !important;
}
.no-stack.two-up .col {
width: 50% !important;
}
.no-stack .col.num4 {
width: 33% !important;
}
.no-stack .col.num8 {
width: 66% !important;
}
.no-stack .col.num4 {
width: 33% !important;
}
.no-stack .col.num3 {
width: 25% !important;
}
.no-stack .col.num6 {
width: 50% !important;
}
.no-stack .col.num9 {
width: 75% !important;
}
}
</style>
</head>
<body class="clean-body" style="margin: 0; padding: 0; -webkit-text-size-adjust: 100%; background-color: <%= @styling.background_color %>;">
<!--[if IE]><div class="ie-browser"><![endif]-->
<table bgcolor="<%= @styling.background_color %>" cellpadding="0" cellspacing="0" class="nl-container" role="presentation"
style="table-layout: fixed; vertical-align: top; min-width: 320px; Margin: 0 auto; border-spacing: 0; border-collapse: collapse; mso-table-lspace: 0pt; mso-table-rspace: 0pt; background-color: <%= @styling.background_color %>; width: 100%;"
valign="top" width="100%">
<tbody>
<tr style="vertical-align: top;" valign="top">
<td style="word-break: break-word; vertical-align: top;" valign="top">
<!--[if (mso)|(IE)]><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td align="center" style="background-color:<%= @styling.background_color %>"><![endif]-->
<div style="background-color:transparent;">
<div class="block-grid"
style="Margin: 0 auto; min-width: 320px; max-width: 590px; overflow-wrap: break-word; word-wrap: break-word; word-break: break-word; background-color: <%= @styling.content_background_color%>;">
<div style="border-collapse: collapse;display: table;width: 100%;background-color:<%= @styling.content_background_color%>;">
<!--[if (mso)|(IE)]><table width="100%" cellpadding="0" cellspacing="0" border="0" style="background-color:transparent;"><tr><td align="center"><table cellpadding="0" cellspacing="0" border="0" style="width:590px"><tr class="layout-full-width" style="background-color:<%= @styling.content_background_color%>"><![endif]-->
<!--[if (mso)|(IE)]><td align="center" width="590" style="background-color:<%= @styling.content_background_color%>;width:590px; border-top: 0px solid transparent; border-left: 0px solid transparent; border-bottom: 0px solid transparent; border-right: 0px solid transparent;" valign="top"><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="padding-right: 0px; padding-left: 0px; padding-top:5px; padding-bottom:5px;"><![endif]-->
<div class="col num12"
style="min-width: 320px; max-width: 590px; display: table-cell; vertical-align: top; width: 590px;">
<div style="width:100% !important;">
<!--[if (!mso)&(!IE)]><!-->
<div
style="border-top:0px solid transparent; border-left:0px solid transparent; border-bottom:0px solid transparent; border-right:0px solid transparent; padding-top:5px; padding-bottom:5px; padding-right: 0px; padding-left: 0px;">
<!--<![endif]-->
<div align="center" class="img-container center"
style="padding-right: 0px;padding-left: 0px;">
<!--[if mso]><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr style="line-height:0px"><td style="padding-right: 0px;padding-left: 0px;" align="center"><![endif]--><img
align="center" alt="Image" border="0" class="center" src="cid:<%= @logo %>"
style="text-decoration: none; -ms-interpolation-mode: bicubic; border: 0; height: 80px; width: auto; max-height: 80px; display: block;"
title="Image" height="80" />
<!--[if mso]></td></tr></table><![endif]-->
</div>
<!--[if (!mso)&(!IE)]><!-->
</div>
<!--<![endif]-->
</div>
</div>
<!--[if (mso)|(IE)]></td></tr></table><![endif]-->
<!--[if (mso)|(IE)]></td></tr></table></td></tr></table><![endif]-->
</div>
</div>
</div>
<div style="background-color:transparent;">
<div class="block-grid"
style="Margin: 0 auto; min-width: 320px; max-width: 590px; overflow-wrap: break-word; word-wrap: break-word; word-break: break-word; background-color: <%= @styling.content_background_color%>;">
<div style="border-collapse: collapse;display: table;width: 100%;background-color:<%= @styling.content_background_color%>;">
<!--[if (mso)|(IE)]><table width="100%" cellpadding="0" cellspacing="0" border="0" style="background-color:transparent;"><tr><td align="center"><table cellpadding="0" cellspacing="0" border="0" style="width:590px"><tr class="layout-full-width" style="background-color:<%= @styling.content_background_color%>"><![endif]-->
<!--[if (mso)|(IE)]><td align="center" width="590" style="background-color:<%= @styling.content_background_color%>;width:590px; border-top: 0px solid transparent; border-left: 0px solid transparent; border-bottom: 0px solid transparent; border-right: 0px solid transparent;" valign="top"><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="padding-right: 0px; padding-left: 0px; padding-top:5px; padding-bottom:5px;"><![endif]-->
<div class="col num12"
style="min-width: 320px; max-width: 590px; display: table-cell; vertical-align: top; width: 590px;">
<div style="width:100% !important;">
<!--[if (!mso)&(!IE)]><!-->
<div
style="border-top:0px solid transparent; border-left:0px solid transparent; border-bottom:0px solid transparent; border-right:0px solid transparent; padding-top:5px; padding-bottom:5px; padding-right: 0px; padding-left: 0px;">
<!--<![endif]-->
<!--[if mso]><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="padding-right: 10px; padding-left: 10px; padding-top: 10px; padding-bottom: 10px; font-family: Arial, sans-serif"><![endif]-->
<div
style="line-height:120%;padding-top:10px;padding-right:10px;padding-bottom:10px;padding-left:10px;">
<div
style="font-family: Arial, 'Helvetica Neue', Helvetica, sans-serif;line-height: 14px; color: <%= @styling.header_color %>;">
<p style="line-height: 36px; text-align: center; margin: 0;"><span
style="font-size: 30px; color: <%= @styling.header_color %>;">Hey <%= @user.nickname %>, here is what you've missed!</span></p>
</div>
</div>
<!--[if mso]></td></tr></table><![endif]-->
<!--[if (!mso)&(!IE)]><!-->
</div>
<!--<![endif]-->
</div>
</div>
<!--[if (mso)|(IE)]></td></tr></table><![endif]-->
<!--[if (mso)|(IE)]></td></tr></table></td></tr></table><![endif]-->
</div>
</div>
</div>
<div style="background-color:transparent;">
<div class="block-grid"
style="Margin: 0 auto; min-width: 320px; max-width: 590px; overflow-wrap: break-word; word-wrap: break-word; word-break: break-word; background-color: <%= @styling.content_background_color%>;">
<div style="border-collapse: collapse;display: table;width: 100%;background-color:<%= @styling.content_background_color%>;">
<!--[if (mso)|(IE)]><table width="100%" cellpadding="0" cellspacing="0" border="0" style="background-color:transparent;"><tr><td align="center"><table cellpadding="0" cellspacing="0" border="0" style="width:590px"><tr class="layout-full-width" style="background-color:<%= @styling.content_background_color%>"><![endif]-->
<!--[if (mso)|(IE)]><td align="center" width="590" style="background-color:<%= @styling.content_background_color%>;width:590px; border-top: 0px solid transparent; border-left: 0px solid transparent; border-bottom: 0px solid transparent; border-right: 0px solid transparent;" valign="top"><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="padding-right: 15px; padding-left: 15px; padding-top:5px; padding-bottom:5px;"><![endif]-->
<div class="col num12"
style="min-width: 320px; max-width: 590px; display: table-cell; vertical-align: top; width: 590px;">
<div style="width:100% !important;">
<!--[if (!mso)&(!IE)]><!-->
<div
style="border-top:0px solid transparent; border-left:0px solid transparent; border-bottom:0px solid transparent; border-right:0px solid transparent; padding-top:5px; padding-bottom:5px; padding-right: 15px; padding-left: 15px;">
<!--<![endif]-->
<table border="0" cellpadding="0" cellspacing="0" class="divider" role="presentation"
style="table-layout: fixed; vertical-align: top; border-spacing: 0; border-collapse: collapse; mso-table-lspace: 0pt; mso-table-rspace: 0pt; min-width: 100%; -ms-text-size-adjust: 100%; -webkit-text-size-adjust: 100%;"
valign="top" width="100%">
<tbody>
<tr style="vertical-align: top;" valign="top">
<td class="divider_inner"
style="word-break: break-word; vertical-align: top; min-width: 100%; -ms-text-size-adjust: 100%; -webkit-text-size-adjust: 100%; padding-top: 10px; padding-right: 10px; padding-bottom: 10px; padding-left: 10px;"
valign="top">
<table align="center" border="0" cellpadding="0" cellspacing="0" class="divider_content"
height="0" role="presentation"
style="table-layout: fixed; vertical-align: top; border-spacing: 0; border-collapse: collapse; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; border-top: 1px solid <%= @styling.text_color %>; height: 0px;"
valign="top" width="100%">
<tbody>
<tr style="vertical-align: top;" valign="top">
<td height="0"
style="word-break: break-word; vertical-align: top; -ms-text-size-adjust: 100%; -webkit-text-size-adjust: 100%;"
valign="top"><span></span></td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
<!--[if mso]><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="padding-right: 10px; padding-left: 10px; padding-top: 10px; padding-bottom: 10px; font-family: Arial, sans-serif"><![endif]-->
<div
style="color:<%= @styling.text_color %>;font-family:Arial, 'Helvetica Neue', Helvetica, sans-serif;line-height:120%;padding-top:10px;padding-right:10px;padding-bottom:10px;padding-left:10px;">
<p
style="font-size: 12px; line-height: 24px; text-align: center; color: <%= @styling.text_color %>; font-family: Arial, 'Helvetica Neue', Helvetica, sans-serif; margin: 0;">
<span style="font-size: 20px;">Mentions</span></p>
</div>
<!--[if mso]></td></tr></table><![endif]-->
<!--[if (!mso)&(!IE)]><!-->
</div>
<!--<![endif]-->
</div>
</div>
<!--[if (mso)|(IE)]></td></tr></table><![endif]-->
<!--[if (mso)|(IE)]></td></tr></table></td></tr></table><![endif]-->
</div>
</div>
</div>
<%= for %{data: mention, object: object, from: from} <- @mentions do %>
<%# mention START %>
<%# user card START %>
<div style="background-color:transparent;">
<div class="block-grid mixed-two-up no-stack"
style="Margin: 0 auto; min-width: 320px; max-width: 590px; overflow-wrap: break-word; word-wrap: break-word; word-break: break-word; background-color: <%= @styling.content_background_color%>;">
<div style="border-collapse: collapse;display: table;width: 100%;background-color:<%= @styling.content_background_color%>;">
<!--[if (mso)|(IE)]><table width="100%" cellpadding="0" cellspacing="0" border="0" style="background-color:transparent;"><tr><td align="center"><table cellpadding="0" cellspacing="0" border="0" style="width:590px"><tr class="layout-full-width" style="background-color:<%= @styling.content_background_color%>"><![endif]-->
<!--[if (mso)|(IE)]><td align="center" width="147" style="background-color:<%= @styling.content_background_color%>;width:76px; border-top: 0px solid transparent; border-left: 0px solid transparent; border-bottom: 0px solid transparent; border-right: 0px solid transparent;" valign="top"><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="padding-right: 0px; padding-left: 20px; padding-top:5px; padding-bottom:5px;"><![endif]-->
<div class="col num3"
style="display: table-cell; vertical-align: top; max-width: 320px; min-width: 76px; width: 76px;">
<div style="width:100% !important;">
<!--[if (!mso)&(!IE)]><!-->
<div
style="border-top:0px solid transparent; border-left:0px solid transparent; border-bottom:0px solid transparent; border-right:0px solid transparent; padding-top:5px; padding-bottom:5px; padding-right: 0px; padding-left: 20px;">
<!--<![endif]-->
<div align="left" class="img-container left "
style="padding-right: 0px;padding-left: 0px;">
<!--[if mso]><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr style="line-height:0px"><td style="padding-right: 0px;padding-left: 0px;" align="left"><![endif]--><img
alt="<%= from.name %>" border="0" class="left " src="<%= avatar_url(from) %>"
style="text-decoration: none; -ms-interpolation-mode: bicubic; border: 0; height: auto; width: 100%; max-width: 76px; display: block;"
title="<%= from.name %>" width="76" />
<!--[if mso]></td></tr></table><![endif]-->
</div>
<!--[if (!mso)&(!IE)]><!-->
</div>
<!--<![endif]-->
</div>
</div>
<!--[if (mso)|(IE)]></td></tr></table><![endif]-->
<!--[if (mso)|(IE)]></td><td align="center" width="442" style="background-color:<%= @styling.content_background_color%>;width:442px; border-top: 0px solid transparent; border-left: 0px solid transparent; border-bottom: 0px solid transparent; border-right: 0px solid transparent;" valign="top"><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="padding-right: 0px; padding-left: 0px; padding-top:5px; padding-bottom:5px;"><![endif]-->
<div class="col num9"
style="display: table-cell; vertical-align: top; min-width: 320px; max-width: 441px; width: 442px;">
<div style="width:100% !important;">
<!--[if (!mso)&(!IE)]><!-->
<div
style="border-top:0px solid transparent; border-left:0px solid transparent; border-bottom:0px solid transparent; border-right:0px solid transparent; padding-top:5px; padding-bottom:5px; padding-right: 0px; padding-left: 0px;">
<!--<![endif]-->
<!--[if mso]><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="padding-right: 10px; padding-left: 10px; padding-top: 10px; padding-bottom: 10px; font-family: Arial, sans-serif"><![endif]-->
<div
style="color:<%= @styling.text_color %>;font-family:Arial, 'Helvetica Neue', Helvetica, sans-serif;line-height:120%;padding-top:10px;padding-right:10px;padding-bottom:10px;padding-left:10px;">
<div
style="font-family: Arial, 'Helvetica Neue', Helvetica, sans-serif; font-size: 12px; line-height: 14px; color: <%= @styling.text_color %>;">
<p style="font-size: 14px; line-height: 19px; margin: 0;"><span
style="font-size: 16px; color: <%= @styling.text_color %>;"><%= from.name %></span></p>
<p style="font-size: 14px; line-height: 19px; margin: 0;"><span
style="font-size: 16px;"><%= link "@" <> from.nickname, style: "color: #{@styling.link_color};text-decoration: none;", to: mention.activity.actor %></span></p>
</div>
</div>
<!--[if mso]></td></tr></table><![endif]-->
<!--[if (!mso)&(!IE)]><!-->
</div>
<!--<![endif]-->
</div>
</div>
<!--[if (mso)|(IE)]></td></tr></table><![endif]-->
<!--[if (mso)|(IE)]></td></tr></table></td></tr></table><![endif]-->
</div>
</div>
</div>
<%# user card END %>
<div style="background-color:transparent;">
<div class="block-grid"
style="Margin: 0 auto; min-width: 320px; max-width: 590px; overflow-wrap: break-word; word-wrap: break-word; word-break: break-word; background-color: <%= @styling.content_background_color%>;">
<div style="border-collapse: collapse;display: table;width: 100%;background-color:<%= @styling.content_background_color%>;">
<!--[if (mso)|(IE)]><table width="100%" cellpadding="0" cellspacing="0" border="0" style="background-color:transparent;"><tr><td align="center"><table cellpadding="0" cellspacing="0" border="0" style="width:590px"><tr class="layout-full-width" style="background-color:<%= @styling.content_background_color%>"><![endif]-->
<!--[if (mso)|(IE)]><td align="center" width="590" style="background-color:<%= @styling.content_background_color%>;width:590px; border-top: 0px solid transparent; border-left: 0px solid transparent; border-bottom: 0px solid transparent; border-right: 0px solid transparent;" valign="top"><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="padding-right: 15px; padding-left: 15px; padding-top:5px; padding-bottom:5px;"><![endif]-->
<div class="col num12"
style="min-width: 320px; max-width: 590px; display: table-cell; vertical-align: top; width: 590px;">
<div style="width:100% !important;">
<!--[if (!mso)&(!IE)]><!-->
<div
style="border-top:0px solid transparent; border-left:0px solid transparent; border-bottom:0px solid transparent; border-right:0px solid transparent; padding-top:5px; padding-bottom:5px; padding-right: 15px; padding-left: 15px;">
<!--<![endif]-->
<!--[if mso]><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="padding-right: 10px; padding-left: 10px; padding-top: 10px; padding-bottom: 10px; font-family: Arial, sans-serif"><![endif]-->
<div
style="color:<%= @styling.text_color %>;font-family:Arial, 'Helvetica Neue', Helvetica, sans-serif;line-height:120%;padding-top:10px;padding-right:10px;padding-bottom:10px;padding-left:10px;">
<div
style="font-family: Arial, 'Helvetica Neue', Helvetica, sans-serif; font-size: 12px; line-height: 14px; color: <%= @styling.text_color %>;">
<span style="font-size: 16px; line-height: 19px;"><%= raw object.data["content"] %></span></div>
</div>
<!--[if mso]></td></tr></table><![endif]-->
<!--[if mso]><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="padding-right: 10px; padding-left: 15px; padding-top: 10px; padding-bottom: 10px; font-family: Arial, sans-serif"><![endif]-->
<div
style="color:<%= @styling.text_muted_color %>;font-family:Arial, 'Helvetica Neue', Helvetica, sans-serif;line-height:120%;padding-top:10px;padding-right:10px;padding-bottom:10px;padding-left:15px;">
<div
style="font-family: Arial, 'Helvetica Neue', Helvetica, sans-serif; font-size: 12px; line-height: 14px; color: <%= @styling.text_muted_color %>;">
<p style="font-size: 14px; line-height: 16px; margin: 0;"><%= format_date object.data["published"] %></p>
</div>
</div>
<!--[if mso]></td></tr></table><![endif]-->
<!--[if (!mso)&(!IE)]><!-->
</div>
<!--<![endif]-->
</div>
</div>
<!--[if (mso)|(IE)]></td></tr></table><![endif]-->
<!--[if (mso)|(IE)]></td></tr></table></td></tr></table><![endif]-->
</div>
</div>
</div>
<%# mention END %>
<% end %>
<%# divider start %>
<div style="background-color:transparent;">
<div class="block-grid"
style="Margin: 0 auto; min-width: 320px; max-width: 590px; overflow-wrap: break-word; word-wrap: break-word; word-break: break-word; background-color: <%= @styling.content_background_color%>;">
<div style="border-collapse: collapse;display: table;width: 100%;background-color:<%= @styling.content_background_color%>;">
<!--[if (mso)|(IE)]><table width="100%" cellpadding="0" cellspacing="0" border="0" style="background-color:transparent;"><tr><td align="center"><table cellpadding="0" cellspacing="0" border="0" style="width:590px"><tr class="layout-full-width" style="background-color:<%= @styling.content_background_color%>"><![endif]-->
<!--[if (mso)|(IE)]><td align="center" width="590" style="background-color:<%= @styling.content_background_color%>;width:590px; border-top: 0px solid transparent; border-left: 0px solid transparent; border-bottom: 0px solid transparent; border-right: 0px solid transparent;" valign="top"><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="padding-right: 0px; padding-left: 0px; padding-top:5px; padding-bottom:5px;"><![endif]-->
<div class="col num12"
style="min-width: 320px; max-width: 590px; display: table-cell; vertical-align: top; width: 590px;">
<div style="width:100% !important;">
<!--[if (!mso)&(!IE)]><!-->
<div
style="border-top:0px solid transparent; border-left:0px solid transparent; border-bottom:0px solid transparent; border-right:0px solid transparent; padding-top:5px; padding-bottom:5px; padding-right: 0px; padding-left: 0px;">
<!--<![endif]-->
<table border="0" cellpadding="0" cellspacing="0" class="divider" role="presentation"
style="table-layout: fixed; vertical-align: top; border-spacing: 0; border-collapse: collapse; mso-table-lspace: 0pt; mso-table-rspace: 0pt; min-width: 100%; -ms-text-size-adjust: 100%; -webkit-text-size-adjust: 100%;"
valign="top" width="100%">
<tbody>
<tr style="vertical-align: top;" valign="top">
<td class="divider_inner"
style="word-break: break-word; vertical-align: top; min-width: 100%; -ms-text-size-adjust: 100%; -webkit-text-size-adjust: 100%; padding-top: 10px; padding-right: 10px; padding-bottom: 10px; padding-left: 10px;"
valign="top">
<table align="center" border="0" cellpadding="0" cellspacing="0" class="divider_content"
height="0" role="presentation"
style="table-layout: fixed; vertical-align: top; border-spacing: 0; border-collapse: collapse; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; border-top: 1px solid <%= @styling.text_color %>; height: 0px;"
valign="top" width="100%">
<tbody>
<tr style="vertical-align: top;" valign="top">
<td height="0"
style="word-break: break-word; vertical-align: top; -ms-text-size-adjust: 100%; -webkit-text-size-adjust: 100%;"
valign="top"><span></span></td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
<!--[if (!mso)&(!IE)]><!-->
</div>
<!--<![endif]-->
</div>
</div>
<!--[if (mso)|(IE)]></td></tr></table><![endif]-->
<!--[if (mso)|(IE)]></td></tr></table></td></tr></table><![endif]-->
</div>
</div>
</div>
<%# divider end %>
<div style="background-color:transparent;">
<div class="block-grid"
style="Margin: 0 auto; min-width: 320px; max-width: 590px; overflow-wrap: break-word; word-wrap: break-word; word-break: break-word; background-color: <%= @styling.content_background_color%>;">
<div style="border-collapse: collapse;display: table;width: 100%;background-color:<%= @styling.content_background_color%>;">
<!--[if (mso)|(IE)]><table width="100%" cellpadding="0" cellspacing="0" border="0" style="background-color:transparent;"><tr><td align="center"><table cellpadding="0" cellspacing="0" border="0" style="width:590px"><tr class="layout-full-width" style="background-color:<%= @styling.content_background_color%>"><![endif]-->
<!--[if (mso)|(IE)]><td align="center" width="590" style="background-color:<%= @styling.content_background_color%>;width:590px; border-top: 0px solid transparent; border-left: 0px solid transparent; border-bottom: 0px solid transparent; border-right: 0px solid transparent;" valign="top"><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="padding-right: 0px; padding-left: 0px; padding-top:5px; padding-bottom:5px;"><![endif]-->
<div class="col num12"
style="min-width: 320px; max-width: 590px; display: table-cell; vertical-align: top; width: 590px;">
<div style="width:100% !important;">
<!--[if (!mso)&(!IE)]><!-->
<div
style="border-top:0px solid transparent; border-left:0px solid transparent; border-bottom:0px solid transparent; border-right:0px solid transparent; padding-top:5px; padding-bottom:5px; padding-right: 0px; padding-left: 0px;">
<!--<![endif]-->
<!--[if mso]><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="padding-right: 10px; padding-left: 10px; padding-top: 10px; padding-bottom: 10px; font-family: Arial, sans-serif"><![endif]-->
<div
style="color:<%= @styling.text_color %>;font-family:Arial, 'Helvetica Neue', Helvetica, sans-serif;line-height:120%;padding-top:10px;padding-right:10px;padding-bottom:10px;padding-left:10px;">
<p
style="font-size: 12px; line-height: 16px; text-align: center; color: <%= @styling.text_color %>; font-family: Arial, 'Helvetica Neue', Helvetica, sans-serif; margin: 0;">
<span style="font-size: 14px;">You have received this email because you have signed up to receive digest emails from <b><%= @instance %></b> Pleroma instance.</span></p>
<p
style="font-size: 12px; line-height: 14px; text-align: center; color: <%= @styling.text_color %>; font-family: Arial, 'Helvetica Neue', Helvetica, sans-serif; margin: 0;">
 </p>
<p
style="font-size: 12px; line-height: 16px; text-align: center; color: <%= @styling.text_color %>; font-family: Arial, 'Helvetica Neue', Helvetica, sans-serif; margin: 0;">
<span style="font-size: 14px;">The email address you are subscribed as is <a href="mailto:<%= @user.email %>" style="color: <%= @styling.link_color %>;text-decoration: none;"><%= @user.email %></a>. </span></p>
<p
style="font-size: 12px; line-height: 16px; text-align: center; color: <%= @styling.text_color %>; font-family: Arial, 'Helvetica Neue', Helvetica, sans-serif; margin: 0;">
<span style="font-size: 14px;">To unsubscribe, please go <%= link "here", style: "color: #{@styling.link_color};text-decoration: none;", to: @unsubscribe_link %>.</span></p>
</div>
<!--[if mso]></td></tr></table><![endif]-->
<!--[if (!mso)&(!IE)]><!-->
</div>
<!--<![endif]-->
</div>
</div>
<!--[if (mso)|(IE)]></td></tr></table><![endif]-->
<!--[if (mso)|(IE)]></td></tr></table></td></tr></table><![endif]-->
</div>
</div>
</div>
<!--[if (mso)|(IE)]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
<!--[if (IE)]></div><![endif]-->
</body>
</html>

View file

@ -0,0 +1,67 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Workers.Cron.EmailMentionsWorker do
use Pleroma.Workers.WorkerHelper, queue: "email_mentions"
@impl true
def perform(%Job{args: %{"op" => "email_mentions", "user_id" => id}}) do
user = Pleroma.User.get_cached_by_id(id)
timeframe =
Pleroma.Config.get([__MODULE__, :timeframe], 30)
|> :timer.minutes()
max_inserted_at =
NaiveDateTime.utc_now()
|> NaiveDateTime.add(-timeframe, :millisecond)
|> NaiveDateTime.truncate(:second)
mentions = Pleroma.Notification.for_user_unread_mentions(user, max_inserted_at)
if mentions != [] do
user
|> Pleroma.Emails.UserEmail.mentions_notification_email(mentions)
|> Pleroma.Emails.Mailer.deliver()
|> case do
{:ok, _} ->
Enum.map(mentions, & &1.id)
_ ->
[]
end
|> Pleroma.Notification.update_notified_at()
end
:ok
end
@impl true
def perform(_) do
config = Pleroma.Config.get(__MODULE__, [])
if Keyword.get(config, :enabled, false) do
timeframe = Keyword.get(config, :timeframe, 30)
period = timeframe * 60
max_at =
NaiveDateTime.utc_now()
|> NaiveDateTime.add(-:timer.minutes(timeframe), :millisecond)
|> NaiveDateTime.truncate(:second)
Pleroma.Notification.users_ids_with_unread_mentions(max_at)
|> Enum.each(&insert_job(&1, unique: [period: period]))
end
:ok
end
defp insert_job(user_id, args) do
Pleroma.Workers.Cron.EmailMentionsWorker.enqueue(
"email_mentions",
%{"user_id" => user_id},
args
)
end
end

View file

@ -0,0 +1,32 @@
defmodule Pleroma.Repo.Migrations.ChangeUserEmailNotificationsSetting do
use Ecto.Migration
import Ecto.Query, only: [from: 2]
def up, do: stream_and_update_users(:up)
def down, do: stream_and_update_users(:down)
defp stream_and_update_users(direction) do
from(u in Pleroma.User, select: [:id, :email_notifications])
|> Pleroma.Repo.stream()
|> Stream.each(&update_user_email_notifications_settings(&1, direction))
|> Stream.run()
end
defp update_user_email_notifications_settings(user, direction) do
email_notifications = change_email_notifications(user.email_notifications, direction)
user
|> Ecto.Changeset.change(email_notifications: email_notifications)
|> Pleroma.Repo.update()
end
defp change_email_notifications(email_notifications, :up) do
Map.put(email_notifications, "notifications", ["mention", "pleroma:chat_mention"])
end
defp change_email_notifications(email_notifications, :down) do
Map.delete(email_notifications, "notifications")
end
end

View file

@ -0,0 +1,15 @@
defmodule Pleroma.Repo.Migrations.AddNotifiedAtToNotifications do
use Ecto.Migration
def up do
alter table(:notifications) do
add_if_not_exists(:notified_at, :naive_datetime)
end
end
def down do
alter table(:notifications) do
remove_if_exists(:notified_at, :naive_datetime)
end
end
end

View file

@ -0,0 +1,23 @@
defmodule Pleroma.Repo.Migrations.FillNotificationsNotifiedAt do
use Ecto.Migration
import Ecto.Query, only: [from: 2]
@types ["mention", "pleroma:chat_mention"]
def up do
from(n in "notifications",
where: is_nil(n.notified_at),
where: n.type in ^@types
)
|> Pleroma.Repo.update_all(set: [notified_at: NaiveDateTime.utc_now()])
end
def down do
from(n in "notifications",
where: not is_nil(n.notified_at),
where: n.type in ^@types
)
|> Pleroma.Repo.update_all(set: [notified_at: nil])
end
end

View file

@ -0,0 +1,7 @@
defmodule Pleroma.Repo.Migrations.AddIndexToNotifications do
use Ecto.Migration
def change do
create_if_not_exists(index(:notifications, [:seen, :notified_at, :type, :inserted_at]))
end
end

View file

@ -617,4 +617,93 @@ defmodule Mix.Tasks.Pleroma.UserTest do
assert mod.is_confirmed
end
end
describe "email_notifications" do
setup do
user = insert(:user, email_notifications: %{"digest" => false, "notifications" => []})
[user: user]
end
test "no changes error", %{user: user} do
Mix.Tasks.Pleroma.User.run(["email_notifications", user.nickname])
assert_received {:mix_shell, :error, ["No changes passed"]}
end
test "user not found" do
Mix.Tasks.Pleroma.User.run(["email_notifications", "nickname", "--digest"])
assert_received {:mix_shell, :error, ["No local user nickname"]}
end
test "all settings", %{user: user} do
assert user.email_notifications == %{"digest" => false, "notifications" => []}
Mix.Tasks.Pleroma.User.run([
"email_notifications",
user.nickname,
"--digest",
"-n",
"mention,pleroma:chat_mention,"
])
from_db = User.get_cached_by_nickname(user.nickname)
assert from_db.email_notifications == %{
"digest" => true,
"notifications" => ["mention", "pleroma:chat_mention"]
}
Mix.Tasks.Pleroma.User.run([
"email_notifications",
user.nickname,
"--no-digest",
"-n",
"off"
])
from_db = User.get_cached_by_nickname(user.nickname)
assert from_db.email_notifications == %{"digest" => false, "notifications" => []}
end
test "partial update", %{user: user} do
Mix.Tasks.Pleroma.User.run([
"email_notifications",
user.nickname,
"--digest"
])
from_db = User.get_cached_by_nickname(user.nickname)
assert from_db.email_notifications == %{"digest" => true, "notifications" => []}
Mix.Tasks.Pleroma.User.run([
"email_notifications",
user.nickname,
"--no-digest"
])
from_db = User.get_cached_by_nickname(user.nickname)
assert from_db.email_notifications == %{"digest" => false, "notifications" => []}
Mix.Tasks.Pleroma.User.run([
"email_notifications",
user.nickname,
"-n",
"mention"
])
from_db = User.get_cached_by_nickname(user.nickname)
assert from_db.email_notifications == %{"digest" => false, "notifications" => ["mention"]}
Mix.Tasks.Pleroma.User.run([
"email_notifications",
user.nickname,
"-n",
"off"
])
from_db = User.get_cached_by_nickname(user.nickname)
assert from_db.email_notifications == %{"digest" => false, "notifications" => []}
end
end
end

View file

@ -1155,4 +1155,94 @@ defmodule Pleroma.NotificationTest do
assert length(Notification.for_user(user)) == 1
end
end
describe "users_ids_with_unread_mentions/0" do
setup do
now = NaiveDateTime.utc_now()
inserted_at = NaiveDateTime.add(now, -61)
insert(:notification, seen: true, type: "mention", inserted_at: inserted_at)
insert(:notification, type: "follow", inserted_at: inserted_at)
insert(:notification, type: "mention")
mention = insert(:notification, type: "mention", inserted_at: inserted_at)
chat_mention = insert(:notification, type: "pleroma:chat_mention", inserted_at: inserted_at)
insert(:notification,
type: "mention",
notified_at: now,
inserted_at: inserted_at
)
[
mention: mention,
chat_mention: chat_mention,
now: now
]
end
test "when mentions are in the timeframe", %{
mention: mention,
chat_mention: chat_mention,
now: now
} do
assert Notification.users_ids_with_unread_mentions(NaiveDateTime.add(now, -60)) == [
mention.user_id,
chat_mention.user_id
]
end
test "when mentions are out of the timeframe", %{now: now} do
assert Notification.users_ids_with_unread_mentions(NaiveDateTime.add(now, -62)) == []
end
end
describe "for_user_unread_mentions/1" do
setup do
[user, muted, blocked] = insert_list(3, :user)
{:ok, _} = User.mute(user, muted)
{:ok, _} = CommonAPI.post(muted, %{status: "hey @#{user.nickname}"})
{:ok, _} = User.block(user, blocked)
{:ok, _} = CommonAPI.post(blocked, %{status: "hey @#{user.nickname}"})
insert(:notification, type: "mention", user: user)
insert(:notification, type: "pleroma:chat_mention", user: user)
inserted_at = NaiveDateTime.add(NaiveDateTime.utc_now(), -61)
Repo.update_all(Notification, set: [inserted_at: inserted_at])
[user: user, max: NaiveDateTime.add(NaiveDateTime.utc_now(), -60)]
end
test "when mentions are in timeframe, exclude blocks and mutes", %{user: user, max: max} do
assert Repo.aggregate(Notification, :count, :id) == 4
assert user |> Notification.for_user_unread_mentions(max) |> length() == 2
end
test "when mentions are out of the timeframe, exclude blocks and mutes", %{
user: user,
max: max
} do
assert Notification.for_user_unread_mentions(user, NaiveDateTime.add(max, -2)) == []
end
test "respect user notification types", %{user: user, max: max} do
user =
Map.update!(
user,
:email_notifications,
&Map.put(&1, "notifications", ["pleroma:chat_mention"])
)
[mention] = Notification.for_user_unread_mentions(user, max)
assert mention.type == "pleroma:chat_mention"
end
end
test "update_notified_at/1" do
notifs = insert_list(2, :notification)
assert {2, nil} =
notifs
|> Enum.map(& &1.id)
|> Notification.update_notified_at()
end
end

View file

@ -2239,18 +2239,19 @@ defmodule Pleroma.UserTest do
end
end
describe "update_email_notifications/2" do
setup do
user = insert(:user, email_notifications: %{"digest" => true})
test "update_email_notifications/2" do
user = insert(:user, email_notifications: %{"digest" => false, "notifications" => []})
assert user.email_notifications["digest"] == false
assert user.email_notifications["notifications"] == []
{:ok, user: user}
end
assert {:ok, result} =
User.update_email_notifications(user, %{
"digest" => true,
"notifications" => ["mention", "pleroma:chat_mention"]
})
test "Notifications are updated", %{user: user} do
true = user.email_notifications["digest"]
assert {:ok, result} = User.update_email_notifications(user, %{"digest" => false})
assert result.email_notifications["digest"] == false
end
assert result.email_notifications["digest"]
assert result.email_notifications["notifications"] == ["mention", "pleroma:chat_mention"]
end
describe "local_nickname/1" do

View file

@ -206,6 +206,38 @@ defmodule Pleroma.Web.MastodonAPI.UpdateCredentialsTest do
assert user_data["source"]["pleroma"]["no_rich_text"] == true
end
test "updates the user's email_notifications setting", %{conn: conn} do
resp =
patch(conn, "/api/v1/accounts/update_credentials", %{
email_notifications: %{
"digest" => true,
"notifications" => []
}
})
assert user_data = json_response_and_validate_schema(resp, 200)
assert user_data["pleroma"]["email_notifications"] == %{
"digest" => true,
"notifications" => []
}
resp =
patch(conn, "/api/v1/accounts/update_credentials", %{
email_notifications: %{
"digest" => false,
"notifications" => ["mention", "pleroma:chat_mention"]
}
})
assert user_data = json_response_and_validate_schema(resp, 200)
assert user_data["pleroma"]["email_notifications"] == %{
"digest" => false,
"notifications" => ["mention", "pleroma:chat_mention"]
}
end
test "updates the user's name", %{conn: conn} do
conn =
patch(conn, "/api/v1/accounts/update_credentials", %{"display_name" => "markorepairs"})

View file

@ -90,7 +90,11 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do
hide_follows_count: false,
relationship: %{},
skip_thread_containment: false,
accepts_chat_messages: nil
accepts_chat_messages: nil,
email_notifications: %{
"digest" => false,
"notifications" => ["mention", "pleroma:chat_mention"]
}
}
}
@ -190,7 +194,11 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do
hide_follows_count: false,
relationship: %{},
skip_thread_containment: false,
accepts_chat_messages: nil
accepts_chat_messages: nil,
email_notifications: %{
"digest" => false,
"notifications" => ["mention", "pleroma:chat_mention"]
}
}
}

View file

@ -0,0 +1,107 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Workers.Cron.EmailMentionsWorkerTest do
use Pleroma.DataCase
use Oban.Testing, repo: Pleroma.Repo
import Pleroma.Factory
import Swoosh.TestAssertions
alias Pleroma.Workers.Cron.EmailMentionsWorker
setup do
clear_config(EmailMentionsWorker, enabled: true, timeframe: 1)
inserted_at = NaiveDateTime.add(NaiveDateTime.utc_now(), -61)
n1 = insert(:notification, seen: true, type: "mention", inserted_at: inserted_at)
n2 = insert(:notification, type: "follow", inserted_at: inserted_at)
n3 = insert(:notification, type: "mention")
mention = insert(:notification, type: "mention", inserted_at: inserted_at)
chat_mention = insert(:notification, type: "pleroma:chat_mention", inserted_at: inserted_at)
n4 =
insert(:notification,
type: "mention",
notified_at: NaiveDateTime.utc_now(),
inserted_at: inserted_at
)
[
mention: mention,
chat_mention: chat_mention,
other_user_ids: [n1.user_id, n2.user_id, n3.user_id, n4.user_id]
]
end
test "creates jobs for users", %{
mention: mention,
chat_mention: chat_mention,
other_user_ids: ids
} do
assert EmailMentionsWorker.perform(%{}) == :ok
assert_enqueued(
worker: EmailMentionsWorker,
args: %{op: "email_mentions", user_id: mention.user_id}
)
assert_enqueued(
worker: EmailMentionsWorker,
args: %{op: "email_mentions", user_id: chat_mention.user_id}
)
Enum.each(ids, fn id ->
refute_enqueued(worker: EmailMentionsWorker, args: %{op: "email_mentions", user_id: id})
end)
assert Repo.aggregate(Oban.Job, :count, :id) == 2
EmailMentionsWorker.perform(%{})
# no duplicates
assert Repo.aggregate(Oban.Job, :count, :id) == 2
end
test "doesn't create jobs for users without emails", %{mention: mention} do
%{user: user} = Repo.preload(mention, :user)
user
|> Ecto.Changeset.change(email: nil)
|> Repo.update()
assert EmailMentionsWorker.perform(%{}) == :ok
refute_enqueued(
worker: EmailMentionsWorker,
args: %{op: "email_mentions", user_id: mention.user_id}
)
end
test "sends emails", %{mention: mention, chat_mention: chat_mention} do
assert EmailMentionsWorker.perform(%{}) == :ok
mention = Repo.preload(mention, :user)
assert EmailMentionsWorker.perform(%Oban.Job{
args: %{"op" => "email_mentions", "user_id" => mention.user_id}
}) == :ok
assert_email_sent(
to: {mention.user.name, mention.user.email},
html_body: ~r/here is what you've missed!/i
)
chat_mention = Repo.preload(chat_mention, :user)
assert EmailMentionsWorker.perform(%Oban.Job{
args: %{"op" => "email_mentions", "user_id" => chat_mention.user_id}
}) == :ok
assert_email_sent(
to: {chat_mention.user.name, chat_mention.user.email},
html_body: ~r/here is what you've missed!/i
)
end
end

View file

@ -469,7 +469,8 @@ defmodule Pleroma.Factory do
def notification_factory do
%Pleroma.Notification{
user: build(:user)
user: build(:user),
activity: build(:note_activity)
}
end