diff options
| author | Alex Gleason <alex@alexgleason.me> | 2020-07-16 14:51:36 -0500 | 
|---|---|---|
| committer | Alex Gleason <alex@alexgleason.me> | 2020-07-16 14:51:36 -0500 | 
| commit | 38425ebdbf157377ccb0402f78dc3d02f81c55f5 (patch) | |
| tree | 740f32837d28e918f097c948d89827d99434d5b2 /lib | |
| parent | 8daacc911498d827fd68ea3d34eb1be9ae4a1ffe (diff) | |
| parent | 3a2b2cb6f2d6ce39ae0d246649189c021c535992 (diff) | |
| download | pleroma-38425ebdbf157377ccb0402f78dc3d02f81c55f5.tar.gz pleroma-38425ebdbf157377ccb0402f78dc3d02f81c55f5.zip  | |
Merge remote-tracking branch 'upstream/develop' into linkify
Diffstat (limited to 'lib')
135 files changed, 2371 insertions, 1725 deletions
diff --git a/lib/mix/pleroma.ex b/lib/mix/pleroma.ex index 3ad6edbfb..9f0bf6ecb 100644 --- a/lib/mix/pleroma.ex +++ b/lib/mix/pleroma.ex @@ -3,15 +3,48 @@  # SPDX-License-Identifier: AGPL-3.0-only  defmodule Mix.Pleroma do +  @apps [ +    :restarter, +    :ecto, +    :ecto_sql, +    :postgrex, +    :db_connection, +    :cachex, +    :flake_id, +    :swoosh, +    :timex +  ] +  @cachex_children ["object", "user"]    @doc "Common functions to be reused in mix tasks"    def start_pleroma do +    Pleroma.Config.Holder.save_default()      Application.put_env(:phoenix, :serve_endpoints, false, persistent: true)      if Pleroma.Config.get(:env) != :test do        Application.put_env(:logger, :console, level: :debug)      end -    {:ok, _} = Application.ensure_all_started(:pleroma) +    apps = +      if Application.get_env(:tesla, :adapter) == Tesla.Adapter.Gun do +        [:gun | @apps] +      else +        [:hackney | @apps] +      end + +    Enum.each(apps, &Application.ensure_all_started/1) + +    children = [ +      Pleroma.Repo, +      {Pleroma.Config.TransferTask, false}, +      Pleroma.Web.Endpoint +    ] + +    cachex_children = Enum.map(@cachex_children, &Pleroma.Application.build_cachex(&1, [])) + +    Supervisor.start_link(children ++ cachex_children, +      strategy: :one_for_one, +      name: Pleroma.Supervisor +    )      if Pleroma.Config.get(:env) not in [:test, :benchmark] do        pleroma_rebooted?() diff --git a/lib/mix/tasks/pleroma/config.ex b/lib/mix/tasks/pleroma/config.ex index d5129d410..904c5a74b 100644 --- a/lib/mix/tasks/pleroma/config.ex +++ b/lib/mix/tasks/pleroma/config.ex @@ -83,7 +83,7 @@ defmodule Mix.Tasks.Pleroma.Config do    defp migrate_from_db(opts) do      if Pleroma.Config.get([:configurable_from_database]) do -      env = opts[:env] || "prod" +      env = opts[:env] || Pleroma.Config.get(:env)        config_path =          if Pleroma.Config.get(:release) do @@ -105,6 +105,10 @@ defmodule Mix.Tasks.Pleroma.Config do        :ok = File.close(file)        System.cmd("mix", ["format", config_path]) + +      shell_info( +        "Database configuration settings have been exported to config/#{env}.exported_from_db.secret.exs" +      )      else        migration_error()      end @@ -112,7 +116,7 @@ defmodule Mix.Tasks.Pleroma.Config do    defp migration_error do      shell_error( -      "Migration is not allowed in config. You can change this behavior by setting `configurable_from_database` to true." +      "Migration is not allowed in config. You can change this behavior by setting `config :pleroma, configurable_from_database: true`"      )    end diff --git a/lib/mix/tasks/pleroma/instance.ex b/lib/mix/tasks/pleroma/instance.ex index 86409738a..91440b453 100644 --- a/lib/mix/tasks/pleroma/instance.ex +++ b/lib/mix/tasks/pleroma/instance.ex @@ -145,7 +145,7 @@ defmodule Mix.Tasks.Pleroma.Instance do            options,            :uploads_dir,            "What directory should media uploads go in (when using the local uploader)?", -          Pleroma.Config.get([Pleroma.Uploaders.Local, :uploads]) +          Config.get([Pleroma.Uploaders.Local, :uploads])          )          |> Path.expand() @@ -154,7 +154,7 @@ defmodule Mix.Tasks.Pleroma.Instance do            options,            :static_dir,            "What directory should custom public files be read from (custom emojis, frontend bundle overrides, robots.txt, etc.)?", -          Pleroma.Config.get([:instance, :static_dir]) +          Config.get([:instance, :static_dir])          )          |> Path.expand() diff --git a/lib/mix/tasks/pleroma/notification_settings.ex b/lib/mix/tasks/pleroma/notification_settings.ex index 7d65f0587..00f5ba7bf 100644 --- a/lib/mix/tasks/pleroma/notification_settings.ex +++ b/lib/mix/tasks/pleroma/notification_settings.ex @@ -3,8 +3,8 @@ defmodule Mix.Tasks.Pleroma.NotificationSettings do    @moduledoc """    Example: -  > mix pleroma.notification_settings --privacy-option=false --nickname-users="parallel588"  # set false only for parallel588 user -  > mix pleroma.notification_settings --privacy-option=true # set true for all users +  > mix pleroma.notification_settings --hide-notification-contents=false --nickname-users="parallel588"  # set false only for parallel588 user +  > mix pleroma.notification_settings --hide-notification-contents=true # set true for all users    """ @@ -19,16 +19,16 @@ defmodule Mix.Tasks.Pleroma.NotificationSettings do        OptionParser.parse(          args,          strict: [ -          privacy_option: :boolean, +          hide_notification_contents: :boolean,            email_users: :string,            nickname_users: :string          ]        ) -    privacy_option = Keyword.get(options, :privacy_option) +    hide_notification_contents = Keyword.get(options, :hide_notification_contents) -    if not is_nil(privacy_option) do -      privacy_option +    if not is_nil(hide_notification_contents) do +      hide_notification_contents        |> build_query(options)        |> Pleroma.Repo.update_all([])      end @@ -36,15 +36,15 @@ defmodule Mix.Tasks.Pleroma.NotificationSettings do      shell_info("Done")    end -  defp build_query(privacy_option, options) do +  defp build_query(hide_notification_contents, options) do      query =        from(u in Pleroma.User,          update: [            set: [              notification_settings:                fragment( -                "jsonb_set(notification_settings, '{privacy_option}', ?)", -                ^privacy_option +                "jsonb_set(notification_settings, '{hide_notification_contents}', ?)", +                ^hide_notification_contents                )            ]          ] diff --git a/lib/mix/tasks/pleroma/user.ex b/lib/mix/tasks/pleroma/user.ex index bca7e87bf..01824aa18 100644 --- a/lib/mix/tasks/pleroma/user.ex +++ b/lib/mix/tasks/pleroma/user.ex @@ -232,7 +232,7 @@ defmodule Mix.Tasks.Pleroma.User do      with %User{} = user <- User.get_cached_by_nickname(nickname) do        user = user |> User.tag(tags) -      shell_info("Tags of #{user.nickname}: #{inspect(tags)}") +      shell_info("Tags of #{user.nickname}: #{inspect(user.tags)}")      else        _ ->          shell_error("Could not change user tags for #{nickname}") @@ -245,7 +245,7 @@ defmodule Mix.Tasks.Pleroma.User do      with %User{} = user <- User.get_cached_by_nickname(nickname) do        user = user |> User.untag(tags) -      shell_info("Tags of #{user.nickname}: #{inspect(tags)}") +      shell_info("Tags of #{user.nickname}: #{inspect(user.tags)}")      else        _ ->          shell_error("Could not change user tags for #{nickname}") diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex index 9615af122..0ffb55358 100644 --- a/lib/pleroma/application.ex +++ b/lib/pleroma/application.ex @@ -35,13 +35,19 @@ defmodule Pleroma.Application do    # See http://elixir-lang.org/docs/stable/elixir/Application.html    # for more information on OTP Applications    def start(_type, _args) do -    Pleroma.Config.Holder.save_default() +    # Scrubbers are compiled at runtime and therefore will cause a conflict +    # every time the application is restarted, so we disable module +    # conflicts at runtime +    Code.compiler_options(ignore_module_conflict: true) +    Pleroma.Telemetry.Logger.attach() +    Config.Holder.save_default()      Pleroma.HTML.compile_scrubbers()      Config.DeprecationWarnings.warn()      Pleroma.Plugs.HTTPSecurityPlug.warn_if_disabled()      Pleroma.ApplicationRequirements.verify!()      setup_instrumenters()      load_custom_modules() +    Pleroma.Docs.JSON.compile()      adapter = Application.get_env(:tesla, :adapter) @@ -162,7 +168,8 @@ defmodule Pleroma.Application do    defp seconds_valid_interval,      do: :timer.seconds(Config.get!([Pleroma.Captcha, :seconds_valid])) -  defp build_cachex(type, opts), +  @spec build_cachex(String.t(), keyword()) :: map() +  def build_cachex(type, opts),      do: %{        id: String.to_atom("cachex_" <> type),        start: {Cachex, :start_link, [String.to_atom(type <> "_cache"), opts]}, @@ -217,9 +224,7 @@ defmodule Pleroma.Application do    # start hackney and gun pools in tests    defp http_children(_, :test) do -    hackney_options = Config.get([:hackney_pools, :federation]) -    hackney_pool = :hackney_pool.child_spec(:federation, hackney_options) -    [hackney_pool, Pleroma.Pool.Supervisor] +    http_children(Tesla.Adapter.Hackney, nil) ++ http_children(Tesla.Adapter.Gun, nil)    end    defp http_children(Tesla.Adapter.Hackney, _) do @@ -238,7 +243,10 @@ defmodule Pleroma.Application do      end    end -  defp http_children(Tesla.Adapter.Gun, _), do: [Pleroma.Pool.Supervisor] +  defp http_children(Tesla.Adapter.Gun, _) do +    Pleroma.Gun.ConnectionPool.children() ++ +      [{Task, &Pleroma.HTTP.AdapterHelper.Gun.limiter_setup/0}] +  end    defp http_children(_, _), do: []  end diff --git a/lib/pleroma/config/deprecation_warnings.ex b/lib/pleroma/config/deprecation_warnings.ex index 0a6c724fb..026871c4f 100644 --- a/lib/pleroma/config/deprecation_warnings.ex +++ b/lib/pleroma/config/deprecation_warnings.ex @@ -54,6 +54,7 @@ defmodule Pleroma.Config.DeprecationWarnings do      check_hellthread_threshold()      mrf_user_allowlist()      check_old_mrf_config() +    check_media_proxy_whitelist_config()    end    def check_old_mrf_config do @@ -65,7 +66,7 @@ defmodule Pleroma.Config.DeprecationWarnings do      move_namespace_and_warn(@mrf_config_map, warning_preface)    end -  @spec move_namespace_and_warn([config_map()], String.t()) :: :ok +  @spec move_namespace_and_warn([config_map()], String.t()) :: :ok | nil    def move_namespace_and_warn(config_map, warning_preface) do      warning =        Enum.reduce(config_map, "", fn @@ -84,4 +85,16 @@ defmodule Pleroma.Config.DeprecationWarnings do        Logger.warn(warning_preface <> warning)      end    end + +  @spec check_media_proxy_whitelist_config() :: :ok | nil +  def check_media_proxy_whitelist_config do +    whitelist = Config.get([:media_proxy, :whitelist]) + +    if Enum.any?(whitelist, &(not String.starts_with?(&1, "http"))) do +      Logger.warn(""" +      !!!DEPRECATION WARNING!!! +      Your config is using old format (only domain) for MediaProxy whitelist option. Setting should work for now, but you are advised to change format to scheme with port to prevent possible issues later. +      """) +    end +  end  end diff --git a/lib/pleroma/config/loader.ex b/lib/pleroma/config/loader.ex index 0f3ecf1ed..64e7de6df 100644 --- a/lib/pleroma/config/loader.ex +++ b/lib/pleroma/config/loader.ex @@ -12,6 +12,11 @@ defmodule Pleroma.Config.Loader do      :swarm    ] +  @reject_groups [ +    :postgrex, +    :tesla +  ] +    if Code.ensure_loaded?(Config.Reader) do      @reader Config.Reader @@ -47,7 +52,8 @@ defmodule Pleroma.Config.Loader do    @spec filter_group(atom(), keyword()) :: keyword()    def filter_group(group, configs) do      Enum.reject(configs[group], fn {key, _v} -> -      key in @reject_keys or (group == :phoenix and key == :serve_endpoints) or group == :postgrex +      key in @reject_keys or group in @reject_groups or +        (group == :phoenix and key == :serve_endpoints)      end)    end  end diff --git a/lib/pleroma/config/transfer_task.ex b/lib/pleroma/config/transfer_task.ex index eb86b8ff4..a0d7b7d71 100644 --- a/lib/pleroma/config/transfer_task.ex +++ b/lib/pleroma/config/transfer_task.ex @@ -31,8 +31,8 @@ defmodule Pleroma.Config.TransferTask do      {:pleroma, :gopher, [:enabled]}    ] -  def start_link(_) do -    load_and_update_env() +  def start_link(restart_pleroma? \\ true) do +    load_and_update_env([], restart_pleroma?)      if Config.get(:env) == :test, do: Ecto.Adapters.SQL.Sandbox.checkin(Repo)      :ignore    end diff --git a/lib/pleroma/docs/generator.ex b/lib/pleroma/docs/generator.ex index e0fc8cd02..a671a6278 100644 --- a/lib/pleroma/docs/generator.ex +++ b/lib/pleroma/docs/generator.ex @@ -6,16 +6,21 @@ defmodule Pleroma.Docs.Generator do      implementation.process(descriptions)    end -  @spec list_modules_in_dir(String.t(), String.t()) :: [module()] -  def list_modules_in_dir(dir, start) do -    with {:ok, files} <- File.ls(dir) do -      files -      |> Enum.filter(&String.ends_with?(&1, ".ex")) -      |> Enum.map(fn filename -> -        module = filename |> String.trim_trailing(".ex") |> Macro.camelize() -        String.to_atom(start <> module) -      end) -    end +  @spec list_behaviour_implementations(behaviour :: module()) :: [module()] +  def list_behaviour_implementations(behaviour) do +    :code.all_loaded() +    |> Enum.filter(fn {module, _} -> +      # This shouldn't be needed as all modules are expected to have module_info/1, +      # but in test enviroments some transient modules `:elixir_compiler_XX` +      # are loaded for some reason (where XX is a random integer). +      if function_exported?(module, :module_info, 1) do +        module.module_info(:attributes) +        |> Keyword.get_values(:behaviour) +        |> List.flatten() +        |> Enum.member?(behaviour) +      end +    end) +    |> Enum.map(fn {module, _} -> module end)    end    @doc """ @@ -87,6 +92,12 @@ defmodule Pleroma.Docs.Generator do        else: string    end +  defp format_suggestions({:list_behaviour_implementations, behaviour}) do +    behaviour +    |> list_behaviour_implementations() +    |> format_suggestions() +  end +    defp format_suggestions([]), do: []    defp format_suggestions([suggestion | tail]) do diff --git a/lib/pleroma/docs/json.ex b/lib/pleroma/docs/json.ex index d1cf1f487..feeb4320e 100644 --- a/lib/pleroma/docs/json.ex +++ b/lib/pleroma/docs/json.ex @@ -1,5 +1,19 @@  defmodule Pleroma.Docs.JSON do    @behaviour Pleroma.Docs.Generator +  @external_resource "config/description.exs" +  @raw_config Pleroma.Config.Loader.read("config/description.exs") +  @raw_descriptions @raw_config[:pleroma][:config_description] +  @term __MODULE__.Compiled + +  @spec compile :: :ok +  def compile do +    :persistent_term.put(@term, Pleroma.Docs.Generator.convert_to_strings(@raw_descriptions)) +  end + +  @spec compiled_descriptions :: Map.t() +  def compiled_descriptions do +    :persistent_term.get(@term) +  end    @spec process(keyword()) :: {:ok, String.t()}    def process(descriptions) do @@ -13,11 +27,4 @@ defmodule Pleroma.Docs.JSON do        {:ok, path}      end    end - -  def compile do -    with config <- Pleroma.Config.Loader.read("config/description.exs") do -      config[:pleroma][:config_description] -      |> Pleroma.Docs.Generator.convert_to_strings() -    end -  end  end diff --git a/lib/pleroma/docs/markdown.ex b/lib/pleroma/docs/markdown.ex index 68b106499..da3f20f43 100644 --- a/lib/pleroma/docs/markdown.ex +++ b/lib/pleroma/docs/markdown.ex @@ -68,6 +68,11 @@ defmodule Pleroma.Docs.Markdown do      IO.write(file, "  #{list_mark}`#{inspect(suggestion)}`\n")    end +  defp print_suggestions(file, {:list_behaviour_implementations, behaviour}) do +    suggestions = Pleroma.Docs.Generator.list_behaviour_implementations(behaviour) +    print_suggestions(file, suggestions) +  end +    defp print_suggestions(_file, nil), do: nil    defp print_suggestions(_file, ""), do: nil diff --git a/lib/pleroma/emails/admin_email.ex b/lib/pleroma/emails/admin_email.ex index 55f61024e..aa0b2a66b 100644 --- a/lib/pleroma/emails/admin_email.ex +++ b/lib/pleroma/emails/admin_email.ex @@ -10,7 +10,7 @@ defmodule Pleroma.Emails.AdminEmail do    alias Pleroma.Config    alias Pleroma.Web.Router.Helpers -  defp instance_config, do: Pleroma.Config.get(:instance) +  defp instance_config, do: Config.get(:instance)    defp instance_name, do: instance_config()[:name]    defp instance_notify_email do @@ -72,6 +72,8 @@ defmodule Pleroma.Emails.AdminEmail do      <p>Reported Account: <a href="#{user_url(account)}">#{account.nickname}</a></p>      #{comment_html}      #{statuses_html} +    <p> +    <a href="#{Pleroma.Web.base_url()}/pleroma/admin/#/reports/index">View Reports in AdminFE</a>      """      new() diff --git a/lib/pleroma/emoji/loader.ex b/lib/pleroma/emoji/loader.ex index 3de2dc762..03a6bca0b 100644 --- a/lib/pleroma/emoji/loader.ex +++ b/lib/pleroma/emoji/loader.ex @@ -108,7 +108,7 @@ defmodule Pleroma.Emoji.Loader do        if File.exists?(emoji_txt) do          load_from_file(emoji_txt, emoji_groups)        else -        extensions = Pleroma.Config.get([:emoji, :pack_extensions]) +        extensions = Config.get([:emoji, :pack_extensions])          Logger.info(            "No emoji.txt found for pack \"#{pack_name}\", assuming all #{ diff --git a/lib/pleroma/filter.ex b/lib/pleroma/filter.ex index 4d61b3650..5d6df9530 100644 --- a/lib/pleroma/filter.ex +++ b/lib/pleroma/filter.ex @@ -34,10 +34,18 @@ defmodule Pleroma.Filter do      Repo.one(query)    end -  def get_filters(%User{id: user_id} = _user) do +  def get_active(query) do +    from(f in query, where: is_nil(f.expires_at) or f.expires_at > ^NaiveDateTime.utc_now()) +  end + +  def get_irreversible(query) do +    from(f in query, where: f.hide) +  end + +  def get_filters(query \\ __MODULE__, %User{id: user_id}) do      query =        from( -        f in Pleroma.Filter, +        f in query,          where: f.user_id == ^user_id,          order_by: [desc: :id]        ) @@ -95,4 +103,34 @@ defmodule Pleroma.Filter do      |> validate_required([:phrase, :context])      |> Repo.update()    end + +  def compose_regex(user_or_filters, format \\ :postgres) + +  def compose_regex(%User{} = user, format) do +    __MODULE__ +    |> get_active() +    |> get_irreversible() +    |> get_filters(user) +    |> compose_regex(format) +  end + +  def compose_regex([_ | _] = filters, format) do +    phrases = +      filters +      |> Enum.map(& &1.phrase) +      |> Enum.join("|") + +    case format do +      :postgres -> +        "\\y(#{phrases})\\y" + +      :re -> +        ~r/\b#{phrases}\b/i + +      _ -> +        nil +    end +  end + +  def compose_regex(_, _), do: nil  end diff --git a/lib/pleroma/gun/api.ex b/lib/pleroma/gun/api.ex index f51cd7db8..09be74392 100644 --- a/lib/pleroma/gun/api.ex +++ b/lib/pleroma/gun/api.ex @@ -19,7 +19,8 @@ defmodule Pleroma.Gun.API do      :tls_opts,      :tcp_opts,      :socks_opts, -    :ws_opts +    :ws_opts, +    :supervise    ]    @impl Gun diff --git a/lib/pleroma/gun/conn.ex b/lib/pleroma/gun/conn.ex index cd25a2e74..a3f75a4bb 100644 --- a/lib/pleroma/gun/conn.ex +++ b/lib/pleroma/gun/conn.ex @@ -3,85 +3,33 @@  # SPDX-License-Identifier: AGPL-3.0-only  defmodule Pleroma.Gun.Conn do -  @moduledoc """ -  Struct for gun connection data -  """    alias Pleroma.Gun -  alias Pleroma.Pool.Connections    require Logger -  @type gun_state :: :up | :down -  @type conn_state :: :active | :idle - -  @type t :: %__MODULE__{ -          conn: pid(), -          gun_state: gun_state(), -          conn_state: conn_state(), -          used_by: [pid()], -          last_reference: pos_integer(), -          crf: float(), -          retries: pos_integer() -        } - -  defstruct conn: nil, -            gun_state: :open, -            conn_state: :init, -            used_by: [], -            last_reference: 0, -            crf: 1, -            retries: 0 - -  @spec open(String.t() | URI.t(), atom(), keyword()) :: :ok | nil -  def open(url, name, opts \\ []) -  def open(url, name, opts) when is_binary(url), do: open(URI.parse(url), name, opts) - -  def open(%URI{} = uri, name, opts) do +  def open(%URI{} = uri, opts) do      pool_opts = Pleroma.Config.get([:connections_pool], [])      opts =        opts        |> Enum.into(%{}) -      |> Map.put_new(:retry, pool_opts[:retry] || 1) -      |> Map.put_new(:retry_timeout, pool_opts[:retry_timeout] || 1000)        |> Map.put_new(:await_up_timeout, pool_opts[:await_up_timeout] || 5_000) +      |> Map.put_new(:supervise, false)        |> maybe_add_tls_opts(uri) -    key = "#{uri.scheme}:#{uri.host}:#{uri.port}" - -    max_connections = pool_opts[:max_connections] || 250 - -    conn_pid = -      if Connections.count(name) < max_connections do -        do_open(uri, opts) -      else -        close_least_used_and_do_open(name, uri, opts) -      end - -    if is_pid(conn_pid) do -      conn = %Pleroma.Gun.Conn{ -        conn: conn_pid, -        gun_state: :up, -        conn_state: :active, -        last_reference: :os.system_time(:second) -      } - -      :ok = Gun.set_owner(conn_pid, Process.whereis(name)) -      Connections.add_conn(name, key, conn) -    end +    do_open(uri, opts)    end    defp maybe_add_tls_opts(opts, %URI{scheme: "http"}), do: opts -  defp maybe_add_tls_opts(opts, %URI{scheme: "https", host: host}) do +  defp maybe_add_tls_opts(opts, %URI{scheme: "https"}) do      tls_opts = [        verify: :verify_peer,        cacertfile: CAStore.file_path(),        depth: 20,        reuse_sessions: false, -      verify_fun: -        {&:ssl_verify_hostname.verify_fun/3, -         [check_hostname: Pleroma.HTTP.Connection.format_host(host)]} +      log_level: :warning, +      customize_hostname_check: [match_fun: :public_key.pkix_verify_hostname_match_fun(:https)]      ]      tls_opts = @@ -105,7 +53,7 @@ defmodule Pleroma.Gun.Conn do           {:ok, _} <- Gun.await_up(conn, opts[:await_up_timeout]),           stream <- Gun.connect(conn, connect_opts),           {:response, :fin, 200, _} <- Gun.await(conn, stream) do -      conn +      {:ok, conn}      else        error ->          Logger.warn( @@ -141,7 +89,7 @@ defmodule Pleroma.Gun.Conn do      with {:ok, conn} <- Gun.open(proxy_host, proxy_port, opts),           {:ok, _} <- Gun.await_up(conn, opts[:await_up_timeout]) do -      conn +      {:ok, conn}      else        error ->          Logger.warn( @@ -155,11 +103,11 @@ defmodule Pleroma.Gun.Conn do    end    defp do_open(%URI{host: host, port: port} = uri, opts) do -    host = Pleroma.HTTP.Connection.parse_host(host) +    host = Pleroma.HTTP.AdapterHelper.parse_host(host)      with {:ok, conn} <- Gun.open(host, port, opts),           {:ok, _} <- Gun.await_up(conn, opts[:await_up_timeout]) do -      conn +      {:ok, conn}      else        error ->          Logger.warn( @@ -171,7 +119,7 @@ defmodule Pleroma.Gun.Conn do    end    defp destination_opts(%URI{host: host, port: port}) do -    host = Pleroma.HTTP.Connection.parse_host(host) +    host = Pleroma.HTTP.AdapterHelper.parse_host(host)      %{host: host, port: port}    end @@ -181,17 +129,6 @@ defmodule Pleroma.Gun.Conn do    defp add_http2_opts(opts, _, _), do: opts -  defp close_least_used_and_do_open(name, uri, opts) do -    with [{key, conn} | _conns] <- Connections.get_unused_conns(name), -         :ok <- Gun.close(conn.conn) do -      Connections.remove_conn(name, key) - -      do_open(uri, opts) -    else -      [] -> {:error, :pool_overflowed} -    end -  end -    def compose_uri_log(%URI{scheme: scheme, host: host, path: path}) do      "#{scheme}://#{host}#{path}"    end diff --git a/lib/pleroma/gun/connection_pool.ex b/lib/pleroma/gun/connection_pool.ex new file mode 100644 index 000000000..8b41a668c --- /dev/null +++ b/lib/pleroma/gun/connection_pool.ex @@ -0,0 +1,79 @@ +defmodule Pleroma.Gun.ConnectionPool do +  @registry __MODULE__ + +  alias Pleroma.Gun.ConnectionPool.WorkerSupervisor + +  def children do +    [ +      {Registry, keys: :unique, name: @registry}, +      Pleroma.Gun.ConnectionPool.WorkerSupervisor +    ] +  end + +  def get_conn(uri, opts) do +    key = "#{uri.scheme}:#{uri.host}:#{uri.port}" + +    case Registry.lookup(@registry, key) do +      # The key has already been registered, but connection is not up yet +      [{worker_pid, nil}] -> +        get_gun_pid_from_worker(worker_pid, true) + +      [{worker_pid, {gun_pid, _used_by, _crf, _last_reference}}] -> +        GenServer.cast(worker_pid, {:add_client, self(), false}) +        {:ok, gun_pid} + +      [] -> +        # :gun.set_owner fails in :connected state for whatevever reason, +        # so we open the connection in the process directly and send it's pid back +        # We trust gun to handle timeouts by itself +        case WorkerSupervisor.start_worker([key, uri, opts, self()]) do +          {:ok, worker_pid} -> +            get_gun_pid_from_worker(worker_pid, false) + +          {:error, {:already_started, worker_pid}} -> +            get_gun_pid_from_worker(worker_pid, true) + +          err -> +            err +        end +    end +  end + +  defp get_gun_pid_from_worker(worker_pid, register) do +    # GenServer.call will block the process for timeout length if +    # the server crashes on startup (which will happen if gun fails to connect) +    # so instead we use cast + monitor + +    ref = Process.monitor(worker_pid) +    if register, do: GenServer.cast(worker_pid, {:add_client, self(), true}) + +    receive do +      {:conn_pid, pid} -> +        Process.demonitor(ref) +        {:ok, pid} + +      {:DOWN, ^ref, :process, ^worker_pid, reason} -> +        case reason do +          {:shutdown, error} -> error +          _ -> {:error, reason} +        end +    end +  end + +  def release_conn(conn_pid) do +    # :ets.fun2ms(fn {_, {worker_pid, {gun_pid, _, _, _}}} when gun_pid == conn_pid -> +    #    worker_pid end) +    query_result = +      Registry.select(@registry, [ +        {{:_, :"$1", {:"$2", :_, :_, :_}}, [{:==, :"$2", conn_pid}], [:"$1"]} +      ]) + +    case query_result do +      [worker_pid] -> +        GenServer.cast(worker_pid, {:remove_client, self()}) + +      [] -> +        :ok +    end +  end +end diff --git a/lib/pleroma/gun/connection_pool/reclaimer.ex b/lib/pleroma/gun/connection_pool/reclaimer.ex new file mode 100644 index 000000000..cea800882 --- /dev/null +++ b/lib/pleroma/gun/connection_pool/reclaimer.ex @@ -0,0 +1,85 @@ +defmodule Pleroma.Gun.ConnectionPool.Reclaimer do +  use GenServer, restart: :temporary + +  @registry Pleroma.Gun.ConnectionPool + +  def start_monitor do +    pid = +      case :gen_server.start(__MODULE__, [], name: {:via, Registry, {@registry, "reclaimer"}}) do +        {:ok, pid} -> +          pid + +        {:error, {:already_registered, pid}} -> +          pid +      end + +    {pid, Process.monitor(pid)} +  end + +  @impl true +  def init(_) do +    {:ok, nil, {:continue, :reclaim}} +  end + +  @impl true +  def handle_continue(:reclaim, _) do +    max_connections = Pleroma.Config.get([:connections_pool, :max_connections]) + +    reclaim_max = +      [:connections_pool, :reclaim_multiplier] +      |> Pleroma.Config.get() +      |> Kernel.*(max_connections) +      |> round +      |> max(1) + +    :telemetry.execute([:pleroma, :connection_pool, :reclaim, :start], %{}, %{ +      max_connections: max_connections, +      reclaim_max: reclaim_max +    }) + +    # :ets.fun2ms( +    # fn {_, {worker_pid, {_, used_by, crf, last_reference}}} when used_by == [] -> +    #   {worker_pid, crf, last_reference} end) +    unused_conns = +      Registry.select( +        @registry, +        [ +          {{:_, :"$1", {:_, :"$2", :"$3", :"$4"}}, [{:==, :"$2", []}], [{{:"$1", :"$3", :"$4"}}]} +        ] +      ) + +    case unused_conns do +      [] -> +        :telemetry.execute( +          [:pleroma, :connection_pool, :reclaim, :stop], +          %{reclaimed_count: 0}, +          %{ +            max_connections: max_connections +          } +        ) + +        {:stop, :no_unused_conns, nil} + +      unused_conns -> +        reclaimed = +          unused_conns +          |> Enum.sort(fn {_pid1, crf1, last_reference1}, {_pid2, crf2, last_reference2} -> +            crf1 <= crf2 and last_reference1 <= last_reference2 +          end) +          |> Enum.take(reclaim_max) + +        reclaimed +        |> Enum.each(fn {pid, _, _} -> +          DynamicSupervisor.terminate_child(Pleroma.Gun.ConnectionPool.WorkerSupervisor, pid) +        end) + +        :telemetry.execute( +          [:pleroma, :connection_pool, :reclaim, :stop], +          %{reclaimed_count: Enum.count(reclaimed)}, +          %{max_connections: max_connections} +        ) + +        {:stop, :normal, nil} +    end +  end +end diff --git a/lib/pleroma/gun/connection_pool/worker.ex b/lib/pleroma/gun/connection_pool/worker.ex new file mode 100644 index 000000000..f33447cb6 --- /dev/null +++ b/lib/pleroma/gun/connection_pool/worker.ex @@ -0,0 +1,127 @@ +defmodule Pleroma.Gun.ConnectionPool.Worker do +  alias Pleroma.Gun +  use GenServer, restart: :temporary + +  @registry Pleroma.Gun.ConnectionPool + +  def start_link([key | _] = opts) do +    GenServer.start_link(__MODULE__, opts, name: {:via, Registry, {@registry, key}}) +  end + +  @impl true +  def init([_key, _uri, _opts, _client_pid] = opts) do +    {:ok, nil, {:continue, {:connect, opts}}} +  end + +  @impl true +  def handle_continue({:connect, [key, uri, opts, client_pid]}, _) do +    with {:ok, conn_pid} <- Gun.Conn.open(uri, opts), +         Process.link(conn_pid) do +      time = :erlang.monotonic_time(:millisecond) + +      {_, _} = +        Registry.update_value(@registry, key, fn _ -> +          {conn_pid, [client_pid], 1, time} +        end) + +      send(client_pid, {:conn_pid, conn_pid}) + +      {:noreply, +       %{key: key, timer: nil, client_monitors: %{client_pid => Process.monitor(client_pid)}}, +       :hibernate} +    else +      err -> +        {:stop, {:shutdown, err}, nil} +    end +  end + +  @impl true +  def handle_cast({:add_client, client_pid, send_pid_back}, %{key: key} = state) do +    time = :erlang.monotonic_time(:millisecond) + +    {{conn_pid, _, _, _}, _} = +      Registry.update_value(@registry, key, fn {conn_pid, used_by, crf, last_reference} -> +        {conn_pid, [client_pid | used_by], crf(time - last_reference, crf), time} +      end) + +    if send_pid_back, do: send(client_pid, {:conn_pid, conn_pid}) + +    state = +      if state.timer != nil do +        Process.cancel_timer(state[:timer]) +        %{state | timer: nil} +      else +        state +      end + +    ref = Process.monitor(client_pid) + +    state = put_in(state.client_monitors[client_pid], ref) +    {:noreply, state, :hibernate} +  end + +  @impl true +  def handle_cast({:remove_client, client_pid}, %{key: key} = state) do +    {{_conn_pid, used_by, _crf, _last_reference}, _} = +      Registry.update_value(@registry, key, fn {conn_pid, used_by, crf, last_reference} -> +        {conn_pid, List.delete(used_by, client_pid), crf, last_reference} +      end) + +    {ref, state} = pop_in(state.client_monitors[client_pid]) +    Process.demonitor(ref) + +    timer = +      if used_by == [] do +        max_idle = Pleroma.Config.get([:connections_pool, :max_idle_time], 30_000) +        Process.send_after(self(), :idle_close, max_idle) +      else +        nil +      end + +    {:noreply, %{state | timer: timer}, :hibernate} +  end + +  @impl true +  def handle_info(:idle_close, state) do +    # Gun monitors the owner process, and will close the connection automatically +    # when it's terminated +    {:stop, :normal, state} +  end + +  # Gracefully shutdown if the connection got closed without any streams left +  @impl true +  def handle_info({:gun_down, _pid, _protocol, _reason, []}, state) do +    {:stop, :normal, state} +  end + +  # Otherwise, shutdown with an error +  @impl true +  def handle_info({:gun_down, _pid, _protocol, _reason, _killed_streams} = down_message, state) do +    {:stop, {:error, down_message}, state} +  end + +  @impl true +  def handle_info({:DOWN, _ref, :process, pid, reason}, state) do +    # Sometimes the client is dead before we demonitor it in :remove_client, so the message +    # arrives anyway + +    case state.client_monitors[pid] do +      nil -> +        {:noreply, state, :hibernate} + +      _ref -> +        :telemetry.execute( +          [:pleroma, :connection_pool, :client_death], +          %{client_pid: pid, reason: reason}, +          %{key: state.key} +        ) + +        handle_cast({:remove_client, pid}, state) +    end +  end + +  # LRFU policy: https://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.55.1478 +  defp crf(time_delta, prev_crf) do +    1 + :math.pow(0.5, 0.0001 * time_delta) * prev_crf +  end +end diff --git a/lib/pleroma/gun/connection_pool/worker_supervisor.ex b/lib/pleroma/gun/connection_pool/worker_supervisor.ex new file mode 100644 index 000000000..39615c956 --- /dev/null +++ b/lib/pleroma/gun/connection_pool/worker_supervisor.ex @@ -0,0 +1,45 @@ +defmodule Pleroma.Gun.ConnectionPool.WorkerSupervisor do +  @moduledoc "Supervisor for pool workers. Does not do anything except enforce max connection limit" + +  use DynamicSupervisor + +  def start_link(opts) do +    DynamicSupervisor.start_link(__MODULE__, opts, name: __MODULE__) +  end + +  def init(_opts) do +    DynamicSupervisor.init( +      strategy: :one_for_one, +      max_children: Pleroma.Config.get([:connections_pool, :max_connections]) +    ) +  end + +  def start_worker(opts, retry \\ false) do +    case DynamicSupervisor.start_child(__MODULE__, {Pleroma.Gun.ConnectionPool.Worker, opts}) do +      {:error, :max_children} -> +        if retry or free_pool() == :error do +          :telemetry.execute([:pleroma, :connection_pool, :provision_failure], %{opts: opts}) +          {:error, :pool_full} +        else +          start_worker(opts, true) +        end + +      res -> +        res +    end +  end + +  defp free_pool do +    wait_for_reclaimer_finish(Pleroma.Gun.ConnectionPool.Reclaimer.start_monitor()) +  end + +  defp wait_for_reclaimer_finish({pid, mon}) do +    receive do +      {:DOWN, ^mon, :process, ^pid, :no_unused_conns} -> +        :error + +      {:DOWN, ^mon, :process, ^pid, :normal} -> +        :ok +    end +  end +end diff --git a/lib/pleroma/html.ex b/lib/pleroma/html.ex index d78c5f202..dc1b9b840 100644 --- a/lib/pleroma/html.ex +++ b/lib/pleroma/html.ex @@ -109,7 +109,7 @@ defmodule Pleroma.HTML do        result =          content          |> Floki.parse_fragment!() -        |> Floki.filter_out("a.mention,a.hashtag,a[rel~=\"tag\"]") +        |> Floki.filter_out("a.mention,a.hashtag,a.attachment,a[rel~=\"tag\"]")          |> Floki.attribute("a", "href")          |> Enum.at(0) diff --git a/lib/pleroma/http/adapter_helper.ex b/lib/pleroma/http/adapter_helper.ex index 510722ff9..9ec3836b0 100644 --- a/lib/pleroma/http/adapter_helper.ex +++ b/lib/pleroma/http/adapter_helper.ex @@ -3,32 +3,30 @@  # SPDX-License-Identifier: AGPL-3.0-only  defmodule Pleroma.HTTP.AdapterHelper do -  alias Pleroma.HTTP.Connection +  @moduledoc """ +  Configure Tesla.Client with default and customized adapter options. +  """ +  @defaults [pool: :federation] + +  @type proxy_type() :: :socks4 | :socks5 +  @type host() :: charlist() | :inet.ip_address() + +  alias Pleroma.Config +  alias Pleroma.HTTP.AdapterHelper +  require Logger    @type proxy ::            {Connection.host(), pos_integer()}            | {Connection.proxy_type(), Connection.host(), pos_integer()}    @callback options(keyword(), URI.t()) :: keyword() -  @callback after_request(keyword()) :: :ok - -  @spec options(keyword(), URI.t()) :: keyword() -  def options(opts, _uri) do -    proxy = Pleroma.Config.get([:http, :proxy_url], nil) -    maybe_add_proxy(opts, format_proxy(proxy)) -  end - -  @spec maybe_get_conn(URI.t(), keyword()) :: keyword() -  def maybe_get_conn(_uri, opts), do: opts - -  @spec after_request(keyword()) :: :ok -  def after_request(_opts), do: :ok +  @callback get_conn(URI.t(), keyword()) :: {:ok, term()} | {:error, term()}    @spec format_proxy(String.t() | tuple() | nil) :: proxy() | nil    def format_proxy(nil), do: nil    def format_proxy(proxy_url) do -    case Connection.parse_proxy(proxy_url) do +    case parse_proxy(proxy_url) do        {:ok, host, port} -> {host, port}        {:ok, type, host, port} -> {type, host, port}        _ -> nil @@ -38,4 +36,105 @@ defmodule Pleroma.HTTP.AdapterHelper do    @spec maybe_add_proxy(keyword(), proxy() | nil) :: keyword()    def maybe_add_proxy(opts, nil), do: opts    def maybe_add_proxy(opts, proxy), do: Keyword.put_new(opts, :proxy, proxy) + +  @doc """ +  Merge default connection & adapter options with received ones. +  """ + +  @spec options(URI.t(), keyword()) :: keyword() +  def options(%URI{} = uri, opts \\ []) do +    @defaults +    |> put_timeout() +    |> Keyword.merge(opts) +    |> adapter_helper().options(uri) +  end + +  # For Hackney, this is the time a connection can stay idle in the pool. +  # For Gun, this is the timeout to receive a message from Gun. +  defp put_timeout(opts) do +    {config_key, default} = +      if adapter() == Tesla.Adapter.Gun do +        {:pools, Config.get([:pools, :default, :timeout], 5_000)} +      else +        {:hackney_pools, 10_000} +      end + +    timeout = Config.get([config_key, opts[:pool], :timeout], default) + +    Keyword.merge(opts, timeout: timeout) +  end + +  def get_conn(uri, opts), do: adapter_helper().get_conn(uri, opts) +  defp adapter, do: Application.get_env(:tesla, :adapter) + +  defp adapter_helper do +    case adapter() do +      Tesla.Adapter.Gun -> AdapterHelper.Gun +      Tesla.Adapter.Hackney -> AdapterHelper.Hackney +      _ -> AdapterHelper.Default +    end +  end + +  @spec parse_proxy(String.t() | tuple() | nil) :: +          {:ok, host(), pos_integer()} +          | {:ok, proxy_type(), host(), pos_integer()} +          | {:error, atom()} +          | nil + +  def parse_proxy(nil), do: nil + +  def parse_proxy(proxy) when is_binary(proxy) do +    with [host, port] <- String.split(proxy, ":"), +         {port, ""} <- Integer.parse(port) do +      {:ok, parse_host(host), port} +    else +      {_, _} -> +        Logger.warn("Parsing port failed #{inspect(proxy)}") +        {:error, :invalid_proxy_port} + +      :error -> +        Logger.warn("Parsing port failed #{inspect(proxy)}") +        {:error, :invalid_proxy_port} + +      _ -> +        Logger.warn("Parsing proxy failed #{inspect(proxy)}") +        {:error, :invalid_proxy} +    end +  end + +  def parse_proxy(proxy) when is_tuple(proxy) do +    with {type, host, port} <- proxy do +      {:ok, type, parse_host(host), port} +    else +      _ -> +        Logger.warn("Parsing proxy failed #{inspect(proxy)}") +        {:error, :invalid_proxy} +    end +  end + +  @spec parse_host(String.t() | atom() | charlist()) :: charlist() | :inet.ip_address() +  def parse_host(host) when is_list(host), do: host +  def parse_host(host) when is_atom(host), do: to_charlist(host) + +  def parse_host(host) when is_binary(host) do +    host = to_charlist(host) + +    case :inet.parse_address(host) do +      {:error, :einval} -> host +      {:ok, ip} -> ip +    end +  end + +  @spec format_host(String.t()) :: charlist() +  def format_host(host) do +    host_charlist = to_charlist(host) + +    case :inet.parse_address(host_charlist) do +      {:error, :einval} -> +        :idna.encode(host_charlist) + +      {:ok, _ip} -> +        host_charlist +    end +  end  end diff --git a/lib/pleroma/http/adapter_helper/default.ex b/lib/pleroma/http/adapter_helper/default.ex new file mode 100644 index 000000000..e13441316 --- /dev/null +++ b/lib/pleroma/http/adapter_helper/default.ex @@ -0,0 +1,14 @@ +defmodule Pleroma.HTTP.AdapterHelper.Default do +  alias Pleroma.HTTP.AdapterHelper + +  @behaviour Pleroma.HTTP.AdapterHelper + +  @spec options(keyword(), URI.t()) :: keyword() +  def options(opts, _uri) do +    proxy = Pleroma.Config.get([:http, :proxy_url], nil) +    AdapterHelper.maybe_add_proxy(opts, AdapterHelper.format_proxy(proxy)) +  end + +  @spec get_conn(URI.t(), keyword()) :: {:ok, keyword()} +  def get_conn(_uri, opts), do: {:ok, opts} +end diff --git a/lib/pleroma/http/adapter_helper/gun.ex b/lib/pleroma/http/adapter_helper/gun.ex index ead7cdc6b..b4ff8306c 100644 --- a/lib/pleroma/http/adapter_helper/gun.ex +++ b/lib/pleroma/http/adapter_helper/gun.ex @@ -5,8 +5,8 @@  defmodule Pleroma.HTTP.AdapterHelper.Gun do    @behaviour Pleroma.HTTP.AdapterHelper +  alias Pleroma.Gun.ConnectionPool    alias Pleroma.HTTP.AdapterHelper -  alias Pleroma.Pool.Connections    require Logger @@ -14,7 +14,7 @@ defmodule Pleroma.HTTP.AdapterHelper.Gun do      connect_timeout: 5_000,      domain_lookup_timeout: 5_000,      tls_handshake_timeout: 5_000, -    retry: 1, +    retry: 0,      retry_timeout: 1000,      await_up_timeout: 5_000    ] @@ -31,16 +31,7 @@ defmodule Pleroma.HTTP.AdapterHelper.Gun do      |> Keyword.merge(config_opts)      |> add_scheme_opts(uri)      |> AdapterHelper.maybe_add_proxy(proxy) -    |> maybe_get_conn(uri, incoming_opts) -  end - -  @spec after_request(keyword()) :: :ok -  def after_request(opts) do -    if opts[:conn] && opts[:body_as] != :chunks do -      Connections.checkout(opts[:conn], self(), :gun_connections) -    end - -    :ok +    |> Keyword.merge(incoming_opts)    end    defp add_scheme_opts(opts, %{scheme: "http"}), do: opts @@ -48,30 +39,40 @@ defmodule Pleroma.HTTP.AdapterHelper.Gun do    defp add_scheme_opts(opts, %{scheme: "https"}) do      opts      |> Keyword.put(:certificates_verification, true) -    |> Keyword.put(:tls_opts, log_level: :warning)    end -  defp maybe_get_conn(adapter_opts, uri, incoming_opts) do -    {receive_conn?, opts} = -      adapter_opts -      |> Keyword.merge(incoming_opts) -      |> Keyword.pop(:receive_conn, true) - -    if Connections.alive?(:gun_connections) and receive_conn? do -      checkin_conn(uri, opts) -    else -      opts +  @spec get_conn(URI.t(), keyword()) :: {:ok, keyword()} | {:error, atom()} +  def get_conn(uri, opts) do +    case ConnectionPool.get_conn(uri, opts) do +      {:ok, conn_pid} -> {:ok, Keyword.merge(opts, conn: conn_pid, close_conn: false)} +      err -> err      end    end -  defp checkin_conn(uri, opts) do -    case Connections.checkin(uri, :gun_connections) do -      nil -> -        Task.start(Pleroma.Gun.Conn, :open, [uri, :gun_connections, opts]) -        opts +  @prefix Pleroma.Gun.ConnectionPool +  def limiter_setup do +    wait = Pleroma.Config.get([:connections_pool, :connection_acquisition_wait]) +    retries = Pleroma.Config.get([:connections_pool, :connection_acquisition_retries]) -      conn when is_pid(conn) -> -        Keyword.merge(opts, conn: conn, close_conn: false) -    end +    :pools +    |> Pleroma.Config.get([]) +    |> Enum.each(fn {name, opts} -> +      max_running = Keyword.get(opts, :size, 50) +      max_waiting = Keyword.get(opts, :max_waiting, 10) + +      result = +        ConcurrentLimiter.new(:"#{@prefix}.#{name}", max_running, max_waiting, +          wait: wait, +          max_retries: retries +        ) + +      case result do +        :ok -> :ok +        {:error, :existing} -> :ok +        e -> raise e +      end +    end) + +    :ok    end  end diff --git a/lib/pleroma/http/adapter_helper/hackney.ex b/lib/pleroma/http/adapter_helper/hackney.ex index 3972a03a9..cd569422b 100644 --- a/lib/pleroma/http/adapter_helper/hackney.ex +++ b/lib/pleroma/http/adapter_helper/hackney.ex @@ -24,5 +24,6 @@ defmodule Pleroma.HTTP.AdapterHelper.Hackney do    defp add_scheme_opts(opts, _), do: opts -  def after_request(_), do: :ok +  @spec get_conn(URI.t(), keyword()) :: {:ok, keyword()} +  def get_conn(_uri, opts), do: {:ok, opts}  end diff --git a/lib/pleroma/http/connection.ex b/lib/pleroma/http/connection.ex deleted file mode 100644 index ebacf7902..000000000 --- a/lib/pleroma/http/connection.ex +++ /dev/null @@ -1,124 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.HTTP.Connection do -  @moduledoc """ -  Configure Tesla.Client with default and customized adapter options. -  """ - -  alias Pleroma.Config -  alias Pleroma.HTTP.AdapterHelper - -  require Logger - -  @defaults [pool: :federation] - -  @type ip_address :: ipv4_address() | ipv6_address() -  @type ipv4_address :: {0..255, 0..255, 0..255, 0..255} -  @type ipv6_address :: -          {0..65_535, 0..65_535, 0..65_535, 0..65_535, 0..65_535, 0..65_535, 0..65_535, 0..65_535} -  @type proxy_type() :: :socks4 | :socks5 -  @type host() :: charlist() | ip_address() - -  @doc """ -  Merge default connection & adapter options with received ones. -  """ - -  @spec options(URI.t(), keyword()) :: keyword() -  def options(%URI{} = uri, opts \\ []) do -    @defaults -    |> pool_timeout() -    |> Keyword.merge(opts) -    |> adapter_helper().options(uri) -  end - -  defp pool_timeout(opts) do -    {config_key, default} = -      if adapter() == Tesla.Adapter.Gun do -        {:pools, Config.get([:pools, :default, :timeout])} -      else -        {:hackney_pools, 10_000} -      end - -    timeout = Config.get([config_key, opts[:pool], :timeout], default) - -    Keyword.merge(opts, timeout: timeout) -  end - -  @spec after_request(keyword()) :: :ok -  def after_request(opts), do: adapter_helper().after_request(opts) - -  defp adapter, do: Application.get_env(:tesla, :adapter) - -  defp adapter_helper do -    case adapter() do -      Tesla.Adapter.Gun -> AdapterHelper.Gun -      Tesla.Adapter.Hackney -> AdapterHelper.Hackney -      _ -> AdapterHelper -    end -  end - -  @spec parse_proxy(String.t() | tuple() | nil) :: -          {:ok, host(), pos_integer()} -          | {:ok, proxy_type(), host(), pos_integer()} -          | {:error, atom()} -          | nil - -  def parse_proxy(nil), do: nil - -  def parse_proxy(proxy) when is_binary(proxy) do -    with [host, port] <- String.split(proxy, ":"), -         {port, ""} <- Integer.parse(port) do -      {:ok, parse_host(host), port} -    else -      {_, _} -> -        Logger.warn("Parsing port failed #{inspect(proxy)}") -        {:error, :invalid_proxy_port} - -      :error -> -        Logger.warn("Parsing port failed #{inspect(proxy)}") -        {:error, :invalid_proxy_port} - -      _ -> -        Logger.warn("Parsing proxy failed #{inspect(proxy)}") -        {:error, :invalid_proxy} -    end -  end - -  def parse_proxy(proxy) when is_tuple(proxy) do -    with {type, host, port} <- proxy do -      {:ok, type, parse_host(host), port} -    else -      _ -> -        Logger.warn("Parsing proxy failed #{inspect(proxy)}") -        {:error, :invalid_proxy} -    end -  end - -  @spec parse_host(String.t() | atom() | charlist()) :: charlist() | ip_address() -  def parse_host(host) when is_list(host), do: host -  def parse_host(host) when is_atom(host), do: to_charlist(host) - -  def parse_host(host) when is_binary(host) do -    host = to_charlist(host) - -    case :inet.parse_address(host) do -      {:error, :einval} -> host -      {:ok, ip} -> ip -    end -  end - -  @spec format_host(String.t()) :: charlist() -  def format_host(host) do -    host_charlist = to_charlist(host) - -    case :inet.parse_address(host_charlist) do -      {:error, :einval} -> -        :idna.encode(host_charlist) - -      {:ok, _ip} -> -        host_charlist -    end -  end -end diff --git a/lib/pleroma/http/http.ex b/lib/pleroma/http/http.ex index 66ca75367..6128bc4cf 100644 --- a/lib/pleroma/http/http.ex +++ b/lib/pleroma/http/http.ex @@ -7,7 +7,7 @@ defmodule Pleroma.HTTP do      Wrapper for `Tesla.request/2`.    """ -  alias Pleroma.HTTP.Connection +  alias Pleroma.HTTP.AdapterHelper    alias Pleroma.HTTP.Request    alias Pleroma.HTTP.RequestBuilder, as: Builder    alias Tesla.Client @@ -60,49 +60,29 @@ defmodule Pleroma.HTTP do            {:ok, Env.t()} | {:error, any()}    def request(method, url, body, headers, options) when is_binary(url) do      uri = URI.parse(url) -    adapter_opts = Connection.options(uri, options[:adapter] || []) -    options = put_in(options[:adapter], adapter_opts) -    params = options[:params] || [] -    request = build_request(method, headers, options, url, body, params) - -    adapter = Application.get_env(:tesla, :adapter) -    client = Tesla.client([Tesla.Middleware.FollowRedirects], adapter) - -    pid = Process.whereis(adapter_opts[:pool]) - -    pool_alive? = -      if adapter == Tesla.Adapter.Gun && pid do -        Process.alive?(pid) -      else -        false -      end - -    request_opts = -      adapter_opts -      |> Enum.into(%{}) -      |> Map.put(:env, Pleroma.Config.get([:env])) -      |> Map.put(:pool_alive?, pool_alive?) - -    response = request(client, request, request_opts) - -    Connection.after_request(adapter_opts) - -    response -  end - -  @spec request(Client.t(), keyword(), map()) :: {:ok, Env.t()} | {:error, any()} -  def request(%Client{} = client, request, %{env: :test}), do: request(client, request) - -  def request(%Client{} = client, request, %{body_as: :chunks}), do: request(client, request) - -  def request(%Client{} = client, request, %{pool_alive?: false}), do: request(client, request) - -  def request(%Client{} = client, request, %{pool: pool, timeout: timeout}) do -    :poolboy.transaction( -      pool, -      &Pleroma.Pool.Request.execute(&1, client, request, timeout), -      timeout -    ) +    adapter_opts = AdapterHelper.options(uri, options[:adapter] || []) + +    case AdapterHelper.get_conn(uri, adapter_opts) do +      {:ok, adapter_opts} -> +        options = put_in(options[:adapter], adapter_opts) +        params = options[:params] || [] +        request = build_request(method, headers, options, url, body, params) + +        adapter = Application.get_env(:tesla, :adapter) +        client = Tesla.client([Pleroma.HTTP.Middleware.FollowRedirects], adapter) + +        maybe_limit( +          fn -> +            request(client, request) +          end, +          adapter, +          adapter_opts +        ) + +      # Connection release is handled in a custom FollowRedirects middleware +      err -> +        err +    end    end    @spec request(Client.t(), keyword()) :: {:ok, Env.t()} | {:error, any()} @@ -118,4 +98,13 @@ defmodule Pleroma.HTTP do      |> Builder.add_param(:query, :query, params)      |> Builder.convert_to_keyword()    end + +  @prefix Pleroma.Gun.ConnectionPool +  defp maybe_limit(fun, Tesla.Adapter.Gun, opts) do +    ConcurrentLimiter.limit(:"#{@prefix}.#{opts[:pool] || :default}", fun) +  end + +  defp maybe_limit(fun, _, _) do +    fun.() +  end  end diff --git a/lib/pleroma/instances/instance.ex b/lib/pleroma/instances/instance.ex index 74458c09a..a1f935232 100644 --- a/lib/pleroma/instances/instance.ex +++ b/lib/pleroma/instances/instance.ex @@ -17,6 +17,8 @@ defmodule Pleroma.Instances.Instance do    schema "instances" do      field(:host, :string)      field(:unreachable_since, :naive_datetime_usec) +    field(:favicon, :string) +    field(:favicon_updated_at, :naive_datetime)      timestamps()    end @@ -25,7 +27,7 @@ defmodule Pleroma.Instances.Instance do    def changeset(struct, params \\ %{}) do      struct -    |> cast(params, [:host, :unreachable_since]) +    |> cast(params, [:host, :unreachable_since, :favicon, :favicon_updated_at])      |> validate_required([:host])      |> unique_constraint(:host)    end @@ -120,4 +122,48 @@ defmodule Pleroma.Instances.Instance do    end    defp parse_datetime(datetime), do: datetime + +  def get_or_update_favicon(%URI{host: host} = instance_uri) do +    existing_record = Repo.get_by(Instance, %{host: host}) +    now = NaiveDateTime.utc_now() + +    if existing_record && existing_record.favicon_updated_at && +         NaiveDateTime.diff(now, existing_record.favicon_updated_at) < 86_400 do +      existing_record.favicon +    else +      favicon = scrape_favicon(instance_uri) + +      if existing_record do +        existing_record +        |> changeset(%{favicon: favicon, favicon_updated_at: now}) +        |> Repo.update() +      else +        %Instance{} +        |> changeset(%{host: host, favicon: favicon, favicon_updated_at: now}) +        |> Repo.insert() +      end + +      favicon +    end +  end + +  defp scrape_favicon(%URI{} = instance_uri) do +    try do +      with {:ok, %Tesla.Env{body: html}} <- +             Pleroma.HTTP.get(to_string(instance_uri), [{:Accept, "text/html"}]), +           favicon_rel <- +             html +             |> Floki.parse_document!() +             |> Floki.attribute("link[rel=icon]", "href") +             |> List.first(), +           favicon <- URI.merge(instance_uri, favicon_rel) |> to_string(), +           true <- is_binary(favicon) do +        favicon +      else +        _ -> nil +      end +    rescue +      _ -> nil +    end +  end  end diff --git a/lib/pleroma/migration_helper/notification_backfill.ex b/lib/pleroma/migration_helper/notification_backfill.ex index b3770307a..d260e62ca 100644 --- a/lib/pleroma/migration_helper/notification_backfill.ex +++ b/lib/pleroma/migration_helper/notification_backfill.ex @@ -3,7 +3,6 @@  # SPDX-License-Identifier: AGPL-3.0-only  defmodule Pleroma.MigrationHelper.NotificationBackfill do -  alias Pleroma.Notification    alias Pleroma.Object    alias Pleroma.Repo    alias Pleroma.User @@ -25,18 +24,27 @@ defmodule Pleroma.MigrationHelper.NotificationBackfill do          |> type_from_activity()        notification -      |> Notification.changeset(%{type: type}) +      |> Ecto.Changeset.change(%{type: type})        |> Repo.update()      end)    end +  defp get_by_ap_id(ap_id) do +    q = +      from(u in User, +        select: u.id +      ) + +    Repo.get_by(q, ap_id: ap_id) +  end +    # This is copied over from Notifications to keep this stable.    defp type_from_activity(%{data: %{"type" => type}} = activity) do      case type do        "Follow" ->          accepted_function = fn activity -> -          with %User{} = follower <- User.get_by_ap_id(activity.data["actor"]), -               %User{} = followed <- User.get_by_ap_id(activity.data["object"]) do +          with %User{} = follower <- get_by_ap_id(activity.data["actor"]), +               %User{} = followed <- get_by_ap_id(activity.data["object"]) do              Pleroma.FollowingRelationship.following?(follower, followed)            end          end diff --git a/lib/pleroma/notification.ex b/lib/pleroma/notification.ex index 9ee9606be..0b171563b 100644 --- a/lib/pleroma/notification.ex +++ b/lib/pleroma/notification.ex @@ -130,6 +130,7 @@ defmodule Pleroma.Notification do      |> preload([n, a, o], activity: {a, object: o})      |> exclude_notification_muted(user, exclude_notification_muted_opts)      |> exclude_blocked(user, exclude_blocked_opts) +    |> exclude_filtered(user)      |> exclude_visibility(opts)    end @@ -158,6 +159,20 @@ defmodule Pleroma.Notification do      |> where([n, a, o, tm], is_nil(tm.user_id))    end +  defp exclude_filtered(query, user) do +    case Pleroma.Filter.compose_regex(user) do +      nil -> +        query + +      regex -> +        from([_n, a, o] in query, +          where: +            fragment("not(?->>'content' ~* ?)", o.data, ^regex) or +              fragment("?->>'actor' = ?", o.data, ^user.ap_id) +        ) +    end +  end +    @valid_visibilities ~w[direct unlisted public private]    defp exclude_visibility(query, %{exclude_visibilities: visibility}) @@ -337,6 +352,7 @@ defmodule Pleroma.Notification do      end    end +  @spec create_notifications(Activity.t(), keyword()) :: {:ok, [Notification.t()] | []}    def create_notifications(activity, options \\ [])    def create_notifications(%Activity{data: %{"to" => _, "type" => "Create"}} = activity, options) do @@ -367,6 +383,7 @@ defmodule Pleroma.Notification do          do_send = do_send && user in enabled_receivers          create_notification(activity, user, do_send)        end) +      |> Enum.reject(&is_nil/1)      {:ok, notifications}    end @@ -480,6 +497,10 @@ defmodule Pleroma.Notification do      end    end +  def get_potential_receiver_ap_ids(%{data: %{"type" => "Follow", "object" => object_id}}) do +    [object_id] +  end +    def get_potential_receiver_ap_ids(activity) do      []      |> Utils.maybe_notify_to_recipients(activity) @@ -550,11 +571,9 @@ defmodule Pleroma.Notification do      [        :self,        :invisible, -      :followers, -      :follows, -      :non_followers, -      :non_follows, -      :recently_followed +      :block_from_strangers, +      :recently_followed, +      :filtered      ]      |> Enum.find(&skip?(&1, activity, user))    end @@ -573,45 +592,15 @@ defmodule Pleroma.Notification do    end    def skip?( -        :followers, +        :block_from_strangers,          %Activity{} = activity, -        %User{notification_settings: %{followers: false}} = user -      ) do -    actor = activity.data["actor"] -    follower = User.get_cached_by_ap_id(actor) -    User.following?(follower, user) -  end - -  def skip?( -        :non_followers, -        %Activity{} = activity, -        %User{notification_settings: %{non_followers: false}} = user +        %User{notification_settings: %{block_from_strangers: true}} = user        ) do      actor = activity.data["actor"]      follower = User.get_cached_by_ap_id(actor)      !User.following?(follower, user)    end -  def skip?( -        :follows, -        %Activity{} = activity, -        %User{notification_settings: %{follows: false}} = user -      ) do -    actor = activity.data["actor"] -    followed = User.get_cached_by_ap_id(actor) -    User.following?(user, followed) -  end - -  def skip?( -        :non_follows, -        %Activity{} = activity, -        %User{notification_settings: %{non_follows: false}} = user -      ) do -    actor = activity.data["actor"] -    followed = User.get_cached_by_ap_id(actor) -    !User.following?(user, followed) -  end -    # To do: consider defining recency in hours and checking FollowingRelationship with a single SQL    def skip?(:recently_followed, %Activity{data: %{"type" => "Follow"}} = activity, %User{} = user) do      actor = activity.data["actor"] @@ -623,6 +612,26 @@ defmodule Pleroma.Notification do      end)    end +  def skip?(:filtered, %{data: %{"type" => type}}, _) when type in ["Follow", "Move"], do: false + +  def skip?(:filtered, activity, user) do +    object = Object.normalize(activity) + +    cond do +      is_nil(object) -> +        false + +      object.data["actor"] == user.ap_id -> +        false + +      not is_nil(regex = Pleroma.Filter.compose_regex(user, :re)) -> +        Regex.match?(regex, object.data["content"]) + +      true -> +        false +    end +  end +    def skip?(_, _, _), do: false    def for_user_and_activity(user, activity) do diff --git a/lib/pleroma/object/fetcher.ex b/lib/pleroma/object/fetcher.ex index 263ded5dd..3e2949ee2 100644 --- a/lib/pleroma/object/fetcher.ex +++ b/lib/pleroma/object/fetcher.ex @@ -83,8 +83,8 @@ defmodule Pleroma.Object.Fetcher do        {:transmogrifier, {:error, {:reject, nil}}} ->          {:reject, nil} -      {:transmogrifier, _} -> -        {:error, "Transmogrifier failure."} +      {:transmogrifier, _} = e -> +        {:error, e}        {:object, data, nil} ->          reinject_object(%Object{}, data) diff --git a/lib/pleroma/plugs/admin_secret_authentication_plug.ex b/lib/pleroma/plugs/admin_secret_authentication_plug.ex index b4b47a31f..2e54df47a 100644 --- a/lib/pleroma/plugs/admin_secret_authentication_plug.ex +++ b/lib/pleroma/plugs/admin_secret_authentication_plug.ex @@ -4,6 +4,9 @@  defmodule Pleroma.Plugs.AdminSecretAuthenticationPlug do    import Plug.Conn + +  alias Pleroma.Plugs.OAuthScopesPlug +  alias Pleroma.Plugs.RateLimiter    alias Pleroma.User    def init(options) do @@ -11,7 +14,10 @@ defmodule Pleroma.Plugs.AdminSecretAuthenticationPlug do    end    def secret_token do -    Pleroma.Config.get(:admin_token) +    case Pleroma.Config.get(:admin_token) do +      blank when blank in [nil, ""] -> nil +      token -> token +    end    end    def call(%{assigns: %{user: %User{}}} = conn, _), do: conn @@ -26,9 +32,9 @@ defmodule Pleroma.Plugs.AdminSecretAuthenticationPlug do    def authenticate(%{params: %{"admin_token" => admin_token}} = conn) do      if admin_token == secret_token() do -      assign(conn, :user, %User{is_admin: true}) +      assign_admin_user(conn)      else -      conn +      handle_bad_token(conn)      end    end @@ -36,8 +42,19 @@ defmodule Pleroma.Plugs.AdminSecretAuthenticationPlug do      token = secret_token()      case get_req_header(conn, "x-admin-token") do -      [^token] -> assign(conn, :user, %User{is_admin: true}) -      _ -> conn +      blank when blank in [[], [""]] -> conn +      [^token] -> assign_admin_user(conn) +      _ -> handle_bad_token(conn)      end    end + +  defp assign_admin_user(conn) do +    conn +    |> assign(:user, %User{is_admin: true}) +    |> OAuthScopesPlug.skip_plug() +  end + +  defp handle_bad_token(conn) do +    RateLimiter.call(conn, name: :authentication) +  end  end diff --git a/lib/pleroma/plugs/http_security_plug.ex b/lib/pleroma/plugs/http_security_plug.ex index 1420a9611..c363b193b 100644 --- a/lib/pleroma/plugs/http_security_plug.ex +++ b/lib/pleroma/plugs/http_security_plug.ex @@ -69,10 +69,11 @@ defmodule Pleroma.Plugs.HTTPSecurityPlug do      img_src = "img-src 'self' data: blob:"      media_src = "media-src 'self'" +    # Strict multimedia CSP enforcement only when MediaProxy is enabled      {img_src, media_src} =        if Config.get([:media_proxy, :enabled]) &&             !Config.get([:media_proxy, :proxy_opts, :redirect_on_failure]) do -        sources = get_proxy_and_attachment_sources() +        sources = build_csp_multimedia_source_list()          {[img_src, sources], [media_src, sources]}        else          {[img_src, " https:"], [media_src, " https:"]} @@ -81,14 +82,14 @@ defmodule Pleroma.Plugs.HTTPSecurityPlug do      connect_src = ["connect-src 'self' blob: ", static_url, ?\s, websocket_url]      connect_src = -      if Pleroma.Config.get(:env) == :dev do +      if Config.get(:env) == :dev do          [connect_src, " http://localhost:3035/"]        else          connect_src        end      script_src = -      if Pleroma.Config.get(:env) == :dev do +      if Config.get(:env) == :dev do          "script-src 'self' 'unsafe-eval'"        else          "script-src 'self'" @@ -107,38 +108,64 @@ defmodule Pleroma.Plugs.HTTPSecurityPlug do      |> :erlang.iolist_to_binary()    end -  defp get_proxy_and_attachment_sources do +  defp build_csp_from_whitelist([], acc), do: acc + +  defp build_csp_from_whitelist([last], acc) do +    [build_csp_param_from_whitelist(last) | acc] +  end + +  defp build_csp_from_whitelist([head | tail], acc) do +    build_csp_from_whitelist(tail, [[?\s, build_csp_param_from_whitelist(head)] | acc]) +  end + +  # TODO: use `build_csp_param/1` after removing support bare domains for media proxy whitelist +  defp build_csp_param_from_whitelist("http" <> _ = url) do +    build_csp_param(url) +  end + +  defp build_csp_param_from_whitelist(url), do: url + +  defp build_csp_multimedia_source_list do      media_proxy_whitelist = -      Enum.reduce(Config.get([:media_proxy, :whitelist]), [], fn host, acc -> -        add_source(acc, host) -      end) - -    media_proxy_base_url = -      if Config.get([:media_proxy, :base_url]), -        do: URI.parse(Config.get([:media_proxy, :base_url])).host - -    upload_base_url = -      if Config.get([Pleroma.Upload, :base_url]), -        do: URI.parse(Config.get([Pleroma.Upload, :base_url])).host - -    s3_endpoint = -      if Config.get([Pleroma.Upload, :uploader]) == Pleroma.Uploaders.S3, -        do: URI.parse(Config.get([Pleroma.Uploaders.S3, :public_endpoint])).host - -    [] -    |> add_source(media_proxy_base_url) -    |> add_source(upload_base_url) -    |> add_source(s3_endpoint) +      [:media_proxy, :whitelist] +      |> Config.get() +      |> build_csp_from_whitelist([]) + +    captcha_method = Config.get([Pleroma.Captcha, :method]) +    captcha_endpoint = Config.get([captcha_method, :endpoint]) + +    base_endpoints = +      [ +        [:media_proxy, :base_url], +        [Pleroma.Upload, :base_url], +        [Pleroma.Uploaders.S3, :public_endpoint] +      ] +      |> Enum.map(&Config.get/1) + +    [captcha_endpoint | base_endpoints] +    |> Enum.map(&build_csp_param/1) +    |> Enum.reduce([], &add_source(&2, &1))      |> add_source(media_proxy_whitelist)    end    defp add_source(iodata, nil), do: iodata +  defp add_source(iodata, []), do: iodata    defp add_source(iodata, source), do: [[?\s, source] | iodata]    defp add_csp_param(csp_iodata, nil), do: csp_iodata    defp add_csp_param(csp_iodata, param), do: [[param, ?;] | csp_iodata] +  defp build_csp_param(nil), do: nil + +  defp build_csp_param(url) when is_binary(url) do +    %{host: host, scheme: scheme} = URI.parse(url) + +    if scheme do +      [scheme, "://", host] +    end +  end +    def warn_if_disabled do      unless Config.get([:http_security, :enabled]) do        Logger.warn(" diff --git a/lib/pleroma/plugs/static_fe_plug.ex b/lib/pleroma/plugs/static_fe_plug.ex index 156e6788e..143665c71 100644 --- a/lib/pleroma/plugs/static_fe_plug.ex +++ b/lib/pleroma/plugs/static_fe_plug.ex @@ -9,7 +9,7 @@ defmodule Pleroma.Plugs.StaticFEPlug do    def init(options), do: options    def call(conn, _) do -    if enabled?() and accepts_html?(conn) do +    if enabled?() and requires_html?(conn) do        conn        |> StaticFEController.call(:show)        |> halt() @@ -20,10 +20,7 @@ defmodule Pleroma.Plugs.StaticFEPlug do    defp enabled?, do: Pleroma.Config.get([:static_fe, :enabled], false) -  defp accepts_html?(conn) do -    case get_req_header(conn, "accept") do -      [accept | _] -> String.contains?(accept, "text/html") -      _ -> false -    end +  defp requires_html?(conn) do +    Phoenix.Controller.get_format(conn) == "html"    end  end diff --git a/lib/pleroma/plugs/user_is_admin_plug.ex b/lib/pleroma/plugs/user_is_admin_plug.ex index 2748102df..488a61d1d 100644 --- a/lib/pleroma/plugs/user_is_admin_plug.ex +++ b/lib/pleroma/plugs/user_is_admin_plug.ex @@ -7,37 +7,18 @@ defmodule Pleroma.Plugs.UserIsAdminPlug do    import Plug.Conn    alias Pleroma.User -  alias Pleroma.Web.OAuth    def init(options) do      options    end -  def call(%{assigns: %{user: %User{is_admin: true}} = assigns} = conn, _) do -    token = assigns[:token] - -    cond do -      not Pleroma.Config.enforce_oauth_admin_scope_usage?() -> -        conn - -      token && OAuth.Scopes.contains_admin_scopes?(token.scopes) -> -        # Note: checking for _any_ admin scope presence, not necessarily fitting requested action. -        #   Thus, controller must explicitly invoke OAuthScopesPlug to verify scope requirements. -        #   Admin might opt out of admin scope for some apps to block any admin actions from them. -        conn - -      true -> -        fail(conn) -    end +  def call(%{assigns: %{user: %User{is_admin: true}}} = conn, _) do +    conn    end    def call(conn, _) do -    fail(conn) -  end - -  defp fail(conn) do      conn -    |> render_error(:forbidden, "User is not an admin or OAuth admin scope is not granted.") +    |> render_error(:forbidden, "User is not an admin.")      |> halt()    end  end diff --git a/lib/pleroma/pool/connections.ex b/lib/pleroma/pool/connections.ex deleted file mode 100644 index acafe1bea..000000000 --- a/lib/pleroma/pool/connections.ex +++ /dev/null @@ -1,283 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Pool.Connections do -  use GenServer - -  alias Pleroma.Config -  alias Pleroma.Gun - -  require Logger - -  @type domain :: String.t() -  @type conn :: Pleroma.Gun.Conn.t() - -  @type t :: %__MODULE__{ -          conns: %{domain() => conn()}, -          opts: keyword() -        } - -  defstruct conns: %{}, opts: [] - -  @spec start_link({atom(), keyword()}) :: {:ok, pid()} -  def start_link({name, opts}) do -    GenServer.start_link(__MODULE__, opts, name: name) -  end - -  @impl true -  def init(opts), do: {:ok, %__MODULE__{conns: %{}, opts: opts}} - -  @spec checkin(String.t() | URI.t(), atom()) :: pid() | nil -  def checkin(url, name) -  def checkin(url, name) when is_binary(url), do: checkin(URI.parse(url), name) - -  def checkin(%URI{} = uri, name) do -    timeout = Config.get([:connections_pool, :checkin_timeout], 250) - -    GenServer.call(name, {:checkin, uri}, timeout) -  end - -  @spec alive?(atom()) :: boolean() -  def alive?(name) do -    if pid = Process.whereis(name) do -      Process.alive?(pid) -    else -      false -    end -  end - -  @spec get_state(atom()) :: t() -  def get_state(name) do -    GenServer.call(name, :state) -  end - -  @spec count(atom()) :: pos_integer() -  def count(name) do -    GenServer.call(name, :count) -  end - -  @spec get_unused_conns(atom()) :: [{domain(), conn()}] -  def get_unused_conns(name) do -    GenServer.call(name, :unused_conns) -  end - -  @spec checkout(pid(), pid(), atom()) :: :ok -  def checkout(conn, pid, name) do -    GenServer.cast(name, {:checkout, conn, pid}) -  end - -  @spec add_conn(atom(), String.t(), Pleroma.Gun.Conn.t()) :: :ok -  def add_conn(name, key, conn) do -    GenServer.cast(name, {:add_conn, key, conn}) -  end - -  @spec remove_conn(atom(), String.t()) :: :ok -  def remove_conn(name, key) do -    GenServer.cast(name, {:remove_conn, key}) -  end - -  @impl true -  def handle_cast({:add_conn, key, conn}, state) do -    state = put_in(state.conns[key], conn) - -    Process.monitor(conn.conn) -    {:noreply, state} -  end - -  @impl true -  def handle_cast({:checkout, conn_pid, pid}, state) do -    state = -      with true <- Process.alive?(conn_pid), -           {key, conn} <- find_conn(state.conns, conn_pid), -           used_by <- List.keydelete(conn.used_by, pid, 0) do -        conn_state = if used_by == [], do: :idle, else: conn.conn_state - -        put_in(state.conns[key], %{conn | conn_state: conn_state, used_by: used_by}) -      else -        false -> -          Logger.debug("checkout for closed conn #{inspect(conn_pid)}") -          state - -        nil -> -          Logger.debug("checkout for alive conn #{inspect(conn_pid)}, but is not in state") -          state -      end - -    {:noreply, state} -  end - -  @impl true -  def handle_cast({:remove_conn, key}, state) do -    state = put_in(state.conns, Map.delete(state.conns, key)) -    {:noreply, state} -  end - -  @impl true -  def handle_call({:checkin, uri}, from, state) do -    key = "#{uri.scheme}:#{uri.host}:#{uri.port}" - -    case state.conns[key] do -      %{conn: pid, gun_state: :up} = conn -> -        time = :os.system_time(:second) -        last_reference = time - conn.last_reference -        crf = crf(last_reference, 100, conn.crf) - -        state = -          put_in(state.conns[key], %{ -            conn -            | last_reference: time, -              crf: crf, -              conn_state: :active, -              used_by: [from | conn.used_by] -          }) - -        {:reply, pid, state} - -      %{gun_state: :down} -> -        {:reply, nil, state} - -      nil -> -        {:reply, nil, state} -    end -  end - -  @impl true -  def handle_call(:state, _from, state), do: {:reply, state, state} - -  @impl true -  def handle_call(:count, _from, state) do -    {:reply, Enum.count(state.conns), state} -  end - -  @impl true -  def handle_call(:unused_conns, _from, state) do -    unused_conns = -      state.conns -      |> Enum.filter(&filter_conns/1) -      |> Enum.sort(&sort_conns/2) - -    {:reply, unused_conns, state} -  end - -  defp filter_conns({_, %{conn_state: :idle, used_by: []}}), do: true -  defp filter_conns(_), do: false - -  defp sort_conns({_, c1}, {_, c2}) do -    c1.crf <= c2.crf and c1.last_reference <= c2.last_reference -  end - -  @impl true -  def handle_info({:gun_up, conn_pid, _protocol}, state) do -    %{origin_host: host, origin_scheme: scheme, origin_port: port} = Gun.info(conn_pid) - -    host = -      case :inet.ntoa(host) do -        {:error, :einval} -> host -        ip -> ip -      end - -    key = "#{scheme}:#{host}:#{port}" - -    state = -      with {key, conn} <- find_conn(state.conns, conn_pid, key), -           {true, key} <- {Process.alive?(conn_pid), key} do -        put_in(state.conns[key], %{ -          conn -          | gun_state: :up, -            conn_state: :active, -            retries: 0 -        }) -      else -        {false, key} -> -          put_in( -            state.conns, -            Map.delete(state.conns, key) -          ) - -        nil -> -          :ok = Gun.close(conn_pid) - -          state -      end - -    {:noreply, state} -  end - -  @impl true -  def handle_info({:gun_down, conn_pid, _protocol, _reason, _killed}, state) do -    retries = Config.get([:connections_pool, :retry], 1) -    # we can't get info on this pid, because pid is dead -    state = -      with {key, conn} <- find_conn(state.conns, conn_pid), -           {true, key} <- {Process.alive?(conn_pid), key} do -        if conn.retries == retries do -          :ok = Gun.close(conn.conn) - -          put_in( -            state.conns, -            Map.delete(state.conns, key) -          ) -        else -          put_in(state.conns[key], %{ -            conn -            | gun_state: :down, -              retries: conn.retries + 1 -          }) -        end -      else -        {false, key} -> -          put_in( -            state.conns, -            Map.delete(state.conns, key) -          ) - -        nil -> -          Logger.debug(":gun_down for conn which isn't found in state") - -          state -      end - -    {:noreply, state} -  end - -  @impl true -  def handle_info({:DOWN, _ref, :process, conn_pid, reason}, state) do -    Logger.debug("received DOWN message for #{inspect(conn_pid)} reason -> #{inspect(reason)}") - -    state = -      with {key, conn} <- find_conn(state.conns, conn_pid) do -        Enum.each(conn.used_by, fn {pid, _ref} -> -          Process.exit(pid, reason) -        end) - -        put_in( -          state.conns, -          Map.delete(state.conns, key) -        ) -      else -        nil -> -          Logger.debug(":DOWN for conn which isn't found in state") - -          state -      end - -    {:noreply, state} -  end - -  defp find_conn(conns, conn_pid) do -    Enum.find(conns, fn {_key, conn} -> -      conn.conn == conn_pid -    end) -  end - -  defp find_conn(conns, conn_pid, conn_key) do -    Enum.find(conns, fn {key, conn} -> -      key == conn_key and conn.conn == conn_pid -    end) -  end - -  def crf(current, steps, crf) do -    1 + :math.pow(0.5, current / steps) * crf -  end -end diff --git a/lib/pleroma/pool/pool.ex b/lib/pleroma/pool/pool.ex deleted file mode 100644 index 21a6fbbc5..000000000 --- a/lib/pleroma/pool/pool.ex +++ /dev/null @@ -1,22 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Pool do -  def child_spec(opts) do -    poolboy_opts = -      opts -      |> Keyword.put(:worker_module, Pleroma.Pool.Request) -      |> Keyword.put(:name, {:local, opts[:name]}) -      |> Keyword.put(:size, opts[:size]) -      |> Keyword.put(:max_overflow, opts[:max_overflow]) - -    %{ -      id: opts[:id] || {__MODULE__, make_ref()}, -      start: {:poolboy, :start_link, [poolboy_opts, [name: opts[:name]]]}, -      restart: :permanent, -      shutdown: 5000, -      type: :worker -    } -  end -end diff --git a/lib/pleroma/pool/request.ex b/lib/pleroma/pool/request.ex deleted file mode 100644 index 3fb930db7..000000000 --- a/lib/pleroma/pool/request.ex +++ /dev/null @@ -1,65 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Pool.Request do -  use GenServer - -  require Logger - -  def start_link(args) do -    GenServer.start_link(__MODULE__, args) -  end - -  @impl true -  def init(_), do: {:ok, []} - -  @spec execute(pid() | atom(), Tesla.Client.t(), keyword(), pos_integer()) :: -          {:ok, Tesla.Env.t()} | {:error, any()} -  def execute(pid, client, request, timeout) do -    GenServer.call(pid, {:execute, client, request}, timeout) -  end - -  @impl true -  def handle_call({:execute, client, request}, _from, state) do -    response = Pleroma.HTTP.request(client, request) - -    {:reply, response, state} -  end - -  @impl true -  def handle_info({:gun_data, _conn, _stream, _, _}, state) do -    {:noreply, state} -  end - -  @impl true -  def handle_info({:gun_up, _conn, _protocol}, state) do -    {:noreply, state} -  end - -  @impl true -  def handle_info({:gun_down, _conn, _protocol, _reason, _killed}, state) do -    {:noreply, state} -  end - -  @impl true -  def handle_info({:gun_error, _conn, _stream, _error}, state) do -    {:noreply, state} -  end - -  @impl true -  def handle_info({:gun_push, _conn, _stream, _new_stream, _method, _uri, _headers}, state) do -    {:noreply, state} -  end - -  @impl true -  def handle_info({:gun_response, _conn, _stream, _, _status, _headers}, state) do -    {:noreply, state} -  end - -  @impl true -  def handle_info(msg, state) do -    Logger.warn("Received unexpected message #{inspect(__MODULE__)} #{inspect(msg)}") -    {:noreply, state} -  end -end diff --git a/lib/pleroma/pool/supervisor.ex b/lib/pleroma/pool/supervisor.ex deleted file mode 100644 index faf646cb2..000000000 --- a/lib/pleroma/pool/supervisor.ex +++ /dev/null @@ -1,42 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Pool.Supervisor do -  use Supervisor - -  alias Pleroma.Config -  alias Pleroma.Pool - -  def start_link(args) do -    Supervisor.start_link(__MODULE__, args, name: __MODULE__) -  end - -  def init(_) do -    conns_child = %{ -      id: Pool.Connections, -      start: -        {Pool.Connections, :start_link, [{:gun_connections, Config.get([:connections_pool])}]} -    } - -    Supervisor.init([conns_child | pools()], strategy: :one_for_one) -  end - -  defp pools do -    pools = Config.get(:pools) - -    pools = -      if Config.get([Pleroma.Upload, :proxy_remote]) == false do -        Keyword.delete(pools, :upload) -      else -        pools -      end - -    for {pool_name, pool_opts} <- pools do -      pool_opts -      |> Keyword.put(:id, {Pool, pool_name}) -      |> Keyword.put(:name, pool_name) -      |> Pool.child_spec() -    end -  end -end diff --git a/lib/pleroma/reverse_proxy/client/tesla.ex b/lib/pleroma/reverse_proxy/client/tesla.ex index e81ea8bde..65785445d 100644 --- a/lib/pleroma/reverse_proxy/client/tesla.ex +++ b/lib/pleroma/reverse_proxy/client/tesla.ex @@ -48,7 +48,7 @@ defmodule Pleroma.ReverseProxy.Client.Tesla do      # if there were redirects we need to checkout old conn      conn = opts[:old_conn] || opts[:conn] -    if conn, do: :ok = Pleroma.Pool.Connections.checkout(conn, self(), :gun_connections) +    if conn, do: :ok = Pleroma.Gun.ConnectionPool.release_conn(conn)      :done    end diff --git a/lib/pleroma/reverse_proxy/reverse_proxy.ex b/lib/pleroma/reverse_proxy/reverse_proxy.ex index 4bbeb493c..28ad4c846 100644 --- a/lib/pleroma/reverse_proxy/reverse_proxy.ex +++ b/lib/pleroma/reverse_proxy/reverse_proxy.ex @@ -3,12 +3,13 @@  # SPDX-License-Identifier: AGPL-3.0-only  defmodule Pleroma.ReverseProxy do +  @range_headers ~w(range if-range)    @keep_req_headers ~w(accept user-agent accept-encoding cache-control if-modified-since) ++ -                      ~w(if-unmodified-since if-none-match if-range range) +                      ~w(if-unmodified-since if-none-match) ++ @range_headers    @resp_cache_headers ~w(etag date last-modified)    @keep_resp_headers @resp_cache_headers ++ -                       ~w(content-type content-disposition content-encoding content-range) ++ -                       ~w(accept-ranges vary) +                       ~w(content-length content-type content-disposition content-encoding) ++ +                       ~w(content-range accept-ranges vary)    @default_cache_control_header "public, max-age=1209600"    @valid_resp_codes [200, 206, 304]    @max_read_duration :timer.seconds(30) @@ -170,6 +171,8 @@ defmodule Pleroma.ReverseProxy do    end    defp response(conn, client, url, status, headers, opts) do +    Logger.debug("#{__MODULE__} #{status} #{url} #{inspect(headers)}") +      result =        conn        |> put_resp_headers(build_resp_headers(headers, opts)) @@ -220,7 +223,9 @@ defmodule Pleroma.ReverseProxy do      end    end -  defp head_response(conn, _url, code, headers, opts) do +  defp head_response(conn, url, code, headers, opts) do +    Logger.debug("#{__MODULE__} #{code} #{url} #{inspect(headers)}") +      conn      |> put_resp_headers(build_resp_headers(headers, opts))      |> send_resp(code, "") @@ -262,20 +267,33 @@ defmodule Pleroma.ReverseProxy do      headers      |> downcase_headers()      |> Enum.filter(fn {k, _} -> k in @keep_req_headers end) -    |> (fn headers -> -          headers = headers ++ Keyword.get(opts, :req_headers, []) - -          if Keyword.get(opts, :keep_user_agent, false) do -            List.keystore( -              headers, -              "user-agent", -              0, -              {"user-agent", Pleroma.Application.user_agent()} -            ) -          else -            headers -          end -        end).() +    |> build_req_range_or_encoding_header(opts) +    |> build_req_user_agent_header(opts) +    |> Keyword.merge(Keyword.get(opts, :req_headers, [])) +  end + +  # Disable content-encoding if any @range_headers are requested (see #1823). +  defp build_req_range_or_encoding_header(headers, _opts) do +    range? = Enum.any?(headers, fn {header, _} -> Enum.member?(@range_headers, header) end) + +    if range? && List.keymember?(headers, "accept-encoding", 0) do +      List.keydelete(headers, "accept-encoding", 0) +    else +      headers +    end +  end + +  defp build_req_user_agent_header(headers, opts) do +    if Keyword.get(opts, :keep_user_agent, false) do +      List.keystore( +        headers, +        "user-agent", +        0, +        {"user-agent", Pleroma.Application.user_agent()} +      ) +    else +      headers +    end    end    defp build_resp_headers(headers, opts) do @@ -283,7 +301,7 @@ defmodule Pleroma.ReverseProxy do      |> Enum.filter(fn {k, _} -> k in @keep_resp_headers end)      |> build_resp_cache_headers(opts)      |> build_resp_content_disposition_header(opts) -    |> (fn headers -> headers ++ Keyword.get(opts, :resp_headers, []) end).() +    |> Keyword.merge(Keyword.get(opts, :resp_headers, []))    end    defp build_resp_cache_headers(headers, _opts) do diff --git a/lib/pleroma/telemetry/logger.ex b/lib/pleroma/telemetry/logger.ex new file mode 100644 index 000000000..4cacae02f --- /dev/null +++ b/lib/pleroma/telemetry/logger.ex @@ -0,0 +1,76 @@ +defmodule Pleroma.Telemetry.Logger do +  @moduledoc "Transforms Pleroma telemetry events to logs" + +  require Logger + +  @events [ +    [:pleroma, :connection_pool, :reclaim, :start], +    [:pleroma, :connection_pool, :reclaim, :stop], +    [:pleroma, :connection_pool, :provision_failure], +    [:pleroma, :connection_pool, :client_death] +  ] +  def attach do +    :telemetry.attach_many("pleroma-logger", @events, &handle_event/4, []) +  end + +  # Passing anonymous functions instead of strings to logger is intentional, +  # that way strings won't be concatenated if the message is going to be thrown +  # out anyway due to higher log level configured + +  def handle_event( +        [:pleroma, :connection_pool, :reclaim, :start], +        _, +        %{max_connections: max_connections, reclaim_max: reclaim_max}, +        _ +      ) do +    Logger.debug(fn -> +      "Connection pool is exhausted (reached #{max_connections} connections). Starting idle connection cleanup to reclaim as much as #{ +        reclaim_max +      } connections" +    end) +  end + +  def handle_event( +        [:pleroma, :connection_pool, :reclaim, :stop], +        %{reclaimed_count: 0}, +        _, +        _ +      ) do +    Logger.error(fn -> +      "Connection pool failed to reclaim any connections due to all of them being in use. It will have to drop requests for opening connections to new hosts" +    end) +  end + +  def handle_event( +        [:pleroma, :connection_pool, :reclaim, :stop], +        %{reclaimed_count: reclaimed_count}, +        _, +        _ +      ) do +    Logger.debug(fn -> "Connection pool cleaned up #{reclaimed_count} idle connections" end) +  end + +  def handle_event( +        [:pleroma, :connection_pool, :provision_failure], +        %{opts: [key | _]}, +        _, +        _ +      ) do +    Logger.error(fn -> +      "Connection pool had to refuse opening a connection to #{key} due to connection limit exhaustion" +    end) +  end + +  def handle_event( +        [:pleroma, :connection_pool, :client_death], +        %{client_pid: client_pid, reason: reason}, +        %{key: key}, +        _ +      ) do +    Logger.warn(fn -> +      "Pool worker for #{key}: Client #{inspect(client_pid)} died before releasing the connection with #{ +        inspect(reason) +      }" +    end) +  end +end diff --git a/lib/pleroma/tesla/middleware/follow_redirects.ex b/lib/pleroma/tesla/middleware/follow_redirects.ex new file mode 100644 index 000000000..5a7032215 --- /dev/null +++ b/lib/pleroma/tesla/middleware/follow_redirects.ex @@ -0,0 +1,110 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2015-2020 Tymon Tobolski <https://github.com/teamon/tesla/blob/master/lib/tesla/middleware/follow_redirects.ex> +# Copyright © 2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.HTTP.Middleware.FollowRedirects do +  @moduledoc """ +  Pool-aware version of https://github.com/teamon/tesla/blob/master/lib/tesla/middleware/follow_redirects.ex + +  Follow 3xx redirects +  ## Options +  - `:max_redirects` - limit number of redirects (default: `5`) +  """ + +  alias Pleroma.Gun.ConnectionPool + +  @behaviour Tesla.Middleware + +  @max_redirects 5 +  @redirect_statuses [301, 302, 303, 307, 308] + +  @impl Tesla.Middleware +  def call(env, next, opts \\ []) do +    max = Keyword.get(opts, :max_redirects, @max_redirects) + +    redirect(env, next, max) +  end + +  defp redirect(env, next, left) do +    opts = env.opts[:adapter] + +    case Tesla.run(env, next) do +      {:ok, %{status: status} = res} when status in @redirect_statuses and left > 0 -> +        release_conn(opts) + +        case Tesla.get_header(res, "location") do +          nil -> +            {:ok, res} + +          location -> +            location = parse_location(location, res) + +            case get_conn(location, opts) do +              {:ok, opts} -> +                %{env | opts: Keyword.put(env.opts, :adapter, opts)} +                |> new_request(res.status, location) +                |> redirect(next, left - 1) + +              e -> +                e +            end +        end + +      {:ok, %{status: status}} when status in @redirect_statuses -> +        release_conn(opts) +        {:error, {__MODULE__, :too_many_redirects}} + +      {:error, _} = e -> +        release_conn(opts) +        e + +      other -> +        unless opts[:body_as] == :chunks do +          release_conn(opts) +        end + +        other +    end +  end + +  defp get_conn(location, opts) do +    uri = URI.parse(location) + +    case ConnectionPool.get_conn(uri, opts) do +      {:ok, conn} -> +        {:ok, Keyword.merge(opts, conn: conn)} + +      e -> +        e +    end +  end + +  defp release_conn(opts) do +    ConnectionPool.release_conn(opts[:conn]) +  end + +  # The 303 (See Other) redirect was added in HTTP/1.1 to indicate that the originally +  # requested resource is not available, however a related resource (or another redirect) +  # available via GET is available at the specified location. +  # https://tools.ietf.org/html/rfc7231#section-6.4.4 +  defp new_request(env, 303, location), do: %{env | url: location, method: :get, query: []} + +  # The 307 (Temporary Redirect) status code indicates that the target +  # resource resides temporarily under a different URI and the user agent +  # MUST NOT change the request method (...) +  # https://tools.ietf.org/html/rfc7231#section-6.4.7 +  defp new_request(env, 307, location), do: %{env | url: location} + +  defp new_request(env, _, location), do: %{env | url: location, query: []} + +  defp parse_location("https://" <> _rest = location, _env), do: location +  defp parse_location("http://" <> _rest = location, _env), do: location + +  defp parse_location(location, env) do +    env.url +    |> URI.parse() +    |> URI.merge(location) +    |> URI.to_string() +  end +end diff --git a/lib/pleroma/upload.ex b/lib/pleroma/upload.ex index 797555bff..0fa6b89dc 100644 --- a/lib/pleroma/upload.ex +++ b/lib/pleroma/upload.ex @@ -63,6 +63,10 @@ defmodule Pleroma.Upload do      with {:ok, upload} <- prepare_upload(upload, opts),           upload = %__MODULE__{upload | path: upload.path || "#{upload.id}/#{upload.name}"},           {:ok, upload} <- Pleroma.Upload.Filter.filter(opts.filters, upload), +         description = Map.get(opts, :description) || upload.name, +         {_, true} <- +           {:description_limit, +            String.length(description) <= Pleroma.Config.get([:instance, :description_limit])},           {:ok, url_spec} <- Pleroma.Uploaders.Uploader.put_file(opts.uploader, upload) do        {:ok,         %{ @@ -75,9 +79,12 @@ defmodule Pleroma.Upload do               "href" => url_from_spec(upload, opts.base_url, url_spec)             }           ], -         "name" => Map.get(opts, :description) || upload.name +         "name" => description         }}      else +      {:description_limit, _} -> +        {:error, :description_too_long} +        {:error, error} ->          Logger.error(            "#{__MODULE__} store (using #{inspect(opts.uploader)}) failed: #{inspect(error)}" diff --git a/lib/pleroma/upload/filter/exiftool.ex b/lib/pleroma/upload/filter/exiftool.ex new file mode 100644 index 000000000..c7fb6aefa --- /dev/null +++ b/lib/pleroma/upload/filter/exiftool.ex @@ -0,0 +1,18 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Upload.Filter.Exiftool do +  @moduledoc """ +  Strips GPS related EXIF tags and overwrites the file in place. +  Also strips or replaces filesystem metadata e.g., timestamps. +  """ +  @behaviour Pleroma.Upload.Filter + +  def filter(%Pleroma.Upload{tempfile: file, content_type: "image" <> _}) do +    System.cmd("exiftool", ["-overwrite_original", "-gps:all=", file], parallelism: true) +    :ok +  end + +  def filter(_), do: :ok +end diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 1d70a37ef..9240e912d 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -89,7 +89,7 @@ defmodule Pleroma.User do      field(:keys, :string)      field(:public_key, :string)      field(:ap_id, :string) -    field(:avatar, :map) +    field(:avatar, :map, default: %{})      field(:local, :boolean, default: true)      field(:follower_address, :string)      field(:following_address, :string) @@ -115,7 +115,7 @@ defmodule Pleroma.User do      field(:is_moderator, :boolean, default: false)      field(:is_admin, :boolean, default: false)      field(:show_role, :boolean, default: true) -    field(:settings, :map, default: nil) +    field(:mastofe_settings, :map, default: nil)      field(:uri, ObjectValidators.Uri, default: nil)      field(:hide_followers_count, :boolean, default: false)      field(:hide_follows_count, :boolean, default: false) @@ -138,6 +138,7 @@ defmodule Pleroma.User do      field(:also_known_as, {:array, :string}, default: [])      field(:inbox, :string)      field(:shared_inbox, :string) +    field(:accepts_chat_messages, :boolean, default: nil)      embeds_one(        :notification_settings, @@ -388,8 +389,8 @@ defmodule Pleroma.User do    defp fix_follower_address(params), do: params    def remote_user_changeset(struct \\ %User{local: false}, params) do -    bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000) -    name_limit = Pleroma.Config.get([:instance, :user_name_length], 100) +    bio_limit = Config.get([:instance, :user_bio_length], 5000) +    name_limit = Config.get([:instance, :user_name_length], 100)      name =        case params[:name] do @@ -436,7 +437,8 @@ defmodule Pleroma.User do          :discoverable,          :invisible,          :actor_type, -        :also_known_as +        :also_known_as, +        :accepts_chat_messages        ]      )      |> validate_required([:name, :ap_id]) @@ -448,8 +450,8 @@ defmodule Pleroma.User do    end    def update_changeset(struct, params \\ %{}) do -    bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000) -    name_limit = Pleroma.Config.get([:instance, :user_name_length], 100) +    bio_limit = Config.get([:instance, :user_bio_length], 5000) +    name_limit = Config.get([:instance, :user_name_length], 100)      struct      |> cast( @@ -481,7 +483,8 @@ defmodule Pleroma.User do          :pleroma_settings_store,          :discoverable,          :actor_type, -        :also_known_as +        :also_known_as, +        :accepts_chat_messages        ]      )      |> unique_constraint(:nickname) @@ -527,11 +530,21 @@ defmodule Pleroma.User do    end    defp put_emoji(changeset) do -    bio = get_change(changeset, :bio) -    name = get_change(changeset, :name) +    emojified_fields = [:bio, :name, :raw_fields] + +    if Enum.any?(changeset.changes, fn {k, _} -> k in emojified_fields end) do +      bio = Emoji.Formatter.get_emoji_map(get_field(changeset, :bio)) +      name = Emoji.Formatter.get_emoji_map(get_field(changeset, :name)) + +      emoji = Map.merge(bio, name) + +      emoji = +        changeset +        |> get_field(:raw_fields) +        |> Enum.reduce(emoji, fn x, acc -> +          Map.merge(acc, Emoji.Formatter.get_emoji_map(x["name"] <> x["value"])) +        end) -    if bio || name do -      emoji = Map.merge(Emoji.Formatter.get_emoji_map(bio), Emoji.Formatter.get_emoji_map(name))        put_change(changeset, :emoji, emoji)      else        changeset @@ -539,14 +552,11 @@ defmodule Pleroma.User do    end    defp put_change_if_present(changeset, map_field, value_function) do -    if value = get_change(changeset, map_field) do -      with {:ok, new_value} <- value_function.(value) do -        put_change(changeset, map_field, new_value) -      else -        _ -> changeset -      end +    with {:ok, value} <- fetch_change(changeset, map_field), +         {:ok, new_value} <- value_function.(value) do +      put_change(changeset, map_field, new_value)      else -      changeset +      _ -> changeset      end    end @@ -621,12 +631,13 @@ defmodule Pleroma.User do    def force_password_reset(user), do: update_password_reset_pending(user, true)    def register_changeset(struct, params \\ %{}, opts \\ []) do -    bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000) -    name_limit = Pleroma.Config.get([:instance, :user_name_length], 100) +    bio_limit = Config.get([:instance, :user_bio_length], 5000) +    name_limit = Config.get([:instance, :user_name_length], 100) +    params = Map.put_new(params, :accepts_chat_messages, true)      need_confirmation? =        if is_nil(opts[:need_confirmation]) do -        Pleroma.Config.get([:instance, :account_activation_required]) +        Config.get([:instance, :account_activation_required])        else          opts[:need_confirmation]        end @@ -641,13 +652,14 @@ defmodule Pleroma.User do        :nickname,        :password,        :password_confirmation, -      :emoji +      :emoji, +      :accepts_chat_messages      ])      |> validate_required([:name, :nickname, :password, :password_confirmation])      |> validate_confirmation(:password)      |> unique_constraint(:email)      |> unique_constraint(:nickname) -    |> validate_exclusion(:nickname, Pleroma.Config.get([User, :restricted_nicknames])) +    |> validate_exclusion(:nickname, Config.get([User, :restricted_nicknames]))      |> validate_format(:nickname, local_nickname_regex())      |> validate_format(:email, @email_regex)      |> validate_length(:bio, max: bio_limit) @@ -662,7 +674,7 @@ defmodule Pleroma.User do    def maybe_validate_required_email(changeset, true), do: changeset    def maybe_validate_required_email(changeset, _) do -    if Pleroma.Config.get([:instance, :account_activation_required]) do +    if Config.get([:instance, :account_activation_required]) do        validate_required(changeset, [:email])      else        changeset @@ -682,7 +694,7 @@ defmodule Pleroma.User do    end    defp autofollow_users(user) do -    candidates = Pleroma.Config.get([:instance, :autofollowed_nicknames]) +    candidates = Config.get([:instance, :autofollowed_nicknames])      autofollowed_users =        User.Query.build(%{nickname: candidates, local: true, deactivated: false}) @@ -709,7 +721,7 @@ defmodule Pleroma.User do    def try_send_confirmation_email(%User{} = user) do      if user.confirmation_pending && -         Pleroma.Config.get([:instance, :account_activation_required]) do +         Config.get([:instance, :account_activation_required]) do        user        |> Pleroma.Emails.UserEmail.account_confirmation_email()        |> Pleroma.Emails.Mailer.deliver_async() @@ -766,7 +778,7 @@ defmodule Pleroma.User do    defdelegate following(user), to: FollowingRelationship    def follow(%User{} = follower, %User{} = followed, state \\ :follow_accept) do -    deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked]) +    deny_follow_blocked = Config.get([:user, :deny_follow_blocked])      cond do        followed.deactivated -> @@ -967,7 +979,7 @@ defmodule Pleroma.User do    end    def get_cached_by_nickname_or_id(nickname_or_id, opts \\ []) do -    restrict_to_local = Pleroma.Config.get([:instance, :limit_to_local_content]) +    restrict_to_local = Config.get([:instance, :limit_to_local_content])      cond do        is_integer(nickname_or_id) or FlakeId.flake_id?(nickname_or_id) -> @@ -1163,7 +1175,7 @@ defmodule Pleroma.User do    @spec update_follower_count(User.t()) :: {:ok, User.t()}    def update_follower_count(%User{} = user) do -    if user.local or !Pleroma.Config.get([:instance, :external_user_synchronization]) do +    if user.local or !Config.get([:instance, :external_user_synchronization]) do        follower_count = FollowingRelationship.follower_count(user)        user @@ -1176,7 +1188,7 @@ defmodule Pleroma.User do    @spec update_following_count(User.t()) :: {:ok, User.t()}    def update_following_count(%User{local: false} = user) do -    if Pleroma.Config.get([:instance, :external_user_synchronization]) do +    if Config.get([:instance, :external_user_synchronization]) do        {:ok, maybe_fetch_follow_information(user)}      else        {:ok, user} @@ -1263,7 +1275,7 @@ defmodule Pleroma.User do    end    def subscribe(%User{} = subscriber, %User{} = target) do -    deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked]) +    deny_follow_blocked = Config.get([:user, :deny_follow_blocked])      if blocks?(target, subscriber) and deny_follow_blocked do        {:error, "Could not subscribe: #{target.nickname} is blocking you"} @@ -1309,7 +1321,8 @@ defmodule Pleroma.User do      unsubscribe(blocked, blocker) -    if following?(blocked, blocker), do: unfollow(blocked, blocker) +    unfollowing_blocked = Config.get([:activitypub, :unfollow_blocked], true) +    if unfollowing_blocked && following?(blocked, blocker), do: unfollow(blocked, blocker)      {:ok, blocker} = update_follower_count(blocker)      {:ok, blocker, _} = Participation.mark_all_as_read(blocker, blocked) @@ -1527,8 +1540,7 @@ defmodule Pleroma.User do        blocked_identifiers,        fn blocked_identifier ->          with {:ok, %User{} = blocked} <- get_or_fetch(blocked_identifier), -             {:ok, _user_block} <- block(blocker, blocked), -             {:ok, _} <- ActivityPub.block(blocker, blocked) do +             {:ok, _block} <- CommonAPI.block(blocker, blocked) do            blocked          else            err -> @@ -1546,7 +1558,7 @@ defmodule Pleroma.User do        fn followed_identifier ->          with {:ok, %User{} = followed} <- get_or_fetch(followed_identifier),               {:ok, follower} <- maybe_direct_follow(follower, followed), -             {:ok, _} <- ActivityPub.follow(follower, followed) do +             {:ok, _, _, _} <- CommonAPI.follow(follower, followed) do            followed          else            err -> @@ -1654,7 +1666,7 @@ defmodule Pleroma.User do      Pleroma.HTML.Scrubber.TwitterText    end -  def html_filter_policy(_), do: Pleroma.Config.get([:markup, :scrub_policy]) +  def html_filter_policy(_), do: Config.get([:markup, :scrub_policy])    def fetch_by_ap_id(ap_id), do: ActivityPub.make_user_from_ap_id(ap_id) @@ -1836,7 +1848,7 @@ defmodule Pleroma.User do    end    defp local_nickname_regex do -    if Pleroma.Config.get([:instance, :extended_nickname_format]) do +    if Config.get([:instance, :extended_nickname_format]) do        @extended_local_nickname_regex      else        @strict_local_nickname_regex @@ -1964,8 +1976,8 @@ defmodule Pleroma.User do    def get_mascot(%{mascot: mascot}) when is_nil(mascot) do      # use instance-default -    config = Pleroma.Config.get([:assets, :mascots]) -    default_mascot = Pleroma.Config.get([:assets, :default_mascot]) +    config = Config.get([:assets, :mascots]) +    default_mascot = Config.get([:assets, :default_mascot])      mascot = Keyword.get(config, default_mascot)      %{ @@ -2060,7 +2072,7 @@ defmodule Pleroma.User do    def validate_fields(changeset, remote? \\ false) do      limit_name = if remote?, do: :max_remote_account_fields, else: :max_account_fields -    limit = Pleroma.Config.get([:instance, limit_name], 0) +    limit = Config.get([:instance, limit_name], 0)      changeset      |> validate_length(:fields, max: limit) @@ -2074,8 +2086,8 @@ defmodule Pleroma.User do    end    defp valid_field?(%{"name" => name, "value" => value}) do -    name_limit = Pleroma.Config.get([:instance, :account_field_name_length], 255) -    value_limit = Pleroma.Config.get([:instance, :account_field_value_length], 255) +    name_limit = Config.get([:instance, :account_field_name_length], 255) +    value_limit = Config.get([:instance, :account_field_value_length], 255)      is_binary(name) && is_binary(value) && String.length(name) <= name_limit &&        String.length(value) <= value_limit @@ -2085,10 +2097,10 @@ defmodule Pleroma.User do    defp truncate_field(%{"name" => name, "value" => value}) do      {name, _chopped} = -      String.split_at(name, Pleroma.Config.get([:instance, :account_field_name_length], 255)) +      String.split_at(name, Config.get([:instance, :account_field_name_length], 255))      {value, _chopped} = -      String.split_at(value, Pleroma.Config.get([:instance, :account_field_value_length], 255)) +      String.split_at(value, Config.get([:instance, :account_field_value_length], 255))      %{"name" => name, "value" => value}    end @@ -2118,8 +2130,8 @@ defmodule Pleroma.User do    def mastodon_settings_update(user, settings) do      user -    |> cast(%{settings: settings}, [:settings]) -    |> validate_required([:settings]) +    |> cast(%{mastofe_settings: settings}, [:mastofe_settings]) +    |> validate_required([:mastofe_settings])      |> update_and_set_cache()    end @@ -2143,7 +2155,7 @@ defmodule Pleroma.User do    def add_pinnned_activity(user, %Pleroma.Activity{id: id}) do      if id not in user.pinned_activities do -      max_pinned_statuses = Pleroma.Config.get([:instance, :max_pinned_statuses], 0) +      max_pinned_statuses = Config.get([:instance, :max_pinned_statuses], 0)        params = %{pinned_activities: user.pinned_activities ++ [id]}        user diff --git a/lib/pleroma/user/notification_setting.ex b/lib/pleroma/user/notification_setting.ex index 4bd55e139..7d9e8a000 100644 --- a/lib/pleroma/user/notification_setting.ex +++ b/lib/pleroma/user/notification_setting.ex @@ -10,21 +10,15 @@ defmodule Pleroma.User.NotificationSetting do    @primary_key false    embedded_schema do -    field(:followers, :boolean, default: true) -    field(:follows, :boolean, default: true) -    field(:non_follows, :boolean, default: true) -    field(:non_followers, :boolean, default: true) -    field(:privacy_option, :boolean, default: false) +    field(:block_from_strangers, :boolean, default: false) +    field(:hide_notification_contents, :boolean, default: false)    end    def changeset(schema, params) do      schema      |> cast(prepare_attrs(params), [ -      :followers, -      :follows, -      :non_follows, -      :non_followers, -      :privacy_option +      :block_from_strangers, +      :hide_notification_contents      ])    end diff --git a/lib/pleroma/user/search.ex b/lib/pleroma/user/search.ex index cec59c372..d4fd31069 100644 --- a/lib/pleroma/user/search.ex +++ b/lib/pleroma/user/search.ex @@ -52,6 +52,7 @@ defmodule Pleroma.User.Search do      |> base_query(following)      |> filter_blocked_user(for_user)      |> filter_invisible_users() +    |> filter_internal_users()      |> filter_blocked_domains(for_user)      |> fts_search(query_string)      |> trigram_rank(query_string) @@ -68,11 +69,15 @@ defmodule Pleroma.User.Search do        u in query,        where:          fragment( +          # The fragment must _exactly_ match `users_fts_index`, otherwise the index won't work            """ -          (to_tsvector('simple', ?) || to_tsvector('simple', ?)) @@ to_tsquery('simple', ?) +          ( +            setweight(to_tsvector('simple', regexp_replace(?, '\\W', ' ', 'g')), 'A') || +            setweight(to_tsvector('simple', regexp_replace(coalesce(?, ''), '\\W', ' ', 'g')), 'B') +          ) @@ to_tsquery('simple', ?)            """, -          u.name,            u.nickname, +          u.name,            ^query_string          )      ) @@ -87,15 +92,23 @@ defmodule Pleroma.User.Search do      |> Enum.join(" | ")    end +  # Considers nickname match, localized nickname match, name match; preferences nickname match    defp trigram_rank(query, query_string) do      from(        u in query,        select_merge: %{          search_rank:            fragment( -            "similarity(?, trim(? || ' ' || coalesce(?, '')))", +            """ +            similarity(?, ?) + +            similarity(?, regexp_replace(?, '@.+', '')) + +            similarity(?, trim(coalesce(?, ''))) +            """,              ^query_string,              u.nickname, +            ^query_string, +            u.nickname, +            ^query_string,              u.name            )        } @@ -109,6 +122,10 @@ defmodule Pleroma.User.Search do      from(q in query, where: q.invisible == false)    end +  defp filter_internal_users(query) do +    from(q in query, where: q.actor_type != "Application") +  end +    defp filter_blocked_user(query, %User{} = blocker) do      query      |> join(:left, [u], b in Pleroma.UserRelationship, diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index 7cd3eab39..bc7b5d95a 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -10,6 +10,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do    alias Pleroma.Constants    alias Pleroma.Conversation    alias Pleroma.Conversation.Participation +  alias Pleroma.Filter    alias Pleroma.Maps    alias Pleroma.Notification    alias Pleroma.Object @@ -321,28 +322,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do      end    end -  @spec follow(User.t(), User.t(), String.t() | nil, boolean(), keyword()) :: -          {:ok, Activity.t()} | {:error, any()} -  def follow(follower, followed, activity_id \\ nil, local \\ true, opts \\ []) do -    with {:ok, result} <- -           Repo.transaction(fn -> do_follow(follower, followed, activity_id, local, opts) end) do -      result -    end -  end - -  defp do_follow(follower, followed, activity_id, local, opts) do -    skip_notify_and_stream = Keyword.get(opts, :skip_notify_and_stream, false) -    data = make_follow_data(follower, followed, activity_id) - -    with {:ok, activity} <- insert(data, local), -         _ <- skip_notify_and_stream || notify_and_stream(activity), -         :ok <- maybe_federate(activity) do -      {:ok, activity} -    else -      {:error, error} -> Repo.rollback(error) -    end -  end -    @spec unfollow(User.t(), User.t(), String.t() | nil, boolean()) ::            {:ok, Activity.t()} | nil | {:error, any()}    def unfollow(follower, followed, activity_id \\ nil, local \\ true) do @@ -366,33 +345,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do      end    end -  @spec block(User.t(), User.t(), String.t() | nil, boolean()) :: -          {:ok, Activity.t()} | {:error, any()} -  def block(blocker, blocked, activity_id \\ nil, local \\ true) do -    with {:ok, result} <- -           Repo.transaction(fn -> do_block(blocker, blocked, activity_id, local) end) do -      result -    end -  end - -  defp do_block(blocker, blocked, activity_id, local) do -    unfollow_blocked = Config.get([:activitypub, :unfollow_blocked]) - -    if unfollow_blocked and fetch_latest_follow(blocker, blocked) do -      unfollow(blocker, blocked, nil, local) -    end - -    block_data = make_block_data(blocker, blocked, activity_id) - -    with {:ok, activity} <- insert(block_data, local), -         _ <- notify_and_stream(activity), -         :ok <- maybe_federate(activity) do -      {:ok, activity} -    else -      {:error, error} -> Repo.rollback(error) -    end -  end -    @spec flag(map()) :: {:ok, Activity.t()} | {:error, any()}    def flag(          %{ @@ -473,6 +425,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do      |> maybe_set_thread_muted_field(opts)      |> restrict_blocked(opts)      |> restrict_recipients(recipients, opts[:user]) +    |> restrict_filtered(opts)      |> where(        [activity],        fragment( @@ -988,6 +941,26 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do    defp restrict_instance(query, _), do: query +  defp restrict_filtered(query, %{user: %User{} = user}) do +    case Filter.compose_regex(user) do +      nil -> +        query + +      regex -> +        from([activity, object] in query, +          where: +            fragment("not(?->>'content' ~* ?)", object.data, ^regex) or +              activity.actor == ^user.ap_id +        ) +    end +  end + +  defp restrict_filtered(query, %{blocking_user: %User{} = user}) do +    restrict_filtered(query, %{user: user}) +  end + +  defp restrict_filtered(query, _), do: query +    defp exclude_poll_votes(query, %{include_poll_votes: true}), do: query    defp exclude_poll_votes(query, _) do @@ -1118,6 +1091,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do      |> restrict_favorited_by(opts)      |> restrict_blocked(restrict_blocked_opts)      |> restrict_muted(restrict_muted_opts) +    |> restrict_filtered(opts)      |> restrict_media(opts)      |> restrict_visibility(opts)      |> restrict_thread_visibility(opts, config) @@ -1126,6 +1100,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do      |> restrict_muted_reblogs(restrict_muted_reblogs_opts)      |> restrict_instance(opts)      |> restrict_announce_object_actor(opts) +    |> restrict_filtered(opts)      |> Activity.restrict_deactivated_users()      |> exclude_poll_votes(opts)      |> exclude_chat_messages(opts) @@ -1251,6 +1226,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do        end)      locked = data["manuallyApprovesFollowers"] || false +    capabilities = data["capabilities"] || %{} +    accepts_chat_messages = capabilities["acceptsChatMessages"]      data = Transmogrifier.maybe_fix_user_object(data)      discoverable = data["discoverable"] || false      invisible = data["invisible"] || false @@ -1289,7 +1266,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do        also_known_as: Map.get(data, "alsoKnownAs", []),        public_key: public_key,        inbox: data["inbox"], -      shared_inbox: shared_inbox +      shared_inbox: shared_inbox, +      accepts_chat_messages: accepts_chat_messages      }      # nickname can be nil because of virtual actors @@ -1398,6 +1376,31 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do      end    end +  def maybe_handle_clashing_nickname(data) do +    nickname = data[:nickname] + +    with %User{} = old_user <- User.get_by_nickname(nickname), +         {_, false} <- {:ap_id_comparison, data[:ap_id] == old_user.ap_id} do +      Logger.info( +        "Found an old user for #{nickname}, the old ap id is #{old_user.ap_id}, new one is #{ +          data[:ap_id] +        }, renaming." +      ) + +      old_user +      |> User.remote_user_changeset(%{nickname: "#{old_user.id}.#{old_user.nickname}"}) +      |> User.update_and_set_cache() +    else +      {:ap_id_comparison, true} -> +        Logger.info( +          "Found an old user for #{nickname}, but the ap id #{data[:ap_id]} is the same as the new user. Race condition? Not changing anything." +        ) + +      _ -> +        nil +    end +  end +    def make_user_from_ap_id(ap_id) do      user = User.get_cached_by_ap_id(ap_id) @@ -1410,6 +1413,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do            |> User.remote_user_changeset(data)            |> User.update_and_set_cache()          else +          maybe_handle_clashing_nickname(data) +            data            |> User.remote_user_changeset()            |> Repo.insert() diff --git a/lib/pleroma/web/activity_pub/builder.ex b/lib/pleroma/web/activity_pub/builder.ex index 135a5c431..d5f3610ed 100644 --- a/lib/pleroma/web/activity_pub/builder.ex +++ b/lib/pleroma/web/activity_pub/builder.ex @@ -14,6 +14,19 @@ defmodule Pleroma.Web.ActivityPub.Builder do    require Pleroma.Constants +  @spec follow(User.t(), User.t()) :: {:ok, map(), keyword()} +  def follow(follower, followed) do +    data = %{ +      "id" => Utils.generate_activity_id(), +      "actor" => follower.ap_id, +      "type" => "Follow", +      "object" => followed.ap_id, +      "to" => [followed.ap_id] +    } + +    {:ok, data, []} +  end +    @spec emoji_react(User.t(), Object.t(), String.t()) :: {:ok, map(), keyword()}    def emoji_react(actor, object, emoji) do      with {:ok, data, meta} <- object_action(actor, object) do @@ -138,6 +151,18 @@ defmodule Pleroma.Web.ActivityPub.Builder do       }, []}    end +  @spec block(User.t(), User.t()) :: {:ok, map(), keyword()} +  def block(blocker, blocked) do +    {:ok, +     %{ +       "id" => Utils.generate_activity_id(), +       "type" => "Block", +       "actor" => blocker.ap_id, +       "object" => blocked.ap_id, +       "to" => [blocked.ap_id] +     }, []} +  end +    @spec announce(User.t(), Object.t(), keyword()) :: {:ok, map(), keyword()}    def announce(actor, object, options \\ []) do      public? = Keyword.get(options, :public, false) diff --git a/lib/pleroma/web/activity_pub/mrf/anti_followbot_policy.ex b/lib/pleroma/web/activity_pub/mrf/anti_followbot_policy.ex index 0270b96ae..b96388489 100644 --- a/lib/pleroma/web/activity_pub/mrf/anti_followbot_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/anti_followbot_policy.ex @@ -60,7 +60,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.AntiFollowbotPolicy do      if score < 0.8 do        {:ok, message}      else -      {:reject, nil} +      {:reject, "[AntiFollowbotPolicy] Scored #{actor_id} as #{score}"}      end    end diff --git a/lib/pleroma/web/activity_pub/mrf/anti_link_spam_policy.ex b/lib/pleroma/web/activity_pub/mrf/anti_link_spam_policy.ex index 9e7800997..b22464111 100644 --- a/lib/pleroma/web/activity_pub/mrf/anti_link_spam_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/anti_link_spam_policy.ex @@ -27,23 +27,25 @@ defmodule Pleroma.Web.ActivityPub.MRF.AntiLinkSpamPolicy do    @impl true    def filter(%{"type" => "Create", "actor" => actor, "object" => object} = message) do -    with {:ok, %User{} = u} <- User.get_or_fetch_by_ap_id(actor), +    with {:ok, %User{local: false} = u} <- User.get_or_fetch_by_ap_id(actor),           {:contains_links, true} <- {:contains_links, contains_links?(object)},           {:old_user, true} <- {:old_user, old_user?(u)} do        {:ok, message}      else +      {:ok, %User{local: true}} -> +        {:ok, message} +        {:contains_links, false} ->          {:ok, message}        {:old_user, false} -> -        {:reject, nil} +        {:reject, "[AntiLinkSpamPolicy] User has no posts nor followers"}        {:error, _} -> -        {:reject, nil} +        {:reject, "[AntiLinkSpamPolicy] Failed to get or fetch user by ap_id"}        e -> -        Logger.warn("[MRF anti-link-spam] WTF: unhandled error #{inspect(e)}") -        {:reject, nil} +        {:reject, "[AntiLinkSpamPolicy] Unhandled error #{inspect(e)}"}      end    end diff --git a/lib/pleroma/web/activity_pub/mrf/hellthread_policy.ex b/lib/pleroma/web/activity_pub/mrf/hellthread_policy.ex index f6b2c4415..9ba07b4e3 100644 --- a/lib/pleroma/web/activity_pub/mrf/hellthread_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/hellthread_policy.ex @@ -43,7 +43,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.HellthreadPolicy do    defp reject_message(message, threshold) when threshold > 0 do      with {_, recipients} <- get_recipient_count(message) do        if recipients > threshold do -        {:reject, nil} +        {:reject, "[HellthreadPolicy] #{recipients} recipients is over the limit of #{threshold}"}        else          {:ok, message}        end @@ -87,7 +87,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.HellthreadPolicy do           {:ok, message} <- delist_message(message, delist_threshold) do        {:ok, message}      else -      _e -> {:reject, nil} +      e -> e      end    end diff --git a/lib/pleroma/web/activity_pub/mrf/keyword_policy.ex b/lib/pleroma/web/activity_pub/mrf/keyword_policy.ex index 88b0d2b39..15e09dcf0 100644 --- a/lib/pleroma/web/activity_pub/mrf/keyword_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/keyword_policy.ex @@ -24,7 +24,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.KeywordPolicy do      if Enum.any?(Pleroma.Config.get([:mrf_keyword, :reject]), fn pattern ->           string_matches?(content, pattern) or string_matches?(summary, pattern)         end) do -      {:reject, nil} +      {:reject, "[KeywordPolicy] Matches with rejected keyword"}      else        {:ok, message}      end @@ -89,8 +89,9 @@ defmodule Pleroma.Web.ActivityPub.MRF.KeywordPolicy do           {:ok, message} <- check_replace(message) do        {:ok, message}      else -      _e -> -        {:reject, nil} +      {:reject, nil} -> {:reject, "[KeywordPolicy] "} +      {:reject, _} = e -> e +      _e -> {:reject, "[KeywordPolicy] "}      end    end diff --git a/lib/pleroma/web/activity_pub/mrf/mention_policy.ex b/lib/pleroma/web/activity_pub/mrf/mention_policy.ex index 06f003921..7910ca131 100644 --- a/lib/pleroma/web/activity_pub/mrf/mention_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/mention_policy.ex @@ -12,8 +12,9 @@ defmodule Pleroma.Web.ActivityPub.MRF.MentionPolicy do      reject_actors = Pleroma.Config.get([:mrf_mention, :actors], [])      recipients = (message["to"] || []) ++ (message["cc"] || []) -    if Enum.any?(recipients, fn recipient -> Enum.member?(reject_actors, recipient) end) do -      {:reject, nil} +    if rejected_mention = +         Enum.find(recipients, fn recipient -> Enum.member?(reject_actors, recipient) end) do +      {:reject, "[MentionPolicy] Rejected for mention of #{rejected_mention}"}      else        {:ok, message}      end diff --git a/lib/pleroma/web/activity_pub/mrf/object_age_policy.ex b/lib/pleroma/web/activity_pub/mrf/object_age_policy.ex index b0ccb63c8..5f111c72f 100644 --- a/lib/pleroma/web/activity_pub/mrf/object_age_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/object_age_policy.ex @@ -28,7 +28,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.ObjectAgePolicy do    defp check_reject(message, actions) do      if :reject in actions do -      {:reject, nil} +      {:reject, "[ObjectAgePolicy]"}      else        {:ok, message}      end @@ -47,9 +47,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.ObjectAgePolicy do          {:ok, message}        else -        # Unhandleable error: somebody is messing around, just drop the message.          _e -> -          {:reject, nil} +          {:reject, "[ObjectAgePolicy] Unhandled error"}        end      else        {:ok, message} @@ -69,9 +68,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.ObjectAgePolicy do          {:ok, message}        else -        # Unhandleable error: somebody is messing around, just drop the message.          _e -> -          {:reject, nil} +          {:reject, "[ObjectAgePolicy] Unhandled error"}        end      else        {:ok, message} @@ -98,7 +96,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.ObjectAgePolicy do    @impl true    def describe do      mrf_object_age = -      Pleroma.Config.get(:mrf_object_age) +      Config.get(:mrf_object_age)        |> Enum.into(%{})      {:ok, %{mrf_object_age: mrf_object_age}} diff --git a/lib/pleroma/web/activity_pub/mrf/reject_non_public.ex b/lib/pleroma/web/activity_pub/mrf/reject_non_public.ex index 3092f3272..0b9ed2224 100644 --- a/lib/pleroma/web/activity_pub/mrf/reject_non_public.ex +++ b/lib/pleroma/web/activity_pub/mrf/reject_non_public.ex @@ -38,7 +38,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.RejectNonPublic do          {:ok, object}        true -> -        {:reject, nil} +        {:reject, "[RejectNonPublic] visibility: #{visibility}"}      end    end @@ -47,5 +47,5 @@ defmodule Pleroma.Web.ActivityPub.MRF.RejectNonPublic do    @impl true    def describe, -    do: {:ok, %{mrf_rejectnonpublic: Pleroma.Config.get(:mrf_rejectnonpublic) |> Enum.into(%{})}} +    do: {:ok, %{mrf_rejectnonpublic: Config.get(:mrf_rejectnonpublic) |> Enum.into(%{})}}  end diff --git a/lib/pleroma/web/activity_pub/mrf/simple_policy.ex b/lib/pleroma/web/activity_pub/mrf/simple_policy.ex index 9cea6bcf9..b77b8c7b4 100644 --- a/lib/pleroma/web/activity_pub/mrf/simple_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/simple_policy.ex @@ -21,7 +21,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do        accepts == [] -> {:ok, object}        actor_host == Config.get([Pleroma.Web.Endpoint, :url, :host]) -> {:ok, object}        MRF.subdomain_match?(accepts, actor_host) -> {:ok, object} -      true -> {:reject, nil} +      true -> {:reject, "[SimplePolicy] host not in accept list"}      end    end @@ -31,7 +31,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do        |> MRF.subdomains_regex()      if MRF.subdomain_match?(rejects, actor_host) do -      {:reject, nil} +      {:reject, "[SimplePolicy] host in reject list"}      else        {:ok, object}      end @@ -114,7 +114,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do        |> MRF.subdomains_regex()      if MRF.subdomain_match?(report_removal, actor_host) do -      {:reject, nil} +      {:reject, "[SimplePolicy] host in report_removal list"}      else        {:ok, object}      end @@ -155,11 +155,11 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do      %{host: actor_host} = URI.parse(actor)      reject_deletes = -      Pleroma.Config.get([:mrf_simple, :reject_deletes]) +      Config.get([:mrf_simple, :reject_deletes])        |> MRF.subdomains_regex()      if MRF.subdomain_match?(reject_deletes, actor_host) do -      {:reject, nil} +      {:reject, "[SimplePolicy] host in reject_deletes list"}      else        {:ok, object}      end @@ -177,7 +177,9 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do           {:ok, object} <- check_report_removal(actor_info, object) do        {:ok, object}      else -      _e -> {:reject, nil} +      {:reject, nil} -> {:reject, "[SimplePolicy]"} +      {:reject, _} = e -> e +      _ -> {:reject, "[SimplePolicy]"}      end    end @@ -191,7 +193,9 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do           {:ok, object} <- check_banner_removal(actor_info, object) do        {:ok, object}      else -      _e -> {:reject, nil} +      {:reject, nil} -> {:reject, "[SimplePolicy]"} +      {:reject, _} = e -> e +      _ -> {:reject, "[SimplePolicy]"}      end    end diff --git a/lib/pleroma/web/activity_pub/mrf/tag_policy.ex b/lib/pleroma/web/activity_pub/mrf/tag_policy.ex index c310462cb..febabda08 100644 --- a/lib/pleroma/web/activity_pub/mrf/tag_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/tag_policy.ex @@ -134,12 +134,13 @@ defmodule Pleroma.Web.ActivityPub.MRF.TagPolicy do      if user.local == true do        {:ok, message}      else -      {:reject, nil} +      {:reject, +       "[TagPolicy] Follow from #{actor} tagged with mrf_tag:disable-remote-subscription"}      end    end -  defp process_tag("mrf_tag:disable-any-subscription", %{"type" => "Follow"}), -    do: {:reject, nil} +  defp process_tag("mrf_tag:disable-any-subscription", %{"type" => "Follow", "actor" => actor}), +    do: {:reject, "[TagPolicy] Follow from #{actor} tagged with mrf_tag:disable-any-subscription"}    defp process_tag(_, message), do: {:ok, message} diff --git a/lib/pleroma/web/activity_pub/mrf/user_allow_list_policy.ex b/lib/pleroma/web/activity_pub/mrf/user_allow_list_policy.ex index 651aed70f..1a28f2ba2 100644 --- a/lib/pleroma/web/activity_pub/mrf/user_allow_list_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/user_allow_list_policy.ex @@ -14,7 +14,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.UserAllowListPolicy do      if actor in allow_list do        {:ok, object}      else -      {:reject, nil} +      {:reject, "[UserAllowListPolicy] #{actor} not in the list"}      end    end diff --git a/lib/pleroma/web/activity_pub/mrf/vocabulary_policy.ex b/lib/pleroma/web/activity_pub/mrf/vocabulary_policy.ex index 6167a74e2..a6c545570 100644 --- a/lib/pleroma/web/activity_pub/mrf/vocabulary_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/vocabulary_policy.ex @@ -11,22 +11,26 @@ defmodule Pleroma.Web.ActivityPub.MRF.VocabularyPolicy do      with {:ok, _} <- filter(child_message) do        {:ok, message}      else -      {:reject, nil} -> -        {:reject, nil} +      {:reject, _} = e -> e      end    end    def filter(%{"type" => message_type} = message) do      with accepted_vocabulary <- Pleroma.Config.get([:mrf_vocabulary, :accept]),           rejected_vocabulary <- Pleroma.Config.get([:mrf_vocabulary, :reject]), -         true <- -           Enum.empty?(accepted_vocabulary) || Enum.member?(accepted_vocabulary, message_type), -         false <- -           length(rejected_vocabulary) > 0 && Enum.member?(rejected_vocabulary, message_type), +         {_, true} <- +           {:accepted, +            Enum.empty?(accepted_vocabulary) || Enum.member?(accepted_vocabulary, message_type)}, +         {_, false} <- +           {:rejected, +            length(rejected_vocabulary) > 0 && Enum.member?(rejected_vocabulary, message_type)},           {:ok, _} <- filter(message["object"]) do        {:ok, message}      else -      _ -> {:reject, nil} +      {:reject, _} = e -> e +      {:accepted, _} -> {:reject, "[VocabularyPolicy] #{message_type} not in accept list"} +      {:rejected, _} -> {:reject, "[VocabularyPolicy] #{message_type} in reject list"} +      _ -> {:reject, "[VocabularyPolicy]"}      end    end diff --git a/lib/pleroma/web/activity_pub/object_validator.ex b/lib/pleroma/web/activity_pub/object_validator.ex index 2c657b467..df926829c 100644 --- a/lib/pleroma/web/activity_pub/object_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validator.ex @@ -13,10 +13,12 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do    alias Pleroma.Object    alias Pleroma.User    alias Pleroma.Web.ActivityPub.ObjectValidators.AnnounceValidator +  alias Pleroma.Web.ActivityPub.ObjectValidators.BlockValidator    alias Pleroma.Web.ActivityPub.ObjectValidators.ChatMessageValidator    alias Pleroma.Web.ActivityPub.ObjectValidators.CreateChatMessageValidator    alias Pleroma.Web.ActivityPub.ObjectValidators.DeleteValidator    alias Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactValidator +  alias Pleroma.Web.ActivityPub.ObjectValidators.FollowValidator    alias Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator    alias Pleroma.Web.ActivityPub.ObjectValidators.UndoValidator    alias Pleroma.Web.ActivityPub.ObjectValidators.UpdateValidator @@ -24,6 +26,35 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do    @spec validate(map(), keyword()) :: {:ok, map(), keyword()} | {:error, any()}    def validate(object, meta) +  def validate(%{"type" => "Follow"} = object, meta) do +    with {:ok, object} <- +           object +           |> FollowValidator.cast_and_validate() +           |> Ecto.Changeset.apply_action(:insert) do +      object = stringify_keys(object) +      {:ok, object, meta} +    end +  end + +  def validate(%{"type" => "Block"} = block_activity, meta) do +    with {:ok, block_activity} <- +           block_activity +           |> BlockValidator.cast_and_validate() +           |> Ecto.Changeset.apply_action(:insert) do +      block_activity = stringify_keys(block_activity) +      outgoing_blocks = Pleroma.Config.get([:activitypub, :outgoing_blocks]) + +      meta = +        if !outgoing_blocks do +          Keyword.put(meta, :do_not_federate, true) +        else +          meta +        end + +      {:ok, block_activity, meta} +    end +  end +    def validate(%{"type" => "Update"} = update_activity, meta) do      with {:ok, update_activity} <-             update_activity diff --git a/lib/pleroma/web/activity_pub/object_validators/block_validator.ex b/lib/pleroma/web/activity_pub/object_validators/block_validator.ex new file mode 100644 index 000000000..1dde77198 --- /dev/null +++ b/lib/pleroma/web/activity_pub/object_validators/block_validator.ex @@ -0,0 +1,42 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.ObjectValidators.BlockValidator do +  use Ecto.Schema + +  alias Pleroma.EctoType.ActivityPub.ObjectValidators + +  import Ecto.Changeset +  import Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations + +  @primary_key false + +  embedded_schema do +    field(:id, ObjectValidators.ObjectID, primary_key: true) +    field(:type, :string) +    field(:actor, ObjectValidators.ObjectID) +    field(:to, ObjectValidators.Recipients, default: []) +    field(:cc, ObjectValidators.Recipients, default: []) +    field(:object, ObjectValidators.ObjectID) +  end + +  def cast_data(data) do +    %__MODULE__{} +    |> cast(data, __schema__(:fields)) +  end + +  def validate_data(cng) do +    cng +    |> validate_required([:id, :type, :actor, :to, :cc, :object]) +    |> validate_inclusion(:type, ["Block"]) +    |> validate_actor_presence() +    |> validate_actor_presence(field_name: :object) +  end + +  def cast_and_validate(data) do +    data +    |> cast_data +    |> validate_data +  end +end diff --git a/lib/pleroma/web/activity_pub/object_validators/chat_message_validator.ex b/lib/pleroma/web/activity_pub/object_validators/chat_message_validator.ex index c481d79e0..91b475393 100644 --- a/lib/pleroma/web/activity_pub/object_validators/chat_message_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/chat_message_validator.ex @@ -93,12 +93,14 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ChatMessageValidator do    - If both users are in our system    - If at least one of the users in this ChatMessage is a local user    - If the recipient is not blocking the actor +  - If the recipient is explicitly not accepting chat messages    """    def validate_local_concern(cng) do      with actor_ap <- get_field(cng, :actor),           {_, %User{} = actor} <- {:find_actor, User.get_cached_by_ap_id(actor_ap)},           {_, %User{} = recipient} <-             {:find_recipient, User.get_cached_by_ap_id(get_field(cng, :to) |> hd())}, +         {_, false} <- {:not_accepting_chats?, recipient.accepts_chat_messages == false},           {_, false} <- {:blocking_actor?, User.blocks?(recipient, actor)},           {_, true} <- {:local?, Enum.any?([actor, recipient], & &1.local)} do        cng @@ -107,6 +109,10 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ChatMessageValidator do          cng          |> add_error(:actor, "actor is blocked by recipient") +      {:not_accepting_chats?, true} -> +        cng +        |> add_error(:to, "recipient does not accept chat messages") +        {:local?, false} ->          cng          |> add_error(:actor, "actor and recipient are both remote") diff --git a/lib/pleroma/web/activity_pub/object_validators/follow_validator.ex b/lib/pleroma/web/activity_pub/object_validators/follow_validator.ex new file mode 100644 index 000000000..ca2724616 --- /dev/null +++ b/lib/pleroma/web/activity_pub/object_validators/follow_validator.ex @@ -0,0 +1,44 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.ObjectValidators.FollowValidator do +  use Ecto.Schema + +  alias Pleroma.EctoType.ActivityPub.ObjectValidators + +  import Ecto.Changeset +  import Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations + +  @primary_key false + +  embedded_schema do +    field(:id, ObjectValidators.ObjectID, primary_key: true) +    field(:type, :string) +    field(:actor, ObjectValidators.ObjectID) +    field(:to, ObjectValidators.Recipients, default: []) +    field(:cc, ObjectValidators.Recipients, default: []) +    field(:object, ObjectValidators.ObjectID) +    field(:state, :string, default: "pending") +  end + +  def cast_data(data) do +    %__MODULE__{} +    |> cast(data, __schema__(:fields)) +  end + +  def validate_data(cng) do +    cng +    |> validate_required([:id, :type, :actor, :to, :cc, :object]) +    |> validate_inclusion(:type, ["Follow"]) +    |> validate_inclusion(:state, ~w{pending reject accept}) +    |> validate_actor_presence() +    |> validate_actor_presence(field_name: :object) +  end + +  def cast_and_validate(data) do +    data +    |> cast_data +    |> validate_data +  end +end diff --git a/lib/pleroma/web/activity_pub/publisher.ex b/lib/pleroma/web/activity_pub/publisher.ex index b70cbd043..d88f7f3ee 100644 --- a/lib/pleroma/web/activity_pub/publisher.ex +++ b/lib/pleroma/web/activity_pub/publisher.ex @@ -49,7 +49,8 @@ defmodule Pleroma.Web.ActivityPub.Publisher do    """    def publish_one(%{inbox: inbox, json: json, actor: %User{} = actor, id: id} = params) do      Logger.debug("Federating #{id} to #{inbox}") -    %{host: host, path: path} = URI.parse(inbox) + +    uri = URI.parse(inbox)      digest = "SHA-256=" <> (:crypto.hash(:sha256, json) |> Base.encode64()) @@ -57,8 +58,8 @@ defmodule Pleroma.Web.ActivityPub.Publisher do      signature =        Pleroma.Signature.sign(actor, %{ -        "(request-target)": "post #{path}", -        host: host, +        "(request-target)": "post #{uri.path}", +        host: signature_host(uri),          "content-length": byte_size(json),          digest: digest,          date: date @@ -76,8 +77,9 @@ defmodule Pleroma.Web.ActivityPub.Publisher do                   {"digest", digest}                 ]               ) do -      if !Map.has_key?(params, :unreachable_since) || params[:unreachable_since], -        do: Instances.set_reachable(inbox) +      if not Map.has_key?(params, :unreachable_since) || params[:unreachable_since] do +        Instances.set_reachable(inbox) +      end        result      else @@ -96,6 +98,14 @@ defmodule Pleroma.Web.ActivityPub.Publisher do      |> publish_one()    end +  defp signature_host(%URI{port: port, scheme: scheme, host: host}) do +    if port == URI.default_port(scheme) do +      host +    else +      "#{host}:#{port}" +    end +  end +    defp should_federate?(inbox, public) do      if public do        true diff --git a/lib/pleroma/web/activity_pub/relay.ex b/lib/pleroma/web/activity_pub/relay.ex index 484178edd..b09764d2b 100644 --- a/lib/pleroma/web/activity_pub/relay.ex +++ b/lib/pleroma/web/activity_pub/relay.ex @@ -28,7 +28,7 @@ defmodule Pleroma.Web.ActivityPub.Relay do    def follow(target_instance) do      with %User{} = local_user <- get_actor(),           {:ok, %User{} = target_user} <- User.get_or_fetch_by_ap_id(target_instance), -         {:ok, activity} <- ActivityPub.follow(local_user, target_user) do +         {:ok, _, _, activity} <- CommonAPI.follow(local_user, target_user) do        Logger.info("relay: followed instance: #{target_instance}; id=#{activity.data["id"]}")        {:ok, activity}      else diff --git a/lib/pleroma/web/activity_pub/side_effects.ex b/lib/pleroma/web/activity_pub/side_effects.ex index de143b8f0..1d2c296a5 100644 --- a/lib/pleroma/web/activity_pub/side_effects.ex +++ b/lib/pleroma/web/activity_pub/side_effects.ex @@ -6,8 +6,10 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do    collection, and so on.    """    alias Pleroma.Activity +  alias Pleroma.Activity.Ir.Topics    alias Pleroma.Chat    alias Pleroma.Chat.MessageReference +  alias Pleroma.FollowingRelationship    alias Pleroma.Notification    alias Pleroma.Object    alias Pleroma.Repo @@ -20,6 +22,84 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do    def handle(object, meta \\ []) +  # Tasks this handle +  # - Follows if possible +  # - Sends a notification +  # - Generates accept or reject if appropriate +  def handle( +        %{ +          data: %{ +            "id" => follow_id, +            "type" => "Follow", +            "object" => followed_user, +            "actor" => following_user +          } +        } = object, +        meta +      ) do +    with %User{} = follower <- User.get_cached_by_ap_id(following_user), +         %User{} = followed <- User.get_cached_by_ap_id(followed_user), +         {_, {:ok, _}, _, _} <- +           {:following, User.follow(follower, followed, :follow_pending), follower, followed} do +      if followed.local && !followed.locked do +        Utils.update_follow_state_for_all(object, "accept") +        FollowingRelationship.update(follower, followed, :follow_accept) +        User.update_follower_count(followed) +        User.update_following_count(follower) + +        %{ +          to: [following_user], +          actor: followed, +          object: follow_id, +          local: true +        } +        |> ActivityPub.accept() +      end +    else +      {:following, {:error, _}, follower, followed} -> +        Utils.update_follow_state_for_all(object, "reject") +        FollowingRelationship.update(follower, followed, :follow_reject) + +        if followed.local do +          %{ +            to: [follower.ap_id], +            actor: followed, +            object: follow_id, +            local: true +          } +          |> ActivityPub.reject() +        end + +      _ -> +        nil +    end + +    {:ok, notifications} = Notification.create_notifications(object, do_send: false) + +    meta = +      meta +      |> add_notifications(notifications) + +    updated_object = Activity.get_by_ap_id(follow_id) + +    {:ok, updated_object, meta} +  end + +  # Tasks this handles: +  # - Unfollow and block +  def handle( +        %{data: %{"type" => "Block", "object" => blocked_user, "actor" => blocking_user}} = +          object, +        meta +      ) do +    with %User{} = blocker <- User.get_cached_by_ap_id(blocking_user), +         %User{} = blocked <- User.get_cached_by_ap_id(blocked_user) do +      User.block(blocker, blocked) +    end + +    {:ok, object, meta} +  end +    # Tasks this handles:    # - Update the user    # @@ -82,7 +162,10 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do      if !User.is_internal_user?(user) do        Notification.create_notifications(object) -      ActivityPub.stream_out(object) + +      object +      |> Topics.get_activity_topics() +      |> Streamer.stream(object)      end      {:ok, object, meta} @@ -190,14 +273,20 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do      {:ok, object}    end -  def handle_undoing(%{data: %{"type" => "Like"}} = object) do -    with %Object{} = liked_object <- Object.get_by_ap_id(object.data["object"]), -         {:ok, _} <- Utils.remove_like_from_object(object, liked_object), -         {:ok, _} <- Repo.delete(object) do -      :ok +  defp undo_like(nil, object), do: delete_object(object) + +  defp undo_like(%Object{} = liked_object, object) do +    with {:ok, _} <- Utils.remove_like_from_object(object, liked_object) do +      delete_object(object)      end    end +  def handle_undoing(%{data: %{"type" => "Like"}} = object) do +    object.data["object"] +    |> Object.get_by_ap_id() +    |> undo_like(object) +  end +    def handle_undoing(%{data: %{"type" => "EmojiReact"}} = object) do      with %Object{} = reacted_object <- Object.get_by_ap_id(object.data["object"]),           {:ok, _} <- Utils.remove_emoji_reaction_from_object(object, reacted_object), @@ -227,6 +316,11 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do    def handle_undoing(object), do: {:error, ["don't know how to handle", object]} +  @spec delete_object(Object.t()) :: :ok | {:error, Ecto.Changeset.t()} +  defp delete_object(object) do +    with {:ok, _} <- Repo.delete(object), do: :ok +  end +    defp send_notifications(meta) do      Keyword.get(meta, :notifications, [])      |> Enum.each(fn notification -> diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex index 4e318e89c..f37bcab3e 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier.ex @@ -62,15 +62,17 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do    def fix_summary(object), do: Map.put(object, "summary", "")    def fix_addressing_list(map, field) do +    addrs = map[field] +      cond do -      is_binary(map[field]) -> -        Map.put(map, field, [map[field]]) +      is_list(addrs) -> +        Map.put(map, field, Enum.filter(addrs, &is_binary/1)) -      is_nil(map[field]) -> -        Map.put(map, field, []) +      is_binary(addrs) -> +        Map.put(map, field, [addrs])        true -> -        map +        Map.put(map, field, [])      end    end @@ -233,18 +235,24 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do              is_map(url) && is_binary(url["href"]) -> url["href"]              is_binary(data["url"]) -> data["url"]              is_binary(data["href"]) -> data["href"] +            true -> nil            end -        attachment_url = -          %{"href" => href} -          |> Maps.put_if_present("mediaType", media_type) -          |> Maps.put_if_present("type", Map.get(url || %{}, "type")) +        if href do +          attachment_url = +            %{"href" => href} +            |> Maps.put_if_present("mediaType", media_type) +            |> Maps.put_if_present("type", Map.get(url || %{}, "type")) -        %{"url" => [attachment_url]} -        |> Maps.put_if_present("mediaType", media_type) -        |> Maps.put_if_present("type", data["type"]) -        |> Maps.put_if_present("name", data["name"]) +          %{"url" => [attachment_url]} +          |> Maps.put_if_present("mediaType", media_type) +          |> Maps.put_if_present("type", data["type"]) +          |> Maps.put_if_present("name", data["name"]) +        else +          nil +        end        end) +      |> Enum.filter(& &1)      Map.put(object, "attachment", attachments)    end @@ -263,12 +271,18 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do    def fix_url(%{"type" => object_type, "url" => url} = object)        when object_type in ["Video", "Audio"] and is_list(url) do -    first_element = Enum.at(url, 0) +    attachment = +      Enum.find(url, fn x -> +        media_type = x["mediaType"] || x["mimeType"] || "" + +        is_map(x) and String.starts_with?(media_type, ["audio/", "video/"]) +      end) -    link_element = Enum.find(url, fn x -> is_map(x) and x["mimeType"] == "text/html" end) +    link_element = +      Enum.find(url, fn x -> is_map(x) and (x["mediaType"] || x["mimeType"]) == "text/html" end)      object -    |> Map.put("attachment", [first_element]) +    |> Map.put("attachment", [attachment])      |> Map.put("url", link_element["href"])    end @@ -446,12 +460,9 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do        when objtype in ["Article", "Event", "Note", "Video", "Page", "Question", "Answer", "Audio"] do      actor = Containment.get_actor(data) -    data = -      Map.put(data, "actor", actor) -      |> fix_addressing -      with nil <- Activity.get_create_by_object_ap_id(object["id"]), -         {:ok, %User{} = user} <- User.get_or_fetch_by_ap_id(data["actor"]) do +         {:ok, %User{} = user} <- User.get_or_fetch_by_ap_id(actor), +         data <- Map.put(data, "actor", actor) |> fix_addressing() do        object = fix_object(object, options)        params = %{ @@ -521,66 +532,6 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do    end    def handle_incoming( -        %{"type" => "Follow", "object" => followed, "actor" => follower, "id" => id} = data, -        _options -      ) do -    with %User{local: true} = followed <- -           User.get_cached_by_ap_id(Containment.get_actor(%{"actor" => followed})), -         {:ok, %User{} = follower} <- -           User.get_or_fetch_by_ap_id(Containment.get_actor(%{"actor" => follower})), -         {:ok, activity} <- -           ActivityPub.follow(follower, followed, id, false, skip_notify_and_stream: true) do -      with deny_follow_blocked <- Pleroma.Config.get([:user, :deny_follow_blocked]), -           {_, false} <- {:user_blocked, User.blocks?(followed, follower) && deny_follow_blocked}, -           {_, false} <- {:user_locked, User.locked?(followed)}, -           {_, {:ok, follower}} <- {:follow, User.follow(follower, followed)}, -           {_, {:ok, _}} <- -             {:follow_state_update, Utils.update_follow_state_for_all(activity, "accept")}, -           {:ok, _relationship} <- -             FollowingRelationship.update(follower, followed, :follow_accept) do -        ActivityPub.accept(%{ -          to: [follower.ap_id], -          actor: followed, -          object: data, -          local: true -        }) -      else -        {:user_blocked, true} -> -          {:ok, _} = Utils.update_follow_state_for_all(activity, "reject") -          {:ok, _relationship} = FollowingRelationship.update(follower, followed, :follow_reject) - -          ActivityPub.reject(%{ -            to: [follower.ap_id], -            actor: followed, -            object: data, -            local: true -          }) - -        {:follow, {:error, _}} -> -          {:ok, _} = Utils.update_follow_state_for_all(activity, "reject") -          {:ok, _relationship} = FollowingRelationship.update(follower, followed, :follow_reject) - -          ActivityPub.reject(%{ -            to: [follower.ap_id], -            actor: followed, -            object: data, -            local: true -          }) - -        {:user_locked, true} -> -          {:ok, _relationship} = FollowingRelationship.update(follower, followed, :follow_pending) -          :noop -      end - -      ActivityPub.notify_and_stream(activity) -      {:ok, activity} -    else -      _e -> -        :error -    end -  end - -  def handle_incoming(          %{"type" => "Accept", "object" => follow_object, "actor" => _actor, "id" => id} = data,          _options        ) do @@ -673,7 +624,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do    end    def handle_incoming(%{"type" => type} = data, _options) -      when type in ["Like", "EmojiReact", "Announce"] do +      when type in ~w{Like EmojiReact Announce} do      with :ok <- ObjectValidator.fetch_actor_and_object(data),           {:ok, activity, _meta} <-             Pipeline.common_pipeline(data, local: false) do @@ -684,9 +635,10 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do    end    def handle_incoming( -        %{"type" => "Update"} = data, +        %{"type" => type} = data,          _options -      ) do +      ) +      when type in ~w{Update Block Follow} do      with {:ok, %User{}} <- ObjectValidator.fetch_actor(data),           {:ok, activity, _} <- Pipeline.common_pipeline(data, local: false) do        {:ok, activity} @@ -766,21 +718,6 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do    end    def handle_incoming( -        %{"type" => "Block", "object" => blocked, "actor" => blocker, "id" => id} = _data, -        _options -      ) do -    with %User{local: true} = blocked = User.get_cached_by_ap_id(blocked), -         {:ok, %User{} = blocker} = User.get_or_fetch_by_ap_id(blocker), -         {:ok, activity} <- ActivityPub.block(blocker, blocked, id, false) do -      User.unfollow(blocker, blocked) -      User.block(blocker, blocked) -      {:ok, activity} -    else -      _e -> :error -    end -  end - -  def handle_incoming(          %{            "type" => "Move",            "actor" => origin_actor, diff --git a/lib/pleroma/web/activity_pub/views/user_view.ex b/lib/pleroma/web/activity_pub/views/user_view.ex index 4a02b09a1..3a4564912 100644 --- a/lib/pleroma/web/activity_pub/views/user_view.ex +++ b/lib/pleroma/web/activity_pub/views/user_view.ex @@ -81,6 +81,15 @@ defmodule Pleroma.Web.ActivityPub.UserView do      fields = Enum.map(user.fields, &Map.put(&1, "type", "PropertyValue")) +    capabilities = +      if is_boolean(user.accepts_chat_messages) do +        %{ +          "acceptsChatMessages" => user.accepts_chat_messages +        } +      else +        %{} +      end +      %{        "id" => user.ap_id,        "type" => user.actor_type, @@ -101,7 +110,8 @@ defmodule Pleroma.Web.ActivityPub.UserView do        "endpoints" => endpoints,        "attachment" => fields,        "tag" => emoji_tags, -      "discoverable" => user.discoverable +      "discoverable" => user.discoverable, +      "capabilities" => capabilities      }      |> Map.merge(maybe_make_image(&User.avatar_url/2, "icon", user))      |> Map.merge(maybe_make_image(&User.banner_url/2, "image", user)) diff --git a/lib/pleroma/web/activity_pub/visibility.ex b/lib/pleroma/web/activity_pub/visibility.ex index 453a6842e..343f41caa 100644 --- a/lib/pleroma/web/activity_pub/visibility.ex +++ b/lib/pleroma/web/activity_pub/visibility.ex @@ -47,6 +47,10 @@ defmodule Pleroma.Web.ActivityPub.Visibility do    @spec visible_for_user?(Activity.t(), User.t() | nil) :: boolean()    def visible_for_user?(%{actor: ap_id}, %User{ap_id: ap_id}), do: true +  def visible_for_user?(nil, _), do: false + +  def visible_for_user?(%{data: %{"listMessage" => _}}, nil), do: false +    def visible_for_user?(%{data: %{"listMessage" => list_ap_id}} = activity, %User{} = user) do      user.ap_id in activity.data["to"] ||        list_ap_id @@ -54,8 +58,6 @@ defmodule Pleroma.Web.ActivityPub.Visibility do        |> Pleroma.List.member?(user)    end -  def visible_for_user?(%{data: %{"listMessage" => _}}, nil), do: false -    def visible_for_user?(%{local: local} = activity, nil) do      cfg_key =        if local, diff --git a/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex b/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex index f9545d895..e5f14269a 100644 --- a/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex +++ b/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex @@ -206,8 +206,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do      end    end -  def user_show(conn, %{"nickname" => nickname}) do -    with %User{} = user <- User.get_cached_by_nickname_or_id(nickname) do +  def user_show(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do +    with %User{} = user <- User.get_cached_by_nickname_or_id(nickname, for: admin) do        conn        |> put_view(AccountView)        |> render("show.json", %{user: user}) @@ -233,11 +233,11 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do      |> render("index.json", %{activities: activities, as: :activity})    end -  def list_user_statuses(conn, %{"nickname" => nickname} = params) do +  def list_user_statuses(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname} = params) do      with_reblogs = params["with_reblogs"] == "true" || params["with_reblogs"] == true      godmode = params["godmode"] == "true" || params["godmode"] == true -    with %User{} = user <- User.get_cached_by_nickname_or_id(nickname) do +    with %User{} = user <- User.get_cached_by_nickname_or_id(nickname, for: admin) do        {_, page_size} = page_params(params)        activities = @@ -526,7 +526,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do    @doc "Show a given user's credentials"    def show_user_credentials(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do -    with %User{} = user <- User.get_cached_by_nickname_or_id(nickname) do +    with %User{} = user <- User.get_cached_by_nickname_or_id(nickname, for: admin) do        conn        |> put_view(AccountView)        |> render("credentials.json", %{user: user, for: admin}) diff --git a/lib/pleroma/web/admin_api/controllers/config_controller.ex b/lib/pleroma/web/admin_api/controllers/config_controller.ex index 7f60470cb..0df13007f 100644 --- a/lib/pleroma/web/admin_api/controllers/config_controller.ex +++ b/lib/pleroma/web/admin_api/controllers/config_controller.ex @@ -9,8 +9,6 @@ defmodule Pleroma.Web.AdminAPI.ConfigController do    alias Pleroma.ConfigDB    alias Pleroma.Plugs.OAuthScopesPlug -  @descriptions Pleroma.Docs.JSON.compile() -    plug(Pleroma.Web.ApiSpec.CastAndValidate)    plug(OAuthScopesPlug, %{scopes: ["write"], admin: true} when action == :update) @@ -25,7 +23,7 @@ defmodule Pleroma.Web.AdminAPI.ConfigController do    defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.Admin.ConfigOperation    def descriptions(conn, _params) do -    descriptions = Enum.filter(@descriptions, &whitelisted_config?/1) +    descriptions = Enum.filter(Pleroma.Docs.JSON.compiled_descriptions(), &whitelisted_config?/1)      json(conn, descriptions)    end diff --git a/lib/pleroma/web/api_spec/cast_and_validate.ex b/lib/pleroma/web/api_spec/cast_and_validate.ex index bd9026237..fbfc27d6f 100644 --- a/lib/pleroma/web/api_spec/cast_and_validate.ex +++ b/lib/pleroma/web/api_spec/cast_and_validate.ex @@ -40,7 +40,7 @@ defmodule Pleroma.Web.ApiSpec.CastAndValidate do            |> List.first()          _ -> -          nil +          "application/json"        end      private_data = Map.put(private_data, :operation_id, operation_id) diff --git a/lib/pleroma/web/api_spec/helpers.ex b/lib/pleroma/web/api_spec/helpers.ex index a258e8421..2a7f1a706 100644 --- a/lib/pleroma/web/api_spec/helpers.ex +++ b/lib/pleroma/web/api_spec/helpers.ex @@ -29,6 +29,10 @@ defmodule Pleroma.Web.ApiSpec.Helpers do      }    end +  def admin_api_params do +    [Operation.parameter(:admin_token, :query, :string, "Allows authorization via admin token.")] +  end +    def pagination_params do      [        Operation.parameter(:max_id, :query, :string, "Return items older than this ID"), diff --git a/lib/pleroma/web/api_spec/operations/account_operation.ex b/lib/pleroma/web/api_spec/operations/account_operation.ex index 9bde8fc0d..952d9347b 100644 --- a/lib/pleroma/web/api_spec/operations/account_operation.ex +++ b/lib/pleroma/web/api_spec/operations/account_operation.ex @@ -61,7 +61,7 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do        description: "Update the user's display and preferences.",        operationId: "AccountController.update_credentials",        security: [%{"oAuth" => ["write:accounts"]}], -      requestBody: request_body("Parameters", update_creadentials_request(), required: true), +      requestBody: request_body("Parameters", update_credentials_request(), required: true),        responses: %{          200 => Operation.response("Account", "application/json", Account),          403 => Operation.response("Error", "application/json", ApiError) @@ -203,14 +203,23 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do        security: [%{"oAuth" => ["follow", "write:follows"]}],        description: "Follow the given account",        parameters: [ -        %Reference{"$ref": "#/components/parameters/accountIdOrNickname"}, -        Operation.parameter( -          :reblogs, -          :query, -          BooleanLike, -          "Receive this account's reblogs in home timeline? Defaults to true." -        ) +        %Reference{"$ref": "#/components/parameters/accountIdOrNickname"}        ], +      requestBody: +        request_body( +          "Parameters", +          %Schema{ +            type: :object, +            properties: %{ +              reblogs: %Schema{ +                type: :boolean, +                description: "Receive this account's reblogs in home timeline? Defaults to true.", +                default: true +              } +            } +          }, +          required: false +        ),        responses: %{          200 => Operation.response("Relationship", "application/json", AccountRelationship),          400 => Operation.response("Error", "application/json", ApiError), @@ -438,6 +447,7 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do      }    end +  # TODO: This is actually a token respone, but there's no oauth operation file yet.    defp create_response do      %Schema{        title: "AccountCreateResponse", @@ -446,19 +456,25 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do        properties: %{          token_type: %Schema{type: :string},          access_token: %Schema{type: :string}, -        scope: %Schema{type: :array, items: %Schema{type: :string}}, -        created_at: %Schema{type: :integer, format: :"date-time"} +        refresh_token: %Schema{type: :string}, +        scope: %Schema{type: :string}, +        created_at: %Schema{type: :integer, format: :"date-time"}, +        me: %Schema{type: :string}, +        expires_in: %Schema{type: :integer}        },        example: %{ +        "token_type" => "Bearer",          "access_token" => "i9hAVVzGld86Pl5JtLtizKoXVvtTlSCJvwaugCxvZzk", +        "refresh_token" => "i9hAVVzGld86Pl5JtLtizKoXVvtTlSCJvwaugCxvZzz",          "created_at" => 1_585_918_714, -        "scope" => ["read", "write", "follow", "push"], -        "token_type" => "Bearer" +        "expires_in" => 600, +        "scope" => "read write follow push", +        "me" => "https://gensokyo.2hu/users/raymoo"        }      }    end -  defp update_creadentials_request do +  defp update_credentials_request do      %Schema{        title: "AccountUpdateCredentialsRequest",        description: "POST body for creating an account", @@ -492,6 +508,11 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do            nullable: true,            description: "Whether manual approval of follow requests is required."          }, +        accepts_chat_messages: %Schema{ +          allOf: [BooleanLike], +          nullable: true, +          description: "Whether the user accepts receiving chat messages." +        },          fields_attributes: %Schema{            nullable: true,            oneOf: [ diff --git a/lib/pleroma/web/api_spec/operations/admin/config_operation.ex b/lib/pleroma/web/api_spec/operations/admin/config_operation.ex index 7b38a2ef4..3a8380797 100644 --- a/lib/pleroma/web/api_spec/operations/admin/config_operation.ex +++ b/lib/pleroma/web/api_spec/operations/admin/config_operation.ex @@ -26,6 +26,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.ConfigOperation do            %Schema{type: :boolean, default: false},            "Get only saved in database settings"          ) +        | admin_api_params()        ],        security: [%{"oAuth" => ["read"]}],        responses: %{ @@ -41,6 +42,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.ConfigOperation do        summary: "Update config settings",        operationId: "AdminAPI.ConfigController.update",        security: [%{"oAuth" => ["write"]}], +      parameters: admin_api_params(),        requestBody:          request_body("Parameters", %Schema{            type: :object, @@ -73,6 +75,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.ConfigOperation do        summary: "Get JSON with config descriptions.",        operationId: "AdminAPI.ConfigController.descriptions",        security: [%{"oAuth" => ["read"]}], +      parameters: admin_api_params(),        responses: %{          200 =>            Operation.response("Config Descriptions", "application/json", %Schema{ diff --git a/lib/pleroma/web/api_spec/operations/admin/invite_operation.ex b/lib/pleroma/web/api_spec/operations/admin/invite_operation.ex index d3af9db49..801024d75 100644 --- a/lib/pleroma/web/api_spec/operations/admin/invite_operation.ex +++ b/lib/pleroma/web/api_spec/operations/admin/invite_operation.ex @@ -20,6 +20,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.InviteOperation do        summary: "Get a list of generated invites",        operationId: "AdminAPI.InviteController.index",        security: [%{"oAuth" => ["read:invites"]}], +      parameters: admin_api_params(),        responses: %{          200 =>            Operation.response("Invites", "application/json", %Schema{ @@ -51,6 +52,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.InviteOperation do        summary: "Create an account registration invite token",        operationId: "AdminAPI.InviteController.create",        security: [%{"oAuth" => ["write:invites"]}], +      parameters: admin_api_params(),        requestBody:          request_body("Parameters", %Schema{            type: :object, @@ -71,6 +73,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.InviteOperation do        summary: "Revoke invite by token",        operationId: "AdminAPI.InviteController.revoke",        security: [%{"oAuth" => ["write:invites"]}], +      parameters: admin_api_params(),        requestBody:          request_body(            "Parameters", @@ -97,6 +100,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.InviteOperation do        summary: "Sends registration invite via email",        operationId: "AdminAPI.InviteController.email",        security: [%{"oAuth" => ["write:invites"]}], +      parameters: admin_api_params(),        requestBody:          request_body(            "Parameters", diff --git a/lib/pleroma/web/api_spec/operations/admin/media_proxy_cache_operation.ex b/lib/pleroma/web/api_spec/operations/admin/media_proxy_cache_operation.ex index 0358cfbad..20d033f66 100644 --- a/lib/pleroma/web/api_spec/operations/admin/media_proxy_cache_operation.ex +++ b/lib/pleroma/web/api_spec/operations/admin/media_proxy_cache_operation.ex @@ -33,6 +33,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.MediaProxyCacheOperation do            %Schema{type: :integer, default: 50},            "Number of statuses to return"          ) +        | admin_api_params()        ],        responses: %{          200 => success_response() @@ -46,6 +47,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.MediaProxyCacheOperation do        summary: "Remove a banned MediaProxy URL from Cachex",        operationId: "AdminAPI.MediaProxyCacheController.delete",        security: [%{"oAuth" => ["write:media_proxy_caches"]}], +      parameters: admin_api_params(),        requestBody:          request_body(            "Parameters", @@ -71,6 +73,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.MediaProxyCacheOperation do        summary: "Purge and optionally ban a MediaProxy URL",        operationId: "AdminAPI.MediaProxyCacheController.purge",        security: [%{"oAuth" => ["write:media_proxy_caches"]}], +      parameters: admin_api_params(),        requestBody:          request_body(            "Parameters", diff --git a/lib/pleroma/web/api_spec/operations/admin/oauth_app_operation.ex b/lib/pleroma/web/api_spec/operations/admin/oauth_app_operation.ex index fbc9f80d7..a75f3e622 100644 --- a/lib/pleroma/web/api_spec/operations/admin/oauth_app_operation.ex +++ b/lib/pleroma/web/api_spec/operations/admin/oauth_app_operation.ex @@ -36,6 +36,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.OAuthAppOperation do            %Schema{type: :integer, default: 50},            "Number of apps to return"          ) +        | admin_api_params()        ],        responses: %{          200 => @@ -72,6 +73,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.OAuthAppOperation do        summary: "Create OAuth App",        operationId: "AdminAPI.OAuthAppController.create",        requestBody: request_body("Parameters", create_request()), +      parameters: admin_api_params(),        security: [%{"oAuth" => ["write"]}],        responses: %{          200 => Operation.response("App", "application/json", oauth_app()), @@ -85,7 +87,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.OAuthAppOperation do        tags: ["Admin", "oAuth Apps"],        summary: "Update OAuth App",        operationId: "AdminAPI.OAuthAppController.update", -      parameters: [id_param()], +      parameters: [id_param() | admin_api_params()],        security: [%{"oAuth" => ["write"]}],        requestBody: request_body("Parameters", update_request()),        responses: %{ @@ -103,7 +105,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.OAuthAppOperation do        tags: ["Admin", "oAuth Apps"],        summary: "Delete OAuth App",        operationId: "AdminAPI.OAuthAppController.delete", -      parameters: [id_param()], +      parameters: [id_param() | admin_api_params()],        security: [%{"oAuth" => ["write"]}],        responses: %{          204 => no_content_response(), diff --git a/lib/pleroma/web/api_spec/operations/admin/relay_operation.ex b/lib/pleroma/web/api_spec/operations/admin/relay_operation.ex index 7672cb467..67ee5eee0 100644 --- a/lib/pleroma/web/api_spec/operations/admin/relay_operation.ex +++ b/lib/pleroma/web/api_spec/operations/admin/relay_operation.ex @@ -19,6 +19,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.RelayOperation do        summary: "List Relays",        operationId: "AdminAPI.RelayController.index",        security: [%{"oAuth" => ["read"]}], +      parameters: admin_api_params(),        responses: %{          200 =>            Operation.response("Response", "application/json", %Schema{ @@ -41,6 +42,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.RelayOperation do        summary: "Follow a Relay",        operationId: "AdminAPI.RelayController.follow",        security: [%{"oAuth" => ["write:follows"]}], +      parameters: admin_api_params(),        requestBody:          request_body("Parameters", %Schema{            type: :object, @@ -64,6 +66,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.RelayOperation do        summary: "Unfollow a Relay",        operationId: "AdminAPI.RelayController.unfollow",        security: [%{"oAuth" => ["write:follows"]}], +      parameters: admin_api_params(),        requestBody:          request_body("Parameters", %Schema{            type: :object, diff --git a/lib/pleroma/web/api_spec/operations/admin/report_operation.ex b/lib/pleroma/web/api_spec/operations/admin/report_operation.ex index 15e78bfaf..3bb7ec49e 100644 --- a/lib/pleroma/web/api_spec/operations/admin/report_operation.ex +++ b/lib/pleroma/web/api_spec/operations/admin/report_operation.ex @@ -48,6 +48,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.ReportOperation do            %Schema{type: :integer, default: 50},            "Number number of log entries per page"          ) +        | admin_api_params()        ],        responses: %{          200 => @@ -71,7 +72,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.ReportOperation do        tags: ["Admin", "Reports"],        summary: "Get an individual report",        operationId: "AdminAPI.ReportController.show", -      parameters: [id_param()], +      parameters: [id_param() | admin_api_params()],        security: [%{"oAuth" => ["read:reports"]}],        responses: %{          200 => Operation.response("Report", "application/json", report()), @@ -86,6 +87,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.ReportOperation do        summary: "Change the state of one or multiple reports",        operationId: "AdminAPI.ReportController.update",        security: [%{"oAuth" => ["write:reports"]}], +      parameters: admin_api_params(),        requestBody: request_body("Parameters", update_request(), required: true),        responses: %{          204 => no_content_response(), @@ -100,7 +102,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.ReportOperation do        tags: ["Admin", "Reports"],        summary: "Create report note",        operationId: "AdminAPI.ReportController.notes_create", -      parameters: [id_param()], +      parameters: [id_param() | admin_api_params()],        requestBody:          request_body("Parameters", %Schema{            type: :object, @@ -124,6 +126,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.ReportOperation do        parameters: [          Operation.parameter(:report_id, :path, :string, "Report ID"),          Operation.parameter(:id, :path, :string, "Note ID") +        | admin_api_params()        ],        security: [%{"oAuth" => ["write:reports"]}],        responses: %{ diff --git a/lib/pleroma/web/api_spec/operations/admin/status_operation.ex b/lib/pleroma/web/api_spec/operations/admin/status_operation.ex index 745399b4b..c105838a4 100644 --- a/lib/pleroma/web/api_spec/operations/admin/status_operation.ex +++ b/lib/pleroma/web/api_spec/operations/admin/status_operation.ex @@ -55,6 +55,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.StatusOperation do            %Schema{type: :integer, default: 50},            "Number of statuses to return"          ) +        | admin_api_params()        ],        responses: %{          200 => @@ -71,7 +72,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.StatusOperation do        tags: ["Admin", "Statuses"],        summary: "Show Status",        operationId: "AdminAPI.StatusController.show", -      parameters: [id_param()], +      parameters: [id_param() | admin_api_params()],        security: [%{"oAuth" => ["read:statuses"]}],        responses: %{          200 => Operation.response("Status", "application/json", status()), @@ -85,7 +86,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.StatusOperation do        tags: ["Admin", "Statuses"],        summary: "Change the scope of an individual reported status",        operationId: "AdminAPI.StatusController.update", -      parameters: [id_param()], +      parameters: [id_param() | admin_api_params()],        security: [%{"oAuth" => ["write:statuses"]}],        requestBody: request_body("Parameters", update_request(), required: true),        responses: %{ @@ -100,7 +101,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.StatusOperation do        tags: ["Admin", "Statuses"],        summary: "Delete an individual reported status",        operationId: "AdminAPI.StatusController.delete", -      parameters: [id_param()], +      parameters: [id_param() | admin_api_params()],        security: [%{"oAuth" => ["write:statuses"]}],        responses: %{          200 => empty_object_response(), diff --git a/lib/pleroma/web/api_spec/operations/pleroma_account_operation.ex b/lib/pleroma/web/api_spec/operations/pleroma_account_operation.ex index 90922c064..97836b2eb 100644 --- a/lib/pleroma/web/api_spec/operations/pleroma_account_operation.ex +++ b/lib/pleroma/web/api_spec/operations/pleroma_account_operation.ex @@ -4,7 +4,6 @@  defmodule Pleroma.Web.ApiSpec.PleromaAccountOperation do    alias OpenApiSpex.Operation -  alias OpenApiSpex.Schema    alias Pleroma.Web.ApiSpec.Schemas.AccountRelationship    alias Pleroma.Web.ApiSpec.Schemas.ApiError    alias Pleroma.Web.ApiSpec.Schemas.FlakeID @@ -40,48 +39,6 @@ defmodule Pleroma.Web.ApiSpec.PleromaAccountOperation do      }    end -  def update_avatar_operation do -    %Operation{ -      tags: ["Accounts"], -      summary: "Set/clear user avatar image", -      operationId: "PleromaAPI.AccountController.update_avatar", -      requestBody: -        request_body("Parameters", update_avatar_or_background_request(), required: true), -      security: [%{"oAuth" => ["write:accounts"]}], -      responses: %{ -        200 => update_response(), -        403 => Operation.response("Forbidden", "application/json", ApiError) -      } -    } -  end - -  def update_banner_operation do -    %Operation{ -      tags: ["Accounts"], -      summary: "Set/clear user banner image", -      operationId: "PleromaAPI.AccountController.update_banner", -      requestBody: request_body("Parameters", update_banner_request(), required: true), -      security: [%{"oAuth" => ["write:accounts"]}], -      responses: %{ -        200 => update_response() -      } -    } -  end - -  def update_background_operation do -    %Operation{ -      tags: ["Accounts"], -      summary: "Set/clear user background image", -      operationId: "PleromaAPI.AccountController.update_background", -      security: [%{"oAuth" => ["write:accounts"]}], -      requestBody: -        request_body("Parameters", update_avatar_or_background_request(), required: true), -      responses: %{ -        200 => update_response() -      } -    } -  end -    def favourites_operation do      %Operation{        tags: ["Accounts"], @@ -136,52 +93,4 @@ defmodule Pleroma.Web.ApiSpec.PleromaAccountOperation do        required: true      )    end - -  defp update_avatar_or_background_request do -    %Schema{ -      title: "PleromaAccountUpdateAvatarOrBackgroundRequest", -      type: :object, -      properties: %{ -        img: %Schema{ -          nullable: true, -          type: :string, -          format: :binary, -          description: "Image encoded using `multipart/form-data` or an empty string to clear" -        } -      } -    } -  end - -  defp update_banner_request do -    %Schema{ -      title: "PleromaAccountUpdateBannerRequest", -      type: :object, -      properties: %{ -        banner: %Schema{ -          type: :string, -          nullable: true, -          format: :binary, -          description: "Image encoded using `multipart/form-data` or an empty string to clear" -        } -      } -    } -  end - -  defp update_response do -    Operation.response("PleromaAccountUpdateResponse", "application/json", %Schema{ -      type: :object, -      properties: %{ -        url: %Schema{ -          type: :string, -          format: :uri, -          nullable: true, -          description: "Image URL" -        } -      }, -      example: %{ -        "url" => -          "https://cofe.party/media/9d0add56-bcb6-4c0f-8225-cbbd0b6dd773/13eadb6972c9ccd3f4ffa3b8196f0e0d38b4d2f27594457c52e52946c054cd9a.gif" -      } -    }) -  end  end diff --git a/lib/pleroma/web/api_spec/operations/status_operation.ex b/lib/pleroma/web/api_spec/operations/status_operation.ex index 0b7fad793..5bd4619d5 100644 --- a/lib/pleroma/web/api_spec/operations/status_operation.ex +++ b/lib/pleroma/web/api_spec/operations/status_operation.ex @@ -84,7 +84,7 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do        operationId: "StatusController.delete",        parameters: [id_param()],        responses: %{ -        200 => empty_object_response(), +        200 => status_response(),          403 => Operation.response("Forbidden", "application/json", ApiError),          404 => Operation.response("Not Found", "application/json", ApiError)        } diff --git a/lib/pleroma/web/api_spec/schemas/account.ex b/lib/pleroma/web/api_spec/schemas/account.ex index d54e2158d..ca79f0747 100644 --- a/lib/pleroma/web/api_spec/schemas/account.ex +++ b/lib/pleroma/web/api_spec/schemas/account.ex @@ -40,33 +40,72 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Account do        pleroma: %Schema{          type: :object,          properties: %{ -          allow_following_move: %Schema{type: :boolean}, -          background_image: %Schema{type: :string, nullable: true}, +          allow_following_move: %Schema{ +            type: :boolean, +            description: "whether the user allows automatically follow moved following accounts" +          }, +          background_image: %Schema{type: :string, nullable: true, format: :uri},            chat_token: %Schema{type: :string}, -          confirmation_pending: %Schema{type: :boolean}, +          confirmation_pending: %Schema{ +            type: :boolean, +            description: +              "whether the user account is waiting on email confirmation to be activated" +          },            hide_favorites: %Schema{type: :boolean}, -          hide_followers_count: %Schema{type: :boolean}, -          hide_followers: %Schema{type: :boolean}, -          hide_follows_count: %Schema{type: :boolean}, -          hide_follows: %Schema{type: :boolean}, -          is_admin: %Schema{type: :boolean}, -          is_moderator: %Schema{type: :boolean}, +          hide_followers_count: %Schema{ +            type: :boolean, +            description: "whether the user has follower stat hiding enabled" +          }, +          hide_followers: %Schema{ +            type: :boolean, +            description: "whether the user has follower hiding enabled" +          }, +          hide_follows_count: %Schema{ +            type: :boolean, +            description: "whether the user has follow stat hiding enabled" +          }, +          hide_follows: %Schema{ +            type: :boolean, +            description: "whether the user has follow hiding enabled" +          }, +          is_admin: %Schema{ +            type: :boolean, +            description: "whether the user is an admin of the local instance" +          }, +          is_moderator: %Schema{ +            type: :boolean, +            description: "whether the user is a moderator of the local instance" +          },            skip_thread_containment: %Schema{type: :boolean}, -          tags: %Schema{type: :array, items: %Schema{type: :string}}, -          unread_conversation_count: %Schema{type: :integer}, +          tags: %Schema{ +            type: :array, +            items: %Schema{type: :string}, +            description: +              "List of tags being used for things like extra roles or moderation(ie. marking all media as nsfw all)." +          }, +          unread_conversation_count: %Schema{ +            type: :integer, +            description: "The count of unread conversations. Only returned to the account owner." +          },            notification_settings: %Schema{              type: :object,              properties: %{ -              followers: %Schema{type: :boolean}, -              follows: %Schema{type: :boolean}, -              non_followers: %Schema{type: :boolean}, -              non_follows: %Schema{type: :boolean}, -              privacy_option: %Schema{type: :boolean} +              block_from_strangers: %Schema{type: :boolean}, +              hide_notification_contents: %Schema{type: :boolean}              }            },            relationship: AccountRelationship,            settings_store: %Schema{ -            type: :object +            type: :object, +            description: +              "A generic map of settings for frontends. Opaque to the backend. Only returned in `verify_credentials` and `update_credentials`" +          }, +          accepts_chat_messages: %Schema{type: :boolean, nullable: true}, +          favicon: %Schema{ +            type: :string, +            format: :uri, +            nullable: true, +            description: "Favicon image of the user's instance"            }          }        }, @@ -74,16 +113,32 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Account do          type: :object,          properties: %{            fields: %Schema{type: :array, items: AccountField}, -          note: %Schema{type: :string}, +          note: %Schema{ +            type: :string, +            description: +              "Plaintext version of the bio without formatting applied by the backend, used for editing the bio." +          },            privacy: VisibilityScope,            sensitive: %Schema{type: :boolean},            pleroma: %Schema{              type: :object,              properties: %{                actor_type: ActorType, -              discoverable: %Schema{type: :boolean}, -              no_rich_text: %Schema{type: :boolean}, -              show_role: %Schema{type: :boolean} +              discoverable: %Schema{ +                type: :boolean, +                description: +                  "whether the user allows discovery of the account in search results and other services." +              }, +              no_rich_text: %Schema{ +                type: :boolean, +                description: +                  "whether the HTML tags for rich-text formatting are stripped from all statuses requested from the API." +              }, +              show_role: %Schema{ +                type: :boolean, +                description: +                  "whether the user wants their role (e.g admin, moderator) to be shown" +              }              }            }          } @@ -118,16 +173,14 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Account do          "is_admin" => false,          "is_moderator" => false,          "skip_thread_containment" => false, +        "accepts_chat_messages" => true,          "chat_token" =>            "SFMyNTY.g3QAAAACZAAEZGF0YW0AAAASOXRLaTNlc2JHN09RZ1oyOTIwZAAGc2lnbmVkbgYARNplS3EB.Mb_Iaqew2bN1I1o79B_iP7encmVCpTKC4OtHZRxdjKc",          "unread_conversation_count" => 0,          "tags" => [],          "notification_settings" => %{ -          "followers" => true, -          "follows" => true, -          "non_followers" => true, -          "non_follows" => true, -          "privacy_option" => false +          "block_from_strangers" => false, +          "hide_notification_contents" => false          },          "relationship" => %{            "blocked_by" => false, diff --git a/lib/pleroma/web/api_spec/schemas/status.ex b/lib/pleroma/web/api_spec/schemas/status.ex index 8b87cb25b..947e42890 100644 --- a/lib/pleroma/web/api_spec/schemas/status.ex +++ b/lib/pleroma/web/api_spec/schemas/status.ex @@ -62,6 +62,11 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Status do          }        },        content: %Schema{type: :string, format: :html, description: "HTML-encoded status content"}, +      text: %Schema{ +        type: :string, +        description: "Original unformatted content in plain text", +        nullable: true +      },        created_at: %Schema{          type: :string,          format: "date-time", @@ -184,6 +189,10 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Status do            thread_muted: %Schema{              type: :boolean,              description: "`true` if the thread the post belongs to is muted" +          }, +          parent_visible: %Schema{ +            type: :boolean, +            description: "`true` if the parent post is visible to the user"            }          }        }, diff --git a/lib/pleroma/web/common_api/activity_draft.ex b/lib/pleroma/web/common_api/activity_draft.ex index 9bcb9f587..f849b2e01 100644 --- a/lib/pleroma/web/common_api/activity_draft.ex +++ b/lib/pleroma/web/common_api/activity_draft.ex @@ -186,6 +186,7 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do          draft.poll        )        |> Map.put("emoji", emoji) +      |> Map.put("source", draft.status)      %__MODULE__{draft | object: object}    end diff --git a/lib/pleroma/web/common_api/common_api.ex b/lib/pleroma/web/common_api/common_api.ex index 04e081a8e..4d5b0decf 100644 --- a/lib/pleroma/web/common_api/common_api.ex +++ b/lib/pleroma/web/common_api/common_api.ex @@ -25,6 +25,13 @@ defmodule Pleroma.Web.CommonAPI do    require Pleroma.Constants    require Logger +  def block(blocker, blocked) do +    with {:ok, block_data, _} <- Builder.block(blocker, blocked), +         {:ok, block, _} <- Pipeline.common_pipeline(block_data, local: true) do +      {:ok, block} +    end +  end +    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_content_length(content, !!maybe_attachment), @@ -94,10 +101,14 @@ defmodule Pleroma.Web.CommonAPI do    def follow(follower, followed) do      timeout = Pleroma.Config.get([:activitypub, :follow_handshake_timeout]) -    with {:ok, follower} <- User.maybe_direct_follow(follower, followed), -         {:ok, activity} <- ActivityPub.follow(follower, followed), +    with {:ok, follow_data, _} <- Builder.follow(follower, followed), +         {:ok, activity, _} <- Pipeline.common_pipeline(follow_data, local: true),           {:ok, follower, followed} <- User.wait_and_refresh(timeout, follower, followed) do -      {:ok, follower, followed, activity} +      if activity.data["state"] == "reject" do +        {:error, :rejected} +      else +        {:ok, follower, followed, activity} +      end      end    end diff --git a/lib/pleroma/web/common_api/utils.ex b/lib/pleroma/web/common_api/utils.ex index 15594125f..9c38b73eb 100644 --- a/lib/pleroma/web/common_api/utils.ex +++ b/lib/pleroma/web/common_api/utils.ex @@ -143,7 +143,7 @@ defmodule Pleroma.Web.CommonAPI.Utils do    def make_poll_data(%{poll: %{options: options, expires_in: expires_in}} = data)        when is_list(options) do -    limits = Pleroma.Config.get([:instance, :poll_limits]) +    limits = Config.get([:instance, :poll_limits])      with :ok <- validate_poll_expiration(expires_in, limits),           :ok <- validate_poll_options_amount(options, limits), @@ -502,7 +502,7 @@ defmodule Pleroma.Web.CommonAPI.Utils do    def make_report_content_html(nil), do: {:ok, {nil, [], []}}    def make_report_content_html(comment) do -    max_size = Pleroma.Config.get([:instance, :max_report_comment_size], 1000) +    max_size = Config.get([:instance, :max_report_comment_size], 1000)      if String.length(comment) <= max_size do        {:ok, format_input(comment, "text/plain")} @@ -564,7 +564,7 @@ defmodule Pleroma.Web.CommonAPI.Utils do    end    def validate_character_limit(full_payload, _attachments) do -    limit = Pleroma.Config.get([:instance, :limit]) +    limit = Config.get([:instance, :limit])      length = String.length(full_payload)      if length <= limit do diff --git a/lib/pleroma/web/fallback_redirect_controller.ex b/lib/pleroma/web/fallback_redirect_controller.ex index 0d9d578fc..431ad5485 100644 --- a/lib/pleroma/web/fallback_redirect_controller.ex +++ b/lib/pleroma/web/fallback_redirect_controller.ex @@ -9,6 +9,7 @@ defmodule Fallback.RedirectController do    alias Pleroma.User    alias Pleroma.Web.Metadata +  alias Pleroma.Web.Preload    def api_not_implemented(conn, _params) do      conn @@ -16,16 +17,7 @@ defmodule Fallback.RedirectController do      |> json(%{error: "Not implemented"})    end -  def redirector(conn, _params, code \\ 200) - -  # redirect to admin section -  # /pleroma/admin -> /pleroma/admin/ -  # -  def redirector(conn, %{"path" => ["pleroma", "admin"]} = _, _code) do -    redirect(conn, to: "/pleroma/admin/") -  end - -  def redirector(conn, _params, code) do +  def redirector(conn, _params, code \\ 200) do      conn      |> put_resp_content_type("text/html")      |> send_file(code, index_file_path()) @@ -43,28 +35,33 @@ defmodule Fallback.RedirectController do    def redirector_with_meta(conn, params) do      {:ok, index_content} = File.read(index_file_path()) -    tags = -      try do -        Metadata.build_tags(params) -      rescue -        e -> -          Logger.error( -            "Metadata rendering for #{conn.request_path} failed.\n" <> -              Exception.format(:error, e, __STACKTRACE__) -          ) - -          "" -      end +    tags = build_tags(conn, params) +    preloads = preload_data(conn, params) -    response = String.replace(index_content, "<!--server-generated-meta-->", tags) +    response = +      index_content +      |> String.replace("<!--server-generated-meta-->", tags <> preloads)      conn      |> put_resp_content_type("text/html")      |> send_resp(200, response)    end -  def index_file_path do -    Pleroma.Plugs.InstanceStatic.file_path("index.html") +  def redirector_with_preload(conn, %{"path" => ["pleroma", "admin"]}) do +    redirect(conn, to: "/pleroma/admin/") +  end + +  def redirector_with_preload(conn, params) do +    {:ok, index_content} = File.read(index_file_path()) +    preloads = preload_data(conn, params) + +    response = +      index_content +      |> String.replace("<!--server-generated-meta-->", preloads) + +    conn +    |> put_resp_content_type("text/html") +    |> send_resp(200, response)    end    def registration_page(conn, params) do @@ -76,4 +73,36 @@ defmodule Fallback.RedirectController do      |> put_status(204)      |> text("")    end + +  defp index_file_path do +    Pleroma.Plugs.InstanceStatic.file_path("index.html") +  end + +  defp build_tags(conn, params) do +    try do +      Metadata.build_tags(params) +    rescue +      e -> +        Logger.error( +          "Metadata rendering for #{conn.request_path} failed.\n" <> +            Exception.format(:error, e, __STACKTRACE__) +        ) + +        "" +    end +  end + +  defp preload_data(conn, params) do +    try do +      Preload.build_tags(conn, params) +    rescue +      e -> +        Logger.error( +          "Preloading for #{conn.request_path} failed.\n" <> +            Exception.format(:error, e, __STACKTRACE__) +        ) + +        "" +    end +  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 7a88a847c..fe5d022f5 100644 --- a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex @@ -27,6 +27,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do    alias Pleroma.Web.MastodonAPI.MastodonAPI    alias Pleroma.Web.MastodonAPI.MastodonAPIController    alias Pleroma.Web.MastodonAPI.StatusView +  alias Pleroma.Web.OAuth.OAuthView    alias Pleroma.Web.OAuth.Token    alias Pleroma.Web.TwitterAPI.TwitterAPI @@ -101,12 +102,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do           :ok <- TwitterAPI.validate_captcha(app, params),           {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),           {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do -      json(conn, %{ -        token_type: "Bearer", -        access_token: token.token, -        scope: app.scopes, -        created_at: Token.Utils.format_created_at(token) -      }) +      json(conn, OAuthView.render("token.json", %{user: user, token: token}))      else        {:error, error} -> json_response(conn, :bad_request, %{error: error})      end @@ -148,6 +144,13 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do        |> Enum.filter(fn {_, value} -> not is_nil(value) end)        |> Enum.into(%{}) +    # We use an empty string as a special value to reset +    # avatars, banners, backgrounds +    user_image_value = fn +      "" -> {:ok, nil} +      value -> {:ok, value} +    end +      user_params =        [          :no_rich_text, @@ -160,7 +163,8 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do          :show_role,          :skip_thread_containment,          :allow_following_move, -        :discoverable +        :discoverable, +        :accepts_chat_messages        ]        |> Enum.reduce(%{}, fn key, acc ->          Maps.put_if_present(acc, key, params[key], &{:ok, truthy_param?(&1)}) @@ -168,9 +172,9 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do        |> Maps.put_if_present(:name, params[:display_name])        |> Maps.put_if_present(:bio, params[:note])        |> Maps.put_if_present(:raw_bio, params[:note]) -      |> Maps.put_if_present(:avatar, params[:avatar]) -      |> Maps.put_if_present(:banner, params[:header]) -      |> Maps.put_if_present(:background, params[:pleroma_background_image]) +      |> Maps.put_if_present(:avatar, params[:avatar], user_image_value) +      |> Maps.put_if_present(:banner, params[:header], user_image_value) +      |> Maps.put_if_present(:background, params[:pleroma_background_image], user_image_value)        |> Maps.put_if_present(          :raw_fields,          params[:fields_attributes], @@ -346,7 +350,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do      {:error, "Can not follow yourself"}    end -  def follow(%{assigns: %{user: follower, account: followed}} = conn, params) do +  def follow(%{body_params: params, assigns: %{user: follower, account: followed}} = conn, _) do      with {:ok, follower} <- MastodonAPI.follow(follower, followed, params) do        render(conn, "relationship.json", user: follower, target: followed)      else @@ -385,8 +389,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do    @doc "POST /api/v1/accounts/:id/block"    def block(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do -    with {:ok, _user_block} <- User.block(blocker, blocked), -         {:ok, _activity} <- ActivityPub.block(blocker, blocked) do +    with {:ok, _activity} <- CommonAPI.block(blocker, blocked) do        render(conn, "relationship.json", user: blocker, target: blocked)      else        {:error, message} -> json_response(conn, :forbidden, %{error: message}) diff --git a/lib/pleroma/web/mastodon_api/controllers/search_controller.ex b/lib/pleroma/web/mastodon_api/controllers/search_controller.ex index e50980122..29affa7d5 100644 --- a/lib/pleroma/web/mastodon_api/controllers/search_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/search_controller.ex @@ -44,6 +44,7 @@ defmodule Pleroma.Web.MastodonAPI.SearchController do    def search(conn, params), do: do_search(:v1, conn, params)    defp do_search(version, %{assigns: %{user: user}} = conn, %{q: query} = params) do +    query = String.trim(query)      options = search_options(params, user)      timeout = Keyword.get(Repo.config(), :timeout, 15_000)      default_values = %{"statuses" => [], "accounts" => [], "hashtags" => []} diff --git a/lib/pleroma/web/mastodon_api/controllers/status_controller.ex b/lib/pleroma/web/mastodon_api/controllers/status_controller.ex index 468b44b67..9bb2ef117 100644 --- a/lib/pleroma/web/mastodon_api/controllers/status_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/status_controller.ex @@ -172,6 +172,11 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do          with_direct_conversation_id: true        )      else +      {:error, {:reject, message}} -> +        conn +        |> put_status(:unprocessable_entity) +        |> json(%{error: message}) +        {:error, message} ->          conn          |> put_status(:unprocessable_entity) @@ -200,11 +205,16 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do    @doc "DELETE /api/v1/statuses/:id"    def delete(%{assigns: %{user: user}} = conn, %{id: id}) do -    with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do -      json(conn, %{}) +    with %Activity{} = activity <- Activity.get_by_id_with_object(id), +         {:ok, %Activity{}} <- CommonAPI.delete(id, user) do +      try_render(conn, "show.json", +        activity: activity, +        for: user, +        with_direct_conversation_id: true, +        with_source: true +      )      else -      {:error, :not_found} = e -> e -      _e -> render_error(conn, :forbidden, "Can't delete this post") +      _e -> {:error, :not_found}      end    end diff --git a/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex b/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex index 4bdd46d7e..ab7b1d6aa 100644 --- a/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex @@ -88,21 +88,20 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do      )    end +  defp restrict_unauthenticated?(true = _local_only) do +    Pleroma.Config.get([:restrict_unauthenticated, :timelines, :local]) +  end + +  defp restrict_unauthenticated?(_) do +    Pleroma.Config.get([:restrict_unauthenticated, :timelines, :federated]) +  end +    # GET /api/v1/timelines/public    def public(%{assigns: %{user: user}} = conn, params) do      local_only = params[:local] -    cfg_key = -      if local_only do -        :local -      else -        :federated -      end - -    restrict? = Pleroma.Config.get([:restrict_unauthenticated, :timelines, cfg_key]) - -    if restrict? and is_nil(user) do -      render_error(conn, :unauthorized, "authorization required for timeline view") +    if is_nil(user) and restrict_unauthenticated?(local_only) do +      fail_on_bad_auth(conn)      else        activities =          params @@ -123,6 +122,10 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do      end    end +  defp fail_on_bad_auth(conn) do +    render_error(conn, :unauthorized, "authorization required for timeline view") +  end +    defp hashtag_fetching(params, user, local_only) do      tags =        [params[:tag], params[:any]] @@ -157,15 +160,20 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do    # GET /api/v1/timelines/tag/:tag    def hashtag(%{assigns: %{user: user}} = conn, params) do      local_only = params[:local] -    activities = hashtag_fetching(params, user, local_only) -    conn -    |> add_link_headers(activities, %{"local" => local_only}) -    |> render("index.json", -      activities: activities, -      for: user, -      as: :activity -    ) +    if is_nil(user) and restrict_unauthenticated?(local_only) do +      fail_on_bad_auth(conn) +    else +      activities = hashtag_fetching(params, user, local_only) + +      conn +      |> add_link_headers(activities, %{"local" => local_only}) +      |> render("index.json", +        activities: activities, +        for: user, +        as: :activity +      ) +    end    end    # GET /api/v1/timelines/list/:list_id diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex index a6e64b4ab..bc9745044 100644 --- a/lib/pleroma/web/mastodon_api/views/account_view.ex +++ b/lib/pleroma/web/mastodon_api/views/account_view.ex @@ -204,6 +204,18 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do          %{}        end +    favicon = +      if Pleroma.Config.get([:instances_favicons, :enabled]) do +        user +        |> Map.get(:ap_id, "") +        |> URI.parse() +        |> URI.merge("/") +        |> Pleroma.Instances.Instance.get_or_update_favicon() +        |> MediaProxy.url() +      else +        nil +      end +      %{        id: to_string(user.id),        username: username_from_nickname(user.nickname), @@ -245,7 +257,9 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do          hide_favorites: user.hide_favorites,          relationship: relationship,          skip_thread_containment: user.skip_thread_containment, -        background_image: image_url(user.background) |> MediaProxy.url() +        background_image: image_url(user.background) |> MediaProxy.url(), +        accepts_chat_messages: user.accepts_chat_messages, +        favicon: favicon        }      }      |> maybe_put_role(user, opts[:for]) diff --git a/lib/pleroma/web/mastodon_api/views/instance_view.ex b/lib/pleroma/web/mastodon_api/views/instance_view.ex index 35c2fc25c..5deb0d7ed 100644 --- a/lib/pleroma/web/mastodon_api/views/instance_view.ex +++ b/lib/pleroma/web/mastodon_api/views/instance_view.ex @@ -34,10 +34,14 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do        background_upload_limit: Keyword.get(instance, :background_upload_limit),        banner_upload_limit: Keyword.get(instance, :banner_upload_limit),        background_image: Keyword.get(instance, :background_image), +      chat_limit: Keyword.get(instance, :chat_limit), +      description_limit: Keyword.get(instance, :description_limit),        pleroma: %{          metadata: %{ +          account_activation_required: Keyword.get(instance, :account_activation_required),            features: features(), -          federation: federation() +          federation: federation(), +          fields_limits: fields_limits()          },          vapid_public_key: Keyword.get(Pleroma.Web.Push.vapid_config(), :public_key)        } @@ -88,4 +92,13 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do      end      |> Map.put(:enabled, Config.get([:instance, :federating]))    end + +  def fields_limits do +    %{ +      max_fields: Config.get([:instance, :max_account_fields]), +      max_remote_fields: Config.get([:instance, :max_remote_account_fields]), +      name_length: Config.get([:instance, :account_field_name_length]), +      value_length: Config.get([:instance, :account_field_value_length]) +    } +  end  end diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex index 2c49bedb3..fa9d695f3 100644 --- a/lib/pleroma/web/mastodon_api/views/status_view.ex +++ b/lib/pleroma/web/mastodon_api/views/status_view.ex @@ -21,7 +21,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do    alias Pleroma.Web.MastodonAPI.StatusView    alias Pleroma.Web.MediaProxy -  import Pleroma.Web.ActivityPub.Visibility, only: [get_visibility: 1] +  import Pleroma.Web.ActivityPub.Visibility, only: [get_visibility: 1, visible_for_user?: 2]    # TODO: Add cached version.    defp get_replied_to_activities([]), do: %{} @@ -333,6 +333,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do        reblog: nil,        card: card,        content: content_html, +      text: opts[:with_source] && object.data["source"],        created_at: created_at,        reblogs_count: announcement_count,        replies_count: object.data["repliesCount"] || 0, @@ -364,7 +365,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do          expires_at: expires_at,          direct_conversation_id: direct_conversation_id,          thread_muted: thread_muted?, -        emoji_reactions: emoji_reactions +        emoji_reactions: emoji_reactions, +        parent_visible: visible_for_user?(reply_to, opts[:for])        }      }    end diff --git a/lib/pleroma/web/media_proxy/media_proxy.ex b/lib/pleroma/web/media_proxy/media_proxy.ex index 077fabe47..dfbfcea6b 100644 --- a/lib/pleroma/web/media_proxy/media_proxy.ex +++ b/lib/pleroma/web/media_proxy/media_proxy.ex @@ -60,22 +60,28 @@ defmodule Pleroma.Web.MediaProxy do    defp whitelisted?(url) do      %{host: domain} = URI.parse(url) -    mediaproxy_whitelist = Config.get([:media_proxy, :whitelist]) - -    upload_base_url_domain = -      if !is_nil(Config.get([Upload, :base_url])) do -        [URI.parse(Config.get([Upload, :base_url])).host] +    mediaproxy_whitelist_domains = +      [:media_proxy, :whitelist] +      |> Config.get() +      |> Enum.map(&maybe_get_domain_from_url/1) + +    whitelist_domains = +      if base_url = Config.get([Upload, :base_url]) do +        %{host: base_domain} = URI.parse(base_url) +        [base_domain | mediaproxy_whitelist_domains]        else -        [] +        mediaproxy_whitelist_domains        end -    whitelist = mediaproxy_whitelist ++ upload_base_url_domain +    domain in whitelist_domains +  end -    Enum.any?(whitelist, fn pattern -> -      String.equivalent?(domain, pattern) -    end) +  defp maybe_get_domain_from_url("http" <> _ = url) do +    URI.parse(url).host    end +  defp maybe_get_domain_from_url(domain), do: domain +    def encode_url(url) do      base64 = Base.url_encode64(url, @base64_opts) @@ -106,7 +112,7 @@ defmodule Pleroma.Web.MediaProxy do    def build_url(sig_base64, url_base64, filename \\ nil) do      [ -      Pleroma.Config.get([:media_proxy, :base_url], Web.base_url()), +      Config.get([:media_proxy, :base_url], Web.base_url()),        "proxy",        sig_base64,        url_base64, diff --git a/lib/pleroma/web/nodeinfo/nodeinfo.ex b/lib/pleroma/web/nodeinfo/nodeinfo.ex new file mode 100644 index 000000000..47fa46376 --- /dev/null +++ b/lib/pleroma/web/nodeinfo/nodeinfo.ex @@ -0,0 +1,91 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Nodeinfo.Nodeinfo do +  alias Pleroma.Config +  alias Pleroma.Stats +  alias Pleroma.User +  alias Pleroma.Web.Federator.Publisher +  alias Pleroma.Web.MastodonAPI.InstanceView + +  # returns a nodeinfo 2.0 map, since 2.1 just adds a repository field +  # under software. +  def get_nodeinfo("2.0") do +    stats = Stats.get_stats() + +    staff_accounts = +      User.all_superusers() +      |> Enum.map(fn u -> u.ap_id end) + +    federation = InstanceView.federation() +    features = InstanceView.features() + +    %{ +      version: "2.0", +      software: %{ +        name: Pleroma.Application.name() |> String.downcase(), +        version: Pleroma.Application.version() +      }, +      protocols: Publisher.gather_nodeinfo_protocol_names(), +      services: %{ +        inbound: [], +        outbound: [] +      }, +      openRegistrations: Config.get([:instance, :registrations_open]), +      usage: %{ +        users: %{ +          total: Map.get(stats, :user_count, 0) +        }, +        localPosts: Map.get(stats, :status_count, 0) +      }, +      metadata: %{ +        nodeName: Config.get([:instance, :name]), +        nodeDescription: Config.get([:instance, :description]), +        private: !Config.get([:instance, :public], true), +        suggestions: %{ +          enabled: false +        }, +        staffAccounts: staff_accounts, +        federation: federation, +        pollLimits: Config.get([:instance, :poll_limits]), +        postFormats: Config.get([:instance, :allowed_post_formats]), +        uploadLimits: %{ +          general: Config.get([:instance, :upload_limit]), +          avatar: Config.get([:instance, :avatar_upload_limit]), +          banner: Config.get([:instance, :banner_upload_limit]), +          background: Config.get([:instance, :background_upload_limit]) +        }, +        fieldsLimits: %{ +          maxFields: Config.get([:instance, :max_account_fields]), +          maxRemoteFields: Config.get([:instance, :max_remote_account_fields]), +          nameLength: Config.get([:instance, :account_field_name_length]), +          valueLength: Config.get([:instance, :account_field_value_length]) +        }, +        accountActivationRequired: Config.get([:instance, :account_activation_required], false), +        invitesEnabled: Config.get([:instance, :invites_enabled], false), +        mailerEnabled: Config.get([Pleroma.Emails.Mailer, :enabled], false), +        features: features, +        restrictedNicknames: Config.get([Pleroma.User, :restricted_nicknames]), +        skipThreadContainment: Config.get([:instance, :skip_thread_containment], false) +      } +    } +  end + +  def get_nodeinfo("2.1") do +    raw_response = get_nodeinfo("2.0") + +    updated_software = +      raw_response +      |> Map.get(:software) +      |> Map.put(:repository, Pleroma.Application.repository()) + +    raw_response +    |> Map.put(:software, updated_software) +    |> Map.put(:version, "2.1") +  end + +  def get_nodeinfo(_version) do +    {:error, :missing} +  end +end diff --git a/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex b/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex index 721b599d4..8c7a9e565 100644 --- a/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex +++ b/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex @@ -5,12 +5,8 @@  defmodule Pleroma.Web.Nodeinfo.NodeinfoController do    use Pleroma.Web, :controller -  alias Pleroma.Config -  alias Pleroma.Stats -  alias Pleroma.User    alias Pleroma.Web -  alias Pleroma.Web.Federator.Publisher -  alias Pleroma.Web.MastodonAPI.InstanceView +  alias Pleroma.Web.Nodeinfo.Nodeinfo    def schemas(conn, _params) do      response = %{ @@ -29,102 +25,20 @@ defmodule Pleroma.Web.Nodeinfo.NodeinfoController do      json(conn, response)    end -  # returns a nodeinfo 2.0 map, since 2.1 just adds a repository field -  # under software. -  def raw_nodeinfo do -    stats = Stats.get_stats() - -    staff_accounts = -      User.all_superusers() -      |> Enum.map(fn u -> u.ap_id end) - -    features = InstanceView.features() -    federation = InstanceView.federation() - -    %{ -      version: "2.0", -      software: %{ -        name: Pleroma.Application.name() |> String.downcase(), -        version: Pleroma.Application.version() -      }, -      protocols: Publisher.gather_nodeinfo_protocol_names(), -      services: %{ -        inbound: [], -        outbound: [] -      }, -      openRegistrations: Config.get([:instance, :registrations_open]), -      usage: %{ -        users: %{ -          total: Map.get(stats, :user_count, 0) -        }, -        localPosts: Map.get(stats, :status_count, 0) -      }, -      metadata: %{ -        nodeName: Config.get([:instance, :name]), -        nodeDescription: Config.get([:instance, :description]), -        private: !Config.get([:instance, :public], true), -        suggestions: %{ -          enabled: false -        }, -        staffAccounts: staff_accounts, -        federation: federation, -        pollLimits: Config.get([:instance, :poll_limits]), -        postFormats: Config.get([:instance, :allowed_post_formats]), -        uploadLimits: %{ -          general: Config.get([:instance, :upload_limit]), -          avatar: Config.get([:instance, :avatar_upload_limit]), -          banner: Config.get([:instance, :banner_upload_limit]), -          background: Config.get([:instance, :background_upload_limit]) -        }, -        fieldsLimits: %{ -          maxFields: Config.get([:instance, :max_account_fields]), -          maxRemoteFields: Config.get([:instance, :max_remote_account_fields]), -          nameLength: Config.get([:instance, :account_field_name_length]), -          valueLength: Config.get([:instance, :account_field_value_length]) -        }, -        accountActivationRequired: Config.get([:instance, :account_activation_required], false), -        invitesEnabled: Config.get([:instance, :invites_enabled], false), -        mailerEnabled: Config.get([Pleroma.Emails.Mailer, :enabled], false), -        features: features, -        restrictedNicknames: Config.get([Pleroma.User, :restricted_nicknames]), -        skipThreadContainment: Config.get([:instance, :skip_thread_containment], false) -      } -    } -  end -    # Schema definition: https://github.com/jhass/nodeinfo/blob/master/schemas/2.0/schema.json    # and https://github.com/jhass/nodeinfo/blob/master/schemas/2.1/schema.json -  def nodeinfo(conn, %{"version" => "2.0"}) do -    conn -    |> put_resp_header( -      "content-type", -      "application/json; profile=http://nodeinfo.diaspora.software/ns/schema/2.0#; charset=utf-8" -    ) -    |> json(raw_nodeinfo()) -  end - -  def nodeinfo(conn, %{"version" => "2.1"}) do -    raw_response = raw_nodeinfo() - -    updated_software = -      raw_response -      |> Map.get(:software) -      |> Map.put(:repository, Pleroma.Application.repository()) - -    response = -      raw_response -      |> Map.put(:software, updated_software) -      |> Map.put(:version, "2.1") - -    conn -    |> put_resp_header( -      "content-type", -      "application/json; profile=http://nodeinfo.diaspora.software/ns/schema/2.1#; charset=utf-8" -    ) -    |> json(response) -  end - -  def nodeinfo(conn, _) do -    render_error(conn, :not_found, "Nodeinfo schema version not handled") +  def nodeinfo(conn, %{"version" => version}) do +    case Nodeinfo.get_nodeinfo(version) do +      {:error, :missing} -> +        render_error(conn, :not_found, "Nodeinfo schema version not handled") + +      node_info -> +        conn +        |> put_resp_header( +          "content-type", +          "application/json; profile=http://nodeinfo.diaspora.software/ns/schema/2.0#; charset=utf-8" +        ) +        |> json(node_info) +    end    end  end diff --git a/lib/pleroma/web/oauth/mfa_controller.ex b/lib/pleroma/web/oauth/mfa_controller.ex index 53e19f82e..f102c93e7 100644 --- a/lib/pleroma/web/oauth/mfa_controller.ex +++ b/lib/pleroma/web/oauth/mfa_controller.ex @@ -13,6 +13,7 @@ defmodule Pleroma.Web.OAuth.MFAController do    alias Pleroma.Web.Auth.TOTPAuthenticator    alias Pleroma.Web.OAuth.MFAView, as: View    alias Pleroma.Web.OAuth.OAuthController +  alias Pleroma.Web.OAuth.OAuthView    alias Pleroma.Web.OAuth.Token    plug(:fetch_session when action in [:show, :verify]) @@ -74,7 +75,7 @@ defmodule Pleroma.Web.OAuth.MFAController do           {:ok, %{user: user, authorization: auth}} <- MFA.Token.validate(mfa_token),           {:ok, _} <- validates_challenge(user, params),           {:ok, token} <- Token.exchange_token(app, auth) do -      json(conn, Token.Response.build(user, token)) +      json(conn, OAuthView.render("token.json", %{user: user, token: token}))      else        _error ->          conn diff --git a/lib/pleroma/web/oauth/mfa_view.ex b/lib/pleroma/web/oauth/mfa_view.ex index 41d5578dc..5d87db268 100644 --- a/lib/pleroma/web/oauth/mfa_view.ex +++ b/lib/pleroma/web/oauth/mfa_view.ex @@ -5,4 +5,13 @@  defmodule Pleroma.Web.OAuth.MFAView do    use Pleroma.Web, :view    import Phoenix.HTML.Form +  alias Pleroma.MFA + +  def render("mfa_response.json", %{token: token, user: user}) do +    %{ +      error: "mfa_required", +      mfa_token: token.token, +      supported_challenge_types: MFA.supported_methods(user) +    } +  end  end diff --git a/lib/pleroma/web/oauth/oauth_controller.ex b/lib/pleroma/web/oauth/oauth_controller.ex index c557778ca..7683589cf 100644 --- a/lib/pleroma/web/oauth/oauth_controller.ex +++ b/lib/pleroma/web/oauth/oauth_controller.ex @@ -17,6 +17,8 @@ defmodule Pleroma.Web.OAuth.OAuthController do    alias Pleroma.Web.OAuth.App    alias Pleroma.Web.OAuth.Authorization    alias Pleroma.Web.OAuth.MFAController +  alias Pleroma.Web.OAuth.MFAView +  alias Pleroma.Web.OAuth.OAuthView    alias Pleroma.Web.OAuth.Scopes    alias Pleroma.Web.OAuth.Token    alias Pleroma.Web.OAuth.Token.Strategy.RefreshToken @@ -233,9 +235,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do      with {:ok, app} <- Token.Utils.fetch_app(conn),           {:ok, %{user: user} = token} <- Token.get_by_refresh_token(app, token),           {:ok, token} <- RefreshToken.grant(token) do -      response_attrs = %{created_at: Token.Utils.format_created_at(token)} - -      json(conn, Token.Response.build(user, token, response_attrs)) +      json(conn, OAuthView.render("token.json", %{user: user, token: token}))      else        _error -> render_invalid_credentials_error(conn)      end @@ -247,9 +247,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do           {:ok, auth} <- Authorization.get_by_token(app, fixed_token),           %User{} = user <- User.get_cached_by_id(auth.user_id),           {:ok, token} <- Token.exchange_token(app, auth) do -      response_attrs = %{created_at: Token.Utils.format_created_at(token)} - -      json(conn, Token.Response.build(user, token, response_attrs)) +      json(conn, OAuthView.render("token.json", %{user: user, token: token}))      else        error ->          handle_token_exchange_error(conn, error) @@ -267,7 +265,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do           {:ok, auth} <- Authorization.create_authorization(app, user, scopes),           {:mfa_required, _, _, false} <- {:mfa_required, user, auth, MFA.require?(user)},           {:ok, token} <- Token.exchange_token(app, auth) do -      json(conn, Token.Response.build(user, token)) +      json(conn, OAuthView.render("token.json", %{user: user, token: token}))      else        error ->          handle_token_exchange_error(conn, error) @@ -290,7 +288,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do      with {:ok, app} <- Token.Utils.fetch_app(conn),           {:ok, auth} <- Authorization.create_authorization(app, %User{}),           {:ok, token} <- Token.exchange_token(app, auth) do -      json(conn, Token.Response.build_for_client_credentials(token)) +      json(conn, OAuthView.render("token.json", %{token: token}))      else        _error ->          handle_token_exchange_error(conn, :invalid_credentails) @@ -548,7 +546,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do    defp build_and_response_mfa_token(user, auth) do      with {:ok, token} <- MFA.Token.create_token(user, auth) do -      Token.Response.build_for_mfa_token(user, token) +      MFAView.render("mfa_response.json", %{token: token, user: user})      end    end diff --git a/lib/pleroma/web/oauth/oauth_view.ex b/lib/pleroma/web/oauth/oauth_view.ex index 94ddaf913..f55247ebd 100644 --- a/lib/pleroma/web/oauth/oauth_view.ex +++ b/lib/pleroma/web/oauth/oauth_view.ex @@ -5,4 +5,26 @@  defmodule Pleroma.Web.OAuth.OAuthView do    use Pleroma.Web, :view    import Phoenix.HTML.Form + +  alias Pleroma.Web.OAuth.Token.Utils + +  def render("token.json", %{token: token} = opts) do +    response = %{ +      token_type: "Bearer", +      access_token: token.token, +      refresh_token: token.refresh_token, +      expires_in: expires_in(), +      scope: Enum.join(token.scopes, " "), +      created_at: Utils.format_created_at(token) +    } + +    if user = opts[:user] do +      response +      |> Map.put(:me, user.ap_id) +    else +      response +    end +  end + +  defp expires_in, do: Pleroma.Config.get([:oauth2, :token_expires_in], 600)  end diff --git a/lib/pleroma/web/oauth/token/response.ex b/lib/pleroma/web/oauth/token/response.ex deleted file mode 100644 index 0e72c31e9..000000000 --- a/lib/pleroma/web/oauth/token/response.ex +++ /dev/null @@ -1,45 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.OAuth.Token.Response do -  @moduledoc false - -  alias Pleroma.MFA -  alias Pleroma.User -  alias Pleroma.Web.OAuth.Token.Utils - -  @doc false -  def build(%User{} = user, token, opts \\ %{}) do -    %{ -      token_type: "Bearer", -      access_token: token.token, -      refresh_token: token.refresh_token, -      expires_in: expires_in(), -      scope: Enum.join(token.scopes, " "), -      me: user.ap_id -    } -    |> Map.merge(opts) -  end - -  def build_for_client_credentials(token) do -    %{ -      token_type: "Bearer", -      access_token: token.token, -      refresh_token: token.refresh_token, -      created_at: Utils.format_created_at(token), -      expires_in: expires_in(), -      scope: Enum.join(token.scopes, " ") -    } -  end - -  def build_for_mfa_token(user, mfa_token) do -    %{ -      error: "mfa_required", -      mfa_token: mfa_token.token, -      supported_challenge_types: MFA.supported_methods(user) -    } -  end - -  defp expires_in, do: Pleroma.Config.get([:oauth2, :token_expires_in], 600) -end diff --git a/lib/pleroma/web/pleroma_api/controllers/account_controller.ex b/lib/pleroma/web/pleroma_api/controllers/account_controller.ex index f3554d919..563edded7 100644 --- a/lib/pleroma/web/pleroma_api/controllers/account_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/account_controller.ex @@ -8,7 +8,6 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do    import Pleroma.Web.ControllerHelper,      only: [json_response: 3, add_link_headers: 2, assign_account_by_id: 2] -  alias Ecto.Changeset    alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug    alias Pleroma.Plugs.OAuthScopesPlug    alias Pleroma.Plugs.RateLimiter @@ -37,17 +36,6 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do    plug(      OAuthScopesPlug, -    %{scopes: ["write:accounts"]} -    # Note: the following actions are not permission-secured in Mastodon: -    when action in [ -           :update_avatar, -           :update_banner, -           :update_background -         ] -  ) - -  plug( -    OAuthScopesPlug,      %{scopes: ["read:favourites"], fallback: :proceed_unauthenticated} when action == :favourites    ) @@ -68,56 +56,6 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do      end    end -  @doc "PATCH /api/v1/pleroma/accounts/update_avatar" -  def update_avatar(%{assigns: %{user: user}, body_params: %{img: ""}} = conn, _) do -    {:ok, _user} = -      user -      |> Changeset.change(%{avatar: nil}) -      |> User.update_and_set_cache() - -    json(conn, %{url: nil}) -  end - -  def update_avatar(%{assigns: %{user: user}, body_params: params} = conn, _params) do -    {:ok, %{data: data}} = ActivityPub.upload(params, type: :avatar) -    {:ok, _user} = user |> Changeset.change(%{avatar: data}) |> User.update_and_set_cache() -    %{"url" => [%{"href" => href} | _]} = data - -    json(conn, %{url: href}) -  end - -  @doc "PATCH /api/v1/pleroma/accounts/update_banner" -  def update_banner(%{assigns: %{user: user}, body_params: %{banner: ""}} = conn, _) do -    with {:ok, _user} <- User.update_banner(user, %{}) do -      json(conn, %{url: nil}) -    end -  end - -  def update_banner(%{assigns: %{user: user}, body_params: params} = conn, _) do -    with {:ok, object} <- ActivityPub.upload(%{img: params[:banner]}, type: :banner), -         {:ok, _user} <- User.update_banner(user, object.data) do -      %{"url" => [%{"href" => href} | _]} = object.data - -      json(conn, %{url: href}) -    end -  end - -  @doc "PATCH /api/v1/pleroma/accounts/update_background" -  def update_background(%{assigns: %{user: user}, body_params: %{img: ""}} = conn, _) do -    with {:ok, _user} <- User.update_background(user, %{}) do -      json(conn, %{url: nil}) -    end -  end - -  def update_background(%{assigns: %{user: user}, body_params: params} = conn, _) do -    with {:ok, object} <- ActivityPub.upload(params, type: :background), -         {:ok, _user} <- User.update_background(user, object.data) do -      %{"url" => [%{"href" => href} | _]} = object.data - -      json(conn, %{url: href}) -    end -  end -    @doc "GET /api/v1/pleroma/accounts/:id/favourites"    def favourites(%{assigns: %{account: %{hide_favorites: true}}} = conn, _params) do      render_error(conn, :forbidden, "Can't get favorites") diff --git a/lib/pleroma/web/preload.ex b/lib/pleroma/web/preload.ex new file mode 100644 index 000000000..90e454468 --- /dev/null +++ b/lib/pleroma/web/preload.ex @@ -0,0 +1,36 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Preload do +  alias Phoenix.HTML +  require Logger + +  def build_tags(_conn, params) do +    preload_data = +      Enum.reduce(Pleroma.Config.get([__MODULE__, :providers], []), %{}, fn parser, acc -> +        terms = +          params +          |> parser.generate_terms() +          |> Enum.map(fn {k, v} -> {k, Base.encode64(Jason.encode!(v))} end) +          |> Enum.into(%{}) + +        Map.merge(acc, terms) +      end) + +    rendered_html = +      preload_data +      |> Jason.encode!() +      |> build_script_tag() +      |> HTML.safe_to_string() + +    rendered_html +  end + +  def build_script_tag(content) do +    HTML.Tag.content_tag(:script, HTML.raw(content), +      id: "initial-results", +      type: "application/json" +    ) +  end +end diff --git a/lib/pleroma/web/preload/instance.ex b/lib/pleroma/web/preload/instance.ex new file mode 100644 index 000000000..50d1f3382 --- /dev/null +++ b/lib/pleroma/web/preload/instance.ex @@ -0,0 +1,50 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Preload.Providers.Instance do +  alias Pleroma.Plugs.InstanceStatic +  alias Pleroma.Web.MastodonAPI.InstanceView +  alias Pleroma.Web.Nodeinfo.Nodeinfo +  alias Pleroma.Web.Preload.Providers.Provider + +  @behaviour Provider +  @instance_url "/api/v1/instance" +  @panel_url "/instance/panel.html" +  @nodeinfo_url "/nodeinfo/2.0.json" + +  @impl Provider +  def generate_terms(_params) do +    %{} +    |> build_info_tag() +    |> build_panel_tag() +    |> build_nodeinfo_tag() +  end + +  defp build_info_tag(acc) do +    info_data = InstanceView.render("show.json", %{}) + +    Map.put(acc, @instance_url, info_data) +  end + +  defp build_panel_tag(acc) do +    instance_path = InstanceStatic.file_path(@panel_url |> to_string()) + +    if File.exists?(instance_path) do +      panel_data = File.read!(instance_path) +      Map.put(acc, @panel_url, panel_data) +    else +      acc +    end +  end + +  defp build_nodeinfo_tag(acc) do +    case Nodeinfo.get_nodeinfo("2.0") do +      {:error, _} -> +        acc + +      nodeinfo_data -> +        Map.put(acc, @nodeinfo_url, nodeinfo_data) +    end +  end +end diff --git a/lib/pleroma/web/preload/provider.ex b/lib/pleroma/web/preload/provider.ex new file mode 100644 index 000000000..7ef595a34 --- /dev/null +++ b/lib/pleroma/web/preload/provider.ex @@ -0,0 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Preload.Providers.Provider do +  @callback generate_terms(map()) :: map() +end diff --git a/lib/pleroma/web/preload/timelines.ex b/lib/pleroma/web/preload/timelines.ex new file mode 100644 index 000000000..57de04051 --- /dev/null +++ b/lib/pleroma/web/preload/timelines.ex @@ -0,0 +1,39 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Preload.Providers.Timelines do +  alias Pleroma.Web.ActivityPub.ActivityPub +  alias Pleroma.Web.MastodonAPI.StatusView +  alias Pleroma.Web.Preload.Providers.Provider + +  @behaviour Provider +  @public_url "/api/v1/timelines/public" + +  @impl Provider +  def generate_terms(params) do +    build_public_tag(%{}, params) +  end + +  def build_public_tag(acc, params) do +    if Pleroma.Config.get([:restrict_unauthenticated, :timelines, :federated], true) do +      acc +    else +      Map.put(acc, @public_url, public_timeline(params)) +    end +  end + +  defp public_timeline(%{"path" => ["main", "all"]}), do: get_public_timeline(false) + +  defp public_timeline(_params), do: get_public_timeline(true) + +  defp get_public_timeline(local_only) do +    activities = +      ActivityPub.fetch_public_activities(%{ +        type: ["Create"], +        local_only: local_only +      }) + +    StatusView.render("index.json", activities: activities, for: nil, as: :activity) +  end +end diff --git a/lib/pleroma/web/preload/user.ex b/lib/pleroma/web/preload/user.ex new file mode 100644 index 000000000..b3d2e9b8d --- /dev/null +++ b/lib/pleroma/web/preload/user.ex @@ -0,0 +1,26 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Preload.Providers.User do +  alias Pleroma.User +  alias Pleroma.Web.MastodonAPI.AccountView +  alias Pleroma.Web.Preload.Providers.Provider + +  @behaviour Provider +  @account_url_base "/api/v1/accounts" + +  @impl Provider +  def generate_terms(%{user: user}) do +    build_accounts_tag(%{}, user) +  end + +  def generate_terms(_params), do: %{} + +  def build_accounts_tag(acc, %User{} = user) do +    account_data = AccountView.render("show.json", %{user: user, for: user}) +    Map.put(acc, "#{@account_url_base}/#{user.id}", account_data) +  end + +  def build_accounts_tag(acc, _), do: acc +end diff --git a/lib/pleroma/web/push/impl.ex b/lib/pleroma/web/push/impl.ex index cdb827e76..16368485e 100644 --- a/lib/pleroma/web/push/impl.ex +++ b/lib/pleroma/web/push/impl.ex @@ -104,7 +104,7 @@ defmodule Pleroma.Web.Push.Impl do    def build_content(          %{ -          user: %{notification_settings: %{privacy_option: true}} +          user: %{notification_settings: %{hide_notification_contents: true}}          } = notification,          _actor,          _object, diff --git a/lib/pleroma/web/rich_media/parser.ex b/lib/pleroma/web/rich_media/parser.ex index ef5ead2da..c8a767935 100644 --- a/lib/pleroma/web/rich_media/parser.ex +++ b/lib/pleroma/web/rich_media/parser.ex @@ -86,7 +86,10 @@ defmodule Pleroma.Web.RichMedia.Parser do        end      try do -      {:ok, %Tesla.Env{body: html}} = Pleroma.HTTP.get(url, [], adapter: opts) +      rich_media_agent = Pleroma.Application.user_agent() <> "; Bot" + +      {:ok, %Tesla.Env{body: html}} = +        Pleroma.HTTP.get(url, [{"user-agent", rich_media_agent}], adapter: opts)        html        |> parse_html() diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 419aa55e4..386308362 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -328,10 +328,6 @@ defmodule Pleroma.Web.Router do        delete("/statuses/:id/reactions/:emoji", EmojiReactionController, :delete)        post("/notifications/read", NotificationController, :mark_as_read) -      patch("/accounts/update_avatar", AccountController, :update_avatar) -      patch("/accounts/update_banner", AccountController, :update_banner) -      patch("/accounts/update_background", AccountController, :update_background) -        get("/mascot", MascotController, :show)        put("/mascot", MascotController, :update) @@ -516,10 +512,6 @@ defmodule Pleroma.Web.Router do    scope "/api", Pleroma.Web do      pipe_through(:config) -    get("/help/test", TwitterAPI.UtilController, :help_test) -    post("/help/test", TwitterAPI.UtilController, :help_test) -    get("/statusnet/config", TwitterAPI.UtilController, :config) -    get("/statusnet/version", TwitterAPI.UtilController, :version)      get("/pleroma/frontend_configurations", TwitterAPI.UtilController, :frontend_configurations)    end @@ -726,7 +718,7 @@ defmodule Pleroma.Web.Router do      get("/registration/:token", RedirectController, :registration_page)      get("/:maybe_nickname_or_id", RedirectController, :redirector_with_meta)      get("/api*path", RedirectController, :api_not_implemented) -    get("/*path", RedirectController, :redirector) +    get("/*path", RedirectController, :redirector_with_preload)      options("/*path", RedirectController, :empty)    end diff --git a/lib/pleroma/web/streamer/streamer.ex b/lib/pleroma/web/streamer/streamer.ex index d1d2c9b9c..d1d70e556 100644 --- a/lib/pleroma/web/streamer/streamer.ex +++ b/lib/pleroma/web/streamer/streamer.ex @@ -104,7 +104,9 @@ defmodule Pleroma.Web.Streamer do      :ok    end -  def filtered_by_user?(%User{} = user, %Activity{} = item) do +  def filtered_by_user?(user, item, streamed_type \\ :activity) + +  def filtered_by_user?(%User{} = user, %Activity{} = item, streamed_type) do      %{block: blocked_ap_ids, mute: muted_ap_ids, reblog_mute: reblog_muted_ap_ids} =        User.outgoing_relationships_ap_ids(user, [:block, :mute, :reblog_mute]) @@ -116,6 +118,9 @@ defmodule Pleroma.Web.Streamer do           true <-             Enum.all?([blocked_ap_ids, muted_ap_ids], &(item.actor not in &1)),           true <- item.data["type"] != "Announce" || item.actor not in reblog_muted_ap_ids, +         true <- +           !(streamed_type == :activity && item.data["type"] == "Announce" && +               parent.data["actor"] == user.ap_id),           true <- Enum.all?([blocked_ap_ids, muted_ap_ids], &(parent.data["actor"] not in &1)),           true <- MapSet.disjoint?(recipients, recipient_blocks),           %{host: item_host} <- URI.parse(item.actor), @@ -130,8 +135,8 @@ defmodule Pleroma.Web.Streamer do      end    end -  def filtered_by_user?(%User{} = user, %Notification{activity: activity}) do -    filtered_by_user?(user, activity) +  def filtered_by_user?(%User{} = user, %Notification{activity: activity}, _) do +    filtered_by_user?(user, activity, :notification)    end    defp do_stream("direct", item) do diff --git a/lib/pleroma/web/twitter_api/controllers/util_controller.ex b/lib/pleroma/web/twitter_api/controllers/util_controller.ex index fd2aee175..f02c4075c 100644 --- a/lib/pleroma/web/twitter_api/controllers/util_controller.ex +++ b/lib/pleroma/web/twitter_api/controllers/util_controller.ex @@ -13,7 +13,6 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do    alias Pleroma.Notification    alias Pleroma.Plugs.OAuthScopesPlug    alias Pleroma.User -  alias Pleroma.Web    alias Pleroma.Web.CommonAPI    alias Pleroma.Web.WebFinger @@ -41,12 +40,6 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do    plug(OAuthScopesPlug, %{scopes: ["write:notifications"]} when action == :notifications_read) -  plug(Pleroma.Plugs.SetFormatPlug when action in [:config, :version]) - -  def help_test(conn, _params) do -    json(conn, "ok") -  end -    def remote_subscribe(conn, %{"nickname" => nick, "profile" => _}) do      with %User{} = user <- User.get_cached_by_nickname(nick),           avatar = User.avatar_url(user) do @@ -88,90 +81,14 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do      end    end -  def config(%{assigns: %{format: "xml"}} = conn, _params) do -    instance = Pleroma.Config.get(:instance) - -    response = """ -    <config> -    <site> -    <name>#{Keyword.get(instance, :name)}</name> -    <site>#{Web.base_url()}</site> -    <textlimit>#{Keyword.get(instance, :limit)}</textlimit> -    <closed>#{!Keyword.get(instance, :registrations_open)}</closed> -    </site> -    </config> -    """ - -    conn -    |> put_resp_content_type("application/xml") -    |> send_resp(200, response) -  end - -  def config(conn, _params) do -    instance = Pleroma.Config.get(:instance) - -    vapid_public_key = Keyword.get(Pleroma.Web.Push.vapid_config(), :public_key) - -    uploadlimit = %{ -      uploadlimit: to_string(Keyword.get(instance, :upload_limit)), -      avatarlimit: to_string(Keyword.get(instance, :avatar_upload_limit)), -      backgroundlimit: to_string(Keyword.get(instance, :background_upload_limit)), -      bannerlimit: to_string(Keyword.get(instance, :banner_upload_limit)) -    } - -    data = %{ -      name: Keyword.get(instance, :name), -      description: Keyword.get(instance, :description), -      server: Web.base_url(), -      textlimit: to_string(Keyword.get(instance, :limit)), -      uploadlimit: uploadlimit, -      closed: bool_to_val(Keyword.get(instance, :registrations_open), "0", "1"), -      private: bool_to_val(Keyword.get(instance, :public, true), "0", "1"), -      vapidPublicKey: vapid_public_key, -      accountActivationRequired: -        bool_to_val(Keyword.get(instance, :account_activation_required, false)), -      invitesEnabled: bool_to_val(Keyword.get(instance, :invites_enabled, false)), -      safeDMMentionsEnabled: bool_to_val(Pleroma.Config.get([:instance, :safe_dm_mentions])) -    } - -    managed_config = Keyword.get(instance, :managed_config) - -    data = -      if managed_config do -        pleroma_fe = Pleroma.Config.get([:frontend_configurations, :pleroma_fe]) -        Map.put(data, "pleromafe", pleroma_fe) -      else -        data -      end - -    json(conn, %{site: data}) -  end - -  defp bool_to_val(true), do: "1" -  defp bool_to_val(_), do: "0" -  defp bool_to_val(true, val, _), do: val -  defp bool_to_val(_, _, val), do: val -    def frontend_configurations(conn, _params) do      config = -      Pleroma.Config.get(:frontend_configurations, %{}) +      Config.get(:frontend_configurations, %{})        |> Enum.into(%{})      json(conn, config)    end -  def version(%{assigns: %{format: "xml"}} = conn, _params) do -    version = Pleroma.Application.named_version() - -    conn -    |> put_resp_content_type("application/xml") -    |> send_resp(200, "<version>#{version}</version>") -  end - -  def version(conn, _params) do -    json(conn, Pleroma.Application.named_version()) -  end -    def emoji(conn, _params) do      emoji =        Enum.reduce(Emoji.get_all(), %{}, fn {code, %Emoji{file: file, tags: tags}}, acc -> diff --git a/lib/pleroma/web/twitter_api/views/util_view.ex b/lib/pleroma/web/twitter_api/views/util_view.ex index 52054e020..d3bdb4f62 100644 --- a/lib/pleroma/web/twitter_api/views/util_view.ex +++ b/lib/pleroma/web/twitter_api/views/util_view.ex @@ -5,4 +5,18 @@  defmodule Pleroma.Web.TwitterAPI.UtilView do    use Pleroma.Web, :view    import Phoenix.HTML.Form +  alias Pleroma.Web + +  def status_net_config(instance) do +    """ +    <config> +    <site> +    <name>#{Keyword.get(instance, :name)}</name> +    <site>#{Web.base_url()}</site> +    <textlimit>#{Keyword.get(instance, :limit)}</textlimit> +    <closed>#{!Keyword.get(instance, :registrations_open)}</closed> +    </site> +    </config> +    """ +  end  end diff --git a/lib/pleroma/web/views/masto_fe_view.ex b/lib/pleroma/web/views/masto_fe_view.ex index c3096006e..f739dacb6 100644 --- a/lib/pleroma/web/views/masto_fe_view.ex +++ b/lib/pleroma/web/views/masto_fe_view.ex @@ -86,7 +86,7 @@ defmodule Pleroma.Web.MastoFEView do            "video\/mp4"          ]        }, -      settings: user.settings || @default_settings, +      settings: user.mastofe_settings || @default_settings,        push_subscription: nil,        accounts: %{user.id => render(AccountView, "show.json", user: user, for: user)},        custom_emojis: render(CustomEmojiView, "index.json", custom_emojis: custom_emojis), diff --git a/lib/pleroma/workers/attachments_cleanup_worker.ex b/lib/pleroma/workers/attachments_cleanup_worker.ex index 8deeabda0..58226b395 100644 --- a/lib/pleroma/workers/attachments_cleanup_worker.ex +++ b/lib/pleroma/workers/attachments_cleanup_worker.ex @@ -11,13 +11,12 @@ defmodule Pleroma.Workers.AttachmentsCleanupWorker do    use Pleroma.Workers.WorkerHelper, queue: "attachments_cleanup"    @impl Oban.Worker -  def perform( -        %{ +  def perform(%Job{ +        args: %{            "op" => "cleanup_attachments",            "object" => %{"data" => %{"attachment" => [_ | _] = attachments, "actor" => actor}} -        }, -        _job -      ) do +        } +      }) do      attachments      |> Enum.flat_map(fn item -> Enum.map(item["url"], & &1["href"]) end)      |> fetch_objects @@ -28,7 +27,7 @@ defmodule Pleroma.Workers.AttachmentsCleanupWorker do      {:ok, :success}    end -  def perform(%{"op" => "cleanup_attachments", "object" => _object}, _job), do: {:ok, :skip} +  def perform(%Job{args: %{"op" => "cleanup_attachments", "object" => _object}}), do: {:ok, :skip}    defp do_clean({object_ids, attachment_urls}) do      uploader = Pleroma.Config.get([Pleroma.Upload, :uploader]) diff --git a/lib/pleroma/workers/background_worker.ex b/lib/pleroma/workers/background_worker.ex index 57c3a9c3a..cec5a7462 100644 --- a/lib/pleroma/workers/background_worker.ex +++ b/lib/pleroma/workers/background_worker.ex @@ -11,59 +11,59 @@ defmodule Pleroma.Workers.BackgroundWorker do    @impl Oban.Worker -  def perform(%{"op" => "deactivate_user", "user_id" => user_id, "status" => status}, _job) do +  def perform(%Job{args: %{"op" => "deactivate_user", "user_id" => user_id, "status" => status}}) do      user = User.get_cached_by_id(user_id)      User.perform(:deactivate_async, user, status)    end -  def perform(%{"op" => "delete_user", "user_id" => user_id}, _job) do +  def perform(%Job{args: %{"op" => "delete_user", "user_id" => user_id}}) do      user = User.get_cached_by_id(user_id)      User.perform(:delete, user)    end -  def perform(%{"op" => "force_password_reset", "user_id" => user_id}, _job) do +  def perform(%Job{args: %{"op" => "force_password_reset", "user_id" => user_id}}) do      user = User.get_cached_by_id(user_id)      User.perform(:force_password_reset, user)    end -  def perform( -        %{ +  def perform(%Job{ +        args: %{            "op" => "blocks_import",            "blocker_id" => blocker_id,            "blocked_identifiers" => blocked_identifiers -        }, -        _job -      ) do +        } +      }) do      blocker = User.get_cached_by_id(blocker_id)      {:ok, User.perform(:blocks_import, blocker, blocked_identifiers)}    end -  def perform( -        %{ +  def perform(%Job{ +        args: %{            "op" => "follow_import",            "follower_id" => follower_id,            "followed_identifiers" => followed_identifiers -        }, -        _job -      ) do +        } +      }) do      follower = User.get_cached_by_id(follower_id)      {:ok, User.perform(:follow_import, follower, followed_identifiers)}    end -  def perform(%{"op" => "media_proxy_preload", "message" => message}, _job) do +  def perform(%Job{args: %{"op" => "media_proxy_preload", "message" => message}}) do      MediaProxyWarmingPolicy.perform(:preload, message)    end -  def perform(%{"op" => "media_proxy_prefetch", "url" => url}, _job) do +  def perform(%Job{args: %{"op" => "media_proxy_prefetch", "url" => url}}) do      MediaProxyWarmingPolicy.perform(:prefetch, url)    end -  def perform(%{"op" => "fetch_data_for_activity", "activity_id" => activity_id}, _job) do +  def perform(%Job{args: %{"op" => "fetch_data_for_activity", "activity_id" => activity_id}}) do      activity = Activity.get_by_id(activity_id)      Pleroma.Web.RichMedia.Helpers.perform(:fetch, activity)    end -  def perform(%{"op" => "move_following", "origin_id" => origin_id, "target_id" => target_id}, _) do +  def perform(%Job{ +        args: %{"op" => "move_following", "origin_id" => origin_id, "target_id" => target_id} +      }) do      origin = User.get_cached_by_id(origin_id)      target = User.get_cached_by_id(target_id) diff --git a/lib/pleroma/workers/cron/clear_oauth_token_worker.ex b/lib/pleroma/workers/cron/clear_oauth_token_worker.ex index a4c3b9516..d41be4e87 100644 --- a/lib/pleroma/workers/cron/clear_oauth_token_worker.ex +++ b/lib/pleroma/workers/cron/clear_oauth_token_worker.ex @@ -13,7 +13,7 @@ defmodule Pleroma.Workers.Cron.ClearOauthTokenWorker do    alias Pleroma.Web.OAuth.Token    @impl Oban.Worker -  def perform(_opts, _job) do +  def perform(_job) do      if Config.get([:oauth2, :clean_expired_tokens], false) do        Token.delete_expired_tokens()      else diff --git a/lib/pleroma/workers/cron/digest_emails_worker.ex b/lib/pleroma/workers/cron/digest_emails_worker.ex index 7f09ff3cf..ee646229f 100644 --- a/lib/pleroma/workers/cron/digest_emails_worker.ex +++ b/lib/pleroma/workers/cron/digest_emails_worker.ex @@ -19,7 +19,7 @@ defmodule Pleroma.Workers.Cron.DigestEmailsWorker do    require Logger    @impl Oban.Worker -  def perform(_opts, _job) do +  def perform(_job) do      config = Config.get([:email_notifications, :digest])      if config[:active] do diff --git a/lib/pleroma/workers/cron/new_users_digest_worker.ex b/lib/pleroma/workers/cron/new_users_digest_worker.ex index 5c816b3fe..abc8a5e95 100644 --- a/lib/pleroma/workers/cron/new_users_digest_worker.ex +++ b/lib/pleroma/workers/cron/new_users_digest_worker.ex @@ -12,7 +12,7 @@ defmodule Pleroma.Workers.Cron.NewUsersDigestWorker do    use Pleroma.Workers.WorkerHelper, queue: "new_users_digest"    @impl Oban.Worker -  def perform(_args, _job) do +  def perform(_job) do      if Pleroma.Config.get([Pleroma.Emails.NewUsersDigestEmail, :enabled]) do        today = NaiveDateTime.utc_now() |> Timex.beginning_of_day() diff --git a/lib/pleroma/workers/cron/purge_expired_activities_worker.ex b/lib/pleroma/workers/cron/purge_expired_activities_worker.ex index 84b3b84de..e926c5dc8 100644 --- a/lib/pleroma/workers/cron/purge_expired_activities_worker.ex +++ b/lib/pleroma/workers/cron/purge_expired_activities_worker.ex @@ -20,7 +20,7 @@ defmodule Pleroma.Workers.Cron.PurgeExpiredActivitiesWorker do    @interval :timer.minutes(1)    @impl Oban.Worker -  def perform(_opts, _job) do +  def perform(_job) do      if Config.get([ActivityExpiration, :enabled]) do        Enum.each(ActivityExpiration.due_expirations(@interval), &delete_activity/1)      else diff --git a/lib/pleroma/workers/cron/stats_worker.ex b/lib/pleroma/workers/cron/stats_worker.ex index e9b8d59c4..e54bd9a7f 100644 --- a/lib/pleroma/workers/cron/stats_worker.ex +++ b/lib/pleroma/workers/cron/stats_worker.ex @@ -10,7 +10,7 @@ defmodule Pleroma.Workers.Cron.StatsWorker do    use Oban.Worker, queue: "background"    @impl Oban.Worker -  def perform(_opts, _job) do +  def perform(_job) do      Pleroma.Stats.do_collect()    end  end diff --git a/lib/pleroma/workers/mailer_worker.ex b/lib/pleroma/workers/mailer_worker.ex index 6955338a5..32273cfa5 100644 --- a/lib/pleroma/workers/mailer_worker.ex +++ b/lib/pleroma/workers/mailer_worker.ex @@ -6,7 +6,7 @@ defmodule Pleroma.Workers.MailerWorker do    use Pleroma.Workers.WorkerHelper, queue: "mailer"    @impl Oban.Worker -  def perform(%{"op" => "email", "encoded_email" => encoded_email, "config" => config}, _job) do +  def perform(%Job{args: %{"op" => "email", "encoded_email" => encoded_email, "config" => config}}) do      encoded_email      |> Base.decode64!()      |> :erlang.binary_to_term() diff --git a/lib/pleroma/workers/publisher_worker.ex b/lib/pleroma/workers/publisher_worker.ex index daf79efc0..e739c3cd0 100644 --- a/lib/pleroma/workers/publisher_worker.ex +++ b/lib/pleroma/workers/publisher_worker.ex @@ -8,17 +8,17 @@ defmodule Pleroma.Workers.PublisherWorker do    use Pleroma.Workers.WorkerHelper, queue: "federator_outgoing" -  def backoff(attempt) when is_integer(attempt) do +  def backoff(%Job{attempt: attempt}) when is_integer(attempt) do      Pleroma.Workers.WorkerHelper.sidekiq_backoff(attempt, 5)    end    @impl Oban.Worker -  def perform(%{"op" => "publish", "activity_id" => activity_id}, _job) do +  def perform(%Job{args: %{"op" => "publish", "activity_id" => activity_id}}) do      activity = Activity.get_by_id(activity_id)      Federator.perform(:publish, activity)    end -  def perform(%{"op" => "publish_one", "module" => module_name, "params" => params}, _job) do +  def perform(%Job{args: %{"op" => "publish_one", "module" => module_name, "params" => params}}) do      params = Map.new(params, fn {k, v} -> {String.to_atom(k), v} end)      Federator.perform(:publish_one, String.to_atom(module_name), params)    end diff --git a/lib/pleroma/workers/receiver_worker.ex b/lib/pleroma/workers/receiver_worker.ex index f7a7124f3..1b97af1a8 100644 --- a/lib/pleroma/workers/receiver_worker.ex +++ b/lib/pleroma/workers/receiver_worker.ex @@ -8,7 +8,7 @@ defmodule Pleroma.Workers.ReceiverWorker do    use Pleroma.Workers.WorkerHelper, queue: "federator_incoming"    @impl Oban.Worker -  def perform(%{"op" => "incoming_ap_doc", "params" => params}, _job) do +  def perform(%Job{args: %{"op" => "incoming_ap_doc", "params" => params}}) do      Federator.perform(:incoming_ap_doc, params)    end  end diff --git a/lib/pleroma/workers/remote_fetcher_worker.ex b/lib/pleroma/workers/remote_fetcher_worker.ex index ec6534f21..27e2e3386 100644 --- a/lib/pleroma/workers/remote_fetcher_worker.ex +++ b/lib/pleroma/workers/remote_fetcher_worker.ex @@ -8,13 +8,7 @@ defmodule Pleroma.Workers.RemoteFetcherWorker do    use Pleroma.Workers.WorkerHelper, queue: "remote_fetcher"    @impl Oban.Worker -  def perform( -        %{ -          "op" => "fetch_remote", -          "id" => id -        } = args, -        _job -      ) do +  def perform(%Job{args: %{"op" => "fetch_remote", "id" => id} = args}) do      {:ok, _object} = Fetcher.fetch_object_from_id(id, depth: args["depth"])    end  end diff --git a/lib/pleroma/workers/scheduled_activity_worker.ex b/lib/pleroma/workers/scheduled_activity_worker.ex index 97d1efbfb..dd9986fe4 100644 --- a/lib/pleroma/workers/scheduled_activity_worker.ex +++ b/lib/pleroma/workers/scheduled_activity_worker.ex @@ -17,7 +17,7 @@ defmodule Pleroma.Workers.ScheduledActivityWorker do    require Logger    @impl Oban.Worker -  def perform(%{"activity_id" => activity_id}, _job) do +  def perform(%Job{args: %{"activity_id" => activity_id}}) do      if Config.get([ScheduledActivity, :enabled]) do        case Pleroma.Repo.get(ScheduledActivity, activity_id) do          %ScheduledActivity{} = scheduled_activity -> diff --git a/lib/pleroma/workers/transmogrifier_worker.ex b/lib/pleroma/workers/transmogrifier_worker.ex index 11239ca5e..15f36375c 100644 --- a/lib/pleroma/workers/transmogrifier_worker.ex +++ b/lib/pleroma/workers/transmogrifier_worker.ex @@ -8,7 +8,7 @@ defmodule Pleroma.Workers.TransmogrifierWorker do    use Pleroma.Workers.WorkerHelper, queue: "transmogrifier"    @impl Oban.Worker -  def perform(%{"op" => "user_upgrade", "user_id" => user_id}, _job) do +  def perform(%Job{args: %{"op" => "user_upgrade", "user_id" => user_id}}) do      user = User.get_cached_by_id(user_id)      Pleroma.Web.ActivityPub.Transmogrifier.perform(:user_upgrade, user)    end diff --git a/lib/pleroma/workers/web_pusher_worker.ex b/lib/pleroma/workers/web_pusher_worker.ex index 58ad25e39..0cfdc6a6f 100644 --- a/lib/pleroma/workers/web_pusher_worker.ex +++ b/lib/pleroma/workers/web_pusher_worker.ex @@ -9,7 +9,7 @@ defmodule Pleroma.Workers.WebPusherWorker do    use Pleroma.Workers.WorkerHelper, queue: "web_push"    @impl Oban.Worker -  def perform(%{"op" => "web_push", "notification_id" => notification_id}, _job) do +  def perform(%Job{args: %{"op" => "web_push", "notification_id" => notification_id}}) do      notification =        Notification        |> Repo.get(notification_id) diff --git a/lib/pleroma/workers/worker_helper.ex b/lib/pleroma/workers/worker_helper.ex index d1f90c35b..7d1289be2 100644 --- a/lib/pleroma/workers/worker_helper.ex +++ b/lib/pleroma/workers/worker_helper.ex @@ -32,6 +32,8 @@ defmodule Pleroma.Workers.WorkerHelper do          queue: unquote(queue),          max_attempts: 1 +      alias Oban.Job +        def enqueue(op, params, worker_args \\ []) do          params = Map.merge(%{"op" => op}, params)          queue_atom = String.to_atom(unquote(queue)) @@ -39,7 +41,7 @@ defmodule Pleroma.Workers.WorkerHelper do          unquote(caller_module)          |> apply(:new, [params, worker_args]) -        |> Pleroma.Repo.insert() +        |> Oban.insert()        end      end    end  | 
