From 3227ebf1e1e46c888921d11e13d75137a50436f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9l=C3=A8ne?= Date: Wed, 10 Aug 2022 04:21:28 +0200 Subject: [PATCH 01/24] CommonFixes: more predictable context generation `context` fields for objects and activities can now be generated based on the object/activity `inReplyTo` field or its ActivityPub ID, as a fallback method in cases where `context` fields are missing for incoming activities and objects. --- .../object_validators/common_fixes.ex | 5 ++- .../mk.absturztau.be-93e7nm8wqg-activity.json | 1 + .../transmogrifier/note_handling_test.exs | 38 +++++++++++++++++++ test/support/http_request_mock.ex | 17 +++++++++ 4 files changed, 60 insertions(+), 1 deletion(-) create mode 100644 test/fixtures/tesla_mock/mk.absturztau.be-93e7nm8wqg-activity.json diff --git a/lib/pleroma/web/activity_pub/object_validators/common_fixes.ex b/lib/pleroma/web/activity_pub/object_validators/common_fixes.ex index 6fa2bbb99..be7df3fb0 100644 --- a/lib/pleroma/web/activity_pub/object_validators/common_fixes.ex +++ b/lib/pleroma/web/activity_pub/object_validators/common_fixes.ex @@ -22,7 +22,10 @@ def cast_and_filter_recipients(message, field, follower_collection, field_fallba end def fix_object_defaults(data) do - context = Utils.maybe_create_context(data["context"] || data["conversation"]) + context = + Utils.maybe_create_context( + data["context"] || data["conversation"] || data["inReplyTo"] || data["id"] + ) %User{follower_address: follower_collection} = User.get_cached_by_ap_id(data["attributedTo"]) diff --git a/test/fixtures/tesla_mock/mk.absturztau.be-93e7nm8wqg-activity.json b/test/fixtures/tesla_mock/mk.absturztau.be-93e7nm8wqg-activity.json new file mode 100644 index 000000000..b45ab78e4 --- /dev/null +++ b/test/fixtures/tesla_mock/mk.absturztau.be-93e7nm8wqg-activity.json @@ -0,0 +1 @@ +{"@context":["https://www.w3.org/ns/activitystreams","https://w3id.org/security/v1",{"manuallyApprovesFollowers":"as:manuallyApprovesFollowers","sensitive":"as:sensitive","Hashtag":"as:Hashtag","quoteUrl":"as:quoteUrl","toot":"http://joinmastodon.org/ns#","Emoji":"toot:Emoji","featured":"toot:featured","discoverable":"toot:discoverable","schema":"http://schema.org#","PropertyValue":"schema:PropertyValue","value":"schema:value","misskey":"https://misskey-hub.net/ns#","_misskey_content":"misskey:_misskey_content","_misskey_quote":"misskey:_misskey_quote","_misskey_reaction":"misskey:_misskey_reaction","_misskey_votes":"misskey:_misskey_votes","_misskey_talk":"misskey:_misskey_talk","isCat":"misskey:isCat","vcard":"http://www.w3.org/2006/vcard/ns#"}],"id":"https://mk.absturztau.be/notes/93e7nm8wqg/activity","actor":"https://mk.absturztau.be/users/8ozbzjs3o8","type":"Create","published":"2022-08-01T11:06:49.568Z","object":{"id":"https://mk.absturztau.be/notes/93e7nm8wqg","type":"Note","attributedTo":"https://mk.absturztau.be/users/8ozbzjs3o8","summary":null,"content":"

meow

","_misskey_content":"meow","published":"2022-08-01T11:06:49.568Z","to":["https://www.w3.org/ns/activitystreams#Public"],"cc":["https://mk.absturztau.be/users/8ozbzjs3o8/followers"],"inReplyTo":null,"attachment":[],"sensitive":false,"tag":[]},"to":["https://www.w3.org/ns/activitystreams#Public"],"cc":["https://mk.absturztau.be/users/8ozbzjs3o8/followers"]} \ No newline at end of file diff --git a/test/pleroma/web/activity_pub/transmogrifier/note_handling_test.exs b/test/pleroma/web/activity_pub/transmogrifier/note_handling_test.exs index 9faee7aa3..9ca12c747 100644 --- a/test/pleroma/web/activity_pub/transmogrifier/note_handling_test.exs +++ b/test/pleroma/web/activity_pub/transmogrifier/note_handling_test.exs @@ -783,4 +783,42 @@ test "quote fetching should stop after n levels", _ do } = Transmogrifier.fix_quote_url(note) end end + + test "the standalone note uses its own ID when context is missing" do + insert(:user, ap_id: "https://mk.absturztau.be/users/8ozbzjs3o8") + + activity = + "test/fixtures/tesla_mock/mk.absturztau.be-93e7nm8wqg-activity.json" + |> File.read!() + |> Jason.decode!() + + {:ok, %Activity{} = modified} = Transmogrifier.handle_incoming(activity) + object = Object.normalize(modified, fetch: false) + + assert object.data["context"] == object.data["id"] + assert modified.data["context"] == object.data["id"] + end + + test "the reply note uses its parent's ID when context is missing and reply is unreachable" do + insert(:user, ap_id: "https://mk.absturztau.be/users/8ozbzjs3o8") + + activity = + "test/fixtures/tesla_mock/mk.absturztau.be-93e7nm8wqg-activity.json" + |> File.read!() + |> Jason.decode!() + + object = + activity["object"] + |> Map.put("inReplyTo", "https://404.site/object/went-to-buy-milk") + + activity = + activity + |> Map.put("object", object) + + {:ok, %Activity{} = modified} = Transmogrifier.handle_incoming(activity) + object = Object.normalize(modified, fetch: false) + + assert object.data["context"] == object.data["inReplyTo"] + assert modified.data["context"] == object.data["inReplyTo"] + end end diff --git a/test/support/http_request_mock.ex b/test/support/http_request_mock.ex index ab44c489b..18fec3e4b 100644 --- a/test/support/http_request_mock.ex +++ b/test/support/http_request_mock.ex @@ -1085,6 +1085,14 @@ def get("http://404.site" <> _, _, _, _) do }} end + def get("https://404.site" <> _, _, _, _) do + {:ok, + %Tesla.Env{ + status: 404, + body: "" + }} + end + def get( "https://zetsubou.xn--q9jyb4c/.well-known/webfinger?resource=acct:lain@zetsubou.xn--q9jyb4c", _, @@ -1399,6 +1407,15 @@ def get("https://mk.absturztau.be/notes/93e7nm8wqg", _, _, _) do }} end + def get("https://mk.absturztau.be/notes/93e7nm8wqg/activity", _, _, _) do + {:ok, + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/tesla_mock/mk.absturztau.be-93e7nm8wqg-activity.json"), + headers: activitypub_object_headers() + }} + end + def get("https://p.helene.moe/objects/fd5910ac-d9dc-412e-8d1d-914b203296c4", _, _, _) do {:ok, %Tesla.Env{ From 5ef7c15d92379c730b37696c385da1a91cb5f50b Mon Sep 17 00:00:00 2001 From: Tusooa Zhu Date: Sat, 27 Aug 2022 19:34:56 -0400 Subject: [PATCH 02/24] Make local-only posts stream in local timeline --- lib/pleroma/activity/ir/topics.ex | 17 +++++++- test/pleroma/activity/ir/topics_test.exs | 53 ++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 1 deletion(-) diff --git a/lib/pleroma/activity/ir/topics.ex b/lib/pleroma/activity/ir/topics.ex index 7a603a615..9936ef917 100644 --- a/lib/pleroma/activity/ir/topics.ex +++ b/lib/pleroma/activity/ir/topics.ex @@ -31,6 +31,10 @@ defp visibility_tags(object, activity) do end |> item_creation_tags(object, activity) + "local" -> + ["public:local"] + |> item_creation_tags(object, activity) + "direct" -> ["direct"] @@ -63,7 +67,18 @@ defp remote_topics(_), do: [] defp attachment_topics(%{data: %{"attachment" => []}}, _act), do: [] - defp attachment_topics(_object, %{local: true}), do: ["public:media", "public:local:media"] + defp attachment_topics(_object, %{local: true} = activity) do + case Visibility.get_visibility(activity) do + "public" -> + ["public:media", "public:local:media"] + + "local" -> + ["public:local:media"] + + _ -> + [] + end + end defp attachment_topics(_object, %{actor: actor}) when is_binary(actor), do: ["public:media", "public:remote:media:" <> URI.parse(actor).host] diff --git a/test/pleroma/activity/ir/topics_test.exs b/test/pleroma/activity/ir/topics_test.exs index 9c8e5d932..326c24893 100644 --- a/test/pleroma/activity/ir/topics_test.exs +++ b/test/pleroma/activity/ir/topics_test.exs @@ -114,6 +114,36 @@ test "local action doesn't produce public:remote topic", %{activity: activity} d end end + describe "local-public visibility create events" do + setup do + activity = %Activity{ + object: %Object{data: %{"attachment" => []}}, + data: %{"type" => "Create", "to" => [Pleroma.Web.ActivityPub.Utils.as_local_public()]} + } + + {:ok, activity: activity} + end + + test "doesn't produce public topics", %{activity: activity} do + topics = Topics.get_activity_topics(activity) + + refute Enum.member?(topics, "public") + end + + test "produces public:local topics", %{activity: activity} do + topics = Topics.get_activity_topics(activity) + + assert Enum.member?(topics, "public:local") + end + + test "with no attachments doesn't produce public:media topics", %{activity: activity} do + topics = Topics.get_activity_topics(activity) + + refute Enum.member?(topics, "public:media") + refute Enum.member?(topics, "public:local:media") + end + end + describe "public visibility create events with attachments" do setup do activity = %Activity{ @@ -152,6 +182,29 @@ test "non-local action produces public:remote:media topic", %{activity: activity end end + describe "local-public visibility create events with attachments" do + setup do + activity = %Activity{ + object: %Object{data: %{"attachment" => ["foo"]}}, + data: %{"type" => "Create", "to" => [Pleroma.Web.ActivityPub.Utils.as_local_public()]} + } + + {:ok, activity: activity} + end + + test "do not produce public:media topics", %{activity: activity} do + topics = Topics.get_activity_topics(activity) + + refute Enum.member?(topics, "public:media") + end + + test "produces public:local:media topics", %{activity: activity} do + topics = Topics.get_activity_topics(activity) + + assert Enum.member?(topics, "public:local:media") + end + end + describe "non-public visibility" do test "produces direct topic" do activity = %Activity{object: %Object{data: %{"type" => "Note"}}, data: %{"to" => []}} From fd38756e923e35006362eb7fffc7ed637a77644e Mon Sep 17 00:00:00 2001 From: Tusooa Zhu Date: Wed, 31 Aug 2022 15:57:06 -0400 Subject: [PATCH 03/24] Do not stream out Announces to public timelines --- lib/pleroma/activity/ir/topics.ex | 6 +++++- test/pleroma/activity/ir/topics_test.exs | 27 ++++++++++++++++++++++-- 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/lib/pleroma/activity/ir/topics.ex b/lib/pleroma/activity/ir/topics.ex index 9936ef917..7ff4ce7c6 100644 --- a/lib/pleroma/activity/ir/topics.ex +++ b/lib/pleroma/activity/ir/topics.ex @@ -21,7 +21,7 @@ defp generate_topics(object, activity) do ["user", "list"] ++ visibility_tags(object, activity) end - defp visibility_tags(object, activity) do + defp visibility_tags(object, %{data: %{"type" => "Create"}} = activity) do case Visibility.get_visibility(activity) do "public" -> if activity.local do @@ -43,6 +43,10 @@ defp visibility_tags(object, activity) do end end + defp visibility_tags(_object, _activity) do + [] + end + defp item_creation_tags(tags, object, %{data: %{"type" => "Create"}} = activity) do tags ++ remote_topics(activity) ++ hashtags_to_topics(object) ++ attachment_topics(object, activity) diff --git a/test/pleroma/activity/ir/topics_test.exs b/test/pleroma/activity/ir/topics_test.exs index 326c24893..03e323469 100644 --- a/test/pleroma/activity/ir/topics_test.exs +++ b/test/pleroma/activity/ir/topics_test.exs @@ -35,7 +35,7 @@ test "always add user and list topics" do setup do activity = %Activity{ object: %Object{data: %{"type" => "Note"}}, - data: %{"to" => [Pleroma.Constants.as_public()]} + data: %{"to" => [Pleroma.Constants.as_public()], "type" => "Create"} } {:ok, activity: activity} @@ -114,6 +114,25 @@ test "local action doesn't produce public:remote topic", %{activity: activity} d end end + describe "public visibility Announces" do + setup do + activity = %Activity{ + object: %Object{data: %{"attachment" => []}}, + data: %{"type" => "Announce", "to" => [Pleroma.Constants.as_public()]} + } + + {:ok, activity: activity} + end + + test "does not generate public topics", %{activity: activity} do + topics = Topics.get_activity_topics(activity) + + refute "public" in topics + refute "public:remote" in topics + refute "public:local" in topics + end + end + describe "local-public visibility create events" do setup do activity = %Activity{ @@ -207,7 +226,11 @@ test "produces public:local:media topics", %{activity: activity} do describe "non-public visibility" do test "produces direct topic" do - activity = %Activity{object: %Object{data: %{"type" => "Note"}}, data: %{"to" => []}} + activity = %Activity{ + object: %Object{data: %{"type" => "Note"}}, + data: %{"to" => [], "type" => "Create"} + } + topics = Topics.get_activity_topics(activity) assert Enum.member?(topics, "direct") From 7b9cc9a9b0755a76130578ccd6ffdb57ffcb221e Mon Sep 17 00:00:00 2001 From: Tusooa Zhu Date: Wed, 31 Aug 2022 22:14:54 -0400 Subject: [PATCH 04/24] Exclude Announce instead of restricting to Create in visibility_tags --- lib/pleroma/activity/ir/topics.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/activity/ir/topics.ex b/lib/pleroma/activity/ir/topics.ex index 7ff4ce7c6..bdbf4a285 100644 --- a/lib/pleroma/activity/ir/topics.ex +++ b/lib/pleroma/activity/ir/topics.ex @@ -21,7 +21,7 @@ defp generate_topics(object, activity) do ["user", "list"] ++ visibility_tags(object, activity) end - defp visibility_tags(object, %{data: %{"type" => "Create"}} = activity) do + defp visibility_tags(object, %{data: %{"type" => type}} = activity) when type != "Announce" do case Visibility.get_visibility(activity) do "public" -> if activity.local do From dfd6c9680887a511d285744f91d761f04c94c92c Mon Sep 17 00:00:00 2001 From: Tusooa Zhu Date: Thu, 1 Sep 2022 07:33:58 -0400 Subject: [PATCH 05/24] Fix SideEffectsTest --- test/pleroma/web/activity_pub/side_effects_test.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/pleroma/web/activity_pub/side_effects_test.exs b/test/pleroma/web/activity_pub/side_effects_test.exs index ee664bb8f..1c721405e 100644 --- a/test/pleroma/web/activity_pub/side_effects_test.exs +++ b/test/pleroma/web/activity_pub/side_effects_test.exs @@ -733,7 +733,7 @@ test "it streams out the announce", %{announce: announce} do {:ok, announce, _} = SideEffects.handle(announce) assert called( - Pleroma.Web.Streamer.stream(["user", "list", "public", "public:local"], announce) + Pleroma.Web.Streamer.stream(["user", "list"], announce) ) assert called(Pleroma.Web.Push.send(:_)) From 2a290cb331bb5d914a418cfa4242d22782806a42 Mon Sep 17 00:00:00 2001 From: Tusooa Zhu Date: Fri, 2 Sep 2022 22:58:35 -0400 Subject: [PATCH 06/24] Lint --- test/pleroma/web/activity_pub/side_effects_test.exs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/test/pleroma/web/activity_pub/side_effects_test.exs b/test/pleroma/web/activity_pub/side_effects_test.exs index 1c721405e..94b6a11b3 100644 --- a/test/pleroma/web/activity_pub/side_effects_test.exs +++ b/test/pleroma/web/activity_pub/side_effects_test.exs @@ -732,9 +732,7 @@ test "it streams out the announce", %{announce: announce} do ]) do {:ok, announce, _} = SideEffects.handle(announce) - assert called( - Pleroma.Web.Streamer.stream(["user", "list"], announce) - ) + assert called(Pleroma.Web.Streamer.stream(["user", "list"], announce)) assert called(Pleroma.Web.Push.send(:_)) end From 997551bac9fc8189f40675c2ff0b312c78b2ba59 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Mon, 19 Dec 2022 14:40:08 -0500 Subject: [PATCH 07/24] Fix TwitterCard meta tags MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TwitterCard meta tags are supposed to use the attributes "name" and "content". OpenGraph tags use the attributes "property" and "content". Twitter itself is smart enough to detect broken meta tags and discover the TwitterCard using "property" and "content", but other platforms that only implement parsing of TwitterCards and not OpenGraph may fail to correctly detect the tags as they're under the wrong attributes. > "Open Graph protocol also specifies the use of property and content attributes for markup while > Twitter cards use name and content. Twitter’s parser will fall back to using property and content, > so there is no need to modify existing Open Graph protocol markup if it already exists." [0] [0] https://developer.twitter.com/en/docs/twitter-for-websites/cards/guides/getting-started --- .../web/metadata/providers/twitter_card.ex | 43 +++++++------- .../metadata/providers/twitter_card_test.exs | 56 +++++++++---------- 2 files changed, 49 insertions(+), 50 deletions(-) diff --git a/lib/pleroma/web/metadata/providers/twitter_card.ex b/lib/pleroma/web/metadata/providers/twitter_card.ex index 79183df86..b2497d14e 100644 --- a/lib/pleroma/web/metadata/providers/twitter_card.ex +++ b/lib/pleroma/web/metadata/providers/twitter_card.ex @@ -20,12 +20,12 @@ def build_tags(%{activity_id: id, object: object, user: user}) do [ title_tag(user), - {:meta, [property: "twitter:description", content: scrubbed_content], []} + {:meta, [name: "twitter:description", content: scrubbed_content], []} ] ++ if attachments == [] or Metadata.activity_nsfw?(object) do [ image_tag(user), - {:meta, [property: "twitter:card", content: "summary"], []} + {:meta, [name: "twitter:card", content: "summary"], []} ] else attachments @@ -37,20 +37,19 @@ def build_tags(%{user: user}) do with truncated_bio = Utils.scrub_html_and_truncate(user.bio) do [ title_tag(user), - {:meta, [property: "twitter:description", content: truncated_bio], []}, + {:meta, [name: "twitter:description", content: truncated_bio], []}, image_tag(user), - {:meta, [property: "twitter:card", content: "summary"], []} + {:meta, [name: "twitter:card", content: "summary"], []} ] end end defp title_tag(user) do - {:meta, [property: "twitter:title", content: Utils.user_name_string(user)], []} + {:meta, [name: "twitter:title", content: Utils.user_name_string(user)], []} end def image_tag(user) do - {:meta, [property: "twitter:image", content: MediaProxy.preview_url(User.avatar_url(user))], - []} + {:meta, [name: "twitter:image", content: MediaProxy.preview_url(User.avatar_url(user))], []} end defp build_attachments(id, %{data: %{"attachment" => attachments}}) do @@ -60,10 +59,10 @@ defp build_attachments(id, %{data: %{"attachment" => attachments}}) do case Utils.fetch_media_type(@media_types, url["mediaType"]) do "audio" -> [ - {:meta, [property: "twitter:card", content: "player"], []}, - {:meta, [property: "twitter:player:width", content: "480"], []}, - {:meta, [property: "twitter:player:height", content: "80"], []}, - {:meta, [property: "twitter:player", content: player_url(id)], []} + {:meta, [name: "twitter:card", content: "player"], []}, + {:meta, [name: "twitter:player:width", content: "480"], []}, + {:meta, [name: "twitter:player:height", content: "80"], []}, + {:meta, [name: "twitter:player", content: player_url(id)], []} | acc ] @@ -74,10 +73,10 @@ defp build_attachments(id, %{data: %{"attachment" => attachments}}) do # workaround. "image" -> [ - {:meta, [property: "twitter:card", content: "summary_large_image"], []}, + {:meta, [name: "twitter:card", content: "summary_large_image"], []}, {:meta, [ - property: "twitter:player", + name: "twitter:player", content: MediaProxy.url(url["href"]) ], []} | acc @@ -90,14 +89,14 @@ defp build_attachments(id, %{data: %{"attachment" => attachments}}) do width = url["width"] || 480 [ - {:meta, [property: "twitter:card", content: "player"], []}, - {:meta, [property: "twitter:player", content: player_url(id)], []}, - {:meta, [property: "twitter:player:width", content: "#{width}"], []}, - {:meta, [property: "twitter:player:height", content: "#{height}"], []}, - {:meta, [property: "twitter:player:stream", content: MediaProxy.url(url["href"])], + {:meta, [name: "twitter:card", content: "player"], []}, + {:meta, [name: "twitter:player", content: player_url(id)], []}, + {:meta, [name: "twitter:player:width", content: "#{width}"], []}, + {:meta, [name: "twitter:player:height", content: "#{height}"], []}, + {:meta, [name: "twitter:player:stream", content: MediaProxy.url(url["href"])], []}, - {:meta, - [property: "twitter:player:stream:content_type", content: url["mediaType"]], []} + {:meta, [name: "twitter:player:stream:content_type", content: url["mediaType"]], + []} | acc ] @@ -123,8 +122,8 @@ defp maybe_add_dimensions(metadata, url) do !is_nil(url["height"]) && !is_nil(url["width"]) -> metadata ++ [ - {:meta, [property: "twitter:player:width", content: "#{url["width"]}"], []}, - {:meta, [property: "twitter:player:height", content: "#{url["height"]}"], []} + {:meta, [name: "twitter:player:width", content: "#{url["width"]}"], []}, + {:meta, [name: "twitter:player:height", content: "#{url["height"]}"], []} ] true -> diff --git a/test/pleroma/web/metadata/providers/twitter_card_test.exs b/test/pleroma/web/metadata/providers/twitter_card_test.exs index 5d7ad08ef..731447094 100644 --- a/test/pleroma/web/metadata/providers/twitter_card_test.exs +++ b/test/pleroma/web/metadata/providers/twitter_card_test.exs @@ -22,10 +22,10 @@ test "it renders twitter card for user info" do res = TwitterCard.build_tags(%{user: user}) assert res == [ - {:meta, [property: "twitter:title", content: Utils.user_name_string(user)], []}, - {:meta, [property: "twitter:description", content: "born 19 March 1994"], []}, - {:meta, [property: "twitter:image", content: avatar_url], []}, - {:meta, [property: "twitter:card", content: "summary"], []} + {:meta, [name: "twitter:title", content: Utils.user_name_string(user)], []}, + {:meta, [name: "twitter:description", content: "born 19 March 1994"], []}, + {:meta, [name: "twitter:image", content: avatar_url], []}, + {:meta, [name: "twitter:card", content: "summary"], []} ] end @@ -47,11 +47,11 @@ test "it uses summary twittercard if post has no attachment" do result = TwitterCard.build_tags(%{object: note, user: user, activity_id: activity.id}) assert [ - {:meta, [property: "twitter:title", content: Utils.user_name_string(user)], []}, - {:meta, [property: "twitter:description", content: "pleroma in a nutshell"], []}, - {:meta, [property: "twitter:image", content: "http://localhost:4001/images/avi.png"], + {:meta, [name: "twitter:title", content: Utils.user_name_string(user)], []}, + {:meta, [name: "twitter:description", content: "pleroma in a nutshell"], []}, + {:meta, [name: "twitter:image", content: "http://localhost:4001/images/avi.png"], []}, - {:meta, [property: "twitter:card", content: "summary"], []} + {:meta, [name: "twitter:card", content: "summary"], []} ] == result end @@ -73,15 +73,15 @@ test "it uses summary as description if post has one" do result = TwitterCard.build_tags(%{object: note, user: user, activity_id: activity.id}) assert [ - {:meta, [property: "twitter:title", content: Utils.user_name_string(user)], []}, + {:meta, [name: "twitter:title", content: Utils.user_name_string(user)], []}, {:meta, [ - property: "twitter:description", + name: "twitter:description", content: "Public service announcement on caffeine consumption" ], []}, - {:meta, [property: "twitter:image", content: "http://localhost:4001/images/avi.png"], + {:meta, [name: "twitter:image", content: "http://localhost:4001/images/avi.png"], []}, - {:meta, [property: "twitter:card", content: "summary"], []} + {:meta, [name: "twitter:card", content: "summary"], []} ] == result end @@ -123,11 +123,11 @@ test "it renders avatar not attachment if post is nsfw and unfurl_nsfw is disabl result = TwitterCard.build_tags(%{object: note, user: user, activity_id: activity.id}) assert [ - {:meta, [property: "twitter:title", content: Utils.user_name_string(user)], []}, - {:meta, [property: "twitter:description", content: "pleroma in a nutshell"], []}, - {:meta, [property: "twitter:image", content: "http://localhost:4001/images/avi.png"], + {:meta, [name: "twitter:title", content: Utils.user_name_string(user)], []}, + {:meta, [name: "twitter:description", content: "pleroma in a nutshell"], []}, + {:meta, [name: "twitter:image", content: "http://localhost:4001/images/avi.png"], []}, - {:meta, [property: "twitter:card", content: "summary"], []} + {:meta, [name: "twitter:card", content: "summary"], []} ] == result end @@ -179,26 +179,26 @@ test "it renders supported types of attachments and skips unknown types" do result = TwitterCard.build_tags(%{object: note, user: user, activity_id: activity.id}) assert [ - {:meta, [property: "twitter:title", content: Utils.user_name_string(user)], []}, - {:meta, [property: "twitter:description", content: "pleroma in a nutshell"], []}, - {:meta, [property: "twitter:card", content: "summary_large_image"], []}, - {:meta, [property: "twitter:player", content: "https://pleroma.gov/tenshi.png"], []}, - {:meta, [property: "twitter:player:width", content: "1280"], []}, - {:meta, [property: "twitter:player:height", content: "1024"], []}, - {:meta, [property: "twitter:card", content: "player"], []}, + {:meta, [name: "twitter:title", content: Utils.user_name_string(user)], []}, + {:meta, [name: "twitter:description", content: "pleroma in a nutshell"], []}, + {:meta, [name: "twitter:card", content: "summary_large_image"], []}, + {:meta, [name: "twitter:player", content: "https://pleroma.gov/tenshi.png"], []}, + {:meta, [name: "twitter:player:width", content: "1280"], []}, + {:meta, [name: "twitter:player:height", content: "1024"], []}, + {:meta, [name: "twitter:card", content: "player"], []}, {:meta, [ - property: "twitter:player", + name: "twitter:player", content: Router.Helpers.o_status_url(Endpoint, :notice_player, activity.id) ], []}, - {:meta, [property: "twitter:player:width", content: "800"], []}, - {:meta, [property: "twitter:player:height", content: "600"], []}, + {:meta, [name: "twitter:player:width", content: "800"], []}, + {:meta, [name: "twitter:player:height", content: "600"], []}, {:meta, [ - property: "twitter:player:stream", + name: "twitter:player:stream", content: "https://pleroma.gov/about/juche.webm" ], []}, - {:meta, [property: "twitter:player:stream:content_type", content: "video/webm"], []} + {:meta, [name: "twitter:player:stream:content_type", content: "video/webm"], []} ] == result end end From 651979217a4b582793294363a9cb3a7bdd451bbb Mon Sep 17 00:00:00 2001 From: tusooa Date: Wed, 14 Dec 2022 01:04:42 -0500 Subject: [PATCH 08/24] Fix failure when registering a user with no email when approval required --- lib/pleroma/user.ex | 6 +++++- test/pleroma/user_test.exs | 15 +++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 273bdf337..119e80a4f 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -876,7 +876,7 @@ def post_register_action(%User{is_approved: true, is_confirmed: true} = user) do end end - defp send_user_approval_email(user) do + defp send_user_approval_email(%User{email: email} = user) when is_binary(email) do user |> Pleroma.Emails.UserEmail.approval_pending_email() |> Pleroma.Emails.Mailer.deliver_async() @@ -884,6 +884,10 @@ defp send_user_approval_email(user) do {:ok, :enqueued} end + defp send_user_approval_email(_user) do + {:ok, :skipped} + end + defp send_admin_approval_emails(user) do all_superusers() |> Enum.filter(fn user -> not is_nil(user.email) end) diff --git a/test/pleroma/user_test.exs b/test/pleroma/user_test.exs index f35a98f08..c33528a67 100644 --- a/test/pleroma/user_test.exs +++ b/test/pleroma/user_test.exs @@ -557,6 +557,21 @@ test "it fails gracefully with invalid email config" do refute_email_sent() end + test "it works when the registering user does not provide an email" do + clear_config([Pleroma.Emails.Mailer, :enabled], false) + clear_config([:instance, :account_activation_required], false) + clear_config([:instance, :account_approval_required], true) + + cng = User.register_changeset(%User{}, @full_user_data |> Map.put(:email, "")) + + # The user is still created + assert {:ok, %User{nickname: "nick"}} = User.register(cng) + + # No emails are sent + ObanHelpers.perform_all() + refute_email_sent() + end + test "it requires an email, name, nickname and password, bio is optional when account_activation_required is enabled" do clear_config([:instance, :account_activation_required], true) From 1268dbc5627bd035d7a933316adba2040ec5081b Mon Sep 17 00:00:00 2001 From: tusooa Date: Sun, 15 Jan 2023 18:41:36 -0500 Subject: [PATCH 09/24] Fix type of admin_account.is_confirmed --- lib/pleroma/web/api_spec/operations/admin/status_operation.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/web/api_spec/operations/admin/status_operation.ex b/lib/pleroma/web/api_spec/operations/admin/status_operation.ex index d25ab5247..9290ab09d 100644 --- a/lib/pleroma/web/api_spec/operations/admin/status_operation.ex +++ b/lib/pleroma/web/api_spec/operations/admin/status_operation.ex @@ -143,7 +143,7 @@ def admin_account do } }, tags: %Schema{type: :string}, - is_confirmed: %Schema{type: :string} + is_confirmed: %Schema{type: :boolean} } } end From 05e80d1879d4472d23edeb9dec09732f45beb4ba Mon Sep 17 00:00:00 2001 From: tusooa Date: Wed, 18 Jan 2023 18:36:52 -0500 Subject: [PATCH 10/24] Fix block_from_stranger setting --- lib/pleroma/notification.ex | 2 +- test/pleroma/notification_test.exs | 26 ++++++++++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/lib/pleroma/notification.ex b/lib/pleroma/notification.ex index 3995be01f..1989bb50c 100644 --- a/lib/pleroma/notification.ex +++ b/lib/pleroma/notification.ex @@ -695,7 +695,7 @@ def skip?( cond do opts[:type] == "poll" -> false user.ap_id == actor -> false - !User.following?(follower, user) -> true + !User.following?(user, follower) -> true true -> false end end diff --git a/test/pleroma/notification_test.exs b/test/pleroma/notification_test.exs index 721836a2c..6bc6dff1a 100644 --- a/test/pleroma/notification_test.exs +++ b/test/pleroma/notification_test.exs @@ -328,6 +328,32 @@ test "it disables notifications from strangers" do refute Notification.create_notification(activity, followed) end + test "it disables notifications from non-followees" do + follower = insert(:user) + + followed = + insert(:user, + notification_settings: %Pleroma.User.NotificationSetting{block_from_strangers: true} + ) + + CommonAPI.follow(follower, followed) + {:ok, activity} = CommonAPI.post(follower, %{status: "hey @#{followed.nickname}"}) + refute Notification.create_notification(activity, followed) + end + + test "it allows notifications from followees" do + poster = insert(:user) + + receiver = + insert(:user, + notification_settings: %Pleroma.User.NotificationSetting{block_from_strangers: true} + ) + + CommonAPI.follow(receiver, poster) + {:ok, activity} = CommonAPI.post(poster, %{status: "hey @#{receiver.nickname}"}) + assert Notification.create_notification(activity, receiver) + end + test "it doesn't create a notification for user if he is the activity author" do activity = insert(:note_activity) author = User.get_cached_by_ap_id(activity.data["actor"]) From 5adce547d0f68c9762246fc2c7da9a1979601e31 Mon Sep 17 00:00:00 2001 From: Alexander Tumin Date: Tue, 7 Feb 2023 15:30:07 +0300 Subject: [PATCH 11/24] Require related object for notifications to filter on content --- lib/pleroma/notification.ex | 1 + test/pleroma/notification_test.exs | 27 +++++++++++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/lib/pleroma/notification.ex b/lib/pleroma/notification.ex index 1989bb50c..885d61233 100644 --- a/lib/pleroma/notification.ex +++ b/lib/pleroma/notification.ex @@ -195,6 +195,7 @@ defp exclude_filtered(query, user) do from([_n, a, o] in query, where: fragment("not(?->>'content' ~* ?)", o.data, ^regex) or + fragment("?->>'content' is null", o.data) or fragment("?->>'actor' = ?", o.data, ^user.ap_id) ) end diff --git a/test/pleroma/notification_test.exs b/test/pleroma/notification_test.exs index 6bc6dff1a..4905cb7eb 100644 --- a/test/pleroma/notification_test.exs +++ b/test/pleroma/notification_test.exs @@ -1251,5 +1251,32 @@ test "it returns notifications about favorites with filtered word", %{user: user assert length(Notification.for_user(user)) == 1 end + + test "it returns notifications when related object is without content and filters are defined", + %{user: user} do + followed_user = insert(:user, is_locked: true) + + insert(:filter, user: followed_user, phrase: "test", hide: true) + + {:ok, _, _, _activity} = CommonAPI.follow(user, followed_user) + refute FollowingRelationship.following?(user, followed_user) + assert [notification] = Notification.for_user(followed_user) + + assert %{type: "follow_request"} = + NotificationView.render("show.json", %{ + notification: notification, + for: followed_user + }) + + assert {:ok, _} = CommonAPI.accept_follow_request(user, followed_user) + + assert [notification] = Notification.for_user(followed_user) + + assert %{type: "follow"} = + NotificationView.render("show.json", %{ + notification: notification, + for: followed_user + }) + end end end From d6271e7613028e9a32e7a04eaf76c5b23de22e6d Mon Sep 17 00:00:00 2001 From: kPherox Date: Wed, 1 Feb 2023 20:24:58 +0000 Subject: [PATCH 12/24] feat: build rel me tags with profile fields --- lib/pleroma/web/metadata/providers/rel_me.ex | 18 +++++++++++++++--- .../web/metadata/providers/rel_me_test.exs | 15 ++++++++++++++- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/lib/pleroma/web/metadata/providers/rel_me.ex b/lib/pleroma/web/metadata/providers/rel_me.ex index f013def51..92528513a 100644 --- a/lib/pleroma/web/metadata/providers/rel_me.ex +++ b/lib/pleroma/web/metadata/providers/rel_me.ex @@ -8,12 +8,24 @@ defmodule Pleroma.Web.Metadata.Providers.RelMe do @impl Provider def build_tags(%{user: user}) do - bio_tree = Floki.parse_fragment!(user.bio) + profile_tree = + Floki.parse_fragment!(user.bio) + |> prepend_fields_tag(user.fields) - (Floki.attribute(bio_tree, "link[rel~=me]", "href") ++ - Floki.attribute(bio_tree, "a[rel~=me]", "href")) + (Floki.attribute(profile_tree, "link[rel~=me]", "href") ++ + Floki.attribute(profile_tree, "a[rel~=me]", "href")) |> Enum.map(fn link -> {:link, [rel: "me", href: link], []} end) end + + defp prepend_fields_tag(bio_tree, fields) do + fields + |> Enum.reduce(bio_tree, fn %{"value" => v}, tree -> + case Floki.parse_fragment(v) do + {:ok, [a | _]} -> [a | tree] + _ -> tree + end + end) + end end diff --git a/test/pleroma/web/metadata/providers/rel_me_test.exs b/test/pleroma/web/metadata/providers/rel_me_test.exs index 0db6e7d22..78e509a40 100644 --- a/test/pleroma/web/metadata/providers/rel_me_test.exs +++ b/test/pleroma/web/metadata/providers/rel_me_test.exs @@ -11,10 +11,23 @@ test "it renders all links with rel='me' from user bio" do bio = ~s(https://some-link.com https://another-link.com ) - user = insert(:user, %{bio: bio}) + fields = [ + %{ + "name" => "profile", + "value" => ~S(http://profile.com) + }, + %{ + "name" => "like", + "value" => ~S(http://cofe.io) + }, + %{"name" => "foo", "value" => "bar"} + ] + + user = insert(:user, %{bio: bio, fields: fields}) assert RelMe.build_tags(%{user: user}) == [ {:link, [rel: "me", href: "http://some3.com"], []}, + {:link, [rel: "me", href: "http://profile.com"], []}, {:link, [rel: "me", href: "https://another-link.com"], []} ] end From 8fb235e71b7a7fa1248d6ae5f3ce7012b2c3e72b Mon Sep 17 00:00:00 2001 From: kPherox Date: Wed, 15 Mar 2023 23:55:24 +0900 Subject: [PATCH 13/24] fix: append field values to bio before parsing --- lib/pleroma/web/metadata/providers/rel_me.ex | 14 +++++--------- .../pleroma/web/metadata/providers/rel_me_test.exs | 4 ++-- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/lib/pleroma/web/metadata/providers/rel_me.ex b/lib/pleroma/web/metadata/providers/rel_me.ex index 92528513a..cf6a3a3ab 100644 --- a/lib/pleroma/web/metadata/providers/rel_me.ex +++ b/lib/pleroma/web/metadata/providers/rel_me.ex @@ -9,8 +9,9 @@ defmodule Pleroma.Web.Metadata.Providers.RelMe do @impl Provider def build_tags(%{user: user}) do profile_tree = - Floki.parse_fragment!(user.bio) - |> prepend_fields_tag(user.fields) + user.bio + |> append_fields_tag(user.fields) + |> Floki.parse_fragment!() (Floki.attribute(profile_tree, "link[rel~=me]", "href") ++ Floki.attribute(profile_tree, "a[rel~=me]", "href")) @@ -19,13 +20,8 @@ def build_tags(%{user: user}) do end) end - defp prepend_fields_tag(bio_tree, fields) do + defp append_fields_tag(bio, fields) do fields - |> Enum.reduce(bio_tree, fn %{"value" => v}, tree -> - case Floki.parse_fragment(v) do - {:ok, [a | _]} -> [a | tree] - _ -> tree - end - end) + |> Enum.reduce(bio, fn %{"value" => v}, res -> res <> v end) end end diff --git a/test/pleroma/web/metadata/providers/rel_me_test.exs b/test/pleroma/web/metadata/providers/rel_me_test.exs index 78e509a40..fedc49789 100644 --- a/test/pleroma/web/metadata/providers/rel_me_test.exs +++ b/test/pleroma/web/metadata/providers/rel_me_test.exs @@ -27,8 +27,8 @@ test "it renders all links with rel='me' from user bio" do assert RelMe.build_tags(%{user: user}) == [ {:link, [rel: "me", href: "http://some3.com"], []}, - {:link, [rel: "me", href: "http://profile.com"], []}, - {:link, [rel: "me", href: "https://another-link.com"], []} + {:link, [rel: "me", href: "https://another-link.com"], []}, + {:link, [rel: "me", href: "http://profile.com"], []} ] end end From 79a18f761bc44b4eda083732b67e54b31b9321c7 Mon Sep 17 00:00:00 2001 From: tusooa Date: Tue, 28 Feb 2023 22:16:01 -0500 Subject: [PATCH 14/24] Allow with_relationships param for blocks --- .../api_spec/operations/account_operation.ex | 2 +- .../controllers/account_controller.ex | 7 +++- .../controllers/account_controller_test.exs | 33 +++++++++++++++++++ 3 files changed, 40 insertions(+), 2 deletions(-) diff --git a/lib/pleroma/web/api_spec/operations/account_operation.ex b/lib/pleroma/web/api_spec/operations/account_operation.ex index 00a591298..2d14316d1 100644 --- a/lib/pleroma/web/api_spec/operations/account_operation.ex +++ b/lib/pleroma/web/api_spec/operations/account_operation.ex @@ -410,7 +410,7 @@ def blocks_operation do operationId: "AccountController.blocks", description: "View your blocks. See also accounts/:id/{block,unblock}", security: [%{"oAuth" => ["read:blocks"]}], - parameters: pagination_params(), + parameters: [with_relationships_param() | pagination_params()], responses: %{ 200 => Operation.response("Accounts", "application/json", array_of_accounts()) } diff --git a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex index 057af762a..b55e843db 100644 --- a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex @@ -518,7 +518,12 @@ def blocks(%{assigns: %{user: user}} = conn, params) do conn |> add_link_headers(users) - |> render("index.json", users: users, for: user, as: :user) + |> render("index.json", + users: users, + for: user, + as: :user, + embed_relationships: embed_relationships?(params) + ) end @doc "GET /api/v1/accounts/lookup" diff --git a/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs index bbede76e9..84214e4bc 100644 --- a/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs +++ b/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs @@ -1895,6 +1895,39 @@ test "getting a list of blocks" do assert [%{"id" => ^id2}] = result end + test "list of blocks with with_relationships parameter" do + %{user: user, conn: conn} = oauth_access(["read:blocks"]) + %{id: id1} = other_user1 = insert(:user) + %{id: id2} = other_user2 = insert(:user) + %{id: id3} = other_user3 = insert(:user) + + {:ok, _, _} = User.follow(other_user1, user) + {:ok, _, _} = User.follow(other_user2, user) + {:ok, _, _} = User.follow(other_user3, user) + + {:ok, _} = User.block(user, other_user1) + {:ok, _} = User.block(user, other_user2) + {:ok, _} = User.block(user, other_user3) + + assert [ + %{ + "id" => ^id1, + "pleroma" => %{"relationship" => %{"blocking" => true, "followed_by" => false}} + }, + %{ + "id" => ^id2, + "pleroma" => %{"relationship" => %{"blocking" => true, "followed_by" => false}} + }, + %{ + "id" => ^id3, + "pleroma" => %{"relationship" => %{"blocking" => true, "followed_by" => false}} + } + ] = + conn + |> get("/api/v1/blocks?with_relationships=true") + |> json_response_and_validate_schema(200) + end + test "account lookup", %{conn: conn} do %{nickname: acct} = insert(:user, %{nickname: "nickname"}) %{nickname: acct_two} = insert(:user, %{nickname: "nickname@notlocaldoma.in"}) From 3095251e6c32f93a85d129e1bf8917d7c63ba8ba Mon Sep 17 00:00:00 2001 From: tusooa Date: Sat, 25 Mar 2023 23:20:07 -0400 Subject: [PATCH 15/24] Dedupe poll options --- lib/pleroma/web/common_api/utils.ex | 15 ++++++++--- .../controllers/status_controller_test.exs | 26 +++++++++++++++++++ 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/lib/pleroma/web/common_api/utils.ex b/lib/pleroma/web/common_api/utils.ex index 54918d13c..b44d3167c 100644 --- a/lib/pleroma/web/common_api/utils.ex +++ b/lib/pleroma/web/common_api/utils.ex @@ -144,6 +144,8 @@ def make_poll_data(%{poll: %{options: options, expires_in: expires_in}} = data) when is_list(options) do limits = Config.get([:instance, :poll_limits]) + options = options |> Enum.uniq() + with :ok <- validate_poll_expiration(expires_in, limits), :ok <- validate_poll_options_amount(options, limits), :ok <- validate_poll_options_length(options, limits) do @@ -179,10 +181,15 @@ def make_poll_data(_data) do end defp validate_poll_options_amount(options, %{max_options: max_options}) do - if Enum.count(options) > max_options do - {:error, "Poll can't contain more than #{max_options} options"} - else - :ok + cond do + Enum.count(options) < 2 -> + {:error, "Poll must contain at least 2 options"} + + Enum.count(options) > max_options -> + {:error, "Poll can't contain more than #{max_options} options"} + + true -> + :ok end end diff --git a/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs index 6f04975b8..90eda5bcb 100644 --- a/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs +++ b/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs @@ -772,6 +772,32 @@ test "scheduled poll", %{conn: conn} do assert object.data["type"] == "Question" assert length(object.data["oneOf"]) == 3 end + + test "cannot have only one option", %{conn: conn} do + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/statuses", %{ + "status" => "desu~", + "poll" => %{"options" => ["mew"], "expires_in" => 1} + }) + + %{"error" => error} = json_response_and_validate_schema(conn, 422) + assert error == "Poll must contain at least 2 options" + end + + test "cannot have only duplicated options", %{conn: conn} do + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/statuses", %{ + "status" => "desu~", + "poll" => %{"options" => ["mew", "mew"], "expires_in" => 1} + }) + + %{"error" => error} = json_response_and_validate_schema(conn, 422) + assert error == "Poll must contain at least 2 options" + end end test "get a status" do From 1def80c2e701fccb216887e4777980221307f557 Mon Sep 17 00:00:00 2001 From: tusooa Date: Sun, 26 Mar 2023 11:11:26 -0400 Subject: [PATCH 16/24] Fix existing tests --- .../mastodon_api/controllers/status_controller_test.exs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs index 90eda5bcb..fe83c96ff 100644 --- a/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs +++ b/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs @@ -674,7 +674,10 @@ test "option limit is enforced", %{conn: conn} do |> put_req_header("content-type", "application/json") |> post("/api/v1/statuses", %{ "status" => "desu~", - "poll" => %{"options" => Enum.map(0..limit, fn _ -> "desu" end), "expires_in" => 1} + "poll" => %{ + "options" => Enum.map(0..limit, fn num -> "desu #{num}" end), + "expires_in" => 1 + } }) %{"error" => error} = json_response_and_validate_schema(conn, 422) @@ -690,7 +693,7 @@ test "option character limit is enforced", %{conn: conn} do |> post("/api/v1/statuses", %{ "status" => "...", "poll" => %{ - "options" => [Enum.reduce(0..limit, "", fn _, acc -> acc <> "." end)], + "options" => [String.duplicate(".", limit + 1), "lol"], "expires_in" => 1 } }) From 37b0d774fa59962e22a24fcc9542b4db9d8ed10d Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Mon, 17 Apr 2023 21:07:08 +0200 Subject: [PATCH 17/24] UploadedMedia: Add missing disposition_type to Content-Disposition Set it to `inline` because the vast majority of what's sent is multimedia content while `attachment` would have the side-effect of triggering a download dialog. Closes: https://git.pleroma.social/pleroma/pleroma/-/issues/3114 --- lib/pleroma/web/plugs/uploaded_media.ex | 2 +- test/pleroma/web/plugs/uploaded_media_plug_test.exs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/pleroma/web/plugs/uploaded_media.ex b/lib/pleroma/web/plugs/uploaded_media.ex index 72f20e8de..0facdd445 100644 --- a/lib/pleroma/web/plugs/uploaded_media.ex +++ b/lib/pleroma/web/plugs/uploaded_media.ex @@ -37,7 +37,7 @@ def call(%{request_path: <<"/", @path, "/", file::binary>>} = conn, opts) do %{query_params: %{"name" => name}} = conn -> name = escape_header_value(name) - put_resp_header(conn, "content-disposition", "filename=\"#{name}\"") + put_resp_header(conn, "content-disposition", "inline; filename=\"#{name}\"") conn -> conn diff --git a/test/pleroma/web/plugs/uploaded_media_plug_test.exs b/test/pleroma/web/plugs/uploaded_media_plug_test.exs index c71a7e789..1bb02c4b1 100644 --- a/test/pleroma/web/plugs/uploaded_media_plug_test.exs +++ b/test/pleroma/web/plugs/uploaded_media_plug_test.exs @@ -37,7 +37,7 @@ test "sends Content-Disposition header when name param is set", %{ assert Enum.any?( conn.resp_headers, - &(&1 == {"content-disposition", "filename=\"\\\"cofe\\\".gif\""}) + &(&1 == {"content-disposition", "inline; filename=\"\\\"cofe\\\".gif\""}) ) end @@ -48,7 +48,7 @@ test "removes control characters from the Content-Disposition header", %{ assert Enum.any?( conn.resp_headers, - &(&1 == {"content-disposition", "filename=\"\\\"cofe\\\".gif\""}) + &(&1 == {"content-disposition", ~s[inline; filename="\\"cofe\\".gif"]}) ) end end From 8669a0abcb094b10cb9a0f042bc19baa1c92e60d Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Tue, 18 Apr 2023 00:07:39 +0200 Subject: [PATCH 18/24] UploadedMedia: Increase readability via ~s sigil --- lib/pleroma/web/plugs/uploaded_media.ex | 2 +- test/pleroma/web/plugs/uploaded_media_plug_test.exs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/pleroma/web/plugs/uploaded_media.ex b/lib/pleroma/web/plugs/uploaded_media.ex index 0facdd445..300c33068 100644 --- a/lib/pleroma/web/plugs/uploaded_media.ex +++ b/lib/pleroma/web/plugs/uploaded_media.ex @@ -37,7 +37,7 @@ def call(%{request_path: <<"/", @path, "/", file::binary>>} = conn, opts) do %{query_params: %{"name" => name}} = conn -> name = escape_header_value(name) - put_resp_header(conn, "content-disposition", "inline; filename=\"#{name}\"") + put_resp_header(conn, "content-disposition", ~s[inline; filename="#{name}"]) conn -> conn diff --git a/test/pleroma/web/plugs/uploaded_media_plug_test.exs b/test/pleroma/web/plugs/uploaded_media_plug_test.exs index 1bb02c4b1..50e0f1bf3 100644 --- a/test/pleroma/web/plugs/uploaded_media_plug_test.exs +++ b/test/pleroma/web/plugs/uploaded_media_plug_test.exs @@ -33,11 +33,11 @@ test "does not send Content-Disposition header when name param is not set", %{ test "sends Content-Disposition header when name param is set", %{ attachment_url: attachment_url } do - conn = get(build_conn(), attachment_url <> "?name=\"cofe\".gif") + conn = get(build_conn(), attachment_url <> ~s[?name="cofe".gif]) assert Enum.any?( conn.resp_headers, - &(&1 == {"content-disposition", "inline; filename=\"\\\"cofe\\\".gif\""}) + &(&1 == {"content-disposition", ~s[inline; filename="\\"cofe\\".gif"]}) ) end From fee6e2aac4d93f1718615f4b424ca35dea14486f Mon Sep 17 00:00:00 2001 From: tusooa Date: Thu, 25 May 2023 18:40:38 -0400 Subject: [PATCH 19/24] Fix deleting banned users' statuses --- lib/pleroma/web/common_api.ex | 2 +- test/pleroma/web/common_api_test.exs | 14 +++++++++++++ .../controllers/status_controller_test.exs | 21 +++++++++++++++++++ 3 files changed, 36 insertions(+), 1 deletion(-) diff --git a/lib/pleroma/web/common_api.ex b/lib/pleroma/web/common_api.ex index f1f51acf5..86c1bdfa7 100644 --- a/lib/pleroma/web/common_api.ex +++ b/lib/pleroma/web/common_api.ex @@ -88,7 +88,7 @@ def reject_follow_request(follower, followed) do def delete(activity_id, user) do with {_, %Activity{data: %{"object" => _, "type" => "Create"}} = activity} <- - {:find_activity, Activity.get_by_id(activity_id)}, + {:find_activity, Activity.get_by_id(activity_id, filter: [])}, {_, %Object{} = object, _} <- {:find_object, Object.normalize(activity, fetch: false), activity}, true <- User.superuser?(user) || user.ap_id == object.data["actor"], diff --git a/test/pleroma/web/common_api_test.exs b/test/pleroma/web/common_api_test.exs index 33709c8f3..e877048cc 100644 --- a/test/pleroma/web/common_api_test.exs +++ b/test/pleroma/web/common_api_test.exs @@ -225,6 +225,20 @@ test "superusers deleting non-local posts won't federate the delete" do refute Activity.get_by_id(post.id) end + + test "it allows privileged users to delete banned user's posts" do + clear_config([:instance, :moderator_privileges], [:messages_delete]) + user = insert(:user) + moderator = insert(:user, is_moderator: true) + + {:ok, post} = CommonAPI.post(user, %{status: "namu amida butsu"}) + User.set_activation(user, false) + + assert {:ok, delete} = CommonAPI.delete(post.id, moderator) + assert delete.local + + refute Activity.get_by_id(post.id) + end end test "favoriting race condition" do diff --git a/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs index fe83c96ff..1e7da0e5f 100644 --- a/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs +++ b/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs @@ -1073,6 +1073,27 @@ test "when you're an admin or moderator", %{conn: conn} do refute Activity.get_by_id(activity1.id) refute Activity.get_by_id(activity2.id) end + + test "when you're privileged and the user is banned", %{conn: conn} do + clear_config([:instance, :moderator_privileges], [:messages_delete]) + posting_user = insert(:user, is_active: false) + refute posting_user.is_active + activity = insert(:note_activity, user: posting_user) + user = insert(:user, is_moderator: true) + + res_conn = + conn + |> assign(:user, user) + |> assign(:token, insert(:oauth_token, user: user, scopes: ["write:statuses"])) + |> delete("/api/v1/statuses/#{activity.id}") + + assert %{} = json_response_and_validate_schema(res_conn, 200) + + # assert ModerationLog |> Repo.one() |> ModerationLog.get_log_entry_message() == + # "@#{user.nickname} deleted status ##{activity.id}" + + refute Activity.get_by_id(activity.id) + end end describe "reblogging" do From c0a01e73cfc655b1cc2f8866da970a017b066af6 Mon Sep 17 00:00:00 2001 From: tusooa Date: Thu, 30 Mar 2023 21:01:37 -0400 Subject: [PATCH 20/24] Enforce unauth restrictions for public streaming endpoints --- lib/pleroma/web/streamer.ex | 46 +++++++++++++++--- test/pleroma/web/streamer_test.exs | 77 ++++++++++++++++++++++++++++++ 2 files changed, 116 insertions(+), 7 deletions(-) diff --git a/lib/pleroma/web/streamer.ex b/lib/pleroma/web/streamer.ex index f009fbd9e..1c3848bfb 100644 --- a/lib/pleroma/web/streamer.ex +++ b/lib/pleroma/web/streamer.ex @@ -25,6 +25,7 @@ defmodule Pleroma.Web.Streamer do def registry, do: @registry @public_streams ["public", "public:local", "public:media", "public:local:media"] + @local_streams ["public:local", "public:local:media"] @user_streams ["user", "user:notification", "direct"] @doc "Expands and authorizes a stream, and registers the process for streaming." @@ -41,14 +42,37 @@ def get_topic_and_add_socket(stream, user, oauth_token, params \\ %{}) do end end + defp can_access_stream(user, oauth_token, kind) do + with {_, true} <- {:restrict?, Config.restrict_unauthenticated_access?(:timelines, kind)}, + {_, %User{id: user_id}, %Token{user_id: user_id}} <- {:user, user, oauth_token}, + {_, true} <- + {:scopes, + OAuthScopesPlug.filter_descendants(["read:statuses"], oauth_token.scopes) != []} do + true + else + {:restrict?, _} -> + true + + _ -> + false + end + end + @doc "Expand and authorizes a stream" @spec get_topic(stream :: String.t(), User.t() | nil, Token.t() | nil, Map.t()) :: {:ok, topic :: String.t()} | {:error, :bad_topic} def get_topic(stream, user, oauth_token, params \\ %{}) - # Allow all public steams. - def get_topic(stream, _user, _oauth_token, _params) when stream in @public_streams do - {:ok, stream} + # Allow all public steams if the instance allows unauthenticated access. + # Otherwise, only allow users with valid oauth tokens. + def get_topic(stream, user, oauth_token, _params) when stream in @public_streams do + kind = if stream in @local_streams, do: :local, else: :federated + + if can_access_stream(user, oauth_token, kind) do + {:ok, stream} + else + {:error, :unauthorized} + end end # Allow all hashtags streams. @@ -57,12 +81,20 @@ def get_topic("hashtag", _user, _oauth_token, %{"tag" => tag} = _params) do end # Allow remote instance streams. - def get_topic("public:remote", _user, _oauth_token, %{"instance" => instance} = _params) do - {:ok, "public:remote:" <> instance} + def get_topic("public:remote", user, oauth_token, %{"instance" => instance} = _params) do + if can_access_stream(user, oauth_token, :federated) do + {:ok, "public:remote:" <> instance} + else + {:error, :unauthorized} + end end - def get_topic("public:remote:media", _user, _oauth_token, %{"instance" => instance} = _params) do - {:ok, "public:remote:media:" <> instance} + def get_topic("public:remote:media", user, oauth_token, %{"instance" => instance} = _params) do + if can_access_stream(user, oauth_token, :federated) do + {:ok, "public:remote:media:" <> instance} + else + {:error, :unauthorized} + end end # Expand user streams. diff --git a/test/pleroma/web/streamer_test.exs b/test/pleroma/web/streamer_test.exs index b07c16faa..5bd04fe9f 100644 --- a/test/pleroma/web/streamer_test.exs +++ b/test/pleroma/web/streamer_test.exs @@ -25,6 +25,26 @@ test "allows public" do assert {:ok, "public:local:media"} = Streamer.get_topic("public:local:media", nil, nil) end + test "rejects local public streams if restricted_unauthenticated is on" do + clear_config([:restrict_unauthenticated, :timelines, :local], true) + + assert {:error, :unauthorized} = Streamer.get_topic("public:local", nil, nil) + assert {:error, :unauthorized} = Streamer.get_topic("public:local:media", nil, nil) + end + + test "rejects remote public streams if restricted_unauthenticated is on" do + clear_config([:restrict_unauthenticated, :timelines, :federated], true) + + assert {:error, :unauthorized} = Streamer.get_topic("public", nil, nil) + assert {:error, :unauthorized} = Streamer.get_topic("public:media", nil, nil) + + assert {:error, :unauthorized} = + Streamer.get_topic("public:remote", nil, nil, %{"instance" => "lain.com"}) + + assert {:error, :unauthorized} = + Streamer.get_topic("public:remote:media", nil, nil, %{"instance" => "lain.com"}) + end + test "allows instance streams" do assert {:ok, "public:remote:lain.com"} = Streamer.get_topic("public:remote", nil, nil, %{"instance" => "lain.com"}) @@ -65,6 +85,63 @@ test "allows public streams (regardless of OAuth token scopes)", %{ end end + test "allows local public streams if restricted_unauthenticated is on", %{ + user: user, + token: oauth_token + } do + clear_config([:restrict_unauthenticated, :timelines, :local], true) + + %{token: read_notifications_token} = oauth_access(["read:notifications"], user: user) + %{token: badly_scoped_token} = oauth_access(["irrelevant:scope"], user: user) + + assert {:ok, "public:local"} = Streamer.get_topic("public:local", user, oauth_token) + + assert {:ok, "public:local:media"} = + Streamer.get_topic("public:local:media", user, oauth_token) + + for token <- [read_notifications_token, badly_scoped_token] do + assert {:error, :unauthorized} = Streamer.get_topic("public:local", user, token) + + assert {:error, :unauthorized} = Streamer.get_topic("public:local:media", user, token) + end + end + + test "allows remote public streams if restricted_unauthenticated is on", %{ + user: user, + token: oauth_token + } do + clear_config([:restrict_unauthenticated, :timelines, :federated], true) + + %{token: read_notifications_token} = oauth_access(["read:notifications"], user: user) + %{token: badly_scoped_token} = oauth_access(["irrelevant:scope"], user: user) + + assert {:ok, "public"} = Streamer.get_topic("public", user, oauth_token) + assert {:ok, "public:media"} = Streamer.get_topic("public:media", user, oauth_token) + + assert {:ok, "public:remote:lain.com"} = + Streamer.get_topic("public:remote", user, oauth_token, %{"instance" => "lain.com"}) + + assert {:ok, "public:remote:media:lain.com"} = + Streamer.get_topic("public:remote:media", user, oauth_token, %{ + "instance" => "lain.com" + }) + + for token <- [read_notifications_token, badly_scoped_token] do + assert {:error, :unauthorized} = Streamer.get_topic("public", user, token) + assert {:error, :unauthorized} = Streamer.get_topic("public:media", user, token) + + assert {:error, :unauthorized} = + Streamer.get_topic("public:remote", user, token, %{ + "instance" => "lain.com" + }) + + assert {:error, :unauthorized} = + Streamer.get_topic("public:remote:media", user, token, %{ + "instance" => "lain.com" + }) + end + end + test "allows user streams (with proper OAuth token scopes)", %{ user: user, token: read_oauth_token From 166ddebdbce7df504abaf17bd6ccc1b99c777906 Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Tue, 13 Jun 2023 12:45:18 +0200 Subject: [PATCH 21/24] Add no_new_privs to OpenRC service files --- installation/init.d/akkoma | 1 + rel/files/installation/init.d/akkoma | 1 + 2 files changed, 2 insertions(+) diff --git a/installation/init.d/akkoma b/installation/init.d/akkoma index 6c1973db4..bd17516f2 100755 --- a/installation/init.d/akkoma +++ b/installation/init.d/akkoma @@ -8,6 +8,7 @@ pidfile="/var/run/akkoma.pid" directory=/opt/akkoma healthcheck_delay=60 healthcheck_timer=30 +no_new_privs="yes" : ${akkoma_port:-4000} diff --git a/rel/files/installation/init.d/akkoma b/rel/files/installation/init.d/akkoma index ea6ea3580..492a0debe 100755 --- a/rel/files/installation/init.d/akkoma +++ b/rel/files/installation/init.d/akkoma @@ -9,6 +9,7 @@ command=/opt/akkoma/bin/pleroma command_args="start" command_user=akkoma command_background=1 +no_new_privs="yes" # Ask process to terminate within 30 seconds, otherwise kill it retry="SIGTERM/30/SIGKILL/5" From a86b010e103771ada1b50ef8ac22e3d791f1a919 Mon Sep 17 00:00:00 2001 From: Norm Date: Thu, 29 Jun 2023 02:14:04 -0400 Subject: [PATCH 22/24] Add NoNewPrivileges to systemd service file for source installs This setting already exists in the OTP installation directory, but doesn't for the one used by source installs. --- installation/akkoma.service | 2 ++ 1 file changed, 2 insertions(+) diff --git a/installation/akkoma.service b/installation/akkoma.service index 2c381ad0d..717693495 100644 --- a/installation/akkoma.service +++ b/installation/akkoma.service @@ -38,6 +38,8 @@ ProtectHome=true ProtectSystem=full ; Sets up a new /dev mount for the process and only adds API pseudo devices like /dev/null, /dev/zero or /dev/random but not physical devices. Disabled by default because it may not work on devices like the Raspberry Pi. PrivateDevices=false +; Ensures that the service process and all its children can never gain new privileges through execve(). +NoNewPrivileges=true ; Drops the sysadmin capability from the daemon. CapabilityBoundingSet=~CAP_SYS_ADMIN From db645563062e6169777cc0eadf172ee282f067b2 Mon Sep 17 00:00:00 2001 From: Norm Date: Thu, 29 Jun 2023 02:15:32 -0400 Subject: [PATCH 23/24] Record no_new_privs hardening to changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a81aad09..c2f3757fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Deactivated users can no longer show up in the emoji reaction list - Embedded posts can no longer bypass `:restrict\_unauthenticated` +## Security + +- Add `no_new_privs` hardening to OpenRC and systemd service files + ## 2023.05 ## Added From d74542148a18dce3d06558ba5e54675d247c65b6 Mon Sep 17 00:00:00 2001 From: Oneric Date: Sat, 22 Jul 2023 21:37:26 +0200 Subject: [PATCH 24/24] Document API changes made for fedibird compatibility Commit 11ec9daa5b742f8a1b408497321392e144f45019 (released with 3.2.0) added the fedibird frontend and tweaked and extended Mastodon API for compatibility with it. Document these changes. --- .../API/differences_in_mastoapi_responses.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/docs/docs/development/API/differences_in_mastoapi_responses.md b/docs/docs/development/API/differences_in_mastoapi_responses.md index b41561c45..053dc9663 100644 --- a/docs/docs/development/API/differences_in_mastoapi_responses.md +++ b/docs/docs/development/API/differences_in_mastoapi_responses.md @@ -25,6 +25,7 @@ Home, public, hashtag & list timelines accept these parameters: ## Statuses - `visibility`: has additional possible values `list` and `local` (for local-only statuses) +- `emoji_reactions`: additional field since Akkoma 3.2.0; identical to `pleroma/emoji_reactions` Has these additional fields under the `pleroma` object: @@ -36,7 +37,9 @@ Has these additional fields under the `pleroma` object: - `spoiler_text`: a map consisting of alternate representations of the `spoiler_text` property with the key being its mimetype. Currently, the only alternate representation supported is `text/plain` - `expires_at`: a datetime (iso8601) that states when the post will expire (be deleted automatically), or empty if the post won't expire - `thread_muted`: true if the thread the post belongs to is muted -- `emoji_reactions`: A list with emoji / reaction maps. The format is `{name: "☕", count: 1, me: true}`. Contains no information about the reacting users, for that use the `/statuses/:id/reactions` endpoint. +- `emoji_reactions`: A list with emoji / reaction maps. The format is `{name: "☕", count: 2, me: true, account_ids: ["UserID1", "UserID2"]}`. + The `account_ids` property was added in Akkoma 3.2.0. + Further info about all reacting users at once, can be found using the `/statuses/:id/reactions` endpoint. - `parent_visible`: If the parent of this post is visible to the user or not. - `pinned_at`: a datetime (iso8601) when status was pinned, `null` otherwise. @@ -214,6 +217,11 @@ Returns: array of Status. The maximum number of statuses is limited to 100 per request. +## PUT `/api/v1/statuses/:id/emoji_reactions/:emoji` + +This endpoint is an extension of the Fedibird Mastodon fork. +It behaves identical to PUT `/api/v1/pleroma/statuses/:id/reactions/:emoji`. + ## PATCH `/api/v1/accounts/update_credentials` Additional parameters can be added to the JSON body/Form data: