Merge branch 'feature/reports' into 'master'

Introduce backend for reports

See merge request framasoft/mobilizon!159
This commit is contained in:
Thomas Citharel 2019-07-23 13:49:22 +02:00
commit 6b529c3057
36 changed files with 2028 additions and 36 deletions

View File

@ -13,12 +13,15 @@ config :mobilizon, :instance,
name: System.get_env("MOBILIZON_INSTANCE_NAME") || "Localhost", name: System.get_env("MOBILIZON_INSTANCE_NAME") || "Localhost",
description: System.get_env("MOBILIZON_INSTANCE_DESCRIPTION") || "This is a Mobilizon instance", description: System.get_env("MOBILIZON_INSTANCE_DESCRIPTION") || "This is a Mobilizon instance",
version: "1.0.0-dev", version: "1.0.0-dev",
hostname: System.get_env("MOBILIZON_INSTANCE_HOST") || "localhost",
registrations_open: System.get_env("MOBILIZON_INSTANCE_REGISTRATIONS_OPEN") || false, registrations_open: System.get_env("MOBILIZON_INSTANCE_REGISTRATIONS_OPEN") || false,
repository: Mix.Project.config()[:source_url], repository: Mix.Project.config()[:source_url],
remote_limit: 100_000, remote_limit: 100_000,
upload_limit: 16_000_000, upload_limit: 16_000_000,
avatar_upload_limit: 2_000_000, avatar_upload_limit: 2_000_000,
banner_upload_limit: 4_000_000 banner_upload_limit: 4_000_000,
email_from: "noreply@localhost",
email_reply_to: "noreply@localhost"
config :mime, :types, %{ config :mime, :types, %{
"application/activity+json" => ["activity-json"], "application/activity+json" => ["activity-json"],
@ -30,10 +33,7 @@ config :mobilizon, MobilizonWeb.Endpoint,
url: [host: "localhost"], url: [host: "localhost"],
secret_key_base: "1yOazsoE0Wqu4kXk3uC5gu3jDbShOimTCzyFL3OjCdBmOXMyHX87Qmf3+Tu9s0iM", secret_key_base: "1yOazsoE0Wqu4kXk3uC5gu3jDbShOimTCzyFL3OjCdBmOXMyHX87Qmf3+Tu9s0iM",
render_errors: [view: MobilizonWeb.ErrorView, accepts: ~w(html json)], render_errors: [view: MobilizonWeb.ErrorView, accepts: ~w(html json)],
pubsub: [name: Mobilizon.PubSub, adapter: Phoenix.PubSub.PG2], pubsub: [name: Mobilizon.PubSub, adapter: Phoenix.PubSub.PG2]
instance: "localhost",
email_from: "noreply@localhost",
email_to: "noreply@localhost"
# Upload configuration # Upload configuration
config :mobilizon, MobilizonWeb.Upload, config :mobilizon, MobilizonWeb.Upload,

View File

@ -35,6 +35,8 @@ defmodule Mobilizon.Actors.Actor do
alias Mobilizon.Events.{Event, FeedToken} alias Mobilizon.Events.{Event, FeedToken}
alias Mobilizon.Media.File alias Mobilizon.Media.File
alias Mobilizon.Reports.{Report, Note}
alias MobilizonWeb.Router.Helpers, as: Routes alias MobilizonWeb.Router.Helpers, as: Routes
alias MobilizonWeb.Endpoint alias MobilizonWeb.Endpoint
@ -72,6 +74,9 @@ defmodule Mobilizon.Actors.Actor do
has_many(:feed_tokens, FeedToken, foreign_key: :actor_id) has_many(:feed_tokens, FeedToken, foreign_key: :actor_id)
embeds_one(:avatar, File, on_replace: :update) embeds_one(:avatar, File, on_replace: :update)
embeds_one(:banner, File, on_replace: :update) embeds_one(:banner, File, on_replace: :update)
has_many(:created_reports, Report, foreign_key: :reporter_id)
has_many(:subject_reports, Report, foreign_key: :reported_id)
has_many(:report_notes, Note, foreign_key: :moderator_id)
timestamps() timestamps()
end end

48
lib/mobilizon/admin.ex Normal file
View File

@ -0,0 +1,48 @@
defmodule Mobilizon.Admin do
@moduledoc """
The Admin context.
"""
import Ecto.Query, warn: false
alias Mobilizon.Repo
import Mobilizon.Ecto
alias Mobilizon.Admin.ActionLog
@doc """
Returns the list of action_logs.
## Examples
iex> list_action_logs()
[%ActionLog{}, ...]
"""
@spec list_action_logs(integer(), integer()) :: list(ActionLog.t())
def list_action_logs(page \\ nil, limit \\ nil) do
from(
r in ActionLog,
preload: [:actor]
)
|> paginate(page, limit)
|> Repo.all()
end
@doc """
Creates a action_log.
## Examples
iex> create_action_log(%{field: value})
{:ok, %ActionLog{}}
iex> create_action_log(%{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
def create_action_log(attrs \\ %{}) do
%ActionLog{}
|> ActionLog.changeset(attrs)
|> Repo.insert()
end
end

View File

@ -0,0 +1,27 @@
defmodule Mobilizon.Admin.ActionLog do
@moduledoc """
ActionLog entity schema
"""
use Ecto.Schema
import Ecto.Changeset
alias Mobilizon.Actors.Actor
@required_attrs [:action, :target_type, :target_id, :changes, :actor_id]
schema "admin_action_logs" do
field(:action, :string)
field(:target_type, :string)
field(:target_id, :integer)
field(:changes, :map)
belongs_to(:actor, Actor)
timestamps()
end
@doc false
def changeset(action_log, attrs) do
action_log
|> cast(attrs, @required_attrs)
|> validate_required(@required_attrs -- [:changes])
end
end

View File

@ -19,7 +19,12 @@ defmodule Mobilizon.CommonConfig do
|> get_in([:description]) |> get_in([:description])
end end
defp instance_config(), do: Application.get_env(:mobilizon, :instance) def instance_hostname() do
instance_config()
|> get_in([:hostname])
end
def instance_config(), do: Application.get_env(:mobilizon, :instance)
defp to_bool(v), do: v == true or v == "true" or v == "True" defp to_bool(v), do: v == true or v == "true" or v == "True"

View File

@ -0,0 +1,38 @@
defmodule Mobilizon.Email.Admin do
@moduledoc """
Handles emails sent to admins
"""
alias Mobilizon.Users.User
import Bamboo.Email
import Bamboo.Phoenix
use Bamboo.Phoenix, view: Mobilizon.EmailView
import MobilizonWeb.Gettext
alias Mobilizon.Reports.Report
def report(%User{email: email} = _user, %Report{} = report, locale \\ "en") do
Gettext.put_locale(locale)
instance_url = get_config(:hostname)
base_email()
|> to(email)
|> subject(gettext("Mobilizon: New report on instance %{instance}", instance: instance_url))
|> put_header("Reply-To", get_config(:email_reply_to))
|> assign(:report, report)
|> assign(:instance, instance_url)
|> render(:report)
end
defp base_email do
# Here you can set a default from, default headers, etc.
new_email()
|> from(get_config(:email_from))
|> put_html_layout({Mobilizon.EmailView, "email.html"})
|> put_text_layout({Mobilizon.EmailView, "email.text"})
end
@spec get_config(atom()) :: any()
defp get_config(key) do
Mobilizon.CommonConfig.instance_config() |> Keyword.get(key)
end
end

View File

@ -18,7 +18,7 @@ defmodule Mobilizon.Email.User do
|> subject( |> subject(
gettext("Mobilizon: Confirmation instructions for %{instance}", instance: instance_url) gettext("Mobilizon: Confirmation instructions for %{instance}", instance: instance_url)
) )
|> put_header("Reply-To", get_config(:reply_to)) |> put_header("Reply-To", get_config(:email_reply_to))
|> assign(:token, user.confirmation_token) |> assign(:token, user.confirmation_token)
|> assign(:instance, instance_url) |> assign(:instance, instance_url)
|> render(:registration_confirmation) |> render(:registration_confirmation)
@ -26,7 +26,7 @@ defmodule Mobilizon.Email.User do
def reset_password_email(%User{} = user, locale \\ "en") do def reset_password_email(%User{} = user, locale \\ "en") do
Gettext.put_locale(locale) Gettext.put_locale(locale)
instance_url = get_config(:instance) instance_url = get_config(:hostname)
base_email() base_email()
|> to(user.email) |> to(user.email)
@ -36,7 +36,7 @@ defmodule Mobilizon.Email.User do
instance: instance_url instance: instance_url
) )
) )
|> put_header("Reply-To", get_config(:reply_to)) |> put_header("Reply-To", get_config(:email_reply_to))
|> assign(:token, user.reset_password_token) |> assign(:token, user.reset_password_token)
|> assign(:instance, instance_url) |> assign(:instance, instance_url)
|> render(:password_reset) |> render(:password_reset)
@ -45,13 +45,13 @@ defmodule Mobilizon.Email.User do
defp base_email do defp base_email do
# Here you can set a default from, default headers, etc. # Here you can set a default from, default headers, etc.
new_email() new_email()
|> from(Application.get_env(:mobilizon, MobilizonWeb.Endpoint)[:email_from]) |> from(get_config(:email_from))
|> put_html_layout({Mobilizon.EmailView, "email.html"}) |> put_html_layout({Mobilizon.EmailView, "email.html"})
|> put_text_layout({Mobilizon.EmailView, "email.text"}) |> put_text_layout({Mobilizon.EmailView, "email.text"})
end end
@spec get_config(atom()) :: any() @spec get_config(atom()) :: any()
defp get_config(key) do defp get_config(key) do
_config = Application.get_env(:mobilizon, MobilizonWeb.Endpoint) |> Keyword.get(key) Mobilizon.CommonConfig.instance_config() |> Keyword.get(key)
end end
end end

View File

@ -1160,6 +1160,19 @@ defmodule Mobilizon.Events do
end end
end end
@doc """
Get all comments by an actor and a list of ids
"""
def get_all_comments_by_actor_and_ids(actor_id, comment_ids \\ [])
def get_all_comments_by_actor_and_ids(_actor_id, []), do: []
def get_all_comments_by_actor_and_ids(actor_id, comment_ids) do
Comment
|> where([c], c.id in ^comment_ids)
|> where([c], c.actor_id == ^actor_id)
|> Repo.all()
end
@doc """ @doc """
Creates a comment. Creates a comment.

236
lib/mobilizon/reports.ex Normal file
View File

@ -0,0 +1,236 @@
defmodule Mobilizon.Reports do
@moduledoc """
The Reports context.
"""
import Ecto.Query, warn: false
alias Mobilizon.Repo
import Mobilizon.Ecto
alias Mobilizon.Reports.Report
alias Mobilizon.Reports.Note
@doc false
def data() do
Dataloader.Ecto.new(Mobilizon.Repo, query: &query/2)
end
@doc false
def query(queryable, _params) do
queryable
end
@doc """
Returns the list of reports.
## Examples
iex> list_reports()
[%Report{}, ...]
"""
@spec list_reports(integer(), integer(), atom(), atom()) :: list(Report.t())
def list_reports(page \\ nil, limit \\ nil, sort \\ :updated_at, direction \\ :asc) do
from(
r in Report,
preload: [:reported, :reporter, :manager, :event, :comments, :notes]
)
|> paginate(page, limit)
|> sort(sort, direction)
|> Repo.all()
end
@doc """
Gets a single report.
Raises `Ecto.NoResultsError` if the Report does not exist.
## Examples
iex> get_report!(123)
%Report{}
iex> get_report!(456)
** (Ecto.NoResultsError)
"""
def get_report!(id) do
with %Report{} = report <- Repo.get!(Report, id) do
Repo.preload(report, [:reported, :reporter, :manager, :event, :comments, :notes])
end
end
@doc """
Gets a single report.
Returns `nil` if the Report does not exist.
## Examples
iex> get_report(123)
%Report{}
iex> get_report(456)
nil
"""
def get_report(id) do
with %Report{} = report <- Repo.get(Report, id) do
Repo.preload(report, [:reported, :reporter, :manager, :event, :comments, :notes])
end
end
@doc """
Get a report by it's URL
"""
@spec get_report_by_url(String.t()) :: Report.t() | nil
def get_report_by_url(url) do
from(
r in Report,
where: r.uri == ^url
)
|> Repo.one()
end
@doc """
Creates a report.
## Examples
iex> create_report(%{field: value})
{:ok, %Report{}}
iex> create_report(%{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
def create_report(attrs \\ %{}) do
with {:ok, %Report{} = report} <-
%Report{}
|> Report.creation_changeset(attrs)
|> Repo.insert() do
{:ok, Repo.preload(report, [:event, :reported, :reporter, :comments])}
end
end
@doc """
Updates a report.
## Examples
iex> update_report(report, %{field: new_value})
{:ok, %Report{}}
iex> update_report(report, %{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
def update_report(%Report{} = report, attrs) do
report
|> Report.changeset(attrs)
|> Repo.update()
end
@doc """
Deletes a Report.
## Examples
iex> delete_report(report)
{:ok, %Report{}}
iex> delete_report(report)
{:error, %Ecto.Changeset{}}
"""
def delete_report(%Report{} = report) do
Repo.delete(report)
end
@doc """
Returns an `%Ecto.Changeset{}` for tracking report changes.
## Examples
iex> change_report(report)
%Ecto.Changeset{source: %Report{}}
"""
def change_report(%Report{} = report) do
Report.changeset(report, %{})
end
@doc """
Returns the list of notes for a report.
## Examples
iex> list_notes_for_report(%Report{id: 1})
[%Note{}, ...]
"""
@spec list_notes_for_report(Report.t()) :: list(Report.t())
def list_notes_for_report(%Report{id: report_id}) do
from(
n in Note,
where: n.report_id == ^report_id,
preload: [:report, :moderator]
)
|> Repo.all()
end
@doc """
Gets a single note.
Raises `Ecto.NoResultsError` if the Note does not exist.
## Examples
iex> get_note!(123)
%Note{}
iex> get_note!(456)
** (Ecto.NoResultsError)
"""
def get_note!(id), do: Repo.get!(Note, id)
def get_note(id), do: Repo.get(Note, id)
@doc """
Creates a note report.
## Examples
iex> create_report_note(%{field: value})
{:ok, %Note{}}
iex> create_report_note(%{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
def create_report_note(attrs \\ %{}) do
with {:ok, %Note{} = note} <-
%Note{}
|> Note.changeset(attrs)
|> Repo.insert() do
{:ok, Repo.preload(note, [:report, :moderator])}
end
end
@doc """
Deletes a note report.
## Examples
iex> delete_report_note(note)
{:ok, %Note{}}
iex> delete_report_note(note)
{:error, %Ecto.Changeset{}}
"""
def delete_report_note(%Note{} = note) do
Repo.delete(note)
end
end

View File

@ -0,0 +1,27 @@
defmodule Mobilizon.Reports.Note do
@moduledoc """
Report Note entity
"""
use Ecto.Schema
import Ecto.Changeset
alias Mobilizon.Actors.Actor
alias Mobilizon.Reports.Report
@attrs [:content, :moderator_id, :report_id]
@derive {Jason.Encoder, only: [:content]}
schema "report_notes" do
field(:content, :string)
belongs_to(:moderator, Actor)
belongs_to(:report, Report)
timestamps()
end
@doc false
def changeset(note, attrs) do
note
|> cast(attrs, @attrs)
|> validate_required(@attrs)
end
end

View File

@ -0,0 +1,59 @@
import EctoEnum
defenum(Mobilizon.Reports.ReportStateEnum, :report_state, [
:open,
:closed,
:resolved
])
defmodule Mobilizon.Reports.Report do
@moduledoc """
Report entity
"""
use Ecto.Schema
import Ecto.Changeset
alias Mobilizon.Events.Comment
alias Mobilizon.Events.Event
alias Mobilizon.Actors.Actor
alias Mobilizon.Reports.Note
@derive {Jason.Encoder, only: [:status, :uri]}
schema "reports" do
field(:content, :string)
field(:status, Mobilizon.Reports.ReportStateEnum, default: :open)
field(:uri, :string)
# The reported actor
belongs_to(:reported, Actor)
# The actor who reported
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 Comments inside the report
many_to_many(:comments, Comment, join_through: "reports_comments", on_replace: :delete)
# The notes associated to the report
has_many(:notes, Note, foreign_key: :report_id)
timestamps()
end
@doc false
def changeset(report, attrs) do
report
|> cast(attrs, [:content, :status, :uri, :reported_id, :reporter_id, :manager_id, :event_id])
|> validate_required([:content, :uri, :reported_id, :reporter_id])
end
def creation_changeset(report, attrs) do
report
|> changeset(attrs)
|> put_assoc(:comments, attrs["comments"])
end
end

View File

@ -258,6 +258,36 @@ defmodule Mobilizon.Users do
) )
end end
@doc """
Returns the list of administrators.
## Examples
iex> list_admins()
[%Mobilizon.Users.User{role: :administrator}]
"""
def list_admins() do
User
|> where([u], u.role == ^:administrator)
|> Repo.all()
end
@doc """
Returns the list of moderators.
## Examples
iex> list_moderators()
[%Mobilizon.Users.User{role: :moderator}, %Mobilizon.Users.User{role: :administrator}]
"""
def list_moderators() do
User
|> where([u], u.role in ^[:administrator, :moderator])
|> Repo.all()
end
def count_users() do def count_users() do
Repo.one( Repo.one(
from( from(

View File

@ -26,7 +26,7 @@ defmodule MobilizonWeb.API.Events do
Actors.get_local_actor_with_everything(organizer_actor_id), Actors.get_local_actor_with_everything(organizer_actor_id),
title <- String.trim(title), title <- String.trim(title),
mentions <- Formatter.parse_mentions(description), mentions <- Formatter.parse_mentions(description),
visibility <- Map.get(args, :visibility, "public"), visibility <- Map.get(args, :visibility, :public),
{to, cc} <- to_for_actor_and_mentions(actor, mentions, nil, Atom.to_string(visibility)), {to, cc} <- to_for_actor_and_mentions(actor, mentions, nil, Atom.to_string(visibility)),
tags <- Formatter.parse_tags(description), tags <- Formatter.parse_tags(description),
picture <- Map.get(args, :picture, nil), picture <- Map.get(args, :picture, nil),

View File

@ -0,0 +1,128 @@
defmodule MobilizonWeb.API.Reports do
@moduledoc """
API for Reports
"""
alias Mobilizon.Actors
alias Mobilizon.Actors.Actor
alias Mobilizon.Events
alias Mobilizon.Activity
alias Mobilizon.Reports, as: ReportsAction
alias Mobilizon.Reports.{Report, Note}
alias Mobilizon.Service.ActivityPub
alias Mobilizon.Users
alias Mobilizon.Users.User
import MobilizonWeb.API.Utils
import Mobilizon.Service.Admin.ActionLogService
@doc """
Create a report/flag on an actor, and optionally on an event or on comments.
"""
def report(
%{
reporter_actor_id: reporter_actor_id,
reported_actor_id: reported_actor_id,
event_id: event_id,
comments_ids: comments_ids,
report_content: report_content
} = args
) do
with {:reporter, %Actor{url: reporter_url} = _reporter_actor} <-
{:reporter, Actors.get_actor!(reporter_actor_id)},
{:reported, %Actor{url: reported_actor_url} = reported_actor} <-
{:reported, Actors.get_actor!(reported_actor_id)},
{:ok, content} <- make_report_content_html(report_content),
{:ok, event} <-
if(event_id, do: Events.get_event(event_id), else: {:ok, nil}),
{:get_report_comments, comments_urls} <-
get_report_comments(reported_actor, comments_ids),
{:make_activity, {:ok, %Activity{} = activity, %Report{} = report}} <-
{:make_activity,
ActivityPub.flag(%{
reporter_url: reporter_url,
reported_actor_url: reported_actor_url,
event_url: (!is_nil(event) && event.url) || nil,
comments_url: comments_urls,
content: content,
forward: args[:forward] || false,
local: args[:local] || args[:forward] || false
})} do
{:ok, activity, report}
else
{:error, err} -> {:error, err}
{:actor_id, %{}} -> {:error, "Valid `actor_id` required"}
{:reporter, nil} -> {:error, "Reporter Actor not found"}
{:reported, nil} -> {:error, "Reported Actor not found"}
end
end
@doc """
Update the state of a report
"""
def update_report_status(%Actor{} = actor, %Report{} = report, state) do
with {:valid_state, true} <-
{:valid_state, Mobilizon.Reports.ReportStateEnum.valid_value?(state)},
{:ok, report} <- ReportsAction.update_report(report, %{"status" => state}),
{:ok, _} <- log_action(actor, "update", report) do
{:ok, report}
else
{:valid_state, false} -> {:error, "Unsupported state"}
end
end
defp get_report_comments(%Actor{id: actor_id}, comment_ids) do
{:get_report_comments,
Events.get_all_comments_by_actor_and_ids(actor_id, comment_ids) |> Enum.map(& &1.url)}
end
defp get_report_comments(_, _), do: {:get_report_comments, nil}
@doc """
Create a note on a report
"""
@spec create_report_note(Report.t(), Actor.t(), String.t()) :: {:ok, Note.t()}
def create_report_note(
%Report{id: report_id},
%Actor{id: moderator_id, user_id: user_id} = moderator,
content
) do
with %User{role: role} <- Users.get_user!(user_id),
{:role, true} <- {:role, role in [:administrator, :moderator]},
{:ok, %Note{} = note} <-
Mobilizon.Reports.create_report_note(%{
"report_id" => report_id,
"moderator_id" => moderator_id,
"content" => content
}),
{:ok, _} <- log_action(moderator, "create", note) do
{:ok, note}
else
{:role, false} ->
{:error, "You need to be a moderator or an administrator to create a note on a report"}
end
end
@doc """
Delete a report note
"""
@spec delete_report_note(Note.t(), Actor.t()) :: {:ok, Note.t()}
def delete_report_note(
%Note{moderator_id: note_moderator_id} = note,
%Actor{id: moderator_id, user_id: user_id} = moderator
) do
with {:same_actor, true} <- {:same_actor, note_moderator_id == moderator_id},
%User{role: role} <- Users.get_user!(user_id),
{:role, true} <- {:role, role in [:administrator, :moderator]},
{:ok, %Note{} = note} <-
Mobilizon.Reports.delete_report_note(note),
{:ok, _} <- log_action(moderator, "delete", note) do
{:ok, note}
else
{:role, false} ->
{:error, "You need to be a moderator or an administrator to create a note on a report"}
{:same_actor, false} ->
{:error, "You can only remove your own notes"}
end
end
end

View File

@ -120,4 +120,16 @@ defmodule MobilizonWeb.API.Utils do
# |> Formatter.add_hashtag_links(tags) # |> Formatter.add_hashtag_links(tags)
# |> Formatter.finalize() # |> Formatter.finalize()
# end # end
def make_report_content_html(nil), do: {:ok, {nil, [], []}}
def make_report_content_html(comment) do
max_size = Mobilizon.CommonConfig.get([:instance, :max_report_comment_size], 1000)
if String.length(comment) <= max_size do
{:ok, Formatter.html_escape(comment, "text/plain")}
else
{:error, "Comment must be up to #{max_size} characters"}
end
end
end end

View File

@ -0,0 +1,72 @@
defmodule MobilizonWeb.Resolvers.Admin do
@moduledoc """
Handles the report-related GraphQL calls
"""
alias Mobilizon.Users.User
import Mobilizon.Users.Guards
alias Mobilizon.Admin.ActionLog
alias Mobilizon.Reports.{Report, Note}
def list_action_logs(_parent, %{page: page, limit: limit}, %{
context: %{current_user: %User{role: role}}
})
when is_moderator(role) do
with action_logs <- Mobilizon.Admin.list_action_logs(page, limit) do
action_logs =
Enum.map(action_logs, fn %ActionLog{
target_type: target_type,
action: action,
actor: actor,
id: id
} = action_log ->
transform_action_log(target_type, action, action_log)
|> Map.merge(%{
actor: actor,
id: id
})
end)
{:ok, action_logs}
end
end
def list_action_logs(_parent, _args, _resolution) do
{:error, "You need to be logged-in and a moderator to list action logs"}
end
defp transform_action_log(
"Elixir.Mobilizon.Reports.Report",
"update",
%ActionLog{} = action_log
) do
with %Report{status: status} = report <- Mobilizon.Reports.get_report(action_log.target_id) do
%{
action: "report_update_" <> to_string(status),
object: report
}
end
end
defp transform_action_log("Elixir.Mobilizon.Reports.Note", "create", %ActionLog{
changes: changes
}) do
%{
action: "note_creation",
object: convert_changes_to_struct(Note, changes)
}
end
defp transform_action_log("Elixir.Mobilizon.Reports.Note", "delete", %ActionLog{
changes: changes
}) do
%{
action: "note_deletion",
object: convert_changes_to_struct(Note, changes)
}
end
# Changes are stored as %{"key" => "value"} so we need to convert them back as struct
defp convert_changes_to_struct(struct, changes) do
struct(struct, for({key, val} <- changes, into: %{}, do: {String.to_atom(key), val}))
end
end

View File

@ -0,0 +1,118 @@
defmodule MobilizonWeb.Resolvers.Report do
@moduledoc """
Handles the report-related GraphQL calls
"""
alias Mobilizon.Reports
alias Mobilizon.Reports.{Report, Note}
alias Mobilizon.Actors.Actor
alias Mobilizon.Actors
alias Mobilizon.Users.User
alias MobilizonWeb.API.Reports, as: ReportsAPI
import Mobilizon.Users.Guards
def list_reports(_parent, %{page: page, limit: limit}, %{
context: %{current_user: %User{role: role}}
})
when is_moderator(role) do
{:ok, Mobilizon.Reports.list_reports(page, limit)}
end
def list_reports(_parent, _args, _resolution) do
{:error, "You need to be logged-in and a moderator to list reports"}
end
def get_report(_parent, %{id: id}, %{
context: %{current_user: %User{role: role}}
})
when is_moderator(role) do
{:ok, Mobilizon.Reports.get_report(id)}
end
def get_report(_parent, _args, _resolution) do
{:error, "You need to be logged-in and a moderator to view a report"}
end
@doc """
Create a report
"""
def create_report(
_parent,
%{reporter_actor_id: reporter_actor_id} = args,
%{context: %{current_user: user}} = _resolution
) do
with {:is_owned, true, _} <- User.owns_actor(user, reporter_actor_id),
{:ok, _, %Report{} = report} <- ReportsAPI.report(args) do
{:ok, report}
else
{:is_owned, false} ->
{:error, "Reporter actor id is not owned by authenticated user"}
_err ->
{:error, "Error while saving report"}
end
end
def create_report(_parent, _args, _resolution) do
{:error, "You need to be logged-in to create reports"}
end
@doc """
Update a report's status
"""
def update_report(
_parent,
%{report_id: report_id, moderator_id: moderator_id, status: status},
%{
context: %{current_user: %User{role: role} = user}
}
)
when is_moderator(role) do
with {:is_owned, true, _} <- User.owns_actor(user, moderator_id),
%Actor{} = actor <- Actors.get_actor!(moderator_id),
%Report{} = report <- Mobilizon.Reports.get_report(report_id),
{:ok, %Report{} = report} <-
MobilizonWeb.API.Reports.update_report_status(actor, report, status) do
{:ok, report}
else
{:is_owned, false} ->
{:error, "Actor id is not owned by authenticated user"}
_err ->
{:error, "Error while updating report"}
end
end
def update_report(_parent, _args, _resolution) do
{:error, "You need to be logged-in and a moderator to update a report"}
end
def create_report_note(
_parent,
%{report_id: report_id, moderator_id: moderator_id, content: content},
%{
context: %{current_user: %User{role: role} = user}
}
)
when is_moderator(role) do
with {:is_owned, true, _} <- User.owns_actor(user, moderator_id),
%Report{} = report <- Reports.get_report(report_id),
%Actor{} = moderator <- Actors.get_local_actor_with_everything(moderator_id),
{:ok, %Note{} = note} <-
MobilizonWeb.API.Reports.create_report_note(report, moderator, content) do
{:ok, note}
end
end
def delete_report_note(_parent, %{note_id: note_id, moderator_id: moderator_id}, %{
context: %{current_user: %User{role: role} = user}
})
when is_moderator(role) do
with {:is_owned, true, _} <- User.owns_actor(user, moderator_id),
%Note{} = note <- Reports.get_note(note_id),
%Actor{} = moderator <- Actors.get_local_actor_with_everything(moderator_id),
{:ok, %Note{} = note} <-
MobilizonWeb.API.Reports.delete_report_note(note, moderator) do
{:ok, %{id: note.id}}
end
end
end

View File

@ -21,6 +21,8 @@ defmodule MobilizonWeb.Schema do
import_types(MobilizonWeb.Schema.CommentType) import_types(MobilizonWeb.Schema.CommentType)
import_types(MobilizonWeb.Schema.SearchType) import_types(MobilizonWeb.Schema.SearchType)
import_types(MobilizonWeb.Schema.ConfigType) import_types(MobilizonWeb.Schema.ConfigType)
import_types(MobilizonWeb.Schema.ReportType)
import_types(MobilizonWeb.Schema.AdminType)
@desc "A struct containing the id of the deleted object" @desc "A struct containing the id of the deleted object"
object :deleted_object do object :deleted_object do
@ -109,6 +111,8 @@ defmodule MobilizonWeb.Schema do
import_fields(:address_queries) import_fields(:address_queries)
import_fields(:config_queries) import_fields(:config_queries)
import_fields(:picture_queries) import_fields(:picture_queries)
import_fields(:report_queries)
import_fields(:admin_queries)
end end
@desc """ @desc """
@ -124,5 +128,6 @@ defmodule MobilizonWeb.Schema do
import_fields(:member_mutations) import_fields(:member_mutations)
import_fields(:feed_token_mutations) import_fields(:feed_token_mutations)
import_fields(:picture_mutations) import_fields(:picture_mutations)
import_fields(:report_mutations)
end end
end end

View File

@ -0,0 +1,41 @@
defmodule MobilizonWeb.Schema.AdminType do
@moduledoc """
Schema representation for ActionLog
"""
use Absinthe.Schema.Notation
alias MobilizonWeb.Resolvers.Admin
alias Mobilizon.Reports.{Report, Note}
@desc "An action log"
object :action_log do
field(:id, :id, description: "Internal ID for this comment")
field(:actor, :actor, description: "The actor that acted")
field(:object, :action_log_object, description: "The object that was acted upon")
field(:action, :string, description: "The action that was done")
end
@desc "The objects that can be in an action log"
interface :action_log_object do
field(:id, :id, description: "Internal ID for this object")
resolve_type(fn
%Report{}, _ ->
:report
%Note{}, _ ->
:report_note
_, _ ->
nil
end)
end
object :admin_queries do
@desc "Get the list of action logs"
field :action_logs, type: list_of(:action_log) do
arg(:page, :integer, default_value: 1)
arg(:limit, :integer, default_value: 10)
resolve(&Admin.list_action_logs/3)
end
end
end

View File

@ -0,0 +1,86 @@
defmodule MobilizonWeb.Schema.ReportType do
@moduledoc """
Schema representation for User
"""
use Absinthe.Schema.Notation
alias MobilizonWeb.Resolvers.Report
@desc "A report object"
object :report do
interfaces([:action_log_object])
field(:id, :id, description: "The internal ID of the report")
field(:content, :string, description: "The comment the reporter added about this report")
field(:status, :report_status, description: "Whether the report is still active")
field(:uri, :string, description: "The URI of the report")
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(:comments, list_of(:comment), description: "The comments that are reported")
end
@desc "A report note object"
object :report_note do
interfaces([:action_log_object])
field(:id, :id, description: "The internal ID of the report note")
field(:content, :string, description: "The content of the note")
field(:moderator, :actor, description: "The moderator who added the note")
field(:report, :report, description: "The report on which this note is added")
end
@desc "The list of possible statuses for a report object"
enum :report_status do
value(:open, description: "The report has been opened")
value(:closed, description: "The report has been closed")
value(:resolved, description: "The report has been marked as resolved")
end
object :report_queries do
@desc "Get all reports"
field :reports, list_of(:report) do
arg(:page, :integer, default_value: 1)
arg(:limit, :integer, default_value: 10)
resolve(&Report.list_reports/3)
end
@desc "Get a report by id"
field :report, :report do
arg(:id, non_null(:id))
resolve(&Report.get_report/3)
end
end
object :report_mutations do
@desc "Create a report"
field :create_report, type: :report do
arg(:report_content, :string)
arg(:reporter_actor_id, non_null(:id))
arg(:reported_actor_id, non_null(:id))
arg(:event_id, :id, default_value: nil)
arg(:comments_ids, list_of(:id), default_value: [])
resolve(&Report.create_report/3)
end
@desc "Update a report"
field :update_report_status, type: :report do
arg(:report_id, non_null(:id))
arg(:moderator_id, non_null(:id))
arg(:status, non_null(:report_status))
resolve(&Report.update_report/3)
end
@desc "Create a note on a report"
field :create_report_note, type: :report_note do
arg(:content, :string)
arg(:moderator_id, non_null(:id))
arg(:report_id, non_null(:id))
resolve(&Report.create_report_note/3)
end
field :delete_report_note, type: :deleted_object do
arg(:note_id, non_null(:id))
arg(:moderator_id, non_null(:id))
resolve(&Report.delete_report_note/3)
end
end
end

View File

@ -0,0 +1,15 @@
<h1><%= gettext "New report from %{reporter} on %{instance}", reporter: @report.reporter, instance: @instance %></h1>
<% if @report.event do %>
<p><%= gettext "Event: %{event}", event: @report.event %></p>
<% end %>
<%= for comment <- @report.comments do %>
<p><%= gettext "Comment: %{comment}", comment: comment %></p>
<% end %>
<% if @content do %>
<p><%= gettext "Reason: %{content}", event: @report.content %></p>
<% end %>
<p><%= link "View the report", to: MobilizonWeb.Endpoint.url() <> "/reports/#{@report.id}", target: "_blank" %></p>

View File

@ -0,0 +1,19 @@
<%= gettext "New report from %{reporter} on %{instance}", reporter: @report.reporter, instance: @instance %>
--
<% if @report.event do %>
<%= gettext "Event: %{event}", event: @report.event %>
<% end %>
<%= for comment <- @report.comments do %>
<%= gettext "Comment: %{comment}", comment: comment %>
<% end %>
<% if @content do %>
<%= gettext "Reason: %{content}", event: @report.content %>
<% end %>
<%= link "View the report", to: MobilizonWeb.Endpoint.url() <> "/reports/#{@report.id}", target: "_blank" %>

View File

@ -41,7 +41,7 @@ defmodule Mobilizon.Service.ActivityPub do
@spec insert(map(), boolean()) :: {:ok, %Activity{}} | {:error, any()} @spec insert(map(), boolean()) :: {:ok, %Activity{}} | {:error, any()}
def insert(map, local \\ true) when is_map(map) do def insert(map, local \\ true) when is_map(map) do
with map <- lazy_put_activity_defaults(map), with map <- lazy_put_activity_defaults(map),
:ok <- insert_full_object(map) do {:ok, object} <- insert_full_object(map) do
object_id = if is_map(map["object"]), do: map["object"]["id"], else: map["id"] object_id = if is_map(map["object"]), do: map["object"]["id"], else: map["id"]
map = if local, do: Map.put(map, "id", "#{object_id}/activity"), else: map map = if local, do: Map.put(map, "id", "#{object_id}/activity"), else: map
@ -55,7 +55,7 @@ defmodule Mobilizon.Service.ActivityPub do
# Notification.create_notifications(activity) # Notification.create_notifications(activity)
# stream_out(activity) # stream_out(activity)
{:ok, activity} {:ok, activity, object}
else else
%Activity{} = activity -> {:ok, activity} %Activity{} = activity -> {:ok, activity}
error -> {:error, error} error -> {:error, error}
@ -130,7 +130,7 @@ defmodule Mobilizon.Service.ActivityPub do
additional additional
), ),
:ok <- Logger.debug(inspect(create_data)), :ok <- Logger.debug(inspect(create_data)),
{:ok, activity} <- insert(create_data, local), {:ok, activity, _object} <- insert(create_data, local),
:ok <- maybe_federate(activity) do :ok <- maybe_federate(activity) do
# {:ok, actor} <- Actors.increase_event_count(actor) do # {:ok, actor} <- Actors.increase_event_count(actor) do
{:ok, activity} {:ok, activity}
@ -147,7 +147,7 @@ defmodule Mobilizon.Service.ActivityPub do
local = !(params[:local] == false) local = !(params[:local] == false)
with data <- %{"to" => to, "type" => "Accept", "actor" => actor, "object" => object}, with data <- %{"to" => to, "type" => "Accept", "actor" => actor, "object" => object},
{:ok, activity} <- insert(data, local), {:ok, activity, _object} <- insert(data, local),
:ok <- maybe_federate(activity) do :ok <- maybe_federate(activity) do
{:ok, activity} {:ok, activity}
end end
@ -164,7 +164,7 @@ defmodule Mobilizon.Service.ActivityPub do
"actor" => actor, "actor" => actor,
"object" => object "object" => object
}, },
{:ok, activity} <- insert(data, local), {:ok, activity, _object} <- insert(data, local),
:ok <- maybe_federate(activity) do :ok <- maybe_federate(activity) do
{:ok, activity} {:ok, activity}
end end
@ -179,7 +179,7 @@ defmodule Mobilizon.Service.ActivityPub do
# ) do # ) do
# with nil <- get_existing_like(url, object), # with nil <- get_existing_like(url, object),
# like_data <- make_like_data(user, object, activity_id), # like_data <- make_like_data(user, object, activity_id),
# {:ok, activity} <- insert(like_data, local), # {:ok, activity, _object} <- insert(like_data, local),
# {:ok, object} <- add_like_to_object(activity, object), # {:ok, object} <- add_like_to_object(activity, object),
# :ok <- maybe_federate(activity) do # :ok <- maybe_federate(activity) do
# {:ok, activity, object} # {:ok, activity, object}
@ -197,7 +197,7 @@ defmodule Mobilizon.Service.ActivityPub do
# ) do # ) do
# with %Activity{} = like_activity <- get_existing_like(actor.ap_id, object), # with %Activity{} = like_activity <- get_existing_like(actor.ap_id, object),
# unlike_data <- make_unlike_data(actor, like_activity, activity_id), # unlike_data <- make_unlike_data(actor, like_activity, activity_id),
# {:ok, unlike_activity} <- insert(unlike_data, local), # {:ok, unlike_activity, _object} <- insert(unlike_data, local),
# {:ok, _activity} <- Repo.delete(like_activity), # {:ok, _activity} <- Repo.delete(like_activity),
# {:ok, object} <- remove_like_from_object(like_activity, object), # {:ok, object} <- remove_like_from_object(like_activity, object),
# :ok <- maybe_federate(unlike_activity) do # :ok <- maybe_federate(unlike_activity) do
@ -215,7 +215,7 @@ defmodule Mobilizon.Service.ActivityPub do
# ) do # ) do
# #with true <- is_public?(object), # #with true <- is_public?(object),
# with announce_data <- make_announce_data(actor, object, activity_id), # with announce_data <- make_announce_data(actor, object, activity_id),
# {:ok, activity} <- insert(announce_data, local), # {:ok, activity, _object} <- insert(announce_data, local),
# # {:ok, object} <- add_announce_to_object(activity, object), # # {:ok, object} <- add_announce_to_object(activity, object),
# :ok <- maybe_federate(activity) do # :ok <- maybe_federate(activity) do
# {:ok, activity, object} # {:ok, activity, object}
@ -232,7 +232,7 @@ defmodule Mobilizon.Service.ActivityPub do
# ) do # ) do
# with %Activity{} = announce_activity <- get_existing_announce(actor.ap_id, object), # with %Activity{} = announce_activity <- get_existing_announce(actor.ap_id, object),
# unannounce_data <- make_unannounce_data(actor, announce_activity, activity_id), # unannounce_data <- make_unannounce_data(actor, announce_activity, activity_id),
# {:ok, unannounce_activity} <- insert(unannounce_data, local), # {:ok, unannounce_activity, _object} <- insert(unannounce_data, local),
# :ok <- maybe_federate(unannounce_activity), # :ok <- maybe_federate(unannounce_activity),
# {:ok, _activity} <- Repo.delete(announce_activity), # {:ok, _activity} <- Repo.delete(announce_activity),
# {:ok, object} <- remove_announce_from_object(announce_activity, object) do # {:ok, object} <- remove_announce_from_object(announce_activity, object) do
@ -250,7 +250,7 @@ defmodule Mobilizon.Service.ActivityPub do
activity_follow_id <- activity_follow_id <-
activity_id || "#{MobilizonWeb.Endpoint.url()}/follow/#{follow_id}/activity", activity_id || "#{MobilizonWeb.Endpoint.url()}/follow/#{follow_id}/activity",
data <- make_follow_data(followed, follower, activity_follow_id), data <- make_follow_data(followed, follower, activity_follow_id),
{:ok, activity} <- insert(data, local), {:ok, activity, _object} <- insert(data, local),
:ok <- maybe_federate(activity) do :ok <- maybe_federate(activity) do
{:ok, activity} {:ok, activity}
else else
@ -267,9 +267,9 @@ defmodule Mobilizon.Service.ActivityPub do
with {:ok, %Follower{id: follow_id}} <- Actor.unfollow(followed, follower), with {:ok, %Follower{id: follow_id}} <- Actor.unfollow(followed, follower),
# We recreate the follow activity # We recreate the follow activity
data <- make_follow_data(followed, follower, follow_id), data <- make_follow_data(followed, follower, follow_id),
{:ok, follow_activity} <- insert(data, local), {:ok, follow_activity, _object} <- insert(data, local),
unfollow_data <- make_unfollow_data(follower, followed, follow_activity, activity_id), unfollow_data <- make_unfollow_data(follower, followed, follow_activity, activity_id),
{:ok, activity} <- insert(unfollow_data, local), {:ok, activity, _object} <- insert(unfollow_data, local),
:ok <- maybe_federate(activity) do :ok <- maybe_federate(activity) do
{:ok, activity} {:ok, activity}
else else
@ -290,7 +290,7 @@ defmodule Mobilizon.Service.ActivityPub do
} }
with Events.delete_event(event), with Events.delete_event(event),
{:ok, activity} <- insert(data, local), {:ok, activity, _object} <- insert(data, local),
:ok <- maybe_federate(activity) do :ok <- maybe_federate(activity) do
{:ok, activity} {:ok, activity}
end end
@ -305,7 +305,7 @@ defmodule Mobilizon.Service.ActivityPub do
} }
with Events.delete_comment(comment), with Events.delete_comment(comment),
{:ok, activity} <- insert(data, local), {:ok, activity, _object} <- insert(data, local),
:ok <- maybe_federate(activity) do :ok <- maybe_federate(activity) do
{:ok, activity} {:ok, activity}
end end
@ -320,12 +320,33 @@ defmodule Mobilizon.Service.ActivityPub do
} }
with Actors.delete_actor(actor), with Actors.delete_actor(actor),
{:ok, activity} <- insert(data, local), {:ok, activity, _object} <- insert(data, local),
:ok <- maybe_federate(activity) do :ok <- maybe_federate(activity) do
{:ok, activity} {:ok, activity}
end end
end end
def flag(params) do
# only accept false as false value
local = !(params[:local] == false)
forward = !(params[:forward] == false)
additional = params[:additional] || %{}
additional =
if forward do
Map.merge(additional, %{"to" => [], "cc" => [params.reported_actor_url]})
else
Map.merge(additional, %{"to" => [], "cc" => []})
end
with flag_data <- make_flag_data(params, additional),
{:ok, activity, report} <- insert(flag_data, local),
:ok <- maybe_federate(activity) do
{:ok, activity, report}
end
end
@doc """ @doc """
Create an actor locally by it's URL (AP ID) Create an actor locally by it's URL (AP ID)
""" """

View File

@ -0,0 +1,85 @@
defmodule Mobilizon.Service.ActivityPub.Converters.Flag do
@moduledoc """
Flag converter
This module allows to convert reports from ActivityStream format to our own internal one, and back.
Note: Reports are named Flag in AS.
"""
alias Mobilizon.Actors
alias Mobilizon.Actors.Actor
alias Mobilizon.Events
alias Mobilizon.Events.Event
alias Mobilizon.Reports.Report
alias Mobilizon.Service.ActivityPub.Converter
@behaviour Converter
@doc """
Converts an AP object data to our internal data structure
"""
@impl Converter
@spec as_to_model_data(map()) :: map()
def as_to_model_data(object) do
with params <- as_to_model(object) do
%{
"reporter_id" => params["reporter"].id,
"uri" => params["uri"],
"content" => params["content"],
"reported_id" => params["reported"].id,
"event_id" => (!is_nil(params["event"]) && params["event"].id) || nil,
"comments" => params["comments"]
}
end
end
def as_to_model(%{"object" => objects} = object) do
with {:ok, %Actor{} = reporter} <- Actors.get_actor_by_url(object["actor"]),
%Actor{} = reported <-
Enum.reduce_while(objects, nil, fn url, _ ->
with {:ok, %Actor{} = actor} <- Actors.get_actor_by_url(url) do
{:halt, actor}
else
_ -> {:cont, nil}
end
end),
event <-
Enum.reduce_while(objects, nil, fn url, _ ->
with %Event{} = event <- Events.get_event_by_url(url) do
{:halt, event}
else
_ -> {:cont, nil}
end
end),
# Remove the reported user from the object list.
comments <-
Enum.filter(objects, fn url ->
!(url == reported.url || (!is_nil(event) && event.url == url))
end),
comments <- Enum.map(comments, &Events.get_comment_from_url/1) do
%{
"reporter" => reporter,
"uri" => object["id"],
"content" => object["content"],
"reported" => reported,
"event" => event,
"comments" => comments
}
end
end
@doc """
Convert an event struct to an ActivityStream representation
"""
@impl Converter
@spec model_to_as(EventModel.t()) :: map()
def model_to_as(%Report{} = report) do
%{
"type" => "Flag",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"actor" => report.reporter.url,
"id" => report.url
}
end
end

View File

@ -116,6 +116,22 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
|> Map.put("tag", combined) |> Map.put("tag", combined)
end end
def handle_incoming(%{"type" => "Flag"} = data) do
with params <- Mobilizon.Service.ActivityPub.Converters.Flag.as_to_model(data) do
params = %{
reporter_url: params["reporter"].url,
reported_actor_url: params["reported"].url,
comments_url: params["comments"] |> Enum.map(& &1.url),
content: params["content"] || "",
additional: %{
"cc" => [params["reported"].url]
}
}
ActivityPub.flag(params)
end
end
# TODO: validate those with a Ecto scheme # TODO: validate those with a Ecto scheme
# - tags # - tags
# - emoji # - emoji

View File

@ -18,6 +18,10 @@ defmodule Mobilizon.Service.ActivityPub.Utils do
alias Mobilizon.Media.Picture alias Mobilizon.Media.Picture
alias Mobilizon.Events alias Mobilizon.Events
alias Mobilizon.Activity alias Mobilizon.Activity
alias Mobilizon.Reports
alias Mobilizon.Reports.Report
alias Mobilizon.Users
alias Mobilizon.Service.ActivityPub.Converters
alias Ecto.Changeset alias Ecto.Changeset
require Logger require Logger
alias MobilizonWeb.Router.Helpers, as: Routes alias MobilizonWeb.Router.Helpers, as: Routes
@ -119,9 +123,9 @@ defmodule Mobilizon.Service.ActivityPub.Utils do
def insert_full_object(%{"object" => %{"type" => "Event"} = object_data}) def insert_full_object(%{"object" => %{"type" => "Event"} = object_data})
when is_map(object_data) do when is_map(object_data) do
with object_data <- with object_data <-
Mobilizon.Service.ActivityPub.Converters.Event.as_to_model_data(object_data), Converters.Event.as_to_model_data(object_data),
{:ok, _} <- Events.create_event(object_data) do {:ok, %Event{} = event} <- Events.create_event(object_data) do
:ok {:ok, event}
end end
end end
@ -129,8 +133,8 @@ defmodule Mobilizon.Service.ActivityPub.Utils do
when is_map(object_data) do when is_map(object_data) do
with object_data <- with object_data <-
Map.put(object_data, "preferred_username", object_data["preferredUsername"]), Map.put(object_data, "preferred_username", object_data["preferredUsername"]),
{:ok, _} <- Actors.create_group(object_data) do {:ok, %Actor{} = group} <- Actors.create_group(object_data) do
:ok {:ok, group}
end end
end end
@ -139,9 +143,9 @@ defmodule Mobilizon.Service.ActivityPub.Utils do
""" """
def insert_full_object(%{"object" => %{"type" => "Note"} = object_data}) def insert_full_object(%{"object" => %{"type" => "Note"} = object_data})
when is_map(object_data) do when is_map(object_data) do
with data <- Mobilizon.Service.ActivityPub.Converters.Comment.as_to_model_data(object_data), with data <- Converters.Comment.as_to_model_data(object_data),
{:ok, _comment} <- Events.create_comment(data) do {:ok, %Comment{} = comment} <- Events.create_comment(data) do
:ok {:ok, comment}
else else
err -> err ->
Logger.error("Error while inserting a remote comment inside database") Logger.error("Error while inserting a remote comment inside database")
@ -150,7 +154,29 @@ defmodule Mobilizon.Service.ActivityPub.Utils do
end end
end end
def insert_full_object(_), do: :ok @doc """
Inserts a full object if it is contained in an activity.
"""
def insert_full_object(%{"type" => "Flag"} = object_data)
when is_map(object_data) do
with data <- Converters.Flag.as_to_model_data(object_data),
{:ok, %Report{} = report} <- Reports.create_report(data) do
Enum.each(Users.list_moderators(), fn moderator ->
moderator
|> Mobilizon.Email.Admin.report(moderator, report)
|> Mobilizon.Mailer.deliver_later()
end)
{:ok, report}
else
err ->
Logger.error("Error while inserting a remote comment inside database")
Logger.error(inspect(err))
{:error, err}
end
end
def insert_full_object(_), do: {:ok, nil}
#### Like-related helpers #### Like-related helpers
@ -497,6 +523,24 @@ defmodule Mobilizon.Service.ActivityPub.Utils do
|> Map.merge(additional) |> Map.merge(additional)
end end
#### Flag-related helpers
@spec make_flag_data(map(), map()) :: map()
def make_flag_data(params, additional) do
object = [params.reported_actor_url] ++ params.comments_url
object = if params[:event_url], do: object ++ [params.event_url], else: object
%{
"type" => "Flag",
"id" => "#{MobilizonWeb.Endpoint.url()}/report/#{Ecto.UUID.generate()}",
"actor" => params.reporter_url,
"content" => params.content,
"object" => object,
"state" => "open"
}
|> Map.merge(additional)
end
@doc """ @doc """
Converts PEM encoded keys to a public key representation Converts PEM encoded keys to a public key representation
""" """

View File

@ -0,0 +1,30 @@
defmodule Mobilizon.Service.Admin.ActionLogService do
@moduledoc """
Module to handle action log creations
"""
alias Mobilizon.Users
alias Mobilizon.Users.User
alias Mobilizon.Actors.Actor
alias Mobilizon.Admin
alias Mobilizon.Admin.ActionLog
@doc """
Log an admin action
"""
@spec log_action(Actor.t(), String.t(), String.t()) :: {:ok, ActionLog.t()}
def log_action(%Actor{user_id: user_id, id: actor_id}, action, target) do
with %User{role: role} <- Users.get_user!(user_id),
{:role, true} <- {:role, role in [:administrator, :moderator]},
{:ok, %ActionLog{} = create_action_log} <-
Admin.create_action_log(%{
"actor_id" => actor_id,
"target_type" => to_string(target.__struct__),
"target_id" => target.id,
"action" => action,
"changes" => Map.from_struct(target) |> Map.take([:status, :uri, :content])
}) do
{:ok, create_action_log}
end
end
end

View File

@ -0,0 +1,33 @@
defmodule Mobilizon.Repo.Migrations.CreateReports do
use Ecto.Migration
alias Mobilizon.Reports.ReportStateEnum
def up do
ReportStateEnum.create_type()
create table(:reports) do
add(:content, :string)
add(:status, ReportStateEnum.type(), default: "open", null: false)
add(:uri, :string, null: false)
add(:reported_id, references(:actors, on_delete: :delete_all), null: false)
add(:reporter_id, references(:actors, on_delete: :delete_all), null: false)
add(:manager_id, references(:actors, on_delete: :delete_all), null: true)
add(:event_id, references(:events, on_delete: :delete_all), null: true)
timestamps()
end
create table(:reports_comments, primary_key: false) do
add(:report_id, references(:reports, on_delete: :delete_all), null: false)
add(:comment_id, references(:comments, on_delete: :delete_all), null: false)
end
end
def down do
drop(table(:reports_comments))
drop(table(:reports))
ReportStateEnum.drop_type()
end
end

View File

@ -0,0 +1,13 @@
defmodule Mobilizon.Repo.Migrations.CreateReportNotes do
use Ecto.Migration
def change do
create table(:report_notes) do
add(:content, :string, null: false)
add(:moderator_id, references(:actors, on_delete: :delete_all), null: false)
add(:report_id, references(:reports, on_delete: :delete_all), null: false)
timestamps()
end
end
end

View File

@ -0,0 +1,15 @@
defmodule Mobilizon.Repo.Migrations.CreateAdminActionLogs do
use Ecto.Migration
def change do
create table(:admin_action_logs) do
add(:action, :string, null: false)
add(:target_type, :string, null: false)
add(:target_id, :int, null: false)
add(:changes, :map)
add(:actor_id, references(:actors, on_delete: :nilify_all), null: false)
timestamps()
end
end
end

View File

@ -676,6 +676,29 @@ defmodule Mobilizon.Service.ActivityPub.TransmogrifierTest do
# :error = Transmogrifier.handle_incoming(data) # :error = Transmogrifier.handle_incoming(data)
# end # end
test "it accepts Flag activities" do
%Actor{url: reporter_url} = _reporter = insert(:actor)
%Actor{url: reported_url} = reported = insert(:actor)
%Comment{url: comment_url} = _comment = insert(:comment, actor: reported)
message = %{
"@context" => "https://www.w3.org/ns/activitystreams",
"cc" => [reported_url],
"object" => [reported_url, comment_url],
"type" => "Flag",
"content" => "blocked AND reported!!!",
"actor" => reporter_url
}
assert {:ok, activity, _} = Transmogrifier.handle_incoming(message)
assert activity.data["object"] == [reported_url, comment_url]
assert activity.data["content"] == "blocked AND reported!!!"
assert activity.data["actor"] == reporter_url
assert activity.data["cc"] == [reported_url]
end
end end
describe "prepare outgoing" do describe "prepare outgoing" do

View File

@ -0,0 +1,43 @@
defmodule Mobilizon.Service.Admin.ActionLogServiceTest do
@moduledoc """
Test the ActionLogService module
"""
use Mobilizon.DataCase
import Mobilizon.Service.Admin.ActionLogService
alias Mobilizon.Reports.{Report, Note}
alias Mobilizon.Admin.ActionLog
import Mobilizon.Factory
setup do
moderator_user = insert(:user, role: :moderator)
moderator_actor = insert(:actor, user: moderator_user)
{:ok, moderator: moderator_actor}
end
describe "action_log_creation" do
test "log a report update", %{moderator: moderator} do
%Report{id: _report_id} = report = insert(:report)
assert {:ok,
%ActionLog{
target_type: "Elixir.Mobilizon.Reports.Report",
target_id: report_id,
action: "update",
actor: moderator
}} = log_action(moderator, "update", report)
end
test "log the creation of a report note", %{moderator: moderator} do
%Report{} = report = insert(:report)
%Note{id: _note_id} = report = insert(:report_note, report: report)
assert {:ok,
%ActionLog{
target_type: "Elixir.Mobilizon.Reports.Note",
target_id: note_id,
action: "create",
actor: moderator
}} = log_action(moderator, "create", report)
end
end
end

View File

@ -0,0 +1,217 @@
defmodule MobilizonWeb.API.ReportTest do
use Mobilizon.DataCase
alias Mobilizon.Events.Event
alias Mobilizon.Events.Comment
alias Mobilizon.Actors.Actor
alias MobilizonWeb.API.Reports
alias Mobilizon.Reports.{Report, Note}
alias Mobilizon.Activity
alias Mobilizon.Users.User
alias Mobilizon.Users
import Mobilizon.Factory
describe "reports" do
test "creates a report on a event" do
%Actor{id: reporter_id, url: reporter_url} = insert(:actor)
%Actor{id: reported_id, url: reported_url} = reported = insert(:actor)
%Event{id: event_id, url: event_url} = _event = insert(:event, organizer_actor: reported)
comment = "This is not acceptable"
assert {:ok, %Activity{} = flag_activity, _} =
Reports.report(%{
reporter_actor_id: reporter_id,
reported_actor_id: reported_id,
report_content: comment,
event_id: event_id,
comments_ids: []
})
assert %Activity{
actor: ^reporter_url,
data: %{
"type" => "Flag",
"cc" => [],
"content" => ^comment,
"object" => [^reported_url, ^event_url],
"state" => "open"
}
} = flag_activity
end
test "creates a report on several comments" do
%Actor{id: reporter_id, url: reporter_url} = insert(:actor)
%Actor{id: reported_id, url: reported_url} = reported = insert(:actor)
%Comment{id: comment_1_id, url: comment_1_url} =
_comment_1 = insert(:comment, actor: reported)
%Comment{id: comment_2_id, url: comment_2_url} =
_comment_2 = insert(:comment, actor: reported)
comment = "This is really not acceptable"
assert {:ok, %Activity{} = flag_activity, _} =
Reports.report(%{
reporter_actor_id: reporter_id,
reported_actor_id: reported_id,
report_content: comment,
event_id: nil,
comments_ids: [comment_1_id, comment_2_id]
})
assert %Activity{
actor: ^reporter_url,
data: %{
"type" => "Flag",
"cc" => [],
"content" => ^comment,
"object" => [^reported_url, ^comment_1_url, ^comment_2_url],
"state" => "open"
}
} = flag_activity
end
test "creates a report that gets federated" do
%Actor{id: reporter_id, url: reporter_url} = insert(:actor)
%Actor{id: reported_id, url: reported_url} = reported = insert(:actor)
%Comment{id: comment_1_id, url: comment_1_url} =
_comment_1 = insert(:comment, actor: reported)
%Comment{id: comment_2_id, url: comment_2_url} =
_comment_2 = insert(:comment, actor: reported)
comment = "This is really not acceptable, remote admin I don't know"
encoded_comment = Mobilizon.Service.Formatter.html_escape(comment, "text/plain")
assert {:ok, %Activity{} = flag_activity, _} =
Reports.report(%{
reporter_actor_id: reporter_id,
reported_actor_id: reported_id,
report_content: comment,
event_id: nil,
comments_ids: [comment_1_id, comment_2_id],
forward: true
})
assert %Activity{
actor: ^reporter_url,
data: %{
"type" => "Flag",
"actor" => ^reporter_url,
"cc" => [^reported_url],
"content" => ^encoded_comment,
"object" => [^reported_url, ^comment_1_url, ^comment_2_url],
"state" => "open"
}
} = flag_activity
end
test "updates report state" do
%Actor{id: reporter_id} = insert(:actor)
%Actor{id: reported_id} = reported = insert(:actor)
%Comment{id: comment_1_id} = _comment_1 = insert(:comment, actor: reported)
assert {:ok, %Activity{} = flag_activity, %Report{id: report_id} = _report} =
Reports.report(%{
reporter_actor_id: reporter_id,
reported_actor_id: reported_id,
report_content: "This is not a nice thing",
event_id: nil,
comments_ids: [comment_1_id],
forward: true
})
%Report{} = report = Mobilizon.Reports.get_report(report_id)
%User{} = manager_user = insert(:user, role: :moderator)
%Actor{} = manager_actor = insert(:actor, user: manager_user)
{:ok, new_report} = Reports.update_report_status(manager_actor, report, :resolved)
assert new_report.status == :resolved
end
test "updates report state with not acceptable status" do
%Actor{id: reporter_id} = insert(:actor)
%Actor{id: reported_id} = reported = insert(:actor)
%Comment{id: comment_1_id} = _comment_1 = insert(:comment, actor: reported)
assert {:ok, %Activity{} = flag_activity, %Report{id: report_id} = _report} =
Reports.report(%{
reporter_actor_id: reporter_id,
reported_actor_id: reported_id,
report_content: "This is not a nice thing",
event_id: nil,
comments_ids: [comment_1_id],
forward: true
})
%Report{} = report = Mobilizon.Reports.get_report(report_id)
%Actor{} = manager_actor = insert(:actor)
{:error, "Unsupported state"} = Reports.update_report_status(manager_actor, report, :test)
end
end
describe "note reports" do
test "creates a note on a report" do
%User{} = moderator_user = insert(:user, role: :moderator)
%Actor{} = moderator_actor = insert(:actor, user: moderator_user)
%Report{} = report = insert(:report)
assert {:ok, %Note{} = _note} =
Reports.create_report_note(
report,
moderator_actor,
"I'll take care of this later today"
)
end
test "doesn't create a note on a report when not moderator" do
%User{} = user = insert(:user)
%Actor{} = actor = insert(:actor, user: user)
%Report{} = report = insert(:report)
assert {:error,
"You need to be a moderator or an administrator to create a note on a report"} =
Reports.create_report_note(report, actor, "I'll take care of this later today")
end
test "deletes a note on a report" do
%User{} = moderator_user = insert(:user, role: :moderator)
%Actor{} = moderator_actor = insert(:actor, user: moderator_user)
%Note{} = note = insert(:report_note, moderator: moderator_actor)
assert {:ok, %Note{}} = Reports.delete_report_note(note, moderator_actor)
end
test "deletes a note on a report with a different moderator actor" do
%Note{} = note = insert(:report_note)
%User{} = other_moderator_user = insert(:user, role: :moderator)
%Actor{} = other_moderator_actor = insert(:actor, user: other_moderator_user)
assert {:error, "You can only remove your own notes"} =
Reports.delete_report_note(note, other_moderator_actor)
end
test "try deletes a note on a report with a actor not moderator anymore" do
%User{} = moderator_user = insert(:user, role: :moderator)
%Actor{} = moderator_actor = insert(:actor, user: moderator_user)
%Note{} = note = insert(:report_note, moderator: moderator_actor)
Users.update_user(moderator_user, %{role: :user})
assert {:error,
"You need to be a moderator or an administrator to create a note on a report"} =
Reports.delete_report_note(note, moderator_actor)
end
end
end

View File

@ -0,0 +1,82 @@
defmodule MobilizonWeb.Resolvers.AdminResolverTest do
alias MobilizonWeb.AbsintheHelpers
use MobilizonWeb.ConnCase
import Mobilizon.Factory
alias Mobilizon.Actors.Actor
alias Mobilizon.Users.User
alias Mobilizon.Reports.{Report, Note}
describe "Resolver: List the action logs" do
@note_content "This a note on a report"
test "list_action_logs/3 list action logs", %{conn: conn} do
%User{} = user_moderator = insert(:user, role: :moderator)
%Actor{} = moderator = insert(:actor, user: user_moderator)
%User{} = user_moderator_2 = insert(:user, role: :moderator)
%Actor{} = moderator_2 = insert(:actor, user: user_moderator_2)
%Report{} = report = insert(:report)
MobilizonWeb.API.Reports.update_report_status(moderator, report, "resolved")
{:ok, %Note{} = note} =
MobilizonWeb.API.Reports.create_report_note(report, moderator_2, @note_content)
MobilizonWeb.API.Reports.delete_report_note(note, moderator_2)
query = """
{
actionLogs {
action,
actor {
preferredUsername
},
object {
... on Report {
id,
status
},
... on ReportNote {
content
}
}
}
}
"""
res =
conn
|> get("/api", AbsintheHelpers.query_skeleton(query, "actionLogs"))
assert json_response(res, 200)["errors"] |> hd |> Map.get("message") ==
"You need to be logged-in and a moderator to list action logs"
res =
conn
|> auth_conn(user_moderator)
|> get("/api", AbsintheHelpers.query_skeleton(query, "actionLogs"))
assert json_response(res, 200)["errors"] == nil
assert json_response(res, 200)["data"]["actionLogs"] |> length == 3
assert json_response(res, 200)["data"]["actionLogs"] == [
%{
"action" => "report_update_resolved",
"actor" => %{"preferredUsername" => moderator.preferred_username},
"object" => %{"id" => to_string(report.id), "status" => "RESOLVED"}
},
%{
"action" => "note_creation",
"actor" => %{"preferredUsername" => moderator_2.preferred_username},
"object" => %{"content" => @note_content}
},
%{
"action" => "note_deletion",
"actor" => %{"preferredUsername" => moderator_2.preferred_username},
"object" => %{"content" => @note_content}
}
]
end
end
end

View File

@ -0,0 +1,366 @@
defmodule MobilizonWeb.Resolvers.ReportResolverTest do
alias MobilizonWeb.AbsintheHelpers
use MobilizonWeb.ConnCase
import Mobilizon.Factory
alias Mobilizon.Actors.Actor
alias Mobilizon.Users.User
alias Mobilizon.Events.Event
alias Mobilizon.Reports.{Report, Note}
describe "Resolver: Report a content" do
test "create_report/3 creates a report", %{conn: conn} do
%User{} = user_reporter = insert(:user)
%Actor{} = reporter = insert(:actor, user: user_reporter)
%Actor{} = reported = insert(:actor)
%Event{} = event = insert(:event, organizer_actor: reported)
mutation = """
mutation {
createReport(
reporter_actor_id: #{reporter.id},
reported_actor_id: #{reported.id},
event_id: #{event.id},
report_content: "This is an issue"
) {
content,
reporter {
id
},
event {
id
},
status
}
}
"""
res =
conn
|> auth_conn(user_reporter)
|> post("/api", AbsintheHelpers.mutation_skeleton(mutation))
assert json_response(res, 200)["errors"] == nil
assert json_response(res, 200)["data"]["createReport"]["content"] == "This is an issue"
assert json_response(res, 200)["data"]["createReport"]["status"] == "OPEN"
assert json_response(res, 200)["data"]["createReport"]["event"]["id"] == event.id
assert json_response(res, 200)["data"]["createReport"]["reporter"]["id"] == reporter.id
end
test "create_report/3 without being connected doesn't create any report", %{conn: conn} do
%Actor{} = reported = insert(:actor)
mutation = """
mutation {
createReport(
reported_actor_id: #{reported.id},
reporter_actor_id: 5,
report_content: "This is an issue"
) {
content
}
}
"""
res =
conn
|> post("/api", AbsintheHelpers.mutation_skeleton(mutation))
assert json_response(res, 200)["errors"] |> hd |> Map.get("message") ==
"You need to be logged-in to create reports"
end
end
describe "Resolver: update a report" do
test "update_report/3 updates the report's status", %{conn: conn} do
%User{} = user_moderator = insert(:user, role: :moderator)
%Actor{} = moderator = insert(:actor, user: user_moderator)
%Report{} = report = insert(:report)
mutation = """
mutation {
updateReportStatus(
report_id: #{report.id},
moderator_id: #{moderator.id},
status: RESOLVED
) {
content,
reporter {
id
},
event {
id
},
status
}
}
"""
res =
conn
|> auth_conn(user_moderator)
|> post("/api", AbsintheHelpers.mutation_skeleton(mutation))
assert json_response(res, 200)["errors"] == nil
assert json_response(res, 200)["data"]["updateReportStatus"]["content"] ==
"This is problematic"
assert json_response(res, 200)["data"]["updateReportStatus"]["status"] == "RESOLVED"
assert json_response(res, 200)["data"]["updateReportStatus"]["reporter"]["id"] ==
report.reporter.id
end
test "create_report/3 without being connected doesn't create any report", %{conn: conn} do
%User{} = user_moderator = insert(:user, role: :moderator)
%Actor{} = moderator = insert(:actor, user: user_moderator)
%Report{} = report = insert(:report)
mutation = """
mutation {
updateReportStatus(
report_id: #{report.id},
moderator_id: #{moderator.id},
status: RESOLVED
) {
content
}
}
"""
res =
conn
|> post("/api", AbsintheHelpers.mutation_skeleton(mutation))
assert json_response(res, 200)["errors"] |> hd |> Map.get("message") ==
"You need to be logged-in and a moderator to update a report"
end
end
describe "Resolver: Get list of reports" do
test "get an empty list of reports", %{conn: conn} do
%User{} = user_moderator = insert(:user, role: :moderator)
query = """
{
reports {
id,
reported {
preferredUsername
}
}
}
"""
res =
conn
|> get("/api", AbsintheHelpers.query_skeleton(query, "report"))
assert json_response(res, 200)["errors"] |> hd |> Map.get("message") ==
"You need to be logged-in and a moderator to list reports"
res =
conn
|> auth_conn(user_moderator)
|> get("/api", AbsintheHelpers.query_skeleton(query, "report"))
assert json_response(res, 200)["errors"] == nil
assert json_response(res, 200)["data"]["reports"] == []
end
test "get a list of reports", %{conn: conn} do
%User{} = user_moderator = insert(:user, role: :moderator)
%Report{id: report_1_id} = insert(:report)
%Report{id: report_2_id} = insert(:report)
%Report{id: report_3_id} = insert(:report)
query = """
{
reports {
id,
reported {
preferredUsername
}
}
}
"""
res =
conn
|> auth_conn(user_moderator)
|> get("/api", AbsintheHelpers.query_skeleton(query, "report"))
assert json_response(res, 200)["errors"] == nil
assert json_response(res, 200)["data"]["reports"]
|> Enum.map(fn report -> Map.get(report, "id") end) ==
Enum.map([report_1_id, report_2_id, report_3_id], &to_string/1)
query = """
{
reports(page: 2, limit: 2) {
id,
reported {
preferredUsername
}
}
}
"""
res =
conn
|> auth_conn(user_moderator)
|> get("/api", AbsintheHelpers.query_skeleton(query, "report"))
assert json_response(res, 200)["errors"] == nil
assert json_response(res, 200)["data"]["reports"] |> length == 1
query = """
{
reports(page: 3, limit: 2) {
id,
reported {
preferredUsername
}
}
}
"""
res =
conn
|> auth_conn(user_moderator)
|> get("/api", AbsintheHelpers.query_skeleton(query, "report"))
assert json_response(res, 200)["errors"] == nil
assert json_response(res, 200)["data"]["reports"] |> length == 0
end
end
describe "Resolver: View a report" do
test "view a report", %{conn: conn} do
%User{} = user_moderator = insert(:user, role: :moderator)
%Actor{} = insert(:actor, user: user_moderator)
%Actor{} = reporter = insert(:actor)
%Report{} = report = insert(:report, reporter: reporter)
query = """
{
report (id: "#{report.id}") {
id,
reported {
preferredUsername
},
reporter {
preferredUsername
},
event {
title
},
comments {
text
},
content
}
}
"""
res =
conn
|> get("/api", AbsintheHelpers.query_skeleton(query, "report"))
assert json_response(res, 200)["errors"] |> hd |> Map.get("message") ==
"You need to be logged-in and a moderator to view a report"
res =
conn
|> auth_conn(user_moderator)
|> get("/api", AbsintheHelpers.query_skeleton(query, "report"))
assert json_response(res, 200)["errors"] == nil
assert json_response(res, 200)["data"]["report"]["reported"]["preferredUsername"] ==
report.reported.preferred_username
assert json_response(res, 200)["data"]["report"]["reporter"]["preferredUsername"] ==
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"]["comments"] |> hd |> Map.get("text") ==
report.comments |> hd |> Map.get(:text)
end
end
describe "Resolver: Add a note on a report" do
@report_note_content "I agree with this this report"
test "create_report_note/3 creates a report note", %{conn: conn} do
%User{} = user_moderator = insert(:user, role: :moderator)
%Actor{id: moderator_id} = moderator = insert(:actor, user: user_moderator)
%Report{id: report_id} = insert(:report)
mutation = """
mutation {
createReportNote(
moderator_id: #{moderator_id},
report_id: #{report_id},
content: "#{@report_note_content}"
) {
content,
moderator {
preferred_username
},
report {
id
}
}
}
"""
res =
conn
|> auth_conn(user_moderator)
|> post("/api", AbsintheHelpers.mutation_skeleton(mutation))
assert json_response(res, 200)["errors"] == nil
assert json_response(res, 200)["data"]["createReportNote"]["content"] ==
@report_note_content
assert json_response(res, 200)["data"]["createReportNote"]["moderator"][
"preferred_username"
] == moderator.preferred_username
assert json_response(res, 200)["data"]["createReportNote"]["report"]["id"] ==
to_string(report_id)
end
test "delete_report_note deletes a report note", %{conn: conn} do
%User{} = user_moderator = insert(:user, role: :moderator)
%Actor{id: moderator_id} = moderator = insert(:actor, user: user_moderator)
%Note{id: report_note_id} = insert(:report_note, moderator: moderator)
mutation = """
mutation {
deleteReportNote(
moderator_id: #{moderator_id},
note_id: #{report_note_id},
) {
id
}
}
"""
res =
conn
|> auth_conn(user_moderator)
|> post("/api", AbsintheHelpers.mutation_skeleton(mutation))
assert json_response(res, 200)["errors"] == nil
assert json_response(res, 200)["data"]["deleteReportNote"]["id"] == report_note_id
end
end
end

View File

@ -202,4 +202,24 @@ defmodule Mobilizon.Factory do
actor: build(:actor) actor: build(:actor)
} }
end end
def report_factory do
%Mobilizon.Reports.Report{
content: "This is problematic",
status: :open,
uri: "http://mobilizon.test/report/deae1020-54b8-47df-9eea-d8c0e943e57f/activity",
reported: build(:actor),
reporter: build(:actor),
event: build(:event),
comments: build_list(1, :comment)
}
end
def report_note_factory do
%Mobilizon.Reports.Note{
content: "My opinion",
moderator: build(:actor),
report: build(:report)
}
end
end end