logo

pleroma

My custom branche(s) on git.pleroma.social/pleroma/pleroma
commit: 1afeaf82fa3f1718233f5012f851912f87f35a88
parent: 574369b42dd7b62e41c3a4429e4e66b9a9b3483d
Author: feld <feld@feld.me>
Date:   Thu, 14 Nov 2019 13:35:41 +0000

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

Diffstat:

MCHANGELOG.md6+++++-
Mdocs/API/admin_api.md196++++++++++++++++++++++++++++++++++++++++++++++++-------------------------------
Mlib/pleroma/activity.ex17+++++++++++++++++
Mlib/pleroma/web/activity_pub/utils.ex142+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------
Mlib/pleroma/web/admin_api/admin_api_controller.ex46+++++++++++++++++++++++++++-------------------
Mlib/pleroma/web/admin_api/views/report_view.ex20++++++++++++++++++++
Mlib/pleroma/web/common_api/common_api.ex7+++++++
Mlib/pleroma/web/router.ex3++-
Mtest/web/activity_pub/utils_test.exs43+++++++++++++++++++++++++++++++++++++++++++
Mtest/web/admin_api/admin_api_controller_test.exs229+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------
Mtest/web/common_api/common_api_test.exs29+++++++++++++++++++++++++++++
11 files changed, 605 insertions(+), 133 deletions(-)

diff --git 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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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