- 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)
- 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`
}
```
--## `/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`
}
```
-### 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.
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
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)
: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
{: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"
})
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