Merge branch 'webpush-polls' into 'develop'

Render nice web push notifications for polls

See merge request pleroma/pleroma!4144
This commit is contained in:
feld 2024-06-11 20:24:15 +00:00
commit f47a124698
9 changed files with 147 additions and 96 deletions

View File

@ -0,0 +1 @@
Render nice web push notifications for polls

View File

@ -479,8 +479,6 @@ defmodule Pleroma.Notification do
end
end)
stream(notifications)
{:ok, notifications}
end
end

View File

@ -20,17 +20,13 @@ defmodule Pleroma.Web.Push do
end
def vapid_config do
Application.get_env(:web_push_encryption, :vapid_details, [])
Application.get_env(:web_push_encryption, :vapid_details, nil)
end
def enabled do
case vapid_config() do
[] -> false
list when is_list(list) -> true
_ -> false
end
end
def enabled, do: match?([subject: _, public_key: _, private_key: _], vapid_config())
@spec send(Pleroma.Notification.t()) ::
{:ok, Oban.Job.t()} | {:error, Oban.Job.changeset() | term()}
def send(notification) do
WebPusherWorker.enqueue("web_push", %{"notification_id" => notification.id})
end

View File

@ -16,6 +16,7 @@ defmodule Pleroma.Web.Push.Impl do
require Logger
import Ecto.Query
@body_chars 140
@types ["Create", "Follow", "Announce", "Like", "Move", "EmojiReact", "Update"]
@doc "Performs sending notifications for user subscriptions"
@ -27,21 +28,20 @@ defmodule Pleroma.Web.Push.Impl do
} = notification
)
when activity_type in @types do
actor = User.get_cached_by_ap_id(notification.activity.data["actor"])
user = User.get_cached_by_ap_id(notification.activity.data["actor"])
mastodon_type = notification.type
gcm_api_key = Application.get_env(:web_push_encryption, :gcm_api_key)
avatar_url = User.avatar_url(actor)
avatar_url = User.avatar_url(user)
object = Object.normalize(activity, fetch: false)
user = User.get_cached_by_id(user_id)
direct_conversation_id = Activity.direct_conversation_id(activity, user)
for subscription <- fetch_subscriptions(user_id),
Subscription.enabled?(subscription, mastodon_type) do
Subscription.enabled?(subscription, notification.type) do
%{
access_token: subscription.token.token,
notification_id: notification.id,
notification_type: mastodon_type,
notification_type: notification.type,
icon: avatar_url,
preferred_locale: "en",
pleroma: %{
@ -49,7 +49,7 @@ defmodule Pleroma.Web.Push.Impl do
direct_conversation_id: direct_conversation_id
}
}
|> Map.merge(build_content(notification, actor, object, mastodon_type))
|> Map.merge(build_content(notification, user, object))
|> Jason.encode!()
|> push_message(build_sub(subscription), gcm_api_key, subscription)
end
@ -106,97 +106,101 @@ defmodule Pleroma.Web.Push.Impl do
}
end
def build_content(notification, actor, object, mastodon_type \\ nil)
def build_content(
%{
user: %{notification_settings: %{hide_notification_contents: true}}
} = notification,
_actor,
_object,
mastodon_type
_user,
_object
) do
%{body: format_title(notification, mastodon_type)}
%{body: format_title(notification)}
end
def build_content(notification, actor, object, mastodon_type) do
mastodon_type = mastodon_type || notification.type
def build_content(notification, user, object) do
%{
title: format_title(notification, mastodon_type),
body: format_body(notification, actor, object, mastodon_type)
title: format_title(notification),
body: format_body(notification, user, object)
}
end
def format_body(activity, actor, object, mastodon_type \\ nil)
def format_body(_activity, actor, %{data: %{"type" => "ChatMessage"} = data}, _) do
case data["content"] do
nil -> "@#{actor.nickname}: (Attachment)"
content -> "@#{actor.nickname}: #{Utils.scrub_html_and_truncate(content, 80)}"
@spec format_body(Notification.t(), User.t(), Object.t()) :: String.t()
def format_body(_notification, user, %{data: %{"type" => "ChatMessage"} = object}) do
case object["content"] do
nil -> "@#{user.nickname}: (Attachment)"
content -> "@#{user.nickname}: #{Utils.scrub_html_and_truncate(content, @body_chars)}"
end
end
def format_body(
%{type: "poll"} = _notification,
_user,
%{data: %{"content" => content} = data} = _object
) do
options = Map.get(data, "anyOf") || Map.get(data, "oneOf")
content_text = content <> "\n"
options_text =
Enum.map(options, fn x -> "#{x["name"]}" end)
|> Enum.join("\n")
[content_text, options_text]
|> Enum.join("\n")
|> Utils.scrub_html_and_truncate(@body_chars)
end
def format_body(
%{activity: %{data: %{"type" => "Create"}}},
actor,
%{data: %{"content" => content}},
_mastodon_type
user,
%{data: %{"content" => content}}
) do
"@#{actor.nickname}: #{Utils.scrub_html_and_truncate(content, 80)}"
"@#{user.nickname}: #{Utils.scrub_html_and_truncate(content, @body_chars)}"
end
def format_body(
%{activity: %{data: %{"type" => "Announce"}}},
actor,
%{data: %{"content" => content}},
_mastodon_type
user,
%{data: %{"content" => content}}
) do
"@#{actor.nickname} repeated: #{Utils.scrub_html_and_truncate(content, 80)}"
"@#{user.nickname} repeated: #{Utils.scrub_html_and_truncate(content, @body_chars)}"
end
def format_body(
%{activity: %{data: %{"type" => "EmojiReact", "content" => content}}},
actor,
_object,
_mastodon_type
user,
_object
) do
"@#{actor.nickname} reacted with #{content}"
"@#{user.nickname} reacted with #{content}"
end
def format_body(
%{activity: %{data: %{"type" => type}}} = notification,
actor,
_object,
mastodon_type
user,
_object
)
when type in ["Follow", "Like"] do
mastodon_type = mastodon_type || notification.type
case mastodon_type do
"follow" -> "@#{actor.nickname} has followed you"
"follow_request" -> "@#{actor.nickname} has requested to follow you"
"favourite" -> "@#{actor.nickname} has favorited your post"
case notification.type do
"follow" -> "@#{user.nickname} has followed you"
"follow_request" -> "@#{user.nickname} has requested to follow you"
"favourite" -> "@#{user.nickname} has favorited your post"
end
end
def format_body(
%{activity: %{data: %{"type" => "Update"}}},
actor,
_object,
_mastodon_type
user,
_object
) do
"@#{actor.nickname} edited a status"
"@#{user.nickname} edited a status"
end
def format_title(activity, mastodon_type \\ nil)
def format_title(%{activity: %{data: %{"directMessage" => true}}}, _mastodon_type) do
@spec format_title(Notification.t()) :: String.t()
def format_title(%{activity: %{data: %{"directMessage" => true}}}) do
"New Direct Message"
end
def format_title(%{type: type}, mastodon_type) do
case mastodon_type || type do
def format_title(%{type: type}) do
case type do
"mention" -> "New Mention"
"status" -> "New Status"
"follow" -> "New Follower"
@ -206,6 +210,7 @@ defmodule Pleroma.Web.Push.Impl do
"update" -> "New Update"
"pleroma:chat_mention" -> "New Chat Message"
"pleroma:emoji_reaction" -> "New Reaction"
"poll" -> "Poll Results"
type -> "New #{String.capitalize(type || "event")}"
end
end

View File

@ -14,8 +14,9 @@ defmodule Pleroma.Workers.PollWorker do
@impl Oban.Worker
def perform(%Job{args: %{"op" => "poll_end", "activity_id" => activity_id}}) do
with %Activity{} = activity <- find_poll_activity(activity_id) do
Notification.create_poll_notifications(activity)
with %Activity{} = activity <- find_poll_activity(activity_id),
{:ok, notifications} <- Notification.create_poll_notifications(activity) do
Notification.stream(notifications)
end
end

View File

@ -5,7 +5,6 @@
defmodule Pleroma.NotificationTest do
use Pleroma.DataCase, async: false
import Mock
import Pleroma.Factory
alias Pleroma.FollowingRelationship
@ -184,32 +183,10 @@ defmodule Pleroma.NotificationTest do
{:ok, _, _} = CommonAPI.vote(user2, question, [0])
{:ok, _, _} = CommonAPI.vote(user3, question, [1])
with_mocks([
{
Pleroma.Web.Streamer,
[],
[
stream: fn _, _ -> nil end
]
},
{
Pleroma.Web.Push,
[],
[
send: fn _ -> nil end
]
}
]) do
{:ok, notifications} = Notification.create_poll_notifications(activity)
Enum.each(notifications, fn notification ->
assert called(Pleroma.Web.Streamer.stream(["user", "user:notification"], notification))
assert called(Pleroma.Web.Push.send(notification))
end)
assert [user2.id, user3.id, user1.id] == Enum.map(notifications, & &1.user_id)
end
end
describe "create_notification" do
test "it disables notifications from strangers" do

View File

@ -41,7 +41,7 @@ defmodule Pleroma.Web.Push.ImplTest do
}
}
@api_key "BASgACIHpN1GYgzSRp"
@message "@Bob: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce sagittis fini..."
@message "@Bob: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce sagittis finibus turpis."
test "performs sending notifications" do
user = insert(:user)
@ -129,7 +129,7 @@ defmodule Pleroma.Web.Push.ImplTest do
user,
object
) ==
"@Bob: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce sagittis fini..."
"@Bob: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce sagittis finibus turpis."
assert Impl.format_title(%{activity: activity, type: "mention"}) ==
"New Mention"
@ -161,7 +161,7 @@ defmodule Pleroma.Web.Push.ImplTest do
object = Object.normalize(activity, fetch: false)
assert Impl.format_body(%{activity: announce_activity}, user, object) ==
"@#{user.nickname} repeated: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce sagittis fini..."
"@#{user.nickname} repeated: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce sagittis finibus turpis."
assert Impl.format_title(%{activity: announce_activity, type: "reblog"}) ==
"New Repeat"
@ -232,6 +232,29 @@ defmodule Pleroma.Web.Push.ImplTest do
"New Direct Message"
end
test "renders poll notification" do
user = insert(:user)
question = insert(:question, user: user)
activity = insert(:question_activity, question: question)
{:ok, [notification]} = Notification.create_poll_notifications(activity)
expected_title = "Poll Results"
expected_body =
"""
Which flavor of ice cream do you prefer?
chocolate
vanilla
"""
|> String.trim_trailing("\n")
content = Impl.build_content(notification, user, question)
assert match?(%{title: ^expected_title, body: ^expected_body}, content)
end
describe "build_content/3" do
test "builds content for chat messages" do
user = insert(:user)
@ -344,7 +367,7 @@ defmodule Pleroma.Web.Push.ImplTest do
assert Impl.build_content(notif, actor, object) == %{
body:
"@Bob: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce sagittis fini...",
"@Bob: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce sagittis finibus turpis.",
title: "New Direct Message"
}
@ -362,7 +385,7 @@ defmodule Pleroma.Web.Push.ImplTest do
assert Impl.build_content(notif, actor, object) == %{
body:
"@Bob: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce sagittis fini...",
"@Bob: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce sagittis finibus turpis.",
title: "New Mention"
}

View File

@ -0,0 +1,49 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Workers.PollWorkerTest do
use Pleroma.DataCase
use Oban.Testing, repo: Pleroma.Repo
import Mock
import Pleroma.Factory
alias Pleroma.Workers.PollWorker
test "poll notification job" do
user = insert(:user)
question = insert(:question, user: user)
activity = insert(:question_activity, question: question)
PollWorker.schedule_poll_end(activity)
expected_job_args = %{"activity_id" => activity.id, "op" => "poll_end"}
assert_enqueued(args: expected_job_args)
with_mocks([
{
Pleroma.Web.Streamer,
[],
[
stream: fn _, _ -> nil end
]
},
{
Pleroma.Web.Push,
[],
[
send: fn _ -> nil end
]
}
]) do
[job] = all_enqueued(worker: PollWorker)
PollWorker.perform(job)
# Ensure notifications were streamed out when job executes
assert called(Pleroma.Web.Streamer.stream(["user", "user:notification"], :_))
assert called(Pleroma.Web.Push.send(:_))
end
end
end

View File

@ -249,6 +249,7 @@ defmodule Pleroma.Factory do
"cc" => [user.follower_address],
"context" => Pleroma.Web.ActivityPub.Utils.generate_context_id(),
"closed" => DateTime.utc_now() |> DateTime.add(86_400) |> DateTime.to_iso8601(),
"content" => "Which flavor of ice cream do you prefer?",
"oneOf" => [
%{
"type" => "Note",