From 4b60d41db9d10e971ee91202389991da294c72de Mon Sep 17 00:00:00 2001 From: Maxim Filippov Date: Tue, 3 Dec 2019 23:54:07 +0900 Subject: [PATCH] Add report notes --- CHANGELOG.md | 1 + docs/API/admin_api.md | 69 +----------- lib/pleroma/activity.ex | 13 +++ lib/pleroma/report_note.ex | 46 ++++++++ lib/pleroma/web/activity_pub/activity_pub.ex | 8 ++ lib/pleroma/web/activity_pub/utils.ex | 1 + .../web/admin_api/admin_api_controller.ex | 34 +++--- .../web/admin_api/views/report_view.ex | 18 ++- lib/pleroma/web/router.ex | 2 +- .../20191203043610_create_report_notes.exs | 13 +++ .../admin_api/admin_api_controller_test.exs | 104 +++++++++--------- test/web/admin_api/views/report_view_test.exs | 2 + 12 files changed, 168 insertions(+), 143 deletions(-) create mode 100644 lib/pleroma/report_note.ex create mode 100644 priv/repo/migrations/20191203043610_create_report_notes.exs diff --git a/CHANGELOG.md b/CHANGELOG.md index e44c892ab..a62222cda 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - **Breaking:** Admin API: Return link alongside with token on password reset - **Breaking:** Admin API: `PUT /api/pleroma/admin/reports/:id` is now `PATCH /api/pleroma/admin/reports`, see admin_api.md for details - **Breaking:** `/api/pleroma/admin/users/invite_token` now uses `POST`, changed accepted params and returns full invite in json instead of only token string. +- **Breaking** replying to reports is now "report notes", enpoint changed from `POST /api/pleroma/admin/reports/:id/respond` to `POST /api/pleroma/admin/reports/:id/notes` - Admin API: Return `total` when querying for reports - Mastodon API: Return `pleroma.direct_conversation_id` when creating a direct message (`POST /api/v1/statuses`) - Admin API: Return link alongside with token on password reset diff --git a/docs/API/admin_api.md b/docs/API/admin_api.md index 2cac317de..e51fad2cb 100644 --- a/docs/API/admin_api.md +++ b/docs/API/admin_api.md @@ -607,78 +607,17 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret - On success: `204`, empty response -## `POST /api/pleroma/admin/reports/:id/respond` +## `POST /api/pleroma/admin/reports/:id/notes` -### Respond to a report +### Create a report note - Params: - `id` - - `status`: required, the message + - `content`: 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" -} -``` + - On success: `204`, empty response ## `PUT /api/pleroma/admin/statuses/:id` diff --git a/lib/pleroma/activity.ex b/lib/pleroma/activity.ex index cd7a5aae9..37b2c041e 100644 --- a/lib/pleroma/activity.ex +++ b/lib/pleroma/activity.ex @@ -12,6 +12,7 @@ defmodule Pleroma.Activity do alias Pleroma.Notification alias Pleroma.Object alias Pleroma.Repo + alias Pleroma.ReportNote alias Pleroma.ThreadMute alias Pleroma.User @@ -47,6 +48,8 @@ defmodule Pleroma.Activity do has_one(:user_actor, User, on_delete: :nothing, foreign_key: :id) # This is a fake relation, do not use outside of with_preloaded_bookmark/get_bookmark has_one(:bookmark, Bookmark) + # This is a fake relation, do not use outside of with_preloaded_report_notes + has_many(:report_notes, ReportNote) has_many(:notifications, Notification, on_delete: :delete_all) # Attention: this is a fake relation, don't try to preload it blindly and expect it to work! @@ -113,6 +116,16 @@ def with_preloaded_bookmark(query, %User{} = user) do def with_preloaded_bookmark(query, _), do: query + def with_preloaded_report_notes(query) do + from([a] in query, + left_join: r in ReportNote, + on: a.id == r.activity_id, + preload: [report_notes: r] + ) + end + + def with_preloaded_report_notes(query, _), do: query + def with_set_thread_muted_field(query, %User{} = user) do from([a] in query, left_join: tm in ThreadMute, diff --git a/lib/pleroma/report_note.ex b/lib/pleroma/report_note.ex new file mode 100644 index 000000000..91102696b --- /dev/null +++ b/lib/pleroma/report_note.ex @@ -0,0 +1,46 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.ReportNote do + use Ecto.Schema + + import Ecto.Changeset + import Ecto.Query + + alias Pleroma.Activity + alias Pleroma.Repo + alias Pleroma.ReportNote + alias Pleroma.User + + @type t :: %__MODULE__{} + + schema "report_notes" do + field(:content, :string) + belongs_to(:user, User, type: FlakeId.Ecto.CompatType) + belongs_to(:activity, Activity, type: FlakeId.Ecto.CompatType) + + timestamps() + end + + @spec create(FlakeId.Ecto.CompatType.t(), FlakeId.Ecto.CompatType.t(), String.t()) :: + {:ok, ReportNote.t()} | {:error, Changeset.t()} + def create(user_id, activity_id, content) do + attrs = %{ + user_id: user_id, + activity_id: activity_id, + content: content + } + + %ReportNote{} + |> cast(attrs, [:user_id, :activity_id, :content]) + |> validate_required([:user_id, :activity_id, :content]) + |> Repo.insert() + end + + def get_all_for_status(status_id) do + ReportNote + |> where(activity_id: ^status_id) + |> Repo.all() + end +end diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index f32d04175..5c6cdfcf1 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -1018,6 +1018,13 @@ defp maybe_preload_bookmarks(query, opts) do |> Activity.with_preloaded_bookmark(opts["user"]) end + defp maybe_preload_report_notes(query, %{"preload_report_notes" => true}) do + query + |> Activity.with_preloaded_report_notes() + end + + defp maybe_preload_report_notes(query, _), do: query + defp maybe_set_thread_muted_field(query, %{"skip_preload" => true}), do: query defp maybe_set_thread_muted_field(query, opts) do @@ -1045,6 +1052,7 @@ def fetch_activities_query(recipients, opts \\ %{}) do Activity |> maybe_preload_objects(opts) |> maybe_preload_bookmarks(opts) + |> maybe_preload_report_notes(opts) |> maybe_set_thread_muted_field(opts) |> maybe_order(opts) |> restrict_recipients(recipients, opts["user"]) diff --git a/lib/pleroma/web/activity_pub/utils.ex b/lib/pleroma/web/activity_pub/utils.ex index 01aacbde3..079d458ba 100644 --- a/lib/pleroma/web/activity_pub/utils.ex +++ b/lib/pleroma/web/activity_pub/utils.ex @@ -781,6 +781,7 @@ def get_reports(params, page, page_size) do params |> Map.put("type", "Flag") |> Map.put("skip_preload", true) + |> Map.put("preload_report_notes", true) |> Map.put("total", true) |> Map.put("limit", page_size) |> Map.put("offset", (page - 1) * page_size) diff --git a/lib/pleroma/web/admin_api/admin_api_controller.ex b/lib/pleroma/web/admin_api/admin_api_controller.ex index 24fdc3c82..ee32bac45 100644 --- a/lib/pleroma/web/admin_api/admin_api_controller.ex +++ b/lib/pleroma/web/admin_api/admin_api_controller.ex @@ -7,6 +7,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do alias Pleroma.Activity alias Pleroma.ModerationLog alias Pleroma.Plugs.OAuthScopesPlug + alias Pleroma.ReportNote alias Pleroma.User alias Pleroma.UserInviteToken alias Pleroma.Web.ActivityPub.ActivityPub @@ -641,9 +642,11 @@ def force_password_reset(%{assigns: %{user: admin}} = conn, %{"nicknames" => nic def list_reports(conn, params) do {page, page_size} = page_params(params) + reports = Utils.get_reports(params, page, page_size) + conn |> put_view(ReportView) - |> render("index.json", %{reports: Utils.get_reports(params, page, page_size)}) + |> render("index.json", %{reports: reports}) end def list_grouped_reports(conn, _params) do @@ -687,32 +690,21 @@ def reports_update(%{assigns: %{user: admin}} = conn, %{"reports" => reports}) d 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) - + def report_notes_create(%{assigns: %{user: user}} = conn, %{ + "id" => status_id, + "content" => content + }) do + with {:ok, _} <- ReportNote.create(user.id, status_id, content) do ModerationLog.insert_log(%{ action: "report_response", actor: user, - subject: activity, - text: params["status"] + subject: Activity.get_by_id(status_id), + text: content }) - conn - |> put_view(StatusView) - |> render("show.json", %{activity: activity}) + json_response(conn, :no_content, "") else - true -> - {:param_cast, nil} - - nil -> - {:error, :not_found} + _ -> json_response(conn, :bad_request, "") end end diff --git a/lib/pleroma/web/admin_api/views/report_view.ex b/lib/pleroma/web/admin_api/views/report_view.ex index ca88595c7..80ca62691 100644 --- a/lib/pleroma/web/admin_api/views/report_view.ex +++ b/lib/pleroma/web/admin_api/views/report_view.ex @@ -38,7 +38,8 @@ def render("show.json", %{report: report, user: user, account: account, statuses content: content, created_at: created_at, statuses: StatusView.render("index.json", %{activities: statuses, as: :activity}), - state: report.data["state"] + state: report.data["state"], + notes: render(__MODULE__, "index_notes.json", %{notes: report.report_notes}) } end @@ -62,6 +63,21 @@ def render("index_grouped.json", %{groups: groups}) do } end + def render("index_notes.json", %{notes: notes}) when is_list(notes) do + Enum.map(notes, &render(__MODULE__, "show_note.json", &1)) + end + + def render("index_notes.json", _), do: [] + + def render("show_note.json", %{content: content, user_id: user_id}) do + user = User.get_by_id(user_id) + + %{ + content: content, + user: merge_account_views(user) + } + end + defp merge_account_views(%User{} = user) do Pleroma.Web.MastodonAPI.AccountView.render("show.json", %{user: user}) |> Map.merge(Pleroma.Web.AdminAPI.AccountView.render("show.json", %{user: user})) diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index e6c4f6f14..af220a98b 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -187,7 +187,7 @@ defmodule Pleroma.Web.Router do get("/grouped_reports", AdminAPIController, :list_grouped_reports) get("/reports/:id", AdminAPIController, :report_show) patch("/reports", AdminAPIController, :reports_update) - post("/reports/:id/respond", AdminAPIController, :report_respond) + post("/reports/:id/notes", AdminAPIController, :report_notes_create) put("/statuses/:id", AdminAPIController, :status_update) delete("/statuses/:id", AdminAPIController, :status_delete) diff --git a/priv/repo/migrations/20191203043610_create_report_notes.exs b/priv/repo/migrations/20191203043610_create_report_notes.exs new file mode 100644 index 000000000..a4f8c096d --- /dev/null +++ b/priv/repo/migrations/20191203043610_create_report_notes.exs @@ -0,0 +1,13 @@ +defmodule Pleroma.Repo.Migrations.CreateReportNotes do + use Ecto.Migration + + def change do + create_if_not_exists table(:report_notes) do + add(:user_id, references(:users, type: :uuid)) + add(:activity_id, references(:activities, type: :uuid)) + add(:content, :string) + + timestamps() + end + end +end diff --git a/test/web/admin_api/admin_api_controller_test.exs b/test/web/admin_api/admin_api_controller_test.exs index 32577afee..44557ea45 100644 --- a/test/web/admin_api/admin_api_controller_test.exs +++ b/test/web/admin_api/admin_api_controller_test.exs @@ -1710,61 +1710,6 @@ test "returns reports grouped by status", %{ end end - describe "POST /api/pleroma/admin/reports/:id/respond" do - setup %{conn: conn} do - admin = insert(:user, is_admin: true) - - %{conn: assign(conn, :user, admin), admin: admin} - end - - test "returns created dm", %{conn: conn, admin: admin} 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 reporter.nickname in recipients - assert response["content"] == "I will check it out" - assert response["visibility"] == "direct" - - log_entry = Repo.one(ModerationLog) - - assert ModerationLog.get_log_entry_message(log_entry) == - "@#{admin.nickname} responded with 'I will check it out' to report ##{ - response["id"] - }" - 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, is_admin: true) @@ -2961,6 +2906,55 @@ test "it resend emails for two users", %{admin: admin} do }" end end + + describe "POST /reports/:id/notes" do + setup do + admin = insert(:user, 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] + }) + + build_conn() + |> assign(:user, admin) + |> post("/api/pleroma/admin/reports/#{report_id}/notes", %{ + content: "this is disgusting!" + }) + + %{ + admin_id: admin.id, + report_id: report_id, + admin: admin + } + end + + test "it creates report note", %{admin_id: admin_id, report_id: report_id} do + assert %{ + activity_id: ^report_id, + content: "this is disgusting!", + user_id: ^admin_id + } = Repo.one(Pleroma.ReportNote) + end + + test "it returns reports with notes", %{admin: admin} do + conn = + build_conn() + |> assign(:user, admin) + |> get("/api/pleroma/admin/reports") + + reponse = json_response(conn, 200) + notes = hd(reponse["reports"])["notes"] + [note] = notes + + assert note["user"]["nickname"] == admin.nickname + assert note["content"] == "this is disgusting!" + end + end end # Needed for testing diff --git a/test/web/admin_api/views/report_view_test.exs b/test/web/admin_api/views/report_view_test.exs index ef4a806e4..a0c6eab3c 100644 --- a/test/web/admin_api/views/report_view_test.exs +++ b/test/web/admin_api/views/report_view_test.exs @@ -30,6 +30,7 @@ test "renders a report" do Pleroma.Web.AdminAPI.AccountView.render("show.json", %{user: other_user}) ), statuses: [], + notes: [], state: "open", id: activity.id } @@ -65,6 +66,7 @@ test "includes reported statuses" do ), statuses: [StatusView.render("show.json", %{activity: activity})], state: "open", + notes: [], id: report_activity.id }