Merge remote-tracking branch 'remotes/upstream/develop' into 1427-oauth-admin-scopes
authorIvan Tashkinov <ivantashkinov@gmail.com>
Tue, 10 Dec 2019 05:55:14 +0000 (08:55 +0300)
committerIvan Tashkinov <ivantashkinov@gmail.com>
Tue, 10 Dec 2019 05:55:14 +0000 (08:55 +0300)
# Conflicts:
# CHANGELOG.md

1  2 
CHANGELOG.md
config/config.exs
lib/mix/tasks/pleroma/user.ex
lib/pleroma/user.ex
lib/pleroma/web/admin_api/admin_api_controller.ex
test/web/admin_api/admin_api_controller_test.exs

diff --combined CHANGELOG.md
index 007c6f114294ddaaac45f8b1372b47f73f85b070,847dbe902869c45059f8151e09b50c8a9535cf54..bb32732866827965b6fe8ff1f646b500f25ceb85
@@@ -20,6 -20,7 +20,7 @@@ The format is based on [Keep a Changelo
  - OStatus: Extract RSS functionality
  - Deprecated `User.Info` embedded schema (fields moved to `User`)
  - Store status data inside Flag activity
+ - Deprecated (reorganized as `UserRelationship` entity) User fields with user AP IDs (`blocks`, `mutes`, `muted_reblogs`, `muted_notifications`, `subscribers`).
  <details>
    <summary>API Changes</summary>
  
  - Mastodon API: `pleroma.thread_muted` to the Status entity
  - Mastodon API: Mark the direct conversation as read for the author when they send a new direct message
  - Mastodon API, streaming: Add `pleroma.direct_conversation_id` to the `conversation` stream event payload.
+ - Admin API: Render whole status in grouped reports
+ - Mastodon API: User timelines will now respect blocks, unless you are getting the user timeline of somebody you blocked (which would be empty otherwise).
  </details>
  
  ### Added
+ - `:chat_limit` option to limit chat characters.
  - Refreshing poll results for remote polls
  - Authentication: Added rate limit for password-authorized actions / login existence checks
  - Static Frontend: Add the ability to render user profiles and notices server-side without requiring JS app.
@@@ -45,7 -49,7 +49,8 @@@
  - Mix task to list all users (`mix pleroma.user list`)
  - Support for `X-Forwarded-For` and similar HTTP headers which used by reverse proxies to pass a real user IP address to the backend. Must not be enabled unless your instance is behind at least one reverse proxy (such as Nginx, Apache HTTPD or Varnish Cache).
  - MRF: New module which handles incoming posts based on their age. By default, all incoming posts that are older than 2 days will be unlisted and not shown to their followers.
+ - User notification settings: Add `privacy_option` option.
 +- OAuth: admin scopes support (relevant setting: `[:auth, :enforce_oauth_admin_scope_usage]`).
  <details>
    <summary>API Changes</summary>
  
  ### Fixed
  - Report emails now include functional links to profiles of remote user accounts
  - Not being able to log in to some third-party apps when logged in to MastoFE
+ - MRF: `Delete` activities being exempt from MRF policies
+ - OTP releases: Not being able to configure OAuth expired token cleanup interval
+ - OTP releases: Not being able to configure HTML sanitization policy
  <details>
    <summary>API Changes</summary>
  
  - Mastodon API: Fix private and direct statuses not being filtered out from the public timeline for an authenticated user (`GET /api/v1/timelines/public`)
  - Mastodon API: Inability to get some local users by nickname in `/api/v1/accounts/:id_or_nickname`
+ - AdminAPI: If some status received reports both in the "new" format and "old" format it was considered reports on two different statuses (in the context of grouped reports)
  - Admin API: Error when trying to update reports in the "old" format
  </details>
  
diff --combined config/config.exs
index a8534da5417ca042eefb815985bfa5cfd1c320c9,4624bded2a8829592fe5c88eac2c12639ab3805a..6ed80005632895dfc319772ced0d1dd9b53f7992
@@@ -225,6 -225,7 +225,7 @@@ config :pleroma, :instance
    notify_email: "noreply@example.com",
    description: "A Pleroma instance, an alternative fediverse server",
    limit: 5_000,
+   chat_limit: 5_000,
    remote_limit: 100_000,
    upload_limit: 16_000_000,
    avatar_upload_limit: 2_000_000,
@@@ -562,10 -563,7 +563,10 @@@ config :ueberauth
         base_path: "/oauth",
         providers: ueberauth_providers
  
 -config :pleroma, :auth, oauth_consumer_strategies: oauth_consumer_strategies
 +config :pleroma,
 +       :auth,
 +       enforce_oauth_admin_scope_usage: false,
 +       oauth_consumer_strategies: oauth_consumer_strategies
  
  config :pleroma, Pleroma.Emails.Mailer, adapter: Swoosh.Adapters.Sendmail, enabled: false
  
index eff5191f5397d98f3b99ec6f58749c1368549d73,0adb78fe3727ed83868ad424a59a2bdb25db475a..85c9e4954afdc344d695c7f132882cf292cb1b30
@@@ -8,6 -8,7 +8,6 @@@ defmodule Mix.Tasks.Pleroma.User d
    alias Ecto.Changeset
    alias Pleroma.User
    alias Pleroma.UserInviteToken
 -  alias Pleroma.Web.OAuth
  
    @shortdoc "Manages Pleroma users"
    @moduledoc File.read!("docs/administration/CLI_tasks/user.md")
      start_pleroma()
  
      with %User{local: true} = user <- User.get_cached_by_nickname(nickname) do
 -      OAuth.Token.delete_user_tokens(user)
 -      OAuth.Authorization.delete_user_authorizations(user)
 +      User.global_sign_out(user)
  
        shell_info("#{nickname} signed out from all apps.")
      else
        users
        |> Enum.each(fn user ->
          shell_info(
-           "#{user.nickname} moderator: #{user.info.is_moderator}, admin: #{user.info.is_admin}, locked: #{
-             user.info.locked
-           }, deactivated: #{user.info.deactivated}"
+           "#{user.nickname} moderator: #{user.is_moderator}, admin: #{user.is_admin}, locked: #{
+             user.locked
+           }, deactivated: #{user.deactivated}"
          )
        end)
      end)
    end
  
    defp set_admin(user, value) do
 -    {:ok, user} =
 -      user
 -      |> Changeset.change(%{is_admin: value})
 -      |> User.update_and_set_cache()
 +    {:ok, user} = User.admin_api_update(user, %{is_admin: value})
  
      shell_info("Admin status of #{user.nickname}: #{user.is_admin}")
      user
diff --combined lib/pleroma/user.ex
index 1006b5bf9bb41e4211a0a479f2c5ee37eefef195,e2afc6de8215b3555f41a8f44b56984e388ecac5..22dd30d97fa55fc4dfb4270b6d909a11682b652b
@@@ -7,6 -7,7 +7,7 @@@ defmodule Pleroma.User d
  
    import Ecto.Changeset
    import Ecto.Query
+   import Ecto, only: [assoc: 2]
  
    alias Comeonin.Pbkdf2
    alias Ecto.Multi
@@@ -21,6 -22,7 +22,7 @@@
    alias Pleroma.Repo
    alias Pleroma.RepoStreamer
    alias Pleroma.User
+   alias Pleroma.UserRelationship
    alias Pleroma.Web
    alias Pleroma.Web.ActivityPub.ActivityPub
    alias Pleroma.Web.ActivityPub.Utils
    @strict_local_nickname_regex ~r/^[a-zA-Z\d]+$/
    @extended_local_nickname_regex ~r/^[a-zA-Z\d_-]+$/
  
+   # AP ID user relationships (blocks, mutes etc.)
+   # Format: [rel_type: [outgoing_rel: :outgoing_rel_target, incoming_rel: :incoming_rel_source]]
+   @user_relationships_config [
+     block: [
+       blocker_blocks: :blocked_users,
+       blockee_blocks: :blocker_users
+     ],
+     mute: [
+       muter_mutes: :muted_users,
+       mutee_mutes: :muter_users
+     ],
+     reblog_mute: [
+       reblog_muter_mutes: :reblog_muted_users,
+       reblog_mutee_mutes: :reblog_muter_users
+     ],
+     notification_mute: [
+       notification_muter_mutes: :notification_muted_users,
+       notification_mutee_mutes: :notification_muter_users
+     ],
+     # Note: `inverse_subscription` relationship is inverse: subscriber acts as relationship target
+     inverse_subscription: [
+       subscribee_subscriptions: :subscriber_users,
+       subscriber_subscriptions: :subscribee_users
+     ]
+   ]
    schema "users" do
      field(:bio, :string)
      field(:email, :string)
@@@ -61,7 -89,6 +89,6 @@@
      field(:tags, {:array, :string}, default: [])
      field(:last_refreshed_at, :naive_datetime_usec)
      field(:last_digest_emailed_at, :naive_datetime)
      field(:banner, :map, default: %{})
      field(:background, :map, default: %{})
      field(:source_data, :map, default: %{})
      field(:password_reset_pending, :boolean, default: false)
      field(:confirmation_token, :string, default: nil)
      field(:default_scope, :string, default: "public")
-     field(:blocks, {:array, :string}, default: [])
      field(:domain_blocks, {:array, :string}, default: [])
-     field(:mutes, {:array, :string}, default: [])
-     field(:muted_reblogs, {:array, :string}, default: [])
-     field(:muted_notifications, {:array, :string}, default: [])
-     field(:subscribers, {:array, :string}, default: [])
      field(:deactivated, :boolean, default: false)
      field(:no_rich_text, :boolean, default: false)
      field(:ap_enabled, :boolean, default: false)
      field(:skip_thread_containment, :boolean, default: false)
      field(:also_known_as, {:array, :string}, default: [])
  
-     field(:notification_settings, :map,
-       default: %{
-         "followers" => true,
-         "follows" => true,
-         "non_follows" => true,
-         "non_followers" => true
-       }
+     embeds_one(
+       :notification_settings,
+       Pleroma.User.NotificationSetting,
+       on_replace: :update
      )
  
      has_many(:notifications, Notification)
      has_many(:registrations, Registration)
      has_many(:deliveries, Delivery)
  
+     has_many(:outgoing_relationships, UserRelationship, foreign_key: :source_id)
+     has_many(:incoming_relationships, UserRelationship, foreign_key: :target_id)
+     for {relationship_type,
+          [
+            {outgoing_relation, outgoing_relation_target},
+            {incoming_relation, incoming_relation_source}
+          ]} <- @user_relationships_config do
+       # Definitions of `has_many :blocker_blocks`, `has_many :muter_mutes` etc.
+       has_many(outgoing_relation, UserRelationship,
+         foreign_key: :source_id,
+         where: [relationship_type: relationship_type]
+       )
+       # Definitions of `has_many :blockee_blocks`, `has_many :mutee_mutes` etc.
+       has_many(incoming_relation, UserRelationship,
+         foreign_key: :target_id,
+         where: [relationship_type: relationship_type]
+       )
+       # Definitions of `has_many :blocked_users`, `has_many :muted_users` etc.
+       has_many(outgoing_relation_target, through: [outgoing_relation, :target])
+       # Definitions of `has_many :blocker_users`, `has_many :muter_users` etc.
+       has_many(incoming_relation_source, through: [incoming_relation, :source])
+     end
+     # `:blocks` is deprecated (replaced with `blocked_users` relation)
+     field(:blocks, {:array, :string}, default: [])
+     # `:mutes` is deprecated (replaced with `muted_users` relation)
+     field(:mutes, {:array, :string}, default: [])
+     # `:muted_reblogs` is deprecated (replaced with `reblog_muted_users` relation)
+     field(:muted_reblogs, {:array, :string}, default: [])
+     # `:muted_notifications` is deprecated (replaced with `notification_muted_users` relation)
+     field(:muted_notifications, {:array, :string}, default: [])
+     # `:subscribers` is deprecated (replaced with `subscriber_users` relation)
+     field(:subscribers, {:array, :string}, default: [])
      timestamps()
    end
  
+   for {_relationship_type, [{_outgoing_relation, outgoing_relation_target}, _]} <-
+         @user_relationships_config do
+     # Definitions of `blocked_users_relation/1`, `muted_users_relation/1`, etc.
+     def unquote(:"#{outgoing_relation_target}_relation")(user, restrict_deactivated? \\ false) do
+       target_users_query = assoc(user, unquote(outgoing_relation_target))
+       if restrict_deactivated? do
+         restrict_deactivated(target_users_query)
+       else
+         target_users_query
+       end
+     end
+     # Definitions of `blocked_users/1`, `muted_users/1`, etc.
+     def unquote(outgoing_relation_target)(user, restrict_deactivated? \\ false) do
+       __MODULE__
+       |> apply(unquote(:"#{outgoing_relation_target}_relation"), [
+         user,
+         restrict_deactivated?
+       ])
+       |> Repo.all()
+     end
+     # Definitions of `blocked_users_ap_ids/1`, `muted_users_ap_ids/1`, etc.
+     def unquote(:"#{outgoing_relation_target}_ap_ids")(user, restrict_deactivated? \\ false) do
+       __MODULE__
+       |> apply(unquote(:"#{outgoing_relation_target}_relation"), [
+         user,
+         restrict_deactivated?
+       ])
+       |> select([u], u.ap_id)
+       |> Repo.all()
+     end
+   end
    @doc "Returns if the user should be allowed to authenticate"
    def auth_active?(%User{deactivated: true}), do: false
  
      |> Repo.all()
    end
  
-   @spec mute(User.t(), User.t(), boolean()) :: {:ok, User.t()} | {:error, String.t()}
-   def mute(muter, %User{ap_id: ap_id}, notifications? \\ true) do
-     add_to_mutes(muter, ap_id, notifications?)
+   @spec mute(User.t(), User.t(), boolean()) ::
+           {:ok, list(UserRelationship.t())} | {:error, String.t()}
+   def mute(%User{} = muter, %User{} = mutee, notifications? \\ true) do
+     add_to_mutes(muter, mutee, notifications?)
    end
  
-   def unmute(muter, %{ap_id: ap_id}) do
-     remove_from_mutes(muter, ap_id)
+   def unmute(%User{} = muter, %User{} = mutee) do
+     remove_from_mutes(muter, mutee)
    end
  
-   def subscribe(subscriber, %{ap_id: ap_id}) do
-     with %User{} = subscribed <- get_cached_by_ap_id(ap_id) do
-       deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
+   def subscribe(%User{} = subscriber, %User{} = target) do
+     deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
  
-       if blocks?(subscribed, subscriber) and deny_follow_blocked do
-         {:error, "Could not subscribe: #{subscribed.nickname} is blocking you"}
-       else
-         User.add_to_subscribers(subscribed, subscriber.ap_id)
-       end
+     if blocks?(target, subscriber) and deny_follow_blocked do
+       {:error, "Could not subscribe: #{target.nickname} is blocking you"}
+     else
+       # Note: the relationship is inverse: subscriber acts as relationship target
+       UserRelationship.create_inverse_subscription(target, subscriber)
      end
    end
  
-   def unsubscribe(unsubscriber, %{ap_id: ap_id}) do
+   def subscribe(%User{} = subscriber, %{ap_id: ap_id}) do
+     with %User{} = subscribee <- get_cached_by_ap_id(ap_id) do
+       subscribe(subscriber, subscribee)
+     end
+   end
+   def unsubscribe(%User{} = unsubscriber, %User{} = target) do
+     # Note: the relationship is inverse: subscriber acts as relationship target
+     UserRelationship.delete_inverse_subscription(target, unsubscriber)
+   end
+   def unsubscribe(%User{} = unsubscriber, %{ap_id: ap_id}) do
      with %User{} = user <- get_cached_by_ap_id(ap_id) do
-       User.remove_from_subscribers(user, unsubscriber.ap_id)
+       unsubscribe(unsubscriber, user)
      end
    end
  
-   def block(blocker, %User{ap_id: ap_id} = blocked) do
+   def block(%User{} = blocker, %User{} = blocked) do
      # sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213)
      blocker =
        if following?(blocker, blocked) do
          nil -> blocked
        end
  
-     blocker =
-       if subscribed_to?(blocked, blocker) do
-         {:ok, blocker} = unsubscribe(blocked, blocker)
-         blocker
-       else
-         blocker
-       end
+     unsubscribe(blocked, blocker)
  
      if following?(blocked, blocker), do: unfollow(blocked, blocker)
  
      {:ok, blocker} = update_follower_count(blocker)
      {:ok, blocker, _} = Participation.mark_all_as_read(blocker, blocked)
-     add_to_block(blocker, ap_id)
+     add_to_block(blocker, blocked)
    end
  
    # helper to handle the block given only an actor's AP id
-   def block(blocker, %{ap_id: ap_id}) do
+   def block(%User{} = blocker, %{ap_id: ap_id}) do
      block(blocker, get_cached_by_ap_id(ap_id))
    end
  
-   def unblock(blocker, %{ap_id: ap_id}) do
-     remove_from_block(blocker, ap_id)
+   def unblock(%User{} = blocker, %User{} = blocked) do
+     remove_from_block(blocker, blocked)
+   end
+   # helper to handle the block given only an actor's AP id
+   def unblock(%User{} = blocker, %{ap_id: ap_id}) do
+     unblock(blocker, get_cached_by_ap_id(ap_id))
    end
  
    def mutes?(nil, _), do: false
-   def mutes?(user, %{ap_id: ap_id}), do: Enum.member?(user.mutes, ap_id)
+   def mutes?(%User{} = user, %User{} = target), do: mutes_user?(user, target)
+   def mutes_user?(%User{} = user, %User{} = target) do
+     UserRelationship.mute_exists?(user, target)
+   end
  
    @spec muted_notifications?(User.t() | nil, User.t() | map()) :: boolean()
    def muted_notifications?(nil, _), do: false
  
-   def muted_notifications?(user, %{ap_id: ap_id}),
-     do: Enum.member?(user.muted_notifications, ap_id)
+   def muted_notifications?(%User{} = user, %User{} = target),
+     do: UserRelationship.notification_mute_exists?(user, target)
+   def blocks?(nil, _), do: false
  
    def blocks?(%User{} = user, %User{} = target) do
-     blocks_ap_id?(user, target) || blocks_domain?(user, target)
+     blocks_user?(user, target) || blocks_domain?(user, target)
    end
  
-   def blocks?(nil, _), do: false
-   def blocks_ap_id?(%User{} = user, %User{} = target) do
-     Enum.member?(user.blocks, target.ap_id)
+   def blocks_user?(%User{} = user, %User{} = target) do
+     UserRelationship.block_exists?(user, target)
    end
  
-   def blocks_ap_id?(_, _), do: false
+   def blocks_user?(_, _), do: false
  
    def blocks_domain?(%User{} = user, %User{} = target) do
      domain_blocks = Pleroma.Web.ActivityPub.MRF.subdomains_regex(user.domain_blocks)
  
    def blocks_domain?(_, _), do: false
  
-   def subscribed_to?(user, %{ap_id: ap_id}) do
+   def subscribed_to?(%User{} = user, %User{} = target) do
+     # Note: the relationship is inverse: subscriber acts as relationship target
+     UserRelationship.inverse_subscription_exists?(target, user)
+   end
+   def subscribed_to?(%User{} = user, %{ap_id: ap_id}) do
      with %User{} = target <- get_cached_by_ap_id(ap_id) do
-       Enum.member?(target.subscribers, user.ap_id)
+       subscribed_to?(user, target)
      end
    end
  
-   @spec muted_users(User.t()) :: [User.t()]
-   def muted_users(user) do
-     User.Query.build(%{ap_id: user.mutes, deactivated: false})
-     |> Repo.all()
-   end
+   @doc """
+   Returns map of outgoing (blocked, muted etc.) relations' user AP IDs by relation type.
+   E.g. `outgoing_relations_ap_ids(user, [:block])` -> `%{block: ["https://some.site/users/userapid"]}`
+   """
+   @spec outgoing_relations_ap_ids(User.t(), list(atom())) :: %{atom() => list(String.t())}
+   def outgoing_relations_ap_ids(_, []), do: %{}
  
-   @spec blocked_users(User.t()) :: [User.t()]
-   def blocked_users(user) do
-     User.Query.build(%{ap_id: user.blocks, deactivated: false})
-     |> Repo.all()
-   end
+   def outgoing_relations_ap_ids(%User{} = user, relationship_types)
+       when is_list(relationship_types) do
+     db_result =
+       user
+       |> assoc(:outgoing_relationships)
+       |> join(:inner, [user_rel], u in assoc(user_rel, :target))
+       |> where([user_rel, u], user_rel.relationship_type in ^relationship_types)
+       |> select([user_rel, u], [user_rel.relationship_type, fragment("array_agg(?)", u.ap_id)])
+       |> group_by([user_rel, u], user_rel.relationship_type)
+       |> Repo.all()
+       |> Enum.into(%{}, fn [k, v] -> {k, v} end)
  
-   @spec subscribers(User.t()) :: [User.t()]
-   def subscribers(user) do
-     User.Query.build(%{ap_id: user.subscribers, deactivated: false})
-     |> Repo.all()
+     Enum.into(
+       relationship_types,
+       %{},
+       fn rel_type -> {rel_type, db_result[rel_type] || []} end
+     )
    end
  
    def deactivate_async(user, status \\ true) do
    end
  
    def update_notification_settings(%User{} = user, settings) do
-     settings =
-       settings
-       |> Enum.map(fn {k, v} -> {k, v in [true, "true", "True", "1"]} end)
-       |> Map.new()
-     notification_settings =
-       user.notification_settings
-       |> Map.merge(settings)
-       |> Map.take(["followers", "follows", "non_follows", "non_followers"])
-     params = %{notification_settings: notification_settings}
      user
-     |> cast(params, [:notification_settings])
+     |> cast(%{notification_settings: settings}, [])
+     |> cast_embed(:notification_settings)
      |> validate_required([:notification_settings])
      |> update_and_set_cache()
    end
        blocked_identifiers,
        fn blocked_identifier ->
          with {:ok, %User{} = blocked} <- get_or_fetch(blocked_identifier),
-              {:ok, blocker} <- block(blocker, blocked),
+              {:ok, _user_block} <- block(blocker, blocked),
               {:ok, _} <- ActivityPub.block(blocker, blocked) do
            blocked
          else
    end
  
    def showing_reblogs?(%User{} = user, %User{} = target) do
-     target.ap_id not in user.muted_reblogs
+     not UserRelationship.reblog_mute_exists?(user, target)
    end
  
    @doc """
    end
  
    def admin_api_update(user, params) do
 -    user
 -    |> cast(params, [
 -      :is_moderator,
 -      :is_admin,
 -      :show_role
 -    ])
 -    |> update_and_set_cache()
 +    changeset =
 +      cast(user, params, [
 +        :is_moderator,
 +        :is_admin,
 +        :show_role
 +      ])
 +
 +    with {:ok, updated_user} <- update_and_set_cache(changeset) do
 +      if user.is_admin && !updated_user.is_admin do
 +        # Tokens & authorizations containing any admin scopes must be revoked (revoking all).
 +        # This is an extra safety measure (tokens' admin scopes won't be accepted for non-admins).
 +        global_sign_out(user)
 +      end
 +
 +      {:ok, updated_user}
 +    end
 +  end
 +
 +  @doc "Signs user out of all applications"
 +  def global_sign_out(user) do
 +    OAuth.Authorization.delete_user_authorizations(user)
 +    OAuth.Token.delete_user_tokens(user)
    end
  
    def mascot_update(user, url) do
      |> update_and_set_cache()
    end
  
-   defp set_subscribers(user, subscribers) do
-     params = %{subscribers: subscribers}
-     user
-     |> cast(params, [:subscribers])
-     |> validate_required([:subscribers])
-     |> update_and_set_cache()
-   end
-   def add_to_subscribers(user, subscribed) do
-     set_subscribers(user, Enum.uniq([subscribed | user.subscribers]))
-   end
-   def remove_from_subscribers(user, subscribed) do
-     set_subscribers(user, List.delete(user.subscribers, subscribed))
-   end
    defp set_domain_blocks(user, domain_blocks) do
      params = %{domain_blocks: domain_blocks}
  
      set_domain_blocks(user, List.delete(user.domain_blocks, domain_blocked))
    end
  
-   defp set_blocks(user, blocks) do
-     params = %{blocks: blocks}
-     user
-     |> cast(params, [:blocks])
-     |> validate_required([:blocks])
-     |> update_and_set_cache()
-   end
-   def add_to_block(user, blocked) do
-     set_blocks(user, Enum.uniq([blocked | user.blocks]))
-   end
-   def remove_from_block(user, blocked) do
-     set_blocks(user, List.delete(user.blocks, blocked))
+   @spec add_to_block(User.t(), User.t()) ::
+           {:ok, UserRelationship.t()} | {:error, Ecto.Changeset.t()}
+   defp add_to_block(%User{} = user, %User{} = blocked) do
+     UserRelationship.create_block(user, blocked)
    end
  
-   defp set_mutes(user, mutes) do
-     params = %{mutes: mutes}
-     user
-     |> cast(params, [:mutes])
-     |> validate_required([:mutes])
-     |> update_and_set_cache()
+   @spec add_to_block(User.t(), User.t()) ::
+           {:ok, UserRelationship.t()} | {:ok, nil} | {:error, Ecto.Changeset.t()}
+   defp remove_from_block(%User{} = user, %User{} = blocked) do
+     UserRelationship.delete_block(user, blocked)
    end
  
-   def add_to_mutes(user, muted, notifications?) do
-     with {:ok, user} <- set_mutes(user, Enum.uniq([muted | user.mutes])) do
-       set_notification_mutes(
-         user,
-         Enum.uniq([muted | user.muted_notifications]),
-         notifications?
-       )
+   defp add_to_mutes(%User{} = user, %User{} = muted_user, notifications?) do
+     with {:ok, user_mute} <- UserRelationship.create_mute(user, muted_user),
+          {:ok, user_notification_mute} <-
+            (notifications? && UserRelationship.create_notification_mute(user, muted_user)) ||
+              {:ok, nil} do
+       {:ok, Enum.filter([user_mute, user_notification_mute], & &1)}
      end
    end
  
-   def remove_from_mutes(user, muted) do
-     with {:ok, user} <- set_mutes(user, List.delete(user.mutes, muted)) do
-       set_notification_mutes(
-         user,
-         List.delete(user.muted_notifications, muted),
-         true
-       )
+   defp remove_from_mutes(user, %User{} = muted_user) do
+     with {:ok, user_mute} <- UserRelationship.delete_mute(user, muted_user),
+          {:ok, user_notification_mute} <-
+            UserRelationship.delete_notification_mute(user, muted_user) do
+       {:ok, [user_mute, user_notification_mute]}
      end
    end
  
-   defp set_notification_mutes(user, _muted_notifications, false = _notifications?) do
-     {:ok, user}
-   end
-   defp set_notification_mutes(user, muted_notifications, true = _notifications?) do
-     params = %{muted_notifications: muted_notifications}
-     user
-     |> cast(params, [:muted_notifications])
-     |> validate_required([:muted_notifications])
-     |> update_and_set_cache()
-   end
-   def add_reblog_mute(user, ap_id) do
-     params = %{muted_reblogs: user.muted_reblogs ++ [ap_id]}
-     user
-     |> cast(params, [:muted_reblogs])
-     |> update_and_set_cache()
-   end
-   def remove_reblog_mute(user, ap_id) do
-     params = %{muted_reblogs: List.delete(user.muted_reblogs, ap_id)}
-     user
-     |> cast(params, [:muted_reblogs])
-     |> update_and_set_cache()
-   end
    def set_invisible(user, invisible) do
      params = %{invisible: invisible}
  
index 0a63f3fe6c2761d1845e366a12cf8e0f3fa2b2d1,b003d1f358333896e42b46f49a14b5b9def70591..0a8a56cd8950b80faff6e88ba4ba54eaf93df99e
@@@ -30,13 -30,13 +30,13 @@@ defmodule Pleroma.Web.AdminAPI.AdminAPI
  
    plug(
      OAuthScopesPlug,
 -    %{scopes: ["read:accounts"]}
 +    %{scopes: ["read:accounts"], admin: true}
      when action in [:list_users, :user_show, :right_get, :invites]
    )
  
    plug(
      OAuthScopesPlug,
 -    %{scopes: ["write:accounts"]}
 +    %{scopes: ["write:accounts"], admin: true}
      when action in [
             :get_invite_token,
             :revoke_invite,
  
    plug(
      OAuthScopesPlug,
 -    %{scopes: ["read:reports"]} when action in [:list_reports, :report_show]
 +    %{scopes: ["read:reports"], admin: true}
 +    when action in [:list_reports, :report_show]
    )
  
    plug(
      OAuthScopesPlug,
 -    %{scopes: ["write:reports"]}
 +    %{scopes: ["write:reports"], admin: true}
      when action in [:report_update_state, :report_respond]
    )
  
    plug(
      OAuthScopesPlug,
 -    %{scopes: ["read:statuses"]} when action == :list_user_statuses
 +    %{scopes: ["read:statuses"], admin: true}
 +    when action == :list_user_statuses
    )
  
    plug(
      OAuthScopesPlug,
 -    %{scopes: ["write:statuses"]}
 +    %{scopes: ["write:statuses"], admin: true}
      when action in [:status_update, :status_delete]
    )
  
    plug(
      OAuthScopesPlug,
 -    %{scopes: ["read"]}
 +    %{scopes: ["read"], admin: true}
      when action in [:config_show, :migrate_to_db, :migrate_from_db, :list_log]
    )
  
    plug(
      OAuthScopesPlug,
 -    %{scopes: ["write"]}
 +    %{scopes: ["write"], admin: true}
      when action in [:relay_follow, :relay_unfollow, :config_update]
    )
  
    end
  
    def list_grouped_reports(conn, _params) do
-     reports = Utils.get_reported_activities()
+     statuses = Utils.get_reported_activities()
  
      conn
      |> put_view(ReportView)
-     |> render("index_grouped.json", Utils.get_reports_grouped_by_status(reports))
+     |> render("index_grouped.json", Utils.get_reports_grouped_by_status(statuses))
    end
  
    def report_show(conn, %{"id" => id}) do
index bcab63cf05193b34f015b1d15344cc8dd83be34f,4148f04bc765d716374c8931d2d4a78e34b16a56..23ca7f110d1d41c400e13d0708e55407ca6dea26
@@@ -15,6 -15,7 +15,7 @@@ defmodule Pleroma.Web.AdminAPI.AdminAPI
    alias Pleroma.UserInviteToken
    alias Pleroma.Web.ActivityPub.Relay
    alias Pleroma.Web.CommonAPI
+   alias Pleroma.Web.MastodonAPI.StatusView
    alias Pleroma.Web.MediaProxy
    import Pleroma.Factory
  
      :ok
    end
  
 +  clear_config([:auth, :enforce_oauth_admin_scope_usage]) do
 +    Pleroma.Config.put([:auth, :enforce_oauth_admin_scope_usage], false)
 +  end
 +
 +  describe "with [:auth, :enforce_oauth_admin_scope_usage]," do
 +    clear_config([:auth, :enforce_oauth_admin_scope_usage]) do
 +      Pleroma.Config.put([:auth, :enforce_oauth_admin_scope_usage], true)
 +    end
 +
 +    test "GET /api/pleroma/admin/users/:nickname requires admin:read:accounts or broader scope" do
 +      user = insert(:user)
 +      admin = insert(:user, is_admin: true)
 +      url = "/api/pleroma/admin/users/#{user.nickname}"
 +
 +      good_token1 = insert(:oauth_token, user: admin, scopes: ["admin"])
 +      good_token2 = insert(:oauth_token, user: admin, scopes: ["admin:read"])
 +      good_token3 = insert(:oauth_token, user: admin, scopes: ["admin:read:accounts"])
 +
 +      bad_token1 = insert(:oauth_token, user: admin, scopes: ["read:accounts"])
 +      bad_token2 = insert(:oauth_token, user: admin, scopes: ["admin:read:accounts:partial"])
 +      bad_token3 = nil
 +
 +      for good_token <- [good_token1, good_token2, good_token3] do
 +        conn =
 +          build_conn()
 +          |> assign(:user, admin)
 +          |> assign(:token, good_token)
 +          |> get(url)
 +
 +        assert json_response(conn, 200)
 +      end
 +
 +      for good_token <- [good_token1, good_token2, good_token3] do
 +        conn =
 +          build_conn()
 +          |> assign(:user, nil)
 +          |> assign(:token, good_token)
 +          |> get(url)
 +
 +        assert json_response(conn, :forbidden)
 +      end
 +
 +      for bad_token <- [bad_token1, bad_token2, bad_token3] do
 +        conn =
 +          build_conn()
 +          |> assign(:user, admin)
 +          |> assign(:token, bad_token)
 +          |> get(url)
 +
 +        assert json_response(conn, :forbidden)
 +      end
 +    end
 +  end
 +
    describe "DELETE /api/pleroma/admin/users" do
      test "single user" do
        admin = insert(:user, is_admin: true)
        assert ["lain", "lain2"] -- Enum.map(log_entry.data["subjects"], & &1["nickname"]) == []
      end
  
 -    test "Cannot create user with exisiting email" do
 +    test "Cannot create user with existing email" do
        admin = insert(:user, is_admin: true)
        user = insert(:user)
  
               ]
      end
  
 -    test "Cannot create user with exisiting nickname" do
 +    test "Cannot create user with existing nickname" do
        admin = insert(:user, is_admin: true)
        user = insert(:user)
  
          |> assign(:user, user)
          |> get("/api/pleroma/admin/reports")
  
 -      assert json_response(conn, :forbidden) == %{"error" => "User is not admin."}
 +      assert json_response(conn, :forbidden) ==
 +               %{"error" => "User is not an admin or OAuth admin scope is not granted."}
      end
  
      test "returns 403 when requested by anonymous" do
          first_status: Activity.get_by_ap_id_with_object(first_status.data["id"]),
          second_status: Activity.get_by_ap_id_with_object(second_status.data["id"]),
          third_status: Activity.get_by_ap_id_with_object(third_status.data["id"]),
+         first_report: first_report,
          first_status_reports: [first_report, second_report, third_report],
          second_status_reports: [first_report, second_report],
          third_status_reports: [first_report],
  
        assert length(response["reports"]) == 3
  
-       first_group =
-         Enum.find(response["reports"], &(&1["status"]["id"] == first_status.data["id"]))
+       first_group = Enum.find(response["reports"], &(&1["status"]["id"] == first_status.id))
  
-       second_group =
-         Enum.find(response["reports"], &(&1["status"]["id"] == second_status.data["id"]))
+       second_group = Enum.find(response["reports"], &(&1["status"]["id"] == second_status.id))
  
-       third_group =
-         Enum.find(response["reports"], &(&1["status"]["id"] == third_status.data["id"]))
+       third_group = Enum.find(response["reports"], &(&1["status"]["id"] == third_status.id))
  
        assert length(first_group["reports"]) == 3
        assert length(second_group["reports"]) == 2
                   NaiveDateTime.from_iso8601!(act.data["published"])
                 end).data["published"]
  
-       assert first_group["status"] == %{
-                "id" => first_status.data["id"],
-                "content" => first_status.object.data["content"],
-                "published" => first_status.object.data["published"]
-              }
+       assert first_group["status"] ==
+                Map.put(
+                  stringify_keys(StatusView.render("show.json", %{activity: first_status})),
+                  "deleted",
+                  false
+                )
  
-       assert first_group["account"]["id"] == target_user.id
+       assert(first_group["account"]["id"] == target_user.id)
  
        assert length(first_group["actors"]) == 1
        assert hd(first_group["actors"])["id"] == reporter.id
                   NaiveDateTime.from_iso8601!(act.data["published"])
                 end).data["published"]
  
-       assert second_group["status"] == %{
-                "id" => second_status.data["id"],
-                "content" => second_status.object.data["content"],
-                "published" => second_status.object.data["published"]
-              }
+       assert second_group["status"] ==
+                Map.put(
+                  stringify_keys(StatusView.render("show.json", %{activity: second_status})),
+                  "deleted",
+                  false
+                )
  
        assert second_group["account"]["id"] == target_user.id
  
                   NaiveDateTime.from_iso8601!(act.data["published"])
                 end).data["published"]
  
-       assert third_group["status"] == %{
-                "id" => third_status.data["id"],
-                "content" => third_status.object.data["content"],
-                "published" => third_status.object.data["published"]
-              }
+       assert third_group["status"] ==
+                Map.put(
+                  stringify_keys(StatusView.render("show.json", %{activity: third_status})),
+                  "deleted",
+                  false
+                )
  
        assert third_group["account"]["id"] == target_user.id
  
        assert Enum.map(third_group["reports"], & &1["id"]) --
                 Enum.map(third_status_reports, & &1.id) == []
      end
+     test "reopened report renders status data", %{
+       conn: conn,
+       first_report: first_report,
+       first_status: first_status
+     } do
+       {:ok, _} = CommonAPI.update_report_state(first_report.id, "resolved")
+       response =
+         conn
+         |> get("/api/pleroma/admin/grouped_reports")
+         |> json_response(:ok)
+       first_group = Enum.find(response["reports"], &(&1["status"]["id"] == first_status.id))
+       assert first_group["status"] ==
+                Map.put(
+                  stringify_keys(StatusView.render("show.json", %{activity: first_status})),
+                  "deleted",
+                  false
+                )
+     end
+     test "reopened report does not render status data if status has been deleted", %{
+       conn: conn,
+       first_report: first_report,
+       first_status: first_status,
+       target_user: target_user
+     } do
+       {:ok, _} = CommonAPI.update_report_state(first_report.id, "resolved")
+       {:ok, _} = CommonAPI.delete(first_status.id, target_user)
+       refute Activity.get_by_ap_id(first_status.id)
+       response =
+         conn
+         |> get("/api/pleroma/admin/grouped_reports")
+         |> json_response(:ok)
+       assert Enum.find(response["reports"], &(&1["status"]["deleted"] == true))["status"][
+                "deleted"
+              ] == true
+       assert length(Enum.filter(response["reports"], &(&1["status"]["deleted"] == false))) == 2
+     end
+     test "account not empty if status was deleted", %{
+       conn: conn,
+       first_report: first_report,
+       first_status: first_status,
+       target_user: target_user
+     } do
+       {:ok, _} = CommonAPI.update_report_state(first_report.id, "resolved")
+       {:ok, _} = CommonAPI.delete(first_status.id, target_user)
+       refute Activity.get_by_ap_id(first_status.id)
+       response =
+         conn
+         |> get("/api/pleroma/admin/grouped_reports")
+         |> json_response(:ok)
+       assert Enum.find(response["reports"], &(&1["status"]["deleted"] == true))["account"]
+     end
    end
  
    describe "POST /api/pleroma/admin/reports/:id/respond" do