Compare commits

...

36 Commits

Author SHA1 Message Date
Alex Gleason 6d710a0f7e
Merge remote-tracking branch 'origin/develop' into quote-post 2022-02-02 12:50:03 -06:00
Alex Gleason 643f78cb22
InlineQuotePolicy: skip objects which already have an .inline-quote span 2022-01-30 10:57:29 -06:00
Alex Gleason deff42f034
Merge remote-tracking branch 'origin/develop' into quote-post 2022-01-30 10:50:35 -06:00
Alex Gleason 4883b996ad
Actually, don't send _misskey_quote anymore 2022-01-28 17:53:19 -06:00
Alex Gleason 108997b764
InlineQuotePolicy: improve the way Markdown quotes are displayed by other software 2022-01-28 16:07:17 -06:00
Alex Gleason b73a53ec69
Handle Fedibird's new quoteUri field 2022-01-28 15:55:52 -06:00
Alex Gleason ac3426cbca
Transmogrifier: federate quotes with _misskey_quote field 2022-01-28 14:06:32 -06:00
Alex Gleason 5134e691f4
StatusView: return quote post inside a reblog 2022-01-28 12:33:55 -06:00
Alex Gleason a3d65937ab
Add InlineQuotePolicy as a default MRF 2022-01-27 15:01:20 -06:00
Alex Gleason 087e060d30
InlineQuotePolicy: don't add line breaks to markdown posts 2022-01-27 14:28:06 -06:00
Alex Gleason 91822c383c
StatusView: add `quote_visible` param 2022-01-26 11:52:50 -06:00
Alex Gleason 660490c2ec
StatusView: fix quote visibility 2022-01-26 11:49:31 -06:00
Alex Gleason 5a1fa6bca2
CommonAPI: disallow quoting private posts through the API 2022-01-26 11:21:49 -06:00
Alex Gleason 93a340668f
Merge remote-tracking branch 'origin/develop' into quote-post 2022-01-25 11:57:04 -06:00
Alex Gleason 1cb8326cfb
Merge remote-tracking branch 'origin/develop' into quote-post 2022-01-24 16:48:37 -06:00
Alex Gleason 0571f8f553
Add InlineQuotePolicy to force quote URLs inline 2022-01-24 16:44:35 -06:00
Alex Gleason cd9341dbff
Scrubber.Default: allow span.quote-inline for quote post compatibility 2022-01-24 15:34:23 -06:00
Alex Gleason c04241eb4a
ActivityDraft: mix format, defensive actor ID 2022-01-23 16:03:46 -06:00
Alex Gleason d39ccd8372
ActivityDraft: mention the OP of a quoted post 2022-01-23 15:46:44 -06:00
Alex Gleason d903a6b85d
Return quote_url through the API, don't render quotes more than 1 level deep 2022-01-23 13:55:25 -06:00
Alex Gleason 4f51c41f9f
@context: add quoteUrl 2022-01-22 23:29:55 -06:00
Alex Gleason c22b354587
InstanceView: add "quote_posting" feature 2022-01-22 23:09:33 -06:00
Alex Gleason d293265b06
Fix typos 2022-01-22 23:02:44 -06:00
Alex Gleason c13ccb0f84
mix format 2022-01-22 22:57:42 -06:00
Alex Gleason 00b2dc7ecb
TransmogrifierTest: prepare an outgoing quote post 2022-01-22 22:41:57 -06:00
Alex Gleason bbd3433f5a
StatusControllerTest: test creating a quote post 2022-01-22 22:35:08 -06:00
Alex Gleason 4b2fe550de
BuilderTest: build quote post 2022-01-22 22:29:13 -06:00
Alex Gleason 69cab8da89
ActivityDraft: allow quoting 2022-01-22 22:15:54 -06:00
Alex Gleason 870023e286
ActivityDraft: create quote posts 2022-01-22 21:27:05 -06:00
Alex Gleason d4e9cb600d
StatusView: render the whole quoted status 2022-01-22 20:05:58 -06:00
Alex Gleason 0584a6f131
StatusView: show quoted posts through the API, probably 2022-01-22 19:47:08 -06:00
Alex Gleason 04709e1a27
Transmogrifier: fix quoteUrl here too 2022-01-22 19:14:39 -06:00
Alex Gleason fbdfeb326d
Transmogrifier: fetch quoted post 2022-01-22 18:46:58 -06:00
Alex Gleason 0f34eb60d4
ObjectValidators: improve quoteUrl compatibility 2022-01-22 18:03:22 -06:00
Alex Gleason 0453713c70
Quote post: add fixtures 2022-01-22 17:30:49 -06:00
Alex Gleason c6ff668b1d
ObjectValidators: accept "quoteUrl" field 2022-01-22 16:41:51 -06:00
29 changed files with 979 additions and 7 deletions

View File

@ -414,6 +414,8 @@
config :pleroma, :mrf_follow_bot, follower_nickname: nil
config :pleroma, :mrf_inline_quote, prefix: "RT"
config :pleroma, :rich_media,
enabled: true,
ignore_hosts: [],
@ -831,7 +833,11 @@
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: []

View File

@ -127,6 +127,7 @@ To add configuration to your config file, you can copy it from the base config.
* `Pleroma.Web.ActivityPub.MRF.FollowBotPolicy`: Automatically follows newly discovered users from the specified bot account. Local accounts, locked accounts, and users with "#nobot" in their bio are respected and excluded from being followed.
* `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.
@ -228,6 +229,8 @@ Notes:
* `follower_nickname`: The name of the bot account to use for following newly discovered users. Using `followbot` or similar is strongly suggested.
#### :mrf_inline_quote
* `prefix`: Prefix before the link (default: `RT`)
### :activitypub
* `unfollow_blocked`: Whether blocks result in people getting unfollowed

View File

@ -142,6 +142,7 @@ def note(%ActivityDraft{} = draft) 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, []}
@ -157,6 +158,16 @@ defp add_in_reply_to(object, in_reply_to) 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(),

View File

@ -0,0 +1,71 @@
# 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(%{"quoteUrl" => 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)
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

View File

@ -65,6 +65,27 @@ defp fix_replies(%{"replies" => replies} = data) when is_bitstring(replies),
defp fix_replies(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()
@ -72,6 +93,7 @@ defp fix(data) do
|> fix_url()
|> fix_tag()
|> fix_replies()
|> fix_quote_url()
|> Transmogrifier.fix_emoji()
|> Transmogrifier.fix_content_map()
end

View File

@ -27,7 +27,7 @@ defmacro activity_fields 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)
@ -59,6 +59,7 @@ defmacro status_object_fields do
field(:like_count, :integer, default: 0)
field(:announcement_count, :integer, default: 0)
field(:inReplyTo, ObjectValidators.ObjectID)
field(:quoteUrl, ObjectValidators.ObjectID)
field(:url, ObjectValidators.Uri)
field(:likes, {:array, ObjectValidators.ObjectID}, default: [])

View File

@ -167,6 +167,45 @@ def fix_in_reply_to(%{"inReplyTo" => in_reply_to} = object, options)
def fix_in_reply_to(object, _options), do: object
def fix_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
Map.put(object, "quoteUrl", quoted_object.data["id"])
else
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) ->
@ -455,6 +494,7 @@ def handle_incoming(
|> strip_internal_fields()
|> fix_type(fetch_options)
|> fix_in_reply_to(fetch_options)
|> fix_quote_url(fetch_options)
data = Map.put(data, "object", object)
options = Keyword.put(options, :local, false)
@ -629,6 +669,16 @@ def set_reply_to_uri(%{"inReplyTo" => in_reply_to} = object) when is_binary(in_r
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.put(object, "quoteUri", 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.
@ -683,6 +733,7 @@ def prepare_object(object) do
|> prepare_attachments
|> set_conversation
|> set_reply_to_uri
|> set_quote_url
|> set_replies
|> strip_internal_fields
|> strip_internal_tags

View File

@ -524,6 +524,11 @@ defp create_request 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: %{

View File

@ -177,6 +177,21 @@ 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)"
},
quote_url: %Schema{
type: :string,
format: :uri,
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"

View File

@ -7,10 +7,12 @@ 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
import Pleroma.Web.Gettext
import Pleroma.Web.Utils.Guards, only: [not_empty_string: 1]
defstruct valid?: true,
errors: [],
@ -22,6 +24,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,7 +56,9 @@ def create(user, params) do
|> poll()
|> with_valid(&in_reply_to/1)
|> with_valid(&in_reply_to_conversation/1)
|> with_valid(&quote_post/1)
|> with_valid(&visibility/1)
|> with_valid(&quoting_visibility/1)
|> content()
|> with_valid(&to_and_cc/1)
|> with_valid(&context/1)
@ -127,6 +132,21 @@ defp in_reply_to(%{params: %{in_reply_to_status_id: %Activity{} = in_reply_to}}
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_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}
_ ->
draft
end
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}
@ -142,6 +162,17 @@ defp visibility(%{params: params} = draft) 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}
@ -159,12 +190,15 @@ defp poll(draft) 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}

View File

@ -67,6 +67,7 @@ def features do
"shareable_emoji_packs",
"multifetch",
"pleroma:api/v1/notifications:include_types_filter",
"quote_posting",
if Config.get([:activitypub, :blockers_visible]) do
"blockers_visible"
end,

View File

@ -57,6 +57,27 @@ defp get_replied_to_activities(activities) 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
defp get_context_id(%{data: %{"context_id" => context_id}}) when not is_nil(context_id),
do: context_id
@ -89,6 +110,7 @@ def render("index.json", opts) 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
@ -121,6 +143,7 @@ def render("index.json", opts) 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)
@ -259,9 +282,18 @@ def render("show.json", %{activity: %{data: %{"object" => _object}} = activity}
created_at = Utils.to_masto_date(object.data["published"])
reply_to = get_reply_to(activity, opts)
reply_to_user = reply_to && CommonAPI.get_user(reply_to.data["actor"])
quote_activity = get_quote(activity, opts)
quote_post =
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
nil
end
content =
object
|> render_content()
@ -368,6 +400,9 @@ def render("show.json", %{activity: %{data: %{"object" => _object}} = activity}
local: activity.local,
conversation_id: get_context_id(activity),
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,
@ -500,6 +535,27 @@ def get_reply_to(%{data: %{"object" => _object}} = activity, _) 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)
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
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"]

View File

@ -56,7 +56,12 @@ defmodule Pleroma.HTML.Scrubber.Default do
Meta.allow_tag_with_these_attributes(:u, [])
Meta.allow_tag_with_these_attributes(:ul, [])
Meta.allow_tag_with_this_attribute_values(:span, "class", ["h-card", "recipients-inline"])
Meta.allow_tag_with_this_attribute_values(:span, "class", [
"h-card",
"quote-inline",
"recipients-inline"
])
Meta.allow_tag_with_these_attributes(:span, [])
Meta.allow_tag_with_this_attribute_values(:code, "class", ["inline"])

View File

@ -17,6 +17,7 @@
"ostatus": "http://ostatus.org#",
"schema": "http://schema.org#",
"toot": "http://joinmastodon.org/ns#",
"fedibird": "http://fedibird.com/ns#",
"value": "schema:value",
"sensitive": "as:sensitive",
"litepub": "http://litepub.social/ns#",
@ -26,6 +27,8 @@
"@id": "litepub:listMessage",
"@type": "@id"
},
"quoteUrl": "as:quoteUrl",
"quoteUri": "fedibird:quoteUri",
"oauthRegistrationEndpoint": {
"@id": "litepub:oauthRegistrationEndpoint",
"@type": "@id"

View File

@ -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": "<p>揺れていたようだ<span class=\"quote-inline\"><br/>QT: <a class=\"status-url-link\" data-status-account-acct=\"UN_NERV@unnerv.jp\" data-status-id=\"107712177062934465\" href=\"https://unnerv.jp/@UN_NERV/107712176849067434\" rel=\"nofollow noopener noreferrer\" target=\"_blank\"><span class=\"invisible\">https://</span><span class=\"ellipsis\">unnerv.jp/@UN_NERV/10771217684</span><span class=\"invisible\">9067434</span></a></span></p>",
"contentMap": {
"ja": "<p>揺れていたようだ<span class=\"quote-inline\"><br/>QT: <a class=\"status-url-link\" data-status-account-acct=\"UN_NERV@unnerv.jp\" data-status-id=\"107712177062934465\" href=\"https://unnerv.jp/@UN_NERV/107712176849067434\" rel=\"nofollow noopener noreferrer\" target=\"_blank\"><span class=\"invisible\">https://</span><span class=\"ellipsis\">unnerv.jp/@UN_NERV/10771217684</span><span class=\"invisible\">9067434</span></a></span></p>"
},
"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": []
}
}
}

View File

@ -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": "<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": []
}
}
}

View File

@ -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": "<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": []
}
}
}

View File

@ -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": "<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": []
}

View File

@ -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": "<p><span>わずかな作曲要素 巣穴で独り言<br>Twitter </span><a href=\"https://twitter.com/aimu_53\">https://twitter.com/aimu_53</a><span><br>Soundcloud </span><a href=\"https://soundcloud.com/aimu-53\">https://soundcloud.com/aimu-53</a></p>",
"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"
}

View File

@ -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": "<p><span>Fantiaこれできないように過去のやつは従量課金だった気がする</span></p>",
"_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": []
}

View File

@ -44,5 +44,34 @@ test "returns note data" 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: "<h1>This is :moominmamma: note</h1>",
quote_post: note,
extra: %{}
}
expected = %{
"actor" => user.ap_id,
"attachment" => [],
"content" => "<h1>This is :moominmamma: note</h1>",
"context" => "2hu",
"sensitive" => false,
"type" => "Note",
"quoteUrl" => note.data["id"],
"cc" => [],
"summary" => nil,
"tag" => [],
"to" => []
}
assert {:ok, ^expected, []} = Builder.note(draft)
end
end
end

View File

@ -0,0 +1,92 @@
# 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://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<span class=\"quote-inline\"><br/><br/>RT: <a href=\"https://gleasonator.com/objects/1234\">https://gleasonator.com/objects/1234</a></span>"
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",
"object" => %{
"type" => "Note",
"content" => "<p>Nice post</p>",
"quoteUrl" => quote_url
}
}
{:ok, %{"object" => %{"content" => filtered}}} = InlineQuotePolicy.filter(activity)
assert filtered ==
"<p>Nice post<span class=\"quote-inline\"><br/><br/>RT: <a href=\"https://gleasonator.com/objects/1234\">https://gleasonator.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
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

View File

@ -33,6 +33,36 @@ test "a basic note validates", %{note: note} do
end
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!()
cng = ArticleNotePageValidator.cast_and_validate(data)
assert cng.valid?
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")
data = File.read!("test/fixtures/quote_post/misskey_quote_post.json") |> Jason.decode!()
cng = ArticleNotePageValidator.cast_and_validate(data)
assert cng.valid?
assert cng.changes.quoteUrl == "https://misskey.io/notes/8vs6wxufd0"
end
test "a Note from Roadhouse validates" do
insert(:user, ap_id: "https://macgirvin.com/channel/mike")

View File

@ -108,6 +108,28 @@ test "it accepts Move activities" do
assert activity.data["type"] == "Move"
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
test "a reply with mismatched context is rejected" do
insert(:user, ap_id: "https://macgirvin.com/channel/mike")
@ -312,6 +334,20 @@ test "custom emoji urls are URI encoded" do
assert url == "http://localhost:4001/emoji/dino%20walking.gif"
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)
%{data: %{"id" => quote_id}} = Object.normalize(quoted_post)
assert modified["object"]["quoteUrl"] == quote_id
assert modified["object"]["quoteUri"] == quote_id
end
end
describe "user upgrade" do

View File

@ -0,0 +1,26 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# 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

View File

@ -696,6 +696,46 @@ test "it can handle activities that expire" do
scheduled_at: expires_at
)
end
test "it 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"]
# 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
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

View File

@ -124,6 +124,28 @@ test "posting a status", %{conn: conn} do
)
end
test "posting a quote post", %{conn: conn} do
user = insert(:user)
{:ok, %{id: activity_id} = activity} = CommonAPI.post(user, %{status: "yolo"})
%{data: %{"id" => quote_url}} = Object.normalize(activity)
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}, "quote_url" => ^quote_url}
} = 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

View File

@ -280,6 +280,9 @@ test "a note activity" do
local: true,
conversation_id: convo_id,
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,
@ -376,6 +379,84 @@ test "a reply" 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 "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"]
refute status.pleroma.quote_visible
# 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)
assert status.pleroma.quote_visible
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"]
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)

View File

@ -1311,6 +1311,24 @@ def get("https://patch.cx/objects/a399c28e-c821-4820-bc3e-4afeb044c16f", _, _, _
}}
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://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("https://gleasonator.com/objects/102eb097-a18b-4cd5-abfc-f952efcb70bb", _, _, _) do
{:ok,
%Tesla.Env{