Merge remote-tracking branch 'pleroma/develop' into mastodon-lookup
authormarcin mikołajczak <git@mkljczk.pl>
Tue, 28 Dec 2021 15:27:27 +0000 (16:27 +0100)
committermarcin mikołajczak <git@mkljczk.pl>
Tue, 28 Dec 2021 15:27:27 +0000 (16:27 +0100)
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
54 files changed:
.gitlab-ci.yml
CHANGELOG.md
config/config.exs
config/description.exs
docs/development/API/differences_in_mastoapi_responses.md
docs/development/API/pleroma_api.md
lib/pleroma/application.ex
lib/pleroma/reverse_proxy/client/wrapper.ex
lib/pleroma/telemetry/logger.ex
lib/pleroma/user.ex
lib/pleroma/user/query.ex
lib/pleroma/user_note.ex [new file with mode: 0644]
lib/pleroma/web/activity_pub/activity_pub.ex
lib/pleroma/web/activity_pub/side_effects.ex
lib/pleroma/web/api_spec/operations/account_operation.ex
lib/pleroma/web/api_spec/operations/app_operation.ex
lib/pleroma/web/api_spec/operations/directory_operation.ex [new file with mode: 0644]
lib/pleroma/web/api_spec/operations/pleroma_app_operation.ex [new file with mode: 0644]
lib/pleroma/web/api_spec/operations/twitter_util_operation.ex
lib/pleroma/web/api_spec/schemas/account.ex
lib/pleroma/web/api_spec/schemas/account_relationship.ex
lib/pleroma/web/api_spec/schemas/app.ex [new file with mode: 0644]
lib/pleroma/web/api_spec/schemas/status.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/directory_controller.ex [new file with mode: 0644]
lib/pleroma/web/mastodon_api/mastodon_api.ex
lib/pleroma/web/mastodon_api/views/account_view.ex
lib/pleroma/web/mastodon_api/views/instance_view.ex
lib/pleroma/web/nodeinfo/nodeinfo.ex
lib/pleroma/web/o_auth/app.ex
lib/pleroma/web/pleroma_api/controllers/app_controller.ex [new file with mode: 0644]
lib/pleroma/web/pleroma_api/views/app_view.ex [new file with mode: 0644]
lib/pleroma/web/plugs/ensure_staff_privileged_plug.ex [new file with mode: 0644]
lib/pleroma/web/router.ex
lib/pleroma/web/static_fe/static_fe_controller.ex
lib/pleroma/web/twitter_api/controllers/util_controller.ex
mix.exs
mix.lock
priv/repo/migrations/20210818023112_add_user_id_to_apps.exs [new file with mode: 0644]
priv/repo/migrations/20211121000000_create_user_notes.exs [new file with mode: 0644]
priv/repo/migrations/20211222165256_add_last_status_at_to_users.exs [new file with mode: 0644]
priv/repo/migrations/20211225154802_add_is_discoverable_index_to_users.exs [new file with mode: 0644]
test/pleroma/gun/connection_pool_test.exs
test/pleroma/web/mastodon_api/controllers/account_controller_test.exs
test/pleroma/web/mastodon_api/controllers/app_controller_test.exs
test/pleroma/web/mastodon_api/controllers/directory_controller_test.exs [new file with mode: 0644]
test/pleroma/web/mastodon_api/views/account_view_test.exs
test/pleroma/web/o_auth/app_test.exs
test/pleroma/web/o_status/o_status_controller_test.exs
test/pleroma/web/pleroma_api/controllers/app_controller_test.exs [new file with mode: 0644]
test/pleroma/web/pleroma_api/views/app_view_test.exs [new file with mode: 0644]
test/pleroma/web/plugs/ensure_staff_privileged_plug_test.exs [new file with mode: 0644]
test/pleroma/web/plugs/frontend_static_plug_test.exs

index 844f5888e44a6495fb65f458ce5fbceb1f8e5344..3860f1db9522a07adb05cbccb1b0d9182b44dd39 100644 (file)
@@ -79,7 +79,6 @@ unit-testing:
       - "**/*.ex"
       - "**/*.exs"
       - "mix.lock"
-  retry: 2
   cache: &testing_cache_policy
     <<: *global_cache_policy
     policy: pull
@@ -94,6 +93,27 @@ unit-testing:
     - mix ecto.migrate
     - mix coveralls --preload-modules
 
+unit-testing-erratic:
+  stage: test
+  retry: 2
+  only:
+    changes:
+      - "**/*.ex"
+      - "**/*.exs"
+      - "mix.lock"
+  cache: &testing_cache_policy
+    <<: *global_cache_policy
+    policy: pull
+
+  services:
+  - name: postgres:13
+    alias: postgres
+    command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"]
+  script:
+    - mix ecto.create
+    - mix ecto.migrate
+    - mix test --only=erratic
+
 # Removed to fix CI issue. In this early state it wasn't adding much value anyway.
 # TODO Fix and reinstate federated testing
 # federated-testing:
@@ -117,7 +137,6 @@ unit-testing-rum:
       - "**/*.ex"
       - "**/*.exs"
       - "mix.lock"
-  retry: 2
   cache: *testing_cache_policy
   services:
   - name: minibikini/postgres-with-rum:12
index 8e97da1896ab9467f0bf9637588821c2c6d1b536..e527f32dee2d6363209d30e5f8e4f40d919ff1a6 100644 (file)
@@ -15,10 +15,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
 
 ### Added
 - `activeMonth` and `activeHalfyear` fields in NodeInfo usage.users object
+- Experimental support for Finch. Put `config :tesla, :adapter, {Tesla.Adapter.Finch, name: MyFinch}` in your secrets file to use it. Reverse Proxy will still use Hackney.
+- AdminAPI: allow moderators to manage reports, users, invites, and custom emojis
+- AdminAPI: restrict moderators to access sensitive data: change user credentials, get password reset token, read private statuses and chats, etc
 
 ### Fixed
 - Subscription(Bell) Notifications: Don't create from Pipeline Ingested replies
 - Handle Reject for already-accepted Follows properly
+- Display OpenGraph data on alternative notice routes.
 
 ### Removed
 
@@ -66,7 +70,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
 - Attachment dimensions and blurhashes are federated when available.
 - Mastodon API: support `poll` notification.
 - Pinned posts federation
-- AdminAPI: allow moderators to manage reports, users, invites, and custom emojis
 
 ### Fixed
 - Don't crash so hard when email settings are invalid.
index c9592511f229dc6cd51061904e59bcb9b197ffd8..2bde5b8268ed61e6cf988daee6faba2ff0973ffd 100644 (file)
@@ -149,6 +149,8 @@ config :pleroma, Pleroma.Web.Endpoint,
   ]
 
 # Configures Elixir's Logger
+config :logger, truncate: 65536
+
 config :logger, :console,
   level: :debug,
   format: "\n$time $metadata[$level] $message\n",
@@ -254,7 +256,9 @@ config :pleroma, :instance,
     ]
   ],
   show_reactions: true,
-  password_reset_token_validity: 60 * 60 * 24
+  password_reset_token_validity: 60 * 60 * 24,
+  profile_directory: true,
+  privileged_staff: false
 
 config :pleroma, :welcome,
   direct_message: [
@@ -852,6 +856,13 @@ config :pleroma, ConcurrentLimiter, [
   {Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy, [max_running: 5, max_waiting: 5]}
 ]
 
+config :pleroma, :telemetry,
+  slow_queries_logging: [
+    enabled: false,
+    min_duration: 500_000,
+    exclude_sources: [nil, "oban_jobs"]
+  ]
+
 # Import environment specific config. This must remain at the bottom
 # of this file so it overrides the configuration defined above.
 import_config "#{Mix.env()}.exs"
index 1c8c3b4a0e8af2e61d5705ac6e02214cfbb288c2..ea3f34abe6c3a71df1d6a121d16ec799ff9d5780 100644 (file)
@@ -936,6 +936,17 @@ config :pleroma, :config_description, [
         key: :show_reactions,
         type: :boolean,
         description: "Let favourites and emoji reactions be viewed through the API."
+      },
+      %{
+        key: :profile_directory,
+        type: :boolean,
+        description: "Enable profile directory."
+      },
+      %{
+        key: :privileged_staff,
+        type: :boolean,
+        description:
+          "Let moderators access sensitive data (e.g. updating user credentials, get password reset token, delete users, index and read private statuses and chats)"
       }
     ]
   },
index 6c1ecb559671e557032f59724a598baa8ceef543..518aca11429e0e49cdece9d5246b82b7bfe432e7 100644 (file)
@@ -383,12 +383,6 @@ Pleroma is generally compatible with the Mastodon 2.7.2 API, but some newer feat
 
 - `GET /api/v1/endorsements`: Returns an empty array, `[]`
 
-### Profile directory
-
-*Added in Mastodon 3.0.0*
-
-- `GET /api/v1/directory`: Returns HTTP 404
-
 ### Featured tags
 
 *Added in Mastodon 3.0.0*
index 8f6422da04bef9d546b3e5f15bb1064b9f62368b..0e7367a726fb086875295684c1c6fdbeb75acfe3 100644 (file)
@@ -159,10 +159,12 @@ See [Admin-API](admin_api.md)
   "muting": false,
   "muting_notifications": false,
   "subscribing": true,
+  "notifying": true,
   "requested": false,
   "domain_blocking": false,
   "showing_reblogs": true,
-  "endorsed": false
+  "endorsed": false,
+  "note": ""
 }
 ```
 
@@ -183,10 +185,12 @@ See [Admin-API](admin_api.md)
   "muting": false,
   "muting_notifications": false,
   "subscribing": false,
+  "notifying": false,
   "requested": false,
   "domain_blocking": false,
   "showing_reblogs": true,
-  "endorsed": false
+  "endorsed": false,
+  "note": ""
 }
 ```
 
index 9824e0a4ac3353ff195014cdcb591f6d1b809dda..952579c7f87dab951739930a05b9e1eff32ec8e5 100644 (file)
@@ -61,6 +61,11 @@ defmodule Pleroma.Application do
 
     adapter = Application.get_env(:tesla, :adapter)
 
+    if match?({Tesla.Adapter.Finch, _}, adapter) do
+      Logger.info("Starting Finch")
+      Finch.start_link(name: MyFinch)
+    end
+
     if adapter == Tesla.Adapter.Gun do
       if version = Pleroma.OTPVersion.version() do
         [major, minor] =
index 06dd29fea037ae36760c7c7ffe5bac7c84baeb2d..ce144559f6dc70945b55dbcadb14843d2d71dfb4 100644 (file)
@@ -25,5 +25,6 @@ defmodule Pleroma.ReverseProxy.Client.Wrapper do
 
   defp client(Tesla.Adapter.Hackney), do: Pleroma.ReverseProxy.Client.Hackney
   defp client(Tesla.Adapter.Gun), do: Pleroma.ReverseProxy.Client.Tesla
+  defp client({Tesla.Adapter.Finch, _}), do: Pleroma.ReverseProxy.Client.Hackney
   defp client(_), do: Pleroma.Config.get!(Pleroma.ReverseProxy.Client)
 end
index 10165c1b221f9a84f8c6690e756da5fef13d2f6b..35e245237ec5abf5004b890df5e273eddd1ac247 100644 (file)
@@ -12,10 +12,16 @@ defmodule Pleroma.Telemetry.Logger do
     [:pleroma, :connection_pool, :reclaim, :stop],
     [:pleroma, :connection_pool, :provision_failure],
     [:pleroma, :connection_pool, :client, :dead],
-    [:pleroma, :connection_pool, :client, :add]
+    [:pleroma, :connection_pool, :client, :add],
+    [:pleroma, :repo, :query]
   ]
   def attach do
-    :telemetry.attach_many("pleroma-logger", @events, &handle_event/4, [])
+    :telemetry.attach_many(
+      "pleroma-logger",
+      @events,
+      &Pleroma.Telemetry.Logger.handle_event/4,
+      []
+    )
   end
 
   # Passing anonymous functions instead of strings to logger is intentional,
@@ -87,4 +93,64 @@ defmodule Pleroma.Telemetry.Logger do
   end
 
   def handle_event([:pleroma, :connection_pool, :client, :add], _, _, _), do: :ok
+
+  def handle_event(
+        [:pleroma, :repo, :query] = _name,
+        %{query_time: query_time} = measurements,
+        %{source: source} = metadata,
+        config
+      ) do
+    logging_config = Pleroma.Config.get([:telemetry, :slow_queries_logging], [])
+
+    if logging_config[:enabled] &&
+         logging_config[:min_duration] &&
+         query_time > logging_config[:min_duration] and
+         (is_nil(logging_config[:exclude_sources]) or
+            source not in logging_config[:exclude_sources]) do
+      log_slow_query(measurements, metadata, config)
+    else
+      :ok
+    end
+  end
+
+  defp log_slow_query(
+         %{query_time: query_time} = _measurements,
+         %{source: _source, query: query, params: query_params, repo: repo} = _metadata,
+         _config
+       ) do
+    sql_explain =
+      with {:ok, %{rows: explain_result_rows}} <-
+             repo.query("EXPLAIN " <> query, query_params, log: false) do
+        Enum.map_join(explain_result_rows, "\n", & &1)
+      end
+
+    {:current_stacktrace, stacktrace} = Process.info(self(), :current_stacktrace)
+
+    pleroma_stacktrace =
+      Enum.filter(stacktrace, fn
+        {__MODULE__, _, _, _} ->
+          false
+
+        {mod, _, _, _} ->
+          mod
+          |> to_string()
+          |> String.starts_with?("Elixir.Pleroma.")
+      end)
+
+    Logger.warn(fn ->
+      """
+      Slow query!
+
+      Total time: #{round(query_time / 1_000)} ms
+
+      #{query}
+
+      #{inspect(query_params, limit: :infinity)}
+
+      #{sql_explain}
+
+      #{Exception.format_stacktrace(pleroma_stacktrace)}
+      """
+    end)
+  end
 end
index c25023dc12b1c1b8b9b20e1b3b95fae17ec4aa28..390de1e2d95c9326f9a558930ed5a7d9c873be0c 100644 (file)
@@ -149,6 +149,7 @@ defmodule Pleroma.User do
     field(:disclose_client, :boolean, default: true)
     field(:pinned_objects, :map, default: %{})
     field(:is_suggested, :boolean, default: false)
+    field(:last_status_at, :naive_datetime)
 
     embeds_one(
       :notification_settings,
@@ -2499,4 +2500,16 @@ defmodule Pleroma.User do
     |> where([u], u.local == true)
     |> Repo.aggregate(:count)
   end
+
+  def update_last_status_at(user) do
+    User
+    |> where(id: ^user.id)
+    |> update([u], set: [last_status_at: fragment("NOW()")])
+    |> select([u], u)
+    |> Repo.update_all([])
+    |> case do
+      {1, [user]} -> set_cache(user)
+      _ -> {:error, user}
+    end
+  end
 end
index 6d4a4ead6c480797ebcc2bed1efdf94389ed99fc..bf78cb32d3f4336460684418a82e7e976da79c5b 100644 (file)
@@ -47,6 +47,7 @@ defmodule Pleroma.User.Query do
             is_admin: boolean(),
             is_moderator: boolean(),
             is_suggested: boolean(),
+            is_discoverable: boolean(),
             super_users: boolean(),
             invisible: boolean(),
             internal: boolean(),
@@ -172,6 +173,10 @@ defmodule Pleroma.User.Query do
     where(query, [u], u.is_suggested == ^bool)
   end
 
+  defp compose_query({:is_discoverable, bool}, query) do
+    where(query, [u], u.is_discoverable == ^bool)
+  end
+
   defp compose_query({:followers, %User{id: id}}, query) do
     query
     |> where([u], u.id != ^id)
diff --git a/lib/pleroma/user_note.ex b/lib/pleroma/user_note.ex
new file mode 100644 (file)
index 0000000..5e82d35
--- /dev/null
@@ -0,0 +1,52 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.UserNote do
+  use Ecto.Schema
+
+  import Ecto.Changeset
+  import Ecto.Query
+
+  alias Pleroma.Repo
+  alias Pleroma.User
+  alias Pleroma.UserNote
+
+  schema "user_notes" do
+    belongs_to(:source, User, type: FlakeId.Ecto.CompatType)
+    belongs_to(:target, User, type: FlakeId.Ecto.CompatType)
+    field(:comment, :string)
+
+    timestamps()
+  end
+
+  def changeset(%UserNote{} = user_note, params \\ %{}) do
+    user_note
+    |> cast(params, [:source_id, :target_id, :comment])
+    |> validate_required([:source_id, :target_id])
+  end
+
+  def show(%User{} = source, %User{} = target) do
+    with %UserNote{} = note <-
+           UserNote
+           |> where(source_id: ^source.id, target_id: ^target.id)
+           |> Repo.one() do
+      note.comment
+    else
+      _ -> ""
+    end
+  end
+
+  def create(%User{} = source, %User{} = target, comment) do
+    %UserNote{}
+    |> changeset(%{
+      source_id: source.id,
+      target_id: target.id,
+      comment: comment
+    })
+    |> Repo.insert(
+      on_conflict: {:replace, [:comment]},
+      conflict_target: [:source_id, :target_id]
+    )
+  end
+end
index 8324ca22c80bcedace3bd1a2d1f8985d941c0945..7560969525f041506c629c488a700aa92cee0d92 100644 (file)
@@ -81,6 +81,10 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
     if is_public?(object), do: User.decrease_note_count(actor), else: {:ok, actor}
   end
 
+  def update_last_status_at_if_public(actor, object) do
+    if is_public?(object), do: User.update_last_status_at(actor), else: {:ok, actor}
+  end
+
   defp increase_replies_count_if_reply(%{
          "object" => %{"inReplyTo" => reply_ap_id} = object,
          "type" => "Create"
@@ -288,6 +292,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
          _ <- increase_replies_count_if_reply(create_data),
          {:quick_insert, false, activity} <- {:quick_insert, quick_insert?, activity},
          {:ok, _actor} <- increase_note_count_if_public(actor, activity),
+         {:ok, _actor} <- update_last_status_at_if_public(actor, activity),
          _ <- notify_and_stream(activity),
          :ok <- maybe_schedule_poll_notifications(activity),
          :ok <- maybe_federate(activity) do
index d55a4b340c9a189f193928add8076bbe3c3dda65..39d37fbcb8210e982d43021b02c7325d6f5bdc5a 100644 (file)
@@ -199,6 +199,7 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
          %User{} = user <- User.get_cached_by_ap_id(activity.data["actor"]) do
       {:ok, notifications} = Notification.create_notifications(activity, do_send: false)
       {:ok, _user} = ActivityPub.increase_note_count_if_public(user, object)
+      {:ok, _user} = ActivityPub.update_last_status_at_if_public(user, object)
 
       if in_reply_to = object.data["type"] != "Answer" && object.data["inReplyTo"] do
         Object.increase_replies_count(in_reply_to)
index 5836cab50dcaca2b44759357699a7316094ff8ce..f5304d7d6fd4d79cdcadbc51b0df680618daad46 100644 (file)
@@ -226,6 +226,12 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
                 type: :boolean,
                 description: "Receive this account's reblogs in home timeline? Defaults to true.",
                 default: true
+              },
+              notify: %Schema{
+                type: :boolean,
+                description:
+                  "Receive notifications for all statuses posted by the account? Defaults to false.",
+                default: false
               }
             }
           },
@@ -328,6 +334,29 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
     }
   end
 
+  def note_operation do
+    %Operation{
+      tags: ["Account actions"],
+      summary: "Set a private note about a user.",
+      operationId: "AccountController.note",
+      security: [%{"oAuth" => ["follow", "write:accounts"]}],
+      requestBody: request_body("Parameters", note_request()),
+      description: "Create a note for the given account.",
+      parameters: [
+        %Reference{"$ref": "#/components/parameters/accountIdOrNickname"},
+        Operation.parameter(
+          :comment,
+          :query,
+          %Schema{type: :string},
+          "Account note body"
+        )
+      ],
+      responses: %{
+        200 => Operation.response("Relationship", "application/json", AccountRelationship)
+      }
+    }
+  end
+
   def follow_by_uri_operation do
     %Operation{
       tags: ["Account actions"],
@@ -705,9 +734,11 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
           "blocked_by" => true,
           "muting" => false,
           "muting_notifications" => false,
+          "note" => "",
           "requested" => false,
           "domain_blocking" => false,
           "subscribing" => false,
+          "notifying" => false,
           "endorsed" => true
         },
         %{
@@ -719,9 +750,11 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
           "blocked_by" => true,
           "muting" => true,
           "muting_notifications" => false,
+          "note" => "",
           "requested" => true,
           "domain_blocking" => false,
           "subscribing" => false,
+          "notifying" => false,
           "endorsed" => false
         },
         %{
@@ -733,9 +766,11 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
           "blocked_by" => false,
           "muting" => true,
           "muting_notifications" => false,
+          "note" => "",
           "requested" => false,
           "domain_blocking" => true,
           "subscribing" => true,
+          "notifying" => true,
           "endorsed" => false
         }
       ]
@@ -780,6 +815,23 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
     }
   end
 
+  defp note_request do
+    %Schema{
+      title: "AccountNoteRequest",
+      description: "POST body for adding a note for an account",
+      type: :object,
+      properties: %{
+        comment: %Schema{
+          type: :string,
+          description: "Account note body"
+        }
+      },
+      example: %{
+        "comment" => "Example note"
+      }
+    }
+  end
+
   defp array_of_lists do
     %Schema{
       title: "ArrayOfLists",
index dfb1c71706c4c4ed6b6be97f986f1c6af97aa81d..2284ac127658778ec9fce13d1d35b0b28f761da3 100644 (file)
@@ -6,6 +6,7 @@ defmodule Pleroma.Web.ApiSpec.AppOperation do
   alias OpenApiSpex.Operation
   alias OpenApiSpex.Schema
   alias Pleroma.Web.ApiSpec.Helpers
+  alias Pleroma.Web.ApiSpec.Schemas.App
 
   @spec open_api_operation(atom) :: Operation.t()
   def open_api_operation(action) do
@@ -22,7 +23,7 @@ defmodule Pleroma.Web.ApiSpec.AppOperation do
       operationId: "AppController.create",
       requestBody: Helpers.request_body("Parameters", create_request(), required: true),
       responses: %{
-        200 => Operation.response("App", "application/json", create_response()),
+        200 => Operation.response("App", "application/json", App),
         422 =>
           Operation.response(
             "Unprocessable Entity",
@@ -119,30 +120,4 @@ defmodule Pleroma.Web.ApiSpec.AppOperation do
       }
     }
   end
-
-  defp create_response do
-    %Schema{
-      title: "AppCreateResponse",
-      description: "Response schema for an app",
-      type: :object,
-      properties: %{
-        id: %Schema{type: :string},
-        name: %Schema{type: :string},
-        client_id: %Schema{type: :string},
-        client_secret: %Schema{type: :string},
-        redirect_uri: %Schema{type: :string},
-        vapid_key: %Schema{type: :string},
-        website: %Schema{type: :string, nullable: true}
-      },
-      example: %{
-        "id" => "123",
-        "name" => "My App",
-        "client_id" => "TWhM-tNSuncnqN7DBJmoyeLnk6K3iJJ71KKXxgL1hPM",
-        "client_secret" => "ZEaFUFmF0umgBX1qKJDjaU99Q31lDkOU8NutzTOoliw",
-        "vapid_key" =>
-          "BCk-QqERU0q-CfYZjcuB6lnyyOYfJ2AifKqfeGIm7Z-HiTU5T9eTG5GxVA0_OH5mMlI4UkkDTpaZwozy0TzdZ2M=",
-        "website" => "https://myapp.com/"
-      }
-    }
-  end
 end
diff --git a/lib/pleroma/web/api_spec/operations/directory_operation.ex b/lib/pleroma/web/api_spec/operations/directory_operation.ex
new file mode 100644 (file)
index 0000000..9be965f
--- /dev/null
@@ -0,0 +1,41 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ApiSpec.DirectoryOperation do
+  alias OpenApiSpex.Operation
+  alias Pleroma.Web.ApiSpec.AccountOperation
+  alias Pleroma.Web.ApiSpec.Schemas.ApiError
+  alias Pleroma.Web.ApiSpec.Schemas.BooleanLike
+
+  import Pleroma.Web.ApiSpec.Helpers
+
+  def open_api_operation(action) do
+    operation = String.to_existing_atom("#{action}_operation")
+    apply(__MODULE__, operation, [])
+  end
+
+  def index_operation do
+    %Operation{
+      tags: ["Directory"],
+      summary: "Profile directory",
+      operationId: "DirectoryController.index",
+      parameters:
+        [
+          Operation.parameter(
+            :order,
+            :query,
+            :string,
+            "Order by recent activity or account creation",
+            required: nil
+          ),
+          Operation.parameter(:local, :query, BooleanLike, "Include local users only")
+        ] ++ pagination_params(),
+      responses: %{
+        200 =>
+          Operation.response("Accounts", "application/json", AccountOperation.array_of_accounts()),
+        404 => Operation.response("Not Found", "application/json", ApiError)
+      }
+    }
+  end
+end
diff --git a/lib/pleroma/web/api_spec/operations/pleroma_app_operation.ex b/lib/pleroma/web/api_spec/operations/pleroma_app_operation.ex
new file mode 100644 (file)
index 0000000..582a169
--- /dev/null
@@ -0,0 +1,31 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ApiSpec.PleromaAppOperation do
+  alias OpenApiSpex.Operation
+  alias OpenApiSpex.Schema
+  alias Pleroma.Web.ApiSpec.Schemas.App
+
+  def open_api_operation(action) do
+    operation = String.to_existing_atom("#{action}_operation")
+    apply(__MODULE__, operation, [])
+  end
+
+  @spec index_operation() :: Operation.t()
+  def index_operation do
+    %Operation{
+      tags: ["Applications"],
+      summary: "List applications",
+      description: "List the OAuth applications for the current user",
+      operationId: "AppController.index",
+      responses: %{
+        200 => Operation.response("Array of App", "application/json", array_of_apps())
+      }
+    }
+  end
+
+  defp array_of_apps do
+    %Schema{type: :array, items: App, example: [App.schema().example]}
+  end
+end
index 5a2b0bc49d574a79ed31b081fde5f01e742c03db..2a701066d409549e5fcdde77fcc04a0558cc7db9 100644 (file)
@@ -239,6 +239,32 @@ defmodule Pleroma.Web.ApiSpec.TwitterUtilOperation do
     }
   end
 
+  def remote_interaction_operation do
+    %Operation{
+      tags: ["Accounts"],
+      summary: "Remote interaction",
+      operationId: "UtilController.remote_interaction",
+      requestBody: request_body("Parameters", remote_interaction_request(), required: true),
+      responses: %{
+        200 =>
+          Operation.response("Remote interaction URL", "application/json", %Schema{type: :object})
+      }
+    }
+  end
+
+  defp remote_interaction_request do
+    %Schema{
+      title: "RemoteInteractionRequest",
+      description: "POST body for remote interaction",
+      type: :object,
+      required: [:ap_id, :profile],
+      properties: %{
+        ap_id: %Schema{type: :string, description: "Profile or status ActivityPub ID"},
+        profile: %Schema{type: :string, description: "Remote profile webfinger"}
+      }
+    }
+  end
+
   defp delete_account_request do
     %Schema{
       title: "AccountDeleteRequest",
index bd7143ab959420bb00e9b2ab2264cf3777f27fa1..548e7054412f32a3874ef7bce139828bed0593ee 100644 (file)
@@ -194,9 +194,11 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Account do
           "id" => "9tKi3esbG7OQgZ2920",
           "muting" => false,
           "muting_notifications" => false,
+          "note" => "",
           "requested" => false,
           "showing_reblogs" => true,
-          "subscribing" => false
+          "subscribing" => false,
+          "notifying" => false
         },
         "settings_store" => %{
           "pleroma-fe" => %{}
index 16b73ebb4191e02d32be50bcbc8e34b7ba13a6ee..5d9e3b56e3310f2893fb784cc2921d9c4aacfdce 100644 (file)
@@ -22,9 +22,11 @@ defmodule Pleroma.Web.ApiSpec.Schemas.AccountRelationship do
       id: FlakeID,
       muting: %Schema{type: :boolean},
       muting_notifications: %Schema{type: :boolean},
+      note: %Schema{type: :string},
       requested: %Schema{type: :boolean},
       showing_reblogs: %Schema{type: :boolean},
-      subscribing: %Schema{type: :boolean}
+      subscribing: %Schema{type: :boolean},
+      notifying: %Schema{type: :boolean}
     },
     example: %{
       "blocked_by" => false,
@@ -36,9 +38,11 @@ defmodule Pleroma.Web.ApiSpec.Schemas.AccountRelationship do
       "id" => "9tKi3esbG7OQgZ2920",
       "muting" => false,
       "muting_notifications" => false,
+      "note" => "",
       "requested" => false,
       "showing_reblogs" => true,
-      "subscribing" => false
+      "subscribing" => false,
+      "notifying" => false
     }
   })
 end
diff --git a/lib/pleroma/web/api_spec/schemas/app.ex b/lib/pleroma/web/api_spec/schemas/app.ex
new file mode 100644 (file)
index 0000000..c3d1af3
--- /dev/null
@@ -0,0 +1,33 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ApiSpec.Schemas.App do
+  alias OpenApiSpex.Schema
+
+  require OpenApiSpex
+
+  OpenApiSpex.schema(%{
+    title: "App",
+    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
index 3d042dc19a225f2d4e268a97695241f9a1c2840d..3caab0f00dd7f723144e73d89d092569b30a115b 100644 (file)
@@ -282,9 +282,11 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Status do
             "id" => "9toJCsKN7SmSf3aj5c",
             "muting" => false,
             "muting_notifications" => false,
+            "note" => "",
             "requested" => false,
             "showing_reblogs" => true,
-            "subscribing" => false
+            "subscribing" => false,
+            "notifying" => false
           },
           "skip_thread_containment" => false,
           "tags" => []
index 3eae0a64618728d261ed04ce3fc707d14deb9830..399a34217e7a1c37278cd0bc6d18e7bd217d3287 100644 (file)
@@ -15,6 +15,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
 
   alias Pleroma.Maps
   alias Pleroma.User
+  alias Pleroma.UserNote
   alias Pleroma.Web.ActivityPub.ActivityPub
   alias Pleroma.Web.ActivityPub.Builder
   alias Pleroma.Web.ActivityPub.Pipeline
@@ -53,7 +54,11 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
     when action in [:verify_credentials, :endorsements, :identity_proofs]
   )
 
-  plug(OAuthScopesPlug, %{scopes: ["write:accounts"]} when action == :update_credentials)
+  plug(
+    OAuthScopesPlug,
+    %{scopes: ["write:accounts"]}
+    when action in [:update_credentials, :note]
+  )
 
   plug(OAuthScopesPlug, %{scopes: ["read:lists"]} when action == :lists)
 
@@ -79,7 +84,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
   plug(OAuthScopesPlug, %{scopes: ["follow", "write:mutes"]} when action in [:mute, :unmute])
 
   @relationship_actions [:follow, :unfollow]
-  @needs_account ~W(followers following lists follow unfollow mute unmute block unblock)a
+  @needs_account ~W(followers following lists follow unfollow mute unmute block unblock note)a
 
   plug(
     RateLimiter,
@@ -435,6 +440,16 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
     end
   end
 
+  @doc "POST /api/v1/accounts/:id/note"
+  def note(
+        %{assigns: %{user: noter, account: target}, body_params: %{comment: comment}} = conn,
+        _params
+      ) do
+    with {:ok, _user_note} <- UserNote.create(noter, target, comment) do
+      render(conn, "relationship.json", user: noter, target: target)
+    end
+  end
+
   @doc "POST /api/v1/follows"
   def follow_by_uri(%{body_params: %{uri: uri}} = conn, _) do
     case User.get_cached_by_nickname(uri) do
index 93e63ba03826fdaa2f33a0608c95aa61b61b5aea..8d18140add66b39d3881ce081cb5ed5dfaae843b 100644 (file)
@@ -10,7 +10,9 @@ defmodule Pleroma.Web.MastodonAPI.AppController do
 
   use Pleroma.Web, :controller
 
+  alias Pleroma.Maps
   alias Pleroma.Repo
+  alias Pleroma.User
   alias Pleroma.Web.OAuth.App
   alias Pleroma.Web.OAuth.Scopes
   alias Pleroma.Web.OAuth.Token
@@ -26,11 +28,13 @@ defmodule Pleroma.Web.MastodonAPI.AppController do
   @doc "POST /api/v1/apps"
   def create(%{body_params: params} = conn, _params) do
     scopes = Scopes.fetch_scopes(params, ["read"])
+    user_id = get_user_id(conn)
 
     app_attrs =
       params
       |> Map.take([:client_name, :redirect_uris, :website])
       |> Map.put(:scopes, scopes)
+      |> Maps.put_if_present(:user_id, user_id)
 
     with cs <- App.register_changeset(%App{}, app_attrs),
          {:ok, app} <- Repo.insert(cs) do
@@ -38,6 +42,9 @@ defmodule Pleroma.Web.MastodonAPI.AppController do
     end
   end
 
+  defp get_user_id(%{assigns: %{user: %User{id: user_id}}}), do: user_id
+  defp get_user_id(_conn), do: nil
+
   @doc """
   GET /api/v1/apps/verify_credentials
   Gets compact non-secret representation of the app. Supports app tokens and user tokens.
diff --git a/lib/pleroma/web/mastodon_api/controllers/directory_controller.ex b/lib/pleroma/web/mastodon_api/controllers/directory_controller.ex
new file mode 100644 (file)
index 0000000..45ef227
--- /dev/null
@@ -0,0 +1,82 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.MastodonAPI.DirectoryController do
+  use Pleroma.Web, :controller
+
+  import Ecto.Query
+  alias Pleroma.Pagination
+  alias Pleroma.User
+  alias Pleroma.UserRelationship
+  alias Pleroma.Web.MastodonAPI.AccountView
+
+  require Logger
+
+  plug(Pleroma.Web.ApiSpec.CastAndValidate)
+
+  plug(:skip_auth when action == "index")
+
+  defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.DirectoryOperation
+
+  @doc "GET /api/v1/directory"
+  def index(%{assigns: %{user: user}} = conn, params) do
+    with true <- Pleroma.Config.get([:instance, :profile_directory]) do
+      limit = Map.get(params, :limit, 20) |> min(80)
+
+      users =
+        User.Query.build(%{is_discoverable: true, invisible: false, limit: limit})
+        |> order_by_creation_date(params)
+        |> exclude_remote(params)
+        |> exclude_user(user)
+        |> exclude_relationships(user, [:block, :mute])
+        |> Pagination.fetch_paginated(params, :offset)
+
+      conn
+      |> put_view(AccountView)
+      |> render("index.json", for: user, users: users, as: :user)
+    else
+      _ -> json(conn, [])
+    end
+  end
+
+  defp order_by_creation_date(query, %{order: "new"}) do
+    query
+  end
+
+  defp order_by_creation_date(query, _params) do
+    query
+    |> order_by([u], desc_nulls_last: u.last_status_at)
+  end
+
+  defp exclude_remote(query, %{local: true}) do
+    where(query, [u], u.local == true)
+  end
+
+  defp exclude_remote(query, _params) do
+    query
+  end
+
+  defp exclude_user(query, %User{id: user_id}) do
+    where(query, [u], u.id != ^user_id)
+  end
+
+  defp exclude_user(query, _user) do
+    query
+  end
+
+  defp exclude_relationships(query, %User{id: user_id}, relationship_types) do
+    query
+    |> join(:left, [u], r in UserRelationship,
+      as: :user_relationships,
+      on:
+        r.target_id == u.id and r.source_id == ^user_id and
+          r.relationship_type in ^relationship_types
+    )
+    |> where([user_relationships: r], is_nil(r.target_id))
+  end
+
+  defp exclude_relationships(query, _user, _relationship_types) do
+    query
+  end
+end
index 71479550eb61402617bc97d3dd560502135dd4a7..23846b36a0a3101784b2e27358b5de14407c26c3 100644 (file)
@@ -24,6 +24,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPI do
     with {:ok, follower, _followed, _} <- result do
       options = cast_params(params)
       set_reblogs_visibility(options[:reblogs], result)
+      set_subscription(options[:notify], result)
       {:ok, follower}
     end
   end
@@ -36,6 +37,16 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPI do
     CommonAPI.show_reblogs(follower, followed)
   end
 
+  defp set_subscription(true, {:ok, follower, followed, _}) do
+    User.subscribe(follower, followed)
+  end
+
+  defp set_subscription(false, {:ok, follower, followed, _}) do
+    User.unsubscribe(follower, followed)
+  end
+
+  defp set_subscription(_, _), do: {:ok, nil}
+
   @spec get_followers(User.t(), map()) :: list(User.t())
   def get_followers(user, params \\ %{}) do
     user
@@ -73,7 +84,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPI do
       exclude_visibilities: {:array, :string},
       reblogs: :boolean,
       with_muted: :boolean,
-      account_ap_id: :string
+      account_ap_id: :string,
+      notify: :boolean
     }
 
     changeset = cast({%{}, param_types}, params, Map.keys(param_types))
index 6114e12b14405db25a879c2a14fe2e233e98d7c5..4b15b163582b824074c53d9a5cabac21dc9716fd 100644 (file)
@@ -7,6 +7,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
 
   alias Pleroma.FollowingRelationship
   alias Pleroma.User
+  alias Pleroma.UserNote
   alias Pleroma.UserRelationship
   alias Pleroma.Web.CommonAPI.Utils
   alias Pleroma.Web.MastodonAPI.AccountView
@@ -101,6 +102,15 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
         User.following?(target, reading_user)
       end
 
+    subscribing =
+      UserRelationship.exists?(
+        user_relationships,
+        :inverse_subscription,
+        target,
+        reading_user,
+        &User.subscribed_to?(&2, &1)
+      )
+
     # NOTE: adjust UserRelationship.view_relationships_option/2 on new relation-related flags
     %{
       id: to_string(target.id),
@@ -138,14 +148,8 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
           target,
           &User.muted_notifications?(&1, &2)
         ),
-      subscribing:
-        UserRelationship.exists?(
-          user_relationships,
-          :inverse_subscription,
-          target,
-          reading_user,
-          &User.subscribed_to?(&2, &1)
-        ),
+      subscribing: subscribing,
+      notifying: subscribing,
       requested: follow_state == :follow_pending,
       domain_blocking: User.blocks_domain?(reading_user, target),
       showing_reblogs:
@@ -156,7 +160,12 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
           target,
           &User.muting_reblogs?(&1, &2)
         ),
-      endorsed: false
+      endorsed: false,
+      note:
+        UserNote.show(
+          reading_user,
+          target
+        )
     }
   end
 
@@ -261,6 +270,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
           actor_type: user.actor_type
         }
       },
+      last_status_at: user.last_status_at,
 
       # Pleroma extensions
       # Note: it's insecure to output :email but fully-qualified nickname may serve as safe stub
index ec7d150a97848603303d16ecee3950bd1d3c22ac..8e657ee0f379acddeacbfc69ba2c2031282e778d 100644 (file)
@@ -45,7 +45,8 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do
           features: features(),
           federation: federation(),
           fields_limits: fields_limits(),
-          post_formats: Config.get([:instance, :allowed_post_formats])
+          post_formats: Config.get([:instance, :allowed_post_formats]),
+          privileged_staff: Config.get([:instance, :privileged_staff])
         },
         stats: %{mau: Pleroma.User.active_user_count()},
         vapid_public_key: Keyword.get(Pleroma.Web.Push.vapid_config(), :public_key)
@@ -87,6 +88,9 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do
       "pleroma_chat_messages",
       if Config.get([:instance, :show_reactions]) do
         "exposable_reactions"
+      end,
+      if Config.get([:instance, :profile_directory]) do
+        "profile_directory"
       end
     ]
     |> Enum.filter(& &1)
index 3781781c849ae3ad8618bd0c8ddc00edd2fb5f8d..80a2ce67681c4f9b07de3b79c9430de0f9eee75f 100644 (file)
@@ -69,7 +69,8 @@ defmodule Pleroma.Web.Nodeinfo.Nodeinfo do
         mailerEnabled: Config.get([Pleroma.Emails.Mailer, :enabled], false),
         features: features,
         restrictedNicknames: Config.get([Pleroma.User, :restricted_nicknames]),
-        skipThreadContainment: Config.get([:instance, :skip_thread_containment], false)
+        skipThreadContainment: Config.get([:instance, :skip_thread_containment], false),
+        privilegedStaff: Config.get([:instance, :privileged_staff])
       }
     }
   end
index 38275001036177a142cb0f2d436242bd2cc9063a..dacfbadc8ed174c881e5664a8b7de58f2bb89077 100644 (file)
@@ -7,6 +7,7 @@ defmodule Pleroma.Web.OAuth.App do
   import Ecto.Changeset
   import Ecto.Query
   alias Pleroma.Repo
+  alias Pleroma.User
 
   @type t :: %__MODULE__{}
 
@@ -19,6 +20,8 @@ defmodule Pleroma.Web.OAuth.App do
     field(:client_secret, :string)
     field(:trusted, :boolean, default: false)
 
+    belongs_to(:user, User, type: FlakeId.Ecto.CompatType)
+
     has_many(:oauth_authorizations, Pleroma.Web.OAuth.Authorization, on_delete: :delete_all)
     has_many(:oauth_tokens, Pleroma.Web.OAuth.Token, on_delete: :delete_all)
 
@@ -27,7 +30,7 @@ defmodule Pleroma.Web.OAuth.App do
 
   @spec changeset(t(), map()) :: Ecto.Changeset.t()
   def changeset(struct, params) do
-    cast(struct, params, [:client_name, :redirect_uris, :scopes, :website, :trusted])
+    cast(struct, params, [:client_name, :redirect_uris, :scopes, :website, :trusted, :user_id])
   end
 
   @spec register_changeset(t(), map()) :: Ecto.Changeset.t()
@@ -129,6 +132,12 @@ defmodule Pleroma.Web.OAuth.App do
     {:ok, Repo.all(query), count}
   end
 
+  @spec get_user_apps(User.t()) :: {:ok, [t()], non_neg_integer()}
+  def get_user_apps(%User{id: user_id}) do
+    from(a in __MODULE__, where: a.user_id == ^user_id)
+    |> Repo.all()
+  end
+
   @spec destroy(pos_integer()) :: {:ok, t()} | {:error, Ecto.Changeset.t()}
   def destroy(id) do
     with %__MODULE__{} = app <- Repo.get(__MODULE__, id) do
diff --git a/lib/pleroma/web/pleroma_api/controllers/app_controller.ex b/lib/pleroma/web/pleroma_api/controllers/app_controller.ex
new file mode 100644 (file)
index 0000000..d857f42
--- /dev/null
@@ -0,0 +1,23 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.PleromaAPI.AppController do
+  use Pleroma.Web, :controller
+
+  alias Pleroma.Web.OAuth.App
+  alias Pleroma.Web.Plugs.OAuthScopesPlug
+
+  plug(OAuthScopesPlug, %{scopes: ["follow", "read"]} when action in [:index])
+
+  plug(Pleroma.Web.ApiSpec.CastAndValidate)
+
+  defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaAppOperation
+
+  @doc "GET /api/v1/pleroma/apps"
+  def index(%{assigns: %{user: user}} = conn, _params) do
+    with apps <- App.get_user_apps(user) do
+      render(conn, "index.json", %{apps: apps})
+    end
+  end
+end
diff --git a/lib/pleroma/web/pleroma_api/views/app_view.ex b/lib/pleroma/web/pleroma_api/views/app_view.ex
new file mode 100644 (file)
index 0000000..6b5d838
--- /dev/null
@@ -0,0 +1,11 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.PleromaAPI.AppView do
+  use Pleroma.Web, :view
+
+  def render("index.json", %{apps: apps}) do
+    render_many(apps, Pleroma.Web.MastodonAPI.AppView, "show.json")
+  end
+end
diff --git a/lib/pleroma/web/plugs/ensure_staff_privileged_plug.ex b/lib/pleroma/web/plugs/ensure_staff_privileged_plug.ex
new file mode 100644 (file)
index 0000000..c6ed456
--- /dev/null
@@ -0,0 +1,36 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.Plugs.EnsureStaffPrivilegedPlug do
+  @moduledoc """
+  Ensures staff are privileged enough to do certain tasks.
+  """
+  import Pleroma.Web.TranslationHelpers
+  import Plug.Conn
+
+  alias Pleroma.Config
+  alias Pleroma.User
+
+  def init(options) do
+    options
+  end
+
+  def call(%{assigns: %{user: %User{is_admin: true}}} = conn, _), do: conn
+
+  def call(%{assigns: %{user: %User{is_moderator: true}}} = conn, _) do
+    if Config.get!([:instance, :privileged_staff]) do
+      conn
+    else
+      conn
+      |> render_error(:forbidden, "User is not an admin.")
+      |> halt()
+    end
+  end
+
+  def call(conn, _) do
+    conn
+    |> render_error(:forbidden, "User is not a staff member.")
+    |> halt()
+  end
+end
index ae373e58c7d4502b9ce7f72564a4f1daaf9dd14c..b9b52b1e54d75577f3008f84b86ebba81fc0a53e 100644 (file)
@@ -101,6 +101,10 @@ defmodule Pleroma.Web.Router do
     plug(Pleroma.Web.Plugs.IdempotencyPlug)
   end
 
+  pipeline :require_privileged_staff do
+    plug(Pleroma.Web.Plugs.EnsureStaffPrivilegedPlug)
+  end
+
   pipeline :require_admin do
     plug(Pleroma.Web.Plugs.UserIsAdminPlug)
   end
@@ -151,6 +155,7 @@ defmodule Pleroma.Web.Router do
     get("/emoji", UtilController, :emoji)
     get("/captcha", UtilController, :captcha)
     get("/healthcheck", UtilController, :healthcheck)
+    post("/remote_interaction", UtilController, :remote_interaction)
   end
 
   scope "/api/v1/pleroma", Pleroma.Web do
@@ -194,7 +199,6 @@ defmodule Pleroma.Web.Router do
     post("/relay", RelayController, :follow)
     delete("/relay", RelayController, :unfollow)
 
-    get("/users/:nickname/password_reset", AdminAPIController, :get_password_reset)
     patch("/users/force_password_reset", AdminAPIController, :force_password_reset)
     get("/users/:nickname/credentials", AdminAPIController, :show_user_credentials)
     patch("/users/:nickname/credentials", AdminAPIController, :update_user_credentials)
@@ -227,6 +231,24 @@ defmodule Pleroma.Web.Router do
     post("/backups", AdminAPIController, :create_backup)
   end
 
+  # AdminAPI: admins and mods (staff) can perform these actions (if enabled by config)
+  scope "/api/v1/pleroma/admin", Pleroma.Web.AdminAPI do
+    pipe_through([:admin_api, :require_privileged_staff])
+
+    delete("/users", UserController, :delete)
+
+    get("/users/:nickname/password_reset", AdminAPIController, :get_password_reset)
+    patch("/users/:nickname/credentials", AdminAPIController, :update_user_credentials)
+
+    get("/users/:nickname/statuses", AdminAPIController, :list_user_statuses)
+    get("/users/:nickname/chats", AdminAPIController, :list_user_chats)
+
+    get("/statuses", StatusController, :index)
+
+    get("/chats/:id", ChatController, :show)
+    get("/chats/:id/messages", ChatController, :messages)
+  end
+
   # AdminAPI: admins and mods (staff) can perform these actions
   scope "/api/v1/pleroma/admin", Pleroma.Web.AdminAPI do
     pipe_through(:admin_api)
@@ -239,22 +261,13 @@ defmodule Pleroma.Web.Router do
     patch("/users/deactivate", UserController, :deactivate)
     patch("/users/approve", UserController, :approve)
 
-    delete("/users", UserController, :delete)
-
     post("/users/invite_token", InviteController, :create)
     get("/users/invites", InviteController, :index)
     post("/users/revoke_invite", InviteController, :revoke)
     post("/users/email_invite", InviteController, :email)
 
-    get("/users/:nickname/password_reset", AdminAPIController, :get_password_reset)
-    patch("/users/force_password_reset", AdminAPIController, :force_password_reset)
-    get("/users/:nickname/credentials", AdminAPIController, :show_user_credentials)
-    patch("/users/:nickname/credentials", AdminAPIController, :update_user_credentials)
-
     get("/users", UserController, :index)
     get("/users/:nickname", UserController, :show)
-    get("/users/:nickname/statuses", AdminAPIController, :list_user_statuses)
-    get("/users/:nickname/chats", AdminAPIController, :list_user_chats)
 
     get("/instances/:instance/statuses", InstanceController, :list_statuses)
     delete("/instances/:instance", InstanceController, :delete)
@@ -268,15 +281,12 @@ defmodule Pleroma.Web.Router do
     get("/statuses/:id", StatusController, :show)
     put("/statuses/:id", StatusController, :update)
     delete("/statuses/:id", StatusController, :delete)
-    get("/statuses", StatusController, :index)
 
     get("/moderation_log", AdminAPIController, :list_log)
 
     post("/reload_emoji", AdminAPIController, :reload_emoji)
     get("/stats", AdminAPIController, :stats)
 
-    get("/chats/:id", ChatController, :show)
-    get("/chats/:id/messages", ChatController, :messages)
     delete("/chats/:id/messages/:message_id", ChatController, :delete_message)
   end
 
@@ -385,6 +395,7 @@ defmodule Pleroma.Web.Router do
   scope "/api/v1/pleroma", Pleroma.Web.PleromaAPI do
     pipe_through(:api)
 
+    get("/apps", AppController, :index)
     get("/statuses/:id/reactions/:emoji", EmojiReactionController, :index)
     get("/statuses/:id/reactions", EmojiReactionController, :index)
   end
@@ -474,6 +485,7 @@ defmodule Pleroma.Web.Router do
     post("/accounts/:id/unblock", AccountController, :unblock)
     post("/accounts/:id/mute", AccountController, :mute)
     post("/accounts/:id/unmute", AccountController, :unmute)
+    post("/accounts/:id/note", AccountController, :note)
 
     get("/conversations", ConversationController, :index)
     post("/conversations/:id/read", ConversationController, :mark_as_read)
@@ -600,6 +612,8 @@ defmodule Pleroma.Web.Router do
     get("/timelines/tag/:tag", TimelineController, :hashtag)
 
     get("/polls/:id", PollController, :show)
+
+    get("/directory", DirectoryController, :index)
   end
 
   scope "/api/v2", Pleroma.Web.MastodonAPI do
@@ -650,6 +664,11 @@ defmodule Pleroma.Web.Router do
     get("/activities/:uuid", OStatus.OStatusController, :activity)
     get("/notice/:id", OStatus.OStatusController, :notice)
 
+    # Notice compatibility routes for other frontends
+    get("/@:nickname/:id", OStatus.OStatusController, :notice)
+    get("/@:nickname/posts/:id", OStatus.OStatusController, :notice)
+    get("/:nickname/status/:id", OStatus.OStatusController, :notice)
+
     # Mastodon compatibility routes
     get("/users/:nickname/statuses/:id", OStatus.OStatusController, :object)
     get("/users/:nickname/statuses/:id/activity", OStatus.OStatusController, :activity)
index 50f0927a36c207863cb4a372856a8b1fa9245b51..827c0a384d4ab3b2ba0920ab54968e5684b367c1 100644 (file)
@@ -167,6 +167,15 @@ defmodule Pleroma.Web.StaticFE.StaticFEController do
   defp assign_id(%{path_info: ["notice", notice_id]} = conn, _opts),
     do: assign(conn, :notice_id, notice_id)
 
+  defp assign_id(%{path_info: ["@" <> _nickname, notice_id]} = conn, _opts),
+    do: assign(conn, :notice_id, notice_id)
+
+  defp assign_id(%{path_info: ["@" <> _nickname, "posts", notice_id]} = conn, _opts),
+    do: assign(conn, :notice_id, notice_id)
+
+  defp assign_id(%{path_info: [_nickname, "status", notice_id]} = conn, _opts),
+    do: assign(conn, :notice_id, notice_id)
+
   defp assign_id(%{path_info: ["users", user_id]} = conn, _opts),
     do: assign(conn, :username_or_id, user_id)
 
index a4e44efddf3b9ef439edf4c85a9ee056939ffc72..ccbef6d9f11a85c80b04b9d180c334f526a01d6b 100644 (file)
@@ -62,6 +62,15 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do
     end
   end
 
+  def remote_interaction(%{body_params: %{ap_id: ap_id, profile: profile}} = conn, _params) do
+    with {:ok, %{"subscribe_address" => template}} <- WebFinger.finger(profile) do
+      conn
+      |> json(%{url: String.replace(template, "{uri}", ap_id)})
+    else
+      _e -> json(conn, %{error: "Couldn't find user"})
+    end
+  end
+
   def frontend_configurations(conn, _params) do
     render(conn, "frontend_configurations.json")
   end
diff --git a/mix.exs b/mix.exs
index 615f1d50b87ae4f8876b9986b7791347cf844ae0..360d499050ab28766a1ae676079c400b6b44486d 100644 (file)
--- a/mix.exs
+++ b/mix.exs
@@ -137,6 +137,7 @@ defmodule Pleroma.Mixfile do
       {:castore, "~> 0.1"},
       {:cowlib, "~> 2.9", override: true},
       {:gun, "~> 2.0.0-rc.1", override: true},
+      {:finch, "~> 0.10.0"},
       {:jason, "~> 1.2"},
       {:mogrify, "~> 0.9.1"},
       {:ex_aws, "~> 2.1.6"},
@@ -192,9 +193,7 @@ defmodule Pleroma.Mixfile do
        git: "https://git.pleroma.social/pleroma/elixir-libraries/elixir-captcha.git",
        ref: "e0f16822d578866e186a0974d65ad58cddc1e2ab"},
       {:restarter, path: "./restarter"},
-      {:majic,
-       git: "https://git.pleroma.social/pleroma/elixir-libraries/majic.git",
-       ref: "289cda1b6d0d70ccb2ba508a2b0bd24638db2880"},
+      {:majic, "~> 1.0"},
       {:eblurhash, "~> 1.1.0"},
       {:open_api_spex, "~> 3.10"},
       {:phoenix_live_dashboard, "~> 0.6.2"},
index 5b95272046634fe8ff4e36d2ca21dd6caead05c4..f371a6e41d6ed80b5a8184f2ed4da8b7a5c5a1c4 100644 (file)
--- a/mix.lock
+++ b/mix.lock
   "ex_machina": {:hex, :ex_machina, "2.7.0", "b792cc3127fd0680fecdb6299235b4727a4944a09ff0fa904cc639272cd92dc7", [:mix], [{:ecto, "~> 2.2 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_sql, "~> 3.0", [hex: :ecto_sql, repo: "hexpm", optional: true]}], "hexpm", "419aa7a39bde11894c87a615c4ecaa52d8f107bbdd81d810465186f783245bf8"},
   "ex_syslogger": {:hex, :ex_syslogger, "1.5.2", "72b6aa2d47a236e999171f2e1ec18698740f40af0bd02c8c650bf5f1fd1bac79", [:mix], [{:poison, ">= 1.5.0", [hex: :poison, repo: "hexpm", optional: true]}, {:syslog, "~> 1.1.0", [hex: :syslog, repo: "hexpm", optional: false]}], "hexpm", "ab9fab4136dbc62651ec6f16fa4842f10cf02ab4433fa3d0976c01be99398399"},
   "excoveralls": {:hex, :excoveralls, "0.12.3", "2142be7cb978a3ae78385487edda6d1aff0e482ffc6123877bb7270a8ffbcfe0", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "568a3e616c264283f5dea5b020783ae40eef3f7ee2163f7a67cbd7b35bcadada"},
-  "fast_html": {:hex, :fast_html, "2.0.4", "4910ee49f2f6b19692e3bf30bf97f1b6b7dac489cd6b0f34cd0fe3042c56ba30", [:make, :mix], [{:elixir_make, "~> 0.4", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.1.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}], "hexpm", "3bb49d541dfc02ad5e425904f53376d758c09f89e521afc7d2b174b3227761ea"},
+  "fast_html": {:hex, :fast_html, "2.0.5", "c61760340606c1077ff1f196f17834056cb1dd3d5cb92a9f2cabf28bc6221c3c", [:make, :mix], [{:elixir_make, "~> 0.4", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}], "hexpm", "605f4f4829443c14127694ebabb681778712ceecb4470ec32aa31012330e6506"},
   "fast_sanitize": {:hex, :fast_sanitize, "0.2.2", "3cbbaebaea6043865dfb5b4ecb0f1af066ad410a51470e353714b10c42007b81", [:mix], [{:fast_html, "~> 2.0", [hex: :fast_html, repo: "hexpm", optional: false]}, {:plug, "~> 1.8", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "69f204db9250afa94a0d559d9110139850f57de2b081719fbafa1e9a89e94466"},
   "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"},
+  "finch": {:hex, :finch, "0.10.0", "8e5e6101ae98e7f1ef830594f774411a2f9cbce4f92d8179502da69fbbff52bc", [:mix], [{:castore, "~> 0.1", [hex: :castore, repo: "hexpm", optional: false]}, {:mint, "~> 1.3", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "80324ba22edbdebca6fac05c8517e7457b79dfe101e3bf6b2f7c5c65c93a9077"},
   "flake_id": {:hex, :flake_id, "0.1.0", "7716b086d2e405d09b647121a166498a0d93d1a623bead243e1f74216079ccb3", [:mix], [{:base62, "~> 1.2", [hex: :base62, repo: "hexpm", optional: false]}, {:ecto, ">= 2.0.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm", "31fc8090fde1acd267c07c36ea7365b8604055f897d3a53dd967658c691bd827"},
   "floki": {:hex, :floki, "0.30.1", "75d35526d3a1459920b6e87fdbc2e0b8a3670f965dd0903708d2b267e0904c55", [:mix], [{:html_entities, "~> 0.5.0", [hex: :html_entities, repo: "hexpm", optional: false]}], "hexpm", "e9c03524447d1c4cbfccd672d739b8c18453eee377846b119d4fd71b1a176bb8"},
   "gen_smtp": {:hex, :gen_smtp, "0.15.0", "9f51960c17769b26833b50df0b96123605a8024738b62db747fece14eb2fbfcc", [:rebar3], [], "hexpm", "29bd14a88030980849c7ed2447b8db6d6c9278a28b11a44cafe41b791205440f"},
@@ -69,7 +70,7 @@
   "jumper": {:hex, :jumper, "1.0.1", "3c00542ef1a83532b72269fab9f0f0c82bf23a35e27d278bfd9ed0865cecabff", [:mix], [], "hexpm", "318c59078ac220e966d27af3646026db9b5a5e6703cb2aa3e26bcfaba65b7433"},
   "libring": {:hex, :libring, "1.4.0", "41246ba2f3fbc76b3971f6bce83119dfec1eee17e977a48d8a9cfaaf58c2a8d6", [:mix], [], "hexpm"},
   "linkify": {:hex, :linkify, "0.5.1", "6dc415cbc948b2f6ecec7cb226aab7ba9d3a1815bb501ae33e042334d707ecee", [:mix], [], "hexpm", "a3128c7e22fada4aa7214009501d8131e1fa3faf2f0a68b33dba379dc84ff944"},
-  "majic": {:git, "https://git.pleroma.social/pleroma/elixir-libraries/majic.git", "289cda1b6d0d70ccb2ba508a2b0bd24638db2880", [ref: "289cda1b6d0d70ccb2ba508a2b0bd24638db2880"]},
+  "majic": {:hex, :majic, "1.0.0", "37e50648db5f5c2ff0c9fb46454d034d11596c03683807b9fb3850676ffdaab3", [:make, :mix], [{:elixir_make, "~> 0.6.1", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "7905858f76650d49695f14ea55cd9aaaee0c6654fa391671d4cf305c275a0a9e"},
   "makeup": {:hex, :makeup, "1.0.5", "d5a830bc42c9800ce07dd97fa94669dfb93d3bf5fcf6ea7a0c67b2e0e4a7f26c", [:mix], [{:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cfa158c02d3f5c0c665d0af11512fed3fba0144cf1aadee0f2ce17747fba2ca9"},
   "makeup_elixir": {:hex, :makeup_elixir, "0.14.1", "4f0e96847c63c17841d42c08107405a005a2680eb9c7ccadfd757bd31dabccfb", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "f2438b1a80eaec9ede832b5c41cd4f373b38fd7aa33e3b22d9db79e640cbde11"},
   "makeup_erlang": {:hex, :makeup_erlang, "0.1.1", "3fcb7f09eb9d98dc4d208f49cc955a34218fc41ff6b84df7c75b3e6e533cc65f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "174d0809e98a4ef0b3309256cbf97101c6ec01c4ab0b23e926a9e17df2077cbb"},
   "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
   "mime": {:hex, :mime, "1.6.0", "dabde576a497cef4bbdd60aceee8160e02a6c89250d6c0b29e56c0dfb00db3d2", [:mix], [], "hexpm", "31a1a8613f8321143dde1dafc36006a17d28d02bdfecb9e95a880fa7aabd19a7"},
   "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"},
+  "mint": {:hex, :mint, "1.4.0", "cd7d2451b201fc8e4a8fd86257fb3878d9e3752899eb67b0c5b25b180bde1212", [:mix], [{:castore, "~> 0.1.0", [hex: :castore, repo: "hexpm", optional: true]}], "hexpm", "10a99e144b815cbf8522dccbc8199d15802440fc7a64d67b6853adb6fa170217"},
   "mochiweb": {:hex, :mochiweb, "2.18.0", "eb55f1db3e6e960fac4e6db4e2db9ec3602cc9f30b86cd1481d56545c3145d2e", [:rebar3], [], "hexpm"},
   "mock": {:hex, :mock, "0.3.7", "75b3bbf1466d7e486ea2052a73c6e062c6256fb429d6797999ab02fa32f29e03", [:mix], [{:meck, "~> 0.9.2", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm", "4da49a4609e41fd99b7836945c26f373623ea968cfb6282742bcb94440cf7e5c"},
   "mogrify": {:hex, :mogrify, "0.9.1", "a26f107c4987477769f272bd0f7e3ac4b7b75b11ba597fd001b877beffa9c068", [:mix], [], "hexpm", "134edf189337d2125c0948bf0c228fdeef975c594317452d536224069a5b7f05"},
   "mox": {:hex, :mox, "1.0.0", "4b3c7005173f47ff30641ba044eb0fe67287743eec9bd9545e37f3002b0a9f8b", [:mix], [], "hexpm", "201b0a20b7abdaaab083e9cf97884950f8a30a1350a1da403b3145e213c6f4df"},
   "myhtmlex": {:git, "https://git.pleroma.social/pleroma/myhtmlex.git", "ad0097e2f61d4953bfef20fb6abddf23b87111e6", [ref: "ad0097e2f61d4953bfef20fb6abddf23b87111e6", submodules: true]},
+  "nimble_options": {:hex, :nimble_options, "0.4.0", "c89babbab52221a24b8d1ff9e7d838be70f0d871be823165c94dd3418eea728f", [:mix], [], "hexpm", "e6701c1af326a11eea9634a3b1c62b475339ace9456c1a23ec3bc9a847bca02d"},
   "nimble_parsec": {:hex, :nimble_parsec, "0.5.0", "90e2eca3d0266e5c53f8fbe0079694740b9c91b6747f2b7e3c5d21966bba8300", [:mix], [], "hexpm", "5c040b8469c1ff1b10093d3186e2e10dbe483cd73d79ec017993fb3985b8a9b3"},
-  "nimble_pool": {:hex, :nimble_pool, "0.1.0", "ffa9d5be27eee2b00b0c634eb649aa27f97b39186fec3c493716c2a33e784ec6", [:mix], [], "hexpm", "343a1eaa620ddcf3430a83f39f2af499fe2370390d4f785cd475b4df5acaf3f9"},
+  "nimble_pool": {:hex, :nimble_pool, "0.2.4", "1db8e9f8a53d967d595e0b32a17030cdb6c0dc4a451b8ac787bf601d3f7704c3", [:mix], [], "hexpm", "367e8071e137b787764e6a9992ccb57b276dc2282535f767a07d881951ebeac6"},
   "nodex": {:git, "https://git.pleroma.social/pleroma/nodex", "cb6730f943cfc6aad674c92161be23a8411f15d1", [ref: "cb6730f943cfc6aad674c92161be23a8411f15d1"]},
   "oban": {:hex, :oban, "2.3.4", "ec7509b9af2524d55f529cb7aee93d36131ae0bf0f37706f65d2fe707f4d9fd8", [:mix], [{:ecto_sql, ">= 3.4.3", [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", "c70ca0434758fd1805422ea4446af5e910ddc697c0c861549c8f0eb0cfbd2fdf"},
   "open_api_spex": {:hex, :open_api_spex, "3.10.0", "94e9521ad525b3fcf6dc77da7c45f87fdac24756d4de588cb0816b413e7c1844", [: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", "2dbb2bde3d2b821f06936e8dfaf3284331186556291946d84eeba3750ac28765"},
diff --git a/priv/repo/migrations/20210818023112_add_user_id_to_apps.exs b/priv/repo/migrations/20210818023112_add_user_id_to_apps.exs
new file mode 100644 (file)
index 0000000..88a6bce
--- /dev/null
@@ -0,0 +1,11 @@
+defmodule Pleroma.Repo.Migrations.AddUserIdToApps do
+  use Ecto.Migration
+
+  def change do
+    alter table(:apps) do
+      add(:user_id, references(:users, type: :uuid, on_delete: :delete_all))
+    end
+
+    create_if_not_exists(index(:apps, [:user_id]))
+  end
+end
diff --git a/priv/repo/migrations/20211121000000_create_user_notes.exs b/priv/repo/migrations/20211121000000_create_user_notes.exs
new file mode 100644 (file)
index 0000000..b75e116
--- /dev/null
@@ -0,0 +1,15 @@
+defmodule Pleroma.Repo.Migrations.CreateUserNotes do
+  use Ecto.Migration
+
+  def change do
+    create_if_not_exists table(:user_notes) do
+      add(:source_id, references(:users, type: :uuid, on_delete: :delete_all))
+      add(:target_id, references(:users, type: :uuid, on_delete: :delete_all))
+      add(:comment, :string)
+
+      timestamps()
+    end
+
+    create_if_not_exists(unique_index(:user_notes, [:source_id, :target_id]))
+  end
+end
diff --git a/priv/repo/migrations/20211222165256_add_last_status_at_to_users.exs b/priv/repo/migrations/20211222165256_add_last_status_at_to_users.exs
new file mode 100644 (file)
index 0000000..9061782
--- /dev/null
@@ -0,0 +1,11 @@
+defmodule Pleroma.Repo.Migrations.AddLastStatusAtToUsers do
+  use Ecto.Migration
+
+  def change do
+    alter table(:users) do
+      add(:last_status_at, :naive_datetime)
+    end
+
+    create_if_not_exists(index(:users, [:last_status_at]))
+  end
+end
diff --git a/priv/repo/migrations/20211225154802_add_is_discoverable_index_to_users.exs b/priv/repo/migrations/20211225154802_add_is_discoverable_index_to_users.exs
new file mode 100644 (file)
index 0000000..9f8f52b
--- /dev/null
@@ -0,0 +1,7 @@
+defmodule Pleroma.Repo.Migrations.AddIsDiscoverableIndexToUsers do
+  use Ecto.Migration
+
+  def change do
+    create(index(:users, [:is_discoverable]))
+  end
+end
index 4b315862560a9ec0a0f0b9ada3f382b7b743d29a..51637f5412d95df5398c7966e9e0070b2a705fb0 100644 (file)
@@ -46,6 +46,7 @@ defmodule Pleroma.Gun.ConnectionPoolTest do
       end
   end
 
+  @tag :erratic
   test "connection limit is respected with concurrent requests" do
     clear_config([:connections_pool, :max_connections]) do
       clear_config([:connections_pool, :max_connections], 1)
index 86349619e0fda677001388aeae8c0c33045ea0fe..374e2048a732114a60c521bcd1a2073d24201a83 100644 (file)
@@ -922,6 +922,27 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do
                |> json_response_and_validate_schema(200)
     end
 
+    test "following with subscription and unsubscribing" do
+      %{conn: conn} = oauth_access(["follow"])
+      followed = insert(:user)
+
+      ret_conn =
+        conn
+        |> put_req_header("content-type", "application/json")
+        |> post("/api/v1/accounts/#{followed.id}/follow", %{notify: true})
+
+      assert %{"id" => _id, "subscribing" => true} =
+               json_response_and_validate_schema(ret_conn, 200)
+
+      ret_conn =
+        conn
+        |> put_req_header("content-type", "application/json")
+        |> post("/api/v1/accounts/#{followed.id}/follow", %{notify: false})
+
+      assert %{"id" => _id, "subscribing" => false} =
+               json_response_and_validate_schema(ret_conn, 200)
+    end
+
     test "following / unfollowing errors", %{user: user, conn: conn} do
       # self follow
       conn_res = post(conn, "/api/v1/accounts/#{user.id}/follow")
@@ -1800,4 +1821,21 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do
       |> get("/api/v1/accounts/lookup?acct=unexisting_nickname")
       |> json_response_and_validate_schema(404)
   end
+
+  test "create a note on a user" do
+    %{conn: conn} = oauth_access(["write:accounts", "read:follows"])
+    other_user = insert(:user)
+
+    conn
+    |> put_req_header("content-type", "application/json")
+    |> post("/api/v1/accounts/#{other_user.id}/note", %{
+      "comment" => "Example note"
+    })
+
+    assert [%{"note" => "Example note"}] =
+             conn
+             |> put_req_header("content-type", "application/json")
+             |> get("/api/v1/accounts/relationships?id=#{other_user.id}")
+             |> json_response_and_validate_schema(200)
+  end
 end
index 76d81b942333274ae69d3c279375fa82c52aff88..bfbb7f32d676a4a2214a887f5c6e2c8eb1cca649 100644 (file)
@@ -35,6 +35,33 @@ defmodule Pleroma.Web.MastodonAPI.AppControllerTest do
   end
 
   test "creates an oauth app", %{conn: conn} do
+    app_attrs = build(:oauth_app)
+
+    conn =
+      conn
+      |> put_req_header("content-type", "application/json")
+      |> post("/api/v1/apps", %{
+        client_name: app_attrs.client_name,
+        redirect_uris: app_attrs.redirect_uris
+      })
+
+    [app] = Repo.all(App)
+
+    expected = %{
+      "name" => app.client_name,
+      "website" => app.website,
+      "client_id" => app.client_id,
+      "client_secret" => app.client_secret,
+      "id" => app.id |> to_string(),
+      "redirect_uri" => app.redirect_uris,
+      "vapid_key" => Push.vapid_config() |> Keyword.get(:public_key)
+    }
+
+    assert expected == json_response_and_validate_schema(conn, 200)
+    assert app.user_id == nil
+  end
+
+  test "creates an oauth app with a user", %{conn: conn} do
     user = insert(:user)
     app_attrs = build(:oauth_app)
 
@@ -60,5 +87,6 @@ defmodule Pleroma.Web.MastodonAPI.AppControllerTest do
     }
 
     assert expected == json_response_and_validate_schema(conn, 200)
+    assert app.user_id == user.id
   end
 end
diff --git a/test/pleroma/web/mastodon_api/controllers/directory_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/directory_controller_test.exs
new file mode 100644 (file)
index 0000000..b8f55f8
--- /dev/null
@@ -0,0 +1,46 @@
+defmodule Pleroma.Web.MastodonAPI.DirectoryControllerTest do
+  use Pleroma.Web.ConnCase, async: true
+  alias Pleroma.Web.CommonAPI
+  import Pleroma.Factory
+
+  test "GET /api/v1/directory with :profile_directory disabled returns empty array", %{conn: conn} do
+    clear_config([:instance, :profile_directory], false)
+
+    insert(:user, is_discoverable: true)
+    insert(:user, is_discoverable: true)
+
+    result =
+      conn
+      |> get("/api/v1/directory")
+      |> json_response_and_validate_schema(200)
+
+    assert result == []
+  end
+
+  test "GET /api/v1/directory returns discoverable users only", %{conn: conn} do
+    %{id: user_id} = insert(:user, is_discoverable: true)
+    insert(:user, is_discoverable: false)
+
+    result =
+      conn
+      |> get("/api/v1/directory")
+      |> json_response_and_validate_schema(200)
+
+    assert [%{"id" => ^user_id}] = result
+  end
+
+  test "GET /api/v1/directory returns users sorted by most recent statuses", %{conn: conn} do
+    insert(:user, is_discoverable: true)
+    %{id: user_id} = user = insert(:user, is_discoverable: true)
+    insert(:user, is_discoverable: true)
+
+    {:ok, _activity} = CommonAPI.post(user, %{status: "yay i'm discoverable"})
+
+    result =
+      conn
+      |> get("/api/v1/directory?order=active")
+      |> json_response_and_validate_schema(200)
+
+    assert [%{"id" => ^user_id} | _tail] = result
+  end
+end
index 9af5887780baf0129d9d166d72da981484eb2b53..c23ffb966d38d0ce99f9679c147a16e0adda0d14 100644 (file)
@@ -74,6 +74,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do
         fields: []
       },
       fqn: "shp@shitposter.club",
+      last_status_at: nil,
       pleroma: %{
         ap_id: user.ap_id,
         also_known_as: ["https://shitposter.zone/users/shp"],
@@ -175,6 +176,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do
         fields: []
       },
       fqn: "shp@shitposter.club",
+      last_status_at: nil,
       pleroma: %{
         ap_id: user.ap_id,
         also_known_as: [],
@@ -270,10 +272,12 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do
       muting: false,
       muting_notifications: false,
       subscribing: false,
+      notifying: false,
       requested: false,
       domain_blocking: false,
       showing_reblogs: true,
-      endorsed: false
+      endorsed: false,
+      note: ""
     }
 
     test "represent a relationship for the following and followed user" do
@@ -295,6 +299,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do
             muting: true,
             muting_notifications: true,
             subscribing: true,
+            notifying: true,
             showing_reblogs: false,
             id: to_string(other_user.id)
           }
index fc2f0d94080812820d1a3102a73f327856ffbbd8..a5223b0a53268d31c1b1213602767c9c0c33afd1 100644 (file)
@@ -41,4 +41,16 @@ defmodule Pleroma.Web.OAuth.AppTest do
       assert error.type == :unique
     end
   end
+
+  test "get_user_apps/1" do
+    user = insert(:user)
+
+    apps = [
+      insert(:oauth_app, user_id: user.id),
+      insert(:oauth_app, user_id: user.id),
+      insert(:oauth_app, user_id: user.id)
+    ]
+
+    assert App.get_user_apps(user) == apps
+  end
 end
index 81d66983763b1f879775d0ab1aaeafaaddc5f68f..41aef98b1cb2bba140f7992eee15082870a202f0 100644 (file)
@@ -343,4 +343,54 @@ defmodule Pleroma.Web.OStatus.OStatusControllerTest do
       |> response(200)
     end
   end
+
+  describe "notice compatibility routes" do
+    test "Soapbox FE", %{conn: conn} do
+      user = insert(:user)
+      note_activity = insert(:note_activity, user: user)
+
+      resp =
+        conn
+        |> put_req_header("accept", "text/html")
+        |> get("/@#{user.nickname}/posts/#{note_activity.id}")
+        |> response(200)
+
+      expected =
+        "<meta content=\"#{Endpoint.url()}/notice/#{note_activity.id}\" property=\"og:url\">"
+
+      assert resp =~ expected
+    end
+
+    test "Mastodon", %{conn: conn} do
+      user = insert(:user)
+      note_activity = insert(:note_activity, user: user)
+
+      resp =
+        conn
+        |> put_req_header("accept", "text/html")
+        |> get("/@#{user.nickname}/#{note_activity.id}")
+        |> response(200)
+
+      expected =
+        "<meta content=\"#{Endpoint.url()}/notice/#{note_activity.id}\" property=\"og:url\">"
+
+      assert resp =~ expected
+    end
+
+    test "Twitter", %{conn: conn} do
+      user = insert(:user)
+      note_activity = insert(:note_activity, user: user)
+
+      resp =
+        conn
+        |> put_req_header("accept", "text/html")
+        |> get("/#{user.nickname}/status/#{note_activity.id}")
+        |> response(200)
+
+      expected =
+        "<meta content=\"#{Endpoint.url()}/notice/#{note_activity.id}\" property=\"og:url\">"
+
+      assert resp =~ expected
+    end
+  end
 end
diff --git a/test/pleroma/web/pleroma_api/controllers/app_controller_test.exs b/test/pleroma/web/pleroma_api/controllers/app_controller_test.exs
new file mode 100644 (file)
index 0000000..5e24e18
--- /dev/null
@@ -0,0 +1,53 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.PleromaAPI.AppControllerTest do
+  use Pleroma.Web.ConnCase, async: true
+
+  alias Pleroma.Web.OAuth.App
+  alias Pleroma.Web.Push
+
+  import Pleroma.Factory
+
+  test "apps", %{conn: conn} do
+    user = insert(:user)
+    app_attrs = build(:oauth_app)
+
+    creation =
+      conn
+      |> put_req_header("content-type", "application/json")
+      |> assign(:user, user)
+      |> post("/api/v1/apps", %{
+        client_name: app_attrs.client_name,
+        redirect_uris: app_attrs.redirect_uris
+      })
+
+    [app] = App.get_user_apps(user)
+
+    expected = %{
+      "name" => app.client_name,
+      "website" => app.website,
+      "client_id" => app.client_id,
+      "client_secret" => app.client_secret,
+      "id" => app.id |> to_string(),
+      "redirect_uri" => app.redirect_uris,
+      "vapid_key" => Push.vapid_config() |> Keyword.get(:public_key)
+    }
+
+    assert expected == json_response_and_validate_schema(creation, 200)
+
+    response =
+      conn
+      |> put_req_header("content-type", "application/json")
+      |> assign(:user, user)
+      |> assign(:token, insert(:oauth_token, user: user, scopes: ["read", "follow"]))
+      |> get("/api/v1/pleroma/apps")
+      |> json_response_and_validate_schema(200)
+
+    [apps] = response
+
+    assert length(response) == 1
+    assert apps["client_id"] == app.client_id
+  end
+end
diff --git a/test/pleroma/web/pleroma_api/views/app_view_test.exs b/test/pleroma/web/pleroma_api/views/app_view_test.exs
new file mode 100644 (file)
index 0000000..f0aee69
--- /dev/null
@@ -0,0 +1,21 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.PleromaAPI.AppViewTest do
+  use Pleroma.DataCase, async: true
+  alias Pleroma.Web.PleromaAPI.AppView
+  import Pleroma.Factory
+
+  test "index.json" do
+    apps = [
+      insert(:oauth_app),
+      insert(:oauth_app),
+      insert(:oauth_app)
+    ]
+
+    results = AppView.render("index.json", %{apps: apps})
+
+    assert [%{client_id: _, client_secret: _}, _, _] = results
+  end
+end
diff --git a/test/pleroma/web/plugs/ensure_staff_privileged_plug_test.exs b/test/pleroma/web/plugs/ensure_staff_privileged_plug_test.exs
new file mode 100644 (file)
index 0000000..74f4ae5
--- /dev/null
@@ -0,0 +1,60 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.Plugs.EnsureStaffPrivilegedPlugTest do
+  use Pleroma.Web.ConnCase, async: true
+
+  alias Pleroma.Web.Plugs.EnsureStaffPrivilegedPlug
+  import Pleroma.Factory
+
+  test "accepts a user that is an admin" do
+    user = insert(:user, is_admin: true)
+
+    conn = assign(build_conn(), :user, user)
+
+    ret_conn = EnsureStaffPrivilegedPlug.call(conn, %{})
+
+    assert conn == ret_conn
+  end
+
+  test "accepts a user that is a moderator when :privileged_staff is enabled" do
+    clear_config([:instance, :privileged_staff], true)
+    user = insert(:user, is_moderator: true)
+
+    conn = assign(build_conn(), :user, user)
+
+    ret_conn = EnsureStaffPrivilegedPlug.call(conn, %{})
+
+    assert conn == ret_conn
+  end
+
+  test "denies a user that is a moderator when :privileged_staff is disabled" do
+    clear_config([:instance, :privileged_staff], false)
+    user = insert(:user, is_moderator: true)
+
+    conn =
+      build_conn()
+      |> assign(:user, user)
+      |> EnsureStaffPrivilegedPlug.call(%{})
+
+    assert conn.status == 403
+  end
+
+  test "denies a user that isn't a staff member" do
+    user = insert(:user)
+
+    conn =
+      build_conn()
+      |> assign(:user, user)
+      |> EnsureStaffPrivilegedPlug.call(%{})
+
+    assert conn.status == 403
+  end
+
+  test "denies when a user isn't set" do
+    conn = EnsureStaffPrivilegedPlug.call(build_conn(), %{})
+
+    assert conn.status == 403
+  end
+end
index 52379b86a6d3355cb0076afb47228afc198bde74..4b3925ad2fd57f056e87bad47386338d03ae8f12 100644 (file)
@@ -86,6 +86,8 @@ defmodule Pleroma.Web.Plugs.FrontendStaticPlugTest do
       "objects",
       "activities",
       "notice",
+      "@:nickname",
+      ":nickname",
       "users",
       "tags",
       "mailer",