Merge branch 'feature/reports' into 'master'
Introduce backend for reports See merge request framasoft/mobilizon!159
This commit is contained in:
commit
5846e97951
@ -13,12 +13,15 @@ config :mobilizon, :instance,
|
||||
name: System.get_env("MOBILIZON_INSTANCE_NAME") || "Localhost",
|
||||
description: System.get_env("MOBILIZON_INSTANCE_DESCRIPTION") || "This is a Mobilizon instance",
|
||||
version: "1.0.0-dev",
|
||||
hostname: System.get_env("MOBILIZON_INSTANCE_HOST") || "localhost",
|
||||
registrations_open: System.get_env("MOBILIZON_INSTANCE_REGISTRATIONS_OPEN") || false,
|
||||
repository: Mix.Project.config()[:source_url],
|
||||
remote_limit: 100_000,
|
||||
upload_limit: 16_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, %{
|
||||
"application/activity+json" => ["activity-json"],
|
||||
@ -30,10 +33,7 @@ config :mobilizon, MobilizonWeb.Endpoint,
|
||||
url: [host: "localhost"],
|
||||
secret_key_base: "1yOazsoE0Wqu4kXk3uC5gu3jDbShOimTCzyFL3OjCdBmOXMyHX87Qmf3+Tu9s0iM",
|
||||
render_errors: [view: MobilizonWeb.ErrorView, accepts: ~w(html json)],
|
||||
pubsub: [name: Mobilizon.PubSub, adapter: Phoenix.PubSub.PG2],
|
||||
instance: "localhost",
|
||||
email_from: "noreply@localhost",
|
||||
email_to: "noreply@localhost"
|
||||
pubsub: [name: Mobilizon.PubSub, adapter: Phoenix.PubSub.PG2]
|
||||
|
||||
# Upload configuration
|
||||
config :mobilizon, MobilizonWeb.Upload,
|
||||
|
@ -35,6 +35,8 @@ defmodule Mobilizon.Actors.Actor do
|
||||
alias Mobilizon.Events.{Event, FeedToken}
|
||||
alias Mobilizon.Media.File
|
||||
|
||||
alias Mobilizon.Reports.{Report, Note}
|
||||
|
||||
alias MobilizonWeb.Router.Helpers, as: Routes
|
||||
alias MobilizonWeb.Endpoint
|
||||
|
||||
@ -72,6 +74,9 @@ defmodule Mobilizon.Actors.Actor do
|
||||
has_many(:feed_tokens, FeedToken, foreign_key: :actor_id)
|
||||
embeds_one(:avatar, 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()
|
||||
end
|
||||
|
48
lib/mobilizon/admin.ex
Normal file
48
lib/mobilizon/admin.ex
Normal 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
|
27
lib/mobilizon/admin/action_log.ex
Normal file
27
lib/mobilizon/admin/action_log.ex
Normal 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
|
@ -19,7 +19,12 @@ defmodule Mobilizon.CommonConfig do
|
||||
|> get_in([:description])
|
||||
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"
|
||||
|
||||
|
38
lib/mobilizon/email/admin.ex
Normal file
38
lib/mobilizon/email/admin.ex
Normal 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
|
@ -18,7 +18,7 @@ defmodule Mobilizon.Email.User do
|
||||
|> subject(
|
||||
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(:instance, instance_url)
|
||||
|> render(:registration_confirmation)
|
||||
@ -26,7 +26,7 @@ defmodule Mobilizon.Email.User do
|
||||
|
||||
def reset_password_email(%User{} = user, locale \\ "en") do
|
||||
Gettext.put_locale(locale)
|
||||
instance_url = get_config(:instance)
|
||||
instance_url = get_config(:hostname)
|
||||
|
||||
base_email()
|
||||
|> to(user.email)
|
||||
@ -36,7 +36,7 @@ defmodule Mobilizon.Email.User do
|
||||
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(:instance, instance_url)
|
||||
|> render(:password_reset)
|
||||
@ -45,13 +45,13 @@ defmodule Mobilizon.Email.User do
|
||||
defp base_email do
|
||||
# Here you can set a default from, default headers, etc.
|
||||
new_email()
|
||||
|> from(Application.get_env(:mobilizon, MobilizonWeb.Endpoint)[:email_from])
|
||||
|> 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
|
||||
_config = Application.get_env(:mobilizon, MobilizonWeb.Endpoint) |> Keyword.get(key)
|
||||
Mobilizon.CommonConfig.instance_config() |> Keyword.get(key)
|
||||
end
|
||||
end
|
||||
|
@ -1160,6 +1160,19 @@ defmodule Mobilizon.Events do
|
||||
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 """
|
||||
Creates a comment.
|
||||
|
||||
|
236
lib/mobilizon/reports.ex
Normal file
236
lib/mobilizon/reports.ex
Normal 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
|
27
lib/mobilizon/reports/note.ex
Normal file
27
lib/mobilizon/reports/note.ex
Normal 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
|
59
lib/mobilizon/reports/report.ex
Normal file
59
lib/mobilizon/reports/report.ex
Normal 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
|
@ -258,6 +258,36 @@ defmodule Mobilizon.Users do
|
||||
)
|
||||
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
|
||||
Repo.one(
|
||||
from(
|
||||
|
@ -26,7 +26,7 @@ defmodule MobilizonWeb.API.Events do
|
||||
Actors.get_local_actor_with_everything(organizer_actor_id),
|
||||
title <- String.trim(title),
|
||||
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)),
|
||||
tags <- Formatter.parse_tags(description),
|
||||
picture <- Map.get(args, :picture, nil),
|
||||
|
128
lib/mobilizon_web/api/reports.ex
Normal file
128
lib/mobilizon_web/api/reports.ex
Normal 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
|
@ -120,4 +120,16 @@ defmodule MobilizonWeb.API.Utils do
|
||||
# |> Formatter.add_hashtag_links(tags)
|
||||
# |> Formatter.finalize()
|
||||
# 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
|
||||
|
72
lib/mobilizon_web/resolvers/admin.ex
Normal file
72
lib/mobilizon_web/resolvers/admin.ex
Normal 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
|
118
lib/mobilizon_web/resolvers/report.ex
Normal file
118
lib/mobilizon_web/resolvers/report.ex
Normal 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
|
@ -21,6 +21,8 @@ defmodule MobilizonWeb.Schema do
|
||||
import_types(MobilizonWeb.Schema.CommentType)
|
||||
import_types(MobilizonWeb.Schema.SearchType)
|
||||
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"
|
||||
object :deleted_object do
|
||||
@ -109,6 +111,8 @@ defmodule MobilizonWeb.Schema do
|
||||
import_fields(:address_queries)
|
||||
import_fields(:config_queries)
|
||||
import_fields(:picture_queries)
|
||||
import_fields(:report_queries)
|
||||
import_fields(:admin_queries)
|
||||
end
|
||||
|
||||
@desc """
|
||||
@ -124,5 +128,6 @@ defmodule MobilizonWeb.Schema do
|
||||
import_fields(:member_mutations)
|
||||
import_fields(:feed_token_mutations)
|
||||
import_fields(:picture_mutations)
|
||||
import_fields(:report_mutations)
|
||||
end
|
||||
end
|
||||
|
41
lib/mobilizon_web/schema/admin.ex
Normal file
41
lib/mobilizon_web/schema/admin.ex
Normal 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
|
86
lib/mobilizon_web/schema/report.ex
Normal file
86
lib/mobilizon_web/schema/report.ex
Normal 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
|
15
lib/mobilizon_web/templates/email/report.html.eex
Normal file
15
lib/mobilizon_web/templates/email/report.html.eex
Normal 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>
|
19
lib/mobilizon_web/templates/email/report.text.eex
Normal file
19
lib/mobilizon_web/templates/email/report.text.eex
Normal 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" %>
|
||||
|
||||
|
@ -41,7 +41,7 @@ defmodule Mobilizon.Service.ActivityPub do
|
||||
@spec insert(map(), boolean()) :: {:ok, %Activity{}} | {:error, any()}
|
||||
def insert(map, local \\ true) when is_map(map) do
|
||||
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"]
|
||||
|
||||
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)
|
||||
# stream_out(activity)
|
||||
{:ok, activity}
|
||||
{:ok, activity, object}
|
||||
else
|
||||
%Activity{} = activity -> {:ok, activity}
|
||||
error -> {:error, error}
|
||||
@ -130,7 +130,7 @@ defmodule Mobilizon.Service.ActivityPub do
|
||||
additional
|
||||
),
|
||||
: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, actor} <- Actors.increase_event_count(actor) do
|
||||
{:ok, activity}
|
||||
@ -147,7 +147,7 @@ defmodule Mobilizon.Service.ActivityPub do
|
||||
local = !(params[:local] == false)
|
||||
|
||||
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, activity}
|
||||
end
|
||||
@ -164,7 +164,7 @@ defmodule Mobilizon.Service.ActivityPub do
|
||||
"actor" => actor,
|
||||
"object" => object
|
||||
},
|
||||
{:ok, activity} <- insert(data, local),
|
||||
{:ok, activity, _object} <- insert(data, local),
|
||||
:ok <- maybe_federate(activity) do
|
||||
{:ok, activity}
|
||||
end
|
||||
@ -179,7 +179,7 @@ defmodule Mobilizon.Service.ActivityPub do
|
||||
# ) do
|
||||
# with nil <- get_existing_like(url, object),
|
||||
# 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 <- maybe_federate(activity) do
|
||||
# {:ok, activity, object}
|
||||
@ -197,7 +197,7 @@ defmodule Mobilizon.Service.ActivityPub do
|
||||
# ) do
|
||||
# with %Activity{} = like_activity <- get_existing_like(actor.ap_id, object),
|
||||
# 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, object} <- remove_like_from_object(like_activity, object),
|
||||
# :ok <- maybe_federate(unlike_activity) do
|
||||
@ -215,7 +215,7 @@ defmodule Mobilizon.Service.ActivityPub do
|
||||
# ) do
|
||||
# #with true <- is_public?(object),
|
||||
# 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 <- maybe_federate(activity) do
|
||||
# {:ok, activity, object}
|
||||
@ -232,7 +232,7 @@ defmodule Mobilizon.Service.ActivityPub do
|
||||
# ) do
|
||||
# with %Activity{} = announce_activity <- get_existing_announce(actor.ap_id, object),
|
||||
# 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, _activity} <- Repo.delete(announce_activity),
|
||||
# {:ok, object} <- remove_announce_from_object(announce_activity, object) do
|
||||
@ -250,7 +250,7 @@ defmodule Mobilizon.Service.ActivityPub do
|
||||
activity_follow_id <-
|
||||
activity_id || "#{MobilizonWeb.Endpoint.url()}/follow/#{follow_id}/activity",
|
||||
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, activity}
|
||||
else
|
||||
@ -267,9 +267,9 @@ defmodule Mobilizon.Service.ActivityPub do
|
||||
with {:ok, %Follower{id: follow_id}} <- Actor.unfollow(followed, follower),
|
||||
# We recreate the follow activity
|
||||
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),
|
||||
{:ok, activity} <- insert(unfollow_data, local),
|
||||
{:ok, activity, _object} <- insert(unfollow_data, local),
|
||||
:ok <- maybe_federate(activity) do
|
||||
{:ok, activity}
|
||||
else
|
||||
@ -290,7 +290,7 @@ defmodule Mobilizon.Service.ActivityPub do
|
||||
}
|
||||
|
||||
with Events.delete_event(event),
|
||||
{:ok, activity} <- insert(data, local),
|
||||
{:ok, activity, _object} <- insert(data, local),
|
||||
:ok <- maybe_federate(activity) do
|
||||
{:ok, activity}
|
||||
end
|
||||
@ -305,7 +305,7 @@ defmodule Mobilizon.Service.ActivityPub do
|
||||
}
|
||||
|
||||
with Events.delete_comment(comment),
|
||||
{:ok, activity} <- insert(data, local),
|
||||
{:ok, activity, _object} <- insert(data, local),
|
||||
:ok <- maybe_federate(activity) do
|
||||
{:ok, activity}
|
||||
end
|
||||
@ -320,12 +320,33 @@ defmodule Mobilizon.Service.ActivityPub do
|
||||
}
|
||||
|
||||
with Actors.delete_actor(actor),
|
||||
{:ok, activity} <- insert(data, local),
|
||||
{:ok, activity, _object} <- insert(data, local),
|
||||
:ok <- maybe_federate(activity) do
|
||||
{:ok, activity}
|
||||
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 """
|
||||
Create an actor locally by it's URL (AP ID)
|
||||
"""
|
||||
|
85
lib/service/activity_pub/converters/flag.ex
Normal file
85
lib/service/activity_pub/converters/flag.ex
Normal 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
|
@ -116,6 +116,22 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
|
||||
|> Map.put("tag", combined)
|
||||
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
|
||||
# - tags
|
||||
# - emoji
|
||||
|
@ -18,6 +18,10 @@ defmodule Mobilizon.Service.ActivityPub.Utils do
|
||||
alias Mobilizon.Media.Picture
|
||||
alias Mobilizon.Events
|
||||
alias Mobilizon.Activity
|
||||
alias Mobilizon.Reports
|
||||
alias Mobilizon.Reports.Report
|
||||
alias Mobilizon.Users
|
||||
alias Mobilizon.Service.ActivityPub.Converters
|
||||
alias Ecto.Changeset
|
||||
require Logger
|
||||
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})
|
||||
when is_map(object_data) do
|
||||
with object_data <-
|
||||
Mobilizon.Service.ActivityPub.Converters.Event.as_to_model_data(object_data),
|
||||
{:ok, _} <- Events.create_event(object_data) do
|
||||
:ok
|
||||
Converters.Event.as_to_model_data(object_data),
|
||||
{:ok, %Event{} = event} <- Events.create_event(object_data) do
|
||||
{:ok, event}
|
||||
end
|
||||
end
|
||||
|
||||
@ -129,8 +133,8 @@ defmodule Mobilizon.Service.ActivityPub.Utils do
|
||||
when is_map(object_data) do
|
||||
with object_data <-
|
||||
Map.put(object_data, "preferred_username", object_data["preferredUsername"]),
|
||||
{:ok, _} <- Actors.create_group(object_data) do
|
||||
:ok
|
||||
{:ok, %Actor{} = group} <- Actors.create_group(object_data) do
|
||||
{:ok, group}
|
||||
end
|
||||
end
|
||||
|
||||
@ -139,9 +143,9 @@ defmodule Mobilizon.Service.ActivityPub.Utils do
|
||||
"""
|
||||
def insert_full_object(%{"object" => %{"type" => "Note"} = object_data})
|
||||
when is_map(object_data) do
|
||||
with data <- Mobilizon.Service.ActivityPub.Converters.Comment.as_to_model_data(object_data),
|
||||
{:ok, _comment} <- Events.create_comment(data) do
|
||||
:ok
|
||||
with data <- Converters.Comment.as_to_model_data(object_data),
|
||||
{:ok, %Comment{} = comment} <- Events.create_comment(data) do
|
||||
{:ok, comment}
|
||||
else
|
||||
err ->
|
||||
Logger.error("Error while inserting a remote comment inside database")
|
||||
@ -150,7 +154,29 @@ defmodule Mobilizon.Service.ActivityPub.Utils do
|
||||
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
|
||||
|
||||
@ -497,6 +523,24 @@ defmodule Mobilizon.Service.ActivityPub.Utils do
|
||||
|> Map.merge(additional)
|
||||
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 """
|
||||
Converts PEM encoded keys to a public key representation
|
||||
"""
|
||||
|
30
lib/service/admin/action_log_service.ex
Normal file
30
lib/service/admin/action_log_service.ex
Normal 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
|
33
priv/repo/migrations/20190712125833_create_reports.exs
Normal file
33
priv/repo/migrations/20190712125833_create_reports.exs
Normal 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
|
13
priv/repo/migrations/20190712132438_create_report_notes.exs
Normal file
13
priv/repo/migrations/20190712132438_create_report_notes.exs
Normal 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
|
@ -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
|
@ -676,6 +676,29 @@ defmodule Mobilizon.Service.ActivityPub.TransmogrifierTest do
|
||||
|
||||
# :error = Transmogrifier.handle_incoming(data)
|
||||
# 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
|
||||
|
||||
describe "prepare outgoing" do
|
||||
|
43
test/mobilizon/service/admin/action_log_service_test.exs
Normal file
43
test/mobilizon/service/admin/action_log_service_test.exs
Normal 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
|
217
test/mobilizon_web/api/report_test.exs
Normal file
217
test/mobilizon_web/api/report_test.exs
Normal 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
|
82
test/mobilizon_web/resolvers/admin_resolver_test.exs
Normal file
82
test/mobilizon_web/resolvers/admin_resolver_test.exs
Normal 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
|
366
test/mobilizon_web/resolvers/report_resolver_test.exs
Normal file
366
test/mobilizon_web/resolvers/report_resolver_test.exs
Normal 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
|
@ -202,4 +202,24 @@ defmodule Mobilizon.Factory do
|
||||
actor: build(:actor)
|
||||
}
|
||||
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
|
||||
|
Loading…
Reference in New Issue
Block a user