Add Reports to Admin API Closes #774 See merge request pleroma/pleroma!1107tags/v1.1.4
@@ -24,6 +24,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). | |||
- Admin API: Endpoints for listing/revoking invite tokens | |||
- Admin API: Endpoints for making users follow/unfollow each other | |||
- Admin API: added filters (role, tags, email, name) for users endpoint | |||
- Admin API: Endpoints for managing reports | |||
- Admin API: Endpoints for deleting and changing the scope of individual reported statuses | |||
- AdminFE: initial release with basic user management accessible at /pleroma/admin/ | |||
- Mastodon API: [Scheduled statuses](https://docs.joinmastodon.org/api/rest/scheduled-statuses/) | |||
- Mastodon API: `/api/v1/notifications/destroy_multiple` (glitch-soc extension) | |||
@@ -24,7 +24,7 @@ Authentication is required and the user must be an admin. | |||
- Example: `https://mypleroma.org/api/pleroma/admin/users?query=john&filters=local,active&page=1&page_size=10&tags[]=some_tag&tags[]=another_tag&name=display_name&email=email@example.com` | |||
- Response: | |||
```JSON | |||
```json | |||
{ | |||
"page_size": integer, | |||
"count": integer, | |||
@@ -92,7 +92,7 @@ Authentication is required and the user must be an admin. | |||
- `nickname` | |||
- Response: User’s object | |||
```JSON | |||
```json | |||
{ | |||
"deactivated": bool, | |||
"id": integer, | |||
@@ -124,7 +124,7 @@ Authentication is required and the user must be an admin. | |||
- Params: none | |||
- Response: | |||
```JSON | |||
```json | |||
{ | |||
"is_moderator": bool, | |||
"is_admin": bool | |||
@@ -141,7 +141,7 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret | |||
- Params: none | |||
- Response: | |||
```JSON | |||
```json | |||
{ | |||
"is_moderator": bool, | |||
"is_admin": bool | |||
@@ -223,7 +223,7 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret | |||
- Params: none | |||
- Response: | |||
```JSON | |||
```json | |||
{ | |||
"invites": [ | |||
@@ -250,7 +250,7 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret | |||
- `token` | |||
- Response: | |||
```JSON | |||
```json | |||
{ | |||
"id": integer, | |||
"token": string, | |||
@@ -280,3 +280,280 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret | |||
- Methods: `GET` | |||
- Params: none | |||
- Response: password reset token (base64 string) | |||
## `/api/pleroma/admin/reports` | |||
### Get a list of reports | |||
- Method `GET` | |||
- Params: | |||
- `state`: optional, the state of reports. Valid values are `open`, `closed` and `resolved` | |||
- `limit`: optional, the number of records to retrieve | |||
- `since_id`: optional, returns results that are more recent than the specified id | |||
- `max_id`: optional, returns results that are older than the specified id | |||
- Response: | |||
- On failure: 403 Forbidden error `{"error": "error_msg"}` when requested by anonymous or non-admin | |||
- On success: JSON, returns a list of reports, where: | |||
- `account`: the user who has been reported | |||
- `actor`: the user who has sent the report | |||
- `statuses`: list of statuses that have been included to the report | |||
```json | |||
{ | |||
"reports": [ | |||
{ | |||
"account": { | |||
"acct": "user", | |||
"avatar": "https://pleroma.example.org/images/avi.png", | |||
"avatar_static": "https://pleroma.example.org/images/avi.png", | |||
"bot": false, | |||
"created_at": "2019-04-23T17:32:04.000Z", | |||
"display_name": "User", | |||
"emojis": [], | |||
"fields": [], | |||
"followers_count": 1, | |||
"following_count": 1, | |||
"header": "https://pleroma.example.org/images/banner.png", | |||
"header_static": "https://pleroma.example.org/images/banner.png", | |||
"id": "9i6dAJqSGSKMzLG2Lo", | |||
"locked": false, | |||
"note": "", | |||
"pleroma": { | |||
"confirmation_pending": false, | |||
"hide_favorites": true, | |||
"hide_followers": false, | |||
"hide_follows": false, | |||
"is_admin": false, | |||
"is_moderator": false, | |||
"relationship": {}, | |||
"tags": [] | |||
}, | |||
"source": { | |||
"note": "", | |||
"pleroma": {}, | |||
"sensitive": false | |||
}, | |||
"statuses_count": 3, | |||
"url": "https://pleroma.example.org/users/user", | |||
"username": "user" | |||
}, | |||
"actor": { | |||
"acct": "lain", | |||
"avatar": "https://pleroma.example.org/images/avi.png", | |||
"avatar_static": "https://pleroma.example.org/images/avi.png", | |||
"bot": false, | |||
"created_at": "2019-03-28T17:36:03.000Z", | |||
"display_name": "Roger Braun", | |||
"emojis": [], | |||
"fields": [], | |||
"followers_count": 1, | |||
"following_count": 1, | |||
"header": "https://pleroma.example.org/images/banner.png", | |||
"header_static": "https://pleroma.example.org/images/banner.png", | |||
"id": "9hEkA5JsvAdlSrocam", | |||
"locked": false, | |||
"note": "", | |||
"pleroma": { | |||
"confirmation_pending": false, | |||
"hide_favorites": false, | |||
"hide_followers": false, | |||
"hide_follows": false, | |||
"is_admin": false, | |||
"is_moderator": false, | |||
"relationship": {}, | |||
"tags": [] | |||
}, | |||
"source": { | |||
"note": "", | |||
"pleroma": {}, | |||
"sensitive": false | |||
}, | |||
"statuses_count": 1, | |||
"url": "https://pleroma.example.org/users/lain", | |||
"username": "lain" | |||
}, | |||
"content": "Please delete it", | |||
"created_at": "2019-04-29T19:48:15.000Z", | |||
"id": "9iJGOv1j8hxuw19bcm", | |||
"state": "open", | |||
"statuses": [ | |||
{ | |||
"account": { ... }, | |||
"application": { | |||
"name": "Web", | |||
"website": null | |||
}, | |||
"bookmarked": false, | |||
"card": null, | |||
"content": "<span class=\"h-card\"><a data-user=\"9hEkA5JsvAdlSrocam\" class=\"u-url mention\" href=\"https://pleroma.example.org/users/lain\">@<span>lain</span></a></span> click on my link <a href=\"https://www.google.com/\">https://www.google.com/</a>", | |||
"created_at": "2019-04-23T19:15:47.000Z", | |||
"emojis": [], | |||
"favourited": false, | |||
"favourites_count": 0, | |||
"id": "9i6mQ9uVrrOmOime8m", | |||
"in_reply_to_account_id": null, | |||
"in_reply_to_id": null, | |||
"language": null, | |||
"media_attachments": [], | |||
"mentions": [ | |||
{ | |||
"acct": "lain", | |||
"id": "9hEkA5JsvAdlSrocam", | |||
"url": "https://pleroma.example.org/users/lain", | |||
"username": "lain" | |||
}, | |||
{ | |||
"acct": "user", | |||
"id": "9i6dAJqSGSKMzLG2Lo", | |||
"url": "https://pleroma.example.org/users/user", | |||
"username": "user" | |||
} | |||
], | |||
"muted": false, | |||
"pinned": false, | |||
"pleroma": { | |||
"content": { | |||
"text/plain": "@lain click on my link https://www.google.com/" | |||
}, | |||
"conversation_id": 28, | |||
"in_reply_to_account_acct": null, | |||
"local": true, | |||
"spoiler_text": { | |||
"text/plain": "" | |||
} | |||
}, | |||
"reblog": null, | |||
"reblogged": false, | |||
"reblogs_count": 0, | |||
"replies_count": 0, | |||
"sensitive": false, | |||
"spoiler_text": "", | |||
"tags": [], | |||
"uri": "https://pleroma.example.org/objects/8717b90f-8e09-4b58-97b0-e3305472b396", | |||
"url": "https://pleroma.example.org/notice/9i6mQ9uVrrOmOime8m", | |||
"visibility": "direct" | |||
} | |||
] | |||
} | |||
] | |||
} | |||
``` | |||
## `/api/pleroma/admin/reports/:id` | |||
### Get an individual report | |||
- Method `GET` | |||
- Params: | |||
- `id` | |||
- Response: | |||
- On failure: | |||
- 403 Forbidden `{"error": "error_msg"}` | |||
- 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` | |||
- Params: | |||
- `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) | |||
## `/api/pleroma/admin/reports/:id/respond` | |||
### Respond to a report | |||
- Method `POST` | |||
- Params: | |||
- `id` | |||
- `status`: required, the message | |||
- Response: | |||
- On failure: | |||
- 400 Bad Request `"Invalid parameters"` when `status` is missing | |||
- 403 Forbidden `{"error": "error_msg"}` | |||
- 404 Not Found `"Not found"` | |||
- On success: JSON, created Mastodon Status entity | |||
```json | |||
{ | |||
"account": { ... }, | |||
"application": { | |||
"name": "Web", | |||
"website": null | |||
}, | |||
"bookmarked": false, | |||
"card": null, | |||
"content": "Your claim is going to be closed", | |||
"created_at": "2019-05-11T17:13:03.000Z", | |||
"emojis": [], | |||
"favourited": false, | |||
"favourites_count": 0, | |||
"id": "9ihuiSL1405I65TmEq", | |||
"in_reply_to_account_id": null, | |||
"in_reply_to_id": null, | |||
"language": null, | |||
"media_attachments": [], | |||
"mentions": [ | |||
{ | |||
"acct": "user", | |||
"id": "9i6dAJqSGSKMzLG2Lo", | |||
"url": "https://pleroma.example.org/users/user", | |||
"username": "user" | |||
}, | |||
{ | |||
"acct": "admin", | |||
"id": "9hEkA5JsvAdlSrocam", | |||
"url": "https://pleroma.example.org/users/admin", | |||
"username": "admin" | |||
} | |||
], | |||
"muted": false, | |||
"pinned": false, | |||
"pleroma": { | |||
"content": { | |||
"text/plain": "Your claim is going to be closed" | |||
}, | |||
"conversation_id": 35, | |||
"in_reply_to_account_acct": null, | |||
"local": true, | |||
"spoiler_text": { | |||
"text/plain": "" | |||
} | |||
}, | |||
"reblog": null, | |||
"reblogged": false, | |||
"reblogs_count": 0, | |||
"replies_count": 0, | |||
"sensitive": false, | |||
"spoiler_text": "", | |||
"tags": [], | |||
"uri": "https://pleroma.example.org/objects/cab0836d-9814-46cd-a0ea-529da9db5fcb", | |||
"url": "https://pleroma.example.org/notice/9ihuiSL1405I65TmEq", | |||
"visibility": "direct" | |||
} | |||
``` | |||
## `/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` | |||
- `visibility`: optional, valid values are `public`, `private` and `unlisted` | |||
- Response: | |||
- On failure: | |||
- 400 Bad Request `"Unsupported visibility"` | |||
- 403 Forbidden `{"error": "error_msg"}` | |||
- 404 Not Found `"Not found"` | |||
- On success: JSON, Mastodon Status entity | |||
## `/api/pleroma/admin/statuses/:id` | |||
### Delete an individual reported status | |||
- Method `DELETE` | |||
- Params: | |||
- `id` | |||
- Response: | |||
- On failure: | |||
- 403 Forbidden `{"error": "error_msg"}` | |||
- 404 Not Found `"Not found"` | |||
- On success: 200 OK `{}` |
@@ -111,7 +111,7 @@ defmodule Pleroma.Activity do | |||
def change(struct, params \\ %{}) do | |||
struct | |||
|> cast(params, [:data]) | |||
|> cast(params, [:data, :recipients]) | |||
|> validate_required([:data]) | |||
|> unique_constraint(:ap_id, name: :activities_unique_apid_index) | |||
end | |||
@@ -29,7 +29,7 @@ defmodule Pleroma.Emails.AdminEmail do | |||
end | |||
statuses_html = | |||
if length(statuses) > 0 do | |||
if is_list(statuses) && length(statuses) > 0 do | |||
statuses_list_html = | |||
statuses | |||
|> Enum.map(fn | |||
@@ -703,6 +703,12 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do | |||
defp restrict_type(query, _), do: query | |||
defp restrict_state(query, %{"state" => state}) do | |||
from(activity in query, where: fragment("?->>'state' = ?", activity.data, ^state)) | |||
end | |||
defp restrict_state(query, _), do: query | |||
defp restrict_favorited_by(query, %{"favorited_by" => ap_id}) do | |||
from( | |||
activity in query, | |||
@@ -855,6 +861,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do | |||
|> restrict_local(opts) | |||
|> restrict_actor(opts) | |||
|> restrict_type(opts) | |||
|> restrict_state(opts) | |||
|> restrict_favorited_by(opts) | |||
|> restrict_blocked(opts) | |||
|> restrict_muted(opts) | |||
@@ -20,6 +20,8 @@ defmodule Pleroma.Web.ActivityPub.Utils do | |||
require Logger | |||
@supported_object_types ["Article", "Note", "Video", "Page"] | |||
@supported_report_states ~w(open closed resolved) | |||
@valid_visibilities ~w(public unlisted private direct) | |||
# Some implementations send the actor URI as the actor field, others send the entire actor object, | |||
# so figure out what the actor's URI is based on what we have. | |||
@@ -670,7 +672,8 @@ defmodule Pleroma.Web.ActivityPub.Utils do | |||
"actor" => params.actor.ap_id, | |||
"content" => params.content, | |||
"object" => object, | |||
"context" => params.context | |||
"context" => params.context, | |||
"state" => "open" | |||
} | |||
|> Map.merge(additional) | |||
end | |||
@@ -713,4 +716,77 @@ defmodule Pleroma.Web.ActivityPub.Utils do | |||
end | |||
end | |||
end | |||
#### Report-related helpers | |||
def update_report_state(%Activity{} = activity, state) when state in @supported_report_states do | |||
with new_data <- Map.put(activity.data, "state", state), | |||
changeset <- Changeset.change(activity, data: new_data), | |||
{:ok, activity} <- Repo.update(changeset) do | |||
{:ok, activity} | |||
end | |||
end | |||
def update_report_state(_, _), do: {:error, "Unsupported state"} | |||
def update_activity_visibility(activity, visibility) when visibility in @valid_visibilities do | |||
[to, cc, recipients] = | |||
activity | |||
|> get_updated_targets(visibility) | |||
|> Enum.map(&Enum.uniq/1) | |||
object_data = | |||
activity.object.data | |||
|> Map.put("to", to) | |||
|> Map.put("cc", cc) | |||
{:ok, object} = | |||
activity.object | |||
|> Object.change(%{data: object_data}) | |||
|> Object.update_and_set_cache() | |||
activity_data = | |||
activity.data | |||
|> Map.put("to", to) | |||
|> Map.put("cc", cc) | |||
activity | |||
|> Map.put(:object, object) | |||
|> Activity.change(%{data: activity_data, recipients: recipients}) | |||
|> Repo.update() | |||
end | |||
def update_activity_visibility(_, _), do: {:error, "Unsupported visibility"} | |||
defp get_updated_targets( | |||
%Activity{data: %{"to" => to} = data, recipients: recipients}, | |||
visibility | |||
) do | |||
cc = Map.get(data, "cc", []) | |||
follower_address = User.get_cached_by_ap_id(data["actor"]).follower_address | |||
public = "https://www.w3.org/ns/activitystreams#Public" | |||
case visibility do | |||
"public" -> | |||
to = [public | List.delete(to, follower_address)] | |||
cc = [follower_address | List.delete(cc, public)] | |||
recipients = [public | recipients] | |||
[to, cc, recipients] | |||
"private" -> | |||
to = [follower_address | List.delete(to, public)] | |||
cc = List.delete(cc, public) | |||
recipients = List.delete(recipients, public) | |||
[to, cc, recipients] | |||
"unlisted" -> | |||
to = [follower_address | List.delete(to, public)] | |||
cc = [public | List.delete(cc, follower_address)] | |||
recipients = recipients ++ [follower_address, public] | |||
[to, cc, recipients] | |||
_ -> | |||
[to, cc, recipients] | |||
end | |||
end | |||
end |
@@ -4,11 +4,16 @@ | |||
defmodule Pleroma.Web.AdminAPI.AdminAPIController do | |||
use Pleroma.Web, :controller | |||
alias Pleroma.Activity | |||
alias Pleroma.User | |||
alias Pleroma.UserInviteToken | |||
alias Pleroma.Web.ActivityPub.ActivityPub | |||
alias Pleroma.Web.ActivityPub.Relay | |||
alias Pleroma.Web.AdminAPI.AccountView | |||
alias Pleroma.Web.AdminAPI.ReportView | |||
alias Pleroma.Web.AdminAPI.Search | |||
alias Pleroma.Web.CommonAPI | |||
alias Pleroma.Web.MastodonAPI.StatusView | |||
import Pleroma.Web.ControllerHelper, only: [json_response: 3] | |||
@@ -287,12 +292,88 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do | |||
|> json(token.token) | |||
end | |||
def list_reports(conn, params) do | |||
params = | |||
params | |||
|> Map.put("type", "Flag") | |||
|> Map.put("skip_preload", true) | |||
reports = | |||
[] | |||
|> ActivityPub.fetch_activities(params) | |||
|> Enum.reverse() | |||
conn | |||
|> put_view(ReportView) | |||
|> render("index.json", %{reports: reports}) | |||
end | |||
def report_show(conn, %{"id" => id}) do | |||
with %Activity{} = report <- Activity.get_by_id(id) do | |||
conn | |||
|> put_view(ReportView) | |||
|> render("show.json", %{report: report}) | |||
else | |||
_ -> {:error, :not_found} | |||
end | |||
end | |||
def report_update_state(conn, %{"id" => id, "state" => state}) do | |||
with {:ok, report} <- CommonAPI.update_report_state(id, state) do | |||
conn | |||
|> put_view(ReportView) | |||
|> render("show.json", %{report: report}) | |||
end | |||
end | |||
def report_respond(%{assigns: %{user: user}} = conn, %{"id" => id} = params) do | |||
with false <- is_nil(params["status"]), | |||
%Activity{} <- Activity.get_by_id(id) do | |||
params = | |||
params | |||
|> Map.put("in_reply_to_status_id", id) | |||
|> Map.put("visibility", "direct") | |||
{:ok, activity} = CommonAPI.post(user, params) | |||
conn | |||
|> put_view(StatusView) | |||
|> render("status.json", %{activity: activity}) | |||
else | |||
true -> | |||
{:param_cast, nil} | |||
nil -> | |||
{:error, :not_found} | |||
end | |||
end | |||
def status_update(conn, %{"id" => id} = params) do | |||
with {:ok, activity} <- CommonAPI.update_activity_scope(id, params) do | |||
conn | |||
|> put_view(StatusView) | |||
|> render("status.json", %{activity: activity}) | |||
end | |||
end | |||
def status_delete(%{assigns: %{user: user}} = conn, %{"id" => id}) do | |||
with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do | |||
json(conn, %{}) | |||
end | |||
end | |||
def errors(conn, {:error, :not_found}) do | |||
conn | |||
|> put_status(404) | |||
|> json("Not found") | |||
end | |||
def errors(conn, {:error, reason}) do | |||
conn | |||
|> put_status(400) | |||
|> json(reason) | |||
end | |||
def errors(conn, {:param_cast, _}) do | |||
conn | |||
|> put_status(400) | |||
@@ -0,0 +1,41 @@ | |||
# Pleroma: A lightweight social networking server | |||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> | |||
# SPDX-License-Identifier: AGPL-3.0-only | |||
defmodule Pleroma.Web.AdminAPI.ReportView do | |||
use Pleroma.Web, :view | |||
alias Pleroma.Activity | |||
alias Pleroma.User | |||
alias Pleroma.Web.CommonAPI.Utils | |||
alias Pleroma.Web.MastodonAPI.AccountView | |||
alias Pleroma.Web.MastodonAPI.StatusView | |||
def render("index.json", %{reports: reports}) do | |||
%{ | |||
reports: render_many(reports, __MODULE__, "show.json", as: :report) | |||
} | |||
end | |||
def render("show.json", %{report: report}) do | |||
user = User.get_cached_by_ap_id(report.data["actor"]) | |||
created_at = Utils.to_masto_date(report.data["published"]) | |||
[account_ap_id | status_ap_ids] = report.data["object"] | |||
account = User.get_cached_by_ap_id(account_ap_id) | |||
statuses = | |||
Enum.map(status_ap_ids, fn ap_id -> | |||
Activity.get_by_ap_id_with_object(ap_id) | |||
end) | |||
%{ | |||
id: report.id, | |||
account: AccountView.render("account.json", %{user: account}), | |||
actor: AccountView.render("account.json", %{user: user}), | |||
content: report.data["content"], | |||
created_at: created_at, | |||
statuses: StatusView.render("index.json", %{activities: statuses, as: :activity}), | |||
state: report.data["state"] | |||
} | |||
end | |||
end |
@@ -71,6 +71,9 @@ defmodule Pleroma.Web.CommonAPI do | |||
{:ok, _} <- unpin(activity_id, user), | |||
{:ok, delete} <- ActivityPub.delete(object) do | |||
{:ok, delete} | |||
else | |||
_ -> | |||
{:error, "Could not delete"} | |||
end | |||
end | |||
@@ -315,6 +318,60 @@ defmodule Pleroma.Web.CommonAPI do | |||
end | |||
end | |||
def update_report_state(activity_id, state) do | |||
with %Activity{} = activity <- Activity.get_by_id(activity_id), | |||
{:ok, activity} <- Utils.update_report_state(activity, state) do | |||
{:ok, activity} | |||
else | |||
nil -> | |||
{:error, :not_found} | |||
{:error, reason} -> | |||
{:error, reason} | |||
_ -> | |||
{:error, "Could not update state"} | |||
end | |||
end | |||
def update_activity_scope(activity_id, opts \\ %{}) do | |||
with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id), | |||
{:ok, activity} <- toggle_sensitive(activity, opts), | |||
{:ok, activity} <- set_visibility(activity, opts) do | |||
{:ok, activity} | |||
else | |||
nil -> | |||
{:error, :not_found} | |||
{:error, reason} -> | |||
{:error, reason} | |||
end | |||
end | |||
defp toggle_sensitive(activity, %{"sensitive" => sensitive}) when sensitive in ~w(true false) do | |||
toggle_sensitive(activity, %{"sensitive" => String.to_existing_atom(sensitive)}) | |||
end | |||
defp toggle_sensitive(%Activity{object: object} = activity, %{"sensitive" => sensitive}) | |||
when is_boolean(sensitive) do | |||
new_data = Map.put(object.data, "sensitive", sensitive) | |||
{:ok, object} = | |||
object | |||
|> Object.change(%{data: new_data}) | |||
|> Object.update_and_set_cache() | |||
{:ok, Map.put(activity, :object, object)} | |||
end | |||
defp toggle_sensitive(activity, _), do: {:ok, activity} | |||
defp set_visibility(activity, %{"visibility" => visibility}) do | |||
Utils.update_activity_visibility(activity, visibility) | |||
end | |||
defp set_visibility(activity, _), do: {:ok, activity} | |||
def hide_reblogs(user, muted) do | |||
ap_id = muted.ap_id | |||
@@ -237,13 +237,11 @@ defmodule Pleroma.Web.CommonAPI.Utils do | |||
"tag" => tags |> Enum.map(fn {_, tag} -> tag end) |> Enum.uniq() | |||
} | |||
if in_reply_to do | |||
in_reply_to_object = Object.normalize(in_reply_to) | |||
object | |||
|> Map.put("inReplyTo", in_reply_to_object.data["id"]) | |||
with false <- is_nil(in_reply_to), | |||
%Object{} = in_reply_to_object <- Object.normalize(in_reply_to) do | |||
Map.put(object, "inReplyTo", in_reply_to_object.data["id"]) | |||
else | |||
object | |||
_ -> object | |||
end | |||
end | |||
@@ -194,6 +194,14 @@ defmodule Pleroma.Web.Router do | |||
get("/users", AdminAPIController, :list_users) | |||
get("/users/:nickname", AdminAPIController, :user_show) | |||
get("/reports", AdminAPIController, :list_reports) | |||
get("/reports/:id", AdminAPIController, :report_show) | |||
put("/reports/:id", AdminAPIController, :report_update_state) | |||
post("/reports/:id/respond", AdminAPIController, :report_respond) | |||
put("/statuses/:id", AdminAPIController, :status_update) | |||
delete("/statuses/:id", AdminAPIController, :status_delete) | |||
end | |||
scope "/", Pleroma.Web.TwitterAPI do | |||
@@ -0,0 +1,19 @@ | |||
defmodule Pleroma.Repo.Migrations.SetDefaultStateToReports do | |||
use Ecto.Migration | |||
def up do | |||
execute """ | |||
UPDATE activities AS a | |||
SET data = jsonb_set(data, '{state}', '"open"', true) | |||
WHERE data->>'type' = 'Flag' | |||
""" | |||
end | |||
def down do | |||
execute """ | |||
UPDATE activities AS a | |||
SET data = data #- '{state}' | |||
WHERE data->>'type' = 'Flag' | |||
""" | |||
end | |||
end |
@@ -43,7 +43,7 @@ defmodule Pleroma.Factory do | |||
def note_factory(attrs \\ %{}) do | |||
text = sequence(:text, &"This is :moominmamma: note #{&1}") | |||
user = insert(:user) | |||
user = attrs[:user] || insert(:user) | |||
data = %{ | |||
"type" => "Note", | |||
@@ -113,7 +113,8 @@ defmodule Pleroma.Factory do | |||
end | |||
def note_activity_factory(attrs \\ %{}) do | |||
note = attrs[:note] || insert(:note) | |||
user = attrs[:user] || insert(:user) | |||
note = attrs[:note] || insert(:note, user: user) | |||
data = %{ | |||
"id" => Pleroma.Web.ActivityPub.Utils.generate_activity_id(), | |||
@@ -5,8 +5,10 @@ | |||
defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do | |||
use Pleroma.Web.ConnCase | |||
alias Pleroma.Activity | |||
alias Pleroma.User | |||
alias Pleroma.UserInviteToken | |||
alias Pleroma.Web.CommonAPI | |||
import Pleroma.Factory | |||
describe "/api/pleroma/admin/users" do | |||
@@ -949,4 +951,329 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do | |||
} | |||
end | |||
end | |||
describe "GET /api/pleroma/admin/reports/:id" do | |||
setup %{conn: conn} do | |||
admin = insert(:user, info: %{is_admin: true}) | |||
%{conn: assign(conn, :user, admin)} | |||
end | |||
test "returns report by its id", %{conn: conn} do | |||
[reporter, target_user] = insert_pair(:user) | |||
activity = insert(:note_activity, user: target_user) | |||
{:ok, %{id: report_id}} = | |||
CommonAPI.report(reporter, %{ | |||
"account_id" => target_user.id, | |||
"comment" => "I feel offended", | |||
"status_ids" => [activity.id] | |||
}) | |||
response = | |||
conn | |||
|> get("/api/pleroma/admin/reports/#{report_id}") | |||
|> json_response(:ok) | |||
assert response["id"] == report_id | |||
end | |||
test "returns 404 when report id is invalid", %{conn: conn} do | |||
conn = get(conn, "/api/pleroma/admin/reports/test") | |||
assert json_response(conn, :not_found) == "Not found" | |||
end | |||
end | |||
describe "PUT /api/pleroma/admin/reports/:id" do | |||
setup %{conn: conn} do | |||
admin = insert(:user, info: %{is_admin: true}) | |||
[reporter, target_user] = insert_pair(:user) | |||
activity = insert(:note_activity, user: target_user) | |||
{:ok, %{id: report_id}} = | |||
CommonAPI.report(reporter, %{ | |||
"account_id" => target_user.id, | |||
"comment" => "I feel offended", | |||
"status_ids" => [activity.id] | |||
}) | |||
%{conn: assign(conn, :user, admin), id: report_id} | |||
end | |||
test "mark report as resolved", %{conn: conn, id: id} do | |||
response = | |||
conn | |||
|> put("/api/pleroma/admin/reports/#{id}", %{"state" => "resolved"}) | |||
|> json_response(:ok) | |||
assert response["state"] == "resolved" | |||
end | |||
test "closes report", %{conn: conn, id: id} do | |||
response = | |||
conn | |||
|> put("/api/pleroma/admin/reports/#{id}", %{"state" => "closed"}) | |||
|> json_response(:ok) | |||
assert response["state"] == "closed" | |||
end | |||
test "returns 400 when state is unknown", %{conn: conn, id: id} do | |||
conn = | |||
conn | |||
|> put("/api/pleroma/admin/reports/#{id}", %{"state" => "test"}) | |||
assert json_response(conn, :bad_request) == "Unsupported state" | |||
end | |||
test "returns 404 when report is not exist", %{conn: conn} do | |||
conn = | |||
conn | |||
|> put("/api/pleroma/admin/reports/test", %{"state" => "closed"}) | |||
assert json_response(conn, :not_found) == "Not found" | |||
end | |||
end | |||
describe "GET /api/pleroma/admin/reports" do | |||
setup %{conn: conn} do | |||
admin = insert(:user, info: %{is_admin: true}) | |||
%{conn: assign(conn, :user, admin)} | |||
end | |||
test "returns empty response when no reports created", %{conn: conn} do | |||
response = | |||
conn | |||
|> get("/api/pleroma/admin/reports") | |||
|> json_response(:ok) | |||
assert Enum.empty?(response["reports"]) | |||
end | |||
test "returns reports", %{conn: conn} do | |||
[reporter, target_user] = insert_pair(:user) | |||
activity = insert(:note_activity, user: target_user) | |||
{:ok, %{id: report_id}} = | |||
CommonAPI.report(reporter, %{ | |||
"account_id" => target_user.id, | |||
"comment" => "I feel offended", | |||
"status_ids" => [activity.id] | |||
}) | |||
response = | |||
conn | |||
|> get("/api/pleroma/admin/reports") | |||
|> json_response(:ok) | |||
[report] = response["reports"] | |||
assert length(response["reports"]) == 1 | |||
assert report["id"] == report_id | |||
end | |||
test "returns reports with specified state", %{conn: conn} do | |||
[reporter, target_user] = insert_pair(:user) | |||
activity = insert(:note_activity, user: target_user) | |||
{:ok, %{id: first_report_id}} = | |||
CommonAPI.report(reporter, %{ | |||
"account_id" => target_user.id, | |||
"comment" => "I feel offended", | |||
"status_ids" => [activity.id] | |||
}) | |||
{:ok, %{id: second_report_id}} = | |||
CommonAPI.report(reporter, %{ | |||
"account_id" => target_user.id, | |||
"comment" => "I don't like this user" | |||
}) | |||
CommonAPI.update_report_state(second_report_id, "closed") | |||
response = | |||
conn | |||
|> get("/api/pleroma/admin/reports", %{ | |||
"state" => "open" | |||
}) | |||
|> json_response(:ok) | |||
[open_report] = response["reports"] | |||
assert length(response["reports"]) == 1 | |||
assert open_report["id"] == first_report_id | |||
response = | |||
conn | |||
|> get("/api/pleroma/admin/reports", %{ | |||
"state" => "closed" | |||
}) | |||
|> json_response(:ok) | |||
[closed_report] = response["reports"] | |||
assert length(response["reports"]) == 1 | |||
assert closed_report["id"] == second_report_id | |||
response = | |||
conn | |||
|> get("/api/pleroma/admin/reports", %{ | |||
"state" => "resolved" | |||
}) | |||
|> json_response(:ok) | |||
assert Enum.empty?(response["reports"]) | |||
end | |||
test "returns 403 when requested by a non-admin" do | |||
user = insert(:user) | |||
conn = | |||
build_conn() | |||
|> assign(:user, user) | |||
|> get("/api/pleroma/admin/reports") | |||
assert json_response(conn, :forbidden) == %{"error" => "User is not admin."} | |||
end | |||
test "returns 403 when requested by anonymous" do | |||
conn = | |||
build_conn() | |||
|> get("/api/pleroma/admin/reports") | |||
assert json_response(conn, :forbidden) == %{"error" => "Invalid credentials."} | |||
end | |||
end | |||
describe "POST /api/pleroma/admin/reports/:id/respond" do | |||
setup %{conn: conn} do | |||
admin = insert(:user, info: %{is_admin: true}) | |||
%{conn: assign(conn, :user, admin)} | |||
end | |||
test "returns created dm", %{conn: conn} do | |||
[reporter, target_user] = insert_pair(:user) | |||
activity = insert(:note_activity, user: target_user) | |||
{:ok, %{id: report_id}} = | |||
CommonAPI.report(reporter, %{ | |||
"account_id" => target_user.id, | |||
"comment" => "I feel offended", | |||
"status_ids" => [activity.id] | |||
}) | |||
response = | |||
conn | |||
|> post("/api/pleroma/admin/reports/#{report_id}/respond", %{ | |||
"status" => "I will check it out" | |||
}) | |||
|> json_response(:ok) | |||
recipients = Enum.map(response["mentions"], & &1["username"]) | |||
assert conn.assigns[:user].nickname in recipients | |||
assert reporter.nickname in recipients | |||
assert response["content"] == "I will check it out" | |||
assert response["visibility"] == "direct" | |||
end | |||
test "returns 400 when status is missing", %{conn: conn} do | |||
conn = post(conn, "/api/pleroma/admin/reports/test/respond") | |||
assert json_response(conn, :bad_request) == "Invalid parameters" | |||
end | |||
test "returns 404 when report id is invalid", %{conn: conn} do | |||
conn = | |||
post(conn, "/api/pleroma/admin/reports/test/respond", %{ | |||
"status" => "foo" | |||
}) | |||
assert json_response(conn, :not_found) == "Not found" | |||
end | |||
end | |||
describe "PUT /api/pleroma/admin/statuses/:id" do | |||
setup %{conn: conn} do | |||
admin = insert(:user, info: %{is_admin: true}) | |||
activity = insert(:note_activity) | |||
%{conn: assign(conn, :user, admin), id: activity.id} | |||
end | |||
test "toggle sensitive flag", %{conn: conn, id: id} do | |||
response = | |||
conn | |||
|> put("/api/pleroma/admin/statuses/#{id}", %{"sensitive" => "true"}) | |||
|> json_response(:ok) | |||
assert response["sensitive"] | |||
response = | |||
conn | |||
|> put("/api/pleroma/admin/statuses/#{id}", %{"sensitive" => "false"}) | |||
|> json_response(:ok) | |||
refute response["sensitive"] | |||
end | |||
test "change visibility flag", %{conn: conn, id: id} do | |||
response = | |||
conn | |||
|> put("/api/pleroma/admin/statuses/#{id}", %{"visibility" => "public"}) | |||
|> json_response(:ok) | |||
assert response["visibility"] == "public" | |||
response = | |||
conn | |||
|> put("/api/pleroma/admin/statuses/#{id}", %{"visibility" => "private"}) | |||
|> json_response(:ok) | |||
assert response["visibility"] == "private" | |||
response = | |||
conn | |||
|> put("/api/pleroma/admin/statuses/#{id}", %{"visibility" => "unlisted"}) | |||
|> json_response(:ok) | |||
assert response["visibility"] == "unlisted" | |||
end | |||
test "returns 400 when visibility is unknown", %{conn: conn, id: id} do | |||
conn = | |||
conn | |||
|> put("/api/pleroma/admin/statuses/#{id}", %{"visibility" => "test"}) | |||
assert json_response(conn, :bad_request) == "Unsupported visibility" | |||
end | |||
end | |||
describe "DELETE /api/pleroma/admin/statuses/:id" do | |||
setup %{conn: conn} do | |||
admin = insert(:user, info: %{is_admin: true}) | |||
activity = insert(:note_activity) | |||
%{conn: assign(conn, :user, admin), id: activity.id} | |||
end | |||
test "deletes status", %{conn: conn, id: id} do | |||
conn | |||
|> delete("/api/pleroma/admin/statuses/#{id}") | |||
|> json_response(:ok) | |||
refute Activity.get_by_id(id) | |||
end | |||
test "returns error when status is not exist", %{conn: conn} do | |||
conn = | |||
conn | |||
|> delete("/api/pleroma/admin/statuses/test") | |||
assert json_response(conn, :bad_request) == "Could not delete" | |||
end | |||
end | |||
end |
@@ -261,10 +261,41 @@ defmodule Pleroma.Web.CommonAPITest do | |||
data: %{ | |||
"type" => "Flag", | |||
"content" => ^comment, | |||
"object" => [^target_ap_id, ^activity_ap_id] | |||
"object" => [^target_ap_id, ^activity_ap_id], | |||
"state" => "open" | |||
} | |||
} = flag_activity | |||
end | |||
test "updates report state" do | |||
[reporter, target_user] = insert_pair(:user) | |||
activity = insert(:note_activity, user: target_user) | |||
{:ok, %Activity{id: report_id}} = | |||
CommonAPI.report(reporter, %{ | |||
"account_id" => target_user.id, | |||
"comment" => "I feel offended", | |||
"status_ids" => [activity.id] | |||
}) | |||
{:ok, report} = CommonAPI.update_report_state(report_id, "resolved") | |||
assert report.data["state"] == "resolved" | |||
end | |||
test "does not update report state when state is unsupported" do | |||
[reporter, target_user] = insert_pair(:user) | |||
activity = insert(:note_activity, user: target_user) | |||
{:ok, %Activity{id: report_id}} = | |||
CommonAPI.report(reporter, %{ | |||
"account_id" => target_user.id, | |||
"comment" => "I feel offended", | |||
"status_ids" => [activity.id] | |||
}) | |||
assert CommonAPI.update_report_state(report_id, "test") == {:error, "Unsupported state"} | |||
end | |||
end | |||
describe "reblog muting" do | |||
@@ -2129,7 +2129,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do | |||
|> get("/api/v1/pleroma/accounts/#{user.id}/favourites") | |||
|> json_response(:ok) | |||
assert length(anonymous_response) == 0 | |||
assert Enum.empty?(anonymous_response) | |||
end | |||
test "does not return others' favorited DM when user is not one of recipients", %{ | |||
@@ -2153,7 +2153,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do | |||
|> get("/api/v1/pleroma/accounts/#{user.id}/favourites") | |||
|> json_response(:ok) | |||
assert length(response) == 0 | |||
assert Enum.empty?(response) | |||
end | |||
test "paginates favorites using since_id and max_id", %{ | |||