DELETE /api/pleroma/admin/users now accepts nicknames array
authorMaxim Filippov <colixer@gmail.com>
Tue, 15 Oct 2019 15:33:29 +0000 (17:33 +0200)
committerMaxim Filippov <colixer@gmail.com>
Tue, 15 Oct 2019 15:33:29 +0000 (17:33 +0200)
1  2 
CHANGELOG.md
docs/API/admin_api.md
lib/pleroma/moderation_log.ex
lib/pleroma/user.ex
lib/pleroma/web/admin_api/admin_api_controller.ex
test/moderation_log_test.exs
test/web/admin_api/admin_api_controller_test.exs

diff --combined CHANGELOG.md
index 30b765251cc7b0ede0bd723b836d0a858a8a7cdf,e3ccfa4ea696cf33512175bd9a3d84709b964eb4..24876d3f2b08fc2909c09bc03b8295b72acd0371
@@@ -17,8 -17,7 +17,9 @@@ The format is based on [Keep a Changelo
  - Authentication: Added rate limit for password-authorized actions / login existence checks
  - Metadata Link: Atom syndication Feed
  - Mix task to re-count statuses for all users (`mix pleroma.count_statuses`)
- - Admin API: `POST /api/pleroma/admin/users/:nickname/permission_group/:permission_group` / `DELETE /api/pleroma/admin/users/:nickname/permission_group/:permission_group` are deprecated in favor of: `POST /api/pleroma/admin/users/permission_group/:permission_group` / `DELETE /api/pleroma/admin/users/permission_group/:permission_group` (both accept `nicknames` array)
+ - Mastodon API: Add `exclude_visibilities` parameter to the timeline and notification endpoints
 +- Admin API: `/users/:nickname/toggle_activation` endpoint is now deprecated in favor of: `/users/activate`, `/users/deactivate`, both accept `nicknames` array
++- Admin API: `POST/DELETE /api/pleroma/admin/users/:nickname/permission_group/:permission_group` are deprecated in favor of: `POST/DELETE /api/pleroma/admin/users/permission_group/:permission_group` (both accept `nicknames` array), `DELETE /api/pleroma/admin/users` (`nickname` query param or `nickname` sent in JSON body) is deprecated in favor of: `DELETE /api/pleroma/admin/users` (`nicknames` query array param or `nicknames` sent in JSON body).
  
  ### Changed
  - **Breaking:** Elixir >=1.8 is now required (was >= 1.7)
@@@ -39,6 -38,7 +40,7 @@@
  - Report emails now include functional links to profiles of remote user accounts
  
  ## [1.1.0] - 2019-??-??
+ **Breaking:** The stable branch has been changed from `master` to `stable`, `master` now points to `release/1.0`
  ### Security
  - Mastodon API: respect post privacy in `/api/v1/statuses/:id/{favourited,reblogged}_by`
  
diff --combined docs/API/admin_api.md
index 2c8237b57f3bbf1c78964cd437d9171f45fc56b7,ee9e68cb1456c79c3231fa9a259e9dddbdfbbacb..60755e40a755053773683483289c1463588fd5b1
@@@ -47,7 -47,7 +47,7 @@@ Authentication is required and the use
  }
  ```
  
--## `/api/pleroma/admin/users`
++## DEPRECATED `DELETE /api/pleroma/admin/users`
  
  ### Remove a user
  
    - `nickname`
  - Response: User’s nickname
  
++## `DELETE /api/pleroma/admin/users`
++
++### Remove a user
++
++- Method `DELETE`
++- Params:
++  - `nicknames`
++- Response: Array of user nicknames
++
  ### Create a user
  
  - Method: `POST`
@@@ -154,86 -154,28 +163,86 @@@ Note: Available `:permission_group` is 
  }
  ```
  
 -### Add user in permission group
 +## DEPRECATED `POST /api/pleroma/admin/users/:nickname/permission_group/:permission_group`
 +
 +### Add user to permission group
  
 -- Method: `POST`
  - Params: none
  - Response:
    - On failure: `{"error": "…"}`
    - On success: JSON of the `user.info`
  
 +## `POST /api/pleroma/admin/users/permission_group/:permission_group`
 +
 +### Add users to permission group
 +
 +- Params:
 +  - `nicknames`: nicknames array
 +- Response:
 +  - On failure: `{"error": "…"}`
 +  - On success: JSON of the `user.info`
 +
 +## DEPRECATED `DELETE /api/pleroma/admin/users/:nickname/permission_group/:permission_group`
 +
  ### Remove user from permission group
  
 -- Method: `DELETE`
  - Params: none
  - Response:
    - On failure: `{"error": "…"}`
    - On success: JSON of the `user.info`
  - Note: An admin cannot revoke their own admin status.
  
 -## `/api/pleroma/admin/users/:nickname/activation_status`
 +## `DELETE /api/pleroma/admin/users/permission_group/:permission_group`
 +
 +### Remove users from permission group
 +
 +- Params:
 +  - `nicknames`: nicknames array
 +- Response:
 +  - On failure: `{"error": "…"}`
 +  - On success: JSON of the `user.info`
 +- Note: An admin cannot revoke their own admin status.
 +
 +## `PATCH /api/pleroma/admin/users/activate`
 +
 +### Activate user
 +
 +- Params:
 +  - `nicknames`: nicknames array
 +- Response:
 +
 +```json
 +{
 +  users: [
 +    {
 +      // user object
 +    }
 +  ]
 +}
 +```
 +
 +## `PATCH /api/pleroma/admin/users/deactivate`
 +
 +### Deactivate user
 +
 +- Params:
 +  - `nicknames`: nicknames array
 +- Response:
 +
 +```json
 +{
 +  users: [
 +    {
 +      // user object
 +    }
 +  ]
 +}
 +```
 +
 +## DEPRECATED `PATCH /api/pleroma/admin/users/:nickname/activation_status`
  
  ### Active or deactivate a user
  
 -- Method: `PUT`
  - Params:
    - `nickname`
    - `status` BOOLEAN field, false value means deactivation.
index 42649ff02b6a421646dbaf25a8deee15f309f211,352cad4335a40f9c1beea6ac5c2b258191527b72..e8884e6e81c01bf15a3e0247fb4320831647af1c
@@@ -86,18 -86,18 +86,18 @@@ defmodule Pleroma.ModerationLog d
      parsed_datetime
    end
  
 -  @spec insert_log(%{actor: User, subject: User, action: String.t(), permission: String.t()}) ::
 +  @spec insert_log(%{actor: User, subject: [User], action: String.t(), permission: String.t()}) ::
            {:ok, ModerationLog} | {:error, any}
    def insert_log(%{
          actor: %User{} = actor,
 -        subject: %User{} = subject,
 +        subject: subjects,
          action: action,
          permission: permission
        }) do
      %ModerationLog{
        data: %{
          "actor" => user_to_map(actor),
 -        "subject" => user_to_map(subject),
 +        "subject" => user_to_map(subjects),
          "action" => action,
          "permission" => permission,
          "message" => ""
    end
  
    @spec insert_log_entry_with_message(ModerationLog) :: {:ok, ModerationLog} | {:error, any}
 -
    defp insert_log_entry_with_message(entry) do
      entry.data["message"]
      |> put_in(get_log_entry_message(entry))
      |> Repo.insert()
    end
  
 +  defp user_to_map(users) when is_list(users) do
 +    users |> Enum.map(&user_to_map/1)
 +  end
 +
    defp user_to_map(%User{} = user) do
      user
      |> Map.from_struct()
          data: %{
            "actor" => %{"nickname" => actor_nickname},
            "action" => "delete",
--          "subject" => %{"nickname" => subject_nickname, "type" => "user"}
++          "subject" => subjects
          }
        }) do
--    "@#{actor_nickname} deleted user @#{subject_nickname}"
++    "@#{actor_nickname} deleted users: #{users_to_nicknames_string(subjects)}"
    end
  
    @spec get_log_entry_message(ModerationLog) :: String.t()
            "subjects" => subjects
          }
        }) do
 -    nicknames =
 -      subjects
 -      |> Enum.map(&"@#{&1["nickname"]}")
 -      |> Enum.join(", ")
 -
 -    "@#{actor_nickname} created users: #{nicknames}"
 +    "@#{actor_nickname} created users: #{users_to_nicknames_string(subjects)}"
    end
  
    @spec get_log_entry_message(ModerationLog) :: String.t()
          data: %{
            "actor" => %{"nickname" => actor_nickname},
            "action" => "activate",
 -          "subject" => %{"nickname" => subject_nickname, "type" => "user"}
 +          "subject" => users
          }
        }) do
 -    "@#{actor_nickname} activated user @#{subject_nickname}"
 +    "@#{actor_nickname} activated users: #{users_to_nicknames_string(users)}"
    end
  
    @spec get_log_entry_message(ModerationLog) :: String.t()
          data: %{
            "actor" => %{"nickname" => actor_nickname},
            "action" => "deactivate",
 -          "subject" => %{"nickname" => subject_nickname, "type" => "user"}
 +          "subject" => users
          }
        }) do
 -    "@#{actor_nickname} deactivated user @#{subject_nickname}"
 +    "@#{actor_nickname} deactivated users: #{users_to_nicknames_string(users)}"
    end
  
    @spec get_log_entry_message(ModerationLog) :: String.t()
            "action" => "tag"
          }
        }) do
 -    nicknames_string =
 -      nicknames
 -      |> Enum.map(&"@#{&1}")
 -      |> Enum.join(", ")
 -
      tags_string = tags |> Enum.join(", ")
  
 -    "@#{actor_nickname} added tags: #{tags_string} to users: #{nicknames_string}"
 +    "@#{actor_nickname} added tags: #{tags_string} to users: #{nicknames_to_string(nicknames)}"
    end
  
    @spec get_log_entry_message(ModerationLog) :: String.t()
            "action" => "untag"
          }
        }) do
 -    nicknames_string =
 -      nicknames
 -      |> Enum.map(&"@#{&1}")
 -      |> Enum.join(", ")
 -
      tags_string = tags |> Enum.join(", ")
  
 -    "@#{actor_nickname} removed tags: #{tags_string} from users: #{nicknames_string}"
 +    "@#{actor_nickname} removed tags: #{tags_string} from users: #{nicknames_to_string(nicknames)}"
    end
  
    @spec get_log_entry_message(ModerationLog) :: String.t()
          data: %{
            "actor" => %{"nickname" => actor_nickname},
            "action" => "grant",
 -          "subject" => %{"nickname" => subject_nickname},
 +          "subject" => users,
            "permission" => permission
          }
        }) do
 -    "@#{actor_nickname} made @#{subject_nickname} #{permission}"
 +    "@#{actor_nickname} made #{users_to_nicknames_string(users)} #{permission}"
    end
  
    @spec get_log_entry_message(ModerationLog) :: String.t()
          data: %{
            "actor" => %{"nickname" => actor_nickname},
            "action" => "revoke",
 -          "subject" => %{"nickname" => subject_nickname},
 +          "subject" => users,
            "permission" => permission
          }
        }) do
 -    "@#{actor_nickname} revoked #{permission} role from @#{subject_nickname}"
 +    "@#{actor_nickname} revoked #{permission} role from #{users_to_nicknames_string(users)}"
    end
  
    @spec get_log_entry_message(ModerationLog) :: String.t()
        }) do
      "@#{actor_nickname} deleted status ##{subject_id}"
    end
 +
 +  defp nicknames_to_string(nicknames) do
 +    nicknames
 +    |> Enum.map(&"@#{&1}")
 +    |> Enum.join(", ")
 +  end
 +
 +  defp users_to_nicknames_string(users) do
 +    users
 +    |> Enum.map(&"@#{&1["nickname"]}")
 +    |> Enum.join(", ")
 +  end
  end
diff --combined lib/pleroma/user.ex
index a76a5ad702415717d88855deefcb2f1ed9a4aca4,2cfb13a8c0164b2b4f1989fae7bdc6a666202c0e..596584062dff3d5f52631726b5441f8b5186c1d2
@@@ -1059,15 -1059,7 +1059,15 @@@ defmodule Pleroma.User d
      BackgroundWorker.enqueue("deactivate_user", %{"user_id" => user.id, "status" => status})
    end
  
 -  def deactivate(%User{} = user, status \\ true) do
 +  def deactivate(user, status \\ true)
 +
 +  def deactivate(users, status) when is_list(users) do
 +    Repo.transaction(fn ->
 +      for user <- users, do: deactivate(user, status)
 +    end)
 +  end
 +
 +  def deactivate(%User{} = user, status) do
      with {:ok, user} <- update_info(user, &User.Info.set_activation_status(&1, status)) do
        Enum.each(get_followers(user), &invalidate_cache/1)
        Enum.each(get_friends(user), &update_follower_count/1)
      update_info(user, &User.Info.update_notification_settings(&1, settings))
    end
  
++  def delete(users) when is_list(users) do
++    for user <- users, do: delete(user)
++  end
++
    def delete(%User{} = user) do
      BackgroundWorker.enqueue("delete_user", %{"user_id" => user.id})
    end
  
    `fun` is called with the `user.info`.
    """
 +  def update_info(users, fun) when is_list(users) do
 +    Repo.transaction(fn ->
 +      for user <- users, do: update_info(user, fun)
 +    end)
 +  end
 +
    def update_info(user, fun) do
      user
      |> change_info(fun)
index 33e2180ec05e9ded842e3904d2cc5c8933596b9b,513bae80060bc5506a1708bee93b99f357bb6ce6..ab0d0fe0a4f976ea6f5bbe64b7e0d9fab6f45009
@@@ -46,8 -46,6 +46,8 @@@ defmodule Pleroma.Web.AdminAPI.AdminAPI
             :user_delete,
             :users_create,
             :user_toggle_activation,
 +           :user_activate,
 +           :user_deactivate,
             :tag_users,
             :untag_users,
             :right_add,
  
      ModerationLog.insert_log(%{
        actor: admin,
--      subject: user,
++      subject: [user],
        action: "delete"
      })
  
      |> json(nickname)
    end
  
++  def user_delete(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
++    users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
++    User.delete(users)
++
++    ModerationLog.insert_log(%{
++      actor: admin,
++      subject: users,
++      action: "delete"
++    })
++
++    conn
++    |> json(nicknames)
++  end
++
    def user_follow(%{assigns: %{user: admin}} = conn, %{
          "follower" => follower_nick,
          "followed" => followed_nick
  
      ModerationLog.insert_log(%{
        actor: admin,
 -      subject: user,
 +      subject: [user],
        action: action
      })
  
      |> render("show.json", %{user: updated_user})
    end
  
 +  def user_activate(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
 +    users = Enum.map(nicknames, &User.get_cached_by_nickname/1)
 +    {:ok, updated_users} = User.deactivate(users, false)
 +
 +    ModerationLog.insert_log(%{
 +      actor: admin,
 +      subject: users,
 +      action: "activate"
 +    })
 +
 +    conn
 +    |> put_view(AccountView)
 +    |> render("index.json", %{users: Keyword.values(updated_users)})
 +  end
 +
 +  def user_deactivate(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
 +    users = Enum.map(nicknames, &User.get_cached_by_nickname/1)
 +    {:ok, updated_users} = User.deactivate(users, true)
 +
 +    ModerationLog.insert_log(%{
 +      actor: admin,
 +      subject: users,
 +      action: "deactivate"
 +    })
 +
 +    conn
 +    |> put_view(AccountView)
 +    |> render("index.json", %{users: Keyword.values(updated_users)})
 +  end
 +
    def tag_users(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames, "tags" => tags}) do
      with {:ok, _} <- User.tag(nicknames, tags) do
        ModerationLog.insert_log(%{
      |> Enum.into(%{}, &{&1, true})
    end
  
 +  def right_add_multiple(%{assigns: %{user: admin}} = conn, %{
 +        "permission_group" => permission_group,
 +        "nicknames" => nicknames
 +      })
 +      when permission_group in ["moderator", "admin"] do
 +    info = Map.put(%{}, "is_" <> permission_group, true)
 +
 +    users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
 +
 +    User.update_info(users, &User.Info.admin_api_update(&1, info))
 +
 +    ModerationLog.insert_log(%{
 +      action: "grant",
 +      actor: admin,
 +      subject: users,
 +      permission: permission_group
 +    })
 +
 +    json(conn, info)
 +  end
 +
 +  def right_add_multiple(conn, _) do
 +    render_error(conn, :not_found, "No such permission_group")
 +  end
 +
    def right_add(%{assigns: %{user: admin}} = conn, %{
          "permission_group" => permission_group,
          "nickname" => nickname
      ModerationLog.insert_log(%{
        action: "grant",
        actor: admin,
 -      subject: user,
 +      subject: [user],
        permission: permission_group
      })
  
      })
    end
  
 -  def right_delete(%{assigns: %{user: %{nickname: nickname}}} = conn, %{"nickname" => nickname}) do
 -    render_error(conn, :forbidden, "You can't revoke your own admin status.")
 +  def right_delete_multiple(
 +        %{assigns: %{user: %{nickname: admin_nickname} = admin}} = conn,
 +        %{
 +          "permission_group" => permission_group,
 +          "nicknames" => nicknames
 +        }
 +      )
 +      when permission_group in ["moderator", "admin"] do
 +    with false <- Enum.member?(nicknames, admin_nickname) do
 +      info = Map.put(%{}, "is_" <> permission_group, false)
 +
 +      users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
 +
 +      User.update_info(users, &User.Info.admin_api_update(&1, info))
 +
 +      ModerationLog.insert_log(%{
 +        action: "revoke",
 +        actor: admin,
 +        subject: users,
 +        permission: permission_group
 +      })
 +
 +      json(conn, info)
 +    else
 +      _ -> render_error(conn, :forbidden, "You can't revoke your own admin/moderator status.")
 +    end
 +  end
 +
 +  def right_delete_multiple(conn, _) do
 +    render_error(conn, :not_found, "No such permission_group")
    end
  
    def right_delete(
      ModerationLog.insert_log(%{
        action: "revoke",
        actor: admin,
 -      subject: user,
 +      subject: [user],
        permission: permission_group
      })
  
      json(conn, info)
    end
  
 -  def right_delete(conn, _) do
 -    render_error(conn, :not_found, "No such permission_group")
 -  end
 -
 -  def set_activation_status(%{assigns: %{user: admin}} = conn, %{
 -        "nickname" => nickname,
 -        "status" => status
 -      }) do
 -    with {:ok, status} <- Ecto.Type.cast(:boolean, status),
 -         %User{} = user <- User.get_cached_by_nickname(nickname),
 -         {:ok, _} <- User.deactivate(user, !status) do
 -      action = if(user.info.deactivated, do: "activate", else: "deactivate")
 -
 -      ModerationLog.insert_log(%{
 -        actor: admin,
 -        subject: user,
 -        action: action
 -      })
 -
 -      json_response(conn, :no_content, "")
 -    end
 +  def right_delete(%{assigns: %{user: %{nickname: nickname}}} = conn, %{"nickname" => nickname}) do
 +    render_error(conn, :forbidden, "You can't revoke your own admin status.")
    end
  
    def relay_follow(%{assigns: %{user: admin}} = conn, %{"relay_url" => target}) do
index ead97e94886d4c8dd0fe9feaae45fdf696526fa3,a39a00e0221ff2b3e8265540da28ab53980ec4a1..81c0fef12766f3d0ef8f55e1dd8fea0206261bd6
@@@ -24,13 -24,13 +24,13 @@@ defmodule Pleroma.ModerationLogTest d
        {:ok, _} =
          ModerationLog.insert_log(%{
            actor: moderator,
--          subject: subject1,
++          subject: [subject1],
            action: "delete"
          })
  
        log = Repo.one(ModerationLog)
  
--      assert log.data["message"] == "@#{moderator.nickname} deleted user @#{subject1.nickname}"
++      assert log.data["message"] == "@#{moderator.nickname} deleted users: @#{subject1.nickname}"
      end
  
      test "logging user creation by moderator", %{
        {:ok, _} =
          ModerationLog.insert_log(%{
            actor: moderator,
 -          subject: subject1,
 +          subject: [subject1],
            action: "grant",
            permission: "moderator"
          })
        {:ok, _} =
          ModerationLog.insert_log(%{
            actor: moderator,
 -          subject: subject1,
 +          subject: [subject1],
            action: "revoke",
            permission: "moderator"
          })
index 81dc5d101d6216ab39a9ab143c399f683db1d42b,b5c355e66f3eb9c36f6ad41bf676306f8f518c26..645b79f5776a55be5b665e18c4ef5446f8cd32cc
@@@ -17,8 -17,8 +17,8 @@@ defmodule Pleroma.Web.AdminAPI.AdminAPI
    alias Pleroma.Web.MediaProxy
    import Pleroma.Factory
  
--  describe "/api/pleroma/admin/users" do
--    test "Delete" do
++  describe "DELETE /api/pleroma/admin/users" do
++    test "single user" do
        admin = insert(:user, info: %{is_admin: true})
        user = insert(:user)
  
  
        log_entry = Repo.one(ModerationLog)
  
--      assert log_entry.data["subject"]["nickname"] == user.nickname
--      assert log_entry.data["action"] == "delete"
--
        assert ModerationLog.get_log_entry_message(log_entry) ==
--               "@#{admin.nickname} deleted user @#{user.nickname}"
++               "@#{admin.nickname} deleted users: @#{user.nickname}"
  
        assert json_response(conn, 200) == user.nickname
      end
  
++    test "multiple users" do
++      admin = insert(:user, info: %{is_admin: true})
++      user_one = insert(:user)
++      user_two = insert(:user)
++
++      conn =
++        build_conn()
++        |> assign(:user, admin)
++        |> put_req_header("accept", "application/json")
++        |> delete("/api/pleroma/admin/users", %{
++          nicknames: [user_one.nickname, user_two.nickname]
++        })
++
++      log_entry = Repo.one(ModerationLog)
++
++      assert ModerationLog.get_log_entry_message(log_entry) ==
++               "@#{admin.nickname} deleted users: @#{user_one.nickname}, @#{user_two.nickname}"
++
++      response = json_response(conn, 200)
++      assert response -- [user_one.nickname, user_two.nickname] == []
++    end
++  end
++
++  describe "/api/pleroma/admin/users" do
      test "Create" do
        admin = insert(:user, info: %{is_admin: true})
  
                 "@#{admin.nickname} made @#{user.nickname} admin"
      end
  
 -    test "/:right DELETE, can remove from a permission group" do
 +    test "/:right POST, can add to a permission group (multiple)" do
        admin = insert(:user, info: %{is_admin: true})
 -      user = insert(:user, info: %{is_admin: true})
 +      user_one = insert(:user)
 +      user_two = insert(:user)
  
        conn =
          build_conn()
          |> assign(:user, admin)
          |> put_req_header("accept", "application/json")
 -        |> delete("/api/pleroma/admin/users/#{user.nickname}/permission_group/admin")
 +        |> post("/api/pleroma/admin/users/permission_group/admin", %{
 +          nicknames: [user_one.nickname, user_two.nickname]
 +        })
  
        assert json_response(conn, 200) == %{
 -               "is_admin" => false
 +               "is_admin" => true
               }
  
        log_entry = Repo.one(ModerationLog)
  
        assert ModerationLog.get_log_entry_message(log_entry) ==
 -               "@#{admin.nickname} revoked admin role from @#{user.nickname}"
 +               "@#{admin.nickname} made @#{user_one.nickname}, @#{user_two.nickname} admin"
      end
 -  end
  
 -  describe "PUT /api/pleroma/admin/users/:nickname/activation_status" do
 -    setup %{conn: conn} do
 +    test "/:right DELETE, can remove from a permission group" do
        admin = insert(:user, info: %{is_admin: true})
 +      user = insert(:user, info: %{is_admin: true})
  
        conn =
 -        conn
 +        build_conn()
          |> assign(:user, admin)
          |> put_req_header("accept", "application/json")
 +        |> delete("/api/pleroma/admin/users/#{user.nickname}/permission_group/admin")
  
 -      %{conn: conn, admin: admin}
 -    end
 -
 -    test "deactivates the user", %{conn: conn, admin: admin} do
 -      user = insert(:user)
 -
 -      conn =
 -        conn
 -        |> put("/api/pleroma/admin/users/#{user.nickname}/activation_status", %{status: false})
 -
 -      user = User.get_cached_by_id(user.id)
 -      assert user.info.deactivated == true
 -      assert json_response(conn, :no_content)
 +      assert json_response(conn, 200) == %{
 +               "is_admin" => false
 +             }
  
        log_entry = Repo.one(ModerationLog)
  
        assert ModerationLog.get_log_entry_message(log_entry) ==
 -               "@#{admin.nickname} deactivated user @#{user.nickname}"
 +               "@#{admin.nickname} revoked admin role from @#{user.nickname}"
      end
  
 -    test "activates the user", %{conn: conn, admin: admin} do
 -      user = insert(:user, info: %{deactivated: true})
 +    test "/:right DELETE, can remove from a permission group (multiple)" do
 +      admin = insert(:user, info: %{is_admin: true})
 +      user_one = insert(:user, info: %{is_admin: true})
 +      user_two = insert(:user, info: %{is_admin: true})
  
        conn =
 -        conn
 -        |> put("/api/pleroma/admin/users/#{user.nickname}/activation_status", %{status: true})
 +        build_conn()
 +        |> assign(:user, admin)
 +        |> put_req_header("accept", "application/json")
 +        |> delete("/api/pleroma/admin/users/permission_group/admin", %{
 +          nicknames: [user_one.nickname, user_two.nickname]
 +        })
  
 -      user = User.get_cached_by_id(user.id)
 -      assert user.info.deactivated == false
 -      assert json_response(conn, :no_content)
 +      assert json_response(conn, 200) == %{
 +               "is_admin" => false
 +             }
  
        log_entry = Repo.one(ModerationLog)
  
        assert ModerationLog.get_log_entry_message(log_entry) ==
 -               "@#{admin.nickname} activated user @#{user.nickname}"
 -    end
 -
 -    test "returns 403 when requested by a non-admin", %{conn: conn} do
 -      user = insert(:user)
 -
 -      conn =
 -        conn
 -        |> assign(:user, user)
 -        |> put("/api/pleroma/admin/users/#{user.nickname}/activation_status", %{status: false})
 -
 -      assert json_response(conn, :forbidden)
 +               "@#{admin.nickname} revoked admin role from @#{user_one.nickname}, @#{
 +                 user_two.nickname
 +               }"
      end
    end
  
      end
    end
  
 +  test "PATCH /api/pleroma/admin/users/activate" do
 +    admin = insert(:user, info: %{is_admin: true})
 +    user_one = insert(:user, info: %{deactivated: true})
 +    user_two = insert(:user, info: %{deactivated: true})
 +
 +    conn =
 +      build_conn()
 +      |> assign(:user, admin)
 +      |> patch(
 +        "/api/pleroma/admin/users/activate",
 +        %{nicknames: [user_one.nickname, user_two.nickname]}
 +      )
 +
 +    response = json_response(conn, 200)
 +    assert Enum.map(response["users"], & &1["deactivated"]) == [false, false]
 +
 +    log_entry = Repo.one(ModerationLog)
 +
 +    assert ModerationLog.get_log_entry_message(log_entry) ==
 +             "@#{admin.nickname} activated users: @#{user_one.nickname}, @#{user_two.nickname}"
 +  end
 +
 +  test "PATCH /api/pleroma/admin/users/deactivate" do
 +    admin = insert(:user, info: %{is_admin: true})
 +    user_one = insert(:user, info: %{deactivated: false})
 +    user_two = insert(:user, info: %{deactivated: false})
 +
 +    conn =
 +      build_conn()
 +      |> assign(:user, admin)
 +      |> patch(
 +        "/api/pleroma/admin/users/deactivate",
 +        %{nicknames: [user_one.nickname, user_two.nickname]}
 +      )
 +
 +    response = json_response(conn, 200)
 +    assert Enum.map(response["users"], & &1["deactivated"]) == [true, true]
 +
 +    log_entry = Repo.one(ModerationLog)
 +
 +    assert ModerationLog.get_log_entry_message(log_entry) ==
 +             "@#{admin.nickname} deactivated users: @#{user_one.nickname}, @#{user_two.nickname}"
 +  end
 +
    test "PATCH /api/pleroma/admin/users/:nickname/toggle_activation" do
      admin = insert(:user, info: %{is_admin: true})
      user = insert(:user)
      log_entry = Repo.one(ModerationLog)
  
      assert ModerationLog.get_log_entry_message(log_entry) ==
 -             "@#{admin.nickname} deactivated user @#{user.nickname}"
 +             "@#{admin.nickname} deactivated users: @#{user.nickname}"
    end
  
    describe "POST /api/pleroma/admin/users/invite_token" do