diff --git a/CHANGELOG.md b/CHANGELOG.md
index fe6ab002c..dc09d4f6b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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)
diff --git a/docs/api/admin_api.md b/docs/api/admin_api.md
index 59578f8d1..b45c5e285 100644
--- a/docs/api/admin_api.md
+++ b/docs/api/admin_api.md
@@ -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": "@lain click on my link https://www.google.com/",
+ "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 `{}`
diff --git a/lib/pleroma/activity.ex b/lib/pleroma/activity.ex
index 3fd4003f8..4e54b15ba 100644
--- a/lib/pleroma/activity.ex
+++ b/lib/pleroma/activity.ex
@@ -111,7 +111,7 @@ def get_bookmark(_, _), do: nil
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
diff --git a/lib/pleroma/emails/admin_email.ex b/lib/pleroma/emails/admin_email.ex
index df0f72f96..d0e254362 100644
--- a/lib/pleroma/emails/admin_email.ex
+++ b/lib/pleroma/emails/admin_email.ex
@@ -29,7 +29,7 @@ def report(to, reporter, account, statuses, comment) do
end
statuses_html =
- if length(statuses) > 0 do
+ if is_list(statuses) && length(statuses) > 0 do
statuses_list_html =
statuses
|> Enum.map(fn
diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex
index 6a186efbf..5c3156978 100644
--- a/lib/pleroma/web/activity_pub/activity_pub.ex
+++ b/lib/pleroma/web/activity_pub/activity_pub.ex
@@ -703,6 +703,12 @@ defp restrict_type(query, %{"type" => type}) 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 @@ def fetch_activities_query(recipients, opts \\ %{}) do
|> restrict_local(opts)
|> restrict_actor(opts)
|> restrict_type(opts)
+ |> restrict_state(opts)
|> restrict_favorited_by(opts)
|> restrict_blocked(opts)
|> restrict_muted(opts)
diff --git a/lib/pleroma/web/activity_pub/utils.ex b/lib/pleroma/web/activity_pub/utils.ex
index 236d1b4ac..ca8a0844b 100644
--- a/lib/pleroma/web/activity_pub/utils.ex
+++ b/lib/pleroma/web/activity_pub/utils.ex
@@ -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 @@ def make_flag_data(params, additional) 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 @@ def fetch_ordered_collection(from, pages_left, acc \\ []) 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
diff --git a/lib/pleroma/web/admin_api/admin_api_controller.ex b/lib/pleroma/web/admin_api/admin_api_controller.ex
index e00b33aba..de2a13c01 100644
--- a/lib/pleroma/web/admin_api/admin_api_controller.ex
+++ b/lib/pleroma/web/admin_api/admin_api_controller.ex
@@ -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 @@ def get_password_reset(conn, %{"nickname" => nickname}) 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)
diff --git a/lib/pleroma/web/admin_api/views/report_view.ex b/lib/pleroma/web/admin_api/views/report_view.ex
new file mode 100644
index 000000000..47a73dc7e
--- /dev/null
+++ b/lib/pleroma/web/admin_api/views/report_view.ex
@@ -0,0 +1,41 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors
+# 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
diff --git a/lib/pleroma/web/common_api/common_api.ex b/lib/pleroma/web/common_api/common_api.ex
index 29c4c1014..208c12c7b 100644
--- a/lib/pleroma/web/common_api/common_api.ex
+++ b/lib/pleroma/web/common_api/common_api.ex
@@ -71,6 +71,9 @@ def delete(activity_id, user) 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 @@ def report(user, data) 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
diff --git a/lib/pleroma/web/common_api/utils.ex b/lib/pleroma/web/common_api/utils.ex
index 1dfe50b40..bee2fd159 100644
--- a/lib/pleroma/web/common_api/utils.ex
+++ b/lib/pleroma/web/common_api/utils.ex
@@ -237,13 +237,11 @@ def make_note_data(
"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
diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex
index 7fef82f82..6a4e4a1d4 100644
--- a/lib/pleroma/web/router.ex
+++ b/lib/pleroma/web/router.ex
@@ -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
diff --git a/priv/repo/migrations/20190511191044_set_default_state_to_reports.exs b/priv/repo/migrations/20190511191044_set_default_state_to_reports.exs
new file mode 100644
index 000000000..0d3d253b6
--- /dev/null
+++ b/priv/repo/migrations/20190511191044_set_default_state_to_reports.exs
@@ -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
diff --git a/test/support/factory.ex b/test/support/factory.ex
index 2a2954ad6..90c7d80f2 100644
--- a/test/support/factory.ex
+++ b/test/support/factory.ex
@@ -43,7 +43,7 @@ def user_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 @@ def direct_note_activity_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(),
diff --git a/test/web/admin_api/admin_api_controller_test.exs b/test/web/admin_api/admin_api_controller_test.exs
index 6c1897b5a..ca12c7215 100644
--- a/test/web/admin_api/admin_api_controller_test.exs
+++ b/test/web/admin_api/admin_api_controller_test.exs
@@ -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 @@ test "with token" 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
diff --git a/test/web/common_api/common_api_test.exs b/test/web/common_api/common_api_test.exs
index 8d4f401ee..38b5aa65f 100644
--- a/test/web/common_api/common_api_test.exs
+++ b/test/web/common_api/common_api_test.exs
@@ -261,10 +261,41 @@ test "creates a report" 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
diff --git a/test/web/mastodon_api/mastodon_api_controller_test.exs b/test/web/mastodon_api/mastodon_api_controller_test.exs
index 40e7739e7..90d67a55f 100644
--- a/test/web/mastodon_api/mastodon_api_controller_test.exs
+++ b/test/web/mastodon_api/mastodon_api_controller_test.exs
@@ -2129,7 +2129,7 @@ test "returns favorited DM only when user is logged in and he is one of recipien
|> 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 @@ test "does not return others' favorited DM when user is not one of recipients",
|> 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", %{