From 31eb3dc24587ad3715da9fe00886867a6c0bf0c4 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 22 Jan 2022 16:41:30 -0600 Subject: [PATCH 01/46] ObjectValidators: accept "quoteUrl" field --- .../web/activity_pub/object_validators/common_fields.ex | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/pleroma/web/activity_pub/object_validators/common_fields.ex b/lib/pleroma/web/activity_pub/object_validators/common_fields.ex index d580208df..835ed97b7 100644 --- a/lib/pleroma/web/activity_pub/object_validators/common_fields.ex +++ b/lib/pleroma/web/activity_pub/object_validators/common_fields.ex @@ -27,7 +27,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonFields do end end - # All objects except Answer and CHatMessage + # All objects except Answer and ChatMessage defmacro object_fields do quote bind_quoted: binding() do field(:content, :string) @@ -58,6 +58,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonFields do field(:like_count, :integer, default: 0) field(:announcement_count, :integer, default: 0) field(:inReplyTo, ObjectValidators.ObjectID) + field(:quoteUrl, ObjectValidators.ObjectID) field(:url, ObjectValidators.BareUri) field(:likes, {:array, ObjectValidators.ObjectID}, default: []) From 7deda1fa18cb95a588ba66950ac45262c467a7f4 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 22 Jan 2022 17:30:49 -0600 Subject: [PATCH 02/46] Quote post: add fixtures --- .../quote_post/fedibird_quote_post.json | 52 +++++++++++++++++++ .../quote_post/misskey_quote_post.json | 46 ++++++++++++++++ 2 files changed, 98 insertions(+) create mode 100644 test/fixtures/quote_post/fedibird_quote_post.json create mode 100644 test/fixtures/quote_post/misskey_quote_post.json diff --git a/test/fixtures/quote_post/fedibird_quote_post.json b/test/fixtures/quote_post/fedibird_quote_post.json new file mode 100644 index 000000000..ebf383356 --- /dev/null +++ b/test/fixtures/quote_post/fedibird_quote_post.json @@ -0,0 +1,52 @@ +{ + "@context": [ + "https://www.w3.org/ns/activitystreams", + { + "ostatus": "http://ostatus.org#", + "atomUri": "ostatus:atomUri", + "inReplyToAtomUri": "ostatus:inReplyToAtomUri", + "conversation": "ostatus:conversation", + "sensitive": "as:sensitive", + "toot": "http://joinmastodon.org/ns#", + "votersCount": "toot:votersCount", + "expiry": "toot:expiry" + } + ], + "id": "https://fedibird.com/users/noellabo/statuses/107663670404015196", + "type": "Note", + "summary": null, + "inReplyTo": null, + "published": "2022-01-22T02:07:16Z", + "url": "https://fedibird.com/@noellabo/107663670404015196", + "attributedTo": "https://fedibird.com/users/noellabo", + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "cc": [ + "https://fedibird.com/users/noellabo/followers" + ], + "sensitive": false, + "atomUri": "https://fedibird.com/users/noellabo/statuses/107663670404015196", + "inReplyToAtomUri": null, + "conversation": "tag:fedibird.com,2022-01-22:objectId=107663670404038002:objectType=Conversation", + "context": "https://fedibird.com/contexts/107663670404038002", + "quoteURL": "https://misskey.io/notes/8vsn2izjwh", + "_misskey_quote": "https://misskey.io/notes/8vsn2izjwh", + "_misskey_content": "いつの生まれだシトリン", + "content": "

いつの生まれだシトリン
QT: https://misskey.io/notes/8vsn2izjwh

", + "contentMap": { + "ja": "

いつの生まれだシトリン
QT: https://misskey.io/notes/8vsn2izjwh

" + }, + "attachment": [], + "tag": [], + "replies": { + "id": "https://fedibird.com/users/noellabo/statuses/107663670404015196/replies", + "type": "Collection", + "first": { + "type": "CollectionPage", + "next": "https://fedibird.com/users/noellabo/statuses/107663670404015196/replies?only_other_accounts=true&page=true", + "partOf": "https://fedibird.com/users/noellabo/statuses/107663670404015196/replies", + "items": [] + } + } +} diff --git a/test/fixtures/quote_post/misskey_quote_post.json b/test/fixtures/quote_post/misskey_quote_post.json new file mode 100644 index 000000000..59f677ca9 --- /dev/null +++ b/test/fixtures/quote_post/misskey_quote_post.json @@ -0,0 +1,46 @@ +{ + "@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.io/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://misskey.io/notes/8vs6ylpfez", + "type": "Note", + "attributedTo": "https://misskey.io/users/7rkrarq81i", + "summary": null, + "content": "

投稿者の設定によるね
Fanboxについても投稿者によっては過去の投稿は高額なプランに移動してることがある

RE:
https://misskey.io/notes/8vs6wxufd0

", + "_misskey_content": "投稿者の設定によるね\nFanboxについても投稿者によっては過去の投稿は高額なプランに移動してることがある", + "_misskey_quote": "https://misskey.io/notes/8vs6wxufd0", + "quoteUrl": "https://misskey.io/notes/8vs6wxufd0", + "published": "2022-01-21T16:38:30.243Z", + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "cc": [ + "https://misskey.io/users/7rkrarq81i/followers" + ], + "inReplyTo": null, + "attachment": [], + "sensitive": false, + "tag": [] +} From 795736af16dca77929725e7dd55f5de04a796fdb Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 22 Jan 2022 18:03:22 -0600 Subject: [PATCH 03/46] ObjectValidators: improve quoteUrl compatibility --- .../article_note_page_validator.ex | 16 +++++++++++++++ .../article_note_page_validator_test.exs | 20 +++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/lib/pleroma/web/activity_pub/object_validators/article_note_page_validator.ex b/lib/pleroma/web/activity_pub/object_validators/article_note_page_validator.ex index 2670e3f17..40bb67934 100644 --- a/lib/pleroma/web/activity_pub/object_validators/article_note_page_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/article_note_page_validator.ex @@ -76,6 +76,21 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidator do def fix_attachments(data), do: data + defp fix_quote_url(%{"quoteUrl" => _quote_url} = data), do: data + + # Fix for Fedibird + # https://github.com/fedibird/mastodon/issues/9 + defp fix_quote_url(%{"quoteURL" => quote_url} = data) do + Map.put(data, "quoteUrl", quote_url) + end + + # Misskey fallback + defp fix_quote_url(%{"_misskey_quote" => quote_url} = data) do + Map.put(data, "quoteUrl", quote_url) + end + + defp fix_quote_url(data), do: data + defp fix(data) do data |> CommonFixes.fix_actor() @@ -84,6 +99,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidator do |> fix_tag() |> fix_replies() |> fix_attachments() + |> fix_quote_url() |> Transmogrifier.fix_emoji() |> Transmogrifier.fix_content_map() end diff --git a/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs b/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs index c7a62be18..c3cde00b5 100644 --- a/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs +++ b/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs @@ -116,4 +116,24 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidatorTest %{valid?: true} = ArticleNotePageValidator.cast_and_validate(note) end + + test "Fedibird quote post" do + insert(:user, ap_id: "https://fedibird.com/users/noellabo") + + data = File.read!("test/fixtures/quote_post/fedibird_quote_post.json") |> Jason.decode!() + chg = ArticleNotePageValidator.cast_and_validate(data) + + assert chg.valid? + assert chg.changes.quoteUrl == "https://misskey.io/notes/8vsn2izjwh" + end + + test "Misskey quote post" do + insert(:user, ap_id: "https://misskey.io/users/7rkrarq81i") + + data = File.read!("test/fixtures/quote_post/misskey_quote_post.json") |> Jason.decode!() + chg = ArticleNotePageValidator.cast_and_validate(data) + + assert chg.valid? + assert chg.changes.quoteUrl == "https://misskey.io/notes/8vs6wxufd0" + end end From b022d6635dad4b2769fbf1fd4b97f77a4cc646b4 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 22 Jan 2022 18:46:58 -0600 Subject: [PATCH 04/46] Transmogrifier: fetch quoted post --- .../web/activity_pub/transmogrifier.ex | 17 +++++ test/fixtures/tesla_mock/aimu@misskey.io.json | 64 +++++++++++++++++++ .../tesla_mock/misskey.io_8vs6wxufd0.json | 44 +++++++++++++ .../web/activity_pub/transmogrifier_test.exs | 22 +++++++ test/support/http_request_mock.ex | 18 ++++++ 5 files changed, 165 insertions(+) create mode 100644 test/fixtures/tesla_mock/aimu@misskey.io.json create mode 100644 test/fixtures/tesla_mock/misskey.io_8vs6wxufd0.json diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex index 0e6c429f9..c466271ca 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier.ex @@ -166,6 +166,22 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do def fix_in_reply_to(object, _options), do: object + def fix_quote(object, options \\ []) + + def fix_quote(%{"quoteUrl" => quote_url} = object, options) + when not is_nil(quote_url) do + with {:ok, quoted_object} <- get_obj_helper(quote_url, options), + %Activity{} <- Activity.get_create_by_object_ap_id(quoted_object.data["id"]) do + Map.put(object, "quoteUrl", quoted_object.data["id"]) + else + e -> + Logger.warn("Couldn't fetch #{inspect(quote_url)}, error: #{inspect(e)}") + object + end + end + + def fix_quote(object, _options), do: object + defp prepare_in_reply_to(in_reply_to) do cond do is_bitstring(in_reply_to) -> @@ -454,6 +470,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do |> strip_internal_fields() |> fix_type(fetch_options) |> fix_in_reply_to(fetch_options) + |> fix_quote(fetch_options) data = Map.put(data, "object", object) options = Keyword.put(options, :local, false) diff --git a/test/fixtures/tesla_mock/aimu@misskey.io.json b/test/fixtures/tesla_mock/aimu@misskey.io.json new file mode 100644 index 000000000..9ff4cb6d0 --- /dev/null +++ b/test/fixtures/tesla_mock/aimu@misskey.io.json @@ -0,0 +1,64 @@ +{ + "@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.io/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#" + } + ], + "type": "Person", + "id": "https://misskey.io/users/83ssedkv53", + "inbox": "https://misskey.io/users/83ssedkv53/inbox", + "outbox": "https://misskey.io/users/83ssedkv53/outbox", + "followers": "https://misskey.io/users/83ssedkv53/followers", + "following": "https://misskey.io/users/83ssedkv53/following", + "sharedInbox": "https://misskey.io/inbox", + "endpoints": { + "sharedInbox": "https://misskey.io/inbox" + }, + "url": "https://misskey.io/@aimu", + "preferredUsername": "aimu", + "name": "あいむ", + "summary": "

わずかな作曲要素 巣穴で独り言
Twitter
https://twitter.com/aimu_53
Soundcloud
https://soundcloud.com/aimu-53

", + "icon": { + "type": "Image", + "url": "https://s3.arkjp.net/misskey/webpublic-3f7e93c0-34f5-443c-acc0-f415cb2342b4.jpg", + "sensitive": false, + "name": null + }, + "image": { + "type": "Image", + "url": "https://s3.arkjp.net/misskey/webpublic-2db63d1d-490b-488b-ab62-c93c285f26b6.png", + "sensitive": false, + "name": null + }, + "tag": [], + "manuallyApprovesFollowers": false, + "discoverable": true, + "publicKey": { + "id": "https://misskey.io/users/83ssedkv53#main-key", + "type": "Key", + "owner": "https://misskey.io/users/83ssedkv53", + "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA1ylhePJ6qGHmwHSBP17b\nIosxGaiFKvgDBgZdm8vzvKeRSqJV9uLHfZL3pO/Zt02EwaZd2GohZAtBZEF8DbMA\n3s93WAesvyGF9mjGrYYKlhp/glwyrrrbf+RdD0DLtyDwRRlrxp3pS2lLmv5Tp1Zl\npH+UKpOnNrpQqjHI5P+lEc9bnflzbRrX+UiyLNsVAP80v4wt7SZfT/telrU6mDru\n998UdfhUo7bDKeDsHG1PfLpyhhtfdoZub4kBpkyacHiwAd+CdCjR54Eu7FDwVK3p\nY3JcrT2q5stgMqN1m4QgSL4XAADIotWwDYttTJejM1n9dr+6VWv5bs0F2Q/6gxOp\nu5DQZLk4Q+64U4LWNox6jCMOq3fYe0g7QalJIHnanYQQo+XjoH6S1Aw64gQ3Ip2Y\nZBmZREAOR7GMFVDPFnVnsbCHnIAv16TdgtLgQBAihkWEUuPqITLi8PMu6kMr3uyq\nYkObEfH0TNTcqaiVpoXv791GZLEUV5ROl0FSUANLNkHZZv29xZ5JDOBOR1rNBLyH\ngVtW8rpszYqOXwzX23hh4WsVXfB7YgNvIijwjiaWbzsecleaENGEnLNMiVKVumTj\nmtyTeFJpH0+OaSrUYpemRRJizmqIjklKsNwUEwUb2WcUUg92o56T2obrBkooabZe\nwgSXSKTOcjsR/ju7+AuIyvkCAwEAAQ==\n-----END PUBLIC KEY-----\n" + }, + "isCat": true, + "vcard:bday": "5353-05-03" +} diff --git a/test/fixtures/tesla_mock/misskey.io_8vs6wxufd0.json b/test/fixtures/tesla_mock/misskey.io_8vs6wxufd0.json new file mode 100644 index 000000000..323ca10ed --- /dev/null +++ b/test/fixtures/tesla_mock/misskey.io_8vs6wxufd0.json @@ -0,0 +1,44 @@ +{ + "@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.io/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://misskey.io/notes/8vs6wxufd0", + "type": "Note", + "attributedTo": "https://misskey.io/users/83ssedkv53", + "summary": null, + "content": "

Fantiaこれできないように過去のやつは従量課金だった気がする

", + "_misskey_content": "Fantiaこれできないように過去のやつは従量課金だった気がする", + "published": "2022-01-21T16:37:12.663Z", + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "cc": [ + "https://misskey.io/users/83ssedkv53/followers" + ], + "inReplyTo": null, + "attachment": [], + "sensitive": false, + "tag": [] +} diff --git a/test/pleroma/web/activity_pub/transmogrifier_test.exs b/test/pleroma/web/activity_pub/transmogrifier_test.exs index 3e0c8dc65..2c8e5ba21 100644 --- a/test/pleroma/web/activity_pub/transmogrifier_test.exs +++ b/test/pleroma/web/activity_pub/transmogrifier_test.exs @@ -136,6 +136,28 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do tag = object.data["tag"] |> List.first() assert tag["type"] == "Mention" end + + test "it accepts quote posts" do + insert(:user, ap_id: "https://misskey.io/users/7rkrarq81i") + + object = File.read!("test/fixtures/quote_post/misskey_quote_post.json") |> Jason.decode!() + + message = %{ + "@context" => "https://www.w3.org/ns/activitystreams", + "type" => "Create", + "actor" => "https://misskey.io/users/7rkrarq81i", + "object" => object + } + + assert {:ok, activity} = Transmogrifier.handle_incoming(message) + + # Object was created in the database + object = Object.normalize(activity) + assert object.data["quoteUrl"] == "https://misskey.io/notes/8vs6wxufd0" + + # It fetched the quoted post + assert Object.normalize("https://misskey.io/notes/8vs6wxufd0") + end end describe "prepare outgoing" do diff --git a/test/support/http_request_mock.ex b/test/support/http_request_mock.ex index b0cf613ac..78a367024 100644 --- a/test/support/http_request_mock.ex +++ b/test/support/http_request_mock.ex @@ -1380,6 +1380,15 @@ defmodule HttpRequestMock do }} end + def get("https://misskey.io/users/83ssedkv53", _, _, _) do + {:ok, + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/tesla_mock/aimu@misskey.io.json"), + headers: activitypub_object_headers() + }} + end + def get("https://gleasonator.com/users/macgirvin", _, _, _) do {:ok, %Tesla.Env{ @@ -1446,6 +1455,15 @@ defmodule HttpRequestMock do }} end + def get("https://misskey.io/notes/8vs6wxufd0", _, _, _) do + {:ok, + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/tesla_mock/misskey.io_8vs6wxufd0.json"), + headers: activitypub_object_headers() + }} + end + def get(url, query, body, headers) do {:error, "Mock response not implemented for GET #{inspect(url)}, #{query}, #{inspect(body)}, #{inspect(headers)}"} From cc4badaf60462fdb8bb57225437e3dd360ee0dfb Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 22 Jan 2022 19:14:39 -0600 Subject: [PATCH 05/46] Transmogrifier: fix quoteUrl here too --- .../web/activity_pub/transmogrifier.ex | 23 +++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex index c466271ca..f5771e75e 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier.ex @@ -166,9 +166,9 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do def fix_in_reply_to(object, _options), do: object - def fix_quote(object, options \\ []) + def fix_quote_url(object, options \\ []) - def fix_quote(%{"quoteUrl" => quote_url} = object, options) + def fix_quote_url(%{"quoteUrl" => quote_url} = object, options) when not is_nil(quote_url) do with {:ok, quoted_object} <- get_obj_helper(quote_url, options), %Activity{} <- Activity.get_create_by_object_ap_id(quoted_object.data["id"]) do @@ -180,7 +180,22 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do end end - def fix_quote(object, _options), do: object + # Fix for Fedibird + # https://github.com/fedibird/mastodon/issues/9 + def fix_quote_url(%{"quoteURL" => quote_url} = object, options) do + object + |> Map.put("quoteUrl", quote_url) + |> fix_quote_url(options) + end + + # Misskey fallback + def fix_quote_url(%{"_misskey_quote" => quote_url} = object, options) do + object + |> Map.put("quoteUrl", quote_url) + |> fix_quote_url(options) + end + + def fix_quote_url(object, _options), do: object defp prepare_in_reply_to(in_reply_to) do cond do @@ -470,7 +485,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do |> strip_internal_fields() |> fix_type(fetch_options) |> fix_in_reply_to(fetch_options) - |> fix_quote(fetch_options) + |> fix_quote_url(fetch_options) data = Map.put(data, "object", object) options = Keyword.put(options, :local, false) From ce5eb3172321f0ef2ae85d7819b44cc8241a5581 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 22 Jan 2022 19:47:08 -0600 Subject: [PATCH 06/46] StatusView: show quoted posts through the API, probably --- .../web/mastodon_api/views/status_view.ex | 42 ++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex index dea22f9c2..b966a84d0 100644 --- a/lib/pleroma/web/mastodon_api/views/status_view.ex +++ b/lib/pleroma/web/mastodon_api/views/status_view.ex @@ -57,6 +57,27 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do end) end + defp get_quoted_activities([]), do: %{} + + defp get_quoted_activities(activities) do + activities + |> Enum.map(fn + %{data: %{"type" => "Create"}} = activity -> + object = Object.normalize(activity, fetch: false) + object && object.data["quoteUrl"] != "" && object.data["quoteUrl"] + + _ -> + nil + end) + |> Enum.filter(& &1) + |> Activity.create_by_object_ap_id_with_object() + |> Repo.all() + |> Enum.reduce(%{}, fn activity, acc -> + object = Object.normalize(activity, fetch: false) + if object, do: Map.put(acc, object.data["id"], activity), else: acc + end) + end + # DEPRECATED This field seems to be a left-over from the StatusNet era. # If your application uses `pleroma.conversation_id`: this field is deprecated. # It is currently stubbed instead by doing a CRC32 of the context, and @@ -97,6 +118,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do # length(activities_with_links) * timeout fetch_rich_media_for_activities(activities) replied_to_activities = get_replied_to_activities(activities) + quoted_activities = get_quoted_activities(activities) parent_activities = activities @@ -129,6 +151,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do opts = opts |> Map.put(:replied_to_activities, replied_to_activities) + |> Map.put(:quoted_activities, quoted_activities) |> Map.put(:parent_activities, parent_activities) |> Map.put(:relationships, relationships_opt) @@ -277,7 +300,6 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do end reply_to = get_reply_to(activity, opts) - reply_to_user = reply_to && CommonAPI.get_user(reply_to.data["actor"]) history_len = @@ -290,6 +312,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do # Here the implicit index of the current content is 0 chrono_order = history_len - 1 + quote_activity = get_quote(activity, opts) + content = object |> render_content() @@ -398,6 +422,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do conversation_id: get_context_id(activity), context: object.data["context"], in_reply_to_account_acct: reply_to_user && reply_to_user.nickname, + quote_id: quote_activity && to_string(quote_activity.id), content: %{"text/plain" => content_plaintext}, spoiler_text: %{"text/plain" => summary}, expires_at: expires_at, @@ -633,6 +658,21 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do end end + def get_quote(activity, %{quoted_activities: quoted_activities}) do + object = Object.normalize(activity, fetch: false) + quoted_activities[object.data["quoteUrl"]] + end + + def get_quote(%{data: %{"object" => _object}} = activity, _) do + object = Object.normalize(activity, fetch: false) + + if object.data["quoteUrl"] && object.data["quoteUrl"] != "" do + Activity.get_create_by_object_ap_id(object.data["quoteUrl"]) + else + nil + end + end + def render_content(%{data: %{"name" => name}} = object) when not is_nil(name) and name != "" do url = object.data["url"] || object.data["id"] From 0d9c443e51c85d9ded3e20954c9620f7a9d2361e Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 22 Jan 2022 20:05:58 -0600 Subject: [PATCH 07/46] StatusView: render the whole quoted status --- lib/pleroma/web/api_spec/schemas/status.ex | 5 +++++ lib/pleroma/web/mastodon_api/views/status_view.ex | 10 +++++++++- .../web/mastodon_api/views/status_view_test.exs | 1 + 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/lib/pleroma/web/api_spec/schemas/status.ex b/lib/pleroma/web/api_spec/schemas/status.ex index bc29cf4a6..39241aa39 100644 --- a/lib/pleroma/web/api_spec/schemas/status.ex +++ b/lib/pleroma/web/api_spec/schemas/status.ex @@ -193,6 +193,11 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Status do nullable: true, description: "The `acct` property of User entity for replied user (if any)" }, + quote: %Schema{ + allOf: [%OpenApiSpex.Reference{"$ref": "#/components/schemas/Status"}], + nullable: true, + description: "Quoted status (if any)" + }, local: %Schema{ type: :boolean, description: "`true` if the post was made on the local instance" diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex index b966a84d0..5bde1ce04 100644 --- a/lib/pleroma/web/mastodon_api/views/status_view.ex +++ b/lib/pleroma/web/mastodon_api/views/status_view.ex @@ -314,6 +314,14 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do quote_activity = get_quote(activity, opts) + quote_post = + if quote_activity do + quote_rendering_opts = Map.put(opts, :activity, quote_activity) + render("show.json", quote_rendering_opts) + else + nil + end + content = object |> render_content() @@ -422,7 +430,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do conversation_id: get_context_id(activity), context: object.data["context"], in_reply_to_account_acct: reply_to_user && reply_to_user.nickname, - quote_id: quote_activity && to_string(quote_activity.id), + quote: quote_post, content: %{"text/plain" => content_plaintext}, spoiler_text: %{"text/plain" => summary}, expires_at: expires_at, diff --git a/test/pleroma/web/mastodon_api/views/status_view_test.exs b/test/pleroma/web/mastodon_api/views/status_view_test.exs index b93335190..b10b0f0b9 100644 --- a/test/pleroma/web/mastodon_api/views/status_view_test.exs +++ b/test/pleroma/web/mastodon_api/views/status_view_test.exs @@ -326,6 +326,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do conversation_id: convo_id, context: object_data["context"], in_reply_to_account_acct: nil, + quote: nil, content: %{"text/plain" => HTML.strip_tags(object_data["content"])}, spoiler_text: %{"text/plain" => HTML.strip_tags(object_data["summary"])}, expires_at: nil, From 6ac19c3999c543e5a26bbf04932a6a7aaa447b99 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 22 Jan 2022 21:27:05 -0600 Subject: [PATCH 08/46] ActivityDraft: create quote posts --- lib/pleroma/web/common_api/activity_draft.ex | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/lib/pleroma/web/common_api/activity_draft.ex b/lib/pleroma/web/common_api/activity_draft.ex index 63ed48a27..375aabc91 100644 --- a/lib/pleroma/web/common_api/activity_draft.ex +++ b/lib/pleroma/web/common_api/activity_draft.ex @@ -22,6 +22,7 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do attachments: [], in_reply_to: nil, in_reply_to_conversation: nil, + quote_post: nil, visibility: nil, expires_at: nil, extra: nil, @@ -53,6 +54,7 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do |> poll() |> with_valid(&in_reply_to/1) |> with_valid(&in_reply_to_conversation/1) + |> with_valid("e_post/1) |> with_valid(&visibility/1) |> content() |> with_valid(&to_and_cc/1) @@ -132,6 +134,18 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do defp in_reply_to(draft), do: draft + defp quote_post(%{params: %{quote_id: ""}} = draft), do: draft + + defp quote_post(%{params: %{quote_id: id}} = draft) when is_binary(id) do + %__MODULE__{draft | quote_post: Activity.get_by_id(id)} + end + + defp quote_post(%{params: %{quote_id: %Activity{} = quote_post}} = draft) do + %__MODULE__{draft | quote_post: quote_post} + end + + defp quote_post(draft), do: draft + defp in_reply_to_conversation(draft) do in_reply_to_conversation = Participation.get(draft.params[:in_reply_to_conversation_id]) %__MODULE__{draft | in_reply_to_conversation: in_reply_to_conversation} From d4fea8b5595e9e6cd37bdb1cee21285f905693f1 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 22 Jan 2022 22:15:54 -0600 Subject: [PATCH 09/46] ActivityDraft: allow quoting --- lib/pleroma/web/activity_pub/builder.ex | 11 +++++++++++ .../web/api_spec/operations/status_operation.ex | 7 ++++++- test/pleroma/web/common_api_test.exs | 12 ++++++++++++ 3 files changed, 29 insertions(+), 1 deletion(-) diff --git a/lib/pleroma/web/activity_pub/builder.ex b/lib/pleroma/web/activity_pub/builder.ex index 8eab3a241..eb0bb0e33 100644 --- a/lib/pleroma/web/activity_pub/builder.ex +++ b/lib/pleroma/web/activity_pub/builder.ex @@ -217,6 +217,7 @@ defmodule Pleroma.Web.ActivityPub.Builder do "tag" => Keyword.values(draft.tags) |> Enum.uniq() } |> add_in_reply_to(draft.in_reply_to) + |> add_quote(draft.quote_post) |> Map.merge(draft.extra) {:ok, data, []} @@ -232,6 +233,16 @@ defmodule Pleroma.Web.ActivityPub.Builder do end end + defp add_quote(object, nil), do: object + + defp add_quote(object, quote_post) do + with %Object{} = quote_object <- Object.normalize(quote_post, fetch: false) do + Map.put(object, "quoteUrl", quote_object.data["id"]) + else + _ -> object + end + end + def chat_message(actor, recipient, content, opts \\ []) do basic = %{ "id" => Utils.generate_object_id(), diff --git a/lib/pleroma/web/api_spec/operations/status_operation.ex b/lib/pleroma/web/api_spec/operations/status_operation.ex index 5d6e82f3c..8fa3b0890 100644 --- a/lib/pleroma/web/api_spec/operations/status_operation.ex +++ b/lib/pleroma/web/api_spec/operations/status_operation.ex @@ -581,7 +581,12 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do type: :string, description: "Will reply to a given conversation, addressing only the people who are part of the recipient set of that conversation. Sets the visibility to `direct`." - } + }, + quote_id: %Schema{ + nullable: true, + allOf: [FlakeID], + description: "ID of the status being quoted, if any" + }, }, example: %{ "status" => "What time is it?", diff --git a/test/pleroma/web/common_api_test.exs b/test/pleroma/web/common_api_test.exs index 0d76d6581..09df27acb 100644 --- a/test/pleroma/web/common_api_test.exs +++ b/test/pleroma/web/common_api_test.exs @@ -796,6 +796,18 @@ defmodule Pleroma.Web.CommonAPITest do scheduled_at: expires_at ) end + + test "it allows allows quote posting" do + user = insert(:user) + + {:ok, quoted} = CommonAPI.post(user, %{status: "Hello world"}) + {:ok, quote_post} = CommonAPI.post(user, %{status: "nice post", quote_id: quoted.id}) + + quoted = Object.normalize(quoted) + quote_post = Object.normalize(quote_post) + + assert quote_post.data["quoteUrl"] == quoted.data["id"] + end end describe "reactions" do From c20e90e898affc9d00d8b7d6b71f11157f5c7837 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 22 Jan 2022 22:29:13 -0600 Subject: [PATCH 10/46] BuilderTest: build quote post --- .../pleroma/web/activity_pub/builder_test.exs | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/test/pleroma/web/activity_pub/builder_test.exs b/test/pleroma/web/activity_pub/builder_test.exs index eb175a1be..52058a0a3 100644 --- a/test/pleroma/web/activity_pub/builder_test.exs +++ b/test/pleroma/web/activity_pub/builder_test.exs @@ -44,5 +44,34 @@ defmodule Pleroma.Web.ActivityPub.BuilderTest do assert {:ok, ^expected, []} = Builder.note(draft) end + + test "quote post" do + user = insert(:user) + note = insert(:note) + + draft = %ActivityDraft{ + user: user, + context: "2hu", + content_html: "

This is :moominmamma: note

", + quote_post: note, + extra: %{} + } + + expected = %{ + "actor" => user.ap_id, + "attachment" => [], + "content" => "

This is :moominmamma: note

", + "context" => "2hu", + "sensitive" => false, + "type" => "Note", + "quoteUrl" => note.data["id"], + "cc" => [], + "summary" => nil, + "tag" => [], + "to" => [] + } + + assert {:ok, ^expected, []} = Builder.note(draft) + end end end From 3a8b5d90df6de502debaee4d670211bcf64ad1db Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 22 Jan 2022 22:35:08 -0600 Subject: [PATCH 11/46] StatusControllerTest: test creating a quote post --- .../controllers/status_controller_test.exs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) 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 76c289ee7..ba49256b5 100644 --- a/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs +++ b/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs @@ -125,6 +125,25 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do ) end + test "posting a quote post", %{conn: conn} do + user = insert(:user) + + {:ok, %{id: activity_id}} = CommonAPI.post(user, %{status: "yolo"}) + + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/statuses", %{ + "status" => "indeed", + "quote_id" => activity_id + }) + + assert %{"id" => id, "pleroma" => %{"quote" => %{"id" => ^activity_id}}} = + json_response_and_validate_schema(conn, 200) + + assert Activity.get_by_id(id) + end + test "it fails to create a status if `expires_in` is less or equal than an hour", %{ conn: conn } do From cbd1760efac872c00edad15f352ffe4d2e0e1e12 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 22 Jan 2022 22:41:57 -0600 Subject: [PATCH 12/46] TransmogrifierTest: prepare an outgoing quote post --- .../pleroma/web/activity_pub/transmogrifier_test.exs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/test/pleroma/web/activity_pub/transmogrifier_test.exs b/test/pleroma/web/activity_pub/transmogrifier_test.exs index 2c8e5ba21..824398e38 100644 --- a/test/pleroma/web/activity_pub/transmogrifier_test.exs +++ b/test/pleroma/web/activity_pub/transmogrifier_test.exs @@ -372,6 +372,18 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do } } = prepared["object"] end + + test "it prepares a quote post" do + user = insert(:user) + + {:ok, quoted_post} = CommonAPI.post(user, %{status: "hey"}) + {:ok, quote_post} = CommonAPI.post(user, %{status: "hey", quote_id: quoted_post.id}) + + {:ok, modified} = Transmogrifier.prepare_outgoing(quote_post.data) + + quoted_post = Object.normalize(quoted_post) + assert modified["object"]["quoteUrl"] == quoted_post.data["id"] + end end describe "actor rewriting" do From 96009739173e5e48a636bb964855eb7aea11c828 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 22 Jan 2022 22:57:42 -0600 Subject: [PATCH 13/46] mix format --- lib/pleroma/web/api_spec/operations/status_operation.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/web/api_spec/operations/status_operation.ex b/lib/pleroma/web/api_spec/operations/status_operation.ex index 8fa3b0890..c133a3aac 100644 --- a/lib/pleroma/web/api_spec/operations/status_operation.ex +++ b/lib/pleroma/web/api_spec/operations/status_operation.ex @@ -586,7 +586,7 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do nullable: true, allOf: [FlakeID], description: "ID of the status being quoted, if any" - }, + } }, example: %{ "status" => "What time is it?", From f4ccdfd5033e7b1136ae0fe4e41dba78d83e80cf Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 22 Jan 2022 23:02:44 -0600 Subject: [PATCH 14/46] Fix typos --- .../article_note_page_validator_test.exs | 12 ++++++------ test/pleroma/web/common_api_test.exs | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs b/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs index c3cde00b5..dec2e28c9 100644 --- a/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs +++ b/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs @@ -121,19 +121,19 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidatorTest insert(:user, ap_id: "https://fedibird.com/users/noellabo") data = File.read!("test/fixtures/quote_post/fedibird_quote_post.json") |> Jason.decode!() - chg = ArticleNotePageValidator.cast_and_validate(data) + cng = ArticleNotePageValidator.cast_and_validate(data) - assert chg.valid? - assert chg.changes.quoteUrl == "https://misskey.io/notes/8vsn2izjwh" + assert cng.valid? + assert cng.changes.quoteUrl == "https://misskey.io/notes/8vsn2izjwh" end test "Misskey quote post" do insert(:user, ap_id: "https://misskey.io/users/7rkrarq81i") data = File.read!("test/fixtures/quote_post/misskey_quote_post.json") |> Jason.decode!() - chg = ArticleNotePageValidator.cast_and_validate(data) + cng = ArticleNotePageValidator.cast_and_validate(data) - assert chg.valid? - assert chg.changes.quoteUrl == "https://misskey.io/notes/8vs6wxufd0" + assert cng.valid? + assert cng.changes.quoteUrl == "https://misskey.io/notes/8vs6wxufd0" end end diff --git a/test/pleroma/web/common_api_test.exs b/test/pleroma/web/common_api_test.exs index 09df27acb..734e6dd82 100644 --- a/test/pleroma/web/common_api_test.exs +++ b/test/pleroma/web/common_api_test.exs @@ -797,7 +797,7 @@ defmodule Pleroma.Web.CommonAPITest do ) end - test "it allows allows quote posting" do + test "it allows quote posting" do user = insert(:user) {:ok, quoted} = CommonAPI.post(user, %{status: "Hello world"}) From 5716f88a1d8424cf7c62a0491b3bf9607dc9aa3f Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 22 Jan 2022 23:09:33 -0600 Subject: [PATCH 15/46] InstanceView: add "quote_posting" feature --- lib/pleroma/web/mastodon_api/views/instance_view.ex | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/pleroma/web/mastodon_api/views/instance_view.ex b/lib/pleroma/web/mastodon_api/views/instance_view.ex index efd2a0af6..1b01d7371 100644 --- a/lib/pleroma/web/mastodon_api/views/instance_view.ex +++ b/lib/pleroma/web/mastodon_api/views/instance_view.ex @@ -69,6 +69,7 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do "multifetch", "pleroma:api/v1/notifications:include_types_filter", "editing", + "quote_posting", if Config.get([:activitypub, :blockers_visible]) do "blockers_visible" end, From db46abce477b83b252e0ad87f74ac9266e9cb118 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 22 Jan 2022 23:29:55 -0600 Subject: [PATCH 16/46] @context: add quoteUrl --- priv/static/schemas/litepub-0.1.jsonld | 1 + 1 file changed, 1 insertion(+) diff --git a/priv/static/schemas/litepub-0.1.jsonld b/priv/static/schemas/litepub-0.1.jsonld index 650118475..5d8244a11 100644 --- a/priv/static/schemas/litepub-0.1.jsonld +++ b/priv/static/schemas/litepub-0.1.jsonld @@ -26,6 +26,7 @@ "@id": "litepub:listMessage", "@type": "@id" }, + "quoteUrl": "as:quoteUrl", "oauthRegistrationEndpoint": { "@id": "litepub:oauthRegistrationEndpoint", "@type": "@id" From 80ab2572a4d5590d738cc763a87156b3f79362fb Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sun, 23 Jan 2022 13:55:25 -0600 Subject: [PATCH 17/46] Return quote_url through the API, don't render quotes more than 1 level deep --- lib/pleroma/web/api_spec/schemas/status.ex | 6 +++++ .../web/mastodon_api/views/status_view.ex | 7 +++++- .../controllers/status_controller_test.exs | 9 +++++--- .../mastodon_api/views/status_view_test.exs | 23 +++++++++++++++++++ 4 files changed, 41 insertions(+), 4 deletions(-) diff --git a/lib/pleroma/web/api_spec/schemas/status.ex b/lib/pleroma/web/api_spec/schemas/status.ex index 39241aa39..f4ee9b38c 100644 --- a/lib/pleroma/web/api_spec/schemas/status.ex +++ b/lib/pleroma/web/api_spec/schemas/status.ex @@ -198,6 +198,12 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Status do nullable: true, description: "Quoted status (if any)" }, + quote_url: %Schema{ + type: :string, + format: :uri, + nullable: true, + description: "URL of the quoted status" + }, local: %Schema{ type: :boolean, description: "`true` if the post was made on the local instance" diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex index 5bde1ce04..06adfb221 100644 --- a/lib/pleroma/web/mastodon_api/views/status_view.ex +++ b/lib/pleroma/web/mastodon_api/views/status_view.ex @@ -316,7 +316,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do quote_post = if quote_activity do - quote_rendering_opts = Map.put(opts, :activity, quote_activity) + quote_rendering_opts = Map.merge(opts, %{activity: quote_activity, show_quote: false}) render("show.json", quote_rendering_opts) else nil @@ -431,6 +431,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do context: object.data["context"], in_reply_to_account_acct: reply_to_user && reply_to_user.nickname, quote: quote_post, + quote_url: object.data["quoteUrl"], content: %{"text/plain" => content_plaintext}, spoiler_text: %{"text/plain" => summary}, expires_at: expires_at, @@ -666,6 +667,10 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do end end + def get_quote(_activity, %{show_quote: false}) do + nil + end + def get_quote(activity, %{quoted_activities: quoted_activities}) do object = Object.normalize(activity, fetch: false) quoted_activities[object.data["quoteUrl"]] 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 ba49256b5..de3b52e26 100644 --- a/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs +++ b/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs @@ -128,7 +128,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do test "posting a quote post", %{conn: conn} do user = insert(:user) - {:ok, %{id: activity_id}} = CommonAPI.post(user, %{status: "yolo"}) + {:ok, %{id: activity_id} = activity} = CommonAPI.post(user, %{status: "yolo"}) + %{data: %{"id" => quote_url}} = Object.normalize(activity) conn = conn @@ -138,8 +139,10 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do "quote_id" => activity_id }) - assert %{"id" => id, "pleroma" => %{"quote" => %{"id" => ^activity_id}}} = - json_response_and_validate_schema(conn, 200) + assert %{ + "id" => id, + "pleroma" => %{"quote" => %{"id" => ^activity_id}, "quote_url" => ^quote_url} + } = json_response_and_validate_schema(conn, 200) assert Activity.get_by_id(id) end diff --git a/test/pleroma/web/mastodon_api/views/status_view_test.exs b/test/pleroma/web/mastodon_api/views/status_view_test.exs index b10b0f0b9..f50b02799 100644 --- a/test/pleroma/web/mastodon_api/views/status_view_test.exs +++ b/test/pleroma/web/mastodon_api/views/status_view_test.exs @@ -327,6 +327,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do context: object_data["context"], in_reply_to_account_acct: nil, quote: nil, + quote_url: nil, content: %{"text/plain" => HTML.strip_tags(object_data["content"])}, spoiler_text: %{"text/plain" => HTML.strip_tags(object_data["summary"])}, expires_at: nil, @@ -423,6 +424,28 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do assert status.in_reply_to_id == to_string(note.id) end + test "a quote post" do + post = insert(:note_activity) + user = insert(:user) + + {:ok, quote_post} = CommonAPI.post(user, %{status: "he", quote_id: post.id}) + {:ok, quoted_quote_post} = CommonAPI.post(user, %{status: "yo", quote_id: quote_post.id}) + + status = StatusView.render("show.json", %{activity: quoted_quote_post}) + + assert status.pleroma.quote.id == to_string(quote_post.id) + assert status.pleroma.quote_url == Object.normalize(quote_post).data["id"] + + # Quotes don't go more than one level deep + refute status.pleroma.quote.pleroma.quote + assert status.pleroma.quote.pleroma.quote_url == Object.normalize(post).data["id"] + + # In an index + [status] = StatusView.render("index.json", %{activities: [quoted_quote_post], as: :activity}) + + assert status.pleroma.quote.id == to_string(quote_post.id) + end + test "contains mentions" do user = insert(:user) mentioned = insert(:user) From 54a989793878c63900d2c6de7b4ffc8fbd8fe8c6 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sun, 23 Jan 2022 15:46:44 -0600 Subject: [PATCH 18/46] ActivityDraft: mention the OP of a quoted post --- lib/pleroma/web/common_api/activity_draft.ex | 19 +++++++++++-------- test/pleroma/web/common_api_test.exs | 12 ++++++++++++ 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/lib/pleroma/web/common_api/activity_draft.ex b/lib/pleroma/web/common_api/activity_draft.ex index 375aabc91..588aba55e 100644 --- a/lib/pleroma/web/common_api/activity_draft.ex +++ b/lib/pleroma/web/common_api/activity_draft.ex @@ -137,11 +137,11 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do defp quote_post(%{params: %{quote_id: ""}} = draft), do: draft defp quote_post(%{params: %{quote_id: id}} = draft) when is_binary(id) do - %__MODULE__{draft | quote_post: Activity.get_by_id(id)} - end - - defp quote_post(%{params: %{quote_id: %Activity{} = quote_post}} = draft) do - %__MODULE__{draft | quote_post: quote_post} + with %Activity{actor: actor_ap_id} = activity <- Activity.get_by_id(id) do + %__MODULE__{draft | quote_post: activity, mentions: [actor_ap_id]} + else + _ -> draft + end end defp quote_post(draft), do: draft @@ -178,12 +178,15 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do end end - defp content(draft) do + defp content(%{mentions: mentions} = draft) do {content_html, mentioned_users, tags} = Utils.make_content_html(draft) + mentioned_ap_ids = + Enum.map(mentioned_users, fn {_, mentioned_user} -> mentioned_user.ap_id end) + mentions = - mentioned_users - |> Enum.map(fn {_, mentioned_user} -> mentioned_user.ap_id end) + mentions + |> Kernel.++(mentioned_ap_ids) |> Utils.get_addressed_users(draft.params[:to]) %__MODULE__{draft | content_html: content_html, mentions: mentions, tags: tags} diff --git a/test/pleroma/web/common_api_test.exs b/test/pleroma/web/common_api_test.exs index 734e6dd82..051e770d7 100644 --- a/test/pleroma/web/common_api_test.exs +++ b/test/pleroma/web/common_api_test.exs @@ -807,6 +807,18 @@ defmodule Pleroma.Web.CommonAPITest do quote_post = Object.normalize(quote_post) assert quote_post.data["quoteUrl"] == quoted.data["id"] + + # The OP is mentioned + assert quoted.data["actor"] in quote_post.data["to"] + end + + test "quote posting with explicit addressing doesn't mention the OP" do + user = insert(:user) + + {:ok, quoted} = CommonAPI.post(user, %{status: "Hello world"}) + {:ok, quote_post} = CommonAPI.post(user, %{status: "nice post", quote_id: quoted.id, to: []}) + + assert Object.normalize(quote_post).data["to"] == [Pleroma.Constants.as_public()] end end From 1f19dd76f66ca657ddfe79a51e59b8997a4c6321 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sun, 23 Jan 2022 16:03:46 -0600 Subject: [PATCH 19/46] ActivityDraft: mix format, defensive actor ID --- lib/pleroma/web/common_api/activity_draft.ex | 16 ++++++++++------ test/pleroma/web/common_api_test.exs | 4 +++- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/lib/pleroma/web/common_api/activity_draft.ex b/lib/pleroma/web/common_api/activity_draft.ex index 588aba55e..95534f3cb 100644 --- a/lib/pleroma/web/common_api/activity_draft.ex +++ b/lib/pleroma/web/common_api/activity_draft.ex @@ -11,6 +11,7 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do alias Pleroma.Web.CommonAPI.Utils import Pleroma.Web.Gettext + import Pleroma.Web.Utils.Guards, only: [not_empty_string: 1] defstruct valid?: true, errors: [], @@ -134,13 +135,16 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do defp in_reply_to(draft), do: draft - defp quote_post(%{params: %{quote_id: ""}} = draft), do: draft + defp quote_post(%{params: %{quote_id: id}} = draft) when not_empty_string(id) do + case Activity.get_by_id(id) do + %Activity{actor: actor_ap_id} = activity when not_empty_string(actor_ap_id) -> + %__MODULE__{draft | quote_post: activity, mentions: [actor_ap_id]} - defp quote_post(%{params: %{quote_id: id}} = draft) when is_binary(id) do - with %Activity{actor: actor_ap_id} = activity <- Activity.get_by_id(id) do - %__MODULE__{draft | quote_post: activity, mentions: [actor_ap_id]} - else - _ -> draft + %Activity{} = activity -> + %__MODULE__{draft | quote_post: activity} + + _ -> + draft end end diff --git a/test/pleroma/web/common_api_test.exs b/test/pleroma/web/common_api_test.exs index 051e770d7..960d0cf16 100644 --- a/test/pleroma/web/common_api_test.exs +++ b/test/pleroma/web/common_api_test.exs @@ -816,7 +816,9 @@ defmodule Pleroma.Web.CommonAPITest do user = insert(:user) {:ok, quoted} = CommonAPI.post(user, %{status: "Hello world"}) - {:ok, quote_post} = CommonAPI.post(user, %{status: "nice post", quote_id: quoted.id, to: []}) + + {:ok, quote_post} = + CommonAPI.post(user, %{status: "nice post", quote_id: quoted.id, to: []}) assert Object.normalize(quote_post).data["to"] == [Pleroma.Constants.as_public()] end From 36a5578d2b16ba9f771fff55daafa85ec606a6be Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 24 Jan 2022 15:34:23 -0600 Subject: [PATCH 20/46] Scrubber.Default: allow span.quote-inline for quote post compatibility --- priv/scrubbers/default.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/priv/scrubbers/default.ex b/priv/scrubbers/default.ex index d1215d2e0..56324a9fa 100644 --- a/priv/scrubbers/default.ex +++ b/priv/scrubbers/default.ex @@ -60,7 +60,7 @@ defmodule Pleroma.HTML.Scrubber.Default do Meta.allow_tag_with_these_attributes(:u, ["lang"]) Meta.allow_tag_with_these_attributes(:ul, ["lang"]) - Meta.allow_tag_with_this_attribute_values(:span, "class", ["h-card", "recipients-inline"]) + Meta.allow_tag_with_this_attribute_values(:span, "class", ["h-card", "recipients-inline", "quote-inline"]) Meta.allow_tag_with_these_attributes(:span, ["lang"]) Meta.allow_tag_with_this_attribute_values(:code, "class", ["inline"]) From 57ef1d121101d785c043ef6aaf2d33bb9be3ec3b Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 24 Jan 2022 16:44:35 -0600 Subject: [PATCH 21/46] Add InlineQuotePolicy to force quote URLs inline --- config/config.exs | 2 + docs/configuration/cheatsheet.md | 4 ++ .../activity_pub/mrf/inline_quote_policy.ex | 53 ++++++++++++++++++ .../mrf/inline_quote_policy_test.exs | 56 +++++++++++++++++++ 4 files changed, 115 insertions(+) create mode 100644 lib/pleroma/web/activity_pub/mrf/inline_quote_policy.ex create mode 100644 test/pleroma/web/activity_pub/mrf/inline_quote_policy_test.exs diff --git a/config/config.exs b/config/config.exs index ebcbf8b49..56cc34db5 100644 --- a/config/config.exs +++ b/config/config.exs @@ -434,6 +434,8 @@ config :pleroma, :mrf_object_age, config :pleroma, :mrf_follow_bot, follower_nickname: nil +config :pleroma, :mrf_inline_quote, prefix: "RT" + config :pleroma, :rich_media, enabled: true, ignore_hosts: [], diff --git a/docs/configuration/cheatsheet.md b/docs/configuration/cheatsheet.md index f43cde114..32cc5811a 100644 --- a/docs/configuration/cheatsheet.md +++ b/docs/configuration/cheatsheet.md @@ -160,6 +160,7 @@ To add configuration to your config file, you can copy it from the base config. * `Pleroma.Web.ActivityPub.MRF.AntiFollowbotPolicy`: Drops follow requests from followbots. Users can still allow bots to follow them by first following the bot. * `Pleroma.Web.ActivityPub.MRF.KeywordPolicy`: Rejects or removes from the federated timeline or replaces keywords. (See [`:mrf_keyword`](#mrf_keyword)). * `Pleroma.Web.ActivityPub.MRF.ForceMentionsInContent`: Forces every mentioned user to be reflected in the post content. + * `Pleroma.Web.ActivityPub.MRF.InlineQuotePolicy`: Forces quote post URLs to be reflected in the message content inline. * `transparency`: Make the content of your Message Rewrite Facility settings public (via nodeinfo). * `transparency_exclusions`: Exclude specific instance names from MRF transparency. The use of the exclusions feature will be disclosed in nodeinfo as a boolean value. @@ -267,6 +268,9 @@ Notes: * `federated_timeline_removal_url`: A list of patterns which result in message with emojis whose URLs match being removed from federated timelines (a.k.a unlisted). This will apply only to statuses. Each pattern can be a string or a [regular expression](https://hexdocs.pm/elixir/Regex.html). * `federated_timeline_removal_shortcode`: A list of patterns which result in message with emojis whose shortcodes match being removed from federated timelines (a.k.a unlisted). This will apply only to statuses. Each pattern can be a string or a [regular expression](https://hexdocs.pm/elixir/Regex.html). +#### :mrf_inline_quote +* `prefix`: Prefix before the link (default: `RT`) + ### :activitypub * `unfollow_blocked`: Whether blocks result in people getting unfollowed * `outgoing_blocks`: Whether to federate blocks to other instances diff --git a/lib/pleroma/web/activity_pub/mrf/inline_quote_policy.ex b/lib/pleroma/web/activity_pub/mrf/inline_quote_policy.ex new file mode 100644 index 000000000..0f1dc9f42 --- /dev/null +++ b/lib/pleroma/web/activity_pub/mrf/inline_quote_policy.ex @@ -0,0 +1,53 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2021 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.MRF.InlineQuotePolicy do + @moduledoc "Force a quote line into the message content." + @behaviour Pleroma.Web.ActivityPub.MRF.Policy + + defp build_inline_quote(prefix, url) do + "

#{prefix}: #{url}
" + end + + defp filter_object(%{"quoteUrl" => quote_url} = object) do + content = object["content"] || "" + + if content =~ quote_url do + object + else + prefix = Pleroma.Config.get([:mrf_inline_quote, :prefix]) + content = content <> build_inline_quote(prefix, quote_url) + Map.put(object, "content", content) + end + end + + @impl true + def filter(%{"object" => %{"quoteUrl" => _} = object} = activity) do + {:ok, Map.put(activity, "object", filter_object(object))} + end + + @impl true + def filter(object), do: {:ok, object} + + @impl true + def describe, do: {:ok, %{}} + + @impl true + def config_description do + %{ + key: :mrf_inline_quote, + related_policy: "Pleroma.Web.ActivityPub.MRF.InlineQuotePolicy", + label: "MRF Inline Quote", + description: "Force quote post URLs inline", + children: [ + %{ + key: :prefix, + type: :string, + description: "Prefix before the link", + suggestions: ["RT", "QT", "RE", "RN"] + } + ] + } + end +end diff --git a/test/pleroma/web/activity_pub/mrf/inline_quote_policy_test.exs b/test/pleroma/web/activity_pub/mrf/inline_quote_policy_test.exs new file mode 100644 index 000000000..81dc06dda --- /dev/null +++ b/test/pleroma/web/activity_pub/mrf/inline_quote_policy_test.exs @@ -0,0 +1,56 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2021 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.MRF.InlineQuotePolicyTest do + alias Pleroma.Web.ActivityPub.MRF.InlineQuotePolicy + use Pleroma.DataCase + + test "adds quote URL to post content" do + quote_url = "https://gleasonator.com/objects/1234" + + activity = %{ + "type" => "Create", + "actor" => "https://gleasonator.com/users/alex", + "object" => %{ + "type" => "Note", + "content" => "

Nice post

", + "quoteUrl" => quote_url + } + } + + {:ok, %{"object" => %{"content" => filtered}}} = InlineQuotePolicy.filter(activity) + + assert filtered == + "

Nice post



RT: https://gleasonator.com/objects/1234
" + end + + test "ignores Misskey quote posts" do + object = File.read!("test/fixtures/quote_post/misskey_quote_post.json") |> Jason.decode!() + + activity = %{ + "type" => "Create", + "actor" => "https://misskey.io/users/7rkrarq81i", + "object" => object + } + + {:ok, filtered} = InlineQuotePolicy.filter(activity) + assert filtered == activity + end + + test "ignores Fedibird quote posts" do + object = File.read!("test/fixtures/quote_post/fedibird_quote_post.json") |> Jason.decode!() + + # Normally the ObjectValidator will fix this before it reaches MRF + object = Map.put(object, "quoteUrl", object["quoteURL"]) + + activity = %{ + "type" => "Create", + "actor" => "https://fedibird.com/users/noellabo", + "object" => object + } + + {:ok, filtered} = InlineQuotePolicy.filter(activity) + assert filtered == activity + end +end From 59326247aa754991add9170e204257a8bf94c40f Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 26 Jan 2022 11:21:49 -0600 Subject: [PATCH 22/46] CommonAPI: disallow quoting private posts through the API --- lib/pleroma/web/common_api/activity_draft.ex | 15 ++++++++++- .../web/common_api/activity_draft_test.exs | 26 +++++++++++++++++++ test/pleroma/web/common_api_test.exs | 14 ++++++++++ 3 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 test/pleroma/web/common_api/activity_draft_test.exs diff --git a/lib/pleroma/web/common_api/activity_draft.ex b/lib/pleroma/web/common_api/activity_draft.ex index 95534f3cb..d4875765c 100644 --- a/lib/pleroma/web/common_api/activity_draft.ex +++ b/lib/pleroma/web/common_api/activity_draft.ex @@ -7,6 +7,7 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do alias Pleroma.Conversation.Participation alias Pleroma.Object alias Pleroma.Web.ActivityPub.Builder + alias Pleroma.Web.ActivityPub.Visibility alias Pleroma.Web.CommonAPI alias Pleroma.Web.CommonAPI.Utils @@ -57,6 +58,7 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do |> with_valid(&in_reply_to_conversation/1) |> with_valid("e_post/1) |> with_valid(&visibility/1) + |> with_valid("ing_visibility/1) |> content() |> with_valid(&to_and_cc/1) |> with_valid(&context/1) @@ -136,7 +138,7 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do defp in_reply_to(draft), do: draft defp quote_post(%{params: %{quote_id: id}} = draft) when not_empty_string(id) do - case Activity.get_by_id(id) do + case Activity.get_by_id_with_object(id) do %Activity{actor: actor_ap_id} = activity when not_empty_string(actor_ap_id) -> %__MODULE__{draft | quote_post: activity, mentions: [actor_ap_id]} @@ -165,6 +167,17 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do end end + defp quoting_visibility(%{quote_post: %Activity{}} = draft) do + with %Object{} = object <- Object.normalize(draft.quote_post, fetch: false), + visibility when visibility in ~w(public unlisted) <- Visibility.get_visibility(object) do + draft + else + _ -> add_error(draft, dgettext("errors", "Cannot quote private message")) + end + end + + defp quoting_visibility(draft), do: draft + defp expires_at(draft) do case CommonAPI.check_expiry_date(draft.params[:expires_in]) do {:ok, expires_at} -> %__MODULE__{draft | expires_at: expires_at} diff --git a/test/pleroma/web/common_api/activity_draft_test.exs b/test/pleroma/web/common_api/activity_draft_test.exs new file mode 100644 index 000000000..8a09fc710 --- /dev/null +++ b/test/pleroma/web/common_api/activity_draft_test.exs @@ -0,0 +1,26 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2021 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.CommonAPI.ActivityDraftTest do + use Pleroma.DataCase + + alias Pleroma.Web.CommonAPI + alias Pleroma.Web.CommonAPI.ActivityDraft + + import Pleroma.Factory + + test "create/2 with a quote post" do + user = insert(:user) + + {:ok, direct} = CommonAPI.post(user, %{status: ".", visibility: "direct"}) + {:ok, private} = CommonAPI.post(user, %{status: ".", visibility: "private"}) + {:ok, unlisted} = CommonAPI.post(user, %{status: ".", visibility: "unlisted"}) + {:ok, public} = CommonAPI.post(user, %{status: ".", visibility: "public"}) + + {:error, _} = ActivityDraft.create(user, %{status: "nice", quote_id: direct.id}) + {:error, _} = ActivityDraft.create(user, %{status: "nice", quote_id: private.id}) + {:ok, _} = ActivityDraft.create(user, %{status: "nice", quote_id: unlisted.id}) + {:ok, _} = ActivityDraft.create(user, %{status: "nice", quote_id: public.id}) + end +end diff --git a/test/pleroma/web/common_api_test.exs b/test/pleroma/web/common_api_test.exs index 960d0cf16..c4eba8b9c 100644 --- a/test/pleroma/web/common_api_test.exs +++ b/test/pleroma/web/common_api_test.exs @@ -822,6 +822,20 @@ defmodule Pleroma.Web.CommonAPITest do assert Object.normalize(quote_post).data["to"] == [Pleroma.Constants.as_public()] end + + test "quote posting visibility" do + user = insert(:user) + + {:ok, direct} = CommonAPI.post(user, %{status: ".", visibility: "direct"}) + {:ok, private} = CommonAPI.post(user, %{status: ".", visibility: "private"}) + {:ok, unlisted} = CommonAPI.post(user, %{status: ".", visibility: "unlisted"}) + {:ok, public} = CommonAPI.post(user, %{status: ".", visibility: "public"}) + + {:error, _} = CommonAPI.post(user, %{status: "nice", quote_id: direct.id}) + {:error, _} = CommonAPI.post(user, %{status: "nice", quote_id: private.id}) + {:ok, _} = CommonAPI.post(user, %{status: "nice", quote_id: unlisted.id}) + {:ok, _} = CommonAPI.post(user, %{status: "nice", quote_id: public.id}) + end end describe "reactions" do From 6f11f11519f9c735f6b059c250f4bf01e09b305f Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 26 Jan 2022 11:49:31 -0600 Subject: [PATCH 23/46] StatusView: fix quote visibility --- .../web/mastodon_api/views/status_view.ex | 2 +- .../mastodon_api/views/status_view_test.exs | 41 +++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex index 06adfb221..7360d1093 100644 --- a/lib/pleroma/web/mastodon_api/views/status_view.ex +++ b/lib/pleroma/web/mastodon_api/views/status_view.ex @@ -315,7 +315,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do quote_activity = get_quote(activity, opts) quote_post = - if quote_activity do + if visible_for_user?(quote_activity, opts[:for]) do quote_rendering_opts = Map.merge(opts, %{activity: quote_activity, show_quote: false}) render("show.json", quote_rendering_opts) else diff --git a/test/pleroma/web/mastodon_api/views/status_view_test.exs b/test/pleroma/web/mastodon_api/views/status_view_test.exs index f50b02799..f41ef580d 100644 --- a/test/pleroma/web/mastodon_api/views/status_view_test.exs +++ b/test/pleroma/web/mastodon_api/views/status_view_test.exs @@ -446,6 +446,47 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do assert status.pleroma.quote.id == to_string(quote_post.id) end + test "quoted private post" do + user = insert(:user) + + # Insert a private post + private = insert(:followers_only_note_activity, user: user) + private_object = Object.normalize(private) + + # Create a public post quoting the private post + quote_private = + insert(:note_activity, note: insert(:note, data: %{"quoteUrl" => private_object.data["id"]})) + + status = StatusView.render("show.json", %{activity: quote_private}) + + # The quote isn't rendered + refute status.pleroma.quote + assert status.pleroma.quote_url == private_object.data["id"] + + # After following the user, the quote is rendered + follower = insert(:user) + CommonAPI.follow(follower, user) + + status = StatusView.render("show.json", %{activity: quote_private, for: follower}) + assert status.pleroma.quote.id == to_string(private.id) + end + + test "quoted direct message" do + # Insert a direct message + direct = insert(:direct_note_activity) + direct_object = Object.normalize(direct) + + # Create a public post quoting the direct message + quote_direct = + insert(:note_activity, note: insert(:note, data: %{"quoteUrl" => direct_object.data["id"]})) + + status = StatusView.render("show.json", %{activity: quote_direct}) + + # The quote isn't rendered + refute status.pleroma.quote + assert status.pleroma.quote_url == direct_object.data["id"] + end + test "contains mentions" do user = insert(:user) mentioned = insert(:user) From 74e0a4555f583a6962ad116bf6e54f06e42fe465 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 26 Jan 2022 11:52:50 -0600 Subject: [PATCH 24/46] StatusView: add `quote_visible` param --- lib/pleroma/web/api_spec/schemas/status.ex | 4 ++++ lib/pleroma/web/mastodon_api/views/status_view.ex | 1 + test/pleroma/web/mastodon_api/views/status_view_test.exs | 4 ++++ 3 files changed, 9 insertions(+) diff --git a/lib/pleroma/web/api_spec/schemas/status.ex b/lib/pleroma/web/api_spec/schemas/status.ex index f4ee9b38c..5d0eedb08 100644 --- a/lib/pleroma/web/api_spec/schemas/status.ex +++ b/lib/pleroma/web/api_spec/schemas/status.ex @@ -204,6 +204,10 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Status do nullable: true, description: "URL of the quoted status" }, + quote_visible: %Schema{ + type: :boolean, + description: "`true` if the quoted post is visible to the user" + }, local: %Schema{ type: :boolean, description: "`true` if the post was made on the local instance" diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex index 7360d1093..2aa44b0f6 100644 --- a/lib/pleroma/web/mastodon_api/views/status_view.ex +++ b/lib/pleroma/web/mastodon_api/views/status_view.ex @@ -432,6 +432,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do in_reply_to_account_acct: reply_to_user && reply_to_user.nickname, quote: quote_post, quote_url: object.data["quoteUrl"], + quote_visible: visible_for_user?(quote_activity, opts[:for]), content: %{"text/plain" => content_plaintext}, spoiler_text: %{"text/plain" => summary}, expires_at: expires_at, diff --git a/test/pleroma/web/mastodon_api/views/status_view_test.exs b/test/pleroma/web/mastodon_api/views/status_view_test.exs index f41ef580d..ed0a87558 100644 --- a/test/pleroma/web/mastodon_api/views/status_view_test.exs +++ b/test/pleroma/web/mastodon_api/views/status_view_test.exs @@ -328,6 +328,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do in_reply_to_account_acct: nil, quote: nil, quote_url: nil, + quote_visible: false, content: %{"text/plain" => HTML.strip_tags(object_data["content"])}, spoiler_text: %{"text/plain" => HTML.strip_tags(object_data["summary"])}, expires_at: nil, @@ -462,6 +463,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do # The quote isn't rendered refute status.pleroma.quote assert status.pleroma.quote_url == private_object.data["id"] + refute status.pleroma.quote_visible # After following the user, the quote is rendered follower = insert(:user) @@ -469,6 +471,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do status = StatusView.render("show.json", %{activity: quote_private, for: follower}) assert status.pleroma.quote.id == to_string(private.id) + assert status.pleroma.quote_visible end test "quoted direct message" do @@ -485,6 +488,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do # The quote isn't rendered refute status.pleroma.quote assert status.pleroma.quote_url == direct_object.data["id"] + refute status.pleroma.quote_visible end test "contains mentions" do From bee7e419597615ac6852942fe563166feba3fe73 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 27 Jan 2022 14:28:06 -0600 Subject: [PATCH 25/46] InlineQuotePolicy: don't add line breaks to markdown posts --- .../activity_pub/mrf/inline_quote_policy.ex | 12 ++++++++--- .../mrf/inline_quote_policy_test.exs | 21 ++++++++++++++++++- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/lib/pleroma/web/activity_pub/mrf/inline_quote_policy.ex b/lib/pleroma/web/activity_pub/mrf/inline_quote_policy.ex index 0f1dc9f42..46013fc5e 100644 --- a/lib/pleroma/web/activity_pub/mrf/inline_quote_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/inline_quote_policy.ex @@ -6,8 +6,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.InlineQuotePolicy do @moduledoc "Force a quote line into the message content." @behaviour Pleroma.Web.ActivityPub.MRF.Policy - defp build_inline_quote(prefix, url) do - "

#{prefix}: #{url}
" + defp build_inline_quote(prefix, url, br) do + "#{String.duplicate("
", br)}#{prefix}: #{url}
" end defp filter_object(%{"quoteUrl" => quote_url} = object) do @@ -17,7 +17,13 @@ defmodule Pleroma.Web.ActivityPub.MRF.InlineQuotePolicy do object else prefix = Pleroma.Config.get([:mrf_inline_quote, :prefix]) - content = content <> build_inline_quote(prefix, quote_url) + + inline_quote = + if String.ends_with?(content, "

"), + do: build_inline_quote(prefix, quote_url, 0), + else: build_inline_quote(prefix, quote_url, 2) + + content = content <> inline_quote Map.put(object, "content", content) end end diff --git a/test/pleroma/web/activity_pub/mrf/inline_quote_policy_test.exs b/test/pleroma/web/activity_pub/mrf/inline_quote_policy_test.exs index 81dc06dda..8e75aaaab 100644 --- a/test/pleroma/web/activity_pub/mrf/inline_quote_policy_test.exs +++ b/test/pleroma/web/activity_pub/mrf/inline_quote_policy_test.exs @@ -9,6 +9,25 @@ defmodule Pleroma.Web.ActivityPub.MRF.InlineQuotePolicyTest do test "adds quote URL to post content" do quote_url = "https://gleasonator.com/objects/1234" + activity = %{ + "type" => "Create", + "actor" => "https://gleasonator.com/users/alex", + "object" => %{ + "type" => "Note", + "content" => "Nice post", + "quoteUrl" => quote_url + } + } + + {:ok, %{"object" => %{"content" => filtered}}} = InlineQuotePolicy.filter(activity) + + assert filtered == + "Nice post

RT: https://gleasonator.com/objects/1234
" + end + + test "doesn't add line breaks to markdown posts" do + quote_url = "https://gleasonator.com/objects/1234" + activity = %{ "type" => "Create", "actor" => "https://gleasonator.com/users/alex", @@ -22,7 +41,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.InlineQuotePolicyTest do {:ok, %{"object" => %{"content" => filtered}}} = InlineQuotePolicy.filter(activity) assert filtered == - "

Nice post



RT: https://gleasonator.com/objects/1234
" + "

Nice post

RT: https://gleasonator.com/objects/1234" end test "ignores Misskey quote posts" do From 93e4972b50f9bb0a07a7074fbab2aedbdc0cc4eb Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 27 Jan 2022 15:01:20 -0600 Subject: [PATCH 26/46] Add InlineQuotePolicy as a default MRF --- config/config.exs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/config/config.exs b/config/config.exs index 56cc34db5..9149e925a 100644 --- a/config/config.exs +++ b/config/config.exs @@ -860,7 +860,11 @@ config :pleroma, :restrict_unauthenticated, config :pleroma, Pleroma.Web.ApiSpec.CastAndValidate, strict: false config :pleroma, :mrf, - policies: [Pleroma.Web.ActivityPub.MRF.ObjectAgePolicy, Pleroma.Web.ActivityPub.MRF.TagPolicy], + policies: [ + Pleroma.Web.ActivityPub.MRF.ObjectAgePolicy, + Pleroma.Web.ActivityPub.MRF.TagPolicy, + Pleroma.Web.ActivityPub.MRF.InlineQuotePolicy + ], transparency: true, transparency_exclusions: [] From cf8e4258830e3f362ab1e54238d622f6b2056502 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 28 Jan 2022 12:33:07 -0600 Subject: [PATCH 27/46] StatusView: return quote post inside a reblog --- lib/pleroma/web/mastodon_api/views/status_view.ex | 10 ++++++---- .../web/mastodon_api/views/status_view_test.exs | 12 ++++++++++++ 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex index 2aa44b0f6..ba4a8f3eb 100644 --- a/lib/pleroma/web/mastodon_api/views/status_view.ex +++ b/lib/pleroma/web/mastodon_api/views/status_view.ex @@ -668,13 +668,15 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do end end - def get_quote(_activity, %{show_quote: false}) do - nil - end + def get_quote(_activity, %{show_quote: false}), do: nil def get_quote(activity, %{quoted_activities: quoted_activities}) do object = Object.normalize(activity, fetch: false) - quoted_activities[object.data["quoteUrl"]] + + with nil <- quoted_activities[object.data["quoteUrl"]] do + # For when a quote post is inside an Announce + Activity.get_create_by_object_ap_id_with_object(object.data["quoteUrl"]) + end end def get_quote(%{data: %{"object" => _object}} = activity, _) do diff --git a/test/pleroma/web/mastodon_api/views/status_view_test.exs b/test/pleroma/web/mastodon_api/views/status_view_test.exs index ed0a87558..6d3a72970 100644 --- a/test/pleroma/web/mastodon_api/views/status_view_test.exs +++ b/test/pleroma/web/mastodon_api/views/status_view_test.exs @@ -491,6 +491,18 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do refute status.pleroma.quote_visible end + test "repost of quote post" do + post = insert(:note_activity) + user = insert(:user) + + {:ok, quote_post} = CommonAPI.post(user, %{status: "he", quote_id: post.id}) + {:ok, repost} = CommonAPI.repeat(quote_post.id, user) + + [status] = StatusView.render("index.json", %{activities: [repost], as: :activity}) + + assert status.reblog.pleroma.quote.id == to_string(post.id) + end + test "contains mentions" do user = insert(:user) mentioned = insert(:user) From 3c8319fe9f7a7c793f8fdc347be2015190981e33 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 28 Jan 2022 14:06:32 -0600 Subject: [PATCH 28/46] Transmogrifier: federate quotes with _misskey_quote field --- lib/pleroma/web/activity_pub/transmogrifier.ex | 9 +++++++++ priv/static/schemas/litepub-0.1.jsonld | 2 ++ test/pleroma/web/activity_pub/transmogrifier_test.exs | 4 ++++ 3 files changed, 15 insertions(+) diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex index f5771e75e..163ae54fa 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier.ex @@ -660,6 +660,14 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do def set_reply_to_uri(obj), do: obj + # Misskey quotes + # Despite being underscored, it's potentially more reliable for interop. + def set_quote_url(%{"quoteUrl" => quote_url} = object) when is_binary(quote_url) do + Map.put(object, "_misskey_quote", quote_url) + end + + def set_quote_url(obj), do: obj + @doc """ Serialized Mastodon-compatible `replies` collection containing _self-replies_. Based on Mastodon's ActivityPub::NoteSerializer#replies. @@ -714,6 +722,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do |> prepare_attachments |> set_conversation |> set_reply_to_uri + |> set_quote_url |> set_replies |> strip_internal_fields |> strip_internal_tags diff --git a/priv/static/schemas/litepub-0.1.jsonld b/priv/static/schemas/litepub-0.1.jsonld index 5d8244a11..8559e744d 100644 --- a/priv/static/schemas/litepub-0.1.jsonld +++ b/priv/static/schemas/litepub-0.1.jsonld @@ -17,6 +17,7 @@ "ostatus": "http://ostatus.org#", "schema": "http://schema.org#", "toot": "http://joinmastodon.org/ns#", + "misskey": "https://misskey-hub.net/ns#", "value": "schema:value", "sensitive": "as:sensitive", "litepub": "http://litepub.social/ns#", @@ -27,6 +28,7 @@ "@type": "@id" }, "quoteUrl": "as:quoteUrl", + "_misskey_quote": "misskey:_misskey_quote", "oauthRegistrationEndpoint": { "@id": "litepub:oauthRegistrationEndpoint", "@type": "@id" diff --git a/test/pleroma/web/activity_pub/transmogrifier_test.exs b/test/pleroma/web/activity_pub/transmogrifier_test.exs index 824398e38..8c7e0a4c9 100644 --- a/test/pleroma/web/activity_pub/transmogrifier_test.exs +++ b/test/pleroma/web/activity_pub/transmogrifier_test.exs @@ -382,7 +382,11 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do {:ok, modified} = Transmogrifier.prepare_outgoing(quote_post.data) quoted_post = Object.normalize(quoted_post) + assert modified["object"]["quoteUrl"] == quoted_post.data["id"] + + # Add Misskey's quote as a fallback + assert modified["object"]["_misskey_quote"] == quoted_post.data["id"] end end From 817e308c0d9e42dfc39742c554e4c7a8da6f1c50 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 28 Jan 2022 15:55:52 -0600 Subject: [PATCH 29/46] Handle Fedibird's new quoteUri field --- .../article_note_page_validator.ex | 8 ++- .../web/activity_pub/transmogrifier.ex | 19 +++++-- priv/static/schemas/litepub-0.1.jsonld | 2 + .../quote_post/fedibird_quote_uri.json | 54 +++++++++++++++++++ .../article_note_page_validator_test.exs | 10 ++++ .../web/activity_pub/transmogrifier_test.exs | 9 ++-- 6 files changed, 92 insertions(+), 10 deletions(-) create mode 100644 test/fixtures/quote_post/fedibird_quote_uri.json diff --git a/lib/pleroma/web/activity_pub/object_validators/article_note_page_validator.ex b/lib/pleroma/web/activity_pub/object_validators/article_note_page_validator.ex index 40bb67934..0b435b251 100644 --- a/lib/pleroma/web/activity_pub/object_validators/article_note_page_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/article_note_page_validator.ex @@ -78,7 +78,13 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidator do defp fix_quote_url(%{"quoteUrl" => _quote_url} = data), do: data - # Fix for Fedibird + # Fedibird + # https://github.com/fedibird/mastodon/commit/dbd7ae6cf58a92ec67c512296b4daaea0d01e6ac + defp fix_quote_url(%{"quoteUri" => quote_url} = data) do + Map.put(data, "quoteUrl", quote_url) + end + + # Old Fedibird (bug) # https://github.com/fedibird/mastodon/issues/9 defp fix_quote_url(%{"quoteURL" => quote_url} = data) do Map.put(data, "quoteUrl", quote_url) diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex index 163ae54fa..01e135fc1 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier.ex @@ -180,7 +180,15 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do end end - # Fix for Fedibird + # Fedibird + # https://github.com/fedibird/mastodon/commit/dbd7ae6cf58a92ec67c512296b4daaea0d01e6ac + def fix_quote_url(%{"quoteUri" => quote_url} = object, options) do + object + |> Map.put("quoteUrl", quote_url) + |> fix_quote_url(options) + end + + # Old Fedibird (bug) # https://github.com/fedibird/mastodon/issues/9 def fix_quote_url(%{"quoteURL" => quote_url} = object, options) do object @@ -660,10 +668,13 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do def set_reply_to_uri(obj), do: obj - # Misskey quotes - # Despite being underscored, it's potentially more reliable for interop. def set_quote_url(%{"quoteUrl" => quote_url} = object) when is_binary(quote_url) do - Map.put(object, "_misskey_quote", quote_url) + Map.merge(object, %{ + # Fedibird quote + "quoteUri" => quote_url, + # Misskey quote + "_misskey_quote" => quote_url + }) end def set_quote_url(obj), do: obj diff --git a/priv/static/schemas/litepub-0.1.jsonld b/priv/static/schemas/litepub-0.1.jsonld index 8559e744d..3d68e0714 100644 --- a/priv/static/schemas/litepub-0.1.jsonld +++ b/priv/static/schemas/litepub-0.1.jsonld @@ -18,6 +18,7 @@ "schema": "http://schema.org#", "toot": "http://joinmastodon.org/ns#", "misskey": "https://misskey-hub.net/ns#", + "fedibird": "http://fedibird.com/ns#", "value": "schema:value", "sensitive": "as:sensitive", "litepub": "http://litepub.social/ns#", @@ -28,6 +29,7 @@ "@type": "@id" }, "quoteUrl": "as:quoteUrl", + "quoteUri": "fedibird:quoteUri", "_misskey_quote": "misskey:_misskey_quote", "oauthRegistrationEndpoint": { "@id": "litepub:oauthRegistrationEndpoint", diff --git a/test/fixtures/quote_post/fedibird_quote_uri.json b/test/fixtures/quote_post/fedibird_quote_uri.json new file mode 100644 index 000000000..7c328fdb9 --- /dev/null +++ b/test/fixtures/quote_post/fedibird_quote_uri.json @@ -0,0 +1,54 @@ +{ + "@context": [ + "https://www.w3.org/ns/activitystreams", + { + "ostatus": "http://ostatus.org#", + "atomUri": "ostatus:atomUri", + "inReplyToAtomUri": "ostatus:inReplyToAtomUri", + "conversation": "ostatus:conversation", + "sensitive": "as:sensitive", + "toot": "http://joinmastodon.org/ns#", + "votersCount": "toot:votersCount", + "fedibird": "http://fedibird.com/ns#", + "quoteUri": "fedibird:quoteUri", + "expiry": "fedibird:expiry" + } + ], + "id": "https://fedibird.com/users/noellabo/statuses/107699335988346142", + "type": "Note", + "summary": null, + "inReplyTo": null, + "published": "2022-01-28T09:17:30Z", + "url": "https://fedibird.com/@noellabo/107699335988346142", + "attributedTo": "https://fedibird.com/users/noellabo", + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "cc": [ + "https://fedibird.com/users/noellabo/followers" + ], + "sensitive": false, + "atomUri": "https://fedibird.com/users/noellabo/statuses/107699335988346142", + "inReplyToAtomUri": null, + "conversation": "tag:fedibird.com,2022-01-28:objectId=107699335988345290:objectType=Conversation", + "context": "https://fedibird.com/contexts/107699335988345290", + "quoteUri": "https://fedibird.com/users/yamako/statuses/107699333438289729", + "_misskey_quote": "https://fedibird.com/users/yamako/statuses/107699333438289729", + "_misskey_content": "美味しそう", + "content": "

美味しそう
QT: https://fedibird.com/@yamako/107699333438289729

", + "contentMap": { + "ja": "

美味しそう
QT: https://fedibird.com/@yamako/107699333438289729

" + }, + "attachment": [], + "tag": [], + "replies": { + "id": "https://fedibird.com/users/noellabo/statuses/107699335988346142/replies", + "type": "Collection", + "first": { + "type": "CollectionPage", + "next": "https://fedibird.com/users/noellabo/statuses/107699335988346142/replies?only_other_accounts=true&page=true", + "partOf": "https://fedibird.com/users/noellabo/statuses/107699335988346142/replies", + "items": [] + } + } +} diff --git a/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs b/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs index dec2e28c9..a4ba38e6a 100644 --- a/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs +++ b/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs @@ -127,6 +127,16 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidatorTest assert cng.changes.quoteUrl == "https://misskey.io/notes/8vsn2izjwh" end + test "Fedibird quote post with quoteUri field" do + insert(:user, ap_id: "https://fedibird.com/users/noellabo") + + data = File.read!("test/fixtures/quote_post/fedibird_quote_uri.json") |> Jason.decode!() + cng = ArticleNotePageValidator.cast_and_validate(data) + + assert cng.valid? + assert cng.changes.quoteUrl == "https://fedibird.com/users/yamako/statuses/107699333438289729" + end + test "Misskey quote post" do insert(:user, ap_id: "https://misskey.io/users/7rkrarq81i") diff --git a/test/pleroma/web/activity_pub/transmogrifier_test.exs b/test/pleroma/web/activity_pub/transmogrifier_test.exs index 8c7e0a4c9..1838f96bb 100644 --- a/test/pleroma/web/activity_pub/transmogrifier_test.exs +++ b/test/pleroma/web/activity_pub/transmogrifier_test.exs @@ -381,12 +381,11 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do {:ok, modified} = Transmogrifier.prepare_outgoing(quote_post.data) - quoted_post = Object.normalize(quoted_post) + %{data: %{"id" => quote_id}} = Object.normalize(quoted_post) - assert modified["object"]["quoteUrl"] == quoted_post.data["id"] - - # Add Misskey's quote as a fallback - assert modified["object"]["_misskey_quote"] == quoted_post.data["id"] + assert modified["object"]["quoteUrl"] == quote_id + assert modified["object"]["quoteUri"] == quote_id + assert modified["object"]["_misskey_quote"] == quote_id end end From 4075eecca0033e4487fa9d5d5bb75384597c3e79 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 28 Jan 2022 16:07:17 -0600 Subject: [PATCH 30/46] InlineQuotePolicy: improve the way Markdown quotes are displayed by other software --- .../web/activity_pub/mrf/inline_quote_policy.ex | 13 +++++++------ .../activity_pub/mrf/inline_quote_policy_test.exs | 4 ++-- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/lib/pleroma/web/activity_pub/mrf/inline_quote_policy.ex b/lib/pleroma/web/activity_pub/mrf/inline_quote_policy.ex index 46013fc5e..7de4935f2 100644 --- a/lib/pleroma/web/activity_pub/mrf/inline_quote_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/inline_quote_policy.ex @@ -6,8 +6,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.InlineQuotePolicy do @moduledoc "Force a quote line into the message content." @behaviour Pleroma.Web.ActivityPub.MRF.Policy - defp build_inline_quote(prefix, url, br) do - "#{String.duplicate("
", br)}#{prefix}: #{url}
" + defp build_inline_quote(prefix, url) do + "

#{prefix}: #{url}
" end defp filter_object(%{"quoteUrl" => quote_url} = object) do @@ -18,12 +18,13 @@ defmodule Pleroma.Web.ActivityPub.MRF.InlineQuotePolicy do else prefix = Pleroma.Config.get([:mrf_inline_quote, :prefix]) - inline_quote = + content = if String.ends_with?(content, "

"), - do: build_inline_quote(prefix, quote_url, 0), - else: build_inline_quote(prefix, quote_url, 2) + do: + String.trim_trailing(content, "

") <> + build_inline_quote(prefix, quote_url) <> "

", + else: content <> build_inline_quote(prefix, quote_url) - content = content <> inline_quote Map.put(object, "content", content) end end diff --git a/test/pleroma/web/activity_pub/mrf/inline_quote_policy_test.exs b/test/pleroma/web/activity_pub/mrf/inline_quote_policy_test.exs index 8e75aaaab..2291c1dac 100644 --- a/test/pleroma/web/activity_pub/mrf/inline_quote_policy_test.exs +++ b/test/pleroma/web/activity_pub/mrf/inline_quote_policy_test.exs @@ -22,7 +22,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.InlineQuotePolicyTest do {:ok, %{"object" => %{"content" => filtered}}} = InlineQuotePolicy.filter(activity) assert filtered == - "Nice post

RT: https://gleasonator.com/objects/1234
" + "Nice post

RT: https://gleasonator.com/objects/1234
" end test "doesn't add line breaks to markdown posts" do @@ -41,7 +41,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.InlineQuotePolicyTest do {:ok, %{"object" => %{"content" => filtered}}} = InlineQuotePolicy.filter(activity) assert filtered == - "

Nice post

RT: https://gleasonator.com/objects/1234" + "

Nice post

RT: https://gleasonator.com/objects/1234

" end test "ignores Misskey quote posts" do From 79fca39faf6d084eabb6be44a2263431943b8dd4 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 28 Jan 2022 17:53:19 -0600 Subject: [PATCH 31/46] Actually, don't send _misskey_quote anymore --- lib/pleroma/web/activity_pub/transmogrifier.ex | 11 +++++------ priv/static/schemas/litepub-0.1.jsonld | 2 -- test/pleroma/web/activity_pub/transmogrifier_test.exs | 1 - 3 files changed, 5 insertions(+), 9 deletions(-) diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex index 01e135fc1..6c6cd712b 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier.ex @@ -668,13 +668,12 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do def set_reply_to_uri(obj), do: obj + @doc """ + Fedibird compatibility + https://github.com/fedibird/mastodon/commit/dbd7ae6cf58a92ec67c512296b4daaea0d01e6ac + """ def set_quote_url(%{"quoteUrl" => quote_url} = object) when is_binary(quote_url) do - Map.merge(object, %{ - # Fedibird quote - "quoteUri" => quote_url, - # Misskey quote - "_misskey_quote" => quote_url - }) + Map.put(object, "quoteUri", quote_url) end def set_quote_url(obj), do: obj diff --git a/priv/static/schemas/litepub-0.1.jsonld b/priv/static/schemas/litepub-0.1.jsonld index 3d68e0714..b499a96f5 100644 --- a/priv/static/schemas/litepub-0.1.jsonld +++ b/priv/static/schemas/litepub-0.1.jsonld @@ -17,7 +17,6 @@ "ostatus": "http://ostatus.org#", "schema": "http://schema.org#", "toot": "http://joinmastodon.org/ns#", - "misskey": "https://misskey-hub.net/ns#", "fedibird": "http://fedibird.com/ns#", "value": "schema:value", "sensitive": "as:sensitive", @@ -30,7 +29,6 @@ }, "quoteUrl": "as:quoteUrl", "quoteUri": "fedibird:quoteUri", - "_misskey_quote": "misskey:_misskey_quote", "oauthRegistrationEndpoint": { "@id": "litepub:oauthRegistrationEndpoint", "@type": "@id" diff --git a/test/pleroma/web/activity_pub/transmogrifier_test.exs b/test/pleroma/web/activity_pub/transmogrifier_test.exs index 1838f96bb..4a192cdc0 100644 --- a/test/pleroma/web/activity_pub/transmogrifier_test.exs +++ b/test/pleroma/web/activity_pub/transmogrifier_test.exs @@ -385,7 +385,6 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do assert modified["object"]["quoteUrl"] == quote_id assert modified["object"]["quoteUri"] == quote_id - assert modified["object"]["_misskey_quote"] == quote_id end end From f9697e68c2b75a77575b9b7c89d08a5687bfd7b4 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sun, 30 Jan 2022 10:57:29 -0600 Subject: [PATCH 32/46] InlineQuotePolicy: skip objects which already have an .inline-quote span --- .../activity_pub/mrf/inline_quote_policy.ex | 13 ++++- .../quote_post/fedibird_quote_mismatched.json | 54 +++++++++++++++++++ .../mrf/inline_quote_policy_test.exs | 17 ++++++ 3 files changed, 83 insertions(+), 1 deletion(-) create mode 100644 test/fixtures/quote_post/fedibird_quote_mismatched.json diff --git a/lib/pleroma/web/activity_pub/mrf/inline_quote_policy.ex b/lib/pleroma/web/activity_pub/mrf/inline_quote_policy.ex index 7de4935f2..c78675caf 100644 --- a/lib/pleroma/web/activity_pub/mrf/inline_quote_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/inline_quote_policy.ex @@ -10,10 +10,21 @@ defmodule Pleroma.Web.ActivityPub.MRF.InlineQuotePolicy do "

#{prefix}: #{url}
" end + defp has_inline_quote?(content, quote_url) do + cond do + # Does the quote URL exist in the content? + content =~ quote_url -> true + # Does the content already have a .quote-inline span? + content =~ "" -> true + # No inline quote found + true -> false + end + end + defp filter_object(%{"quoteUrl" => quote_url} = object) do content = object["content"] || "" - if content =~ quote_url do + if has_inline_quote?(content, quote_url) do object else prefix = Pleroma.Config.get([:mrf_inline_quote, :prefix]) diff --git a/test/fixtures/quote_post/fedibird_quote_mismatched.json b/test/fixtures/quote_post/fedibird_quote_mismatched.json new file mode 100644 index 000000000..8dee5daff --- /dev/null +++ b/test/fixtures/quote_post/fedibird_quote_mismatched.json @@ -0,0 +1,54 @@ +{ + "@context": [ + "https://www.w3.org/ns/activitystreams", + { + "ostatus": "http://ostatus.org#", + "atomUri": "ostatus:atomUri", + "inReplyToAtomUri": "ostatus:inReplyToAtomUri", + "conversation": "ostatus:conversation", + "sensitive": "as:sensitive", + "toot": "http://joinmastodon.org/ns#", + "votersCount": "toot:votersCount", + "fedibird": "http://fedibird.com/ns#", + "quoteUri": "fedibird:quoteUri", + "expiry": "fedibird:expiry" + } + ], + "id": "https://fedibird.com/users/noellabo/statuses/107712183700212249", + "type": "Note", + "summary": null, + "inReplyTo": null, + "published": "2022-01-30T15:44:50Z", + "url": "https://fedibird.com/@noellabo/107712183700212249", + "attributedTo": "https://fedibird.com/users/noellabo", + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "cc": [ + "https://fedibird.com/users/noellabo/followers" + ], + "sensitive": false, + "atomUri": "https://fedibird.com/users/noellabo/statuses/107712183700212249", + "inReplyToAtomUri": null, + "conversation": "tag:fedibird.com,2022-01-30:objectId=107712183700170473:objectType=Conversation", + "context": "https://fedibird.com/contexts/107712183700170473", + "quoteUri": "https://unnerv.jp/users/UN_NERV/statuses/107712176849067434", + "_misskey_quote": "https://unnerv.jp/users/UN_NERV/statuses/107712176849067434", + "_misskey_content": "揺れていたようだ", + "content": "

揺れていたようだ
QT: https://unnerv.jp/@UN_NERV/107712176849067434

", + "contentMap": { + "ja": "

揺れていたようだ
QT: https://unnerv.jp/@UN_NERV/107712176849067434

" + }, + "attachment": [], + "tag": [], + "replies": { + "id": "https://fedibird.com/users/noellabo/statuses/107712183700212249/replies", + "type": "Collection", + "first": { + "type": "CollectionPage", + "next": "https://fedibird.com/users/noellabo/statuses/107712183700212249/replies?only_other_accounts=true&page=true", + "partOf": "https://fedibird.com/users/noellabo/statuses/107712183700212249/replies", + "items": [] + } + } +} diff --git a/test/pleroma/web/activity_pub/mrf/inline_quote_policy_test.exs b/test/pleroma/web/activity_pub/mrf/inline_quote_policy_test.exs index 2291c1dac..44ee91d4b 100644 --- a/test/pleroma/web/activity_pub/mrf/inline_quote_policy_test.exs +++ b/test/pleroma/web/activity_pub/mrf/inline_quote_policy_test.exs @@ -72,4 +72,21 @@ defmodule Pleroma.Web.ActivityPub.MRF.InlineQuotePolicyTest do {:ok, filtered} = InlineQuotePolicy.filter(activity) assert filtered == activity end + + test "skips objects which already have an .inline-quote span" do + object = + File.read!("test/fixtures/quote_post/fedibird_quote_mismatched.json") |> Jason.decode!() + + # Normally the ObjectValidator will fix this before it reaches MRF + object = Map.put(object, "quoteUrl", object["quoteUri"]) + + activity = %{ + "type" => "Create", + "actor" => "https://fedibird.com/users/noellabo", + "object" => object + } + + {:ok, filtered} = InlineQuotePolicy.filter(activity) + assert filtered == activity + end end From b0a7e795e799d3c8d750ab909657ec7b3d0bfd58 Mon Sep 17 00:00:00 2001 From: tusooa Date: Mon, 10 Jul 2023 17:57:09 -0400 Subject: [PATCH 33/46] Unify logic for normalizing quoteUri --- .../article_note_page_validator.ex | 23 +--------- .../object_validators/common_fixes.ex | 21 ++++++++++ .../web/activity_pub/transmogrifier.ex | 42 ++++++------------- priv/scrubbers/default.ex | 7 +++- 4 files changed, 40 insertions(+), 53 deletions(-) diff --git a/lib/pleroma/web/activity_pub/object_validators/article_note_page_validator.ex b/lib/pleroma/web/activity_pub/object_validators/article_note_page_validator.ex index 0b435b251..1b5b2e8fb 100644 --- a/lib/pleroma/web/activity_pub/object_validators/article_note_page_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/article_note_page_validator.ex @@ -76,27 +76,6 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidator do def fix_attachments(data), do: data - defp fix_quote_url(%{"quoteUrl" => _quote_url} = data), do: data - - # Fedibird - # https://github.com/fedibird/mastodon/commit/dbd7ae6cf58a92ec67c512296b4daaea0d01e6ac - defp fix_quote_url(%{"quoteUri" => quote_url} = data) do - Map.put(data, "quoteUrl", quote_url) - end - - # Old Fedibird (bug) - # https://github.com/fedibird/mastodon/issues/9 - defp fix_quote_url(%{"quoteURL" => quote_url} = data) do - Map.put(data, "quoteUrl", quote_url) - end - - # Misskey fallback - defp fix_quote_url(%{"_misskey_quote" => quote_url} = data) do - Map.put(data, "quoteUrl", quote_url) - end - - defp fix_quote_url(data), do: data - defp fix(data) do data |> CommonFixes.fix_actor() @@ -105,7 +84,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidator do |> fix_tag() |> fix_replies() |> fix_attachments() - |> fix_quote_url() + |> CommonFixes.fix_quote_url() |> Transmogrifier.fix_emoji() |> Transmogrifier.fix_content_map() end 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 add46d561..cc2ad9116 100644 --- a/lib/pleroma/web/activity_pub/object_validators/common_fixes.ex +++ b/lib/pleroma/web/activity_pub/object_validators/common_fixes.ex @@ -76,4 +76,25 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes do Map.put(data, "to", to) end + + def fix_quote_url(%{"quoteUrl" => _quote_url} = data), do: data + + # Fedibird + # https://github.com/fedibird/mastodon/commit/dbd7ae6cf58a92ec67c512296b4daaea0d01e6ac + def fix_quote_url(%{"quoteUri" => quote_url} = data) do + Map.put(data, "quoteUrl", quote_url) + end + + # Old Fedibird (bug) + # https://github.com/fedibird/mastodon/issues/9 + def fix_quote_url(%{"quoteURL" => quote_url} = data) do + Map.put(data, "quoteUrl", quote_url) + end + + # Misskey fallback + def fix_quote_url(%{"_misskey_quote" => quote_url} = data) do + Map.put(data, "quoteUrl", quote_url) + end + + def fix_quote_url(data), do: data end diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex index 6c6cd712b..86d3ac60f 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier.ex @@ -166,45 +166,27 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do def fix_in_reply_to(object, _options), do: object - def fix_quote_url(object, options \\ []) + def fix_quote_url_and_maybe_fetch(object, options \\ []) do + quote_url = + case Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes.fix_quote_url(object) do + %{"quoteUrl" => quote_url} -> quote_url + _ -> nil + end - def fix_quote_url(%{"quoteUrl" => quote_url} = object, options) - when not is_nil(quote_url) do - with {:ok, quoted_object} <- get_obj_helper(quote_url, options), + with {:quoting?, true} <- {:quoting?, not is_nil(quote_url)}, + {:ok, quoted_object} <- get_obj_helper(quote_url, options), %Activity{} <- Activity.get_create_by_object_ap_id(quoted_object.data["id"]) do Map.put(object, "quoteUrl", quoted_object.data["id"]) else + {:quoting?, _} -> + object + e -> Logger.warn("Couldn't fetch #{inspect(quote_url)}, error: #{inspect(e)}") object end end - # Fedibird - # https://github.com/fedibird/mastodon/commit/dbd7ae6cf58a92ec67c512296b4daaea0d01e6ac - def fix_quote_url(%{"quoteUri" => quote_url} = object, options) do - object - |> Map.put("quoteUrl", quote_url) - |> fix_quote_url(options) - end - - # Old Fedibird (bug) - # https://github.com/fedibird/mastodon/issues/9 - def fix_quote_url(%{"quoteURL" => quote_url} = object, options) do - object - |> Map.put("quoteUrl", quote_url) - |> fix_quote_url(options) - end - - # Misskey fallback - def fix_quote_url(%{"_misskey_quote" => quote_url} = object, options) do - object - |> Map.put("quoteUrl", quote_url) - |> fix_quote_url(options) - end - - def fix_quote_url(object, _options), do: object - defp prepare_in_reply_to(in_reply_to) do cond do is_bitstring(in_reply_to) -> @@ -493,7 +475,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do |> strip_internal_fields() |> fix_type(fetch_options) |> fix_in_reply_to(fetch_options) - |> fix_quote_url(fetch_options) + |> fix_quote_url_and_maybe_fetch(fetch_options) data = Map.put(data, "object", object) options = Keyword.put(options, :local, false) diff --git a/priv/scrubbers/default.ex b/priv/scrubbers/default.ex index 56324a9fa..4e7950547 100644 --- a/priv/scrubbers/default.ex +++ b/priv/scrubbers/default.ex @@ -60,7 +60,12 @@ defmodule Pleroma.HTML.Scrubber.Default do Meta.allow_tag_with_these_attributes(:u, ["lang"]) Meta.allow_tag_with_these_attributes(:ul, ["lang"]) - Meta.allow_tag_with_this_attribute_values(:span, "class", ["h-card", "recipients-inline", "quote-inline"]) + Meta.allow_tag_with_this_attribute_values(:span, "class", [ + "h-card", + "recipients-inline", + "quote-inline" + ]) + Meta.allow_tag_with_these_attributes(:span, ["lang"]) Meta.allow_tag_with_this_attribute_values(:code, "class", ["inline"]) From 9bcec87aba5ce4de6b61b5a95d6832da9dfa0fd8 Mon Sep 17 00:00:00 2001 From: tusooa Date: Mon, 10 Jul 2023 18:27:23 -0400 Subject: [PATCH 34/46] Allow local quote and private self-quote --- lib/pleroma/web/common_api/activity_draft.ex | 14 +++++++++++++- .../pleroma/web/common_api/activity_draft_test.exs | 9 ++++++++- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/lib/pleroma/web/common_api/activity_draft.ex b/lib/pleroma/web/common_api/activity_draft.ex index d4875765c..32921aa5a 100644 --- a/lib/pleroma/web/common_api/activity_draft.ex +++ b/lib/pleroma/web/common_api/activity_draft.ex @@ -167,9 +167,21 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do end end + defp can_quote?(_draft, _object, visibility) when visibility in ~w(public unlisted local) do + true + end + + defp can_quote?(draft, object, "private") do + draft.user.ap_id == object.data["actor"] + end + + defp can_quote?(_, _, _) do + false + end + defp quoting_visibility(%{quote_post: %Activity{}} = draft) do with %Object{} = object <- Object.normalize(draft.quote_post, fetch: false), - visibility when visibility in ~w(public unlisted) <- Visibility.get_visibility(object) do + true <- can_quote?(draft, object, Visibility.get_visibility(object)) do draft else _ -> add_error(draft, dgettext("errors", "Cannot quote private message")) diff --git a/test/pleroma/web/common_api/activity_draft_test.exs b/test/pleroma/web/common_api/activity_draft_test.exs index 8a09fc710..02bc6cf3b 100644 --- a/test/pleroma/web/common_api/activity_draft_test.exs +++ b/test/pleroma/web/common_api/activity_draft_test.exs @@ -12,15 +12,22 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraftTest do test "create/2 with a quote post" do user = insert(:user) + another_user = insert(:user) {:ok, direct} = CommonAPI.post(user, %{status: ".", visibility: "direct"}) {:ok, private} = CommonAPI.post(user, %{status: ".", visibility: "private"}) {:ok, unlisted} = CommonAPI.post(user, %{status: ".", visibility: "unlisted"}) + {:ok, local} = CommonAPI.post(user, %{status: ".", visibility: "local"}) {:ok, public} = CommonAPI.post(user, %{status: ".", visibility: "public"}) {:error, _} = ActivityDraft.create(user, %{status: "nice", quote_id: direct.id}) - {:error, _} = ActivityDraft.create(user, %{status: "nice", quote_id: private.id}) + {:ok, _} = ActivityDraft.create(user, %{status: "nice", quote_id: private.id}) + {:error, _} = ActivityDraft.create(another_user, %{status: "nice", quote_id: private.id}) {:ok, _} = ActivityDraft.create(user, %{status: "nice", quote_id: unlisted.id}) + {:ok, _} = ActivityDraft.create(another_user, %{status: "nice", quote_id: unlisted.id}) + {:ok, _} = ActivityDraft.create(user, %{status: "nice", quote_id: local.id}) + {:ok, _} = ActivityDraft.create(another_user, %{status: "nice", quote_id: local.id}) {:ok, _} = ActivityDraft.create(user, %{status: "nice", quote_id: public.id}) + {:ok, _} = ActivityDraft.create(another_user, %{status: "nice", quote_id: public.id}) end end From d244c9d2984d21887f50737597fc03d2d0dd1601 Mon Sep 17 00:00:00 2001 From: tusooa Date: Mon, 10 Jul 2023 18:28:13 -0400 Subject: [PATCH 35/46] Add changelog --- changelog.d/quote.add | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/quote.add diff --git a/changelog.d/quote.add b/changelog.d/quote.add new file mode 100644 index 000000000..1c368ae75 --- /dev/null +++ b/changelog.d/quote.add @@ -0,0 +1 @@ +Implement quotes From 762794eed9e6fc8a03d1416e2a6e080b00df9e10 Mon Sep 17 00:00:00 2001 From: tusooa Date: Mon, 10 Jul 2023 19:43:18 -0400 Subject: [PATCH 36/46] Fix CommonAPITest --- test/pleroma/web/common_api_test.exs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/test/pleroma/web/common_api_test.exs b/test/pleroma/web/common_api_test.exs index c4eba8b9c..70a5b6fed 100644 --- a/test/pleroma/web/common_api_test.exs +++ b/test/pleroma/web/common_api_test.exs @@ -825,16 +825,23 @@ defmodule Pleroma.Web.CommonAPITest do test "quote posting visibility" do user = insert(:user) + another_user = insert(:user) {:ok, direct} = CommonAPI.post(user, %{status: ".", visibility: "direct"}) {:ok, private} = CommonAPI.post(user, %{status: ".", visibility: "private"}) {:ok, unlisted} = CommonAPI.post(user, %{status: ".", visibility: "unlisted"}) + {:ok, local} = CommonAPI.post(user, %{status: ".", visibility: "local"}) {:ok, public} = CommonAPI.post(user, %{status: ".", visibility: "public"}) {:error, _} = CommonAPI.post(user, %{status: "nice", quote_id: direct.id}) - {:error, _} = CommonAPI.post(user, %{status: "nice", quote_id: private.id}) + {:ok, _} = CommonAPI.post(user, %{status: "nice", quote_id: private.id}) + {:error, _} = CommonAPI.post(another_user, %{status: "nice", quote_id: private.id}) {:ok, _} = CommonAPI.post(user, %{status: "nice", quote_id: unlisted.id}) + {:ok, _} = CommonAPI.post(another_user, %{status: "nice", quote_id: unlisted.id}) + {:ok, _} = CommonAPI.post(user, %{status: "nice", quote_id: local.id}) + {:ok, _} = CommonAPI.post(another_user, %{status: "nice", quote_id: local.id}) {:ok, _} = CommonAPI.post(user, %{status: "nice", quote_id: public.id}) + {:ok, _} = CommonAPI.post(another_user, %{status: "nice", quote_id: public.id}) end end From 163e5637335f9454688d3cc83530f82fc640a5b9 Mon Sep 17 00:00:00 2001 From: tusooa Date: Wed, 12 Jul 2023 09:30:43 -0400 Subject: [PATCH 37/46] Allow more flexibility in InlineQuotePolicy --- config/config.exs | 2 +- config/description.exs | 18 ++++++++++++++ docs/configuration/cheatsheet.md | 2 +- .../activity_pub/mrf/inline_quote_policy.ex | 12 ++++++---- priv/scrubbers/default.ex | 1 + .../mrf/inline_quote_policy_test.exs | 24 +++++++++++++++++-- 6 files changed, 50 insertions(+), 9 deletions(-) diff --git a/config/config.exs b/config/config.exs index 9149e925a..e8ae31542 100644 --- a/config/config.exs +++ b/config/config.exs @@ -434,7 +434,7 @@ config :pleroma, :mrf_object_age, config :pleroma, :mrf_follow_bot, follower_nickname: nil -config :pleroma, :mrf_inline_quote, prefix: "RT" +config :pleroma, :mrf_inline_quote, template: "RT: {url}" config :pleroma, :rich_media, enabled: true, diff --git a/config/description.exs b/config/description.exs index d18649ae8..079d187d5 100644 --- a/config/description.exs +++ b/config/description.exs @@ -2994,6 +2994,24 @@ config :pleroma, :config_description, [ } ] }, + %{ + group: :pleroma, + key: :mrf_inline_quote, + tab: :mrf, + related_policy: "Pleroma.Web.ActivityPub.MRF.InlineQuotePolicy", + label: "MRF Inline Quote Policy", + type: :group, + description: "Force quote url to appear in post content.", + children: [ + %{ + key: :template, + type: :string, + description: + "The template to append to the post. `{url}` will be replaced with the actual link to the quoted post.", + suggestions: ["RT: {url}"] + } + ] + }, %{ group: :pleroma, key: :modules, diff --git a/docs/configuration/cheatsheet.md b/docs/configuration/cheatsheet.md index 32cc5811a..a17f8735a 100644 --- a/docs/configuration/cheatsheet.md +++ b/docs/configuration/cheatsheet.md @@ -269,7 +269,7 @@ Notes: * `federated_timeline_removal_shortcode`: A list of patterns which result in message with emojis whose shortcodes match being removed from federated timelines (a.k.a unlisted). This will apply only to statuses. Each pattern can be a string or a [regular expression](https://hexdocs.pm/elixir/Regex.html). #### :mrf_inline_quote -* `prefix`: Prefix before the link (default: `RT`) +* `template`: The template to append to the post. `{url}` will be replaced with the actual link to the quoted post. Default: `RT: {url}` ### :activitypub * `unfollow_blocked`: Whether blocks result in people getting unfollowed diff --git a/lib/pleroma/web/activity_pub/mrf/inline_quote_policy.ex b/lib/pleroma/web/activity_pub/mrf/inline_quote_policy.ex index c78675caf..a0eefefc0 100644 --- a/lib/pleroma/web/activity_pub/mrf/inline_quote_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/inline_quote_policy.ex @@ -6,8 +6,10 @@ defmodule Pleroma.Web.ActivityPub.MRF.InlineQuotePolicy do @moduledoc "Force a quote line into the message content." @behaviour Pleroma.Web.ActivityPub.MRF.Policy - defp build_inline_quote(prefix, url) do - "

#{prefix}: #{url}
" + defp build_inline_quote(template, url) do + quote_line = String.replace(template, "{url}", "#{url}") + + "

#{quote_line}
" end defp has_inline_quote?(content, quote_url) do @@ -27,14 +29,14 @@ defmodule Pleroma.Web.ActivityPub.MRF.InlineQuotePolicy do if has_inline_quote?(content, quote_url) do object else - prefix = Pleroma.Config.get([:mrf_inline_quote, :prefix]) + template = Pleroma.Config.get([:mrf_inline_quote, :template]) content = if String.ends_with?(content, "

"), do: String.trim_trailing(content, "

") <> - build_inline_quote(prefix, quote_url) <> "

", - else: content <> build_inline_quote(prefix, quote_url) + build_inline_quote(template, quote_url) <> "

", + else: content <> build_inline_quote(template, quote_url) Map.put(object, "content", content) end diff --git a/priv/scrubbers/default.ex b/priv/scrubbers/default.ex index 4e7950547..24a76263b 100644 --- a/priv/scrubbers/default.ex +++ b/priv/scrubbers/default.ex @@ -38,6 +38,7 @@ defmodule Pleroma.HTML.Scrubber.Default do Meta.allow_tag_with_these_attributes(:abbr, ["title", "lang"]) Meta.allow_tag_with_these_attributes(:b, ["lang"]) + Meta.allow_tag_with_these_attributes(:bdi, []) Meta.allow_tag_with_these_attributes(:blockquote, ["lang"]) Meta.allow_tag_with_these_attributes(:br, ["lang"]) Meta.allow_tag_with_these_attributes(:code, ["lang"]) diff --git a/test/pleroma/web/activity_pub/mrf/inline_quote_policy_test.exs b/test/pleroma/web/activity_pub/mrf/inline_quote_policy_test.exs index 44ee91d4b..d5762766f 100644 --- a/test/pleroma/web/activity_pub/mrf/inline_quote_policy_test.exs +++ b/test/pleroma/web/activity_pub/mrf/inline_quote_policy_test.exs @@ -22,7 +22,27 @@ defmodule Pleroma.Web.ActivityPub.MRF.InlineQuotePolicyTest do {:ok, %{"object" => %{"content" => filtered}}} = InlineQuotePolicy.filter(activity) assert filtered == - "Nice post

RT: https://gleasonator.com/objects/1234
" + "Nice post

RT: https://gleasonator.com/objects/1234
" + end + + test "adds quote URL to post content, custom template" do + clear_config([:mrf_inline_quote, :template], "{url}'s quoting") + quote_url = "https://gleasonator.com/objects/1234" + + activity = %{ + "type" => "Create", + "actor" => "https://gleasonator.com/users/alex", + "object" => %{ + "type" => "Note", + "content" => "Nice post", + "quoteUrl" => quote_url + } + } + + {:ok, %{"object" => %{"content" => filtered}}} = InlineQuotePolicy.filter(activity) + + assert filtered == + "Nice post

https://gleasonator.com/objects/1234's quoting
" end test "doesn't add line breaks to markdown posts" do @@ -41,7 +61,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.InlineQuotePolicyTest do {:ok, %{"object" => %{"content" => filtered}}} = InlineQuotePolicy.filter(activity) assert filtered == - "

Nice post

RT: https://gleasonator.com/objects/1234

" + "

Nice post

RT: https://gleasonator.com/objects/1234

" end test "ignores Misskey quote posts" do From e9cd004ba1b904d92b1c07446bbf03dc070cce6a Mon Sep 17 00:00:00 2001 From: tusooa Date: Wed, 12 Jul 2023 11:09:10 -0400 Subject: [PATCH 38/46] Parse object link as quoteUrl --- lib/pleroma/constants.ex | 7 +++++ .../audio_image_video_validator.ex | 1 + .../object_validators/common_fixes.ex | 27 +++++++++++++++++++ .../object_validators/question_validator.ex | 1 + .../quote_post/fep-e232-tag-example.json | 17 ++++++++++++ .../article_note_page_validator_test.exs | 12 +++++++++ 6 files changed, 65 insertions(+) create mode 100644 test/fixtures/quote_post/fep-e232-tag-example.json diff --git a/lib/pleroma/constants.ex b/lib/pleroma/constants.ex index 6befc6897..9d764ec25 100644 --- a/lib/pleroma/constants.ex +++ b/lib/pleroma/constants.ex @@ -83,4 +83,11 @@ defmodule Pleroma.Constants do ) const(upload_object_types, do: ["Document", "Image"]) + + const(activity_json_mime_types, + do: [ + "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"", + "application/activity+json" + ] + ) end diff --git a/lib/pleroma/web/activity_pub/object_validators/audio_image_video_validator.ex b/lib/pleroma/web/activity_pub/object_validators/audio_image_video_validator.ex index 79ff76104..65ac6bb93 100644 --- a/lib/pleroma/web/activity_pub/object_validators/audio_image_video_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/audio_image_video_validator.ex @@ -99,6 +99,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AudioImageVideoValidator do data |> CommonFixes.fix_actor() |> CommonFixes.fix_object_defaults() + |> CommonFixes.fix_quote_url() |> Transmogrifier.fix_emoji() |> fix_url() |> fix_content() 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 cc2ad9116..65b8d9a2c 100644 --- a/lib/pleroma/web/activity_pub/object_validators/common_fixes.ex +++ b/lib/pleroma/web/activity_pub/object_validators/common_fixes.ex @@ -10,6 +10,8 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes do alias Pleroma.Web.ActivityPub.Transmogrifier alias Pleroma.Web.ActivityPub.Utils + require Pleroma.Constants + def cast_and_filter_recipients(message, field, follower_collection, field_fallback \\ []) do {:ok, data} = ObjectValidators.Recipients.cast(message[field] || field_fallback) @@ -96,5 +98,30 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes do Map.put(data, "quoteUrl", quote_url) end + def fix_quote_url(%{"tag" => [_ | _] = tags} = data) do + tag = Enum.find(tags, &is_object_link_tag/1) + + if not is_nil(tag) do + data + |> Map.put("quoteUrl", tag["href"]) + else + data + end + end + def fix_quote_url(data), do: data + + # https://codeberg.org/fediverse/fep/src/branch/main/fep/e232/fep-e232.md + defp is_object_link_tag( + %{ + "type" => "Link", + "mediaType" => media_type, + "href" => href + } = tag + ) + when media_type in Pleroma.Constants.activity_json_mime_types() and is_binary(href) do + true + end + + defp is_object_link_tag(_), do: false end diff --git a/lib/pleroma/web/activity_pub/object_validators/question_validator.ex b/lib/pleroma/web/activity_pub/object_validators/question_validator.ex index ce3305142..621085e6c 100644 --- a/lib/pleroma/web/activity_pub/object_validators/question_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/question_validator.ex @@ -62,6 +62,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.QuestionValidator do data |> CommonFixes.fix_actor() |> CommonFixes.fix_object_defaults() + |> CommonFixes.fix_quote_url() |> Transmogrifier.fix_emoji() |> fix_closed() end diff --git a/test/fixtures/quote_post/fep-e232-tag-example.json b/test/fixtures/quote_post/fep-e232-tag-example.json new file mode 100644 index 000000000..23c7fb5ac --- /dev/null +++ b/test/fixtures/quote_post/fep-e232-tag-example.json @@ -0,0 +1,17 @@ +{ + "@context": "https://www.w3.org/ns/activitystreams", + "type": "Note", + "content": "This is a quote:
RE: https://server.example/objects/123", + "tag": [ + { + "type": "Link", + "mediaType": "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"", + "href": "https://server.example/objects/123", + "name": "RE: https://server.example/objects/123" + } + ], + "id": "https://server.example/objects/1", + "to": "https://server.example/users/1", + "attributedTo": "https://server.example/users/1", + "actor": "https://server.example/users/1" +} diff --git a/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs b/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs index a4ba38e6a..73141cac1 100644 --- a/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs +++ b/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs @@ -146,4 +146,16 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidatorTest assert cng.valid? assert cng.changes.quoteUrl == "https://misskey.io/notes/8vs6wxufd0" end + + test "Parse tag as quote" do + # https://codeberg.org/fediverse/fep/src/branch/main/fep/e232/fep-e232.md + + insert(:user, ap_id: "https://server.example/users/1") + + data = File.read!("test/fixtures/quote_post/fep-e232-tag-example.json") |> Jason.decode!() + cng = ArticleNotePageValidator.cast_and_validate(data) + + assert cng.valid? + assert cng.changes.quoteUrl == "https://server.example/objects/123" + end end From 479a6f11dbbba0c945c08883956ffab198f91688 Mon Sep 17 00:00:00 2001 From: tusooa Date: Wed, 12 Jul 2023 14:08:24 -0400 Subject: [PATCH 39/46] Keep incoming Link tag --- .../object_validators/tag_validator.ex | 14 +++++++++++++- .../article_note_page_validator_test.exs | 7 +++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/lib/pleroma/web/activity_pub/object_validators/tag_validator.ex b/lib/pleroma/web/activity_pub/object_validators/tag_validator.ex index cfd510c19..47cf7b415 100644 --- a/lib/pleroma/web/activity_pub/object_validators/tag_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/tag_validator.ex @@ -9,15 +9,20 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.TagValidator do import Ecto.Changeset + require Pleroma.Constants + @primary_key false embedded_schema do # Common field(:type, :string) field(:name, :string) - # Mention, Hashtag + # Mention, Hashtag, Link field(:href, ObjectValidators.Uri) + # Link + field(:mediaType, :string) + # Emoji embeds_one :icon, IconObjectValidator, primary_key: false do field(:type, :string) @@ -68,6 +73,13 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.TagValidator do |> validate_required([:type, :name, :icon]) end + def changeset(struct, %{"type" => "Link"} = data) do + struct + |> cast(data, [:type, :name, :mediaType, :href]) + |> validate_inclusion(:mediaType, Pleroma.Constants.activity_json_mime_types()) + |> validate_required([:type, :href, :mediaType]) + end + def changeset(struct, %{"type" => _} = data) do struct |> cast(data, []) diff --git a/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs b/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs index 73141cac1..4703c3801 100644 --- a/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs +++ b/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs @@ -157,5 +157,12 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidatorTest assert cng.valid? assert cng.changes.quoteUrl == "https://server.example/objects/123" + + assert Enum.at(cng.changes.tag, 0).changes == %{ + type: "Link", + mediaType: "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"", + href: "https://server.example/objects/123", + name: "RE: https://server.example/objects/123" + } end end From e349e92a441840bbbdbf13cacd307e65f85a38ff Mon Sep 17 00:00:00 2001 From: tusooa Date: Wed, 12 Jul 2023 14:27:29 -0400 Subject: [PATCH 40/46] Add mrf to force link tag of quoting posts --- docs/configuration/cheatsheet.md | 1 + lib/pleroma/constants.ex | 4 + .../mrf/quote_to_link_tag_policy.ex | 49 +++++++++++++ .../object_validators/common_fixes.ex | 16 ++-- .../mrf/quote_to_link_tag_policy_test.exs | 73 +++++++++++++++++++ 5 files changed, 134 insertions(+), 9 deletions(-) create mode 100644 lib/pleroma/web/activity_pub/mrf/quote_to_link_tag_policy.ex create mode 100644 test/pleroma/web/activity_pub/mrf/quote_to_link_tag_policy_test.exs diff --git a/docs/configuration/cheatsheet.md b/docs/configuration/cheatsheet.md index a17f8735a..a4cae4dbb 100644 --- a/docs/configuration/cheatsheet.md +++ b/docs/configuration/cheatsheet.md @@ -161,6 +161,7 @@ To add configuration to your config file, you can copy it from the base config. * `Pleroma.Web.ActivityPub.MRF.KeywordPolicy`: Rejects or removes from the federated timeline or replaces keywords. (See [`:mrf_keyword`](#mrf_keyword)). * `Pleroma.Web.ActivityPub.MRF.ForceMentionsInContent`: Forces every mentioned user to be reflected in the post content. * `Pleroma.Web.ActivityPub.MRF.InlineQuotePolicy`: Forces quote post URLs to be reflected in the message content inline. + * `Pleroma.Web.ActivityPub.MRF.QuoteToLinkTagPolicy`: Force a Link tag for posts quoting another post. (may break outgoing federation of quote posts with older Pleroma versions) * `transparency`: Make the content of your Message Rewrite Facility settings public (via nodeinfo). * `transparency_exclusions`: Exclude specific instance names from MRF transparency. The use of the exclusions feature will be disclosed in nodeinfo as a boolean value. diff --git a/lib/pleroma/constants.ex b/lib/pleroma/constants.ex index 9d764ec25..ed60bcc37 100644 --- a/lib/pleroma/constants.ex +++ b/lib/pleroma/constants.ex @@ -84,6 +84,10 @@ defmodule Pleroma.Constants do const(upload_object_types, do: ["Document", "Image"]) + const(activity_json_canonical_mime_type, + do: "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"" + ) + const(activity_json_mime_types, do: [ "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"", diff --git a/lib/pleroma/web/activity_pub/mrf/quote_to_link_tag_policy.ex b/lib/pleroma/web/activity_pub/mrf/quote_to_link_tag_policy.ex new file mode 100644 index 000000000..f1c573d1b --- /dev/null +++ b/lib/pleroma/web/activity_pub/mrf/quote_to_link_tag_policy.ex @@ -0,0 +1,49 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2023 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.MRF.QuoteToLinkTagPolicy do + @moduledoc "Force a Link tag for posts quoting another post. (may break outgoing federation of quote posts with older Pleroma versions)" + @behaviour Pleroma.Web.ActivityPub.MRF.Policy + + alias Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes + + require Pleroma.Constants + + @impl Pleroma.Web.ActivityPub.MRF.Policy + def filter(%{"object" => %{"quoteUrl" => _} = object} = activity) do + {:ok, Map.put(activity, "object", filter_object(object))} + end + + @impl Pleroma.Web.ActivityPub.MRF.Policy + def filter(object), do: {:ok, object} + + @impl Pleroma.Web.ActivityPub.MRF.Policy + def describe, do: {:ok, %{}} + + @impl Pleroma.Web.ActivityPub.MRF.Policy + def history_awareness, do: :auto + + defp filter_object(%{"quoteUrl" => quote_url} = object) do + tags = object["tag"] || [] + + if Enum.any?(tags, fn tag -> + CommonFixes.is_object_link_tag(tag) and tag["href"] == quote_url + end) do + object + else + object + |> Map.put( + "tag", + tags ++ + [ + %{ + "type" => "Link", + "mediaType" => Pleroma.Constants.activity_json_canonical_mime_type(), + "href" => quote_url + } + ] + ) + end + end +end 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 65b8d9a2c..4d9be0bdd 100644 --- a/lib/pleroma/web/activity_pub/object_validators/common_fixes.ex +++ b/lib/pleroma/web/activity_pub/object_validators/common_fixes.ex @@ -112,16 +112,14 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes do def fix_quote_url(data), do: data # https://codeberg.org/fediverse/fep/src/branch/main/fep/e232/fep-e232.md - defp is_object_link_tag( - %{ - "type" => "Link", - "mediaType" => media_type, - "href" => href - } = tag - ) - when media_type in Pleroma.Constants.activity_json_mime_types() and is_binary(href) do + def is_object_link_tag(%{ + "type" => "Link", + "mediaType" => media_type, + "href" => href + }) + when media_type in Pleroma.Constants.activity_json_mime_types() and is_binary(href) do true end - defp is_object_link_tag(_), do: false + def is_object_link_tag(_), do: false end diff --git a/test/pleroma/web/activity_pub/mrf/quote_to_link_tag_policy_test.exs b/test/pleroma/web/activity_pub/mrf/quote_to_link_tag_policy_test.exs new file mode 100644 index 000000000..96b49b6a0 --- /dev/null +++ b/test/pleroma/web/activity_pub/mrf/quote_to_link_tag_policy_test.exs @@ -0,0 +1,73 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2023 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.MRF.QuoteToLinkTagPolicyTest do + alias Pleroma.Web.ActivityPub.MRF.QuoteToLinkTagPolicy + + use Pleroma.DataCase + + require Pleroma.Constants + + test "Add quote url to Link tag" do + quote_url = "https://gleasonator.com/objects/1234" + + activity = %{ + "type" => "Create", + "actor" => "https://gleasonator.com/users/alex", + "object" => %{ + "type" => "Note", + "content" => "Nice post", + "quoteUrl" => quote_url + } + } + + {:ok, %{"object" => object}} = QuoteToLinkTagPolicy.filter(activity) + + assert object["tag"] == [ + %{ + "type" => "Link", + "href" => quote_url, + "mediaType" => Pleroma.Constants.activity_json_canonical_mime_type() + } + ] + end + + test "Add quote url to Link tag, append to the end" do + quote_url = "https://gleasonator.com/objects/1234" + + activity = %{ + "type" => "Create", + "actor" => "https://gleasonator.com/users/alex", + "object" => %{ + "type" => "Note", + "content" => "Nice post", + "quoteUrl" => quote_url, + "tag" => [%{"type" => "Hashtag", "name" => "#foo"}] + } + } + + {:ok, %{"object" => object}} = QuoteToLinkTagPolicy.filter(activity) + + assert [_, tag] = object["tag"] + + assert tag == %{ + "type" => "Link", + "href" => quote_url, + "mediaType" => Pleroma.Constants.activity_json_canonical_mime_type() + } + end + + test "Bypass posts without quoteUrl" do + activity = %{ + "type" => "Create", + "actor" => "https://gleasonator.com/users/alex", + "object" => %{ + "type" => "Note", + "content" => "Nice post" + } + } + + assert {:ok, ^activity} = QuoteToLinkTagPolicy.filter(activity) + end +end From 8b98a98dfb4ab3db9b4f2347713d86ed9c87b61b Mon Sep 17 00:00:00 2001 From: tusooa Date: Wed, 12 Jul 2023 14:37:12 -0400 Subject: [PATCH 41/46] Make InlineQuotePolicy history aware --- lib/pleroma/web/activity_pub/mrf/inline_quote_policy.ex | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/pleroma/web/activity_pub/mrf/inline_quote_policy.ex b/lib/pleroma/web/activity_pub/mrf/inline_quote_policy.ex index a0eefefc0..aaa209aa1 100644 --- a/lib/pleroma/web/activity_pub/mrf/inline_quote_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/inline_quote_policy.ex @@ -53,6 +53,9 @@ defmodule Pleroma.Web.ActivityPub.MRF.InlineQuotePolicy do @impl true def describe, do: {:ok, %{}} + @impl Pleroma.Web.ActivityPub.MRF.Policy + def history_awareness, do: :auto + @impl true def config_description do %{ From 8596f926543126efdb4b8cfe70beab6812824398 Mon Sep 17 00:00:00 2001 From: tusooa Date: Wed, 12 Jul 2023 14:58:20 -0400 Subject: [PATCH 42/46] Fix TransmogrifierTest --- test/pleroma/web/activity_pub/transmogrifier_test.exs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/test/pleroma/web/activity_pub/transmogrifier_test.exs b/test/pleroma/web/activity_pub/transmogrifier_test.exs index 4a192cdc0..5e58d75db 100644 --- a/test/pleroma/web/activity_pub/transmogrifier_test.exs +++ b/test/pleroma/web/activity_pub/transmogrifier_test.exs @@ -123,7 +123,7 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do assert activity.data["context"] == object.data["context"] end - test "it drops link tags" do + test "it keeps link tags" do insert(:user, ap_id: "https://example.org/users/alice") message = File.read!("test/fixtures/fep-e232.json") |> Jason.decode!() @@ -131,10 +131,7 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do assert {:ok, activity} = Transmogrifier.handle_incoming(message) object = Object.normalize(activity) - assert length(object.data["tag"]) == 1 - - tag = object.data["tag"] |> List.first() - assert tag["type"] == "Mention" + assert [%{"type" => "Mention"}, %{"type" => "Link"}] = object.data["tag"] end test "it accepts quote posts" do From 87353e5ad12799d12507253fe9a0363fd9f0c817 Mon Sep 17 00:00:00 2001 From: tusooa Date: Wed, 12 Jul 2023 22:07:16 -0400 Subject: [PATCH 43/46] Fix config descriptions for mrf inline quote --- config/description.exs | 18 ------------------ .../activity_pub/mrf/inline_quote_policy.ex | 12 +++++++----- 2 files changed, 7 insertions(+), 23 deletions(-) diff --git a/config/description.exs b/config/description.exs index 079d187d5..d18649ae8 100644 --- a/config/description.exs +++ b/config/description.exs @@ -2994,24 +2994,6 @@ config :pleroma, :config_description, [ } ] }, - %{ - group: :pleroma, - key: :mrf_inline_quote, - tab: :mrf, - related_policy: "Pleroma.Web.ActivityPub.MRF.InlineQuotePolicy", - label: "MRF Inline Quote Policy", - type: :group, - description: "Force quote url to appear in post content.", - children: [ - %{ - key: :template, - type: :string, - description: - "The template to append to the post. `{url}` will be replaced with the actual link to the quoted post.", - suggestions: ["RT: {url}"] - } - ] - }, %{ group: :pleroma, key: :modules, diff --git a/lib/pleroma/web/activity_pub/mrf/inline_quote_policy.ex b/lib/pleroma/web/activity_pub/mrf/inline_quote_policy.ex index aaa209aa1..171b22c5e 100644 --- a/lib/pleroma/web/activity_pub/mrf/inline_quote_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/inline_quote_policy.ex @@ -61,14 +61,16 @@ defmodule Pleroma.Web.ActivityPub.MRF.InlineQuotePolicy do %{ key: :mrf_inline_quote, related_policy: "Pleroma.Web.ActivityPub.MRF.InlineQuotePolicy", - label: "MRF Inline Quote", - description: "Force quote post URLs inline", + label: "MRF Inline Quote Policy", + type: :group, + description: "Force quote url to appear in post content.", children: [ %{ - key: :prefix, + key: :template, type: :string, - description: "Prefix before the link", - suggestions: ["RT", "QT", "RE", "RN"] + description: + "The template to append to the post. `{url}` will be replaced with the actual link to the quoted post.", + suggestions: ["RT: {url}"] } ] } From 875b46d97d910ffd2c33ac26ed8dfe38f7672f52 Mon Sep 17 00:00:00 2001 From: tusooa Date: Wed, 12 Jul 2023 23:29:23 -0400 Subject: [PATCH 44/46] Do not mention original poster when quoting --- lib/pleroma/web/common_api/activity_draft.ex | 3 --- test/pleroma/web/common_api_test.exs | 4 ++-- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/lib/pleroma/web/common_api/activity_draft.ex b/lib/pleroma/web/common_api/activity_draft.ex index 32921aa5a..ca1329284 100644 --- a/lib/pleroma/web/common_api/activity_draft.ex +++ b/lib/pleroma/web/common_api/activity_draft.ex @@ -139,9 +139,6 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do defp quote_post(%{params: %{quote_id: id}} = draft) when not_empty_string(id) do case Activity.get_by_id_with_object(id) do - %Activity{actor: actor_ap_id} = activity when not_empty_string(actor_ap_id) -> - %__MODULE__{draft | quote_post: activity, mentions: [actor_ap_id]} - %Activity{} = activity -> %__MODULE__{draft | quote_post: activity} diff --git a/test/pleroma/web/common_api_test.exs b/test/pleroma/web/common_api_test.exs index 70a5b6fed..a98b16d4b 100644 --- a/test/pleroma/web/common_api_test.exs +++ b/test/pleroma/web/common_api_test.exs @@ -808,8 +808,8 @@ defmodule Pleroma.Web.CommonAPITest do assert quote_post.data["quoteUrl"] == quoted.data["id"] - # The OP is mentioned - assert quoted.data["actor"] in quote_post.data["to"] + # The OP is not mentioned + refute quoted.data["actor"] in quote_post.data["to"] end test "quote posting with explicit addressing doesn't mention the OP" do From a8b2f9205d16465a3b11d3802c966db3da908c5d Mon Sep 17 00:00:00 2001 From: tusooa Date: Wed, 12 Jul 2023 23:47:31 -0400 Subject: [PATCH 45/46] Expose quote_id parameter on the api --- lib/pleroma/web/api_spec/schemas/status.ex | 5 +++++ lib/pleroma/web/mastodon_api/views/status_view.ex | 10 ++++++++++ .../web/mastodon_api/views/status_view_test.exs | 3 +++ 3 files changed, 18 insertions(+) diff --git a/lib/pleroma/web/api_spec/schemas/status.ex b/lib/pleroma/web/api_spec/schemas/status.ex index 5d0eedb08..07f03134a 100644 --- a/lib/pleroma/web/api_spec/schemas/status.ex +++ b/lib/pleroma/web/api_spec/schemas/status.ex @@ -198,6 +198,11 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Status do nullable: true, description: "Quoted status (if any)" }, + quote_id: %Schema{ + nullable: true, + allOf: [FlakeID], + description: "ID of the status being quoted, if any" + }, quote_url: %Schema{ type: :string, format: :uri, diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex index ba4a8f3eb..3d3039751 100644 --- a/lib/pleroma/web/mastodon_api/views/status_view.ex +++ b/lib/pleroma/web/mastodon_api/views/status_view.ex @@ -312,6 +312,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do # Here the implicit index of the current content is 0 chrono_order = history_len - 1 + quote_id = get_quote_id(activity) + quote_activity = get_quote(activity, opts) quote_post = @@ -431,6 +433,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do context: object.data["context"], in_reply_to_account_acct: reply_to_user && reply_to_user.nickname, quote: quote_post, + quote_id: quote_id, quote_url: object.data["quoteUrl"], quote_visible: visible_for_user?(quote_activity, opts[:for]), content: %{"text/plain" => content_plaintext}, @@ -689,6 +692,13 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do end end + defp get_quote_id(activity) do + case get_quote(activity, %{}) do + %Activity{id: id} -> id + _ -> nil + end + end + def render_content(%{data: %{"name" => name}} = object) when not is_nil(name) and name != "" do url = object.data["url"] || object.data["id"] diff --git a/test/pleroma/web/mastodon_api/views/status_view_test.exs b/test/pleroma/web/mastodon_api/views/status_view_test.exs index 6d3a72970..221244d4e 100644 --- a/test/pleroma/web/mastodon_api/views/status_view_test.exs +++ b/test/pleroma/web/mastodon_api/views/status_view_test.exs @@ -327,6 +327,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do context: object_data["context"], in_reply_to_account_acct: nil, quote: nil, + quote_id: nil, quote_url: nil, quote_visible: false, content: %{"text/plain" => HTML.strip_tags(object_data["content"])}, @@ -435,10 +436,12 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do status = StatusView.render("show.json", %{activity: quoted_quote_post}) assert status.pleroma.quote.id == to_string(quote_post.id) + assert status.pleroma.quote_id == to_string(quote_post.id) assert status.pleroma.quote_url == Object.normalize(quote_post).data["id"] # Quotes don't go more than one level deep refute status.pleroma.quote.pleroma.quote + assert status.pleroma.quote.pleroma.quote_id == to_string(post.id) assert status.pleroma.quote.pleroma.quote_url == Object.normalize(post).data["id"] # In an index From 08608afca5566f712acdc14b7c43976d6d071106 Mon Sep 17 00:00:00 2001 From: tusooa Date: Wed, 12 Jul 2023 23:56:54 -0400 Subject: [PATCH 46/46] Fix quote_visible attribute --- .../web/mastodon_api/views/status_view.ex | 19 +++++++------------ .../mastodon_api/views/status_view_test.exs | 2 ++ 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex index 3d3039751..d070262cc 100644 --- a/lib/pleroma/web/mastodon_api/views/status_view.ex +++ b/lib/pleroma/web/mastodon_api/views/status_view.ex @@ -312,12 +312,16 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do # Here the implicit index of the current content is 0 chrono_order = history_len - 1 - quote_id = get_quote_id(activity) - quote_activity = get_quote(activity, opts) + quote_id = + case quote_activity do + %Activity{id: id} -> id + _ -> nil + end + quote_post = - if visible_for_user?(quote_activity, opts[:for]) do + if visible_for_user?(quote_activity, opts[:for]) and opts[:show_quote] != false do quote_rendering_opts = Map.merge(opts, %{activity: quote_activity, show_quote: false}) render("show.json", quote_rendering_opts) else @@ -671,8 +675,6 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do end end - def get_quote(_activity, %{show_quote: false}), do: nil - def get_quote(activity, %{quoted_activities: quoted_activities}) do object = Object.normalize(activity, fetch: false) @@ -692,13 +694,6 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do end end - defp get_quote_id(activity) do - case get_quote(activity, %{}) do - %Activity{id: id} -> id - _ -> nil - end - end - def render_content(%{data: %{"name" => name}} = object) when not is_nil(name) and name != "" do url = object.data["url"] || object.data["id"] diff --git a/test/pleroma/web/mastodon_api/views/status_view_test.exs b/test/pleroma/web/mastodon_api/views/status_view_test.exs index 221244d4e..baa9b32f5 100644 --- a/test/pleroma/web/mastodon_api/views/status_view_test.exs +++ b/test/pleroma/web/mastodon_api/views/status_view_test.exs @@ -438,11 +438,13 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do assert status.pleroma.quote.id == to_string(quote_post.id) assert status.pleroma.quote_id == to_string(quote_post.id) assert status.pleroma.quote_url == Object.normalize(quote_post).data["id"] + assert status.pleroma.quote_visible # Quotes don't go more than one level deep refute status.pleroma.quote.pleroma.quote assert status.pleroma.quote.pleroma.quote_id == to_string(post.id) assert status.pleroma.quote.pleroma.quote_url == Object.normalize(post).data["id"] + assert status.pleroma.quote.pleroma.quote_visible # In an index [status] = StatusView.render("index.json", %{activities: [quoted_quote_post], as: :activity})