Merge branch '1364-no-pushes-from-blocked-domains-users' into 'develop'
authorlain <lain@soykaf.club>
Thu, 30 Apr 2020 10:29:46 +0000 (10:29 +0000)
committerlain <lain@soykaf.club>
Thu, 30 Apr 2020 10:29:46 +0000 (10:29 +0000)
[#1364] [FIX] Disabled notifications on activities from blocked domains (unless actors are followed)

See merge request pleroma/pleroma!2367

118 files changed:
CHANGELOG.md
benchmarks/load_testing/activities.ex
benchmarks/load_testing/fetcher.ex
benchmarks/mix/tasks/pleroma/load_testing.ex
docs/API/differences_in_mastoapi_responses.md
docs/configuration/hardening.md
docs/dev.md [new file with mode: 0644]
lib/pleroma/plugs/auth_expected_plug.ex [deleted file]
lib/pleroma/plugs/ensure_authenticated_plug.ex
lib/pleroma/plugs/ensure_public_or_authenticated_plug.ex
lib/pleroma/plugs/expect_authenticated_check_plug.ex [new file with mode: 0644]
lib/pleroma/plugs/expect_public_or_authenticated_check_plug.ex [new file with mode: 0644]
lib/pleroma/plugs/http_security_plug.ex
lib/pleroma/plugs/oauth_scopes_plug.ex
lib/pleroma/stats.ex
lib/pleroma/tests/auth_test_controller.ex [new file with mode: 0644]
lib/pleroma/tests/oauth_test_controller.ex [deleted file]
lib/pleroma/user.ex
lib/pleroma/user/query.ex
lib/pleroma/web/activity_pub/activity_pub.ex
lib/pleroma/web/activity_pub/activity_pub_controller.ex
lib/pleroma/web/activity_pub/side_effects.ex
lib/pleroma/web/admin_api/admin_api_controller.ex
lib/pleroma/web/admin_api/views/status_view.ex
lib/pleroma/web/api_spec.ex
lib/pleroma/web/api_spec/helpers.ex
lib/pleroma/web/api_spec/operations/account_operation.ex [new file with mode: 0644]
lib/pleroma/web/api_spec/operations/app_operation.ex
lib/pleroma/web/api_spec/operations/custom_emoji_operation.ex
lib/pleroma/web/api_spec/operations/domain_block_operation.ex
lib/pleroma/web/api_spec/render_error.ex [new file with mode: 0644]
lib/pleroma/web/api_spec/schemas/account.ex [new file with mode: 0644]
lib/pleroma/web/api_spec/schemas/account_field.ex [new file with mode: 0644]
lib/pleroma/web/api_spec/schemas/account_relationship.ex [new file with mode: 0644]
lib/pleroma/web/api_spec/schemas/actor_type.ex [new file with mode: 0644]
lib/pleroma/web/api_spec/schemas/api_error.ex [moved from lib/pleroma/web/api_spec/schemas/domain_block_request.ex with 56% similarity]
lib/pleroma/web/api_spec/schemas/app_create_request.ex [deleted file]
lib/pleroma/web/api_spec/schemas/app_create_response.ex [deleted file]
lib/pleroma/web/api_spec/schemas/boolean_like.ex [new file with mode: 0644]
lib/pleroma/web/api_spec/schemas/custom_emoji.ex [deleted file]
lib/pleroma/web/api_spec/schemas/custom_emojis_response.ex [deleted file]
lib/pleroma/web/api_spec/schemas/domain_blocks_response.ex [deleted file]
lib/pleroma/web/api_spec/schemas/emoji.ex [new file with mode: 0644]
lib/pleroma/web/api_spec/schemas/flake_id.ex [new file with mode: 0644]
lib/pleroma/web/api_spec/schemas/poll.ex [new file with mode: 0644]
lib/pleroma/web/api_spec/schemas/status.ex [new file with mode: 0644]
lib/pleroma/web/api_spec/schemas/visibility_scope.ex [new file with mode: 0644]
lib/pleroma/web/common_api/activity_draft.ex
lib/pleroma/web/common_api/common_api.ex
lib/pleroma/web/common_api/utils.ex
lib/pleroma/web/controller_helper.ex
lib/pleroma/web/fallback_redirect_controller.ex
lib/pleroma/web/masto_fe_controller.ex
lib/pleroma/web/mastodon_api/controllers/account_controller.ex
lib/pleroma/web/mastodon_api/controllers/app_controller.ex
lib/pleroma/web/mastodon_api/controllers/auth_controller.ex
lib/pleroma/web/mastodon_api/controllers/conversation_controller.ex
lib/pleroma/web/mastodon_api/controllers/custom_emoji_controller.ex
lib/pleroma/web/mastodon_api/controllers/domain_block_controller.ex
lib/pleroma/web/mastodon_api/controllers/filter_controller.ex
lib/pleroma/web/mastodon_api/controllers/follow_request_controller.ex
lib/pleroma/web/mastodon_api/controllers/instance_controller.ex
lib/pleroma/web/mastodon_api/controllers/list_controller.ex
lib/pleroma/web/mastodon_api/controllers/marker_controller.ex
lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex
lib/pleroma/web/mastodon_api/controllers/media_controller.ex
lib/pleroma/web/mastodon_api/controllers/notification_controller.ex
lib/pleroma/web/mastodon_api/controllers/poll_controller.ex
lib/pleroma/web/mastodon_api/controllers/report_controller.ex
lib/pleroma/web/mastodon_api/controllers/scheduled_activity_controller.ex
lib/pleroma/web/mastodon_api/controllers/search_controller.ex
lib/pleroma/web/mastodon_api/controllers/status_controller.ex
lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex
lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex
lib/pleroma/web/mastodon_api/views/status_view.ex
lib/pleroma/web/media_proxy/media_proxy_controller.ex
lib/pleroma/web/mongooseim/mongoose_im_controller.ex
lib/pleroma/web/oauth/oauth_controller.ex
lib/pleroma/web/oauth/scopes.ex
lib/pleroma/web/pleroma_api/controllers/account_controller.ex
lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex
lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex
lib/pleroma/web/pleroma_api/controllers/pleroma_api_controller.ex
lib/pleroma/web/pleroma_api/controllers/scrobble_controller.ex
lib/pleroma/web/router.ex
lib/pleroma/web/twitter_api/controllers/util_controller.ex
lib/pleroma/web/twitter_api/twitter_api.ex
lib/pleroma/web/twitter_api/twitter_api_controller.ex
lib/pleroma/web/web.ex
mix.exs
mix.lock
test/plugs/ensure_authenticated_plug_test.exs
test/plugs/ensure_public_or_authenticated_plug_test.exs
test/plugs/oauth_scopes_plug_test.exs
test/stats_test.exs [moved from test/stat_test.exs with 86% similarity]
test/support/api_spec_helpers.ex [new file with mode: 0644]
test/support/conn_case.ex
test/web/activity_pub/activity_pub_controller_test.exs
test/web/activity_pub/activity_pub_test.exs
test/web/activity_pub/utils_test.exs
test/web/api_spec/app_operation_test.exs [deleted file]
test/web/api_spec/schema_examples_test.exs [new file with mode: 0644]
test/web/auth/auth_test_controller_test.exs [new file with mode: 0644]
test/web/auth/oauth_test_controller_test.exs [deleted file]
test/web/common_api/common_api_test.exs
test/web/common_api/common_api_utils_test.exs
test/web/mastodon_api/controllers/account_controller/update_credentials_test.exs
test/web/mastodon_api/controllers/account_controller_test.exs
test/web/mastodon_api/controllers/app_controller_test.exs
test/web/mastodon_api/controllers/custom_emoji_controller_test.exs
test/web/mastodon_api/controllers/domain_block_controller_test.exs
test/web/mastodon_api/controllers/status_controller_test.exs
test/web/mastodon_api/mastodon_api_controller_test.exs
test/web/mongooseim/mongoose_im_controller_test.exs
test/web/pleroma_api/controllers/account_controller_test.exs
test/web/pleroma_api/controllers/emoji_api_controller_test.exs
test/web/twitter_api/twitter_api_controller_test.exs
test/web/twitter_api/twitter_api_test.exs

index 27746726eb6bfde1c3ab553090e73be7aff7c366..c0f1bcf5708352c24e3662b6a0939a89d1d2efea 100644 (file)
@@ -19,6 +19,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
   <summary>API Changes</summary>
 - Mastodon API: Support for `include_types` in `/api/v1/notifications`.
 - Mastodon API: Added `/api/v1/notifications/:id/dismiss` endpoint.
+- Mastodon API: Add support for filtering replies in public and home timelines
 - Admin API: endpoints for create/update/delete OAuth Apps.
 </details>
 
index 23ee2b987cd26258b4fa58b59e7493e622965596..482e42fc14d5841ef03d9a493140beaa02c65deb 100644 (file)
@@ -279,7 +279,7 @@ defmodule Pleroma.LoadTesting.Activities do
     actor = get_actor(group, user, friends, non_friends)
 
     with activity_id when not is_nil(activity_id) <- get_random_create_activity_id(),
-         {:ok, _activity, _object} <- CommonAPI.favorite(activity_id, actor) do
+         {:ok, _activity} <- CommonAPI.favorite(actor, activity_id) do
       :ok
     else
       {:error, _} ->
@@ -313,7 +313,7 @@ defmodule Pleroma.LoadTesting.Activities do
     tasks = get_reply_tasks(visibility, group)
 
     {:ok, activity} =
-      CommonAPI.post(user, %{"status" => "Simple status", "visibility" => "unlisted"})
+      CommonAPI.post(user, %{"status" => "Simple status", "visibility" => visibility})
 
     acc = {activity.id, ["@" <> actor.nickname, "reply to status"]}
     insert_replies(tasks, visibility, user, friends, non_friends, acc)
index 786929ace9faa1989e96e2896f554b7a6c533116..12c30f6f55a1b71f88070a83b7106b7667b4da43 100644 (file)
@@ -41,6 +41,7 @@ defmodule Pleroma.LoadTesting.Fetcher do
     fetch_notifications(user)
     fetch_favourites(user)
     fetch_long_thread(user)
+    fetch_timelines_with_reply_filtering(user)
   end
 
   defp render_views(user) do
@@ -495,4 +496,58 @@ defmodule Pleroma.LoadTesting.Fetcher do
       formatters: formatters()
     )
   end
+
+  defp fetch_timelines_with_reply_filtering(user) do
+    public_params = opts_for_public_timeline(user)
+
+    Benchee.run(
+      %{
+        "Public timeline without reply filtering" => fn ->
+          ActivityPub.fetch_public_activities(public_params)
+        end,
+        "Public timeline with reply filtering - following" => fn ->
+          public_params
+          |> Map.put("reply_visibility", "following")
+          |> Map.put("reply_filtering_user", user)
+          |> ActivityPub.fetch_public_activities()
+        end,
+        "Public timeline with reply filtering - self" => fn ->
+          public_params
+          |> Map.put("reply_visibility", "self")
+          |> Map.put("reply_filtering_user", user)
+          |> ActivityPub.fetch_public_activities()
+        end
+      },
+      formatters: formatters()
+    )
+
+    private_params = opts_for_home_timeline(user)
+
+    recipients = [user.ap_id | User.following(user)]
+
+    Benchee.run(
+      %{
+        "Home timeline without reply filtering" => fn ->
+          ActivityPub.fetch_activities(recipients, private_params)
+        end,
+        "Home timeline with reply filtering - following" => fn ->
+          private_params =
+            private_params
+            |> Map.put("reply_filtering_user", user)
+            |> Map.put("reply_visibility", "following")
+
+          ActivityPub.fetch_activities(recipients, private_params)
+        end,
+        "Home timeline with reply filtering - self" => fn ->
+          private_params =
+            private_params
+            |> Map.put("reply_filtering_user", user)
+            |> Map.put("reply_visibility", "self")
+
+          ActivityPub.fetch_activities(recipients, private_params)
+        end
+      },
+      formatters: formatters()
+    )
+  end
 end
index 72b225f09e330f872c99eca21d90db4e2d5a2a28..3888832402230a57cdb7adbf8bcf461c71be5be0 100644 (file)
@@ -44,6 +44,7 @@ defmodule Mix.Tasks.Pleroma.LoadTesting do
   ]
 
   def run(args) do
+    Logger.configure(level: :error)
     Mix.Pleroma.start_pleroma()
     clean_tables()
     {opts, _} = OptionParser.parse!(args, strict: @switches, aliases: @aliases)
index 1059155cfccba73ec292a048f7fd5c13fbd3f9ed..289f859306fe229b96b0a5fb8989577ca78d49eb 100644 (file)
@@ -4,7 +4,7 @@ A Pleroma instance can be identified by "<Mastodon version> (compatible; Pleroma
 
 ## Flake IDs
 
-Pleroma uses 128-bit ids as opposed to Mastodon's 64 bits. However just like Mastodon's ids they are sortable strings
+Pleroma uses 128-bit ids as opposed to Mastodon's 64 bits. However just like Mastodon's ids they are lexically sortable strings
 
 ## Attachment cap
 
@@ -14,6 +14,7 @@ Some apps operate under the assumption that no more than 4 attachments can be re
 
 Adding the parameter `with_muted=true` to the timeline queries will also return activities by muted (not by blocked!) users.
 Adding the parameter `exclude_visibilities` to the timeline queries will exclude the statuses with the given visibilities. The parameter accepts an array of visibility types (`public`, `unlisted`, `private`, `direct`), e.g., `exclude_visibilities[]=direct&exclude_visibilities[]=private`.
+Adding the parameter `reply_visibility` to the public and home timelines queries will filter replies. Possible values: without parameter (default) shows all replies, `following` - replies directed to you or users you follow, `self` - replies directed to you.
 
 ## Statuses
 
@@ -119,6 +120,18 @@ Accepts additional parameters:
 - `exclude_visibilities`: will exclude the notifications for activities with the given visibilities. The parameter accepts an array of visibility types (`public`, `unlisted`, `private`, `direct`). Usage example: `GET /api/v1/notifications?exclude_visibilities[]=direct&exclude_visibilities[]=private`.
 - `include_types`: will include the notifications for activities with the given types. The parameter accepts an array of types (`mention`, `follow`, `reblog`, `favourite`, `move`, `pleroma:emoji_reaction`). Usage example: `GET /api/v1/notifications?include_types[]=mention&include_types[]=reblog`.
 
+## DELETE `/api/v1/notifications/destroy_multiple`
+
+An endpoint to delete multiple statuses by IDs.
+
+Required parameters:
+
+- `ids`: array of activity ids
+
+Usage example: `DELETE /api/v1/notifications/destroy_multiple/?ids[]=1&ids[]=2`.
+
+Returns on success: 200 OK `{}`
+
 ## POST `/api/v1/statuses`
 
 Additional parameters can be added to the JSON body/Form data:
index b54c28850d04e93d2e245e9bca3263ed20558a3a..d3bfc4e4a692abb34f8fbc038136d876d39ae186 100644 (file)
@@ -36,7 +36,7 @@ content-security-policy:
   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/docs/dev.md b/docs/dev.md
new file mode 100644 (file)
index 0000000..f1b4cbf
--- /dev/null
@@ -0,0 +1,23 @@
+This document contains notes and guidelines for Pleroma developers.
+
+# Authentication & Authorization
+
+## OAuth token-based authentication & authorization
+
+* Pleroma supports hierarchical OAuth scopes, just like Mastodon but with added granularity of admin scopes. For a reference, see [Mastodon OAuth scopes](https://docs.joinmastodon.org/api/oauth-scopes/).
+
+* It is important to either define OAuth scope restrictions or explicitly mark OAuth scope check as skipped, for every controller action. To define scopes, call `plug(Pleroma.Plugs.OAuthScopesPlug, %{scopes: [...]})`. To explicitly set OAuth scopes check skipped, call `plug(:skip_plug, Pleroma.Plugs.OAuthScopesPlug <when ...>)`.
+
+* In controllers, `use Pleroma.Web, :controller` will result in `action/2` (see `Pleroma.Web.controller/0` for definition) be called prior to actual controller action, and it'll perform security / privacy checks before passing control to actual controller action.
+
+  For routes with `:authenticated_api` pipeline, authentication & authorization are expected, thus `OAuthScopesPlug` will be run unless explicitly skipped (also `EnsureAuthenticatedPlug` will be executed immediately before action even if there was an early run to give an early error, since `OAuthScopesPlug` supports `:proceed_unauthenticated` option, and other plugs may support similar options as well).
+
+  For `:api` pipeline routes, it'll be verified whether `OAuthScopesPlug` was called or explicitly skipped, and if it was not then auth information will be dropped for request. Then `EnsurePublicOrAuthenticatedPlug` will be called to ensure that either the instance is not private or user is authenticated (unless explicitly skipped). Such automated checks help to prevent human errors and result in higher security / privacy for users.
+
+## [HTTP Basic Authentication](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Authorization)
+
+* With HTTP Basic Auth, OAuth scopes check is _not_ performed for any action (since password is provided during the auth, requester is able to obtain a token with full permissions anyways). `Pleroma.Plugs.AuthenticationPlug` and `Pleroma.Plugs.LegacyAuthenticationPlug` both call `Pleroma.Plugs.OAuthScopesPlug.skip_plug(conn)` when password is provided.
+
+## Auth-related configuration, OAuth consumer mode etc.
+
+See `Authentication` section of [`docs/configuration/cheatsheet.md`](docs/configuration/cheatsheet.md#authentication).
diff --git a/lib/pleroma/plugs/auth_expected_plug.ex b/lib/pleroma/plugs/auth_expected_plug.ex
deleted file mode 100644 (file)
index f79597d..0000000
+++ /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
index 054d2297f1906a31ee4810823299474c8caf5e17..9c8f5597f72764ee9dd31dc6b701a9ce21854299 100644 (file)
@@ -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].()
index d980ff13d133bf64edd0e45dfea861e26a28e502..7265bb87aaa0a158f94c9da3dc16260fd1fa4d28 100644 (file)
@@ -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 (file)
index 0000000..66b8d5d
--- /dev/null
@@ -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 (file)
index 0000000..ba0ef76
--- /dev/null
@@ -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
index 81e6b4f2a36c04bc3b693582384c46090be11bc8..6462797b635787d39160b192c80d857e462c1482 100644 (file)
@@ -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'",
index 66f48c28c600f8fda50dea5c326edf47027125cc..efc25b79ff7e6c8afaa6175e79eb079d1f8a9a88 100644 (file)
@@ -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
index 4446562ac0b797fc28762cee480387796852b28d..8d2809bbbe546bce9399f069d72f11027da5ba45 100644 (file)
@@ -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 (file)
index 0000000..fb04411
--- /dev/null
@@ -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 (file)
index 58d517f..0000000
+++ /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
index 477237756b0449105eada1726835ccfea6f3c9bb..b451202b255209cee0672c9ffd72e26c982ef5b7 100644 (file)
@@ -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
index ec88088cf7459e8bf35a99b718bde76308de7e9c..ac77aab7134769f4b525c0df9ca4f9304bd9dc1a 100644 (file)
@@ -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
index 4a133498e974ddb10a59beea9db8189109ce7584..1f4a093702053b1a82e5adc66328b5d4939db728 100644 (file)
@@ -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)
index 8b9eb4a2c718598d372c5ca6c288f75ac50f808f..d625530ecb575586986e22735544299733fe498a 100644 (file)
@@ -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")}
index 6a8f1af960ffe34aadb35b287d2c0e6d38c9365e..5981e754567e0c098340a1c77395b4402c87dc1f 100644 (file)
@@ -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
index 9c79310c08ef1ff1a2614bc52acbfe233bf59361..816c11e01f593ff8809811bb0fc461ff56776d9a 100644 (file)
@@ -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"))
index 360ddc22ccf12cdbc6995b465c7c5775fb4ac815..3637dee24eb42fef069b9307bd5ad04a40bf3b87 100644 (file)
@@ -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
index 3890489e36b6b302127a1607320969b3854c03b7..b3c1e3ea24e7a403b4202893e0f96a3330f0b561 100644 (file)
@@ -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",
index 7348dcbee6c5d32a667f7e6a9a1e565627d486d5..ce40fb9e800575063ea7b81db416679a5efc7fa7 100644 (file)
@@ -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 (file)
index 0000000..d3e8bd4
--- /dev/null
@@ -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
index 26d8dbd421a7331367ad73fca00587971ee44923..f6ccd073fec07d8348d33e20f257e4a1eabcee2a 100644 (file)
@@ -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
index cf2215823f4c00bdd9a731999edf43cf2c224c3f..2f812ac77a710d0eb7e42414c7a431a2ffba5724 100644 (file)
@@ -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
index dd14837c3876d8f8d32cd4e431b251b5a65d91c3..3b7f51cebb152f71636f49d878de2d750d8c7b24 100644 (file)
@@ -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 (file)
index 0000000..b5877ca
--- /dev/null
@@ -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 (file)
index 0000000..d54e215
--- /dev/null
@@ -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 (file)
index 0000000..fa97073
--- /dev/null
@@ -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 (file)
index 0000000..8b98266
--- /dev/null
@@ -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 (file)
index 0000000..ac9b466
--- /dev/null
@@ -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
similarity index 56%
rename from lib/pleroma/web/api_spec/schemas/domain_block_request.ex
rename to lib/pleroma/web/api_spec/schemas/api_error.ex
index ee9238361aea1a6acbaf7b5f10a8cb51c17bd380..5815df94c657c27c2e7e4a040f9faba53dc332ff 100644 (file)
@@ -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 (file)
index 8a83abe..0000000
+++ /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 (file)
index f290fb0..0000000
+++ /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 (file)
index 0000000..f3bfb74
--- /dev/null
@@ -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 (file)
index 5531b20..0000000
+++ /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 (file)
index 01582a6..0000000
+++ /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 (file)
index d895aca..0000000
+++ /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 (file)
index 0000000..26f35e6
--- /dev/null
@@ -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 (file)
index 0000000..3b5f647
--- /dev/null
@@ -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 (file)
index 0000000..0474b55
--- /dev/null
@@ -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 (file)
index 0000000..aef0588
--- /dev/null
@@ -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 (file)
index 0000000..8c81a4d
--- /dev/null
@@ -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
index c1cd15bb2a37ba98e923f50ab50dc7083995fc9c..244cf2be5fdc313374a0e541c91f4cc37e03bbcd 100644 (file)
@@ -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}
index f50a909aade322b95423e5a646af90b2ce8a63ee..d1efe0c36e0d1a19eb9377d700639c2434ef8bb0 100644 (file)
@@ -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
index 7eec5aa096d8af88913e3db9f61e70eed02731ed..945e63e22a7c24be8fcc4142bd2050d4600d69e6 100644 (file)
@@ -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
index 4780081b26868bb3ba08f6b508ed14782fbe3999..eb97ae975e32b3c0ff0886e3c75e526cbbdf32cd 100644 (file)
@@ -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
index c13518030116ee9abe1532e2ed46c8a5013ab7c3..0d9d578fcc2895f9e0690e8f4a28abeef98e4e01 100644 (file)
@@ -4,7 +4,9 @@
 
 defmodule Fallback.RedirectController do
   use Pleroma.Web, :controller
+
   require Logger
+
   alias Pleroma.User
   alias Pleroma.Web.Metadata
 
index 557cde328f4cdd47d646d1b2f21769d34a66e8a4..d0d8bc8eb9c5ef3bb28a18b2025937a8201ba43d 100644 (file)
@@ -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)
index 5a92cebd806880d26d97e2c2e851c7cbbfb1c039..1eedf02d66a823d0cfc62ce60e2dc964b3252679 100644 (file)
@@ -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
index 005c604447e3999cf756ec2dd1b842930d46df5e..408e1147494eaa383c04d994e46f8740edea8ae2 100644 (file)
@@ -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"
index 37b38938241109b36741cee67fb835958b01dcfa..753b3db3ef9c9cfdd93081b038cadcef9290e6ae 100644 (file)
@@ -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))
index 7c9b11bf1753354a73f3ea2f4366b77094829a7c..c446415261978ef0a2dabbdd8d921ff6699ff696 100644 (file)
@@ -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
index 3bfebef8b3c94f6d2464cde5da4630001c183fc1..000ad743f93205afd2ae0fc0ff3f31185a1773de 100644 (file)
@@ -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
index 84de794136fee87c2c8b73818c8b8e5e886db669..c4fa383f222df5743f36df597db1e3ae1f8c1d6f 100644 (file)
@@ -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, []))
index 7b0b937a26ea4f0290c6fd199315064542e1e495..7fd0562c98ceeee9838658063a73ecd66bddbf90 100644 (file)
@@ -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)
index 1ca86f63fb2371aba7d0af409cc48603cae8cf13..25f2269b97687a07c59b4193f59c51bc918b608c 100644 (file)
@@ -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)
index 27b5b1a524c99c25c42e89df9bcc67b384445569..237f8567758e1c19de872b84ac94d38135f6e084 100644 (file)
@@ -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")
index dac4daa7bc0dd0a3b67d9f224bf43434a850578e..bfe856025af0303882afbce96460a84da69fc1d1 100644 (file)
@@ -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
index 58e8a30c29f49763c43607db20420d12daf5b84a..9f9d4574ee28db18bdb197b080e30e3d8bf10738 100644 (file)
@@ -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
index ac8c18f24272892d387406aa30d3d8f24cd87a3d..e7767de4eca50536d26f7f54a310ce54771cfc86 100644 (file)
@@ -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)
 
index 2b6f00952a4c99057c661d6dd334f45691c2f318..e367512201f934437d59e6dcb8a8ba9020acfac8 100644 (file)
@@ -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} <-
index 7fb536b0935e7e9c69590b3724487f66700c45ce..31140527783f6bd90c3f0ae413fe689d302c3483 100644 (file)
@@ -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
index d9f89411881c5a223486f4a50b9b554d2c6943db..af9b66eff17cf1d2f94d0d8f1af7acfc8130f8ac 100644 (file)
@@ -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),
index f5782be130f7845a533c7391bf4afe67a8e5b8bb..9fbaa7bd1681487cf691fc63919bf3b84377a427 100644 (file)
@@ -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
index e1e6bd89b9185b4d19d90bff7e59bd52bf8d685f..899b7887391cdae6ac7c563ed3602098ff1afbb0 100644 (file)
@@ -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"
index c258742dd80eacb710dac953650dbe9d73b0298c..cd49da6ad5e236ddfb9ce072e55328e2f6423d83 100644 (file)
@@ -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])
 
index 397dd10e3462c7744ef6a3d78066149302577db7..9eea2e9eb1b4fd65cec5ceb5787d4317ca00f800 100644 (file)
@@ -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,
index 4647c1f96db01a13928019824a0a61d16319ed1a..d184ea1d025924ce9b874e3506578cdf3af7fad0 100644 (file)
@@ -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
index b3c58005eb170e6a2cbebef154eb63449ce2732f..2d67e19da616f143e88beaa7da5590d558f2a90f 100644 (file)
@@ -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()
index b5850e1ae8aa1a9a1320faf22e876c4cc63001b5..24167f66f76f4f8a614b8dbc6439ee2456692b4e 100644 (file)
@@ -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: []
index 1a09ac62a0245d74b80c2a0d65405355618a5ae5..4657a4383563802f19fef4a25730ac6e121ebf82 100644 (file)
@@ -4,6 +4,7 @@
 
 defmodule Pleroma.Web.MediaProxy.MediaProxyController do
   use Pleroma.Web, :controller
+
   alias Pleroma.ReverseProxy
   alias Pleroma.Web.MediaProxy
 
index 04d823b362a7d807972340330c702f628f9102c9..1ed6ee521a41a77a3eedde98661dc3e5cfba48b4 100644 (file)
@@ -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
index 0121cd661001211b45f509f0bce2344f6f9f0800..685269877f8e2c90246cfa711e7adb827867b673 100644 (file)
@@ -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)
 
index 1023f16d4911cb0fc3c4b8334d1b6cf9cd742300..6f06f1431587388d8c923a3e7ec852314d666b94 100644 (file)
@@ -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
index 60405fbff2c7aac9376df0a59b5283f21da87481..be7477867b3592766fa10170331922c90c50c6a9 100644 (file)
@@ -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
@@ -17,6 +18,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)
 
index 03e95e0202fa7454705c24f2a3ec6c4ae7ec91cd..e01825b48b28b9374f8d949c749a5f7bb3621b57 100644 (file)
@@ -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
index d9c1c863622e80b41e764e81e6e53807b0dc9a0a..d4e0d8b7cc00359fb2fd75a1ecfadf23d9042e18 100644 (file)
@@ -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))
index fe1b97a208c9738b81bbc75d7e2c07cf91b5169a..2c1874051ed833aac8336be4a58351ae30b5f41b 100644 (file)
@@ -26,6 +26,12 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIController do
     when action in [:conversation, :conversation_statuses]
   )
 
+  plug(
+    OAuthScopesPlug,
+    %{scopes: ["read:statuses"], fallback: :proceed_unauthenticated}
+    when action == :emoji_reactions_by
+  )
+
   plug(
     OAuthScopesPlug,
     %{scopes: ["write:statuses"]}
@@ -34,12 +40,14 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIController do
 
   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)
 
index 4463ec47741762244640da303617f26ed951bdc3..22da6c0ad6cf820a691e052635ac1a74059d95bb 100644 (file)
@@ -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 =
index 153802a432f42ff702b0684e87bce09ceab27d93..becce3098f3838d39ac7c641b25187159fc7389a 100644 (file)
@@ -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
index d5d5ce08f817938c9508e05af72b7e463a499178..fd2aee17569c241ca4d715349f27e0a65a8ad40c 100644 (file)
@@ -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(
index 7a1ba6936e3758fe1ebfc259b0f97199a361adc9..cf1d9c74c00ab9f60f1d10e73bb129e99c114430 100644 (file)
@@ -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
 
index 31adc28174bf1c7af9855c82f2907290d4f681c0..c2de26b0ba9da42e15cb95c3575beff5ceccc63a 100644 (file)
@@ -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
 
index bf48ce26c8380202b9208095aed92f9d072abf7a..08e42a7e5398737cd14f1f8b9e7c80b1f8c60834 100644 (file)
@@ -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
diff --git a/mix.exs b/mix.exs
index b76aef18075f666976edb0c04d10c3bcf8678616..beb05aab9895d4ea016d2a354a91e85af17d2f33 100644 (file)
--- a/mix.exs
+++ b/mix.exs
@@ -189,7 +189,9 @@ defmodule Pleroma.Mixfile do
        ref: "e0f16822d578866e186a0974d65ad58cddc1e2ab"},
       {:mox, "~> 0.5", only: :test},
       {:restarter, path: "./restarter"},
-      {:open_api_spex, "~> 3.6"}
+      {:open_api_spex,
+       git: "https://git.pleroma.social/pleroma/elixir-libraries/open_api_spex.git",
+       ref: "b862ebd78de0df95875cf46feb6e9607130dc2a8"}
     ] ++ oauth_deps()
   end
 
index 2b9c545486cbf2d8a60ec9512bf72865292ae629..ee9d93bfbb954767e57b8d5a409ce8a5f0507aad 100644 (file)
--- a/mix.lock
+++ b/mix.lock
@@ -74,7 +74,7 @@
   "nimble_parsec": {:hex, :nimble_parsec, "0.5.3", "def21c10a9ed70ce22754fdeea0810dafd53c2db3219a0cd54cf5526377af1c6", [:mix], [], "hexpm", "589b5af56f4afca65217a1f3eb3fee7e79b09c40c742fddc1c312b3ac0b3399f"},
   "nodex": {:git, "https://git.pleroma.social/pleroma/nodex", "cb6730f943cfc6aad674c92161be23a8411f15d1", [ref: "cb6730f943cfc6aad674c92161be23a8411f15d1"]},
   "oban": {:hex, :oban, "1.2.0", "7cca94d341be43d220571e28f69131c4afc21095b25257397f50973d3fc59b07", [:mix], [{:ecto_sql, "~> 3.1", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.14", [hex: :postgrex, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ba5f8b3f7d76967b3e23cf8014f6a13e4ccb33431e4808f036709a7f822362ee"},
-  "open_api_spex": {:hex, :open_api_spex, "3.6.0", "64205aba9f2607f71b08fd43e3351b9c5e9898ec5ef49fc0ae35890da502ade9", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, "~> 3.1", [hex: :poison, repo: "hexpm", optional: true]}], "hexpm", "126ba3473966277132079cb1d5bf1e3df9e36fe2acd00166e75fd125cecb59c5"},
+  "open_api_spex": {:git, "https://git.pleroma.social/pleroma/elixir-libraries/open_api_spex.git", "b862ebd78de0df95875cf46feb6e9607130dc2a8", [ref: "b862ebd78de0df95875cf46feb6e9607130dc2a8"]},
   "parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm", "17ef63abde837ad30680ea7f857dd9e7ced9476cdd7b0394432af4bfc241b960"},
   "pbkdf2_elixir": {:hex, :pbkdf2_elixir, "0.12.4", "8dd29ed783f2e12195d7e0a4640effc0a7c37e6537da491f1db01839eee6d053", [:mix], [], "hexpm", "595d09db74cb093b1903381c9de423276a931a2480a46a1a5dc7f932a2a6375b"},
   "phoenix": {:hex, :phoenix, "1.4.13", "67271ad69b51f3719354604f4a3f968f83aa61c19199343656c9caee057ff3b8", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 1.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.8.1 or ~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 1.0 or ~> 2.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ab765a0feddb81fc62e2116c827b5f068df85159c162bee760745276ad7ddc1b"},
index 7f3559b837cc921d9fc4633081b0c6cd651f851e..689fe757f5d00f2d712814dcca9a53a39cfbccb9 100644 (file)
@@ -20,7 +20,7 @@ defmodule Pleroma.Plugs.EnsureAuthenticatedPlugTest do
       conn = assign(conn, :user, %User{})
       ret_conn = EnsureAuthenticatedPlug.call(conn, %{})
 
-      assert ret_conn == conn
+      refute ret_conn.halted
     end
   end
 
@@ -34,20 +34,22 @@ defmodule Pleroma.Plugs.EnsureAuthenticatedPlugTest do
 
     test "it continues if a user is assigned", %{conn: conn, true_fn: true_fn, false_fn: false_fn} do
       conn = assign(conn, :user, %User{})
-      assert EnsureAuthenticatedPlug.call(conn, if_func: true_fn) == conn
-      assert EnsureAuthenticatedPlug.call(conn, if_func: false_fn) == conn
-      assert EnsureAuthenticatedPlug.call(conn, unless_func: true_fn) == conn
-      assert EnsureAuthenticatedPlug.call(conn, unless_func: false_fn) == conn
+      refute EnsureAuthenticatedPlug.call(conn, if_func: true_fn).halted
+      refute EnsureAuthenticatedPlug.call(conn, if_func: false_fn).halted
+      refute EnsureAuthenticatedPlug.call(conn, unless_func: true_fn).halted
+      refute EnsureAuthenticatedPlug.call(conn, unless_func: false_fn).halted
     end
 
     test "it continues if a user is NOT assigned but :if_func evaluates to `false`",
          %{conn: conn, false_fn: false_fn} do
-      assert EnsureAuthenticatedPlug.call(conn, if_func: false_fn) == conn
+      ret_conn = EnsureAuthenticatedPlug.call(conn, if_func: false_fn)
+      refute ret_conn.halted
     end
 
     test "it continues if a user is NOT assigned but :unless_func evaluates to `true`",
          %{conn: conn, true_fn: true_fn} do
-      assert EnsureAuthenticatedPlug.call(conn, unless_func: true_fn) == conn
+      ret_conn = EnsureAuthenticatedPlug.call(conn, unless_func: true_fn)
+      refute ret_conn.halted
     end
 
     test "it halts if a user is NOT assigned and :if_func evaluates to `true`",
index 411252274c5f0f0a008d8604bcd9257cbde8ced8..fc2934369b573101a209c0cc7f868ed92656ddd9 100644 (file)
@@ -29,7 +29,7 @@ defmodule Pleroma.Plugs.EnsurePublicOrAuthenticatedPlugTest do
       conn
       |> EnsurePublicOrAuthenticatedPlug.call(%{})
 
-    assert ret_conn == conn
+    refute ret_conn.halted
   end
 
   test "it continues if a user is assigned, even if not public", %{conn: conn} do
@@ -43,6 +43,6 @@ defmodule Pleroma.Plugs.EnsurePublicOrAuthenticatedPlugTest do
       conn
       |> EnsurePublicOrAuthenticatedPlug.call(%{})
 
-    assert ret_conn == conn
+    refute ret_conn.halted
   end
 end
index edbc942273ef1c54a4d235141deca37ee37b7c37..884de7b4d5d37319d42c9d25cbcfd30541c1e7af 100644 (file)
@@ -5,17 +5,12 @@
 defmodule Pleroma.Plugs.OAuthScopesPlugTest do
   use Pleroma.Web.ConnCase, async: true
 
-  alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug
   alias Pleroma.Plugs.OAuthScopesPlug
   alias Pleroma.Repo
 
   import Mock
   import Pleroma.Factory
 
-  setup_with_mocks([{EnsurePublicOrAuthenticatedPlug, [], [call: fn conn, _ -> conn end]}]) do
-    :ok
-  end
-
   test "is not performed if marked as skipped", %{conn: conn} do
     with_mock OAuthScopesPlug, [:passthrough], perform: &passthrough([&1, &2]) do
       conn =
@@ -60,7 +55,7 @@ defmodule Pleroma.Plugs.OAuthScopesPlugTest do
 
   describe "with `fallback: :proceed_unauthenticated` option, " do
     test "if `token.scopes` doesn't fulfill specified conditions, " <>
-           "clears :user and :token assigns and calls EnsurePublicOrAuthenticatedPlug",
+           "clears :user and :token assigns",
          %{conn: conn} do
       user = insert(:user)
       token1 = insert(:oauth_token, scopes: ["read", "write"], user: user)
@@ -79,35 +74,6 @@ defmodule Pleroma.Plugs.OAuthScopesPlugTest do
         refute ret_conn.halted
         refute ret_conn.assigns[:user]
         refute ret_conn.assigns[:token]
-
-        assert called(EnsurePublicOrAuthenticatedPlug.call(ret_conn, :_))
-      end
-    end
-
-    test "with :skip_instance_privacy_check option, " <>
-           "if `token.scopes` doesn't fulfill specified conditions, " <>
-           "clears :user and :token assigns and does NOT call EnsurePublicOrAuthenticatedPlug",
-         %{conn: conn} do
-      user = insert(:user)
-      token1 = insert(:oauth_token, scopes: ["read:statuses", "write"], user: user)
-
-      for token <- [token1, nil], op <- [:|, :&] do
-        ret_conn =
-          conn
-          |> assign(:user, user)
-          |> assign(:token, token)
-          |> OAuthScopesPlug.call(%{
-            scopes: ["read"],
-            op: op,
-            fallback: :proceed_unauthenticated,
-            skip_instance_privacy_check: true
-          })
-
-        refute ret_conn.halted
-        refute ret_conn.assigns[:user]
-        refute ret_conn.assigns[:token]
-
-        refute called(EnsurePublicOrAuthenticatedPlug.call(ret_conn, :_))
       end
     end
   end
similarity index 86%
rename from test/stat_test.exs
rename to test/stats_test.exs
index bccc1c8d07a435bb5d2df6c07d4507a2deb80f5c..c1aeb2c7f525871c569ab6aa8f59b93233c33102 100644 (file)
@@ -2,11 +2,21 @@
 # Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
 # SPDX-License-Identifier: AGPL-3.0-only
 
-defmodule Pleroma.StateTest do
+defmodule Pleroma.StatsTest do
   use Pleroma.DataCase
   import Pleroma.Factory
   alias Pleroma.Web.CommonAPI
 
+  describe "user count" do
+    test "it ignores internal users" do
+      _user = insert(:user, local: true)
+      _internal = insert(:user, local: true, nickname: nil)
+      _internal = Pleroma.Web.ActivityPub.Relay.get_actor()
+
+      assert match?(%{stats: %{user_count: 1}}, Pleroma.Stats.calculate_stat_data())
+    end
+  end
+
   describe "status visibility count" do
     test "on new status" do
       user = insert(:user)
diff --git a/test/support/api_spec_helpers.ex b/test/support/api_spec_helpers.ex
new file mode 100644 (file)
index 0000000..80c69c7
--- /dev/null
@@ -0,0 +1,57 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Tests.ApiSpecHelpers do
+  @moduledoc """
+  OpenAPI spec test helpers
+  """
+
+  import ExUnit.Assertions
+
+  alias OpenApiSpex.Cast.Error
+  alias OpenApiSpex.Reference
+  alias OpenApiSpex.Schema
+
+  def assert_schema(value, schema) do
+    api_spec = Pleroma.Web.ApiSpec.spec()
+
+    case OpenApiSpex.cast_value(value, schema, api_spec) do
+      {:ok, data} ->
+        data
+
+      {:error, errors} ->
+        errors =
+          Enum.map(errors, fn error ->
+            message = Error.message(error)
+            path = Error.path_to_string(error)
+            "#{message} at #{path}"
+          end)
+
+        flunk(
+          "Value does not conform to schema #{schema.title}: #{Enum.join(errors, "\n")}\n#{
+            inspect(value)
+          }"
+        )
+    end
+  end
+
+  def resolve_schema(%Schema{} = schema), do: schema
+
+  def resolve_schema(%Reference{} = ref) do
+    schemas = Pleroma.Web.ApiSpec.spec().components.schemas
+    Reference.resolve_schema(ref, schemas)
+  end
+
+  def api_operations do
+    paths = Pleroma.Web.ApiSpec.spec().paths
+
+    Enum.flat_map(paths, fn {_, path_item} ->
+      path_item
+      |> Map.take([:delete, :get, :head, :options, :patch, :post, :put, :trace])
+      |> Map.values()
+      |> Enum.reject(&is_nil/1)
+      |> Enum.uniq()
+    end)
+  end
+end
index 06487420158ebed5412c15aa05800f4823207e6e..fa30a0c41f109f2151e4285d8d26a19267ebe477 100644 (file)
@@ -51,6 +51,60 @@ defmodule Pleroma.Web.ConnCase do
         %{user: user, token: token, conn: conn}
       end
 
+      defp request_content_type(%{conn: conn}) do
+        conn = put_req_header(conn, "content-type", "multipart/form-data")
+        [conn: conn]
+      end
+
+      defp json_response_and_validate_schema(
+             %{
+               private: %{
+                 open_api_spex: %{operation_id: op_id, operation_lookup: lookup, spec: spec}
+               }
+             } = conn,
+             status
+           ) do
+        content_type =
+          conn
+          |> Plug.Conn.get_resp_header("content-type")
+          |> List.first()
+          |> String.split(";")
+          |> List.first()
+
+        status = Plug.Conn.Status.code(status)
+
+        unless lookup[op_id].responses[status] do
+          err = "Response schema not found for #{conn.status} #{conn.method} #{conn.request_path}"
+          flunk(err)
+        end
+
+        schema = lookup[op_id].responses[status].content[content_type].schema
+        json = json_response(conn, status)
+
+        case OpenApiSpex.cast_value(json, schema, spec) do
+          {:ok, _data} ->
+            json
+
+          {:error, errors} ->
+            errors =
+              Enum.map(errors, fn error ->
+                message = OpenApiSpex.Cast.Error.message(error)
+                path = OpenApiSpex.Cast.Error.path_to_string(error)
+                "#{message} at #{path}"
+              end)
+
+            flunk(
+              "Response does not conform to schema of #{op_id} operation: #{
+                Enum.join(errors, "\n")
+              }\n#{inspect(json)}"
+            )
+        end
+      end
+
+      defp json_response_and_validate_schema(conn, _status) do
+        flunk("Response schema not found for #{conn.method} #{conn.request_path} #{conn.status}")
+      end
+
       defp ensure_federating_or_authenticated(conn, url, user) do
         initial_setting = Config.get([:instance, :federating])
         on_exit(fn -> Config.put([:instance, :federating], initial_setting) end)
index fbacb399335cd5bc56e4e198504175a98f8776b5..eca526604af7e970c204979180d9dc013c21054c 100644 (file)
@@ -766,7 +766,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
   end
 
   describe "POST /users/:nickname/outbox" do
-    test "it rejects posts from other users / unauuthenticated users", %{conn: conn} do
+    test "it rejects posts from other users / unauthenticated users", %{conn: conn} do
       data = File.read!("test/fixtures/activitypub-client-post-activity.json") |> Poison.decode!()
       user = insert(:user)
       other_user = insert(:user)
index 6410df49bb4d78a5e16e3b2ff217cc5014c0a33d..edd7dfb22d231bb6e12316e94278a899dbadfec2 100644 (file)
@@ -994,72 +994,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
     end
   end
 
-  describe "like an object" do
-    test_with_mock "sends an activity to federation", Federator, [:passthrough], [] do
-      Config.put([:instance, :federating], true)
-      note_activity = insert(:note_activity)
-      assert object_activity = Object.normalize(note_activity)
-
-      user = insert(:user)
-
-      {:ok, like_activity, _object} = ActivityPub.like(user, object_activity)
-      assert called(Federator.publish(like_activity))
-    end
-
-    test "returns exist activity if object already liked" do
-      note_activity = insert(:note_activity)
-      assert object_activity = Object.normalize(note_activity)
-
-      user = insert(:user)
-
-      {:ok, like_activity, _object} = ActivityPub.like(user, object_activity)
-
-      {:ok, like_activity_exist, _object} = ActivityPub.like(user, object_activity)
-      assert like_activity == like_activity_exist
-    end
-
-    test "reverts like activity on error" do
-      note_activity = insert(:note_activity)
-      object = Object.normalize(note_activity)
-      user = insert(:user)
-
-      with_mock(Utils, [:passthrough], maybe_federate: fn _ -> {:error, :reverted} end) do
-        assert {:error, :reverted} = ActivityPub.like(user, object)
-      end
-
-      assert Repo.aggregate(Activity, :count, :id) == 1
-      assert Repo.get(Object, object.id) == object
-    end
-
-    test "adds a like activity to the db" do
-      note_activity = insert(:note_activity)
-      assert object = Object.normalize(note_activity)
-
-      user = insert(:user)
-      user_two = insert(:user)
-
-      {:ok, like_activity, object} = ActivityPub.like(user, object)
-
-      assert like_activity.data["actor"] == user.ap_id
-      assert like_activity.data["type"] == "Like"
-      assert like_activity.data["object"] == object.data["id"]
-      assert like_activity.data["to"] == [User.ap_followers(user), note_activity.data["actor"]]
-      assert like_activity.data["context"] == object.data["context"]
-      assert object.data["like_count"] == 1
-      assert object.data["likes"] == [user.ap_id]
-
-      # Just return the original activity if the user already liked it.
-      {:ok, same_like_activity, object} = ActivityPub.like(user, object)
-
-      assert like_activity == same_like_activity
-      assert object.data["likes"] == [user.ap_id]
-      assert object.data["like_count"] == 1
-
-      {:ok, _like_activity, object} = ActivityPub.like(user_two, object)
-      assert object.data["like_count"] == 2
-    end
-  end
-
   describe "unliking" do
     test_with_mock "sends an activity to federation", Federator, [:passthrough], [] do
       Config.put([:instance, :federating], true)
@@ -1071,7 +1005,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
       {:ok, object} = ActivityPub.unlike(user, object)
       refute called(Federator.publish())
 
-      {:ok, _like_activity, object} = ActivityPub.like(user, object)
+      {:ok, _like_activity} = CommonAPI.favorite(user, note_activity.id)
+      object = Object.get_by_id(object.id)
       assert object.data["like_count"] == 1
 
       {:ok, unlike_activity, _, object} = ActivityPub.unlike(user, object)
@@ -1082,10 +1017,10 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
 
     test "reverts unliking on error" do
       note_activity = insert(:note_activity)
-      object = Object.normalize(note_activity)
       user = insert(:user)
 
-      {:ok, like_activity, object} = ActivityPub.like(user, object)
+      {:ok, like_activity} = CommonAPI.favorite(user, note_activity.id)
+      object = Object.normalize(note_activity)
       assert object.data["like_count"] == 1
 
       with_mock(Utils, [:passthrough], maybe_federate: fn _ -> {:error, :reverted} end) do
@@ -1106,7 +1041,9 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
       {:ok, object} = ActivityPub.unlike(user, object)
       assert object.data["like_count"] == 0
 
-      {:ok, like_activity, object} = ActivityPub.like(user, object)
+      {:ok, like_activity} = CommonAPI.favorite(user, note_activity.id)
+
+      object = Object.get_by_id(object.id)
       assert object.data["like_count"] == 1
 
       {:ok, unlike_activity, _, object} = ActivityPub.unlike(user, object)
@@ -1973,4 +1910,497 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
                ActivityPub.move(old_user, new_user)
     end
   end
+
+  test "doesn't retrieve replies activities with exclude_replies" do
+    user = insert(:user)
+
+    {:ok, activity} = CommonAPI.post(user, %{"status" => "yeah"})
+
+    {:ok, _reply} =
+      CommonAPI.post(user, %{"status" => "yeah", "in_reply_to_status_id" => activity.id})
+
+    [result] = ActivityPub.fetch_public_activities(%{"exclude_replies" => "true"})
+
+    assert result.id == activity.id
+
+    assert length(ActivityPub.fetch_public_activities()) == 2
+  end
+
+  describe "replies filtering with public messages" do
+    setup :public_messages
+
+    test "public timeline", %{users: %{u1: user}} do
+      activities_ids =
+        %{}
+        |> Map.put("type", ["Create", "Announce"])
+        |> Map.put("local_only", false)
+        |> Map.put("blocking_user", user)
+        |> Map.put("muting_user", user)
+        |> Map.put("reply_filtering_user", user)
+        |> ActivityPub.fetch_public_activities()
+        |> Enum.map(& &1.id)
+
+      assert length(activities_ids) == 16
+    end
+
+    test "public timeline with reply_visibility `following`", %{
+      users: %{u1: user},
+      u1: u1,
+      u2: u2,
+      u3: u3,
+      u4: u4,
+      activities: activities
+    } do
+      activities_ids =
+        %{}
+        |> Map.put("type", ["Create", "Announce"])
+        |> Map.put("local_only", false)
+        |> Map.put("blocking_user", user)
+        |> Map.put("muting_user", user)
+        |> Map.put("reply_visibility", "following")
+        |> Map.put("reply_filtering_user", user)
+        |> ActivityPub.fetch_public_activities()
+        |> Enum.map(& &1.id)
+
+      assert length(activities_ids) == 14
+
+      visible_ids =
+        Map.values(u1) ++ Map.values(u2) ++ Map.values(u4) ++ Map.values(activities) ++ [u3[:r1]]
+
+      assert Enum.all?(visible_ids, &(&1 in activities_ids))
+    end
+
+    test "public timeline with reply_visibility `self`", %{
+      users: %{u1: user},
+      u1: u1,
+      u2: u2,
+      u3: u3,
+      u4: u4,
+      activities: activities
+    } do
+      activities_ids =
+        %{}
+        |> Map.put("type", ["Create", "Announce"])
+        |> Map.put("local_only", false)
+        |> Map.put("blocking_user", user)
+        |> Map.put("muting_user", user)
+        |> Map.put("reply_visibility", "self")
+        |> Map.put("reply_filtering_user", user)
+        |> ActivityPub.fetch_public_activities()
+        |> Enum.map(& &1.id)
+
+      assert length(activities_ids) == 10
+      visible_ids = Map.values(u1) ++ [u2[:r1], u3[:r1], u4[:r1]] ++ Map.values(activities)
+      assert Enum.all?(visible_ids, &(&1 in activities_ids))
+    end
+
+    test "home timeline", %{
+      users: %{u1: user},
+      activities: activities,
+      u1: u1,
+      u2: u2,
+      u3: u3,
+      u4: u4
+    } do
+      params =
+        %{}
+        |> Map.put("type", ["Create", "Announce"])
+        |> Map.put("blocking_user", user)
+        |> Map.put("muting_user", user)
+        |> Map.put("user", user)
+        |> Map.put("reply_filtering_user", user)
+
+      activities_ids =
+        ActivityPub.fetch_activities([user.ap_id | User.following(user)], params)
+        |> Enum.map(& &1.id)
+
+      assert length(activities_ids) == 13
+
+      visible_ids =
+        Map.values(u1) ++
+          Map.values(u3) ++
+          [
+            activities[:a1],
+            activities[:a2],
+            activities[:a4],
+            u2[:r1],
+            u2[:r3],
+            u4[:r1],
+            u4[:r2]
+          ]
+
+      assert Enum.all?(visible_ids, &(&1 in activities_ids))
+    end
+
+    test "home timeline with reply_visibility `following`", %{
+      users: %{u1: user},
+      activities: activities,
+      u1: u1,
+      u2: u2,
+      u3: u3,
+      u4: u4
+    } do
+      params =
+        %{}
+        |> Map.put("type", ["Create", "Announce"])
+        |> Map.put("blocking_user", user)
+        |> Map.put("muting_user", user)
+        |> Map.put("user", user)
+        |> Map.put("reply_visibility", "following")
+        |> Map.put("reply_filtering_user", user)
+
+      activities_ids =
+        ActivityPub.fetch_activities([user.ap_id | User.following(user)], params)
+        |> Enum.map(& &1.id)
+
+      assert length(activities_ids) == 11
+
+      visible_ids =
+        Map.values(u1) ++
+          [
+            activities[:a1],
+            activities[:a2],
+            activities[:a4],
+            u2[:r1],
+            u2[:r3],
+            u3[:r1],
+            u4[:r1],
+            u4[:r2]
+          ]
+
+      assert Enum.all?(visible_ids, &(&1 in activities_ids))
+    end
+
+    test "home timeline with reply_visibility `self`", %{
+      users: %{u1: user},
+      activities: activities,
+      u1: u1,
+      u2: u2,
+      u3: u3,
+      u4: u4
+    } do
+      params =
+        %{}
+        |> Map.put("type", ["Create", "Announce"])
+        |> Map.put("blocking_user", user)
+        |> Map.put("muting_user", user)
+        |> Map.put("user", user)
+        |> Map.put("reply_visibility", "self")
+        |> Map.put("reply_filtering_user", user)
+
+      activities_ids =
+        ActivityPub.fetch_activities([user.ap_id | User.following(user)], params)
+        |> Enum.map(& &1.id)
+
+      assert length(activities_ids) == 9
+
+      visible_ids =
+        Map.values(u1) ++
+          [
+            activities[:a1],
+            activities[:a2],
+            activities[:a4],
+            u2[:r1],
+            u3[:r1],
+            u4[:r1]
+          ]
+
+      assert Enum.all?(visible_ids, &(&1 in activities_ids))
+    end
+  end
+
+  describe "replies filtering with private messages" do
+    setup :private_messages
+
+    test "public timeline", %{users: %{u1: user}} do
+      activities_ids =
+        %{}
+        |> Map.put("type", ["Create", "Announce"])
+        |> Map.put("local_only", false)
+        |> Map.put("blocking_user", user)
+        |> Map.put("muting_user", user)
+        |> Map.put("user", user)
+        |> ActivityPub.fetch_public_activities()
+        |> Enum.map(& &1.id)
+
+      assert activities_ids == []
+    end
+
+    test "public timeline with default reply_visibility `following`", %{users: %{u1: user}} do
+      activities_ids =
+        %{}
+        |> Map.put("type", ["Create", "Announce"])
+        |> Map.put("local_only", false)
+        |> Map.put("blocking_user", user)
+        |> Map.put("muting_user", user)
+        |> Map.put("reply_visibility", "following")
+        |> Map.put("reply_filtering_user", user)
+        |> Map.put("user", user)
+        |> ActivityPub.fetch_public_activities()
+        |> Enum.map(& &1.id)
+
+      assert activities_ids == []
+    end
+
+    test "public timeline with default reply_visibility `self`", %{users: %{u1: user}} do
+      activities_ids =
+        %{}
+        |> Map.put("type", ["Create", "Announce"])
+        |> Map.put("local_only", false)
+        |> Map.put("blocking_user", user)
+        |> Map.put("muting_user", user)
+        |> Map.put("reply_visibility", "self")
+        |> Map.put("reply_filtering_user", user)
+        |> Map.put("user", user)
+        |> ActivityPub.fetch_public_activities()
+        |> Enum.map(& &1.id)
+
+      assert activities_ids == []
+    end
+
+    test "home timeline", %{users: %{u1: user}} do
+      params =
+        %{}
+        |> Map.put("type", ["Create", "Announce"])
+        |> Map.put("blocking_user", user)
+        |> Map.put("muting_user", user)
+        |> Map.put("user", user)
+
+      activities_ids =
+        ActivityPub.fetch_activities([user.ap_id | User.following(user)], params)
+        |> Enum.map(& &1.id)
+
+      assert length(activities_ids) == 12
+    end
+
+    test "home timeline with default reply_visibility `following`", %{users: %{u1: user}} do
+      params =
+        %{}
+        |> Map.put("type", ["Create", "Announce"])
+        |> Map.put("blocking_user", user)
+        |> Map.put("muting_user", user)
+        |> Map.put("user", user)
+        |> Map.put("reply_visibility", "following")
+        |> Map.put("reply_filtering_user", user)
+
+      activities_ids =
+        ActivityPub.fetch_activities([user.ap_id | User.following(user)], params)
+        |> Enum.map(& &1.id)
+
+      assert length(activities_ids) == 12
+    end
+
+    test "home timeline with default reply_visibility `self`", %{
+      users: %{u1: user},
+      activities: activities,
+      u1: u1,
+      u2: u2,
+      u3: u3,
+      u4: u4
+    } do
+      params =
+        %{}
+        |> Map.put("type", ["Create", "Announce"])
+        |> Map.put("blocking_user", user)
+        |> Map.put("muting_user", user)
+        |> Map.put("user", user)
+        |> Map.put("reply_visibility", "self")
+        |> Map.put("reply_filtering_user", user)
+
+      activities_ids =
+        ActivityPub.fetch_activities([user.ap_id | User.following(user)], params)
+        |> Enum.map(& &1.id)
+
+      assert length(activities_ids) == 10
+
+      visible_ids =
+        Map.values(u1) ++ Map.values(u4) ++ [u2[:r1], u3[:r1]] ++ Map.values(activities)
+
+      assert Enum.all?(visible_ids, &(&1 in activities_ids))
+    end
+  end
+
+  defp public_messages(_) do
+    [u1, u2, u3, u4] = insert_list(4, :user)
+    {:ok, u1} = User.follow(u1, u2)
+    {:ok, u2} = User.follow(u2, u1)
+    {:ok, u1} = User.follow(u1, u4)
+    {:ok, u4} = User.follow(u4, u1)
+
+    {:ok, u2} = User.follow(u2, u3)
+    {:ok, u3} = User.follow(u3, u2)
+
+    {:ok, a1} = CommonAPI.post(u1, %{"status" => "Status"})
+
+    {:ok, r1_1} =
+      CommonAPI.post(u2, %{
+        "status" => "@#{u1.nickname} reply from u2 to u1",
+        "in_reply_to_status_id" => a1.id
+      })
+
+    {:ok, r1_2} =
+      CommonAPI.post(u3, %{
+        "status" => "@#{u1.nickname} reply from u3 to u1",
+        "in_reply_to_status_id" => a1.id
+      })
+
+    {:ok, r1_3} =
+      CommonAPI.post(u4, %{
+        "status" => "@#{u1.nickname} reply from u4 to u1",
+        "in_reply_to_status_id" => a1.id
+      })
+
+    {:ok, a2} = CommonAPI.post(u2, %{"status" => "Status"})
+
+    {:ok, r2_1} =
+      CommonAPI.post(u1, %{
+        "status" => "@#{u2.nickname} reply from u1 to u2",
+        "in_reply_to_status_id" => a2.id
+      })
+
+    {:ok, r2_2} =
+      CommonAPI.post(u3, %{
+        "status" => "@#{u2.nickname} reply from u3 to u2",
+        "in_reply_to_status_id" => a2.id
+      })
+
+    {:ok, r2_3} =
+      CommonAPI.post(u4, %{
+        "status" => "@#{u2.nickname} reply from u4 to u2",
+        "in_reply_to_status_id" => a2.id
+      })
+
+    {:ok, a3} = CommonAPI.post(u3, %{"status" => "Status"})
+
+    {:ok, r3_1} =
+      CommonAPI.post(u1, %{
+        "status" => "@#{u3.nickname} reply from u1 to u3",
+        "in_reply_to_status_id" => a3.id
+      })
+
+    {:ok, r3_2} =
+      CommonAPI.post(u2, %{
+        "status" => "@#{u3.nickname} reply from u2 to u3",
+        "in_reply_to_status_id" => a3.id
+      })
+
+    {:ok, r3_3} =
+      CommonAPI.post(u4, %{
+        "status" => "@#{u3.nickname} reply from u4 to u3",
+        "in_reply_to_status_id" => a3.id
+      })
+
+    {:ok, a4} = CommonAPI.post(u4, %{"status" => "Status"})
+
+    {:ok, r4_1} =
+      CommonAPI.post(u1, %{
+        "status" => "@#{u4.nickname} reply from u1 to u4",
+        "in_reply_to_status_id" => a4.id
+      })
+
+    {:ok, r4_2} =
+      CommonAPI.post(u2, %{
+        "status" => "@#{u4.nickname} reply from u2 to u4",
+        "in_reply_to_status_id" => a4.id
+      })
+
+    {:ok, r4_3} =
+      CommonAPI.post(u3, %{
+        "status" => "@#{u4.nickname} reply from u3 to u4",
+        "in_reply_to_status_id" => a4.id
+      })
+
+    {:ok,
+     users: %{u1: u1, u2: u2, u3: u3, u4: u4},
+     activities: %{a1: a1.id, a2: a2.id, a3: a3.id, a4: a4.id},
+     u1: %{r1: r1_1.id, r2: r1_2.id, r3: r1_3.id},
+     u2: %{r1: r2_1.id, r2: r2_2.id, r3: r2_3.id},
+     u3: %{r1: r3_1.id, r2: r3_2.id, r3: r3_3.id},
+     u4: %{r1: r4_1.id, r2: r4_2.id, r3: r4_3.id}}
+  end
+
+  defp private_messages(_) do
+    [u1, u2, u3, u4] = insert_list(4, :user)
+    {:ok, u1} = User.follow(u1, u2)
+    {:ok, u2} = User.follow(u2, u1)
+    {:ok, u1} = User.follow(u1, u3)
+    {:ok, u3} = User.follow(u3, u1)
+    {:ok, u1} = User.follow(u1, u4)
+    {:ok, u4} = User.follow(u4, u1)
+
+    {:ok, u2} = User.follow(u2, u3)
+    {:ok, u3} = User.follow(u3, u2)
+
+    {:ok, a1} = CommonAPI.post(u1, %{"status" => "Status", "visibility" => "private"})
+
+    {:ok, r1_1} =
+      CommonAPI.post(u2, %{
+        "status" => "@#{u1.nickname} reply from u2 to u1",
+        "in_reply_to_status_id" => a1.id,
+        "visibility" => "private"
+      })
+
+    {:ok, r1_2} =
+      CommonAPI.post(u3, %{
+        "status" => "@#{u1.nickname} reply from u3 to u1",
+        "in_reply_to_status_id" => a1.id,
+        "visibility" => "private"
+      })
+
+    {:ok, r1_3} =
+      CommonAPI.post(u4, %{
+        "status" => "@#{u1.nickname} reply from u4 to u1",
+        "in_reply_to_status_id" => a1.id,
+        "visibility" => "private"
+      })
+
+    {:ok, a2} = CommonAPI.post(u2, %{"status" => "Status", "visibility" => "private"})
+
+    {:ok, r2_1} =
+      CommonAPI.post(u1, %{
+        "status" => "@#{u2.nickname} reply from u1 to u2",
+        "in_reply_to_status_id" => a2.id,
+        "visibility" => "private"
+      })
+
+    {:ok, r2_2} =
+      CommonAPI.post(u3, %{
+        "status" => "@#{u2.nickname} reply from u3 to u2",
+        "in_reply_to_status_id" => a2.id,
+        "visibility" => "private"
+      })
+
+    {:ok, a3} = CommonAPI.post(u3, %{"status" => "Status", "visibility" => "private"})
+
+    {:ok, r3_1} =
+      CommonAPI.post(u1, %{
+        "status" => "@#{u3.nickname} reply from u1 to u3",
+        "in_reply_to_status_id" => a3.id,
+        "visibility" => "private"
+      })
+
+    {:ok, r3_2} =
+      CommonAPI.post(u2, %{
+        "status" => "@#{u3.nickname} reply from u2 to u3",
+        "in_reply_to_status_id" => a3.id,
+        "visibility" => "private"
+      })
+
+    {:ok, a4} = CommonAPI.post(u4, %{"status" => "Status", "visibility" => "private"})
+
+    {:ok, r4_1} =
+      CommonAPI.post(u1, %{
+        "status" => "@#{u4.nickname} reply from u1 to u4",
+        "in_reply_to_status_id" => a4.id,
+        "visibility" => "private"
+      })
+
+    {:ok,
+     users: %{u1: u1, u2: u2, u3: u3, u4: u4},
+     activities: %{a1: a1.id, a2: a2.id, a3: a3.id, a4: a4.id},
+     u1: %{r1: r1_1.id, r2: r1_2.id, r3: r1_3.id},
+     u2: %{r1: r2_1.id, r2: r2_2.id},
+     u3: %{r1: r3_1.id, r2: r3_2.id},
+     u4: %{r1: r4_1.id}}
+  end
 end
index e913a5148a3ac6bcfb730581e5fbbf9a1f514575..b0bfed9178a53cba7a5f034cbe0cc1060d4f487e 100644 (file)
@@ -224,8 +224,7 @@ defmodule Pleroma.Web.ActivityPub.UtilsTest do
 
       object = Object.normalize(activity)
       {:ok, [vote], object} = CommonAPI.vote(other_user, object, [0])
-      vote_object = Object.normalize(vote)
-      {:ok, _activity, _object} = ActivityPub.like(user, vote_object)
+      {:ok, _activity} = CommonAPI.favorite(user, activity.id)
       [fetched_vote] = Utils.get_existing_votes(other_user.ap_id, object)
       assert fetched_vote.id == vote.id
     end
@@ -346,7 +345,7 @@ defmodule Pleroma.Web.ActivityPub.UtilsTest do
 
       user = insert(:user)
       refute Utils.get_existing_like(user.ap_id, object)
-      {:ok, like_activity, _object} = ActivityPub.like(user, object)
+      {:ok, like_activity} = CommonAPI.favorite(user, note_activity.id)
 
       assert ^like_activity = Utils.get_existing_like(user.ap_id, object)
     end
diff --git a/test/web/api_spec/app_operation_test.exs b/test/web/api_spec/app_operation_test.exs
deleted file mode 100644 (file)
index 5b96abb..0000000
+++ /dev/null
@@ -1,45 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
-# SPDX-License-Identifier: AGPL-3.0-only
-
-defmodule Pleroma.Web.ApiSpec.AppOperationTest do
-  use Pleroma.Web.ConnCase, async: true
-
-  alias Pleroma.Web.ApiSpec
-  alias Pleroma.Web.ApiSpec.Schemas.AppCreateRequest
-  alias Pleroma.Web.ApiSpec.Schemas.AppCreateResponse
-
-  import OpenApiSpex.TestAssertions
-  import Pleroma.Factory
-
-  test "AppCreateRequest example matches schema" do
-    api_spec = ApiSpec.spec()
-    schema = AppCreateRequest.schema()
-    assert_schema(schema.example, "AppCreateRequest", api_spec)
-  end
-
-  test "AppCreateResponse example matches schema" do
-    api_spec = ApiSpec.spec()
-    schema = AppCreateResponse.schema()
-    assert_schema(schema.example, "AppCreateResponse", api_spec)
-  end
-
-  test "AppController produces a AppCreateResponse", %{conn: conn} do
-    api_spec = ApiSpec.spec()
-    app_attrs = build(:oauth_app)
-
-    json =
-      conn
-      |> put_req_header("content-type", "application/json")
-      |> post(
-        "/api/v1/apps",
-        Jason.encode!(%{
-          client_name: app_attrs.client_name,
-          redirect_uris: app_attrs.redirect_uris
-        })
-      )
-      |> json_response(200)
-
-    assert_schema(json, "AppCreateResponse", api_spec)
-  end
-end
diff --git a/test/web/api_spec/schema_examples_test.exs b/test/web/api_spec/schema_examples_test.exs
new file mode 100644 (file)
index 0000000..88b6f07
--- /dev/null
@@ -0,0 +1,43 @@
+# 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.SchemaExamplesTest do
+  use ExUnit.Case, async: true
+  import Pleroma.Tests.ApiSpecHelpers
+
+  @content_type "application/json"
+
+  for operation <- api_operations() do
+    describe operation.operationId <> " Request Body" do
+      if operation.requestBody do
+        @media_type operation.requestBody.content[@content_type]
+        @schema resolve_schema(@media_type.schema)
+
+        if @media_type.example do
+          test "request body media type example matches schema" do
+            assert_schema(@media_type.example, @schema)
+          end
+        end
+
+        if @schema.example do
+          test "request body schema example matches schema" do
+            assert_schema(@schema.example, @schema)
+          end
+        end
+      end
+    end
+
+    for {status, response} <- operation.responses do
+      describe "#{operation.operationId} - #{status} Response" do
+        @schema resolve_schema(response.content[@content_type].schema)
+
+        if @schema.example do
+          test "example matches schema" do
+            assert_schema(@schema.example, @schema)
+          end
+        end
+      end
+    end
+  end
+end
diff --git a/test/web/auth/auth_test_controller_test.exs b/test/web/auth/auth_test_controller_test.exs
new file mode 100644 (file)
index 0000000..fed52b7
--- /dev/null
@@ -0,0 +1,242 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Tests.AuthTestControllerTest do
+  use Pleroma.Web.ConnCase
+
+  import Pleroma.Factory
+
+  describe "do_oauth_check" do
+    test "serves with proper OAuth token (fulfilling requested scopes)" do
+      %{conn: good_token_conn, user: user} = oauth_access(["read"])
+
+      assert %{"user_id" => user.id} ==
+               good_token_conn
+               |> get("/test/authenticated_api/do_oauth_check")
+               |> json_response(200)
+
+      # Unintended usage (:api) — use with :authenticated_api instead
+      assert %{"user_id" => user.id} ==
+               good_token_conn
+               |> get("/test/api/do_oauth_check")
+               |> json_response(200)
+    end
+
+    test "fails on no token / missing scope(s)" do
+      %{conn: bad_token_conn} = oauth_access(["irrelevant_scope"])
+
+      bad_token_conn
+      |> get("/test/authenticated_api/do_oauth_check")
+      |> json_response(403)
+
+      bad_token_conn
+      |> assign(:token, nil)
+      |> get("/test/api/do_oauth_check")
+      |> json_response(403)
+    end
+  end
+
+  describe "fallback_oauth_check" do
+    test "serves with proper OAuth token (fulfilling requested scopes)" do
+      %{conn: good_token_conn, user: user} = oauth_access(["read"])
+
+      assert %{"user_id" => user.id} ==
+               good_token_conn
+               |> get("/test/api/fallback_oauth_check")
+               |> json_response(200)
+
+      # Unintended usage (:authenticated_api) — use with :api instead
+      assert %{"user_id" => user.id} ==
+               good_token_conn
+               |> get("/test/authenticated_api/fallback_oauth_check")
+               |> json_response(200)
+    end
+
+    test "for :api on public instance, drops :user and renders on no token / missing scope(s)" do
+      clear_config([:instance, :public], true)
+
+      %{conn: bad_token_conn} = oauth_access(["irrelevant_scope"])
+
+      assert %{"user_id" => nil} ==
+               bad_token_conn
+               |> get("/test/api/fallback_oauth_check")
+               |> json_response(200)
+
+      assert %{"user_id" => nil} ==
+               bad_token_conn
+               |> assign(:token, nil)
+               |> get("/test/api/fallback_oauth_check")
+               |> json_response(200)
+    end
+
+    test "for :api on private instance, fails on no token / missing scope(s)" do
+      clear_config([:instance, :public], false)
+
+      %{conn: bad_token_conn} = oauth_access(["irrelevant_scope"])
+
+      bad_token_conn
+      |> get("/test/api/fallback_oauth_check")
+      |> json_response(403)
+
+      bad_token_conn
+      |> assign(:token, nil)
+      |> get("/test/api/fallback_oauth_check")
+      |> json_response(403)
+    end
+  end
+
+  describe "skip_oauth_check" do
+    test "for :authenticated_api, serves if :user is set (regardless of token / token scopes)" do
+      user = insert(:user)
+
+      assert %{"user_id" => user.id} ==
+               build_conn()
+               |> assign(:user, user)
+               |> get("/test/authenticated_api/skip_oauth_check")
+               |> json_response(200)
+
+      %{conn: bad_token_conn, user: user} = oauth_access(["irrelevant_scope"])
+
+      assert %{"user_id" => user.id} ==
+               bad_token_conn
+               |> get("/test/authenticated_api/skip_oauth_check")
+               |> json_response(200)
+    end
+
+    test "serves via :api on public instance if :user is not set" do
+      clear_config([:instance, :public], true)
+
+      assert %{"user_id" => nil} ==
+               build_conn()
+               |> get("/test/api/skip_oauth_check")
+               |> json_response(200)
+
+      build_conn()
+      |> get("/test/authenticated_api/skip_oauth_check")
+      |> json_response(403)
+    end
+
+    test "fails on private instance if :user is not set" do
+      clear_config([:instance, :public], false)
+
+      build_conn()
+      |> get("/test/api/skip_oauth_check")
+      |> json_response(403)
+
+      build_conn()
+      |> get("/test/authenticated_api/skip_oauth_check")
+      |> json_response(403)
+    end
+  end
+
+  describe "fallback_oauth_skip_publicity_check" do
+    test "serves with proper OAuth token (fulfilling requested scopes)" do
+      %{conn: good_token_conn, user: user} = oauth_access(["read"])
+
+      assert %{"user_id" => user.id} ==
+               good_token_conn
+               |> get("/test/api/fallback_oauth_skip_publicity_check")
+               |> json_response(200)
+
+      # Unintended usage (:authenticated_api)
+      assert %{"user_id" => user.id} ==
+               good_token_conn
+               |> get("/test/authenticated_api/fallback_oauth_skip_publicity_check")
+               |> json_response(200)
+    end
+
+    test "for :api on private / public instance, drops :user and renders on token issue" do
+      %{conn: bad_token_conn} = oauth_access(["irrelevant_scope"])
+
+      for is_public <- [true, false] do
+        clear_config([:instance, :public], is_public)
+
+        assert %{"user_id" => nil} ==
+                 bad_token_conn
+                 |> get("/test/api/fallback_oauth_skip_publicity_check")
+                 |> json_response(200)
+
+        assert %{"user_id" => nil} ==
+                 bad_token_conn
+                 |> assign(:token, nil)
+                 |> get("/test/api/fallback_oauth_skip_publicity_check")
+                 |> json_response(200)
+      end
+    end
+  end
+
+  describe "skip_oauth_skip_publicity_check" do
+    test "for :authenticated_api, serves if :user is set (regardless of token / token scopes)" do
+      user = insert(:user)
+
+      assert %{"user_id" => user.id} ==
+               build_conn()
+               |> assign(:user, user)
+               |> get("/test/authenticated_api/skip_oauth_skip_publicity_check")
+               |> json_response(200)
+
+      %{conn: bad_token_conn, user: user} = oauth_access(["irrelevant_scope"])
+
+      assert %{"user_id" => user.id} ==
+               bad_token_conn
+               |> get("/test/authenticated_api/skip_oauth_skip_publicity_check")
+               |> json_response(200)
+    end
+
+    test "for :api, serves on private and public instances regardless of whether :user is set" do
+      user = insert(:user)
+
+      for is_public <- [true, false] do
+        clear_config([:instance, :public], is_public)
+
+        assert %{"user_id" => nil} ==
+                 build_conn()
+                 |> get("/test/api/skip_oauth_skip_publicity_check")
+                 |> json_response(200)
+
+        assert %{"user_id" => user.id} ==
+                 build_conn()
+                 |> assign(:user, user)
+                 |> get("/test/api/skip_oauth_skip_publicity_check")
+                 |> json_response(200)
+      end
+    end
+  end
+
+  describe "missing_oauth_check_definition" do
+    def test_missing_oauth_check_definition_failure(endpoint, expected_error) do
+      %{conn: conn} = oauth_access(["read", "write", "follow", "push", "admin"])
+
+      assert %{"error" => expected_error} ==
+               conn
+               |> get(endpoint)
+               |> json_response(403)
+    end
+
+    test "fails if served via :authenticated_api" do
+      test_missing_oauth_check_definition_failure(
+        "/test/authenticated_api/missing_oauth_check_definition",
+        "Security violation: OAuth scopes check was neither handled nor explicitly skipped."
+      )
+    end
+
+    test "fails if served via :api and the instance is private" do
+      clear_config([:instance, :public], false)
+
+      test_missing_oauth_check_definition_failure(
+        "/test/api/missing_oauth_check_definition",
+        "This resource requires authentication."
+      )
+    end
+
+    test "succeeds with dropped :user if served via :api on public instance" do
+      %{conn: conn} = oauth_access(["read", "write", "follow", "push", "admin"])
+
+      assert %{"user_id" => nil} ==
+               conn
+               |> get("/test/api/missing_oauth_check_definition")
+               |> json_response(200)
+    end
+  end
+end
diff --git a/test/web/auth/oauth_test_controller_test.exs b/test/web/auth/oauth_test_controller_test.exs
deleted file mode 100644 (file)
index a2f6009..0000000
+++ /dev/null
@@ -1,49 +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.Tests.OAuthTestControllerTest do
-  use Pleroma.Web.ConnCase
-
-  import Pleroma.Factory
-
-  setup %{conn: conn} do
-    user = insert(:user)
-    conn = assign(conn, :user, user)
-    %{conn: conn, user: user}
-  end
-
-  test "missed_oauth", %{conn: conn} do
-    res =
-      conn
-      |> get("/test/authenticated_api/missed_oauth")
-      |> json_response(403)
-
-    assert res ==
-             %{
-               "error" =>
-                 "Security violation: OAuth scopes check was neither handled nor explicitly skipped."
-             }
-  end
-
-  test "skipped_oauth", %{conn: conn} do
-    conn
-    |> assign(:token, nil)
-    |> get("/test/authenticated_api/skipped_oauth")
-    |> json_response(200)
-  end
-
-  test "performed_oauth", %{user: user} do
-    %{conn: good_token_conn} = oauth_access(["read"], user: user)
-
-    good_token_conn
-    |> get("/test/authenticated_api/performed_oauth")
-    |> json_response(200)
-
-    %{conn: bad_token_conn} = oauth_access(["follow"], user: user)
-
-    bad_token_conn
-    |> get("/test/authenticated_api/performed_oauth")
-    |> json_response(403)
-  end
-end
index e130736ecec21cfe1f475892077bb2d4fabad547..1758662b0c69b3caccd8289a33e4a916f8413dde 100644 (file)
@@ -21,6 +21,60 @@ defmodule Pleroma.Web.CommonAPITest do
   setup do: clear_config([:instance, :limit])
   setup do: clear_config([:instance, :max_pinned_statuses])
 
+  test "favoriting race condition" do
+    user = insert(:user)
+    users_serial = insert_list(10, :user)
+    users = insert_list(10, :user)
+
+    {:ok, activity} = CommonAPI.post(user, %{"status" => "."})
+
+    users_serial
+    |> Enum.map(fn user ->
+      CommonAPI.favorite(user, activity.id)
+    end)
+
+    object = Object.get_by_ap_id(activity.data["object"])
+    assert object.data["like_count"] == 10
+
+    users
+    |> Enum.map(fn user ->
+      Task.async(fn ->
+        CommonAPI.favorite(user, activity.id)
+      end)
+    end)
+    |> Enum.map(&Task.await/1)
+
+    object = Object.get_by_ap_id(activity.data["object"])
+    assert object.data["like_count"] == 20
+  end
+
+  test "repeating race condition" do
+    user = insert(:user)
+    users_serial = insert_list(10, :user)
+    users = insert_list(10, :user)
+
+    {:ok, activity} = CommonAPI.post(user, %{"status" => "."})
+
+    users_serial
+    |> Enum.map(fn user ->
+      CommonAPI.repeat(activity.id, user)
+    end)
+
+    object = Object.get_by_ap_id(activity.data["object"])
+    assert object.data["announcement_count"] == 10
+
+    users
+    |> Enum.map(fn user ->
+      Task.async(fn ->
+        CommonAPI.repeat(activity.id, user)
+      end)
+    end)
+    |> Enum.map(&Task.await/1)
+
+    object = Object.get_by_ap_id(activity.data["object"])
+    assert object.data["announcement_count"] == 20
+  end
+
   test "when replying to a conversation / participation, it will set the correct context id even if no explicit reply_to is given" do
     user = insert(:user)
     {:ok, activity} = CommonAPI.post(user, %{"status" => ".", "visibility" => "direct"})
@@ -256,6 +310,16 @@ defmodule Pleroma.Web.CommonAPITest do
       {:ok, %Activity{}, _} = CommonAPI.repeat(activity.id, user)
     end
 
+    test "can't repeat a repeat" do
+      user = insert(:user)
+      other_user = insert(:user)
+      {:ok, activity} = CommonAPI.post(other_user, %{"status" => "cofe"})
+
+      {:ok, %Activity{} = announce, _} = CommonAPI.repeat(activity.id, other_user)
+
+      refute match?({:ok, %Activity{}, _}, CommonAPI.repeat(announce.id, user))
+    end
+
     test "repeating a status privately" do
       user = insert(:user)
       other_user = insert(:user)
@@ -285,8 +349,8 @@ defmodule Pleroma.Web.CommonAPITest do
       other_user = insert(:user)
 
       {:ok, activity} = CommonAPI.post(other_user, %{"status" => "cofe"})
-      {:ok, %Activity{} = activity, object} = CommonAPI.repeat(activity.id, user)
-      {:ok, ^activity, ^object} = CommonAPI.repeat(activity.id, user)
+      {:ok, %Activity{} = announce, object} = CommonAPI.repeat(activity.id, user)
+      {:ok, ^announce, ^object} = CommonAPI.repeat(activity.id, user)
     end
 
     test "favoriting a status twice returns ok, but without the like activity" do
@@ -360,7 +424,9 @@ defmodule Pleroma.Web.CommonAPITest do
 
       user = refresh_record(user)
 
-      assert {:ok, ^activity} = CommonAPI.unpin(activity.id, user)
+      id = activity.id
+
+      assert match?({:ok, %{id: ^id}}, CommonAPI.unpin(activity.id, user))
 
       user = refresh_record(user)
 
index b21445fe9a857defd8378176a6d9a496645ff8c7..18a3b3b8761624f34c61478562166f9724fef9d0 100644 (file)
@@ -335,26 +335,6 @@ defmodule Pleroma.Web.CommonAPI.UtilsTest do
     end
   end
 
-  describe "get_by_id_or_ap_id/1" do
-    test "get activity by id" do
-      activity = insert(:note_activity)
-      %Pleroma.Activity{} = note = Utils.get_by_id_or_ap_id(activity.id)
-      assert note.id == activity.id
-    end
-
-    test "get activity by ap_id" do
-      activity = insert(:note_activity)
-      %Pleroma.Activity{} = note = Utils.get_by_id_or_ap_id(activity.data["object"])
-      assert note.id == activity.id
-    end
-
-    test "get activity by object when type isn't `Create` " do
-      activity = insert(:like_activity)
-      %Pleroma.Activity{} = like = Utils.get_by_id_or_ap_id(activity.id)
-      assert like.data["object"] == activity.data["object"]
-    end
-  end
-
   describe "to_master_date/1" do
     test "removes microseconds from date (NaiveDateTime)" do
       assert Utils.to_masto_date(~N[2015-01-23 23:50:07.123]) == "2015-01-23T23:50:07.000Z"
index 2d256f63c1f3e5b2284cad219a7f0c181da87f03..fdb6d4c5d7f8d0ffa074025481bfc5c476c2534d 100644 (file)
@@ -14,6 +14,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController.UpdateCredentialsTest do
 
   describe "updating credentials" do
     setup do: oauth_access(["write:accounts"])
+    setup :request_content_type
 
     test "sets user settings in a generic way", %{conn: conn} do
       res_conn =
@@ -25,7 +26,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController.UpdateCredentialsTest do
           }
         })
 
-      assert user_data = json_response(res_conn, 200)
+      assert user_data = json_response_and_validate_schema(res_conn, 200)
       assert user_data["pleroma"]["settings_store"] == %{"pleroma_fe" => %{"theme" => "bla"}}
 
       user = Repo.get(User, user_data["id"])
@@ -41,7 +42,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController.UpdateCredentialsTest do
           }
         })
 
-      assert user_data = json_response(res_conn, 200)
+      assert user_data = json_response_and_validate_schema(res_conn, 200)
 
       assert user_data["pleroma"]["settings_store"] ==
                %{
@@ -62,7 +63,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController.UpdateCredentialsTest do
           }
         })
 
-      assert user_data = json_response(res_conn, 200)
+      assert user_data = json_response_and_validate_schema(res_conn, 200)
 
       assert user_data["pleroma"]["settings_store"] ==
                %{
@@ -79,7 +80,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController.UpdateCredentialsTest do
           "note" => "I drink #cofe with @#{user2.nickname}\n\nsuya.."
         })
 
-      assert user_data = json_response(conn, 200)
+      assert user_data = json_response_and_validate_schema(conn, 200)
 
       assert user_data["note"] ==
                ~s(I drink <a class="hashtag" data-tag="cofe" href="http://localhost:4001/tag/cofe">#cofe</a> with <span class="h-card"><a class="u-url mention" data-user="#{
@@ -90,7 +91,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController.UpdateCredentialsTest do
     test "updates the user's locking status", %{conn: conn} do
       conn = patch(conn, "/api/v1/accounts/update_credentials", %{locked: "true"})
 
-      assert user_data = json_response(conn, 200)
+      assert user_data = json_response_and_validate_schema(conn, 200)
       assert user_data["locked"] == true
     end
 
@@ -100,21 +101,21 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController.UpdateCredentialsTest do
       conn = patch(conn, "/api/v1/accounts/update_credentials", %{allow_following_move: "false"})
 
       assert refresh_record(user).allow_following_move == false
-      assert user_data = json_response(conn, 200)
+      assert user_data = json_response_and_validate_schema(conn, 200)
       assert user_data["pleroma"]["allow_following_move"] == false
     end
 
     test "updates the user's default scope", %{conn: conn} do
-      conn = patch(conn, "/api/v1/accounts/update_credentials", %{default_scope: "cofe"})
+      conn = patch(conn, "/api/v1/accounts/update_credentials", %{default_scope: "unlisted"})
 
-      assert user_data = json_response(conn, 200)
-      assert user_data["source"]["privacy"] == "cofe"
+      assert user_data = json_response_and_validate_schema(conn, 200)
+      assert user_data["source"]["privacy"] == "unlisted"
     end
 
     test "updates the user's hide_followers status", %{conn: conn} do
       conn = patch(conn, "/api/v1/accounts/update_credentials", %{hide_followers: "true"})
 
-      assert user_data = json_response(conn, 200)
+      assert user_data = json_response_and_validate_schema(conn, 200)
       assert user_data["pleroma"]["hide_followers"] == true
     end
 
@@ -122,12 +123,12 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController.UpdateCredentialsTest do
       assert %{"source" => %{"pleroma" => %{"discoverable" => true}}} =
                conn
                |> patch("/api/v1/accounts/update_credentials", %{discoverable: "true"})
-               |> json_response(:ok)
+               |> json_response_and_validate_schema(:ok)
 
       assert %{"source" => %{"pleroma" => %{"discoverable" => false}}} =
                conn
                |> patch("/api/v1/accounts/update_credentials", %{discoverable: "false"})
-               |> json_response(:ok)
+               |> json_response_and_validate_schema(:ok)
     end
 
     test "updates the user's hide_followers_count and hide_follows_count", %{conn: conn} do
@@ -137,7 +138,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController.UpdateCredentialsTest do
           hide_follows_count: "true"
         })
 
-      assert user_data = json_response(conn, 200)
+      assert user_data = json_response_and_validate_schema(conn, 200)
       assert user_data["pleroma"]["hide_followers_count"] == true
       assert user_data["pleroma"]["hide_follows_count"] == true
     end
@@ -146,7 +147,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController.UpdateCredentialsTest do
       response =
         conn
         |> patch("/api/v1/accounts/update_credentials", %{skip_thread_containment: "true"})
-        |> json_response(200)
+        |> json_response_and_validate_schema(200)
 
       assert response["pleroma"]["skip_thread_containment"] == true
       assert refresh_record(user).skip_thread_containment
@@ -155,28 +156,28 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController.UpdateCredentialsTest do
     test "updates the user's hide_follows status", %{conn: conn} do
       conn = patch(conn, "/api/v1/accounts/update_credentials", %{hide_follows: "true"})
 
-      assert user_data = json_response(conn, 200)
+      assert user_data = json_response_and_validate_schema(conn, 200)
       assert user_data["pleroma"]["hide_follows"] == true
     end
 
     test "updates the user's hide_favorites status", %{conn: conn} do
       conn = patch(conn, "/api/v1/accounts/update_credentials", %{hide_favorites: "true"})
 
-      assert user_data = json_response(conn, 200)
+      assert user_data = json_response_and_validate_schema(conn, 200)
       assert user_data["pleroma"]["hide_favorites"] == true
     end
 
     test "updates the user's show_role status", %{conn: conn} do
       conn = patch(conn, "/api/v1/accounts/update_credentials", %{show_role: "false"})
 
-      assert user_data = json_response(conn, 200)
+      assert user_data = json_response_and_validate_schema(conn, 200)
       assert user_data["source"]["pleroma"]["show_role"] == false
     end
 
     test "updates the user's no_rich_text status", %{conn: conn} do
       conn = patch(conn, "/api/v1/accounts/update_credentials", %{no_rich_text: "true"})
 
-      assert user_data = json_response(conn, 200)
+      assert user_data = json_response_and_validate_schema(conn, 200)
       assert user_data["source"]["pleroma"]["no_rich_text"] == true
     end
 
@@ -184,7 +185,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController.UpdateCredentialsTest do
       conn =
         patch(conn, "/api/v1/accounts/update_credentials", %{"display_name" => "markorepairs"})
 
-      assert user_data = json_response(conn, 200)
+      assert user_data = json_response_and_validate_schema(conn, 200)
       assert user_data["display_name"] == "markorepairs"
     end
 
@@ -197,7 +198,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController.UpdateCredentialsTest do
 
       conn = patch(conn, "/api/v1/accounts/update_credentials", %{"avatar" => new_avatar})
 
-      assert user_response = json_response(conn, 200)
+      assert user_response = json_response_and_validate_schema(conn, 200)
       assert user_response["avatar"] != User.avatar_url(user)
     end
 
@@ -210,7 +211,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController.UpdateCredentialsTest do
 
       conn = patch(conn, "/api/v1/accounts/update_credentials", %{"header" => new_header})
 
-      assert user_response = json_response(conn, 200)
+      assert user_response = json_response_and_validate_schema(conn, 200)
       assert user_response["header"] != User.banner_url(user)
     end
 
@@ -226,7 +227,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController.UpdateCredentialsTest do
           "pleroma_background_image" => new_header
         })
 
-      assert user_response = json_response(conn, 200)
+      assert user_response = json_response_and_validate_schema(conn, 200)
       assert user_response["pleroma"]["background_image"]
     end
 
@@ -237,14 +238,15 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController.UpdateCredentialsTest do
       for token <- [token1, token2] do
         conn =
           build_conn()
+          |> put_req_header("content-type", "multipart/form-data")
           |> put_req_header("authorization", "Bearer #{token.token}")
           |> patch("/api/v1/accounts/update_credentials", %{})
 
         if token == token1 do
           assert %{"error" => "Insufficient permissions: write:accounts."} ==
-                   json_response(conn, 403)
+                   json_response_and_validate_schema(conn, 403)
         else
-          assert json_response(conn, 200)
+          assert json_response_and_validate_schema(conn, 200)
         end
       end
     end
@@ -259,11 +261,11 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController.UpdateCredentialsTest do
           "display_name" => name
         })
 
-      assert json_response(ret_conn, 200)
+      assert json_response_and_validate_schema(ret_conn, 200)
 
       conn = get(conn, "/api/v1/accounts/#{user.id}")
 
-      assert user_data = json_response(conn, 200)
+      assert user_data = json_response_and_validate_schema(conn, 200)
 
       assert user_data["note"] == note
       assert user_data["display_name"] == name
@@ -279,7 +281,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController.UpdateCredentialsTest do
       account_data =
         conn
         |> patch("/api/v1/accounts/update_credentials", %{"fields_attributes" => fields})
-        |> json_response(200)
+        |> json_response_and_validate_schema(200)
 
       assert account_data["fields"] == [
                %{"name" => "<a href=\"http://google.com\">foo</a>", "value" => "bar"},
@@ -312,7 +314,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController.UpdateCredentialsTest do
         conn
         |> put_req_header("content-type", "application/x-www-form-urlencoded")
         |> patch("/api/v1/accounts/update_credentials", fields)
-        |> json_response(200)
+        |> json_response_and_validate_schema(200)
 
       assert account["fields"] == [
                %{"name" => "foo", "value" => "bar"},
@@ -337,7 +339,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController.UpdateCredentialsTest do
       account =
         conn
         |> patch("/api/v1/accounts/update_credentials", %{"fields_attributes" => fields})
-        |> json_response(200)
+        |> json_response_and_validate_schema(200)
 
       assert account["fields"] == [
                %{"name" => "foo", "value" => ""}
@@ -356,14 +358,14 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController.UpdateCredentialsTest do
       assert %{"error" => "Invalid request"} ==
                conn
                |> patch("/api/v1/accounts/update_credentials", %{"fields_attributes" => fields})
-               |> json_response(403)
+               |> json_response_and_validate_schema(403)
 
       fields = [%{"name" => long_name, "value" => "bar"}]
 
       assert %{"error" => "Invalid request"} ==
                conn
                |> patch("/api/v1/accounts/update_credentials", %{"fields_attributes" => fields})
-               |> json_response(403)
+               |> json_response_and_validate_schema(403)
 
       Pleroma.Config.put([:instance, :max_account_fields], 1)
 
@@ -375,7 +377,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController.UpdateCredentialsTest do
       assert %{"error" => "Invalid request"} ==
                conn
                |> patch("/api/v1/accounts/update_credentials", %{"fields_attributes" => fields})
-               |> json_response(403)
+               |> json_response_and_validate_schema(403)
     end
   end
 end
index 8c428efeee0c0114b89b39e5c0c1955751449b6b..ba70ba66c95998c6965710c46de456ab1b15a210 100644 (file)
@@ -19,43 +19,37 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do
     setup do: clear_config([:instance, :limit_to_local_content])
 
     test "works by id" do
-      user = insert(:user)
+      %User{id: user_id} = insert(:user)
 
-      conn =
-        build_conn()
-        |> get("/api/v1/accounts/#{user.id}")
-
-      assert %{"id" => id} = json_response(conn, 200)
-      assert id == to_string(user.id)
+      assert %{"id" => ^user_id} =
+               build_conn()
+               |> get("/api/v1/accounts/#{user_id}")
+               |> json_response_and_validate_schema(200)
 
-      conn =
-        build_conn()
-        |> get("/api/v1/accounts/-1")
-
-      assert %{"error" => "Can't find user"} = json_response(conn, 404)
+      assert %{"error" => "Can't find user"} =
+               build_conn()
+               |> get("/api/v1/accounts/-1")
+               |> json_response_and_validate_schema(404)
     end
 
     test "works by nickname" do
       user = insert(:user)
 
-      conn =
-        build_conn()
-        |> get("/api/v1/accounts/#{user.nickname}")
-
-      assert %{"id" => id} = json_response(conn, 200)
-      assert id == user.id
+      assert %{"id" => user_id} =
+               build_conn()
+               |> get("/api/v1/accounts/#{user.nickname}")
+               |> json_response_and_validate_schema(200)
     end
 
     test "works by nickname for remote users" do
       Config.put([:instance, :limit_to_local_content], false)
-      user = insert(:user, nickname: "user@example.com", local: false)
 
-      conn =
-        build_conn()
-        |> get("/api/v1/accounts/#{user.nickname}")
+      user = insert(:user, nickname: "user@example.com", local: false)
 
-      assert %{"id" => id} = json_response(conn, 200)
-      assert id == user.id
+      assert %{"id" => user_id} =
+               build_conn()
+               |> get("/api/v1/accounts/#{user.nickname}")
+               |> json_response_and_validate_schema(200)
     end
 
     test "respects limit_to_local_content == :all for remote user nicknames" do
@@ -63,11 +57,9 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do
 
       user = insert(:user, nickname: "user@example.com", local: false)
 
-      conn =
-        build_conn()
-        |> get("/api/v1/accounts/#{user.nickname}")
-
-      assert json_response(conn, 404)
+      assert build_conn()
+             |> get("/api/v1/accounts/#{user.nickname}")
+             |> json_response_and_validate_schema(404)
     end
 
     test "respects limit_to_local_content == :unauthenticated for remote user nicknames" do
@@ -80,7 +72,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do
         build_conn()
         |> get("/api/v1/accounts/#{user.nickname}")
 
-      assert json_response(conn, 404)
+      assert json_response_and_validate_schema(conn, 404)
 
       conn =
         build_conn()
@@ -88,7 +80,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do
         |> assign(:token, insert(:oauth_token, user: reading_user, scopes: ["read:accounts"]))
         |> get("/api/v1/accounts/#{user.nickname}")
 
-      assert %{"id" => id} = json_response(conn, 200)
+      assert %{"id" => id} = json_response_and_validate_schema(conn, 200)
       assert id == user.id
     end
 
@@ -99,21 +91,21 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do
       user_one = insert(:user, %{id: 1212})
       user_two = insert(:user, %{nickname: "#{user_one.id}garbage"})
 
-      resp_one =
+      acc_one =
         conn
         |> get("/api/v1/accounts/#{user_one.id}")
+        |> json_response_and_validate_schema(:ok)
 
-      resp_two =
+      acc_two =
         conn
         |> get("/api/v1/accounts/#{user_two.nickname}")
+        |> json_response_and_validate_schema(:ok)
 
-      resp_three =
+      acc_three =
         conn
         |> get("/api/v1/accounts/#{user_two.id}")
+        |> json_response_and_validate_schema(:ok)
 
-      acc_one = json_response(resp_one, 200)
-      acc_two = json_response(resp_two, 200)
-      acc_three = json_response(resp_three, 200)
       refute acc_one == acc_two
       assert acc_two == acc_three
     end
@@ -121,23 +113,19 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do
     test "returns 404 when user is invisible", %{conn: conn} do
       user = insert(:user, %{invisible: true})
 
-      resp =
-        conn
-        |> get("/api/v1/accounts/#{user.nickname}")
-        |> json_response(404)
-
-      assert %{"error" => "Can't find user"} = resp
+      assert %{"error" => "Can't find user"} =
+               conn
+               |> get("/api/v1/accounts/#{user.nickname}")
+               |> json_response_and_validate_schema(404)
     end
 
     test "returns 404 for internal.fetch actor", %{conn: conn} do
       %User{nickname: "internal.fetch"} = InternalFetchActor.get_actor()
 
-      resp =
-        conn
-        |> get("/api/v1/accounts/internal.fetch")
-        |> json_response(404)
-
-      assert %{"error" => "Can't find user"} = resp
+      assert %{"error" => "Can't find user"} =
+               conn
+               |> get("/api/v1/accounts/internal.fetch")
+               |> json_response_and_validate_schema(404)
     end
   end
 
@@ -155,27 +143,25 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do
     setup do: clear_config([:restrict_unauthenticated, :profiles, :remote], true)
 
     test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do
-      res_conn = get(conn, "/api/v1/accounts/#{local.id}")
-
-      assert json_response(res_conn, :not_found) == %{
-               "error" => "Can't find user"
-             }
-
-      res_conn = get(conn, "/api/v1/accounts/#{remote.id}")
-
-      assert json_response(res_conn, :not_found) == %{
-               "error" => "Can't find user"
-             }
+      assert %{"error" => "Can't find user"} ==
+               conn
+               |> get("/api/v1/accounts/#{local.id}")
+               |> json_response_and_validate_schema(:not_found)
+
+      assert %{"error" => "Can't find user"} ==
+               conn
+               |> get("/api/v1/accounts/#{remote.id}")
+               |> json_response_and_validate_schema(:not_found)
     end
 
     test "if user is authenticated", %{local: local, remote: remote} do
       %{conn: conn} = oauth_access(["read"])
 
       res_conn = get(conn, "/api/v1/accounts/#{local.id}")
-      assert %{"id" => _} = json_response(res_conn, 200)
+      assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200)
 
       res_conn = get(conn, "/api/v1/accounts/#{remote.id}")
-      assert %{"id" => _} = json_response(res_conn, 200)
+      assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200)
     end
   end
 
@@ -187,22 +173,22 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do
     test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do
       res_conn = get(conn, "/api/v1/accounts/#{local.id}")
 
-      assert json_response(res_conn, :not_found) == %{
+      assert json_response_and_validate_schema(res_conn, :not_found) == %{
                "error" => "Can't find user"
              }
 
       res_conn = get(conn, "/api/v1/accounts/#{remote.id}")
-      assert %{"id" => _} = json_response(res_conn, 200)
+      assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200)
     end
 
     test "if user is authenticated", %{local: local, remote: remote} do
       %{conn: conn} = oauth_access(["read"])
 
       res_conn = get(conn, "/api/v1/accounts/#{local.id}")
-      assert %{"id" => _} = json_response(res_conn, 200)
+      assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200)
 
       res_conn = get(conn, "/api/v1/accounts/#{remote.id}")
-      assert %{"id" => _} = json_response(res_conn, 200)
+      assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200)
     end
   end
 
@@ -213,11 +199,11 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do
 
     test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do
       res_conn = get(conn, "/api/v1/accounts/#{local.id}")
-      assert %{"id" => _} = json_response(res_conn, 200)
+      assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200)
 
       res_conn = get(conn, "/api/v1/accounts/#{remote.id}")
 
-      assert json_response(res_conn, :not_found) == %{
+      assert json_response_and_validate_schema(res_conn, :not_found) == %{
                "error" => "Can't find user"
              }
     end
@@ -226,10 +212,10 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do
       %{conn: conn} = oauth_access(["read"])
 
       res_conn = get(conn, "/api/v1/accounts/#{local.id}")
-      assert %{"id" => _} = json_response(res_conn, 200)
+      assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200)
 
       res_conn = get(conn, "/api/v1/accounts/#{remote.id}")
-      assert %{"id" => _} = json_response(res_conn, 200)
+      assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200)
     end
   end
 
@@ -245,27 +231,37 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do
       {:ok, activity} = CommonAPI.post(user_two, %{"status" => "User one sux0rz"})
       {:ok, repeat, _} = CommonAPI.repeat(activity.id, user_three)
 
-      resp = get(conn, "/api/v1/accounts/#{user_two.id}/statuses")
+      assert resp =
+               conn
+               |> get("/api/v1/accounts/#{user_two.id}/statuses")
+               |> json_response_and_validate_schema(200)
 
-      assert [%{"id" => id}] = json_response(resp, 200)
+      assert [%{"id" => id}] = resp
       assert id == activity.id
 
       # Even a blocked user will deliver the full user timeline, there would be
       #   no point in looking at a blocked users timeline otherwise
-      resp = get(conn, "/api/v1/accounts/#{user_two.id}/statuses")
+      assert resp =
+               conn
+               |> get("/api/v1/accounts/#{user_two.id}/statuses")
+               |> json_response_and_validate_schema(200)
 
-      assert [%{"id" => id}] = json_response(resp, 200)
+      assert [%{"id" => id}] = resp
       assert id == activity.id
 
       # Third user's timeline includes the repeat when viewed by unauthenticated user
-      resp = get(build_conn(), "/api/v1/accounts/#{user_three.id}/statuses")
-      assert [%{"id" => id}] = json_response(resp, 200)
+      resp =
+        build_conn()
+        |> get("/api/v1/accounts/#{user_three.id}/statuses")
+        |> json_response_and_validate_schema(200)
+
+      assert [%{"id" => id}] = resp
       assert id == repeat.id
 
       # When viewing a third user's timeline, the blocked users' statuses will NOT be shown
       resp = get(conn, "/api/v1/accounts/#{user_three.id}/statuses")
 
-      assert [] = json_response(resp, 200)
+      assert [] == json_response_and_validate_schema(resp, 200)
     end
 
     test "gets users statuses", %{conn: conn} do
@@ -286,9 +282,13 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do
       {:ok, private_activity} =
         CommonAPI.post(user_one, %{"status" => "private", "visibility" => "private"})
 
-      resp = get(conn, "/api/v1/accounts/#{user_one.id}/statuses")
+      # TODO!!!
+      resp =
+        conn
+        |> get("/api/v1/accounts/#{user_one.id}/statuses")
+        |> json_response_and_validate_schema(200)
 
-      assert [%{"id" => id}] = json_response(resp, 200)
+      assert [%{"id" => id}] = resp
       assert id == to_string(activity.id)
 
       resp =
@@ -296,8 +296,9 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do
         |> assign(:user, user_two)
         |> assign(:token, insert(:oauth_token, user: user_two, scopes: ["read:statuses"]))
         |> get("/api/v1/accounts/#{user_one.id}/statuses")
+        |> json_response_and_validate_schema(200)
 
-      assert [%{"id" => id_one}, %{"id" => id_two}] = json_response(resp, 200)
+      assert [%{"id" => id_one}, %{"id" => id_two}] = resp
       assert id_one == to_string(direct_activity.id)
       assert id_two == to_string(activity.id)
 
@@ -306,8 +307,9 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do
         |> assign(:user, user_three)
         |> assign(:token, insert(:oauth_token, user: user_three, scopes: ["read:statuses"]))
         |> get("/api/v1/accounts/#{user_one.id}/statuses")
+        |> json_response_and_validate_schema(200)
 
-      assert [%{"id" => id_one}, %{"id" => id_two}] = json_response(resp, 200)
+      assert [%{"id" => id_one}, %{"id" => id_two}] = resp
       assert id_one == to_string(private_activity.id)
       assert id_two == to_string(activity.id)
     end
@@ -318,7 +320,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do
 
       conn = get(conn, "/api/v1/accounts/#{user.id}/statuses?pinned=true")
 
-      assert json_response(conn, 200) == []
+      assert json_response_and_validate_schema(conn, 200) == []
     end
 
     test "gets an users media", %{conn: conn} do
@@ -333,56 +335,48 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do
 
       {:ok, %{id: media_id}} = ActivityPub.upload(file, actor: user.ap_id)
 
-      {:ok, image_post} = CommonAPI.post(user, %{"status" => "cofe", "media_ids" => [media_id]})
+      {:ok, %{id: image_post_id}} =
+        CommonAPI.post(user, %{"status" => "cofe", "media_ids" => [media_id]})
 
-      conn = get(conn, "/api/v1/accounts/#{user.id}/statuses", %{"only_media" => "true"})
+      conn = get(conn, "/api/v1/accounts/#{user.id}/statuses?only_media=true")
 
-      assert [%{"id" => id}] = json_response(conn, 200)
-      assert id == to_string(image_post.id)
+      assert [%{"id" => ^image_post_id}] = json_response_and_validate_schema(conn, 200)
 
-      conn = get(build_conn(), "/api/v1/accounts/#{user.id}/statuses", %{"only_media" => "1"})
+      conn = get(build_conn(), "/api/v1/accounts/#{user.id}/statuses?only_media=1")
 
-      assert [%{"id" => id}] = json_response(conn, 200)
-      assert id == to_string(image_post.id)
+      assert [%{"id" => ^image_post_id}] = json_response_and_validate_schema(conn, 200)
     end
 
     test "gets a user's statuses without reblogs", %{user: user, conn: conn} do
-      {:ok, post} = CommonAPI.post(user, %{"status" => "HI!!!"})
-      {:ok, _, _} = CommonAPI.repeat(post.id, user)
-
-      conn = get(conn, "/api/v1/accounts/#{user.id}/statuses", %{"exclude_reblogs" => "true"})
+      {:ok, %{id: post_id}} = CommonAPI.post(user, %{"status" => "HI!!!"})
+      {:ok, _, _} = CommonAPI.repeat(post_id, user)
 
-      assert [%{"id" => id}] = json_response(conn, 200)
-      assert id == to_string(post.id)
+      conn = get(conn, "/api/v1/accounts/#{user.id}/statuses?exclude_reblogs=true")
+      assert [%{"id" => ^post_id}] = json_response_and_validate_schema(conn, 200)
 
-      conn = get(conn, "/api/v1/accounts/#{user.id}/statuses", %{"exclude_reblogs" => "1"})
-
-      assert [%{"id" => id}] = json_response(conn, 200)
-      assert id == to_string(post.id)
+      conn = get(conn, "/api/v1/accounts/#{user.id}/statuses?exclude_reblogs=1")
+      assert [%{"id" => ^post_id}] = json_response_and_validate_schema(conn, 200)
     end
 
     test "filters user's statuses by a hashtag", %{user: user, conn: conn} do
-      {:ok, post} = CommonAPI.post(user, %{"status" => "#hashtag"})
+      {:ok, %{id: post_id}} = CommonAPI.post(user, %{"status" => "#hashtag"})
       {:ok, _post} = CommonAPI.post(user, %{"status" => "hashtag"})
 
-      conn = get(conn, "/api/v1/accounts/#{user.id}/statuses", %{"tagged" => "hashtag"})
-
-      assert [%{"id" => id}] = json_response(conn, 200)
-      assert id == to_string(post.id)
+      conn = get(conn, "/api/v1/accounts/#{user.id}/statuses?tagged=hashtag")
+      assert [%{"id" => ^post_id}] = json_response_and_validate_schema(conn, 200)
     end
 
     test "the user views their own timelines and excludes direct messages", %{
       user: user,
       conn: conn
     } do
-      {:ok, public_activity} = CommonAPI.post(user, %{"status" => ".", "visibility" => "public"})
-      {:ok, _direct_activity} = CommonAPI.post(user, %{"status" => ".", "visibility" => "direct"})
+      {:ok, %{id: public_activity_id}} =
+        CommonAPI.post(user, %{"status" => ".", "visibility" => "public"})
 
-      conn =
-        get(conn, "/api/v1/accounts/#{user.id}/statuses", %{"exclude_visibilities" => ["direct"]})
+      {:ok, _direct_activity} = CommonAPI.post(user, %{"status" => ".", "visibility" => "direct"})
 
-      assert [%{"id" => id}] = json_response(conn, 200)
-      assert id == to_string(public_activity.id)
+      conn = get(conn, "/api/v1/accounts/#{user.id}/statuses?exclude_visibilities[]=direct")
+      assert [%{"id" => ^public_activity_id}] = json_response_and_validate_schema(conn, 200)
     end
   end
 
@@ -402,27 +396,25 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do
     setup do: clear_config([:restrict_unauthenticated, :profiles, :remote], true)
 
     test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do
-      res_conn = get(conn, "/api/v1/accounts/#{local.id}/statuses")
-
-      assert json_response(res_conn, :not_found) == %{
-               "error" => "Can't find user"
-             }
-
-      res_conn = get(conn, "/api/v1/accounts/#{remote.id}/statuses")
-
-      assert json_response(res_conn, :not_found) == %{
-               "error" => "Can't find user"
-             }
+      assert %{"error" => "Can't find user"} ==
+               conn
+               |> get("/api/v1/accounts/#{local.id}/statuses")
+               |> json_response_and_validate_schema(:not_found)
+
+      assert %{"error" => "Can't find user"} ==
+               conn
+               |> get("/api/v1/accounts/#{remote.id}/statuses")
+               |> json_response_and_validate_schema(:not_found)
     end
 
     test "if user is authenticated", %{local: local, remote: remote} do
       %{conn: conn} = oauth_access(["read"])
 
       res_conn = get(conn, "/api/v1/accounts/#{local.id}/statuses")
-      assert length(json_response(res_conn, 200)) == 1
+      assert length(json_response_and_validate_schema(res_conn, 200)) == 1
 
       res_conn = get(conn, "/api/v1/accounts/#{remote.id}/statuses")
-      assert length(json_response(res_conn, 200)) == 1
+      assert length(json_response_and_validate_schema(res_conn, 200)) == 1
     end
   end
 
@@ -433,24 +425,23 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do
     setup do: clear_config([:restrict_unauthenticated, :profiles, :local], true)
 
     test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do
-      res_conn = get(conn, "/api/v1/accounts/#{local.id}/statuses")
-
-      assert json_response(res_conn, :not_found) == %{
-               "error" => "Can't find user"
-             }
+      assert %{"error" => "Can't find user"} ==
+               conn
+               |> get("/api/v1/accounts/#{local.id}/statuses")
+               |> json_response_and_validate_schema(:not_found)
 
       res_conn = get(conn, "/api/v1/accounts/#{remote.id}/statuses")
-      assert length(json_response(res_conn, 200)) == 1
+      assert length(json_response_and_validate_schema(res_conn, 200)) == 1
     end
 
     test "if user is authenticated", %{local: local, remote: remote} do
       %{conn: conn} = oauth_access(["read"])
 
       res_conn = get(conn, "/api/v1/accounts/#{local.id}/statuses")
-      assert length(json_response(res_conn, 200)) == 1
+      assert length(json_response_and_validate_schema(res_conn, 200)) == 1
 
       res_conn = get(conn, "/api/v1/accounts/#{remote.id}/statuses")
-      assert length(json_response(res_conn, 200)) == 1
+      assert length(json_response_and_validate_schema(res_conn, 200)) == 1
     end
   end
 
@@ -462,23 +453,22 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do
 
     test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do
       res_conn = get(conn, "/api/v1/accounts/#{local.id}/statuses")
-      assert length(json_response(res_conn, 200)) == 1
-
-      res_conn = get(conn, "/api/v1/accounts/#{remote.id}/statuses")
+      assert length(json_response_and_validate_schema(res_conn, 200)) == 1
 
-      assert json_response(res_conn, :not_found) == %{
-               "error" => "Can't find user"
-             }
+      assert %{"error" => "Can't find user"} ==
+               conn
+               |> get("/api/v1/accounts/#{remote.id}/statuses")
+               |> json_response_and_validate_schema(:not_found)
     end
 
     test "if user is authenticated", %{local: local, remote: remote} do
       %{conn: conn} = oauth_access(["read"])
 
       res_conn = get(conn, "/api/v1/accounts/#{local.id}/statuses")
-      assert length(json_response(res_conn, 200)) == 1
+      assert length(json_response_and_validate_schema(res_conn, 200)) == 1
 
       res_conn = get(conn, "/api/v1/accounts/#{remote.id}/statuses")
-      assert length(json_response(res_conn, 200)) == 1
+      assert length(json_response_and_validate_schema(res_conn, 200)) == 1
     end
   end
 
@@ -487,12 +477,11 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do
 
     test "getting followers", %{user: user, conn: conn} do
       other_user = insert(:user)
-      {:ok, user} = User.follow(user, other_user)
+      {:ok, %{id: user_id}} = User.follow(user, other_user)
 
       conn = get(conn, "/api/v1/accounts/#{other_user.id}/followers")
 
-      assert [%{"id" => id}] = json_response(conn, 200)
-      assert id == to_string(user.id)
+      assert [%{"id" => ^user_id}] = json_response_and_validate_schema(conn, 200)
     end
 
     test "getting followers, hide_followers", %{user: user, conn: conn} do
@@ -501,7 +490,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do
 
       conn = get(conn, "/api/v1/accounts/#{other_user.id}/followers")
 
-      assert [] == json_response(conn, 200)
+      assert [] == json_response_and_validate_schema(conn, 200)
     end
 
     test "getting followers, hide_followers, same user requesting" do
@@ -515,37 +504,31 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do
         |> assign(:token, insert(:oauth_token, user: other_user, scopes: ["read:accounts"]))
         |> get("/api/v1/accounts/#{other_user.id}/followers")
 
-      refute [] == json_response(conn, 200)
+      refute [] == json_response_and_validate_schema(conn, 200)
     end
 
     test "getting followers, pagination", %{user: user, conn: conn} do
-      follower1 = insert(:user)
-      follower2 = insert(:user)
-      follower3 = insert(:user)
-      {:ok, _} = User.follow(follower1, user)
-      {:ok, _} = User.follow(follower2, user)
-      {:ok, _} = User.follow(follower3, user)
-
-      res_conn = get(conn, "/api/v1/accounts/#{user.id}/followers?since_id=#{follower1.id}")
+      {:ok, %User{id: follower1_id}} = :user |> insert() |> User.follow(user)
+      {:ok, %User{id: follower2_id}} = :user |> insert() |> User.follow(user)
+      {:ok, %User{id: follower3_id}} = :user |> insert() |> User.follow(user)
 
-      assert [%{"id" => id3}, %{"id" => id2}] = json_response(res_conn, 200)
-      assert id3 == follower3.id
-      assert id2 == follower2.id
+      assert [%{"id" => ^follower3_id}, %{"id" => ^follower2_id}] =
+               conn
+               |> get("/api/v1/accounts/#{user.id}/followers?since_id=#{follower1_id}")
+               |> json_response_and_validate_schema(200)
 
-      res_conn = get(conn, "/api/v1/accounts/#{user.id}/followers?max_id=#{follower3.id}")
+      assert [%{"id" => ^follower2_id}, %{"id" => ^follower1_id}] =
+               conn
+               |> get("/api/v1/accounts/#{user.id}/followers?max_id=#{follower3_id}")
+               |> json_response_and_validate_schema(200)
 
-      assert [%{"id" => id2}, %{"id" => id1}] = json_response(res_conn, 200)
-      assert id2 == follower2.id
-      assert id1 == follower1.id
+      res_conn = get(conn, "/api/v1/accounts/#{user.id}/followers?limit=1&max_id=#{follower3_id}")
 
-      res_conn = get(conn, "/api/v1/accounts/#{user.id}/followers?limit=1&max_id=#{follower3.id}")
-
-      assert [%{"id" => id2}] = json_response(res_conn, 200)
-      assert id2 == follower2.id
+      assert [%{"id" => ^follower2_id}] = json_response_and_validate_schema(res_conn, 200)
 
       assert [link_header] = get_resp_header(res_conn, "link")
-      assert link_header =~ ~r/min_id=#{follower2.id}/
-      assert link_header =~ ~r/max_id=#{follower2.id}/
+      assert link_header =~ ~r/min_id=#{follower2_id}/
+      assert link_header =~ ~r/max_id=#{follower2_id}/
     end
   end
 
@@ -558,7 +541,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do
 
       conn = get(conn, "/api/v1/accounts/#{user.id}/following")
 
-      assert [%{"id" => id}] = json_response(conn, 200)
+      assert [%{"id" => id}] = json_response_and_validate_schema(conn, 200)
       assert id == to_string(other_user.id)
     end
 
@@ -573,7 +556,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do
         |> assign(:token, insert(:oauth_token, user: other_user, scopes: ["read:accounts"]))
         |> get("/api/v1/accounts/#{user.id}/following")
 
-      assert [] == json_response(conn, 200)
+      assert [] == json_response_and_validate_schema(conn, 200)
     end
 
     test "getting following, hide_follows, same user requesting" do
@@ -587,7 +570,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do
         |> assign(:token, insert(:oauth_token, user: user, scopes: ["read:accounts"]))
         |> get("/api/v1/accounts/#{user.id}/following")
 
-      refute [] == json_response(conn, 200)
+      refute [] == json_response_and_validate_schema(conn, 200)
     end
 
     test "getting following, pagination", %{user: user, conn: conn} do
@@ -600,20 +583,20 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do
 
       res_conn = get(conn, "/api/v1/accounts/#{user.id}/following?since_id=#{following1.id}")
 
-      assert [%{"id" => id3}, %{"id" => id2}] = json_response(res_conn, 200)
+      assert [%{"id" => id3}, %{"id" => id2}] = json_response_and_validate_schema(res_conn, 200)
       assert id3 == following3.id
       assert id2 == following2.id
 
       res_conn = get(conn, "/api/v1/accounts/#{user.id}/following?max_id=#{following3.id}")
 
-      assert [%{"id" => id2}, %{"id" => id1}] = json_response(res_conn, 200)
+      assert [%{"id" => id2}, %{"id" => id1}] = json_response_and_validate_schema(res_conn, 200)
       assert id2 == following2.id
       assert id1 == following1.id
 
       res_conn =
         get(conn, "/api/v1/accounts/#{user.id}/following?limit=1&max_id=#{following3.id}")
 
-      assert [%{"id" => id2}] = json_response(res_conn, 200)
+      assert [%{"id" => id2}] = json_response_and_validate_schema(res_conn, 200)
       assert id2 == following2.id
 
       assert [link_header] = get_resp_header(res_conn, "link")
@@ -626,30 +609,37 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do
     setup do: oauth_access(["follow"])
 
     test "following / unfollowing a user", %{conn: conn} do
-      other_user = insert(:user)
-
-      ret_conn = post(conn, "/api/v1/accounts/#{other_user.id}/follow")
-
-      assert %{"id" => _id, "following" => true} = json_response(ret_conn, 200)
-
-      ret_conn = post(conn, "/api/v1/accounts/#{other_user.id}/unfollow")
-
-      assert %{"id" => _id, "following" => false} = json_response(ret_conn, 200)
-
-      conn = post(conn, "/api/v1/follows", %{"uri" => other_user.nickname})
-
-      assert %{"id" => id} = json_response(conn, 200)
-      assert id == to_string(other_user.id)
+      %{id: other_user_id, nickname: other_user_nickname} = insert(:user)
+
+      assert %{"id" => _id, "following" => true} =
+               conn
+               |> post("/api/v1/accounts/#{other_user_id}/follow")
+               |> json_response_and_validate_schema(200)
+
+      assert %{"id" => _id, "following" => false} =
+               conn
+               |> post("/api/v1/accounts/#{other_user_id}/unfollow")
+               |> json_response_and_validate_schema(200)
+
+      assert %{"id" => ^other_user_id} =
+               conn
+               |> put_req_header("content-type", "application/json")
+               |> post("/api/v1/follows", %{"uri" => other_user_nickname})
+               |> json_response_and_validate_schema(200)
     end
 
     test "cancelling follow request", %{conn: conn} do
       %{id: other_user_id} = insert(:user, %{locked: true})
 
       assert %{"id" => ^other_user_id, "following" => false, "requested" => true} =
-               conn |> post("/api/v1/accounts/#{other_user_id}/follow") |> json_response(:ok)
+               conn
+               |> post("/api/v1/accounts/#{other_user_id}/follow")
+               |> json_response_and_validate_schema(:ok)
 
       assert %{"id" => ^other_user_id, "following" => false, "requested" => false} =
-               conn |> post("/api/v1/accounts/#{other_user_id}/unfollow") |> json_response(:ok)
+               conn
+               |> post("/api/v1/accounts/#{other_user_id}/unfollow")
+               |> json_response_and_validate_schema(:ok)
     end
 
     test "following without reblogs" do
@@ -659,51 +649,65 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do
 
       ret_conn = post(conn, "/api/v1/accounts/#{followed.id}/follow?reblogs=false")
 
-      assert %{"showing_reblogs" => false} = json_response(ret_conn, 200)
+      assert %{"showing_reblogs" => false} = json_response_and_validate_schema(ret_conn, 200)
 
       {:ok, activity} = CommonAPI.post(other_user, %{"status" => "hey"})
-      {:ok, reblog, _} = CommonAPI.repeat(activity.id, followed)
-
-      ret_conn = get(conn, "/api/v1/timelines/home")
-
-      assert [] == json_response(ret_conn, 200)
-
-      ret_conn = post(conn, "/api/v1/accounts/#{followed.id}/follow?reblogs=true")
-
-      assert %{"showing_reblogs" => true} = json_response(ret_conn, 200)
-
-      conn = get(conn, "/api/v1/timelines/home")
-
-      expected_activity_id = reblog.id
-      assert [%{"id" => ^expected_activity_id}] = json_response(conn, 200)
+      {:ok, %{id: reblog_id}, _} = CommonAPI.repeat(activity.id, followed)
+
+      assert [] ==
+               conn
+               |> get("/api/v1/timelines/home")
+               |> json_response(200)
+
+      assert %{"showing_reblogs" => true} =
+               conn
+               |> post("/api/v1/accounts/#{followed.id}/follow?reblogs=true")
+               |> json_response_and_validate_schema(200)
+
+      assert [%{"id" => ^reblog_id}] =
+               conn
+               |> get("/api/v1/timelines/home")
+               |> json_response(200)
     end
 
     test "following / unfollowing errors", %{user: user, conn: conn} do
       # self follow
       conn_res = post(conn, "/api/v1/accounts/#{user.id}/follow")
-      assert %{"error" => "Can not follow yourself"} = json_response(conn_res, 400)
+
+      assert %{"error" => "Can not follow yourself"} =
+               json_response_and_validate_schema(conn_res, 400)
 
       # self unfollow
       user = User.get_cached_by_id(user.id)
       conn_res = post(conn, "/api/v1/accounts/#{user.id}/unfollow")
-      assert %{"error" => "Can not unfollow yourself"} = json_response(conn_res, 400)
+
+      assert %{"error" => "Can not unfollow yourself"} =
+               json_response_and_validate_schema(conn_res, 400)
 
       # self follow via uri
       user = User.get_cached_by_id(user.id)
-      conn_res = post(conn, "/api/v1/follows", %{"uri" => user.nickname})
-      assert %{"error" => "Can not follow yourself"} = json_response(conn_res, 400)
+
+      assert %{"error" => "Can not follow yourself"} =
+               conn
+               |> put_req_header("content-type", "multipart/form-data")
+               |> post("/api/v1/follows", %{"uri" => user.nickname})
+               |> json_response_and_validate_schema(400)
 
       # follow non existing user
       conn_res = post(conn, "/api/v1/accounts/doesntexist/follow")
-      assert %{"error" => "Record not found"} = json_response(conn_res, 404)
+      assert %{"error" => "Record not found"} = json_response_and_validate_schema(conn_res, 404)
 
       # follow non existing user via uri
-      conn_res = post(conn, "/api/v1/follows", %{"uri" => "doesntexist"})
-      assert %{"error" => "Record not found"} = json_response(conn_res, 404)
+      conn_res =
+        conn
+        |> put_req_header("content-type", "multipart/form-data")
+        |> post("/api/v1/follows", %{"uri" => "doesntexist"})
+
+      assert %{"error" => "Record not found"} = json_response_and_validate_schema(conn_res, 404)
 
       # unfollow non existing user
       conn_res = post(conn, "/api/v1/accounts/doesntexist/unfollow")
-      assert %{"error" => "Record not found"} = json_response(conn_res, 404)
+      assert %{"error" => "Record not found"} = json_response_and_validate_schema(conn_res, 404)
     end
   end
 
@@ -713,32 +717,33 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do
     test "with notifications", %{conn: conn} do
       other_user = insert(:user)
 
-      ret_conn = post(conn, "/api/v1/accounts/#{other_user.id}/mute")
-
-      response = json_response(ret_conn, 200)
-
-      assert %{"id" => _id, "muting" => true, "muting_notifications" => true} = response
+      assert %{"id" => _id, "muting" => true, "muting_notifications" => true} =
+               conn
+               |> put_req_header("content-type", "application/json")
+               |> post("/api/v1/accounts/#{other_user.id}/mute")
+               |> json_response_and_validate_schema(200)
 
       conn = post(conn, "/api/v1/accounts/#{other_user.id}/unmute")
 
-      response = json_response(conn, 200)
-      assert %{"id" => _id, "muting" => false, "muting_notifications" => false} = response
+      assert %{"id" => _id, "muting" => false, "muting_notifications" => false} =
+               json_response_and_validate_schema(conn, 200)
     end
 
     test "without notifications", %{conn: conn} do
       other_user = insert(:user)
 
       ret_conn =
-        post(conn, "/api/v1/accounts/#{other_user.id}/mute", %{"notifications" => "false"})
-
-      response = json_response(ret_conn, 200)
+        conn
+        |> put_req_header("content-type", "multipart/form-data")
+        |> post("/api/v1/accounts/#{other_user.id}/mute", %{"notifications" => "false"})
 
-      assert %{"id" => _id, "muting" => true, "muting_notifications" => false} = response
+      assert %{"id" => _id, "muting" => true, "muting_notifications" => false} =
+               json_response_and_validate_schema(ret_conn, 200)
 
       conn = post(conn, "/api/v1/accounts/#{other_user.id}/unmute")
 
-      response = json_response(conn, 200)
-      assert %{"id" => _id, "muting" => false, "muting_notifications" => false} = response
+      assert %{"id" => _id, "muting" => false, "muting_notifications" => false} =
+               json_response_and_validate_schema(conn, 200)
     end
   end
 
@@ -751,17 +756,13 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do
       [conn: conn, user: user, activity: activity]
     end
 
-    test "returns pinned statuses", %{conn: conn, user: user, activity: activity} do
-      {:ok, _} = CommonAPI.pin(activity.id, user)
+    test "returns pinned statuses", %{conn: conn, user: user, activity: %{id: activity_id}} do
+      {:ok, _} = CommonAPI.pin(activity_id, user)
 
-      result =
-        conn
-        |> get("/api/v1/accounts/#{user.id}/statuses?pinned=true")
-        |> json_response(200)
-
-      id_str = to_string(activity.id)
-
-      assert [%{"id" => ^id_str, "pinned" => true}] = result
+      assert [%{"id" => ^activity_id, "pinned" => true}] =
+               conn
+               |> get("/api/v1/accounts/#{user.id}/statuses?pinned=true")
+               |> json_response_and_validate_schema(200)
     end
   end
 
@@ -771,11 +772,11 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do
 
     ret_conn = post(conn, "/api/v1/accounts/#{other_user.id}/block")
 
-    assert %{"id" => _id, "blocking" => true} = json_response(ret_conn, 200)
+    assert %{"id" => _id, "blocking" => true} = json_response_and_validate_schema(ret_conn, 200)
 
     conn = post(conn, "/api/v1/accounts/#{other_user.id}/unblock")
 
-    assert %{"id" => _id, "blocking" => false} = json_response(conn, 200)
+    assert %{"id" => _id, "blocking" => false} = json_response_and_validate_schema(conn, 200)
   end
 
   describe "create account by app" do
@@ -802,15 +803,15 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do
           scopes: "read, write, follow"
         })
 
-      %{
-        "client_id" => client_id,
-        "client_secret" => client_secret,
-        "id" => _,
-        "name" => "client_name",
-        "redirect_uri" => "urn:ietf:wg:oauth:2.0:oob",
-        "vapid_key" => _,
-        "website" => nil
-      } = json_response(conn, 200)
+      assert %{
+               "client_id" => client_id,
+               "client_secret" => client_secret,
+               "id" => _,
+               "name" => "client_name",
+               "redirect_uri" => "urn:ietf:wg:oauth:2.0:oob",
+               "vapid_key" => _,
+               "website" => nil
+             } = json_response_and_validate_schema(conn, 200)
 
       conn =
         post(conn, "/oauth/token", %{
@@ -830,6 +831,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do
 
       conn =
         build_conn()
+        |> put_req_header("content-type", "multipart/form-data")
         |> put_req_header("authorization", "Bearer " <> token)
         |> post("/api/v1/accounts", %{
           username: "lain",
@@ -844,7 +846,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do
         "created_at" => _created_at,
         "scope" => _scope,
         "token_type" => "Bearer"
-      } = json_response(conn, 200)
+      } = json_response_and_validate_schema(conn, 200)
 
       token_from_db = Repo.get_by(Token, token: token)
       assert token_from_db
@@ -858,12 +860,15 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do
       _user = insert(:user, email: "lain@example.org")
       app_token = insert(:oauth_token, user: nil)
 
-      conn =
+      res =
         conn
         |> put_req_header("authorization", "Bearer " <> app_token.token)
+        |> put_req_header("content-type", "application/json")
+        |> post("/api/v1/accounts", valid_params)
 
-      res = post(conn, "/api/v1/accounts", valid_params)
-      assert json_response(res, 400) == %{"error" => "{\"email\":[\"has already been taken\"]}"}
+      assert json_response_and_validate_schema(res, 400) == %{
+               "error" => "{\"email\":[\"has already been taken\"]}"
+             }
     end
 
     test "returns bad_request if missing required params", %{
@@ -872,10 +877,13 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do
     } do
       app_token = insert(:oauth_token, user: nil)
 
-      conn = put_req_header(conn, "authorization", "Bearer " <> app_token.token)
+      conn =
+        conn
+        |> put_req_header("authorization", "Bearer " <> app_token.token)
+        |> put_req_header("content-type", "application/json")
 
       res = post(conn, "/api/v1/accounts", valid_params)
-      assert json_response(res, 200)
+      assert json_response_and_validate_schema(res, 200)
 
       [{127, 0, 0, 1}, {127, 0, 0, 2}, {127, 0, 0, 3}, {127, 0, 0, 4}]
       |> Stream.zip(Map.delete(valid_params, :email))
@@ -884,9 +892,18 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do
           conn
           |> Map.put(:remote_ip, ip)
           |> post("/api/v1/accounts", Map.delete(valid_params, attr))
-          |> json_response(400)
-
-        assert res == %{"error" => "Missing parameters"}
+          |> json_response_and_validate_schema(400)
+
+        assert res == %{
+                 "error" => "Missing field: #{attr}.",
+                 "errors" => [
+                   %{
+                     "message" => "Missing field: #{attr}",
+                     "source" => %{"pointer" => "/#{attr}"},
+                     "title" => "Invalid value"
+                   }
+                 ]
+               }
       end)
     end
 
@@ -897,21 +914,27 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do
       Pleroma.Config.put([:instance, :account_activation_required], true)
 
       app_token = insert(:oauth_token, user: nil)
-      conn = put_req_header(conn, "authorization", "Bearer " <> app_token.token)
+
+      conn =
+        conn
+        |> put_req_header("authorization", "Bearer " <> app_token.token)
+        |> put_req_header("content-type", "application/json")
 
       res =
         conn
         |> Map.put(:remote_ip, {127, 0, 0, 5})
         |> post("/api/v1/accounts", Map.delete(valid_params, :email))
 
-      assert json_response(res, 400) == %{"error" => "Missing parameters"}
+      assert json_response_and_validate_schema(res, 400) == %{"error" => "Missing parameters"}
 
       res =
         conn
         |> Map.put(:remote_ip, {127, 0, 0, 6})
         |> post("/api/v1/accounts", Map.put(valid_params, :email, ""))
 
-      assert json_response(res, 400) == %{"error" => "{\"email\":[\"can't be blank\"]}"}
+      assert json_response_and_validate_schema(res, 400) == %{
+               "error" => "{\"email\":[\"can't be blank\"]}"
+             }
     end
 
     test "allow registration without an email", %{conn: conn, valid_params: valid_params} do
@@ -920,10 +943,11 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do
 
       res =
         conn
+        |> put_req_header("content-type", "application/json")
         |> Map.put(:remote_ip, {127, 0, 0, 7})
         |> post("/api/v1/accounts", Map.delete(valid_params, :email))
 
-      assert json_response(res, 200)
+      assert json_response_and_validate_schema(res, 200)
     end
 
     test "allow registration with an empty email", %{conn: conn, valid_params: valid_params} do
@@ -932,17 +956,21 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do
 
       res =
         conn
+        |> put_req_header("content-type", "application/json")
         |> Map.put(:remote_ip, {127, 0, 0, 8})
         |> post("/api/v1/accounts", Map.put(valid_params, :email, ""))
 
-      assert json_response(res, 200)
+      assert json_response_and_validate_schema(res, 200)
     end
 
     test "returns forbidden if token is invalid", %{conn: conn, valid_params: valid_params} do
-      conn = put_req_header(conn, "authorization", "Bearer " <> "invalid-token")
+      res =
+        conn
+        |> put_req_header("authorization", "Bearer " <> "invalid-token")
+        |> put_req_header("content-type", "multipart/form-data")
+        |> post("/api/v1/accounts", valid_params)
 
-      res = post(conn, "/api/v1/accounts", valid_params)
-      assert json_response(res, 403) == %{"error" => "Invalid credentials"}
+      assert json_response_and_validate_schema(res, 403) == %{"error" => "Invalid credentials"}
     end
 
     test "registration from trusted app" do
@@ -962,6 +990,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do
       response =
         build_conn()
         |> Plug.Conn.put_req_header("authorization", "Bearer " <> token)
+        |> put_req_header("content-type", "multipart/form-data")
         |> post("/api/v1/accounts", %{
           nickname: "nickanme",
           agreement: true,
@@ -971,7 +1000,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do
           password: "some_password",
           confirm: "some_password"
         })
-        |> json_response(200)
+        |> json_response_and_validate_schema(200)
 
       assert %{
                "access_token" => access_token,
@@ -984,7 +1013,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do
         build_conn()
         |> Plug.Conn.put_req_header("authorization", "Bearer " <> access_token)
         |> get("/api/v1/accounts/verify_credentials")
-        |> json_response(200)
+        |> json_response_and_validate_schema(200)
 
       assert %{
                "acct" => "Lain",
@@ -1023,10 +1052,12 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do
         conn
         |> put_req_header("authorization", "Bearer " <> app_token.token)
         |> Map.put(:remote_ip, {15, 15, 15, 15})
+        |> put_req_header("content-type", "multipart/form-data")
 
       for i <- 1..2 do
         conn =
-          post(conn, "/api/v1/accounts", %{
+          conn
+          |> post("/api/v1/accounts", %{
             username: "#{i}lain",
             email: "#{i}lain@example.org",
             password: "PlzDontHackLain",
@@ -1038,7 +1069,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do
           "created_at" => _created_at,
           "scope" => _scope,
           "token_type" => "Bearer"
-        } = json_response(conn, 200)
+        } = json_response_and_validate_schema(conn, 200)
 
         token_from_db = Repo.get_by(Token, token: token)
         assert token_from_db
@@ -1056,7 +1087,9 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do
           agreement: true
         })
 
-      assert json_response(conn, :too_many_requests) == %{"error" => "Throttled"}
+      assert json_response_and_validate_schema(conn, :too_many_requests) == %{
+               "error" => "Throttled"
+             }
     end
   end
 
@@ -1064,15 +1097,13 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do
     test "returns lists to which the account belongs" do
       %{user: user, conn: conn} = oauth_access(["read:lists"])
       other_user = insert(:user)
-      assert {:ok, %Pleroma.List{} = list} = Pleroma.List.create("Test List", user)
+      assert {:ok, %Pleroma.List{id: list_id} = list} = Pleroma.List.create("Test List", user)
       {:ok, %{following: _following}} = Pleroma.List.follow(list, other_user)
 
-      res =
-        conn
-        |> get("/api/v1/accounts/#{other_user.id}/lists")
-        |> json_response(200)
-
-      assert res == [%{"id" => to_string(list.id), "title" => "Test List"}]
+      assert [%{"id" => list_id, "title" => "Test List"}] =
+               conn
+               |> get("/api/v1/accounts/#{other_user.id}/lists")
+               |> json_response_and_validate_schema(200)
     end
   end
 
@@ -1081,7 +1112,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do
       %{user: user, conn: conn} = oauth_access(["read:accounts"])
       conn = get(conn, "/api/v1/accounts/verify_credentials")
 
-      response = json_response(conn, 200)
+      response = json_response_and_validate_schema(conn, 200)
 
       assert %{"id" => id, "source" => %{"privacy" => "public"}} = response
       assert response["pleroma"]["chat_token"]
@@ -1094,7 +1125,9 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do
 
       conn = get(conn, "/api/v1/accounts/verify_credentials")
 
-      assert %{"id" => id, "source" => %{"privacy" => "unlisted"}} = json_response(conn, 200)
+      assert %{"id" => id, "source" => %{"privacy" => "unlisted"}} =
+               json_response_and_validate_schema(conn, 200)
+
       assert id == to_string(user.id)
     end
 
@@ -1104,7 +1137,9 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do
 
       conn = get(conn, "/api/v1/accounts/verify_credentials")
 
-      assert %{"id" => id, "source" => %{"privacy" => "private"}} = json_response(conn, 200)
+      assert %{"id" => id, "source" => %{"privacy" => "private"}} =
+               json_response_and_validate_schema(conn, 200)
+
       assert id == to_string(user.id)
     end
   end
@@ -1113,20 +1148,24 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do
     setup do: oauth_access(["read:follows"])
 
     test "returns the relationships for the current user", %{user: user, conn: conn} do
-      other_user = insert(:user)
+      %{id: other_user_id} = other_user = insert(:user)
       {:ok, _user} = User.follow(user, other_user)
 
-      conn = get(conn, "/api/v1/accounts/relationships", %{"id" => [other_user.id]})
-
-      assert [relationship] = json_response(conn, 200)
+      assert [%{"id" => ^other_user_id}] =
+               conn
+               |> get("/api/v1/accounts/relationships?id=#{other_user.id}")
+               |> json_response_and_validate_schema(200)
 
-      assert to_string(other_user.id) == relationship["id"]
+      assert [%{"id" => ^other_user_id}] =
+               conn
+               |> get("/api/v1/accounts/relationships?id[]=#{other_user.id}")
+               |> json_response_and_validate_schema(200)
     end
 
     test "returns an empty list on a bad request", %{conn: conn} do
       conn = get(conn, "/api/v1/accounts/relationships", %{})
 
-      assert [] = json_response(conn, 200)
+      assert [] = json_response_and_validate_schema(conn, 200)
     end
   end
 
@@ -1139,7 +1178,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do
     conn = get(conn, "/api/v1/mutes")
 
     other_user_id = to_string(other_user.id)
-    assert [%{"id" => ^other_user_id}] = json_response(conn, 200)
+    assert [%{"id" => ^other_user_id}] = json_response_and_validate_schema(conn, 200)
   end
 
   test "getting a list of blocks" do
@@ -1154,6 +1193,6 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do
       |> get("/api/v1/blocks")
 
     other_user_id = to_string(other_user.id)
-    assert [%{"id" => ^other_user_id}] = json_response(conn, 200)
+    assert [%{"id" => ^other_user_id}] = json_response_and_validate_schema(conn, 200)
   end
 end
index e7b11d14e1461fa910f72ddd73c8b5359a4f5b21..a0b8b126c9536594ba307b875bfa41ff7477bb78 100644 (file)
@@ -27,7 +27,7 @@ defmodule Pleroma.Web.MastodonAPI.AppControllerTest do
       "vapid_key" => Push.vapid_config() |> Keyword.get(:public_key)
     }
 
-    assert expected == json_response(conn, 200)
+    assert expected == json_response_and_validate_schema(conn, 200)
   end
 
   test "creates an oauth app", %{conn: conn} do
@@ -55,6 +55,6 @@ defmodule Pleroma.Web.MastodonAPI.AppControllerTest do
       "vapid_key" => Push.vapid_config() |> Keyword.get(:public_key)
     }
 
-    assert expected == json_response(conn, 200)
+    assert expected == json_response_and_validate_schema(conn, 200)
   end
 end
index 0b2ffa470d57c59fcf264e9d0f9b60578d3b735a..ab0027f9027dc6f6bdce76c2d826741f2d51b11c 100644 (file)
@@ -4,16 +4,12 @@
 
 defmodule Pleroma.Web.MastodonAPI.CustomEmojiControllerTest do
   use Pleroma.Web.ConnCase, async: true
-  alias Pleroma.Web.ApiSpec
-  alias Pleroma.Web.ApiSpec.Schemas.CustomEmoji
-  alias Pleroma.Web.ApiSpec.Schemas.CustomEmojisResponse
-  import OpenApiSpex.TestAssertions
 
   test "with tags", %{conn: conn} do
     assert resp =
              conn
              |> get("/api/v1/custom_emojis")
-             |> json_response(200)
+             |> json_response_and_validate_schema(200)
 
     assert [emoji | _body] = resp
     assert Map.has_key?(emoji, "shortcode")
@@ -23,19 +19,5 @@ defmodule Pleroma.Web.MastodonAPI.CustomEmojiControllerTest do
     assert Map.has_key?(emoji, "category")
     assert Map.has_key?(emoji, "url")
     assert Map.has_key?(emoji, "visible_in_picker")
-    assert_schema(resp, "CustomEmojisResponse", ApiSpec.spec())
-    assert_schema(emoji, "CustomEmoji", ApiSpec.spec())
-  end
-
-  test "CustomEmoji example matches schema" do
-    api_spec = ApiSpec.spec()
-    schema = CustomEmoji.schema()
-    assert_schema(schema.example, "CustomEmoji", api_spec)
-  end
-
-  test "CustomEmojisResponse example matches schema" do
-    api_spec = ApiSpec.spec()
-    schema = CustomEmojisResponse.schema()
-    assert_schema(schema.example, "CustomEmojisResponse", api_spec)
   end
 end
index d66190c90040d6460d886e4ec9e3b6c79843ed56..01a24afcf2225a35589b65b1c7aff63dd49648a4 100644 (file)
@@ -6,11 +6,8 @@ defmodule Pleroma.Web.MastodonAPI.DomainBlockControllerTest do
   use Pleroma.Web.ConnCase
 
   alias Pleroma.User
-  alias Pleroma.Web.ApiSpec
-  alias Pleroma.Web.ApiSpec.Schemas.DomainBlocksResponse
 
   import Pleroma.Factory
-  import OpenApiSpex.TestAssertions
 
   test "blocking / unblocking a domain" do
     %{user: user, conn: conn} = oauth_access(["write:blocks"])
@@ -21,7 +18,7 @@ defmodule Pleroma.Web.MastodonAPI.DomainBlockControllerTest do
       |> put_req_header("content-type", "application/json")
       |> post("/api/v1/domain_blocks", %{"domain" => "dogwhistle.zone"})
 
-    assert %{} = json_response(ret_conn, 200)
+    assert %{} == json_response_and_validate_schema(ret_conn, 200)
     user = User.get_cached_by_ap_id(user.ap_id)
     assert User.blocks?(user, other_user)
 
@@ -30,7 +27,7 @@ defmodule Pleroma.Web.MastodonAPI.DomainBlockControllerTest do
       |> put_req_header("content-type", "application/json")
       |> delete("/api/v1/domain_blocks", %{"domain" => "dogwhistle.zone"})
 
-    assert %{} = json_response(ret_conn, 200)
+    assert %{} == json_response_and_validate_schema(ret_conn, 200)
     user = User.get_cached_by_ap_id(user.ap_id)
     refute User.blocks?(user, other_user)
   end
@@ -41,21 +38,10 @@ defmodule Pleroma.Web.MastodonAPI.DomainBlockControllerTest do
     {:ok, user} = User.block_domain(user, "bad.site")
     {:ok, user} = User.block_domain(user, "even.worse.site")
 
-    conn =
-      conn
-      |> assign(:user, user)
-      |> get("/api/v1/domain_blocks")
-
-    domain_blocks = json_response(conn, 200)
-
-    assert "bad.site" in domain_blocks
-    assert "even.worse.site" in domain_blocks
-    assert_schema(domain_blocks, "DomainBlocksResponse", ApiSpec.spec())
-  end
-
-  test "DomainBlocksResponse example matches schema" do
-    api_spec = ApiSpec.spec()
-    schema = DomainBlocksResponse.schema()
-    assert_schema(schema.example, "DomainBlocksResponse", api_spec)
+    assert ["even.worse.site", "bad.site"] ==
+             conn
+             |> assign(:user, user)
+             |> get("/api/v1/domain_blocks")
+             |> json_response_and_validate_schema(200)
   end
 end
index 162f7b1b2ccdb36d01ec87c35b24cec2ccbd39b5..85068edd00eddf9a0551073ed6273d1962604f6d 100644 (file)
@@ -302,6 +302,17 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
       assert [] == Repo.all(Activity)
     end
 
+    test "ignores nil values", %{conn: conn} do
+      conn =
+        post(conn, "/api/v1/statuses", %{
+          "status" => "not scheduled",
+          "scheduled_at" => nil
+        })
+
+      assert result = json_response(conn, 200)
+      assert Activity.get_by_id(result["id"])
+    end
+
     test "creates a scheduled activity with a media attachment", %{user: user, conn: conn} do
       scheduled_at = NaiveDateTime.add(NaiveDateTime.utc_now(), :timer.minutes(120), :millisecond)
 
index 75f184242ff13096268d34494d109523ac6f7907..bb4bc43965fb2274c7016ff937e6bc0fa498d4ec 100644 (file)
@@ -7,35 +7,28 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do
 
   describe "empty_array/2 (stubs)" do
     test "GET /api/v1/accounts/:id/identity_proofs" do
-      %{user: user, conn: conn} = oauth_access(["n/a"])
+      %{user: user, conn: conn} = oauth_access(["read:accounts"])
 
-      res =
-        conn
-        |> assign(:user, user)
-        |> get("/api/v1/accounts/#{user.id}/identity_proofs")
-        |> json_response(200)
-
-      assert res == []
+      assert [] ==
+               conn
+               |> get("/api/v1/accounts/#{user.id}/identity_proofs")
+               |> json_response(200)
     end
 
     test "GET /api/v1/endorsements" do
       %{conn: conn} = oauth_access(["read:accounts"])
 
-      res =
-        conn
-        |> get("/api/v1/endorsements")
-        |> json_response(200)
-
-      assert res == []
+      assert [] ==
+               conn
+               |> get("/api/v1/endorsements")
+               |> json_response(200)
     end
 
     test "GET /api/v1/trends", %{conn: conn} do
-      res =
-        conn
-        |> get("/api/v1/trends")
-        |> json_response(200)
-
-      assert res == []
+      assert [] ==
+               conn
+               |> get("/api/v1/trends")
+               |> json_response(200)
     end
   end
 end
index 291ae54fc1639ddb53f6d268c2b98a7a7b13721a..1ac2f2c27aa4fb3e4ab8bc0b626eb3ef10808b3c 100644 (file)
@@ -9,6 +9,7 @@ defmodule Pleroma.Web.MongooseIMController do
   test "/user_exists", %{conn: conn} do
     _user = insert(:user, nickname: "lain")
     _remote_user = insert(:user, nickname: "alice", local: false)
+    _deactivated_user = insert(:user, nickname: "konata", deactivated: true)
 
     res =
       conn
@@ -30,11 +31,25 @@ defmodule Pleroma.Web.MongooseIMController do
       |> json_response(404)
 
     assert res == false
+
+    res =
+      conn
+      |> get(mongoose_im_path(conn, :user_exists), user: "konata")
+      |> json_response(404)
+
+    assert res == false
   end
 
   test "/check_password", %{conn: conn} do
     user = insert(:user, password_hash: Comeonin.Pbkdf2.hashpwsalt("cool"))
 
+    _deactivated_user =
+      insert(:user,
+        nickname: "konata",
+        deactivated: true,
+        password_hash: Comeonin.Pbkdf2.hashpwsalt("cool")
+      )
+
     res =
       conn
       |> get(mongoose_im_path(conn, :check_password), user: user.nickname, pass: "cool")
@@ -49,6 +64,13 @@ defmodule Pleroma.Web.MongooseIMController do
 
     assert res == false
 
+    res =
+      conn
+      |> get(mongoose_im_path(conn, :check_password), user: "konata", pass: "cool")
+      |> json_response(404)
+
+    assert res == false
+
     res =
       conn
       |> get(mongoose_im_path(conn, :check_password), user: "nobody", pass: "cool")
index ae5334015aa56e029203a922328236a6050b9d10..6b671a667a7c9811601e277571083c0d779aeefa 100644 (file)
@@ -151,15 +151,18 @@ defmodule Pleroma.Web.PleromaAPI.AccountControllerTest do
       assert like["id"] == activity.id
     end
 
-    test "does not return favorites for specified user_id when user is not logged in", %{
+    test "returns favorites for specified user_id when requester is not logged in", %{
       user: user
     } do
       activity = insert(:note_activity)
       CommonAPI.favorite(user, activity.id)
 
-      build_conn()
-      |> get("/api/v1/pleroma/accounts/#{user.id}/favourites")
-      |> json_response(403)
+      response =
+        build_conn()
+        |> get("/api/v1/pleroma/accounts/#{user.id}/favourites")
+        |> json_response(200)
+
+      assert length(response) == 1
     end
 
     test "returns favorited DM only when user is logged in and he is one of recipients", %{
@@ -185,9 +188,12 @@ defmodule Pleroma.Web.PleromaAPI.AccountControllerTest do
         assert length(response) == 1
       end
 
-      build_conn()
-      |> get("/api/v1/pleroma/accounts/#{user.id}/favourites")
-      |> json_response(403)
+      response =
+        build_conn()
+        |> get("/api/v1/pleroma/accounts/#{user.id}/favourites")
+        |> json_response(200)
+
+      assert length(response) == 0
     end
 
     test "does not return others' favorited DM when user is not one of recipients", %{
index 435fb65921d83b582539a56405f2209778aaf605..4246eb400046422efb4c5c5ec6466711519cdfbc 100644 (file)
@@ -38,8 +38,7 @@ defmodule Pleroma.Web.PleromaAPI.EmojiAPIControllerTest do
   end
 
   test "listing remote packs" do
-    admin = insert(:user, is_admin: true)
-    %{conn: conn} = oauth_access(["admin:write"], user: admin)
+    conn = build_conn()
 
     resp =
       build_conn()
@@ -76,7 +75,7 @@ defmodule Pleroma.Web.PleromaAPI.EmojiAPIControllerTest do
     assert Enum.find(arch, fn {n, _} -> n == 'blank.png' end)
   end
 
-  test "downloading shared & unshared packs from another instance via download_from, deleting them" do
+  test "downloading shared & unshared packs from another instance, deleting them" do
     on_exit(fn ->
       File.rm_rf!("#{@emoji_dir_path}/test_pack2")
       File.rm_rf!("#{@emoji_dir_path}/test_pack_nonshared2")
@@ -136,7 +135,7 @@ defmodule Pleroma.Web.PleromaAPI.EmojiAPIControllerTest do
             |> post(
               emoji_api_path(
                 conn,
-                :download_from
+                :save_from
               ),
               %{
                 instance_address: "https://old-instance",
@@ -152,7 +151,7 @@ defmodule Pleroma.Web.PleromaAPI.EmojiAPIControllerTest do
            |> post(
              emoji_api_path(
                conn,
-               :download_from
+               :save_from
              ),
              %{
                instance_address: "https://example.com",
@@ -179,7 +178,7 @@ defmodule Pleroma.Web.PleromaAPI.EmojiAPIControllerTest do
            |> post(
              emoji_api_path(
                conn,
-               :download_from
+               :save_from
              ),
              %{
                instance_address: "https://example.com",
index ab0a2c3df094cf7dc744421871ee7b0f77ab8a43..464d0ea2eed10825ed7210df2b4c323cc2ccb195 100644 (file)
@@ -19,13 +19,9 @@ defmodule Pleroma.Web.TwitterAPI.ControllerTest do
     end
 
     test "with credentials, without any params" do
-      %{user: current_user, conn: conn} =
-        oauth_access(["read:notifications", "write:notifications"])
+      %{conn: conn} = oauth_access(["write:notifications"])
 
-      conn =
-        conn
-        |> assign(:user, current_user)
-        |> post("/api/qvitter/statuses/notifications/read")
+      conn = post(conn, "/api/qvitter/statuses/notifications/read")
 
       assert json_response(conn, 400) == %{
                "error" => "You need to specify latest_id",
index f6e13b66127030a0faf24f7a1d9a9619d3d242c3..7926a075789ba6b64168e5e5252048bc4bdd40ec 100644 (file)
@@ -18,11 +18,11 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do
 
   test "it registers a new user and returns the user." do
     data = %{
-      "nickname" => "lain",
-      "email" => "lain@wired.jp",
-      "fullname" => "lain iwakura",
-      "password" => "bear",
-      "confirm" => "bear"
+      :nickname => "lain",
+      :email => "lain@wired.jp",
+      :fullname => "lain iwakura",
+      :password => "bear",
+      :confirm => "bear"
     }
 
     {:ok, user} = TwitterAPI.register_user(data)
@@ -35,12 +35,12 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do
 
   test "it registers a new user with empty string in bio and returns the user." do
     data = %{
-      "nickname" => "lain",
-      "email" => "lain@wired.jp",
-      "fullname" => "lain iwakura",
-      "bio" => "",
-      "password" => "bear",
-      "confirm" => "bear"
+      :nickname => "lain",
+      :email => "lain@wired.jp",
+      :fullname => "lain iwakura",
+      :bio => "",
+      :password => "bear",
+      :confirm => "bear"
     }
 
     {:ok, user} = TwitterAPI.register_user(data)
@@ -60,12 +60,12 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do
     end
 
     data = %{
-      "nickname" => "lain",
-      "email" => "lain@wired.jp",
-      "fullname" => "lain iwakura",
-      "bio" => "",
-      "password" => "bear",
-      "confirm" => "bear"
+      :nickname => "lain",
+      :email => "lain@wired.jp",
+      :fullname => "lain iwakura",
+      :bio => "",
+      :password => "bear",
+      :confirm => "bear"
     }
 
     {:ok, user} = TwitterAPI.register_user(data)
@@ -87,23 +87,23 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do
 
   test "it registers a new user and parses mentions in the bio" do
     data1 = %{
-      "nickname" => "john",
-      "email" => "john@gmail.com",
-      "fullname" => "John Doe",
-      "bio" => "test",
-      "password" => "bear",
-      "confirm" => "bear"
+      :nickname => "john",
+      :email => "john@gmail.com",
+      :fullname => "John Doe",
+      :bio => "test",
+      :password => "bear",
+      :confirm => "bear"
     }
 
     {:ok, user1} = TwitterAPI.register_user(data1)
 
     data2 = %{
-      "nickname" => "lain",
-      "email" => "lain@wired.jp",
-      "fullname" => "lain iwakura",
-      "bio" => "@john test",
-      "password" => "bear",
-      "confirm" => "bear"
+      :nickname => "lain",
+      :email => "lain@wired.jp",
+      :fullname => "lain iwakura",
+      :bio => "@john test",
+      :password => "bear",
+      :confirm => "bear"
     }
 
     {:ok, user2} = TwitterAPI.register_user(data2)
@@ -123,13 +123,13 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do
       {:ok, invite} = UserInviteToken.create_invite()
 
       data = %{
-        "nickname" => "vinny",
-        "email" => "pasta@pizza.vs",
-        "fullname" => "Vinny Vinesauce",
-        "bio" => "streamer",
-        "password" => "hiptofbees",
-        "confirm" => "hiptofbees",
-        "token" => invite.token
+        :nickname => "vinny",
+        :email => "pasta@pizza.vs",
+        :fullname => "Vinny Vinesauce",
+        :bio => "streamer",
+        :password => "hiptofbees",
+        :confirm => "hiptofbees",
+        :token => invite.token
       }
 
       {:ok, user} = TwitterAPI.register_user(data)
@@ -145,13 +145,13 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do
 
     test "returns error on invalid token" do
       data = %{
-        "nickname" => "GrimReaper",
-        "email" => "death@reapers.afterlife",
-        "fullname" => "Reaper Grim",
-        "bio" => "Your time has come",
-        "password" => "scythe",
-        "confirm" => "scythe",
-        "token" => "DudeLetMeInImAFairy"
+        :nickname => "GrimReaper",
+        :email => "death@reapers.afterlife",
+        :fullname => "Reaper Grim",
+        :bio => "Your time has come",
+        :password => "scythe",
+        :confirm => "scythe",
+        :token => "DudeLetMeInImAFairy"
       }
 
       {:error, msg} = TwitterAPI.register_user(data)
@@ -165,13 +165,13 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do
       UserInviteToken.update_invite!(invite, used: true)
 
       data = %{
-        "nickname" => "GrimReaper",
-        "email" => "death@reapers.afterlife",
-        "fullname" => "Reaper Grim",
-        "bio" => "Your time has come",
-        "password" => "scythe",
-        "confirm" => "scythe",
-        "token" => invite.token
+        :nickname => "GrimReaper",
+        :email => "death@reapers.afterlife",
+        :fullname => "Reaper Grim",
+        :bio => "Your time has come",
+        :password => "scythe",
+        :confirm => "scythe",
+        :token => invite.token
       }
 
       {:error, msg} = TwitterAPI.register_user(data)
@@ -186,16 +186,16 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do
 
     setup do
       data = %{
-        "nickname" => "vinny",
-        "email" => "pasta@pizza.vs",
-        "fullname" => "Vinny Vinesauce",
-        "bio" => "streamer",
-        "password" => "hiptofbees",
-        "confirm" => "hiptofbees"
+        :nickname => "vinny",
+        :email => "pasta@pizza.vs",
+        :fullname => "Vinny Vinesauce",
+        :bio => "streamer",
+        :password => "hiptofbees",
+        :confirm => "hiptofbees"
       }
 
       check_fn = fn invite ->
-        data = Map.put(data, "token", invite.token)
+        data = Map.put(data, :token, invite.token)
         {:ok, user} = TwitterAPI.register_user(data)
         fetched_user = User.get_cached_by_nickname("vinny")
 
@@ -250,13 +250,13 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do
       UserInviteToken.update_invite!(invite, uses: 99)
 
       data = %{
-        "nickname" => "vinny",
-        "email" => "pasta@pizza.vs",
-        "fullname" => "Vinny Vinesauce",
-        "bio" => "streamer",
-        "password" => "hiptofbees",
-        "confirm" => "hiptofbees",
-        "token" => invite.token
+        :nickname => "vinny",
+        :email => "pasta@pizza.vs",
+        :fullname => "Vinny Vinesauce",
+        :bio => "streamer",
+        :password => "hiptofbees",
+        :confirm => "hiptofbees",
+        :token => invite.token
       }
 
       {:ok, user} = TwitterAPI.register_user(data)
@@ -269,13 +269,13 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do
                AccountView.render("show.json", %{user: fetched_user})
 
       data = %{
-        "nickname" => "GrimReaper",
-        "email" => "death@reapers.afterlife",
-        "fullname" => "Reaper Grim",
-        "bio" => "Your time has come",
-        "password" => "scythe",
-        "confirm" => "scythe",
-        "token" => invite.token
+        :nickname => "GrimReaper",
+        :email => "death@reapers.afterlife",
+        :fullname => "Reaper Grim",
+        :bio => "Your time has come",
+        :password => "scythe",
+        :confirm => "scythe",
+        :token => invite.token
       }
 
       {:error, msg} = TwitterAPI.register_user(data)
@@ -292,13 +292,13 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do
       {:ok, invite} = UserInviteToken.create_invite(%{expires_at: Date.utc_today(), max_use: 100})
 
       data = %{
-        "nickname" => "vinny",
-        "email" => "pasta@pizza.vs",
-        "fullname" => "Vinny Vinesauce",
-        "bio" => "streamer",
-        "password" => "hiptofbees",
-        "confirm" => "hiptofbees",
-        "token" => invite.token
+        :nickname => "vinny",
+        :email => "pasta@pizza.vs",
+        :fullname => "Vinny Vinesauce",
+        :bio => "streamer",
+        :password => "hiptofbees",
+        :confirm => "hiptofbees",
+        :token => invite.token
       }
 
       {:ok, user} = TwitterAPI.register_user(data)
@@ -317,13 +317,13 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do
       UserInviteToken.update_invite!(invite, uses: 99)
 
       data = %{
-        "nickname" => "vinny",
-        "email" => "pasta@pizza.vs",
-        "fullname" => "Vinny Vinesauce",
-        "bio" => "streamer",
-        "password" => "hiptofbees",
-        "confirm" => "hiptofbees",
-        "token" => invite.token
+        :nickname => "vinny",
+        :email => "pasta@pizza.vs",
+        :fullname => "Vinny Vinesauce",
+        :bio => "streamer",
+        :password => "hiptofbees",
+        :confirm => "hiptofbees",
+        :token => invite.token
       }
 
       {:ok, user} = TwitterAPI.register_user(data)
@@ -335,13 +335,13 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do
                AccountView.render("show.json", %{user: fetched_user})
 
       data = %{
-        "nickname" => "GrimReaper",
-        "email" => "death@reapers.afterlife",
-        "fullname" => "Reaper Grim",
-        "bio" => "Your time has come",
-        "password" => "scythe",
-        "confirm" => "scythe",
-        "token" => invite.token
+        :nickname => "GrimReaper",
+        :email => "death@reapers.afterlife",
+        :fullname => "Reaper Grim",
+        :bio => "Your time has come",
+        :password => "scythe",
+        :confirm => "scythe",
+        :token => invite.token
       }
 
       {:error, msg} = TwitterAPI.register_user(data)
@@ -355,13 +355,13 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do
         UserInviteToken.create_invite(%{expires_at: Date.add(Date.utc_today(), -1), max_use: 100})
 
       data = %{
-        "nickname" => "GrimReaper",
-        "email" => "death@reapers.afterlife",
-        "fullname" => "Reaper Grim",
-        "bio" => "Your time has come",
-        "password" => "scythe",
-        "confirm" => "scythe",
-        "token" => invite.token
+        :nickname => "GrimReaper",
+        :email => "death@reapers.afterlife",
+        :fullname => "Reaper Grim",
+        :bio => "Your time has come",
+        :password => "scythe",
+        :confirm => "scythe",
+        :token => invite.token
       }
 
       {:error, msg} = TwitterAPI.register_user(data)
@@ -377,13 +377,13 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do
       UserInviteToken.update_invite!(invite, uses: 100)
 
       data = %{
-        "nickname" => "GrimReaper",
-        "email" => "death@reapers.afterlife",
-        "fullname" => "Reaper Grim",
-        "bio" => "Your time has come",
-        "password" => "scythe",
-        "confirm" => "scythe",
-        "token" => invite.token
+        :nickname => "GrimReaper",
+        :email => "death@reapers.afterlife",
+        :fullname => "Reaper Grim",
+        :bio => "Your time has come",
+        :password => "scythe",
+        :confirm => "scythe",
+        :token => invite.token
       }
 
       {:error, msg} = TwitterAPI.register_user(data)
@@ -395,11 +395,11 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do
 
   test "it returns the error on registration problems" do
     data = %{
-      "nickname" => "lain",
-      "email" => "lain@wired.jp",
-      "fullname" => "lain iwakura",
-      "bio" => "close the world.",
-      "password" => "bear"
+      :nickname => "lain",
+      :email => "lain@wired.jp",
+      :fullname => "lain iwakura",
+      :bio => "close the world.",
+      :password => "bear"
     }
 
     {:error, error_object} = TwitterAPI.register_user(data)