diff --git a/js/src/components/Comment/EventComment.vue b/js/src/components/Comment/EventComment.vue index d0069c250..7219fb2f1 100644 --- a/js/src/components/Comment/EventComment.vue +++ b/js/src/components/Comment/EventComment.vue @@ -352,7 +352,7 @@ const reportComment = async ( ): Promise => { if (!props.comment.actor) return; createReportMutation({ - eventId: props.event.id, + eventsIds: [props.event.id ?? ""], reportedId: props.comment.actor?.id ?? "", commentsIds: [props.comment.id ?? ""], content, diff --git a/js/src/components/Event/EventActionSection.vue b/js/src/components/Event/EventActionSection.vue index 4330ec750..7a5dff456 100644 --- a/js/src/components/Event/EventActionSection.vue +++ b/js/src/components/Event/EventActionSection.vue @@ -648,7 +648,7 @@ const reportEvent = async ( if (!organizer.value) return; createReportMutation({ - eventId: event.value?.id ?? "", + eventsIds: [event.value?.id ?? ""], reportedId: organizer.value?.id ?? "", content, forward, diff --git a/js/src/composition/apollo/report.ts b/js/src/composition/apollo/report.ts index 37ecaece6..adfdf8a48 100644 --- a/js/src/composition/apollo/report.ts +++ b/js/src/composition/apollo/report.ts @@ -5,7 +5,7 @@ export function useCreateReport() { return useMutation< { createReport: { id: string } }, { - eventId?: string; + eventsIds?: string[]; reportedId: string; content?: string; commentsIds?: string[]; diff --git a/js/src/graphql/report.ts b/js/src/graphql/report.ts index 0c4c0c3b6..d623b6393 100644 --- a/js/src/graphql/report.ts +++ b/js/src/graphql/report.ts @@ -20,7 +20,7 @@ export const REPORTS = gql` ...ActorFragment suspended } - event { + events { id uuid title @@ -46,7 +46,7 @@ const REPORT_FRAGMENT = gql` reporter { ...ActorFragment } - event { + events { id uuid title @@ -97,14 +97,14 @@ export const REPORT = gql` export const CREATE_REPORT = gql` mutation CreateReport( - $eventId: ID + $eventsIds: [ID] $reportedId: ID! $content: String $commentsIds: [ID] $forward: Boolean ) { createReport( - eventId: $eventId + eventsIds: $eventsIds reportedId: $reportedId content: $content commentsIds: $commentsIds diff --git a/js/src/types/report.model.ts b/js/src/types/report.model.ts index dd54158a3..ff79b36af 100644 --- a/js/src/types/report.model.ts +++ b/js/src/types/report.model.ts @@ -16,7 +16,7 @@ export interface IReport extends IActionLogObject { id: string; reported: IActor; reporter: IPerson; - event?: IEvent; + events?: IEvent[]; comments: IComment[]; content: string; notes: IReportNote[]; diff --git a/js/src/views/Moderation/ReportView.vue b/js/src/views/Moderation/ReportView.vue index 221d88824..efcd8bed0 100644 --- a/js/src/views/Moderation/ReportView.vue +++ b/js/src/views/Moderation/ReportView.vue @@ -150,14 +150,14 @@ {{ t("Unknown") }} - - {{ t("Event") }} + @@ -206,17 +206,23 @@

{{ t("Reported content") }}

- - {{ t("Delete") }} +
+ + {{ t("Delete") }} +
{ const dialog = inject("dialog"); -const confirmEventDelete = (): void => { +const confirmEventDelete = (event: IEvent): void => { dialog?.confirm({ title: t("Deleting event"), message: t( @@ -428,7 +435,7 @@ const confirmEventDelete = (): void => { confirmText: t("Delete Event"), variant: "danger", hasIcon: true, - onConfirm: () => deleteEvent(), + onConfirm: () => deleteEvent(event), }); }; @@ -451,8 +458,8 @@ const { onError: deleteEventMutationError, } = useMutation<{ deleteEvent: { id: string } }>(DELETE_EVENT); -deleteEventMutationDone(() => { - const eventTitle = report.value?.event?.title; +deleteEventMutationDone((result) => { + const eventTitle = result?.context?.eventTitle; notifier?.success( t("Event {eventTitle} deleted", { eventTitle, @@ -464,10 +471,13 @@ deleteEventMutationError((error) => { console.error(error); }); -const deleteEvent = async (): Promise => { - if (!report.value?.event?.id) return; +const deleteEvent = async (event: IEvent): Promise => { + if (!event?.id) return; - deleteEventMutation({ eventId: report.value.event.id }); + deleteEventMutation( + { eventId: event.id }, + { context: { eventTitle: event.title } } + ); }; const { diff --git a/lib/federation/activity_pub/types/reports.ex b/lib/federation/activity_pub/types/reports.ex index ddcfd34ac..9f6c94ac9 100644 --- a/lib/federation/activity_pub/types/reports.ex +++ b/lib/federation/activity_pub/types/reports.ex @@ -2,7 +2,6 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Reports do @moduledoc false alias Mobilizon.{Actors, Discussions, Events, Reports} alias Mobilizon.Actors.Actor - alias Mobilizon.Events.Event alias Mobilizon.Federation.ActivityStream alias Mobilizon.Federation.ActivityStream.Convertible alias Mobilizon.Reports.Report @@ -26,15 +25,16 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Reports do %Actor{} = reported_actor = Actors.get_actor!(args.reported_id) content = HTML.strip_tags(args.content) - event_id = Map.get(args, :event_id) - - event = - if is_nil(event_id) do - nil - else - {:ok, %Event{} = event} = Events.get_event(event_id) - event - end + events = + args + |> Map.get(:events_ids, []) + |> Enum.map(fn event_id -> + case Events.get_event(event_id) do + {:ok, event} -> event + {:error, :event_not_found} -> nil + end + end) + |> Enum.filter(& &1) comments = Discussions.list_comments_by_actor_and_ids( @@ -46,7 +46,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Reports do reporter: reporter_actor, reported: reported_actor, content: content, - event: event, + events: events, comments: comments }) end diff --git a/lib/federation/activity_stream/converter/flag.ex b/lib/federation/activity_stream/converter/flag.ex index 4ff34f678..347ee8d00 100644 --- a/lib/federation/activity_stream/converter/flag.ex +++ b/lib/federation/activity_stream/converter/flag.ex @@ -9,11 +9,11 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Flag do """ alias Mobilizon.Actors.Actor - alias Mobilizon.Discussions - alias Mobilizon.Events + alias Mobilizon.Discussions.Comment alias Mobilizon.Events.Event alias Mobilizon.Reports.Report + alias Mobilizon.Federation.ActivityPub alias Mobilizon.Federation.ActivityPub.Actor, as: ActivityPubActor alias Mobilizon.Federation.ActivityPub.Relay alias Mobilizon.Federation.ActivityStream.{Converter, Convertible} @@ -38,7 +38,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Flag do "uri" => params["uri"], "content" => params["content"], "reported_id" => params["reported"].id, - "event_id" => (!is_nil(params["event"]) && params["event"].id) || nil, + "events" => params["events"], "comments" => params["comments"] } end @@ -50,9 +50,10 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Flag do @impl Converter @spec model_to_as(Report.t()) :: map def model_to_as(%Report{} = report) do - object = [report.reported.url] ++ Enum.map(report.comments, fn comment -> comment.url end) - - object = if report.event, do: object ++ [report.event.url], else: object + object = + [report.reported.url] ++ + Enum.map(report.comments, fn comment -> comment.url end) ++ + Enum.map(report.events, & &1.url) %{ "type" => "Flag", @@ -68,14 +69,13 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Flag do with {:ok, %Actor{} = reporter} <- ActivityPubActor.get_or_fetch_actor_by_url(object["actor"]), %Actor{} = reported <- find_reported(objects), - event <- find_event(objects), - comments <- find_comments(objects, reported, event) do + %{events: events, comments: comments} <- find_events_and_comments(objects) do %{ "reporter" => reporter, "uri" => object["id"], "content" => object["content"], "reported" => reported, - "event" => event, + "events" => events, "comments" => comments } end @@ -94,26 +94,19 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Flag do end) end - # Remove the reported actor and the event from the object list. - @spec find_comments(list(String.t()), Actor.t() | nil, Event.t() | nil) :: list(Comment.t()) - defp find_comments(objects, reported, event) do + defp find_events_and_comments(objects) do objects - |> Enum.filter(fn url -> - !((!is_nil(reported) && url == reported.url) || (!is_nil(event) && event.url == url)) - end) - |> Enum.map(&Discussions.get_comment_from_url/1) - |> Enum.filter(& &1) - end + |> Enum.map(&ActivityPub.fetch_object_from_url/1) + |> Enum.reduce(%{comments: [], events: []}, fn res, acc -> + case res do + {:ok, %Event{} = event} -> + Map.put(acc, :events, [event | acc.events]) - @spec find_event(list(String.t())) :: Event.t() | nil - defp find_event(objects) do - Enum.reduce_while(objects, nil, fn url, _ -> - case Events.get_event_by_url(url) do - %Event{} = event -> - {:halt, event} + {:ok, %Comment{} = comment} -> + Map.put(acc, :comments, [comment | acc.comments]) _ -> - {:cont, nil} + acc end end) end diff --git a/lib/graphql/resolvers/report.ex b/lib/graphql/resolvers/report.ex index e8c99453b..fc7112dd9 100644 --- a/lib/graphql/resolvers/report.ex +++ b/lib/graphql/resolvers/report.ex @@ -67,7 +67,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Report do {:ok, _, %Report{} = report} -> {:ok, report} - _error -> + error -> {:error, dgettext("errors", "Error while saving report")} end end diff --git a/lib/graphql/schema/report.ex b/lib/graphql/schema/report.ex index 43451776e..8d57c67c8 100644 --- a/lib/graphql/schema/report.ex +++ b/lib/graphql/schema/report.ex @@ -19,7 +19,7 @@ defmodule Mobilizon.GraphQL.Schema.ReportType do field(:uri, :string, description: "The URI of the report", meta: [private: true]) field(:reported, :actor, description: "The actor that is being reported") field(:reporter, :actor, description: "The actor that created the report") - field(:event, :event, description: "The event that is being reported") + field(:events, list_of(:event), description: "The event that is being reported") field(:comments, list_of(:comment), description: "The comments that are reported") field(:notes, list_of(:report_note), @@ -100,11 +100,15 @@ defmodule Mobilizon.GraphQL.Schema.ReportType do field :create_report, type: :report do arg(:content, :string, description: "The message sent with the report") arg(:reported_id, non_null(:id), description: "The actor's ID that is being reported") - arg(:event_id, :id, default_value: nil, description: "The event ID that is being reported") + + arg(:events_ids, list_of(:id), + default_value: [], + description: "The list of event IDs that are being reported" + ) arg(:comments_ids, list_of(:id), default_value: [], - description: "The comment ID that is being reported" + description: "The comment IDs that are being reported" ) arg(:forward, :boolean, diff --git a/lib/mobilizon/reports/report.ex b/lib/mobilizon/reports/report.ex index 11a9a3701..931898695 100644 --- a/lib/mobilizon/reports/report.ex +++ b/lib/mobilizon/reports/report.ex @@ -22,13 +22,13 @@ defmodule Mobilizon.Reports.Report do reported: Actor.t(), reporter: Actor.t(), manager: Actor.t(), - event: Event.t(), + events: [Event.t()], comments: [Comment.t()], notes: [Note.t()] } @required_attrs [:url, :reported_id, :reporter_id] - @optional_attrs [:content, :status, :manager_id, :event_id, :local] + @optional_attrs [:content, :status, :manager_id, :local] @attrs @required_attrs ++ @optional_attrs @timestamps_opts [type: :utc_datetime] @@ -46,8 +46,8 @@ defmodule Mobilizon.Reports.Report do belongs_to(:reporter, Actor) # The actor who last acted on this report belongs_to(:manager, Actor) - # The eventual Event inside the report - belongs_to(:event, Event) + # The eventual Events inside the report + many_to_many(:events, Event, join_through: "reports_events", on_replace: :delete) # The eventual Comments inside the report many_to_many(:comments, Comment, join_through: "reports_comments", on_replace: :delete) # The notes associated to the report @@ -62,6 +62,7 @@ defmodule Mobilizon.Reports.Report do report |> cast(attrs, @attrs) |> maybe_generate_url() + |> maybe_put_events(attrs) |> maybe_put_comments(attrs) |> validate_required(@required_attrs) end @@ -72,6 +73,12 @@ defmodule Mobilizon.Reports.Report do defp maybe_put_comments(%Ecto.Changeset{} = changeset, _), do: changeset + defp maybe_put_events(%Ecto.Changeset{} = changeset, %{events: events}) do + put_assoc(changeset, :events, events) + end + + defp maybe_put_events(%Ecto.Changeset{} = changeset, _), do: changeset + @spec maybe_generate_url(Ecto.Changeset.t()) :: Ecto.Changeset.t() defp maybe_generate_url(%Ecto.Changeset{} = changeset) do with res when res in [:error, {:data, nil}] <- fetch_field(changeset, :url), diff --git a/lib/mobilizon/reports/reports.ex b/lib/mobilizon/reports/reports.ex index 36da1cb67..5e407b10c 100644 --- a/lib/mobilizon/reports/reports.ex +++ b/lib/mobilizon/reports/reports.ex @@ -21,7 +21,7 @@ defmodule Mobilizon.Reports do def get_report(id) do Report |> Repo.get(id) - |> Repo.preload([:reported, :reporter, :manager, :event, :comments, :notes]) + |> Repo.preload([:reported, :reporter, :manager, :events, :comments, :notes]) end @doc """ @@ -33,7 +33,7 @@ defmodule Mobilizon.Reports do %Report{} |> Report.changeset(attrs) |> Repo.insert() do - {:ok, Repo.preload(report, [:event, :reported, :reporter, :comments])} + {:ok, Repo.preload(report, [:events, :reported, :reporter, :comments])} end end @@ -102,7 +102,7 @@ defmodule Mobilizon.Reports do @spec list_reports_query(atom()) :: Ecto.Query.t() defp list_reports_query(status) do Report - |> preload([:reported, :reporter, :manager, :event, :comments, :notes]) + |> preload([:reported, :reporter, :manager, :events, :comments, :notes]) |> where([r], r.status == ^status) end diff --git a/lib/service/anti_spam/akismet.ex b/lib/service/anti_spam/akismet.ex index f17946e6c..7c1bf98e0 100644 --- a/lib/service/anti_spam/akismet.ex +++ b/lib/service/anti_spam/akismet.ex @@ -139,7 +139,7 @@ defmodule Mobilizon.Service.AntiSpam.Akismet do end end - defp report_to_akismet_comment(%Report{event: %Event{id: event_id}}) do + defp report_to_akismet_comment(%Report{events: [%Event{id: event_id} | _]}) do with %Event{description: body, organizer_actor: %Actor{} = actor} <- Events.get_event_with_preload!(event_id), {email, preferred_username, ip} <- actor_details(actor) do diff --git a/lib/web/templates/email/report.html.heex b/lib/web/templates/email/report.html.heex index 48c5a62e6..9dedeeb8c 100644 --- a/lib/web/templates/email/report.html.heex +++ b/lib/web/templates/email/report.html.heex @@ -104,7 +104,7 @@ <% end %> - <%= if Map.has_key?(@report, :event) and @report.event do %> + <%= if Map.has_key?(@report, :events) and length(@report.events) > 0 do %>

-

<%= gettext("Event") %>

- - <%= gettext("%{title} by %{creator}", - title: @report.event.title, - creator: Mobilizon.Actors.Actor.preferred_username_and_domain(@report.reported) - ) %> - +

<%= gettext("Flagged events") %>

+ <%= for event <- @report.events do %> + + <%= gettext("%{title} by %{creator}", + title: event.title, + creator: + Mobilizon.Actors.Actor.preferred_username_and_domain(@report.reported) + ) %> + + <% end %>

<% end %> <% end %> -<%= if Map.has_key?(@report, :event) and @report.event do %> - <%= gettext "Event" %> - <%= @report.event.title %> +<%= if Map.has_key?(@report, :event) && length(@report.events) > 0 do %> + <%= gettext "Events" %> + <%= for event <- @report.events do %> + <%= event.title %> + <% end %> <% end %> <%= if Map.has_key?(@report, :comments) && length(@report.comments) > 0 do %> <%= gettext "Comments" %> diff --git a/priv/repo/migrations/20230605143534_allow_multiple_events_to_be_reported.exs b/priv/repo/migrations/20230605143534_allow_multiple_events_to_be_reported.exs new file mode 100644 index 000000000..dc1c9ba08 --- /dev/null +++ b/priv/repo/migrations/20230605143534_allow_multiple_events_to_be_reported.exs @@ -0,0 +1,14 @@ +defmodule Mobilizon.Storage.Repo.Migrations.AllowMultipleEventsToBeReported do + use Ecto.Migration + + def up do + create table(:reports_events, primary_key: false) do + add(:report_id, references(:reports, on_delete: :delete_all), null: false) + add(:event_id, references(:events, on_delete: :delete_all), null: false) + end + end + + def down do + drop table(:reports_events) + end +end diff --git a/priv/repo/migrations/20230605153836_backfill_report_events_with_old_events.exs b/priv/repo/migrations/20230605153836_backfill_report_events_with_old_events.exs new file mode 100644 index 000000000..a9bdfc995 --- /dev/null +++ b/priv/repo/migrations/20230605153836_backfill_report_events_with_old_events.exs @@ -0,0 +1,31 @@ +defmodule Mobilizon.Storage.Repo.Migrations.BackfillReportEventsWithOldEvents do + use Ecto.Migration + + def up do + process_reports_with_events() + end + + def down do + IO.puts("Doing nothing, migration can't be reverted") + end + + defp process_reports_with_events do + %Postgrex.Result{rows: rows} = + Ecto.Adapters.SQL.query!( + Mobilizon.Storage.Repo, + "SELECT id, event_id FROM reports WHERE event_id IS NOT NULL" + ) + + Enum.map(rows, &migrate_event_row/1) + end + + defp migrate_event_row([report_id, event_id]) when not is_nil(event_id) do + Ecto.Adapters.SQL.query!( + Mobilizon.Storage.Repo, + "INSERT INTO reports_events VALUES ($1, $2)", + [report_id, event_id] + ) + end + + defp migrate_event_row(_), do: :ok +end diff --git a/priv/repo/migrations/20230605154106_remove_obsolete_event_id_on_reports.exs b/priv/repo/migrations/20230605154106_remove_obsolete_event_id_on_reports.exs new file mode 100644 index 000000000..1f3294a4d --- /dev/null +++ b/priv/repo/migrations/20230605154106_remove_obsolete_event_id_on_reports.exs @@ -0,0 +1,15 @@ +defmodule Mobilizon.Storage.Repo.Migrations.RemoveObsoleteEventIdOnReports do + use Ecto.Migration + + def up do + alter table(:reports) do + remove_if_exists :event_id, :integer + end + end + + def down do + alter table(:reports) do + add(:event_id, references(:events, on_delete: :delete_all), null: true) + end + end +end diff --git a/test/graphql/api/report_test.exs b/test/graphql/api/report_test.exs index 1960f09bf..85ddec7cd 100644 --- a/test/graphql/api/report_test.exs +++ b/test/graphql/api/report_test.exs @@ -30,7 +30,7 @@ defmodule Mobilizon.GraphQL.API.ReportTest do reporter_id: reporter_id, reported_id: reported_id, content: comment, - event_id: event_id, + events_ids: [event_id], comments_ids: [], forward: false }) @@ -64,7 +64,7 @@ defmodule Mobilizon.GraphQL.API.ReportTest do reporter_id: reporter_id, reported_id: reported_id, content: comment, - event_id: nil, + events_ids: [], comments_ids: [comment_1_id, comment_2_id] }) @@ -100,7 +100,7 @@ defmodule Mobilizon.GraphQL.API.ReportTest do reporter_id: reporter_id, reported_id: reported_id, content: comment, - event_id: nil, + events_ids: [], comments_ids: [comment_1_id, comment_2_id], forward: true }) @@ -131,7 +131,7 @@ defmodule Mobilizon.GraphQL.API.ReportTest do reporter_id: reporter_id, reported_id: reported_id, content: "This is not a nice thing", - event_id: nil, + events_ids: [], comments_ids: [comment_1_id], forward: true }) @@ -157,7 +157,7 @@ defmodule Mobilizon.GraphQL.API.ReportTest do reporter_id: reporter_id, reported_id: reported_id, content: "This is not a nice thing", - event_id: nil, + events_ids: [], comments_ids: [comment_1_id], forward: true }) diff --git a/test/graphql/resolvers/report_test.exs b/test/graphql/resolvers/report_test.exs index 30ab06996..5773bbcb0 100644 --- a/test/graphql/resolvers/report_test.exs +++ b/test/graphql/resolvers/report_test.exs @@ -15,17 +15,17 @@ defmodule Mobilizon.GraphQL.Resolvers.ReportTest do describe "Resolver: Report a content" do @create_report_mutation """ - mutation CreateReport($reportedId: ID!, $eventId: ID, $content: String) { + mutation CreateReport($reportedId: ID!, $eventsIds: [ID], $content: String) { createReport( reportedId: $reportedId, - eventId: $eventId, + eventsIds: $eventsIds, content: $content ) { content, reporter { id }, - event { + events { id }, status @@ -55,7 +55,7 @@ defmodule Mobilizon.GraphQL.Resolvers.ReportTest do query: @create_report_mutation, variables: %{ reportedId: reported.id, - eventId: event.id, + eventsIds: [event.id], content: "This is an issue" } ) @@ -63,7 +63,7 @@ defmodule Mobilizon.GraphQL.Resolvers.ReportTest do assert res["errors"] == nil assert res["data"]["createReport"]["content"] == "This is an issue" assert res["data"]["createReport"]["status"] == "OPEN" - assert res["data"]["createReport"]["event"]["id"] == to_string(event.id) + assert res["data"]["createReport"]["events"] |> hd |> Map.get("id") == to_string(event.id) assert res["data"]["createReport"]["reporter"]["id"] == to_string(reporter.id) @@ -122,7 +122,7 @@ defmodule Mobilizon.GraphQL.Resolvers.ReportTest do reporter { id }, - event { + events { id }, status @@ -280,7 +280,7 @@ defmodule Mobilizon.GraphQL.Resolvers.ReportTest do reporter { preferredUsername }, - event { + events { title }, comments { @@ -312,7 +312,9 @@ defmodule Mobilizon.GraphQL.Resolvers.ReportTest do reporter.preferred_username assert json_response(res, 200)["data"]["report"]["content"] == report.content - assert json_response(res, 200)["data"]["report"]["event"]["title"] == report.event.title + + assert json_response(res, 200)["data"]["report"]["events"] |> hd |> Map.get("title") == + report.events |> hd |> Map.get(:title) assert json_response(res, 200)["data"]["report"]["comments"] |> hd |> Map.get("text") == report.comments |> hd |> Map.get(:text) diff --git a/test/support/factory.ex b/test/support/factory.ex index 699f834e8..7ca138b34 100644 --- a/test/support/factory.ex +++ b/test/support/factory.ex @@ -322,7 +322,7 @@ defmodule Mobilizon.Factory do url: "http://mobilizon.test/report/deae1020-54b8-47df-9eea-d8c0e943e57f/activity", reported: build(:actor), reporter: build(:actor), - event: build(:event), + events: build_list(1, :event), comments: build_list(1, :comment) } end