diff --git a/config/config.exs b/config/config.exs index cfe207dcc..d2a55a0ec 100644 --- a/config/config.exs +++ b/config/config.exs @@ -407,6 +407,8 @@ accept: [], reject: [] +config :pleroma, :mrf_inline_quote, prefix: "Quote" + # threshold of 7 days config :pleroma, :mrf_object_age, threshold: 604_800, diff --git a/lib/pleroma/activity.ex b/lib/pleroma/activity.ex index abfe778d2..026a0ea2a 100644 --- a/lib/pleroma/activity.ex +++ b/lib/pleroma/activity.ex @@ -292,6 +292,13 @@ 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 + IO.puts(ap_id) + 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) diff --git a/lib/pleroma/web/activity_pub/builder.ex b/lib/pleroma/web/activity_pub/builder.ex index 8c51b8d63..97ceaf08e 100644 --- a/lib/pleroma/web/activity_pub/builder.ex +++ b/lib/pleroma/web/activity_pub/builder.ex @@ -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, %{ diff --git a/lib/pleroma/web/activity_pub/mrf/inline_quote_policy.ex b/lib/pleroma/web/activity_pub/mrf/inline_quote_policy.ex new file mode 100644 index 000000000..c78675caf --- /dev/null +++ b/lib/pleroma/web/activity_pub/mrf/inline_quote_policy.ex @@ -0,0 +1,71 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2021 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.MRF.InlineQuotePolicy do + @moduledoc "Force a quote line into the message content." + @behaviour Pleroma.Web.ActivityPub.MRF.Policy + + defp build_inline_quote(prefix, url) do + "

#{prefix}: #{url}
" + end + + defp has_inline_quote?(content, quote_url) do + cond do + # Does the quote URL exist in the content? + content =~ quote_url -> true + # Does the content already have a .quote-inline span? + content =~ "" -> true + # No inline quote found + true -> false + end + end + + defp filter_object(%{"quoteUrl" => quote_url} = object) do + content = object["content"] || "" + + if has_inline_quote?(content, quote_url) do + object + else + prefix = Pleroma.Config.get([:mrf_inline_quote, :prefix]) + + content = + if String.ends_with?(content, "

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

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

", + else: content <> build_inline_quote(prefix, quote_url) + + Map.put(object, "content", content) + end + end + + @impl true + def filter(%{"object" => %{"quoteUrl" => _} = object} = activity) do + {:ok, Map.put(activity, "object", filter_object(object))} + end + + @impl true + def filter(object), do: {:ok, object} + + @impl true + def describe, do: {:ok, %{}} + + @impl true + def config_description do + %{ + key: :mrf_inline_quote, + related_policy: "Pleroma.Web.ActivityPub.MRF.InlineQuotePolicy", + label: "MRF Inline Quote", + description: "Force quote post URLs inline", + children: [ + %{ + key: :prefix, + type: :string, + description: "Prefix before the link", + suggestions: ["RT", "QT", "RE", "RN"] + } + ] + } + end +end diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex index 115dfc470..ff73005c7 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier.ex @@ -43,6 +43,7 @@ def fix_object(object, options \\ []) do |> fix_content_map() |> fix_addressing() |> fix_summary() + |> fix_quote_url() end def fix_summary(%{"summary" => nil} = object) do @@ -879,6 +880,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}) diff --git a/lib/pleroma/web/api_spec/operations/status_operation.ex b/lib/pleroma/web/api_spec/operations/status_operation.ex index c7df676c3..ad3889f02 100644 --- a/lib/pleroma/web/api_spec/operations/status_operation.ex +++ b/lib/pleroma/web/api_spec/operations/status_operation.ex @@ -496,6 +496,12 @@ 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: %{ diff --git a/lib/pleroma/web/api_spec/schemas/status.ex b/lib/pleroma/web/api_spec/schemas/status.ex index 3caab0f00..91fe7b0b5 100644 --- a/lib/pleroma/web/api_spec/schemas/status.ex +++ b/lib/pleroma/web/api_spec/schemas/status.ex @@ -132,6 +132,14 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Status do pinned: %Schema{ 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{ + }, pleroma: %Schema{ type: :object, @@ -204,6 +212,25 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Status do } } }, + akkoma: %Schema{ + type: :object, + properties: %{ + source: %Schema{ + type: :object, + properties: %{ + content: %Schema{ + type: :string, + description: "The source content of the status" + }, + mediaType: %{ + type: :string, + description: "The source MIME type of the status", + example: "text/plain" + }, + } + } + } + }, poll: %Schema{allOf: [Poll], nullable: true, description: "The poll attached to the status"}, reblog: %Schema{ allOf: [%OpenApiSpex.Reference{"$ref": "#/components/schemas/Status"}], diff --git a/lib/pleroma/web/common_api/activity_draft.ex b/lib/pleroma/web/common_api/activity_draft.ex index ea88213fb..2450cf853 100644 --- a/lib/pleroma/web/common_api/activity_draft.ex +++ b/lib/pleroma/web/common_api/activity_draft.ex @@ -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, @@ -37,6 +39,7 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do preview?: false, changes: %{} + def new(user, params) do %__MODULE__{user: user} |> put_params(params) @@ -54,6 +57,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("e_id/1) |> content() |> with_valid(&to_and_cc/1) |> with_valid(&context/1) @@ -108,6 +112,18 @@ 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 + %__MODULE__{draft | quote: Activity.get_by_id(id)} + 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" -> diff --git a/lib/pleroma/web/federator.ex b/lib/pleroma/web/federator.ex index 82fb9e4e0..7f525a525 100644 --- a/lib/pleroma/web/federator.ex +++ b/lib/pleroma/web/federator.ex @@ -112,6 +112,7 @@ def perform(:incoming_ap_doc, params) do e -> # Just drop those for now Logger.debug(fn -> "Unhandled activity\n" <> Jason.encode!(params, pretty: true) end) + IO.inspect(e) {:error, e} end end diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex index c0f467592..06415b700 100644 --- a/lib/pleroma/web/mastodon_api/views/status_view.ex +++ b/lib/pleroma/web/mastodon_api/views/status_view.ex @@ -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,18 @@ 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] do + nil + else + opts = + opts + |> Map.put(:activity, quote) + |> Map.put(:do_not_recurse, true) + render("show.json", opts) + end + end end