From 7d005e8c93b22dc3d7be1a66dd2d404b7f54306a Mon Sep 17 00:00:00 2001 From: tusooa Date: Sat, 1 Apr 2023 01:25:13 -0400 Subject: [PATCH] Return stream attribute in server-sent events --- lib/pleroma/constants.ex | 4 ++ .../web/mastodon_api/websocket_handler.ex | 4 +- lib/pleroma/web/streamer.ex | 27 +++++---- lib/pleroma/web/views/streamer_view.ex | 42 +++++++++++--- .../integration/mastodon_websocket_test.exs | 33 +++++++++++ test/pleroma/web/streamer_test.exs | 58 +++++++++---------- 6 files changed, 119 insertions(+), 49 deletions(-) diff --git a/lib/pleroma/constants.ex b/lib/pleroma/constants.ex index ed60bcc37..77bc4bfac 100644 --- a/lib/pleroma/constants.ex +++ b/lib/pleroma/constants.ex @@ -94,4 +94,8 @@ defmodule Pleroma.Constants do "application/activity+json" ] ) + + const(public_streams, + do: ["public", "public:local", "public:media", "public:local:media"] + ) end diff --git a/lib/pleroma/web/mastodon_api/websocket_handler.ex b/lib/pleroma/web/mastodon_api/websocket_handler.ex index 6233c3340..2707673ba 100644 --- a/lib/pleroma/web/mastodon_api/websocket_handler.ex +++ b/lib/pleroma/web/mastodon_api/websocket_handler.ex @@ -89,11 +89,11 @@ defmodule Pleroma.Web.MastodonAPI.WebsocketHandler do {:ok, state} end - def websocket_info({:render_with_user, view, template, item}, state) do + def websocket_info({:render_with_user, view, template, item, topic}, state) do user = %User{} = User.get_cached_by_ap_id(state.user.ap_id) unless Streamer.filtered_by_user?(user, item) do - websocket_info({:text, view.render(template, item, user)}, %{state | user: user}) + websocket_info({:text, view.render(template, item, user, topic)}, %{state | user: user}) else {:ok, state} end diff --git a/lib/pleroma/web/streamer.ex b/lib/pleroma/web/streamer.ex index 6b5fdbf0c..70e46617a 100644 --- a/lib/pleroma/web/streamer.ex +++ b/lib/pleroma/web/streamer.ex @@ -4,6 +4,7 @@ defmodule Pleroma.Web.Streamer do require Logger + require Pleroma.Constants alias Pleroma.Activity alias Pleroma.Chat.MessageReference @@ -24,7 +25,7 @@ defmodule Pleroma.Web.Streamer do def registry, do: @registry - @public_streams ["public", "public:local", "public:media", "public:local:media"] + @public_streams Pleroma.Constants.public_streams() @local_streams ["public:local", "public:local:media"] @user_streams ["user", "user:notification", "direct", "user:pleroma_chat"] @@ -223,8 +224,8 @@ defmodule Pleroma.Web.Streamer do end defp do_stream("follow_relationship", item) do - text = StreamerView.render("follow_relationships_update.json", item) user_topic = "user:#{item.follower.id}" + text = StreamerView.render("follow_relationships_update.json", item, user_topic) Logger.debug("Trying to push follow relationship update to #{user_topic}\n\n") @@ -270,9 +271,11 @@ defmodule Pleroma.Web.Streamer do defp do_stream(topic, %Notification{} = item) when topic in ["user", "user:notification"] do - Registry.dispatch(@registry, "#{topic}:#{item.user_id}", fn list -> + user_topic = "#{topic}:#{item.user_id}" + + Registry.dispatch(@registry, user_topic, fn list -> Enum.each(list, fn {pid, _auth} -> - send(pid, {:render_with_user, StreamerView, "notification.json", item}) + send(pid, {:render_with_user, StreamerView, "notification.json", item, user_topic}) end) end) end @@ -281,7 +284,7 @@ defmodule Pleroma.Web.Streamer do when topic in ["user", "user:pleroma_chat"] do topic = "#{topic}:#{user.id}" - text = StreamerView.render("chat_update.json", %{chat_message_reference: cm_ref}) + text = StreamerView.render("chat_update.json", %{chat_message_reference: cm_ref}, topic) Registry.dispatch(@registry, topic, fn list -> Enum.each(list, fn {pid, _auth} -> @@ -309,7 +312,7 @@ defmodule Pleroma.Web.Streamer do end defp push_to_socket(topic, %Participation{} = participation) do - rendered = StreamerView.render("conversation.json", participation) + rendered = StreamerView.render("conversation.json", participation, topic) Registry.dispatch(@registry, topic, fn list -> Enum.each(list, fn {pid, _} -> @@ -337,12 +340,15 @@ defmodule Pleroma.Web.Streamer do Pleroma.Activity.get_create_by_object_ap_id(item.object.data["id"]) |> Map.put(:object, item.object) - anon_render = StreamerView.render("status_update.json", create_activity) + anon_render = StreamerView.render("status_update.json", create_activity, topic) Registry.dispatch(@registry, topic, fn list -> Enum.each(list, fn {pid, auth?} -> if auth? do - send(pid, {:render_with_user, StreamerView, "status_update.json", create_activity}) + send( + pid, + {:render_with_user, StreamerView, "status_update.json", create_activity, topic} + ) else send(pid, {:text, anon_render}) end @@ -351,12 +357,13 @@ defmodule Pleroma.Web.Streamer do end defp push_to_socket(topic, item) do - anon_render = StreamerView.render("update.json", item) + Logger.debug("topic=#{topic}") + anon_render = StreamerView.render("update.json", item, topic) Registry.dispatch(@registry, topic, fn list -> Enum.each(list, fn {pid, auth?} -> if auth? do - send(pid, {:render_with_user, StreamerView, "update.json", item}) + send(pid, {:render_with_user, StreamerView, "update.json", item, topic}) else send(pid, {:text, anon_render}) end diff --git a/lib/pleroma/web/views/streamer_view.ex b/lib/pleroma/web/views/streamer_view.ex index 0cdcb1918..f591da9a6 100644 --- a/lib/pleroma/web/views/streamer_view.ex +++ b/lib/pleroma/web/views/streamer_view.ex @@ -11,8 +11,11 @@ defmodule Pleroma.Web.StreamerView do alias Pleroma.User alias Pleroma.Web.MastodonAPI.NotificationView - def render("update.json", %Activity{} = activity, %User{} = user) do + require Pleroma.Constants + + def render("update.json", %Activity{} = activity, %User{} = user, topic) do %{ + stream: render("stream.json", %{topic: topic}), event: "update", payload: Pleroma.Web.MastodonAPI.StatusView.render( @@ -25,8 +28,9 @@ defmodule Pleroma.Web.StreamerView do |> Jason.encode!() end - def render("status_update.json", %Activity{} = activity, %User{} = user) do + def render("status_update.json", %Activity{} = activity, %User{} = user, topic) do %{ + stream: render("stream.json", %{topic: topic}), event: "status.update", payload: Pleroma.Web.MastodonAPI.StatusView.render( @@ -39,8 +43,9 @@ defmodule Pleroma.Web.StreamerView do |> Jason.encode!() end - def render("notification.json", %Notification{} = notify, %User{} = user) do + def render("notification.json", %Notification{} = notify, %User{} = user, topic) do %{ + stream: render("stream.json", %{topic: topic}), event: "notification", payload: NotificationView.render( @@ -52,8 +57,9 @@ defmodule Pleroma.Web.StreamerView do |> Jason.encode!() end - def render("update.json", %Activity{} = activity) do + def render("update.json", %Activity{} = activity, topic) do %{ + stream: render("stream.json", %{topic: topic}), event: "update", payload: Pleroma.Web.MastodonAPI.StatusView.render( @@ -65,8 +71,9 @@ defmodule Pleroma.Web.StreamerView do |> Jason.encode!() end - def render("status_update.json", %Activity{} = activity) do + def render("status_update.json", %Activity{} = activity, topic) do %{ + stream: render("stream.json", %{topic: topic}), event: "status.update", payload: Pleroma.Web.MastodonAPI.StatusView.render( @@ -78,7 +85,7 @@ defmodule Pleroma.Web.StreamerView do |> Jason.encode!() end - def render("chat_update.json", %{chat_message_reference: cm_ref}) do + def render("chat_update.json", %{chat_message_reference: cm_ref}, topic) do # Explicitly giving the cmr for the object here, so we don't accidentally # send a later 'last_message' that was inserted between inserting this and # streaming it out @@ -93,6 +100,7 @@ defmodule Pleroma.Web.StreamerView do ) %{ + stream: render("stream.json", %{topic: topic}), event: "pleroma:chat_update", payload: representation @@ -101,8 +109,9 @@ defmodule Pleroma.Web.StreamerView do |> Jason.encode!() end - def render("follow_relationships_update.json", item) do + def render("follow_relationships_update.json", item, topic) do %{ + stream: render("stream.json", %{topic: topic}), event: "pleroma:follow_relationships_update", payload: %{ @@ -123,8 +132,9 @@ defmodule Pleroma.Web.StreamerView do |> Jason.encode!() end - def render("conversation.json", %Participation{} = participation) do + def render("conversation.json", %Participation{} = participation, topic) do %{ + stream: render("stream.json", %{topic: topic}), event: "conversation", payload: Pleroma.Web.MastodonAPI.ConversationView.render("participation.json", %{ @@ -150,6 +160,22 @@ defmodule Pleroma.Web.StreamerView do |> Jason.encode!() end + def render("stream.json", %{topic: "user:pleroma_chat:" <> _}), do: ["user:pleroma_chat"] + def render("stream.json", %{topic: "user:notification:" <> _}), do: ["user:notification"] + def render("stream.json", %{topic: "user:" <> _}), do: ["user"] + def render("stream.json", %{topic: "direct:" <> _}), do: ["direct"] + def render("stream.json", %{topic: "list:" <> id}), do: ["list", id] + def render("stream.json", %{topic: "hashtag:" <> tag}), do: ["hashtag", tag] + + def render("stream.json", %{topic: "public:remote:media:" <> instance}), + do: ["public:remote:media", instance] + + def render("stream.json", %{topic: "public:remote:" <> instance}), + do: ["public:remote", instance] + + def render("stream.json", %{topic: stream}) when stream in Pleroma.Constants.public_streams(), + do: [stream] + defp maybe_error(%{error: :bad_topic}), do: %{error: "bad_topic"} defp maybe_error(%{error: :unauthorized}), do: %{error: "unauthorized"} defp maybe_error(%{error: :already_authenticated}), do: %{error: "already_authenticated"} diff --git a/test/pleroma/integration/mastodon_websocket_test.exs b/test/pleroma/integration/mastodon_websocket_test.exs index 827c7b5b0..d2e98df3b 100644 --- a/test/pleroma/integration/mastodon_websocket_test.exs +++ b/test/pleroma/integration/mastodon_websocket_test.exs @@ -116,6 +116,39 @@ defmodule Pleroma.Integration.MastodonWebsocketTest do assert json == view_json end + test "can subscribe to multiple streams" do + user = insert(:user) + {:ok, pid} = start_socket() + WebsocketClient.send_text(pid, %{type: "subscribe", stream: "public"} |> Jason.encode!()) + assert_receive {:text, raw_json}, 1_000 + + assert {:ok, + %{ + "event" => "pleroma.respond", + "payload" => %{"type" => "subscribe", "result" => "success"} + }} = decode_json(raw_json) + + WebsocketClient.send_text(pid, %{type: "subscribe", stream: "hashtag", tag: "mew"} |> Jason.encode!()) + assert_receive {:text, raw_json}, 1_000 + + assert {:ok, + %{ + "event" => "pleroma.respond", + "payload" => %{"type" => "subscribe", "result" => "success"} + }} = decode_json(raw_json) + + {:ok, _activity} = CommonAPI.post(user, %{status: "nice echo chamber #mew"}) + + assert_receive {:text, raw_json}, 1_000 + assert {:ok, %{"stream" => stream1}} = Jason.decode(raw_json) + assert_receive {:text, raw_json}, 1_000 + assert {:ok, %{"stream" => stream2}} = Jason.decode(raw_json) + + streams = [stream1, stream2] + assert ["hashtag", "mew"] in streams + assert ["public"] in streams + end + test "won't double subscribe" do user = insert(:user) {:ok, pid} = start_socket() diff --git a/test/pleroma/web/streamer_test.exs b/test/pleroma/web/streamer_test.exs index da97b87d8..cc57a2989 100644 --- a/test/pleroma/web/streamer_test.exs +++ b/test/pleroma/web/streamer_test.exs @@ -246,7 +246,7 @@ defmodule Pleroma.Web.StreamerTest do Streamer.get_topic_and_add_socket("user", user, oauth_token) {:ok, activity} = CommonAPI.post(user, %{status: "hey"}) - assert_receive {:render_with_user, _, _, ^activity} + assert_receive {:render_with_user, _, _, ^activity, _} refute Streamer.filtered_by_user?(user, activity) end @@ -257,7 +257,7 @@ defmodule Pleroma.Web.StreamerTest do {:ok, activity} = CommonAPI.post(other_user, %{status: "hey"}) {:ok, announce} = CommonAPI.repeat(activity.id, user) - assert_receive {:render_with_user, Pleroma.Web.StreamerView, "update.json", ^announce} + assert_receive {:render_with_user, Pleroma.Web.StreamerView, "update.json", ^announce, _} refute Streamer.filtered_by_user?(user, announce) end @@ -310,7 +310,7 @@ defmodule Pleroma.Web.StreamerTest do {:ok, %Pleroma.Activity{data: _data, local: false} = announce} = Pleroma.Web.ActivityPub.Transmogrifier.handle_incoming(data) - assert_receive {:render_with_user, Pleroma.Web.StreamerView, "update.json", ^announce} + assert_receive {:render_with_user, Pleroma.Web.StreamerView, "update.json", ^announce, _} refute Streamer.filtered_by_user?(user, announce) end @@ -322,7 +322,7 @@ defmodule Pleroma.Web.StreamerTest do Streamer.get_topic_and_add_socket("user", user, oauth_token) Streamer.stream("user", notify) - assert_receive {:render_with_user, _, _, ^notify} + assert_receive {:render_with_user, _, _, ^notify, _} refute Streamer.filtered_by_user?(user, notify) end @@ -334,7 +334,7 @@ defmodule Pleroma.Web.StreamerTest do Streamer.get_topic_and_add_socket("user:notification", user, oauth_token) Streamer.stream("user:notification", notify) - assert_receive {:render_with_user, _, _, ^notify} + assert_receive {:render_with_user, _, _, ^notify, _} refute Streamer.filtered_by_user?(user, notify) end @@ -355,7 +355,7 @@ defmodule Pleroma.Web.StreamerTest do Streamer.get_topic_and_add_socket("user:pleroma_chat", user, oauth_token) Streamer.stream("user:pleroma_chat", {user, cm_ref}) - text = StreamerView.render("chat_update.json", %{chat_message_reference: cm_ref}) + text = StreamerView.render("chat_update.json", %{chat_message_reference: cm_ref}, "user:pleroma_chat:#{user.id}") assert text =~ "hey cirno" assert_receive {:text, ^text} @@ -373,7 +373,7 @@ defmodule Pleroma.Web.StreamerTest do Streamer.get_topic_and_add_socket("user", user, oauth_token) Streamer.stream("user", {user, cm_ref}) - text = StreamerView.render("chat_update.json", %{chat_message_reference: cm_ref}) + text = StreamerView.render("chat_update.json", %{chat_message_reference: cm_ref}, "user:#{user.id}") assert text =~ "hey cirno" assert_receive {:text, ^text} @@ -394,7 +394,7 @@ defmodule Pleroma.Web.StreamerTest do Streamer.get_topic_and_add_socket("user:notification", user, oauth_token) Streamer.stream("user:notification", notify) - assert_receive {:render_with_user, _, _, ^notify} + assert_receive {:render_with_user, _, _, ^notify, _} refute Streamer.filtered_by_user?(user, notify) end @@ -440,7 +440,7 @@ defmodule Pleroma.Web.StreamerTest do Streamer.get_topic_and_add_socket("user:notification", user, oauth_token) {:ok, favorite_activity} = CommonAPI.favorite(user2, activity.id) - assert_receive {:render_with_user, _, "notification.json", notif} + assert_receive {:render_with_user, _, "notification.json", notif, _} assert notif.activity.id == favorite_activity.id refute Streamer.filtered_by_user?(user, notif) end @@ -469,7 +469,7 @@ defmodule Pleroma.Web.StreamerTest do Streamer.get_topic_and_add_socket("user:notification", user, oauth_token) {:ok, _follower, _followed, follow_activity} = CommonAPI.follow(user2, user) - assert_receive {:render_with_user, _, "notification.json", notif} + assert_receive {:render_with_user, _, "notification.json", notif, _} assert notif.activity.id == follow_activity.id refute Streamer.filtered_by_user?(user, notif) end @@ -534,7 +534,7 @@ defmodule Pleroma.Web.StreamerTest do {:ok, edited} = CommonAPI.update(sender, activity, %{status: "mew mew"}) create = Pleroma.Activity.get_create_by_object_ap_id_with_object(activity.object.data["id"]) - assert_receive {:render_with_user, _, "status_update.json", ^create} + assert_receive {:render_with_user, _, "status_update.json", ^create, _} refute Streamer.filtered_by_user?(user, edited) end @@ -545,7 +545,7 @@ defmodule Pleroma.Web.StreamerTest do {:ok, edited} = CommonAPI.update(user, activity, %{status: "mew mew"}) create = Pleroma.Activity.get_create_by_object_ap_id_with_object(activity.object.data["id"]) - assert_receive {:render_with_user, _, "status_update.json", ^create} + assert_receive {:render_with_user, _, "status_update.json", ^create, _} refute Streamer.filtered_by_user?(user, edited) end end @@ -558,7 +558,7 @@ defmodule Pleroma.Web.StreamerTest do Streamer.get_topic_and_add_socket("public", user, oauth_token) {:ok, activity} = CommonAPI.post(other_user, %{status: "Test"}) - assert_receive {:render_with_user, _, _, ^activity} + assert_receive {:render_with_user, _, _, ^activity, _} refute Streamer.filtered_by_user?(other_user, activity) end @@ -658,7 +658,7 @@ defmodule Pleroma.Web.StreamerTest do Streamer.get_topic_and_add_socket("public", user, oauth_token) Streamer.stream("public", activity) - assert_receive {:render_with_user, _, _, ^activity} + assert_receive {:render_with_user, _, _, ^activity, _} assert Streamer.filtered_by_user?(user, activity) end @@ -680,7 +680,7 @@ defmodule Pleroma.Web.StreamerTest do Streamer.get_topic_and_add_socket("public", user, oauth_token) Streamer.stream("public", activity) - assert_receive {:render_with_user, _, _, ^activity} + assert_receive {:render_with_user, _, _, ^activity, _} refute Streamer.filtered_by_user?(user, activity) end @@ -703,7 +703,7 @@ defmodule Pleroma.Web.StreamerTest do Streamer.get_topic_and_add_socket("public", user, oauth_token) Streamer.stream("public", activity) - assert_receive {:render_with_user, _, _, ^activity} + assert_receive {:render_with_user, _, _, ^activity, _} refute Streamer.filtered_by_user?(user, activity) end end @@ -717,7 +717,7 @@ defmodule Pleroma.Web.StreamerTest do Streamer.get_topic_and_add_socket("public", user, oauth_token) {:ok, activity} = CommonAPI.post(blocked_user, %{status: "Test"}) - assert_receive {:render_with_user, _, _, ^activity} + assert_receive {:render_with_user, _, _, ^activity, _} assert Streamer.filtered_by_user?(user, activity) end @@ -734,17 +734,17 @@ defmodule Pleroma.Web.StreamerTest do {:ok, activity_one} = CommonAPI.post(friend, %{status: "hey! @#{blockee.nickname}"}) - assert_receive {:render_with_user, _, _, ^activity_one} + assert_receive {:render_with_user, _, _, ^activity_one, _} assert Streamer.filtered_by_user?(blocker, activity_one) {:ok, activity_two} = CommonAPI.post(blockee, %{status: "hey! @#{friend.nickname}"}) - assert_receive {:render_with_user, _, _, ^activity_two} + assert_receive {:render_with_user, _, _, ^activity_two, _} assert Streamer.filtered_by_user?(blocker, activity_two) {:ok, activity_three} = CommonAPI.post(blockee, %{status: "hey! @#{blocker.nickname}"}) - assert_receive {:render_with_user, _, _, ^activity_three} + assert_receive {:render_with_user, _, _, ^activity_three, _} assert Streamer.filtered_by_user?(blocker, activity_three) end end @@ -805,7 +805,7 @@ defmodule Pleroma.Web.StreamerTest do visibility: "private" }) - assert_receive {:render_with_user, _, _, ^activity} + assert_receive {:render_with_user, _, _, ^activity, _} refute Streamer.filtered_by_user?(user_a, activity) end end @@ -823,7 +823,7 @@ defmodule Pleroma.Web.StreamerTest do Streamer.get_topic_and_add_socket("user", user1, user1_token) {:ok, announce_activity} = CommonAPI.repeat(create_activity.id, user2) - assert_receive {:render_with_user, _, _, ^announce_activity} + assert_receive {:render_with_user, _, _, ^announce_activity, _} assert Streamer.filtered_by_user?(user1, announce_activity) end @@ -839,7 +839,7 @@ defmodule Pleroma.Web.StreamerTest do Streamer.get_topic_and_add_socket("user", user1, user1_token) {:ok, _announce_activity} = CommonAPI.repeat(create_activity.id, user2) - assert_receive {:render_with_user, _, "notification.json", notif} + assert_receive {:render_with_user, _, "notification.json", notif, _} assert Streamer.filtered_by_user?(user1, notif) end @@ -855,7 +855,7 @@ defmodule Pleroma.Web.StreamerTest do Streamer.get_topic_and_add_socket("user", user1, user1_token) {:ok, _favorite_activity} = CommonAPI.favorite(user2, create_activity.id) - assert_receive {:render_with_user, _, "notification.json", notif} + assert_receive {:render_with_user, _, "notification.json", notif, _} refute Streamer.filtered_by_user?(user1, notif) end end @@ -870,7 +870,7 @@ defmodule Pleroma.Web.StreamerTest do {:ok, activity} = CommonAPI.post(user, %{status: "super hot take"}) {:ok, _} = CommonAPI.add_mute(user2, activity) - assert_receive {:render_with_user, _, _, ^activity} + assert_receive {:render_with_user, _, _, ^activity, _} assert Streamer.filtered_by_user?(user2, activity) end end @@ -912,7 +912,7 @@ defmodule Pleroma.Web.StreamerTest do }) create_activity_id = create_activity.id - assert_receive {:render_with_user, _, _, ^create_activity} + assert_receive {:render_with_user, _, _, ^create_activity, _} assert_receive {:text, received_conversation1} assert %{"event" => "conversation", "payload" => _} = Jason.decode!(received_conversation1) @@ -947,8 +947,8 @@ defmodule Pleroma.Web.StreamerTest do visibility: "direct" }) - assert_receive {:render_with_user, _, _, ^create_activity} - assert_receive {:render_with_user, _, _, ^create_activity2} + assert_receive {:render_with_user, _, _, ^create_activity, _} + assert_receive {:render_with_user, _, _, ^create_activity2, _} assert_receive {:text, received_conversation1} assert %{"event" => "conversation", "payload" => _} = Jason.decode!(received_conversation1) assert_receive {:text, received_conversation1} @@ -977,7 +977,7 @@ defmodule Pleroma.Web.StreamerTest do receive do {StreamerTest, :ready} -> - assert_receive {:render_with_user, _, "update.json", _} + assert_receive {:render_with_user, _, "update.json", _, _} receive do {StreamerTest, :revoked} -> finalize.()