summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorfeld <feld@feld.me>2019-11-14 13:35:41 +0000
committerfeld <feld@feld.me>2019-11-14 13:35:41 +0000
commit1afeaf82fa3f1718233f5012f851912f87f35a88 (patch)
tree8d0e6b91d34edc574cf18c9188eaadf2e69443b4
parent574369b42dd7b62e41c3a4429e4e66b9a9b3483d (diff)
parent46787b9fe1c61d5139198187a0d2013f1a2e5162 (diff)
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
-rw-r--r--CHANGELOG.md6
-rw-r--r--docs/API/admin_api.md196
-rw-r--r--lib/pleroma/activity.ex17
-rw-r--r--lib/pleroma/web/activity_pub/utils.ex142
-rw-r--r--lib/pleroma/web/admin_api/admin_api_controller.ex46
-rw-r--r--lib/pleroma/web/admin_api/views/report_view.ex20
-rw-r--r--lib/pleroma/web/common_api/common_api.ex7
-rw-r--r--lib/pleroma/web/router.ex3
-rw-r--r--test/web/activity_pub/utils_test.exs43
-rw-r--r--test/web/admin_api/admin_api_controller_test.exs229
-rw-r--r--test/web/common_api/common_api_test.exs29
11 files changed, 605 insertions, 133 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e04b96281..b4ad91b0d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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: 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.
- Admin API: Return `total` when querying for reports
- 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>
- 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
- 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
@@ -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`)
- 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).
+- 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
- 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
diff --git a/docs/API/admin_api.md b/docs/API/admin_api.md
index c042b08ac..9d914c9a6 100644
--- a/docs/API/admin_api.md
+++ b/docs/API/admin_api.md
@@ -2,11 +2,10 @@
Authentication is required and the user must be an admin.
-## `/api/pleroma/admin/users`
+## `GET /api/pleroma/admin/users`
### List users
-- Method `GET`
- Query Params:
- *optional* `query`: **string** search term (e.g. nickname, domain, nickname@domain)
- *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
-- Method `DELETE`
- Params:
- `nickname`
- Response: User’s nickname
@@ -60,7 +58,6 @@ Authentication is required and the user must be an admin.
### Remove a user
-- Method `DELETE`
- Params:
- `nicknames`
- Response: Array of user nicknames
@@ -78,31 +75,30 @@ Authentication is required and the user must be an admin.
]
- Response: User’s nickname
-## `/api/pleroma/admin/users/follow`
+## `POST /api/pleroma/admin/users/follow`
+
### Make a user follow another user
-- Methods: `POST`
- Params:
- - `follower`: The nickname of the follower
- - `followed`: The nickname of the followed
+ - `follower`: The nickname of the follower
+ - `followed`: The nickname of the followed
- Response:
- - "ok"
+ - "ok"
+
+## `POST /api/pleroma/admin/users/unfollow`
-## `/api/pleroma/admin/users/unfollow`
### Make a user unfollow another user
-- Methods: `POST`
- Params:
- - `follower`: The nickname of the follower
- - `followed`: The nickname of the followed
+ - `follower`: The nickname of the follower
+ - `followed`: The nickname of the followed
- Response:
- - "ok"
+ - "ok"
-## `/api/pleroma/admin/users/:nickname/toggle_activation`
+## `PATCH /api/pleroma/admin/users/:nickname/toggle_activation`
### Toggle user activation
-- Method: `PATCH`
- Params:
- `nickname`
- Response: User’s 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
-- Method: `PUT`
- Params:
- `nicknames` (array)
- `tags` (array)
+## `DELETE /api/pleroma/admin/users/tag`
+
### Untag a list of users
-- Method: `DELETE`
- Params:
- `nicknames` (array)
- `tags` (array)
-## `/api/pleroma/admin/users/:nickname/permission_group`
+## `GET /api/pleroma/admin/users/:nickname/permission_group`
### Get user user permission groups membership
-- Method: `GET`
- Params: none
- 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 doesn’t exist.
### Get user user permission groups membership per permission group
-- Method: `GET`
- Params: none
- 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`
+## `DELETE /api/pleroma/admin/users/:nickname/permission_group/:permission_group`
+
### Remove user from permission group
- Params: none
@@ -247,22 +243,20 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
- `nickname`
- `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
-- Method: `GET`
- Params:
- `nickname` or `id`
- Response:
- On failure: `Not found`
- 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
-- Method: `GET`
- Params:
- `nickname` or `id`
- *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 success: JSON array of user's latest statuses
-## `/api/pleroma/admin/relay`
+## `POST /api/pleroma/admin/relay`
### Follow a Relay
-- Methods: `POST`
- Params:
- `relay_url`
- Response:
- On success: URL of the followed relay
+## `DELETE /api/pleroma/admin/relay`
+
### Unfollow a Relay
-- Methods: `DELETE`
- Params:
- `relay_url`
- Response:
@@ -297,11 +291,10 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
- Response:
- 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
-- Methods: `POST`
- Params:
- *optional* `max_use` (integer)
- *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
-- Methods: `GET`
- Params: none
- 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
-- Methods: `POST`
- Params:
- `token`
- Response:
@@ -367,21 +358,18 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
}
```
-
-## `/api/pleroma/admin/users/email_invite`
+## `POST /api/pleroma/admin/users/email_invite`
### Sends registration invite via email
-- Methods: `POST`
- Params:
- `email`
- `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
-- Methods: `GET`
- Params: none
- 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
-- Methods: `PATCH`
- Params:
- `nicknames`
- Response: none (code `204`)
-## `/api/pleroma/admin/reports`
+## `GET /api/pleroma/admin/reports`
+
### Get a list of reports
-- Method `GET`
+
- Params:
- *optional* `state`: **string** the state of reports. Valid values are `open`, `closed` and `resolved`
- *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
{
- "total" : 1,
+ "totalReports" : 1,
"reports": [
{
"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
-- Method `GET`
+
- Params:
- `id`
- Response:
@@ -571,22 +584,41 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
- 404 Not Found `"Not found"`
- On success: JSON, Report object (see above)
-## `/api/pleroma/admin/reports/:id`
-### Change the state of the report
-- Method `PUT`
+## `PATCH /api/pleroma/admin/reports`
+
+### Change the state of one or multiple reports
+
- 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:
- On failure:
- - 400 Bad Request `"Unsupported state"`
- - 403 Forbidden `{"error": "error_msg"}`
- - 404 Not Found `"Not found"`
- - On success: JSON, Report object (see above)
+ - 400 Bad Request, JSON:
+
+ ```json
+ [
+ {
+ `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
-- Method `POST`
+
- Params:
- `id`
- `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
-- Method `PUT`
+
- Params:
- `id`
- `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"`
- On success: JSON, Mastodon Status entity
-## `/api/pleroma/admin/statuses/:id`
+## `DELETE /api/pleroma/admin/statuses/:id`
+
### Delete an individual reported status
-- Method `DELETE`
+
- Params:
- `id`
- Response:
@@ -681,11 +715,12 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
- 404 Not Found `"Not found"`
- 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
+
Copy settings on key `:pleroma` to DB.
-- Method `GET`
+
- Params: none
- 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
+
Copy all settings from DB to `config/prod.exported_from_db.secret.exs` with deletion from DB.
-- Method `GET`
+
- Params: none
- 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 only works with `:pleroma => :instance => :dynamic_configuration` setting to `true`.
-- Method `GET`
+
- Params: none
- 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
+
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"`.
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`
- `:instance` -> `:upload_limit`
-- Method `POST`
- Params:
- `configs` => [
- `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
-- Method `GET`
+
- Params:
- *optional* `page`: **integer** page number
- *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`
+
### Reload the instance's custom emoji
-* Method `POST`
-* Authentication: required
-* Params: None
-* Response: JSON, "ok" and 200 status
+
+- Authentication: required
+- Params: None
+- Response: JSON, "ok" and 200 status
diff --git a/lib/pleroma/activity.ex b/lib/pleroma/activity.ex
index c1065611b..7e283df32 100644
--- a/lib/pleroma/activity.ex
+++ b/lib/pleroma/activity.ex
@@ -41,6 +41,10 @@ defmodule Pleroma.Activity do
field(:actor, :string)
field(:recipients, {:array, :string}, default: [])
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
has_one(:bookmark, Bookmark)
has_many(:notifications, Notification, on_delete: :delete_all)
@@ -86,6 +90,19 @@ defmodule Pleroma.Activity do
|> preload([activity, object: object], object: object)
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
from([a] in query,
left_join: b in Bookmark,
diff --git a/lib/pleroma/web/activity_pub/utils.ex b/lib/pleroma/web/activity_pub/utils.ex
index 91cfa2194..c45662359 100644
--- a/lib/pleroma/web/activity_pub/utils.ex
+++ b/lib/pleroma/web/activity_pub/utils.ex
@@ -11,6 +11,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web
+ alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Visibility
alias Pleroma.Web.AdminAPI.AccountView
alias Pleroma.Web.Endpoint
@@ -706,26 +707,31 @@ defmodule Pleroma.Web.ActivityPub.Utils do
def make_flag_data(_, _), do: %{}
defp build_flag_object(%{account: account, statuses: statuses} = _) do
- [account.ap_id] ++
- Enum.map(statuses || [], fn act ->
- id =
- case act do
- %Activity{} = act -> act.data["id"]
- act when is_map(act) -> act["id"]
- act when is_binary(act) -> act
- end
+ [account.ap_id] ++ build_flag_object(%{statuses: statuses})
+ end
- activity = Activity.get_by_ap_id_with_object(id)
- actor = User.get_by_ap_id(activity.object.data["actor"])
+ defp build_flag_object(%{statuses: statuses}) do
+ Enum.map(statuses || [], &build_flag_object/1)
+ end
- %{
- "type" => "Note",
- "id" => activity.data["id"],
- "content" => activity.object.data["content"],
- "published" => activity.object.data["published"],
- "actor" => AccountView.render("show.json", %{user: actor})
- }
- end)
+ defp build_flag_object(act) when is_map(act) or is_binary(act) do
+ id =
+ case act do
+ %Activity{} = act -> act.data["id"]
+ act when is_map(act) -> act["id"]
+ act when is_binary(act) -> act
+ end
+
+ activity = Activity.get_by_ap_id_with_object(id)
+ actor = User.get_by_ap_id(activity.object.data["actor"])
+
+ %{
+ "type" => "Note",
+ "id" => activity.data["id"],
+ "content" => activity.object.data["content"],
+ "published" => activity.object.data["published"],
+ "actor" => AccountView.render("show.json", %{user: actor})
+ }
end
defp build_flag_object(_), do: []
@@ -770,6 +776,94 @@ defmodule Pleroma.Web.ActivityPub.Utils do
end
#### 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)
when state in @strip_status_report_states do
@@ -793,6 +887,18 @@ defmodule Pleroma.Web.ActivityPub.Utils do
|> Repo.update()
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 strip_report_status_data(activity) do
diff --git a/lib/pleroma/web/admin_api/admin_api_controller.ex b/lib/pleroma/web/admin_api/admin_api_controller.ex
index 30fc01755..8c1318d1b 100644
--- a/lib/pleroma/web/admin_api/admin_api_controller.ex
+++ b/lib/pleroma/web/admin_api/admin_api_controller.ex
@@ -11,6 +11,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
alias Pleroma.UserInviteToken
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Relay
+ alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.AdminAPI.AccountView
alias Pleroma.Web.AdminAPI.Config
alias Pleroma.Web.AdminAPI.ConfigView
@@ -624,19 +625,17 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
def list_reports(conn, params) do
{page, page_size} = page_params(params)
- 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)
+ conn
+ |> put_view(ReportView)
+ |> render("index.json", %{reports: Utils.get_reports(params, page, page_size)})
+ end
- reports = ActivityPub.fetch_activities([], params, :offset)
+ def list_grouped_reports(conn, _params) do
+ reports = Utils.get_reported_activities()
conn
|> put_view(ReportView)
- |> render("index.json", %{reports: reports})
+ |> render("index_grouped.json", Utils.get_reports_grouped_by_status(reports))
end
def report_show(conn, %{"id" => id}) do
@@ -649,17 +648,26 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
end
end
- def report_update_state(%{assigns: %{user: admin}} = conn, %{"id" => id, "state" => state}) do
- with {:ok, report} <- CommonAPI.update_report_state(id, state) do
- ModerationLog.insert_log(%{
- action: "report_update",
- actor: admin,
- subject: report
- })
+ def reports_update(%{assigns: %{user: admin}} = conn, %{"reports" => reports}) do
+ result =
+ reports
+ |> Enum.map(fn report ->
+ with {:ok, activity} <- CommonAPI.update_report_state(report["id"], report["state"]) do
+ ModerationLog.insert_log(%{
+ action: "report_update",
+ actor: admin,
+ subject: activity
+ })
+
+ activity
+ else
+ {:error, message} -> %{id: report["id"], error: message}
+ end
+ end)
- conn
- |> put_view(ReportView)
- |> render("show.json", Report.extract_report_info(report))
+ 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
diff --git a/lib/pleroma/web/admin_api/views/report_view.ex b/lib/pleroma/web/admin_api/views/report_view.ex
index 101a74c63..ca88595c7 100644
--- a/lib/pleroma/web/admin_api/views/report_view.ex
+++ b/lib/pleroma/web/admin_api/views/report_view.ex
@@ -42,6 +42,26 @@ defmodule Pleroma.Web.AdminAPI.ReportView do
}
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
Pleroma.Web.MastodonAPI.AccountView.render("show.json", %{user: user})
|> Map.merge(Pleroma.Web.AdminAPI.AccountView.render("show.json", %{user: user}))
diff --git a/lib/pleroma/web/common_api/common_api.ex b/lib/pleroma/web/common_api/common_api.ex
index f1937b1ec..fe6e26a90 100644
--- a/lib/pleroma/web/common_api/common_api.ex
+++ b/lib/pleroma/web/common_api/common_api.ex
@@ -370,6 +370,13 @@ defmodule Pleroma.Web.CommonAPI do
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
with %Activity{} = activity <- Activity.get_by_id(activity_id) do
Utils.update_report_state(activity, state)
diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex
index 93e7e45f9..9b8b373b8 100644
--- a/lib/pleroma/web/router.ex
+++ b/lib/pleroma/web/router.ex
@@ -178,8 +178,9 @@ defmodule Pleroma.Web.Router do
get("/users/:nickname/statuses", AdminAPIController, :list_user_statuses)
get("/reports", AdminAPIController, :list_reports)
+ get("/grouped_reports", AdminAPIController, :list_grouped_reports)
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)
put("/statuses/:id", AdminAPIController, :status_update)
diff --git a/test/web/activity_pub/utils_test.exs b/test/web/activity_pub/utils_test.exs
index 586eb1d2f..1feb076ba 100644
--- a/test/web/activity_pub/utils_test.exs
+++ b/test/web/activity_pub/utils_test.exs
@@ -636,4 +636,47 @@ defmodule Pleroma.Web.ActivityPub.UtilsTest do
assert updated_object.data["announcement_count"] == 1
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
diff --git a/test/web/admin_api/admin_api_controller_test.exs b/test/web/admin_api/admin_api_controller_test.exs
index bc9235309..3a4c4d65c 100644
--- a/test/web/admin_api/admin_api_controller_test.exs
+++ b/test/web/admin_api/admin_api_controller_test.exs
@@ -1312,7 +1312,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
end
end
- describe "PUT /api/pleroma/admin/reports/:id" do
+ describe "PATCH /api/pleroma/admin/reports" do
setup %{conn: conn} do
admin = insert(:user, is_admin: true)
[reporter, target_user] = insert_pair(:user)
@@ -1325,16 +1325,32 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
"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
test "mark report as resolved", %{conn: conn, id: id, admin: admin} do
- response =
- conn
- |> put("/api/pleroma/admin/reports/#{id}", %{"state" => "resolved"})
- |> json_response(:ok)
+ conn
+ |> patch("/api/pleroma/admin/reports", %{
+ "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)
@@ -1343,12 +1359,16 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
end
test "closes report", %{conn: conn, id: id, admin: admin} do
- response =
- conn
- |> put("/api/pleroma/admin/reports/#{id}", %{"state" => "closed"})
- |> json_response(:ok)
+ conn
+ |> patch("/api/pleroma/admin/reports", %{
+ "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)
@@ -1359,17 +1379,54 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
test "returns 400 when state is unknown", %{conn: conn, id: id} do
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
test "returns 404 when report is not exist", %{conn: conn} do
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
@@ -1492,7 +1549,145 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
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
setup %{conn: conn} do
admin = insert(:user, is_admin: true)
diff --git a/test/web/common_api/common_api_test.exs b/test/web/common_api/common_api_test.exs
index 09a3e8dcf..138488d44 100644
--- a/test/web/common_api/common_api_test.exs
+++ b/test/web/common_api/common_api_test.exs
@@ -468,6 +468,35 @@ defmodule Pleroma.Web.CommonAPITest do
assert CommonAPI.update_report_state(report_id, "test") == {:error, "Unsupported state"}
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
describe "reblog muting" do