diff options
Diffstat (limited to 'lib/pleroma/web/activity_pub')
45 files changed, 1539 insertions, 435 deletions
diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index e54adf611..219a208d2 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -96,7 +96,18 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do    defp increase_replies_count_if_reply(_create_data), do: :noop -  @object_types ~w[ChatMessage Question Answer Audio Video Event Article Note Page] +  defp increase_quotes_count_if_quote(%{ +         "object" => %{"quoteUrl" => quote_ap_id} = object, +         "type" => "Create" +       }) do +    if is_public?(object) do +      Object.increase_quotes_count(quote_ap_id) +    end +  end + +  defp increase_quotes_count_if_quote(_create_data), do: :noop + +  @object_types ~w[ChatMessage Question Answer Audio Video Image Event Article Note Page]    @impl true    def persist(%{"type" => type} = object, meta) when type in @object_types do      with {:ok, object} <- Object.create(object) do @@ -140,6 +151,9 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do          Task.start(fn -> Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity) end)        end) +      # Add local posts to search index +      if local, do: Pleroma.Search.add_to_index(activity) +        {:ok, activity}      else        %Activity{} = activity -> @@ -190,7 +204,16 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do    def notify_and_stream(activity) do      Notification.create_notifications(activity) -    conversation = create_or_bump_conversation(activity, activity.actor) +    original_activity = +      case activity do +        %{data: %{"type" => "Update"}, object: %{data: %{"id" => id}}} -> +          Activity.get_create_by_object_ap_id_with_object(id) + +        _ -> +          activity +      end + +    conversation = create_or_bump_conversation(original_activity, original_activity.actor)      participations = get_participations(conversation)      stream_out(activity)      stream_out_participations(participations) @@ -256,7 +279,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do    @impl true    def stream_out(%Activity{data: %{"type" => data_type}} = activity) -      when data_type in ["Create", "Announce", "Delete"] do +      when data_type in ["Create", "Announce", "Delete", "Update"] do      activity      |> Topics.get_activity_topics()      |> Streamer.stream(activity) @@ -290,6 +313,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do      with {:ok, activity} <- insert(create_data, local, fake),           {:fake, false, activity} <- {:fake, fake, activity},           _ <- increase_replies_count_if_reply(create_data), +         _ <- increase_quotes_count_if_quote(create_data),           {:quick_insert, false, activity} <- {:quick_insert, quick_insert?, activity},           {:ok, _actor} <- increase_note_count_if_public(actor, activity),           {:ok, _actor} <- update_last_status_at_if_public(actor, activity), @@ -392,11 +416,11 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do           _ <- notify_and_stream(activity),           :ok <-             maybe_federate(stripped_activity) do -      User.all_superusers() +      User.all_users_with_privilege(:reports_manage_reports)        |> Enum.filter(fn user -> user.ap_id != actor end)        |> Enum.filter(fn user -> not is_nil(user.email) end) -      |> Enum.each(fn superuser -> -        superuser +      |> Enum.each(fn privileged_user -> +        privileged_user          |> Pleroma.Emails.AdminEmail.report(actor, account, statuses, content)          |> Pleroma.Emails.Mailer.deliver_async()        end) @@ -413,7 +437,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do        "type" => "Move",        "actor" => origin.ap_id,        "object" => origin.ap_id, -      "target" => target.ap_id +      "target" => target.ap_id, +      "to" => [origin.follower_address]      }      with true <- origin.ap_id in target.also_known_as, @@ -445,6 +470,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do      |> maybe_preload_objects(opts)      |> maybe_preload_bookmarks(opts)      |> maybe_set_thread_muted_field(opts) +    |> restrict_unauthenticated(opts[:user])      |> restrict_blocked(opts)      |> restrict_blockers_visibility(opts)      |> restrict_recipients(recipients, opts[:user]) @@ -501,9 +527,18 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do    @spec fetch_public_or_unlisted_activities(map(), Pagination.type()) :: [Activity.t()]    def fetch_public_or_unlisted_activities(opts \\ %{}, pagination \\ :keyset) do +    includes_local_public = Map.get(opts, :includes_local_public, false) +      opts = Map.delete(opts, :user) -    [Constants.as_public()] +    intended_recipients = +      if includes_local_public do +        [Constants.as_public(), as_local_public()] +      else +        [Constants.as_public()] +      end + +    intended_recipients      |> fetch_activities_query(opts)      |> restrict_unlisted(opts)      |> fetch_paginated_optimized(opts, pagination) @@ -603,9 +638,11 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do      do: query    defp restrict_thread_visibility(query, %{user: %User{ap_id: ap_id}}, _) do +    local_public = as_local_public() +      from(        a in query, -      where: fragment("thread_visibility(?, (?)->>'id') = true", ^ap_id, a.data) +      where: fragment("thread_visibility(?, (?)->>'id', ?) = true", ^ap_id, a.data, ^local_public)      )    end @@ -692,8 +729,12 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do    defp user_activities_recipients(%{godmode: true}), do: []    defp user_activities_recipients(%{reading_user: reading_user}) do -    if reading_user do -      [Constants.as_public(), reading_user.ap_id | User.following(reading_user)] +    if not is_nil(reading_user) and reading_user.local do +      [ +        Constants.as_public(), +        as_local_public(), +        reading_user.ap_id | User.following(reading_user) +      ]      else        [Constants.as_public()]      end @@ -1134,8 +1175,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do        [activity, object: o] in query,        where:          fragment( -          "(?)->>'type' = 'Create' and coalesce((?)->'object'->>'id', (?)->>'object') = any (?)", -          activity.data, +          "(?)->>'type' = 'Create' and associated_object_id((?)) = any (?)",            activity.data,            activity.data,            ^ids @@ -1191,6 +1231,35 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do    defp restrict_filtered(query, _), do: query +  defp restrict_unauthenticated(query, nil) do +    local = Config.restrict_unauthenticated_access?(:activities, :local) +    remote = Config.restrict_unauthenticated_access?(:activities, :remote) + +    cond do +      local and remote -> +        from(activity in query, where: false) + +      local -> +        from(activity in query, where: activity.local == false) + +      remote -> +        from(activity in query, where: activity.local == true) + +      true -> +        query +    end +  end + +  defp restrict_unauthenticated(query, _), do: query + +  defp restrict_quote_url(query, %{quote_url: quote_url}) do +    from([_activity, object] in query, +      where: fragment("(?)->'quoteUrl' = ?", object.data, ^quote_url) +    ) +  end + +  defp restrict_quote_url(query, _), do: query +    defp restrict_rule(query, %{rule_id: rule_id}) do      from(        activity in query, @@ -1224,15 +1293,15 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do      end    end +  defp exclude_invisible_actors(query, %{type: "Flag"}), do: query    defp exclude_invisible_actors(query, %{invisible_actors: true}), do: query    defp exclude_invisible_actors(query, _opts) do -    invisible_ap_ids = -      User.Query.build(%{invisible: true, select: [:ap_id]}) -      |> Repo.all() -      |> Enum.map(fn %{ap_id: ap_id} -> ap_id end) - -    from([activity] in query, where: activity.actor not in ^invisible_ap_ids) +    query +    |> join(:inner, [activity], u in User, +      as: :u, +      on: activity.actor == u.ap_id and u.invisible == false +    )    end    defp exclude_id(query, %{exclude_id: id}) when is_binary(id) do @@ -1363,7 +1432,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do        |> restrict_announce_object_actor(opts)        |> restrict_filtered(opts)        |> restrict_rule(opts) -      |> Activity.restrict_deactivated_users() +      |> restrict_quote_url(opts) +      |> maybe_restrict_deactivated_users(opts)        |> exclude_poll_votes(opts)        |> exclude_chat_messages(opts)        |> exclude_invisible_actors(opts) @@ -1439,13 +1509,22 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do    @spec upload(Upload.source(), keyword()) :: {:ok, Object.t()} | {:error, any()}    def upload(file, opts \\ []) do -    with {:ok, data} <- Upload.store(file, opts) do +    with {:ok, data} <- Upload.store(sanitize_upload_file(file), opts) do        obj_data = Maps.put_if_present(data, "actor", opts[:actor])        Repo.insert(%Object{data: obj_data})      end    end +  defp sanitize_upload_file(%Plug.Upload{filename: filename} = upload) when is_binary(filename) do +    %Plug.Upload{ +      upload +      | filename: Path.basename(filename) +    } +  end + +  defp sanitize_upload_file(upload), do: upload +    @spec get_actor_url(any()) :: binary() | nil    defp get_actor_url(url) when is_binary(url), do: url    defp get_actor_url(%{"href" => href}) when is_binary(href), do: href @@ -1468,7 +1547,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do    defp normalize_image(urls) when is_list(urls), do: urls |> List.first() |> normalize_image()    defp normalize_image(_), do: nil -  defp object_to_user_data(data) do +  defp object_to_user_data(data, additional) do      fields =        data        |> Map.get("attachment", []) @@ -1500,15 +1579,11 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do      public_key =        if is_map(data["publicKey"]) && is_binary(data["publicKey"]["publicKeyPem"]) do          data["publicKey"]["publicKeyPem"] -      else -        nil        end      shared_inbox =        if is_map(data["endpoints"]) && is_binary(data["endpoints"]["sharedInbox"]) do          data["endpoints"]["sharedInbox"] -      else -        nil        end      birthday = @@ -1517,16 +1592,17 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do            {:ok, date} -> date            {:error, _} -> nil          end -      else -        nil        end      show_birthday = !!birthday -    user_data = %{ +    # if WebFinger request was already done, we probably have acct, otherwise +    # we request WebFinger here +    nickname = additional[:nickname_from_acct] || generate_nickname(data) + +    %{        ap_id: data["id"],        uri: get_actor_url(data["url"]), -      ap_enabled: true,        banner: normalize_image(data["image"]),        fields: fields,        emoji: emojis, @@ -1545,23 +1621,29 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do        inbox: data["inbox"],        shared_inbox: shared_inbox,        accepts_chat_messages: accepts_chat_messages, -      pinned_objects: pinned_objects,        birthday: birthday, -      show_birthday: show_birthday +      show_birthday: show_birthday, +      pinned_objects: pinned_objects, +      nickname: nickname      } +  end -    # nickname can be nil because of virtual actors -    if data["preferredUsername"] do -      Map.put( -        user_data, -        :nickname, -        "#{data["preferredUsername"]}@#{URI.parse(data["id"]).host}" -      ) +  defp generate_nickname(%{"preferredUsername" => username} = data) when is_binary(username) do +    generated = "#{username}@#{URI.parse(data["id"]).host}" + +    if Config.get([WebFinger, :update_nickname_on_user_fetch]) do +      case WebFinger.finger(generated) do +        {:ok, %{"subject" => "acct:" <> acct}} -> acct +        _ -> generated +      end      else -      Map.put(user_data, :nickname, nil) +      generated      end    end +  # nickname can be nil because of virtual actors +  defp generate_nickname(_), do: nil +    def fetch_follow_information_for_user(user) do      with {:ok, following_data} <-             Fetcher.fetch_and_contain_remote_object_from_id(user.following_address), @@ -1633,17 +1715,17 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do    defp collection_private(_data), do: {:ok, true} -  def user_data_from_user_object(data) do +  def user_data_from_user_object(data, additional \\ []) do      with {:ok, data} <- MRF.filter(data) do -      {:ok, object_to_user_data(data)} +      {:ok, object_to_user_data(data, additional)}      else        e -> {:error, e}      end    end -  def fetch_and_prepare_user_from_ap_id(ap_id) do +  defp fetch_and_prepare_user_from_ap_id(ap_id, additional) do      with {:ok, data} <- Fetcher.fetch_and_contain_remote_object_from_id(ap_id), -         {:ok, data} <- user_data_from_user_object(data) do +         {:ok, data} <- user_data_from_user_object(data, additional) do        {:ok, maybe_update_follow_information(data)}      else        # If this has been deleted, only log a debug and not an error @@ -1694,6 +1776,11 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do      end)    end +  def pin_data_from_featured_collection(obj) do +    Logger.error("Could not parse featured collection #{inspect(obj)}") +    %{} +  end +    def fetch_and_prepare_featured_from_ap_id(nil) do      {:ok, %{}}    end @@ -1721,34 +1808,31 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do      end    end -  def make_user_from_ap_id(ap_id) do +  def make_user_from_ap_id(ap_id, additional \\ []) do      user = User.get_cached_by_ap_id(ap_id) -    if user && !User.ap_enabled?(user) do -      Transmogrifier.upgrade_user_from_ap_id(ap_id) -    else -      with {:ok, data} <- fetch_and_prepare_user_from_ap_id(ap_id) do -        {:ok, _pid} = Task.start(fn -> pinned_fetch_task(data) end) +    with {:ok, data} <- fetch_and_prepare_user_from_ap_id(ap_id, additional) do +      {:ok, _pid} = Task.start(fn -> pinned_fetch_task(data) end) -        if user do -          user -          |> User.remote_user_changeset(data) -          |> User.update_and_set_cache() -        else -          maybe_handle_clashing_nickname(data) +      if user do +        user +        |> User.remote_user_changeset(data) +        |> User.update_and_set_cache() +      else +        maybe_handle_clashing_nickname(data) -          data -          |> User.remote_user_changeset() -          |> Repo.insert() -          |> User.set_cache() -        end +        data +        |> User.remote_user_changeset() +        |> Repo.insert() +        |> User.set_cache()        end      end    end    def make_user_from_nickname(nickname) do -    with {:ok, %{"ap_id" => ap_id}} when not is_nil(ap_id) <- WebFinger.finger(nickname) do -      make_user_from_ap_id(ap_id) +    with {:ok, %{"ap_id" => ap_id, "subject" => "acct:" <> acct}} when not is_nil(ap_id) <- +           WebFinger.finger(nickname) do +      make_user_from_ap_id(ap_id, nickname_from_acct: acct)      else        _e -> {:error, "No AP id in WebFinger"}      end @@ -1770,4 +1854,9 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do      |> restrict_visibility(%{visibility: "direct"})      |> order_by([activity], asc: activity.id)    end + +  defp maybe_restrict_deactivated_users(activity, %{type: "Flag"}), do: activity + +  defp maybe_restrict_deactivated_users(activity, _opts), +    do: Activity.restrict_deactivated_users(activity)  end diff --git a/lib/pleroma/web/activity_pub/activity_pub_controller.ex b/lib/pleroma/web/activity_pub/activity_pub_controller.ex index b8f63d69d..e38a94966 100644 --- a/lib/pleroma/web/activity_pub/activity_pub_controller.ex +++ b/lib/pleroma/web/activity_pub/activity_pub_controller.ex @@ -66,8 +66,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do    end    def user(conn, %{"nickname" => nickname}) do -    with %User{local: true} = user <- User.get_cached_by_nickname(nickname), -         {:ok, user} <- User.ensure_keys_present(user) do +    with %User{local: true} = user <- User.get_cached_by_nickname(nickname) do        conn        |> put_resp_content_type("application/activity+json")        |> put_view(UserView) @@ -174,7 +173,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do    def following(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname, "page" => page}) do      with %User{} = user <- User.get_cached_by_nickname(nickname), -         {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user),           {:show_follows, true} <-             {:show_follows, (for_user && for_user == user) || !user.hide_follows} do        {page, _} = Integer.parse(page) @@ -192,8 +190,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do    end    def following(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname}) do -    with %User{} = user <- User.get_cached_by_nickname(nickname), -         {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do +    with %User{} = user <- User.get_cached_by_nickname(nickname) do        conn        |> put_resp_content_type("application/activity+json")        |> put_view(UserView) @@ -213,7 +210,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do    def followers(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname, "page" => page}) do      with %User{} = user <- User.get_cached_by_nickname(nickname), -         {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user),           {:show_followers, true} <-             {:show_followers, (for_user && for_user == user) || !user.hide_followers} do        {page, _} = Integer.parse(page) @@ -231,8 +227,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do    end    def followers(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname}) do -    with %User{} = user <- User.get_cached_by_nickname(nickname), -         {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do +    with %User{} = user <- User.get_cached_by_nickname(nickname) do        conn        |> put_resp_content_type("application/activity+json")        |> put_view(UserView) @@ -245,8 +240,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do          %{"nickname" => nickname, "page" => page?} = params        )        when page? in [true, "true"] do -    with %User{} = user <- User.get_cached_by_nickname(nickname), -         {:ok, user} <- User.ensure_keys_present(user) do +    with %User{} = user <- User.get_cached_by_nickname(nickname) do        # "include_poll_votes" is a hack because postgres generates inefficient        # queries when filtering by 'Answer', poll votes will be hidden by the        # visibility filter in this case anyway @@ -270,8 +264,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do    end    def outbox(conn, %{"nickname" => nickname}) do -    with %User{} = user <- User.get_cached_by_nickname(nickname), -         {:ok, user} <- User.ensure_keys_present(user) do +    with %User{} = user <- User.get_cached_by_nickname(nickname) do        conn        |> put_resp_content_type("application/activity+json")        |> put_view(UserView) @@ -280,12 +273,17 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do    end    def inbox(%{assigns: %{valid_signature: true}} = conn, %{"nickname" => nickname} = params) do -    with %User{} = recipient <- User.get_cached_by_nickname(nickname), -         {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(params["actor"]), +    with %User{is_active: true} = recipient <- User.get_cached_by_nickname(nickname), +         {:ok, %User{is_active: true} = actor} <- User.get_or_fetch_by_ap_id(params["actor"]),           true <- Utils.recipient_in_message(recipient, actor, params),           params <- Utils.maybe_splice_recipient(recipient.ap_id, params) do        Federator.incoming_ap_doc(params)        json(conn, "ok") +    else +      _ -> +        conn +        |> put_status(:bad_request) +        |> json("Invalid request.")      end    end @@ -294,10 +292,9 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do      json(conn, "ok")    end -  def inbox(%{assigns: %{valid_signature: false}} = conn, _params) do -    conn -    |> put_status(:bad_request) -    |> json("Invalid HTTP Signature") +  def inbox(%{assigns: %{valid_signature: false}, req_headers: req_headers} = conn, params) do +    Federator.incoming_ap_doc(%{req_headers: req_headers, params: params}) +    json(conn, "ok")    end    # POST /relay/inbox -or- POST /internal/fetch/inbox @@ -328,14 +325,10 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do    end    defp represent_service_actor(%User{} = user, conn) do -    with {:ok, user} <- User.ensure_keys_present(user) do -      conn -      |> put_resp_content_type("application/activity+json") -      |> put_view(UserView) -      |> render("user.json", %{user: user}) -    else -      nil -> {:error, :not_found} -    end +    conn +    |> put_resp_content_type("application/activity+json") +    |> put_view(UserView) +    |> render("user.json", %{user: user})    end    defp represent_service_actor(nil, _), do: {:error, :not_found} @@ -388,12 +381,10 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do    def read_inbox(%{assigns: %{user: %User{nickname: nickname} = user}} = conn, %{          "nickname" => nickname        }) do -    with {:ok, user} <- User.ensure_keys_present(user) do -      conn -      |> put_resp_content_type("application/activity+json") -      |> put_view(UserView) -      |> render("activity_collection.json", %{iri: "#{user.ap_id}/inbox"}) -    end +    conn +    |> put_resp_content_type("application/activity+json") +    |> put_view(UserView) +    |> render("activity_collection.json", %{iri: "#{user.ap_id}/inbox"})    end    def read_inbox(%{assigns: %{user: %User{nickname: as_nickname}}} = conn, %{ @@ -489,7 +480,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do          |> json(message)        e -> -        Logger.warn(fn -> "AP C2S: #{inspect(e)}" end) +        Logger.warning(fn -> "AP C2S: #{inspect(e)}" end)          conn          |> put_status(:bad_request) @@ -530,19 +521,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do      conn    end -  defp ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do -    {:ok, new_user} = User.ensure_keys_present(user) - -    for_user = -      if new_user != user and match?(%User{}, for_user) do -        User.get_cached_by_nickname(for_user.nickname) -      else -        for_user -      end - -    {new_user, for_user} -  end -    def upload_media(%{assigns: %{user: %User{} = user}} = conn, %{"file" => file} = data) do      with {:ok, object} <-             ActivityPub.upload( diff --git a/lib/pleroma/web/activity_pub/builder.ex b/lib/pleroma/web/activity_pub/builder.ex index 5b25138a4..eb0bb0e33 100644 --- a/lib/pleroma/web/activity_pub/builder.ex +++ b/lib/pleroma/web/activity_pub/builder.ex @@ -16,6 +16,7 @@ defmodule Pleroma.Web.ActivityPub.Builder do    alias Pleroma.Web.ActivityPub.Utils    alias Pleroma.Web.ActivityPub.Visibility    alias Pleroma.Web.CommonAPI.ActivityDraft +  alias Pleroma.Web.Endpoint    require Pleroma.Constants @@ -54,13 +55,87 @@ defmodule Pleroma.Web.ActivityPub.Builder do      {:ok, data, []}    end +  defp unicode_emoji_react(_object, data, emoji) do +    data +    |> Map.put("content", emoji) +    |> Map.put("type", "EmojiReact") +  end + +  defp add_emoji_content(data, emoji, url) do +    tag = [ +      %{ +        "id" => url, +        "type" => "Emoji", +        "name" => Emoji.maybe_quote(emoji), +        "icon" => %{ +          "type" => "Image", +          "url" => url +        } +      } +    ] + +    data +    |> Map.put("content", Emoji.maybe_quote(emoji)) +    |> Map.put("type", "EmojiReact") +    |> Map.put("tag", tag) +  end + +  defp remote_custom_emoji_react( +         %{data: %{"reactions" => existing_reactions}}, +         data, +         emoji +       ) do +    [emoji_code, instance] = String.split(Emoji.maybe_strip_name(emoji), "@") + +    matching_reaction = +      Enum.find( +        existing_reactions, +        fn [name, _, url] -> +          if url != nil do +            url = URI.parse(url) +            url.host == instance && name == emoji_code +          end +        end +      ) + +    if matching_reaction do +      [name, _, url] = matching_reaction +      add_emoji_content(data, name, url) +    else +      {:error, "Could not react"} +    end +  end + +  defp remote_custom_emoji_react(_object, _data, _emoji) do +    {:error, "Could not react"} +  end + +  defp local_custom_emoji_react(data, emoji) do +    with %{file: path} = emojo <- Emoji.get(emoji) do +      url = "#{Endpoint.url()}#{path}" +      add_emoji_content(data, emojo.code, url) +    else +      _ -> {:error, "Emoji does not exist"} +    end +  end + +  defp custom_emoji_react(object, data, emoji) do +    if String.contains?(emoji, "@") do +      remote_custom_emoji_react(object, data, emoji) +    else +      local_custom_emoji_react(data, emoji) +    end +  end +    @spec emoji_react(User.t(), Object.t(), String.t()) :: {:ok, map(), keyword()}    def emoji_react(actor, object, emoji) do      with {:ok, data, meta} <- object_action(actor, object) do        data = -        data -        |> Map.put("content", emoji) -        |> Map.put("type", "EmojiReact") +        if Emoji.is_unicode_emoji?(emoji) do +          unicode_emoji_react(object, data, emoji) +        else +          custom_emoji_react(object, data, emoji) +        end        {:ok, data, meta}      end @@ -142,6 +217,7 @@ defmodule Pleroma.Web.ActivityPub.Builder do          "tag" => Keyword.values(draft.tags) |> Enum.uniq()        }        |> add_in_reply_to(draft.in_reply_to) +      |> add_quote(draft.quote_post)        |> Map.merge(draft.extra)      {:ok, data, []} @@ -157,6 +233,16 @@ defmodule Pleroma.Web.ActivityPub.Builder do      end    end +  defp add_quote(object, nil), do: object + +  defp add_quote(object, quote_post) do +    with %Object{} = quote_object <- Object.normalize(quote_post, fetch: false) do +      Map.put(object, "quoteUrl", quote_object.data["id"]) +    else +      _ -> object +    end +  end +    def chat_message(actor, recipient, content, opts \\ []) do      basic = %{        "id" => Utils.generate_object_id(), @@ -218,10 +304,16 @@ defmodule Pleroma.Web.ActivityPub.Builder do      end    end -  # Retricted to user updates for now, always public    @spec update(User.t(), Object.t()) :: {:ok, map(), keyword()}    def update(actor, object) do -    to = [Pleroma.Constants.as_public(), actor.follower_address] +    {to, cc} = +      if object["type"] in Pleroma.Constants.actor_types() do +        # User updates, always public +        {[Pleroma.Constants.as_public(), actor.follower_address], []} +      else +        # Status updates, follow the recipients in the object +        {object["to"] || [], object["cc"] || []} +      end      {:ok,       %{ @@ -229,7 +321,8 @@ defmodule Pleroma.Web.ActivityPub.Builder do         "type" => "Update",         "actor" => actor.ap_id,         "object" => object, -       "to" => to +       "to" => to, +       "cc" => cc       }, []}    end diff --git a/lib/pleroma/web/activity_pub/mrf.ex b/lib/pleroma/web/activity_pub/mrf.ex index 323ecdbf1..7f6dce925 100644 --- a/lib/pleroma/web/activity_pub/mrf.ex +++ b/lib/pleroma/web/activity_pub/mrf.ex @@ -53,10 +53,55 @@ defmodule Pleroma.Web.ActivityPub.MRF do    @required_description_keys [:key, :related_policy] +  def filter_one(policy, message) do +    Code.ensure_loaded(policy) + +    should_plug_history? = +      if function_exported?(policy, :history_awareness, 0) do +        policy.history_awareness() +      else +        :manual +      end +      |> Kernel.==(:auto) + +    if not should_plug_history? do +      policy.filter(message) +    else +      main_result = policy.filter(message) + +      with {_, {:ok, main_message}} <- {:main, main_result}, +           {_, +            %{ +              "formerRepresentations" => %{ +                "orderedItems" => [_ | _] +              } +            }} = {_, object} <- {:object, message["object"]}, +           {_, {:ok, new_history}} <- +             {:history, +              Pleroma.Object.Updater.for_each_history_item( +                object["formerRepresentations"], +                object, +                fn item -> +                  with {:ok, filtered} <- policy.filter(Map.put(message, "object", item)) do +                    {:ok, filtered["object"]} +                  else +                    e -> e +                  end +                end +              )} do +        {:ok, put_in(main_message, ["object", "formerRepresentations"], new_history)} +      else +        {:main, _} -> main_result +        {:object, _} -> main_result +        {:history, e} -> e +      end +    end +  end +    def filter(policies, %{} = message) do      policies      |> Enum.reduce({:ok, message}, fn -      policy, {:ok, message} -> policy.filter(message) +      policy, {:ok, message} -> filter_one(policy, message)        _, error -> error      end)    end @@ -145,6 +190,8 @@ defmodule Pleroma.Web.ActivityPub.MRF do    def config_descriptions(policies) do      Enum.reduce(policies, @mrf_config_descriptions, fn policy, acc -> +      Code.ensure_loaded(policy) +        if function_exported?(policy, :config_description, 0) do          description =            @default_description @@ -156,7 +203,7 @@ defmodule Pleroma.Web.ActivityPub.MRF do          if Enum.all?(@required_description_keys, &Map.has_key?(description, &1)) do            [description | acc]          else -          Logger.warn( +          Logger.warning(              "#{policy} config description doesn't have one or all required keys #{inspect(@required_description_keys)}"            ) diff --git a/lib/pleroma/web/activity_pub/mrf/anti_link_spam_policy.ex b/lib/pleroma/web/activity_pub/mrf/anti_link_spam_policy.ex index f0504ead4..3ec9c52ee 100644 --- a/lib/pleroma/web/activity_pub/mrf/anti_link_spam_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/anti_link_spam_policy.ex @@ -9,6 +9,9 @@ defmodule Pleroma.Web.ActivityPub.MRF.AntiLinkSpamPolicy do    require Logger +  @impl true +  def history_awareness, do: :auto +    # has the user successfully posted before?    defp old_user?(%User{} = u) do      u.note_count > 0 || u.follower_count > 0 diff --git a/lib/pleroma/web/activity_pub/mrf/emoji_policy.ex b/lib/pleroma/web/activity_pub/mrf/emoji_policy.ex new file mode 100644 index 000000000..f884962b9 --- /dev/null +++ b/lib/pleroma/web/activity_pub/mrf/emoji_policy.ex @@ -0,0 +1,281 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2023 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.MRF.EmojiPolicy do +  require Pleroma.Constants + +  alias Pleroma.Object.Updater +  alias Pleroma.Web.ActivityPub.MRF.Utils + +  @moduledoc "Reject or force-unlisted emojis with certain URLs or names" + +  @behaviour Pleroma.Web.ActivityPub.MRF.Policy + +  defp config_remove_url do +    Pleroma.Config.get([:mrf_emoji, :remove_url], []) +  end + +  defp config_remove_shortcode do +    Pleroma.Config.get([:mrf_emoji, :remove_shortcode], []) +  end + +  defp config_unlist_url do +    Pleroma.Config.get([:mrf_emoji, :federated_timeline_removal_url], []) +  end + +  defp config_unlist_shortcode do +    Pleroma.Config.get([:mrf_emoji, :federated_timeline_removal_shortcode], []) +  end + +  @impl Pleroma.Web.ActivityPub.MRF.Policy +  def history_awareness, do: :manual + +  @impl Pleroma.Web.ActivityPub.MRF.Policy +  def filter(%{"type" => type, "object" => %{"type" => objtype} = object} = message) +      when type in ["Create", "Update"] and objtype in Pleroma.Constants.status_object_types() do +    with {:ok, object} <- +           Updater.do_with_history(object, fn object -> +             {:ok, process_remove(object, :url, config_remove_url())} +           end), +         {:ok, object} <- +           Updater.do_with_history(object, fn object -> +             {:ok, process_remove(object, :shortcode, config_remove_shortcode())} +           end), +         activity <- Map.put(message, "object", object), +         activity <- maybe_delist(activity) do +      {:ok, activity} +    end +  end + +  @impl Pleroma.Web.ActivityPub.MRF.Policy +  def filter(%{"type" => type} = object) when type in Pleroma.Constants.actor_types() do +    with object <- process_remove(object, :url, config_remove_url()), +         object <- process_remove(object, :shortcode, config_remove_shortcode()) do +      {:ok, object} +    end +  end + +  @impl Pleroma.Web.ActivityPub.MRF.Policy +  def filter(%{"type" => "EmojiReact"} = object) do +    with {:ok, _} <- +           matched_emoji_checker(config_remove_url(), config_remove_shortcode()).(object) do +      {:ok, object} +    else +      _ -> +        {:reject, "[EmojiPolicy] Rejected for having disallowed emoji"} +    end +  end + +  @impl Pleroma.Web.ActivityPub.MRF.Policy +  def filter(message) do +    {:ok, message} +  end + +  defp match_string?(string, pattern) when is_binary(pattern) do +    string == pattern +  end + +  defp match_string?(string, %Regex{} = pattern) do +    String.match?(string, pattern) +  end + +  defp match_any?(string, patterns) do +    Enum.any?(patterns, &match_string?(string, &1)) +  end + +  defp url_from_tag(%{"icon" => %{"url" => url}}), do: url +  defp url_from_tag(_), do: nil + +  defp url_from_emoji({_name, url}), do: url + +  defp shortcode_from_tag(%{"name" => name}) when is_binary(name), do: String.trim(name, ":") +  defp shortcode_from_tag(_), do: nil + +  defp shortcode_from_emoji({name, _url}), do: name + +  defp process_remove(object, :url, patterns) do +    process_remove_impl(object, &url_from_tag/1, &url_from_emoji/1, patterns) +  end + +  defp process_remove(object, :shortcode, patterns) do +    process_remove_impl(object, &shortcode_from_tag/1, &shortcode_from_emoji/1, patterns) +  end + +  defp process_remove_impl(object, extract_from_tag, extract_from_emoji, patterns) do +    object = +      if object["tag"] do +        Map.put( +          object, +          "tag", +          Enum.filter( +            object["tag"], +            fn +              %{"type" => "Emoji"} = tag -> +                str = extract_from_tag.(tag) + +                if is_binary(str) do +                  not match_any?(str, patterns) +                else +                  true +                end + +              _ -> +                true +            end +          ) +        ) +      else +        object +      end + +    object = +      if object["emoji"] do +        Map.put( +          object, +          "emoji", +          object["emoji"] +          |> Enum.reduce(%{}, fn {name, url} = emoji, acc -> +            if not match_any?(extract_from_emoji.(emoji), patterns) do +              Map.put(acc, name, url) +            else +              acc +            end +          end) +        ) +      else +        object +      end + +    object +  end + +  defp matched_emoji_checker(urls, shortcodes) do +    fn object -> +      if any_emoji_match?(object, &url_from_tag/1, &url_from_emoji/1, urls) or +           any_emoji_match?( +             object, +             &shortcode_from_tag/1, +             &shortcode_from_emoji/1, +             shortcodes +           ) do +        {:matched, nil} +      else +        {:ok, %{}} +      end +    end +  end + +  defp maybe_delist(%{"object" => object, "to" => to, "type" => "Create"} = activity) do +    check = matched_emoji_checker(config_unlist_url(), config_unlist_shortcode()) + +    should_delist? = fn object -> +      with {:ok, _} <- Pleroma.Object.Updater.do_with_history(object, check) do +        false +      else +        _ -> true +      end +    end + +    if Pleroma.Constants.as_public() in to and should_delist?.(object) do +      to = List.delete(to, Pleroma.Constants.as_public()) +      cc = [Pleroma.Constants.as_public() | activity["cc"] || []] + +      activity +      |> Map.put("to", to) +      |> Map.put("cc", cc) +    else +      activity +    end +  end + +  defp maybe_delist(activity), do: activity + +  defp any_emoji_match?(object, extract_from_tag, extract_from_emoji, patterns) do +    Kernel.||( +      Enum.any?( +        object["tag"] || [], +        fn +          %{"type" => "Emoji"} = tag -> +            str = extract_from_tag.(tag) + +            if is_binary(str) do +              match_any?(str, patterns) +            else +              false +            end + +          _ -> +            false +        end +      ), +      (object["emoji"] || []) +      |> Enum.any?(fn emoji -> match_any?(extract_from_emoji.(emoji), patterns) end) +    ) +  end + +  @impl Pleroma.Web.ActivityPub.MRF.Policy +  def describe do +    mrf_emoji = +      Pleroma.Config.get(:mrf_emoji, []) +      |> Enum.map(fn {key, value} -> +        {key, Enum.map(value, &Utils.describe_regex_or_string/1)} +      end) +      |> Enum.into(%{}) + +    {:ok, %{mrf_emoji: mrf_emoji}} +  end + +  @impl Pleroma.Web.ActivityPub.MRF.Policy +  def config_description do +    %{ +      key: :mrf_emoji, +      related_policy: "Pleroma.Web.ActivityPub.MRF.EmojiPolicy", +      label: "MRF Emoji", +      description: +        "Reject or force-unlisted emojis whose URLs or names match a keyword or [Regex](https://hexdocs.pm/elixir/Regex.html).", +      children: [ +        %{ +          key: :remove_url, +          type: {:list, :string}, +          description: """ +            A list of patterns which result in emoji whose URL matches being removed from the message. This will apply to statuses, emoji reactions, and user profiles. + +            Each pattern can be a string or [Regex](https://hexdocs.pm/elixir/Regex.html) in the format of `~r/PATTERN/`. +          """, +          suggestions: ["https://example.org/foo.png", ~r/example.org\/foo/iu] +        }, +        %{ +          key: :remove_shortcode, +          type: {:list, :string}, +          description: """ +            A list of patterns which result in emoji whose shortcode matches being removed from the message. This will apply to statuses, emoji reactions, and user profiles. + +            Each pattern can be a string or [Regex](https://hexdocs.pm/elixir/Regex.html) in the format of `~r/PATTERN/`. +          """, +          suggestions: ["foo", ~r/foo/iu] +        }, +        %{ +          key: :federated_timeline_removal_url, +          type: {:list, :string}, +          description: """ +            A list of patterns which result in message with emojis whose URLs match being removed from federated timelines (a.k.a unlisted). This will apply only to statuses. + +            Each pattern can be a string or [Regex](https://hexdocs.pm/elixir/Regex.html) in the format of `~r/PATTERN/`. +          """, +          suggestions: ["https://example.org/foo.png", ~r/example.org\/foo/iu] +        }, +        %{ +          key: :federated_timeline_removal_shortcode, +          type: {:list, :string}, +          description: """ +            A list of patterns which result in message with emojis whose shortcodes match being removed from federated timelines (a.k.a unlisted). This will apply only to statuses. + +            Each pattern can be a string or [Regex](https://hexdocs.pm/elixir/Regex.html) in the format of `~r/PATTERN/`. +          """, +          suggestions: ["foo", ~r/foo/iu] +        } +      ] +    } +  end +end diff --git a/lib/pleroma/web/activity_pub/mrf/ensure_re_prepended.ex b/lib/pleroma/web/activity_pub/mrf/ensure_re_prepended.ex index 51596c09f..a148cc1e7 100644 --- a/lib/pleroma/web/activity_pub/mrf/ensure_re_prepended.ex +++ b/lib/pleroma/web/activity_pub/mrf/ensure_re_prepended.ex @@ -10,6 +10,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.EnsureRePrepended do    @reply_prefix Regex.compile!("^re:[[:space:]]*", [:caseless]) +  def history_awareness, do: :auto +    def filter_by_summary(          %{data: %{"summary" => parent_summary}} = _in_reply_to,          %{"summary" => child_summary} = child @@ -27,8 +29,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.EnsureRePrepended do    def filter_by_summary(_in_reply_to, child), do: child -  def filter(%{"type" => "Create", "object" => child_object} = object) -      when is_map(child_object) do +  def filter(%{"type" => type, "object" => child_object} = object) +      when type in ["Create", "Update"] and is_map(child_object) do      child =        child_object["inReplyTo"]        |> Object.normalize(fetch: false) diff --git a/lib/pleroma/web/activity_pub/mrf/follow_bot_policy.ex b/lib/pleroma/web/activity_pub/mrf/follow_bot_policy.ex index 5b6adbb4b..5a4a97626 100644 --- a/lib/pleroma/web/activity_pub/mrf/follow_bot_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/follow_bot_policy.ex @@ -19,7 +19,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.FollowBotPolicy do        try_follow(follower, message)      else        nil -> -        Logger.warn( +        Logger.warning(            "#{__MODULE__} skipped because of missing `:mrf_follow_bot, :follower_nickname` configuration, the :follower_nickname              account does not exist, or the account is not correctly configured as a bot."          ) diff --git a/lib/pleroma/web/activity_pub/mrf/force_mentions_in_content.ex b/lib/pleroma/web/activity_pub/mrf/force_mentions_in_content.ex index 255910b2f..5532093cb 100644 --- a/lib/pleroma/web/activity_pub/mrf/force_mentions_in_content.ex +++ b/lib/pleroma/web/activity_pub/mrf/force_mentions_in_content.ex @@ -1,5 +1,5 @@  # Pleroma: A lightweight social networking server -# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/> +# Copyright © 2017-2023 Pleroma Authors <https://pleroma.social/>  # SPDX-License-Identifier: AGPL-3.0-only  defmodule Pleroma.Web.ActivityPub.MRF.ForceMentionsInContent do @@ -11,6 +11,9 @@ defmodule Pleroma.Web.ActivityPub.MRF.ForceMentionsInContent do    @behaviour Pleroma.Web.ActivityPub.MRF.Policy +  @impl true +  def history_awareness, do: :auto +    defp do_extract({:a, attrs, _}, acc) do      if Enum.find(attrs, fn {name, value} ->           name == "class" && value in ["mention", "u-url mention", "mention u-url"] @@ -74,11 +77,11 @@ defmodule Pleroma.Web.ActivityPub.MRF.ForceMentionsInContent do    @impl true    def filter(          %{ -          "type" => "Create", +          "type" => type,            "object" => %{"type" => "Note", "to" => to, "inReplyTo" => in_reply_to}          } = object        ) -      when is_list(to) and is_binary(in_reply_to) do +      when type in ["Create", "Update"] and is_list(to) and is_binary(in_reply_to) do      # image-only posts from pleroma apparently reach this MRF without the content field      content = object["object"]["content"] || "" @@ -92,11 +95,13 @@ defmodule Pleroma.Web.ActivityPub.MRF.ForceMentionsInContent do        |> Enum.reject(&is_nil/1)        |> sort_replied_user(replied_to_user) -    explicitly_mentioned_uris = extract_mention_uris_from_content(content) +    explicitly_mentioned_uris = +      extract_mention_uris_from_content(content) +      |> MapSet.new()      added_mentions = -      Enum.reduce(mention_users, "", fn %User{ap_id: uri} = user, acc -> -        unless uri in explicitly_mentioned_uris do +      Enum.reduce(mention_users, "", fn %User{ap_id: ap_id, uri: uri} = user, acc -> +        if MapSet.disjoint?(MapSet.new([ap_id, uri]), explicitly_mentioned_uris) do            acc <> Formatter.mention_from_user(user, %{mentions_format: :compact}) <> " "          else            acc diff --git a/lib/pleroma/web/activity_pub/mrf/hashtag_policy.ex b/lib/pleroma/web/activity_pub/mrf/hashtag_policy.ex index 2142b7add..b73fd974c 100644 --- a/lib/pleroma/web/activity_pub/mrf/hashtag_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/hashtag_policy.ex @@ -16,6 +16,9 @@ defmodule Pleroma.Web.ActivityPub.MRF.HashtagPolicy do    @behaviour Pleroma.Web.ActivityPub.MRF.Policy +  @impl true +  def history_awareness, do: :manual +    defp check_reject(message, hashtags) do      if Enum.any?(Config.get([:mrf_hashtag, :reject]), fn match -> match in hashtags end) do        {:reject, "[HashtagPolicy] Matches with rejected keyword"} @@ -47,22 +50,46 @@ defmodule Pleroma.Web.ActivityPub.MRF.HashtagPolicy do    defp check_ftl_removal(message, _hashtags), do: {:ok, message} -  defp check_sensitive(message, hashtags) do -    if Enum.any?(Config.get([:mrf_hashtag, :sensitive]), fn match -> match in hashtags end) do -      {:ok, Kernel.put_in(message, ["object", "sensitive"], true)} -    else -      {:ok, message} -    end +  defp check_sensitive(message) do +    {:ok, new_object} = +      Object.Updater.do_with_history(message["object"], fn object -> +        hashtags = Object.hashtags(%Object{data: object}) + +        if Enum.any?(Config.get([:mrf_hashtag, :sensitive]), fn match -> match in hashtags end) do +          {:ok, Map.put(object, "sensitive", true)} +        else +          {:ok, object} +        end +      end) + +    {:ok, Map.put(message, "object", new_object)}    end    @impl true -  def filter(%{"type" => "Create", "object" => object} = message) do -    hashtags = Object.hashtags(%Object{data: object}) +  def filter(%{"type" => type, "object" => object} = message) when type in ["Create", "Update"] do +    history_items = +      with %{"formerRepresentations" => %{"orderedItems" => items}} <- object do +        items +      else +        _ -> [] +      end + +    historical_hashtags = +      Enum.reduce(history_items, [], fn item, acc -> +        acc ++ Object.hashtags(%Object{data: item}) +      end) + +    hashtags = Object.hashtags(%Object{data: object}) ++ historical_hashtags      if hashtags != [] do        with {:ok, message} <- check_reject(message, hashtags), -           {:ok, message} <- check_ftl_removal(message, hashtags), -           {:ok, message} <- check_sensitive(message, hashtags) do +           {:ok, message} <- +             (if "type" == "Create" do +                check_ftl_removal(message, hashtags) +              else +                {:ok, message} +              end), +           {:ok, message} <- check_sensitive(message) do          {:ok, message}        end      else 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..171b22c5e --- /dev/null +++ b/lib/pleroma/web/activity_pub/mrf/inline_quote_policy.ex @@ -0,0 +1,78 @@ +# 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(template, url) do +    quote_line = String.replace(template, "{url}", "<a href=\"#{url}\">#{url}</a>") + +    "<span class=\"quote-inline\"><br/><br/>#{quote_line}</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 +      template = Pleroma.Config.get([:mrf_inline_quote, :template]) + +      content = +        if String.ends_with?(content, "</p>"), +          do: +            String.trim_trailing(content, "</p>") <> +              build_inline_quote(template, quote_url) <> "</p>", +          else: content <> build_inline_quote(template, 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 Pleroma.Web.ActivityPub.MRF.Policy +  def history_awareness, do: :auto + +  @impl true +  def config_description do +    %{ +      key: :mrf_inline_quote, +      related_policy: "Pleroma.Web.ActivityPub.MRF.InlineQuotePolicy", +      label: "MRF Inline Quote Policy", +      type: :group, +      description: "Force quote url to appear in post content.", +      children: [ +        %{ +          key: :template, +          type: :string, +          description: +            "The template to append to the post. `{url}` will be replaced with the actual link to the quoted post.", +          suggestions: ["<bdi>RT:</bdi> {url}"] +        } +      ] +    } +  end +end diff --git a/lib/pleroma/web/activity_pub/mrf/keyword_policy.ex b/lib/pleroma/web/activity_pub/mrf/keyword_policy.ex index 00b64744f..874fe9ab9 100644 --- a/lib/pleroma/web/activity_pub/mrf/keyword_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/keyword_policy.ex @@ -5,6 +5,8 @@  defmodule Pleroma.Web.ActivityPub.MRF.KeywordPolicy do    require Pleroma.Constants +  alias Pleroma.Web.ActivityPub.MRF.Utils +    @moduledoc "Reject or Word-Replace messages with a keyword or regex"    @behaviour Pleroma.Web.ActivityPub.MRF.Policy @@ -27,24 +29,46 @@ defmodule Pleroma.Web.ActivityPub.MRF.KeywordPolicy do    end    defp check_reject(%{"object" => %{} = object} = message) do -    payload = object_payload(object) - -    if Enum.any?(Pleroma.Config.get([:mrf_keyword, :reject]), fn pattern -> -         string_matches?(payload, pattern) -       end) do -      {:reject, "[KeywordPolicy] Matches with rejected keyword"} -    else +    with {:ok, _new_object} <- +           Pleroma.Object.Updater.do_with_history(object, fn object -> +             payload = object_payload(object) + +             if Enum.any?(Pleroma.Config.get([:mrf_keyword, :reject]), fn pattern -> +                  string_matches?(payload, pattern) +                end) do +               {:reject, "[KeywordPolicy] Matches with rejected keyword"} +             else +               {:ok, message} +             end +           end) do        {:ok, message} +    else +      e -> e      end    end -  defp check_ftl_removal(%{"to" => to, "object" => %{} = object} = message) do -    payload = object_payload(object) +  defp check_ftl_removal(%{"type" => "Create", "to" => to, "object" => %{} = object} = message) do +    check_keyword = fn object -> +      payload = object_payload(object) -    if Pleroma.Constants.as_public() in to and -         Enum.any?(Pleroma.Config.get([:mrf_keyword, :federated_timeline_removal]), fn pattern -> +      if Enum.any?(Pleroma.Config.get([:mrf_keyword, :federated_timeline_removal]), fn pattern ->             string_matches?(payload, pattern)           end) do +        {:should_delist, nil} +      else +        {:ok, %{}} +      end +    end + +    should_delist? = fn object -> +      with {:ok, _} <- Pleroma.Object.Updater.do_with_history(object, check_keyword) do +        false +      else +        _ -> true +      end +    end + +    if Pleroma.Constants.as_public() in to and should_delist?.(object) do        to = List.delete(to, Pleroma.Constants.as_public())        cc = [Pleroma.Constants.as_public() | message["cc"] || []] @@ -59,8 +83,12 @@ defmodule Pleroma.Web.ActivityPub.MRF.KeywordPolicy do      end    end +  defp check_ftl_removal(message) do +    {:ok, message} +  end +    defp check_replace(%{"object" => %{} = object} = message) do -    object = +    replace_kw = fn object ->        ["content", "name", "summary"]        |> Enum.filter(fn field -> Map.has_key?(object, field) && object[field] end)        |> Enum.reduce(object, fn field, object -> @@ -73,6 +101,10 @@ defmodule Pleroma.Web.ActivityPub.MRF.KeywordPolicy do          Map.put(object, field, data)        end) +      |> (fn object -> {:ok, object} end).() +    end + +    {:ok, object} = Pleroma.Object.Updater.do_with_history(object, replace_kw)      message = Map.put(message, "object", object) @@ -80,7 +112,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.KeywordPolicy do    end    @impl true -  def filter(%{"type" => "Create", "object" => %{"content" => _content}} = message) do +  def filter(%{"type" => type, "object" => %{"content" => _content}} = message) +      when type in ["Create", "Update"] do      with {:ok, message} <- check_reject(message),           {:ok, message} <- check_ftl_removal(message),           {:ok, message} <- check_replace(message) do @@ -97,7 +130,6 @@ defmodule Pleroma.Web.ActivityPub.MRF.KeywordPolicy do    @impl true    def describe do -    # This horror is needed to convert regex sigils to strings      mrf_keyword =        Pleroma.Config.get(:mrf_keyword, [])        |> Enum.map(fn {key, value} -> @@ -105,21 +137,12 @@ defmodule Pleroma.Web.ActivityPub.MRF.KeywordPolicy do           Enum.map(value, fn             {pattern, replacement} ->               %{ -               "pattern" => -                 if not is_binary(pattern) do -                   inspect(pattern) -                 else -                   pattern -                 end, +               "pattern" => Utils.describe_regex_or_string(pattern),                 "replacement" => replacement               }             pattern -> -             if not is_binary(pattern) do -               inspect(pattern) -             else -               pattern -             end +             Utils.describe_regex_or_string(pattern)           end)}        end)        |> Enum.into(%{}) diff --git a/lib/pleroma/web/activity_pub/mrf/media_proxy_warming_policy.ex b/lib/pleroma/web/activity_pub/mrf/media_proxy_warming_policy.ex index 0eac8f021..c95d35bb9 100644 --- a/lib/pleroma/web/activity_pub/mrf/media_proxy_warming_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/media_proxy_warming_policy.ex @@ -16,6 +16,9 @@ defmodule Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy do      recv_timeout: 10_000    ] +  @impl true +  def history_awareness, do: :auto +    defp prefetch(url) do      # Fetching only proxiable resources      if MediaProxy.enabled?() and MediaProxy.url_proxiable?(url) do @@ -54,10 +57,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy do    end    @impl true -  def filter( -        %{"type" => "Create", "object" => %{"attachment" => attachments} = _object} = message -      ) -      when is_list(attachments) and length(attachments) > 0 do +  def filter(%{"type" => type, "object" => %{"attachment" => attachments} = _object} = message) +      when type in ["Create", "Update"] and is_list(attachments) and length(attachments) > 0 do      preload(message)      {:ok, message} diff --git a/lib/pleroma/web/activity_pub/mrf/no_empty_policy.ex b/lib/pleroma/web/activity_pub/mrf/no_empty_policy.ex index 4dc96e068..855cda3b9 100644 --- a/lib/pleroma/web/activity_pub/mrf/no_empty_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/no_empty_policy.ex @@ -11,6 +11,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.NoEmptyPolicy do    @impl true    def filter(%{"actor" => actor} = object) do      with true <- is_local?(actor), +         true <- is_eligible_type?(object),           true <- is_note?(object),           false <- has_attachment?(object),           true <- only_mentions?(object) do @@ -32,7 +33,6 @@ defmodule Pleroma.Web.ActivityPub.MRF.NoEmptyPolicy do    end    defp has_attachment?(%{ -         "type" => "Create",           "object" => %{"type" => "Note", "attachment" => attachments}         })         when length(attachments) > 0, @@ -40,7 +40,13 @@ defmodule Pleroma.Web.ActivityPub.MRF.NoEmptyPolicy do    defp has_attachment?(_), do: false -  defp only_mentions?(%{"type" => "Create", "object" => %{"type" => "Note", "source" => source}}) do +  defp only_mentions?(%{"object" => %{"type" => "Note", "source" => source}}) do +    source = +      case source do +        %{"content" => text} -> text +        _ -> source +      end +      non_mentions =        source |> String.split() |> Enum.filter(&(not String.starts_with?(&1, "@"))) |> length @@ -53,9 +59,12 @@ defmodule Pleroma.Web.ActivityPub.MRF.NoEmptyPolicy do    defp only_mentions?(_), do: false -  defp is_note?(%{"type" => "Create", "object" => %{"type" => "Note"}}), do: true +  defp is_note?(%{"object" => %{"type" => "Note"}}), do: true    defp is_note?(_), do: false +  defp is_eligible_type?(%{"type" => type}) when type in ["Create", "Update"], do: true +  defp is_eligible_type?(_), do: false +    @impl true    def describe, do: {:ok, %{}}  end diff --git a/lib/pleroma/web/activity_pub/mrf/no_placeholder_text_policy.ex b/lib/pleroma/web/activity_pub/mrf/no_placeholder_text_policy.ex index aab647d8e..f81e9e52a 100644 --- a/lib/pleroma/web/activity_pub/mrf/no_placeholder_text_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/no_placeholder_text_policy.ex @@ -7,13 +7,16 @@ defmodule Pleroma.Web.ActivityPub.MRF.NoPlaceholderTextPolicy do    @behaviour Pleroma.Web.ActivityPub.MRF.Policy    @impl true +  def history_awareness, do: :auto + +  @impl true    def filter(          %{ -          "type" => "Create", +          "type" => type,            "object" => %{"content" => content, "attachment" => _} = _child_object          } = object        ) -      when content in [".", "<p>.</p>"] do +      when type in ["Create", "Update"] and content in [".", "<p>.</p>"] do      {:ok, put_in(object, ["object", "content"], "")}    end diff --git a/lib/pleroma/web/activity_pub/mrf/normalize_markup.ex b/lib/pleroma/web/activity_pub/mrf/normalize_markup.ex index dc2c19d49..2dfc9a901 100644 --- a/lib/pleroma/web/activity_pub/mrf/normalize_markup.ex +++ b/lib/pleroma/web/activity_pub/mrf/normalize_markup.ex @@ -9,7 +9,11 @@ defmodule Pleroma.Web.ActivityPub.MRF.NormalizeMarkup do    @behaviour Pleroma.Web.ActivityPub.MRF.Policy    @impl true -  def filter(%{"type" => "Create", "object" => child_object} = object) do +  def history_awareness, do: :auto + +  @impl true +  def filter(%{"type" => type, "object" => child_object} = object) +      when type in ["Create", "Update"] do      scrub_policy = Pleroma.Config.get([:mrf_normalize_markup, :scrub_policy])      content = diff --git a/lib/pleroma/web/activity_pub/mrf/object_age_policy.ex b/lib/pleroma/web/activity_pub/mrf/object_age_policy.ex index 0e9d25a0a..df1a6dcbb 100644 --- a/lib/pleroma/web/activity_pub/mrf/object_age_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/object_age_policy.ex @@ -131,7 +131,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.ObjectAgePolicy do            type: {:list, :atom},            description:              "A list of actions to apply to the post. `:delist` removes the post from public timelines; " <> -              "`:strip_followers` removes followers from the ActivityPub recipient list ensuring they won't be delivered to home timelines; " <> +              "`:strip_followers` removes followers from the ActivityPub recipient list ensuring they won't be delivered to home timelines, additionally for followers-only it degrades to a direct message; " <>                "`:reject` rejects the message entirely",            suggestions: [:delist, :strip_followers, :reject]          } diff --git a/lib/pleroma/web/activity_pub/mrf/policy.ex b/lib/pleroma/web/activity_pub/mrf/policy.ex index 0ac250c3d..0234de4d5 100644 --- a/lib/pleroma/web/activity_pub/mrf/policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/policy.ex @@ -12,5 +12,6 @@ defmodule Pleroma.Web.ActivityPub.MRF.Policy do                label: String.t(),                description: String.t()              } -  @optional_callbacks config_description: 0 +  @callback history_awareness() :: :auto | :manual +  @optional_callbacks config_description: 0, history_awareness: 0  end diff --git a/lib/pleroma/web/activity_pub/mrf/quote_to_link_tag_policy.ex b/lib/pleroma/web/activity_pub/mrf/quote_to_link_tag_policy.ex new file mode 100644 index 000000000..f1c573d1b --- /dev/null +++ b/lib/pleroma/web/activity_pub/mrf/quote_to_link_tag_policy.ex @@ -0,0 +1,49 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2023 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.MRF.QuoteToLinkTagPolicy do +  @moduledoc "Force a Link tag for posts quoting another post. (may break outgoing federation of quote posts with older Pleroma versions)" +  @behaviour Pleroma.Web.ActivityPub.MRF.Policy + +  alias Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes + +  require Pleroma.Constants + +  @impl Pleroma.Web.ActivityPub.MRF.Policy +  def filter(%{"object" => %{"quoteUrl" => _} = object} = activity) do +    {:ok, Map.put(activity, "object", filter_object(object))} +  end + +  @impl Pleroma.Web.ActivityPub.MRF.Policy +  def filter(object), do: {:ok, object} + +  @impl Pleroma.Web.ActivityPub.MRF.Policy +  def describe, do: {:ok, %{}} + +  @impl Pleroma.Web.ActivityPub.MRF.Policy +  def history_awareness, do: :auto + +  defp filter_object(%{"quoteUrl" => quote_url} = object) do +    tags = object["tag"] || [] + +    if Enum.any?(tags, fn tag -> +         CommonFixes.is_object_link_tag(tag) and tag["href"] == quote_url +       end) do +      object +    else +      object +      |> Map.put( +        "tag", +        tags ++ +          [ +            %{ +              "type" => "Link", +              "mediaType" => Pleroma.Constants.activity_json_canonical_mime_type(), +              "href" => quote_url +            } +          ] +      ) +    end +  end +end diff --git a/lib/pleroma/web/activity_pub/mrf/simple_policy.ex b/lib/pleroma/web/activity_pub/mrf/simple_policy.ex index c0c7f3806..829ddeaea 100644 --- a/lib/pleroma/web/activity_pub/mrf/simple_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/simple_policy.ex @@ -40,9 +40,9 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do    defp check_media_removal(           %{host: actor_host} = _actor_info, -         %{"type" => "Create", "object" => %{"attachment" => child_attachment}} = object +         %{"type" => type, "object" => %{"attachment" => child_attachment}} = object         ) -       when length(child_attachment) > 0 do +       when length(child_attachment) > 0 and type in ["Create", "Update"] do      media_removal =        instance_list(:media_removal)        |> MRF.subdomains_regex() @@ -63,10 +63,11 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do    defp check_media_nsfw(           %{host: actor_host} = _actor_info,           %{ -           "type" => "Create", +           "type" => type,             "object" => %{} = _child_object           } = object -       ) do +       ) +       when type in ["Create", "Update"] do      media_nsfw =        instance_list(:media_nsfw)        |> MRF.subdomains_regex() diff --git a/lib/pleroma/web/activity_pub/mrf/steal_emoji_policy.ex b/lib/pleroma/web/activity_pub/mrf/steal_emoji_policy.ex index 06305235e..28c2cf3b3 100644 --- a/lib/pleroma/web/activity_pub/mrf/steal_emoji_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/steal_emoji_policy.ex @@ -12,6 +12,14 @@ defmodule Pleroma.Web.ActivityPub.MRF.StealEmojiPolicy do    defp accept_host?(host), do: host in Config.get([:mrf_steal_emoji, :hosts], []) +  defp shortcode_matches?(shortcode, pattern) when is_binary(pattern) do +    shortcode == pattern +  end + +  defp shortcode_matches?(shortcode, pattern) do +    String.match?(shortcode, pattern) +  end +    defp steal_emoji({shortcode, url}, emoji_dir_path) do      url = Pleroma.Web.MediaProxy.url(url) @@ -33,7 +41,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.StealEmojiPolicy do              shortcode            e -> -            Logger.warn("MRF.StealEmojiPolicy: Failed to write to #{file_path}: #{inspect(e)}") +            Logger.warning("MRF.StealEmojiPolicy: Failed to write to #{file_path}: #{inspect(e)}")              nil          end        else @@ -45,7 +53,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.StealEmojiPolicy do        end      else        e -> -        Logger.warn("MRF.StealEmojiPolicy: Failed to fetch #{url}: #{inspect(e)}") +        Logger.warning("MRF.StealEmojiPolicy: Failed to fetch #{url}: #{inspect(e)}")          nil      end    end @@ -72,7 +80,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.StealEmojiPolicy do            reject_emoji? =              [:mrf_steal_emoji, :rejected_shortcodes]              |> Config.get([]) -            |> Enum.find(false, fn regex -> String.match?(shortcode, regex) end) +            |> Enum.find(false, fn pattern -> shortcode_matches?(shortcode, pattern) end)            !reject_emoji?          end) @@ -122,8 +130,12 @@ defmodule Pleroma.Web.ActivityPub.MRF.StealEmojiPolicy do          %{            key: :rejected_shortcodes,            type: {:list, :string}, -          description: "Regex-list of shortcodes to reject", -          suggestions: [""] +          description: """ +            A list of patterns or matches to reject shortcodes with. + +            Each pattern can be a string or [Regex](https://hexdocs.pm/elixir/Regex.html) in the format of `~r/PATTERN/`. +          """, +          suggestions: ["foo", ~r/foo/]          },          %{            key: :size_limit, diff --git a/lib/pleroma/web/activity_pub/mrf/tag_policy.ex b/lib/pleroma/web/activity_pub/mrf/tag_policy.ex index 10072b693..73760ca8f 100644 --- a/lib/pleroma/web/activity_pub/mrf/tag_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/tag_policy.ex @@ -27,22 +27,22 @@ defmodule Pleroma.Web.ActivityPub.MRF.TagPolicy do    defp process_tag(           "mrf_tag:media-force-nsfw",           %{ -           "type" => "Create", +           "type" => type,             "object" => %{"attachment" => child_attachment}           } = message         ) -       when length(child_attachment) > 0 do +       when length(child_attachment) > 0 and type in ["Create", "Update"] do      {:ok, Kernel.put_in(message, ["object", "sensitive"], true)}    end    defp process_tag(           "mrf_tag:media-strip",           %{ -           "type" => "Create", +           "type" => type,             "object" => %{"attachment" => child_attachment} = object           } = message         ) -       when length(child_attachment) > 0 do +       when length(child_attachment) > 0 and type in ["Create", "Update"] do      object = Map.delete(object, "attachment")      message = Map.put(message, "object", object) @@ -152,7 +152,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.TagPolicy do      do: filter_message(target_actor, message)    @impl true -  def filter(%{"actor" => actor, "type" => "Create"} = message), +  def filter(%{"actor" => actor, "type" => type} = message) when type in ["Create", "Update"],      do: filter_message(actor, message)    @impl true diff --git a/lib/pleroma/web/activity_pub/mrf/utils.ex b/lib/pleroma/web/activity_pub/mrf/utils.ex new file mode 100644 index 000000000..f2dc9eea9 --- /dev/null +++ b/lib/pleroma/web/activity_pub/mrf/utils.ex @@ -0,0 +1,15 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2023 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.MRF.Utils do +  @spec describe_regex_or_string(String.t() | Regex.t()) :: String.t() +  def describe_regex_or_string(pattern) do +    # This horror is needed to convert regex sigils to strings +    if not is_binary(pattern) do +      inspect(pattern) +    else +      pattern +    end +  end +end diff --git a/lib/pleroma/web/activity_pub/object_validator.ex b/lib/pleroma/web/activity_pub/object_validator.ex index f3e31c931..5e0d1aa8e 100644 --- a/lib/pleroma/web/activity_pub/object_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validator.ex @@ -21,7 +21,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do    alias Pleroma.Web.ActivityPub.ObjectValidators.AnnounceValidator    alias Pleroma.Web.ActivityPub.ObjectValidators.AnswerValidator    alias Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidator -  alias Pleroma.Web.ActivityPub.ObjectValidators.AudioVideoValidator +  alias Pleroma.Web.ActivityPub.ObjectValidators.AudioImageVideoValidator    alias Pleroma.Web.ActivityPub.ObjectValidators.BlockValidator    alias Pleroma.Web.ActivityPub.ObjectValidators.ChatMessageValidator    alias Pleroma.Web.ActivityPub.ObjectValidators.CreateChatMessageValidator @@ -102,9 +102,9 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do          %{"type" => "Create", "object" => %{"type" => objtype} = object} = create_activity,          meta        ) -      when objtype in ~w[Question Answer Audio Video Event Article Note Page] do -    with {:ok, object_data} <- cast_and_apply(object), -         meta = Keyword.put(meta, :object_data, object_data |> stringify_keys), +      when objtype in ~w[Question Answer Audio Video Image Event Article Note Page] do +    with {:ok, object_data} <- cast_and_apply_and_stringify_with_history(object), +         meta = Keyword.put(meta, :object_data, object_data),           {:ok, create_activity} <-             create_activity             |> CreateGenericValidator.cast_and_validate(meta) @@ -115,29 +115,64 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do    end    def validate(%{"type" => type} = object, meta) -      when type in ~w[Event Question Audio Video Article Note Page] do +      when type in ~w[Event Question Audio Video Image Article Note Page] do      validator =        case type do          "Event" -> EventValidator          "Question" -> QuestionValidator -        "Audio" -> AudioVideoValidator -        "Video" -> AudioVideoValidator +        "Audio" -> AudioImageVideoValidator +        "Video" -> AudioImageVideoValidator +        "Image" -> AudioImageVideoValidator          "Article" -> ArticleNotePageValidator          "Note" -> ArticleNotePageValidator          "Page" -> ArticleNotePageValidator        end      with {:ok, object} <- -           object -           |> validator.cast_and_validate() -           |> Ecto.Changeset.apply_action(:insert) do -      object = stringify_keys(object) +           do_separate_with_history(object, fn object -> +             with {:ok, object} <- +                    object +                    |> validator.cast_and_validate() +                    |> Ecto.Changeset.apply_action(:insert) do +               object = stringify_keys(object) + +               # Insert copy of hashtags as strings for the non-hashtag table indexing +               tag = (object["tag"] || []) ++ Object.hashtags(%Object{data: object}) +               object = Map.put(object, "tag", tag) + +               {:ok, object} +             end +           end) do +      {:ok, object, meta} +    end +  end -      # Insert copy of hashtags as strings for the non-hashtag table indexing -      tag = (object["tag"] || []) ++ Object.hashtags(%Object{data: object}) -      object = Map.put(object, "tag", tag) +  def validate( +        %{"type" => "Update", "object" => %{"type" => objtype} = object} = update_activity, +        meta +      ) +      when objtype in ~w[Question Answer Audio Video Event Article Note Page] do +    with {_, false} <- {:local, Access.get(meta, :local, false)}, +         {_, {:ok, object_data, _}} <- {:object_validation, validate(object, meta)}, +         meta = Keyword.put(meta, :object_data, object_data), +         {:ok, update_activity} <- +           update_activity +           |> UpdateValidator.cast_and_validate() +           |> Ecto.Changeset.apply_action(:insert) do +      update_activity = stringify_keys(update_activity) +      {:ok, update_activity, meta} +    else +      {:local, _} -> +        with {:ok, object} <- +               update_activity +               |> UpdateValidator.cast_and_validate() +               |> Ecto.Changeset.apply_action(:insert) do +          object = stringify_keys(object) +          {:ok, object, meta} +        end -      {:ok, object, meta} +      {:object_validation, e} -> +        e      end    end @@ -178,6 +213,15 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do    def validate(o, m), do: {:error, {:validator_not_set, {o, m}}} +  def cast_and_apply_and_stringify_with_history(object) do +    do_separate_with_history(object, fn object -> +      with {:ok, object_data} <- cast_and_apply(object), +           object_data <- object_data |> stringify_keys() do +        {:ok, object_data} +      end +    end) +  end +    def cast_and_apply(%{"type" => "ChatMessage"} = object) do      ChatMessageValidator.cast_and_apply(object)    end @@ -190,8 +234,8 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do      AnswerValidator.cast_and_apply(object)    end -  def cast_and_apply(%{"type" => type} = object) when type in ~w[Audio Video] do -    AudioVideoValidator.cast_and_apply(object) +  def cast_and_apply(%{"type" => type} = object) when type in ~w[Audio Image Video] do +    AudioImageVideoValidator.cast_and_apply(object)    end    def cast_and_apply(%{"type" => "Event"} = object) do @@ -204,8 +248,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do    def cast_and_apply(o), do: {:error, {:validator_not_set, o}} -  # is_struct/1 appears in Elixir 1.11 -  def stringify_keys(%{__struct__: _} = object) do +  def stringify_keys(object) when is_struct(object) do      object      |> Map.from_struct()      |> stringify_keys @@ -236,4 +279,54 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do      Object.normalize(object["object"], fetch: true)      :ok    end + +  defp for_each_history_item( +         %{"type" => "OrderedCollection", "orderedItems" => items} = history, +         object, +         fun +       ) do +    processed_items = +      Enum.map(items, fn item -> +        with item <- Map.put(item, "id", object["id"]), +             {:ok, item} <- fun.(item) do +          item +        else +          _ -> nil +        end +      end) + +    if Enum.all?(processed_items, &(not is_nil(&1))) do +      {:ok, Map.put(history, "orderedItems", processed_items)} +    else +      {:error, :invalid_history} +    end +  end + +  defp for_each_history_item(nil, _object, _fun) do +    {:ok, nil} +  end + +  defp for_each_history_item(_, _object, _fun) do +    {:error, :invalid_history} +  end + +  # fun is (object -> {:ok, validated_object_with_string_keys}) +  defp do_separate_with_history(object, fun) do +    with history <- object["formerRepresentations"], +         object <- Map.drop(object, ["formerRepresentations"]), +         {_, {:ok, object}} <- {:main_body, fun.(object)}, +         {_, {:ok, history}} <- {:history_items, for_each_history_item(history, object, fun)} do +      object = +        if history do +          Map.put(object, "formerRepresentations", history) +        else +          object +        end + +      {:ok, object} +    else +      {:main_body, e} -> e +      {:history_items, e} -> e +    end +  end  end diff --git a/lib/pleroma/web/activity_pub/object_validators/add_remove_validator.ex b/lib/pleroma/web/activity_pub/object_validators/add_remove_validator.ex index 5202db7f1..db3259550 100644 --- a/lib/pleroma/web/activity_pub/object_validators/add_remove_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/add_remove_validator.ex @@ -73,6 +73,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AddRemoveValidator do    end    defp maybe_refetch_user(%User{ap_id: ap_id}) do -    Pleroma.Web.ActivityPub.Transmogrifier.upgrade_user_from_ap_id(ap_id) +    # Maybe it could use User.get_or_fetch_by_ap_id to avoid refreshing too often +    User.fetch_by_ap_id(ap_id)    end  end diff --git a/lib/pleroma/web/activity_pub/object_validators/article_note_page_validator.ex b/lib/pleroma/web/activity_pub/object_validators/article_note_page_validator.ex index ca335bc8a..1b5b2e8fb 100644 --- a/lib/pleroma/web/activity_pub/object_validators/article_note_page_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/article_note_page_validator.ex @@ -49,7 +49,10 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidator do    defp fix_url(%{"url" => url} = data) when is_map(url), do: Map.put(data, "url", url["href"])    defp fix_url(data), do: data -  defp fix_tag(%{"tag" => tag} = data) when is_list(tag), do: data +  defp fix_tag(%{"tag" => tag} = data) when is_list(tag) do +    Map.put(data, "tag", Enum.filter(tag, &is_map/1)) +  end +    defp fix_tag(%{"tag" => tag} = data) when is_map(tag), do: Map.put(data, "tag", [tag])    defp fix_tag(data), do: Map.drop(data, ["tag"]) @@ -60,11 +63,19 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidator do    defp fix_replies(%{"replies" => %{"items" => replies}} = data) when is_list(replies),      do: Map.put(data, "replies", replies) -  defp fix_replies(%{"replies" => replies} = data) when is_bitstring(replies), +  # TODO: Pleroma does not have any support for Collections at the moment. +  # If the `replies` field is not something the ObjectID validator can handle, +  # the activity/object would be rejected, which is bad behavior. +  defp fix_replies(%{"replies" => replies} = data) when not is_list(replies),      do: Map.drop(data, ["replies"])    defp fix_replies(data), do: data +  def fix_attachments(%{"attachment" => attachment} = data) when is_map(attachment), +    do: Map.put(data, "attachment", [attachment]) + +  def fix_attachments(data), do: data +    defp fix(data) do      data      |> CommonFixes.fix_actor() @@ -72,6 +83,8 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidator do      |> fix_url()      |> fix_tag()      |> fix_replies() +    |> fix_attachments() +    |> CommonFixes.fix_quote_url()      |> Transmogrifier.fix_emoji()      |> Transmogrifier.fix_content_map()    end @@ -88,7 +101,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidator do    defp validate_data(data_cng) do      data_cng      |> validate_inclusion(:type, ["Article", "Note", "Page"]) -    |> validate_required([:id, :actor, :attributedTo, :type, :context, :context_id]) +    |> validate_required([:id, :actor, :attributedTo, :type, :context])      |> CommonValidations.validate_any_presence([:cc, :to])      |> CommonValidations.validate_fields_match([:actor, :attributedTo])      |> CommonValidations.validate_actor_presence() diff --git a/lib/pleroma/web/activity_pub/object_validators/attachment_validator.ex b/lib/pleroma/web/activity_pub/object_validators/attachment_validator.ex index d1c61ac82..398020bff 100644 --- a/lib/pleroma/web/activity_pub/object_validators/attachment_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/attachment_validator.ex @@ -11,15 +11,16 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AttachmentValidator do    @primary_key false    embedded_schema do +    field(:id, :string)      field(:type, :string) -    field(:mediaType, :string, default: "application/octet-stream") +    field(:mediaType, ObjectValidators.MIME, default: "application/octet-stream")      field(:name, :string)      field(:blurhash, :string)      embeds_many :url, UrlObjectValidator, primary_key: false do        field(:type, :string)        field(:href, ObjectValidators.Uri) -      field(:mediaType, :string, default: "application/octet-stream") +      field(:mediaType, ObjectValidators.MIME, default: "application/octet-stream")        field(:width, :integer)        field(:height, :integer)      end @@ -43,10 +44,10 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AttachmentValidator do        |> fix_url()      struct -    |> cast(data, [:type, :mediaType, :name, :blurhash]) -    |> cast_embed(:url, with: &url_changeset/2) +    |> cast(data, [:id, :type, :mediaType, :name, :blurhash]) +    |> cast_embed(:url, with: &url_changeset/2, required: true)      |> validate_inclusion(:type, ~w[Link Document Audio Image Video]) -    |> validate_required([:type, :mediaType, :url]) +    |> validate_required([:type, :mediaType])    end    def url_changeset(struct, data) do @@ -59,13 +60,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AttachmentValidator do    end    def fix_media_type(data) do -    data = Map.put_new(data, "mediaType", data["mimeType"]) - -    if is_bitstring(data["mediaType"]) && MIME.extensions(data["mediaType"]) != [] do -      data -    else -      Map.put(data, "mediaType", "application/octet-stream") -    end +    Map.put_new(data, "mediaType", data["mimeType"] || "application/octet-stream")    end    defp handle_href(href, mediaType, data) do @@ -96,6 +91,6 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AttachmentValidator do    defp validate_data(cng) do      cng      |> validate_inclusion(:type, ~w[Document Audio Image Video]) -    |> validate_required([:mediaType, :url, :type]) +    |> validate_required([:mediaType, :type])    end  end diff --git a/lib/pleroma/web/activity_pub/object_validators/audio_video_validator.ex b/lib/pleroma/web/activity_pub/object_validators/audio_image_video_validator.ex index 432bd9039..65ac6bb93 100644 --- a/lib/pleroma/web/activity_pub/object_validators/audio_video_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/audio_image_video_validator.ex @@ -2,7 +2,7 @@  # Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>  # SPDX-License-Identifier: AGPL-3.0-only -defmodule Pleroma.Web.ActivityPub.ObjectValidators.AudioVideoValidator do +defmodule Pleroma.Web.ActivityPub.ObjectValidators.AudioImageVideoValidator do    use Ecto.Schema    alias Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes @@ -55,9 +55,14 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AudioVideoValidator do      url      |> Enum.concat(mpeg_url["tag"] || [])      |> Enum.find(fn -      %{"mediaType" => mime_type} -> String.starts_with?(mime_type, ["video/", "audio/"]) -      %{"mimeType" => mime_type} -> String.starts_with?(mime_type, ["video/", "audio/"]) -      _ -> false +      %{"mediaType" => mime_type} -> +        String.starts_with?(mime_type, ["video/", "audio/", "image/"]) + +      %{"mimeType" => mime_type} -> +        String.starts_with?(mime_type, ["video/", "audio/", "image/"]) + +      _ -> +        false      end)    end @@ -94,6 +99,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AudioVideoValidator do      data      |> CommonFixes.fix_actor()      |> CommonFixes.fix_object_defaults() +    |> CommonFixes.fix_quote_url()      |> Transmogrifier.fix_emoji()      |> fix_url()      |> fix_content() @@ -104,14 +110,14 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AudioVideoValidator do      struct      |> cast(data, __schema__(:fields) -- [:attachment, :tag]) -    |> cast_embed(:attachment) +    |> cast_embed(:attachment, required: true)      |> cast_embed(:tag)    end    defp validate_data(data_cng) do      data_cng -    |> validate_inclusion(:type, ["Audio", "Video"]) -    |> validate_required([:id, :actor, :attributedTo, :type, :context, :attachment]) +    |> validate_inclusion(:type, ~w[Audio Image Video]) +    |> validate_required([:id, :actor, :attributedTo, :type, :context])      |> CommonValidations.validate_any_presence([:cc, :to])      |> CommonValidations.validate_fields_match([:actor, :attributedTo])      |> CommonValidations.validate_actor_presence() diff --git a/lib/pleroma/web/activity_pub/object_validators/common_fields.ex b/lib/pleroma/web/activity_pub/object_validators/common_fields.ex index 8e768ffbf..1a5d02601 100644 --- a/lib/pleroma/web/activity_pub/object_validators/common_fields.ex +++ b/lib/pleroma/web/activity_pub/object_validators/common_fields.ex @@ -27,12 +27,13 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonFields do      end    end -  # All objects except Answer and CHatMessage +  # All objects except Answer and ChatMessage    defmacro object_fields do      quote bind_quoted: binding() do        field(:content, :string)        field(:published, ObjectValidators.DateTime) +      field(:updated, ObjectValidators.DateTime)        field(:emoji, ObjectValidators.Emoji, default: %{})        embeds_many(:attachment, AttachmentValidator)      end @@ -51,15 +52,15 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonFields do        field(:summary, :string)        field(:context, :string) -      # short identifier for PleromaFE to group statuses by context -      field(:context_id, :integer)        field(:sensitive, :boolean, default: false)        field(:replies_count, :integer, default: 0)        field(:like_count, :integer, default: 0)        field(:announcement_count, :integer, default: 0) +      field(:quotes_count, :integer, default: 0)        field(:inReplyTo, ObjectValidators.ObjectID) -      field(:url, ObjectValidators.Uri) +      field(:quoteUrl, ObjectValidators.ObjectID) +      field(:url, ObjectValidators.BareUri)        field(:likes, {:array, ObjectValidators.ObjectID}, default: [])        field(:announcements, {:array, ObjectValidators.ObjectID}, default: []) diff --git a/lib/pleroma/web/activity_pub/object_validators/common_fixes.ex b/lib/pleroma/web/activity_pub/object_validators/common_fixes.ex index 4f8c083eb..4d9be0bdd 100644 --- a/lib/pleroma/web/activity_pub/object_validators/common_fixes.ex +++ b/lib/pleroma/web/activity_pub/object_validators/common_fixes.ex @@ -10,6 +10,8 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes do    alias Pleroma.Web.ActivityPub.Transmogrifier    alias Pleroma.Web.ActivityPub.Utils +  require Pleroma.Constants +    def cast_and_filter_recipients(message, field, follower_collection, field_fallback \\ []) do      {:ok, data} = ObjectValidators.Recipients.cast(message[field] || field_fallback) @@ -22,14 +24,15 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes do    end    def fix_object_defaults(data) do -    %{data: %{"id" => context}, id: context_id} = -      Utils.create_context(data["context"] || data["conversation"]) +    context = +      Utils.maybe_create_context( +        data["context"] || data["conversation"] || data["inReplyTo"] || data["id"] +      )      %User{follower_address: follower_collection} = User.get_cached_by_ap_id(data["attributedTo"])      data      |> Map.put("context", context) -    |> Map.put("context_id", context_id)      |> cast_and_filter_recipients("to", follower_collection)      |> cast_and_filter_recipients("cc", follower_collection)      |> cast_and_filter_recipients("bto", follower_collection) @@ -75,4 +78,48 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes do      Map.put(data, "to", to)    end + +  def fix_quote_url(%{"quoteUrl" => _quote_url} = data), do: data + +  # Fedibird +  # https://github.com/fedibird/mastodon/commit/dbd7ae6cf58a92ec67c512296b4daaea0d01e6ac +  def fix_quote_url(%{"quoteUri" => quote_url} = data) do +    Map.put(data, "quoteUrl", quote_url) +  end + +  # Old Fedibird (bug) +  # https://github.com/fedibird/mastodon/issues/9 +  def fix_quote_url(%{"quoteURL" => quote_url} = data) do +    Map.put(data, "quoteUrl", quote_url) +  end + +  # Misskey fallback +  def fix_quote_url(%{"_misskey_quote" => quote_url} = data) do +    Map.put(data, "quoteUrl", quote_url) +  end + +  def fix_quote_url(%{"tag" => [_ | _] = tags} = data) do +    tag = Enum.find(tags, &is_object_link_tag/1) + +    if not is_nil(tag) do +      data +      |> Map.put("quoteUrl", tag["href"]) +    else +      data +    end +  end + +  def fix_quote_url(data), do: data + +  # https://codeberg.org/fediverse/fep/src/branch/main/fep/e232/fep-e232.md +  def is_object_link_tag(%{ +        "type" => "Link", +        "mediaType" => media_type, +        "href" => href +      }) +      when media_type in Pleroma.Constants.activity_json_mime_types() and is_binary(href) do +    true +  end + +  def is_object_link_tag(_), do: false  end diff --git a/lib/pleroma/web/activity_pub/object_validators/common_validations.ex b/lib/pleroma/web/activity_pub/object_validators/common_validations.ex index 704b3abc9..1c5b1a059 100644 --- a/lib/pleroma/web/activity_pub/object_validators/common_validations.ex +++ b/lib/pleroma/web/activity_pub/object_validators/common_validations.ex @@ -136,11 +136,11 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations do    # This figures out if a user is able to create, delete or modify something    # based on the domain and superuser status -  @spec validate_modification_rights(Ecto.Changeset.t()) :: Ecto.Changeset.t() -  def validate_modification_rights(cng) do +  @spec validate_modification_rights(Ecto.Changeset.t(), atom()) :: Ecto.Changeset.t() +  def validate_modification_rights(cng, privilege) do      actor = User.get_cached_by_ap_id(get_field(cng, :actor)) -    if User.superuser?(actor) || same_domain?(cng) do +    if User.privileged?(actor, privilege) || same_domain?(cng) do        cng      else        cng diff --git a/lib/pleroma/web/activity_pub/object_validators/create_generic_validator.ex b/lib/pleroma/web/activity_pub/object_validators/create_generic_validator.ex index c9a621cb1..2395abfd4 100644 --- a/lib/pleroma/web/activity_pub/object_validators/create_generic_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/create_generic_validator.ex @@ -75,7 +75,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CreateGenericValidator do      data      |> CommonFixes.fix_actor() -    |> Map.put_new("context", object["context"]) +    |> Map.put("context", object["context"])      |> fix_addressing(object)    end diff --git a/lib/pleroma/web/activity_pub/object_validators/delete_validator.ex b/lib/pleroma/web/activity_pub/object_validators/delete_validator.ex index 035fd5bc9..4d8502ada 100644 --- a/lib/pleroma/web/activity_pub/object_validators/delete_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/delete_validator.ex @@ -61,7 +61,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.DeleteValidator do      |> validate_required([:id, :type, :actor, :to, :cc, :object])      |> validate_inclusion(:type, ["Delete"])      |> validate_delete_actor(:actor) -    |> validate_modification_rights() +    |> validate_modification_rights(:messages_delete)      |> validate_object_or_user_presence(allowed_types: @deletable_types)      |> add_deleted_activity_id()    end diff --git a/lib/pleroma/web/activity_pub/object_validators/emoji_react_validator.ex b/lib/pleroma/web/activity_pub/object_validators/emoji_react_validator.ex index ed072b888..a0b82b325 100644 --- a/lib/pleroma/web/activity_pub/object_validators/emoji_react_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/emoji_react_validator.ex @@ -5,8 +5,10 @@  defmodule Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactValidator do    use Ecto.Schema +  alias Pleroma.Emoji    alias Pleroma.Object    alias Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes +  alias Pleroma.Web.ActivityPub.ObjectValidators.TagValidator    import Ecto.Changeset    import Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations @@ -19,6 +21,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactValidator do          import Elixir.Pleroma.Web.ActivityPub.ObjectValidators.CommonFields          message_fields()          activity_fields() +        embeds_many(:tag, TagValidator)        end      end @@ -43,32 +46,75 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactValidator do    def changeset(struct, data) do      struct -    |> cast(data, __schema__(:fields)) +    |> cast(data, __schema__(:fields) -- [:tag]) +    |> cast_embed(:tag)    end    defp fix(data) do      data =        data +      |> fix_emoji_qualification()        |> CommonFixes.fix_actor()        |> CommonFixes.fix_activity_addressing() -    with %Object{} = object <- Object.normalize(data["object"]) do -      data -      |> CommonFixes.fix_activity_context(object) -      |> CommonFixes.fix_object_action_recipients(object) -    else -      _ -> data +    data = Map.put_new(data, "tag", []) + +    case Object.normalize(data["object"]) do +      %Object{} = object -> +        data +        |> CommonFixes.fix_activity_context(object) +        |> CommonFixes.fix_object_action_recipients(object) + +      _ -> +        data      end    end +  defp fix_emoji_qualification(%{"content" => emoji} = data) do +    new_emoji = Pleroma.Emoji.fully_qualify_emoji(emoji) + +    cond do +      Pleroma.Emoji.is_unicode_emoji?(emoji) -> +        data + +      Pleroma.Emoji.is_unicode_emoji?(new_emoji) -> +        data |> Map.put("content", new_emoji) + +      true -> +        data +    end +  end + +  defp fix_emoji_qualification(data), do: data +    defp validate_emoji(cng) do      content = get_field(cng, :content) -    if Pleroma.Emoji.is_unicode_emoji?(content) do +    if Emoji.is_unicode_emoji?(content) || Emoji.is_custom_emoji?(content) do        cng      else        cng -      |> add_error(:content, "must be a single character emoji") +      |> add_error(:content, "is not a valid emoji") +    end +  end + +  defp maybe_validate_tag_presence(cng) do +    content = get_field(cng, :content) + +    if Emoji.is_unicode_emoji?(content) do +      cng +    else +      tag = get_field(cng, :tag) +      emoji_name = Emoji.maybe_strip_name(content) + +      case tag do +        [%{name: ^emoji_name, type: "Emoji", icon: %{url: _}}] -> +          cng + +        _ -> +          cng +          |> add_error(:tag, "does not contain an Emoji tag") +      end      end    end @@ -79,5 +125,6 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactValidator do      |> validate_actor_presence()      |> validate_object_presence()      |> validate_emoji() +    |> maybe_validate_tag_presence()    end  end diff --git a/lib/pleroma/web/activity_pub/object_validators/event_validator.ex b/lib/pleroma/web/activity_pub/object_validators/event_validator.ex index 0e99f2037..ab204f69a 100644 --- a/lib/pleroma/web/activity_pub/object_validators/event_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/event_validator.ex @@ -62,7 +62,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.EventValidator do    defp validate_data(data_cng) do      data_cng      |> validate_inclusion(:type, ["Event"]) -    |> validate_required([:id, :actor, :attributedTo, :type, :context, :context_id]) +    |> validate_required([:id, :actor, :attributedTo, :type, :context])      |> CommonValidations.validate_any_presence([:cc, :to])      |> CommonValidations.validate_fields_match([:actor, :attributedTo])      |> CommonValidations.validate_actor_presence() diff --git a/lib/pleroma/web/activity_pub/object_validators/question_validator.ex b/lib/pleroma/web/activity_pub/object_validators/question_validator.ex index 9412be4bc..621085e6c 100644 --- a/lib/pleroma/web/activity_pub/object_validators/question_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/question_validator.ex @@ -62,6 +62,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.QuestionValidator do      data      |> CommonFixes.fix_actor()      |> CommonFixes.fix_object_defaults() +    |> CommonFixes.fix_quote_url()      |> Transmogrifier.fix_emoji()      |> fix_closed()    end @@ -80,7 +81,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.QuestionValidator do    defp validate_data(data_cng) do      data_cng      |> validate_inclusion(:type, ["Question"]) -    |> validate_required([:id, :actor, :attributedTo, :type, :context, :context_id]) +    |> validate_required([:id, :actor, :attributedTo, :type, :context])      |> CommonValidations.validate_any_presence([:cc, :to])      |> CommonValidations.validate_fields_match([:actor, :attributedTo])      |> CommonValidations.validate_actor_presence() diff --git a/lib/pleroma/web/activity_pub/object_validators/tag_validator.ex b/lib/pleroma/web/activity_pub/object_validators/tag_validator.ex index 9f15f1981..47cf7b415 100644 --- a/lib/pleroma/web/activity_pub/object_validators/tag_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/tag_validator.ex @@ -9,15 +9,20 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.TagValidator do    import Ecto.Changeset +  require Pleroma.Constants +    @primary_key false    embedded_schema do      # Common      field(:type, :string)      field(:name, :string) -    # Mention, Hashtag +    # Mention, Hashtag, Link      field(:href, ObjectValidators.Uri) +    # Link +    field(:mediaType, :string) +      # Emoji      embeds_one :icon, IconObjectValidator, primary_key: false do        field(:type, :string) @@ -68,6 +73,19 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.TagValidator do      |> validate_required([:type, :name, :icon])    end +  def changeset(struct, %{"type" => "Link"} = data) do +    struct +    |> cast(data, [:type, :name, :mediaType, :href]) +    |> validate_inclusion(:mediaType, Pleroma.Constants.activity_json_mime_types()) +    |> validate_required([:type, :href, :mediaType]) +  end + +  def changeset(struct, %{"type" => _} = data) do +    struct +    |> cast(data, []) +    |> Map.put(:action, :ignore) +  end +    def icon_changeset(struct, data) do      struct      |> cast(data, [:type, :url]) diff --git a/lib/pleroma/web/activity_pub/object_validators/update_validator.ex b/lib/pleroma/web/activity_pub/object_validators/update_validator.ex index a5def312e..1e940a400 100644 --- a/lib/pleroma/web/activity_pub/object_validators/update_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/update_validator.ex @@ -51,7 +51,9 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.UpdateValidator do      with actor = get_field(cng, :actor),           object = get_field(cng, :object),           {:ok, object_id} <- ObjectValidators.ObjectID.cast(object), -         true <- actor == object_id do +         actor_uri <- URI.parse(actor), +         object_uri <- URI.parse(object_id), +         true <- actor_uri.host == object_uri.host do        cng      else        _e -> diff --git a/lib/pleroma/web/activity_pub/publisher.ex b/lib/pleroma/web/activity_pub/publisher.ex index 6c1ba76a3..a580994b1 100644 --- a/lib/pleroma/web/activity_pub/publisher.ex +++ b/lib/pleroma/web/activity_pub/publisher.ex @@ -118,7 +118,7 @@ defmodule Pleroma.Web.ActivityPub.Publisher do      end    end -  @spec recipients(User.t(), Activity.t()) :: list(User.t()) | [] +  @spec recipients(User.t(), Activity.t()) :: [[User.t()]]    defp recipients(actor, activity) do      followers =        if actor.follower_address in activity.recipients do @@ -138,7 +138,10 @@ defmodule Pleroma.Web.ActivityPub.Publisher do            []        end -    Pleroma.Web.Federator.Publisher.remote_users(actor, activity) ++ followers ++ fetchers +    mentioned = Pleroma.Web.Federator.Publisher.remote_users(actor, activity) +    non_mentioned = (followers ++ fetchers) -- mentioned + +    [mentioned, non_mentioned]    end    defp get_cc_ap_ids(ap_id, recipients) do @@ -195,35 +198,39 @@ defmodule Pleroma.Web.ActivityPub.Publisher do      public = is_public?(activity)      {:ok, data} = Transmogrifier.prepare_outgoing(activity.data) -    recipients = recipients(actor, activity) +    [priority_recipients, recipients] = recipients(actor, activity)      inboxes = -      recipients -      |> Enum.filter(&User.ap_enabled?/1) -      |> Enum.map(fn actor -> actor.inbox end) -      |> Enum.filter(fn inbox -> should_federate?(inbox, public) end) -      |> Instances.filter_reachable() +      [priority_recipients, recipients] +      |> Enum.map(fn recipients -> +        recipients +        |> Enum.map(fn actor -> actor.inbox end) +        |> Enum.filter(fn inbox -> should_federate?(inbox, public) end) +        |> Instances.filter_reachable() +      end)      Repo.checkout(fn -> -      Enum.each(inboxes, fn {inbox, unreachable_since} -> -        %User{ap_id: ap_id} = Enum.find(recipients, fn actor -> actor.inbox == inbox end) - -        # Get all the recipients on the same host and add them to cc. Otherwise, a remote -        # instance would only accept a first message for the first recipient and ignore the rest. -        cc = get_cc_ap_ids(ap_id, recipients) - -        json = -          data -          |> Map.put("cc", cc) -          |> Jason.encode!() - -        Pleroma.Web.Federator.Publisher.enqueue_one(__MODULE__, %{ -          inbox: inbox, -          json: json, -          actor_id: actor.id, -          id: activity.data["id"], -          unreachable_since: unreachable_since -        }) +      Enum.each(inboxes, fn inboxes -> +        Enum.each(inboxes, fn {inbox, unreachable_since} -> +          %User{ap_id: ap_id} = Enum.find(recipients, fn actor -> actor.inbox == inbox end) + +          # Get all the recipients on the same host and add them to cc. Otherwise, a remote +          # instance would only accept a first message for the first recipient and ignore the rest. +          cc = get_cc_ap_ids(ap_id, recipients) + +          json = +            data +            |> Map.put("cc", cc) +            |> Jason.encode!() + +          Pleroma.Web.Federator.Publisher.enqueue_one(__MODULE__, %{ +            inbox: inbox, +            json: json, +            actor_id: actor.id, +            id: activity.data["id"], +            unreachable_since: unreachable_since +          }) +        end)        end)      end)    end @@ -240,26 +247,36 @@ defmodule Pleroma.Web.ActivityPub.Publisher do      {:ok, data} = Transmogrifier.prepare_outgoing(activity.data)      json = Jason.encode!(data) -    recipients(actor, activity) -    |> Enum.filter(fn user -> User.ap_enabled?(user) end) -    |> Enum.map(fn %User{} = user -> -      determine_inbox(activity, user) -    end) -    |> Enum.uniq() -    |> Enum.filter(fn inbox -> should_federate?(inbox, public) end) -    |> Instances.filter_reachable() -    |> Enum.each(fn {inbox, unreachable_since} -> -      Pleroma.Web.Federator.Publisher.enqueue_one( -        __MODULE__, -        %{ -          inbox: inbox, -          json: json, -          actor_id: actor.id, -          id: activity.data["id"], -          unreachable_since: unreachable_since -        } -      ) +    [priority_inboxes, inboxes] = +      recipients(actor, activity) +      |> Enum.map(fn recipients -> +        recipients +        |> Enum.map(fn actor -> actor.inbox end) +        |> Enum.filter(fn inbox -> should_federate?(inbox, public) end) +      end) + +    inboxes = inboxes -- priority_inboxes + +    [{priority_inboxes, 0}, {inboxes, 1}] +    |> Enum.each(fn {inboxes, priority} -> +      inboxes +      |> Instances.filter_reachable() +      |> Enum.each(fn {inbox, unreachable_since} -> +        Pleroma.Web.Federator.Publisher.enqueue_one( +          __MODULE__, +          %{ +            inbox: inbox, +            json: json, +            actor_id: actor.id, +            id: activity.data["id"], +            unreachable_since: unreachable_since +          }, +          priority: priority +        ) +      end)      end) + +    :ok    end    def gather_webfinger_links(%User{} = user) do diff --git a/lib/pleroma/web/activity_pub/side_effects.ex b/lib/pleroma/web/activity_pub/side_effects.ex index b997c15db..10f268f05 100644 --- a/lib/pleroma/web/activity_pub/side_effects.ex +++ b/lib/pleroma/web/activity_pub/side_effects.ex @@ -25,6 +25,7 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do    alias Pleroma.Web.Streamer    alias Pleroma.Workers.PollWorker +  require Pleroma.Constants    require Logger    @cachex Pleroma.Config.get([:cachex, :provider], Cachex) @@ -153,23 +154,26 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do    # Tasks this handles:    # - Update the user +  # - Update a non-user object (Note, Question, etc.)    #    # For a local user, we also get a changeset with the full information, so we    # can update non-federating, non-activitypub settings as well.    @impl true    def handle(%{data: %{"type" => "Update", "object" => updated_object}} = object, meta) do -    if changeset = Keyword.get(meta, :user_update_changeset) do -      changeset -      |> User.update_and_set_cache() +    updated_object_id = updated_object["id"] + +    with {_, true} <- {:has_id, is_binary(updated_object_id)}, +         %{"type" => type} <- updated_object, +         {_, is_user} <- {:is_user, type in Pleroma.Constants.actor_types()} do +      if is_user do +        handle_update_user(object, meta) +      else +        handle_update_object(object, meta) +      end      else -      {:ok, new_user_data} = ActivityPub.user_data_from_user_object(updated_object) - -      User.get_by_ap_id(updated_object["id"]) -      |> User.remote_user_changeset(new_user_data) -      |> User.update_and_set_cache() +      _ -> +        {:ok, object, meta}      end - -    {:ok, object, meta}    end    # Tasks this handles: @@ -193,6 +197,7 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do    # - Increase replies count    # - Set up ActivityExpiration    # - Set up notifications +  # - Index incoming posts for search (if needed)    @impl true    def handle(%{data: %{"type" => "Create"}} = activity, meta) do      with {:ok, object, meta} <- handle_object_creation(meta[:object_data], activity, meta), @@ -205,6 +210,10 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do          Object.increase_replies_count(in_reply_to)        end +      if quote_url = object.data["quoteUrl"] do +        Object.increase_quotes_count(quote_url) +      end +        reply_depth = (meta[:depth] || 0) + 1        # FIXME: Force inReplyTo to replies @@ -222,6 +231,8 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do          Task.start(fn -> Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity) end)        end) +      Pleroma.Search.add_to_index(Map.put(activity, :object, object)) +        meta =          meta          |> add_notifications(notifications) @@ -278,10 +289,10 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do    # Tasks this handles:    # - Delete and unpins the create activity    # - Replace object with Tombstone -  # - Set up notification    # - Reduce the user note count    # - Reduce the reply count    # - Stream out the activity +  # - Removes posts from search index (if needed)    @impl true    def handle(%{data: %{"type" => "Delete", "object" => deleted_object}} = object, meta) do      deleted_object = @@ -302,6 +313,10 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do                Object.decrease_replies_count(in_reply_to)              end +            if quote_url = deleted_object.data["quoteUrl"] do +              Object.decrease_quotes_count(quote_url) +            end +              MessageReference.delete_for_object(deleted_object)              ap_streamer().stream_out(object) @@ -320,7 +335,11 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do        end      if result == :ok do -      Notification.create_notifications(object) +      # Only remove from index when deleting actual objects, not users or anything else +      with %Pleroma.Object{} <- deleted_object do +        Pleroma.Search.remove_from_index(deleted_object) +      end +        {:ok, object, meta}      else        {:error, result} @@ -390,6 +409,55 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do      {:ok, object, meta}    end +  defp handle_update_user( +         %{data: %{"type" => "Update", "object" => updated_object}} = object, +         meta +       ) do +    if changeset = Keyword.get(meta, :user_update_changeset) do +      changeset +      |> User.update_and_set_cache() +    else +      {:ok, new_user_data} = ActivityPub.user_data_from_user_object(updated_object) + +      User.get_by_ap_id(updated_object["id"]) +      |> User.remote_user_changeset(new_user_data) +      |> User.update_and_set_cache() +    end + +    {:ok, object, meta} +  end + +  defp handle_update_object( +         %{data: %{"type" => "Update", "object" => updated_object}} = object, +         meta +       ) do +    orig_object_ap_id = updated_object["id"] +    orig_object = Object.get_by_ap_id(orig_object_ap_id) +    orig_object_data = orig_object.data + +    updated_object = +      if meta[:local] do +        # If this is a local Update, we don't process it by transmogrifier, +        # so we use the embedded object as-is. +        updated_object +      else +        meta[:object_data] +      end + +    if orig_object_data["type"] in Pleroma.Constants.updatable_object_types() do +      {:ok, _, updated} = +        Object.Updater.do_update_and_invalidate_cache(orig_object, updated_object) + +      if updated do +        object +        |> Activity.normalize() +        |> ActivityPub.notify_and_stream() +      end +    end + +    {:ok, object, meta} +  end +    def handle_object_creation(%{"type" => "ChatMessage"} = object, _activity, meta) do      with {:ok, object, meta} <- Pipeline.common_pipeline(object, meta) do        actor = User.get_cached_by_ap_id(object.data["actor"]) @@ -445,7 +513,7 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do    end    def handle_object_creation(%{"type" => objtype} = object, _activity, meta) -      when objtype in ~w[Audio Video Event Article Note Page] do +      when objtype in ~w[Audio Video Image Event Article Note Page] do      with {:ok, object, meta} <- Pipeline.common_pipeline(object, meta) do        {:ok, object, meta}      end diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex index a70330f0e..35f3aea03 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier.ex @@ -20,7 +20,6 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do    alias Pleroma.Web.ActivityPub.Utils    alias Pleroma.Web.ActivityPub.Visibility    alias Pleroma.Web.Federator -  alias Pleroma.Workers.TransmogrifierWorker    import Ecto.Query @@ -157,7 +156,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do          |> Map.drop(["conversation", "inReplyToAtomUri"])        else          e -> -          Logger.warn("Couldn't fetch #{inspect(in_reply_to_id)}, error: #{inspect(e)}") +          Logger.warning("Couldn't fetch #{inspect(in_reply_to_id)}, error: #{inspect(e)}")            object        end      else @@ -167,6 +166,27 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do    def fix_in_reply_to(object, _options), do: object +  def fix_quote_url_and_maybe_fetch(object, options \\ []) do +    quote_url = +      case Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes.fix_quote_url(object) do +        %{"quoteUrl" => quote_url} -> quote_url +        _ -> nil +      end + +    with {:quoting?, true} <- {:quoting?, not is_nil(quote_url)}, +         {:ok, quoted_object} <- get_obj_helper(quote_url, options), +         %Activity{} <- Activity.get_create_by_object_ap_id(quoted_object.data["id"]) do +      Map.put(object, "quoteUrl", quoted_object.data["id"]) +    else +      {:quoting?, _} -> +        object + +      e -> +        Logger.warning("Couldn't fetch #{inspect(quote_url)}, error: #{inspect(e)}") +        object +    end +  end +    defp prepare_in_reply_to(in_reply_to) do      cond do        is_bitstring(in_reply_to) -> @@ -203,13 +223,13 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do          media_type =            cond do -            is_map(url) && MIME.extensions(url["mediaType"]) != [] -> +            is_map(url) && url =~ Pleroma.Constants.mime_regex() ->                url["mediaType"] -            is_bitstring(data["mediaType"]) && MIME.extensions(data["mediaType"]) != [] -> +            is_bitstring(data["mediaType"]) && data["mediaType"] =~ Pleroma.Constants.mime_regex() ->                data["mediaType"] -            is_bitstring(data["mimeType"]) && MIME.extensions(data["mimeType"]) != [] -> +            is_bitstring(data["mimeType"]) && data["mimeType"] =~ Pleroma.Constants.mime_regex() ->                data["mimeType"]              true -> @@ -447,7 +467,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do          %{"type" => "Create", "object" => %{"type" => objtype, "id" => obj_id}} = data,          options        ) -      when objtype in ~w{Question Answer ChatMessage Audio Video Event Article Note Page} do +      when objtype in ~w{Question Answer ChatMessage Audio Video Event Article Note Page Image} do      fetch_options = Keyword.put(options, :depth, (options[:depth] || 0) + 1)      object = @@ -455,6 +475,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do        |> strip_internal_fields()        |> fix_type(fetch_options)        |> fix_in_reply_to(fetch_options) +      |> fix_quote_url_and_maybe_fetch(fetch_options)      data = Map.put(data, "object", object)      options = Keyword.put(options, :local, false) @@ -630,6 +651,16 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do    def set_reply_to_uri(obj), do: obj    @doc """ +  Fedibird compatibility +  https://github.com/fedibird/mastodon/commit/dbd7ae6cf58a92ec67c512296b4daaea0d01e6ac +  """ +  def set_quote_url(%{"quoteUrl" => quote_url} = object) when is_binary(quote_url) do +    Map.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,10 +714,29 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do      |> prepare_attachments      |> set_conversation      |> set_reply_to_uri +    |> set_quote_url      |> set_replies      |> strip_internal_fields      |> strip_internal_tags      |> set_type +    |> maybe_process_history +  end + +  defp maybe_process_history(%{"formerRepresentations" => %{"orderedItems" => history}} = object) do +    processed_history = +      Enum.map( +        history, +        fn +          item when is_map(item) -> prepare_object(item) +          item -> item +        end +      ) + +    put_in(object, ["formerRepresentations", "orderedItems"], processed_history) +  end + +  defp maybe_process_history(object) do +    object    end    #  @doc @@ -711,6 +761,21 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do      {:ok, data}    end +  def prepare_outgoing(%{"type" => "Update", "object" => %{"type" => objtype} = object} = data) +      when objtype in Pleroma.Constants.updatable_object_types() do +    object = +      object +      |> prepare_object + +    data = +      data +      |> Map.put("object", object) +      |> Map.merge(Utils.make_json_ld_header()) +      |> Map.delete("bcc") + +    {:ok, data} +  end +    def prepare_outgoing(%{"type" => "Announce", "actor" => ap_id, "object" => object_id} = data) do      object =        object_id @@ -913,47 +978,6 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do    defp strip_internal_tags(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}) - -    from( -      a in Activity, -      where: ^old_follower_address in a.recipients, -      update: [ -        set: [ -          recipients: -            fragment( -              "array_replace(?,?,?)", -              a.recipients, -              ^old_follower_address, -              ^user.follower_address -            ) -        ] -      ] -    ) -    |> Repo.update_all([]) -  end - -  def upgrade_user_from_ap_id(ap_id) do -    with %User{local: false} = user <- User.get_cached_by_ap_id(ap_id), -         {:ok, data} <- ActivityPub.fetch_and_prepare_user_from_ap_id(ap_id), -         {:ok, user} <- update_user(user, data) do -      {:ok, _pid} = Task.start(fn -> ActivityPub.pinned_fetch_task(user) end) -      TransmogrifierWorker.enqueue("user_upgrade", %{"user_id" => user.id}) -      {:ok, user} -    else -      %User{} = user -> {:ok, user} -      e -> e -    end -  end - -  defp update_user(user, data) do -    user -    |> User.remote_user_changeset(data) -    |> User.update_and_set_cache() -  end -    def maybe_fix_user_url(%{"url" => url} = data) when is_map(url) do      Map.put(data, "url", url["href"])    end diff --git a/lib/pleroma/web/activity_pub/utils.ex b/lib/pleroma/web/activity_pub/utils.ex index 72d17e2aa..45a37a02f 100644 --- a/lib/pleroma/web/activity_pub/utils.ex +++ b/lib/pleroma/web/activity_pub/utils.ex @@ -7,6 +7,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do    alias Ecto.UUID    alias Pleroma.Activity    alias Pleroma.Config +  alias Pleroma.EctoType.ActivityPub.ObjectValidators.ObjectID    alias Pleroma.Maps    alias Pleroma.Notification    alias Pleroma.Object @@ -31,7 +32,8 @@ defmodule Pleroma.Web.ActivityPub.Utils do      "Page",      "Question",      "Answer", -    "Audio" +    "Audio", +    "Image"    ]    @strip_status_report_states ~w(closed resolved)    @supported_report_states ~w(open closed resolved) @@ -154,22 +156,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do      Notification.get_notified_from_activity(%Activity{data: object}, false)    end -  def create_context(context) do -    context = context || generate_id("contexts") - -    # Ecto has problems accessing the constraint inside the jsonb, -    # so we explicitly check for the existed object before insert -    object = Object.get_cached_by_ap_id(context) - -    with true <- is_nil(object), -         changeset <- Object.context_mapping(context), -         {:ok, inserted_object} <- Repo.insert(changeset) do -      inserted_object -    else -      _ -> -        object -    end -  end +  def maybe_create_context(context), do: context || generate_id("contexts")    @doc """    Enqueues an activity for federation if it's local @@ -201,18 +188,16 @@ defmodule Pleroma.Web.ActivityPub.Utils do      |> Map.put_new("id", "pleroma:fakeid")      |> Map.put_new_lazy("published", &make_date/0)      |> Map.put_new("context", "pleroma:fakecontext") -    |> Map.put_new("context_id", -1)      |> lazy_put_object_defaults(true)    end    def lazy_put_activity_defaults(map, _fake?) do -    %{data: %{"id" => context}, id: context_id} = create_context(map["context"]) +    context = maybe_create_context(map["context"])      map      |> Map.put_new_lazy("id", &generate_activity_id/0)      |> Map.put_new_lazy("published", &make_date/0)      |> Map.put_new("context", context) -    |> Map.put_new("context_id", context_id)      |> lazy_put_object_defaults(false)    end @@ -226,7 +211,6 @@ defmodule Pleroma.Web.ActivityPub.Utils do        |> Map.put_new("id", "pleroma:fake_object_id")        |> Map.put_new_lazy("published", &make_date/0)        |> Map.put_new("context", activity["context"]) -      |> Map.put_new("context_id", activity["context_id"])        |> Map.put_new("fake", true)      %{activity | "object" => object} @@ -239,7 +223,6 @@ defmodule Pleroma.Web.ActivityPub.Utils do        |> Map.put_new_lazy("id", &generate_object_id/0)        |> Map.put_new_lazy("published", &make_date/0)        |> Map.put_new("context", activity["context"]) -      |> Map.put_new("context_id", activity["context_id"])      %{activity | "object" => object}    end @@ -344,21 +327,29 @@ defmodule Pleroma.Web.ActivityPub.Utils do            {:ok, Object.t()} | {:error, Ecto.Changeset.t()}    def add_emoji_reaction_to_object( -        %Activity{data: %{"content" => emoji, "actor" => actor}}, +        %Activity{data: %{"content" => emoji, "actor" => actor}} = activity,          object        ) do      reactions = get_cached_emoji_reactions(object) +    emoji = Pleroma.Emoji.maybe_strip_name(emoji) +    url = maybe_emoji_url(emoji, activity)      new_reactions = -      case Enum.find_index(reactions, fn [candidate, _] -> emoji == candidate end) do +      case Enum.find_index(reactions, fn [candidate, _, candidate_url] -> +             if is_nil(candidate_url) do +               emoji == candidate +             else +               url == candidate_url +             end +           end) do          nil -> -          reactions ++ [[emoji, [actor]]] +          reactions ++ [[emoji, [actor], url]]          index ->            List.update_at(              reactions,              index, -            fn [emoji, users] -> [emoji, Enum.uniq([actor | users])] end +            fn [emoji, users, url] -> [emoji, Enum.uniq([actor | users]), url] end            )        end @@ -367,18 +358,40 @@ defmodule Pleroma.Web.ActivityPub.Utils do      update_element_in_object("reaction", new_reactions, object, count)    end +  defp maybe_emoji_url( +         name, +         %Activity{ +           data: %{ +             "tag" => [ +               %{"type" => "Emoji", "name" => name, "icon" => %{"url" => url}} +             ] +           } +         } +       ), +       do: url + +  defp maybe_emoji_url(_, _), do: nil +    def emoji_count(reactions_list) do -    Enum.reduce(reactions_list, 0, fn [_, users], acc -> acc + length(users) end) +    Enum.reduce(reactions_list, 0, fn [_, users, _], acc -> acc + length(users) end)    end    def remove_emoji_reaction_from_object( -        %Activity{data: %{"content" => emoji, "actor" => actor}}, +        %Activity{data: %{"content" => emoji, "actor" => actor}} = activity,          object        ) do +    emoji = Pleroma.Emoji.maybe_strip_name(emoji)      reactions = get_cached_emoji_reactions(object) +    url = maybe_emoji_url(emoji, activity)      new_reactions = -      case Enum.find_index(reactions, fn [candidate, _] -> emoji == candidate end) do +      case Enum.find_index(reactions, fn [candidate, _, candidate_url] -> +             if is_nil(candidate_url) do +               emoji == candidate +             else +               url == candidate_url +             end +           end) do          nil ->            reactions @@ -386,9 +399,9 @@ defmodule Pleroma.Web.ActivityPub.Utils do            List.update_at(              reactions,              index, -            fn [emoji, users] -> [emoji, List.delete(users, actor)] end +            fn [emoji, users, url] -> [emoji, List.delete(users, actor), url] end            ) -          |> Enum.reject(fn [_, users] -> Enum.empty?(users) end) +          |> Enum.reject(fn [_, users, _] -> Enum.empty?(users) end)        end      count = emoji_count(new_reactions) @@ -396,11 +409,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do    end    def get_cached_emoji_reactions(object) do -    if is_list(object.data["reactions"]) do -      object.data["reactions"] -    else -      [] -    end +    Object.get_emoji_reactions(object)    end    @spec add_like_to_object(Activity.t(), Object.t()) :: @@ -508,17 +517,37 @@ defmodule Pleroma.Web.ActivityPub.Utils do    def get_latest_reaction(internal_activity_id, %{ap_id: ap_id}, emoji) do      %{data: %{"object" => object_ap_id}} = Activity.get_by_id(internal_activity_id) +    emoji = Pleroma.Emoji.maybe_quote(emoji)      "EmojiReact"      |> Activity.Queries.by_type()      |> where(actor: ^ap_id) -    |> where([activity], fragment("?->>'content' = ?", activity.data, ^emoji)) +    |> custom_emoji_discriminator(emoji)      |> Activity.Queries.by_object_id(object_ap_id)      |> order_by([activity], fragment("? desc nulls last", activity.id))      |> limit(1)      |> Repo.one()    end +  defp custom_emoji_discriminator(query, emoji) do +    if String.contains?(emoji, "@") do +      stripped = Pleroma.Emoji.maybe_strip_name(emoji) +      [name, domain] = String.split(stripped, "@") +      domain_pattern = "%/" <> domain <> "/%" +      emoji_pattern = Pleroma.Emoji.maybe_quote(name) + +      query +      |> where([activity], fragment("?->>'content' = ? +        AND EXISTS ( +          SELECT FROM jsonb_array_elements(?->'tag') elem +          WHERE elem->>'id' ILIKE ? +        )", activity.data, ^emoji_pattern, activity.data, ^domain_pattern)) +    else +      query +      |> where([activity], fragment("?->>'content' = ?", activity.data, ^emoji)) +    end +  end +    #### Announce-related helpers    @doc """ @@ -718,20 +747,24 @@ defmodule Pleroma.Web.ActivityPub.Utils do      Enum.map(statuses || [], &build_flag_object/1)    end -  defp build_flag_object(%Activity{data: %{"id" => id}, object: %{data: data}}) do -    activity_actor = User.get_by_ap_id(data["actor"]) +  defp build_flag_object(%Activity{} = activity) do +    object = Object.normalize(activity, fetch: false) -    %{ -      "type" => "Note", -      "id" => id, -      "content" => data["content"], -      "published" => data["published"], -      "actor" => -        AccountView.render( -          "show.json", -          %{user: activity_actor, skip_visibility_check: true} -        ) -    } +    # Do not allow people to report Creates. Instead, report the Object that is Created. +    if activity.data["type"] != "Create" do +      build_flag_object_with_actor_and_id( +        object, +        User.get_by_ap_id(activity.data["actor"]), +        activity.data["id"] +      ) +    else +      build_flag_object(object) +    end +  end + +  defp build_flag_object(%Object{} = object) do +    actor = User.get_by_ap_id(object.data["actor"]) +    build_flag_object_with_actor_and_id(object, actor, object.data["id"])    end    defp build_flag_object(act) when is_map(act) or is_binary(act) do @@ -743,12 +776,12 @@ defmodule Pleroma.Web.ActivityPub.Utils do        end      case Activity.get_by_ap_id_with_object(id) do -      %Activity{} = activity -> -        build_flag_object(activity) +      %Activity{object: object} = _ -> +        build_flag_object(object)        nil -> -        if activity = Activity.get_by_object_ap_id_with_object(id) do -          build_flag_object(activity) +        if %Object{} = object = Object.get_by_ap_id(id) do +          build_flag_object(object)          else            %{"id" => id, "deleted" => true}          end @@ -757,6 +790,20 @@ defmodule Pleroma.Web.ActivityPub.Utils do    defp build_flag_object(_), do: [] +  defp build_flag_object_with_actor_and_id(%Object{data: data}, actor, id) do +    %{ +      "type" => "Note", +      "id" => id, +      "content" => data["content"], +      "published" => data["published"], +      "actor" => +        AccountView.render( +          "show.json", +          %{user: actor, skip_visibility_check: true} +        ) +    } +  end +    #### Report-related helpers    def get_reports(params, page, page_size) do      params = @@ -771,22 +818,21 @@ defmodule Pleroma.Web.ActivityPub.Utils do      ActivityPub.fetch_activities([], params, :offset)    end -  def update_report_state(%Activity{} = activity, state) -      when state in @strip_status_report_states do -    {:ok, stripped_activity} = strip_report_status_data(activity) +  defp maybe_strip_report_status(data, state) do +    with true <- Config.get([:instance, :report_strip_status]), +         true <- state in @strip_status_report_states, +         {:ok, stripped_activity} = strip_report_status_data(%Activity{data: data}) do +      data |> Map.put("object", stripped_activity.data["object"]) +    else +      _ -> data +    end +  end +  def update_report_state(%Activity{} = activity, state) when state in @supported_report_states do      new_data =        activity.data        |> Map.put("state", state) -      |> Map.put("object", stripped_activity.data["object"]) - -    activity -    |> Changeset.change(data: new_data) -    |> Repo.update() -  end - -  def update_report_state(%Activity{} = activity, state) when state in @supported_report_states do -    new_data = Map.put(activity.data, "state", state) +      |> maybe_strip_report_status(state)      activity      |> Changeset.change(data: new_data) @@ -811,9 +857,11 @@ defmodule Pleroma.Web.ActivityPub.Utils do      [actor | reported_activities] = activity.data["object"]      stripped_activities = -      Enum.map(reported_activities, fn -        act when is_map(act) -> act["id"] -        act when is_binary(act) -> act +      Enum.reduce(reported_activities, [], fn act, acc -> +        case ObjectID.cast(act) do +          {:ok, act} -> [act | acc] +          _ -> acc +        end        end)      new_data = put_in(activity.data, ["object"], [actor | stripped_activities]) diff --git a/lib/pleroma/web/activity_pub/views/object_view.ex b/lib/pleroma/web/activity_pub/views/object_view.ex index f848aba3a..63caa915c 100644 --- a/lib/pleroma/web/activity_pub/views/object_view.ex +++ b/lib/pleroma/web/activity_pub/views/object_view.ex @@ -29,11 +29,11 @@ defmodule Pleroma.Web.ActivityPub.ObjectView do    def render("object.json", %{object: %Activity{} = activity}) do      base = Pleroma.Web.ActivityPub.Utils.make_json_ld_header() -    object = Object.normalize(activity, fetch: false) +    object_id = Object.normalize(activity, id_only: true)      additional =        Transmogrifier.prepare_object(activity.data) -      |> Map.put("object", object.data["id"]) +      |> Map.put("object", object_id)      Map.merge(base, additional)    end diff --git a/lib/pleroma/web/activity_pub/views/user_view.ex b/lib/pleroma/web/activity_pub/views/user_view.ex index 52f6bb56d..24ee683ae 100644 --- a/lib/pleroma/web/activity_pub/views/user_view.ex +++ b/lib/pleroma/web/activity_pub/views/user_view.ex @@ -34,7 +34,6 @@ defmodule Pleroma.Web.ActivityPub.UserView do    def render("endpoints.json", _), do: %{}    def render("service.json", %{user: user}) do -    {:ok, user} = User.ensure_keys_present(user)      {:ok, _, public_key} = Keys.keys_from_pem(user.keys)      public_key = :public_key.pem_entry_encode(:SubjectPublicKeyInfo, public_key)      public_key = :public_key.pem_encode([public_key]) @@ -47,6 +46,7 @@ defmodule Pleroma.Web.ActivityPub.UserView do        "following" => "#{user.ap_id}/following",        "followers" => "#{user.ap_id}/followers",        "inbox" => "#{user.ap_id}/inbox", +      "outbox" => "#{user.ap_id}/outbox",        "name" => "Pleroma",        "summary" =>          "An internal service actor for this Pleroma instance.  No user-serviceable parts inside.", @@ -71,7 +71,6 @@ defmodule Pleroma.Web.ActivityPub.UserView do      do: render("service.json", %{user: user}) |> Map.put("preferredUsername", user.nickname)    def render("user.json", %{user: user}) do -    {:ok, user} = User.ensure_keys_present(user)      {:ok, _, public_key} = Keys.keys_from_pem(user.keys)      public_key = :public_key.pem_entry_encode(:SubjectPublicKeyInfo, public_key)      public_key = :public_key.pem_encode([public_key]) diff --git a/lib/pleroma/web/activity_pub/visibility.ex b/lib/pleroma/web/activity_pub/visibility.ex index 465f8a9b7..7c57f88f9 100644 --- a/lib/pleroma/web/activity_pub/visibility.ex +++ b/lib/pleroma/web/activity_pub/visibility.ex @@ -84,7 +84,10 @@ defmodule Pleroma.Web.ActivityPub.Visibility do        when module in [Activity, Object] do      x = [user.ap_id | User.following(user)]      y = [message.data["actor"]] ++ message.data["to"] ++ (message.data["cc"] || []) -    is_public?(message) || Enum.any?(x, &(&1 in y)) + +    user_is_local = user.local +    federatable = not is_local_public?(message) +    (is_public?(message) || Enum.any?(x, &(&1 in y))) and (user_is_local || federatable)    end    def entire_thread_visible_for_user?(%Activity{} = activity, %User{} = user) do  | 
