diff options
Diffstat (limited to 'lib')
82 files changed, 2486 insertions, 666 deletions
diff --git a/lib/pleroma/plugs/auth_expected_plug.ex b/lib/pleroma/plugs/auth_expected_plug.ex deleted file mode 100644 index f79597dc3..000000000 --- a/lib/pleroma/plugs/auth_expected_plug.ex +++ /dev/null @@ -1,17 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.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/ensure_authenticated_plug.ex b/lib/pleroma/plugs/ensure_authenticated_plug.ex index 054d2297f..9c8f5597f 100644 --- a/lib/pleroma/plugs/ensure_authenticated_plug.ex +++ b/lib/pleroma/plugs/ensure_authenticated_plug.ex @@ -5,17 +5,21 @@ defmodule Pleroma.Plugs.EnsureAuthenticatedPlug do import Plug.Conn import Pleroma.Web.TranslationHelpers + alias Pleroma.User + use Pleroma.Web, :plug + def init(options) do options end - def call(%{assigns: %{user: %User{}}} = conn, _) do + @impl true + def perform(%{assigns: %{user: %User{}}} = conn, _) do conn end - def call(conn, options) do + def perform(conn, options) do perform = cond do options[:if_func] -> options[:if_func].() diff --git a/lib/pleroma/plugs/ensure_public_or_authenticated_plug.ex b/lib/pleroma/plugs/ensure_public_or_authenticated_plug.ex index d980ff13d..7265bb87a 100644 --- a/lib/pleroma/plugs/ensure_public_or_authenticated_plug.ex +++ b/lib/pleroma/plugs/ensure_public_or_authenticated_plug.ex @@ -5,14 +5,18 @@ defmodule Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug do import Pleroma.Web.TranslationHelpers import Plug.Conn + alias Pleroma.Config alias Pleroma.User + use Pleroma.Web, :plug + def init(options) do options end - def call(conn, _) do + @impl true + def perform(conn, _) do public? = Config.get!([:instance, :public]) case {public?, conn} do diff --git a/lib/pleroma/plugs/expect_authenticated_check_plug.ex b/lib/pleroma/plugs/expect_authenticated_check_plug.ex new file mode 100644 index 000000000..66b8d5de5 --- /dev/null +++ b/lib/pleroma/plugs/expect_authenticated_check_plug.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.Plugs.ExpectAuthenticatedCheckPlug do + @moduledoc """ + Marks `Pleroma.Plugs.EnsureAuthenticatedPlug` as expected to be executed later in plug chain. + + No-op plug which affects `Pleroma.Web` operation (is checked with `PlugHelper.plug_called?/2`). + """ + + use Pleroma.Web, :plug + + def init(options), do: options + + @impl true + def perform(conn, _) do + conn + end +end diff --git a/lib/pleroma/plugs/expect_public_or_authenticated_check_plug.ex b/lib/pleroma/plugs/expect_public_or_authenticated_check_plug.ex new file mode 100644 index 000000000..ba0ef76bd --- /dev/null +++ b/lib/pleroma/plugs/expect_public_or_authenticated_check_plug.ex @@ -0,0 +1,21 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Plugs.ExpectPublicOrAuthenticatedCheckPlug do + @moduledoc """ + Marks `Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug` as expected to be executed later in plug + chain. + + No-op plug which affects `Pleroma.Web` operation (is checked with `PlugHelper.plug_called?/2`). + """ + + use Pleroma.Web, :plug + + def init(options), do: options + + @impl true + def perform(conn, _) do + conn + end +end diff --git a/lib/pleroma/plugs/http_security_plug.ex b/lib/pleroma/plugs/http_security_plug.ex index 81e6b4f2a..6462797b6 100644 --- a/lib/pleroma/plugs/http_security_plug.ex +++ b/lib/pleroma/plugs/http_security_plug.ex @@ -75,7 +75,7 @@ defmodule Pleroma.Plugs.HTTPSecurityPlug do "default-src 'none'", "base-uri 'self'", "frame-ancestors 'none'", - "img-src 'self' data: https:", + "img-src 'self' data: blob: https:", "media-src 'self' https:", "style-src 'self' 'unsafe-inline'", "font-src 'self'", diff --git a/lib/pleroma/plugs/oauth_scopes_plug.ex b/lib/pleroma/plugs/oauth_scopes_plug.ex index 66f48c28c..efc25b79f 100644 --- a/lib/pleroma/plugs/oauth_scopes_plug.ex +++ b/lib/pleroma/plugs/oauth_scopes_plug.ex @@ -7,15 +7,12 @@ defmodule Pleroma.Plugs.OAuthScopesPlug do import Pleroma.Web.Gettext alias Pleroma.Config - alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug - alias Pleroma.Plugs.PlugHelper use Pleroma.Web, :plug - @behaviour Plug - def init(%{scopes: _} = options), do: options + @impl true def perform(%Plug.Conn{assigns: assigns} = conn, %{scopes: scopes} = options) do op = options[:op] || :| token = assigns[:token] @@ -31,10 +28,7 @@ defmodule Pleroma.Plugs.OAuthScopesPlug do conn options[:fallback] == :proceed_unauthenticated -> - conn - |> assign(:user, nil) - |> assign(:token, nil) - |> maybe_perform_instance_privacy_check(options) + drop_auth_info(conn) true -> missing_scopes = scopes -- matched_scopes @@ -50,6 +44,15 @@ defmodule Pleroma.Plugs.OAuthScopesPlug do end end + @doc "Drops authentication info from connection" + def drop_auth_info(conn) do + # To simplify debugging, setting a private variable on `conn` if auth info is dropped + conn + |> put_private(:authentication_ignored, true) + |> assign(:user, nil) + |> assign(:token, nil) + end + @doc "Filters descendants of supported scopes" def filter_descendants(scopes, supported_scopes) do Enum.filter( @@ -71,12 +74,4 @@ defmodule Pleroma.Plugs.OAuthScopesPlug do scopes end end - - defp maybe_perform_instance_privacy_check(%Plug.Conn{} = conn, options) do - if options[:skip_instance_privacy_check] do - conn - else - EnsurePublicOrAuthenticatedPlug.call(conn, []) - end - end end diff --git a/lib/pleroma/stats.ex b/lib/pleroma/stats.ex index 4446562ac..8d2809bbb 100644 --- a/lib/pleroma/stats.ex +++ b/lib/pleroma/stats.ex @@ -45,11 +45,11 @@ defmodule Pleroma.Stats do end def init(_args) do - {:ok, get_stat_data()} + {:ok, calculate_stat_data()} end def handle_call(:force_update, _from, _state) do - new_stats = get_stat_data() + new_stats = calculate_stat_data() {:reply, new_stats, new_stats} end @@ -58,12 +58,12 @@ defmodule Pleroma.Stats do end def handle_cast(:run_update, _state) do - new_stats = get_stat_data() + new_stats = calculate_stat_data() {:noreply, new_stats} end - defp get_stat_data do + def calculate_stat_data do peers = from( u in User, @@ -77,7 +77,15 @@ defmodule Pleroma.Stats do status_count = Repo.aggregate(User.Query.build(%{local: true}), :sum, :note_count) - user_count = Repo.aggregate(User.Query.build(%{local: true, active: true}), :count, :id) + users_query = + from(u in User, + where: u.deactivated != true, + where: u.local == true, + where: not is_nil(u.nickname), + where: not u.invisible + ) + + user_count = Repo.aggregate(users_query, :count, :id) %{ peers: peers, diff --git a/lib/pleroma/tests/auth_test_controller.ex b/lib/pleroma/tests/auth_test_controller.ex new file mode 100644 index 000000000..fb04411d9 --- /dev/null +++ b/lib/pleroma/tests/auth_test_controller.ex @@ -0,0 +1,93 @@ +# 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. +defmodule Pleroma.Tests.AuthTestController do + @moduledoc false + + use Pleroma.Web, :controller + + alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug + alias Pleroma.Plugs.OAuthScopesPlug + alias Pleroma.User + + # Serves only with proper OAuth token (:api and :authenticated_api) + # Skipping EnsurePublicOrAuthenticatedPlug has no effect in this case + # + # Suggested use case: all :authenticated_api endpoints (makes no sense for :api endpoints) + plug(OAuthScopesPlug, %{scopes: ["read"]} when action == :do_oauth_check) + + # Via :api, keeps :user if token has requested scopes (if :user is dropped, serves if public) + # Via :authenticated_api, serves if token is present and has requested scopes + # + # Suggested use case: vast majority of :api endpoints (no sense for :authenticated_api ones) + plug( + OAuthScopesPlug, + %{scopes: ["read"], fallback: :proceed_unauthenticated} + when action == :fallback_oauth_check + ) + + # Keeps :user if present, executes regardless of token / token scopes + # Fails with no :user for :authenticated_api / no user for :api on private instance + # Note: EnsurePublicOrAuthenticatedPlug is not skipped (private instance fails on no :user) + # Note: Basic Auth processing results in :skip_plug call for OAuthScopesPlug + # + # Suggested use: suppressing OAuth checks for other auth mechanisms (like Basic Auth) + # For controller-level use, see :skip_oauth_skip_publicity_check instead + plug( + :skip_plug, + OAuthScopesPlug when action == :skip_oauth_check + ) + + # (Shouldn't be executed since the plug is skipped) + plug(OAuthScopesPlug, %{scopes: ["admin"]} when action == :skip_oauth_check) + + # Via :api, keeps :user if token has requested scopes, and continues with nil :user otherwise + # Via :authenticated_api, serves if token is present and has requested scopes + # + # Suggested use: as :fallback_oauth_check but open with nil :user for :api on private instances + plug( + :skip_plug, + EnsurePublicOrAuthenticatedPlug when action == :fallback_oauth_skip_publicity_check + ) + + plug( + OAuthScopesPlug, + %{scopes: ["read"], fallback: :proceed_unauthenticated} + when action == :fallback_oauth_skip_publicity_check + ) + + # Via :api, keeps :user if present, serves regardless of token presence / scopes / :user presence + # Via :authenticated_api, serves if :user is set (regardless of token presence and its scopes) + # + # Suggested use: making an :api endpoint always accessible (e.g. email confirmation endpoint) + plug( + :skip_plug, + [OAuthScopesPlug, EnsurePublicOrAuthenticatedPlug] + when action == :skip_oauth_skip_publicity_check + ) + + # Via :authenticated_api, always fails with 403 (endpoint is insecure) + # Via :api, drops :user if present and serves if public (private instance rejects on no user) + # + # Suggested use: none; please define OAuth rules for all :api / :authenticated_api endpoints + plug(:skip_plug, [] when action == :missing_oauth_check_definition) + + def do_oauth_check(conn, _params), do: conn_state(conn) + + def fallback_oauth_check(conn, _params), do: conn_state(conn) + + def skip_oauth_check(conn, _params), do: conn_state(conn) + + def fallback_oauth_skip_publicity_check(conn, _params), do: conn_state(conn) + + def skip_oauth_skip_publicity_check(conn, _params), do: conn_state(conn) + + def missing_oauth_check_definition(conn, _params), do: conn_state(conn) + + defp conn_state(%{assigns: %{user: %User{} = user}} = conn), + do: json(conn, %{user_id: user.id}) + + defp conn_state(conn), do: json(conn, %{user_id: nil}) +end diff --git a/lib/pleroma/tests/oauth_test_controller.ex b/lib/pleroma/tests/oauth_test_controller.ex deleted file mode 100644 index 58d517f78..000000000 --- a/lib/pleroma/tests/oauth_test_controller.ex +++ /dev/null @@ -1,31 +0,0 @@ -# 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 477237756..b451202b2 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -832,6 +832,7 @@ defmodule Pleroma.User do def set_cache(%User{} = user) do Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user) Cachex.put(:user_cache, "nickname:#{user.nickname}", user) + Cachex.put(:user_cache, "friends_ap_ids:#{user.nickname}", get_user_friends_ap_ids(user)) {:ok, user} end @@ -847,9 +848,22 @@ defmodule Pleroma.User do end end + def get_user_friends_ap_ids(user) do + from(u in User.get_friends_query(user), select: u.ap_id) + |> Repo.all() + end + + @spec get_cached_user_friends_ap_ids(User.t()) :: [String.t()] + def get_cached_user_friends_ap_ids(user) do + Cachex.fetch!(:user_cache, "friends_ap_ids:#{user.ap_id}", fn _ -> + get_user_friends_ap_ids(user) + end) + end + def invalidate_cache(user) do Cachex.del(:user_cache, "ap_id:#{user.ap_id}") Cachex.del(:user_cache, "nickname:#{user.nickname}") + Cachex.del(:user_cache, "friends_ap_ids:#{user.ap_id}") end @spec get_cached_by_ap_id(String.t()) :: User.t() | nil diff --git a/lib/pleroma/user/query.ex b/lib/pleroma/user/query.ex index ec88088cf..ac77aab71 100644 --- a/lib/pleroma/user/query.ex +++ b/lib/pleroma/user/query.ex @@ -54,13 +54,13 @@ defmodule Pleroma.User.Query do select: term(), limit: pos_integer() } - | %{} + | map() @ilike_criteria [:nickname, :name, :query] @equal_criteria [:email] @contains_criteria [:ap_id, :nickname] - @spec build(criteria()) :: Query.t() + @spec build(Query.t(), criteria()) :: Query.t() def build(query \\ base_query(), criteria) do prepare_query(query, criteria) end diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index 4a133498e..1f4a09370 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -398,36 +398,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do end end - # TODO: This is weird, maybe we shouldn't check here if we can make the activity. - @spec like(User.t(), Object.t(), String.t() | nil, boolean()) :: - {:ok, Activity.t(), Object.t()} | {:error, any()} - def like(user, object, activity_id \\ nil, local \\ true) do - with {:ok, result} <- Repo.transaction(fn -> do_like(user, object, activity_id, local) end) do - result - end - end - - defp do_like( - %User{ap_id: ap_id} = user, - %Object{data: %{"id" => _}} = object, - activity_id, - local - ) do - with nil <- get_existing_like(ap_id, object), - like_data <- make_like_data(user, object, activity_id), - {:ok, activity} <- insert(like_data, local), - {:ok, object} <- add_like_to_object(activity, object), - :ok <- maybe_federate(activity) do - {:ok, activity, object} - else - %Activity{} = activity -> - {:ok, activity, object} - - {:error, error} -> - Repo.rollback(error) - end - end - @spec unlike(User.t(), Object.t(), String.t() | nil, boolean()) :: {:ok, Activity.t(), Activity.t(), Object.t()} | {:ok, Object.t()} | {:error, any()} def unlike(%User{} = actor, %Object{} = object, activity_id \\ nil, local \\ true) do @@ -468,6 +438,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do defp do_announce(user, object, activity_id, local, public) do with true <- is_announceable?(object, user, public), + object <- Object.get_by_id(object.id), announce_data <- make_announce_data(user, object, activity_id, public), {:ok, activity} <- insert(announce_data, local), {:ok, object} <- add_announce_to_object(activity, object), @@ -854,7 +825,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do end defp exclude_visibility(query, %{"exclude_visibilities" => visibility}) - when visibility not in @valid_visibilities do + when visibility not in [nil | @valid_visibilities] do Logger.error("Could not exclude visibility to #{visibility}") query end @@ -1061,7 +1032,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do raise "Can't use the child object without preloading!" end - defp restrict_media(query, %{"only_media" => val}) when val == "true" or val == "1" do + defp restrict_media(query, %{"only_media" => val}) when val in [true, "true", "1"] do from( [_activity, object] in query, where: fragment("not (?)->'attachment' = (?)", object.data, ^[]) @@ -1070,16 +1041,51 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do defp restrict_media(query, _), do: query - defp restrict_replies(query, %{"exclude_replies" => val}) when val == "true" or val == "1" do + defp restrict_replies(query, %{"exclude_replies" => val}) when val in [true, "true", "1"] do from( [_activity, object] in query, where: fragment("?->>'inReplyTo' is null", object.data) ) end + defp restrict_replies(query, %{ + "reply_filtering_user" => user, + "reply_visibility" => "self" + }) do + from( + [activity, object] in query, + where: + fragment( + "?->>'inReplyTo' is null OR ? = ANY(?)", + object.data, + ^user.ap_id, + activity.recipients + ) + ) + end + + defp restrict_replies(query, %{ + "reply_filtering_user" => user, + "reply_visibility" => "following" + }) do + from( + [activity, object] in query, + where: + fragment( + "?->>'inReplyTo' is null OR ? && array_remove(?, ?) OR ? = ?", + object.data, + ^[user.ap_id | User.get_cached_user_friends_ap_ids(user)], + activity.recipients, + activity.actor, + activity.actor, + ^user.ap_id + ) + ) + end + defp restrict_replies(query, _), do: query - defp restrict_reblogs(query, %{"exclude_reblogs" => val}) when val == "true" or val == "1" do + defp restrict_reblogs(query, %{"exclude_reblogs" => val}) when val in [true, "true", "1"] do from(activity in query, where: fragment("?->>'type' != 'Announce'", activity.data)) end @@ -1158,7 +1164,12 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do ) end - defp restrict_pinned(query, %{"pinned" => "true", "pinned_activity_ids" => ids}) do + # TODO: when all endpoints migrated to OpenAPI compare `pinned` with `true` (boolean) only, + # the same for `restrict_media/2`, `restrict_replies/2`, 'restrict_reblogs/2' + # and `restrict_muted/2` + + defp restrict_pinned(query, %{"pinned" => pinned, "pinned_activity_ids" => ids}) + when pinned in [true, "true", "1"] do from(activity in query, where: activity.id in ^ids) end @@ -1291,6 +1302,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do |> maybe_set_thread_muted_field(opts) |> maybe_order(opts) |> restrict_recipients(recipients, opts["user"]) + |> restrict_replies(opts) |> restrict_tag(opts) |> restrict_tag_reject(opts) |> restrict_tag_all(opts) @@ -1305,7 +1317,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do |> restrict_media(opts) |> restrict_visibility(opts) |> restrict_thread_visibility(opts, config) - |> restrict_replies(opts) |> restrict_reblogs(opts) |> restrict_pinned(opts) |> restrict_muted_reblogs(restrict_muted_reblogs_opts) diff --git a/lib/pleroma/web/activity_pub/activity_pub_controller.ex b/lib/pleroma/web/activity_pub/activity_pub_controller.ex index 8b9eb4a2c..d625530ec 100644 --- a/lib/pleroma/web/activity_pub/activity_pub_controller.ex +++ b/lib/pleroma/web/activity_pub/activity_pub_controller.ex @@ -12,8 +12,10 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do alias Pleroma.Plugs.EnsureAuthenticatedPlug alias Pleroma.User alias Pleroma.Web.ActivityPub.ActivityPub + alias Pleroma.Web.ActivityPub.Builder alias Pleroma.Web.ActivityPub.InternalFetchActor alias Pleroma.Web.ActivityPub.ObjectView + alias Pleroma.Web.ActivityPub.Pipeline alias Pleroma.Web.ActivityPub.Relay alias Pleroma.Web.ActivityPub.Transmogrifier alias Pleroma.Web.ActivityPub.UserView @@ -421,7 +423,10 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do defp handle_user_activity(%User{} = user, %{"type" => "Like"} = params) do with %Object{} = object <- Object.normalize(params["object"]), - {:ok, activity, _object} <- ActivityPub.like(user, object) do + {_, {:ok, like_object, meta}} <- {:build_object, Builder.like(user, object)}, + {_, {:ok, %Activity{} = activity, _meta}} <- + {:common_pipeline, + Pipeline.common_pipeline(like_object, Keyword.put(meta, :local, true))} do {:ok, activity} else _ -> {:error, dgettext("errors", "Can't like object")} diff --git a/lib/pleroma/web/activity_pub/side_effects.ex b/lib/pleroma/web/activity_pub/side_effects.ex index 6a8f1af96..5981e7545 100644 --- a/lib/pleroma/web/activity_pub/side_effects.ex +++ b/lib/pleroma/web/activity_pub/side_effects.ex @@ -15,12 +15,17 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do # - Add like to object # - Set up notification def handle(%{data: %{"type" => "Like"}} = object, meta) do - liked_object = Object.get_by_ap_id(object.data["object"]) - Utils.add_like_to_object(object, liked_object) + {:ok, result} = + Pleroma.Repo.transaction(fn -> + liked_object = Object.get_by_ap_id(object.data["object"]) + Utils.add_like_to_object(object, liked_object) - Notification.create_notifications(object) + Notification.create_notifications(object) - {:ok, object, meta} + {:ok, object, meta} + end) + + result end # Nothing to do diff --git a/lib/pleroma/web/admin_api/admin_api_controller.ex b/lib/pleroma/web/admin_api/admin_api_controller.ex index 9c79310c0..816c11e01 100644 --- a/lib/pleroma/web/admin_api/admin_api_controller.ex +++ b/lib/pleroma/web/admin_api/admin_api_controller.ex @@ -48,6 +48,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do %{scopes: ["write:accounts"], admin: true} when action in [ :get_password_reset, + :force_password_reset, :user_delete, :users_create, :user_toggle_activation, @@ -56,7 +57,9 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do :tag_users, :untag_users, :right_add, + :right_add_multiple, :right_delete, + :right_delete_multiple, :update_user_credentials ] ) @@ -84,13 +87,13 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do plug( OAuthScopesPlug, %{scopes: ["write:reports"], admin: true} - when action in [:reports_update] + when action in [:reports_update, :report_notes_create, :report_notes_delete] ) plug( OAuthScopesPlug, %{scopes: ["read:statuses"], admin: true} - when action == :list_user_statuses + when action in [:list_statuses, :list_user_statuses, :list_instance_statuses] ) plug( @@ -102,13 +105,30 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do plug( OAuthScopesPlug, %{scopes: ["read"], admin: true} - when action in [:config_show, :list_log, :stats] + when action in [ + :config_show, + :list_log, + :stats, + :relay_list, + :config_descriptions, + :need_reboot + ] ) plug( OAuthScopesPlug, %{scopes: ["write"], admin: true} - when action == :config_update + when action in [ + :restart, + :config_update, + :resend_confirmation_email, + :confirm_email, + :oauth_app_create, + :oauth_app_list, + :oauth_app_update, + :oauth_app_delete, + :reload_emoji + ] ) action_fallback(:errors) @@ -1103,25 +1123,25 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do |> json(%{"status_visibility" => count}) end - def errors(conn, {:error, :not_found}) do + defp errors(conn, {:error, :not_found}) do conn |> put_status(:not_found) |> json(dgettext("errors", "Not found")) end - def errors(conn, {:error, reason}) do + defp errors(conn, {:error, reason}) do conn |> put_status(:bad_request) |> json(reason) end - def errors(conn, {:param_cast, _}) do + defp errors(conn, {:param_cast, _}) do conn |> put_status(:bad_request) |> json(dgettext("errors", "Invalid parameters")) end - def errors(conn, _) do + defp errors(conn, _) do conn |> put_status(:internal_server_error) |> json(dgettext("errors", "Something went wrong")) diff --git a/lib/pleroma/web/admin_api/views/status_view.ex b/lib/pleroma/web/admin_api/views/status_view.ex index 360ddc22c..3637dee24 100644 --- a/lib/pleroma/web/admin_api/views/status_view.ex +++ b/lib/pleroma/web/admin_api/views/status_view.ex @@ -8,15 +8,16 @@ defmodule Pleroma.Web.AdminAPI.StatusView do require Pleroma.Constants alias Pleroma.User + alias Pleroma.Web.MastodonAPI.StatusView def render("index.json", opts) do safe_render_many(opts.activities, __MODULE__, "show.json", opts) end def render("show.json", %{activity: %{data: %{"object" => _object}} = activity} = opts) do - user = get_user(activity.data["actor"]) + user = StatusView.get_user(activity.data["actor"]) - Pleroma.Web.MastodonAPI.StatusView.render("show.json", opts) + StatusView.render("show.json", opts) |> Map.merge(%{account: merge_account_views(user)}) end @@ -26,17 +27,4 @@ defmodule Pleroma.Web.AdminAPI.StatusView do end defp merge_account_views(_), do: %{} - - defp get_user(ap_id) do - cond do - user = User.get_cached_by_ap_id(ap_id) -> - user - - user = User.get_by_guessed_nickname(ap_id) -> - user - - true -> - User.error_user(ap_id) - end - end end diff --git a/lib/pleroma/web/api_spec.ex b/lib/pleroma/web/api_spec.ex index 3890489e3..b3c1e3ea2 100644 --- a/lib/pleroma/web/api_spec.ex +++ b/lib/pleroma/web/api_spec.ex @@ -4,6 +4,7 @@ defmodule Pleroma.Web.ApiSpec do alias OpenApiSpex.OpenApi + alias OpenApiSpex.Operation alias Pleroma.Web.Endpoint alias Pleroma.Web.Router @@ -24,6 +25,13 @@ defmodule Pleroma.Web.ApiSpec do # populate the paths from a phoenix router paths: OpenApiSpex.Paths.from_router(Router), components: %OpenApiSpex.Components{ + parameters: %{ + "accountIdOrNickname" => + Operation.parameter(:id, :path, :string, "Account ID or nickname", + example: "123", + required: true + ) + }, securitySchemes: %{ "oAuth" => %OpenApiSpex.SecurityScheme{ type: "oauth2", diff --git a/lib/pleroma/web/api_spec/helpers.ex b/lib/pleroma/web/api_spec/helpers.ex index 7348dcbee..ce40fb9e8 100644 --- a/lib/pleroma/web/api_spec/helpers.ex +++ b/lib/pleroma/web/api_spec/helpers.ex @@ -3,6 +3,9 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.ApiSpec.Helpers do + alias OpenApiSpex.Operation + alias OpenApiSpex.Schema + def request_body(description, schema_ref, opts \\ []) do media_types = ["application/json", "multipart/form-data", "application/x-www-form-urlencoded"] @@ -24,4 +27,23 @@ defmodule Pleroma.Web.ApiSpec.Helpers do required: opts[:required] || false } end + + def pagination_params do + [ + Operation.parameter(:max_id, :query, :string, "Return items older than this ID"), + Operation.parameter(:min_id, :query, :string, "Return the oldest items newer than this ID"), + Operation.parameter( + :since_id, + :query, + :string, + "Return the newest items newer than this ID" + ), + Operation.parameter( + :limit, + :query, + %Schema{type: :integer, default: 20, maximum: 40}, + "Limit" + ) + ] + end end diff --git a/lib/pleroma/web/api_spec/operations/account_operation.ex b/lib/pleroma/web/api_spec/operations/account_operation.ex new file mode 100644 index 000000000..d3e8bd484 --- /dev/null +++ b/lib/pleroma/web/api_spec/operations/account_operation.ex @@ -0,0 +1,702 @@ +# 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.AccountOperation do + alias OpenApiSpex.Operation + alias OpenApiSpex.Reference + alias OpenApiSpex.Schema + alias Pleroma.Web.ApiSpec.Schemas.Account + alias Pleroma.Web.ApiSpec.Schemas.AccountRelationship + alias Pleroma.Web.ApiSpec.Schemas.ActorType + alias Pleroma.Web.ApiSpec.Schemas.ApiError + alias Pleroma.Web.ApiSpec.Schemas.BooleanLike + alias Pleroma.Web.ApiSpec.Schemas.Status + alias Pleroma.Web.ApiSpec.Schemas.VisibilityScope + + import Pleroma.Web.ApiSpec.Helpers + + @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: ["accounts"], + summary: "Register an account", + description: + "Creates a user and account records. Returns an account access token for the app that initiated the request. The app should save this token for later, and should wait for the user to confirm their account by clicking a link in their email inbox.", + operationId: "AccountController.create", + requestBody: request_body("Parameters", create_request(), required: true), + responses: %{ + 200 => Operation.response("Account", "application/json", create_response()), + 400 => Operation.response("Error", "application/json", ApiError), + 403 => Operation.response("Error", "application/json", ApiError), + 429 => Operation.response("Error", "application/json", ApiError) + } + } + end + + def verify_credentials_operation do + %Operation{ + tags: ["accounts"], + description: "Test to make sure that the user token works.", + summary: "Verify account credentials", + operationId: "AccountController.verify_credentials", + security: [%{"oAuth" => ["read:accounts"]}], + responses: %{ + 200 => Operation.response("Account", "application/json", Account) + } + } + end + + def update_credentials_operation do + %Operation{ + tags: ["accounts"], + summary: "Update account credentials", + description: "Update the user's display and preferences.", + operationId: "AccountController.update_credentials", + security: [%{"oAuth" => ["write:accounts"]}], + requestBody: request_body("Parameters", update_creadentials_request(), required: true), + responses: %{ + 200 => Operation.response("Account", "application/json", Account), + 403 => Operation.response("Error", "application/json", ApiError) + } + } + end + + def relationships_operation do + %Operation{ + tags: ["accounts"], + summary: "Check relationships to other accounts", + operationId: "AccountController.relationships", + description: "Find out whether a given account is followed, blocked, muted, etc.", + security: [%{"oAuth" => ["read:follows"]}], + parameters: [ + Operation.parameter( + :id, + :query, + %Schema{ + oneOf: [%Schema{type: :array, items: %Schema{type: :string}}, %Schema{type: :string}] + }, + "Account IDs", + example: "123" + ) + ], + responses: %{ + 200 => Operation.response("Account", "application/json", array_of_relationships()) + } + } + end + + def show_operation do + %Operation{ + tags: ["accounts"], + summary: "Account", + operationId: "AccountController.show", + description: "View information about a profile.", + parameters: [%Reference{"$ref": "#/components/parameters/accountIdOrNickname"}], + responses: %{ + 200 => Operation.response("Account", "application/json", Account), + 404 => Operation.response("Error", "application/json", ApiError) + } + } + end + + def statuses_operation do + %Operation{ + tags: ["accounts"], + summary: "Statuses", + operationId: "AccountController.statuses", + description: + "Statuses posted to the given account. Public (for public statuses only), or user token + `read:statuses` (for private statuses the user is authorized to see)", + parameters: + [ + %Reference{"$ref": "#/components/parameters/accountIdOrNickname"}, + Operation.parameter(:pinned, :query, BooleanLike, "Include only pinned statuses"), + Operation.parameter(:tagged, :query, :string, "With tag"), + Operation.parameter( + :only_media, + :query, + BooleanLike, + "Include only statuses with media attached" + ), + Operation.parameter( + :with_muted, + :query, + BooleanLike, + "Include statuses from muted acccounts." + ), + Operation.parameter(:exclude_reblogs, :query, BooleanLike, "Exclude reblogs"), + Operation.parameter(:exclude_replies, :query, BooleanLike, "Exclude replies"), + Operation.parameter( + :exclude_visibilities, + :query, + %Schema{type: :array, items: VisibilityScope}, + "Exclude visibilities" + ) + ] ++ pagination_params(), + responses: %{ + 200 => Operation.response("Statuses", "application/json", array_of_statuses()), + 404 => Operation.response("Error", "application/json", ApiError) + } + } + end + + def followers_operation do + %Operation{ + tags: ["accounts"], + summary: "Followers", + operationId: "AccountController.followers", + security: [%{"oAuth" => ["read:accounts"]}], + description: + "Accounts which follow the given account, if network is not hidden by the account owner.", + parameters: + [%Reference{"$ref": "#/components/parameters/accountIdOrNickname"}] ++ pagination_params(), + responses: %{ + 200 => Operation.response("Accounts", "application/json", array_of_accounts()) + } + } + end + + def following_operation do + %Operation{ + tags: ["accounts"], + summary: "Following", + operationId: "AccountController.following", + security: [%{"oAuth" => ["read:accounts"]}], + description: + "Accounts which the given account is following, if network is not hidden by the account owner.", + parameters: + [%Reference{"$ref": "#/components/parameters/accountIdOrNickname"}] ++ pagination_params(), + responses: %{200 => Operation.response("Accounts", "application/json", array_of_accounts())} + } + end + + def lists_operation do + %Operation{ + tags: ["accounts"], + summary: "Lists containing this account", + operationId: "AccountController.lists", + security: [%{"oAuth" => ["read:lists"]}], + description: "User lists that you have added this account to.", + parameters: [%Reference{"$ref": "#/components/parameters/accountIdOrNickname"}], + responses: %{200 => Operation.response("Lists", "application/json", array_of_lists())} + } + end + + def follow_operation do + %Operation{ + tags: ["accounts"], + summary: "Follow", + operationId: "AccountController.follow", + security: [%{"oAuth" => ["follow", "write:follows"]}], + description: "Follow the given account", + parameters: [ + %Reference{"$ref": "#/components/parameters/accountIdOrNickname"}, + Operation.parameter( + :reblogs, + :query, + BooleanLike, + "Receive this account's reblogs in home timeline? Defaults to true." + ) + ], + responses: %{ + 200 => Operation.response("Relationship", "application/json", AccountRelationship), + 400 => Operation.response("Error", "application/json", ApiError), + 404 => Operation.response("Error", "application/json", ApiError) + } + } + end + + def unfollow_operation do + %Operation{ + tags: ["accounts"], + summary: "Unfollow", + operationId: "AccountController.unfollow", + security: [%{"oAuth" => ["follow", "write:follows"]}], + description: "Unfollow the given account", + parameters: [%Reference{"$ref": "#/components/parameters/accountIdOrNickname"}], + responses: %{ + 200 => Operation.response("Relationship", "application/json", AccountRelationship), + 400 => Operation.response("Error", "application/json", ApiError), + 404 => Operation.response("Error", "application/json", ApiError) + } + } + end + + def mute_operation do + %Operation{ + tags: ["accounts"], + summary: "Mute", + operationId: "AccountController.mute", + security: [%{"oAuth" => ["follow", "write:mutes"]}], + requestBody: request_body("Parameters", mute_request()), + description: + "Mute the given account. Clients should filter statuses and notifications from this account, if received (e.g. due to a boost in the Home timeline).", + parameters: [ + %Reference{"$ref": "#/components/parameters/accountIdOrNickname"}, + Operation.parameter( + :notifications, + :query, + %Schema{allOf: [BooleanLike], default: true}, + "Mute notifications in addition to statuses? Defaults to `true`." + ) + ], + responses: %{ + 200 => Operation.response("Relationship", "application/json", AccountRelationship) + } + } + end + + def unmute_operation do + %Operation{ + tags: ["accounts"], + summary: "Unmute", + operationId: "AccountController.unmute", + security: [%{"oAuth" => ["follow", "write:mutes"]}], + description: "Unmute the given account.", + parameters: [%Reference{"$ref": "#/components/parameters/accountIdOrNickname"}], + responses: %{ + 200 => Operation.response("Relationship", "application/json", AccountRelationship) + } + } + end + + def block_operation do + %Operation{ + tags: ["accounts"], + summary: "Block", + operationId: "AccountController.block", + security: [%{"oAuth" => ["follow", "write:blocks"]}], + description: + "Block the given account. Clients should filter statuses from this account if received (e.g. due to a boost in the Home timeline)", + parameters: [%Reference{"$ref": "#/components/parameters/accountIdOrNickname"}], + responses: %{ + 200 => Operation.response("Relationship", "application/json", AccountRelationship) + } + } + end + + def unblock_operation do + %Operation{ + tags: ["accounts"], + summary: "Unblock", + operationId: "AccountController.unblock", + security: [%{"oAuth" => ["follow", "write:blocks"]}], + description: "Unblock the given account.", + parameters: [%Reference{"$ref": "#/components/parameters/accountIdOrNickname"}], + responses: %{ + 200 => Operation.response("Relationship", "application/json", AccountRelationship) + } + } + end + + def follow_by_uri_operation do + %Operation{ + tags: ["accounts"], + summary: "Follow by URI", + operationId: "AccountController.follows", + security: [%{"oAuth" => ["follow", "write:follows"]}], + requestBody: request_body("Parameters", follow_by_uri_request(), required: true), + responses: %{ + 200 => Operation.response("Account", "application/json", AccountRelationship), + 400 => Operation.response("Error", "application/json", ApiError), + 404 => Operation.response("Error", "application/json", ApiError) + } + } + end + + def mutes_operation do + %Operation{ + tags: ["accounts"], + summary: "Muted accounts", + operationId: "AccountController.mutes", + description: "Accounts the user has muted.", + security: [%{"oAuth" => ["follow", "read:mutes"]}], + responses: %{ + 200 => Operation.response("Accounts", "application/json", array_of_accounts()) + } + } + end + + def blocks_operation do + %Operation{ + tags: ["accounts"], + summary: "Blocked users", + operationId: "AccountController.blocks", + description: "View your blocks. See also accounts/:id/{block,unblock}", + security: [%{"oAuth" => ["read:blocks"]}], + responses: %{ + 200 => Operation.response("Accounts", "application/json", array_of_accounts()) + } + } + end + + def endorsements_operation do + %Operation{ + tags: ["accounts"], + summary: "Endorsements", + operationId: "AccountController.endorsements", + description: "Not implemented", + security: [%{"oAuth" => ["read:accounts"]}], + responses: %{ + 200 => Operation.response("Empry array", "application/json", %Schema{type: :array}) + } + } + end + + def identity_proofs_operation do + %Operation{ + tags: ["accounts"], + summary: "Identity proofs", + operationId: "AccountController.identity_proofs", + description: "Not implemented", + responses: %{ + 200 => Operation.response("Empry array", "application/json", %Schema{type: :array}) + } + } + end + + defp create_request do + %Schema{ + title: "AccountCreateRequest", + description: "POST body for creating an account", + type: :object, + properties: %{ + reason: %Schema{ + type: :string, + description: + "Text that will be reviewed by moderators if registrations require manual approval" + }, + username: %Schema{type: :string, description: "The desired username for the account"}, + email: %Schema{ + type: :string, + description: + "The email address to be used for login. Required when `account_activation_required` is enabled.", + format: :email + }, + password: %Schema{ + type: :string, + description: "The password to be used for login", + format: :password + }, + agreement: %Schema{ + type: :boolean, + description: + "Whether the user agrees to the local rules, terms, and policies. These should be presented to the user in order to allow them to consent before setting this parameter to TRUE." + }, + locale: %Schema{ + type: :string, + description: "The language of the confirmation email that will be sent" + }, + # Pleroma-specific properties: + fullname: %Schema{type: :string, description: "Full name"}, + bio: %Schema{type: :string, description: "Bio", default: ""}, + captcha_solution: %Schema{ + type: :string, + description: "Provider-specific captcha solution" + }, + captcha_token: %Schema{type: :string, description: "Provider-specific captcha token"}, + captcha_answer_data: %Schema{type: :string, description: "Provider-specific captcha data"}, + token: %Schema{ + type: :string, + description: "Invite token required when the registrations aren't public" + } + }, + required: [:username, :password, :agreement], + example: %{ + "username" => "cofe", + "email" => "cofe@example.com", + "password" => "secret", + "agreement" => "true", + "bio" => "☕️" + } + } + end + + defp create_response do + %Schema{ + title: "AccountCreateResponse", + description: "Response schema for an account", + type: :object, + properties: %{ + token_type: %Schema{type: :string}, + access_token: %Schema{type: :string}, + scope: %Schema{type: :array, items: %Schema{type: :string}}, + created_at: %Schema{type: :integer, format: :"date-time"} + }, + example: %{ + "access_token" => "i9hAVVzGld86Pl5JtLtizKoXVvtTlSCJvwaugCxvZzk", + "created_at" => 1_585_918_714, + "scope" => ["read", "write", "follow", "push"], + "token_type" => "Bearer" + } + } + end + + defp update_creadentials_request do + %Schema{ + title: "AccountUpdateCredentialsRequest", + description: "POST body for creating an account", + type: :object, + properties: %{ + bot: %Schema{ + type: :boolean, + description: "Whether the account has a bot flag." + }, + display_name: %Schema{ + type: :string, + description: "The display name to use for the profile." + }, + note: %Schema{type: :string, description: "The account bio."}, + avatar: %Schema{ + type: :string, + description: "Avatar image encoded using multipart/form-data", + format: :binary + }, + header: %Schema{ + type: :string, + description: "Header image encoded using multipart/form-data", + format: :binary + }, + locked: %Schema{ + type: :boolean, + description: "Whether manual approval of follow requests is required." + }, + fields_attributes: %Schema{ + oneOf: [ + %Schema{type: :array, items: attribute_field()}, + %Schema{type: :object, additionalProperties: %Schema{type: attribute_field()}} + ] + }, + # NOTE: `source` field is not supported + # + # source: %Schema{ + # type: :object, + # properties: %{ + # privacy: %Schema{type: :string}, + # sensitive: %Schema{type: :boolean}, + # language: %Schema{type: :string} + # } + # }, + + # Pleroma-specific fields + no_rich_text: %Schema{ + type: :boolean, + description: "html tags are stripped from all statuses requested from the API" + }, + hide_followers: %Schema{type: :boolean, description: "user's followers will be hidden"}, + hide_follows: %Schema{type: :boolean, description: "user's follows will be hidden"}, + hide_followers_count: %Schema{ + type: :boolean, + description: "user's follower count will be hidden" + }, + hide_follows_count: %Schema{ + type: :boolean, + description: "user's follow count will be hidden" + }, + hide_favorites: %Schema{ + type: :boolean, + description: "user's favorites timeline will be hidden" + }, + show_role: %Schema{ + type: :boolean, + description: "user's role (e.g admin, moderator) will be exposed to anyone in the + API" + }, + default_scope: VisibilityScope, + pleroma_settings_store: %Schema{ + type: :object, + description: "Opaque user settings to be saved on the backend." + }, + skip_thread_containment: %Schema{ + type: :boolean, + description: "Skip filtering out broken threads" + }, + allow_following_move: %Schema{ + type: :boolean, + description: "Allows automatically follow moved following accounts" + }, + pleroma_background_image: %Schema{ + type: :string, + description: "Sets the background image of the user.", + format: :binary + }, + discoverable: %Schema{ + type: :boolean, + description: + "Discovery of this account in search results and other services is allowed." + }, + actor_type: ActorType + }, + example: %{ + bot: false, + display_name: "cofe", + note: "foobar", + fields_attributes: [%{name: "foo", value: "bar"}], + no_rich_text: false, + hide_followers: true, + hide_follows: false, + hide_followers_count: false, + hide_follows_count: false, + hide_favorites: false, + show_role: false, + default_scope: "private", + pleroma_settings_store: %{"pleroma-fe" => %{"key" => "val"}}, + skip_thread_containment: false, + allow_following_move: false, + discoverable: false, + actor_type: "Person" + } + } + end + + defp array_of_accounts do + %Schema{ + title: "ArrayOfAccounts", + type: :array, + items: Account + } + end + + defp array_of_relationships do + %Schema{ + title: "ArrayOfRelationships", + description: "Response schema for account relationships", + type: :array, + items: AccountRelationship, + example: [ + %{ + "id" => "1", + "following" => true, + "showing_reblogs" => true, + "followed_by" => true, + "blocking" => false, + "blocked_by" => true, + "muting" => false, + "muting_notifications" => false, + "requested" => false, + "domain_blocking" => false, + "subscribing" => false, + "endorsed" => true + }, + %{ + "id" => "2", + "following" => true, + "showing_reblogs" => true, + "followed_by" => true, + "blocking" => false, + "blocked_by" => true, + "muting" => true, + "muting_notifications" => false, + "requested" => true, + "domain_blocking" => false, + "subscribing" => false, + "endorsed" => false + }, + %{ + "id" => "3", + "following" => true, + "showing_reblogs" => true, + "followed_by" => true, + "blocking" => true, + "blocked_by" => false, + "muting" => true, + "muting_notifications" => false, + "requested" => false, + "domain_blocking" => true, + "subscribing" => true, + "endorsed" => false + } + ] + } + end + + defp follow_by_uri_request do + %Schema{ + title: "AccountFollowsRequest", + description: "POST body for muting an account", + type: :object, + properties: %{ + uri: %Schema{type: :string, format: :uri} + }, + required: [:uri] + } + end + + defp mute_request do + %Schema{ + title: "AccountMuteRequest", + description: "POST body for muting an account", + type: :object, + properties: %{ + notifications: %Schema{ + type: :boolean, + description: "Mute notifications in addition to statuses? Defaults to true.", + default: true + } + }, + example: %{ + "notifications" => true + } + } + end + + defp list do + %Schema{ + title: "List", + description: "Response schema for a list", + type: :object, + properties: %{ + id: %Schema{type: :string}, + title: %Schema{type: :string} + }, + example: %{ + "id" => "123", + "title" => "my list" + } + } + end + + defp array_of_lists do + %Schema{ + title: "ArrayOfLists", + description: "Response schema for lists", + type: :array, + items: list(), + example: [ + %{"id" => "123", "title" => "my list"}, + %{"id" => "1337", "title" => "anotehr list"} + ] + } + end + + defp array_of_statuses do + %Schema{ + title: "ArrayOfStatuses", + type: :array, + items: Status + } + end + + defp attribute_field do + %Schema{ + title: "AccountAttributeField", + description: "Request schema for account custom fields", + type: :object, + properties: %{ + name: %Schema{type: :string}, + value: %Schema{type: :string} + }, + required: [:name, :value], + example: %{ + "name" => "Website", + "value" => "https://pleroma.com" + } + } + end +end diff --git a/lib/pleroma/web/api_spec/operations/app_operation.ex b/lib/pleroma/web/api_spec/operations/app_operation.ex index 26d8dbd42..f6ccd073f 100644 --- a/lib/pleroma/web/api_spec/operations/app_operation.ex +++ b/lib/pleroma/web/api_spec/operations/app_operation.ex @@ -6,8 +6,6 @@ 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 @@ -22,9 +20,9 @@ defmodule Pleroma.Web.ApiSpec.AppOperation do summary: "Create an application", description: "Create a new application to obtain OAuth2 credentials", operationId: "AppController.create", - requestBody: Helpers.request_body("Parameters", AppCreateRequest, required: true), + requestBody: Helpers.request_body("Parameters", create_request(), required: true), responses: %{ - 200 => Operation.response("App", "application/json", AppCreateResponse), + 200 => Operation.response("App", "application/json", create_response()), 422 => Operation.response( "Unprocessable Entity", @@ -51,11 +49,7 @@ defmodule Pleroma.Web.ApiSpec.AppOperation do summary: "Verify your app works", description: "Confirm that the app's OAuth2 credentials work.", operationId: "AppController.verify_credentials", - security: [ - %{ - "oAuth" => ["read"] - } - ], + security: [%{"oAuth" => ["read"]}], responses: %{ 200 => Operation.response("App", "application/json", %Schema{ @@ -93,4 +87,58 @@ defmodule Pleroma.Web.ApiSpec.AppOperation do } } end + + defp create_request do + %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", + default: "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 + + defp create_response do + %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 end diff --git a/lib/pleroma/web/api_spec/operations/custom_emoji_operation.ex b/lib/pleroma/web/api_spec/operations/custom_emoji_operation.ex index cf2215823..2f812ac77 100644 --- a/lib/pleroma/web/api_spec/operations/custom_emoji_operation.ex +++ b/lib/pleroma/web/api_spec/operations/custom_emoji_operation.ex @@ -4,7 +4,8 @@ defmodule Pleroma.Web.ApiSpec.CustomEmojiOperation do alias OpenApiSpex.Operation - alias Pleroma.Web.ApiSpec.Schemas.CustomEmojisResponse + alias OpenApiSpex.Schema + alias Pleroma.Web.ApiSpec.Schemas.Emoji def open_api_operation(action) do operation = String.to_existing_atom("#{action}_operation") @@ -18,7 +19,69 @@ defmodule Pleroma.Web.ApiSpec.CustomEmojiOperation do description: "Returns custom emojis that are available on the server.", operationId: "CustomEmojiController.index", responses: %{ - 200 => Operation.response("Custom Emojis", "application/json", CustomEmojisResponse) + 200 => Operation.response("Custom Emojis", "application/json", resposnse()) + } + } + end + + defp resposnse do + %Schema{ + title: "CustomEmojisResponse", + description: "Response schema for custom emojis", + type: :array, + items: custom_emoji(), + example: [ + %{ + "category" => "Fun", + "shortcode" => "blank", + "static_url" => "https://lain.com/emoji/blank.png", + "tags" => ["Fun"], + "url" => "https://lain.com/emoji/blank.png", + "visible_in_picker" => false + }, + %{ + "category" => "Gif,Fun", + "shortcode" => "firefox", + "static_url" => "https://lain.com/emoji/Firefox.gif", + "tags" => ["Gif", "Fun"], + "url" => "https://lain.com/emoji/Firefox.gif", + "visible_in_picker" => true + }, + %{ + "category" => "pack:mixed", + "shortcode" => "sadcat", + "static_url" => "https://lain.com/emoji/mixed/sadcat.png", + "tags" => ["pack:mixed"], + "url" => "https://lain.com/emoji/mixed/sadcat.png", + "visible_in_picker" => true + } + ] + } + end + + defp custom_emoji do + %Schema{ + title: "CustomEmoji", + description: "Schema for a CustomEmoji", + allOf: [ + Emoji, + %Schema{ + type: :object, + properties: %{ + category: %Schema{type: :string}, + tags: %Schema{type: :array} + } + } + ], + example: %{ + "category" => "Fun", + "shortcode" => "aaaa", + "url" => + "https://files.mastodon.social/custom_emojis/images/000/007/118/original/aaaa.png", + "static_url" => + "https://files.mastodon.social/custom_emojis/images/000/007/118/static/aaaa.png", + "visible_in_picker" => true, + "tags" => ["Gif", "Fun"] } } 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 index dd14837c3..3b7f51ceb 100644 --- a/lib/pleroma/web/api_spec/operations/domain_block_operation.ex +++ b/lib/pleroma/web/api_spec/operations/domain_block_operation.ex @@ -6,8 +6,6 @@ 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") @@ -22,7 +20,13 @@ defmodule Pleroma.Web.ApiSpec.DomainBlockOperation do security: [%{"oAuth" => ["follow", "read:blocks"]}], operationId: "DomainBlockController.index", responses: %{ - 200 => Operation.response("Domain blocks", "application/json", DomainBlocksResponse) + 200 => + Operation.response("Domain blocks", "application/json", %Schema{ + description: "Response schema for domain blocks", + type: :array, + items: %Schema{type: :string}, + example: ["google.com", "facebook.com"] + }) } } end @@ -40,7 +44,7 @@ defmodule Pleroma.Web.ApiSpec.DomainBlockOperation do - prevent following new users from it (but does not remove existing follows) """, operationId: "DomainBlockController.create", - requestBody: Helpers.request_body("Parameters", DomainBlockRequest, required: true), + requestBody: domain_block_request(), security: [%{"oAuth" => ["follow", "write:blocks"]}], responses: %{ 200 => Operation.response("Empty object", "application/json", %Schema{type: :object}) @@ -54,11 +58,28 @@ defmodule Pleroma.Web.ApiSpec.DomainBlockOperation do 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), + requestBody: domain_block_request(), security: [%{"oAuth" => ["follow", "write:blocks"]}], responses: %{ 200 => Operation.response("Empty object", "application/json", %Schema{type: :object}) } } end + + defp domain_block_request do + Helpers.request_body( + "Parameters", + %Schema{ + type: :object, + properties: %{ + domain: %Schema{type: :string} + }, + required: [:domain] + }, + required: true, + example: %{ + "domain" => "facebook.com" + } + ) + end end diff --git a/lib/pleroma/web/api_spec/render_error.ex b/lib/pleroma/web/api_spec/render_error.ex new file mode 100644 index 000000000..b5877ca9c --- /dev/null +++ b/lib/pleroma/web/api_spec/render_error.ex @@ -0,0 +1,231 @@ +# 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.RenderError do + @behaviour Plug + + import Plug.Conn, only: [put_status: 2] + import Phoenix.Controller, only: [json: 2] + import Pleroma.Web.Gettext + + @impl Plug + def init(opts), do: opts + + @impl Plug + + def call(conn, errors) do + errors = + Enum.map(errors, fn + %{name: nil} = err -> + %OpenApiSpex.Cast.Error{err | name: List.last(err.path)} + + err -> + err + end) + + conn + |> put_status(:bad_request) + |> json(%{ + error: errors |> Enum.map(&message/1) |> Enum.join(" "), + errors: errors |> Enum.map(&render_error/1) + }) + end + + defp render_error(error) do + pointer = OpenApiSpex.path_to_string(error) + + %{ + title: "Invalid value", + source: %{ + pointer: pointer + }, + message: OpenApiSpex.Cast.Error.message(error) + } + end + + defp message(%{reason: :invalid_schema_type, type: type, name: name}) do + gettext("%{name} - Invalid schema.type. Got: %{type}.", + name: name, + type: inspect(type) + ) + end + + defp message(%{reason: :null_value, name: name} = error) do + case error.type do + nil -> + gettext("%{name} - null value.", name: name) + + type -> + gettext("%{name} - null value where %{type} expected.", + name: name, + type: type + ) + end + end + + defp message(%{reason: :all_of, meta: %{invalid_schema: invalid_schema}}) do + gettext( + "Failed to cast value as %{invalid_schema}. Value must be castable using `allOf` schemas listed.", + invalid_schema: invalid_schema + ) + end + + defp message(%{reason: :any_of, meta: %{failed_schemas: failed_schemas}}) do + gettext("Failed to cast value using any of: %{failed_schemas}.", + failed_schemas: failed_schemas + ) + end + + defp message(%{reason: :one_of, meta: %{failed_schemas: failed_schemas}}) do + gettext("Failed to cast value to one of: %{failed_schemas}.", failed_schemas: failed_schemas) + end + + defp message(%{reason: :min_length, length: length, name: name}) do + gettext("%{name} - String length is smaller than minLength: %{length}.", + name: name, + length: length + ) + end + + defp message(%{reason: :max_length, length: length, name: name}) do + gettext("%{name} - String length is larger than maxLength: %{length}.", + name: name, + length: length + ) + end + + defp message(%{reason: :unique_items, name: name}) do + gettext("%{name} - Array items must be unique.", name: name) + end + + defp message(%{reason: :min_items, length: min, value: array, name: name}) do + gettext("%{name} - Array length %{length} is smaller than minItems: %{min}.", + name: name, + length: length(array), + min: min + ) + end + + defp message(%{reason: :max_items, length: max, value: array, name: name}) do + gettext("%{name} - Array length %{length} is larger than maxItems: %{}.", + name: name, + length: length(array), + max: max + ) + end + + defp message(%{reason: :multiple_of, length: multiple, value: count, name: name}) do + gettext("%{name} - %{count} is not a multiple of %{multiple}.", + name: name, + count: count, + multiple: multiple + ) + end + + defp message(%{reason: :exclusive_max, length: max, value: value, name: name}) + when value >= max do + gettext("%{name} - %{value} is larger than exclusive maximum %{max}.", + name: name, + value: value, + max: max + ) + end + + defp message(%{reason: :maximum, length: max, value: value, name: name}) + when value > max do + gettext("%{name} - %{value} is larger than inclusive maximum %{max}.", + name: name, + value: value, + max: max + ) + end + + defp message(%{reason: :exclusive_multiple, length: min, value: value, name: name}) + when value <= min do + gettext("%{name} - %{value} is smaller than exclusive minimum %{min}.", + name: name, + value: value, + min: min + ) + end + + defp message(%{reason: :minimum, length: min, value: value, name: name}) + when value < min do + gettext("%{name} - %{value} is smaller than inclusive minimum %{min}.", + name: name, + value: value, + min: min + ) + end + + defp message(%{reason: :invalid_type, type: type, value: value, name: name}) do + gettext("%{name} - Invalid %{type}. Got: %{value}.", + name: name, + value: OpenApiSpex.TermType.type(value), + type: type + ) + end + + defp message(%{reason: :invalid_format, format: format, name: name}) do + gettext("%{name} - Invalid format. Expected %{format}.", name: name, format: inspect(format)) + end + + defp message(%{reason: :invalid_enum, name: name}) do + gettext("%{name} - Invalid value for enum.", name: name) + end + + defp message(%{reason: :polymorphic_failed, type: polymorphic_type}) do + gettext("Failed to cast to any schema in %{polymorphic_type}", + polymorphic_type: polymorphic_type + ) + end + + defp message(%{reason: :unexpected_field, name: name}) do + gettext("Unexpected field: %{name}.", name: safe_string(name)) + end + + defp message(%{reason: :no_value_for_discriminator, name: field}) do + gettext("Value used as discriminator for `%{field}` matches no schemas.", name: field) + end + + defp message(%{reason: :invalid_discriminator_value, name: field}) do + gettext("No value provided for required discriminator `%{field}`.", name: field) + end + + defp message(%{reason: :unknown_schema, name: name}) do + gettext("Unknown schema: %{name}.", name: name) + end + + defp message(%{reason: :missing_field, name: name}) do + gettext("Missing field: %{name}.", name: name) + end + + defp message(%{reason: :missing_header, name: name}) do + gettext("Missing header: %{name}.", name: name) + end + + defp message(%{reason: :invalid_header, name: name}) do + gettext("Invalid value for header: %{name}.", name: name) + end + + defp message(%{reason: :max_properties, meta: meta}) do + gettext( + "Object property count %{property_count} is greater than maxProperties: %{max_properties}.", + property_count: meta.property_count, + max_properties: meta.max_properties + ) + end + + defp message(%{reason: :min_properties, meta: meta}) do + gettext( + "Object property count %{property_count} is less than minProperties: %{min_properties}", + property_count: meta.property_count, + min_properties: meta.min_properties + ) + end + + defp safe_string(string) do + to_string(string) |> String.slice(0..39) + end +end diff --git a/lib/pleroma/web/api_spec/schemas/account.ex b/lib/pleroma/web/api_spec/schemas/account.ex new file mode 100644 index 000000000..d54e2158d --- /dev/null +++ b/lib/pleroma/web/api_spec/schemas/account.ex @@ -0,0 +1,167 @@ +# 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.Account do + alias OpenApiSpex.Schema + alias Pleroma.Web.ApiSpec.Schemas.AccountField + alias Pleroma.Web.ApiSpec.Schemas.AccountRelationship + alias Pleroma.Web.ApiSpec.Schemas.ActorType + alias Pleroma.Web.ApiSpec.Schemas.Emoji + alias Pleroma.Web.ApiSpec.Schemas.FlakeID + alias Pleroma.Web.ApiSpec.Schemas.VisibilityScope + + require OpenApiSpex + + OpenApiSpex.schema(%{ + title: "Account", + description: "Response schema for an account", + type: :object, + properties: %{ + acct: %Schema{type: :string}, + avatar_static: %Schema{type: :string, format: :uri}, + avatar: %Schema{type: :string, format: :uri}, + bot: %Schema{type: :boolean}, + created_at: %Schema{type: :string, format: "date-time"}, + display_name: %Schema{type: :string}, + emojis: %Schema{type: :array, items: Emoji}, + fields: %Schema{type: :array, items: AccountField}, + follow_requests_count: %Schema{type: :integer}, + followers_count: %Schema{type: :integer}, + following_count: %Schema{type: :integer}, + header_static: %Schema{type: :string, format: :uri}, + header: %Schema{type: :string, format: :uri}, + id: FlakeID, + locked: %Schema{type: :boolean}, + note: %Schema{type: :string, format: :html}, + statuses_count: %Schema{type: :integer}, + url: %Schema{type: :string, format: :uri}, + username: %Schema{type: :string}, + pleroma: %Schema{ + type: :object, + properties: %{ + allow_following_move: %Schema{type: :boolean}, + background_image: %Schema{type: :string, nullable: true}, + chat_token: %Schema{type: :string}, + confirmation_pending: %Schema{type: :boolean}, + hide_favorites: %Schema{type: :boolean}, + hide_followers_count: %Schema{type: :boolean}, + hide_followers: %Schema{type: :boolean}, + hide_follows_count: %Schema{type: :boolean}, + hide_follows: %Schema{type: :boolean}, + is_admin: %Schema{type: :boolean}, + is_moderator: %Schema{type: :boolean}, + skip_thread_containment: %Schema{type: :boolean}, + tags: %Schema{type: :array, items: %Schema{type: :string}}, + unread_conversation_count: %Schema{type: :integer}, + notification_settings: %Schema{ + type: :object, + properties: %{ + followers: %Schema{type: :boolean}, + follows: %Schema{type: :boolean}, + non_followers: %Schema{type: :boolean}, + non_follows: %Schema{type: :boolean}, + privacy_option: %Schema{type: :boolean} + } + }, + relationship: AccountRelationship, + settings_store: %Schema{ + type: :object + } + } + }, + source: %Schema{ + type: :object, + properties: %{ + fields: %Schema{type: :array, items: AccountField}, + note: %Schema{type: :string}, + privacy: VisibilityScope, + sensitive: %Schema{type: :boolean}, + pleroma: %Schema{ + type: :object, + properties: %{ + actor_type: ActorType, + discoverable: %Schema{type: :boolean}, + no_rich_text: %Schema{type: :boolean}, + show_role: %Schema{type: :boolean} + } + } + } + } + }, + example: %{ + "acct" => "foobar", + "avatar" => "https://mypleroma.com/images/avi.png", + "avatar_static" => "https://mypleroma.com/images/avi.png", + "bot" => false, + "created_at" => "2020-03-24T13:05:58.000Z", + "display_name" => "foobar", + "emojis" => [], + "fields" => [], + "follow_requests_count" => 0, + "followers_count" => 0, + "following_count" => 1, + "header" => "https://mypleroma.com/images/banner.png", + "header_static" => "https://mypleroma.com/images/banner.png", + "id" => "9tKi3esbG7OQgZ2920", + "locked" => false, + "note" => "cofe", + "pleroma" => %{ + "allow_following_move" => true, + "background_image" => nil, + "confirmation_pending" => true, + "hide_favorites" => true, + "hide_followers" => false, + "hide_followers_count" => false, + "hide_follows" => false, + "hide_follows_count" => false, + "is_admin" => false, + "is_moderator" => false, + "skip_thread_containment" => false, + "chat_token" => + "SFMyNTY.g3QAAAACZAAEZGF0YW0AAAASOXRLaTNlc2JHN09RZ1oyOTIwZAAGc2lnbmVkbgYARNplS3EB.Mb_Iaqew2bN1I1o79B_iP7encmVCpTKC4OtHZRxdjKc", + "unread_conversation_count" => 0, + "tags" => [], + "notification_settings" => %{ + "followers" => true, + "follows" => true, + "non_followers" => true, + "non_follows" => true, + "privacy_option" => false + }, + "relationship" => %{ + "blocked_by" => false, + "blocking" => false, + "domain_blocking" => false, + "endorsed" => false, + "followed_by" => false, + "following" => false, + "id" => "9tKi3esbG7OQgZ2920", + "muting" => false, + "muting_notifications" => false, + "requested" => false, + "showing_reblogs" => true, + "subscribing" => false + }, + "settings_store" => %{ + "pleroma-fe" => %{} + } + }, + "source" => %{ + "fields" => [], + "note" => "foobar", + "pleroma" => %{ + "actor_type" => "Person", + "discoverable" => false, + "no_rich_text" => false, + "show_role" => true + }, + "privacy" => "public", + "sensitive" => false + }, + "statuses_count" => 0, + "url" => "https://mypleroma.com/users/foobar", + "username" => "foobar" + } + }) +end diff --git a/lib/pleroma/web/api_spec/schemas/account_field.ex b/lib/pleroma/web/api_spec/schemas/account_field.ex new file mode 100644 index 000000000..fa97073a0 --- /dev/null +++ b/lib/pleroma/web/api_spec/schemas/account_field.ex @@ -0,0 +1,26 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ApiSpec.Schemas.AccountField do + alias OpenApiSpex.Schema + + require OpenApiSpex + + OpenApiSpex.schema(%{ + title: "AccountField", + description: "Response schema for account custom fields", + type: :object, + properties: %{ + name: %Schema{type: :string}, + value: %Schema{type: :string, format: :html}, + verified_at: %Schema{type: :string, format: :"date-time", nullable: true} + }, + example: %{ + "name" => "Website", + "value" => + "<a href=\"https://pleroma.com\" rel=\"me nofollow noopener noreferrer\" target=\"_blank\"><span class=\"invisible\">https://</span><span class=\"\">pleroma.com</span><span class=\"invisible\"></span></a>", + "verified_at" => "2019-08-29T04:14:55.571+00:00" + } + }) +end diff --git a/lib/pleroma/web/api_spec/schemas/account_relationship.ex b/lib/pleroma/web/api_spec/schemas/account_relationship.ex new file mode 100644 index 000000000..8b982669e --- /dev/null +++ b/lib/pleroma/web/api_spec/schemas/account_relationship.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.Schemas.AccountRelationship do + alias OpenApiSpex.Schema + alias Pleroma.Web.ApiSpec.Schemas.FlakeID + + require OpenApiSpex + + OpenApiSpex.schema(%{ + title: "AccountRelationship", + description: "Response schema for relationship", + type: :object, + properties: %{ + blocked_by: %Schema{type: :boolean}, + blocking: %Schema{type: :boolean}, + domain_blocking: %Schema{type: :boolean}, + endorsed: %Schema{type: :boolean}, + followed_by: %Schema{type: :boolean}, + following: %Schema{type: :boolean}, + id: FlakeID, + muting: %Schema{type: :boolean}, + muting_notifications: %Schema{type: :boolean}, + requested: %Schema{type: :boolean}, + showing_reblogs: %Schema{type: :boolean}, + subscribing: %Schema{type: :boolean} + }, + example: %{ + "blocked_by" => false, + "blocking" => false, + "domain_blocking" => false, + "endorsed" => false, + "followed_by" => false, + "following" => false, + "id" => "9tKi3esbG7OQgZ2920", + "muting" => false, + "muting_notifications" => false, + "requested" => false, + "showing_reblogs" => true, + "subscribing" => false + } + }) +end diff --git a/lib/pleroma/web/api_spec/schemas/actor_type.ex b/lib/pleroma/web/api_spec/schemas/actor_type.ex new file mode 100644 index 000000000..ac9b46678 --- /dev/null +++ b/lib/pleroma/web/api_spec/schemas/actor_type.ex @@ -0,0 +1,13 @@ +# 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.ActorType do + require OpenApiSpex + + OpenApiSpex.schema(%{ + title: "ActorType", + type: :string, + enum: ["Application", "Group", "Organization", "Person", "Service"] + }) +end diff --git a/lib/pleroma/web/api_spec/schemas/domain_block_request.ex b/lib/pleroma/web/api_spec/schemas/api_error.ex index ee9238361..5815df94c 100644 --- a/lib/pleroma/web/api_spec/schemas/domain_block_request.ex +++ b/lib/pleroma/web/api_spec/schemas/api_error.ex @@ -2,19 +2,18 @@ # Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> # SPDX-License-Identifier: AGPL-3.0-only -defmodule Pleroma.Web.ApiSpec.Schemas.DomainBlockRequest do +defmodule Pleroma.Web.ApiSpec.Schemas.ApiError do alias OpenApiSpex.Schema + require OpenApiSpex OpenApiSpex.schema(%{ - title: "DomainBlockRequest", + title: "ApiError", + description: "Response schema for API error", type: :object, - properties: %{ - domain: %Schema{type: :string} - }, - required: [:domain], + properties: %{error: %Schema{type: :string}}, example: %{ - "domain" => "facebook.com" + "error" => "Something went wrong" } }) 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 deleted file mode 100644 index 8a83abef3..000000000 --- a/lib/pleroma/web/api_spec/schemas/app_create_request.ex +++ /dev/null @@ -1,33 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.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 deleted file mode 100644 index f290fb031..000000000 --- a/lib/pleroma/web/api_spec/schemas/app_create_response.ex +++ /dev/null @@ -1,33 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.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/boolean_like.ex b/lib/pleroma/web/api_spec/schemas/boolean_like.ex new file mode 100644 index 000000000..f3bfb74da --- /dev/null +++ b/lib/pleroma/web/api_spec/schemas/boolean_like.ex @@ -0,0 +1,36 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ApiSpec.Schemas.BooleanLike do + alias OpenApiSpex.Schema + + require OpenApiSpex + + OpenApiSpex.schema(%{ + title: "BooleanLike", + description: """ + The following values will be treated as `false`: + - false + - 0 + - "0", + - "f", + - "F", + - "false", + - "FALSE", + - "off", + - "OFF" + + All other non-null values will be treated as `true` + """, + anyOf: [ + %Schema{type: :boolean}, + %Schema{type: :string}, + %Schema{type: :integer} + ] + }) + + def after_cast(value, _schmea) do + {:ok, Pleroma.Web.ControllerHelper.truthy_param?(value)} + end +end diff --git a/lib/pleroma/web/api_spec/schemas/custom_emoji.ex b/lib/pleroma/web/api_spec/schemas/custom_emoji.ex deleted file mode 100644 index 5531b2081..000000000 --- a/lib/pleroma/web/api_spec/schemas/custom_emoji.ex +++ /dev/null @@ -1,30 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ApiSpec.Schemas.CustomEmoji do - alias OpenApiSpex.Schema - - require OpenApiSpex - - OpenApiSpex.schema(%{ - title: "CustomEmoji", - description: "Response schema for an CustomEmoji", - type: :object, - properties: %{ - shortcode: %Schema{type: :string}, - url: %Schema{type: :string}, - static_url: %Schema{type: :string}, - visible_in_picker: %Schema{type: :boolean}, - category: %Schema{type: :string}, - tags: %Schema{type: :array} - }, - example: %{ - "shortcode" => "aaaa", - "url" => "https://files.mastodon.social/custom_emojis/images/000/007/118/original/aaaa.png", - "static_url" => - "https://files.mastodon.social/custom_emojis/images/000/007/118/static/aaaa.png", - "visible_in_picker" => true - } - }) -end diff --git a/lib/pleroma/web/api_spec/schemas/custom_emojis_response.ex b/lib/pleroma/web/api_spec/schemas/custom_emojis_response.ex deleted file mode 100644 index 01582a63d..000000000 --- a/lib/pleroma/web/api_spec/schemas/custom_emojis_response.ex +++ /dev/null @@ -1,42 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ApiSpec.Schemas.CustomEmojisResponse do - alias Pleroma.Web.ApiSpec.Schemas.CustomEmoji - - require OpenApiSpex - - OpenApiSpex.schema(%{ - title: "CustomEmojisResponse", - description: "Response schema for custom emojis", - type: :array, - items: CustomEmoji, - example: [ - %{ - "category" => "Fun", - "shortcode" => "blank", - "static_url" => "https://lain.com/emoji/blank.png", - "tags" => ["Fun"], - "url" => "https://lain.com/emoji/blank.png", - "visible_in_picker" => true - }, - %{ - "category" => "Gif,Fun", - "shortcode" => "firefox", - "static_url" => "https://lain.com/emoji/Firefox.gif", - "tags" => ["Gif", "Fun"], - "url" => "https://lain.com/emoji/Firefox.gif", - "visible_in_picker" => true - }, - %{ - "category" => "pack:mixed", - "shortcode" => "sadcat", - "static_url" => "https://lain.com/emoji/mixed/sadcat.png", - "tags" => ["pack:mixed"], - "url" => "https://lain.com/emoji/mixed/sadcat.png", - "visible_in_picker" => true - } - ] - }) -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 deleted file mode 100644 index d895aca4e..000000000 --- a/lib/pleroma/web/api_spec/schemas/domain_blocks_response.ex +++ /dev/null @@ -1,16 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.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/api_spec/schemas/emoji.ex b/lib/pleroma/web/api_spec/schemas/emoji.ex new file mode 100644 index 000000000..26f35e648 --- /dev/null +++ b/lib/pleroma/web/api_spec/schemas/emoji.ex @@ -0,0 +1,29 @@ +# 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.Emoji do + alias OpenApiSpex.Schema + + require OpenApiSpex + + OpenApiSpex.schema(%{ + title: "Emoji", + description: "Response schema for an emoji", + type: :object, + properties: %{ + shortcode: %Schema{type: :string}, + url: %Schema{type: :string, format: :uri}, + static_url: %Schema{type: :string, format: :uri}, + visible_in_picker: %Schema{type: :boolean} + }, + example: %{ + "shortcode" => "fatyoshi", + "url" => + "https://files.mastodon.social/custom_emojis/images/000/023/920/original/e57ecb623faa0dc9.png", + "static_url" => + "https://files.mastodon.social/custom_emojis/images/000/023/920/static/e57ecb623faa0dc9.png", + "visible_in_picker" => true + } + }) +end diff --git a/lib/pleroma/web/api_spec/schemas/flake_id.ex b/lib/pleroma/web/api_spec/schemas/flake_id.ex new file mode 100644 index 000000000..3b5f6477a --- /dev/null +++ b/lib/pleroma/web/api_spec/schemas/flake_id.ex @@ -0,0 +1,14 @@ +# 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.FlakeID do + require OpenApiSpex + + OpenApiSpex.schema(%{ + title: "FlakeID", + description: + "Pleroma uses 128-bit ids as opposed to Mastodon's 64 bits. However just like Mastodon's ids they are lexically sortable strings", + type: :string + }) +end diff --git a/lib/pleroma/web/api_spec/schemas/poll.ex b/lib/pleroma/web/api_spec/schemas/poll.ex new file mode 100644 index 000000000..0474b550b --- /dev/null +++ b/lib/pleroma/web/api_spec/schemas/poll.ex @@ -0,0 +1,36 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ApiSpec.Schemas.Poll do + alias OpenApiSpex.Schema + alias Pleroma.Web.ApiSpec.Schemas.Emoji + alias Pleroma.Web.ApiSpec.Schemas.FlakeID + + require OpenApiSpex + + OpenApiSpex.schema(%{ + title: "Poll", + description: "Response schema for account custom fields", + type: :object, + properties: %{ + id: FlakeID, + expires_at: %Schema{type: :string, format: "date-time"}, + expired: %Schema{type: :boolean}, + multiple: %Schema{type: :boolean}, + votes_count: %Schema{type: :integer}, + voted: %Schema{type: :boolean}, + emojis: %Schema{type: :array, items: Emoji}, + options: %Schema{ + type: :array, + items: %Schema{ + type: :object, + properties: %{ + title: %Schema{type: :string}, + votes_count: %Schema{type: :integer} + } + } + } + } + }) +end diff --git a/lib/pleroma/web/api_spec/schemas/status.ex b/lib/pleroma/web/api_spec/schemas/status.ex new file mode 100644 index 000000000..aef0588d4 --- /dev/null +++ b/lib/pleroma/web/api_spec/schemas/status.ex @@ -0,0 +1,226 @@ +# 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.Status do + alias OpenApiSpex.Schema + alias Pleroma.Web.ApiSpec.Schemas.Account + alias Pleroma.Web.ApiSpec.Schemas.Emoji + alias Pleroma.Web.ApiSpec.Schemas.FlakeID + alias Pleroma.Web.ApiSpec.Schemas.Poll + alias Pleroma.Web.ApiSpec.Schemas.VisibilityScope + + require OpenApiSpex + + OpenApiSpex.schema(%{ + title: "Status", + description: "Response schema for a status", + type: :object, + properties: %{ + account: Account, + application: %Schema{ + type: :object, + properties: %{ + name: %Schema{type: :string}, + website: %Schema{type: :string, nullable: true, format: :uri} + } + }, + bookmarked: %Schema{type: :boolean}, + card: %Schema{ + type: :object, + nullable: true, + properties: %{ + type: %Schema{type: :string, enum: ["link", "photo", "video", "rich"]}, + provider_name: %Schema{type: :string, nullable: true}, + provider_url: %Schema{type: :string, format: :uri}, + url: %Schema{type: :string, format: :uri}, + image: %Schema{type: :string, nullable: true, format: :uri}, + title: %Schema{type: :string}, + description: %Schema{type: :string} + } + }, + content: %Schema{type: :string, format: :html}, + created_at: %Schema{type: :string, format: "date-time"}, + emojis: %Schema{type: :array, items: Emoji}, + favourited: %Schema{type: :boolean}, + favourites_count: %Schema{type: :integer}, + id: FlakeID, + in_reply_to_account_id: %Schema{type: :string, nullable: true}, + in_reply_to_id: %Schema{type: :string, nullable: true}, + language: %Schema{type: :string, nullable: true}, + media_attachments: %Schema{ + type: :array, + items: %Schema{ + type: :object, + properties: %{ + id: %Schema{type: :string}, + url: %Schema{type: :string, format: :uri}, + remote_url: %Schema{type: :string, format: :uri}, + preview_url: %Schema{type: :string, format: :uri}, + text_url: %Schema{type: :string, format: :uri}, + description: %Schema{type: :string}, + type: %Schema{type: :string, enum: ["image", "video", "audio", "unknown"]}, + pleroma: %Schema{ + type: :object, + properties: %{mime_type: %Schema{type: :string}} + } + } + } + }, + mentions: %Schema{ + type: :array, + items: %Schema{ + type: :object, + properties: %{ + id: %Schema{type: :string}, + acct: %Schema{type: :string}, + username: %Schema{type: :string}, + url: %Schema{type: :string, format: :uri} + } + } + }, + muted: %Schema{type: :boolean}, + pinned: %Schema{type: :boolean}, + pleroma: %Schema{ + type: :object, + properties: %{ + content: %Schema{type: :object, additionalProperties: %Schema{type: :string}}, + conversation_id: %Schema{type: :integer}, + direct_conversation_id: %Schema{type: :string, nullable: true}, + emoji_reactions: %Schema{ + type: :array, + items: %Schema{ + type: :object, + properties: %{ + name: %Schema{type: :string}, + count: %Schema{type: :integer}, + me: %Schema{type: :boolean} + } + } + }, + expires_at: %Schema{type: :string, format: "date-time", nullable: true}, + in_reply_to_account_acct: %Schema{type: :string, nullable: true}, + local: %Schema{type: :boolean}, + spoiler_text: %Schema{type: :object, additionalProperties: %Schema{type: :string}}, + thread_muted: %Schema{type: :boolean} + } + }, + poll: %Schema{type: Poll, nullable: true}, + reblog: %Schema{ + allOf: [%OpenApiSpex.Reference{"$ref": "#/components/schemas/Status"}], + nullable: true + }, + reblogged: %Schema{type: :boolean}, + reblogs_count: %Schema{type: :integer}, + replies_count: %Schema{type: :integer}, + sensitive: %Schema{type: :boolean}, + spoiler_text: %Schema{type: :string}, + tags: %Schema{ + type: :array, + items: %Schema{ + type: :object, + properties: %{ + name: %Schema{type: :string}, + url: %Schema{type: :string, format: :uri} + } + } + }, + uri: %Schema{type: :string, format: :uri}, + url: %Schema{type: :string, nullable: true, format: :uri}, + visibility: VisibilityScope + }, + example: %{ + "account" => %{ + "acct" => "nick6", + "avatar" => "http://localhost:4001/images/avi.png", + "avatar_static" => "http://localhost:4001/images/avi.png", + "bot" => false, + "created_at" => "2020-04-07T19:48:51.000Z", + "display_name" => "Test テスト User 6", + "emojis" => [], + "fields" => [], + "followers_count" => 1, + "following_count" => 0, + "header" => "http://localhost:4001/images/banner.png", + "header_static" => "http://localhost:4001/images/banner.png", + "id" => "9toJCsKN7SmSf3aj5c", + "locked" => false, + "note" => "Tester Number 6", + "pleroma" => %{ + "background_image" => nil, + "confirmation_pending" => false, + "hide_favorites" => true, + "hide_followers" => false, + "hide_followers_count" => false, + "hide_follows" => false, + "hide_follows_count" => false, + "is_admin" => false, + "is_moderator" => false, + "relationship" => %{ + "blocked_by" => false, + "blocking" => false, + "domain_blocking" => false, + "endorsed" => false, + "followed_by" => false, + "following" => true, + "id" => "9toJCsKN7SmSf3aj5c", + "muting" => false, + "muting_notifications" => false, + "requested" => false, + "showing_reblogs" => true, + "subscribing" => false + }, + "skip_thread_containment" => false, + "tags" => [] + }, + "source" => %{ + "fields" => [], + "note" => "Tester Number 6", + "pleroma" => %{"actor_type" => "Person", "discoverable" => false}, + "sensitive" => false + }, + "statuses_count" => 1, + "url" => "http://localhost:4001/users/nick6", + "username" => "nick6" + }, + "application" => %{"name" => "Web", "website" => nil}, + "bookmarked" => false, + "card" => nil, + "content" => "foobar", + "created_at" => "2020-04-07T19:48:51.000Z", + "emojis" => [], + "favourited" => false, + "favourites_count" => 0, + "id" => "9toJCu5YZW7O7gfvH6", + "in_reply_to_account_id" => nil, + "in_reply_to_id" => nil, + "language" => nil, + "media_attachments" => [], + "mentions" => [], + "muted" => false, + "pinned" => false, + "pleroma" => %{ + "content" => %{"text/plain" => "foobar"}, + "conversation_id" => 345_972, + "direct_conversation_id" => nil, + "emoji_reactions" => [], + "expires_at" => nil, + "in_reply_to_account_acct" => nil, + "local" => true, + "spoiler_text" => %{"text/plain" => ""}, + "thread_muted" => false + }, + "poll" => nil, + "reblog" => nil, + "reblogged" => false, + "reblogs_count" => 0, + "replies_count" => 0, + "sensitive" => false, + "spoiler_text" => "", + "tags" => [], + "uri" => "http://localhost:4001/objects/0f5dad44-0e9e-4610-b377-a2631e499190", + "url" => "http://localhost:4001/notice/9toJCu5YZW7O7gfvH6", + "visibility" => "private" + } + }) +end diff --git a/lib/pleroma/web/api_spec/schemas/visibility_scope.ex b/lib/pleroma/web/api_spec/schemas/visibility_scope.ex new file mode 100644 index 000000000..8c81a4d73 --- /dev/null +++ b/lib/pleroma/web/api_spec/schemas/visibility_scope.ex @@ -0,0 +1,14 @@ +# 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.VisibilityScope do + require OpenApiSpex + + OpenApiSpex.schema(%{ + title: "VisibilityScope", + description: "Status visibility", + type: :string, + enum: ["public", "unlisted", "private", "direct"] + }) +end diff --git a/lib/pleroma/web/common_api/activity_draft.ex b/lib/pleroma/web/common_api/activity_draft.ex index c1cd15bb2..244cf2be5 100644 --- a/lib/pleroma/web/common_api/activity_draft.ex +++ b/lib/pleroma/web/common_api/activity_draft.ex @@ -84,14 +84,18 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do %__MODULE__{draft | attachments: attachments} end - defp in_reply_to(draft) do - case Map.get(draft.params, "in_reply_to_status_id") do - "" -> draft - nil -> draft - id -> %__MODULE__{draft | in_reply_to: Activity.get_by_id(id)} - end + defp in_reply_to(%{params: %{"in_reply_to_status_id" => ""}} = draft), do: draft + + defp in_reply_to(%{params: %{"in_reply_to_status_id" => id}} = draft) when is_binary(id) do + %__MODULE__{draft | in_reply_to: Activity.get_by_id(id)} end + defp in_reply_to(%{params: %{"in_reply_to_status_id" => %Activity{} = in_reply_to}} = draft) do + %__MODULE__{draft | in_reply_to: in_reply_to} + end + + defp in_reply_to(draft), do: draft + defp in_reply_to_conversation(draft) do in_reply_to_conversation = Participation.get(draft.params["in_reply_to_conversation_id"]) %__MODULE__{draft | in_reply_to_conversation: in_reply_to_conversation} diff --git a/lib/pleroma/web/common_api/common_api.ex b/lib/pleroma/web/common_api/common_api.ex index f50a909aa..d1efe0c36 100644 --- a/lib/pleroma/web/common_api/common_api.ex +++ b/lib/pleroma/web/common_api/common_api.ex @@ -86,8 +86,9 @@ defmodule Pleroma.Web.CommonAPI do end end - def repeat(id_or_ap_id, user, params \\ %{}) do - with {_, %Activity{} = activity} <- {:find_activity, get_by_id_or_ap_id(id_or_ap_id)}, + def repeat(id, user, params \\ %{}) do + with {_, %Activity{data: %{"type" => "Create"}} = activity} <- + {:find_activity, Activity.get_by_id(id)}, object <- Object.normalize(activity), announce_activity <- Utils.get_existing_announce(user.ap_id, object), public <- public_announce?(object, params) do @@ -102,8 +103,9 @@ defmodule Pleroma.Web.CommonAPI do end end - def unrepeat(id_or_ap_id, user) do - with {_, %Activity{} = activity} <- {:find_activity, get_by_id_or_ap_id(id_or_ap_id)} do + def unrepeat(id, user) do + with {_, %Activity{data: %{"type" => "Create"}} = activity} <- + {:find_activity, Activity.get_by_id(id)} do object = Object.normalize(activity) ActivityPub.unannounce(user, object) else @@ -160,8 +162,9 @@ defmodule Pleroma.Web.CommonAPI do end end - def unfavorite(id_or_ap_id, user) do - with {_, %Activity{} = activity} <- {:find_activity, get_by_id_or_ap_id(id_or_ap_id)} do + def unfavorite(id, user) do + with {_, %Activity{data: %{"type" => "Create"}} = activity} <- + {:find_activity, Activity.get_by_id(id)} do object = Object.normalize(activity) ActivityPub.unlike(user, object) else @@ -332,12 +335,12 @@ defmodule Pleroma.Web.CommonAPI do defp maybe_create_activity_expiration(result, _), do: result - def pin(id_or_ap_id, %{ap_id: user_ap_id} = user) do + def pin(id, %{ap_id: user_ap_id} = user) do with %Activity{ actor: ^user_ap_id, data: %{"type" => "Create"}, object: %Object{data: %{"type" => object_type}} - } = activity <- get_by_id_or_ap_id(id_or_ap_id), + } = activity <- Activity.get_by_id_with_object(id), true <- object_type in ["Note", "Article", "Question"], true <- Visibility.is_public?(activity), {:ok, _user} <- User.add_pinnned_activity(user, activity) do @@ -348,8 +351,8 @@ defmodule Pleroma.Web.CommonAPI do end end - def unpin(id_or_ap_id, user) do - with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id), + def unpin(id, user) do + with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id(id), {:ok, _user} <- User.remove_pinnned_activity(user, activity) do {:ok, activity} else diff --git a/lib/pleroma/web/common_api/utils.ex b/lib/pleroma/web/common_api/utils.ex index 7eec5aa09..945e63e22 100644 --- a/lib/pleroma/web/common_api/utils.ex +++ b/lib/pleroma/web/common_api/utils.ex @@ -22,24 +22,6 @@ defmodule Pleroma.Web.CommonAPI.Utils do require Logger require Pleroma.Constants - # This is a hack for twidere. - def get_by_id_or_ap_id(id) do - activity = - with true <- FlakeId.flake_id?(id), - %Activity{} = activity <- Activity.get_by_id_with_object(id) do - activity - else - _ -> Activity.get_create_by_object_ap_id_with_object(id) - end - - activity && - if activity.data["type"] == "Create" do - activity - else - Activity.get_create_by_object_ap_id_with_object(activity.data["object"]) - end - end - def attachments_from_ids(%{"media_ids" => ids, "descriptions" => desc} = _) do attachments_from_ids_descs(ids, desc) end diff --git a/lib/pleroma/web/controller_helper.ex b/lib/pleroma/web/controller_helper.ex index 4780081b2..eb97ae975 100644 --- a/lib/pleroma/web/controller_helper.ex +++ b/lib/pleroma/web/controller_helper.ex @@ -82,8 +82,9 @@ defmodule Pleroma.Web.ControllerHelper do end end - def assign_account_by_id(%{params: %{"id" => id}} = conn, _) do - case Pleroma.User.get_cached_by_id(id) do + def assign_account_by_id(conn, _) do + # TODO: use `conn.params[:id]` only after moving to OpenAPI + case Pleroma.User.get_cached_by_id(conn.params[:id] || conn.params["id"]) do %Pleroma.User{} = account -> assign(conn, :account, account) nil -> Pleroma.Web.MastodonAPI.FallbackController.call(conn, {:error, :not_found}) |> halt() end diff --git a/lib/pleroma/web/fallback_redirect_controller.ex b/lib/pleroma/web/fallback_redirect_controller.ex index c13518030..0d9d578fc 100644 --- a/lib/pleroma/web/fallback_redirect_controller.ex +++ b/lib/pleroma/web/fallback_redirect_controller.ex @@ -4,7 +4,9 @@ defmodule Fallback.RedirectController do use Pleroma.Web, :controller + require Logger + alias Pleroma.User alias Pleroma.Web.Metadata diff --git a/lib/pleroma/web/masto_fe_controller.ex b/lib/pleroma/web/masto_fe_controller.ex index 557cde328..d0d8bc8eb 100644 --- a/lib/pleroma/web/masto_fe_controller.ex +++ b/lib/pleroma/web/masto_fe_controller.ex @@ -5,19 +5,25 @@ defmodule Pleroma.Web.MastoFEController do use Pleroma.Web, :controller + alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug alias Pleroma.Plugs.OAuthScopesPlug alias Pleroma.User plug(OAuthScopesPlug, %{scopes: ["write:accounts"]} when action == :put_settings) # Note: :index action handles attempt of unauthenticated access to private instance with redirect + plug(:skip_plug, EnsurePublicOrAuthenticatedPlug when action == :index) + plug( OAuthScopesPlug, - %{scopes: ["read"], fallback: :proceed_unauthenticated, skip_instance_privacy_check: true} + %{scopes: ["read"], fallback: :proceed_unauthenticated} when action == :index ) - plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug when action not in [:index, :manifest]) + plug( + :skip_plug, + [OAuthScopesPlug, EnsurePublicOrAuthenticatedPlug] when action == :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 5a92cebd8..1eedf02d6 100644 --- a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex @@ -14,6 +14,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do skip_relationships?: 1 ] + alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug alias Pleroma.Plugs.OAuthScopesPlug alias Pleroma.Plugs.RateLimiter alias Pleroma.User @@ -26,18 +27,28 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do alias Pleroma.Web.OAuth.Token alias Pleroma.Web.TwitterAPI.TwitterAPI - plug(:skip_plug, OAuthScopesPlug when action == :identity_proofs) + plug(OpenApiSpex.Plug.CastAndValidate, render_error: Pleroma.Web.ApiSpec.RenderError) + + plug(:skip_plug, [OAuthScopesPlug, EnsurePublicOrAuthenticatedPlug] when action == :create) + + plug(:skip_plug, EnsurePublicOrAuthenticatedPlug when action in [:show, :statuses]) plug( OAuthScopesPlug, %{fallback: :proceed_unauthenticated, scopes: ["read:accounts"]} - when action == :show + when action in [:show, :followers, :following] + ) + + plug( + OAuthScopesPlug, + %{fallback: :proceed_unauthenticated, scopes: ["read:statuses"]} + when action == :statuses ) plug( OAuthScopesPlug, %{scopes: ["read:accounts"]} - when action in [:endorsements, :verify_credentials, :followers, :following] + when action in [:verify_credentials, :endorsements, :identity_proofs] ) plug(OAuthScopesPlug, %{scopes: ["write:accounts"]} when action == :update_credentials) @@ -56,21 +67,15 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do plug(OAuthScopesPlug, %{scopes: ["read:follows"]} when action == :relationships) - # Note: :follows (POST /api/v1/follows) is the same as :follow, consider removing :follows plug( OAuthScopesPlug, - %{scopes: ["follow", "write:follows"]} when action in [:follows, :follow, :unfollow] + %{scopes: ["follow", "write:follows"]} when action in [:follow_by_uri, :follow, :unfollow] ) plug(OAuthScopesPlug, %{scopes: ["follow", "read:mutes"]} when action == :mutes) plug(OAuthScopesPlug, %{scopes: ["follow", "write:mutes"]} when action in [:mute, :unmute]) - plug( - Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug - when action not in [:create, :show, :statuses] - ) - @relationship_actions [:follow, :unfollow] @needs_account ~W(followers following lists follow unfollow mute unmute block unblock)a @@ -85,26 +90,26 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do action_fallback(Pleroma.Web.MastodonAPI.FallbackController) + defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.AccountOperation + @doc "POST /api/v1/accounts" - def create( - %{assigns: %{app: app}} = conn, - %{"username" => nickname, "password" => _, "agreement" => true} = params - ) do + def create(%{assigns: %{app: app}, body_params: params} = conn, _params) do params = params |> Map.take([ - "email", - "captcha_solution", - "captcha_token", - "captcha_answer_data", - "token", - "password" + :email, + :bio, + :captcha_solution, + :captcha_token, + :captcha_answer_data, + :token, + :password, + :fullname ]) - |> Map.put("nickname", nickname) - |> Map.put("fullname", params["fullname"] || nickname) - |> Map.put("bio", params["bio"] || "") - |> Map.put("confirm", params["password"]) - |> Map.put("trusted_app", app.trusted) + |> Map.put(:nickname, params.username) + |> Map.put(:fullname, Map.get(params, :fullname, params.username)) + |> 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), @@ -128,7 +133,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do render_error(conn, :forbidden, "Invalid credentials") end - defp validate_email_param(%{"email" => _}), do: :ok + defp validate_email_param(%{:email => email}) when not is_nil(email), do: :ok defp validate_email_param(_) do case Pleroma.Config.get([:instance, :account_activation_required]) do @@ -150,7 +155,14 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do end @doc "PATCH /api/v1/accounts/update_credentials" - def update_credentials(%{assigns: %{user: user}} = conn, params) do + def update_credentials(%{assigns: %{user: original_user}, body_params: params} = conn, _params) do + user = original_user + + params = + params + |> Enum.filter(fn {_, value} -> not is_nil(value) end) + |> Enum.into(%{}) + user_params = [ :no_rich_text, @@ -166,22 +178,22 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do :discoverable ] |> Enum.reduce(%{}, fn key, acc -> - add_if_present(acc, params, to_string(key), key, &{:ok, truthy_param?(&1)}) + add_if_present(acc, params, key, key, &{:ok, truthy_param?(&1)}) end) - |> add_if_present(params, "display_name", :name) - |> add_if_present(params, "note", :bio) - |> add_if_present(params, "avatar", :avatar) - |> add_if_present(params, "header", :banner) - |> add_if_present(params, "pleroma_background_image", :background) + |> add_if_present(params, :display_name, :name) + |> add_if_present(params, :note, :bio) + |> add_if_present(params, :avatar, :avatar) + |> add_if_present(params, :header, :banner) + |> add_if_present(params, :pleroma_background_image, :background) |> add_if_present( params, - "fields_attributes", + :fields_attributes, :raw_fields, &{:ok, normalize_fields_attributes(&1)} ) - |> add_if_present(params, "pleroma_settings_store", :pleroma_settings_store) - |> add_if_present(params, "default_scope", :default_scope) - |> add_if_present(params, "actor_type", :actor_type) + |> add_if_present(params, :pleroma_settings_store, :pleroma_settings_store) + |> add_if_present(params, :default_scope, :default_scope) + |> add_if_present(params, :actor_type, :actor_type) changeset = User.update_changeset(user, user_params) @@ -194,7 +206,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do defp add_if_present(map, params, params_field, map_field, value_function \\ &{:ok, &1}) do with true <- Map.has_key?(params, params_field), - {:ok, new_value} <- value_function.(params[params_field]) do + {:ok, new_value} <- value_function.(Map.get(params, params_field)) do Map.put(map, map_field, new_value) else _ -> map @@ -205,12 +217,15 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do if Enum.all?(fields, &is_tuple/1) do Enum.map(fields, fn {_, v} -> v end) else - fields + Enum.map(fields, fn + %{} = field -> %{"name" => field.name, "value" => field.value} + field -> field + end) end end @doc "GET /api/v1/accounts/relationships" - def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do + def relationships(%{assigns: %{user: user}} = conn, %{id: id}) do targets = User.get_all_by_ids(List.wrap(id)) render(conn, "relationships.json", user: user, targets: targets) @@ -220,7 +235,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, []) @doc "GET /api/v1/accounts/:id" - def show(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do + def show(%{assigns: %{user: for_user}} = conn, %{id: nickname_or_id}) do with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id, for: for_user), true <- User.visible_for?(user, for_user) do render(conn, "show.json", user: user, for: for_user) @@ -231,12 +246,14 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do @doc "GET /api/v1/accounts/:id/statuses" def statuses(%{assigns: %{user: reading_user}} = conn, params) do - with %User{} = user <- User.get_cached_by_nickname_or_id(params["id"], for: reading_user), + with %User{} = user <- User.get_cached_by_nickname_or_id(params.id, for: reading_user), true <- User.visible_for?(user, reading_user) do params = params - |> Map.put("tag", params["tagged"]) - |> Map.delete("godmode") + |> Map.delete(:tagged) + |> Enum.filter(&(not is_nil(&1))) + |> Map.new(fn {key, value} -> {to_string(key), value} end) + |> Map.put("tag", params[:tagged]) activities = ActivityPub.fetch_user_activities(user, reading_user, params) @@ -256,6 +273,11 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do @doc "GET /api/v1/accounts/:id/followers" def followers(%{assigns: %{user: for_user, account: user}} = conn, params) do + params = + params + |> Enum.map(fn {key, value} -> {to_string(key), value} end) + |> Enum.into(%{}) + followers = cond do for_user && user.id == for_user.id -> MastodonAPI.get_followers(user, params) @@ -270,6 +292,11 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do @doc "GET /api/v1/accounts/:id/following" def following(%{assigns: %{user: for_user, account: user}} = conn, params) do + params = + params + |> Enum.map(fn {key, value} -> {to_string(key), value} end) + |> Enum.into(%{}) + followers = cond do for_user && user.id == for_user.id -> MastodonAPI.get_friends(user, params) @@ -296,8 +323,8 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do {:error, "Can not follow yourself"} end - def follow(%{assigns: %{user: follower, account: followed}} = conn, _params) do - with {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do + def follow(%{assigns: %{user: follower, account: followed}} = conn, params) do + with {:ok, follower} <- MastodonAPI.follow(follower, followed, params) do render(conn, "relationship.json", user: follower, target: followed) else {:error, message} -> json_response(conn, :forbidden, %{error: message}) @@ -316,10 +343,8 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do end @doc "POST /api/v1/accounts/:id/mute" - def mute(%{assigns: %{user: muter, account: muted}} = conn, params) do - notifications? = params |> Map.get("notifications", true) |> truthy_param?() - - with {:ok, _user_relationships} <- User.mute(muter, muted, notifications?) do + def mute(%{assigns: %{user: muter, account: muted}, body_params: params} = conn, _params) do + with {:ok, _user_relationships} <- User.mute(muter, muted, params.notifications) do render(conn, "relationship.json", user: muter, target: muted) else {:error, message} -> json_response(conn, :forbidden, %{error: message}) @@ -356,7 +381,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do end @doc "POST /api/v1/follows" - def follows(conn, %{"uri" => uri}) do + def follow_by_uri(%{body_params: %{uri: uri}} = conn, _) do case User.get_cached_by_nickname(uri) do %User{} = user -> conn diff --git a/lib/pleroma/web/mastodon_api/controllers/app_controller.ex b/lib/pleroma/web/mastodon_api/controllers/app_controller.ex index 005c60444..408e11474 100644 --- a/lib/pleroma/web/mastodon_api/controllers/app_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/app_controller.ex @@ -5,6 +5,7 @@ defmodule Pleroma.Web.MastodonAPI.AppController do use Pleroma.Web, :controller + alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug alias Pleroma.Plugs.OAuthScopesPlug alias Pleroma.Repo alias Pleroma.Web.OAuth.App @@ -13,7 +14,14 @@ defmodule Pleroma.Web.MastodonAPI.AppController do action_fallback(Pleroma.Web.MastodonAPI.FallbackController) + plug( + :skip_plug, + [OAuthScopesPlug, EnsurePublicOrAuthenticatedPlug] + when action == :create + ) + plug(OAuthScopesPlug, %{scopes: ["read"]} when action == :verify_credentials) + plug(OpenApiSpex.Plug.CastAndValidate) @local_mastodon_name "Mastodon-Local" diff --git a/lib/pleroma/web/mastodon_api/controllers/auth_controller.ex b/lib/pleroma/web/mastodon_api/controllers/auth_controller.ex index 37b389382..753b3db3e 100644 --- a/lib/pleroma/web/mastodon_api/controllers/auth_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/auth_controller.ex @@ -13,10 +13,10 @@ defmodule Pleroma.Web.MastodonAPI.AuthController do action_fallback(Pleroma.Web.MastodonAPI.FallbackController) - @local_mastodon_name "Mastodon-Local" - plug(Pleroma.Plugs.RateLimiter, [name: :password_reset] when action == :password_reset) + @local_mastodon_name "Mastodon-Local" + @doc "GET /web/login" def login(%{assigns: %{user: %User{}}} = conn, _params) do redirect(conn, to: local_mastodon_root_path(conn)) diff --git a/lib/pleroma/web/mastodon_api/controllers/conversation_controller.ex b/lib/pleroma/web/mastodon_api/controllers/conversation_controller.ex index 7c9b11bf1..c44641526 100644 --- a/lib/pleroma/web/mastodon_api/controllers/conversation_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/conversation_controller.ex @@ -14,9 +14,7 @@ defmodule Pleroma.Web.MastodonAPI.ConversationController do action_fallback(Pleroma.Web.MastodonAPI.FallbackController) plug(OAuthScopesPlug, %{scopes: ["read:statuses"]} when action == :index) - plug(OAuthScopesPlug, %{scopes: ["write:conversations"]} when action == :read) - - plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug) + plug(OAuthScopesPlug, %{scopes: ["write:conversations"]} when action != :index) @doc "GET /api/v1/conversations" def index(%{assigns: %{user: user}} = conn, params) do @@ -28,7 +26,7 @@ defmodule Pleroma.Web.MastodonAPI.ConversationController do end @doc "POST /api/v1/conversations/:id/read" - def read(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do + def mark_as_read(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do with %Participation{} = participation <- Repo.get_by(Participation, id: participation_id, user_id: user.id), {:ok, participation} <- Participation.mark_as_read(participation) do diff --git a/lib/pleroma/web/mastodon_api/controllers/custom_emoji_controller.ex b/lib/pleroma/web/mastodon_api/controllers/custom_emoji_controller.ex index 3bfebef8b..000ad743f 100644 --- a/lib/pleroma/web/mastodon_api/controllers/custom_emoji_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/custom_emoji_controller.ex @@ -7,6 +7,12 @@ defmodule Pleroma.Web.MastodonAPI.CustomEmojiController do plug(OpenApiSpex.Plug.CastAndValidate) + plug( + :skip_plug, + [Pleroma.Plugs.OAuthScopesPlug, Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug] + when action == :index + ) + defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.CustomEmojiOperation def index(conn, _params) do 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 84de79413..c4fa383f2 100644 --- a/lib/pleroma/web/mastodon_api/controllers/domain_block_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/domain_block_controller.ex @@ -21,8 +21,6 @@ defmodule Pleroma.Web.MastodonAPI.DomainBlockController do %{scopes: ["follow", "write:blocks"]} when action != :index ) - plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug) - @doc "GET /api/v1/domain_blocks" def index(%{assigns: %{user: user}} = conn, _) do json(conn, Map.get(user, :domain_blocks, [])) diff --git a/lib/pleroma/web/mastodon_api/controllers/filter_controller.ex b/lib/pleroma/web/mastodon_api/controllers/filter_controller.ex index 7b0b937a2..7fd0562c9 100644 --- a/lib/pleroma/web/mastodon_api/controllers/filter_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/filter_controller.ex @@ -17,8 +17,6 @@ defmodule Pleroma.Web.MastodonAPI.FilterController do %{scopes: ["write:filters"]} when action not in @oauth_read_actions ) - plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug) - @doc "GET /api/v1/filters" def index(%{assigns: %{user: user}} = conn, _) do filters = Filter.get_filters(user) diff --git a/lib/pleroma/web/mastodon_api/controllers/follow_request_controller.ex b/lib/pleroma/web/mastodon_api/controllers/follow_request_controller.ex index 1ca86f63f..25f2269b9 100644 --- a/lib/pleroma/web/mastodon_api/controllers/follow_request_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/follow_request_controller.ex @@ -21,8 +21,6 @@ defmodule Pleroma.Web.MastodonAPI.FollowRequestController do %{scopes: ["follow", "write:follows"]} when action != :index ) - plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug) - @doc "GET /api/v1/follow_requests" def index(%{assigns: %{user: followed}} = conn, _params) do follow_requests = User.get_follow_requests(followed) diff --git a/lib/pleroma/web/mastodon_api/controllers/instance_controller.ex b/lib/pleroma/web/mastodon_api/controllers/instance_controller.ex index 27b5b1a52..237f85677 100644 --- a/lib/pleroma/web/mastodon_api/controllers/instance_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/instance_controller.ex @@ -5,6 +5,12 @@ defmodule Pleroma.Web.MastodonAPI.InstanceController do use Pleroma.Web, :controller + plug( + :skip_plug, + [Pleroma.Plugs.OAuthScopesPlug, Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug] + when action in [:show, :peers] + ) + @doc "GET /api/v1/instance" def show(conn, _params) do render(conn, "show.json") diff --git a/lib/pleroma/web/mastodon_api/controllers/list_controller.ex b/lib/pleroma/web/mastodon_api/controllers/list_controller.ex index dac4daa7b..bfe856025 100644 --- a/lib/pleroma/web/mastodon_api/controllers/list_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/list_controller.ex @@ -11,16 +11,16 @@ defmodule Pleroma.Web.MastodonAPI.ListController do plug(:list_by_id_and_user when action not in [:index, :create]) - plug(OAuthScopesPlug, %{scopes: ["read:lists"]} when action in [:index, :show, :list_accounts]) + @oauth_read_actions [:index, :show, :list_accounts] + + plug(OAuthScopesPlug, %{scopes: ["read:lists"]} when action in @oauth_read_actions) plug( OAuthScopesPlug, %{scopes: ["write:lists"]} - when action in [:create, :update, :delete, :add_to_list, :remove_from_list] + when action not in @oauth_read_actions ) - plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug) - action_fallback(Pleroma.Web.MastodonAPI.FallbackController) # GET /api/v1/lists diff --git a/lib/pleroma/web/mastodon_api/controllers/marker_controller.ex b/lib/pleroma/web/mastodon_api/controllers/marker_controller.ex index 58e8a30c2..9f9d4574e 100644 --- a/lib/pleroma/web/mastodon_api/controllers/marker_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/marker_controller.ex @@ -13,7 +13,7 @@ defmodule Pleroma.Web.MastodonAPI.MarkerController do ) plug(OAuthScopesPlug, %{scopes: ["write:statuses"]} when action == :upsert) - plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug) + action_fallback(Pleroma.Web.MastodonAPI.FallbackController) # GET /api/v1/markers 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 ac8c18f24..e7767de4e 100644 --- a/lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex @@ -15,9 +15,11 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do require Logger - plug(:skip_plug, Pleroma.Plugs.OAuthScopesPlug when action in [:empty_array, :empty_object]) - - plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug) + plug( + :skip_plug, + [Pleroma.Plugs.OAuthScopesPlug, Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug] + when action in [:empty_array, :empty_object] + ) action_fallback(Pleroma.Web.MastodonAPI.FallbackController) diff --git a/lib/pleroma/web/mastodon_api/controllers/media_controller.ex b/lib/pleroma/web/mastodon_api/controllers/media_controller.ex index 2b6f00952..e36751220 100644 --- a/lib/pleroma/web/mastodon_api/controllers/media_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/media_controller.ex @@ -15,8 +15,6 @@ defmodule Pleroma.Web.MastodonAPI.MediaController do plug(OAuthScopesPlug, %{scopes: ["write:media"]}) - plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug) - @doc "POST /api/v1/media" def create(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do with {:ok, object} <- diff --git a/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex b/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex index 7fb536b09..311405277 100644 --- a/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex @@ -20,8 +20,6 @@ defmodule Pleroma.Web.MastodonAPI.NotificationController do plug(OAuthScopesPlug, %{scopes: ["write:notifications"]} when action not in @oauth_read_actions) - plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug) - # GET /api/v1/notifications def index(conn, %{"account_id" => account_id} = params) do case Pleroma.User.get_cached_by_id(account_id) do diff --git a/lib/pleroma/web/mastodon_api/controllers/poll_controller.ex b/lib/pleroma/web/mastodon_api/controllers/poll_controller.ex index d9f894118..af9b66eff 100644 --- a/lib/pleroma/web/mastodon_api/controllers/poll_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/poll_controller.ex @@ -22,8 +22,6 @@ defmodule Pleroma.Web.MastodonAPI.PollController do plug(OAuthScopesPlug, %{scopes: ["write:statuses"]} when action == :vote) - plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug) - @doc "GET /api/v1/polls/:id" def show(%{assigns: %{user: user}} = conn, %{"id" => id}) do with %Object{} = object <- Object.get_by_id_and_maybe_refetch(id, interval: 60), diff --git a/lib/pleroma/web/mastodon_api/controllers/report_controller.ex b/lib/pleroma/web/mastodon_api/controllers/report_controller.ex index f5782be13..9fbaa7bd1 100644 --- a/lib/pleroma/web/mastodon_api/controllers/report_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/report_controller.ex @@ -11,8 +11,6 @@ defmodule Pleroma.Web.MastodonAPI.ReportController do plug(OAuthScopesPlug, %{scopes: ["write:reports"]} when action == :create) - plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug) - @doc "POST /api/v1/reports" def create(%{assigns: %{user: user}} = conn, params) do with {:ok, activity} <- Pleroma.Web.CommonAPI.report(user, params) do diff --git a/lib/pleroma/web/mastodon_api/controllers/scheduled_activity_controller.ex b/lib/pleroma/web/mastodon_api/controllers/scheduled_activity_controller.ex index e1e6bd89b..899b78873 100644 --- a/lib/pleroma/web/mastodon_api/controllers/scheduled_activity_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/scheduled_activity_controller.ex @@ -18,8 +18,6 @@ defmodule Pleroma.Web.MastodonAPI.ScheduledActivityController do plug(OAuthScopesPlug, %{scopes: ["read:statuses"]} when action in @oauth_read_actions) plug(OAuthScopesPlug, %{scopes: ["write:statuses"]} when action not in @oauth_read_actions) - plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug) - action_fallback(Pleroma.Web.MastodonAPI.FallbackController) @doc "GET /api/v1/scheduled_statuses" diff --git a/lib/pleroma/web/mastodon_api/controllers/search_controller.ex b/lib/pleroma/web/mastodon_api/controllers/search_controller.ex index c258742dd..cd49da6ad 100644 --- a/lib/pleroma/web/mastodon_api/controllers/search_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/search_controller.ex @@ -21,7 +21,7 @@ defmodule Pleroma.Web.MastodonAPI.SearchController do # Note: Mastodon doesn't allow unauthenticated access (requires read:accounts / read:search) plug(OAuthScopesPlug, %{scopes: ["read:search"], fallback: :proceed_unauthenticated}) - plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug) + # Note: on private instances auth is required (EnsurePublicOrAuthenticatedPlug is not skipped) plug(RateLimiter, [name: :search] when action in [:search, :search2, :account_search]) diff --git a/lib/pleroma/web/mastodon_api/controllers/status_controller.ex b/lib/pleroma/web/mastodon_api/controllers/status_controller.ex index 397dd10e3..9eea2e9eb 100644 --- a/lib/pleroma/web/mastodon_api/controllers/status_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/status_controller.ex @@ -24,6 +24,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do alias Pleroma.Web.MastodonAPI.AccountView alias Pleroma.Web.MastodonAPI.ScheduledActivityView + plug(:skip_plug, Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug when action in [:index, :show]) + @unauthenticated_access %{fallback: :proceed_unauthenticated, scopes: []} plug( @@ -77,8 +79,6 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do %{scopes: ["write:bookmarks"]} when action in [:bookmark, :unbookmark] ) - plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug when action not in [:index, :show]) - @rate_limited_status_actions ~w(reblog unreblog favourite unfavourite create delete)a plug( @@ -127,7 +127,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do def create( %{assigns: %{user: user}} = conn, %{"status" => _, "scheduled_at" => scheduled_at} = params - ) do + ) + when not is_nil(scheduled_at) do params = Map.put(params, "in_reply_to_status_id", params["in_reply_to_id"]) with {:far_enough, true} <- {:far_enough, ScheduledActivity.far_enough?(scheduled_at)}, @@ -357,7 +358,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do end @doc "GET /api/v1/favourites" - def favourites(%{assigns: %{user: user}} = conn, params) do + def favourites(%{assigns: %{user: %User{} = user}} = conn, params) do activities = ActivityPub.fetch_favourites( user, diff --git a/lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex b/lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex index 4647c1f96..d184ea1d0 100644 --- a/lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex @@ -12,7 +12,7 @@ defmodule Pleroma.Web.MastodonAPI.SubscriptionController do action_fallback(:errors) plug(Pleroma.Plugs.OAuthScopesPlug, %{scopes: ["push"]}) - plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug) + plug(:restrict_push_enabled) # Creates PushSubscription diff --git a/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex b/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex index b3c58005e..2d67e19da 100644 --- a/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex @@ -9,11 +9,14 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do only: [add_link_headers: 2, add_link_headers: 3, truthy_param?: 1, skip_relationships?: 1] alias Pleroma.Pagination + alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug alias Pleroma.Plugs.OAuthScopesPlug alias Pleroma.Plugs.RateLimiter alias Pleroma.User alias Pleroma.Web.ActivityPub.ActivityPub + plug(:skip_plug, EnsurePublicOrAuthenticatedPlug when action in [:public, :hashtag]) + # TODO: Replace with a macro when there is a Phoenix release with the following commit in it: # https://github.com/phoenixframework/phoenix/commit/2e8c63c01fec4dde5467dbbbf9705ff9e780735e @@ -26,7 +29,11 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do plug(OAuthScopesPlug, %{scopes: ["read:statuses"]} when action in [:home, :direct]) plug(OAuthScopesPlug, %{scopes: ["read:lists"]} when action == :list) - plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug when action != :public) + plug( + OAuthScopesPlug, + %{scopes: ["read:statuses"], fallback: :proceed_unauthenticated} + when action in [:public, :hashtag] + ) plug(:put_view, Pleroma.Web.MastodonAPI.StatusView) @@ -37,6 +44,7 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do |> Map.put("type", ["Create", "Announce"]) |> Map.put("blocking_user", user) |> Map.put("muting_user", user) + |> Map.put("reply_filtering_user", user) |> Map.put("user", user) recipients = [user.ap_id | User.following(user)] @@ -93,13 +101,16 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do restrict? = Pleroma.Config.get([:restrict_unauthenticated, :timelines, cfg_key]) - if not (restrict? and is_nil(user)) do + if restrict? and is_nil(user) do + render_error(conn, :unauthorized, "authorization required for timeline view") + else activities = params |> Map.put("type", ["Create", "Announce"]) |> Map.put("local_only", local_only) |> Map.put("blocking_user", user) |> Map.put("muting_user", user) + |> Map.put("reply_filtering_user", user) |> ActivityPub.fetch_public_activities() conn @@ -110,12 +121,10 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do as: :activity, skip_relationships: skip_relationships?(params) ) - else - render_error(conn, :unauthorized, "authorization required for timeline view") end end - def hashtag_fetching(params, user, local_only) do + defp hashtag_fetching(params, user, local_only) do tags = [params["tag"], params["any"]] |> List.flatten() diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex index b5850e1ae..24167f66f 100644 --- a/lib/pleroma/web/mastodon_api/views/status_view.ex +++ b/lib/pleroma/web/mastodon_api/views/status_view.ex @@ -45,7 +45,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do end) end - defp get_user(ap_id) do + def get_user(ap_id, fake_record_fallback \\ true) do cond do user = User.get_cached_by_ap_id(ap_id) -> user @@ -53,8 +53,12 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do user = User.get_by_guessed_nickname(ap_id) -> user - true -> + fake_record_fallback -> + # TODO: refactor (fake records is never a good idea) User.error_user(ap_id) + + true -> + nil end end @@ -97,7 +101,11 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do UserRelationship.view_relationships_option(nil, []) true -> - actors = Enum.map(activities ++ parent_activities, &get_user(&1.data["actor"])) + # Note: unresolved users are filtered out + actors = + (activities ++ parent_activities) + |> Enum.map(&get_user(&1.data["actor"], false)) + |> Enum.filter(& &1) UserRelationship.view_relationships_option(reading_user, actors, source_mutes_only: opts[:skip_relationships] @@ -521,11 +529,9 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do """ @spec build_tags(list(any())) :: list(map()) def build_tags(object_tags) when is_list(object_tags) do - object_tags = for tag when is_binary(tag) <- object_tags, do: tag - - Enum.reduce(object_tags, [], fn tag, tags -> - tags ++ [%{name: tag, url: "/tag/#{URI.encode(tag)}"}] - end) + object_tags + |> Enum.filter(&is_binary/1) + |> Enum.map(&%{name: &1, url: "/tag/#{URI.encode(&1)}"}) end def build_tags(_), do: [] diff --git a/lib/pleroma/web/media_proxy/media_proxy_controller.ex b/lib/pleroma/web/media_proxy/media_proxy_controller.ex index 1a09ac62a..4657a4383 100644 --- a/lib/pleroma/web/media_proxy/media_proxy_controller.ex +++ b/lib/pleroma/web/media_proxy/media_proxy_controller.ex @@ -4,6 +4,7 @@ defmodule Pleroma.Web.MediaProxy.MediaProxyController do use Pleroma.Web, :controller + alias Pleroma.ReverseProxy alias Pleroma.Web.MediaProxy diff --git a/lib/pleroma/web/mongooseim/mongoose_im_controller.ex b/lib/pleroma/web/mongooseim/mongoose_im_controller.ex index 04d823b36..1ed6ee521 100644 --- a/lib/pleroma/web/mongooseim/mongoose_im_controller.ex +++ b/lib/pleroma/web/mongooseim/mongoose_im_controller.ex @@ -14,7 +14,7 @@ defmodule Pleroma.Web.MongooseIM.MongooseIMController do plug(RateLimiter, [name: :authentication, params: ["user"]] when action == :check_password) def user_exists(conn, %{"user" => username}) do - with %User{} <- Repo.get_by(User, nickname: username, local: true) do + with %User{} <- Repo.get_by(User, nickname: username, local: true, deactivated: false) do conn |> json(true) else @@ -26,7 +26,7 @@ defmodule Pleroma.Web.MongooseIM.MongooseIMController do end def check_password(conn, %{"user" => username, "pass" => password}) do - with %User{password_hash: password_hash} <- + with %User{password_hash: password_hash, deactivated: false} <- Repo.get_by(User, nickname: username, local: true), true <- Pbkdf2.checkpw(password, password_hash) do conn diff --git a/lib/pleroma/web/oauth/oauth_controller.ex b/lib/pleroma/web/oauth/oauth_controller.ex index 0121cd661..685269877 100644 --- a/lib/pleroma/web/oauth/oauth_controller.ex +++ b/lib/pleroma/web/oauth/oauth_controller.ex @@ -25,9 +25,10 @@ defmodule Pleroma.Web.OAuth.OAuthController do plug(:fetch_session) plug(:fetch_flash) - plug(RateLimiter, [name: :authentication] when action == :create_authorization) - plug(:skip_plug, Pleroma.Plugs.OAuthScopesPlug) + plug(:skip_plug, [Pleroma.Plugs.OAuthScopesPlug, Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug]) + + plug(RateLimiter, [name: :authentication] when action == :create_authorization) action_fallback(Pleroma.Web.OAuth.FallbackController) diff --git a/lib/pleroma/web/oauth/scopes.ex b/lib/pleroma/web/oauth/scopes.ex index 1023f16d4..6f06f1431 100644 --- a/lib/pleroma/web/oauth/scopes.ex +++ b/lib/pleroma/web/oauth/scopes.ex @@ -17,12 +17,8 @@ defmodule Pleroma.Web.OAuth.Scopes do """ @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) + parse_scopes(params["scope"] || params["scopes"] || params[:scopes], default) end def parse_scopes(scopes, _default) when is_list(scopes) do diff --git a/lib/pleroma/web/pleroma_api/controllers/account_controller.ex b/lib/pleroma/web/pleroma_api/controllers/account_controller.ex index 60405fbff..be7477867 100644 --- a/lib/pleroma/web/pleroma_api/controllers/account_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/account_controller.ex @@ -9,6 +9,7 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do only: [json_response: 3, add_link_headers: 2, assign_account_by_id: 2, skip_relationships?: 1] alias Ecto.Changeset + alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug alias Pleroma.Plugs.OAuthScopesPlug alias Pleroma.Plugs.RateLimiter alias Pleroma.User @@ -18,6 +19,11 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do require Pleroma.Constants plug( + :skip_plug, + [OAuthScopesPlug, EnsurePublicOrAuthenticatedPlug] when action == :confirmation_resend + ) + + plug( OAuthScopesPlug, %{scopes: ["follow", "write:follows"]} when action in [:subscribe, :unsubscribe] ) @@ -33,15 +39,13 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do ] ) - plug(OAuthScopesPlug, %{scopes: ["read:favourites"]} when action == :favourites) - - # An extra safety measure for possible actions not guarded by OAuth permissions specification plug( - Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug - when action != :confirmation_resend + OAuthScopesPlug, + %{scopes: ["read:favourites"], fallback: :proceed_unauthenticated} when action == :favourites ) plug(RateLimiter, [name: :account_confirmation_resend] when action == :confirmation_resend) + plug(:assign_account_by_id when action in [:favourites, :subscribe, :unsubscribe]) plug(:put_view, Pleroma.Web.MastodonAPI.AccountView) diff --git a/lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex b/lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex index 03e95e020..e01825b48 100644 --- a/lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex @@ -1,6 +1,7 @@ defmodule Pleroma.Web.PleromaAPI.EmojiAPIController do use Pleroma.Web, :controller + alias Pleroma.Plugs.ExpectPublicOrAuthenticatedCheckPlug alias Pleroma.Plugs.OAuthScopesPlug require Logger @@ -11,17 +12,20 @@ defmodule Pleroma.Web.PleromaAPI.EmojiAPIController do when action in [ :create, :delete, - :download_from, - :list_from, + :save_from, :import_from_fs, :update_file, :update_metadata ] ) - plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug) + plug( + :skip_plug, + [OAuthScopesPlug, ExpectPublicOrAuthenticatedCheckPlug] + when action in [:download_shared, :list_packs, :list_from] + ) - def emoji_dir_path do + defp emoji_dir_path do Path.join( Pleroma.Config.get!([:instance, :static_dir]), "emoji" @@ -212,13 +216,13 @@ keeping it in cache for #{div(cache_ms, 1000)}s") end @doc """ - An admin endpoint to request downloading a pack named `pack_name` from the instance + An admin endpoint to request downloading and storing a pack named `pack_name` from the instance `instance_address`. If the requested instance's admin chose to share the pack, it will be downloaded from that instance, otherwise it will be downloaded from the fallback source, if there is one. """ - def download_from(conn, %{"instance_address" => address, "pack_name" => name} = data) do + def save_from(conn, %{"instance_address" => address, "pack_name" => name} = data) do address = String.trim(address) if shareable_packs_available(address) do diff --git a/lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex b/lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex index d9c1c8636..d4e0d8b7c 100644 --- a/lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex @@ -12,8 +12,6 @@ defmodule Pleroma.Web.PleromaAPI.MascotController do plug(OAuthScopesPlug, %{scopes: ["read:accounts"]} when action == :show) plug(OAuthScopesPlug, %{scopes: ["write:accounts"]} when action != :show) - plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug) - @doc "GET /api/v1/pleroma/mascot" def show(%{assigns: %{user: user}} = conn, _params) do json(conn, User.get_mascot(user)) 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 fe1b97a20..2c1874051 100644 --- a/lib/pleroma/web/pleroma_api/controllers/pleroma_api_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/pleroma_api_controller.ex @@ -28,18 +28,26 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIController do plug( OAuthScopesPlug, + %{scopes: ["read:statuses"], fallback: :proceed_unauthenticated} + when action == :emoji_reactions_by + ) + + plug( + OAuthScopesPlug, %{scopes: ["write:statuses"]} when action in [:react_with_emoji, :unreact_with_emoji] ) plug( OAuthScopesPlug, - %{scopes: ["write:conversations"]} when action in [:update_conversation, :read_conversations] + %{scopes: ["write:conversations"]} + when action in [:update_conversation, :mark_conversations_as_read] ) - plug(OAuthScopesPlug, %{scopes: ["write:notifications"]} when action == :read_notification) - - plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug) + plug( + OAuthScopesPlug, + %{scopes: ["write:notifications"]} when action == :mark_notifications_as_read + ) def emoji_reactions_by(%{assigns: %{user: user}} = conn, %{"id" => activity_id} = params) do with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id), @@ -167,7 +175,7 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIController do end end - def read_conversations(%{assigns: %{user: user}} = conn, _params) do + def mark_conversations_as_read(%{assigns: %{user: user}} = conn, _params) do with {:ok, _, participations} <- Participation.mark_all_as_read(user) do conn |> add_link_headers(participations) @@ -176,7 +184,7 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIController do end end - def read_notification(%{assigns: %{user: user}} = conn, %{"id" => notification_id}) do + def mark_notifications_as_read(%{assigns: %{user: user}} = conn, %{"id" => notification_id}) do with {:ok, notification} <- Notification.read_one(user, notification_id) do conn |> put_view(NotificationView) @@ -189,7 +197,7 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIController do end end - def read_notification(%{assigns: %{user: user}} = conn, %{"max_id" => max_id} = params) do + def mark_notifications_as_read(%{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) diff --git a/lib/pleroma/web/pleroma_api/controllers/scrobble_controller.ex b/lib/pleroma/web/pleroma_api/controllers/scrobble_controller.ex index 4463ec477..22da6c0ad 100644 --- a/lib/pleroma/web/pleroma_api/controllers/scrobble_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/scrobble_controller.ex @@ -13,10 +13,12 @@ defmodule Pleroma.Web.PleromaAPI.ScrobbleController do alias Pleroma.Web.CommonAPI alias Pleroma.Web.MastodonAPI.StatusView - plug(OAuthScopesPlug, %{scopes: ["read"]} when action == :user_scrobbles) - plug(OAuthScopesPlug, %{scopes: ["write"]} when action != :user_scrobbles) + plug( + OAuthScopesPlug, + %{scopes: ["read"], fallback: :proceed_unauthenticated} when action == :user_scrobbles + ) - plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug) + plug(OAuthScopesPlug, %{scopes: ["write"]} when action != :user_scrobbles) def new_scrobble(%{assigns: %{user: user}} = conn, %{"title" => _} = params) do params = diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 153802a43..becce3098 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -16,6 +16,14 @@ defmodule Pleroma.Web.Router do plug(Pleroma.Plugs.UserEnabledPlug) end + pipeline :expect_authentication do + plug(Pleroma.Plugs.ExpectAuthenticatedCheckPlug) + end + + pipeline :expect_public_instance_or_authentication do + plug(Pleroma.Plugs.ExpectPublicOrAuthenticatedCheckPlug) + end + pipeline :authenticate do plug(Pleroma.Plugs.OAuthPlug) plug(Pleroma.Plugs.BasicAuthDecoderPlug) @@ -39,20 +47,22 @@ defmodule Pleroma.Web.Router do end pipeline :api do + plug(:expect_public_instance_or_authentication) plug(:base_api) plug(:after_auth) plug(Pleroma.Plugs.IdempotencyPlug) end pipeline :authenticated_api do + plug(:expect_authentication) plug(:base_api) - plug(Pleroma.Plugs.AuthExpectedPlug) plug(:after_auth) plug(Pleroma.Plugs.EnsureAuthenticatedPlug) plug(Pleroma.Plugs.IdempotencyPlug) end pipeline :admin_api do + plug(:expect_authentication) plug(:base_api) plug(Pleroma.Plugs.AdminSecretAuthenticationPlug) plug(:after_auth) @@ -200,24 +210,28 @@ defmodule Pleroma.Web.Router do end scope "/api/pleroma/emoji", Pleroma.Web.PleromaAPI do + # Modifying packs scope "/packs" do - # Modifying packs pipe_through(:admin_api) post("/import_from_fs", EmojiAPIController, :import_from_fs) - post("/:pack_name/update_file", EmojiAPIController, :update_file) post("/:pack_name/update_metadata", EmojiAPIController, :update_metadata) put("/:name", EmojiAPIController, :create) delete("/:name", EmojiAPIController, :delete) - post("/download_from", EmojiAPIController, :download_from) - post("/list_from", EmojiAPIController, :list_from) + + # Note: /download_from downloads and saves to instance, not to requester + post("/download_from", EmojiAPIController, :save_from) end + # Pack info / downloading scope "/packs" do - # Pack info / downloading get("/", EmojiAPIController, :list_packs) get("/:name/download_shared/", EmojiAPIController, :download_shared) + get("/list_from", EmojiAPIController, :list_from) + + # Deprecated: POST /api/pleroma/emoji/packs/list_from (use GET instead) + post("/list_from", EmojiAPIController, :list_from) end end @@ -277,7 +291,7 @@ defmodule Pleroma.Web.Router do get("/conversations/:id/statuses", PleromaAPIController, :conversation_statuses) get("/conversations/:id", PleromaAPIController, :conversation) - post("/conversations/read", PleromaAPIController, :read_conversations) + post("/conversations/read", PleromaAPIController, :mark_conversations_as_read) end scope [] do @@ -286,7 +300,7 @@ defmodule Pleroma.Web.Router do patch("/conversations/:id", PleromaAPIController, :update_conversation) put("/statuses/:id/reactions/:emoji", PleromaAPIController, :react_with_emoji) delete("/statuses/:id/reactions/:emoji", PleromaAPIController, :unreact_with_emoji) - post("/notifications/read", PleromaAPIController, :read_notification) + post("/notifications/read", PleromaAPIController, :mark_notifications_as_read) patch("/accounts/update_avatar", AccountController, :update_avatar) patch("/accounts/update_banner", AccountController, :update_banner) @@ -322,53 +336,84 @@ defmodule Pleroma.Web.Router do pipe_through(:authenticated_api) get("/accounts/verify_credentials", AccountController, :verify_credentials) + patch("/accounts/update_credentials", AccountController, :update_credentials) get("/accounts/relationships", AccountController, :relationships) - get("/accounts/:id/lists", AccountController, :lists) get("/accounts/:id/identity_proofs", AccountController, :identity_proofs) - - get("/follow_requests", FollowRequestController, :index) + get("/endorsements", AccountController, :endorsements) get("/blocks", AccountController, :blocks) get("/mutes", AccountController, :mutes) - get("/timelines/home", TimelineController, :home) - get("/timelines/direct", TimelineController, :direct) + post("/follows", AccountController, :follow_by_uri) + post("/accounts/:id/follow", AccountController, :follow) + post("/accounts/:id/unfollow", AccountController, :unfollow) + post("/accounts/:id/block", AccountController, :block) + post("/accounts/:id/unblock", AccountController, :unblock) + post("/accounts/:id/mute", AccountController, :mute) + post("/accounts/:id/unmute", AccountController, :unmute) - get("/favourites", StatusController, :favourites) - get("/bookmarks", StatusController, :bookmarks) + get("/apps/verify_credentials", AppController, :verify_credentials) + + get("/conversations", ConversationController, :index) + post("/conversations/:id/read", ConversationController, :mark_as_read) + + get("/domain_blocks", DomainBlockController, :index) + post("/domain_blocks", DomainBlockController, :create) + delete("/domain_blocks", DomainBlockController, :delete) + + get("/filters", FilterController, :index) + + post("/filters", FilterController, :create) + get("/filters/:id", FilterController, :show) + put("/filters/:id", FilterController, :update) + delete("/filters/:id", FilterController, :delete) + + get("/follow_requests", FollowRequestController, :index) + post("/follow_requests/:id/authorize", FollowRequestController, :authorize) + post("/follow_requests/:id/reject", FollowRequestController, :reject) + + get("/lists", ListController, :index) + get("/lists/:id", ListController, :show) + get("/lists/:id/accounts", ListController, :list_accounts) + + delete("/lists/:id", ListController, :delete) + post("/lists", ListController, :create) + put("/lists/:id", ListController, :update) + post("/lists/:id/accounts", ListController, :add_to_list) + delete("/lists/:id/accounts", ListController, :remove_from_list) + + get("/markers", MarkerController, :index) + post("/markers", MarkerController, :upsert) + + post("/media", MediaController, :create) + put("/media/:id", MediaController, :update) get("/notifications", NotificationController, :index) get("/notifications/:id", NotificationController, :show) + post("/notifications/:id/dismiss", NotificationController, :dismiss) post("/notifications/clear", NotificationController, :clear) 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) - - get("/lists", ListController, :index) - get("/lists/:id", ListController, :show) - get("/lists/:id/accounts", ListController, :list_accounts) - - get("/domain_blocks", DomainBlockController, :index) - - get("/filters", FilterController, :index) + post("/polls/:id/votes", PollController, :vote) - get("/suggestions", SuggestionController, :index) + post("/reports", ReportController, :create) - get("/conversations", ConversationController, :index) - post("/conversations/:id/read", ConversationController, :read) + get("/scheduled_statuses", ScheduledActivityController, :index) + get("/scheduled_statuses/:id", ScheduledActivityController, :show) - get("/endorsements", AccountController, :endorsements) + put("/scheduled_statuses/:id", ScheduledActivityController, :update) + delete("/scheduled_statuses/:id", ScheduledActivityController, :delete) - patch("/accounts/update_credentials", AccountController, :update_credentials) + # Unlike `GET /api/v1/accounts/:id/favourites`, demands authentication + get("/favourites", StatusController, :favourites) + get("/bookmarks", StatusController, :bookmarks) post("/statuses", StatusController, :create) delete("/statuses/:id", StatusController, :delete) - post("/statuses/:id/reblog", StatusController, :reblog) post("/statuses/:id/unreblog", StatusController, :unreblog) post("/statuses/:id/favourite", StatusController, :favourite) @@ -380,49 +425,16 @@ defmodule Pleroma.Web.Router do post("/statuses/:id/mute", StatusController, :mute_conversation) post("/statuses/:id/unmute", StatusController, :unmute_conversation) - put("/scheduled_statuses/:id", ScheduledActivityController, :update) - delete("/scheduled_statuses/:id", ScheduledActivityController, :delete) - - post("/polls/:id/votes", PollController, :vote) - - post("/media", MediaController, :create) - put("/media/:id", MediaController, :update) - - delete("/lists/:id", ListController, :delete) - post("/lists", ListController, :create) - put("/lists/:id", ListController, :update) - - post("/lists/:id/accounts", ListController, :add_to_list) - delete("/lists/:id/accounts", ListController, :remove_from_list) - - post("/filters", FilterController, :create) - get("/filters/:id", FilterController, :show) - put("/filters/:id", FilterController, :update) - delete("/filters/:id", FilterController, :delete) - - post("/reports", ReportController, :create) - - post("/follows", AccountController, :follows) - post("/accounts/:id/follow", AccountController, :follow) - post("/accounts/:id/unfollow", AccountController, :unfollow) - post("/accounts/:id/block", AccountController, :block) - post("/accounts/:id/unblock", AccountController, :unblock) - post("/accounts/:id/mute", AccountController, :mute) - post("/accounts/:id/unmute", AccountController, :unmute) - - post("/follow_requests/:id/authorize", FollowRequestController, :authorize) - post("/follow_requests/:id/reject", FollowRequestController, :reject) - - post("/domain_blocks", DomainBlockController, :create) - delete("/domain_blocks", DomainBlockController, :delete) - post("/push/subscription", SubscriptionController, :create) get("/push/subscription", SubscriptionController, :get) put("/push/subscription", SubscriptionController, :update) delete("/push/subscription", SubscriptionController, :delete) - get("/markers", MarkerController, :index) - post("/markers", MarkerController, :upsert) + get("/suggestions", SuggestionController, :index) + + get("/timelines/home", TimelineController, :home) + get("/timelines/direct", TimelineController, :direct) + get("/timelines/list/:list_id", TimelineController, :list) end scope "/api/web", Pleroma.Web do @@ -434,15 +446,24 @@ defmodule Pleroma.Web.Router do scope "/api/v1", Pleroma.Web.MastodonAPI do pipe_through(:api) - post("/accounts", AccountController, :create) get("/accounts/search", SearchController, :account_search) + get("/search", SearchController, :search) + + get("/accounts/:id/statuses", AccountController, :statuses) + get("/accounts/:id/followers", AccountController, :followers) + get("/accounts/:id/following", AccountController, :following) + get("/accounts/:id", AccountController, :show) + + post("/accounts", AccountController, :create) get("/instance", InstanceController, :show) get("/instance/peers", InstanceController, :peers) post("/apps", AppController, :create) - get("/apps/verify_credentials", AppController, :verify_credentials) + get("/statuses", StatusController, :index) + get("/statuses/:id", StatusController, :show) + get("/statuses/:id/context", StatusController, :context) get("/statuses/:id/card", StatusController, :card) get("/statuses/:id/favourited_by", StatusController, :favourited_by) get("/statuses/:id/reblogged_by", StatusController, :reblogged_by) @@ -453,20 +474,8 @@ defmodule Pleroma.Web.Router do get("/timelines/public", TimelineController, :public) get("/timelines/tag/:tag", TimelineController, :hashtag) - get("/timelines/list/:list_id", TimelineController, :list) - - get("/statuses", StatusController, :index) - get("/statuses/:id", StatusController, :show) - get("/statuses/:id/context", StatusController, :context) get("/polls/:id", PollController, :show) - - get("/accounts/:id/statuses", AccountController, :statuses) - get("/accounts/:id/followers", AccountController, :followers) - get("/accounts/:id/following", AccountController, :following) - get("/accounts/:id", AccountController, :show) - - get("/search", SearchController, :search) end scope "/api/v2", Pleroma.Web.MastodonAPI do @@ -507,7 +516,11 @@ defmodule Pleroma.Web.Router do get("/oauth_tokens", TwitterAPI.Controller, :oauth_tokens) delete("/oauth_tokens/:id", TwitterAPI.Controller, :revoke_token) - post("/qvitter/statuses/notifications/read", TwitterAPI.Controller, :notifications_read) + post( + "/qvitter/statuses/notifications/read", + TwitterAPI.Controller, + :mark_notifications_as_read + ) end pipeline :ostatus do @@ -647,11 +660,28 @@ defmodule Pleroma.Web.Router do # Test-only routes needed to test action dispatching and plug chain execution if Pleroma.Config.get(:env) == :test do + @test_actions [ + :do_oauth_check, + :fallback_oauth_check, + :skip_oauth_check, + :fallback_oauth_skip_publicity_check, + :skip_oauth_skip_publicity_check, + :missing_oauth_check_definition + ] + + scope "/test/api", Pleroma.Tests do + pipe_through(:api) + + for action <- @test_actions do + get("/#{action}", AuthTestController, action) + end + end + scope "/test/authenticated_api", Pleroma.Tests do pipe_through(:authenticated_api) - for action <- [:skipped_oauth, :performed_oauth, :missed_oauth] do - get("/#{action}", OAuthTestController, action) + for action <- @test_actions do + get("/#{action}", AuthTestController, action) end end end diff --git a/lib/pleroma/web/twitter_api/controllers/util_controller.ex b/lib/pleroma/web/twitter_api/controllers/util_controller.ex index d5d5ce08f..fd2aee175 100644 --- a/lib/pleroma/web/twitter_api/controllers/util_controller.ex +++ b/lib/pleroma/web/twitter_api/controllers/util_controller.ex @@ -25,13 +25,6 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do when action == :follow_import ) - # Note: follower can submit the form (with password auth) not being signed in (having no token) - plug( - OAuthScopesPlug, - %{fallback: :proceed_unauthenticated, scopes: ["follow", "write:follows"]} - when action == :do_remote_follow - ) - plug(OAuthScopesPlug, %{scopes: ["follow", "write:blocks"]} when action == :blocks_import) plug( diff --git a/lib/pleroma/web/twitter_api/twitter_api.ex b/lib/pleroma/web/twitter_api/twitter_api.ex index 7a1ba6936..cf1d9c74c 100644 --- a/lib/pleroma/web/twitter_api/twitter_api.ex +++ b/lib/pleroma/web/twitter_api/twitter_api.ex @@ -12,73 +12,57 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI do require Pleroma.Constants def register_user(params, opts \\ []) do - token = params["token"] - trusted_app? = params["trusted_app"] - - params = %{ - nickname: params["nickname"], - name: params["fullname"], - bio: User.parse_bio(params["bio"]), - email: params["email"], - password: params["password"], - password_confirmation: params["confirm"], - captcha_solution: params["captcha_solution"], - captcha_token: params["captcha_token"], - captcha_answer_data: params["captcha_answer_data"] - } - - captcha_enabled = Pleroma.Config.get([Pleroma.Captcha, :enabled]) - # true if captcha is disabled or enabled and valid, false otherwise - captcha_ok = - if trusted_app? || not captcha_enabled do - :ok - else - Pleroma.Captcha.validate( - params[:captcha_token], - params[:captcha_solution], - params[:captcha_answer_data] - ) - end - - # Captcha invalid - if captcha_ok != :ok do - {:error, error} = captcha_ok - # I have no idea how this error handling works - {:error, %{error: Jason.encode!(%{captcha: [error]})}} - else - registration_process( - params, - %{ - registrations_open: Pleroma.Config.get([:instance, :registrations_open]), - token: token - }, - opts - ) + params = + params + |> Map.take([ + :nickname, + :password, + :captcha_solution, + :captcha_token, + :captcha_answer_data, + :token, + :email, + :trusted_app + ]) + |> Map.put(:bio, User.parse_bio(params[:bio] || "")) + |> Map.put(:name, params.fullname) + |> Map.put(:password_confirmation, params[:confirm]) + + case validate_captcha(params) do + :ok -> + if Pleroma.Config.get([:instance, :registrations_open]) do + create_user(params, opts) + else + create_user_with_invite(params, opts) + end + + {:error, error} -> + # I have no idea how this error handling works + {:error, %{error: Jason.encode!(%{captcha: [error]})}} end end - defp registration_process(params, %{registrations_open: true}, opts) do - create_user(params, opts) + defp validate_captcha(params) do + if params[:trusted_app] || not Pleroma.Config.get([Pleroma.Captcha, :enabled]) do + :ok + else + Pleroma.Captcha.validate( + params.captcha_token, + params.captcha_solution, + params.captcha_answer_data + ) + end end - defp registration_process(params, %{token: token}, opts) do - invite = - unless is_nil(token) do - Repo.get_by(UserInviteToken, %{token: token}) - end - - valid_invite? = invite && UserInviteToken.valid_invite?(invite) - - case invite do - nil -> - {:error, "Invalid token"} - - invite when valid_invite? -> - UserInviteToken.update_usage!(invite) - create_user(params, opts) - - _ -> - {:error, "Expired token"} + defp create_user_with_invite(params, opts) do + with %{token: token} when is_binary(token) <- params, + %UserInviteToken{} = invite <- Repo.get_by(UserInviteToken, %{token: token}), + true <- UserInviteToken.valid_invite?(invite) do + UserInviteToken.update_usage!(invite) + create_user(params, opts) + else + nil -> {:error, "Invalid token"} + _ -> {:error, "Expired token"} end end diff --git a/lib/pleroma/web/twitter_api/twitter_api_controller.ex b/lib/pleroma/web/twitter_api/twitter_api_controller.ex index 31adc2817..c2de26b0b 100644 --- a/lib/pleroma/web/twitter_api/twitter_api_controller.ex +++ b/lib/pleroma/web/twitter_api/twitter_api_controller.ex @@ -6,6 +6,7 @@ defmodule Pleroma.Web.TwitterAPI.Controller do use Pleroma.Web, :controller alias Pleroma.Notification + alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug alias Pleroma.Plugs.OAuthScopesPlug alias Pleroma.User alias Pleroma.Web.OAuth.Token @@ -13,11 +14,17 @@ defmodule Pleroma.Web.TwitterAPI.Controller do require Logger - plug(OAuthScopesPlug, %{scopes: ["write:notifications"]} when action == :notifications_read) + plug( + OAuthScopesPlug, + %{scopes: ["write:notifications"]} when action == :mark_notifications_as_read + ) - plug(:skip_plug, OAuthScopesPlug when action in [:oauth_tokens, :revoke_token]) + plug( + :skip_plug, + [OAuthScopesPlug, EnsurePublicOrAuthenticatedPlug] when action == :confirm_email + ) - plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug) + plug(:skip_plug, OAuthScopesPlug when action in [:oauth_tokens, :revoke_token]) action_fallback(:errors) @@ -46,13 +53,13 @@ defmodule Pleroma.Web.TwitterAPI.Controller do json_reply(conn, 201, "") end - def errors(conn, {:param_cast, _}) do + defp errors(conn, {:param_cast, _}) do conn |> put_status(400) |> json("Invalid parameters") end - def errors(conn, _) do + defp errors(conn, _) do conn |> put_status(500) |> json("Something went wrong") @@ -64,7 +71,10 @@ defmodule Pleroma.Web.TwitterAPI.Controller do |> send_resp(status, json) end - def notifications_read(%{assigns: %{user: user}} = conn, %{"latest_id" => latest_id} = params) do + def mark_notifications_as_read( + %{assigns: %{user: user}} = conn, + %{"latest_id" => latest_id} = params + ) do Notification.set_read_up_to(user, latest_id) notifications = Notification.for_user(user, params) @@ -75,7 +85,7 @@ defmodule Pleroma.Web.TwitterAPI.Controller do |> render("index.json", %{notifications: notifications, for: user}) end - def notifications_read(%{assigns: %{user: _user}} = conn, _) do + def mark_notifications_as_read(%{assigns: %{user: _user}} = conn, _) do bad_request_reply(conn, "You need to specify latest_id") end diff --git a/lib/pleroma/web/web.ex b/lib/pleroma/web/web.ex index bf48ce26c..08e42a7e5 100644 --- a/lib/pleroma/web/web.ex +++ b/lib/pleroma/web/web.ex @@ -2,6 +2,11 @@ # Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> # SPDX-License-Identifier: AGPL-3.0-only +defmodule Pleroma.Web.Plug do + # Substitute for `call/2` which is defined with `use Pleroma.Web, :plug` + @callback perform(Plug.Conn.t(), Plug.opts()) :: Plug.Conn.t() +end + defmodule Pleroma.Web do @moduledoc """ A module that keeps using definitions for controllers, @@ -20,44 +25,91 @@ defmodule Pleroma.Web do below. """ + alias Pleroma.Plugs.EnsureAuthenticatedPlug + alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug + alias Pleroma.Plugs.ExpectAuthenticatedCheckPlug + alias Pleroma.Plugs.ExpectPublicOrAuthenticatedCheckPlug + alias Pleroma.Plugs.OAuthScopesPlug + alias Pleroma.Plugs.PlugHelper + def controller do quote do use Phoenix.Controller, namespace: Pleroma.Web import Plug.Conn + import Pleroma.Web.Gettext 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.skip_plug(conn) - rescue - UndefinedFunctionError -> - raise "#{plug_module} is not skippable. Append `use Pleroma.Web, :plug` to its code." - end + # Marks plugs intentionally skipped and blocks their execution if present in plugs chain + defp skip_plug(conn, plug_modules) do + plug_modules + |> List.wrap() + |> Enum.reduce( + conn, + fn plug_module, conn -> + try do + plug_module.skip_plug(conn) + rescue + UndefinedFunctionError -> + raise "`#{plug_module}` is not skippable. Append `use Pleroma.Web, :plug` to its code." + end + end + ) end # Executed just before actual controller action, invokes before-action hooks (callbacks) defp action(conn, params) do - with %Plug.Conn{halted: false} <- maybe_halt_on_missing_oauth_scopes_check(conn) do + with %{halted: false} = conn <- maybe_drop_authentication_if_oauth_check_ignored(conn), + %{halted: false} = conn <- maybe_perform_public_or_authenticated_check(conn), + %{halted: false} = conn <- maybe_perform_authenticated_check(conn), + %{halted: false} = conn <- maybe_halt_on_missing_oauth_scopes_check(conn) do super(conn, params) end end + # For non-authenticated API actions, drops auth info if OAuth scopes check was ignored + # (neither performed nor explicitly skipped) + defp maybe_drop_authentication_if_oauth_check_ignored(conn) do + if PlugHelper.plug_called?(conn, ExpectPublicOrAuthenticatedCheckPlug) and + not PlugHelper.plug_called_or_skipped?(conn, OAuthScopesPlug) do + OAuthScopesPlug.drop_auth_info(conn) + else + conn + end + end + + # Ensures instance is public -or- user is authenticated if such check was scheduled + defp maybe_perform_public_or_authenticated_check(conn) do + if PlugHelper.plug_called?(conn, ExpectPublicOrAuthenticatedCheckPlug) do + EnsurePublicOrAuthenticatedPlug.call(conn, %{}) + else + conn + end + end + + # Ensures user is authenticated if such check was scheduled + # Note: runs prior to action even if it was already executed earlier in plug chain + # (since OAuthScopesPlug has option of proceeding unauthenticated) + defp maybe_perform_authenticated_check(conn) do + if PlugHelper.plug_called?(conn, ExpectAuthenticatedCheckPlug) do + EnsureAuthenticatedPlug.call(conn, %{}) + else + conn + end + end + # Halts if authenticated API action neither performs nor explicitly skips OAuth scopes check defp maybe_halt_on_missing_oauth_scopes_check(conn) do - if Pleroma.Plugs.AuthExpectedPlug.auth_expected?(conn) && - not PlugHelper.plug_called_or_skipped?(conn, Pleroma.Plugs.OAuthScopesPlug) do + if PlugHelper.plug_called?(conn, ExpectAuthenticatedCheckPlug) and + not PlugHelper.plug_called_or_skipped?(conn, OAuthScopesPlug) do conn |> render_error( :forbidden, @@ -132,7 +184,8 @@ defmodule Pleroma.Web do def plug do quote do - alias Pleroma.Plugs.PlugHelper + @behaviour Pleroma.Web.Plug + @behaviour Plug @doc """ Marks a plug intentionally skipped and blocks its execution if it's present in plugs chain. @@ -146,14 +199,22 @@ defmodule Pleroma.Web do end @impl Plug - @doc "If marked as skipped, returns `conn`, and calls `perform/2` otherwise." + @doc """ + If marked as skipped, returns `conn`, otherwise calls `perform/2`. + Note: multiple invocations of the same plug (with different or same options) are allowed. + """ def call(%Plug.Conn{} = conn, options) do if PlugHelper.plug_skipped?(conn, __MODULE__) do conn else - conn - |> PlugHelper.append_to_private_list(PlugHelper.called_plugs_list_id(), __MODULE__) - |> perform(options) + conn = + PlugHelper.append_to_private_list( + conn, + PlugHelper.called_plugs_list_id(), + __MODULE__ + ) + + apply(__MODULE__, :perform, [conn, options]) end end end |