commit: f47a1246985d6ce69ceca4104e43f630f1f33610
parent 61506f8d9360f501b0e771e4a948cafbb50d6401
Author: feld <feld@feld.me>
Date: Tue, 11 Jun 2024 20:24:15 +0000
Merge branch 'webpush-polls' into 'develop'
Render nice web push notifications for polls
See merge request pleroma/pleroma!4144
Diffstat:
9 files changed, 146 insertions(+), 95 deletions(-)
diff --git a/changelog.d/webpush-polls.change b/changelog.d/webpush-polls.change
@@ -0,0 +1 @@
+Render nice web push notifications for polls
diff --git a/lib/pleroma/notification.ex b/lib/pleroma/notification.ex
@@ -479,8 +479,6 @@ defmodule Pleroma.Notification do
end
end)
- stream(notifications)
-
{:ok, notifications}
end
end
diff --git a/lib/pleroma/web/push.ex b/lib/pleroma/web/push.ex
@@ -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
diff --git a/lib/pleroma/web/push/impl.ex b/lib/pleroma/web/push/impl.ex
@@ -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
diff --git a/lib/pleroma/workers/poll_worker.ex b/lib/pleroma/workers/poll_worker.ex
@@ -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
diff --git a/test/pleroma/notification_test.exs b/test/pleroma/notification_test.exs
@@ -5,7 +5,6 @@
defmodule Pleroma.NotificationTest do
use Pleroma.DataCase, async: false
- import Mock
import Pleroma.Factory
alias Pleroma.FollowingRelationship
@@ -184,31 +183,9 @@ 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)
+ {:ok, notifications} = Notification.create_poll_notifications(activity)
- assert [user2.id, user3.id, user1.id] == Enum.map(notifications, & &1.user_id)
- end
+ assert [user2.id, user3.id, user1.id] == Enum.map(notifications, & &1.user_id)
end
describe "create_notification" do
diff --git a/test/pleroma/web/push/impl_test.exs b/test/pleroma/web/push/impl_test.exs
@@ -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"
}
diff --git a/test/pleroma/workers/poll_worker_test.exs b/test/pleroma/workers/poll_worker_test.exs
@@ -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
diff --git a/test/support/factory.ex b/test/support/factory.ex
@@ -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",