diff options
Diffstat (limited to 'lib')
74 files changed, 1458 insertions, 518 deletions
| diff --git a/lib/mix/pleroma.ex b/lib/mix/pleroma.ex index 4dfcc32e7..3ad6edbfb 100644 --- a/lib/mix/pleroma.ex +++ b/lib/mix/pleroma.ex @@ -5,7 +5,6 @@  defmodule Mix.Pleroma do    @doc "Common functions to be reused in mix tasks"    def start_pleroma do -    Mix.Task.run("app.start")      Application.put_env(:phoenix, :serve_endpoints, false, persistent: true)      if Pleroma.Config.get(:env) != :test do diff --git a/lib/mix/tasks/pleroma/app.ex b/lib/mix/tasks/pleroma/app.ex new file mode 100644 index 000000000..463e2449f --- /dev/null +++ b/lib/mix/tasks/pleroma/app.ex @@ -0,0 +1,49 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Mix.Tasks.Pleroma.App do +  @moduledoc File.read!("docs/administration/CLI_tasks/oauth_app.md") +  use Mix.Task + +  import Mix.Pleroma + +  @shortdoc "Creates trusted OAuth App" + +  def run(["create" | options]) do +    start_pleroma() + +    {opts, _} = +      OptionParser.parse!(options, +        strict: [name: :string, redirect_uri: :string, scopes: :string], +        aliases: [n: :name, r: :redirect_uri, s: :scopes] +      ) + +    scopes = +      if opts[:scopes] do +        String.split(opts[:scopes], ",") +      else +        ["read", "write", "follow", "push"] +      end + +    params = %{ +      client_name: opts[:name], +      redirect_uris: opts[:redirect_uri], +      trusted: true, +      scopes: scopes +    } + +    with {:ok, app} <- Pleroma.Web.OAuth.App.create(params) do +      shell_info("#{app.client_name} successfully created:") +      shell_info("App client_id: " <> app.client_id) +      shell_info("App client_secret: " <> app.client_secret) +    else +      {:error, changeset} -> +        shell_error("Creating failed:") + +        Enum.each(Pleroma.Web.OAuth.App.errors(changeset), fn {key, error} -> +          shell_error("#{key}: #{error}") +        end) +    end +  end +end diff --git a/lib/mix/tasks/pleroma/benchmark.ex b/lib/mix/tasks/pleroma/benchmark.ex index dd2b9c8f2..6ab7fe8ef 100644 --- a/lib/mix/tasks/pleroma/benchmark.ex +++ b/lib/mix/tasks/pleroma/benchmark.ex @@ -67,7 +67,8 @@ defmodule Mix.Tasks.Pleroma.Benchmark do            Pleroma.Web.MastodonAPI.StatusView.render("index.json", %{              activities: activities,              for: user, -            as: :activity +            as: :activity, +            skip_relationships: true            })          end        }, diff --git a/lib/pleroma/config/transfer_task.ex b/lib/pleroma/config/transfer_task.ex index 936bc9ab1..f4722f99d 100644 --- a/lib/pleroma/config/transfer_task.ex +++ b/lib/pleroma/config/transfer_task.ex @@ -54,10 +54,19 @@ defmodule Pleroma.Config.TransferTask do            [:pleroma, nil, :prometheus]          end +      {logger, other} = +        (Repo.all(ConfigDB) ++ deleted_settings) +        |> Enum.map(&transform_and_merge/1) +        |> Enum.split_with(fn {group, _, _, _} -> group in [:logger, :quack] end) + +      logger +      |> Enum.sort() +      |> Enum.each(&configure/1) +        started_applications = Application.started_applications() -      (Repo.all(ConfigDB) ++ deleted_settings) -      |> Enum.map(&merge_and_update/1) +      other +      |> Enum.map(&update/1)        |> Enum.uniq()        |> Enum.reject(&(&1 in reject_restart))        |> maybe_set_pleroma_last() @@ -81,51 +90,71 @@ defmodule Pleroma.Config.TransferTask do      end    end -  defp group_for_restart(:logger, key, _, merged_value) do -    # change logger configuration in runtime, without restart -    if Keyword.keyword?(merged_value) and -         key not in [:compile_time_application, :backends, :compile_time_purge_matching] do -      Logger.configure_backend(key, merged_value) -    else -      Logger.configure([{key, merged_value}]) -    end +  defp transform_and_merge(%{group: group, key: key, value: value} = setting) do +    group = ConfigDB.from_string(group) +    key = ConfigDB.from_string(key) +    value = ConfigDB.from_binary(value) -    nil -  end +    default = Config.Holder.default_config(group, key) -  defp group_for_restart(group, _, _, _) when group != :pleroma, do: group +    merged = +      cond do +        Ecto.get_meta(setting, :state) == :deleted -> default +        can_be_merged?(default, value) -> ConfigDB.merge_group(group, key, default, value) +        true -> value +      end -  defp group_for_restart(group, key, value, _) do -    if pleroma_need_restart?(group, key, value), do: group +    {group, key, value, merged}    end -  defp merge_and_update(setting) do -    try do -      key = ConfigDB.from_string(setting.key) -      group = ConfigDB.from_string(setting.group) +  # change logger configuration in runtime, without restart +  defp configure({:quack, key, _, merged}) do +    Logger.configure_backend(Quack.Logger, [{key, merged}]) +    :ok = update_env(:quack, key, merged) +  end -      default = Config.Holder.default_config(group, key) -      value = ConfigDB.from_binary(setting.value) +  defp configure({_, :backends, _, merged}) do +    # removing current backends +    Enum.each(Application.get_env(:logger, :backends), &Logger.remove_backend/1) -      merged_value = -        cond do -          Ecto.get_meta(setting, :state) == :deleted -> default -          can_be_merged?(default, value) -> ConfigDB.merge_group(group, key, default, value) -          true -> value -        end +    Enum.each(merged, &Logger.add_backend/1) -      :ok = update_env(group, key, merged_value) +    :ok = update_env(:logger, :backends, merged) +  end + +  defp configure({_, key, _, merged}) when key in [:console, :ex_syslogger] do +    merged = +      if key == :console do +        put_in(merged[:format], merged[:format] <> "\n") +      else +        merged +      end + +    backend = +      if key == :ex_syslogger, +        do: {ExSyslogger, :ex_syslogger}, +        else: key + +    Logger.configure_backend(backend, merged) +    :ok = update_env(:logger, key, merged) +  end -      group_for_restart(group, key, value, merged_value) +  defp configure({_, key, _, merged}) do +    Logger.configure([{key, merged}]) +    :ok = update_env(:logger, key, merged) +  end + +  defp update({group, key, value, merged}) do +    try do +      :ok = update_env(group, key, merged) + +      if group != :pleroma or pleroma_need_restart?(group, key, value), do: group      rescue        error ->          error_msg = -          "updating env causes error, group: " <> -            inspect(setting.group) <> -            " key: " <> -            inspect(setting.key) <> -            " value: " <> -            inspect(ConfigDB.from_binary(setting.value)) <> " error: " <> inspect(error) +          "updating env causes error, group: #{inspect(group)}, key: #{inspect(key)}, value: #{ +            inspect(value) +          } error: #{inspect(error)}"          Logger.warn(error_msg) @@ -133,6 +162,9 @@ defmodule Pleroma.Config.TransferTask do      end    end +  defp update_env(group, key, nil), do: Application.delete_env(group, key) +  defp update_env(group, key, value), do: Application.put_env(group, key, value) +    @spec pleroma_need_restart?(atom(), atom(), any()) :: boolean()    def pleroma_need_restart?(group, key, value) do      group_and_key_need_reboot?(group, key) or group_and_subkey_need_reboot?(group, key, value) @@ -150,9 +182,6 @@ defmodule Pleroma.Config.TransferTask do        end)    end -  defp update_env(group, key, nil), do: Application.delete_env(group, key) -  defp update_env(group, key, value), do: Application.put_env(group, key, value) -    defp restart(_, :pleroma, env), do: Restarter.Pleroma.restart_after_boot(env)    defp restart(started_applications, app, _) do diff --git a/lib/pleroma/ecto_enums.ex b/lib/pleroma/ecto_enums.ex index d9b601223..6fc47620c 100644 --- a/lib/pleroma/ecto_enums.ex +++ b/lib/pleroma/ecto_enums.ex @@ -4,10 +4,16 @@  import EctoEnum -defenum(UserRelationshipTypeEnum, +defenum(Pleroma.UserRelationship.Type,    block: 1,    mute: 2,    reblog_mute: 3,    notification_mute: 4,    inverse_subscription: 5  ) + +defenum(Pleroma.FollowingRelationship.State, +  follow_pending: 1, +  follow_accept: 2, +  follow_reject: 3 +) diff --git a/lib/pleroma/emoji/formatter.ex b/lib/pleroma/emoji/formatter.ex index 59ff2cac3..dc45b8a38 100644 --- a/lib/pleroma/emoji/formatter.ex +++ b/lib/pleroma/emoji/formatter.ex @@ -38,22 +38,14 @@ defmodule Pleroma.Emoji.Formatter do    def demojify(text, nil), do: text -  @doc "Outputs a list of the emoji-shortcodes in a text" -  def get_emoji(text) when is_binary(text) do -    Enum.filter(Emoji.get_all(), fn {emoji, %Emoji{}} -> -      String.contains?(text, ":#{emoji}:") -    end) -  end - -  def get_emoji(_), do: [] -    @doc "Outputs a list of the emoji-Maps in a text"    def get_emoji_map(text) when is_binary(text) do -    get_emoji(text) +    Emoji.get_all() +    |> Enum.filter(fn {emoji, %Emoji{}} -> String.contains?(text, ":#{emoji}:") end)      |> Enum.reduce(%{}, fn {name, %Emoji{file: file}}, acc ->        Map.put(acc, name, "#{Pleroma.Web.Endpoint.static_url()}#{file}")      end)    end -  def get_emoji_map(_), do: [] +  def get_emoji_map(_), do: %{}  end diff --git a/lib/pleroma/following_relationship.ex b/lib/pleroma/following_relationship.ex index a9538ea4e..9ccf40495 100644 --- a/lib/pleroma/following_relationship.ex +++ b/lib/pleroma/following_relationship.ex @@ -8,12 +8,13 @@ defmodule Pleroma.FollowingRelationship do    import Ecto.Changeset    import Ecto.Query +  alias Ecto.Changeset    alias FlakeId.Ecto.CompatType    alias Pleroma.Repo    alias Pleroma.User    schema "following_relationships" do -    field(:state, :string, default: "accept") +    field(:state, Pleroma.FollowingRelationship.State, default: :follow_pending)      belongs_to(:follower, User, type: CompatType)      belongs_to(:following, User, type: CompatType) @@ -27,6 +28,18 @@ defmodule Pleroma.FollowingRelationship do      |> put_assoc(:follower, attrs.follower)      |> put_assoc(:following, attrs.following)      |> validate_required([:state, :follower, :following]) +    |> unique_constraint(:follower_id, +      name: :following_relationships_follower_id_following_id_index +    ) +    |> validate_not_self_relationship() +  end + +  def state_to_enum(state) when state in ["pending", "accept", "reject"] do +    String.to_existing_atom("follow_#{state}") +  end + +  def state_to_enum(state) do +    raise "State is not convertible to Pleroma.FollowingRelationship.State: #{state}"    end    def get(%User{} = follower, %User{} = following) do @@ -35,7 +48,7 @@ defmodule Pleroma.FollowingRelationship do      |> Repo.one()    end -  def update(follower, following, "reject"), do: unfollow(follower, following) +  def update(follower, following, :follow_reject), do: unfollow(follower, following)    def update(%User{} = follower, %User{} = following, state) do      case get(follower, following) do @@ -50,7 +63,7 @@ defmodule Pleroma.FollowingRelationship do      end    end -  def follow(%User{} = follower, %User{} = following, state \\ "accept") do +  def follow(%User{} = follower, %User{} = following, state \\ :follow_accept) do      %__MODULE__{}      |> changeset(%{follower: follower, following: following, state: state})      |> Repo.insert(on_conflict: :nothing) @@ -80,7 +93,7 @@ defmodule Pleroma.FollowingRelationship do    def get_follow_requests(%User{id: id}) do      __MODULE__      |> join(:inner, [r], f in assoc(r, :follower)) -    |> where([r], r.state == "pending") +    |> where([r], r.state == ^:follow_pending)      |> where([r], r.following_id == ^id)      |> select([r, f], f)      |> Repo.all() @@ -88,7 +101,7 @@ defmodule Pleroma.FollowingRelationship do    def following?(%User{id: follower_id}, %User{id: followed_id}) do      __MODULE__ -    |> where(follower_id: ^follower_id, following_id: ^followed_id, state: "accept") +    |> where(follower_id: ^follower_id, following_id: ^followed_id, state: ^:follow_accept)      |> Repo.exists?()    end @@ -97,7 +110,7 @@ defmodule Pleroma.FollowingRelationship do        __MODULE__        |> join(:inner, [r], u in User, on: r.following_id == u.id)        |> where([r], r.follower_id == ^user.id) -      |> where([r], r.state == "accept") +      |> where([r], r.state == ^:follow_accept)        |> select([r, u], u.follower_address)        |> Repo.all() @@ -157,4 +170,30 @@ defmodule Pleroma.FollowingRelationship do        fr -> fr.follower_id == follower.id and fr.following_id == following.id      end)    end + +  defp validate_not_self_relationship(%Changeset{} = changeset) do +    changeset +    |> validate_follower_id_following_id_inequality() +    |> validate_following_id_follower_id_inequality() +  end + +  defp validate_follower_id_following_id_inequality(%Changeset{} = changeset) do +    validate_change(changeset, :follower_id, fn _, follower_id -> +      if follower_id == get_field(changeset, :following_id) do +        [source_id: "can't be equal to following_id"] +      else +        [] +      end +    end) +  end + +  defp validate_following_id_follower_id_inequality(%Changeset{} = changeset) do +    validate_change(changeset, :following_id, fn _, following_id -> +      if following_id == get_field(changeset, :follower_id) do +        [target_id: "can't be equal to follower_id"] +      else +        [] +      end +    end) +  end  end diff --git a/lib/pleroma/formatter.ex b/lib/pleroma/formatter.ex index e2a658cb3..02a93a8dc 100644 --- a/lib/pleroma/formatter.ex +++ b/lib/pleroma/formatter.ex @@ -31,13 +31,23 @@ defmodule Pleroma.Formatter do    def mention_handler("@" <> nickname, buffer, opts, acc) do      case User.get_cached_by_nickname(nickname) do        %User{id: id} = user -> -        ap_id = get_ap_id(user) +        user_url = user.uri || user.ap_id          nickname_text = get_nickname_text(nickname, opts)          link = -          ~s(<span class="h-card"><a data-user="#{id}" class="u-url mention" href="#{ap_id}" rel="ugc">@<span>#{ -            nickname_text -          }</span></a></span>) +          Phoenix.HTML.Tag.content_tag( +            :span, +            Phoenix.HTML.Tag.content_tag( +              :a, +              ["@", Phoenix.HTML.Tag.content_tag(:span, nickname_text)], +              "data-user": id, +              class: "u-url mention", +              href: user_url, +              rel: "ugc" +            ), +            class: "h-card" +          ) +          |> Phoenix.HTML.safe_to_string()          {link, %{acc | mentions: MapSet.put(acc.mentions, {"@" <> nickname, user})}} @@ -49,7 +59,15 @@ defmodule Pleroma.Formatter do    def hashtag_handler("#" <> tag = tag_text, _buffer, _opts, acc) do      tag = String.downcase(tag)      url = "#{Pleroma.Web.base_url()}/tag/#{tag}" -    link = ~s(<a class="hashtag" data-tag="#{tag}" href="#{url}" rel="tag ugc">#{tag_text}</a>) + +    link = +      Phoenix.HTML.Tag.content_tag(:a, tag_text, +        class: "hashtag", +        "data-tag": tag, +        href: url, +        rel: "tag ugc" +      ) +      |> Phoenix.HTML.safe_to_string()      {link, %{acc | tags: MapSet.put(acc.tags, {tag_text, tag})}}    end @@ -128,9 +146,6 @@ defmodule Pleroma.Formatter do      end    end -  defp get_ap_id(%User{source_data: %{"url" => url}}) when is_binary(url), do: url -  defp get_ap_id(%User{ap_id: ap_id}), do: ap_id -    defp get_nickname_text(nickname, %{mentions_format: :full}), do: User.full_nickname(nickname)    defp get_nickname_text(nickname, _), do: User.local_nickname(nickname)  end diff --git a/lib/pleroma/gun/conn.ex b/lib/pleroma/gun/conn.ex index 20823a765..cd25a2e74 100644 --- a/lib/pleroma/gun/conn.ex +++ b/lib/pleroma/gun/conn.ex @@ -49,8 +49,10 @@ defmodule Pleroma.Gun.Conn do      key = "#{uri.scheme}:#{uri.host}:#{uri.port}" +    max_connections = pool_opts[:max_connections] || 250 +      conn_pid = -      if Connections.count(name) < opts[:max_connection] do +      if Connections.count(name) < max_connections do          do_open(uri, opts)        else          close_least_used_and_do_open(name, uri, opts) diff --git a/lib/pleroma/plugs/auth_expected_plug.ex b/lib/pleroma/plugs/auth_expected_plug.ex new file mode 100644 index 000000000..f79597dc3 --- /dev/null +++ b/lib/pleroma/plugs/auth_expected_plug.ex @@ -0,0 +1,17 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Plugs.AuthExpectedPlug do +  import Plug.Conn + +  def init(options), do: options + +  def call(conn, _) do +    put_private(conn, :auth_expected, true) +  end + +  def auth_expected?(conn) do +    conn.private[:auth_expected] +  end +end diff --git a/lib/pleroma/plugs/mapped_signature_to_identity_plug.ex b/lib/pleroma/plugs/mapped_signature_to_identity_plug.ex index 4f124ed4d..84b7c5d83 100644 --- a/lib/pleroma/plugs/mapped_signature_to_identity_plug.ex +++ b/lib/pleroma/plugs/mapped_signature_to_identity_plug.ex @@ -42,13 +42,13 @@ defmodule Pleroma.Web.Plugs.MappedSignatureToIdentityPlug do      else        {:user_match, false} ->          Logger.debug("Failed to map identity from signature (payload actor mismatch)") -        Logger.debug("key_id=#{key_id_from_conn(conn)}, actor=#{actor}") +        Logger.debug("key_id=#{inspect(key_id_from_conn(conn))}, actor=#{inspect(actor)}")          assign(conn, :valid_signature, false)        # remove me once testsuite uses mapped capabilities instead of what we do now        {:user, nil} ->          Logger.debug("Failed to map identity from signature (lookup failure)") -        Logger.debug("key_id=#{key_id_from_conn(conn)}, actor=#{actor}") +        Logger.debug("key_id=#{inspect(key_id_from_conn(conn))}, actor=#{actor}")          conn      end    end @@ -60,7 +60,7 @@ defmodule Pleroma.Web.Plugs.MappedSignatureToIdentityPlug do      else        _ ->          Logger.debug("Failed to map identity from signature (no payload actor mismatch)") -        Logger.debug("key_id=#{key_id_from_conn(conn)}") +        Logger.debug("key_id=#{inspect(key_id_from_conn(conn))}")          assign(conn, :valid_signature, false)      end    end diff --git a/lib/pleroma/plugs/oauth_scopes_plug.ex b/lib/pleroma/plugs/oauth_scopes_plug.ex index 38df074ad..66f48c28c 100644 --- a/lib/pleroma/plugs/oauth_scopes_plug.ex +++ b/lib/pleroma/plugs/oauth_scopes_plug.ex @@ -8,12 +8,15 @@ defmodule Pleroma.Plugs.OAuthScopesPlug do    alias Pleroma.Config    alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug +  alias Pleroma.Plugs.PlugHelper + +  use Pleroma.Web, :plug    @behaviour Plug    def init(%{scopes: _} = options), do: options -  def call(%Plug.Conn{assigns: assigns} = conn, %{scopes: scopes} = options) do +  def perform(%Plug.Conn{assigns: assigns} = conn, %{scopes: scopes} = options) do      op = options[:op] || :|      token = assigns[:token] diff --git a/lib/pleroma/plugs/plug_helper.ex b/lib/pleroma/plugs/plug_helper.ex new file mode 100644 index 000000000..4f83e9414 --- /dev/null +++ b/lib/pleroma/plugs/plug_helper.ex @@ -0,0 +1,38 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Plugs.PlugHelper do +  @moduledoc "Pleroma Plug helper" + +  def append_to_called_plugs(conn, plug_module) do +    append_to_private_list(conn, :called_plugs, plug_module) +  end + +  def append_to_skipped_plugs(conn, plug_module) do +    append_to_private_list(conn, :skipped_plugs, plug_module) +  end + +  def plug_called?(conn, plug_module) do +    contained_in_private_list?(conn, :called_plugs, plug_module) +  end + +  def plug_skipped?(conn, plug_module) do +    contained_in_private_list?(conn, :skipped_plugs, plug_module) +  end + +  def plug_called_or_skipped?(conn, plug_module) do +    plug_called?(conn, plug_module) || plug_skipped?(conn, plug_module) +  end + +  defp append_to_private_list(conn, private_variable, value) do +    list = conn.private[private_variable] || [] +    modified_list = Enum.uniq(list ++ [value]) +    Plug.Conn.put_private(conn, private_variable, modified_list) +  end + +  defp contained_in_private_list?(conn, private_variable, value) do +    list = conn.private[private_variable] || [] +    value in list +  end +end diff --git a/lib/pleroma/plugs/rate_limiter/rate_limiter.ex b/lib/pleroma/plugs/rate_limiter/rate_limiter.ex index 1529da717..c51e2c634 100644 --- a/lib/pleroma/plugs/rate_limiter/rate_limiter.ex +++ b/lib/pleroma/plugs/rate_limiter/rate_limiter.ex @@ -110,20 +110,9 @@ defmodule Pleroma.Plugs.RateLimiter do    end    def disabled?(conn) do -    localhost_or_socket = -      case Config.get([Pleroma.Web.Endpoint, :http, :ip]) do -        {127, 0, 0, 1} -> true -        {0, 0, 0, 0, 0, 0, 0, 1} -> true -        {:local, _} -> true -        _ -> false -      end - -    remote_ip_not_found = -      if Map.has_key?(conn.assigns, :remote_ip_found), -        do: !conn.assigns.remote_ip_found, -        else: false - -    localhost_or_socket and remote_ip_not_found +    if Map.has_key?(conn.assigns, :remote_ip_found), +      do: !conn.assigns.remote_ip_found, +      else: false    end    @inspect_bucket_not_found {:error, :not_found} diff --git a/lib/pleroma/plugs/remote_ip.ex b/lib/pleroma/plugs/remote_ip.ex index 0ac9050d0..2eca4f8f6 100644 --- a/lib/pleroma/plugs/remote_ip.ex +++ b/lib/pleroma/plugs/remote_ip.ex @@ -7,8 +7,6 @@ defmodule Pleroma.Plugs.RemoteIp do    This is a shim to call [`RemoteIp`](https://git.pleroma.social/pleroma/remote_ip) but with runtime configuration.    """ -  import Plug.Conn -    @behaviour Plug    @headers ~w[ @@ -28,12 +26,11 @@ defmodule Pleroma.Plugs.RemoteIp do    def init(_), do: nil -  def call(%{remote_ip: original_remote_ip} = conn, _) do +  def call(conn, _) do      config = Pleroma.Config.get(__MODULE__, [])      if Keyword.get(config, :enabled, false) do -      %{remote_ip: new_remote_ip} = conn = RemoteIp.call(conn, remote_ip_opts(config)) -      assign(conn, :remote_ip_found, original_remote_ip != new_remote_ip) +      RemoteIp.call(conn, remote_ip_opts(config))      else        conn      end diff --git a/lib/pleroma/plugs/uploaded_media.ex b/lib/pleroma/plugs/uploaded_media.ex index 36ff024a7..94147e0c4 100644 --- a/lib/pleroma/plugs/uploaded_media.ex +++ b/lib/pleroma/plugs/uploaded_media.ex @@ -41,6 +41,7 @@ defmodule Pleroma.Plugs.UploadedMedia do          conn ->            conn        end +      |> merge_resp_headers([{"content-security-policy", "sandbox"}])      config = Pleroma.Config.get(Pleroma.Upload) diff --git a/lib/pleroma/pool/connections.ex b/lib/pleroma/pool/connections.ex index 4d4ba913c..acafe1bea 100644 --- a/lib/pleroma/pool/connections.ex +++ b/lib/pleroma/pool/connections.ex @@ -243,7 +243,7 @@ defmodule Pleroma.Pool.Connections do    @impl true    def handle_info({:DOWN, _ref, :process, conn_pid, reason}, state) do -    Logger.debug("received DOWM message for #{inspect(conn_pid)} reason -> #{inspect(reason)}") +    Logger.debug("received DOWN message for #{inspect(conn_pid)} reason -> #{inspect(reason)}")      state =        with {key, conn} <- find_conn(state.conns, conn_pid) do diff --git a/lib/pleroma/tests/oauth_test_controller.ex b/lib/pleroma/tests/oauth_test_controller.ex new file mode 100644 index 000000000..58d517f78 --- /dev/null +++ b/lib/pleroma/tests/oauth_test_controller.ex @@ -0,0 +1,31 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +# A test controller reachable only in :test env. +# Serves to test OAuth scopes check skipping / enforcement. +defmodule Pleroma.Tests.OAuthTestController do +  @moduledoc false + +  use Pleroma.Web, :controller + +  alias Pleroma.Plugs.OAuthScopesPlug + +  plug(:skip_plug, OAuthScopesPlug when action == :skipped_oauth) + +  plug(OAuthScopesPlug, %{scopes: ["read"]} when action != :missed_oauth) + +  def skipped_oauth(conn, _params) do +    noop(conn) +  end + +  def performed_oauth(conn, _params) do +    noop(conn) +  end + +  def missed_oauth(conn, _params) do +    noop(conn) +  end + +  defp noop(conn), do: json(conn, %{}) +end diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index ac2594417..bef4679cb 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -15,6 +15,7 @@ defmodule Pleroma.User do    alias Pleroma.Config    alias Pleroma.Conversation.Participation    alias Pleroma.Delivery +  alias Pleroma.Emoji    alias Pleroma.FollowingRelationship    alias Pleroma.Formatter    alias Pleroma.HTML @@ -28,6 +29,7 @@ defmodule Pleroma.User do    alias Pleroma.UserRelationship    alias Pleroma.Web    alias Pleroma.Web.ActivityPub.ActivityPub +  alias Pleroma.Web.ActivityPub.ObjectValidators.Types    alias Pleroma.Web.ActivityPub.Utils    alias Pleroma.Web.CommonAPI    alias Pleroma.Web.CommonAPI.Utils, as: CommonUtils @@ -82,6 +84,7 @@ defmodule Pleroma.User do      field(:password, :string, virtual: true)      field(:password_confirmation, :string, virtual: true)      field(:keys, :string) +    field(:public_key, :string)      field(:ap_id, :string)      field(:avatar, :map)      field(:local, :boolean, default: true) @@ -94,7 +97,6 @@ defmodule Pleroma.User do      field(:last_digest_emailed_at, :naive_datetime)      field(:banner, :map, default: %{})      field(:background, :map, default: %{}) -    field(:source_data, :map, default: %{})      field(:note_count, :integer, default: 0)      field(:follower_count, :integer, default: 0)      field(:following_count, :integer, default: 0) @@ -112,7 +114,7 @@ defmodule Pleroma.User do      field(:show_role, :boolean, default: true)      field(:settings, :map, default: nil)      field(:magic_key, :string, default: nil) -    field(:uri, :string, default: nil) +    field(:uri, Types.Uri, default: nil)      field(:hide_followers_count, :boolean, default: false)      field(:hide_follows_count, :boolean, default: false)      field(:hide_followers, :boolean, default: false) @@ -122,7 +124,7 @@ defmodule Pleroma.User do      field(:pinned_activities, {:array, :string}, default: [])      field(:email_notifications, :map, default: %{"digest" => false})      field(:mascot, :map, default: nil) -    field(:emoji, {:array, :map}, default: []) +    field(:emoji, :map, default: %{})      field(:pleroma_settings_store, :map, default: %{})      field(:fields, {:array, :map}, default: [])      field(:raw_fields, {:array, :map}, default: []) @@ -132,6 +134,8 @@ defmodule Pleroma.User do      field(:skip_thread_containment, :boolean, default: false)      field(:actor_type, :string, default: "Person")      field(:also_known_as, {:array, :string}, default: []) +    field(:inbox, :string) +    field(:shared_inbox, :string)      embeds_one(        :notification_settings, @@ -306,6 +310,7 @@ defmodule Pleroma.User do      end    end +  # Should probably be renamed or removed    def ap_id(%User{nickname: nickname}), do: "#{Web.base_url()}/users/#{nickname}"    def ap_followers(%User{follower_address: fa}) when is_binary(fa), do: fa @@ -339,62 +344,72 @@ defmodule Pleroma.User do      end    end -  def remote_user_creation(params) do +  defp fix_follower_address(%{follower_address: _, following_address: _} = params), do: params + +  defp fix_follower_address(%{nickname: nickname} = params), +    do: Map.put(params, :follower_address, ap_followers(%User{nickname: nickname})) + +  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) +    name = +      case params[:name] do +        name when is_binary(name) and byte_size(name) > 0 -> name +        _ -> params[:nickname] +      end +      params =        params +      |> Map.put(:name, name) +      |> Map.put_new(:last_refreshed_at, NaiveDateTime.utc_now())        |> truncate_if_exists(:name, name_limit)        |> truncate_if_exists(:bio, bio_limit)        |> truncate_fields_param() +      |> fix_follower_address() -    changeset = -      %User{local: false} -      |> cast( -        params, -        [ -          :bio, -          :name, -          :ap_id, -          :nickname, -          :avatar, -          :ap_enabled, -          :source_data, -          :banner, -          :locked, -          :magic_key, -          :uri, -          :hide_followers, -          :hide_follows, -          :hide_followers_count, -          :hide_follows_count, -          :follower_count, -          :fields, -          :following_count, -          :discoverable, -          :invisible, -          :actor_type, -          :also_known_as -        ] -      ) -      |> validate_required([:name, :ap_id]) -      |> unique_constraint(:nickname) -      |> validate_format(:nickname, @email_regex) -      |> validate_length(:bio, max: bio_limit) -      |> validate_length(:name, max: name_limit) -      |> validate_fields(true) - -    case params[:source_data] do -      %{"followers" => followers, "following" => following} -> -        changeset -        |> put_change(:follower_address, followers) -        |> put_change(:following_address, following) - -      _ -> -        followers = ap_followers(%User{nickname: get_field(changeset, :nickname)}) -        put_change(changeset, :follower_address, followers) -    end +    struct +    |> cast( +      params, +      [ +        :bio, +        :name, +        :emoji, +        :ap_id, +        :inbox, +        :shared_inbox, +        :nickname, +        :public_key, +        :avatar, +        :ap_enabled, +        :banner, +        :locked, +        :last_refreshed_at, +        :magic_key, +        :uri, +        :follower_address, +        :following_address, +        :hide_followers, +        :hide_follows, +        :hide_followers_count, +        :hide_follows_count, +        :follower_count, +        :fields, +        :following_count, +        :discoverable, +        :invisible, +        :actor_type, +        :also_known_as +      ] +    ) +    |> validate_required([:name, :ap_id]) +    |> unique_constraint(:nickname) +    |> validate_format(:nickname, @email_regex) +    |> validate_length(:bio, max: bio_limit) +    |> validate_length(:name, max: name_limit) +    |> validate_fields(true)    end    def update_changeset(struct, params \\ %{}) do @@ -407,7 +422,11 @@ defmodule Pleroma.User do        [          :bio,          :name, +        :emoji,          :avatar, +        :public_key, +        :inbox, +        :shared_inbox,          :locked,          :no_rich_text,          :default_scope, @@ -434,6 +453,7 @@ defmodule Pleroma.User do      |> validate_length(:bio, max: bio_limit)      |> validate_length(:name, min: 1, max: name_limit)      |> put_fields() +    |> put_emoji()      |> put_change_if_present(:bio, &{:ok, parse_bio(&1, struct)})      |> put_change_if_present(:avatar, &put_upload(&1, :avatar))      |> put_change_if_present(:banner, &put_upload(&1, :banner)) @@ -469,6 +489,18 @@ defmodule Pleroma.User do      |> elem(0)    end +  defp put_emoji(changeset) do +    bio = get_change(changeset, :bio) +    name = get_change(changeset, :name) + +    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 +    end +  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 @@ -488,49 +520,6 @@ defmodule Pleroma.User do      end    end -  def upgrade_changeset(struct, params \\ %{}, remote? \\ false) do -    bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000) -    name_limit = Pleroma.Config.get([:instance, :user_name_length], 100) - -    params = Map.put(params, :last_refreshed_at, NaiveDateTime.utc_now()) - -    params = if remote?, do: truncate_fields_param(params), else: params - -    struct -    |> cast( -      params, -      [ -        :bio, -        :name, -        :follower_address, -        :following_address, -        :avatar, -        :last_refreshed_at, -        :ap_enabled, -        :source_data, -        :banner, -        :locked, -        :magic_key, -        :follower_count, -        :following_count, -        :hide_follows, -        :fields, -        :hide_followers, -        :allow_following_move, -        :discoverable, -        :hide_followers_count, -        :hide_follows_count, -        :actor_type, -        :also_known_as -      ] -    ) -    |> unique_constraint(:nickname) -    |> validate_format(:nickname, local_nickname_regex()) -    |> validate_length(:bio, max: bio_limit) -    |> validate_length(:name, max: name_limit) -    |> validate_fields(remote?) -  end -    def update_as_admin_changeset(struct, params) do      struct      |> update_changeset(params) @@ -606,7 +595,7 @@ defmodule Pleroma.User do      struct      |> confirmation_changeset(need_confirmation: need_confirmation?) -    |> cast(params, [:bio, :email, :name, :nickname, :password, :password_confirmation]) +    |> cast(params, [:bio, :email, :name, :nickname, :password, :password_confirmation, :emoji])      |> validate_required([:name, :nickname, :password, :password_confirmation])      |> validate_confirmation(:password)      |> unique_constraint(:email) @@ -702,7 +691,7 @@ defmodule Pleroma.User do    # "Locked" (self-locked) users demand explicit authorization of follow requests    def maybe_direct_follow(%User{} = follower, %User{local: true, locked: true} = followed) do -    follow(follower, followed, "pending") +    follow(follower, followed, :follow_pending)    end    def maybe_direct_follow(%User{} = follower, %User{local: true} = followed) do @@ -722,14 +711,14 @@ defmodule Pleroma.User do    def follow_all(follower, followeds) do      followeds      |> Enum.reject(fn followed -> blocks?(follower, followed) || blocks?(followed, follower) end) -    |> Enum.each(&follow(follower, &1, "accept")) +    |> Enum.each(&follow(follower, &1, :follow_accept))      set_cache(follower)    end    defdelegate following(user), to: FollowingRelationship -  def follow(%User{} = follower, %User{} = followed, state \\ "accept") do +  def follow(%User{} = follower, %User{} = followed, state \\ :follow_accept) do      deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])      cond do @@ -756,7 +745,7 @@ defmodule Pleroma.User do    def unfollow(%User{} = follower, %User{} = followed) do      case get_follow_state(follower, followed) do -      state when state in ["accept", "pending"] -> +      state when state in [:follow_pending, :follow_accept] ->          FollowingRelationship.unfollow(follower, followed)          {:ok, followed} = update_follower_count(followed) @@ -774,6 +763,7 @@ defmodule Pleroma.User do    defdelegate following?(follower, followed), to: FollowingRelationship +  @doc "Returns follow state as Pleroma.FollowingRelationship.State value"    def get_follow_state(%User{} = follower, %User{} = following) do      following_relationship = FollowingRelationship.get(follower, following)      get_follow_state(follower, following, following_relationship) @@ -787,8 +777,11 @@ defmodule Pleroma.User do      case {following_relationship, following.local} do        {nil, false} ->          case Utils.fetch_latest_follow(follower, following) do -          %{data: %{"state" => state}} when state in ["pending", "accept"] -> state -          _ -> nil +          %Activity{data: %{"state" => state}} when state in ["pending", "accept"] -> +            FollowingRelationship.state_to_enum(state) + +          _ -> +            nil          end        {%{state: state}, _} -> @@ -1287,7 +1280,7 @@ defmodule Pleroma.User do    def blocks?(%User{} = user, %User{} = target) do      blocks_user?(user, target) || -      (!User.following?(user, target) && blocks_domain?(user, target)) +      (blocks_domain?(user, target) and not User.following?(user, target))    end    def blocks_user?(%User{} = user, %User{} = target) do @@ -1619,8 +1612,7 @@ defmodule Pleroma.User do      |> set_cache()    end -  # AP style -  def public_key(%{source_data: %{"publicKey" => %{"publicKeyPem" => public_key_pem}}}) do +  def public_key(%{public_key: public_key_pem}) when is_binary(public_key_pem) do      key =        public_key_pem        |> :public_key.pem_decode() @@ -1630,7 +1622,7 @@ defmodule Pleroma.User do      {:ok, key}    end -  def public_key(_), do: {:error, "not found key"} +  def public_key(_), do: {:error, "key not found"}    def get_public_key_for_ap_id(ap_id) do      with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id), @@ -1641,17 +1633,6 @@ defmodule Pleroma.User do      end    end -  defp blank?(""), do: nil -  defp blank?(n), do: n - -  def insert_or_update_user(data) do -    data -    |> Map.put(:name, blank?(data[:name]) || data[:nickname]) -    |> remote_user_creation() -    |> Repo.insert(on_conflict: {:replace_all_except, [:id]}, conflict_target: :nickname) -    |> set_cache() -  end -    def ap_enabled?(%User{local: true}), do: true    def ap_enabled?(%User{ap_enabled: ap_enabled}), do: ap_enabled    def ap_enabled?(_), do: false @@ -1960,12 +1941,6 @@ defmodule Pleroma.User do      |> update_and_set_cache()    end -  def update_source_data(user, source_data) do -    user -    |> cast(%{source_data: source_data}, [:source_data]) -    |> update_and_set_cache() -  end -    def roles(%{is_moderator: is_moderator, is_admin: is_admin}) do      %{        admin: is_admin, @@ -1973,21 +1948,6 @@ defmodule Pleroma.User do      }    end -  # ``fields`` is an array of mastodon profile field, containing ``{"name": "…", "value": "…"}``. -  # For example: [{"name": "Pronoun", "value": "she/her"}, …] -  def fields(%{fields: nil, source_data: %{"attachment" => attachment}}) do -    limit = Pleroma.Config.get([:instance, :max_remote_account_fields], 0) - -    attachment -    |> Enum.filter(fn %{"type" => t} -> t == "PropertyValue" end) -    |> Enum.map(fn fields -> Map.take(fields, ["name", "value"]) end) -    |> Enum.take(limit) -  end - -  def fields(%{fields: nil}), do: [] - -  def fields(%{fields: fields}), do: fields -    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) @@ -2175,9 +2135,7 @@ defmodule Pleroma.User do    # - display name    def sanitize_html(%User{} = user, filter) do      fields = -      user -      |> User.fields() -      |> Enum.map(fn %{"name" => name, "value" => value} -> +      Enum.map(user.fields, fn %{"name" => name, "value" => value} ->          %{            "name" => name,            "value" => HTML.filter_tags(value, Pleroma.HTML.Scrubber.LinksOnly) diff --git a/lib/pleroma/user/query.ex b/lib/pleroma/user/query.ex index 884e33039..ec88088cf 100644 --- a/lib/pleroma/user/query.ex +++ b/lib/pleroma/user/query.ex @@ -148,7 +148,7 @@ defmodule Pleroma.User.Query do        as: :relationships,        on: r.following_id == ^id and r.follower_id == u.id      ) -    |> where([relationships: r], r.state == "accept") +    |> where([relationships: r], r.state == ^:follow_accept)    end    defp compose_query({:friends, %User{id: id}}, query) do @@ -158,7 +158,7 @@ defmodule Pleroma.User.Query do        as: :relationships,        on: r.following_id == u.id and r.follower_id == ^id      ) -    |> where([relationships: r], r.state == "accept") +    |> where([relationships: r], r.state == ^:follow_accept)    end    defp compose_query({:recipients_from_activity, to}, query) do @@ -173,7 +173,7 @@ defmodule Pleroma.User.Query do      )      |> where(        [u, following: f, relationships: r], -      u.ap_id in ^to or (f.follower_address in ^to and r.state == "accept") +      u.ap_id in ^to or (f.follower_address in ^to and r.state == ^:follow_accept)      )      |> distinct(true)    end diff --git a/lib/pleroma/user_relationship.ex b/lib/pleroma/user_relationship.ex index 18a5eec72..235ad427c 100644 --- a/lib/pleroma/user_relationship.ex +++ b/lib/pleroma/user_relationship.ex @@ -8,6 +8,7 @@ defmodule Pleroma.UserRelationship do    import Ecto.Changeset    import Ecto.Query +  alias Ecto.Changeset    alias Pleroma.FollowingRelationship    alias Pleroma.Repo    alias Pleroma.User @@ -16,12 +17,12 @@ defmodule Pleroma.UserRelationship do    schema "user_relationships" do      belongs_to(:source, User, type: FlakeId.Ecto.CompatType)      belongs_to(:target, User, type: FlakeId.Ecto.CompatType) -    field(:relationship_type, UserRelationshipTypeEnum) +    field(:relationship_type, Pleroma.UserRelationship.Type)      timestamps(updated_at: false)    end -  for relationship_type <- Keyword.keys(UserRelationshipTypeEnum.__enum_map__()) do +  for relationship_type <- Keyword.keys(Pleroma.UserRelationship.Type.__enum_map__()) do      # `def create_block/2`, `def create_mute/2`, `def create_reblog_mute/2`,      #   `def create_notification_mute/2`, `def create_inverse_subscription/2`      def unquote(:"create_#{relationship_type}")(source, target), @@ -40,7 +41,7 @@ defmodule Pleroma.UserRelationship do    def user_relationship_types, do: Keyword.keys(user_relationship_mappings()) -  def user_relationship_mappings, do: UserRelationshipTypeEnum.__enum_map__() +  def user_relationship_mappings, do: Pleroma.UserRelationship.Type.__enum_map__()    def changeset(%UserRelationship{} = user_relationship, params \\ %{}) do      user_relationship @@ -129,17 +130,27 @@ defmodule Pleroma.UserRelationship do    end    @doc ":relationships option for StatusView / AccountView / NotificationView" -  def view_relationships_option(nil = _reading_user, _actors) do +  def view_relationships_option(reading_user, actors, opts \\ []) + +  def view_relationships_option(nil = _reading_user, _actors, _opts) do      %{user_relationships: [], following_relationships: []}    end -  def view_relationships_option(%User{} = reading_user, actors) do +  def view_relationships_option(%User{} = reading_user, actors, opts) do +    {source_to_target_rel_types, target_to_source_rel_types} = +      if opts[:source_mutes_only] do +        # This option is used for rendering statuses (FE needs `muted` flag for each one anyways) +        {[:mute], []} +      else +        {[:block, :mute, :notification_mute, :reblog_mute], [:block, :inverse_subscription]} +      end +      user_relationships =        UserRelationship.dictionary(          [reading_user],          actors, -        [:block, :mute, :notification_mute, :reblog_mute], -        [:block, :inverse_subscription] +        source_to_target_rel_types, +        target_to_source_rel_types        )      following_relationships = FollowingRelationship.all_between_user_sets([reading_user], actors) @@ -147,18 +158,26 @@ defmodule Pleroma.UserRelationship do      %{user_relationships: user_relationships, following_relationships: following_relationships}    end -  defp validate_not_self_relationship(%Ecto.Changeset{} = changeset) do +  defp validate_not_self_relationship(%Changeset{} = changeset) do      changeset -    |> validate_change(:target_id, fn _, target_id -> -      if target_id == get_field(changeset, :source_id) do -        [target_id: "can't be equal to source_id"] +    |> validate_source_id_target_id_inequality() +    |> validate_target_id_source_id_inequality() +  end + +  defp validate_source_id_target_id_inequality(%Changeset{} = changeset) do +    validate_change(changeset, :source_id, fn _, source_id -> +      if source_id == get_field(changeset, :target_id) do +        [source_id: "can't be equal to target_id"]        else          []        end      end) -    |> validate_change(:source_id, fn _, source_id -> -      if source_id == get_field(changeset, :target_id) do -        [source_id: "can't be equal to target_id"] +  end + +  defp validate_target_id_source_id_inequality(%Changeset{} = changeset) do +    validate_change(changeset, :target_id, fn _, target_id -> +      if target_id == get_field(changeset, :source_id) do +        [target_id: "can't be equal to source_id"]        else          []        end diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index 19286fd01..35af0f7dc 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -721,7 +721,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do      end    end -  defp fetch_activities_for_context_query(context, opts) do +  def fetch_activities_for_context_query(context, opts) do      public = [Constants.as_public()]      recipients = @@ -1427,19 +1427,41 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do        |> Enum.filter(fn %{"type" => t} -> t == "PropertyValue" end)        |> Enum.map(fn fields -> Map.take(fields, ["name", "value"]) end) +    emojis = +      data +      |> Map.get("tag", []) +      |> Enum.filter(fn %{"type" => t} -> t == "Emoji" end) +      |> Enum.reduce(%{}, fn %{"icon" => %{"url" => url}, "name" => name}, acc -> +        Map.put(acc, String.trim(name, ":"), url) +      end) +      locked = data["manuallyApprovesFollowers"] || false      data = Transmogrifier.maybe_fix_user_object(data)      discoverable = data["discoverable"] || false      invisible = data["invisible"] || false      actor_type = data["type"] || "Person" +    public_key = +      if is_map(data["publicKey"]) && is_binary(data["publicKey"]["publicKeyPem"]) do +        data["publicKey"]["publicKeyPem"] +      else +        nil +      end + +    shared_inbox = +      if is_map(data["endpoints"]) && is_binary(data["endpoints"]["sharedInbox"]) do +        data["endpoints"]["sharedInbox"] +      else +        nil +      end +      user_data = %{        ap_id: data["id"],        uri: get_actor_url(data["url"]),        ap_enabled: true, -      source_data: data,        banner: banner,        fields: fields, +      emoji: emojis,        locked: locked,        discoverable: discoverable,        invisible: invisible, @@ -1449,7 +1471,10 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do        following_address: data["following"],        bio: data["summary"],        actor_type: actor_type, -      also_known_as: Map.get(data, "alsoKnownAs", []) +      also_known_as: Map.get(data, "alsoKnownAs", []), +      public_key: public_key, +      inbox: data["inbox"], +      shared_inbox: shared_inbox      }      # nickname can be nil because of virtual actors @@ -1551,11 +1576,22 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do    end    def make_user_from_ap_id(ap_id) do -    if _user = User.get_cached_by_ap_id(ap_id) do +    user = User.get_cached_by_ap_id(ap_id) + +    if user && !User.ap_enabled?(user) do        Transmogrifier.upgrade_user_from_ap_id(ap_id)      else        with {:ok, data} <- fetch_and_prepare_user_from_ap_id(ap_id) do -        User.insert_or_update_user(data) +        if user do +          user +          |> User.remote_user_changeset(data) +          |> User.update_and_set_cache() +        else +          data +          |> User.remote_user_changeset() +          |> Repo.insert() +          |> User.set_cache() +        end        else          e -> {:error, e}        end diff --git a/lib/pleroma/web/activity_pub/object_validators/note_validator.ex b/lib/pleroma/web/activity_pub/object_validators/note_validator.ex index c95b622e4..462a5620a 100644 --- a/lib/pleroma/web/activity_pub/object_validators/note_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/note_validator.ex @@ -35,6 +35,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.NoteValidator do      field(:like_count, :integer, default: 0)      field(:announcement_count, :integer, default: 0)      field(:inRepyTo, :string) +    field(:uri, Types.Uri)      field(:likes, {:array, :string}, default: [])      field(:announcements, {:array, :string}, default: []) diff --git a/lib/pleroma/web/activity_pub/object_validators/types/object_id.ex b/lib/pleroma/web/activity_pub/object_validators/types/object_id.ex index ee10be0b0..f71f76370 100644 --- a/lib/pleroma/web/activity_pub/object_validators/types/object_id.ex +++ b/lib/pleroma/web/activity_pub/object_validators/types/object_id.ex @@ -6,28 +6,18 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.Types.ObjectID do    def cast(object) when is_binary(object) do      # Host has to be present and scheme has to be an http scheme (for now)      case URI.parse(object) do -      %URI{host: nil} -> -        :error - -      %URI{scheme: scheme} when scheme in ["https", "http"] -> -        {:ok, object} - -      _ -> -        :error +      %URI{host: nil} -> :error +      %URI{host: ""} -> :error +      %URI{scheme: scheme} when scheme in ["https", "http"] -> {:ok, object} +      _ -> :error      end    end    def cast(%{"id" => object}), do: cast(object) -  def cast(_) do -    :error -  end +  def cast(_), do: :error -  def dump(data) do -    {:ok, data} -  end +  def dump(data), do: {:ok, data} -  def load(data) do -    {:ok, data} -  end +  def load(data), do: {:ok, data}  end diff --git a/lib/pleroma/web/activity_pub/object_validators/types/uri.ex b/lib/pleroma/web/activity_pub/object_validators/types/uri.ex new file mode 100644 index 000000000..24845bcc0 --- /dev/null +++ b/lib/pleroma/web/activity_pub/object_validators/types/uri.ex @@ -0,0 +1,20 @@ +defmodule Pleroma.Web.ActivityPub.ObjectValidators.Types.Uri do +  use Ecto.Type + +  def type, do: :string + +  def cast(uri) when is_binary(uri) do +    case URI.parse(uri) do +      %URI{host: nil} -> :error +      %URI{host: ""} -> :error +      %URI{scheme: scheme} when scheme in ["https", "http"] -> {:ok, uri} +      _ -> :error +    end +  end + +  def cast(_), do: :error + +  def dump(data), do: {:ok, data} + +  def load(data), do: {:ok, data} +end diff --git a/lib/pleroma/web/activity_pub/publisher.ex b/lib/pleroma/web/activity_pub/publisher.ex index 6c558e7f0..b70cbd043 100644 --- a/lib/pleroma/web/activity_pub/publisher.ex +++ b/lib/pleroma/web/activity_pub/publisher.ex @@ -141,8 +141,8 @@ defmodule Pleroma.Web.ActivityPub.Publisher do      |> Enum.map(& &1.ap_id)    end -  defp maybe_use_sharedinbox(%User{source_data: data}), -    do: (is_map(data["endpoints"]) && Map.get(data["endpoints"], "sharedInbox")) || data["inbox"] +  defp maybe_use_sharedinbox(%User{shared_inbox: nil, inbox: inbox}), do: inbox +  defp maybe_use_sharedinbox(%User{shared_inbox: shared_inbox}), do: shared_inbox    @doc """    Determine a user inbox to use based on heuristics.  These heuristics @@ -157,7 +157,7 @@ defmodule Pleroma.Web.ActivityPub.Publisher do    """    def determine_inbox(          %Activity{data: activity_data}, -        %User{source_data: data} = user +        %User{inbox: inbox} = user        ) do      to = activity_data["to"] || []      cc = activity_data["cc"] || [] @@ -174,7 +174,7 @@ defmodule Pleroma.Web.ActivityPub.Publisher do          maybe_use_sharedinbox(user)        true -> -        data["inbox"] +        inbox      end    end @@ -192,14 +192,13 @@ defmodule Pleroma.Web.ActivityPub.Publisher do      inboxes =        recipients        |> Enum.filter(&User.ap_enabled?/1) -      |> Enum.map(fn %{source_data: data} -> data["inbox"] end) +      |> Enum.map(fn actor -> actor.inbox end)        |> Enum.filter(fn inbox -> should_federate?(inbox, public) end)        |> Instances.filter_reachable()      Repo.checkout(fn ->        Enum.each(inboxes, fn {inbox, unreachable_since} -> -        %User{ap_id: ap_id} = -          Enum.find(recipients, fn %{source_data: data} -> data["inbox"] == inbox end) +        %User{ap_id: ap_id} = Enum.find(recipients, fn actor -> actor.inbox == inbox end)          # Get all the recipients on the same host and add them to cc. Otherwise, a remote          # instance would only accept a first message for the first recipient and ignore the rest. diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex index 0a8ad62ad..09119137b 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier.ex @@ -205,16 +205,46 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do      |> Map.put("conversation", context)    end +  defp add_if_present(map, _key, nil), do: map + +  defp add_if_present(map, key, value) do +    Map.put(map, key, value) +  end +    def fix_attachments(%{"attachment" => attachment} = object) when is_list(attachment) do      attachments =        Enum.map(attachment, fn data -> -        media_type = data["mediaType"] || data["mimeType"] -        href = data["url"] || data["href"] -        url = [%{"type" => "Link", "mediaType" => media_type, "href" => href}] +        url = +          cond do +            is_list(data["url"]) -> List.first(data["url"]) +            is_map(data["url"]) -> data["url"] +            true -> nil +          end -        data -        |> Map.put("mediaType", media_type) -        |> Map.put("url", url) +        media_type = +          cond do +            is_map(url) && is_binary(url["mediaType"]) -> url["mediaType"] +            is_binary(data["mediaType"]) -> data["mediaType"] +            is_binary(data["mimeType"]) -> data["mimeType"] +            true -> nil +          end + +        href = +          cond do +            is_map(url) && is_binary(url["href"]) -> url["href"] +            is_binary(data["url"]) -> data["url"] +            is_binary(data["href"]) -> data["href"] +          end + +        attachment_url = +          %{"href" => href} +          |> add_if_present("mediaType", media_type) +          |> add_if_present("type", Map.get(url || %{}, "type")) + +        %{"url" => [attachment_url]} +        |> add_if_present("mediaType", media_type) +        |> add_if_present("type", data["type"]) +        |> add_if_present("name", data["name"])        end)      Map.put(object, "attachment", attachments) @@ -494,7 +524,8 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do             {_, {: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, "accept") do +           {:ok, _relationship} <- +             FollowingRelationship.update(follower, followed, :follow_accept) do          ActivityPub.accept(%{            to: [follower.ap_id],            actor: followed, @@ -504,7 +535,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do        else          {:user_blocked, true} ->            {:ok, _} = Utils.update_follow_state_for_all(activity, "reject") -          {:ok, _relationship} = FollowingRelationship.update(follower, followed, "reject") +          {:ok, _relationship} = FollowingRelationship.update(follower, followed, :follow_reject)            ActivityPub.reject(%{              to: [follower.ap_id], @@ -515,7 +546,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do          {:follow, {:error, _}} ->            {:ok, _} = Utils.update_follow_state_for_all(activity, "reject") -          {:ok, _relationship} = FollowingRelationship.update(follower, followed, "reject") +          {:ok, _relationship} = FollowingRelationship.update(follower, followed, :follow_reject)            ActivityPub.reject(%{              to: [follower.ap_id], @@ -525,7 +556,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do            })          {:user_locked, true} -> -          {:ok, _relationship} = FollowingRelationship.update(follower, followed, "pending") +          {:ok, _relationship} = FollowingRelationship.update(follower, followed, :follow_pending)            :noop        end @@ -545,7 +576,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do           {:ok, follow_activity} <- get_follow_activity(follow_object, followed),           {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "accept"),           %User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]), -         {:ok, _relationship} <- FollowingRelationship.update(follower, followed, "accept") do +         {:ok, _relationship} <- FollowingRelationship.update(follower, followed, :follow_accept) do        ActivityPub.accept(%{          to: follow_activity.data["to"],          type: "Accept", @@ -568,7 +599,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do           {:ok, follow_activity} <- get_follow_activity(follow_object, followed),           {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "reject"),           %User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]), -         {:ok, _relationship} <- FollowingRelationship.update(follower, followed, "reject"), +         {:ok, _relationship} <- FollowingRelationship.update(follower, followed, :follow_reject),           {:ok, activity} <-             ActivityPub.reject(%{               to: follow_activity.data["to"], @@ -680,7 +711,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do        {:ok, new_user_data} = ActivityPub.user_data_from_user_object(object)        actor -      |> User.upgrade_changeset(new_user_data, true) +      |> User.remote_user_changeset(new_user_data)        |> User.update_and_set_cache()        ActivityPub.update(%{ @@ -1129,7 +1160,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do    def take_emoji_tags(%User{emoji: emoji}) do      emoji -    |> Enum.flat_map(&Map.to_list/1) +    |> Map.to_list()      |> Enum.map(&build_emoji_tag/1)    end @@ -1223,12 +1254,8 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do    def upgrade_user_from_ap_id(ap_id) do      with %User{local: false} = user <- User.get_cached_by_ap_id(ap_id),           {:ok, data} <- ActivityPub.fetch_and_prepare_user_from_ap_id(ap_id), -         already_ap <- User.ap_enabled?(user), -         {:ok, user} <- upgrade_user(user, data) do -      if not already_ap do -        TransmogrifierWorker.enqueue("user_upgrade", %{"user_id" => user.id}) -      end - +         {:ok, user} <- update_user(user, data) do +      TransmogrifierWorker.enqueue("user_upgrade", %{"user_id" => user.id})        {:ok, user}      else        %User{} = user -> {:ok, user} @@ -1236,9 +1263,9 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do      end    end -  defp upgrade_user(user, data) do +  defp update_user(user, data) do      user -    |> User.upgrade_changeset(data, true) +    |> User.remote_user_changeset(data)      |> User.update_and_set_cache()    end diff --git a/lib/pleroma/web/activity_pub/views/user_view.ex b/lib/pleroma/web/activity_pub/views/user_view.ex index bc21ac6c7..34590b16d 100644 --- a/lib/pleroma/web/activity_pub/views/user_view.ex +++ b/lib/pleroma/web/activity_pub/views/user_view.ex @@ -79,10 +79,7 @@ defmodule Pleroma.Web.ActivityPub.UserView do      emoji_tags = Transmogrifier.take_emoji_tags(user) -    fields = -      user -      |> User.fields() -      |> Enum.map(&Map.put(&1, "type", "PropertyValue")) +    fields = Enum.map(user.fields, &Map.put(&1, "type", "PropertyValue"))      %{        "id" => user.ap_id, @@ -103,7 +100,7 @@ defmodule Pleroma.Web.ActivityPub.UserView do        },        "endpoints" => endpoints,        "attachment" => fields, -      "tag" => (user.source_data["tag"] || []) ++ emoji_tags, +      "tag" => emoji_tags,        "discoverable" => user.discoverable      }      |> Map.merge(maybe_make_image(&User.avatar_url/2, "icon", user)) diff --git a/lib/pleroma/web/admin_api/admin_api_controller.ex b/lib/pleroma/web/admin_api/admin_api_controller.ex index ca5439920..9c79310c0 100644 --- a/lib/pleroma/web/admin_api/admin_api_controller.ex +++ b/lib/pleroma/web/admin_api/admin_api_controller.ex @@ -27,7 +27,9 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do    alias Pleroma.Web.AdminAPI.Search    alias Pleroma.Web.CommonAPI    alias Pleroma.Web.Endpoint +  alias Pleroma.Web.MastodonAPI.AppView    alias Pleroma.Web.MastodonAPI.StatusView +  alias Pleroma.Web.OAuth.App    alias Pleroma.Web.Router    require Logger @@ -258,7 +260,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do      conn      |> put_view(Pleroma.Web.AdminAPI.StatusView) -    |> render("index.json", %{activities: activities, as: :activity}) +    |> render("index.json", %{activities: activities, as: :activity, skip_relationships: false})    end    def list_user_statuses(conn, %{"nickname" => nickname} = params) do @@ -277,7 +279,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do        conn        |> put_view(StatusView) -      |> render("index.json", %{activities: activities, as: :activity}) +      |> render("index.json", %{activities: activities, as: :activity, skip_relationships: false})      else        _ -> {:error, :not_found}      end @@ -576,9 +578,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do    @doc "Sends registration invite via email"    def email_invite(%{assigns: %{user: user}} = conn, %{"email" => email} = params) do -    with true <- -           Config.get([:instance, :invites_enabled]) && -             !Config.get([:instance, :registrations_open]), +    with {_, false} <- {:registrations_open, Config.get([:instance, :registrations_open])}, +         {_, true} <- {:invites_enabled, Config.get([:instance, :invites_enabled])},           {:ok, invite_token} <- UserInviteToken.create_invite(),           email <-             Pleroma.Emails.UserEmail.user_invitation_email( @@ -589,6 +590,18 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do             ),           {:ok, _} <- Pleroma.Emails.Mailer.deliver(email) do        json_response(conn, :no_content, "") +    else +      {:registrations_open, _} -> +        errors( +          conn, +          {:error, "To send invites you need to set the `registrations_open` option to false."} +        ) + +      {:invites_enabled, _} -> +        errors( +          conn, +          {:error, "To send invites you need to set the `invites_enabled` option to true."} +        )      end    end @@ -801,7 +814,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do      conn      |> put_view(Pleroma.Web.AdminAPI.StatusView) -    |> render("index.json", %{activities: activities, as: :activity}) +    |> render("index.json", %{activities: activities, as: :activity, skip_relationships: false})    end    def status_update(%{assigns: %{user: admin}} = conn, %{"id" => id} = params) do @@ -903,16 +916,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do          end)          |> List.flatten() -      response = %{configs: merged} - -      response = -        if Restarter.Pleroma.need_reboot?() do -          Map.put(response, :need_reboot, true) -        else -          response -        end - -      json(conn, response) +      json(conn, %{configs: merged, need_reboot: Restarter.Pleroma.need_reboot?()})      end    end @@ -939,28 +943,22 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do        Config.TransferTask.load_and_update_env(deleted, false) -      need_reboot? = -        Restarter.Pleroma.need_reboot?() || -          Enum.any?(updated, fn config -> +      if !Restarter.Pleroma.need_reboot?() do +        changed_reboot_settings? = +          (updated ++ deleted) +          |> Enum.any?(fn config ->              group = ConfigDB.from_string(config.group)              key = ConfigDB.from_string(config.key)              value = ConfigDB.from_binary(config.value)              Config.TransferTask.pleroma_need_restart?(group, key, value)            end) -      response = %{configs: updated} - -      response = -        if need_reboot? do -          Restarter.Pleroma.need_reboot() -          Map.put(response, :need_reboot, need_reboot?) -        else -          response -        end +        if changed_reboot_settings?, do: Restarter.Pleroma.need_reboot() +      end        conn        |> put_view(ConfigView) -      |> render("index.json", response) +      |> render("index.json", %{configs: updated, need_reboot: Restarter.Pleroma.need_reboot?()})      end    end @@ -972,6 +970,10 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do      end    end +  def need_reboot(conn, _params) do +    json(conn, %{need_reboot: Restarter.Pleroma.need_reboot?()}) +  end +    defp configurable_from_database(conn) do      if Config.get(:configurable_from_database) do        :ok @@ -1017,6 +1019,83 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do      conn |> json("")    end +  def oauth_app_create(conn, params) do +    params = +      if params["name"] do +        Map.put(params, "client_name", params["name"]) +      else +        params +      end + +    result = +      case App.create(params) do +        {:ok, app} -> +          AppView.render("show.json", %{app: app, admin: true}) + +        {:error, changeset} -> +          App.errors(changeset) +      end + +    json(conn, result) +  end + +  def oauth_app_update(conn, params) do +    params = +      if params["name"] do +        Map.put(params, "client_name", params["name"]) +      else +        params +      end + +    with {:ok, app} <- App.update(params) do +      json(conn, AppView.render("show.json", %{app: app, admin: true})) +    else +      {:error, changeset} -> +        json(conn, App.errors(changeset)) + +      nil -> +        json_response(conn, :bad_request, "") +    end +  end + +  def oauth_app_list(conn, params) do +    {page, page_size} = page_params(params) + +    search_params = %{ +      client_name: params["name"], +      client_id: params["client_id"], +      page: page, +      page_size: page_size +    } + +    search_params = +      if Map.has_key?(params, "trusted") do +        Map.put(search_params, :trusted, params["trusted"]) +      else +        search_params +      end + +    with {:ok, apps, count} <- App.search(search_params) do +      json( +        conn, +        AppView.render("index.json", +          apps: apps, +          count: count, +          page_size: page_size, +          admin: true +        ) +      ) +    end +  end + +  def oauth_app_delete(conn, params) do +    with {:ok, _app} <- App.destroy(params["id"]) do +      json_response(conn, :no_content, "") +    else +      _ -> json_response(conn, :bad_request, "") +    end +  end +    def stats(conn, _) do      count = Stats.get_status_visibility_count() diff --git a/lib/pleroma/web/admin_api/views/report_view.ex b/lib/pleroma/web/admin_api/views/report_view.ex index ca0bcebc7..d50969b2a 100644 --- a/lib/pleroma/web/admin_api/views/report_view.ex +++ b/lib/pleroma/web/admin_api/views/report_view.ex @@ -38,7 +38,12 @@ defmodule Pleroma.Web.AdminAPI.ReportView do        actor: merge_account_views(user),        content: content,        created_at: created_at, -      statuses: StatusView.render("index.json", %{activities: statuses, as: :activity}), +      statuses: +        StatusView.render("index.json", %{ +          activities: statuses, +          as: :activity, +          skip_relationships: false +        }),        state: report.data["state"],        notes: render(__MODULE__, "index_notes.json", %{notes: report.report_notes})      } diff --git a/lib/pleroma/web/api_spec.ex b/lib/pleroma/web/api_spec.ex new file mode 100644 index 000000000..3890489e3 --- /dev/null +++ b/lib/pleroma/web/api_spec.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.ApiSpec do +  alias OpenApiSpex.OpenApi +  alias Pleroma.Web.Endpoint +  alias Pleroma.Web.Router + +  @behaviour OpenApi + +  @impl OpenApi +  def spec do +    %OpenApi{ +      servers: [ +        # Populate the Server info from a phoenix endpoint +        OpenApiSpex.Server.from_endpoint(Endpoint) +      ], +      info: %OpenApiSpex.Info{ +        title: "Pleroma", +        description: Application.spec(:pleroma, :description) |> to_string(), +        version: Application.spec(:pleroma, :vsn) |> to_string() +      }, +      # populate the paths from a phoenix router +      paths: OpenApiSpex.Paths.from_router(Router), +      components: %OpenApiSpex.Components{ +        securitySchemes: %{ +          "oAuth" => %OpenApiSpex.SecurityScheme{ +            type: "oauth2", +            flows: %OpenApiSpex.OAuthFlows{ +              password: %OpenApiSpex.OAuthFlow{ +                authorizationUrl: "/oauth/authorize", +                tokenUrl: "/oauth/token", +                scopes: %{"read" => "read", "write" => "write", "follow" => "follow"} +              } +            } +          } +        } +      } +    } +    # discover request/response schemas from path specs +    |> OpenApiSpex.resolve_schema_modules() +  end +end diff --git a/lib/pleroma/web/api_spec/helpers.ex b/lib/pleroma/web/api_spec/helpers.ex new file mode 100644 index 000000000..7348dcbee --- /dev/null +++ b/lib/pleroma/web/api_spec/helpers.ex @@ -0,0 +1,27 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ApiSpec.Helpers do +  def request_body(description, schema_ref, opts \\ []) do +    media_types = ["application/json", "multipart/form-data", "application/x-www-form-urlencoded"] + +    content = +      media_types +      |> Enum.map(fn type -> +        {type, +         %OpenApiSpex.MediaType{ +           schema: schema_ref, +           example: opts[:example], +           examples: opts[:examples] +         }} +      end) +      |> Enum.into(%{}) + +    %OpenApiSpex.RequestBody{ +      description: description, +      content: content, +      required: opts[:required] || false +    } +  end +end diff --git a/lib/pleroma/web/api_spec/operations/app_operation.ex b/lib/pleroma/web/api_spec/operations/app_operation.ex new file mode 100644 index 000000000..26d8dbd42 --- /dev/null +++ b/lib/pleroma/web/api_spec/operations/app_operation.ex @@ -0,0 +1,96 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ApiSpec.AppOperation do +  alias OpenApiSpex.Operation +  alias OpenApiSpex.Schema +  alias Pleroma.Web.ApiSpec.Helpers +  alias Pleroma.Web.ApiSpec.Schemas.AppCreateRequest +  alias Pleroma.Web.ApiSpec.Schemas.AppCreateResponse + +  @spec open_api_operation(atom) :: Operation.t() +  def open_api_operation(action) do +    operation = String.to_existing_atom("#{action}_operation") +    apply(__MODULE__, operation, []) +  end + +  @spec create_operation() :: Operation.t() +  def create_operation do +    %Operation{ +      tags: ["apps"], +      summary: "Create an application", +      description: "Create a new application to obtain OAuth2 credentials", +      operationId: "AppController.create", +      requestBody: Helpers.request_body("Parameters", AppCreateRequest, required: true), +      responses: %{ +        200 => Operation.response("App", "application/json", AppCreateResponse), +        422 => +          Operation.response( +            "Unprocessable Entity", +            "application/json", +            %Schema{ +              type: :object, +              description: +                "If a required parameter is missing or improperly formatted, the request will fail.", +              properties: %{ +                error: %Schema{type: :string} +              }, +              example: %{ +                "error" => "Validation failed: Redirect URI must be an absolute URI." +              } +            } +          ) +      } +    } +  end + +  def verify_credentials_operation do +    %Operation{ +      tags: ["apps"], +      summary: "Verify your app works", +      description: "Confirm that the app's OAuth2 credentials work.", +      operationId: "AppController.verify_credentials", +      security: [ +        %{ +          "oAuth" => ["read"] +        } +      ], +      responses: %{ +        200 => +          Operation.response("App", "application/json", %Schema{ +            type: :object, +            description: +              "If the Authorization header was provided with a valid token, you should see your app returned as an Application entity.", +            properties: %{ +              name: %Schema{type: :string}, +              vapid_key: %Schema{type: :string}, +              website: %Schema{type: :string, nullable: true} +            }, +            example: %{ +              "name" => "My App", +              "vapid_key" => +                "BCk-QqERU0q-CfYZjcuB6lnyyOYfJ2AifKqfeGIm7Z-HiTU5T9eTG5GxVA0_OH5mMlI4UkkDTpaZwozy0TzdZ2M=", +              "website" => "https://myapp.com/" +            } +          }), +        422 => +          Operation.response( +            "Unauthorized", +            "application/json", +            %Schema{ +              type: :object, +              description: +                "If the Authorization header contains an invalid token, is malformed, or is not present, an error will be returned indicating an authorization failure.", +              properties: %{ +                error: %Schema{type: :string} +              }, +              example: %{ +                "error" => "The access token is invalid." +              } +            } +          ) +      } +    } +  end +end diff --git a/lib/pleroma/web/api_spec/operations/domain_block_operation.ex b/lib/pleroma/web/api_spec/operations/domain_block_operation.ex new file mode 100644 index 000000000..dd14837c3 --- /dev/null +++ b/lib/pleroma/web/api_spec/operations/domain_block_operation.ex @@ -0,0 +1,64 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ApiSpec.DomainBlockOperation do +  alias OpenApiSpex.Operation +  alias OpenApiSpex.Schema +  alias Pleroma.Web.ApiSpec.Helpers +  alias Pleroma.Web.ApiSpec.Schemas.DomainBlockRequest +  alias Pleroma.Web.ApiSpec.Schemas.DomainBlocksResponse + +  def open_api_operation(action) do +    operation = String.to_existing_atom("#{action}_operation") +    apply(__MODULE__, operation, []) +  end + +  def index_operation do +    %Operation{ +      tags: ["domain_blocks"], +      summary: "Fetch domain blocks", +      description: "View domains the user has blocked.", +      security: [%{"oAuth" => ["follow", "read:blocks"]}], +      operationId: "DomainBlockController.index", +      responses: %{ +        200 => Operation.response("Domain blocks", "application/json", DomainBlocksResponse) +      } +    } +  end + +  def create_operation do +    %Operation{ +      tags: ["domain_blocks"], +      summary: "Block a domain", +      description: """ +      Block a domain to: + +      - hide all public posts from it +      - hide all notifications from it +      - remove all followers from it +      - prevent following new users from it (but does not remove existing follows) +      """, +      operationId: "DomainBlockController.create", +      requestBody: Helpers.request_body("Parameters", DomainBlockRequest, required: true), +      security: [%{"oAuth" => ["follow", "write:blocks"]}], +      responses: %{ +        200 => Operation.response("Empty object", "application/json", %Schema{type: :object}) +      } +    } +  end + +  def delete_operation do +    %Operation{ +      tags: ["domain_blocks"], +      summary: "Unblock a domain", +      description: "Remove a domain block, if it exists in the user's array of blocked domains.", +      operationId: "DomainBlockController.delete", +      requestBody: Helpers.request_body("Parameters", DomainBlockRequest, required: true), +      security: [%{"oAuth" => ["follow", "write:blocks"]}], +      responses: %{ +        200 => Operation.response("Empty object", "application/json", %Schema{type: :object}) +      } +    } +  end +end diff --git a/lib/pleroma/web/api_spec/schemas/app_create_request.ex b/lib/pleroma/web/api_spec/schemas/app_create_request.ex new file mode 100644 index 000000000..8a83abef3 --- /dev/null +++ b/lib/pleroma/web/api_spec/schemas/app_create_request.ex @@ -0,0 +1,33 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ApiSpec.Schemas.AppCreateRequest do +  alias OpenApiSpex.Schema +  require OpenApiSpex + +  OpenApiSpex.schema(%{ +    title: "AppCreateRequest", +    description: "POST body for creating an app", +    type: :object, +    properties: %{ +      client_name: %Schema{type: :string, description: "A name for your application."}, +      redirect_uris: %Schema{ +        type: :string, +        description: +          "Where the user should be redirected after authorization. To display the authorization code to the user instead of redirecting to a web page, use `urn:ietf:wg:oauth:2.0:oob` in this parameter." +      }, +      scopes: %Schema{ +        type: :string, +        description: "Space separated list of scopes. If none is provided, defaults to `read`." +      }, +      website: %Schema{type: :string, description: "A URL to the homepage of your app"} +    }, +    required: [:client_name, :redirect_uris], +    example: %{ +      "client_name" => "My App", +      "redirect_uris" => "https://myapp.com/auth/callback", +      "website" => "https://myapp.com/" +    } +  }) +end diff --git a/lib/pleroma/web/api_spec/schemas/app_create_response.ex b/lib/pleroma/web/api_spec/schemas/app_create_response.ex new file mode 100644 index 000000000..f290fb031 --- /dev/null +++ b/lib/pleroma/web/api_spec/schemas/app_create_response.ex @@ -0,0 +1,33 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ApiSpec.Schemas.AppCreateResponse do +  alias OpenApiSpex.Schema + +  require OpenApiSpex + +  OpenApiSpex.schema(%{ +    title: "AppCreateResponse", +    description: "Response schema for an app", +    type: :object, +    properties: %{ +      id: %Schema{type: :string}, +      name: %Schema{type: :string}, +      client_id: %Schema{type: :string}, +      client_secret: %Schema{type: :string}, +      redirect_uri: %Schema{type: :string}, +      vapid_key: %Schema{type: :string}, +      website: %Schema{type: :string, nullable: true} +    }, +    example: %{ +      "id" => "123", +      "name" => "My App", +      "client_id" => "TWhM-tNSuncnqN7DBJmoyeLnk6K3iJJ71KKXxgL1hPM", +      "client_secret" => "ZEaFUFmF0umgBX1qKJDjaU99Q31lDkOU8NutzTOoliw", +      "vapid_key" => +        "BCk-QqERU0q-CfYZjcuB6lnyyOYfJ2AifKqfeGIm7Z-HiTU5T9eTG5GxVA0_OH5mMlI4UkkDTpaZwozy0TzdZ2M=", +      "website" => "https://myapp.com/" +    } +  }) +end diff --git a/lib/pleroma/web/api_spec/schemas/domain_block_request.ex b/lib/pleroma/web/api_spec/schemas/domain_block_request.ex new file mode 100644 index 000000000..ee9238361 --- /dev/null +++ b/lib/pleroma/web/api_spec/schemas/domain_block_request.ex @@ -0,0 +1,20 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ApiSpec.Schemas.DomainBlockRequest do +  alias OpenApiSpex.Schema +  require OpenApiSpex + +  OpenApiSpex.schema(%{ +    title: "DomainBlockRequest", +    type: :object, +    properties: %{ +      domain: %Schema{type: :string} +    }, +    required: [:domain], +    example: %{ +      "domain" => "facebook.com" +    } +  }) +end diff --git a/lib/pleroma/web/api_spec/schemas/domain_blocks_response.ex b/lib/pleroma/web/api_spec/schemas/domain_blocks_response.ex new file mode 100644 index 000000000..d895aca4e --- /dev/null +++ b/lib/pleroma/web/api_spec/schemas/domain_blocks_response.ex @@ -0,0 +1,16 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ApiSpec.Schemas.DomainBlocksResponse do +  require OpenApiSpex +  alias OpenApiSpex.Schema + +  OpenApiSpex.schema(%{ +    title: "DomainBlocksResponse", +    description: "Response schema for domain blocks", +    type: :array, +    items: %Schema{type: :string}, +    example: ["google.com", "facebook.com"] +  }) +end diff --git a/lib/pleroma/web/common_api/activity_draft.ex b/lib/pleroma/web/common_api/activity_draft.ex index c4356f93b..c1cd15bb2 100644 --- a/lib/pleroma/web/common_api/activity_draft.ex +++ b/lib/pleroma/web/common_api/activity_draft.ex @@ -187,7 +187,7 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do    end    defp preview?(draft) do -    preview? = Pleroma.Web.ControllerHelper.truthy_param?(draft.params["preview"]) || false +    preview? = Pleroma.Web.ControllerHelper.truthy_param?(draft.params["preview"])      %__MODULE__{draft | preview?: preview?}    end diff --git a/lib/pleroma/web/common_api/common_api.ex b/lib/pleroma/web/common_api/common_api.ex index 636cf3301..f50a909aa 100644 --- a/lib/pleroma/web/common_api/common_api.ex +++ b/lib/pleroma/web/common_api/common_api.ex @@ -45,7 +45,7 @@ defmodule Pleroma.Web.CommonAPI do      with {:ok, follower} <- User.follow(follower, followed),           %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),           {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "accept"), -         {:ok, _relationship} <- FollowingRelationship.update(follower, followed, "accept"), +         {:ok, _relationship} <- FollowingRelationship.update(follower, followed, :follow_accept),           {:ok, _activity} <-             ActivityPub.accept(%{               to: [follower.ap_id], @@ -60,7 +60,7 @@ defmodule Pleroma.Web.CommonAPI do    def reject_follow_request(follower, followed) do      with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),           {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "reject"), -         {:ok, _relationship} <- FollowingRelationship.update(follower, followed, "reject"), +         {:ok, _relationship} <- FollowingRelationship.update(follower, followed, :follow_reject),           {:ok, _activity} <-             ActivityPub.reject(%{               to: [follower.ap_id], @@ -332,26 +332,6 @@ defmodule Pleroma.Web.CommonAPI do    defp maybe_create_activity_expiration(result, _), do: result -  # Updates the emojis for a user based on their profile -  def update(user) do -    emoji = emoji_from_profile(user) -    source_data = Map.put(user.source_data, "tag", emoji) - -    user = -      case User.update_source_data(user, source_data) do -        {:ok, user} -> user -        _ -> user -      end - -    ActivityPub.update(%{ -      local: true, -      to: [Pleroma.Constants.as_public(), user.follower_address], -      cc: [], -      actor: user.ap_id, -      object: Pleroma.Web.ActivityPub.UserView.render("user.json", %{user: user}) -    }) -  end -    def pin(id_or_ap_id, %{ap_id: user_ap_id} = user) do      with %Activity{             actor: ^user_ap_id, diff --git a/lib/pleroma/web/common_api/utils.ex b/lib/pleroma/web/common_api/utils.ex index 635e7cd38..7eec5aa09 100644 --- a/lib/pleroma/web/common_api/utils.ex +++ b/lib/pleroma/web/common_api/utils.ex @@ -10,7 +10,6 @@ defmodule Pleroma.Web.CommonAPI.Utils do    alias Pleroma.Activity    alias Pleroma.Config    alias Pleroma.Conversation.Participation -  alias Pleroma.Emoji    alias Pleroma.Formatter    alias Pleroma.Object    alias Pleroma.Plugs.AuthenticationPlug @@ -18,7 +17,6 @@ defmodule Pleroma.Web.CommonAPI.Utils do    alias Pleroma.User    alias Pleroma.Web.ActivityPub.Utils    alias Pleroma.Web.ActivityPub.Visibility -  alias Pleroma.Web.Endpoint    alias Pleroma.Web.MediaProxy    require Logger @@ -175,7 +173,7 @@ defmodule Pleroma.Web.CommonAPI.Utils do              "replies" => %{"type" => "Collection", "totalItems" => 0}            } -          {note, Map.merge(emoji, Emoji.Formatter.get_emoji_map(option))} +          {note, Map.merge(emoji, Pleroma.Emoji.Formatter.get_emoji_map(option))}          end)        end_time = @@ -431,19 +429,6 @@ defmodule Pleroma.Web.CommonAPI.Utils do      end    end -  def emoji_from_profile(%User{bio: bio, name: name}) do -    [bio, name] -    |> Enum.map(&Emoji.Formatter.get_emoji/1) -    |> Enum.concat() -    |> Enum.map(fn {shortcode, %Emoji{file: path}} -> -      %{ -        "type" => "Emoji", -        "icon" => %{"type" => "Image", "url" => "#{Endpoint.url()}#{path}"}, -        "name" => ":#{shortcode}:" -      } -    end) -  end -    def maybe_notify_to_recipients(          recipients,          %Activity{data: %{"to" => to, "type" => _type}} = _activity diff --git a/lib/pleroma/web/controller_helper.ex b/lib/pleroma/web/controller_helper.ex index b49523ec3..4780081b2 100644 --- a/lib/pleroma/web/controller_helper.ex +++ b/lib/pleroma/web/controller_helper.ex @@ -5,10 +5,18 @@  defmodule Pleroma.Web.ControllerHelper do    use Pleroma.Web, :controller -  # As in MastoAPI, per https://api.rubyonrails.org/classes/ActiveModel/Type/Boolean.html +  alias Pleroma.Config + +  # As in Mastodon API, per https://api.rubyonrails.org/classes/ActiveModel/Type/Boolean.html    @falsy_param_values [false, 0, "0", "f", "F", "false", "False", "FALSE", "off", "OFF"] -  def truthy_param?(blank_value) when blank_value in [nil, ""], do: nil -  def truthy_param?(value), do: value not in @falsy_param_values + +  def explicitly_falsy_param?(value), do: value in @falsy_param_values + +  # Note: `nil` and `""` are considered falsy values in Pleroma +  def falsy_param?(value), +    do: explicitly_falsy_param?(value) or value in [nil, ""] + +  def truthy_param?(value), do: not falsy_param?(value)    def json_response(conn, status, json) do      conn @@ -96,4 +104,14 @@ defmodule Pleroma.Web.ControllerHelper do    def put_if_exist(map, _key, nil), do: map    def put_if_exist(map, key, value), do: Map.put(map, key, value) + +  @doc "Whether to skip rendering `[:account][:pleroma][:relationship]`for statuses/notifications" +  def skip_relationships?(params) do +    if Config.get([:extensions, :output_relationships_in_statuses_by_default]) do +      false +    else +      # BREAKING: older PleromaFE versions do not send this param but _do_ expect relationships. +      not truthy_param?(params["with_relationships"]) +    end +  end  end diff --git a/lib/pleroma/web/feed/feed_view.ex b/lib/pleroma/web/feed/feed_view.ex index e18adaea8..1ae03e7e2 100644 --- a/lib/pleroma/web/feed/feed_view.ex +++ b/lib/pleroma/web/feed/feed_view.ex @@ -23,7 +23,7 @@ defmodule Pleroma.Web.Feed.FeedView do    def pub_date(%DateTime{} = date), do: Timex.format!(date, "{RFC822}")    def prepare_activity(activity, opts \\ []) do -    object = activity_object(activity) +    object = Object.normalize(activity)      actor =        if opts[:actor] do @@ -33,7 +33,6 @@ defmodule Pleroma.Web.Feed.FeedView do      %{        activity: activity,        data: Map.get(object, :data), -      object: object,        actor: actor      }    end @@ -68,9 +67,7 @@ defmodule Pleroma.Web.Feed.FeedView do    def last_activity(activities), do: List.last(activities) -  def activity_object(activity), do: Object.normalize(activity) - -  def activity_title(%{data: %{"content" => content}}, opts \\ %{}) do +  def activity_title(%{"content" => content}, opts \\ %{}) do      content      |> Pleroma.Web.Metadata.Utils.scrub_html()      |> Pleroma.Emoji.Formatter.demojify() @@ -78,7 +75,7 @@ defmodule Pleroma.Web.Feed.FeedView do      |> escape()    end -  def activity_content(%{data: %{"content" => content}}) do +  def activity_content(%{"content" => content}) do      content      |> String.replace(~r/[\n\r]/, "")      |> escape() diff --git a/lib/pleroma/web/masto_fe_controller.ex b/lib/pleroma/web/masto_fe_controller.ex index 43649ad26..557cde328 100644 --- a/lib/pleroma/web/masto_fe_controller.ex +++ b/lib/pleroma/web/masto_fe_controller.ex @@ -17,7 +17,7 @@ defmodule Pleroma.Web.MastoFEController do      when action == :index    ) -  plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug when action != :index) +  plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug when action not in [:index, :manifest])    @doc "GET /web/*path"    def index(%{assigns: %{user: user, token: token}} = conn, _params) diff --git a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex index 21bc3d5a5..e8e59ac66 100644 --- a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex @@ -6,7 +6,13 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do    use Pleroma.Web, :controller    import Pleroma.Web.ControllerHelper, -    only: [add_link_headers: 2, truthy_param?: 1, assign_account_by_id: 2, json_response: 3] +    only: [ +      add_link_headers: 2, +      truthy_param?: 1, +      assign_account_by_id: 2, +      json_response: 3, +      skip_relationships?: 1 +    ]    alias Pleroma.Plugs.OAuthScopesPlug    alias Pleroma.Plugs.RateLimiter @@ -15,10 +21,13 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do    alias Pleroma.Web.CommonAPI    alias Pleroma.Web.MastodonAPI.ListView    alias Pleroma.Web.MastodonAPI.MastodonAPI +  alias Pleroma.Web.MastodonAPI.MastodonAPIController    alias Pleroma.Web.MastodonAPI.StatusView    alias Pleroma.Web.OAuth.Token    alias Pleroma.Web.TwitterAPI.TwitterAPI +  plug(:skip_plug, OAuthScopesPlug when action == :identity_proofs) +    plug(      OAuthScopesPlug,      %{fallback: :proceed_unauthenticated, scopes: ["read:accounts"]} @@ -95,6 +104,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do        |> Map.put("fullname", params["fullname"] || nickname)        |> Map.put("bio", params["bio"] || "")        |> Map.put("confirm", params["password"]) +      |> Map.put("trusted_app", app.trusted)      with :ok <- validate_email_param(params),           {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true), @@ -140,9 +150,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do    end    @doc "PATCH /api/v1/accounts/update_credentials" -  def update_credentials(%{assigns: %{user: original_user}} = conn, params) do -    user = original_user - +  def update_credentials(%{assigns: %{user: user}} = conn, params) do      user_params =        [          :no_rich_text, @@ -178,8 +186,6 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do      changeset = User.update_changeset(user, user_params)      with {:ok, user} <- User.update_and_set_cache(changeset) do -      if original_user != user, do: CommonAPI.update(user) -        render(conn, "show.json", user: user, for: user, with_pleroma_settings: true)      else        _e -> render_error(conn, :forbidden, "Invalid request") @@ -237,7 +243,12 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do        conn        |> add_link_headers(activities)        |> put_view(StatusView) -      |> render("index.json", activities: activities, for: reading_user, as: :activity) +      |> render("index.json", +        activities: activities, +        for: reading_user, +        as: :activity, +        skip_relationships: skip_relationships?(params) +      )      else        _e -> render_error(conn, :not_found, "Can't find user")      end @@ -369,6 +380,8 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do    end    @doc "GET /api/v1/endorsements" -  def endorsements(conn, params), -    do: Pleroma.Web.MastodonAPI.MastodonAPIController.empty_array(conn, params) +  def endorsements(conn, params), do: MastodonAPIController.empty_array(conn, params) + +  @doc "GET /api/v1/identity_proofs" +  def identity_proofs(conn, params), do: MastodonAPIController.empty_array(conn, params)  end diff --git a/lib/pleroma/web/mastodon_api/controllers/app_controller.ex b/lib/pleroma/web/mastodon_api/controllers/app_controller.ex index 5e2871f18..005c60444 100644 --- a/lib/pleroma/web/mastodon_api/controllers/app_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/app_controller.ex @@ -14,17 +14,20 @@ defmodule Pleroma.Web.MastodonAPI.AppController do    action_fallback(Pleroma.Web.MastodonAPI.FallbackController)    plug(OAuthScopesPlug, %{scopes: ["read"]} when action == :verify_credentials) +  plug(OpenApiSpex.Plug.CastAndValidate)    @local_mastodon_name "Mastodon-Local" +  defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.AppOperation +    @doc "POST /api/v1/apps" -  def create(conn, params) do +  def create(%{body_params: params} = conn, _params) do      scopes = Scopes.fetch_scopes(params, ["read"])      app_attrs =        params -      |> Map.drop(["scope", "scopes"]) -      |> Map.put("scopes", scopes) +      |> Map.take([:client_name, :redirect_uris, :website]) +      |> Map.put(:scopes, scopes)      with cs <- App.register_changeset(%App{}, app_attrs),           false <- cs.changes[:client_name] == @local_mastodon_name, diff --git a/lib/pleroma/web/mastodon_api/controllers/domain_block_controller.ex b/lib/pleroma/web/mastodon_api/controllers/domain_block_controller.ex index e4156cbe6..84de79413 100644 --- a/lib/pleroma/web/mastodon_api/controllers/domain_block_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/domain_block_controller.ex @@ -8,6 +8,9 @@ defmodule Pleroma.Web.MastodonAPI.DomainBlockController do    alias Pleroma.Plugs.OAuthScopesPlug    alias Pleroma.User +  plug(OpenApiSpex.Plug.CastAndValidate) +  defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.DomainBlockOperation +    plug(      OAuthScopesPlug,      %{scopes: ["follow", "read:blocks"]} when action == :index @@ -26,13 +29,13 @@ defmodule Pleroma.Web.MastodonAPI.DomainBlockController do    end    @doc "POST /api/v1/domain_blocks" -  def create(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do +  def create(%{assigns: %{user: blocker}, body_params: %{domain: domain}} = conn, _params) do      User.block_domain(blocker, domain)      json(conn, %{})    end    @doc "DELETE /api/v1/domain_blocks" -  def delete(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do +  def delete(%{assigns: %{user: blocker}, body_params: %{domain: domain}} = conn, _params) do      User.unblock_domain(blocker, domain)      json(conn, %{})    end diff --git a/lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex b/lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex index 14075307d..ac8c18f24 100644 --- a/lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex @@ -3,21 +3,31 @@  # SPDX-License-Identifier: AGPL-3.0-only  defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do +  @moduledoc """ +  Contains stubs for unimplemented Mastodon API endpoints. + +  Note: instead of routing directly to this controller's action, +    it's preferable to define an action in relevant (non-generic) controller, +    set up OAuth rules for it and call this controller's function from it. +  """ +    use Pleroma.Web, :controller    require Logger +  plug(:skip_plug, Pleroma.Plugs.OAuthScopesPlug when action in [:empty_array, :empty_object]) + +  plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug) +    action_fallback(Pleroma.Web.MastodonAPI.FallbackController) -  # Stubs for unimplemented mastodon api -  #    def empty_array(conn, _) do -    Logger.debug("Unimplemented, returning an empty array") +    Logger.debug("Unimplemented, returning an empty array (list)")      json(conn, [])    end    def empty_object(conn, _) do -    Logger.debug("Unimplemented, returning an empty object") +    Logger.debug("Unimplemented, returning an empty object (map)")      json(conn, %{})    end  end diff --git a/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex b/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex index 0c9218454..7fb536b09 100644 --- a/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex @@ -5,7 +5,7 @@  defmodule Pleroma.Web.MastodonAPI.NotificationController do    use Pleroma.Web, :controller -  import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2] +  import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2, skip_relationships?: 1]    alias Pleroma.Notification    alias Pleroma.Plugs.OAuthScopesPlug @@ -45,7 +45,11 @@ defmodule Pleroma.Web.MastodonAPI.NotificationController do      conn      |> add_link_headers(notifications) -    |> render("index.json", notifications: notifications, for: user) +    |> render("index.json", +      notifications: notifications, +      for: user, +      skip_relationships: skip_relationships?(params) +    )    end    # GET /api/v1/notifications/:id @@ -66,7 +70,8 @@ defmodule Pleroma.Web.MastodonAPI.NotificationController do      json(conn, %{})    end -  # POST /api/v1/notifications/dismiss +  # POST /api/v1/notifications/:id/dismiss +  # POST /api/v1/notifications/dismiss (deprecated)    def dismiss(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do      with {:ok, _notif} <- Notification.dismiss(user, id) do        json(conn, %{}) diff --git a/lib/pleroma/web/mastodon_api/controllers/search_controller.ex b/lib/pleroma/web/mastodon_api/controllers/search_controller.ex index fcab4ef63..c258742dd 100644 --- a/lib/pleroma/web/mastodon_api/controllers/search_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/search_controller.ex @@ -5,13 +5,14 @@  defmodule Pleroma.Web.MastodonAPI.SearchController do    use Pleroma.Web, :controller +  import Pleroma.Web.ControllerHelper, only: [fetch_integer_param: 2, skip_relationships?: 1] +    alias Pleroma.Activity    alias Pleroma.Plugs.OAuthScopesPlug    alias Pleroma.Plugs.RateLimiter    alias Pleroma.Repo    alias Pleroma.User    alias Pleroma.Web -  alias Pleroma.Web.ControllerHelper    alias Pleroma.Web.MastodonAPI.AccountView    alias Pleroma.Web.MastodonAPI.StatusView @@ -66,10 +67,11 @@ defmodule Pleroma.Web.MastodonAPI.SearchController do    defp search_options(params, user) do      [ +      skip_relationships: skip_relationships?(params),        resolve: params["resolve"] == "true",        following: params["following"] == "true", -      limit: ControllerHelper.fetch_integer_param(params, "limit"), -      offset: ControllerHelper.fetch_integer_param(params, "offset"), +      limit: fetch_integer_param(params, "limit"), +      offset: fetch_integer_param(params, "offset"),        type: params["type"],        author: get_author(params),        for_user: user @@ -79,12 +81,24 @@ defmodule Pleroma.Web.MastodonAPI.SearchController do    defp resource_search(_, "accounts", query, options) do      accounts = with_fallback(fn -> User.search(query, options) end) -    AccountView.render("index.json", users: accounts, for: options[:for_user], as: :user) + +    AccountView.render("index.json", +      users: accounts, +      for: options[:for_user], +      as: :user, +      skip_relationships: false +    )    end    defp resource_search(_, "statuses", query, options) do      statuses = with_fallback(fn -> Activity.search(options[:for_user], query, options) end) -    StatusView.render("index.json", activities: statuses, for: options[:for_user], as: :activity) + +    StatusView.render("index.json", +      activities: statuses, +      for: options[:for_user], +      as: :activity, +      skip_relationships: options[:skip_relationships] +    )    end    defp resource_search(:v2, "hashtags", query, _options) do diff --git a/lib/pleroma/web/mastodon_api/controllers/status_controller.ex b/lib/pleroma/web/mastodon_api/controllers/status_controller.ex index ec8f0d8a0..397dd10e3 100644 --- a/lib/pleroma/web/mastodon_api/controllers/status_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/status_controller.ex @@ -5,7 +5,8 @@  defmodule Pleroma.Web.MastodonAPI.StatusController do    use Pleroma.Web, :controller -  import Pleroma.Web.ControllerHelper, only: [try_render: 3, add_link_headers: 2] +  import Pleroma.Web.ControllerHelper, +    only: [try_render: 3, add_link_headers: 2, skip_relationships?: 1]    require Ecto.Query @@ -101,7 +102,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do    `ids` query param is required    """ -  def index(%{assigns: %{user: user}} = conn, %{"ids" => ids}) do +  def index(%{assigns: %{user: user}} = conn, %{"ids" => ids} = params) do      limit = 100      activities = @@ -110,7 +111,12 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do        |> Activity.all_by_ids_with_object()        |> Enum.filter(&Visibility.visible_for_user?(&1, user)) -    render(conn, "index.json", activities: activities, for: user, as: :activity) +    render(conn, "index.json", +      activities: activities, +      for: user, +      as: :activity, +      skip_relationships: skip_relationships?(params) +    )    end    @doc """ @@ -360,7 +366,12 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do      conn      |> add_link_headers(activities) -    |> render("index.json", activities: activities, for: user, as: :activity) +    |> render("index.json", +      activities: activities, +      for: user, +      as: :activity, +      skip_relationships: skip_relationships?(params) +    )    end    @doc "GET /api/v1/bookmarks" @@ -378,6 +389,11 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do      conn      |> add_link_headers(bookmarks) -    |> render("index.json", %{activities: activities, for: user, as: :activity}) +    |> render("index.json", +      activities: activities, +      for: user, +      as: :activity, +      skip_relationships: skip_relationships?(params) +    )    end  end diff --git a/lib/pleroma/web/mastodon_api/controllers/suggestion_controller.ex b/lib/pleroma/web/mastodon_api/controllers/suggestion_controller.ex index 0cdc7bd8d..c93a43969 100644 --- a/lib/pleroma/web/mastodon_api/controllers/suggestion_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/suggestion_controller.ex @@ -5,10 +5,13 @@  defmodule Pleroma.Web.MastodonAPI.SuggestionController do    use Pleroma.Web, :controller +  alias Pleroma.Plugs.OAuthScopesPlug +    require Logger +  plug(OAuthScopesPlug, %{scopes: ["read"]} when action == :index) +    @doc "GET /api/v1/suggestions" -  def index(conn, _) do -    json(conn, []) -  end +  def index(conn, params), +    do: Pleroma.Web.MastodonAPI.MastodonAPIController.empty_array(conn, params)  end diff --git a/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex b/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex index 91f41416d..b3c58005e 100644 --- a/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex @@ -6,7 +6,7 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do    use Pleroma.Web, :controller    import Pleroma.Web.ControllerHelper, -    only: [add_link_headers: 2, add_link_headers: 3, truthy_param?: 1] +    only: [add_link_headers: 2, add_link_headers: 3, truthy_param?: 1, skip_relationships?: 1]    alias Pleroma.Pagination    alias Pleroma.Plugs.OAuthScopesPlug @@ -14,9 +14,8 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do    alias Pleroma.User    alias Pleroma.Web.ActivityPub.ActivityPub -  # TODO: Replace with a macro when there is a Phoenix release with +  # TODO: Replace with a macro when there is a Phoenix release with the following commit in it:    # https://github.com/phoenixframework/phoenix/commit/2e8c63c01fec4dde5467dbbbf9705ff9e780735e -  # in it    plug(RateLimiter, [name: :timeline, bucket_name: :direct_timeline] when action == :direct)    plug(RateLimiter, [name: :timeline, bucket_name: :public_timeline] when action == :public) @@ -49,7 +48,12 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do      conn      |> add_link_headers(activities) -    |> render("index.json", activities: activities, for: user, as: :activity) +    |> render("index.json", +      activities: activities, +      for: user, +      as: :activity, +      skip_relationships: skip_relationships?(params) +    )    end    # GET /api/v1/timelines/direct @@ -68,7 +72,12 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do      conn      |> add_link_headers(activities) -    |> render("index.json", activities: activities, for: user, as: :activity) +    |> render("index.json", +      activities: activities, +      for: user, +      as: :activity, +      skip_relationships: skip_relationships?(params) +    )    end    # GET /api/v1/timelines/public @@ -95,7 +104,12 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do        conn        |> add_link_headers(activities, %{"local" => local_only}) -      |> render("index.json", activities: activities, for: user, as: :activity) +      |> render("index.json", +        activities: activities, +        for: user, +        as: :activity, +        skip_relationships: skip_relationships?(params) +      )      else        render_error(conn, :unauthorized, "authorization required for timeline view")      end @@ -140,7 +154,12 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do      conn      |> add_link_headers(activities, %{"local" => local_only}) -    |> render("index.json", activities: activities, for: user, as: :activity) +    |> render("index.json", +      activities: activities, +      for: user, +      as: :activity, +      skip_relationships: skip_relationships?(params) +    )    end    # GET /api/v1/timelines/list/:list_id @@ -164,7 +183,12 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do          |> ActivityPub.fetch_activities_bounded(following, params)          |> Enum.reverse() -      render(conn, "index.json", activities: activities, for: user, as: :activity) +      render(conn, "index.json", +        activities: activities, +        for: user, +        as: :activity, +        skip_relationships: skip_relationships?(params) +      )      else        _e -> render_error(conn, :forbidden, "Error.")      end diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex index 99e62f580..b4b61e74c 100644 --- a/lib/pleroma/web/mastodon_api/views/account_view.ex +++ b/lib/pleroma/web/mastodon_api/views/account_view.ex @@ -15,6 +15,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do    def render("index.json", %{users: users} = opts) do      reading_user = opts[:for] +    # Note: :skip_relationships option is currently intentionally not supported for accounts      relationships_opt =        cond do          Map.has_key?(opts, :relationships) -> @@ -73,7 +74,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do      followed_by =        if following_relationships do          case FollowingRelationship.find(following_relationships, target, reading_user) do -          %{state: "accept"} -> true +          %{state: :follow_accept} -> true            _ -> false          end        else @@ -83,7 +84,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do      # NOTE: adjust UserRelationship.view_relationships_option/2 on new relation-related flags      %{        id: to_string(target.id), -      following: follow_state == "accept", +      following: follow_state == :follow_accept,        followed_by: followed_by,        blocking:          UserRelationship.exists?( @@ -125,7 +126,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do            reading_user,            &User.subscribed_to?(&2, &1)          ), -      requested: follow_state == "pending", +      requested: follow_state == :follow_pending,        domain_blocking: User.blocks_domain?(reading_user, target),        showing_reblogs:          not UserRelationship.exists?( @@ -180,23 +181,25 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do      bot = user.actor_type in ["Application", "Service"]      emojis = -      (user.source_data["tag"] || []) -      |> Enum.filter(fn %{"type" => t} -> t == "Emoji" end) -      |> Enum.map(fn %{"icon" => %{"url" => url}, "name" => name} -> +      Enum.map(user.emoji, fn {shortcode, url} ->          %{ -          "shortcode" => String.trim(name, ":"), -          "url" => MediaProxy.url(url), -          "static_url" => MediaProxy.url(url), +          "shortcode" => shortcode, +          "url" => url, +          "static_url" => url,            "visible_in_picker" => false          }        end)      relationship = -      render("relationship.json", %{ -        user: opts[:for], -        target: user, -        relationships: opts[:relationships] -      }) +      if opts[:skip_relationships] do +        %{} +      else +        render("relationship.json", %{ +          user: opts[:for], +          target: user, +          relationships: opts[:relationships] +        }) +      end      %{        id: to_string(user.id), diff --git a/lib/pleroma/web/mastodon_api/views/app_view.ex b/lib/pleroma/web/mastodon_api/views/app_view.ex index d934e2107..36071cd25 100644 --- a/lib/pleroma/web/mastodon_api/views/app_view.ex +++ b/lib/pleroma/web/mastodon_api/views/app_view.ex @@ -7,6 +7,21 @@ defmodule Pleroma.Web.MastodonAPI.AppView do    alias Pleroma.Web.OAuth.App +  def render("index.json", %{apps: apps, count: count, page_size: page_size, admin: true}) do +    %{ +      apps: render_many(apps, Pleroma.Web.MastodonAPI.AppView, "show.json", %{admin: true}), +      count: count, +      page_size: page_size +    } +  end + +  def render("show.json", %{admin: true, app: %App{} = app} = assigns) do +    "show.json" +    |> render(Map.delete(assigns, :admin)) +    |> Map.put(:trusted, app.trusted) +    |> Map.put(:id, app.id) +  end +    def render("show.json", %{app: %App{} = app}) do      %{        id: app.id |> to_string, diff --git a/lib/pleroma/web/mastodon_api/views/notification_view.ex b/lib/pleroma/web/mastodon_api/views/notification_view.ex index 7001fd7b9..4da1ab67f 100644 --- a/lib/pleroma/web/mastodon_api/views/notification_view.ex +++ b/lib/pleroma/web/mastodon_api/views/notification_view.ex @@ -51,14 +51,15 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do              |> Enum.filter(& &1)              |> Kernel.++(move_activities_targets) -          UserRelationship.view_relationships_option(reading_user, actors) +          UserRelationship.view_relationships_option(reading_user, actors, +            source_mutes_only: opts[:skip_relationships] +          )        end -    opts = %{ -      for: reading_user, -      parent_activities: parent_activities, -      relationships: relationships_opt -    } +    opts = +      opts +      |> Map.put(:parent_activities, parent_activities) +      |> Map.put(:relationships, relationships_opt)      safe_render_many(notifications, NotificationView, "show.json", opts)    end @@ -82,12 +83,16 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do      mastodon_type = Activity.mastodon_notification_type(activity) +    render_opts = %{ +      relationships: opts[:relationships], +      skip_relationships: opts[:skip_relationships] +    } +      with %{id: _} = account <- -           AccountView.render("show.json", %{ -             user: actor, -             for: reading_user, -             relationships: opts[:relationships] -           }) do +           AccountView.render( +             "show.json", +             Map.merge(render_opts, %{user: actor, for: reading_user}) +           ) do        response = %{          id: to_string(notification.id),          type: mastodon_type, @@ -98,8 +103,6 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do          }        } -      render_opts = %{relationships: opts[:relationships]} -        case mastodon_type do          "mention" ->            put_status(response, activity, reading_user, render_opts) @@ -111,6 +114,7 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do            put_status(response, parent_activity_fn.(), reading_user, render_opts)          "move" -> +          # Note: :skip_relationships option being applied to _account_ rendering (here)            put_target(response, activity, reading_user, render_opts)          "pleroma:emoji_reaction" -> diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex index cea76e735..b5850e1ae 100644 --- a/lib/pleroma/web/mastodon_api/views/status_view.ex +++ b/lib/pleroma/web/mastodon_api/views/status_view.ex @@ -99,7 +99,9 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do          true ->            actors = Enum.map(activities ++ parent_activities, &get_user(&1.data["actor"])) -          UserRelationship.view_relationships_option(reading_user, actors) +          UserRelationship.view_relationships_option(reading_user, actors, +            source_mutes_only: opts[:skip_relationships] +          )        end      opts = @@ -153,7 +155,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do          AccountView.render("show.json", %{            user: user,            for: opts[:for], -          relationships: opts[:relationships] +          relationships: opts[:relationships], +          skip_relationships: opts[:skip_relationships]          }),        in_reply_to_id: nil,        in_reply_to_account_id: nil, @@ -301,6 +304,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do          _ -> []        end +    # Status muted state (would do 1 request per status unless user mutes are preloaded)      muted =        thread_muted? ||          UserRelationship.exists?( @@ -319,7 +323,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do          AccountView.render("show.json", %{            user: user,            for: opts[:for], -          relationships: opts[:relationships] +          relationships: opts[:relationships], +          skip_relationships: opts[:skip_relationships]          }),        in_reply_to_id: reply_to && to_string(reply_to.id),        in_reply_to_account_id: reply_to_user && to_string(reply_to_user.id), diff --git a/lib/pleroma/web/oauth/app.ex b/lib/pleroma/web/oauth/app.ex index 01ed326f4..6a6d5f2e2 100644 --- a/lib/pleroma/web/oauth/app.ex +++ b/lib/pleroma/web/oauth/app.ex @@ -5,6 +5,7 @@  defmodule Pleroma.Web.OAuth.App do    use Ecto.Schema    import Ecto.Changeset +  import Ecto.Query    alias Pleroma.Repo    @type t :: %__MODULE__{} @@ -16,14 +17,24 @@ defmodule Pleroma.Web.OAuth.App do      field(:website, :string)      field(:client_id, :string)      field(:client_secret, :string) +    field(:trusted, :boolean, default: false) + +    has_many(:oauth_authorizations, Pleroma.Web.OAuth.Authorization, on_delete: :delete_all) +    has_many(:oauth_tokens, Pleroma.Web.OAuth.Token, on_delete: :delete_all)      timestamps()    end +  @spec changeset(App.t(), map()) :: Ecto.Changeset.t() +  def changeset(struct, params) do +    cast(struct, params, [:client_name, :redirect_uris, :scopes, :website, :trusted]) +  end + +  @spec register_changeset(App.t(), map()) :: Ecto.Changeset.t()    def register_changeset(struct, params \\ %{}) do      changeset =        struct -      |> cast(params, [:client_name, :redirect_uris, :scopes, :website]) +      |> changeset(params)        |> validate_required([:client_name, :redirect_uris, :scopes])      if changeset.valid? do @@ -41,6 +52,21 @@ defmodule Pleroma.Web.OAuth.App do      end    end +  @spec create(map()) :: {:ok, App.t()} | {:error, Ecto.Changeset.t()} +  def create(params) do +    with changeset <- __MODULE__.register_changeset(%__MODULE__{}, params) do +      Repo.insert(changeset) +    end +  end + +  @spec update(map()) :: {:ok, App.t()} | {:error, Ecto.Changeset.t()} +  def update(params) do +    with %__MODULE__{} = app <- Repo.get(__MODULE__, params["id"]), +         changeset <- changeset(app, params) do +      Repo.update(changeset) +    end +  end +    @doc """    Gets app by attrs or create new  with attrs.    And updates the scopes if need. @@ -65,4 +91,58 @@ defmodule Pleroma.Web.OAuth.App do      |> change(%{scopes: scopes})      |> Repo.update()    end + +  @spec search(map()) :: {:ok, [App.t()], non_neg_integer()} +  def search(params) do +    query = from(a in __MODULE__) + +    query = +      if params[:client_name] do +        from(a in query, where: a.client_name == ^params[:client_name]) +      else +        query +      end + +    query = +      if params[:client_id] do +        from(a in query, where: a.client_id == ^params[:client_id]) +      else +        query +      end + +    query = +      if Map.has_key?(params, :trusted) do +        from(a in query, where: a.trusted == ^params[:trusted]) +      else +        query +      end + +    query = +      from(u in query, +        limit: ^params[:page_size], +        offset: ^((params[:page] - 1) * params[:page_size]) +      ) + +    count = Repo.aggregate(__MODULE__, :count, :id) + +    {:ok, Repo.all(query), count} +  end + +  @spec destroy(pos_integer()) :: {:ok, App.t()} | {:error, Ecto.Changeset.t()} +  def destroy(id) do +    with %__MODULE__{} = app <- Repo.get(__MODULE__, id) do +      Repo.delete(app) +    end +  end + +  @spec errors(Ecto.Changeset.t()) :: map() +  def errors(changeset) do +    Enum.reduce(changeset.errors, %{}, fn +      {:client_name, {error, _}}, acc -> +        Map.put(acc, :name, error) + +      {key, {error, _}}, acc -> +        Map.put(acc, key, error) +    end) +  end  end diff --git a/lib/pleroma/web/oauth/oauth_controller.ex b/lib/pleroma/web/oauth/oauth_controller.ex index 46688db7e..0121cd661 100644 --- a/lib/pleroma/web/oauth/oauth_controller.ex +++ b/lib/pleroma/web/oauth/oauth_controller.ex @@ -27,6 +27,8 @@ defmodule Pleroma.Web.OAuth.OAuthController do    plug(:fetch_flash)    plug(RateLimiter, [name: :authentication] when action == :create_authorization) +  plug(:skip_plug, Pleroma.Plugs.OAuthScopesPlug) +    action_fallback(Pleroma.Web.OAuth.FallbackController)    @oob_token_redirect_uri "urn:ietf:wg:oauth:2.0:oob" diff --git a/lib/pleroma/web/oauth/scopes.ex b/lib/pleroma/web/oauth/scopes.ex index 8ecf901f3..1023f16d4 100644 --- a/lib/pleroma/web/oauth/scopes.ex +++ b/lib/pleroma/web/oauth/scopes.ex @@ -15,7 +15,12 @@ defmodule Pleroma.Web.OAuth.Scopes do    Note: `scopes` is used by Mastodon — supporting it but sticking to    OAuth's standard `scope` wherever we control it    """ -  @spec fetch_scopes(map(), list()) :: list() +  @spec fetch_scopes(map() | struct(), list()) :: list() + +  def fetch_scopes(%Pleroma.Web.ApiSpec.Schemas.AppCreateRequest{scopes: scopes}, default) do +    parse_scopes(scopes, default) +  end +    def fetch_scopes(params, default) do      parse_scopes(params["scope"] || params["scopes"], default)    end diff --git a/lib/pleroma/web/pleroma_api/controllers/account_controller.ex b/lib/pleroma/web/pleroma_api/controllers/account_controller.ex index dcba67d03..60405fbff 100644 --- a/lib/pleroma/web/pleroma_api/controllers/account_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/account_controller.ex @@ -6,14 +6,13 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do    use Pleroma.Web, :controller    import Pleroma.Web.ControllerHelper, -    only: [json_response: 3, add_link_headers: 2, assign_account_by_id: 2] +    only: [json_response: 3, add_link_headers: 2, assign_account_by_id: 2, skip_relationships?: 1]    alias Ecto.Changeset    alias Pleroma.Plugs.OAuthScopesPlug    alias Pleroma.Plugs.RateLimiter    alias Pleroma.User    alias Pleroma.Web.ActivityPub.ActivityPub -  alias Pleroma.Web.CommonAPI    alias Pleroma.Web.MastodonAPI.StatusView    require Pleroma.Constants @@ -58,38 +57,32 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do    @doc "PATCH /api/v1/pleroma/accounts/update_avatar"    def update_avatar(%{assigns: %{user: user}} = conn, %{"img" => ""}) do -    {:ok, user} = +    {:ok, _user} =        user        |> Changeset.change(%{avatar: nil})        |> User.update_and_set_cache() -    CommonAPI.update(user) -      json(conn, %{url: nil})    end    def update_avatar(%{assigns: %{user: user}} = conn, params) do      {:ok, %{data: data}} = ActivityPub.upload(params, type: :avatar) -    {:ok, user} = user |> Changeset.change(%{avatar: data}) |> User.update_and_set_cache() +    {:ok, _user} = user |> Changeset.change(%{avatar: data}) |> User.update_and_set_cache()      %{"url" => [%{"href" => href} | _]} = data -    CommonAPI.update(user) -      json(conn, %{url: href})    end    @doc "PATCH /api/v1/pleroma/accounts/update_banner"    def update_banner(%{assigns: %{user: user}} = conn, %{"banner" => ""}) do -    with {:ok, user} <- User.update_banner(user, %{}) do -      CommonAPI.update(user) +    with {:ok, _user} <- User.update_banner(user, %{}) do        json(conn, %{url: nil})      end    end    def update_banner(%{assigns: %{user: user}} = conn, params) do      with {:ok, object} <- ActivityPub.upload(%{"img" => params["banner"]}, type: :banner), -         {:ok, user} <- User.update_banner(user, object.data) do -      CommonAPI.update(user) +         {:ok, _user} <- User.update_banner(user, object.data) do        %{"url" => [%{"href" => href} | _]} = object.data        json(conn, %{url: href}) @@ -139,7 +132,12 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do      conn      |> add_link_headers(activities)      |> put_view(StatusView) -    |> render("index.json", activities: activities, for: for_user, as: :activity) +    |> render("index.json", +      activities: activities, +      for: for_user, +      as: :activity, +      skip_relationships: skip_relationships?(params) +    )    end    @doc "POST /api/v1/pleroma/accounts/:id/subscribe" diff --git a/lib/pleroma/web/pleroma_api/controllers/pleroma_api_controller.ex b/lib/pleroma/web/pleroma_api/controllers/pleroma_api_controller.ex index dae7f0f2f..fe1b97a20 100644 --- a/lib/pleroma/web/pleroma_api/controllers/pleroma_api_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/pleroma_api_controller.ex @@ -5,7 +5,7 @@  defmodule Pleroma.Web.PleromaAPI.PleromaAPIController do    use Pleroma.Web, :controller -  import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2] +  import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2, skip_relationships?: 1]    alias Pleroma.Activity    alias Pleroma.Conversation.Participation @@ -34,7 +34,7 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIController do    plug(      OAuthScopesPlug, -    %{scopes: ["write:conversations"]} when action == :update_conversation +    %{scopes: ["write:conversations"]} when action in [:update_conversation, :read_conversations]    )    plug(OAuthScopesPlug, %{scopes: ["write:notifications"]} when action == :read_notification) @@ -110,12 +110,11 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIController do    end    def conversation_statuses( -        %{assigns: %{user: user}} = conn, +        %{assigns: %{user: %{id: user_id} = user}} = conn,          %{"id" => participation_id} = params        ) do -    with %Participation{} = participation <- -           Participation.get(participation_id, preload: [:conversation]), -         true <- user.id == participation.user_id do +    with %Participation{user_id: ^user_id} = participation <- +           Participation.get(participation_id, preload: [:conversation]) do        params =          params          |> Map.put("blocking_user", user) @@ -124,13 +123,19 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIController do        activities =          participation.conversation.ap_id -        |> ActivityPub.fetch_activities_for_context(params) +        |> ActivityPub.fetch_activities_for_context_query(params) +        |> Pleroma.Pagination.fetch_paginated(Map.put(params, "total", false))          |> Enum.reverse()        conn        |> add_link_headers(activities)        |> put_view(StatusView) -      |> render("index.json", %{activities: activities, for: user, as: :activity}) +      |> render("index.json", +        activities: activities, +        for: user, +        as: :activity, +        skip_relationships: skip_relationships?(params) +      )      else        _error ->          conn @@ -184,13 +189,17 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIController do      end    end -  def read_notification(%{assigns: %{user: user}} = conn, %{"max_id" => max_id}) do +  def read_notification(%{assigns: %{user: user}} = conn, %{"max_id" => max_id} = params) do      with notifications <- Notification.set_read_up_to(user, max_id) do        notifications = Enum.take(notifications, 80)        conn        |> put_view(NotificationView) -      |> render("index.json", %{notifications: notifications, for: user}) +      |> render("index.json", +        notifications: notifications, +        for: user, +        skip_relationships: skip_relationships?(params) +      )      end    end  end diff --git a/lib/pleroma/web/rich_media/helpers.ex b/lib/pleroma/web/rich_media/helpers.ex index 0314535d2..9d3d7f978 100644 --- a/lib/pleroma/web/rich_media/helpers.ex +++ b/lib/pleroma/web/rich_media/helpers.ex @@ -64,5 +64,8 @@ defmodule Pleroma.Web.RichMedia.Helpers do    def fetch_data_for_activity(_), do: %{} -  def perform(:fetch, %Activity{} = activity), do: fetch_data_for_activity(activity) +  def perform(:fetch, %Activity{} = activity) do +    fetch_data_for_activity(activity) +    :ok +  end  end diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 5a0902739..7e5960949 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -29,11 +29,13 @@ defmodule Pleroma.Web.Router do      plug(Pleroma.Plugs.SetUserSessionIdPlug)      plug(Pleroma.Plugs.EnsureUserKeyPlug)      plug(Pleroma.Plugs.IdempotencyPlug) +    plug(OpenApiSpex.Plug.PutApiSpec, module: Pleroma.Web.ApiSpec)    end    pipeline :authenticated_api do      plug(:accepts, ["json"])      plug(:fetch_session) +    plug(Pleroma.Plugs.AuthExpectedPlug)      plug(Pleroma.Plugs.OAuthPlug)      plug(Pleroma.Plugs.BasicAuthDecoderPlug)      plug(Pleroma.Plugs.UserFetcherPlug) @@ -44,6 +46,7 @@ defmodule Pleroma.Web.Router do      plug(Pleroma.Plugs.SetUserSessionIdPlug)      plug(Pleroma.Plugs.EnsureAuthenticatedPlug)      plug(Pleroma.Plugs.IdempotencyPlug) +    plug(OpenApiSpex.Plug.PutApiSpec, module: Pleroma.Web.ApiSpec)    end    pipeline :admin_api do @@ -61,6 +64,7 @@ defmodule Pleroma.Web.Router do      plug(Pleroma.Plugs.EnsureAuthenticatedPlug)      plug(Pleroma.Plugs.UserIsAdminPlug)      plug(Pleroma.Plugs.IdempotencyPlug) +    plug(OpenApiSpex.Plug.PutApiSpec, module: Pleroma.Web.ApiSpec)    end    pipeline :mastodon_html do @@ -94,10 +98,12 @@ defmodule Pleroma.Web.Router do    pipeline :config do      plug(:accepts, ["json", "xml"]) +    plug(OpenApiSpex.Plug.PutApiSpec, module: Pleroma.Web.ApiSpec)    end    pipeline :pleroma_api do      plug(:accepts, ["html", "json"]) +    plug(OpenApiSpex.Plug.PutApiSpec, module: Pleroma.Web.ApiSpec)    end    pipeline :mailbox_preview do @@ -198,12 +204,18 @@ defmodule Pleroma.Web.Router do      get("/config", AdminAPIController, :config_show)      post("/config", AdminAPIController, :config_update)      get("/config/descriptions", AdminAPIController, :config_descriptions) +    get("/need_reboot", AdminAPIController, :need_reboot)      get("/restart", AdminAPIController, :restart)      get("/moderation_log", AdminAPIController, :list_log)      post("/reload_emoji", AdminAPIController, :reload_emoji)      get("/stats", AdminAPIController, :stats) + +    get("/oauth_app", AdminAPIController, :oauth_app_list) +    post("/oauth_app", AdminAPIController, :oauth_app_create) +    patch("/oauth_app/:id", AdminAPIController, :oauth_app_update) +    delete("/oauth_app/:id", AdminAPIController, :oauth_app_delete)    end    scope "/api/pleroma/emoji", Pleroma.Web.PleromaAPI do @@ -333,7 +345,7 @@ defmodule Pleroma.Web.Router do      get("/accounts/relationships", AccountController, :relationships)      get("/accounts/:id/lists", AccountController, :lists) -    get("/accounts/:id/identity_proofs", MastodonAPIController, :empty_array) +    get("/accounts/:id/identity_proofs", AccountController, :identity_proofs)      get("/follow_requests", FollowRequestController, :index)      get("/blocks", AccountController, :blocks) @@ -347,9 +359,11 @@ defmodule Pleroma.Web.Router do      get("/notifications", NotificationController, :index)      get("/notifications/:id", NotificationController, :show) +    post("/notifications/:id/dismiss", NotificationController, :dismiss)      post("/notifications/clear", NotificationController, :clear) -    post("/notifications/dismiss", NotificationController, :dismiss)      delete("/notifications/destroy_multiple", NotificationController, :destroy_multiple) +    # Deprecated: was removed in Mastodon v3, use `/notifications/:id/dismiss` instead +    post("/notifications/dismiss", NotificationController, :dismiss)      get("/scheduled_statuses", ScheduledActivityController, :index)      get("/scheduled_statuses/:id", ScheduledActivityController, :show) @@ -500,6 +514,12 @@ defmodule Pleroma.Web.Router do      )    end +  scope "/api" do +    pipe_through(:api) + +    get("/openapi", OpenApiSpex.Plug.RenderSpec, []) +  end +    scope "/api", Pleroma.Web, as: :authenticated_twitter_api do      pipe_through(:authenticated_api) @@ -658,6 +678,17 @@ defmodule Pleroma.Web.Router do      end    end +  # Test-only routes needed to test action dispatching and plug chain execution +  if Pleroma.Config.get(:env) == :test do +    scope "/test/authenticated_api", Pleroma.Tests do +      pipe_through(:authenticated_api) + +      for action <- [:skipped_oauth, :performed_oauth, :missed_oauth] do +        get("/#{action}", OAuthTestController, action) +      end +    end +  end +    scope "/", Pleroma.Web.MongooseIM do      get("/user_exists", MongooseIMController, :user_exists)      get("/check_password", MongooseIMController, :check_password) diff --git a/lib/pleroma/web/static_fe/static_fe_view.ex b/lib/pleroma/web/static_fe/static_fe_view.ex index 66d87620c..b3d1d1ec8 100644 --- a/lib/pleroma/web/static_fe/static_fe_view.ex +++ b/lib/pleroma/web/static_fe/static_fe_view.ex @@ -18,15 +18,6 @@ defmodule Pleroma.Web.StaticFE.StaticFEView do    @media_types ["image", "audio", "video"] -  def emoji_for_user(%User{} = user) do -    user.source_data -    |> Map.get("tag", []) -    |> Enum.filter(fn %{"type" => t} -> t == "Emoji" end) -    |> Enum.map(fn %{"icon" => %{"url" => url}, "name" => name} -> -      {String.trim(name, ":"), url} -    end) -  end -    def fetch_media_type(%{"mediaType" => mediaType}) do      Utils.fetch_media_type(@media_types, mediaType)    end diff --git a/lib/pleroma/web/templates/feed/feed/_activity.atom.eex b/lib/pleroma/web/templates/feed/feed/_activity.atom.eex index ac8a75009..78350f2aa 100644 --- a/lib/pleroma/web/templates/feed/feed/_activity.atom.eex +++ b/lib/pleroma/web/templates/feed/feed/_activity.atom.eex @@ -2,10 +2,10 @@    <activity:object-type>http://activitystrea.ms/schema/1.0/note</activity:object-type>    <activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb>    <id><%= @data["id"] %></id> -  <title><%= activity_title(@object, Keyword.get(@feed_config, :post_title, %{})) %></title> -  <content type="html"><%= activity_content(@object) %></content> -  <published><%= @data["published"] %></published> -  <updated><%= @data["published"] %></updated> +  <title><%= activity_title(@data, Keyword.get(@feed_config, :post_title, %{})) %></title> +  <content type="html"><%= activity_content(@data) %></content> +  <published><%= @activity.data["published"] %></published> +  <updated><%= @activity.data["published"] %></updated>    <ostatus:conversation ref="<%= activity_context(@activity) %>">      <%= activity_context(@activity) %>    </ostatus:conversation> diff --git a/lib/pleroma/web/templates/feed/feed/_activity.rss.eex b/lib/pleroma/web/templates/feed/feed/_activity.rss.eex index a4dbed638..a304a16af 100644 --- a/lib/pleroma/web/templates/feed/feed/_activity.rss.eex +++ b/lib/pleroma/web/templates/feed/feed/_activity.rss.eex @@ -2,10 +2,10 @@    <activity:object-type>http://activitystrea.ms/schema/1.0/note</activity:object-type>    <activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb>    <guid><%= @data["id"] %></guid> -  <title><%= activity_title(@object, Keyword.get(@feed_config, :post_title, %{})) %></title> -  <description><%= activity_content(@object) %></description> -  <pubDate><%= @data["published"] %></pubDate> -  <updated><%= @data["published"] %></updated> +  <title><%= activity_title(@data, Keyword.get(@feed_config, :post_title, %{})) %></title> +  <description><%= activity_content(@data) %></description> +  <pubDate><%= @activity.data["published"] %></pubDate> +  <updated><%= @activity.data["published"] %></updated>    <ostatus:conversation ref="<%= activity_context(@activity) %>">      <%= activity_context(@activity) %>    </ostatus:conversation> diff --git a/lib/pleroma/web/templates/feed/feed/_tag_activity.atom.eex b/lib/pleroma/web/templates/feed/feed/_tag_activity.atom.eex index da4fa6d6c..cf5874a91 100644 --- a/lib/pleroma/web/templates/feed/feed/_tag_activity.atom.eex +++ b/lib/pleroma/web/templates/feed/feed/_tag_activity.atom.eex @@ -1,12 +1,12 @@  <entry>      <activity:object-type>http://activitystrea.ms/schema/1.0/note</activity:object-type>      <activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb> -     +      <%= render @view_module, "_tag_author.atom", assigns %> -     +      <id><%= @data["id"] %></id> -    <title><%= activity_title(@object, Keyword.get(@feed_config, :post_title, %{})) %></title> -    <content type="html"><%= activity_content(@object) %></content> +    <title><%= activity_title(@data, Keyword.get(@feed_config, :post_title, %{})) %></title> +    <content type="html"><%= activity_content(@data) %></content>    <%= if @activity.local do %>      <link type="application/atom+xml" href='<%= @data["id"] %>' rel="self"/> @@ -15,8 +15,8 @@      <link type="text/html" href='<%= @data["external_url"] %>' rel="alternate"/>    <% end %> -    <published><%= @data["published"] %></published> -    <updated><%= @data["published"] %></updated> +    <published><%= @activity.data["published"] %></published> +    <updated><%= @activity.data["published"] %></updated>      <ostatus:conversation ref="<%= activity_context(@activity) %>">        <%= activity_context(@activity) %> @@ -26,7 +26,7 @@     <%= if @data["summary"] do %>      <summary><%= @data["summary"] %></summary>     <% end %> -   +      <%= for id <- @activity.recipients do %>        <%= if id == Pleroma.Constants.as_public() do %>          <link rel="mentioned" @@ -40,7 +40,7 @@          <% end %>        <% end %>      <% end %> -   +      <%= for tag <- @data["tag"] || [] do %>        <category term="<%= tag %>"></category>      <% end %> diff --git a/lib/pleroma/web/templates/feed/feed/_tag_activity.xml.eex b/lib/pleroma/web/templates/feed/feed/_tag_activity.xml.eex index 295574df1..2334e24a2 100644 --- a/lib/pleroma/web/templates/feed/feed/_tag_activity.xml.eex +++ b/lib/pleroma/web/templates/feed/feed/_tag_activity.xml.eex @@ -1,15 +1,14 @@  <item> -  <title><%= activity_title(@object, Keyword.get(@feed_config, :post_title, %{})) %></title> -   -   +  <title><%= activity_title(@data, Keyword.get(@feed_config, :post_title, %{})) %></title> + +    <guid isPermalink="true"><%= activity_context(@activity) %></guid>    <link><%= activity_context(@activity) %></link> -  <pubDate><%= pub_date(@data["published"]) %></pubDate> -   -  <description><%= activity_content(@object) %></description> +  <pubDate><%= pub_date(@activity.data["published"]) %></pubDate> + +  <description><%= activity_content(@data) %></description>    <%= for attachment <- @data["attachment"] || [] do %>      <enclosure url="<%= attachment_href(attachment) %>" type="<%= attachment_type(attachment) %>"/>    <% end %> -   -</item> +</item> diff --git a/lib/pleroma/web/templates/static_fe/static_fe/_user_card.html.eex b/lib/pleroma/web/templates/static_fe/static_fe/_user_card.html.eex index 2a7582d45..56f3a1524 100644 --- a/lib/pleroma/web/templates/static_fe/static_fe/_user_card.html.eex +++ b/lib/pleroma/web/templates/static_fe/static_fe/_user_card.html.eex @@ -4,7 +4,7 @@        <img src="<%= User.avatar_url(@user) |> MediaProxy.url %>" width="48" height="48" alt="">      </div>      <span class="display-name"> -      <bdi><%= raw (@user.name |> Formatter.emojify(emoji_for_user(@user))) %></bdi> +      <bdi><%= raw Formatter.emojify(@user.name, @user.emoji) %></bdi>        <span class="nickname"><%= @user.nickname %></span>      </span>    </a> diff --git a/lib/pleroma/web/templates/static_fe/static_fe/profile.html.eex b/lib/pleroma/web/templates/static_fe/static_fe/profile.html.eex index e7d2aecad..3191bf450 100644 --- a/lib/pleroma/web/templates/static_fe/static_fe/profile.html.eex +++ b/lib/pleroma/web/templates/static_fe/static_fe/profile.html.eex @@ -7,7 +7,7 @@        <input type="hidden" name="profile" value="">        <button type="submit" class="collapse">Remote follow</button>      </form> -    <%= raw Formatter.emojify(@user.name, emoji_for_user(@user)) %> | +    <%= raw Formatter.emojify(@user.name, @user.emoji) %> |      <%= link "@#{@user.nickname}@#{Endpoint.host()}", to: (@user.uri || @user.ap_id) %>    </h3>    <p><%= raw @user.bio %></p> diff --git a/lib/pleroma/web/twitter_api/twitter_api.ex b/lib/pleroma/web/twitter_api/twitter_api.ex index f9c0994da..7a1ba6936 100644 --- a/lib/pleroma/web/twitter_api/twitter_api.ex +++ b/lib/pleroma/web/twitter_api/twitter_api.ex @@ -13,6 +13,7 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI do    def register_user(params, opts \\ []) do      token = params["token"] +    trusted_app? = params["trusted_app"]      params = %{        nickname: params["nickname"], @@ -29,7 +30,7 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI do      captcha_enabled = Pleroma.Config.get([Pleroma.Captcha, :enabled])      # true if captcha is disabled or enabled and valid, false otherwise      captcha_ok = -      if not captcha_enabled do +      if trusted_app? || not captcha_enabled do          :ok        else          Pleroma.Captcha.validate( diff --git a/lib/pleroma/web/twitter_api/twitter_api_controller.ex b/lib/pleroma/web/twitter_api/twitter_api_controller.ex index 0229aea97..31adc2817 100644 --- a/lib/pleroma/web/twitter_api/twitter_api_controller.ex +++ b/lib/pleroma/web/twitter_api/twitter_api_controller.ex @@ -15,6 +15,8 @@ defmodule Pleroma.Web.TwitterAPI.Controller do    plug(OAuthScopesPlug, %{scopes: ["write:notifications"]} when action == :notifications_read) +  plug(:skip_plug, OAuthScopesPlug when action in [:oauth_tokens, :revoke_token]) +    plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)    action_fallback(:errors) diff --git a/lib/pleroma/web/web.ex b/lib/pleroma/web/web.ex index cf3ac1287..ae7c94640 100644 --- a/lib/pleroma/web/web.ex +++ b/lib/pleroma/web/web.ex @@ -29,11 +29,40 @@ defmodule Pleroma.Web do        import Pleroma.Web.Router.Helpers        import Pleroma.Web.TranslationHelpers +      alias Pleroma.Plugs.PlugHelper +        plug(:set_put_layout)        defp set_put_layout(conn, _) do          put_layout(conn, Pleroma.Config.get(:app_layout, "app.html"))        end + +      # Marks a plug intentionally skipped and blocks its execution if it's present in plugs chain +      defp skip_plug(conn, plug_module) do +        try do +          plug_module.ensure_skippable() +        rescue +          UndefinedFunctionError -> +            raise "#{plug_module} is not skippable. Append `use Pleroma.Web, :plug` to its code." +        end + +        PlugHelper.append_to_skipped_plugs(conn, plug_module) +      end + +      # Here we can apply before-action hooks (e.g. verify whether auth checks were preformed) +      defp action(conn, params) do +        if Pleroma.Plugs.AuthExpectedPlug.auth_expected?(conn) && +             not PlugHelper.plug_called_or_skipped?(conn, Pleroma.Plugs.OAuthScopesPlug) do +          conn +          |> render_error( +            :forbidden, +            "Security violation: OAuth scopes check was neither handled nor explicitly skipped." +          ) +          |> halt() +        else +          super(conn, params) +        end +      end      end    end @@ -96,6 +125,26 @@ defmodule Pleroma.Web do      end    end +  def plug do +    quote do +      alias Pleroma.Plugs.PlugHelper + +      def ensure_skippable, do: :noop + +      @impl Plug +      @doc "If marked as skipped, returns `conn`, and calls `perform/2` otherwise." +      def call(%Plug.Conn{} = conn, options) do +        if PlugHelper.plug_skipped?(conn, __MODULE__) do +          conn +        else +          conn +          |> PlugHelper.append_to_called_plugs(__MODULE__) +          |> perform(options) +        end +      end +    end +  end +    @doc """    When used, dispatch to the appropriate controller/view/etc.    """ | 
