Merge branch 'feature/reports-groups-and-multiple-state-update' into 'develop'

Admin API: Grouped reports, update multiple reports in one query

Closes admin-fe#43

See merge request pleroma/pleroma!1815
This commit is contained in:
feld 2019-11-14 13:35:41 +00:00
commit 1afeaf82fa
11 changed files with 605 additions and 133 deletions

View file

@ -25,6 +25,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- **Breaking** Admin API: `PATCH /api/pleroma/admin/users/:nickname/force_password_reset` is now `PATCH /api/pleroma/admin/users/force_password_reset` (accepts `nicknames` array in the request body) - **Breaking** Admin API: `PATCH /api/pleroma/admin/users/:nickname/force_password_reset` is now `PATCH /api/pleroma/admin/users/force_password_reset` (accepts `nicknames` array in the request body)
- **Breaking:** Admin API: Return link alongside with token on password reset - **Breaking:** Admin API: Return link alongside with token on password reset
- **Breaking:** Admin API: `PUT /api/pleroma/admin/reports/:id` is now `PATCH /api/pleroma/admin/reports`, see admin_api.md for details
- **Breaking:** `/api/pleroma/admin/users/invite_token` now uses `POST`, changed accepted params and returns full invite in json instead of only token string. - **Breaking:** `/api/pleroma/admin/users/invite_token` now uses `POST`, changed accepted params and returns full invite in json instead of only token string.
- Admin API: Return `total` when querying for reports - Admin API: Return `total` when querying for reports
- Mastodon API: Return `pleroma.direct_conversation_id` when creating a direct message (`POST /api/v1/statuses`) - Mastodon API: Return `pleroma.direct_conversation_id` when creating a direct message (`POST /api/v1/statuses`)
@ -45,6 +46,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
<summary>API Changes</summary> <summary>API Changes</summary>
- Job queue stats to the healthcheck page - Job queue stats to the healthcheck page
- Admin API: Add ability to fetch reports, grouped by status `GET /api/pleroma/admin/grouped_reports`
- Admin API: Add ability to require password reset - Admin API: Add ability to require password reset
- Mastodon API: Account entities now include `follow_requests_count` (planned Mastodon 3.x addition) - Mastodon API: Account entities now include `follow_requests_count` (planned Mastodon 3.x addition)
- Pleroma API: `GET /api/v1/pleroma/accounts/:id/scrobbles` to get a list of recently scrobbled items - Pleroma API: `GET /api/v1/pleroma/accounts/:id/scrobbles` to get a list of recently scrobbled items
@ -56,7 +58,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Mix task to re-count statuses for all users (`mix pleroma.count_statuses`) - Mix task to re-count statuses for all users (`mix pleroma.count_statuses`)
- Mastodon API: Add `exclude_visibilities` parameter to the timeline and notification endpoints - 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: `/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). - Admin API: Multiple endpoints now require `nicknames` array, instead of singe `nickname`:
- `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`
- `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)
- Admin API: Add `GET /api/pleroma/admin/relay` endpoint - lists all followed relays - Admin API: Add `GET /api/pleroma/admin/relay` endpoint - lists all followed relays
- Pleroma API: `POST /api/v1/pleroma/conversations/read` to mark all conversations as read - Pleroma API: `POST /api/v1/pleroma/conversations/read` to mark all conversations as read
- Mastodon API: Add `/api/v1/markers` for managing timeline read markers - Mastodon API: Add `/api/v1/markers` for managing timeline read markers

View file

@ -2,11 +2,10 @@
Authentication is required and the user must be an admin. Authentication is required and the user must be an admin.
## `/api/pleroma/admin/users` ## `GET /api/pleroma/admin/users`
### List users ### List users
- Method `GET`
- Query Params: - Query Params:
- *optional* `query`: **string** search term (e.g. nickname, domain, nickname@domain) - *optional* `query`: **string** search term (e.g. nickname, domain, nickname@domain)
- *optional* `filters`: **string** comma-separated string of filters: - *optional* `filters`: **string** comma-separated string of filters:
@ -51,7 +50,6 @@ Authentication is required and the user must be an admin.
### Remove a user ### Remove a user
- Method `DELETE`
- Params: - Params:
- `nickname` - `nickname`
- Response: Users nickname - Response: Users nickname
@ -60,7 +58,6 @@ Authentication is required and the user must be an admin.
### Remove a user ### Remove a user
- Method `DELETE`
- Params: - Params:
- `nicknames` - `nicknames`
- Response: Array of user nicknames - Response: Array of user nicknames
@ -78,31 +75,30 @@ Authentication is required and the user must be an admin.
] ]
- Response: Users nickname - Response: Users nickname
## `/api/pleroma/admin/users/follow` ## `POST /api/pleroma/admin/users/follow`
### Make a user follow another user ### Make a user follow another user
- Methods: `POST`
- Params: - Params:
- `follower`: The nickname of the follower - `follower`: The nickname of the follower
- `followed`: The nickname of the followed - `followed`: The nickname of the followed
- Response: - Response:
- "ok" - "ok"
## `/api/pleroma/admin/users/unfollow` ## `POST /api/pleroma/admin/users/unfollow`
### Make a user unfollow another user ### Make a user unfollow another user
- Methods: `POST`
- Params: - Params:
- `follower`: The nickname of the follower - `follower`: The nickname of the follower
- `followed`: The nickname of the followed - `followed`: The nickname of the followed
- Response: - Response:
- "ok" - "ok"
## `/api/pleroma/admin/users/:nickname/toggle_activation` ## `PATCH /api/pleroma/admin/users/:nickname/toggle_activation`
### Toggle user activation ### Toggle user activation
- Method: `PATCH`
- Params: - Params:
- `nickname` - `nickname`
- Response: Users object - Response: Users object
@ -115,27 +111,26 @@ Authentication is required and the user must be an admin.
} }
``` ```
## `/api/pleroma/admin/users/tag` ## `PUT /api/pleroma/admin/users/tag`
### Tag a list of users ### Tag a list of users
- Method: `PUT`
- Params: - Params:
- `nicknames` (array) - `nicknames` (array)
- `tags` (array) - `tags` (array)
## `DELETE /api/pleroma/admin/users/tag`
### Untag a list of users ### Untag a list of users
- Method: `DELETE`
- Params: - Params:
- `nicknames` (array) - `nicknames` (array)
- `tags` (array) - `tags` (array)
## `/api/pleroma/admin/users/:nickname/permission_group` ## `GET /api/pleroma/admin/users/:nickname/permission_group`
### Get user user permission groups membership ### Get user user permission groups membership
- Method: `GET`
- Params: none - Params: none
- Response: - Response:
@ -146,13 +141,12 @@ Authentication is required and the user must be an admin.
} }
``` ```
## `/api/pleroma/admin/users/:nickname/permission_group/:permission_group` ## `GET /api/pleroma/admin/users/:nickname/permission_group/:permission_group`
Note: Available `:permission_group` is currently moderator and admin. 404 is returned when the permission group doesnt exist. Note: Available `:permission_group` is currently moderator and admin. 404 is returned when the permission group doesnt exist.
### Get user user permission groups membership per permission group ### Get user user permission groups membership per permission group
- Method: `GET`
- Params: none - Params: none
- Response: - Response:
@ -184,6 +178,8 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
## DEPRECATED `DELETE /api/pleroma/admin/users/:nickname/permission_group/:permission_group` ## DEPRECATED `DELETE /api/pleroma/admin/users/:nickname/permission_group/:permission_group`
## `DELETE /api/pleroma/admin/users/:nickname/permission_group/:permission_group`
### Remove user from permission group ### Remove user from permission group
- Params: none - Params: none
@ -247,22 +243,20 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
- `nickname` - `nickname`
- `status` BOOLEAN field, false value means deactivation. - `status` BOOLEAN field, false value means deactivation.
## `/api/pleroma/admin/users/:nickname_or_id` ## `GET /api/pleroma/admin/users/:nickname_or_id`
### Retrive the details of a user ### Retrive the details of a user
- Method: `GET`
- Params: - Params:
- `nickname` or `id` - `nickname` or `id`
- Response: - Response:
- On failure: `Not found` - On failure: `Not found`
- On success: JSON of the user - On success: JSON of the user
## `/api/pleroma/admin/users/:nickname_or_id/statuses` ## `GET /api/pleroma/admin/users/:nickname_or_id/statuses`
### Retrive user's latest statuses ### Retrive user's latest statuses
- Method: `GET`
- Params: - Params:
- `nickname` or `id` - `nickname` or `id`
- *optional* `page_size`: number of statuses to return (default is `20`) - *optional* `page_size`: number of statuses to return (default is `20`)
@ -271,19 +265,19 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
- On failure: `Not found` - On failure: `Not found`
- On success: JSON array of user's latest statuses - On success: JSON array of user's latest statuses
## `/api/pleroma/admin/relay` ## `POST /api/pleroma/admin/relay`
### Follow a Relay ### Follow a Relay
- Methods: `POST`
- Params: - Params:
- `relay_url` - `relay_url`
- Response: - Response:
- On success: URL of the followed relay - On success: URL of the followed relay
## `DELETE /api/pleroma/admin/relay`
### Unfollow a Relay ### Unfollow a Relay
- Methods: `DELETE`
- Params: - Params:
- `relay_url` - `relay_url`
- Response: - Response:
@ -297,11 +291,10 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
- Response: - Response:
- On success: JSON array of relays - On success: JSON array of relays
## `/api/pleroma/admin/users/invite_token` ## `POST /api/pleroma/admin/users/invite_token`
### Create an account registration invite token ### Create an account registration invite token
- Methods: `POST`
- Params: - Params:
- *optional* `max_use` (integer) - *optional* `max_use` (integer)
- *optional* `expires_at` (date string e.g. "2019-04-07") - *optional* `expires_at` (date string e.g. "2019-04-07")
@ -319,11 +312,10 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
} }
``` ```
## `/api/pleroma/admin/users/invites` ## `GET /api/pleroma/admin/users/invites`
### Get a list of generated invites ### Get a list of generated invites
- Methods: `GET`
- Params: none - Params: none
- Response: - Response:
@ -345,11 +337,10 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
} }
``` ```
## `/api/pleroma/admin/users/revoke_invite` ## `POST /api/pleroma/admin/users/revoke_invite`
### Revoke invite by token ### Revoke invite by token
- Methods: `POST`
- Params: - Params:
- `token` - `token`
- Response: - Response:
@ -367,21 +358,18 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
} }
``` ```
## `POST /api/pleroma/admin/users/email_invite`
## `/api/pleroma/admin/users/email_invite`
### Sends registration invite via email ### Sends registration invite via email
- Methods: `POST`
- Params: - Params:
- `email` - `email`
- `name`, optional - `name`, optional
## `/api/pleroma/admin/users/:nickname/password_reset` ## `GET /api/pleroma/admin/users/:nickname/password_reset`
### Get a password reset token for a given nickname ### Get a password reset token for a given nickname
- Methods: `GET`
- Params: none - Params: none
- Response: - Response:
@ -392,18 +380,18 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
} }
``` ```
## `/api/pleroma/admin/users/force_password_reset` ## `PATCH /api/pleroma/admin/users/force_password_reset`
### Force passord reset for a user with a given nickname ### Force passord reset for a user with a given nickname
- Methods: `PATCH`
- Params: - Params:
- `nicknames` - `nicknames`
- Response: none (code `204`) - Response: none (code `204`)
## `/api/pleroma/admin/reports` ## `GET /api/pleroma/admin/reports`
### Get a list of reports ### Get a list of reports
- Method `GET`
- Params: - Params:
- *optional* `state`: **string** the state of reports. Valid values are `open`, `closed` and `resolved` - *optional* `state`: **string** the state of reports. Valid values are `open`, `closed` and `resolved`
- *optional* `limit`: **integer** the number of records to retrieve - *optional* `limit`: **integer** the number of records to retrieve
@ -418,7 +406,7 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
```json ```json
{ {
"total" : 1, "totalReports" : 1,
"reports": [ "reports": [
{ {
"account": { "account": {
@ -560,9 +548,34 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
} }
``` ```
## `/api/pleroma/admin/reports/:id` ## `GET /api/pleroma/admin/grouped_reports`
### Get a list of reports, grouped by status
- Params: none
- On success: JSON, returns a list of reports, where:
- `date`: date of the latest report
- `account`: the user who has been reported (see `/api/pleroma/admin/reports` for reference)
- `status`: reported status (see `/api/pleroma/admin/reports` for reference)
- `actors`: users who had reported this status (see `/api/pleroma/admin/reports` for reference)
- `reports`: reports (see `/api/pleroma/admin/reports` for reference)
```json
"reports": [
{
"date": "2019-10-07T12:31:39.615149Z",
"account": { ... },
"status": { ... },
"actors": [{ ... }, { ... }],
"reports": [{ ... }]
}
]
```
## `GET /api/pleroma/admin/reports/:id`
### Get an individual report ### Get an individual report
- Method `GET`
- Params: - Params:
- `id` - `id`
- Response: - Response:
@ -571,22 +584,41 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
- 404 Not Found `"Not found"` - 404 Not Found `"Not found"`
- On success: JSON, Report object (see above) - On success: JSON, Report object (see above)
## `/api/pleroma/admin/reports/:id` ## `PATCH /api/pleroma/admin/reports`
### Change the state of the report
- Method `PUT` ### Change the state of one or multiple reports
- Params: - Params:
- `id`
- `state`: required, the new state. Valid values are `open`, `closed` and `resolved` ```json
`reports`: [
{
`id`, // required, report id
`state` // required, the new state. Valid values are `open`, `closed` and `resolved`
},
...
]
```
- Response: - Response:
- On failure: - On failure:
- 400 Bad Request `"Unsupported state"` - 400 Bad Request, JSON:
- 403 Forbidden `{"error": "error_msg"}`
- 404 Not Found `"Not found"` ```json
- On success: JSON, Report object (see above) [
{
`id`, // report id
`error` // error message
}
]
```
- On success: `204`, empty response
## `POST /api/pleroma/admin/reports/:id/respond`
## `/api/pleroma/admin/reports/:id/respond`
### Respond to a report ### Respond to a report
- Method `POST`
- Params: - Params:
- `id` - `id`
- `status`: required, the message - `status`: required, the message
@ -656,9 +688,10 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
} }
``` ```
## `/api/pleroma/admin/statuses/:id` ## `PUT /api/pleroma/admin/statuses/:id`
### Change the scope of an individual reported status ### Change the scope of an individual reported status
- Method `PUT`
- Params: - Params:
- `id` - `id`
- `sensitive`: optional, valid values are `true` or `false` - `sensitive`: optional, valid values are `true` or `false`
@ -670,9 +703,10 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
- 404 Not Found `"Not found"` - 404 Not Found `"Not found"`
- On success: JSON, Mastodon Status entity - On success: JSON, Mastodon Status entity
## `/api/pleroma/admin/statuses/:id` ## `DELETE /api/pleroma/admin/statuses/:id`
### Delete an individual reported status ### Delete an individual reported status
- Method `DELETE`
- Params: - Params:
- `id` - `id`
- Response: - Response:
@ -681,11 +715,12 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
- 404 Not Found `"Not found"` - 404 Not Found `"Not found"`
- On success: 200 OK `{}` - On success: 200 OK `{}`
## `GET /api/pleroma/admin/config/migrate_to_db`
## `/api/pleroma/admin/config/migrate_to_db`
### Run mix task pleroma.config migrate_to_db ### Run mix task pleroma.config migrate_to_db
Copy settings on key `:pleroma` to DB. Copy settings on key `:pleroma` to DB.
- Method `GET`
- Params: none - Params: none
- Response: - Response:
@ -693,10 +728,12 @@ Copy settings on key `:pleroma` to DB.
{} {}
``` ```
## `/api/pleroma/admin/config/migrate_from_db` ## `GET /api/pleroma/admin/config/migrate_from_db`
### Run mix task pleroma.config migrate_from_db ### Run mix task pleroma.config migrate_from_db
Copy all settings from DB to `config/prod.exported_from_db.secret.exs` with deletion from DB. Copy all settings from DB to `config/prod.exported_from_db.secret.exs` with deletion from DB.
- Method `GET`
- Params: none - Params: none
- Response: - Response:
@ -704,10 +741,12 @@ Copy all settings from DB to `config/prod.exported_from_db.secret.exs` with dele
{} {}
``` ```
## `/api/pleroma/admin/config` ## `GET /api/pleroma/admin/config`
### List config settings ### List config settings
List config settings only works with `:pleroma => :instance => :dynamic_configuration` setting to `true`. List config settings only works with `:pleroma => :instance => :dynamic_configuration` setting to `true`.
- Method `GET`
- Params: none - Params: none
- Response: - Response:
@ -723,8 +762,10 @@ List config settings only works with `:pleroma => :instance => :dynamic_configur
} }
``` ```
## `/api/pleroma/admin/config` ## `POST /api/pleroma/admin/config`
### Update config settings ### Update config settings
Updating config settings only works with `:pleroma => :instance => :dynamic_configuration` setting to `true`. Updating config settings only works with `:pleroma => :instance => :dynamic_configuration` setting to `true`.
Module name can be passed as string, which starts with `Pleroma`, e.g. `"Pleroma.Upload"`. Module name can be passed as string, which starts with `Pleroma`, e.g. `"Pleroma.Upload"`.
Atom keys and values can be passed with `:` in the beginning, e.g. `":upload"`. Atom keys and values can be passed with `:` in the beginning, e.g. `":upload"`.
@ -747,7 +788,6 @@ Compile time settings (need instance reboot):
- `Pleroma.Upload` -> `:proxy_remote` - `Pleroma.Upload` -> `:proxy_remote`
- `:instance` -> `:upload_limit` - `:instance` -> `:upload_limit`
- Method `POST`
- Params: - Params:
- `configs` => [ - `configs` => [
- `group` (string) - `group` (string)
@ -802,9 +842,10 @@ Compile time settings (need instance reboot):
} }
``` ```
## `/api/pleroma/admin/moderation_log` ## `GET /api/pleroma/admin/moderation_log`
### Get moderation log ### Get moderation log
- Method `GET`
- Params: - Params:
- *optional* `page`: **integer** page number - *optional* `page`: **integer** page number
- *optional* `page_size`: **integer** number of log entries per page (default is `50`) - *optional* `page_size`: **integer** number of log entries per page (default is `50`)
@ -831,8 +872,9 @@ Compile time settings (need instance reboot):
``` ```
## `POST /api/pleroma/admin/reload_emoji` ## `POST /api/pleroma/admin/reload_emoji`
### Reload the instance's custom emoji ### Reload the instance's custom emoji
* Method `POST`
* Authentication: required - Authentication: required
* Params: None - Params: None
* Response: JSON, "ok" and 200 status - Response: JSON, "ok" and 200 status

View file

@ -41,6 +41,10 @@ defmodule Pleroma.Activity do
field(:actor, :string) field(:actor, :string)
field(:recipients, {:array, :string}, default: []) field(:recipients, {:array, :string}, default: [])
field(:thread_muted?, :boolean, virtual: true) field(:thread_muted?, :boolean, virtual: true)
# This is a fake relation,
# do not use outside of with_preloaded_user_actor/with_joined_user_actor
has_one(:user_actor, User, on_delete: :nothing, foreign_key: :id)
# This is a fake relation, do not use outside of with_preloaded_bookmark/get_bookmark # This is a fake relation, do not use outside of with_preloaded_bookmark/get_bookmark
has_one(:bookmark, Bookmark) has_one(:bookmark, Bookmark)
has_many(:notifications, Notification, on_delete: :delete_all) has_many(:notifications, Notification, on_delete: :delete_all)
@ -86,6 +90,19 @@ def with_preloaded_object(query, join_type \\ :inner) do
|> preload([activity, object: object], object: object) |> preload([activity, object: object], object: object)
end end
def with_joined_user_actor(query, join_type \\ :inner) do
join(query, join_type, [activity], u in User,
on: u.ap_id == activity.actor,
as: :user_actor
)
end
def with_preloaded_user_actor(query, join_type \\ :inner) do
query
|> with_joined_user_actor(join_type)
|> preload([activity, user_actor: user_actor], user_actor: user_actor)
end
def with_preloaded_bookmark(query, %User{} = user) do def with_preloaded_bookmark(query, %User{} = user) do
from([a] in query, from([a] in query,
left_join: b in Bookmark, left_join: b in Bookmark,

View file

@ -11,6 +11,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do
alias Pleroma.Repo alias Pleroma.Repo
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web alias Pleroma.Web
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Visibility alias Pleroma.Web.ActivityPub.Visibility
alias Pleroma.Web.AdminAPI.AccountView alias Pleroma.Web.AdminAPI.AccountView
alias Pleroma.Web.Endpoint alias Pleroma.Web.Endpoint
@ -706,8 +707,14 @@ def make_flag_data(%{actor: actor, context: context, content: content} = params,
def make_flag_data(_, _), do: %{} def make_flag_data(_, _), do: %{}
defp build_flag_object(%{account: account, statuses: statuses} = _) do defp build_flag_object(%{account: account, statuses: statuses} = _) do
[account.ap_id] ++ [account.ap_id] ++ build_flag_object(%{statuses: statuses})
Enum.map(statuses || [], fn act -> end
defp build_flag_object(%{statuses: statuses}) do
Enum.map(statuses || [], &build_flag_object/1)
end
defp build_flag_object(act) when is_map(act) or is_binary(act) do
id = id =
case act do case act do
%Activity{} = act -> act.data["id"] %Activity{} = act -> act.data["id"]
@ -725,7 +732,6 @@ defp build_flag_object(%{account: account, statuses: statuses} = _) do
"published" => activity.object.data["published"], "published" => activity.object.data["published"],
"actor" => AccountView.render("show.json", %{user: actor}) "actor" => AccountView.render("show.json", %{user: actor})
} }
end)
end end
defp build_flag_object(_), do: [] defp build_flag_object(_), do: []
@ -770,6 +776,94 @@ def fetch_ordered_collection(from, pages_left, acc \\ []) do
end end
#### Report-related helpers #### Report-related helpers
def get_reports(params, page, page_size) do
params =
params
|> Map.put("type", "Flag")
|> Map.put("skip_preload", true)
|> Map.put("total", true)
|> Map.put("limit", page_size)
|> Map.put("offset", (page - 1) * page_size)
ActivityPub.fetch_activities([], params, :offset)
end
@spec get_reports_grouped_by_status(%{required(:activity) => String.t()}) :: %{
required(:groups) => [
%{
required(:date) => String.t(),
required(:account) => %{},
required(:status) => %{},
required(:actors) => [%User{}],
required(:reports) => [%Activity{}]
}
],
required(:total) => integer
}
def get_reports_grouped_by_status(groups) do
parsed_groups =
groups
|> Enum.map(fn entry ->
activity =
case Jason.decode(entry.activity) do
{:ok, activity} -> activity
_ -> build_flag_object(entry.activity)
end
parse_report_group(activity)
end)
%{
groups: parsed_groups
}
end
def parse_report_group(activity) do
reports = get_reports_by_status_id(activity["id"])
max_date = Enum.max_by(reports, &NaiveDateTime.from_iso8601!(&1.data["published"]))
actors = Enum.map(reports, & &1.user_actor)
%{
date: max_date.data["published"],
account: activity["actor"],
status: %{
id: activity["id"],
content: activity["content"],
published: activity["published"]
},
actors: Enum.uniq(actors),
reports: reports
}
end
def get_reports_by_status_id(ap_id) do
from(a in Activity,
where: fragment("(?)->>'type' = 'Flag'", a.data),
where: fragment("(?)->'object' @> ?", a.data, ^[%{id: ap_id}])
)
|> Activity.with_preloaded_user_actor()
|> Repo.all()
end
@spec get_reported_activities() :: [
%{
required(:activity) => String.t(),
required(:date) => String.t()
}
]
def get_reported_activities do
from(a in Activity,
where: fragment("(?)->>'type' = 'Flag'", a.data),
select: %{
date: fragment("max(?->>'published') date", a.data),
activity:
fragment("jsonb_array_elements_text((? #- '{object,0}')->'object') activity", a.data)
},
group_by: fragment("activity"),
order_by: fragment("date DESC")
)
|> Repo.all()
end
def update_report_state(%Activity{} = activity, state) def update_report_state(%Activity{} = activity, state)
when state in @strip_status_report_states do when state in @strip_status_report_states do
@ -793,6 +887,18 @@ def update_report_state(%Activity{} = activity, state) when state in @supported_
|> Repo.update() |> Repo.update()
end end
def update_report_state(activity_ids, state) when state in @supported_report_states do
activities_num = length(activity_ids)
from(a in Activity, where: a.id in ^activity_ids)
|> update(set: [data: fragment("jsonb_set(data, '{state}', ?)", ^state)])
|> Repo.update_all([])
|> case do
{^activities_num, _} -> :ok
_ -> {:error, activity_ids}
end
end
def update_report_state(_, _), do: {:error, "Unsupported state"} def update_report_state(_, _), do: {:error, "Unsupported state"}
def strip_report_status_data(activity) do def strip_report_status_data(activity) do

View file

@ -11,6 +11,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
alias Pleroma.UserInviteToken alias Pleroma.UserInviteToken
alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Relay alias Pleroma.Web.ActivityPub.Relay
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.AdminAPI.AccountView alias Pleroma.Web.AdminAPI.AccountView
alias Pleroma.Web.AdminAPI.Config alias Pleroma.Web.AdminAPI.Config
alias Pleroma.Web.AdminAPI.ConfigView alias Pleroma.Web.AdminAPI.ConfigView
@ -624,19 +625,17 @@ def force_password_reset(%{assigns: %{user: admin}} = conn, %{"nicknames" => nic
def list_reports(conn, params) do def list_reports(conn, params) do
{page, page_size} = page_params(params) {page, page_size} = page_params(params)
params = conn
params |> put_view(ReportView)
|> Map.put("type", "Flag") |> render("index.json", %{reports: Utils.get_reports(params, page, page_size)})
|> Map.put("skip_preload", true) end
|> Map.put("total", true)
|> Map.put("limit", page_size)
|> Map.put("offset", (page - 1) * page_size)
reports = ActivityPub.fetch_activities([], params, :offset) def list_grouped_reports(conn, _params) do
reports = Utils.get_reported_activities()
conn conn
|> put_view(ReportView) |> put_view(ReportView)
|> render("index.json", %{reports: reports}) |> render("index_grouped.json", Utils.get_reports_grouped_by_status(reports))
end end
def report_show(conn, %{"id" => id}) do def report_show(conn, %{"id" => id}) do
@ -649,17 +648,26 @@ def report_show(conn, %{"id" => id}) do
end end
end end
def report_update_state(%{assigns: %{user: admin}} = conn, %{"id" => id, "state" => state}) do def reports_update(%{assigns: %{user: admin}} = conn, %{"reports" => reports}) do
with {:ok, report} <- CommonAPI.update_report_state(id, state) do result =
reports
|> Enum.map(fn report ->
with {:ok, activity} <- CommonAPI.update_report_state(report["id"], report["state"]) do
ModerationLog.insert_log(%{ ModerationLog.insert_log(%{
action: "report_update", action: "report_update",
actor: admin, actor: admin,
subject: report subject: activity
}) })
conn activity
|> put_view(ReportView) else
|> render("show.json", Report.extract_report_info(report)) {:error, message} -> %{id: report["id"], error: message}
end
end)
case Enum.any?(result, &Map.has_key?(&1, :error)) do
true -> json_response(conn, :bad_request, result)
false -> json_response(conn, :no_content, "")
end end
end end

View file

@ -42,6 +42,26 @@ def render("show.json", %{report: report, user: user, account: account, statuses
} }
end end
def render("index_grouped.json", %{groups: groups}) do
reports =
Enum.map(groups, fn group ->
%{
date: group[:date],
account: group[:account],
status: group[:status],
actors: Enum.map(group[:actors], &merge_account_views/1),
reports:
group[:reports]
|> Enum.map(&Report.extract_report_info(&1))
|> Enum.map(&render(__MODULE__, "show.json", &1))
}
end)
%{
reports: reports
}
end
defp merge_account_views(%User{} = user) do defp merge_account_views(%User{} = user) do
Pleroma.Web.MastodonAPI.AccountView.render("show.json", %{user: user}) Pleroma.Web.MastodonAPI.AccountView.render("show.json", %{user: user})
|> Map.merge(Pleroma.Web.AdminAPI.AccountView.render("show.json", %{user: user})) |> Map.merge(Pleroma.Web.AdminAPI.AccountView.render("show.json", %{user: user}))

View file

@ -370,6 +370,13 @@ defp get_reported_account(account_id) do
end end
end end
def update_report_state(activity_ids, state) when is_list(activity_ids) do
case Utils.update_report_state(activity_ids, state) do
:ok -> {:ok, activity_ids}
_ -> {:error, dgettext("errors", "Could not update state")}
end
end
def update_report_state(activity_id, state) do def update_report_state(activity_id, state) do
with %Activity{} = activity <- Activity.get_by_id(activity_id) do with %Activity{} = activity <- Activity.get_by_id(activity_id) do
Utils.update_report_state(activity, state) Utils.update_report_state(activity, state)

View file

@ -178,8 +178,9 @@ defmodule Pleroma.Web.Router do
get("/users/:nickname/statuses", AdminAPIController, :list_user_statuses) get("/users/:nickname/statuses", AdminAPIController, :list_user_statuses)
get("/reports", AdminAPIController, :list_reports) get("/reports", AdminAPIController, :list_reports)
get("/grouped_reports", AdminAPIController, :list_grouped_reports)
get("/reports/:id", AdminAPIController, :report_show) get("/reports/:id", AdminAPIController, :report_show)
put("/reports/:id", AdminAPIController, :report_update_state) patch("/reports", AdminAPIController, :reports_update)
post("/reports/:id/respond", AdminAPIController, :report_respond) post("/reports/:id/respond", AdminAPIController, :report_respond)
put("/statuses/:id", AdminAPIController, :status_update) put("/statuses/:id", AdminAPIController, :status_update)

View file

@ -636,4 +636,47 @@ test "removes actor from announcements" do
assert updated_object.data["announcement_count"] == 1 assert updated_object.data["announcement_count"] == 1
end end
end end
describe "get_reports_grouped_by_status/1" do
setup do
[reporter, target_user] = insert_pair(:user)
first_status = insert(:note_activity, user: target_user)
second_status = insert(:note_activity, user: target_user)
CommonAPI.report(reporter, %{
"account_id" => target_user.id,
"comment" => "I feel offended",
"status_ids" => [first_status.id]
})
CommonAPI.report(reporter, %{
"account_id" => target_user.id,
"comment" => "I feel offended2",
"status_ids" => [second_status.id]
})
data = [%{activity: first_status.data["id"]}, %{activity: second_status.data["id"]}]
{:ok,
%{
first_status: first_status,
second_status: second_status,
data: data
}}
end
test "works for deprecated reports format", %{
first_status: first_status,
second_status: second_status,
data: data
} do
groups = Utils.get_reports_grouped_by_status(data).groups
first_group = Enum.find(groups, &(&1.status.id == first_status.data["id"]))
second_group = Enum.find(groups, &(&1.status.id == second_status.data["id"]))
assert first_group.status.id == first_status.data["id"]
assert second_group.status.id == second_status.data["id"]
end
end
end end

View file

@ -1312,7 +1312,7 @@ test "returns 404 when report id is invalid", %{conn: conn} do
end end
end end
describe "PUT /api/pleroma/admin/reports/:id" do describe "PATCH /api/pleroma/admin/reports" do
setup %{conn: conn} do setup %{conn: conn} do
admin = insert(:user, is_admin: true) admin = insert(:user, is_admin: true)
[reporter, target_user] = insert_pair(:user) [reporter, target_user] = insert_pair(:user)
@ -1325,16 +1325,32 @@ test "returns 404 when report id is invalid", %{conn: conn} do
"status_ids" => [activity.id] "status_ids" => [activity.id]
}) })
%{conn: assign(conn, :user, admin), id: report_id, admin: admin} {:ok, %{id: second_report_id}} =
CommonAPI.report(reporter, %{
"account_id" => target_user.id,
"comment" => "I feel very offended",
"status_ids" => [activity.id]
})
%{
conn: assign(conn, :user, admin),
id: report_id,
admin: admin,
second_report_id: second_report_id
}
end end
test "mark report as resolved", %{conn: conn, id: id, admin: admin} do test "mark report as resolved", %{conn: conn, id: id, admin: admin} do
response =
conn conn
|> put("/api/pleroma/admin/reports/#{id}", %{"state" => "resolved"}) |> patch("/api/pleroma/admin/reports", %{
|> json_response(:ok) "reports" => [
%{"state" => "resolved", "id" => id}
]
})
|> json_response(:no_content)
assert response["state"] == "resolved" activity = Activity.get_by_id(id)
assert activity.data["state"] == "resolved"
log_entry = Repo.one(ModerationLog) log_entry = Repo.one(ModerationLog)
@ -1343,12 +1359,16 @@ test "mark report as resolved", %{conn: conn, id: id, admin: admin} do
end end
test "closes report", %{conn: conn, id: id, admin: admin} do test "closes report", %{conn: conn, id: id, admin: admin} do
response =
conn conn
|> put("/api/pleroma/admin/reports/#{id}", %{"state" => "closed"}) |> patch("/api/pleroma/admin/reports", %{
|> json_response(:ok) "reports" => [
%{"state" => "closed", "id" => id}
]
})
|> json_response(:no_content)
assert response["state"] == "closed" activity = Activity.get_by_id(id)
assert activity.data["state"] == "closed"
log_entry = Repo.one(ModerationLog) log_entry = Repo.one(ModerationLog)
@ -1359,17 +1379,54 @@ test "closes report", %{conn: conn, id: id, admin: admin} do
test "returns 400 when state is unknown", %{conn: conn, id: id} do test "returns 400 when state is unknown", %{conn: conn, id: id} do
conn = conn =
conn conn
|> put("/api/pleroma/admin/reports/#{id}", %{"state" => "test"}) |> patch("/api/pleroma/admin/reports", %{
"reports" => [
%{"state" => "test", "id" => id}
]
})
assert json_response(conn, :bad_request) == "Unsupported state" assert hd(json_response(conn, :bad_request))["error"] == "Unsupported state"
end end
test "returns 404 when report is not exist", %{conn: conn} do test "returns 404 when report is not exist", %{conn: conn} do
conn = conn =
conn conn
|> put("/api/pleroma/admin/reports/test", %{"state" => "closed"}) |> patch("/api/pleroma/admin/reports", %{
"reports" => [
%{"state" => "closed", "id" => "test"}
]
})
assert json_response(conn, :not_found) == "Not found" assert hd(json_response(conn, :bad_request))["error"] == "not_found"
end
test "updates state of multiple reports", %{
conn: conn,
id: id,
admin: admin,
second_report_id: second_report_id
} do
conn
|> patch("/api/pleroma/admin/reports", %{
"reports" => [
%{"state" => "resolved", "id" => id},
%{"state" => "closed", "id" => second_report_id}
]
})
|> json_response(:no_content)
activity = Activity.get_by_id(id)
second_activity = Activity.get_by_id(second_report_id)
assert activity.data["state"] == "resolved"
assert second_activity.data["state"] == "closed"
[first_log_entry, second_log_entry] = Repo.all(ModerationLog)
assert ModerationLog.get_log_entry_message(first_log_entry) ==
"@#{admin.nickname} updated report ##{id} with 'resolved' state"
assert ModerationLog.get_log_entry_message(second_log_entry) ==
"@#{admin.nickname} updated report ##{second_report_id} with 'closed' state"
end end
end end
@ -1492,7 +1549,145 @@ test "returns 403 when requested by anonymous" do
end end
end end
# describe "GET /api/pleroma/admin/grouped_reports" do
setup %{conn: conn} do
admin = insert(:user, is_admin: true)
[reporter, target_user] = insert_pair(:user)
date1 = (DateTime.to_unix(DateTime.utc_now()) + 1000) |> DateTime.from_unix!()
date2 = (DateTime.to_unix(DateTime.utc_now()) + 2000) |> DateTime.from_unix!()
date3 = (DateTime.to_unix(DateTime.utc_now()) + 3000) |> DateTime.from_unix!()
first_status =
insert(:note_activity, user: target_user, data_attrs: %{"published" => date1})
second_status =
insert(:note_activity, user: target_user, data_attrs: %{"published" => date2})
third_status =
insert(:note_activity, user: target_user, data_attrs: %{"published" => date3})
{:ok, first_report} =
CommonAPI.report(reporter, %{
"account_id" => target_user.id,
"status_ids" => [first_status.id, second_status.id, third_status.id]
})
{:ok, second_report} =
CommonAPI.report(reporter, %{
"account_id" => target_user.id,
"status_ids" => [first_status.id, second_status.id]
})
{:ok, third_report} =
CommonAPI.report(reporter, %{
"account_id" => target_user.id,
"status_ids" => [first_status.id]
})
%{
conn: assign(conn, :user, admin),
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_status_reports: [first_report, second_report, third_report],
second_status_reports: [first_report, second_report],
third_status_reports: [first_report],
target_user: target_user,
reporter: reporter
}
end
test "returns reports grouped by status", %{
conn: conn,
first_status: first_status,
second_status: second_status,
third_status: third_status,
first_status_reports: first_status_reports,
second_status_reports: second_status_reports,
third_status_reports: third_status_reports,
target_user: target_user,
reporter: reporter
} do
response =
conn
|> get("/api/pleroma/admin/grouped_reports")
|> json_response(:ok)
assert length(response["reports"]) == 3
first_group =
Enum.find(response["reports"], &(&1["status"]["id"] == first_status.data["id"]))
second_group =
Enum.find(response["reports"], &(&1["status"]["id"] == second_status.data["id"]))
third_group =
Enum.find(response["reports"], &(&1["status"]["id"] == third_status.data["id"]))
assert length(first_group["reports"]) == 3
assert length(second_group["reports"]) == 2
assert length(third_group["reports"]) == 1
assert first_group["date"] ==
Enum.max_by(first_status_reports, fn act ->
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["account"]["id"] == target_user.id
assert length(first_group["actors"]) == 1
assert hd(first_group["actors"])["id"] == reporter.id
assert Enum.map(first_group["reports"], & &1["id"]) --
Enum.map(first_status_reports, & &1.id) == []
assert second_group["date"] ==
Enum.max_by(second_status_reports, fn act ->
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["account"]["id"] == target_user.id
assert length(second_group["actors"]) == 1
assert hd(second_group["actors"])["id"] == reporter.id
assert Enum.map(second_group["reports"], & &1["id"]) --
Enum.map(second_status_reports, & &1.id) == []
assert third_group["date"] ==
Enum.max_by(third_status_reports, fn act ->
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["account"]["id"] == target_user.id
assert length(third_group["actors"]) == 1
assert hd(third_group["actors"])["id"] == reporter.id
assert Enum.map(third_group["reports"], & &1["id"]) --
Enum.map(third_status_reports, & &1.id) == []
end
end
describe "POST /api/pleroma/admin/reports/:id/respond" do describe "POST /api/pleroma/admin/reports/:id/respond" do
setup %{conn: conn} do setup %{conn: conn} do
admin = insert(:user, is_admin: true) admin = insert(:user, is_admin: true)

View file

@ -468,6 +468,35 @@ test "does not update report state when state is unsupported" do
assert CommonAPI.update_report_state(report_id, "test") == {:error, "Unsupported state"} assert CommonAPI.update_report_state(report_id, "test") == {:error, "Unsupported state"}
end end
test "updates state of multiple reports" do
[reporter, target_user] = insert_pair(:user)
activity = insert(:note_activity, user: target_user)
{:ok, %Activity{id: first_report_id}} =
CommonAPI.report(reporter, %{
"account_id" => target_user.id,
"comment" => "I feel offended",
"status_ids" => [activity.id]
})
{:ok, %Activity{id: second_report_id}} =
CommonAPI.report(reporter, %{
"account_id" => target_user.id,
"comment" => "I feel very offended!",
"status_ids" => [activity.id]
})
{:ok, report_ids} =
CommonAPI.update_report_state([first_report_id, second_report_id], "resolved")
first_report = Activity.get_by_id(first_report_id)
second_report = Activity.get_by_id(second_report_id)
assert report_ids -- [first_report_id, second_report_id] == []
assert first_report.data["state"] == "resolved"
assert second_report.data["state"] == "resolved"
end
end end
describe "reblog muting" do describe "reblog muting" do