diff --git a/changelog.d/webpush-polls.change b/changelog.d/webpush-polls.change new file mode 100644 index 000000000..5607d6bfc --- /dev/null +++ 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 index b9694a353..de2508b93 100644 --- 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 index 0d43f402e..d4693f63e 100644 --- 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 index 53334e72c..13c054e05 100644 --- 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( - %{activity: %{data: %{"type" => "Create"}}}, - actor, - %{data: %{"content" => content}}, - _mastodon_type + %{type: "poll"} = _notification, + _user, + %{data: %{"content" => content} = data} = _object ) do - "@#{actor.nickname}: #{Utils.scrub_html_and_truncate(content, 80)}" + 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"}}}, + user, + %{data: %{"content" => content}} + ) do + "@#{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 index 70df54193..3fcac9bc3 100644 --- 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 index 02ae06c63..2c582c708 100644 --- 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) + {: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 + 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 index 3ceea3d71..c263a1280 100644 --- 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 new file mode 100644 index 000000000..749df8aff --- /dev/null +++ b/test/pleroma/workers/poll_worker_test.exs @@ -0,0 +1,49 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors +# 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 index 20bc5162e..b248508fa 100644 --- 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",