diff options
| author | mkljczk <git@mkljczk.pl> | 2025-02-17 17:36:02 +0100 | 
|---|---|---|
| committer | mkljczk <git@mkljczk.pl> | 2025-02-17 17:36:02 +0100 | 
| commit | ea01b5934f41a13f480221e554723f2b214d67e3 (patch) | |
| tree | 631c5aa815ef8a133461e45437ff917149f23e0c /lib | |
| parent | 3e5517e7bb549d22258d2d7788ba52797648d6b7 (diff) | |
| parent | a1f4da7ae2ed5d063b3d146f223a96b9db9bb505 (diff) | |
| download | pleroma-ea01b5934f41a13f480221e554723f2b214d67e3.tar.gz pleroma-ea01b5934f41a13f480221e554723f2b214d67e3.zip  | |
Merge remote-tracking branch 'origin/develop' into post-languages
Diffstat (limited to 'lib')
84 files changed, 1346 insertions, 375 deletions
diff --git a/lib/mix/tasks/pleroma/search/meilisearch.ex b/lib/mix/tasks/pleroma/search/meilisearch.ex index 8379a0c25..edce9e871 100644 --- a/lib/mix/tasks/pleroma/search/meilisearch.ex +++ b/lib/mix/tasks/pleroma/search/meilisearch.ex @@ -9,7 +9,7 @@ defmodule Mix.Tasks.Pleroma.Search.Meilisearch do    import Ecto.Query    import Pleroma.Search.Meilisearch, -    only: [meili_post: 2, meili_put: 2, meili_get: 1, meili_delete: 1] +    only: [meili_put: 2, meili_get: 1, meili_delete: 1]    def run(["index"]) do      start_pleroma() @@ -28,7 +28,7 @@ defmodule Mix.Tasks.Pleroma.Search.Meilisearch do      end      {:ok, _} = -      meili_post( +      meili_put(          "/indexes/objects/settings/ranking-rules",          [            "published:desc", @@ -42,7 +42,7 @@ defmodule Mix.Tasks.Pleroma.Search.Meilisearch do        )      {:ok, _} = -      meili_post( +      meili_put(          "/indexes/objects/settings/searchable-attributes",          [            "content" diff --git a/lib/mix/tasks/pleroma/test_runner.ex b/lib/mix/tasks/pleroma/test_runner.ex new file mode 100644 index 000000000..69fefb001 --- /dev/null +++ b/lib/mix/tasks/pleroma/test_runner.ex @@ -0,0 +1,25 @@ +defmodule Mix.Tasks.Pleroma.TestRunner do +  @shortdoc "Retries tests once if they fail" + +  use Mix.Task + +  def run(args \\ []) do +    case System.cmd("mix", ["test"] ++ args, into: IO.stream(:stdio, :line)) do +      {_, 0} -> +        :ok + +      _ -> +        retry(args) +    end +  end + +  def retry(args) do +    case System.cmd("mix", ["test", "--failed"] ++ args, into: IO.stream(:stdio, :line)) do +      {_, 0} -> +        :ok + +      _ -> +        exit(1) +    end +  end +end diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex index cb15dc1e9..3f199c002 100644 --- a/lib/pleroma/application.ex +++ b/lib/pleroma/application.ex @@ -94,6 +94,7 @@ defmodule Pleroma.Application do      children =        [          Pleroma.PromEx, +        Pleroma.LDAP,          Pleroma.Repo,          Config.TransferTask,          Pleroma.Emoji, diff --git a/lib/pleroma/config/transfer_task.ex b/lib/pleroma/config/transfer_task.ex index ffc95f144..140dd7711 100644 --- a/lib/pleroma/config/transfer_task.ex +++ b/lib/pleroma/config/transfer_task.ex @@ -22,7 +22,8 @@ defmodule Pleroma.Config.TransferTask do        {:pleroma, :markup},        {:pleroma, :streamer},        {:pleroma, :pools}, -      {:pleroma, :connections_pool} +      {:pleroma, :connections_pool}, +      {:pleroma, :ldap}      ]    defp reboot_time_subkeys, diff --git a/lib/pleroma/constants.ex b/lib/pleroma/constants.ex index a018e91fc..2d08cd7a1 100644 --- a/lib/pleroma/constants.ex +++ b/lib/pleroma/constants.ex @@ -87,6 +87,41 @@ defmodule Pleroma.Constants do      ]    ) +  const(activity_types, +    do: [ +      "Block", +      "Create", +      "Update", +      "Delete", +      "Follow", +      "Accept", +      "Reject", +      "Add", +      "Remove", +      "Like", +      "Announce", +      "Undo", +      "Flag", +      "EmojiReact" +    ] +  ) + +  const(allowed_activity_types_from_strangers, +    do: [ +      "Block", +      "Create", +      "Flag", +      "Follow", +      "Like", +      "EmojiReact", +      "Announce" +    ] +  ) + +  const(object_types, +    do: ~w[Event Question Answer Audio Video Image Article Note Page ChatMessage] +  ) +    # basic regex, just there to weed out potential mistakes    # https://datatracker.ietf.org/doc/html/rfc2045#section-5.1    const(mime_regex, diff --git a/lib/pleroma/frontend.ex b/lib/pleroma/frontend.ex index 816499917..a4f427ae5 100644 --- a/lib/pleroma/frontend.ex +++ b/lib/pleroma/frontend.ex @@ -74,11 +74,14 @@ defmodule Pleroma.Frontend do          new_file_path = Path.join(dest, path) -        new_file_path +        path          |> Path.dirname() +        |> then(&Path.join(dest, &1))          |> File.mkdir_p!() -        File.write!(new_file_path, data) +        if not File.dir?(new_file_path) do +          File.write!(new_file_path, data) +        end        end)      end    end diff --git a/lib/pleroma/http/adapter_helper.ex b/lib/pleroma/http/adapter_helper.ex index dcb27a29d..32c1080f7 100644 --- a/lib/pleroma/http/adapter_helper.ex +++ b/lib/pleroma/http/adapter_helper.ex @@ -52,6 +52,7 @@ defmodule Pleroma.HTTP.AdapterHelper do      case adapter() do        Tesla.Adapter.Gun -> AdapterHelper.Gun        Tesla.Adapter.Hackney -> AdapterHelper.Hackney +      {Tesla.Adapter.Finch, _} -> AdapterHelper.Finch        _ -> AdapterHelper.Default      end    end @@ -118,4 +119,13 @@ defmodule Pleroma.HTTP.AdapterHelper do          host_charlist      end    end + +  @spec can_stream? :: bool() +  def can_stream? do +    case Application.get_env(:tesla, :adapter) do +      Tesla.Adapter.Gun -> true +      {Tesla.Adapter.Finch, _} -> true +      _ -> false +    end +  end  end diff --git a/lib/pleroma/http/adapter_helper/finch.ex b/lib/pleroma/http/adapter_helper/finch.ex new file mode 100644 index 000000000..181caed7e --- /dev/null +++ b/lib/pleroma/http/adapter_helper/finch.ex @@ -0,0 +1,33 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.HTTP.AdapterHelper.Finch do +  @behaviour Pleroma.HTTP.AdapterHelper + +  alias Pleroma.Config +  alias Pleroma.HTTP.AdapterHelper + +  @spec options(keyword(), URI.t()) :: keyword() +  def options(incoming_opts \\ [], %URI{} = _uri) do +    proxy = +      [:http, :proxy_url] +      |> Config.get() +      |> AdapterHelper.format_proxy() + +    config_opts = Config.get([:http, :adapter], []) + +    config_opts +    |> Keyword.merge(incoming_opts) +    |> AdapterHelper.maybe_add_proxy(proxy) +    |> maybe_stream() +  end + +  # Finch uses [response: :stream] +  defp maybe_stream(opts) do +    case Keyword.pop(opts, :stream, nil) do +      {true, opts} -> Keyword.put(opts, :response, :stream) +      {_, opts} -> opts +    end +  end +end diff --git a/lib/pleroma/http/adapter_helper/gun.ex b/lib/pleroma/http/adapter_helper/gun.ex index 1fe8dd4b2..30ba26765 100644 --- a/lib/pleroma/http/adapter_helper/gun.ex +++ b/lib/pleroma/http/adapter_helper/gun.ex @@ -32,6 +32,7 @@ defmodule Pleroma.HTTP.AdapterHelper.Gun do      |> AdapterHelper.maybe_add_proxy(proxy)      |> Keyword.merge(incoming_opts)      |> put_timeout() +    |> maybe_stream()    end    defp add_scheme_opts(opts, %{scheme: "http"}), do: opts @@ -47,6 +48,14 @@ defmodule Pleroma.HTTP.AdapterHelper.Gun do      Keyword.put(opts, :timeout, recv_timeout)    end +  # Gun uses [body_as: :stream] +  defp maybe_stream(opts) do +    case Keyword.pop(opts, :stream, nil) do +      {true, opts} -> Keyword.put(opts, :body_as, :stream) +      {_, opts} -> opts +    end +  end +    @spec pool_timeout(pool()) :: non_neg_integer()    def pool_timeout(pool) do      default = Config.get([:pools, :default, :recv_timeout], 5_000) diff --git a/lib/pleroma/ldap.ex b/lib/pleroma/ldap.ex new file mode 100644 index 000000000..b591c2918 --- /dev/null +++ b/lib/pleroma/ldap.ex @@ -0,0 +1,271 @@ +defmodule Pleroma.LDAP do +  use GenServer + +  require Logger + +  alias Pleroma.Config +  alias Pleroma.User + +  import Pleroma.Web.Auth.Helpers, only: [fetch_user: 1] + +  @connection_timeout 2_000 +  @search_timeout 2_000 + +  def start_link(_) do +    GenServer.start_link(__MODULE__, [], name: __MODULE__) +  end + +  def bind_user(name, password) do +    GenServer.call(__MODULE__, {:bind_user, name, password}) +  end + +  def change_password(name, password, new_password) do +    GenServer.call(__MODULE__, {:change_password, name, password, new_password}) +  end + +  @impl true +  def init(state) do +    case {Config.get(Pleroma.Web.Auth.Authenticator), Config.get([:ldap, :enabled])} do +      {Pleroma.Web.Auth.LDAPAuthenticator, true} -> +        {:ok, state, {:continue, :connect}} + +      {Pleroma.Web.Auth.LDAPAuthenticator, false} -> +        Logger.error( +          "LDAP Authenticator enabled but :pleroma, :ldap is not enabled. Auth will not work." +        ) + +        {:ok, state} + +      {_, true} -> +        Logger.warning( +          ":pleroma, :ldap is enabled but Pleroma.Web.Authenticator is not set to the LDAPAuthenticator. LDAP will not be used." +        ) + +        {:ok, state} + +      _ -> +        {:ok, state} +    end +  end + +  @impl true +  def handle_continue(:connect, _state), do: do_handle_connect() + +  @impl true +  def handle_info(:connect, _state), do: do_handle_connect() + +  def handle_info({:bind_after_reconnect, name, password, from}, state) do +    result = do_bind_user(state[:handle], name, password) + +    GenServer.reply(from, result) + +    {:noreply, state} +  end + +  @impl true +  def handle_call({:bind_user, name, password}, from, state) do +    case do_bind_user(state[:handle], name, password) do +      :needs_reconnect -> +        Process.send(self(), {:bind_after_reconnect, name, password, from}, []) +        {:noreply, state, {:continue, :connect}} + +      result -> +        {:reply, result, state, :hibernate} +    end +  end + +  def handle_call({:change_password, name, password, new_password}, _from, state) do +    result = change_password(state[:handle], name, password, new_password) + +    {:reply, result, state, :hibernate} +  end + +  @impl true +  def terminate(_, state) do +    handle = Keyword.get(state, :handle) + +    if not is_nil(handle) do +      :eldap.close(handle) +    end + +    :ok +  end + +  defp do_handle_connect do +    state = +      case connect() do +        {:ok, handle} -> +          :eldap.controlling_process(handle, self()) +          Process.link(handle) +          [handle: handle] + +        _ -> +          Logger.error("Failed to connect to LDAP. Retrying in 5000ms") +          Process.send_after(self(), :connect, 5_000) +          [] +      end + +    {:noreply, state} +  end + +  defp connect do +    ldap = Config.get(:ldap, []) +    host = Keyword.get(ldap, :host, "localhost") +    port = Keyword.get(ldap, :port, 389) +    ssl = Keyword.get(ldap, :ssl, false) +    tls = Keyword.get(ldap, :tls, false) +    cacertfile = Keyword.get(ldap, :cacertfile) || CAStore.file_path() + +    if ssl, do: Application.ensure_all_started(:ssl) + +    default_secure_opts = [ +      verify: :verify_peer, +      cacerts: decode_certfile(cacertfile), +      customize_hostname_check: [ +        fqdn_fun: fn _ -> to_charlist(host) end +      ] +    ] + +    sslopts = Keyword.merge(default_secure_opts, Keyword.get(ldap, :sslopts, [])) +    tlsopts = Keyword.merge(default_secure_opts, Keyword.get(ldap, :tlsopts, [])) + +    default_options = [{:port, port}, {:ssl, ssl}, {:timeout, @connection_timeout}] + +    # :sslopts can only be included in :eldap.open/2 when {ssl: true} +    # or the connection will fail +    options = +      if ssl do +        default_options ++ [{:sslopts, sslopts}] +      else +        default_options +      end + +    case :eldap.open([to_charlist(host)], options) do +      {:ok, handle} -> +        try do +          cond do +            tls -> +              case :eldap.start_tls( +                     handle, +                     tlsopts, +                     @connection_timeout +                   ) do +                :ok -> +                  {:ok, handle} + +                error -> +                  Logger.error("Could not start TLS: #{inspect(error)}") +                  :eldap.close(handle) +              end + +            true -> +              {:ok, handle} +          end +        after +          :ok +        end + +      {:error, error} -> +        Logger.error("Could not open LDAP connection: #{inspect(error)}") +        {:error, {:ldap_connection_error, error}} +    end +  end + +  defp do_bind_user(handle, name, password) do +    dn = make_dn(name) + +    case :eldap.simple_bind(handle, dn, password) do +      :ok -> +        case fetch_user(name) do +          %User{} = user -> +            user + +          _ -> +            register_user(handle, ldap_base(), ldap_uid(), name) +        end + +      # eldap does not inform us of socket closure +      # until it is used +      {:error, {:gen_tcp_error, :closed}} -> +        :eldap.close(handle) +        :needs_reconnect + +      {:error, error} = e -> +        Logger.error("Could not bind LDAP user #{name}: #{inspect(error)}") +        e +    end +  end + +  defp register_user(handle, base, uid, name) do +    case :eldap.search(handle, [ +           {:base, to_charlist(base)}, +           {:filter, :eldap.equalityMatch(to_charlist(uid), to_charlist(name))}, +           {:scope, :eldap.wholeSubtree()}, +           {:timeout, @search_timeout} +         ]) do +      # The :eldap_search_result record structure changed in OTP 24.3 and added a controls field +      # https://github.com/erlang/otp/pull/5538 +      {:ok, {:eldap_search_result, [{:eldap_entry, _object, attributes}], _referrals}} -> +        try_register(name, attributes) + +      {:ok, {:eldap_search_result, [{:eldap_entry, _object, attributes}], _referrals, _controls}} -> +        try_register(name, attributes) + +      error -> +        Logger.error("Couldn't register user because LDAP search failed: #{inspect(error)}") +        {:error, {:ldap_search_error, error}} +    end +  end + +  defp try_register(name, attributes) do +    mail_attribute = Config.get([:ldap, :mail]) + +    params = %{ +      name: name, +      nickname: name, +      password: nil +    } + +    params = +      case List.keyfind(attributes, to_charlist(mail_attribute), 0) do +        {_, [mail]} -> Map.put_new(params, :email, :erlang.list_to_binary(mail)) +        _ -> params +      end + +    changeset = User.register_changeset_ldap(%User{}, params) + +    case User.register(changeset) do +      {:ok, user} -> user +      error -> error +    end +  end + +  defp change_password(handle, name, password, new_password) do +    dn = make_dn(name) + +    with :ok <- :eldap.simple_bind(handle, dn, password) do +      :eldap.modify_password(handle, dn, to_charlist(new_password), to_charlist(password)) +    end +  end + +  defp decode_certfile(file) do +    with {:ok, data} <- File.read(file) do +      data +      |> :public_key.pem_decode() +      |> Enum.map(fn {_, b, _} -> b end) +    else +      _ -> +        Logger.error("Unable to read certfile: #{file}") +        [] +    end +  end + +  defp ldap_uid, do: to_charlist(Config.get([:ldap, :uid], "cn")) +  defp ldap_base, do: to_charlist(Config.get([:ldap, :base])) + +  defp make_dn(name) do +    uid = ldap_uid() +    base = ldap_base() +    ~c"#{uid}=#{name},#{base}" +  end +end diff --git a/lib/pleroma/maps.ex b/lib/pleroma/maps.ex index 5020a8ff8..1afbde484 100644 --- a/lib/pleroma/maps.ex +++ b/lib/pleroma/maps.ex @@ -20,15 +20,13 @@ defmodule Pleroma.Maps do    end    def filter_empty_values(data) do -    # TODO: Change to Map.filter in Elixir 1.13+      data -    |> Enum.filter(fn +    |> Map.filter(fn        {_k, nil} -> false        {_k, ""} -> false        {_k, []} -> false        {_k, %{} = v} -> Map.keys(v) != []        {_k, _v} -> true      end) -    |> Map.new()    end  end diff --git a/lib/pleroma/object.ex b/lib/pleroma/object.ex index 748f18e6c..77dfda851 100644 --- a/lib/pleroma/object.ex +++ b/lib/pleroma/object.ex @@ -99,27 +99,6 @@ defmodule Pleroma.Object do    def get_by_id(nil), do: nil    def get_by_id(id), do: Repo.get(Object, id) -  @spec get_by_id_and_maybe_refetch(integer(), list()) :: Object.t() | nil -  def get_by_id_and_maybe_refetch(id, opts \\ []) do -    with %Object{updated_at: updated_at} = object <- get_by_id(id) do -      if opts[:interval] && -           NaiveDateTime.diff(NaiveDateTime.utc_now(), updated_at) > opts[:interval] do -        case Fetcher.refetch_object(object) do -          {:ok, %Object{} = object} -> -            object - -          e -> -            Logger.error("Couldn't refresh #{object.data["id"]}:\n#{inspect(e)}") -            object -        end -      else -        object -      end -    else -      nil -> nil -    end -  end -    def get_by_ap_id(nil), do: nil    def get_by_ap_id(ap_id) do diff --git a/lib/pleroma/object/fetcher.ex b/lib/pleroma/object/fetcher.ex index 9d9a201ca..c85a8b09f 100644 --- a/lib/pleroma/object/fetcher.ex +++ b/lib/pleroma/object/fetcher.ex @@ -58,8 +58,12 @@ defmodule Pleroma.Object.Fetcher do      end    end +  @typep fetcher_errors :: +           :error | :reject | :allowed_depth | :fetch | :containment | :transmogrifier +    # Note: will create a Create activity, which we need internally at the moment. -  @spec fetch_object_from_id(String.t(), list()) :: {:ok, Object.t()} | {:error | :reject, any()} +  @spec fetch_object_from_id(String.t(), list()) :: +          {:ok, Object.t()} | {fetcher_errors(), any()} | Pipeline.errors()    def fetch_object_from_id(id, options \\ []) do      with {_, nil} <- {:fetch_object, Object.get_cached_by_ap_id(id)},           {_, true} <- {:allowed_depth, Federator.allowed_thread_distance?(options[:depth])}, @@ -141,6 +145,7 @@ defmodule Pleroma.Object.Fetcher do      Logger.debug("Fetching object #{id} via AP")      with {:scheme, true} <- {:scheme, String.starts_with?(id, "http")}, +         {_, true} <- {:mrf, MRF.id_filter(id)},           {:ok, body} <- get_object(id),           {:ok, data} <- safe_json_decode(body),           :ok <- Containment.contain_origin_from_id(id, data) do @@ -156,6 +161,9 @@ defmodule Pleroma.Object.Fetcher do        {:error, e} ->          {:error, e} +      {:mrf, false} -> +        {:error, {:reject, "Filtered by id"}} +        e ->          {:error, e}      end diff --git a/lib/pleroma/release_tasks.ex b/lib/pleroma/release_tasks.ex index bcfcd1243..af2d35c8f 100644 --- a/lib/pleroma/release_tasks.ex +++ b/lib/pleroma/release_tasks.ex @@ -16,17 +16,24 @@ defmodule Pleroma.ReleaseTasks do      end    end +  def find_module(task) do +    module_name = +      task +      |> String.split(".") +      |> Enum.map(&String.capitalize/1) +      |> then(fn x -> [Mix, Tasks, Pleroma] ++ x end) +      |> Module.concat() + +    case Code.ensure_loaded(module_name) do +      {:module, _} -> module_name +      _ -> nil +    end +  end +    defp mix_task(task, args) do      Application.load(:pleroma) -    {:ok, modules} = :application.get_key(:pleroma, :modules) - -    module = -      Enum.find(modules, fn module -> -        module = Module.split(module) -        match?(["Mix", "Tasks", "Pleroma" | _], module) and -          String.downcase(List.last(module)) == task -      end) +    module = find_module(task)      if module do        module.run(args) diff --git a/lib/pleroma/search/meilisearch.ex b/lib/pleroma/search/meilisearch.ex index 9bba5b30f..cafae8099 100644 --- a/lib/pleroma/search/meilisearch.ex +++ b/lib/pleroma/search/meilisearch.ex @@ -122,6 +122,7 @@ defmodule Pleroma.Search.Meilisearch do      # Only index public or unlisted Notes      if not is_nil(object) and object.data["type"] == "Note" and           not is_nil(object.data["content"]) and +         not is_nil(object.data["published"]) and           (Pleroma.Constants.as_public() in object.data["to"] or              Pleroma.Constants.as_public() in object.data["cc"]) and           object.data["content"] not in ["", "."] do diff --git a/lib/pleroma/upload/filter/analyze_metadata.ex b/lib/pleroma/upload/filter/analyze_metadata.ex index 7ee643277..a8480bf36 100644 --- a/lib/pleroma/upload/filter/analyze_metadata.ex +++ b/lib/pleroma/upload/filter/analyze_metadata.ex @@ -90,9 +90,13 @@ defmodule Pleroma.Upload.Filter.AnalyzeMetadata do        {:ok, rgb} =          if Image.has_alpha?(resized_image) do            # remove alpha channel -          resized_image -          |> Operation.extract_band!(0, n: 3) -          |> Image.write_to_binary() +          case Operation.extract_band(resized_image, 0, n: 3) do +            {:ok, data} -> +              Image.write_to_binary(data) + +            _ -> +              Image.write_to_binary(resized_image) +          end          else            Image.write_to_binary(resized_image)          end diff --git a/lib/pleroma/upload/filter/dedupe.ex b/lib/pleroma/upload/filter/dedupe.ex index ef793d390..7b278d299 100644 --- a/lib/pleroma/upload/filter/dedupe.ex +++ b/lib/pleroma/upload/filter/dedupe.ex @@ -17,8 +17,16 @@ defmodule Pleroma.Upload.Filter.Dedupe do        |> Base.encode16(case: :lower)      filename = shasum <> "." <> extension -    {:ok, :filtered, %Upload{upload | id: shasum, path: filename}} + +    {:ok, :filtered, %Upload{upload | id: shasum, path: shard_path(filename)}}    end    def filter(_), do: {:ok, :noop} + +  @spec shard_path(String.t()) :: String.t() +  def shard_path( +        <<a::binary-size(2), b::binary-size(2), c::binary-size(2), _::binary>> = filename +      ) do +    Path.join([a, b, c, filename]) +  end  end diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index c6c536943..7a36ece77 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -419,6 +419,11 @@ defmodule Pleroma.User do      end    end +  def image_description(image, default \\ "") + +  def image_description(%{"name" => name}, _default), do: name +  def image_description(_, default), do: default +    # Should probably be renamed or removed    @spec ap_id(User.t()) :: String.t()    def ap_id(%User{nickname: nickname}), do: "#{Endpoint.url()}/users/#{nickname}" @@ -586,16 +591,26 @@ defmodule Pleroma.User do      |> validate_length(:bio, max: bio_limit)      |> validate_length(:name, min: 1, max: name_limit)      |> validate_inclusion(:actor_type, Pleroma.Constants.allowed_user_actor_types()) +    |> validate_image_description(:avatar_description, params) +    |> validate_image_description(:header_description, params)      |> put_fields()      |> put_emoji()      |> put_change_if_present(:bio, &{:ok, parse_bio(&1, struct)}) -    |> put_change_if_present(:avatar, &put_upload(&1, :avatar)) -    |> put_change_if_present(:banner, &put_upload(&1, :banner)) +    |> put_change_if_present( +      :avatar, +      &put_upload(&1, :avatar, Map.get(params, :avatar_description)) +    ) +    |> put_change_if_present( +      :banner, +      &put_upload(&1, :banner, Map.get(params, :header_description)) +    )      |> put_change_if_present(:background, &put_upload(&1, :background))      |> put_change_if_present(        :pleroma_settings_store,        &{:ok, Map.merge(struct.pleroma_settings_store, &1)}      ) +    |> maybe_update_image_description(:avatar, Map.get(params, :avatar_description)) +    |> maybe_update_image_description(:banner, Map.get(params, :header_description))      |> validate_fields(false)    end @@ -674,13 +689,41 @@ defmodule Pleroma.User do      end    end -  defp put_upload(value, type) do +  defp put_upload(value, type, description \\ nil) do      with %Plug.Upload{} <- value, -         {:ok, object} <- ActivityPub.upload(value, type: type) do +         {:ok, object} <- ActivityPub.upload(value, type: type, description: description) do        {:ok, object.data}      end    end +  defp validate_image_description(changeset, key, params) do +    description_limit = Config.get([:instance, :description_limit], 5_000) +    description = Map.get(params, key) + +    if is_binary(description) and String.length(description) > description_limit do +      changeset +      |> add_error(key, "#{key} is too long") +    else +      changeset +    end +  end + +  defp maybe_update_image_description(changeset, image_field, description) +       when is_binary(description) do +    with {:image_missing, true} <- {:image_missing, not changed?(changeset, image_field)}, +         {:existing_image, %{"id" => id}} <- +           {:existing_image, Map.get(changeset.data, image_field)}, +         {:object, %Object{} = object} <- {:object, Object.get_by_ap_id(id)}, +         {:ok, object} <- Object.update_data(object, %{"name" => description}) do +      put_change(changeset, image_field, object.data) +    else +      {:description_too_long, true} -> {:error} +      _ -> changeset +    end +  end + +  defp maybe_update_image_description(changeset, _, _), do: changeset +    def update_as_admin_changeset(struct, params) do      struct      |> update_changeset(params) diff --git a/lib/pleroma/user/backup.ex b/lib/pleroma/user/backup.ex index 7feaa22bf..cdff297a9 100644 --- a/lib/pleroma/user/backup.ex +++ b/lib/pleroma/user/backup.ex @@ -92,9 +92,6 @@ defmodule Pleroma.User.Backup do      else        true ->          {:error, "Backup is missing id. Please insert it into the Repo first."} - -      e -> -        {:error, e}      end    end @@ -121,14 +118,13 @@ defmodule Pleroma.User.Backup do    end    defp permitted?(user) do -    with {_, %__MODULE__{inserted_at: inserted_at}} <- {:last, get_last(user)}, -         days = Config.get([__MODULE__, :limit_days]), -         diff = Timex.diff(NaiveDateTime.utc_now(), inserted_at, :days), -         {_, true} <- {:diff, diff > days} do -      true +    with {_, %__MODULE__{inserted_at: inserted_at}} <- {:last, get_last(user)} do +      days = Config.get([__MODULE__, :limit_days]) +      diff = Timex.diff(NaiveDateTime.utc_now(), inserted_at, :days) + +      diff > days      else        {:last, nil} -> true -      {:diff, false} -> false      end    end @@ -250,7 +246,13 @@ defmodule Pleroma.User.Backup do    defp actor(dir, user) do      with {:ok, json} <-             UserView.render("user.json", %{user: user}) -           |> Map.merge(%{"likes" => "likes.json", "bookmarks" => "bookmarks.json"}) +           |> Map.merge(%{ +             "bookmarks" => "bookmarks.json", +             "likes" => "likes.json", +             "outbox" => "outbox.json", +             "followers" => "followers.json", +             "following" => "following.json" +           })             |> Jason.encode() do        File.write(Path.join(dir, "actor.json"), json)      end @@ -297,9 +299,6 @@ defmodule Pleroma.User.Backup do                )                acc - -            _ -> -              acc            end          end) diff --git a/lib/pleroma/user/import.ex b/lib/pleroma/user/import.ex index 11905237c..ab6bdb8d4 100644 --- a/lib/pleroma/user/import.ex +++ b/lib/pleroma/user/import.ex @@ -5,87 +5,107 @@  defmodule Pleroma.User.Import do    use Ecto.Schema +  alias Pleroma.Repo    alias Pleroma.User    alias Pleroma.Web.CommonAPI    alias Pleroma.Workers.BackgroundWorker    require Logger -  @spec perform(atom(), User.t(), list()) :: :ok | list() | {:error, any()} -  def perform(:mutes_import, %User{} = user, [_ | _] = identifiers) do -    Enum.map( -      identifiers, -      fn identifier -> -        with {:ok, %User{} = muted_user} <- User.get_or_fetch(identifier), -             {:ok, _} <- User.mute(user, muted_user) do -          muted_user -        else -          error -> handle_error(:mutes_import, identifier, error) -        end -      end -    ) +  @spec perform(atom(), User.t(), String.t()) :: :ok | {:error, any()} +  def perform(:mute_import, %User{} = user, actor) do +    with {:ok, %User{} = muted_user} <- User.get_or_fetch(actor), +         {_, false} <- {:existing_mute, User.mutes_user?(user, muted_user)}, +         {:ok, _} <- User.mute(user, muted_user) do +      {:ok, muted_user} +    else +      {:existing_mute, true} -> :ok +      error -> handle_error(:mutes_import, actor, error) +    end    end -  def perform(:blocks_import, %User{} = blocker, [_ | _] = identifiers) do -    Enum.map( -      identifiers, -      fn identifier -> -        with {:ok, %User{} = blocked} <- User.get_or_fetch(identifier), -             {:ok, _block} <- CommonAPI.block(blocked, blocker) do -          blocked -        else -          error -> handle_error(:blocks_import, identifier, error) -        end -      end -    ) +  def perform(:block_import, %User{} = user, actor) do +    with {:ok, %User{} = blocked} <- User.get_or_fetch(actor), +         {_, false} <- {:existing_block, User.blocks_user?(user, blocked)}, +         {:ok, _block} <- CommonAPI.block(blocked, user) do +      {:ok, blocked} +    else +      {:existing_block, true} -> :ok +      error -> handle_error(:blocks_import, actor, error) +    end    end -  def perform(:follow_import, %User{} = follower, [_ | _] = identifiers) do -    Enum.map( -      identifiers, -      fn identifier -> -        with {:ok, %User{} = followed} <- User.get_or_fetch(identifier), -             {:ok, follower, followed} <- User.maybe_direct_follow(follower, followed), -             {:ok, _, _, _} <- CommonAPI.follow(followed, follower) do -          followed -        else -          error -> handle_error(:follow_import, identifier, error) -        end -      end -    ) +  def perform(:follow_import, %User{} = user, actor) do +    with {:ok, %User{} = followed} <- User.get_or_fetch(actor), +         {_, false} <- {:existing_follow, User.following?(user, followed)}, +         {:ok, user, followed} <- User.maybe_direct_follow(user, followed), +         {:ok, _, _, _} <- CommonAPI.follow(followed, user) do +      {:ok, followed} +    else +      {:existing_follow, true} -> :ok +      error -> handle_error(:follow_import, actor, error) +    end    end -  def perform(_, _, _), do: :ok -    defp handle_error(op, user_id, error) do      Logger.debug("#{op} failed for #{user_id} with: #{inspect(error)}") -    error +    {:error, error}    end -  def blocks_import(%User{} = blocker, [_ | _] = identifiers) do -    BackgroundWorker.new(%{ -      "op" => "blocks_import", -      "user_id" => blocker.id, -      "identifiers" => identifiers -    }) -    |> Oban.insert() +  def blocks_import(%User{} = user, [_ | _] = actors) do +    jobs = +      Repo.checkout(fn -> +        Enum.reduce(actors, [], fn actor, acc -> +          {:ok, job} = +            BackgroundWorker.new(%{ +              "op" => "block_import", +              "user_id" => user.id, +              "actor" => actor +            }) +            |> Oban.insert() + +          acc ++ [job] +        end) +      end) + +    {:ok, jobs}    end -  def follow_import(%User{} = follower, [_ | _] = identifiers) do -    BackgroundWorker.new(%{ -      "op" => "follow_import", -      "user_id" => follower.id, -      "identifiers" => identifiers -    }) -    |> Oban.insert() +  def follows_import(%User{} = user, [_ | _] = actors) do +    jobs = +      Repo.checkout(fn -> +        Enum.reduce(actors, [], fn actor, acc -> +          {:ok, job} = +            BackgroundWorker.new(%{ +              "op" => "follow_import", +              "user_id" => user.id, +              "actor" => actor +            }) +            |> Oban.insert() + +          acc ++ [job] +        end) +      end) + +    {:ok, jobs}    end -  def mutes_import(%User{} = user, [_ | _] = identifiers) do -    BackgroundWorker.new(%{ -      "op" => "mutes_import", -      "user_id" => user.id, -      "identifiers" => identifiers -    }) -    |> Oban.insert() +  def mutes_import(%User{} = user, [_ | _] = actors) do +    jobs = +      Repo.checkout(fn -> +        Enum.reduce(actors, [], fn actor, acc -> +          {:ok, job} = +            BackgroundWorker.new(%{ +              "op" => "mute_import", +              "user_id" => user.id, +              "actor" => actor +            }) +            |> Oban.insert() + +          acc ++ [job] +        end) +      end) + +    {:ok, jobs}    end  end diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index a2a94a0ff..df8795fe4 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -1542,16 +1542,23 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do    defp get_actor_url(_url), do: nil -  defp normalize_image(%{"url" => url}) do +  defp normalize_image(%{"url" => url} = data) do      %{        "type" => "Image",        "url" => [%{"href" => url}]      } +    |> maybe_put_description(data)    end    defp normalize_image(urls) when is_list(urls), do: urls |> List.first() |> normalize_image()    defp normalize_image(_), do: nil +  defp maybe_put_description(map, %{"name" => description}) when is_binary(description) do +    Map.put(map, "name", description) +  end + +  defp maybe_put_description(map, _), do: map +    defp object_to_user_data(data, additional) do      fields =        data diff --git a/lib/pleroma/web/activity_pub/activity_pub_controller.ex b/lib/pleroma/web/activity_pub/activity_pub_controller.ex index cdd054e1a..7ac0bbab4 100644 --- a/lib/pleroma/web/activity_pub/activity_pub_controller.ex +++ b/lib/pleroma/web/activity_pub/activity_pub_controller.ex @@ -311,7 +311,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do        post_inbox_relayed_create(conn, params)      else        conn -      |> put_status(:bad_request) +      |> put_status(403)        |> json("Not federating")      end    end @@ -482,7 +482,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do          |> put_status(:forbidden)          |> json(message) -      {:error, message} -> +      {:error, message} when is_binary(message) ->          conn          |> put_status(:bad_request)          |> json(message) diff --git a/lib/pleroma/web/activity_pub/mrf.ex b/lib/pleroma/web/activity_pub/mrf.ex index bc418d908..51ab476b7 100644 --- a/lib/pleroma/web/activity_pub/mrf.ex +++ b/lib/pleroma/web/activity_pub/mrf.ex @@ -108,6 +108,14 @@ defmodule Pleroma.Web.ActivityPub.MRF do    def filter(%{} = object), do: get_policies() |> filter(object) +  def id_filter(policies, id) when is_binary(id) do +    policies +    |> Enum.filter(&function_exported?(&1, :id_filter, 1)) +    |> Enum.all?(& &1.id_filter(id)) +  end + +  def id_filter(id) when is_binary(id), do: get_policies() |> id_filter(id) +    @impl true    def pipeline_filter(%{} = message, meta) do      object = meta[:object_data] diff --git a/lib/pleroma/web/activity_pub/mrf/drop_policy.ex b/lib/pleroma/web/activity_pub/mrf/drop_policy.ex index e4fcc9935..cf07db7f3 100644 --- a/lib/pleroma/web/activity_pub/mrf/drop_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/drop_policy.ex @@ -14,5 +14,11 @@ defmodule Pleroma.Web.ActivityPub.MRF.DropPolicy do    end    @impl true +  def id_filter(id) do +    Logger.debug("REJECTING #{id}") +    false +  end + +  @impl true    def describe, do: {:ok, %{}}  end diff --git a/lib/pleroma/web/activity_pub/mrf/policy.ex b/lib/pleroma/web/activity_pub/mrf/policy.ex index 54ca4b735..08bcac08a 100644 --- a/lib/pleroma/web/activity_pub/mrf/policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/policy.ex @@ -4,6 +4,7 @@  defmodule Pleroma.Web.ActivityPub.MRF.Policy do    @callback filter(Pleroma.Activity.t()) :: {:ok | :reject, Pleroma.Activity.t()} +  @callback id_filter(String.t()) :: boolean()    @callback describe() :: {:ok | :error, map()}    @callback config_description() :: %{                optional(:children) => [map()], @@ -13,5 +14,5 @@ defmodule Pleroma.Web.ActivityPub.MRF.Policy do                description: String.t()              }    @callback history_awareness() :: :auto | :manual -  @optional_callbacks config_description: 0, history_awareness: 0 +  @optional_callbacks config_description: 0, history_awareness: 0, id_filter: 1  end diff --git a/lib/pleroma/web/activity_pub/mrf/remote_report_policy.ex b/lib/pleroma/web/activity_pub/mrf/remote_report_policy.ex new file mode 100644 index 000000000..fa0610bf1 --- /dev/null +++ b/lib/pleroma/web/activity_pub/mrf/remote_report_policy.ex @@ -0,0 +1,118 @@ +defmodule Pleroma.Web.ActivityPub.MRF.RemoteReportPolicy do +  @moduledoc "Drop remote reports if they don't contain enough information." +  @behaviour Pleroma.Web.ActivityPub.MRF.Policy + +  alias Pleroma.Config + +  @impl true +  def filter(%{"type" => "Flag"} = object) do +    with {_, false} <- {:local, local?(object)}, +         {:ok, _} <- maybe_reject_all(object), +         {:ok, _} <- maybe_reject_anonymous(object), +         {:ok, _} <- maybe_reject_third_party(object), +         {:ok, _} <- maybe_reject_empty_message(object) do +      {:ok, object} +    else +      {:local, true} -> {:ok, object} +      {:reject, message} -> {:reject, message} +      error -> {:reject, error} +    end +  end + +  def filter(object), do: {:ok, object} + +  defp maybe_reject_all(object) do +    if Config.get([:mrf_remote_report, :reject_all]) do +      {:reject, "[RemoteReportPolicy] Remote report"} +    else +      {:ok, object} +    end +  end + +  defp maybe_reject_anonymous(%{"actor" => actor} = object) do +    with true <- Config.get([:mrf_remote_report, :reject_anonymous]), +         %URI{path: "/actor"} <- URI.parse(actor) do +      {:reject, "[RemoteReportPolicy] Anonymous: #{actor}"} +    else +      _ -> {:ok, object} +    end +  end + +  defp maybe_reject_third_party(%{"object" => objects} = object) do +    {_, to} = +      case objects do +        [head | tail] when is_binary(head) -> {tail, head} +        s when is_binary(s) -> {[], s} +        _ -> {[], ""} +      end + +    with true <- Config.get([:mrf_remote_report, :reject_third_party]), +         false <- String.starts_with?(to, Pleroma.Web.Endpoint.url()) do +      {:reject, "[RemoteReportPolicy] Third-party: #{to}"} +    else +      _ -> {:ok, object} +    end +  end + +  defp maybe_reject_empty_message(%{"content" => content} = object) +       when is_binary(content) and content != "" do +    {:ok, object} +  end + +  defp maybe_reject_empty_message(object) do +    if Config.get([:mrf_remote_report, :reject_empty_message]) do +      {:reject, ["RemoteReportPolicy] No content"]} +    else +      {:ok, object} +    end +  end + +  defp local?(%{"actor" => actor}) do +    String.starts_with?(actor, Pleroma.Web.Endpoint.url()) +  end + +  @impl true +  def describe do +    mrf_remote_report = +      Config.get(:mrf_remote_report) +      |> Enum.into(%{}) + +    {:ok, %{mrf_remote_report: mrf_remote_report}} +  end + +  @impl true +  def config_description do +    %{ +      key: :mrf_remote_report, +      related_policy: "Pleroma.Web.ActivityPub.MRF.RemoteReportPolicy", +      label: "MRF Remote Report", +      description: "Drop remote reports if they don't contain enough information.", +      children: [ +        %{ +          key: :reject_all, +          type: :boolean, +          description: "Reject all remote reports? (this option takes precedence)", +          suggestions: [false] +        }, +        %{ +          key: :reject_anonymous, +          type: :boolean, +          description: "Reject anonymous remote reports?", +          suggestions: [true] +        }, +        %{ +          key: :reject_third_party, +          type: :boolean, +          description: "Reject reports on users from third-party instances?", +          suggestions: [true] +        }, +        %{ +          key: :reject_empty_message, +          type: :boolean, +          description: "Reject remote reports with no message?", +          suggestions: [true] +        } +      ] +    } +  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 ae7f18bfe..a97e8db7b 100644 --- a/lib/pleroma/web/activity_pub/mrf/simple_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/simple_policy.ex @@ -192,6 +192,18 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do    end    @impl true +  def id_filter(id) do +    host_info = URI.parse(id) + +    with {:ok, _} <- check_accept(host_info, %{}), +         {:ok, _} <- check_reject(host_info, %{}) do +      true +    else +      _ -> false +    end +  end + +  @impl true    def filter(%{"type" => "Delete", "actor" => actor} = activity) do      %{host: actor_host} = URI.parse(actor) diff --git a/lib/pleroma/web/activity_pub/object_validator.ex b/lib/pleroma/web/activity_pub/object_validator.ex index abec9b038..ee12f3ebf 100644 --- a/lib/pleroma/web/activity_pub/object_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validator.ex @@ -11,6 +11,8 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do    @behaviour Pleroma.Web.ActivityPub.ObjectValidator.Validating +  import Pleroma.Constants, only: [activity_types: 0, object_types: 0] +    alias Pleroma.Activity    alias Pleroma.EctoType.ActivityPub.ObjectValidators    alias Pleroma.Object @@ -39,6 +41,16 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do    @impl true    def validate(object, meta) +  # This overload works together with the InboxGuardPlug +  # and ensures that we are not accepting any activity type +  # that cannot pass InboxGuardPlug. +  # If we want to support any more activity types, make sure to +  # add it in Pleroma.Constants's activity_types or object_types, +  # and, if applicable, allowed_activity_types_from_strangers. +  def validate(%{"type" => type}, _meta) +      when type not in activity_types() and type not in object_types(), +      do: {:error, :not_allowed_object_type} +    def validate(%{"type" => "Block"} = block_activity, meta) do      with {:ok, block_activity} <-             block_activity @@ -165,7 +177,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do           meta = Keyword.put(meta, :object_data, object_data),           {:ok, update_activity} <-             update_activity -           |> UpdateValidator.cast_and_validate() +           |> UpdateValidator.cast_and_validate(meta)             |> Ecto.Changeset.apply_action(:insert) do        update_activity = stringify_keys(update_activity)        {:ok, update_activity, meta} @@ -173,7 +185,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do        {:local, _} ->          with {:ok, object} <-                 update_activity -               |> UpdateValidator.cast_and_validate() +               |> UpdateValidator.cast_and_validate(meta)                 |> Ecto.Changeset.apply_action(:insert) do            object = stringify_keys(object)            {:ok, object, meta} @@ -203,9 +215,16 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do          "Answer" -> AnswerValidator        end +    cast_func = +      if type == "Update" do +        fn o -> validator.cast_and_validate(o, meta) end +      else +        fn o -> validator.cast_and_validate(o) end +      end +      with {:ok, object} <-             object -           |> validator.cast_and_validate() +           |> cast_func.()             |> Ecto.Changeset.apply_action(:insert) do        object = stringify_keys(object)        {:ok, object, meta} 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 4e27284aa..81ab354fe 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 @@ -85,6 +85,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidator do      |> fix_replies()      |> fix_attachments()      |> CommonFixes.fix_quote_url() +    |> CommonFixes.fix_likes()      |> Transmogrifier.fix_emoji()      |> Transmogrifier.fix_content_map()      |> CommonFixes.maybe_add_language() diff --git a/lib/pleroma/web/activity_pub/object_validators/audio_image_video_validator.ex b/lib/pleroma/web/activity_pub/object_validators/audio_image_video_validator.ex index 65ac6bb93..034c6f33f 100644 --- a/lib/pleroma/web/activity_pub/object_validators/audio_image_video_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/audio_image_video_validator.ex @@ -100,6 +100,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AudioImageVideoValidator do      |> CommonFixes.fix_actor()      |> CommonFixes.fix_object_defaults()      |> CommonFixes.fix_quote_url() +    |> CommonFixes.fix_likes()      |> Transmogrifier.fix_emoji()      |> fix_url()      |> fix_content() 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 a9dc4a312..87d3e0c8f 100644 --- a/lib/pleroma/web/activity_pub/object_validators/common_fixes.ex +++ b/lib/pleroma/web/activity_pub/object_validators/common_fixes.ex @@ -119,6 +119,13 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes do    def fix_quote_url(data), do: data +  # On Mastodon, `"likes"` attribute includes an inlined `Collection` with `totalItems`, +  # not a list of users. +  # https://github.com/mastodon/mastodon/pull/32007 +  def fix_likes(%{"likes" => %{}} = data), do: Map.drop(data, ["likes"]) + +  def fix_likes(data), do: data +    # https://codeberg.org/fediverse/fep/src/branch/main/fep/e232/fep-e232.md    def object_link_tag?(%{          "type" => "Link", 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 ec23770ad..ea14d6aca 100644 --- a/lib/pleroma/web/activity_pub/object_validators/event_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/event_validator.ex @@ -48,6 +48,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.EventValidator do      data      |> CommonFixes.fix_actor()      |> CommonFixes.fix_object_defaults() +    |> CommonFixes.fix_likes()      |> Transmogrifier.fix_emoji()      |> CommonFixes.maybe_add_language()      |> CommonFixes.maybe_add_content_map() 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 7f9d4d648..21940f4f1 100644 --- a/lib/pleroma/web/activity_pub/object_validators/question_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/question_validator.ex @@ -64,6 +64,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.QuestionValidator do      |> CommonFixes.fix_actor()      |> CommonFixes.fix_object_defaults()      |> CommonFixes.fix_quote_url() +    |> CommonFixes.fix_likes()      |> Transmogrifier.fix_emoji()      |> fix_closed()    end 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 1e940a400..aab90235f 100644 --- a/lib/pleroma/web/activity_pub/object_validators/update_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/update_validator.ex @@ -6,6 +6,8 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.UpdateValidator do    use Ecto.Schema    alias Pleroma.EctoType.ActivityPub.ObjectValidators +  alias Pleroma.Object +  alias Pleroma.User    import Ecto.Changeset    import Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations @@ -31,23 +33,50 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.UpdateValidator do      |> cast(data, __schema__(:fields))    end -  defp validate_data(cng) do +  defp validate_data(cng, meta) do      cng      |> validate_required([:id, :type, :actor, :to, :cc, :object])      |> validate_inclusion(:type, ["Update"])      |> validate_actor_presence() -    |> validate_updating_rights() +    |> validate_updating_rights(meta)    end -  def cast_and_validate(data) do +  def cast_and_validate(data, meta \\ []) do      data      |> cast_data -    |> validate_data +    |> validate_data(meta)    end -  # For now we only support updating users, and here the rule is easy: -  # object id == actor id -  def validate_updating_rights(cng) do +  def validate_updating_rights(cng, meta) do +    if meta[:local] do +      validate_updating_rights_local(cng) +    else +      validate_updating_rights_remote(cng) +    end +  end + +  # For local Updates, verify the actor can edit the object +  def validate_updating_rights_local(cng) do +    actor = get_field(cng, :actor) +    updated_object = get_field(cng, :object) + +    if {:ok, actor} == ObjectValidators.ObjectID.cast(updated_object) do +      cng +    else +      with %User{} = user <- User.get_cached_by_ap_id(actor), +           {_, %Object{} = orig_object} <- {:object, Object.normalize(updated_object)}, +           :ok <- Object.authorize_access(orig_object, user) do +        cng +      else +        _e -> +          cng +          |> add_error(:object, "Can't be updated by this actor") +      end +    end +  end + +  # For remote Updates, verify the host is the same. +  def validate_updating_rights_remote(cng) do      with actor = get_field(cng, :actor),           object = get_field(cng, :object),           {:ok, object_id} <- ObjectValidators.ObjectID.cast(object), diff --git a/lib/pleroma/web/activity_pub/pipeline.ex b/lib/pleroma/web/activity_pub/pipeline.ex index 7f11a4d67..fc36935d5 100644 --- a/lib/pleroma/web/activity_pub/pipeline.ex +++ b/lib/pleroma/web/activity_pub/pipeline.ex @@ -22,22 +22,27 @@ defmodule Pleroma.Web.ActivityPub.Pipeline do    defp activity_pub, do: Config.get([:pipeline, :activity_pub], ActivityPub)    defp config, do: Config.get([:pipeline, :config], Config) -  @spec common_pipeline(map(), keyword()) :: -          {:ok, Activity.t() | Object.t(), keyword()} | {:error | :reject, any()} +  @type results :: {:ok, Activity.t() | Object.t(), keyword()} +  @type errors :: {:error | :reject, any()} + +  # The Repo.transaction will wrap the result in an {:ok, _} +  # and only returns an {:error, _} if the error encountered was related +  # to the SQL transaction +  @spec common_pipeline(map(), keyword()) :: results() | errors()    def common_pipeline(object, meta) 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} -      {:ok, value} -> -        value +      {:ok, {:error, _} = error} -> +        error + +      {:ok, {:reject, _} = error} -> +        error        {:error, e} ->          {:error, e} - -      {:reject, e} -> -        {:reject, e}      end    end diff --git a/lib/pleroma/web/activity_pub/views/user_view.ex b/lib/pleroma/web/activity_pub/views/user_view.ex index 937e4fd67..61975387b 100644 --- a/lib/pleroma/web/activity_pub/views/user_view.ex +++ b/lib/pleroma/web/activity_pub/views/user_view.ex @@ -127,10 +127,25 @@ defmodule Pleroma.Web.ActivityPub.UserView do        "capabilities" => capabilities,        "alsoKnownAs" => user.also_known_as,        "vcard:bday" => birthday, -      "webfinger" => "acct:#{User.full_nickname(user)}" +      "webfinger" => "acct:#{User.full_nickname(user)}", +      "published" => Pleroma.Web.CommonAPI.Utils.to_masto_date(user.inserted_at)      } -    |> Map.merge(maybe_make_image(&User.avatar_url/2, "icon", user)) -    |> Map.merge(maybe_make_image(&User.banner_url/2, "image", user)) +    |> Map.merge( +      maybe_make_image( +        &User.avatar_url/2, +        User.image_description(user.avatar, nil), +        "icon", +        user +      ) +    ) +    |> Map.merge( +      maybe_make_image( +        &User.banner_url/2, +        User.image_description(user.banner, nil), +        "image", +        user +      ) +    )      |> Map.merge(Utils.make_json_ld_header())    end @@ -305,16 +320,24 @@ defmodule Pleroma.Web.ActivityPub.UserView do      end    end -  defp maybe_make_image(func, key, user) do +  defp maybe_make_image(func, description, key, user) do      if image = func.(user, no_default: true) do        %{ -        key => %{ -          "type" => "Image", -          "url" => image -        } +        key => +          %{ +            "type" => "Image", +            "url" => image +          } +          |> maybe_put_description(description)        }      else        %{}      end    end + +  defp maybe_put_description(map, description) when is_binary(description) do +    Map.put(map, "name", description) +  end + +  defp maybe_put_description(map, _description), do: map  end diff --git a/lib/pleroma/web/api_spec/operations/account_operation.ex b/lib/pleroma/web/api_spec/operations/account_operation.ex index d9614bc48..21a779dcb 100644 --- a/lib/pleroma/web/api_spec/operations/account_operation.ex +++ b/lib/pleroma/web/api_spec/operations/account_operation.ex @@ -813,6 +813,16 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do            allOf: [BooleanLike],            nullable: true,            description: "User's birthday will be visible" +        }, +        avatar_description: %Schema{ +          type: :string, +          nullable: true, +          description: "Avatar image description." +        }, +        header_description: %Schema{ +          type: :string, +          nullable: true, +          description: "Header image description."          }        },        example: %{ diff --git a/lib/pleroma/web/api_spec/operations/media_operation.ex b/lib/pleroma/web/api_spec/operations/media_operation.ex index e6df21246..588b42e06 100644 --- a/lib/pleroma/web/api_spec/operations/media_operation.ex +++ b/lib/pleroma/web/api_spec/operations/media_operation.ex @@ -121,7 +121,7 @@ defmodule Pleroma.Web.ApiSpec.MediaOperation do        security: [%{"oAuth" => ["write:media"]}],        requestBody: Helpers.request_body("Parameters", create_request()),        responses: %{ -        202 => Operation.response("Media", "application/json", Attachment), +        200 => Operation.response("Media", "application/json", Attachment),          400 => Operation.response("Media", "application/json", ApiError),          422 => Operation.response("Media", "application/json", ApiError),          500 => Operation.response("Media", "application/json", ApiError) diff --git a/lib/pleroma/web/api_spec/operations/notification_operation.ex b/lib/pleroma/web/api_spec/operations/notification_operation.ex index 2dc0f66df..94d1f6b82 100644 --- a/lib/pleroma/web/api_spec/operations/notification_operation.ex +++ b/lib/pleroma/web/api_spec/operations/notification_operation.ex @@ -158,6 +158,10 @@ defmodule Pleroma.Web.ApiSpec.NotificationOperation do        type: :object,        properties: %{          id: %Schema{type: :string}, +        group_key: %Schema{ +          type: :string, +          description: "Group key shared by similar notifications" +        },          type: notification_type(),          created_at: %Schema{type: :string, format: :"date-time"},          account: %Schema{ @@ -180,6 +184,7 @@ defmodule Pleroma.Web.ApiSpec.NotificationOperation do        },        example: %{          "id" => "34975861", +        "group-key" => "ungrouped-34975861",          "type" => "mention",          "created_at" => "2019-11-23T07:49:02.064Z",          "account" => Account.schema().example, diff --git a/lib/pleroma/web/api_spec/schemas/account.ex b/lib/pleroma/web/api_spec/schemas/account.ex index 8aeb821a8..1f73ef60c 100644 --- a/lib/pleroma/web/api_spec/schemas/account.ex +++ b/lib/pleroma/web/api_spec/schemas/account.ex @@ -111,7 +111,9 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Account do              format: :uri,              nullable: true,              description: "Favicon image of the user's instance" -          } +          }, +          avatar_description: %Schema{type: :string}, +          header_description: %Schema{type: :string}          }        },        source: %Schema{ @@ -152,6 +154,7 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Account do      example: %{        "acct" => "foobar",        "avatar" => "https://mypleroma.com/images/avi.png", +      "avatar_description" => "",        "avatar_static" => "https://mypleroma.com/images/avi.png",        "bot" => false,        "created_at" => "2020-03-24T13:05:58.000Z", @@ -162,6 +165,7 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Account do        "followers_count" => 0,        "following_count" => 1,        "header" => "https://mypleroma.com/images/banner.png", +      "header_description" => "",        "header_static" => "https://mypleroma.com/images/banner.png",        "id" => "9tKi3esbG7OQgZ2920",        "locked" => false, diff --git a/lib/pleroma/web/api_spec/schemas/status.ex b/lib/pleroma/web/api_spec/schemas/status.ex index 6e537b5da..25548d75b 100644 --- a/lib/pleroma/web/api_spec/schemas/status.ex +++ b/lib/pleroma/web/api_spec/schemas/status.ex @@ -249,6 +249,12 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Status do              nullable: true,              description:                "A datetime (ISO 8601) that states when the post was pinned or `null` if the post is not pinned" +          }, +          list_id: %Schema{ +            type: :integer, +            nullable: true, +            description: +              "The ID of the list the post is addressed to (if any, only returned to author)"            }          }        }, diff --git a/lib/pleroma/web/auth/authenticator.ex b/lib/pleroma/web/auth/authenticator.ex index 01bf1575c..95be892cd 100644 --- a/lib/pleroma/web/auth/authenticator.ex +++ b/lib/pleroma/web/auth/authenticator.ex @@ -10,4 +10,9 @@ defmodule Pleroma.Web.Auth.Authenticator do    @callback handle_error(Plug.Conn.t(), any()) :: any()    @callback auth_template() :: String.t() | nil    @callback oauth_consumer_template() :: String.t() | nil + +  @callback change_password(Pleroma.User.t(), String.t(), String.t(), String.t()) :: +              {:ok, Pleroma.User.t()} | {:error, term()} + +  @optional_callbacks change_password: 4  end diff --git a/lib/pleroma/web/auth/ldap_authenticator.ex b/lib/pleroma/web/auth/ldap_authenticator.ex index ea5620cf6..ec6601fb9 100644 --- a/lib/pleroma/web/auth/ldap_authenticator.ex +++ b/lib/pleroma/web/auth/ldap_authenticator.ex @@ -3,18 +3,14 @@  # SPDX-License-Identifier: AGPL-3.0-only  defmodule Pleroma.Web.Auth.LDAPAuthenticator do +  alias Pleroma.LDAP    alias Pleroma.User -  require Logger - -  import Pleroma.Web.Auth.Helpers, only: [fetch_credentials: 1, fetch_user: 1] +  import Pleroma.Web.Auth.Helpers, only: [fetch_credentials: 1]    @behaviour Pleroma.Web.Auth.Authenticator    @base Pleroma.Web.Auth.PleromaAuthenticator -  @connection_timeout 10_000 -  @search_timeout 10_000 -    defdelegate get_registration(conn), to: @base    defdelegate create_from_registration(conn, registration), to: @base    defdelegate handle_error(conn, error), to: @base @@ -24,7 +20,7 @@ defmodule Pleroma.Web.Auth.LDAPAuthenticator do    def get_user(%Plug.Conn{} = conn) do      with {:ldap, true} <- {:ldap, Pleroma.Config.get([:ldap, :enabled])},           {:ok, {name, password}} <- fetch_credentials(conn), -         %User{} = user <- ldap_user(name, password) do +         %User{} = user <- LDAP.bind_user(name, password) do        {:ok, user}      else        {:ldap, _} -> @@ -35,106 +31,12 @@ defmodule Pleroma.Web.Auth.LDAPAuthenticator do      end    end -  defp ldap_user(name, password) do -    ldap = Pleroma.Config.get(:ldap, []) -    host = Keyword.get(ldap, :host, "localhost") -    port = Keyword.get(ldap, :port, 389) -    ssl = Keyword.get(ldap, :ssl, false) -    sslopts = Keyword.get(ldap, :sslopts, []) - -    options = -      [{:port, port}, {:ssl, ssl}, {:timeout, @connection_timeout}] ++ -        if sslopts != [], do: [{:sslopts, sslopts}], else: [] - -    case :eldap.open([to_charlist(host)], options) do -      {:ok, connection} -> -        try do -          if Keyword.get(ldap, :tls, false) do -            :application.ensure_all_started(:ssl) - -            case :eldap.start_tls( -                   connection, -                   Keyword.get(ldap, :tlsopts, []), -                   @connection_timeout -                 ) do -              :ok -> -                :ok - -              error -> -                Logger.error("Could not start TLS: #{inspect(error)}") -            end -          end - -          bind_user(connection, ldap, name, password) -        after -          :eldap.close(connection) -        end - -      {:error, error} -> -        Logger.error("Could not open LDAP connection: #{inspect(error)}") -        {:error, {:ldap_connection_error, error}} -    end -  end - -  defp bind_user(connection, ldap, name, password) do -    uid = Keyword.get(ldap, :uid, "cn") -    base = Keyword.get(ldap, :base) - -    case :eldap.simple_bind(connection, "#{uid}=#{name},#{base}", password) do -      :ok -> -        case fetch_user(name) do -          %User{} = user -> -            user - -          _ -> -            register_user(connection, base, uid, name) -        end - -      error -> -        Logger.error("Could not bind LDAP user #{name}: #{inspect(error)}") -        {:error, {:ldap_bind_error, error}} -    end -  end - -  defp register_user(connection, base, uid, name) do -    case :eldap.search(connection, [ -           {:base, to_charlist(base)}, -           {:filter, :eldap.equalityMatch(to_charlist(uid), to_charlist(name))}, -           {:scope, :eldap.wholeSubtree()}, -           {:timeout, @search_timeout} -         ]) do -      # The :eldap_search_result record structure changed in OTP 24.3 and added a controls field -      # https://github.com/erlang/otp/pull/5538 -      {:ok, {:eldap_search_result, [{:eldap_entry, _object, attributes}], _referrals}} -> -        try_register(name, attributes) - -      {:ok, {:eldap_search_result, [{:eldap_entry, _object, attributes}], _referrals, _controls}} -> -        try_register(name, attributes) - -      error -> -        Logger.error("Couldn't register user because LDAP search failed: #{inspect(error)}") -        {:error, {:ldap_search_error, error}} +  def change_password(user, password, new_password, new_password) do +    case LDAP.change_password(user.nickname, password, new_password) do +      :ok -> {:ok, user} +      e -> e      end    end -  defp try_register(name, attributes) do -    params = %{ -      name: name, -      nickname: name, -      password: nil -    } - -    params = -      case List.keyfind(attributes, ~c"mail", 0) do -        {_, [mail]} -> Map.put_new(params, :email, :erlang.list_to_binary(mail)) -        _ -> params -      end - -    changeset = User.register_changeset_ldap(%User{}, params) - -    case User.register(changeset) do -      {:ok, user} -> user -      error -> error -    end -  end +  def change_password(_, _, _, _), do: {:error, :password_confirmation}  end diff --git a/lib/pleroma/web/auth/pleroma_authenticator.ex b/lib/pleroma/web/auth/pleroma_authenticator.ex index 09a58eb66..0da3f19fc 100644 --- a/lib/pleroma/web/auth/pleroma_authenticator.ex +++ b/lib/pleroma/web/auth/pleroma_authenticator.ex @@ -6,6 +6,7 @@ defmodule Pleroma.Web.Auth.PleromaAuthenticator do    alias Pleroma.Registration    alias Pleroma.Repo    alias Pleroma.User +  alias Pleroma.Web.CommonAPI    alias Pleroma.Web.Plugs.AuthenticationPlug    import Pleroma.Web.Auth.Helpers, only: [fetch_credentials: 1, fetch_user: 1] @@ -101,4 +102,23 @@ defmodule Pleroma.Web.Auth.PleromaAuthenticator do    def auth_template, do: nil    def oauth_consumer_template, do: nil + +  @doc "Changes Pleroma.User password in the database" +  def change_password(user, password, new_password, new_password) do +    case CommonAPI.Utils.confirm_current_password(user, password) do +      {:ok, user} -> +        with {:ok, _user} <- +               User.reset_password(user, %{ +                 password: new_password, +                 password_confirmation: new_password +               }) do +          {:ok, user} +        end + +      error -> +        error +    end +  end + +  def change_password(_, _, _, _), do: {:error, :password_confirmation}  end diff --git a/lib/pleroma/web/auth/wrapper_authenticator.ex b/lib/pleroma/web/auth/wrapper_authenticator.ex index a077cfa41..97b901036 100644 --- a/lib/pleroma/web/auth/wrapper_authenticator.ex +++ b/lib/pleroma/web/auth/wrapper_authenticator.ex @@ -39,4 +39,8 @@ defmodule Pleroma.Web.Auth.WrapperAuthenticator do      implementation().oauth_consumer_template() ||        Pleroma.Config.get([:auth, :oauth_consumer_template], "consumer.html")    end + +  @impl true +  def change_password(user, password, new_password, new_password_confirmation), +    do: implementation().change_password(user, password, new_password, new_password_confirmation)  end diff --git a/lib/pleroma/web/common_api.ex b/lib/pleroma/web/common_api.ex index 921e414c3..412424dae 100644 --- a/lib/pleroma/web/common_api.ex +++ b/lib/pleroma/web/common_api.ex @@ -26,7 +26,7 @@ defmodule Pleroma.Web.CommonAPI do    require Pleroma.Constants    require Logger -  @spec block(User.t(), User.t()) :: {:ok, Activity.t()} | {:error, any()} +  @spec block(User.t(), User.t()) :: {:ok, Activity.t()} | Pipeline.errors()    def block(blocked, blocker) do      with {:ok, block_data, _} <- Builder.block(blocker, blocked),           {:ok, block, _} <- Pipeline.common_pipeline(block_data, local: true) do @@ -35,7 +35,7 @@ defmodule Pleroma.Web.CommonAPI do    end    @spec post_chat_message(User.t(), User.t(), String.t(), list()) :: -          {:ok, Activity.t()} | {:error, any()} +          {:ok, Activity.t()} | Pipeline.errors()    def post_chat_message(%User{} = user, %User{} = recipient, content, opts \\ []) do      with maybe_attachment <- opts[:media_id] && Object.get_by_id(opts[:media_id]),           :ok <- validate_chat_attachment_attribution(maybe_attachment, user), @@ -58,7 +58,7 @@ defmodule Pleroma.Web.CommonAPI do              )} do        {:ok, activity}      else -      {:common_pipeline, {:reject, _} = e} -> e +      {:common_pipeline, e} -> e        e -> e      end    end @@ -99,7 +99,8 @@ defmodule Pleroma.Web.CommonAPI do      end    end -  @spec unblock(User.t(), User.t()) :: {:ok, Activity.t()} | {:error, any()} +  @spec unblock(User.t(), User.t()) :: +          {:ok, Activity.t()} | {:ok, :no_activity} | Pipeline.errors() | {:error, :not_blocking}    def unblock(blocked, blocker) do      with {_, %Activity{} = block} <- {:fetch_block, Utils.fetch_latest_block(blocker, blocked)},           {:ok, unblock_data, _} <- Builder.undo(blocker, block), @@ -120,7 +121,9 @@ defmodule Pleroma.Web.CommonAPI do    end    @spec follow(User.t(), User.t()) :: -          {:ok, User.t(), User.t(), Activity.t() | Object.t()} | {:error, :rejected} +          {:ok, User.t(), User.t(), Activity.t() | Object.t()} +          | {:error, :rejected} +          | Pipeline.errors()    def follow(followed, follower) do      timeout = Pleroma.Config.get([:activitypub, :follow_handshake_timeout]) @@ -145,7 +148,7 @@ defmodule Pleroma.Web.CommonAPI do      end    end -  @spec accept_follow_request(User.t(), User.t()) :: {:ok, User.t()} | {:error, any()} +  @spec accept_follow_request(User.t(), User.t()) :: {:ok, User.t()} | Pipeline.errors()    def accept_follow_request(follower, followed) do      with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),           {:ok, accept_data, _} <- Builder.accept(followed, follow_activity), @@ -154,7 +157,7 @@ defmodule Pleroma.Web.CommonAPI do      end    end -  @spec reject_follow_request(User.t(), User.t()) :: {:ok, User.t()} | {:error, any()} | nil +  @spec reject_follow_request(User.t(), User.t()) :: {:ok, User.t()} | Pipeline.errors() | nil    def reject_follow_request(follower, followed) do      with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),           {:ok, reject_data, _} <- Builder.reject(followed, follow_activity), @@ -163,7 +166,8 @@ defmodule Pleroma.Web.CommonAPI do      end    end -  @spec delete(String.t(), User.t()) :: {:ok, Activity.t()} | {:error, any()} +  @spec delete(String.t(), User.t()) :: +          {:ok, Activity.t()} | Pipeline.errors() | {:error, :not_found | String.t()}    def delete(activity_id, user) do      with {_, %Activity{data: %{"object" => _, "type" => "Create"}} = activity} <-             {:find_activity, Activity.get_by_id(activity_id, filter: [])}, @@ -213,7 +217,7 @@ defmodule Pleroma.Web.CommonAPI do      end    end -  @spec repeat(String.t(), User.t(), map()) :: {:ok, Activity.t()} | {:error, any()} +  @spec repeat(String.t(), User.t(), map()) :: {:ok, Activity.t()} | {:error, :not_found}    def repeat(id, user, params \\ %{}) do      with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id(id),           object = %Object{} <- Object.normalize(activity, fetch: false), @@ -231,7 +235,7 @@ defmodule Pleroma.Web.CommonAPI do      end    end -  @spec unrepeat(String.t(), User.t()) :: {:ok, Activity.t()} | {:error, any()} +  @spec unrepeat(String.t(), User.t()) :: {:ok, Activity.t()} | {:error, :not_found | String.t()}    def unrepeat(id, user) do      with {_, %Activity{data: %{"type" => "Create"}} = activity} <-             {:find_activity, Activity.get_by_id(id)}, @@ -247,7 +251,8 @@ defmodule Pleroma.Web.CommonAPI do      end    end -  @spec favorite(String.t(), User.t()) :: {:ok, Activity.t()} | {:error, any()} +  @spec favorite(String.t(), User.t()) :: +          {:ok, Activity.t()} | {:ok, :already_liked} | {:error, :not_found | String.t()}    def favorite(id, %User{} = user) do      case favorite_helper(user, id) do        {:ok, _} = res -> @@ -285,7 +290,8 @@ defmodule Pleroma.Web.CommonAPI do      end    end -  @spec unfavorite(String.t(), User.t()) :: {:ok, Activity.t()} | {:error, any()} +  @spec unfavorite(String.t(), User.t()) :: +          {:ok, Activity.t()} | {:error, :not_found | String.t()}    def unfavorite(id, user) do      with {_, %Activity{data: %{"type" => "Create"}} = activity} <-             {:find_activity, Activity.get_by_id(id)}, @@ -302,7 +308,7 @@ defmodule Pleroma.Web.CommonAPI do    end    @spec react_with_emoji(String.t(), User.t(), String.t()) :: -          {:ok, Activity.t()} | {:error, any()} +          {:ok, Activity.t()} | {:error, String.t()}    def react_with_emoji(id, user, emoji) do      with %Activity{} = activity <- Activity.get_by_id(id),           object <- Object.normalize(activity, fetch: false), @@ -316,7 +322,7 @@ defmodule Pleroma.Web.CommonAPI do    end    @spec unreact_with_emoji(String.t(), User.t(), String.t()) :: -          {:ok, Activity.t()} | {:error, any()} +          {:ok, Activity.t()} | {:error, String.t()}    def unreact_with_emoji(id, user, emoji) do      with %Activity{} = reaction_activity <- Utils.get_latest_reaction(id, user, emoji),           {_, {:ok, _}} <- {:cancel_jobs, maybe_cancel_jobs(reaction_activity)}, @@ -329,7 +335,7 @@ defmodule Pleroma.Web.CommonAPI do      end    end -  @spec vote(Object.t(), User.t(), list()) :: {:ok, list(), Object.t()} | {:error, any()} +  @spec vote(Object.t(), User.t(), list()) :: {:ok, list(), Object.t()} | Pipeline.errors()    def vote(%Object{data: %{"type" => "Question"}} = object, %User{} = user, choices) do      with :ok <- validate_not_author(object, user),           :ok <- validate_existing_votes(user, object), @@ -461,7 +467,7 @@ defmodule Pleroma.Web.CommonAPI do      end    end -  @spec update(Activity.t(), User.t(), map()) :: {:ok, Activity.t()} | {:error, any()} +  @spec update(Activity.t(), User.t(), map()) :: {:ok, Activity.t()} | {:error, nil}    def update(orig_activity, %User{} = user, changes) do      with orig_object <- Object.normalize(orig_activity),           {:ok, new_object} <- make_update_data(user, orig_object, changes), @@ -497,7 +503,7 @@ defmodule Pleroma.Web.CommonAPI do      end    end -  @spec pin(String.t(), User.t()) :: {:ok, Activity.t()} | {:error, term()} +  @spec pin(String.t(), User.t()) :: {:ok, Activity.t()} | Pipeline.errors()    def pin(id, %User{} = user) do      with %Activity{} = activity <- create_activity_by_id(id),           true <- activity_belongs_to_actor(activity, user.ap_id), @@ -537,7 +543,7 @@ defmodule Pleroma.Web.CommonAPI do      end    end -  @spec unpin(String.t(), User.t()) :: {:ok, Activity.t()} | {:error, term()} +  @spec unpin(String.t(), User.t()) :: {:ok, Activity.t()} | Pipeline.errors()    def unpin(id, user) do      with %Activity{} = activity <- create_activity_by_id(id),           {:ok, unpin_data, _} <- Builder.unpin(user, activity.object), @@ -552,7 +558,7 @@ defmodule Pleroma.Web.CommonAPI do      end    end -  @spec add_mute(Activity.t(), User.t(), map()) :: {:ok, Activity.t()} | {:error, any()} +  @spec add_mute(Activity.t(), User.t(), map()) :: {:ok, Activity.t()} | {:error, String.t()}    def add_mute(activity, user, params \\ %{}) do      expires_in = Map.get(params, :expires_in, 0) diff --git a/lib/pleroma/web/endpoint.ex b/lib/pleroma/web/endpoint.ex index fef907ace..bab3c9fd0 100644 --- a/lib/pleroma/web/endpoint.ex +++ b/lib/pleroma/web/endpoint.ex @@ -14,6 +14,7 @@ defmodule Pleroma.Web.Endpoint do      websocket: [        path: "/",        compress: false, +      connect_info: [:sec_websocket_protocol],        error_handler: {Pleroma.Web.MastodonAPI.WebsocketHandler, :handle_error, []},        fullsweep_after: 20      ] diff --git a/lib/pleroma/web/fallback/redirect_controller.ex b/lib/pleroma/web/fallback/redirect_controller.ex index 4a0885fab..6637848a9 100644 --- a/lib/pleroma/web/fallback/redirect_controller.ex +++ b/lib/pleroma/web/fallback/redirect_controller.ex @@ -46,7 +46,7 @@ defmodule Pleroma.Web.Fallback.RedirectController do        redirector_with_meta(conn, %{user: user})      else        nil -> -        redirector(conn, params) +        redirector_with_meta(conn, Map.delete(params, "maybe_nickname_or_id"))      end    end diff --git a/lib/pleroma/web/federator.ex b/lib/pleroma/web/federator.ex index 2df716556..58260afa8 100644 --- a/lib/pleroma/web/federator.ex +++ b/lib/pleroma/web/federator.ex @@ -102,7 +102,8 @@ defmodule Pleroma.Web.Federator do      # NOTE: we use the actor ID to do the containment, this is fine because an      # actor shouldn't be acting on objects outside their own AP server. -    with {_, {:ok, _user}} <- {:actor, User.get_or_fetch_by_ap_id(actor)}, +    with {_, {:ok, user}} <- {:actor, User.get_or_fetch_by_ap_id(actor)}, +         {:user_active, true} <- {:user_active, match?(true, user.is_active)},           nil <- Activity.normalize(params["id"]),           {_, :ok} <-             {:correct_origin?, Containment.contain_origin_from_id(actor, params)}, @@ -121,11 +122,6 @@ defmodule Pleroma.Web.Federator do          Logger.debug("Unhandled actor #{actor}, #{inspect(e)}")          {:error, e} -      {:error, {:validate_object, _}} = e -> -        Logger.error("Incoming AP doc validation error: #{inspect(e)}") -        Logger.debug(Jason.encode!(params, pretty: true)) -        e -        e ->          # Just drop those for now          Logger.debug(fn -> "Unhandled activity\n" <> Jason.encode!(params, pretty: true) end) diff --git a/lib/pleroma/web/feed/tag_controller.ex b/lib/pleroma/web/feed/tag_controller.ex index e60767327..02d639296 100644 --- a/lib/pleroma/web/feed/tag_controller.ex +++ b/lib/pleroma/web/feed/tag_controller.ex @@ -10,7 +10,7 @@ defmodule Pleroma.Web.Feed.TagController do    alias Pleroma.Web.Feed.FeedView    def feed(conn, params) do -    if Config.get!([:instance, :public]) do +    if not Config.restrict_unauthenticated_access?(:timelines, :local) do        render_feed(conn, params)      else        render_error(conn, :not_found, "Not found") @@ -18,10 +18,12 @@ defmodule Pleroma.Web.Feed.TagController do    end    defp render_feed(conn, %{"tag" => raw_tag} = params) do +    local_only = Config.restrict_unauthenticated_access?(:timelines, :federated) +      {format, tag} = parse_tag(raw_tag)      activities = -      %{type: ["Create"], tag: tag} +      %{type: ["Create"], tag: tag, local_only: local_only}        |> Pleroma.Maps.put_if_present(:max_id, params["max_id"])        |> ActivityPub.fetch_public_activities() diff --git a/lib/pleroma/web/feed/user_controller.ex b/lib/pleroma/web/feed/user_controller.ex index 6657c2b3e..304313068 100644 --- a/lib/pleroma/web/feed/user_controller.ex +++ b/lib/pleroma/web/feed/user_controller.ex @@ -15,11 +15,11 @@ defmodule Pleroma.Web.Feed.UserController do    action_fallback(:errors) -  def feed_redirect(%{assigns: %{format: "html"}} = conn, %{"nickname" => nickname}) do +  def feed_redirect(%{assigns: %{format: "html"}} = conn, %{"nickname" => nickname} = params) do      with {_, %User{} = user} <- {:fetch_user, User.get_cached_by_nickname_or_id(nickname)} do        Pleroma.Web.Fallback.RedirectController.redirector_with_meta(conn, %{user: user})      else -      _ -> Pleroma.Web.Fallback.RedirectController.redirector(conn, nil) +      _ -> Pleroma.Web.Fallback.RedirectController.redirector_with_meta(conn, params)      end    end diff --git a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex index 54d46c86b..68157b0c4 100644 --- a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex @@ -232,6 +232,8 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do        |> Maps.put_if_present(:is_discoverable, params[:discoverable])        |> Maps.put_if_present(:birthday, params[:birthday])        |> Maps.put_if_present(:language, Pleroma.Web.Gettext.normalize_locale(params[:language])) +      |> Maps.put_if_present(:avatar_description, params[:avatar_description]) +      |> Maps.put_if_present(:header_description, params[:header_description])      # What happens here:      # @@ -277,6 +279,12 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do        {:error, %Ecto.Changeset{errors: [{:name, {_, _}} | _]}} ->          render_error(conn, :request_entity_too_large, "Name is too long") +      {:error, %Ecto.Changeset{errors: [{:avatar_description, {_, _}} | _]}} -> +        render_error(conn, :request_entity_too_large, "Avatar description is too long") + +      {:error, %Ecto.Changeset{errors: [{:header_description, {_, _}} | _]}} -> +        render_error(conn, :request_entity_too_large, "Banner description is too long") +        {:error, %Ecto.Changeset{errors: [{:fields, {"invalid", _}} | _]}} ->          render_error(conn, :request_entity_too_large, "One or more field entries are too long") diff --git a/lib/pleroma/web/mastodon_api/controllers/app_controller.ex b/lib/pleroma/web/mastodon_api/controllers/app_controller.ex index 844673ae0..6cfeb712e 100644 --- a/lib/pleroma/web/mastodon_api/controllers/app_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/app_controller.ex @@ -19,6 +19,8 @@ defmodule Pleroma.Web.MastodonAPI.AppController do    action_fallback(Pleroma.Web.MastodonAPI.FallbackController) +  plug(Pleroma.Web.Plugs.RateLimiter, [name: :oauth_app_creation] when action == :create) +    plug(:skip_auth when action in [:create, :verify_credentials])    plug(Pleroma.Web.ApiSpec.CastAndValidate) diff --git a/lib/pleroma/web/mastodon_api/controllers/media_controller.ex b/lib/pleroma/web/mastodon_api/controllers/media_controller.ex index 056bad844..41056d389 100644 --- a/lib/pleroma/web/mastodon_api/controllers/media_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/media_controller.ex @@ -53,9 +53,7 @@ defmodule Pleroma.Web.MastodonAPI.MediaController do             ) do        attachment_data = Map.put(object.data, "id", object.id) -      conn -      |> put_status(202) -      |> render("attachment.json", %{attachment: attachment_data}) +      render(conn, "attachment.json", %{attachment: attachment_data})      end    end diff --git a/lib/pleroma/web/mastodon_api/controllers/poll_controller.ex b/lib/pleroma/web/mastodon_api/controllers/poll_controller.ex index a2af8148c..6526457df 100644 --- a/lib/pleroma/web/mastodon_api/controllers/poll_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/poll_controller.ex @@ -12,6 +12,7 @@ defmodule Pleroma.Web.MastodonAPI.PollController do    alias Pleroma.Web.ActivityPub.Visibility    alias Pleroma.Web.CommonAPI    alias Pleroma.Web.Plugs.OAuthScopesPlug +  alias Pleroma.Workers.PollWorker    action_fallback(Pleroma.Web.MastodonAPI.FallbackController) @@ -27,12 +28,16 @@ defmodule Pleroma.Web.MastodonAPI.PollController do    defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PollOperation    @cachex Pleroma.Config.get([:cachex, :provider], Cachex) +  @poll_refresh_interval 120    @doc "GET /api/v1/polls/:id"    def show(%{assigns: %{user: user}, private: %{open_api_spex: %{params: %{id: id}}}} = conn, _) do -    with %Object{} = object <- Object.get_by_id_and_maybe_refetch(id, interval: 60), -         %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]), +    with %Object{} = object <- Object.get_by_id(id), +         %Activity{} = activity <- +           Activity.get_create_by_object_ap_id_with_object(object.data["id"]),           true <- Visibility.visible_for_user?(activity, user) do +      maybe_refresh_poll(activity) +        try_render(conn, "show.json", %{object: object, for: user})      else        error when is_nil(error) or error == false -> @@ -70,4 +75,13 @@ defmodule Pleroma.Web.MastodonAPI.PollController do        end      end)    end + +  defp maybe_refresh_poll(%Activity{object: %Object{} = object} = activity) do +    with false <- activity.local, +         {:ok, end_time} <- NaiveDateTime.from_iso8601(object.data["closed"]), +         {_, :lt} <- {:closed_compare, NaiveDateTime.compare(object.updated_at, end_time)} do +      PollWorker.new(%{"op" => "refresh", "activity_id" => activity.id}) +      |> Oban.insert(unique: [period: @poll_refresh_interval]) +    end +  end  end diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex index 6976ca6e5..f6727d29d 100644 --- a/lib/pleroma/web/mastodon_api/views/account_view.ex +++ b/lib/pleroma/web/mastodon_api/views/account_view.ex @@ -92,14 +92,13 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do          User.get_follow_state(reading_user, target)        end -    followed_by = -      if following_relationships do -        case FollowingRelationship.find(following_relationships, target, reading_user) do -          %{state: :follow_accept} -> true -          _ -> false -        end -      else -        User.following?(target, reading_user) +    followed_by = FollowingRelationship.following?(target, reading_user) +    following = FollowingRelationship.following?(reading_user, target) + +    requested = +      cond do +        following -> false +        true -> match?(:follow_pending, follow_state)        end      subscribing = @@ -114,7 +113,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do      # NOTE: adjust UserRelationship.view_relationships_option/2 on new relation-related flags      %{        id: to_string(target.id), -      following: follow_state == :follow_accept, +      following: following,        followed_by: followed_by,        blocking:          UserRelationship.exists?( @@ -150,7 +149,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do          ),        subscribing: subscribing,        notifying: subscribing, -      requested: follow_state == :follow_pending, +      requested: requested,        domain_blocking: User.blocks_domain?(reading_user, target),        showing_reblogs:          not UserRelationship.exists?( @@ -220,8 +219,10 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do      avatar = User.avatar_url(user) |> MediaProxy.url()      avatar_static = User.avatar_url(user) |> MediaProxy.preview_url(static: true) +    avatar_description = User.image_description(user.avatar)      header = User.banner_url(user) |> MediaProxy.url()      header_static = User.banner_url(user) |> MediaProxy.preview_url(static: true) +    header_description = User.image_description(user.banner)      following_count =        if !user.hide_follows_count or !user.hide_follows or self, @@ -322,7 +323,9 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do          skip_thread_containment: user.skip_thread_containment,          background_image: image_url(user.background) |> MediaProxy.url(),          accepts_chat_messages: user.accepts_chat_messages, -        favicon: favicon +        favicon: favicon, +        avatar_description: avatar_description, +        header_description: header_description        }      }      |> maybe_put_role(user, opts[:for]) diff --git a/lib/pleroma/web/mastodon_api/views/notification_view.ex b/lib/pleroma/web/mastodon_api/views/notification_view.ex index 3f2478719..c277af98b 100644 --- a/lib/pleroma/web/mastodon_api/views/notification_view.ex +++ b/lib/pleroma/web/mastodon_api/views/notification_view.ex @@ -95,6 +95,7 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do      response = %{        id: to_string(notification.id), +      group_key: "ungrouped-" <> to_string(notification.id),        type: notification.type,        created_at: CommonAPI.Utils.to_masto_date(notification.inserted_at),        account: account, diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex index a739b5d1a..10966edd6 100644 --- a/lib/pleroma/web/mastodon_api/views/status_view.ex +++ b/lib/pleroma/web/mastodon_api/views/status_view.ex @@ -465,7 +465,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do          parent_visible: visible_for_user?(reply_to, opts[:for]),          pinned_at: pinned_at,          quotes_count: object.data["quotesCount"] || 0, -        bookmark_folder: bookmark_folder +        bookmark_folder: bookmark_folder, +        list_id: get_list_id(object, client_posted_this_activity)        }      }    end @@ -803,19 +804,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do    defp build_application(_), do: nil -  # Workaround for Elixir issue #10771 -  # Avoid applying URI.merge unless necessary -  # TODO: revert to always attempting URI.merge(image_url_data, page_url_data) -  # when Elixir 1.12 is the minimum supported version -  @spec build_image_url(struct() | nil, struct()) :: String.t() | nil -  defp build_image_url( -         %URI{scheme: image_scheme, host: image_host} = image_url_data, -         %URI{} = _page_url_data -       ) -       when not is_nil(image_scheme) and not is_nil(image_host) do -    image_url_data |> to_string -  end - +  @spec build_image_url(URI.t(), URI.t()) :: String.t()    defp build_image_url(%URI{} = image_url_data, %URI{} = page_url_data) do      URI.merge(page_url_data, image_url_data) |> to_string    end @@ -851,4 +840,14 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do        nil      end    end + +  defp get_list_id(object, client_posted_this_activity) do +    with true <- client_posted_this_activity, +         %{data: %{"listMessage" => list_ap_id}} when is_binary(list_ap_id) <- object, +         %{id: list_id} <- Pleroma.List.get_by_ap_id(list_ap_id) do +      list_id +    else +      _ -> nil +    end +  end  end diff --git a/lib/pleroma/web/mastodon_api/websocket_handler.ex b/lib/pleroma/web/mastodon_api/websocket_handler.ex index 730295a4c..3ed1cdd6c 100644 --- a/lib/pleroma/web/mastodon_api/websocket_handler.ex +++ b/lib/pleroma/web/mastodon_api/websocket_handler.ex @@ -22,7 +22,7 @@ defmodule Pleroma.Web.MastodonAPI.WebsocketHandler do    # This only prepares the connection and is not in the process yet    @impl Phoenix.Socket.Transport    def connect(%{params: params} = transport_info) do -    with access_token <- Map.get(params, "access_token"), +    with access_token <- find_access_token(transport_info),           {:ok, user, oauth_token} <- authenticate_request(access_token),           {:ok, topic} <-             Streamer.get_topic(params["stream"], user, oauth_token, params) do @@ -244,4 +244,13 @@ defmodule Pleroma.Web.MastodonAPI.WebsocketHandler do    def handle_error(conn, _reason) do      Plug.Conn.send_resp(conn, 404, "Not Found")    end + +  defp find_access_token(%{ +         connect_info: %{sec_websocket_protocol: [token]} +       }), +       do: token + +  defp find_access_token(%{params: %{"access_token" => token}}), do: token + +  defp find_access_token(_), do: nil  end diff --git a/lib/pleroma/web/media_proxy/media_proxy_controller.ex b/lib/pleroma/web/media_proxy/media_proxy_controller.ex index 0b446e0a6..a0aafc32e 100644 --- a/lib/pleroma/web/media_proxy/media_proxy_controller.ex +++ b/lib/pleroma/web/media_proxy/media_proxy_controller.ex @@ -71,11 +71,15 @@ defmodule Pleroma.Web.MediaProxy.MediaProxyController do            drop_static_param_and_redirect(conn)          content_type == "image/gif" -> -          redirect(conn, external: media_proxy_url) +          conn +          |> put_status(301) +          |> redirect(external: media_proxy_url)          min_content_length_for_preview() > 0 and content_length > 0 and              content_length < min_content_length_for_preview() -> -          redirect(conn, external: media_proxy_url) +          conn +          |> put_status(301) +          |> redirect(external: media_proxy_url)          true ->            handle_preview(content_type, conn, media_proxy_url) diff --git a/lib/pleroma/web/metadata.ex b/lib/pleroma/web/metadata.ex index 59d018730..4ee7c41ec 100644 --- a/lib/pleroma/web/metadata.ex +++ b/lib/pleroma/web/metadata.ex @@ -7,6 +7,7 @@ defmodule Pleroma.Web.Metadata do    def build_tags(params) do      providers = [ +      Pleroma.Web.Metadata.Providers.ActivityPub,        Pleroma.Web.Metadata.Providers.RelMe,        Pleroma.Web.Metadata.Providers.RestrictIndexing        | activated_providers() diff --git a/lib/pleroma/web/metadata/providers/activity_pub.ex b/lib/pleroma/web/metadata/providers/activity_pub.ex new file mode 100644 index 000000000..bd9f92332 --- /dev/null +++ b/lib/pleroma/web/metadata/providers/activity_pub.ex @@ -0,0 +1,22 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2024 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Metadata.Providers.ActivityPub do +  alias Pleroma.Web.Metadata.Providers.Provider + +  @behaviour Provider + +  @impl Provider +  def build_tags(%{object: %{data: %{"id" => object_id}}}) do +    [{:link, [rel: "alternate", type: "application/activity+json", href: object_id], []}] +  end + +  @impl Provider +  def build_tags(%{user: user}) do +    [{:link, [rel: "alternate", type: "application/activity+json", href: user.ap_id], []}] +  end + +  @impl Provider +  def build_tags(_), do: [] +end diff --git a/lib/pleroma/web/metadata/providers/feed.ex b/lib/pleroma/web/metadata/providers/feed.ex index e97d6a54f..5a0f2338e 100644 --- a/lib/pleroma/web/metadata/providers/feed.ex +++ b/lib/pleroma/web/metadata/providers/feed.ex @@ -10,7 +10,7 @@ defmodule Pleroma.Web.Metadata.Providers.Feed do    @behaviour Provider    @impl Provider -  def build_tags(%{user: user}) do +  def build_tags(%{user: %{local: true} = user}) do      [        {:link,         [ @@ -20,4 +20,7 @@ defmodule Pleroma.Web.Metadata.Providers.Feed do         ], []}      ]    end + +  @impl Provider +  def build_tags(_), do: []  end diff --git a/lib/pleroma/web/metadata/providers/open_graph.ex b/lib/pleroma/web/metadata/providers/open_graph.ex index 97d3865ed..fa5fbe553 100644 --- a/lib/pleroma/web/metadata/providers/open_graph.ex +++ b/lib/pleroma/web/metadata/providers/open_graph.ex @@ -67,6 +67,9 @@ defmodule Pleroma.Web.Metadata.Providers.OpenGraph do      end    end +  @impl Provider +  def build_tags(_), do: [] +    defp build_attachments(%{data: %{"attachment" => attachments}}) do      Enum.reduce(attachments, [], fn attachment, acc ->        rendered_tags = diff --git a/lib/pleroma/web/metadata/providers/rel_me.ex b/lib/pleroma/web/metadata/providers/rel_me.ex index eabd8cb00..39aa71f06 100644 --- a/lib/pleroma/web/metadata/providers/rel_me.ex +++ b/lib/pleroma/web/metadata/providers/rel_me.ex @@ -20,6 +20,9 @@ defmodule Pleroma.Web.Metadata.Providers.RelMe do      end)    end +  @impl Provider +  def build_tags(_), do: [] +    defp append_fields_tag(bio, fields) do      fields      |> Enum.reduce(bio, fn %{"value" => v}, res -> res <> v end) diff --git a/lib/pleroma/web/metadata/providers/twitter_card.ex b/lib/pleroma/web/metadata/providers/twitter_card.ex index 426022c65..7f50877c3 100644 --- a/lib/pleroma/web/metadata/providers/twitter_card.ex +++ b/lib/pleroma/web/metadata/providers/twitter_card.ex @@ -44,6 +44,9 @@ defmodule Pleroma.Web.Metadata.Providers.TwitterCard do      end    end +  @impl Provider +  def build_tags(_), do: [] +    defp title_tag(user) do      {:meta, [name: "twitter:title", content: Utils.user_name_string(user)], []}    end diff --git a/lib/pleroma/web/o_auth/app.ex b/lib/pleroma/web/o_auth/app.ex index d1bf6dd18..7661c2566 100644 --- a/lib/pleroma/web/o_auth/app.ex +++ b/lib/pleroma/web/o_auth/app.ex @@ -8,6 +8,7 @@ defmodule Pleroma.Web.OAuth.App do    import Ecto.Query    alias Pleroma.Repo    alias Pleroma.User +  alias Pleroma.Web.OAuth.Token    @type t :: %__MODULE__{} @@ -155,4 +156,29 @@ defmodule Pleroma.Web.OAuth.App do          Map.put(acc, key, error)      end)    end + +  @spec maybe_update_owner(Token.t()) :: :ok +  def maybe_update_owner(%Token{app_id: app_id, user_id: user_id}) when not is_nil(user_id) do +    __MODULE__.update(app_id, %{user_id: user_id}) + +    :ok +  end + +  def maybe_update_owner(_), do: :ok + +  @spec remove_orphans(pos_integer()) :: :ok +  def remove_orphans(limit \\ 100) do +    fifteen_mins_ago = DateTime.add(DateTime.utc_now(), -900, :second) + +    Repo.transaction(fn -> +      from(a in __MODULE__, +        where: is_nil(a.user_id) and a.inserted_at < ^fifteen_mins_ago, +        limit: ^limit +      ) +      |> Repo.all() +      |> Enum.each(&Repo.delete(&1)) +    end) + +    :ok +  end  end diff --git a/lib/pleroma/web/o_auth/o_auth_controller.ex b/lib/pleroma/web/o_auth/o_auth_controller.ex index 47b03215f..0b3de5481 100644 --- a/lib/pleroma/web/o_auth/o_auth_controller.ex +++ b/lib/pleroma/web/o_auth/o_auth_controller.ex @@ -318,6 +318,8 @@ defmodule Pleroma.Web.OAuth.OAuthController do    def token_exchange(%Plug.Conn{} = conn, params), do: bad_request(conn, params)    def after_token_exchange(%Plug.Conn{} = conn, %{token: token} = view_params) do +    App.maybe_update_owner(token) +      conn      |> AuthHelper.put_session_token(token.token)      |> json(OAuthView.render("token.json", view_params)) diff --git a/lib/pleroma/web/pleroma_api/controllers/user_import_controller.ex b/lib/pleroma/web/pleroma_api/controllers/user_import_controller.ex index 96466f192..d65c30dab 100644 --- a/lib/pleroma/web/pleroma_api/controllers/user_import_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/user_import_controller.ex @@ -38,8 +38,8 @@ defmodule Pleroma.Web.PleromaAPI.UserImportController do        |> Enum.map(&(&1 |> String.trim() |> String.trim_leading("@")))        |> Enum.reject(&(&1 == "")) -    User.Import.follow_import(follower, identifiers) -    json(conn, "job started") +    User.Import.follows_import(follower, identifiers) +    json(conn, "jobs started")    end    def blocks( @@ -55,7 +55,7 @@ defmodule Pleroma.Web.PleromaAPI.UserImportController do    defp do_block(%{assigns: %{user: blocker}} = conn, list) do      User.Import.blocks_import(blocker, prepare_user_identifiers(list)) -    json(conn, "job started") +    json(conn, "jobs started")    end    def mutes( @@ -71,7 +71,7 @@ defmodule Pleroma.Web.PleromaAPI.UserImportController do    defp do_mute(%{assigns: %{user: user}} = conn, list) do      User.Import.mutes_import(user, prepare_user_identifiers(list)) -    json(conn, "job started") +    json(conn, "jobs started")    end    defp prepare_user_identifiers(list) do diff --git a/lib/pleroma/web/plugs/authentication_plug.ex b/lib/pleroma/web/plugs/authentication_plug.ex index f912a1542..af7d7f45a 100644 --- a/lib/pleroma/web/plugs/authentication_plug.ex +++ b/lib/pleroma/web/plugs/authentication_plug.ex @@ -47,6 +47,11 @@ defmodule Pleroma.Web.Plugs.AuthenticationPlug do      Pleroma.Password.Pbkdf2.verify_pass(password, password_hash)    end +  def checkpw(password, "$argon2" <> _ = password_hash) do +    # Handle argon2 passwords for Akkoma migration +    Argon2.verify_pass(password, password_hash) +  end +    def checkpw(_password, _password_hash) do      Logger.error("Password hash not recognized")      false @@ -56,6 +61,10 @@ defmodule Pleroma.Web.Plugs.AuthenticationPlug do      do_update_password(user, password)    end +  def maybe_update_password(%User{password_hash: "$argon2" <> _} = user, password) do +    do_update_password(user, password) +  end +    def maybe_update_password(user, _), do: {:ok, user}    defp do_update_password(user, password) do diff --git a/lib/pleroma/web/plugs/inbox_guard_plug.ex b/lib/pleroma/web/plugs/inbox_guard_plug.ex new file mode 100644 index 000000000..0064cce76 --- /dev/null +++ b/lib/pleroma/web/plugs/inbox_guard_plug.ex @@ -0,0 +1,89 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Plugs.InboxGuardPlug do +  import Plug.Conn +  import Pleroma.Constants, only: [activity_types: 0, allowed_activity_types_from_strangers: 0] + +  alias Pleroma.Config +  alias Pleroma.User + +  def init(options) do +    options +  end + +  def call(%{assigns: %{valid_signature: true}} = conn, _opts) do +    with {_, true} <- {:federating, Config.get!([:instance, :federating])} do +      conn +      |> filter_activity_types() +    else +      {:federating, false} -> +        conn +        |> json(403, "Not federating") +        |> halt() +    end +  end + +  def call(conn, _opts) do +    with {_, true} <- {:federating, Config.get!([:instance, :federating])}, +         conn = filter_activity_types(conn), +         {:known, true} <- {:known, known_actor?(conn)} do +      conn +    else +      {:federating, false} -> +        conn +        |> json(403, "Not federating") +        |> halt() + +      {:known, false} -> +        conn +        |> filter_from_strangers() +    end +  end + +  # Early rejection of unrecognized types +  defp filter_activity_types(%{body_params: %{"type" => type}} = conn) do +    with true <- type in activity_types() do +      conn +    else +      _ -> +        conn +        |> json(400, "Invalid activity type") +        |> halt() +    end +  end + +  # If signature failed but we know this actor we should +  # accept it as we may only need to refetch their public key +  # during processing +  defp known_actor?(%{body_params: data}) do +    case Pleroma.Object.Containment.get_actor(data) |> User.get_cached_by_ap_id() do +      %User{} -> true +      _ -> false +    end +  end + +  # Only permit a subset of activity types from strangers +  # or else it will add actors you've never interacted with +  # to the database +  defp filter_from_strangers(%{body_params: %{"type" => type}} = conn) do +    with true <- type in allowed_activity_types_from_strangers() do +      conn +    else +      _ -> +        conn +        |> json(400, "Invalid activity type for an unknown actor") +        |> halt() +    end +  end + +  defp json(conn, status, resp) do +    json_resp = Jason.encode!(resp) + +    conn +    |> put_resp_content_type("application/json") +    |> resp(status, json_resp) +    |> halt() +  end +end diff --git a/lib/pleroma/web/push.ex b/lib/pleroma/web/push.ex index 6d777142e..77f77f88e 100644 --- a/lib/pleroma/web/push.ex +++ b/lib/pleroma/web/push.ex @@ -20,7 +20,7 @@ defmodule Pleroma.Web.Push do    end    def vapid_config do -    Application.get_env(:web_push_encryption, :vapid_details, nil) +    Application.get_env(:web_push_encryption, :vapid_details, [])    end    def enabled, do: match?([subject: _, public_key: _, private_key: _], vapid_config()) diff --git a/lib/pleroma/web/rich_media/helpers.ex b/lib/pleroma/web/rich_media/helpers.ex index e2889b351..d4be97957 100644 --- a/lib/pleroma/web/rich_media/helpers.ex +++ b/lib/pleroma/web/rich_media/helpers.ex @@ -11,16 +11,39 @@ defmodule Pleroma.Web.RichMedia.Helpers do    @spec rich_media_get(String.t()) :: {:ok, String.t()} | get_errors()    def rich_media_get(url) do -    headers = [{"user-agent", Pleroma.Application.user_agent() <> "; Bot"}] +    case Pleroma.HTTP.AdapterHelper.can_stream?() do +      true -> stream(url) +      false -> head_first(url) +    end +    |> handle_result(url) +  end + +  defp stream(url) do +    with {_, {:ok, %Tesla.Env{status: 200, body: stream_body, headers: headers}}} <- +           {:get, Pleroma.HTTP.get(url, req_headers(), http_options())}, +         {_, :ok} <- {:content_type, check_content_type(headers)}, +         {_, :ok} <- {:content_length, check_content_length(headers)}, +         {:read_stream, {:ok, body}} <- {:read_stream, read_stream(stream_body)} do +      {:ok, body} +    end +  end +  defp head_first(url) do      with {_, {:ok, %Tesla.Env{status: 200, headers: headers}}} <- -           {:head, Pleroma.HTTP.head(url, headers, http_options())}, +           {:head, Pleroma.HTTP.head(url, req_headers(), http_options())},           {_, :ok} <- {:content_type, check_content_type(headers)},           {_, :ok} <- {:content_length, check_content_length(headers)},           {_, {:ok, %Tesla.Env{status: 200, body: body}}} <- -           {:get, Pleroma.HTTP.get(url, headers, http_options())} do +           {:get, Pleroma.HTTP.get(url, req_headers(), http_options())} do        {:ok, body} -    else +    end +  end + +  defp handle_result(result, url) do +    case result do +      {:ok, body} -> +        {:ok, body} +        {:head, _} ->          Logger.debug("Rich media error for #{url}: HTTP HEAD failed")          {:error, :head} @@ -29,8 +52,12 @@ defmodule Pleroma.Web.RichMedia.Helpers do          Logger.debug("Rich media error for #{url}: content-type is #{type}")          {:error, :content_type} -      {:content_length, {_, length}} -> -        Logger.debug("Rich media error for #{url}: content-length is #{length}") +      {:content_length, :error} -> +        Logger.debug("Rich media error for #{url}: content-length exceeded") +        {:error, :body_too_large} + +      {:read_stream, :error} -> +        Logger.debug("Rich media error for #{url}: content-length exceeded")          {:error, :body_too_large}        {:get, _} -> @@ -59,7 +86,7 @@ defmodule Pleroma.Web.RichMedia.Helpers do        {_, maybe_content_length} ->          case Integer.parse(maybe_content_length) do            {content_length, ""} when content_length <= max_body -> :ok -          {_, ""} -> {:error, maybe_content_length} +          {_, ""} -> :error            _ -> :ok          end @@ -68,13 +95,37 @@ defmodule Pleroma.Web.RichMedia.Helpers do      end    end -  defp http_options do -    timeout = Config.get!([:rich_media, :timeout]) +  defp read_stream(stream) do +    max_body = Keyword.get(http_options(), :max_body) + +    try do +      result = +        Stream.transform(stream, 0, fn chunk, total_bytes -> +          new_total = total_bytes + byte_size(chunk) + +          if new_total > max_body do +            raise("Exceeds max body limit of #{max_body}") +          else +            {[chunk], new_total} +          end +        end) +        |> Enum.into(<<>>) +      {:ok, result} +    rescue +      _ -> :error +    end +  end + +  defp http_options do      [        pool: :rich_media,        max_body: Config.get([:rich_media, :max_body], 5_000_000), -      tesla_middleware: [{Tesla.Middleware.Timeout, timeout: timeout}] +      stream: true      ]    end + +  defp req_headers do +    [{"user-agent", Pleroma.Application.user_agent() <> "; Bot"}] +  end  end diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 6492e3861..0423ca9e2 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -189,7 +189,7 @@ defmodule Pleroma.Web.Router do    end    pipeline :well_known do -    plug(:accepts, ["json", "jrd", "jrd+json", "xml", "xrd+xml"]) +    plug(:accepts, ["activity+json", "json", "jrd", "jrd+json", "xml", "xrd+xml"])    end    pipeline :config do @@ -217,6 +217,10 @@ defmodule Pleroma.Web.Router do      plug(Pleroma.Web.Plugs.MappedSignatureToIdentityPlug)    end +  pipeline :inbox_guard do +    plug(Pleroma.Web.Plugs.InboxGuardPlug) +  end +    pipeline :static_fe do      plug(Pleroma.Web.Plugs.StaticFEPlug)    end @@ -920,7 +924,7 @@ defmodule Pleroma.Web.Router do    end    scope "/", Pleroma.Web.ActivityPub do -    pipe_through(:activitypub) +    pipe_through([:activitypub, :inbox_guard])      post("/inbox", ActivityPubController, :inbox)      post("/users/:nickname/inbox", ActivityPubController, :inbox)    end diff --git a/lib/pleroma/web/twitter_api/controllers/util_controller.ex b/lib/pleroma/web/twitter_api/controllers/util_controller.ex index 6805233df..aeafa195d 100644 --- a/lib/pleroma/web/twitter_api/controllers/util_controller.ex +++ b/lib/pleroma/web/twitter_api/controllers/util_controller.ex @@ -13,6 +13,7 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do    alias Pleroma.Healthcheck    alias Pleroma.User    alias Pleroma.Web.ActivityPub.ActivityPub +  alias Pleroma.Web.Auth.WrapperAuthenticator, as: Authenticator    alias Pleroma.Web.CommonAPI    alias Pleroma.Web.Plugs.OAuthScopesPlug    alias Pleroma.Web.WebFinger @@ -195,19 +196,21 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do          %{assigns: %{user: user}, private: %{open_api_spex: %{body_params: body_params}}} = conn,          _        ) do -    case CommonAPI.Utils.confirm_current_password(user, body_params.password) do -      {:ok, user} -> -        with {:ok, _user} <- -               User.reset_password(user, %{ -                 password: body_params.new_password, -                 password_confirmation: body_params.new_password_confirmation -               }) do -          json(conn, %{status: "success"}) -        else -          {:error, changeset} -> -            {_, {error, _}} = Enum.at(changeset.errors, 0) -            json(conn, %{error: "New password #{error}."}) -        end +    with {:ok, %User{}} <- +           Authenticator.change_password( +             user, +             body_params.password, +             body_params.new_password, +             body_params.new_password_confirmation +           ) do +      json(conn, %{status: "success"}) +    else +      {:error, %Ecto.Changeset{} = changeset} -> +        {_, {error, _}} = Enum.at(changeset.errors, 0) +        json(conn, %{error: "New password #{error}."}) + +      {:error, :password_confirmation} -> +        json(conn, %{error: "New password does not match confirmation."})        {:error, msg} ->          json(conn, %{error: msg}) diff --git a/lib/pleroma/web/twitter_api/views/token_view.ex b/lib/pleroma/web/twitter_api/views/token_view.ex index 2e492c13f..36776ce3b 100644 --- a/lib/pleroma/web/twitter_api/views/token_view.ex +++ b/lib/pleroma/web/twitter_api/views/token_view.ex @@ -15,7 +15,8 @@ defmodule Pleroma.Web.TwitterAPI.TokenView do      %{        id: token_entry.id,        valid_until: token_entry.valid_until, -      app_name: token_entry.app.client_name +      app_name: token_entry.app.client_name, +      scopes: token_entry.scopes      }    end  end diff --git a/lib/pleroma/workers/background_worker.ex b/lib/pleroma/workers/background_worker.ex index 60da2d5ca..4737c6ea2 100644 --- a/lib/pleroma/workers/background_worker.ex +++ b/lib/pleroma/workers/background_worker.ex @@ -19,10 +19,10 @@ defmodule Pleroma.Workers.BackgroundWorker do      User.perform(:force_password_reset, user)    end -  def perform(%Job{args: %{"op" => op, "user_id" => user_id, "identifiers" => identifiers}}) -      when op in ["blocks_import", "follow_import", "mutes_import"] do +  def perform(%Job{args: %{"op" => op, "user_id" => user_id, "actor" => actor}}) +      when op in ["block_import", "follow_import", "mute_import"] do      user = User.get_cached_by_id(user_id) -    {:ok, User.Import.perform(String.to_existing_atom(op), user, identifiers)} +    User.Import.perform(String.to_existing_atom(op), user, actor)    end    def perform(%Job{ diff --git a/lib/pleroma/workers/cron/app_cleanup_worker.ex b/lib/pleroma/workers/cron/app_cleanup_worker.ex new file mode 100644 index 000000000..ee71cd7b6 --- /dev/null +++ b/lib/pleroma/workers/cron/app_cleanup_worker.ex @@ -0,0 +1,21 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Workers.Cron.AppCleanupWorker do +  @moduledoc """ +  Cleans up registered apps that were never associated with a user. +  """ + +  use Oban.Worker, queue: "background" + +  alias Pleroma.Web.OAuth.App + +  @impl true +  def perform(_job) do +    App.remove_orphans() +  end + +  @impl true +  def timeout(_job), do: :timer.seconds(30) +end diff --git a/lib/pleroma/workers/poll_worker.ex b/lib/pleroma/workers/poll_worker.ex index d263aa1b9..a9afe9d63 100644 --- a/lib/pleroma/workers/poll_worker.ex +++ b/lib/pleroma/workers/poll_worker.ex @@ -11,27 +11,46 @@ defmodule Pleroma.Workers.PollWorker do    alias Pleroma.Activity    alias Pleroma.Notification    alias Pleroma.Object +  alias Pleroma.Object.Fetcher + +  @stream_out_impl Pleroma.Config.get( +                     [__MODULE__, :stream_out], +                     Pleroma.Web.ActivityPub.ActivityPub +                   )    @impl true    def perform(%Job{args: %{"op" => "poll_end", "activity_id" => activity_id}}) do -    with %Activity{} = activity <- find_poll_activity(activity_id), +    with {_, %Activity{} = activity} <- {:activity, Activity.get_by_id(activity_id)},           {:ok, notifications} <- Notification.create_poll_notifications(activity) do +      unless activity.local do +        # Schedule a final refresh +        __MODULE__.new(%{"op" => "refresh", "activity_id" => activity_id}) +        |> Oban.insert() +      end +        Notification.stream(notifications)      else -      {:error, :poll_activity_not_found} = e -> {:cancel, e} +      {:activity, nil} -> {:cancel, :poll_activity_not_found}        e -> {:error, e}      end    end -  @impl true -  def timeout(_job), do: :timer.seconds(5) +  def perform(%Job{args: %{"op" => "refresh", "activity_id" => activity_id}}) do +    with {_, %Activity{object: object}} <- +           {:activity, Activity.get_by_id_with_object(activity_id)}, +         {_, {:ok, _object}} <- {:refetch, Fetcher.refetch_object(object)} do +      stream_update(activity_id) -  defp find_poll_activity(activity_id) do -    with nil <- Activity.get_by_id(activity_id) do -      {:error, :poll_activity_not_found} +      :ok +    else +      {:activity, nil} -> {:cancel, :poll_activity_not_found} +      {:refetch, _} = e -> {:cancel, e}      end    end +  @impl true +  def timeout(_job), do: :timer.seconds(5) +    def schedule_poll_end(%Activity{data: %{"type" => "Create"}, id: activity_id} = activity) do      with %Object{data: %{"type" => "Question", "closed" => closed}} when is_binary(closed) <-             Object.normalize(activity), @@ -49,4 +68,10 @@ defmodule Pleroma.Workers.PollWorker do    end    def schedule_poll_end(activity), do: {:error, activity} + +  defp stream_update(activity_id) do +    Activity.get_by_id(activity_id) +    |> Activity.normalize() +    |> @stream_out_impl.stream_out() +  end  end diff --git a/lib/pleroma/workers/receiver_worker.ex b/lib/pleroma/workers/receiver_worker.ex index d4db97b63..11b672bef 100644 --- a/lib/pleroma/workers/receiver_worker.ex +++ b/lib/pleroma/workers/receiver_worker.ex @@ -7,7 +7,7 @@ defmodule Pleroma.Workers.ReceiverWorker do    alias Pleroma.User    alias Pleroma.Web.Federator -  use Oban.Worker, queue: :federator_incoming, max_attempts: 5 +  use Oban.Worker, queue: :federator_incoming, max_attempts: 5, unique: [period: :infinity]    @impl true @@ -33,7 +33,7 @@ defmodule Pleroma.Workers.ReceiverWorker do        query_string: query_string      } -    with {:ok, %User{} = _actor} <- User.get_or_fetch_by_ap_id(conn_data.params["actor"]), +    with {:ok, %User{}} <- User.get_or_fetch_by_ap_id(conn_data.params["actor"]),           {:ok, _public_key} <- Signature.refetch_public_key(conn_data),           {:signature, true} <- {:signature, Signature.validate_signature(conn_data)},           {:ok, res} <- Federator.perform(:incoming_ap_doc, params) do @@ -56,17 +56,29 @@ defmodule Pleroma.Workers.ReceiverWorker do    def timeout(_job), do: :timer.seconds(5) +  defp process_errors({:error, {:error, _} = error}), do: process_errors(error) +    defp process_errors(errors) do      case errors do -      {:error, :origin_containment_failed} -> {:cancel, :origin_containment_failed} +      # User fetch failures +      {:error, :not_found} = reason -> {:cancel, reason} +      {:error, :forbidden} = reason -> {:cancel, reason} +      # Inactive user +      {:error, {:user_active, false} = reason} -> {:cancel, reason} +      # Validator will error and return a changeset error +      # e.g., duplicate activities or if the object was deleted +      {:error, {:validate, {:error, _changeset} = reason}} -> {:cancel, reason} +      # Duplicate detection during Normalization        {:error, :already_present} -> {:cancel, :already_present} -      {:error, {:validate_object, _} = reason} -> {:cancel, reason} -      {:error, {:error, {:validate, {:error, _changeset} = reason}}} -> {:cancel, reason} +      # MRFs will return a reject        {:error, {:reject, _} = reason} -> {:cancel, reason} +      # HTTP Sigs        {:signature, false} -> {:cancel, :invalid_signature} -      {:error, "Object has been deleted"} = reason -> {:cancel, reason} +      # Origin / URL validation failed somewhere possibly due to spoofing +      {:error, :origin_containment_failed} -> {:cancel, :origin_containment_failed} +      # Unclear if this can be reached        {:error, {:side_effects, {:error, :no_object_actor}} = reason} -> {:cancel, reason} -      {:error, :not_found} = reason -> {:cancel, reason} +      # Catchall        {:error, _} = e -> e        e -> {:error, e}      end diff --git a/lib/pleroma/workers/remote_fetcher_worker.ex b/lib/pleroma/workers/remote_fetcher_worker.ex index 9d3f1ec53..aa09362f5 100644 --- a/lib/pleroma/workers/remote_fetcher_worker.ex +++ b/lib/pleroma/workers/remote_fetcher_worker.ex @@ -5,7 +5,7 @@  defmodule Pleroma.Workers.RemoteFetcherWorker do    alias Pleroma.Object.Fetcher -  use Oban.Worker, queue: :background +  use Oban.Worker, queue: :background, unique: [period: :infinity]    @impl true    def perform(%Job{args: %{"op" => "fetch_remote", "id" => id} = args}) do diff --git a/lib/pleroma/workers/rich_media_worker.ex b/lib/pleroma/workers/rich_media_worker.ex index d5ba7b63e..e351ecd6e 100644 --- a/lib/pleroma/workers/rich_media_worker.ex +++ b/lib/pleroma/workers/rich_media_worker.ex @@ -7,7 +7,7 @@ defmodule Pleroma.Workers.RichMediaWorker do    alias Pleroma.Web.RichMedia.Backfill    alias Pleroma.Web.RichMedia.Card -  use Oban.Worker, queue: :background, max_attempts: 3, unique: [period: 300] +  use Oban.Worker, queue: :background, max_attempts: 3, unique: [period: :infinity]    @impl true    def perform(%Job{args: %{"op" => "expire", "url" => url} = _args}) do diff --git a/lib/pleroma/workers/user_refresh_worker.ex b/lib/pleroma/workers/user_refresh_worker.ex index 222a4a8f7..ee276774b 100644 --- a/lib/pleroma/workers/user_refresh_worker.ex +++ b/lib/pleroma/workers/user_refresh_worker.ex @@ -3,7 +3,7 @@  # SPDX-License-Identifier: AGPL-3.0-only  defmodule Pleroma.Workers.UserRefreshWorker do -  use Oban.Worker, queue: :background, max_attempts: 1, unique: [period: 300] +  use Oban.Worker, queue: :background, max_attempts: 1, unique: [period: :infinity]    alias Pleroma.User diff --git a/lib/pleroma/workers/web_pusher_worker.ex b/lib/pleroma/workers/web_pusher_worker.ex index f4232d02a..879b26cc3 100644 --- a/lib/pleroma/workers/web_pusher_worker.ex +++ b/lib/pleroma/workers/web_pusher_worker.ex @@ -7,7 +7,7 @@ defmodule Pleroma.Workers.WebPusherWorker do    alias Pleroma.Repo    alias Pleroma.Web.Push.Impl -  use Oban.Worker, queue: :web_push +  use Oban.Worker, queue: :web_push, unique: [period: :infinity]    @impl true    def perform(%Job{args: %{"op" => "web_push", "notification_id" => notification_id}}) do  | 
