### Added
- extended runtime module support, see config cheatsheet
+- quote posting; quotes are limited to public posts
### Fixed
- Updated mastoFE path, for the newer version
accept: [],
reject: []
+config :pleroma, :mrf_inline_quote, prefix: "RE"
+
# threshold of 7 days
config :pleroma, :mrf_object_age,
threshold: 604_800,
```sh
mix pleroma.user unconfirm_all
```
+
+## Fix following state
+
+Sometimes the system can get into a situation where
+it think you're already following someone and won't send a request
+to the remote instance, or won't let you unfollow someone. This
+bug was fixed, but in case you encounter these weird states:
+
+=== "OTP"
+
+ ```sh
+ ./bin/pleroma_ctl user fix_follow_state localuser remoteuser@example.com
+ ```
+
+=== "From Source"
+
+ ```sh
+ mix pleroma.user fix_follow_state localuser remoteuser@example.com
+ ```
+
+The first argument is the local user's nickname - if you are `myuser@myinstance`, this should be `myuser`.
+
+The second is the remote user, consisting of both nickname AND domain.
+
+If you are a weird follow state situation and cannot resolve it with the above, you may need to co-operate with the remote admin to clear the state their side too - they should provide the arguments *backwards*, i.e `fix_follow_state remote local`.
get_in_reply_to_activity_from_object(Object.normalize(activity, fetch: false))
end
+ def get_quoted_activity_from_object(%Object{data: %{"quoteUri" => ap_id}}) do
+ get_create_by_object_ap_id_with_object(ap_id)
+ end
+
+ def get_quoted_activity_from_object(_), do: nil
+
def normalize(%Activity{data: %{"id" => ap_id}}), do: get_by_ap_id_with_object(ap_id)
def normalize(%{"id" => ap_id}), do: get_by_ap_id_with_object(ap_id)
def normalize(ap_id) when is_binary(ap_id), do: get_by_ap_id_with_object(ap_id)
"tag" => Keyword.values(draft.tags) |> Enum.uniq()
}
|> add_in_reply_to(draft.in_reply_to)
+ |> add_quote(draft.quote)
|> Map.merge(draft.extra)
{:ok, data, []}
end
end
+ defp add_quote(object, nil), do: object
+
+ defp add_quote(object, quote) do
+ with %Object{} = quote_object <- Object.normalize(quote, fetch: false) do
+ Map.put(object, "quoteUri", quote_object.data["id"])
+ else
+ _ -> object
+ end
+ end
+
def answer(user, object, name) do
{:ok,
%{
--- /dev/null
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
+# 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
+ "<span class=\"quote-inline\"><br/><br/>#{prefix}: <a href=\"#{url}\">#{url}</a></span>"
+ 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 =~ "<span class=\"quote-inline\">" -> true
+ # No inline quote found
+ true -> false
+ end
+ end
+
+ defp filter_object(%{"quoteUri" => quote_url} = object) do
+ content = object["content"] || ""
+
+ if has_inline_quote?(content, quote_url) do
+ object
+ else
+ prefix = Pleroma.Config.get([:mrf_inline_quote, :prefix])
+
+ content =
+ if String.ends_with?(content, "</p>") do
+ String.trim_trailing(content, "</p>") <> build_inline_quote(prefix, quote_url) <> "</p>"
+ else
+ content <> build_inline_quote(prefix, quote_url)
+ end
+
+ Map.put(object, "content", content)
+ end
+ end
+
+ @impl true
+ def filter(%{"object" => %{"quoteUri" => _} = 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: ["RE", "QT", "RT", "RN"]
+ }
+ ]
+ }
+ end
+end
|> fix_replies()
|> fix_source()
|> fix_misskey_content()
+ |> Transmogrifier.fix_quote_url()
|> Transmogrifier.fix_attachments()
|> Transmogrifier.fix_emoji()
|> Transmogrifier.fix_content_map()
field(:like_count, :integer, default: 0)
field(:announcement_count, :integer, default: 0)
field(:inReplyTo, ObjectValidators.ObjectID)
+ field(:quoteUri, ObjectValidators.ObjectID)
field(:url, ObjectValidators.Uri)
field(:likes, {:array, ObjectValidators.ObjectID}, default: [])
def set_reply_to_uri(obj), do: obj
+ def set_quote_url(%{"quoteUri" => quote} = object) when is_binary(quote) do
+ Map.put(object, "quoteUrl", quote)
+ end
+
+ def set_quote_url(obj), do: obj
+
@doc """
Serialized Mastodon-compatible `replies` collection containing _self-replies_.
Based on Mastodon's ActivityPub::NoteSerializer#replies.
|> prepare_attachments
|> set_conversation
|> set_reply_to_uri
+ |> set_quote_url()
|> set_replies
|> strip_internal_fields
|> strip_internal_tags
defp strip_internal_tags(object), do: object
+ def fix_quote_url(object, options \\ [])
+
+ def fix_quote_url(%{"quoteUri" => 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, "quoteUri", quoted_object.data["id"])
+ else
+ e ->
+ Logger.warn("Couldn't fetch #{inspect(quote_url)}, error: #{inspect(e)}")
+ object
+ end
+ end
+
+ # Soapbox
+ def fix_quote_url(%{"quoteUrl" => quote_url} = object, options) do
+ object
+ |> Map.put("quoteUri", 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("quoteUri", quote_url)
+ |> fix_quote_url(options)
+ end
+
+ def fix_quote_url(%{"_misskey_quote" => quote_url} = object, options) do
+ object
+ |> Map.put("quoteUri", quote_url)
+ |> fix_quote_url(options)
+ end
+
+ def fix_quote_url(object, _), do: object
+
def perform(:user_upgrade, user) do
# we pass a fake user so that the followers collection is stripped away
old_follower_address = User.ap_followers(%User{nickname: user.nickname})
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,
+ type: :string,
+ description: "Will quote a given status."
}
},
example: %{
type: :boolean,
description: "Have you pinned this status? Only appears if the status is pinnable."
},
+ quote_id: %Schema{
+ type: :string,
+ description: "ID of the status being quoted",
+ nullable: true
+ },
+ quote: %Schema{
+ allOf: [%OpenApiSpex.Reference{"$ref": "#/components/schemas/Status"}],
+ nullable: true,
+ description: "Quoted status (if any)"
+ },
pleroma: %Schema{
type: :object,
properties: %{
}
}
},
+ akkoma: %Schema{
+ type: :object,
+ properties: %{
+ source: %Schema{
+ nullable: true,
+ oneOf: [
+ %Schema{type: :string, example: 'plaintext content'},
+ %Schema{
+ type: :object,
+ properties: %{
+ content: %Schema{
+ type: :string,
+ description: "The source content of the status",
+ nullable: true
+ },
+ mediaType: %Schema{
+ type: :string,
+ description: "The source MIME type of the status",
+ example: "text/plain",
+ nullable: true
+ }
+ }
+ }
+ ]
+ }
+ }
+ },
poll: %Schema{allOf: [Poll], nullable: true, description: "The poll attached to the status"},
reblog: %Schema{
allOf: [%OpenApiSpex.Reference{"$ref": "#/components/schemas/Status"}],
end
end
+ def get_quoted_visibility(nil), do: nil
+
+ def get_quoted_visibility(activity), do: get_replied_to_visibility(activity)
+
def check_expiry_date({:ok, nil} = res), do: res
def check_expiry_date({:ok, in_seconds}) do
attachments: [],
in_reply_to: nil,
in_reply_to_conversation: nil,
+ quote_id: nil,
+ quote: nil,
visibility: nil,
expires_at: nil,
extra: nil,
|> with_valid(&in_reply_to/1)
|> with_valid(&in_reply_to_conversation/1)
|> with_valid(&visibility/1)
+ |> with_valid("e_id/1)
|> content()
|> with_valid(&to_and_cc/1)
|> with_valid(&context/1)
%__MODULE__{draft | in_reply_to_conversation: in_reply_to_conversation}
end
+ defp quote_id(%{params: %{quote_id: ""}} = draft), do: draft
+
+ defp quote_id(%{params: %{quote_id: id}} = draft) when is_binary(id) do
+ with {:activity, %Activity{} = quote} <- {:activity, Activity.get_by_id(id)},
+ visibility <- CommonAPI.get_quoted_visibility(quote),
+ {:visibility, true} <- {:visibility, visibility in ["public", "unlisted"]} do
+ %__MODULE__{draft | quote: Activity.get_by_id(id)}
+ else
+ {:activity, _} ->
+ add_error(draft, dgettext("errors", "You can't quote a status that doesn't exist"))
+
+ {:visibility, false} ->
+ add_error(draft, dgettext("errors", "You can only quote public or unlisted statuses"))
+ end
+ end
+
+ defp quote_id(%{params: %{quote_id: %Activity{} = quote}} = draft) do
+ %__MODULE__{draft | quote: quote}
+ end
+
+ defp quote_id(draft), do: draft
+
defp visibility(%{params: params} = draft) do
case CommonAPI.get_visibility(params, draft.in_reply_to, draft.in_reply_to_conversation) do
{visibility, "direct"} when visibility != "direct" ->
{pinned?, pinned_at} = pin_data(object, user)
+ quote = Activity.get_quoted_activity_from_object(object)
+
%{
id: to_string(activity.id),
uri: object.data["id"],
application: build_application(object.data["generator"]),
language: nil,
emojis: build_emojis(object.data["emoji"]),
+ quote_id: if(quote, do: quote.id, else: nil),
+ quote: maybe_render_quote(quote, opts),
pleroma: %{
local: activity.local,
conversation_id: get_context_id(activity),
end
defp build_image_url(_, _), do: nil
+
+ defp maybe_render_quote(nil, _), do: nil
+
+ defp maybe_render_quote(quote, opts) do
+ if opts[:do_not_recurse] || !visible_for_user?(quote, opts[:for]) do
+ nil
+ else
+ opts =
+ opts
+ |> Map.put(:activity, quote)
+ |> Map.put(:do_not_recurse, true)
+
+ render("show.json", opts)
+ end
+ end
end
"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",
"litepub": "http://litepub.social/ns#",
"@id": "litepub:listMessage",
"@type": "@id"
},
+ "quoteUrl": "as:quoteUrl",
+ "quoteUri": "fedibird:quoteUri",
"oauthRegistrationEndpoint": {
"@id": "litepub:oauthRegistrationEndpoint",
"@type": "@id"
--- /dev/null
+{
+ "@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",
+ "references": {
+ "@id": "fedibird:references",
+ "@type": "@id"
+ },
+ "emojiReactions": {
+ "@id": "fedibird:emojiReactions",
+ "@type": "@id"
+ }
+ }
+ ],
+ "id": "https://fedibird.com/users/akkoma_ap_integration_tester/statuses/108707679228362674",
+ "type": "Note",
+ "summary": null,
+ "inReplyTo": null,
+ "published": "2022-07-25T11:12:26Z",
+ "url": "https://fedibird.com/@akkoma_ap_integration_tester/108707679228362674",
+ "attributedTo": "https://fedibird.com/users/akkoma_ap_integration_tester",
+ "to": [
+ "https://fedibird.com/users/akkoma_ap_integration_tester/followers"
+ ],
+ "cc": [
+ "https://www.w3.org/ns/activitystreams#Public"
+ ],
+ "sensitive": false,
+ "atomUri": "https://fedibird.com/users/akkoma_ap_integration_tester/statuses/108707679228362674",
+ "inReplyToAtomUri": null,
+ "conversation": "tag:fedibird.com,2022-07-25:objectId=108707679228389900:objectType=Conversation",
+ "context": "https://fedibird.com/contexts/108707679228389900",
+ "quoteUri": "https://example.com/objects/43479e20-c0f8-4f49-bf7f-13fab8234924",
+ "_misskey_quote": "https://example.com/objects/43479e20-c0f8-4f49-bf7f-13fab8234924",
+ "_misskey_content": "public quote",
+ "content": "<p>public quote<span class=\"quote-inline\"><br/>QT: <a class=\"status-url-link\" data-status-account-acct=\"ArtMirror@example.com\" data-status-id=\"108703793483919195\" href=\"https://example.com/objects/24d9f2e1-32d2-4bd5-bdf2-8ea61d3fb5e8\" rel=\"nofollow noopener noreferrer\" target=\"_blank\"><span class=\"invisible\">https://</span><span class=\"ellipsis\">example.com/objects/24d9f</span><span class=\"invisible\">2e1-32d2-4bd5-bdf2-8ea61d3fb5e8</span></a></span><span class=\"reference-link-inline\"> <a href=\"https://fedibird.com/@akkoma_ap_integration_tester/108707679228362674/references\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"status-link unhandled-link\" data-status-id=\"108707679228362674\">[参照]</a></span></p>",
+ "contentMap": {
+ "ja": "<p>public quote<span class=\"quote-inline\"><br/>QT: <a class=\"status-url-link\" data-status-account-acct=\"ArtMirror@example.com\" data-status-id=\"108703793483919195\" href=\"https://example.com/objects/24d9f2e1-32d2-4bd5-bdf2-8ea61d3fb5e8\" rel=\"nofollow noopener noreferrer\" target=\"_blank\"><span class=\"invisible\">https://</span><span class=\"ellipsis\">example.com/objects/24d9f</span><span class=\"invisible\">2e1-32d2-4bd5-bdf2-8ea61d3fb5e8</span></a></span><span class=\"reference-link-inline\"> <a href=\"https://fedibird.com/@akkoma_ap_integration_tester/108707679228362674/references\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"status-link unhandled-link\" data-status-id=\"108707679228362674\">[参照]</a></span></p>"
+ },
+ "attachment": [],
+ "tag": [],
+ "replies": {
+ "id": "https://fedibird.com/users/akkoma_ap_integration_tester/statuses/108707679228362674/replies",
+ "type": "Collection",
+ "first": {
+ "type": "CollectionPage",
+ "next": "https://fedibird.com/users/akkoma_ap_integration_tester/statuses/108707679228362674/replies?only_other_accounts=true&page=true",
+ "partOf": "https://fedibird.com/users/akkoma_ap_integration_tester/statuses/108707679228362674/replies",
+ "items": []
+ }
+ },
+ "references": {
+ "id": "https://fedibird.com/users/akkoma_ap_integration_tester/statuses/108707679228362674/references",
+ "type": "Collection",
+ "first": {
+ "type": "CollectionPage",
+ "partOf": "https://fedibird.com/users/akkoma_ap_integration_tester/statuses/108707679228362674/references",
+ "items": [
+ "https://example.com/objects/24d9f2e1-32d2-4bd5-bdf2-8ea61d3fb5e8"
+ ]
+ }
+ }
+}
--- /dev/null
+{
+ "@context": [
+ "https://www.w3.org/ns/activitystreams",
+ "https://w3id.org/security/v1",
+ {
+ "manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
+ "sensitive": "as:sensitive",
+ "Hashtag": "as:Hashtag",
+ "quoteUrl": "as:quoteUrl",
+ "toot": "http://joinmastodon.org/ns#",
+ "Emoji": "toot:Emoji",
+ "featured": "toot:featured",
+ "discoverable": "toot:discoverable",
+ "schema": "http://schema.org#",
+ "PropertyValue": "schema:PropertyValue",
+ "value": "schema:value",
+ "misskey": "https://misskey-hub.net/ns#",
+ "_misskey_content": "misskey:_misskey_content",
+ "_misskey_quote": "misskey:_misskey_quote",
+ "_misskey_reaction": "misskey:_misskey_reaction",
+ "_misskey_votes": "misskey:_misskey_votes",
+ "_misskey_talk": "misskey:_misskey_talk",
+ "isCat": "misskey:isCat",
+ "vcard": "http://www.w3.org/2006/vcard/ns#"
+ }
+ ],
+ "id": "https://misskey.io/notes/934gok3482",
+ "type": "Note",
+ "attributedTo": "https://misskey.io/users/93492q0ip0",
+ "summary": null,
+ "content": "<p><span>i quompt u<br><br>RE: </span><a href=\"https://example.com/objects/30c543fb-a165-40dd-87fd-4e249ec5a40b\">https://example.com/objects/30c543fb-a165-40dd-87fd-4e249ec5a40b</a></p>",
+ "_misskey_content": "i quompt u",
+ "source": {
+ "content": "i quompt u",
+ "mediaType": "text/x.misskeymarkdown"
+ },
+ "_misskey_quote": "https://example.com/objects/43479e20-c0f8-4f49-bf7f-13fab8234924",
+ "quoteUrl": "https://example.com/objects/43479e20-c0f8-4f49-bf7f-13fab8234924",
+ "published": "2022-07-25T15:21:48.208Z",
+ "to": [
+ "https://www.w3.org/ns/activitystreams#Public"
+ ],
+ "cc": [
+ "https://misskey.io/users/93492q0ip0/followers"
+ ],
+ "inReplyTo": null,
+ "attachment": [],
+ "sensitive": false,
+ "tag": []
+}
--- /dev/null
+{
+ "@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": "<p>いつの生まれだシトリン<span class=\"quote-inline\"><br/>QT: <a class=\"status-url-link\" data-status-account-acct=\"Citrine@misskey.io\" data-status-id=\"107663207194225003\" href=\"https://misskey.io/notes/8vsn2izjwh\" rel=\"nofollow noopener noreferrer\" target=\"_blank\"><span class=\"invisible\">https://</span><span class=\"\">misskey.io/notes/8vsn2izjwh</span><span class=\"invisible\"></span></a></span></p>",
+ "contentMap": {
+ "ja": "<p>いつの生まれだシトリン<span class=\"quote-inline\"><br/>QT: <a class=\"status-url-link\" data-status-account-acct=\"Citrine@misskey.io\" data-status-id=\"107663207194225003\" href=\"https://misskey.io/notes/8vsn2izjwh\" rel=\"nofollow noopener noreferrer\" target=\"_blank\"><span class=\"invisible\">https://</span><span class=\"\">misskey.io/notes/8vsn2izjwh</span><span class=\"invisible\"></span></a></span></p>"
+ },
+ "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": []
+ }
+ }
+}
--- /dev/null
+{
+ "@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": "<p>美味しそう<span class=\"quote-inline\"><br/>QT: <a class=\"status-url-link\" data-status-account-acct=\"yamako\" data-status-id=\"107699333438289729\" href=\"https://fedibird.com/@yamako/107699333438289729\" rel=\"nofollow noopener noreferrer\" target=\"_blank\"><span class=\"invisible\">https://</span><span class=\"ellipsis\">fedibird.com/@yamako/107699333</span><span class=\"invisible\">438289729</span></a></span></p>",
+ "contentMap": {
+ "ja": "<p>美味しそう<span class=\"quote-inline\"><br/>QT: <a class=\"status-url-link\" data-status-account-acct=\"yamako\" data-status-id=\"107699333438289729\" href=\"https://fedibird.com/@yamako/107699333438289729\" rel=\"nofollow noopener noreferrer\" target=\"_blank\"><span class=\"invisible\">https://</span><span class=\"ellipsis\">fedibird.com/@yamako/107699333</span><span class=\"invisible\">438289729</span></a></span></p>"
+ },
+ "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": []
+ }
+ }
+}
--- /dev/null
+{
+ "@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": "<p><span>投稿者の設定によるね<br>Fanboxについても投稿者によっては過去の投稿は高額なプランに移動してることがある<br><br>RE: </span><a href=\"https://misskey.io/notes/8vs6wxufd0\">https://misskey.io/notes/8vs6wxufd0</a></p>",
+ "_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": []
+}
--- /dev/null
+{
+ "@context": [
+ "https://www.w3.org/ns/activitystreams",
+ "https://example.com/schemas/litepub-0.1.jsonld",
+ {
+ "@language": "und"
+ }
+ ],
+ "actor": "https://example.com/users/user",
+ "attachment": [
+ {
+ "mediaType": "image/png",
+ "name": "",
+ "type": "Document",
+ "url": "https://example.com/media/4d6097ae20200ac371f51d24eae0a94cb4b424b6aff81dcc0f7411b1a74c796f.png"
+ }
+ ],
+ "attributedTo": "https://example.com/users/user",
+ "cc": [
+ "https://example.com/users/user/followers"
+ ],
+ "content": "",
+ "context": "https://example.com/contexts/c2c52511-977e-4168-996c-bcf006789dca",
+ "conversation": "https://example.com/contexts/c2c52511-977e-4168-996c-bcf006789dca",
+ "id": "https://example.com/objects/43479e20-c0f8-4f49-bf7f-13fab8234924",
+ "published": "2022-07-24T17:25:51.614495Z",
+ "sensitive": null,
+ "source": {
+ "content": "",
+ "mediaType": "text/plain"
+ },
+ "summary": "",
+ "tag": [],
+ "to": [
+ "https://www.w3.org/ns/activitystreams#Public"
+ ],
+ "type": "Note"
+}
test "returns note data" do
user = insert(:user)
note = insert(:note)
+ quote = insert(:note)
user2 = insert(:user)
user3 = insert(:user)
tags: [name: "jimm"],
summary: "test summary",
cc: [user3.ap_id],
- extra: %{"custom_tag" => "test"}
+ extra: %{"custom_tag" => "test"},
+ quote: quote
}
expected = %{
"tag" => ["jimm"],
"to" => [user2.ap_id],
"type" => "Note",
- "custom_tag" => "test"
+ "custom_tag" => "test",
+ "quoteUri" => quote.data["id"]
}
assert {:ok, ^expected, []} = Builder.note(draft)
--- /dev/null
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
+# 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://example.com/objects/1234"
+
+ activity = %{
+ "type" => "Create",
+ "actor" => "https://example.com/users/alex",
+ "object" => %{
+ "type" => "Note",
+ "content" => "<p>Nice post</p>",
+ "quoteUri" => quote_url
+ }
+ }
+
+ {:ok, %{"object" => %{"content" => filtered}}} = InlineQuotePolicy.filter(activity)
+
+ assert filtered ==
+ "<p>Nice post<span class=\"quote-inline\"><br/><br/>RE: <a href=\"https://example.com/objects/1234\">https://example.com/objects/1234</a></span></p>"
+ 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
}
} = ArticleNotePageValidator.cast_and_validate(note)
end
+
+ test "a misskey quote should work", _ do
+ Tesla.Mock.mock(fn %{
+ method: :get,
+ url: "https://example.com/objects/43479e20-c0f8-4f49-bf7f-13fab8234924"
+ } ->
+ %Tesla.Env{
+ status: 200,
+ body: File.read!("test/fixtures/quoted_status.json"),
+ headers: HttpRequestMock.activitypub_object_headers()
+ }
+ end)
+
+ insert(:user, %{ap_id: "https://misskey.io/users/93492q0ip0"})
+ insert(:user, %{ap_id: "https://example.com/users/user"})
+
+ note =
+ "test/fixtures/misskey/quote.json"
+ |> File.read!()
+ |> Jason.decode!()
+
+ %{
+ valid?: true,
+ changes: %{
+ quoteUri: "https://example.com/objects/43479e20-c0f8-4f49-bf7f-13fab8234924"
+ }
+ } = ArticleNotePageValidator.cast_and_validate(note)
+ end
+
+ test "a fedibird quote should work", _ do
+ Tesla.Mock.mock(fn %{
+ method: :get,
+ url: "https://example.com/objects/43479e20-c0f8-4f49-bf7f-13fab8234924"
+ } ->
+ %Tesla.Env{
+ status: 200,
+ body: File.read!("test/fixtures/quoted_status.json"),
+ headers: HttpRequestMock.activitypub_object_headers()
+ }
+ end)
+
+ insert(:user, %{ap_id: "https://fedibird.com/users/akkoma_ap_integration_tester"})
+ insert(:user, %{ap_id: "https://example.com/users/user"})
+
+ note =
+ "test/fixtures/fedibird/quote.json"
+ |> File.read!()
+ |> Jason.decode!()
+
+ %{
+ valid?: true,
+ changes: %{
+ quoteUri: "https://example.com/objects/43479e20-c0f8-4f49-bf7f-13fab8234924"
+ }
+ } = ArticleNotePageValidator.cast_and_validate(note)
+ end
end
end
assert response["irreversible"] == true
- assert response["expires_at"] ==
- NaiveDateTime.utc_now()
- |> NaiveDateTime.add(in_seconds)
- |> Pleroma.Web.CommonAPI.Utils.to_masto_date()
+ expected_time =
+ NaiveDateTime.utc_now()
+ |> NaiveDateTime.add(in_seconds)
+
+ assert NaiveDateTime.diff(
+ NaiveDateTime.from_iso8601!(response["expires_at"]),
+ expected_time
+ ) < 5
filter = Filter.get(response["id"], user)
} = result
end
end
+
+ describe "posting quotes" do
+ setup do: oauth_access(["write:statuses"])
+
+ test "posting a quote", %{conn: conn} do
+ user = insert(:user)
+ {:ok, quoted_status} = CommonAPI.post(user, %{status: "tell me, for whom do you fight?"})
+
+ conn =
+ conn
+ |> put_req_header("content-type", "application/json")
+ |> post("/api/v1/statuses", %{
+ "status" => "Hmph, how very glib",
+ "quote_id" => quoted_status.id
+ })
+
+ response = json_response_and_validate_schema(conn, 200)
+
+ assert response["quote_id"] == quoted_status.id
+ assert response["quote"]["id"] == quoted_status.id
+ assert response["quote"]["content"] == quoted_status.object.data["content"]
+ end
+
+ test "posting a quote, quoting a status that isn't public", %{conn: conn} do
+ user = insert(:user)
+
+ Enum.each(["private", "local", "direct"], fn visibility ->
+ {:ok, quoted_status} =
+ CommonAPI.post(user, %{
+ status: "tell me, for whom do you fight?",
+ visibility: visibility
+ })
+
+ assert %{"error" => "You can only quote public or unlisted statuses"} =
+ conn
+ |> put_req_header("content-type", "application/json")
+ |> post("/api/v1/statuses", %{
+ "status" => "Hmph, how very glib",
+ "quote_id" => quoted_status.id
+ })
+ |> json_response_and_validate_schema(422)
+ end)
+ end
+
+ test "posting a quote, after quote, the status gets deleted", %{conn: conn} do
+ user = insert(:user)
+
+ {:ok, quoted_status} =
+ CommonAPI.post(user, %{status: "tell me, for whom do you fight?", visibility: "public"})
+
+ resp =
+ conn
+ |> put_req_header("content-type", "application/json")
+ |> post("/api/v1/statuses", %{
+ "status" => "I fight for eorzea!",
+ "quote_id" => quoted_status.id
+ })
+ |> json_response_and_validate_schema(200)
+
+ {:ok, _} = CommonAPI.delete(quoted_status.id, user)
+
+ resp =
+ conn
+ |> get("/api/v1/statuses/#{resp["id"]}")
+ |> json_response_and_validate_schema(200)
+
+ assert is_nil(resp["quote"])
+ end
+
+ test "posting a quote of a deleted status", %{conn: conn} do
+ user = insert(:user)
+
+ {:ok, quoted_status} =
+ CommonAPI.post(user, %{status: "tell me, for whom do you fight?", visibility: "public"})
+
+ {:ok, _} = CommonAPI.delete(quoted_status.id, user)
+
+ assert %{"error" => _} =
+ conn
+ |> put_req_header("content-type", "application/json")
+ |> post("/api/v1/statuses", %{
+ "status" => "I fight for eorzea!",
+ "quote_id" => quoted_status.id
+ })
+ |> json_response_and_validate_schema(422)
+ end
+
+ test "posting a quote of a status that doesn't exist", %{conn: conn} do
+ assert %{"error" => "You can't quote a status that doesn't exist"} =
+ conn
+ |> put_req_header("content-type", "application/json")
+ |> post("/api/v1/statuses", %{
+ "status" => "I fight for eorzea!",
+ "quote_id" => "oops"
+ })
+ |> json_response_and_validate_schema(422)
+ end
+ end
end
},
akkoma: %{
source: HTML.filter_tags(object_data["content"])
- }
+ },
+ quote_id: nil,
+ quote: nil
}
assert status == expected
assert status.in_reply_to_id == to_string(note.id)
end
+ test "a quote" do
+ note = insert(:note_activity)
+ user = insert(:user)
+
+ {:ok, activity} = CommonAPI.post(user, %{status: "hehe", quote_id: note.id})
+
+ status = StatusView.render("show.json", %{activity: activity})
+
+ assert status.quote_id == to_string(note.id)
+
+ [status] = StatusView.render("index.json", %{activities: [activity], as: :activity})
+
+ assert status.quote_id == to_string(note.id)
+ end
+
+ test "a quote that we can't resolve" do
+ note = insert(:note_activity, quoteUri: "oopsie")
+
+ status = StatusView.render("show.json", %{activity: note})
+
+ assert is_nil(status.quote_id)
+ assert is_nil(status.quote)
+ end
+
test "contains mentions" do
user = insert(:user)
mentioned = insert(:user)