logo

pleroma

My custom branche(s) on git.pleroma.social/pleroma/pleroma
commit: 2aa609db8a43e0eca8b998a4f42de6f0b19011ac
parent: fd697cf2090b61db60a02694c3227850df176e2d
Author: feld <feld@feld.me>
Date:   Wed, 11 Dec 2019 23:45:44 +0000

Merge branch 'feature/report-notes' into 'develop'

AdminAPI: Add report notes

See merge request pleroma/pleroma!2031

Diffstat:

MCHANGELOG.md1+
Mdocs/API/admin_api.md81++++++++++++++++---------------------------------------------------------------
Mlib/pleroma/activity.ex13+++++++++++++
Mlib/pleroma/moderation_log.ex42++++++++++++++++++++++++++++++++++++------
Mlib/pleroma/pagination.ex5++++-
Alib/pleroma/report_note.ex48++++++++++++++++++++++++++++++++++++++++++++++++
Mlib/pleroma/web/activity_pub/activity_pub.ex8++++++++
Mlib/pleroma/web/activity_pub/utils.ex1+
Mlib/pleroma/web/admin_api/admin_api_controller.ex50++++++++++++++++++++++++++++++--------------------
Mlib/pleroma/web/admin_api/views/report_view.ex25++++++++++++++++++++++++-
Mlib/pleroma/web/router.ex3++-
Apriv/repo/migrations/20191203043610_create_report_notes.exs13+++++++++++++
Mtest/moderation_log_test.exs4++--
Mtest/web/admin_api/admin_api_controller_test.exs127+++++++++++++++++++++++++++++++++++++++++++++----------------------------------
Mtest/web/admin_api/views/report_view_test.exs2++
15 files changed, 272 insertions(+), 151 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md @@ -28,6 +28,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 @@ -614,78 +614,29 @@ 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 report note - Params: - - `id` - - `status`: required, the message + - `id`: required, report id + - `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 + - On success: `204`, empty response -```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" -} -``` +## `POST /api/pleroma/admin/reports/:report_id/notes/:id` + +### Delete report note + +- Params: + - `report_id`: required, report id + - `id`: required, note id +- Response: + - On failure: + - 400 Bad Request `"Invalid parameters"` when `status` is missing + - On success: `204`, empty response ## `PUT /api/pleroma/admin/statuses/:id` diff --git 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 @@ -48,6 +49,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! @@ -114,6 +117,16 @@ defmodule Pleroma.Activity 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/moderation_log.ex b/lib/pleroma/moderation_log.ex @@ -128,17 +128,35 @@ defmodule Pleroma.ModerationLog do {:ok, ModerationLog} | {:error, any} def insert_log(%{ actor: %User{} = actor, - action: "report_response", + action: "report_note", subject: %Activity{} = subject, text: text }) do %ModerationLog{ data: %{ "actor" => user_to_map(actor), - "action" => "report_response", + "action" => "report_note", "subject" => report_to_map(subject), - "text" => text, - "message" => "" + "text" => text + } + } + |> insert_log_entry_with_message() + end + + @spec insert_log(%{actor: User, subject: Activity, action: String.t(), text: String.t()}) :: + {:ok, ModerationLog} | {:error, any} + def insert_log(%{ + actor: %User{} = actor, + action: "report_note_delete", + subject: %Activity{} = subject, + text: text + }) do + %ModerationLog{ + data: %{ + "actor" => user_to_map(actor), + "action" => "report_note_delete", + "subject" => report_to_map(subject), + "text" => text } } |> insert_log_entry_with_message() @@ -556,12 +574,24 @@ defmodule Pleroma.ModerationLog do def get_log_entry_message(%ModerationLog{ data: %{ "actor" => %{"nickname" => actor_nickname}, - "action" => "report_response", + "action" => "report_note", + "subject" => %{"id" => subject_id, "type" => "report"}, + "text" => text + } + }) do + "@#{actor_nickname} added note '#{text}' to report ##{subject_id}" + end + + @spec get_log_entry_message(ModerationLog) :: String.t() + def get_log_entry_message(%ModerationLog{ + data: %{ + "actor" => %{"nickname" => actor_nickname}, + "action" => "report_note_delete", "subject" => %{"id" => subject_id, "type" => "report"}, "text" => text } }) do - "@#{actor_nickname} responded with '#{text}' to report ##{subject_id}" + "@#{actor_nickname} deleted note '#{text}' from report ##{subject_id}" end @spec get_log_entry_message(ModerationLog) :: String.t() diff --git a/lib/pleroma/pagination.ex b/lib/pleroma/pagination.ex @@ -38,7 +38,10 @@ defmodule Pleroma.Pagination do end def fetch_paginated(query, %{"total" => true} = params, :offset, table_binding) do - total = Repo.aggregate(query, :count, :id) + total = + query + |> Ecto.Query.exclude(:left_join) + |> Repo.aggregate(:count, :id) %{ total: total, diff --git a/lib/pleroma/report_note.ex b/lib/pleroma/report_note.ex @@ -0,0 +1,48 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# 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 + + @spec destroy(FlakeId.Ecto.CompatType.t()) :: + {:ok, ReportNote.t()} | {:error, Changeset.t()} + def destroy(id) do + from(r in ReportNote, where: r.id == ^id) + |> Repo.one() + |> Repo.delete() + end +end diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -1068,6 +1068,13 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub 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 @@ -1121,6 +1128,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub 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 @@ -787,6 +787,7 @@ defmodule Pleroma.Web.ActivityPub.Utils 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 @@ -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 @@ -643,9 +644,11 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do 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 @@ -689,32 +692,39 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do 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") + def report_notes_create(%{assigns: %{user: user}} = conn, %{ + "id" => report_id, + "content" => content + }) do + with {:ok, _} <- ReportNote.create(user.id, report_id, content) do + ModerationLog.insert_log(%{ + action: "report_note", + actor: user, + subject: Activity.get_by_id(report_id), + text: content + }) - {:ok, activity} = CommonAPI.post(user, params) + json_response(conn, :no_content, "") + else + _ -> json_response(conn, :bad_request, "") + end + end + def report_notes_delete(%{assigns: %{user: user}} = conn, %{ + "id" => note_id, + "report_id" => report_id + }) do + with {:ok, note} <- ReportNote.destroy(note_id) do ModerationLog.insert_log(%{ - action: "report_response", + action: "report_note_delete", actor: user, - subject: activity, - text: params["status"] + subject: Activity.get_by_id(report_id), + text: note.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 @@ -39,7 +39,8 @@ defmodule Pleroma.Web.AdminAPI.ReportView do 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 @@ -69,6 +70,28 @@ defmodule Pleroma.Web.AdminAPI.ReportView 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", %{ + id: id, + content: content, + user_id: user_id, + inserted_at: inserted_at + }) do + user = User.get_by_id(user_id) + + %{ + id: id, + content: content, + user: merge_account_views(user), + created_at: Utils.to_masto_date(inserted_at) + } + 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 @@ -187,7 +187,8 @@ 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) + delete("/reports/:report_id/notes/:id", AdminAPIController, :report_notes_delete) 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 @@ -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/moderation_log_test.exs b/test/moderation_log_test.exs @@ -214,7 +214,7 @@ defmodule Pleroma.ModerationLogTest do {:ok, _} = ModerationLog.insert_log(%{ actor: moderator, - action: "report_response", + action: "report_note", subject: report, text: "look at this" }) @@ -222,7 +222,7 @@ defmodule Pleroma.ModerationLogTest do log = Repo.one(ModerationLog) assert log.data["message"] == - "@#{moderator.nickname} responded with 'look at this' to report ##{report.id}" + "@#{moderator.nickname} added note 'look at this' to report ##{report.id}" end test "logging status sensitivity update", %{moderator: moderator} do diff --git a/test/web/admin_api/admin_api_controller_test.exs b/test/web/admin_api/admin_api_controller_test.exs @@ -10,6 +10,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do alias Pleroma.HTML alias Pleroma.ModerationLog alias Pleroma.Repo + alias Pleroma.ReportNote alias Pleroma.Tests.ObanHelpers alias Pleroma.User alias Pleroma.UserInviteToken @@ -1831,61 +1832,6 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do 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) @@ -3082,6 +3028,77 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest 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!" + }) + + build_conn() + |> assign(:user, admin) + |> post("/api/pleroma/admin/reports/#{report_id}/notes", %{ + content: "this is disgusting2!" + }) + + %{ + 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 + [note, _] = Repo.all(ReportNote) + + assert %{ + activity_id: ^report_id, + content: "this is disgusting!", + user_id: ^admin_id + } = note + end + + test "it returns reports with notes", %{admin: admin} do + conn = + build_conn() + |> assign(:user, admin) + |> get("/api/pleroma/admin/reports") + + response = json_response(conn, 200) + notes = hd(response["reports"])["notes"] + [note, _] = notes + + assert note["user"]["nickname"] == admin.nickname + assert note["content"] == "this is disgusting!" + assert note["created_at"] + assert response["total"] == 1 + end + + test "it deletes the note", %{admin: admin, report_id: report_id} do + assert ReportNote |> Repo.all() |> length() == 2 + + [note, _] = Repo.all(ReportNote) + + build_conn() + |> assign(:user, admin) + |> delete("/api/pleroma/admin/reports/#{report_id}/notes/#{note.id}") + + assert ReportNote |> Repo.all() |> length() == 1 + 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 @@ -30,6 +30,7 @@ defmodule Pleroma.Web.AdminAPI.ReportViewTest do Pleroma.Web.AdminAPI.AccountView.render("show.json", %{user: other_user}) ), statuses: [], + notes: [], state: "open", id: activity.id } @@ -65,6 +66,7 @@ defmodule Pleroma.Web.AdminAPI.ReportViewTest do ), statuses: [StatusView.render("show.json", %{activity: activity})], state: "open", + notes: [], id: report_activity.id }