Merge branch 'develop' into feature/reports-groups-and-multiple-state-update
authorMaxim Filippov <colixer@gmail.com>
Mon, 4 Nov 2019 23:12:48 +0000 (02:12 +0300)
committerMaxim Filippov <colixer@gmail.com>
Mon, 4 Nov 2019 23:12:48 +0000 (02:12 +0300)
CHANGELOG.md
docs/API/admin_api.md
lib/pleroma/activity.ex
lib/pleroma/web/activity_pub/utils.ex
lib/pleroma/web/admin_api/admin_api_controller.ex
lib/pleroma/web/admin_api/views/report_view.ex
lib/pleroma/web/common_api/common_api.ex
lib/pleroma/web/router.ex
test/web/admin_api/admin_api_controller_test.exs
test/web/common_api/common_api_test.exs

index 77edf7bf0d2897e780013109b3e7e061e853131b..64a28218d20fce42cb2595e2a06f9e029194d441 100644 (file)
@@ -53,10 +53,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
 - 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
+- Admin API: Add ability to fetch reports, grouped by status `GET /api/pleroma/admin/grouped_reports`
 
 ### Changed
 - **Breaking:** Elixir >=1.8 is now required (was >= 1.7)
 - **Breaking:** Admin API: Return link alongside with token on password reset
+- **Breaking:** Admin API: Changing report state now uses `PATCH` (it was `PUT` before) and allows updating multiple reports at once (API changed)
 - Replaced [pleroma_job_queue](https://git.pleroma.social/pleroma/pleroma_job_queue) and `Pleroma.Web.Federator.RetryQueue` with [Oban](https://github.com/sorentwo/oban) (see [`docs/config.md`](docs/config.md) on migrating customized worker / retry settings)
 - Introduced [quantum](https://github.com/quantum-elixir/quantum-core) job scheduler
 - Admin API: Return `total` when querying for reports
index e64ae6429496266b4dd4043a22f3922bc563f80c..9499d0140b3a0612001d861c73c21c682eb2cebd 100644 (file)
@@ -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
@@ -78,31 +76,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 +112,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 +142,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 +179,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 +244,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 +266,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 +292,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 +313,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 +338,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 +359,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 +381,17 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
 }
 ```
 
-
-## `/api/pleroma/admin/users/:nickname/force_password_reset`
+## `PATCH /api/pleroma/admin/users/:nickname/force_password_reset`
 
 ### Force passord reset for a user with a given nickname
 
-- Methods: `PATCH`
 - Params: none
 - 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,9 +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 +742,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 +763,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 +789,6 @@ Compile time settings (need instance reboot):
   - `Pleroma.Upload` -> `:proxy_remote`
   - `:instance` -> `:upload_limit`
 
-- Method `POST`
 - Params:
   - `configs` => [
     - `group` (string)
@@ -802,9 +843,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 +873,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
index c1065611bf31d9eccc5e8eba005ac8cc210c55e4..7b77f72c26fe5b8b2513d4715970728581b56209 100644 (file)
@@ -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,24 @@ 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:
+        fragment(
+          "? = ?->>'actor'",
+          u.ap_id,
+          activity.data
+        ),
+      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,
index d812fd734292cf9d3b481cbfe2dc520517653a3f..57349e30463cb7d8a190d426bd89acea91f99b5c 100644 (file)
@@ -6,11 +6,13 @@ defmodule Pleroma.Web.ActivityPub.Utils do
   alias Ecto.Changeset
   alias Ecto.UUID
   alias Pleroma.Activity
+  alias Pleroma.Activity.Queries
   alias Pleroma.Notification
   alias Pleroma.Object
   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
@@ -679,6 +681,92 @@ 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(:groups) => [
+            %{
+              required(:date) => String.t(),
+              required(:account) => %User{},
+              required(:status) => %Activity{},
+              required(:actors) => [%User{}],
+              required(:reports) => [%Activity{}]
+            }
+          ],
+          required(:total) => integer
+        }
+  def get_reports_grouped_by_status do
+    paginated_activities = get_reported_status_ids()
+
+    groups =
+      paginated_activities
+      |> Enum.map(fn entry ->
+        status =
+          Activity
+          |> Queries.by_ap_id(entry[:activity_id])
+          |> Activity.with_preloaded_object(:left)
+          |> Activity.with_preloaded_user_actor()
+          |> Repo.one()
+
+        reports = get_reports_by_status_id(status.data["id"])
+
+        max_date =
+          Enum.max_by(reports, &Pleroma.Web.CommonAPI.Utils.to_masto_date(&1.data["published"])).data[
+            "published"
+          ]
+
+        actors = Enum.map(reports, & &1.user_actor)
+
+        %{
+          date: max_date,
+          account: status.user_actor,
+          status: status,
+          actors: actors,
+          reports: reports
+        }
+      end)
+
+    %{
+      groups: groups
+    }
+  end
+
+  def get_reports_by_status_id(status_id) do
+    from(a in Activity,
+      where: fragment("(?)->>'type' = 'Flag'", a.data),
+      where: fragment("(?)->'object' \\? (?)", a.data, ^status_id)
+    )
+    |> Activity.with_preloaded_user_actor()
+    |> Repo.all()
+  end
+
+  @spec get_reported_status_ids() :: %{
+          required(:items) => [%Activity{}],
+          required(:total) => integer
+        }
+  def get_reported_status_ids do
+    from(a in Activity,
+      where: fragment("(?)->>'type' = 'Flag'", a.data),
+      select: %{
+        date: fragment("max(?->>'published') date", a.data),
+        activity_id:
+          fragment("jsonb_array_elements_text((? #- '{object,0}')->'object') activity_id", a.data)
+      },
+      group_by: fragment("activity_id"),
+      order_by: fragment("date DESC")
+    )
+    |> Repo.all()
+  end
 
   def update_report_state(%Activity{} = activity, state)
       when state in @strip_status_report_states do
@@ -702,6 +790,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
index b47618bde4b668238f1c9d6b9ec0f23eebdf459d..1f48ce8c17efd3cf120874b40e108b3675b00f6c 100644 (file)
@@ -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
@@ -618,19 +619,15 @@ 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)
-
-    reports = ActivityPub.fetch_activities([], params, :offset)
+    conn
+    |> put_view(ReportView)
+    |> render("index.json", %{reports: Utils.get_reports(params, page, page_size)})
+  end
 
+  def list_grouped_reports(conn, _params) do
     conn
     |> put_view(ReportView)
-    |> render("index.json", %{reports: reports})
+    |> render("index_grouped.json", Utils.get_reports_grouped_by_status())
   end
 
   def report_show(conn, %{"id" => id}) do
@@ -643,17 +640,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
 
index 101a74c630833ad17c123c355e32c9563097b1b8..ac25925dafd4442a12b394a9c67d5885827d81d3 100644 (file)
@@ -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: merge_account_views(group[:account]),
+          status: StatusView.render("show.json", %{activity: 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}))
index e5734562145769ab205f3ecd3249b291ea8caf54..4bf39baa94e6d2fddaf03b5e40c557b9184c9a59 100644 (file)
@@ -351,6 +351,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)
index f69c5c2bc610799d8725efcbb63852a83de3105c..7b9e9b1b73a3dbc50c25d7686ab286174d0742b8 100644 (file)
@@ -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)
index 045c87e952bef70158c0ddd5aca0c68607042f80..35367bed3469cfef76e0dee9be5019524de2a6ee 100644 (file)
@@ -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,74 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
     end
   end
 
-  #
+  describe "GET /api/pleroma/admin/grouped_reports" do
+    setup %{conn: conn} do
+      admin = insert(:user, info: %{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})
+
+      %{
+        conn: assign(conn, :user, admin),
+        reporter: reporter,
+        target_user: target_user,
+        first_status: first_status,
+        second_status: second_status,
+        third_status: third_status
+      }
+    end
+
+    test "returns reports grouped by status", %{
+      conn: conn,
+      reporter: reporter,
+      target_user: target_user,
+      first_status: first_status,
+      second_status: second_status,
+      third_status: third_status
+    } do
+      {:ok, %{id: _}} =
+        CommonAPI.report(reporter, %{
+          "account_id" => target_user.id,
+          "status_ids" => [first_status.id, second_status.id, third_status.id]
+        })
+
+      {:ok, %{id: _}} =
+        CommonAPI.report(reporter, %{
+          "account_id" => target_user.id,
+          "status_ids" => [first_status.id, second_status.id]
+        })
+
+      {:ok, %{id: _}} =
+        CommonAPI.report(reporter, %{
+          "account_id" => target_user.id,
+          "status_ids" => [first_status.id]
+        })
+
+      response =
+        conn
+        |> get("/api/pleroma/admin/grouped_reports")
+        |> json_response(:ok)
+
+      assert length(response["reports"]) == 3
+      [third_group, second_group, first_group] = response["reports"]
+
+      assert length(third_group["reports"]) == 3
+      assert length(second_group["reports"]) == 2
+      assert length(first_group["reports"]) == 1
+    end
+  end
+
   describe "POST /api/pleroma/admin/reports/:id/respond" do
     setup %{conn: conn} do
       admin = insert(:user, is_admin: true)
index 8e6fbd7f09c65f2795eb9dac90cb2b95996341b2..89990483959331edd99e5be346ad65580281547a 100644 (file)
@@ -441,6 +441,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