Admin API: Reports, grouped by status

This commit is contained in:
Maxim Filippov 2019-10-07 15:01:18 +03:00
parent 8dcc2f9f5e
commit 7aceaa517b
8 changed files with 230 additions and 20 deletions

View file

@ -11,6 +11,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Pleroma API: `GET /api/v1/pleroma/accounts/:id/scrobbles` to get a list of recently scrobbled items - Pleroma API: `GET /api/v1/pleroma/accounts/:id/scrobbles` to get a list of recently scrobbled items
- Pleroma API: `POST /api/v1/pleroma/scrobble` to scrobble a media item - Pleroma API: `POST /api/v1/pleroma/scrobble` to scrobble a media item
- Mastodon API: Add `upload_limit`, `avatar_upload_limit`, `background_upload_limit`, and `banner_upload_limit` to `/api/v1/instance` - Mastodon API: Add `upload_limit`, `avatar_upload_limit`, `background_upload_limit`, and `banner_upload_limit` to `/api/v1/instance`
- Admin API: Add ability to fetch reports, grouped by status `GET /api/pleroma/admin/grouped_reports`
### Changed ### Changed
- **Breaking:** Elixir >=1.8 is now required (was >= 1.7) - **Breaking:** Elixir >=1.8 is now required (was >= 1.7)

View file

@ -468,18 +468,32 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
} }
] ]
} }
], ]
"totalGroupedReports": 1, }
"groupedReports": [ ```
## `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-01-01", // date of the latest report "date": "2019-10-07T12:31:39.615149Z",
"account": { ... }, // author of the reported status "account": { ... },
"status": { ... }, // reported status "status": { ... },
"actors": [{ ... }, { ... }], // accounts that sent reports on the status "actors": [{ ... }, { ... }],
"reports": [{ ... }] "reports": [{ ... }]
} }
] ]
}
``` ```
## `GET /api/pleroma/admin/reports/:id` ## `GET /api/pleroma/admin/reports/:id`

View file

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

View file

@ -6,11 +6,13 @@ defmodule Pleroma.Web.ActivityPub.Utils do
alias Ecto.Changeset alias Ecto.Changeset
alias Ecto.UUID alias Ecto.UUID
alias Pleroma.Activity alias Pleroma.Activity
alias Pleroma.Activity.Queries
alias Pleroma.Notification alias Pleroma.Notification
alias Pleroma.Object alias Pleroma.Object
alias Pleroma.Repo alias Pleroma.Repo
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web alias Pleroma.Web
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Visibility alias Pleroma.Web.ActivityPub.Visibility
alias Pleroma.Web.Endpoint alias Pleroma.Web.Endpoint
alias Pleroma.Web.Router.Helpers alias Pleroma.Web.Router.Helpers
@ -664,6 +666,93 @@ def fetch_ordered_collection(from, pages_left, acc \\ []) do
#### Report-related helpers #### Report-related helpers
def get_reports(params, page, page_size) do
params =
params
|> Map.put("type", "Flag")
|> Map.put("skip_preload", true)
|> Map.put("total", true)
|> Map.put("limit", page_size)
|> Map.put("offset", (page - 1) * page_size)
ActivityPub.fetch_activities([], params, :offset)
end
@spec get_reports_grouped_by_status() :: %{
required(: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 @supported_report_states do def update_report_state(%Activity{} = activity, state) when state in @supported_report_states do
new_data = Map.put(activity.data, "state", state) new_data = Map.put(activity.data, "state", state)

View file

@ -10,6 +10,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
alias Pleroma.UserInviteToken alias Pleroma.UserInviteToken
alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Relay alias Pleroma.Web.ActivityPub.Relay
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.AdminAPI.AccountView alias Pleroma.Web.AdminAPI.AccountView
alias Pleroma.Web.AdminAPI.Config alias Pleroma.Web.AdminAPI.Config
alias Pleroma.Web.AdminAPI.ConfigView alias Pleroma.Web.AdminAPI.ConfigView
@ -455,19 +456,15 @@ def force_password_reset(conn, %{"nickname" => nickname}) do
def list_reports(conn, params) do def list_reports(conn, params) do
{page, page_size} = page_params(params) {page, page_size} = page_params(params)
params =
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 conn
|> put_view(ReportView) |> put_view(ReportView)
|> render("index.json", %{reports: reports}) |> 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_grouped.json", Utils.get_reports_grouped_by_status())
end end
def report_show(conn, %{"id" => id}) do def report_show(conn, %{"id" => id}) do

View file

@ -42,6 +42,26 @@ def render("show.json", %{report: report, user: user, account: account, statuses
} }
end end
def render("index_grouped.json", %{groups: groups}) do
reports =
Enum.map(groups, fn group ->
%{
date: group[:date],
account: 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 defp merge_account_views(%User{} = user) do
Pleroma.Web.MastodonAPI.AccountView.render("show.json", %{user: user}) Pleroma.Web.MastodonAPI.AccountView.render("show.json", %{user: user})
|> Map.merge(Pleroma.Web.AdminAPI.AccountView.render("show.json", %{user: user})) |> Map.merge(Pleroma.Web.AdminAPI.AccountView.render("show.json", %{user: user}))

View file

@ -193,6 +193,7 @@ defmodule Pleroma.Web.Router do
get("/users/:nickname/statuses", AdminAPIController, :list_user_statuses) get("/users/:nickname/statuses", AdminAPIController, :list_user_statuses)
get("/reports", AdminAPIController, :list_reports) get("/reports", AdminAPIController, :list_reports)
get("/grouped_reports", AdminAPIController, :list_grouped_reports)
get("/reports/:id", AdminAPIController, :report_show) get("/reports/:id", AdminAPIController, :report_show)
patch("/reports", AdminAPIController, :reports_update) patch("/reports", AdminAPIController, :reports_update)
post("/reports/:id/respond", AdminAPIController, :report_respond) post("/reports/:id/respond", AdminAPIController, :report_respond)

View file

@ -1461,7 +1461,74 @@ test "returns 403 when requested by anonymous" do
end end
end end
# describe "GET /api/pleroma/admin/grouped_reports" do
setup %{conn: conn} do
admin = insert(:user, 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 describe "POST /api/pleroma/admin/reports/:id/respond" do
setup %{conn: conn} do setup %{conn: conn} do
admin = insert(:user, info: %{is_admin: true}) admin = insert(:user, info: %{is_admin: true})