Compare commits

..

6 commits

36 changed files with 871 additions and 16 deletions

View file

@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [Unreleased]
### Added
- extended runtime module support, see config cheatsheet
- quote posting; quotes are limited to public posts
### Fixed
- Updated mastoFE path, for the newer version
@ -18,7 +22,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- `/api/v1/notifications/dismiss`
- `/api/v1/search`
- `/api/v1/statuses/{id}/card`
- LDAP authenticator
- LDAP authenticator (use the akkoma-contrib-authenticator-ldap runtime module)
- Chats, they were half-baked. Just use PMs.
- Prometheus, it causes massive slowdown

View file

@ -407,6 +407,8 @@
accept: [],
reject: []
config :pleroma, :mrf_inline_quote, prefix: "RE"
# threshold of 7 days
config :pleroma, :mrf_object_age,
threshold: 604_800,

View file

@ -300,3 +300,28 @@
```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`.

View file

@ -1012,7 +1012,22 @@ config :pleroma, Pleroma.Formatter,
## Custom Runtime Modules (`:modules`)
* `runtime_dir`: A path to custom Elixir modules (such as MRF policies).
* `runtime_dir`: A path to custom Elixir modules, such as MRF policies or
custom authenticators. These modules will be loaded on boot, and can be
contained in subdirectories. It is advised to use version-controlled
subdirectories to make management of them a bit easier. Note that only
files with the extension `.ex` will be loaded.
```elixir
config :pleroma, :modules, runtime_dir: "instance/modules"
```
### Adding a module
```bash
cd instance/modules/
git clone <MY MODULE>
```
## :configurable_from_database

View file

@ -292,6 +292,12 @@ def get_in_reply_to_activity(%Activity{} = activity) do
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)

View file

@ -730,12 +730,12 @@ def maybe_validate_required_email(changeset, _) do
end
end
defp put_ap_id(changeset) do
def put_ap_id(changeset) do
ap_id = ap_id(%User{nickname: get_field(changeset, :nickname)})
put_change(changeset, :ap_id, ap_id)
end
defp put_following_and_follower_and_featured_address(changeset) do
def put_following_and_follower_and_featured_address(changeset) do
user = %User{nickname: get_field(changeset, :nickname)}
followers = ap_followers(user)
following = ap_following(user)
@ -2041,7 +2041,7 @@ defp normalize_tags(tags) do
|> Enum.map(&String.downcase/1)
end
defp local_nickname_regex do
def local_nickname_regex do
if Config.get([:instance, :extended_nickname_format]) do
@extended_local_nickname_regex
else

View file

@ -14,10 +14,23 @@ defmodule Pleroma.Utils do
@repo_timeout Pleroma.Config.get([Pleroma.Repo, :timeout], 15_000)
def compile_dir(dir) when is_binary(dir) do
dir
|> elixir_files()
|> Kernel.ParallelCompiler.compile()
end
defp elixir_files(dir) when is_binary(dir) do
dir
|> File.ls!()
|> Enum.map(&Path.join(dir, &1))
|> Kernel.ParallelCompiler.compile()
|> Enum.flat_map(fn path ->
if File.dir?(path) do
elixir_files(path)
else
[path]
end
end)
|> Enum.filter(fn path -> String.ends_with?(path, ".ex") end)
end
@doc """

View file

@ -168,6 +168,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)
|> Map.merge(draft.extra)
{:ok, data, []}
@ -183,6 +184,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) 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,
%{

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(%{"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

View file

@ -156,6 +156,7 @@ defp fix(data) do
|> fix_replies()
|> fix_source()
|> fix_misskey_content()
|> Transmogrifier.fix_quote_url()
|> Transmogrifier.fix_attachments()
|> Transmogrifier.fix_emoji()
|> Transmogrifier.fix_content_map()

View file

@ -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(:quoteUri, ObjectValidators.ObjectID)
field(:url, ObjectValidators.Uri)
field(:likes, {:array, ObjectValidators.ObjectID}, default: [])

View file

@ -598,6 +598,12 @@ def set_reply_to_uri(%{"inReplyTo" => in_reply_to} = object) when is_binary(in_r
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.
@ -652,6 +658,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
@ -879,6 +886,43 @@ defp strip_internal_tags(%{"tag" => tags} = object) do
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})

View file

@ -496,6 +496,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,
type: :string,
description: "Will quote a given status."
}
},
example: %{

View file

@ -133,6 +133,16 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Status do
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: %{
@ -204,6 +214,33 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Status do
}
}
},
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"}],

View file

@ -319,6 +319,10 @@ def get_replied_to_visibility(activity) do
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

View file

@ -22,6 +22,8 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
attachments: [],
in_reply_to: nil,
in_reply_to_conversation: nil,
quote_id: nil,
quote: nil,
visibility: nil,
expires_at: nil,
extra: nil,
@ -54,6 +56,7 @@ def create(user, params) do
|> with_valid(&in_reply_to/1)
|> with_valid(&in_reply_to_conversation/1)
|> with_valid(&visibility/1)
|> with_valid(&quote_id/1)
|> content()
|> with_valid(&to_and_cc/1)
|> with_valid(&context/1)
@ -108,6 +111,28 @@ defp in_reply_to_conversation(draft) do
%__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" ->

View file

@ -329,6 +329,8 @@ def render("show.json", %{activity: %{data: %{"object" => _object}} = activity}
{pinned?, pinned_at} = pin_data(object, user)
quote = Activity.get_quoted_activity_from_object(object)
%{
id: to_string(activity.id),
uri: object.data["id"],
@ -363,6 +365,8 @@ def render("show.json", %{activity: %{data: %{"object" => _object}} = activity}
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),
@ -604,4 +608,19 @@ defp build_image_url(%URI{} = image_url_data, %URI{} = page_url_data) do
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

View file

@ -117,7 +117,7 @@ defp csp_string do
if Config.get(:env) == :dev do
"script-src 'self' 'unsafe-eval'"
else
"script-src 'self'"
"script-src 'self' 'unsafe-eval'"
end
report = if report_uri, do: ["report-uri ", report_uri, ";report-to csp-endpoint"]

View file

@ -4,7 +4,7 @@ defmodule Pleroma.Mixfile do
def project do
[
app: :pleroma,
version: version("3.0.0"),
version: version("3.0.1"),
elixir: "~> 1.9",
elixirc_paths: elixirc_paths(Mix.env()),
compilers: [:phoenix, :gettext] ++ Mix.compilers(),
@ -34,7 +34,7 @@ def project do
releases: [
pleroma: [
include_executables_for: [:unix],
applications: [ex_syslogger: :load, syslog: :load],
applications: [ex_syslogger: :load, syslog: :load, eldap: :transient],
steps: [:assemble, &put_otp_version/1, &copy_files/1, &copy_nginx_config/1],
config_providers: [{Pleroma.Config.ReleaseRuntimeProvider, []}]
]

View file

@ -17,6 +17,8 @@
"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#",
@ -26,6 +28,8 @@
"@id": "litepub:listMessage",
"@type": "@id"
},
"quoteUrl": "as:quoteUrl",
"quoteUri": "fedibird:quoteUri",
"oauthRegistrationEndpoint": {
"@id": "litepub:oauthRegistrationEndpoint",
"@type": "@id"

73
test/fixtures/fedibird/quote.json vendored Normal file
View file

@ -0,0 +1,73 @@
{
"@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"
]
}
}
}

50
test/fixtures/misskey/quote.json vendored Normal file
View file

@ -0,0 +1,50 @@
{
"@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": []
}

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

38
test/fixtures/quoted_status.json vendored Normal file
View file

@ -0,0 +1,38 @@
{
"@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"
}

View file

@ -0,0 +1,2 @@
defmodule DynamicModule.First do
end

View file

@ -0,0 +1 @@
def thisisnotloaded

View file

@ -0,0 +1,2 @@
defmodule DynamicModule.Second do
end

View file

@ -12,4 +12,11 @@ test "returns unique temporary directory" do
File.rm_rf(path)
end
end
describe "compile_dir/1" do
test "recursively compiles directories" do
{:ok, [DynamicModule.First, DynamicModule.Second], []} =
Pleroma.Utils.compile_dir("test/fixtures/runtime_modules")
end
end
end

View file

@ -13,6 +13,7 @@ defmodule Pleroma.Web.ActivityPub.BuilderTest do
test "returns note data" do
user = insert(:user)
note = insert(:note)
quote = insert(:note)
user2 = insert(:user)
user3 = insert(:user)
@ -25,7 +26,8 @@ test "returns note data" do
tags: [name: "jimm"],
summary: "test summary",
cc: [user3.ap_id],
extra: %{"custom_tag" => "test"}
extra: %{"custom_tag" => "test"},
quote: quote
}
expected = %{
@ -39,7 +41,8 @@ test "returns note data" do
"tag" => ["jimm"],
"to" => [user2.ap_id],
"type" => "Note",
"custom_tag" => "test"
"custom_tag" => "test",
"quoteUri" => quote.data["id"]
}
assert {:ok, ^expected, []} = Builder.note(draft)

View file

@ -0,0 +1,56 @@
# 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

View file

@ -143,5 +143,61 @@ test "a misskey MFM status with a _misskey_content field should work and be link
}
} = 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

View file

@ -193,10 +193,14 @@ test "with adding expires_at", %{conn: conn, user: user} do
assert response["irreversible"] == true
assert response["expires_at"] ==
expected_time =
NaiveDateTime.utc_now()
|> NaiveDateTime.add(in_seconds)
|> Pleroma.Web.CommonAPI.Utils.to_masto_date()
assert NaiveDateTime.diff(
NaiveDateTime.from_iso8601!(response["expires_at"]),
expected_time
) < 5
filter = Filter.get(response["id"], user)

View file

@ -1944,4 +1944,102 @@ test "show" do
} = 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

View file

@ -305,7 +305,9 @@ test "a note activity" do
},
akkoma: %{
source: HTML.filter_tags(object_data["content"])
}
},
quote_id: nil,
quote: nil
}
assert status == expected
@ -393,6 +395,30 @@ test "a reply" do
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)