Add Reports to Admin API
authorSergey Suprunenko <suprunenko.s@gmail.com>
Thu, 16 May 2019 19:09:18 +0000 (19:09 +0000)
committerfeld <feld@feld.me>
Thu, 16 May 2019 19:09:18 +0000 (19:09 +0000)
16 files changed:
CHANGELOG.md
docs/api/admin_api.md
lib/pleroma/activity.ex
lib/pleroma/emails/admin_email.ex
lib/pleroma/web/activity_pub/activity_pub.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 [new file with mode: 0644]
lib/pleroma/web/common_api/common_api.ex
lib/pleroma/web/common_api/utils.ex
lib/pleroma/web/router.ex
priv/repo/migrations/20190511191044_set_default_state_to_reports.exs [new file with mode: 0644]
test/support/factory.ex
test/web/admin_api/admin_api_controller_test.exs
test/web/common_api/common_api_test.exs
test/web/mastodon_api/mastodon_api_controller_test.exs

index fe6ab002ced820c9718b4fa606c69fdb5ae519f2..dc09d4f6b8e32707fe8ce7fcf5bb245834932ae9 100644 (file)
@@ -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)
index 59578f8d1fb4d65181277b2154dc01a0fbe47b9f..b45c5e2856778c1fef1f3ec3e84162eba51761fa 100644 (file)
@@ -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 `{}`
index 3fd4003f88f4774a6870ce237bd319d190adb4de..4e54b15baf072d35a233fb069014c0abe65391af 100644 (file)
@@ -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
index df0f72f964d597d22915d1b2033352b0a19d898a..d0e254362ba60c386582fc3c4f5835da3b36712a 100644 (file)
@@ -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
index 6a186efbfddc5b9ab546e543e61d532cb5ff0b26..5c315697858537ea505f8d4611fc139657f848bb 100644 (file)
@@ -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)
index 236d1b4aca4f9c2b9447f68fe2af03867b1c96c1..ca8a0844be1e4840b7d384d64a4a815e2adaaba6 100644 (file)
@@ -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
index e00b33aba698585f43adc1254096231ac6180afe..de2a13c015c80d172ca224583d485f47a0819da5 100644 (file)
@@ -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)
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 (file)
index 0000000..47a73dc
--- /dev/null
@@ -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
index 29c4c101425b6f4190d3b2188dc28ef0b29dc194..208c12c7b62fb847b10c9efffa80f70289ec1154 100644 (file)
@@ -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
 
index 1dfe50b408266efd29f341ac37f5dc3b7b652d53..bee2fd159a2beec8bdebea14dec19562f962ab1d 100644 (file)
@@ -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
 
index 7fef82f82a3eb613d37e6b63d321155b91fe5079..6a4e4a1d4e77d098d701352833ccdfc0686ba42d 100644 (file)
@@ -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 (file)
index 0000000..0d3d253
--- /dev/null
@@ -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
index 2a2954ad615ea0993b1479f3f81b6c697bfdc267..90c7d80f2d607111a50fe441bfcbc31f40ed744f 100644 (file)
@@ -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(),
index 6c1897b5a9354336d00fe2df857abf0a68ff0331..ca12c72153931131f93c853128d95208a596a873 100644 (file)
@@ -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
index 8d4f401eec8a0303cac3c8d17243af9590589ade..38b5aa65f31d4998cec9eb87b00b538c93eabfa4 100644 (file)
@@ -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
index 40e7739e71a03bd03114473bb7d8150fe4e529f2..90d67a55f868eabb314cfd4b5d7f95cc1849c953 100644 (file)
@@ -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", %{