diff options
Diffstat (limited to 'lib')
46 files changed, 774 insertions, 285 deletions
| diff --git a/lib/pleroma/activity.ex b/lib/pleroma/activity.ex index d59403884..53beca5e6 100644 --- a/lib/pleroma/activity.ex +++ b/lib/pleroma/activity.ex @@ -184,40 +184,48 @@ defmodule Pleroma.Activity do      |> Repo.one()    end -  @spec get_by_id(String.t()) :: Activity.t() | nil -  def get_by_id(id) do -    case FlakeId.flake_id?(id) do -      true -> -        Activity -        |> where([a], a.id == ^id) -        |> restrict_deactivated_users() -        |> Repo.one() - -      _ -> -        nil -    end -  end - -  def get_by_id_with_user_actor(id) do -    case FlakeId.flake_id?(id) do -      true -> -        Activity -        |> where([a], a.id == ^id) -        |> with_preloaded_user_actor() -        |> Repo.one() - -      _ -> -        nil +  @doc """ +  Gets activity by ID, doesn't load activities from deactivated actors by default. +  """ +  @spec get_by_id(String.t(), keyword()) :: t() | nil +  def get_by_id(id, opts \\ [filter: [:restrict_deactivated]]), do: get_by_id_with_opts(id, opts) + +  @spec get_by_id_with_user_actor(String.t()) :: t() | nil +  def get_by_id_with_user_actor(id), do: get_by_id_with_opts(id, preload: [:user_actor]) + +  @spec get_by_id_with_object(String.t()) :: t() | nil +  def get_by_id_with_object(id), do: get_by_id_with_opts(id, preload: [:object]) + +  defp get_by_id_with_opts(id, opts) do +    if FlakeId.flake_id?(id) do +      query = Queries.by_id(id) + +      with_filters_query = +        if is_list(opts[:filter]) do +          Enum.reduce(opts[:filter], query, fn +            {:type, type}, acc -> Queries.by_type(acc, type) +            :restrict_deactivated, acc -> restrict_deactivated_users(acc) +            _, acc -> acc +          end) +        else +          query +        end + +      with_preloads_query = +        if is_list(opts[:preload]) do +          Enum.reduce(opts[:preload], with_filters_query, fn +            :user_actor, acc -> with_preloaded_user_actor(acc) +            :object, acc -> with_preloaded_object(acc) +            _, acc -> acc +          end) +        else +          with_filters_query +        end + +      Repo.one(with_preloads_query)      end    end -  def get_by_id_with_object(id) do -    Activity -    |> where(id: ^id) -    |> with_preloaded_object() -    |> Repo.one() -  end -    def all_by_ids_with_object(ids) do      Activity      |> where([a], a.id in ^ids) @@ -269,6 +277,11 @@ defmodule Pleroma.Activity do    def get_create_by_object_ap_id_with_object(_), do: nil +  @spec create_by_id_with_object(String.t()) :: t() | nil +  def create_by_id_with_object(id) do +    get_by_id_with_opts(id, preload: [:object], filter: [type: "Create"]) +  end +    defp get_in_reply_to_activity_from_object(%Object{data: %{"inReplyTo" => ap_id}}) do      get_create_by_object_ap_id_with_object(ap_id)    end @@ -368,12 +381,6 @@ defmodule Pleroma.Activity do      end    end -  @spec pinned_by_actor?(Activity.t()) :: boolean() -  def pinned_by_actor?(%Activity{} = activity) do -    actor = user_actor(activity) -    activity.id in actor.pinned_activities -  end -    @spec get_by_object_ap_id_with_object(String.t()) :: t() | nil    def get_by_object_ap_id_with_object(ap_id) when is_binary(ap_id) do      ap_id @@ -384,4 +391,13 @@ defmodule Pleroma.Activity do    end    def get_by_object_ap_id_with_object(_), do: nil + +  @spec add_by_params_query(String.t(), String.t(), String.t()) :: Ecto.Query.t() +  def add_by_params_query(object_id, actor, target) do +    object_id +    |> Queries.by_object_id() +    |> Queries.by_type("Add") +    |> Queries.by_actor(actor) +    |> where([a], fragment("?->>'target' = ?", a.data, ^target)) +  end  end diff --git a/lib/pleroma/activity/queries.ex b/lib/pleroma/activity/queries.ex index a6b02a889..4632651b0 100644 --- a/lib/pleroma/activity/queries.ex +++ b/lib/pleroma/activity/queries.ex @@ -14,6 +14,11 @@ defmodule Pleroma.Activity.Queries do    alias Pleroma.Activity    alias Pleroma.User +  @spec by_id(query(), String.t()) :: query() +  def by_id(query \\ Activity, id) do +    from(a in query, where: a.id == ^id) +  end +    @spec by_ap_id(query, String.t()) :: query    def by_ap_id(query \\ Activity, ap_id) do      from( diff --git a/lib/pleroma/config/release_runtime_provider.ex b/lib/pleroma/config/release_runtime_provider.ex index 8227195dc..e5e9d3dcd 100644 --- a/lib/pleroma/config/release_runtime_provider.ex +++ b/lib/pleroma/config/release_runtime_provider.ex @@ -1,6 +1,6 @@  defmodule Pleroma.Config.ReleaseRuntimeProvider do    @moduledoc """ -  Imports `runtime.exs` and `{env}.exported_from_db.secret.exs` for elixir releases. +  Imports runtime config and `{env}.exported_from_db.secret.exs` for releases.    """    @behaviour Config.Provider @@ -8,10 +8,11 @@ defmodule Pleroma.Config.ReleaseRuntimeProvider do    def init(opts), do: opts    @impl true -  def load(config, _opts) do +  def load(config, opts) do      with_defaults = Config.Reader.merge(config, Pleroma.Config.Holder.release_defaults()) -    config_path = System.get_env("PLEROMA_CONFIG_PATH") || "/etc/pleroma/config.exs" +    config_path = +      opts[:config_path] || System.get_env("PLEROMA_CONFIG_PATH") || "/etc/pleroma/config.exs"      with_runtime_config =        if File.exists?(config_path) do @@ -24,7 +25,7 @@ defmodule Pleroma.Config.ReleaseRuntimeProvider do          warning = [            IO.ANSI.red(),            IO.ANSI.bright(), -          "!!! #{config_path} not found! Please ensure it exists and that PLEROMA_CONFIG_PATH is unset or points to an existing file", +          "!!! Config path is not declared! Please ensure it exists and that PLEROMA_CONFIG_PATH is unset or points to an existing file",            IO.ANSI.reset()          ] @@ -33,13 +34,14 @@ defmodule Pleroma.Config.ReleaseRuntimeProvider do        end      exported_config_path = -      config_path -      |> Path.dirname() -      |> Path.join("prod.exported_from_db.secret.exs") +      opts[:exported_config_path] || +        config_path +        |> Path.dirname() +        |> Path.join("#{Pleroma.Config.get(:env)}.exported_from_db.secret.exs")      with_exported =        if File.exists?(exported_config_path) do -        exported_config = Config.Reader.read!(with_runtime_config) +        exported_config = Config.Reader.read!(exported_config_path)          Config.Reader.merge(with_runtime_config, exported_config)        else          with_runtime_config diff --git a/lib/pleroma/config_db.ex b/lib/pleroma/config_db.ex index b874e0e37..cb57673e3 100644 --- a/lib/pleroma/config_db.ex +++ b/lib/pleroma/config_db.ex @@ -387,6 +387,6 @@ defmodule Pleroma.ConfigDB do    @spec module_name?(String.t()) :: boolean()    def module_name?(string) do      Regex.match?(~r/^(Pleroma|Phoenix|Tesla|Quack|Ueberauth|Swoosh)\./, string) or -      string in ["Oban", "Ueberauth", "ExSyslogger"] +      string in ["Oban", "Ueberauth", "ExSyslogger", "ConcurrentLimiter"]    end  end diff --git a/lib/pleroma/object/containment.ex b/lib/pleroma/object/containment.ex index fb0398f92..040537acf 100644 --- a/lib/pleroma/object/containment.ex +++ b/lib/pleroma/object/containment.ex @@ -71,6 +71,14 @@ defmodule Pleroma.Object.Containment do      compare_uris(id_uri, other_uri)    end +  # Mastodon pin activities don't have an id, so we check the object field, which will be pinned. +  def contain_origin_from_id(id, %{"object" => object}) when is_binary(object) do +    id_uri = URI.parse(id) +    object_uri = URI.parse(object) + +    compare_uris(id_uri, object_uri) +  end +    def contain_origin_from_id(_id, _data), do: :error    def contain_child(%{"object" => %{"id" => id, "attributedTo" => _} = object}), diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index c1aa0f716..b78777141 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -99,6 +99,7 @@ defmodule Pleroma.User do      field(:local, :boolean, default: true)      field(:follower_address, :string)      field(:following_address, :string) +    field(:featured_address, :string)      field(:search_rank, :float, virtual: true)      field(:search_type, :integer, virtual: true)      field(:tags, {:array, :string}, default: []) @@ -130,7 +131,6 @@ defmodule Pleroma.User do      field(:hide_followers, :boolean, default: false)      field(:hide_follows, :boolean, default: false)      field(:hide_favorites, :boolean, default: true) -    field(:pinned_activities, {:array, :string}, default: [])      field(:email_notifications, :map, default: %{"digest" => false})      field(:mascot, :map, default: nil)      field(:emoji, :map, default: %{}) @@ -148,6 +148,7 @@ defmodule Pleroma.User do      field(:accepts_chat_messages, :boolean, default: nil)      field(:last_active_at, :naive_datetime)      field(:disclose_client, :boolean, default: true) +    field(:pinned_objects, :map, default: %{})      embeds_one(        :notification_settings, @@ -372,8 +373,10 @@ defmodule Pleroma.User do    end    # Should probably be renamed or removed +  @spec ap_id(User.t()) :: String.t()    def ap_id(%User{nickname: nickname}), do: "#{Web.base_url()}/users/#{nickname}" +  @spec ap_followers(User.t()) :: String.t()    def ap_followers(%User{follower_address: fa}) when is_binary(fa), do: fa    def ap_followers(%User{} = user), do: "#{ap_id(user)}/followers" @@ -381,6 +384,11 @@ defmodule Pleroma.User do    def ap_following(%User{following_address: fa}) when is_binary(fa), do: fa    def ap_following(%User{} = user), do: "#{ap_id(user)}/following" +  @spec ap_featured_collection(User.t()) :: String.t() +  def ap_featured_collection(%User{featured_address: fa}) when is_binary(fa), do: fa + +  def ap_featured_collection(%User{} = user), do: "#{ap_id(user)}/collections/featured" +    defp truncate_fields_param(params) do      if Map.has_key?(params, :fields) do        Map.put(params, :fields, Enum.map(params[:fields], &truncate_field/1)) @@ -443,6 +451,7 @@ defmodule Pleroma.User do          :uri,          :follower_address,          :following_address, +        :featured_address,          :hide_followers,          :hide_follows,          :hide_followers_count, @@ -454,7 +463,8 @@ defmodule Pleroma.User do          :invisible,          :actor_type,          :also_known_as, -        :accepts_chat_messages +        :accepts_chat_messages, +        :pinned_objects        ]      )      |> cast(params, [:name], empty_values: []) @@ -686,7 +696,7 @@ defmodule Pleroma.User do      |> validate_format(:nickname, local_nickname_regex())      |> put_ap_id()      |> unique_constraint(:ap_id) -    |> put_following_and_follower_address() +    |> put_following_and_follower_and_featured_address()    end    def register_changeset(struct, params \\ %{}, opts \\ []) do @@ -747,7 +757,7 @@ defmodule Pleroma.User do      |> put_password_hash      |> put_ap_id()      |> unique_constraint(:ap_id) -    |> put_following_and_follower_address() +    |> put_following_and_follower_and_featured_address()    end    def maybe_validate_required_email(changeset, true), do: changeset @@ -765,11 +775,16 @@ defmodule Pleroma.User do      put_change(changeset, :ap_id, ap_id)    end -  defp put_following_and_follower_address(changeset) do -    followers = ap_followers(%User{nickname: get_field(changeset, :nickname)}) +  defp put_following_and_follower_and_featured_address(changeset) do +    user = %User{nickname: get_field(changeset, :nickname)} +    followers = ap_followers(user) +    following = ap_following(user) +    featured = ap_featured_collection(user)      changeset      |> put_change(:follower_address, followers) +    |> put_change(:following_address, following) +    |> put_change(:featured_address, featured)    end    defp autofollow_users(user) do @@ -2343,45 +2358,35 @@ defmodule Pleroma.User do      cast(user, %{is_approved: approved?}, [:is_approved])    end -  def add_pinnned_activity(user, %Pleroma.Activity{id: id}) do -    if id not in user.pinned_activities do -      max_pinned_statuses = Config.get([:instance, :max_pinned_statuses], 0) -      params = %{pinned_activities: user.pinned_activities ++ [id]} - -      # if pinned activity was scheduled for deletion, we remove job -      if expiration = Pleroma.Workers.PurgeExpiredActivity.get_expiration(id) do -        Oban.cancel_job(expiration.id) -      end +  @spec add_pinned_object_id(User.t(), String.t()) :: {:ok, User.t()} | {:error, term()} +  def add_pinned_object_id(%User{} = user, object_id) do +    if !user.pinned_objects[object_id] do +      params = %{pinned_objects: Map.put(user.pinned_objects, object_id, NaiveDateTime.utc_now())}        user -      |> cast(params, [:pinned_activities]) -      |> validate_length(:pinned_activities, -        max: max_pinned_statuses, -        message: "You have already pinned the maximum number of statuses" -      ) +      |> cast(params, [:pinned_objects]) +      |> validate_change(:pinned_objects, fn :pinned_objects, pinned_objects -> +        max_pinned_statuses = Config.get([:instance, :max_pinned_statuses], 0) + +        if Enum.count(pinned_objects) <= max_pinned_statuses do +          [] +        else +          [pinned_objects: "You have already pinned the maximum number of statuses"] +        end +      end)      else        change(user)      end      |> update_and_set_cache()    end -  def remove_pinnned_activity(user, %Pleroma.Activity{id: id, data: data}) do -    params = %{pinned_activities: List.delete(user.pinned_activities, id)} - -    # if pinned activity was scheduled for deletion, we reschedule it for deletion -    if data["expires_at"] do -      # MRF.ActivityExpirationPolicy used UTC timestamps for expires_at in original implementation -      {:ok, expires_at} = -        data["expires_at"] |> Pleroma.EctoType.ActivityPub.ObjectValidators.DateTime.cast() - -      Pleroma.Workers.PurgeExpiredActivity.enqueue(%{ -        activity_id: id, -        expires_at: expires_at -      }) -    end - +  @spec remove_pinned_object_id(User.t(), String.t()) :: {:ok, t()} | {:error, term()} +  def remove_pinned_object_id(%User{} = user, object_id) do      user -    |> cast(params, [:pinned_activities]) +    |> cast( +      %{pinned_objects: Map.delete(user.pinned_objects, object_id)}, +      [:pinned_objects] +    )      |> update_and_set_cache()    end diff --git a/lib/pleroma/utils.ex b/lib/pleroma/utils.ex index bc0c95332..a446d3ae6 100644 --- a/lib/pleroma/utils.ex +++ b/lib/pleroma/utils.ex @@ -11,6 +11,8 @@ defmodule Pleroma.Utils do      eperm epipe erange erofs espipe esrch estale etxtbsy exdev    )a +  @repo_timeout Pleroma.Config.get([Pleroma.Repo, :timeout], 15_000) +    def compile_dir(dir) when is_binary(dir) do      dir      |> File.ls!() @@ -63,4 +65,21 @@ defmodule Pleroma.Utils do    end    def posix_error_message(_), do: "" + +  @doc """ +  Returns [timeout: integer] suitable for passing as an option to Repo functions. + +  This function detects if the execution was triggered from IEx shell, Mix task, or +  ./bin/pleroma_ctl and sets the timeout to :infinity, else returns the default timeout value. +  """ +  @spec query_timeout() :: [timeout: integer] +  def query_timeout do +    {parent, _, _, _} = Process.info(self(), :current_stacktrace) |> elem(1) |> Enum.fetch!(2) + +    cond do +      parent |> to_string |> String.starts_with?("Elixir.Mix.Task") -> [timeout: :infinity] +      parent == :erl_eval -> [timeout: :infinity] +      true -> [timeout: @repo_timeout] +    end +  end  end diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index efbf92c70..d0051d1cb 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -630,7 +630,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do        |> Map.put(:type, ["Create", "Announce"])        |> Map.put(:user, reading_user)        |> Map.put(:actor_id, user.ap_id) -      |> Map.put(:pinned_activity_ids, user.pinned_activities) +      |> Map.put(:pinned_object_ids, Map.keys(user.pinned_objects))      params =        if User.blocks?(reading_user, user) do @@ -1075,8 +1075,18 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do    defp restrict_unlisted(query, _), do: query -  defp restrict_pinned(query, %{pinned: true, pinned_activity_ids: ids}) do -    from(activity in query, where: activity.id in ^ids) +  defp restrict_pinned(query, %{pinned: true, pinned_object_ids: ids}) do +    from( +      [activity, object: o] in query, +      where: +        fragment( +          "(?)->>'type' = 'Create' and coalesce((?)->'object'->>'id', (?)->>'object') = any (?)", +          activity.data, +          activity.data, +          activity.data, +          ^ids +        ) +    )    end    defp restrict_pinned(query, _), do: query @@ -1419,6 +1429,9 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do      invisible = data["invisible"] || false      actor_type = data["type"] || "Person" +    featured_address = data["featured"] +    {:ok, pinned_objects} = fetch_and_prepare_featured_from_ap_id(featured_address) +      public_key =        if is_map(data["publicKey"]) && is_binary(data["publicKey"]["publicKeyPem"]) do          data["publicKey"]["publicKeyPem"] @@ -1447,13 +1460,15 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do        name: data["name"],        follower_address: data["followers"],        following_address: data["following"], +      featured_address: featured_address,        bio: data["summary"] || "",        actor_type: actor_type,        also_known_as: Map.get(data, "alsoKnownAs", []),        public_key: public_key,        inbox: data["inbox"],        shared_inbox: shared_inbox, -      accepts_chat_messages: accepts_chat_messages +      accepts_chat_messages: accepts_chat_messages, +      pinned_objects: pinned_objects      }      # nickname can be nil because of virtual actors @@ -1591,6 +1606,41 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do      end    end +  def pin_data_from_featured_collection(%{ +        "type" => type, +        "orderedItems" => objects +      }) +      when type in ["OrderedCollection", "Collection"] do +    Map.new(objects, fn %{"id" => object_ap_id} -> {object_ap_id, NaiveDateTime.utc_now()} end) +  end + +  def fetch_and_prepare_featured_from_ap_id(nil) do +    {:ok, %{}} +  end + +  def fetch_and_prepare_featured_from_ap_id(ap_id) do +    with {:ok, data} <- Fetcher.fetch_and_contain_remote_object_from_id(ap_id) do +      {:ok, pin_data_from_featured_collection(data)} +    else +      e -> +        Logger.error("Could not decode featured collection at fetch #{ap_id}, #{inspect(e)}") +        {:ok, %{}} +    end +  end + +  def pinned_fetch_task(nil), do: nil + +  def pinned_fetch_task(%{pinned_objects: pins}) do +    if Enum.all?(pins, fn {ap_id, _} -> +         Object.get_cached_by_ap_id(ap_id) || +           match?({:ok, _object}, Fetcher.fetch_object_from_id(ap_id)) +       end) do +      :ok +    else +      :error +    end +  end +    def make_user_from_ap_id(ap_id) do      user = User.get_cached_by_ap_id(ap_id) @@ -1598,6 +1648,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub 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) +          if user do            user            |> User.remote_user_changeset(data) diff --git a/lib/pleroma/web/activity_pub/activity_pub_controller.ex b/lib/pleroma/web/activity_pub/activity_pub_controller.ex index 9d3dcc7f9..5aa3b281a 100644 --- a/lib/pleroma/web/activity_pub/activity_pub_controller.ex +++ b/lib/pleroma/web/activity_pub/activity_pub_controller.ex @@ -543,4 +543,12 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do        |> json(object.data)      end    end + +  def pinned(conn, %{"nickname" => nickname}) do +    with %User{} = user <- User.get_cached_by_nickname(nickname) do +      conn +      |> put_resp_header("content-type", "application/activity+json") +      |> json(UserView.render("featured.json", %{user: user})) +    end +  end  end diff --git a/lib/pleroma/web/activity_pub/builder.ex b/lib/pleroma/web/activity_pub/builder.ex index f56bfc600..91a45836f 100644 --- a/lib/pleroma/web/activity_pub/builder.ex +++ b/lib/pleroma/web/activity_pub/builder.ex @@ -273,4 +273,36 @@ defmodule Pleroma.Web.ActivityPub.Builder do         "context" => object.data["context"]       }, []}    end + +  @spec pin(User.t(), Object.t()) :: {:ok, map(), keyword()} +  def pin(%User{} = user, object) do +    {:ok, +     %{ +       "id" => Utils.generate_activity_id(), +       "target" => pinned_url(user.nickname), +       "object" => object.data["id"], +       "actor" => user.ap_id, +       "type" => "Add", +       "to" => [Pleroma.Constants.as_public()], +       "cc" => [user.follower_address] +     }, []} +  end + +  @spec unpin(User.t(), Object.t()) :: {:ok, map, keyword()} +  def unpin(%User{} = user, object) do +    {:ok, +     %{ +       "id" => Utils.generate_activity_id(), +       "target" => pinned_url(user.nickname), +       "object" => object.data["id"], +       "actor" => user.ap_id, +       "type" => "Remove", +       "to" => [Pleroma.Constants.as_public()], +       "cc" => [user.follower_address] +     }, []} +  end + +  defp pinned_url(nickname) when is_binary(nickname) do +    Pleroma.Web.Router.Helpers.activity_pub_url(Pleroma.Web.Endpoint, :pinned, nickname) +  end  end diff --git a/lib/pleroma/web/activity_pub/mrf/follow_bot_policy.ex b/lib/pleroma/web/activity_pub/mrf/follow_bot_policy.ex new file mode 100644 index 000000000..7307c9c14 --- /dev/null +++ b/lib/pleroma/web/activity_pub/mrf/follow_bot_policy.ex @@ -0,0 +1,59 @@ +defmodule Pleroma.Web.ActivityPub.MRF.FollowBotPolicy do +  @behaviour Pleroma.Web.ActivityPub.MRF +  alias Pleroma.Config +  alias Pleroma.User +  alias Pleroma.Web.CommonAPI + +  require Logger + +  @impl true +  def filter(message) do +    with follower_nickname <- Config.get([:mrf_follow_bot, :follower_nickname]), +         %User{actor_type: "Service"} = follower <- +           User.get_cached_by_nickname(follower_nickname), +         %{"type" => "Create", "object" => %{"type" => "Note"}} <- message do +      try_follow(follower, message) +    else +      nil -> +        Logger.warn( +          "#{__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." +        ) + +        {:ok, message} + +      _ -> +        {:ok, message} +    end +  end + +  defp try_follow(follower, message) do +    to = Map.get(message, "to", []) +    cc = Map.get(message, "cc", []) +    actor = [message["actor"]] + +    Enum.concat([to, cc, actor]) +    |> List.flatten() +    |> Enum.uniq() +    |> User.get_all_by_ap_id() +    |> Enum.each(fn user -> +      with false <- user.local, +           false <- User.following?(follower, user), +           false <- User.locked?(user), +           false <- (user.bio || "") |> String.downcase() |> String.contains?("nobot") do +        Logger.debug( +          "#{__MODULE__}: Follow request from #{follower.nickname} to #{user.nickname}" +        ) + +        CommonAPI.follow(follower, user) +      end +    end) + +    {:ok, message} +  end + +  @impl true +  def describe do +    {:ok, %{}} +  end +end diff --git a/lib/pleroma/web/activity_pub/object_validator.ex b/lib/pleroma/web/activity_pub/object_validator.ex index 297c19cc0..1dce33f1a 100644 --- a/lib/pleroma/web/activity_pub/object_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validator.ex @@ -17,6 +17,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do    alias Pleroma.Object.Containment    alias Pleroma.User    alias Pleroma.Web.ActivityPub.ObjectValidators.AcceptRejectValidator +  alias Pleroma.Web.ActivityPub.ObjectValidators.AddRemoveValidator    alias Pleroma.Web.ActivityPub.ObjectValidators.AnnounceValidator    alias Pleroma.Web.ActivityPub.ObjectValidators.AnswerValidator    alias Pleroma.Web.ActivityPub.ObjectValidators.ArticleNoteValidator @@ -37,37 +38,6 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do    @impl true    def validate(object, meta) -  def validate(%{"type" => type} = object, meta) -      when type in ~w[Accept Reject] do -    with {:ok, object} <- -           object -           |> AcceptRejectValidator.cast_and_validate() -           |> Ecto.Changeset.apply_action(:insert) do -      object = stringify_keys(object) -      {:ok, object, meta} -    end -  end - -  def validate(%{"type" => "Event"} = object, meta) do -    with {:ok, object} <- -           object -           |> EventValidator.cast_and_validate() -           |> Ecto.Changeset.apply_action(:insert) do -      object = stringify_keys(object) -      {:ok, object, meta} -    end -  end - -  def validate(%{"type" => "Follow"} = object, meta) do -    with {:ok, object} <- -           object -           |> FollowValidator.cast_and_validate() -           |> Ecto.Changeset.apply_action(:insert) do -      object = stringify_keys(object) -      {:ok, object, meta} -    end -  end -    def validate(%{"type" => "Block"} = block_activity, meta) do      with {:ok, block_activity} <-             block_activity @@ -87,16 +57,6 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do      end    end -  def validate(%{"type" => "Update"} = update_activity, meta) do -    with {: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} -    end -  end -    def validate(%{"type" => "Undo"} = object, meta) do      with {:ok, object} <-             object @@ -123,76 +83,6 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do      end    end -  def validate(%{"type" => "Like"} = object, meta) do -    with {:ok, object} <- -           object -           |> LikeValidator.cast_and_validate() -           |> Ecto.Changeset.apply_action(:insert) do -      object = stringify_keys(object) -      {:ok, object, meta} -    end -  end - -  def validate(%{"type" => "ChatMessage"} = object, meta) do -    with {:ok, object} <- -           object -           |> ChatMessageValidator.cast_and_validate() -           |> Ecto.Changeset.apply_action(:insert) do -      object = stringify_keys(object) -      {:ok, object, meta} -    end -  end - -  def validate(%{"type" => "Question"} = object, meta) do -    with {:ok, object} <- -           object -           |> QuestionValidator.cast_and_validate() -           |> Ecto.Changeset.apply_action(:insert) do -      object = stringify_keys(object) -      {:ok, object, meta} -    end -  end - -  def validate(%{"type" => type} = object, meta) when type in ~w[Audio Video] do -    with {:ok, object} <- -           object -           |> AudioVideoValidator.cast_and_validate() -           |> Ecto.Changeset.apply_action(:insert) do -      object = stringify_keys(object) -      {:ok, object, meta} -    end -  end - -  def validate(%{"type" => "Article"} = object, meta) do -    with {:ok, object} <- -           object -           |> ArticleNoteValidator.cast_and_validate() -           |> Ecto.Changeset.apply_action(:insert) do -      object = stringify_keys(object) -      {:ok, object, meta} -    end -  end - -  def validate(%{"type" => "Answer"} = object, meta) do -    with {:ok, object} <- -           object -           |> AnswerValidator.cast_and_validate() -           |> Ecto.Changeset.apply_action(:insert) do -      object = stringify_keys(object) -      {:ok, object, meta} -    end -  end - -  def validate(%{"type" => "EmojiReact"} = object, meta) do -    with {:ok, object} <- -           object -           |> EmojiReactValidator.cast_and_validate() -           |> Ecto.Changeset.apply_action(:insert) do -      object = stringify_keys(object) -      {:ok, object, meta} -    end -  end -    def validate(          %{"type" => "Create", "object" => %{"type" => "ChatMessage"} = object} = create_activity,          meta @@ -224,10 +114,60 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do      end    end -  def validate(%{"type" => "Announce"} = object, meta) do +  def validate(%{"type" => type} = object, meta) +      when type in ~w[Event Question Audio Video Article] do +    validator = +      case type do +        "Event" -> EventValidator +        "Question" -> QuestionValidator +        "Audio" -> AudioVideoValidator +        "Video" -> AudioVideoValidator +        "Article" -> ArticleNoteValidator +      end + +    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, meta} +    end +  end + +  def validate(%{"type" => type} = object, meta) +      when type in ~w[Accept Reject Follow Update Like EmojiReact Announce +      ChatMessage Answer] do +    validator = +      case type do +        "Accept" -> AcceptRejectValidator +        "Reject" -> AcceptRejectValidator +        "Follow" -> FollowValidator +        "Update" -> UpdateValidator +        "Like" -> LikeValidator +        "EmojiReact" -> EmojiReactValidator +        "Announce" -> AnnounceValidator +        "ChatMessage" -> ChatMessageValidator +        "Answer" -> AnswerValidator +      end + +    with {:ok, object} <- +           object +           |> validator.cast_and_validate() +           |> Ecto.Changeset.apply_action(:insert) do +      object = stringify_keys(object) +      {:ok, object, meta} +    end +  end + +  def validate(%{"type" => type} = object, meta) when type in ~w(Add Remove) do      with {:ok, object} <-             object -           |> AnnounceValidator.cast_and_validate() +           |> AddRemoveValidator.cast_and_validate()             |> Ecto.Changeset.apply_action(:insert) do        object = stringify_keys(object)        {:ok, object, meta} @@ -260,7 +200,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do    def cast_and_apply(o), do: {:error, {:validator_not_set, o}} -  # is_struct/1 isn't present in Elixir 1.8.x +  # is_struct/1 appears in Elixir 1.11    def stringify_keys(%{__struct__: _} = object) do      object      |> Map.from_struct() diff --git a/lib/pleroma/web/activity_pub/object_validators/accept_reject_validator.ex b/lib/pleroma/web/activity_pub/object_validators/accept_reject_validator.ex index d31e780c3..b577a1044 100644 --- a/lib/pleroma/web/activity_pub/object_validators/accept_reject_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/accept_reject_validator.ex @@ -27,7 +27,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AcceptRejectValidator do      |> cast(data, __schema__(:fields))    end -  def validate_data(cng) do +  defp validate_data(cng) do      cng      |> validate_required([:id, :type, :actor, :to, :cc, :object])      |> validate_inclusion(:type, ["Accept", "Reject"]) 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 new file mode 100644 index 000000000..f885aabe4 --- /dev/null +++ b/lib/pleroma/web/activity_pub/object_validators/add_remove_validator.ex @@ -0,0 +1,77 @@ +# 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.ObjectValidators.AddRemoveValidator do +  use Ecto.Schema + +  import Ecto.Changeset +  import Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations + +  require Pleroma.Constants + +  alias Pleroma.EctoType.ActivityPub.ObjectValidators +  alias Pleroma.User + +  @primary_key false + +  embedded_schema do +    field(:id, ObjectValidators.ObjectID, primary_key: true) +    field(:target) +    field(:object, ObjectValidators.ObjectID) +    field(:actor, ObjectValidators.ObjectID) +    field(:type) +    field(:to, ObjectValidators.Recipients, default: []) +    field(:cc, ObjectValidators.Recipients, default: []) +  end + +  def cast_and_validate(data) do +    {:ok, actor} = User.get_or_fetch_by_ap_id(data["actor"]) + +    {:ok, actor} = maybe_refetch_user(actor) + +    data +    |> maybe_fix_data_for_mastodon(actor) +    |> cast_data() +    |> validate_data(actor) +  end + +  defp maybe_fix_data_for_mastodon(data, actor) do +    # Mastodon sends pin/unpin objects without id, to, cc fields +    data +    |> Map.put_new("id", Pleroma.Web.ActivityPub.Utils.generate_activity_id()) +    |> Map.put_new("to", [Pleroma.Constants.as_public()]) +    |> Map.put_new("cc", [actor.follower_address]) +  end + +  defp cast_data(data) do +    cast(%__MODULE__{}, data, __schema__(:fields)) +  end + +  defp validate_data(changeset, actor) do +    changeset +    |> validate_required([:id, :target, :object, :actor, :type, :to, :cc]) +    |> validate_inclusion(:type, ~w(Add Remove)) +    |> validate_actor_presence() +    |> validate_collection_belongs_to_actor(actor) +    |> validate_object_presence() +  end + +  defp validate_collection_belongs_to_actor(changeset, actor) do +    validate_change(changeset, :target, fn :target, target -> +      if target == actor.featured_address do +        [] +      else +        [target: "collection doesn't belong to actor"] +      end +    end) +  end + +  defp maybe_refetch_user(%User{featured_address: address} = user) when is_binary(address) do +    {:ok, user} +  end + +  defp maybe_refetch_user(%User{ap_id: ap_id}) do +    Pleroma.Web.ActivityPub.Transmogrifier.upgrade_user_from_ap_id(ap_id) +  end +end diff --git a/lib/pleroma/web/activity_pub/object_validators/announce_validator.ex b/lib/pleroma/web/activity_pub/object_validators/announce_validator.ex index b08a33e68..576341790 100644 --- a/lib/pleroma/web/activity_pub/object_validators/announce_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/announce_validator.ex @@ -50,7 +50,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AnnounceValidator do      cng    end -  def validate_data(data_cng) do +  defp validate_data(data_cng) do      data_cng      |> validate_inclusion(:type, ["Announce"])      |> validate_required([:id, :type, :object, :actor, :to, :cc]) diff --git a/lib/pleroma/web/activity_pub/object_validators/answer_validator.ex b/lib/pleroma/web/activity_pub/object_validators/answer_validator.ex index 15e4413cd..c9bd9e42d 100644 --- a/lib/pleroma/web/activity_pub/object_validators/answer_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/answer_validator.ex @@ -50,7 +50,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AnswerValidator do      |> cast(data, __schema__(:fields))    end -  def validate_data(data_cng) do +  defp validate_data(data_cng) do      data_cng      |> validate_inclusion(:type, ["Answer"])      |> validate_required([:id, :inReplyTo, :name, :attributedTo, :actor]) diff --git a/lib/pleroma/web/activity_pub/object_validators/article_note_validator.ex b/lib/pleroma/web/activity_pub/object_validators/article_note_validator.ex index b0388ef3b..39ef6dc29 100644 --- a/lib/pleroma/web/activity_pub/object_validators/article_note_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/article_note_validator.ex @@ -9,6 +9,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNoteValidator do    alias Pleroma.Web.ActivityPub.ObjectValidators.AttachmentValidator    alias Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes    alias Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations +  alias Pleroma.Web.ActivityPub.ObjectValidators.TagValidator    alias Pleroma.Web.ActivityPub.Transmogrifier    import Ecto.Changeset @@ -22,8 +23,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNoteValidator do      field(:cc, ObjectValidators.Recipients, default: [])      field(:bto, ObjectValidators.Recipients, default: [])      field(:bcc, ObjectValidators.Recipients, default: []) -    # TODO: Write type -    field(:tag, {:array, :map}, default: []) +    embeds_many(:tag, TagValidator)      field(:type, :string)      field(:name, :string) @@ -90,11 +90,12 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNoteValidator do      data = fix(data)      struct -    |> cast(data, __schema__(:fields) -- [:attachment]) +    |> cast(data, __schema__(:fields) -- [:attachment, :tag])      |> cast_embed(:attachment) +    |> cast_embed(:tag)    end -  def validate_data(data_cng) do +  defp validate_data(data_cng) do      data_cng      |> validate_inclusion(:type, ["Article", "Note"])      |> validate_required([:id, :actor, :attributedTo, :type, :context, :context_id]) 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 3175427ad..4a0d1473d 100644 --- a/lib/pleroma/web/activity_pub/object_validators/attachment_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/attachment_validator.ex @@ -6,7 +6,6 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AttachmentValidator do    use Ecto.Schema    alias Pleroma.EctoType.ActivityPub.ObjectValidators -  alias Pleroma.Web.ActivityPub.ObjectValidators.UrlObjectValidator    import Ecto.Changeset @@ -90,7 +89,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AttachmentValidator do      end    end -  def validate_data(cng) do +  defp validate_data(cng) do      cng      |> validate_inclusion(:type, ~w[Document Audio Image Video])      |> validate_required([:mediaType, :url, :type]) diff --git a/lib/pleroma/web/activity_pub/object_validators/audio_video_validator.ex b/lib/pleroma/web/activity_pub/object_validators/audio_video_validator.ex index 4a96fef52..8a5a60526 100644 --- a/lib/pleroma/web/activity_pub/object_validators/audio_video_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/audio_video_validator.ex @@ -10,6 +10,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AudioVideoValidator do    alias Pleroma.Web.ActivityPub.ObjectValidators.AttachmentValidator    alias Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes    alias Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations +  alias Pleroma.Web.ActivityPub.ObjectValidators.TagValidator    alias Pleroma.Web.ActivityPub.Transmogrifier    import Ecto.Changeset @@ -23,8 +24,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AudioVideoValidator do      field(:cc, ObjectValidators.Recipients, default: [])      field(:bto, ObjectValidators.Recipients, default: [])      field(:bcc, ObjectValidators.Recipients, default: []) -    # TODO: Write type -    field(:tag, {:array, :map}, default: []) +    embeds_many(:tag, TagValidator)      field(:type, :string)      field(:name, :string) @@ -132,11 +132,12 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AudioVideoValidator do      data = fix(data)      struct -    |> cast(data, __schema__(:fields) -- [:attachment]) +    |> cast(data, __schema__(:fields) -- [:attachment, :tag])      |> cast_embed(:attachment) +    |> cast_embed(:tag)    end -  def validate_data(data_cng) do +  defp validate_data(data_cng) do      data_cng      |> validate_inclusion(:type, ["Audio", "Video"])      |> validate_required([:id, :actor, :attributedTo, :type, :context, :attachment]) diff --git a/lib/pleroma/web/activity_pub/object_validators/block_validator.ex b/lib/pleroma/web/activity_pub/object_validators/block_validator.ex index c5f77bb76..88948135f 100644 --- a/lib/pleroma/web/activity_pub/object_validators/block_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/block_validator.ex @@ -26,7 +26,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.BlockValidator do      |> cast(data, __schema__(:fields))    end -  def validate_data(cng) do +  defp validate_data(cng) do      cng      |> validate_required([:id, :type, :actor, :to, :cc, :object])      |> validate_inclusion(:type, ["Block"]) diff --git a/lib/pleroma/web/activity_pub/object_validators/chat_message_validator.ex b/lib/pleroma/web/activity_pub/object_validators/chat_message_validator.ex index 1189778f2..b153156b0 100644 --- a/lib/pleroma/web/activity_pub/object_validators/chat_message_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/chat_message_validator.ex @@ -67,7 +67,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ChatMessageValidator do      |> cast_embed(:attachment)    end -  def validate_data(data_cng) do +  defp validate_data(data_cng) do      data_cng      |> validate_inclusion(:type, ["ChatMessage"])      |> validate_required([:id, :actor, :to, :type, :published]) 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 093549a45..940430588 100644 --- a/lib/pleroma/web/activity_pub/object_validators/common_validations.ex +++ b/lib/pleroma/web/activity_pub/object_validators/common_validations.ex @@ -9,6 +9,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations do    alias Pleroma.Object    alias Pleroma.User +  @spec validate_any_presence(Ecto.Changeset.t(), [atom()]) :: Ecto.Changeset.t()    def validate_any_presence(cng, fields) do      non_empty =        fields @@ -29,6 +30,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations do      end    end +  @spec validate_actor_presence(Ecto.Changeset.t(), keyword()) :: Ecto.Changeset.t()    def validate_actor_presence(cng, options \\ []) do      field_name = Keyword.get(options, :field_name, :actor) @@ -47,6 +49,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations do      end)    end +  @spec validate_object_presence(Ecto.Changeset.t(), keyword()) :: Ecto.Changeset.t()    def validate_object_presence(cng, options \\ []) do      field_name = Keyword.get(options, :field_name, :object)      allowed_types = Keyword.get(options, :allowed_types, false) @@ -68,6 +71,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations do      end)    end +  @spec validate_object_or_user_presence(Ecto.Changeset.t(), keyword()) :: Ecto.Changeset.t()    def validate_object_or_user_presence(cng, options \\ []) do      field_name = Keyword.get(options, :field_name, :object)      options = Keyword.put(options, :field_name, field_name) @@ -83,6 +87,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations do      if actor_cng.valid?, do: actor_cng, else: object_cng    end +  @spec validate_host_match(Ecto.Changeset.t(), [atom()]) :: Ecto.Changeset.t()    def validate_host_match(cng, fields \\ [:id, :actor]) do      if same_domain?(cng, fields) do        cng @@ -95,6 +100,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations do      end    end +  @spec validate_fields_match(Ecto.Changeset.t(), [atom()]) :: Ecto.Changeset.t()    def validate_fields_match(cng, fields) do      if map_unique?(cng, fields) do        cng @@ -122,12 +128,14 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations do      end)    end +  @spec same_domain?(Ecto.Changeset.t(), [atom()]) :: boolean()    def same_domain?(cng, fields \\ [:actor, :object]) do      map_unique?(cng, fields, fn value -> URI.parse(value).host end)    end    # 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      actor = User.get_cached_by_ap_id(get_field(cng, :actor)) diff --git a/lib/pleroma/web/activity_pub/object_validators/create_chat_message_validator.ex b/lib/pleroma/web/activity_pub/object_validators/create_chat_message_validator.ex index 8384c16a7..7a31a99bf 100644 --- a/lib/pleroma/web/activity_pub/object_validators/create_chat_message_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/create_chat_message_validator.ex @@ -39,7 +39,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CreateChatMessageValidator do      |> validate_data(meta)    end -  def validate_data(cng, meta \\ []) do +  defp validate_data(cng, meta) do      cng      |> validate_required([:id, :actor, :to, :type, :object])      |> validate_inclusion(:type, ["Create"]) 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 bf56a918c..e06e442f4 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 @@ -79,7 +79,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CreateGenericValidator do      |> CommonFixes.fix_actor()    end -  def validate_data(cng, meta \\ []) do +  defp validate_data(cng, meta) do      cng      |> validate_required([:actor, :type, :object])      |> validate_inclusion(:type, ["Create"]) 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 fc1a79a72..7da67bf16 100644 --- a/lib/pleroma/web/activity_pub/object_validators/delete_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/delete_validator.ex @@ -53,7 +53,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.DeleteValidator do      Tombstone      Video    } -  def validate_data(cng) do +  defp validate_data(cng) do      cng      |> validate_required([:id, :type, :actor, :to, :cc, :object])      |> validate_inclusion(:type, ["Delete"]) 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 1906e597e..ec7566515 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 @@ -70,7 +70,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactValidator do      end    end -  def validate_data(data_cng) do +  defp validate_data(data_cng) do      data_cng      |> validate_inclusion(:type, ["EmojiReact"])      |> validate_required([:id, :type, :object, :actor, :context, :to, :cc, :content]) 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 2e26726f8..d42458ef5 100644 --- a/lib/pleroma/web/activity_pub/object_validators/event_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/event_validator.ex @@ -9,6 +9,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.EventValidator do    alias Pleroma.Web.ActivityPub.ObjectValidators.AttachmentValidator    alias Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes    alias Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations +  alias Pleroma.Web.ActivityPub.ObjectValidators.TagValidator    alias Pleroma.Web.ActivityPub.Transmogrifier    import Ecto.Changeset @@ -23,8 +24,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.EventValidator do      field(:cc, ObjectValidators.Recipients, default: [])      field(:bto, ObjectValidators.Recipients, default: [])      field(:bcc, ObjectValidators.Recipients, default: []) -    # TODO: Write type -    field(:tag, {:array, :map}, default: []) +    embeds_many(:tag, TagValidator)      field(:type, :string)      field(:name, :string) @@ -81,11 +81,12 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.EventValidator do      data = fix(data)      struct -    |> cast(data, __schema__(:fields) -- [:attachment]) +    |> cast(data, __schema__(:fields) -- [:attachment, :tag])      |> cast_embed(:attachment) +    |> cast_embed(:tag)    end -  def validate_data(data_cng) do +  defp validate_data(data_cng) do      data_cng      |> validate_inclusion(:type, ["Event"])      |> validate_required([:id, :actor, :attributedTo, :type, :context, :context_id]) diff --git a/lib/pleroma/web/activity_pub/object_validators/follow_validator.ex b/lib/pleroma/web/activity_pub/object_validators/follow_validator.ex index 6e428bacc..239cee5e7 100644 --- a/lib/pleroma/web/activity_pub/object_validators/follow_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/follow_validator.ex @@ -27,7 +27,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.FollowValidator do      |> cast(data, __schema__(:fields))    end -  def validate_data(cng) do +  defp validate_data(cng) do      cng      |> validate_required([:id, :type, :actor, :to, :cc, :object])      |> validate_inclusion(:type, ["Follow"]) diff --git a/lib/pleroma/web/activity_pub/object_validators/like_validator.ex b/lib/pleroma/web/activity_pub/object_validators/like_validator.ex index 30c40b238..509da507b 100644 --- a/lib/pleroma/web/activity_pub/object_validators/like_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/like_validator.ex @@ -76,7 +76,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator do      end    end -  def validate_data(data_cng) do +  defp validate_data(data_cng) do      data_cng      |> validate_inclusion(:type, ["Like"])      |> validate_required([:id, :type, :object, :actor, :context, :to, :cc]) 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 6b746c997..7012e2e1d 100644 --- a/lib/pleroma/web/activity_pub/object_validators/question_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/question_validator.ex @@ -10,6 +10,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.QuestionValidator do    alias Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes    alias Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations    alias Pleroma.Web.ActivityPub.ObjectValidators.QuestionOptionsValidator +  alias Pleroma.Web.ActivityPub.ObjectValidators.TagValidator    alias Pleroma.Web.ActivityPub.Transmogrifier    import Ecto.Changeset @@ -24,8 +25,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.QuestionValidator do      field(:cc, ObjectValidators.Recipients, default: [])      field(:bto, ObjectValidators.Recipients, default: [])      field(:bcc, ObjectValidators.Recipients, default: []) -    # TODO: Write type -    field(:tag, {:array, :map}, default: []) +    embeds_many(:tag, TagValidator)      field(:type, :string)      field(:content, :string)      field(:context, :string) @@ -93,13 +93,14 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.QuestionValidator do      data = fix(data)      struct -    |> cast(data, __schema__(:fields) -- [:anyOf, :oneOf, :attachment]) +    |> cast(data, __schema__(:fields) -- [:anyOf, :oneOf, :attachment, :tag])      |> cast_embed(:attachment)      |> cast_embed(:anyOf)      |> cast_embed(:oneOf) +    |> cast_embed(:tag)    end -  def validate_data(data_cng) do +  defp validate_data(data_cng) do      data_cng      |> validate_inclusion(:type, ["Question"])      |> validate_required([:id, :actor, :attributedTo, :type, :context, :context_id]) diff --git a/lib/pleroma/web/activity_pub/object_validators/tag_validator.ex b/lib/pleroma/web/activity_pub/object_validators/tag_validator.ex new file mode 100644 index 000000000..751021585 --- /dev/null +++ b/lib/pleroma/web/activity_pub/object_validators/tag_validator.ex @@ -0,0 +1,77 @@ +# 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.ObjectValidators.TagValidator do +  use Ecto.Schema + +  alias Pleroma.EctoType.ActivityPub.ObjectValidators + +  import Ecto.Changeset + +  @primary_key false +  embedded_schema do +    # Common +    field(:type, :string) +    field(:name, :string) + +    # Mention, Hashtag +    field(:href, ObjectValidators.Uri) + +    # Emoji +    embeds_one :icon, IconObjectValidator, primary_key: false do +      field(:type, :string) +      field(:url, ObjectValidators.Uri) +    end + +    field(:updated, ObjectValidators.DateTime) +    field(:id, ObjectValidators.Uri) +  end + +  def cast_and_validate(data) do +    data +    |> cast_data() +  end + +  def cast_data(data) do +    %__MODULE__{} +    |> changeset(data) +  end + +  def changeset(struct, %{"type" => "Mention"} = data) do +    struct +    |> cast(data, [:type, :name, :href]) +    |> validate_required([:type, :href]) +  end + +  def changeset(struct, %{"type" => "Hashtag", "name" => name} = data) do +    name = +      cond do +        "#" <> name -> name +        name -> name +      end +      |> String.downcase() + +    data = Map.put(data, "name", name) + +    struct +    |> cast(data, [:type, :name, :href]) +    |> validate_required([:type, :name]) +  end + +  def changeset(struct, %{"type" => "Emoji"} = data) do +    data = Map.put(data, "name", String.trim(data["name"], ":")) + +    struct +    |> cast(data, [:type, :name, :updated, :id]) +    |> cast_embed(:icon, with: &icon_changeset/2) +    |> validate_required([:type, :name, :icon]) +  end + +  def icon_changeset(struct, data) do +    struct +    |> cast(data, [:type, :url]) +    |> validate_inclusion(:type, ~w[Image]) +    |> validate_required([:type, :url]) +  end +end diff --git a/lib/pleroma/web/activity_pub/object_validators/undo_validator.ex b/lib/pleroma/web/activity_pub/object_validators/undo_validator.ex index 783a79ddb..e8af60ffa 100644 --- a/lib/pleroma/web/activity_pub/object_validators/undo_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/undo_validator.ex @@ -38,7 +38,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.UndoValidator do      |> cast(data, __schema__(:fields))    end -  def validate_data(data_cng) do +  defp validate_data(data_cng) do      data_cng      |> validate_inclusion(:type, ["Undo"])      |> validate_required([:id, :type, :object, :actor, :to, :cc]) 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 a66d41400..6bb1dc7fa 100644 --- a/lib/pleroma/web/activity_pub/object_validators/update_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/update_validator.ex @@ -28,7 +28,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.UpdateValidator do      |> cast(data, __schema__(:fields))    end -  def validate_data(cng) do +  defp validate_data(cng) do      cng      |> validate_required([:id, :type, :actor, :to, :cc, :object])      |> validate_inclusion(:type, ["Update"]) diff --git a/lib/pleroma/web/activity_pub/pipeline.ex b/lib/pleroma/web/activity_pub/pipeline.ex index 195596f94..a0f2e0312 100644 --- a/lib/pleroma/web/activity_pub/pipeline.ex +++ b/lib/pleroma/web/activity_pub/pipeline.ex @@ -7,6 +7,7 @@ defmodule Pleroma.Web.ActivityPub.Pipeline do    alias Pleroma.Config    alias Pleroma.Object    alias Pleroma.Repo +  alias Pleroma.Utils    alias Pleroma.Web.ActivityPub.ActivityPub    alias Pleroma.Web.ActivityPub.MRF    alias Pleroma.Web.ActivityPub.ObjectValidator @@ -24,7 +25,7 @@ defmodule Pleroma.Web.ActivityPub.Pipeline do    @spec common_pipeline(map(), keyword()) ::            {:ok, Activity.t() | Object.t(), keyword()} | {:error, any()}    def common_pipeline(object, meta) do -    case Repo.transaction(fn -> do_common_pipeline(object, meta) end) do +    case Repo.transaction(fn -> do_common_pipeline(object, meta) end, Utils.query_timeout()) do        {:ok, {:ok, activity, meta}} ->          @side_effects.handle_after_transaction(meta)          {:ok, activity, meta} @@ -40,19 +41,17 @@ defmodule Pleroma.Web.ActivityPub.Pipeline do      end    end -  def do_common_pipeline(object, meta) do -    with {_, {:ok, validated_object, meta}} <- -           {:validate_object, @object_validator.validate(object, meta)}, -         {_, {:ok, mrfd_object, meta}} <- -           {:mrf_object, @mrf.pipeline_filter(validated_object, meta)}, -         {_, {:ok, activity, meta}} <- -           {:persist_object, @activity_pub.persist(mrfd_object, meta)}, -         {_, {:ok, activity, meta}} <- -           {:execute_side_effects, @side_effects.handle(activity, meta)}, -         {_, {:ok, _}} <- {:federation, maybe_federate(activity, meta)} do -      {:ok, activity, meta} +  def do_common_pipeline(%{__struct__: _}, _meta), do: {:error, :is_struct} + +  def do_common_pipeline(message, meta) do +    with {_, {:ok, message, meta}} <- {:validate, @object_validator.validate(message, meta)}, +         {_, {:ok, message, meta}} <- {:mrf, @mrf.pipeline_filter(message, meta)}, +         {_, {:ok, message, meta}} <- {:persist, @activity_pub.persist(message, meta)}, +         {_, {:ok, message, meta}} <- {:side_effects, @side_effects.handle(message, meta)}, +         {_, {:ok, _}} <- {:federation, maybe_federate(message, meta)} do +      {:ok, message, meta}      else -      {:mrf_object, {:reject, message, _}} -> {:reject, message} +      {:mrf, {:reject, message, _}} -> {:reject, message}        e -> {:error, e}      end    end diff --git a/lib/pleroma/web/activity_pub/side_effects.ex b/lib/pleroma/web/activity_pub/side_effects.ex index 0b9a9f0c5..5fe143c2b 100644 --- a/lib/pleroma/web/activity_pub/side_effects.ex +++ b/lib/pleroma/web/activity_pub/side_effects.ex @@ -276,10 +276,10 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do      result =        case deleted_object do          %Object{} -> -          with {:ok, deleted_object, activity} <- Object.delete(deleted_object), +          with {:ok, deleted_object, _activity} <- Object.delete(deleted_object),                 {_, actor} when is_binary(actor) <- {:actor, deleted_object.data["actor"]},                 %User{} = user <- User.get_cached_by_ap_id(actor) do -            User.remove_pinnned_activity(user, activity) +            User.remove_pinned_object_id(user, deleted_object.data["id"])              {:ok, user} = ActivityPub.decrease_note_count_if_public(user, deleted_object) @@ -312,6 +312,63 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do      end    end +  # Tasks this handles: +  # - adds pin to user +  # - removes expiration job for pinned activity, if was set for expiration +  @impl true +  def handle(%{data: %{"type" => "Add"} = data} = object, meta) do +    with %User{} = user <- User.get_cached_by_ap_id(data["actor"]), +         {:ok, _user} <- User.add_pinned_object_id(user, data["object"]) do +      # if pinned activity was scheduled for deletion, we remove job +      if expiration = Pleroma.Workers.PurgeExpiredActivity.get_expiration(meta[:activity_id]) do +        Oban.cancel_job(expiration.id) +      end + +      {:ok, object, meta} +    else +      nil -> +        {:error, :user_not_found} + +      {:error, changeset} -> +        if changeset.errors[:pinned_objects] do +          {:error, :pinned_statuses_limit_reached} +        else +          changeset.errors +        end +    end +  end + +  # Tasks this handles: +  # - removes pin from user +  # - removes corresponding Add activity +  # - if activity had expiration, recreates activity expiration job +  @impl true +  def handle(%{data: %{"type" => "Remove"} = data} = object, meta) do +    with %User{} = user <- User.get_cached_by_ap_id(data["actor"]), +         {:ok, _user} <- User.remove_pinned_object_id(user, data["object"]) do +      data["object"] +      |> Activity.add_by_params_query(user.ap_id, user.featured_address) +      |> Repo.delete_all() + +      # if pinned activity was scheduled for deletion, we reschedule it for deletion +      if meta[:expires_at] do +        # MRF.ActivityExpirationPolicy used UTC timestamps for expires_at in original implementation +        {:ok, expires_at} = +          Pleroma.EctoType.ActivityPub.ObjectValidators.DateTime.cast(meta[:expires_at]) + +        Pleroma.Workers.PurgeExpiredActivity.enqueue(%{ +          activity_id: meta[:activity_id], +          expires_at: expires_at +        }) +      end + +      {:ok, object, meta} +    else +      nil -> {:error, :user_not_found} +      error -> error +    end +  end +    # Nothing to do    @impl true    def handle(object, meta) do diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex index 8c7d6a747..d27d0bed4 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier.ex @@ -534,7 +534,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do    end    def handle_incoming(%{"type" => type} = data, _options) -      when type in ~w{Like EmojiReact Announce} do +      when type in ~w{Like EmojiReact Announce Add Remove} do      with :ok <- ObjectValidator.fetch_actor_and_object(data),           {:ok, activity, _meta} <-             Pipeline.common_pipeline(data, local: false) do @@ -564,7 +564,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do             Pipeline.common_pipeline(data, local: false) do        {:ok, activity}      else -      {:error, {:validate_object, _}} = e -> +      {:error, {:validate, _}} = e ->          # Check if we have a create activity for this          with {:ok, object_id} <- ObjectValidators.ObjectID.cast(data["object"]),               %Activity{data: %{"actor" => actor}} <- @@ -1000,6 +1000,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier 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 diff --git a/lib/pleroma/web/activity_pub/views/user_view.ex b/lib/pleroma/web/activity_pub/views/user_view.ex index 8adc9878a..462f3b4a7 100644 --- a/lib/pleroma/web/activity_pub/views/user_view.ex +++ b/lib/pleroma/web/activity_pub/views/user_view.ex @@ -6,8 +6,10 @@ defmodule Pleroma.Web.ActivityPub.UserView do    use Pleroma.Web, :view    alias Pleroma.Keys +  alias Pleroma.Object    alias Pleroma.Repo    alias Pleroma.User +  alias Pleroma.Web.ActivityPub.ObjectView    alias Pleroma.Web.ActivityPub.Transmogrifier    alias Pleroma.Web.ActivityPub.Utils    alias Pleroma.Web.Endpoint @@ -97,6 +99,7 @@ defmodule Pleroma.Web.ActivityPub.UserView do        "followers" => "#{user.ap_id}/followers",        "inbox" => "#{user.ap_id}/inbox",        "outbox" => "#{user.ap_id}/outbox", +      "featured" => "#{user.ap_id}/collections/featured",        "preferredUsername" => user.nickname,        "name" => user.name,        "summary" => user.bio, @@ -245,6 +248,24 @@ defmodule Pleroma.Web.ActivityPub.UserView do      |> Map.merge(pagination)    end +  def render("featured.json", %{ +        user: %{featured_address: featured_address, pinned_objects: pinned_objects} +      }) do +    objects = +      pinned_objects +      |> Enum.sort_by(fn {_, pinned_at} -> pinned_at end, &>=/2) +      |> Enum.map(fn {id, _} -> +        ObjectView.render("object.json", %{object: Object.get_cached_by_ap_id(id)}) +      end) + +    %{ +      "id" => featured_address, +      "type" => "OrderedCollection", +      "orderedItems" => objects +    } +    |> Map.merge(Utils.make_json_ld_header()) +  end +    defp maybe_put_total_items(map, false, _total), do: map    defp maybe_put_total_items(map, true, total) do diff --git a/lib/pleroma/web/api_spec/operations/status_operation.ex b/lib/pleroma/web/api_spec/operations/status_operation.ex index 4bdb8e281..802fbef3e 100644 --- a/lib/pleroma/web/api_spec/operations/status_operation.ex +++ b/lib/pleroma/web/api_spec/operations/status_operation.ex @@ -182,7 +182,34 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do        parameters: [id_param()],        responses: %{          200 => status_response(), -        400 => Operation.response("Error", "application/json", ApiError) +        400 => +          Operation.response("Bad Request", "application/json", %Schema{ +            allOf: [ApiError], +            title: "Unprocessable Entity", +            example: %{ +              "error" => "You have already pinned the maximum number of statuses" +            } +          }), +        404 => +          Operation.response("Not found", "application/json", %Schema{ +            allOf: [ApiError], +            title: "Unprocessable Entity", +            example: %{ +              "error" => "Record not found" +            } +          }), +        422 => +          Operation.response( +            "Unprocessable Entity", +            "application/json", +            %Schema{ +              allOf: [ApiError], +              title: "Unprocessable Entity", +              example: %{ +                "error" => "Someone else's status cannot be pinned" +              } +            } +          )        }      }    end @@ -197,7 +224,22 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do        parameters: [id_param()],        responses: %{          200 => status_response(), -        400 => Operation.response("Error", "application/json", ApiError) +        400 => +          Operation.response("Bad Request", "application/json", %Schema{ +            allOf: [ApiError], +            title: "Unprocessable Entity", +            example: %{ +              "error" => "You have already pinned the maximum number of statuses" +            } +          }), +        404 => +          Operation.response("Not found", "application/json", %Schema{ +            allOf: [ApiError], +            title: "Unprocessable Entity", +            example: %{ +              "error" => "Record not found" +            } +          })        }      }    end diff --git a/lib/pleroma/web/api_spec/schemas/status.ex b/lib/pleroma/web/api_spec/schemas/status.ex index 42fa98718..3d042dc19 100644 --- a/lib/pleroma/web/api_spec/schemas/status.ex +++ b/lib/pleroma/web/api_spec/schemas/status.ex @@ -194,6 +194,13 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Status do            parent_visible: %Schema{              type: :boolean,              description: "`true` if the parent post is visible to the user" +          }, +          pinned_at: %Schema{ +            type: :string, +            format: "date-time", +            nullable: true, +            description: +              "A datetime (ISO 8601) that states when the post was pinned or `null` if the post is not pinned"            }          }        }, diff --git a/lib/pleroma/web/common_api.ex b/lib/pleroma/web/common_api.ex index b003e30c7..1b5f8491e 100644 --- a/lib/pleroma/web/common_api.ex +++ b/lib/pleroma/web/common_api.ex @@ -228,17 +228,7 @@ defmodule Pleroma.Web.CommonAPI do        {:find_object, _} ->          {:error, :not_found} -      {:common_pipeline, -       { -         :error, -         { -           :validate_object, -           { -             :error, -             changeset -           } -         } -       }} = e -> +      {:common_pipeline, {:error, {:validate, {:error, changeset}}}} = e ->          if {:object, {"already liked by this actor", []}} in changeset.errors do            {:ok, :already_liked}          else @@ -411,29 +401,58 @@ defmodule Pleroma.Web.CommonAPI do      end    end -  def pin(id, %{ap_id: user_ap_id} = user) do -    with %Activity{ -           actor: ^user_ap_id, -           data: %{"type" => "Create"}, -           object: %Object{data: %{"type" => object_type}} -         } = activity <- Activity.get_by_id_with_object(id), -         true <- object_type in ["Note", "Article", "Question"], -         true <- Visibility.is_public?(activity), -         {:ok, _user} <- User.add_pinnned_activity(user, activity) do +  @spec pin(String.t(), User.t()) :: {:ok, Activity.t()} | {:error, term()} +  def pin(id, %User{} = user) do +    with %Activity{} = activity <- create_activity_by_id(id), +         true <- activity_belongs_to_actor(activity, user.ap_id), +         true <- object_type_is_allowed_for_pin(activity.object), +         true <- activity_is_public(activity), +         {:ok, pin_data, _} <- Builder.pin(user, activity.object), +         {:ok, _pin, _} <- +           Pipeline.common_pipeline(pin_data, +             local: true, +             activity_id: id +           ) do        {:ok, activity}      else -      {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err} -      _ -> {:error, dgettext("errors", "Could not pin")} +      {:error, {:side_effects, error}} -> error +      error -> error      end    end +  defp create_activity_by_id(id) do +    with nil <- Activity.create_by_id_with_object(id) do +      {:error, :not_found} +    end +  end + +  defp activity_belongs_to_actor(%{actor: actor}, actor), do: true +  defp activity_belongs_to_actor(_, _), do: {:error, :ownership_error} + +  defp object_type_is_allowed_for_pin(%{data: %{"type" => type}}) do +    with false <- type in ["Note", "Article", "Question"] do +      {:error, :not_allowed} +    end +  end + +  defp activity_is_public(activity) do +    with false <- Visibility.is_public?(activity) do +      {:error, :visibility_error} +    end +  end + +  @spec unpin(String.t(), User.t()) :: {:ok, User.t()} | {:error, term()}    def unpin(id, user) do -    with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id(id), -         {:ok, _user} <- User.remove_pinnned_activity(user, activity) do +    with %Activity{} = activity <- create_activity_by_id(id), +         {:ok, unpin_data, _} <- Builder.unpin(user, activity.object), +         {:ok, _unpin, _} <- +           Pipeline.common_pipeline(unpin_data, +             local: true, +             activity_id: activity.id, +             expires_at: activity.data["expires_at"], +             featured_address: user.featured_address +           ) do        {:ok, activity} -    else -      {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err} -      _ -> {:error, dgettext("errors", "Could not unpin")}      end    end diff --git a/lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex b/lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex index d25f84837..84621500e 100644 --- a/lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex @@ -30,6 +30,12 @@ defmodule Pleroma.Web.MastodonAPI.FallbackController do      |> json(%{error: error_message})    end +  def call(conn, {:error, status, message}) do +    conn +    |> put_status(status) +    |> json(%{error: message}) +  end +    def call(conn, _) do      conn      |> put_status(:internal_server_error) diff --git a/lib/pleroma/web/mastodon_api/controllers/status_controller.ex b/lib/pleroma/web/mastodon_api/controllers/status_controller.ex index b051fca74..724dc5c5d 100644 --- a/lib/pleroma/web/mastodon_api/controllers/status_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/status_controller.ex @@ -260,6 +260,18 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do    def pin(%{assigns: %{user: user}} = conn, %{id: ap_id_or_id}) do      with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do        try_render(conn, "show.json", activity: activity, for: user, as: :activity) +    else +      {:error, :pinned_statuses_limit_reached} -> +        {:error, "You have already pinned the maximum number of statuses"} + +      {:error, :ownership_error} -> +        {:error, :unprocessable_entity, "Someone else's status cannot be pinned"} + +      {:error, :visibility_error} -> +        {:error, :unprocessable_entity, "Non-public status cannot be pinned"} + +      error -> +        error      end    end diff --git a/lib/pleroma/web/mastodon_api/views/instance_view.ex b/lib/pleroma/web/mastodon_api/views/instance_view.ex index 73205fb6d..dac68d8e6 100644 --- a/lib/pleroma/web/mastodon_api/views/instance_view.ex +++ b/lib/pleroma/web/mastodon_api/views/instance_view.ex @@ -23,7 +23,8 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do          streaming_api: Pleroma.Web.Endpoint.websocket_url()        },        stats: Pleroma.Stats.get_stats(), -      thumbnail: Pleroma.Web.base_url() <> Keyword.get(instance, :instance_thumbnail), +      thumbnail: +        URI.merge(Pleroma.Web.base_url(), Keyword.get(instance, :instance_thumbnail)) |> to_string,        languages: ["en"],        registrations: Keyword.get(instance, :registrations_open),        approval_required: Keyword.get(instance, :account_approval_required), diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex index 3753588f2..814b3d142 100644 --- a/lib/pleroma/web/mastodon_api/views/status_view.ex +++ b/lib/pleroma/web/mastodon_api/views/status_view.ex @@ -152,6 +152,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do        |> Enum.filter(& &1)        |> Enum.map(fn user -> AccountView.render("mention.json", %{user: user}) end) +    {pinned?, pinned_at} = pin_data(object, user) +      %{        id: to_string(activity.id),        uri: object.data["id"], @@ -173,7 +175,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do        favourited: present?(favorited),        bookmarked: present?(bookmarked),        muted: false, -      pinned: pinned?(activity, user), +      pinned: pinned?,        sensitive: false,        spoiler_text: "",        visibility: get_visibility(activity), @@ -184,7 +186,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do        language: nil,        emojis: [],        pleroma: %{ -        local: activity.local +        local: activity.local, +        pinned_at: pinned_at        }      }    end @@ -316,6 +319,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do            fn for_user, user -> User.mutes?(for_user, user) end          ) +    {pinned?, pinned_at} = pin_data(object, user) +      %{        id: to_string(activity.id),        uri: object.data["id"], @@ -339,7 +344,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do        favourited: present?(favorited),        bookmarked: present?(bookmarked),        muted: muted, -      pinned: pinned?(activity, user), +      pinned: pinned?,        sensitive: sensitive,        spoiler_text: summary,        visibility: get_visibility(object), @@ -360,7 +365,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do          direct_conversation_id: direct_conversation_id,          thread_muted: thread_muted?,          emoji_reactions: emoji_reactions, -        parent_visible: visible_for_user?(reply_to, opts[:for]) +        parent_visible: visible_for_user?(reply_to, opts[:for]), +        pinned_at: pinned_at        }      }    end @@ -529,8 +535,13 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do    defp present?(false), do: false    defp present?(_), do: true -  defp pinned?(%Activity{id: id}, %User{pinned_activities: pinned_activities}), -    do: id in pinned_activities +  defp pin_data(%Object{data: %{"id" => object_id}}, %User{pinned_objects: pinned_objects}) do +    if pinned_at = pinned_objects[object_id] do +      {true, Utils.to_masto_date(pinned_at)} +    else +      {false, nil} +    end +  end    defp build_emoji_map(emoji, users, current_user) do      %{ diff --git a/lib/pleroma/web/plugs/http_security_plug.ex b/lib/pleroma/web/plugs/http_security_plug.ex index 0025b042a..d1e6cc9d3 100644 --- a/lib/pleroma/web/plugs/http_security_plug.ex +++ b/lib/pleroma/web/plugs/http_security_plug.ex @@ -48,7 +48,8 @@ defmodule Pleroma.Web.Plugs.HTTPSecurityPlug do        {"x-content-type-options", "nosniff"},        {"referrer-policy", referrer_policy},        {"x-download-options", "noopen"}, -      {"content-security-policy", csp_string()} +      {"content-security-policy", csp_string()}, +      {"permissions-policy", "interest-cohort=()"}      ]      headers = diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index de0bd27d7..ccf2ef796 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -704,6 +704,7 @@ defmodule Pleroma.Web.Router do      # The following two are S2S as well, see `ActivityPub.fetch_follow_information_for_user/1`:      get("/users/:nickname/followers", ActivityPubController, :followers)      get("/users/:nickname/following", ActivityPubController, :following) +    get("/users/:nickname/collections/featured", ActivityPubController, :pinned)    end    scope "/", Pleroma.Web.ActivityPub do | 
