From bff9eb5ef7ad446376f68807d4e51db5f2de9515 Mon Sep 17 00:00:00 2001
From: Egor
Date: Wed, 20 Feb 2019 16:51:25 +0000
Subject: [PATCH] Reports
---
config/config.exs | 6 +-
docs/config.md | 1 +
lib/pleroma/activity.ex | 10 +++
lib/pleroma/emails/admin_email.ex | 63 ++++++++++++++++++
lib/pleroma/emails/mailer.ex | 6 ++
lib/pleroma/user.ex | 11 +++-
lib/pleroma/web/activity_pub/activity_pub.ex | 25 +++++++
lib/pleroma/web/activity_pub/utils.ex | 16 +++++
lib/pleroma/web/common_api/common_api.ex | 27 ++++++++
lib/pleroma/web/common_api/utils.ex | 18 +++++
.../mastodon_api/mastodon_api_controller.ex | 15 +++++
.../web/mastodon_api/views/report_view.ex | 14 ++++
lib/pleroma/web/router.ex | 2 +
lib/pleroma/web/twitter_api/twitter_api.ex | 2 +-
...0190123092341_users_add_is_admin_index.exs | 7 ++
test/web/activity_pub/activity_pub_test.exs | 33 +++++++++-
test/web/common_api/common_api_test.exs | 31 +++++++++
.../mastodon_api_controller_test.exs | 65 +++++++++++++++++++
18 files changed, 347 insertions(+), 5 deletions(-)
create mode 100644 lib/pleroma/emails/admin_email.ex
create mode 100644 lib/pleroma/web/mastodon_api/views/report_view.ex
create mode 100644 priv/repo/migrations/20190123092341_users_add_is_admin_index.exs
diff --git a/config/config.exs b/config/config.exs
index 317299bf1..6119aaea1 100644
--- a/config/config.exs
+++ b/config/config.exs
@@ -164,7 +164,8 @@
max_pinned_statuses: 1,
no_attachment_links: false,
welcome_user_nickname: nil,
- welcome_message: nil
+ welcome_message: nil,
+ max_report_comment_size: 1000
config :pleroma, :markup,
# XXX - unfortunately, inline images must be enabled by default right now, because
@@ -340,7 +341,8 @@
config :pleroma, Pleroma.Jobs,
federator_incoming: [max_jobs: 50],
- federator_outgoing: [max_jobs: 50]
+ federator_outgoing: [max_jobs: 50],
+ mailer: [max_jobs: 10]
# Import environment specific config. This must remain at the bottom
# of this file so it overrides the configuration defined above.
diff --git a/docs/config.md b/docs/config.md
index 6647549a2..14723b727 100644
--- a/docs/config.md
+++ b/docs/config.md
@@ -100,6 +100,7 @@ config :pleroma, Pleroma.Mailer,
* `no_attachment_links`: Set to true to disable automatically adding attachment link text to statuses
* `welcome_message`: A message that will be send to a newly registered users as a direct message.
* `welcome_user_nickname`: The nickname of the local user that sends the welcome message.
+* `max_report_size`: The maximum size of the report comment (Default: `1000`)
## :logger
* `backends`: `:console` is used to send logs to stdout, `{ExSyslogger, :ex_syslogger}` to log to syslog
diff --git a/lib/pleroma/activity.ex b/lib/pleroma/activity.ex
index cdfe7ea9e..66854dc2d 100644
--- a/lib/pleroma/activity.ex
+++ b/lib/pleroma/activity.ex
@@ -113,4 +113,14 @@ def mastodon_notification_type(%Activity{data: %{"type" => unquote(ap_type)}}),
end
def mastodon_notification_type(%Activity{}), do: nil
+
+ def all_by_actor_and_id(actor, status_ids \\ [])
+ def all_by_actor_and_id(_actor, []), do: []
+
+ def all_by_actor_and_id(actor, status_ids) do
+ Activity
+ |> where([s], s.id in ^status_ids)
+ |> where([s], s.actor == ^actor)
+ |> Repo.all()
+ end
end
diff --git a/lib/pleroma/emails/admin_email.ex b/lib/pleroma/emails/admin_email.ex
new file mode 100644
index 000000000..9b20c7e08
--- /dev/null
+++ b/lib/pleroma/emails/admin_email.ex
@@ -0,0 +1,63 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.AdminEmail do
+ @moduledoc "Admin emails"
+
+ import Swoosh.Email
+
+ alias Pleroma.Web.Router.Helpers
+
+ defp instance_config, do: Pleroma.Config.get(:instance)
+ defp instance_name, do: instance_config()[:name]
+ defp instance_email, do: instance_config()[:email]
+
+ defp user_url(user) do
+ Helpers.o_status_url(Pleroma.Web.Endpoint, :feed_redirect, user.nickname)
+ end
+
+ def report(to, reporter, account, statuses, comment) do
+ comment_html =
+ if comment do
+ "Comment: #{comment}"
+ else
+ ""
+ end
+
+ statuses_html =
+ if length(statuses) > 0 do
+ statuses_list_html =
+ statuses
+ |> Enum.map(fn %{id: id} ->
+ status_url = Helpers.o_status_url(Pleroma.Web.Endpoint, :notice, id)
+ "
#{status_url}"
+ end)
+ |> Enum.join("\n")
+
+ """
+ Statuses:
+
+ #{statuses_list_html}
+
+
+ """
+ else
+ ""
+ end
+
+ html_body = """
+ Reported by: #{reporter.nickname}
+ Reported Account: #{account.nickname}
+ #{comment_html}
+ #{statuses_html}
+ """
+
+ new()
+ |> to({to.name, to.email})
+ |> from({instance_name(), instance_email()})
+ |> reply_to({reporter.name, reporter.email})
+ |> subject("#{instance_name()} Report")
+ |> html_body(html_body)
+ end
+end
diff --git a/lib/pleroma/emails/mailer.ex b/lib/pleroma/emails/mailer.ex
index 8d12641f2..f7e3aa78b 100644
--- a/lib/pleroma/emails/mailer.ex
+++ b/lib/pleroma/emails/mailer.ex
@@ -4,4 +4,10 @@
defmodule Pleroma.Mailer do
use Swoosh.Mailer, otp_app: :pleroma
+
+ def deliver_async(email, config \\ []) do
+ Pleroma.Jobs.enqueue(:mailer, __MODULE__, [:deliver_async, email, config])
+ end
+
+ def perform(:deliver_async, email, config), do: deliver(email, config)
end
diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex
index 35ba4ad99..c98b942ff 100644
--- a/lib/pleroma/user.ex
+++ b/lib/pleroma/user.ex
@@ -273,7 +273,7 @@ def try_send_confirmation_email(%User{} = user) do
Pleroma.Config.get([:instance, :account_activation_required]) do
user
|> Pleroma.UserEmail.account_confirmation_email()
- |> Pleroma.Mailer.deliver()
+ |> Pleroma.Mailer.deliver_async()
else
{:ok, :noop}
end
@@ -1284,4 +1284,13 @@ def error_user(ap_id) do
inserted_at: NaiveDateTime.utc_now()
}
end
+
+ def all_superusers do
+ from(
+ u in User,
+ where: u.local == true,
+ where: fragment("?->'is_admin' @> 'true' OR ?->'is_moderator' @> 'true'", u.info, u.info)
+ )
+ |> Repo.all()
+ end
end
diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex
index cb8a2139e..d1ac8172e 100644
--- a/lib/pleroma/web/activity_pub/activity_pub.ex
+++ b/lib/pleroma/web/activity_pub/activity_pub.ex
@@ -353,6 +353,31 @@ def unblock(blocker, blocked, activity_id \\ nil, local \\ true) do
end
end
+ def flag(
+ %{
+ actor: actor,
+ context: context,
+ account: account,
+ statuses: statuses,
+ content: content
+ } = params
+ ) do
+ additional = params[:additional] || %{}
+
+ # only accept false as false value
+ local = !(params[:local] == false)
+
+ %{
+ actor: actor,
+ context: context,
+ account: account,
+ statuses: statuses,
+ content: content
+ }
+ |> make_flag_data(additional)
+ |> insert(local)
+ end
+
def fetch_activities_for_context(context, opts \\ %{}) do
public = ["https://www.w3.org/ns/activitystreams#Public"]
diff --git a/lib/pleroma/web/activity_pub/utils.ex b/lib/pleroma/web/activity_pub/utils.ex
index 6a89374d0..88f4779c8 100644
--- a/lib/pleroma/web/activity_pub/utils.ex
+++ b/lib/pleroma/web/activity_pub/utils.ex
@@ -598,4 +598,20 @@ def make_create_data(params, additional) do
}
|> Map.merge(additional)
end
+
+ #### Flag-related helpers
+
+ def make_flag_data(params, additional) do
+ status_ap_ids = Enum.map(params.statuses || [], & &1.data["id"])
+ object = [params.account.ap_id] ++ status_ap_ids
+
+ %{
+ "type" => "Flag",
+ "actor" => params.actor.ap_id,
+ "content" => params.content,
+ "object" => object,
+ "context" => params.context
+ }
+ |> Map.merge(additional)
+ end
end
diff --git a/lib/pleroma/web/common_api/common_api.ex b/lib/pleroma/web/common_api/common_api.ex
index 90b208e54..e788337cc 100644
--- a/lib/pleroma/web/common_api/common_api.ex
+++ b/lib/pleroma/web/common_api/common_api.ex
@@ -243,4 +243,31 @@ def thread_muted?(user, activity) do
_ -> true
end
end
+
+ def report(user, data) do
+ with {:account_id, %{"account_id" => account_id}} <- {:account_id, data},
+ {:account, %User{} = account} <- {:account, User.get_by_id(account_id)},
+ {:ok, content_html} <- make_report_content_html(data["comment"]),
+ {:ok, statuses} <- get_report_statuses(account, data),
+ {:ok, activity} <-
+ ActivityPub.flag(%{
+ context: Utils.generate_context_id(),
+ actor: user,
+ account: account,
+ statuses: statuses,
+ content: content_html
+ }) do
+ Enum.each(User.all_superusers(), fn superuser ->
+ superuser
+ |> Pleroma.AdminEmail.report(user, account, statuses, content_html)
+ |> Pleroma.Mailer.deliver_async()
+ end)
+
+ {:ok, activity}
+ else
+ {:error, err} -> {:error, err}
+ {:account_id, %{}} -> {:error, "Valid `account_id` required"}
+ {:account, nil} -> {:error, "Account not found"}
+ end
+ end
end
diff --git a/lib/pleroma/web/common_api/utils.ex b/lib/pleroma/web/common_api/utils.ex
index abdeee947..1d3a314ce 100644
--- a/lib/pleroma/web/common_api/utils.ex
+++ b/lib/pleroma/web/common_api/utils.ex
@@ -322,4 +322,22 @@ def maybe_extract_mentions(%{"tag" => tag}) do
end
def maybe_extract_mentions(_), do: []
+
+ def make_report_content_html(nil), do: {:ok, nil}
+
+ def make_report_content_html(comment) do
+ max_size = Pleroma.Config.get([:instance, :max_report_comment_size], 1000)
+
+ if String.length(comment) <= max_size do
+ {:ok, format_input(comment, [], [], "text/plain")}
+ else
+ {:error, "Comment must be up to #{max_size} characters"}
+ end
+ end
+
+ def get_report_statuses(%User{ap_id: actor}, %{"status_ids" => status_ids}) do
+ {:ok, Activity.all_by_actor_and_id(actor, status_ids)}
+ end
+
+ def get_report_statuses(_, _), do: {:ok, nil}
end
diff --git a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex
index 17b95eb44..60738301b 100644
--- a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex
+++ b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex
@@ -24,6 +24,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
alias Pleroma.Web.MastodonAPI.MastodonView
alias Pleroma.Web.MastodonAPI.PushSubscriptionView
alias Pleroma.Web.MastodonAPI.StatusView
+ alias Pleroma.Web.MastodonAPI.ReportView
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.OAuth.App
@@ -1533,6 +1534,20 @@ def status_card(conn, %{"id" => status_id}) do
end
end
+ def reports(%{assigns: %{user: user}} = conn, params) do
+ case CommonAPI.report(user, params) do
+ {:ok, activity} ->
+ conn
+ |> put_view(ReportView)
+ |> try_render("report.json", %{activity: activity})
+
+ {:error, err} ->
+ conn
+ |> put_status(:bad_request)
+ |> json(%{error: err})
+ end
+ end
+
def try_render(conn, target, params)
when is_binary(target) do
res = render(conn, target, params)
diff --git a/lib/pleroma/web/mastodon_api/views/report_view.ex b/lib/pleroma/web/mastodon_api/views/report_view.ex
new file mode 100644
index 000000000..a16e7ff10
--- /dev/null
+++ b/lib/pleroma/web/mastodon_api/views/report_view.ex
@@ -0,0 +1,14 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.MastodonAPI.ReportView do
+ use Pleroma.Web, :view
+
+ def render("report.json", %{activity: activity}) do
+ %{
+ id: to_string(activity.id),
+ action_taken: false
+ }
+ end
+end
diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex
index 559d3aa0c..357ed7843 100644
--- a/lib/pleroma/web/router.ex
+++ b/lib/pleroma/web/router.ex
@@ -275,6 +275,8 @@ defmodule Pleroma.Web.Router do
delete("/filters/:id", MastodonAPIController, :delete_filter)
post("/pleroma/flavour/:flavour", MastodonAPIController, :set_flavour)
+
+ post("/reports", MastodonAPIController, :reports)
end
scope [] do
diff --git a/lib/pleroma/web/twitter_api/twitter_api.ex b/lib/pleroma/web/twitter_api/twitter_api.ex
index db521a3ad..efdd0bf43 100644
--- a/lib/pleroma/web/twitter_api/twitter_api.ex
+++ b/lib/pleroma/web/twitter_api/twitter_api.ex
@@ -216,7 +216,7 @@ def password_reset(nickname_or_email) do
{:ok, token_record} <- Pleroma.PasswordResetToken.create_token(user) do
user
|> UserEmail.password_reset_email(token_record.token)
- |> Mailer.deliver()
+ |> Mailer.deliver_async()
else
false ->
{:error, "bad user identifier"}
diff --git a/priv/repo/migrations/20190123092341_users_add_is_admin_index.exs b/priv/repo/migrations/20190123092341_users_add_is_admin_index.exs
new file mode 100644
index 000000000..ba6ff78b5
--- /dev/null
+++ b/priv/repo/migrations/20190123092341_users_add_is_admin_index.exs
@@ -0,0 +1,7 @@
+defmodule Pleroma.Repo.Migrations.UsersAddIsAdminIndex do
+ use Ecto.Migration
+
+ def change do
+ create(index(:users, ["(info->'is_admin')"], name: :users_is_admin_index, using: :gin))
+ end
+end
diff --git a/test/web/activity_pub/activity_pub_test.exs b/test/web/activity_pub/activity_pub_test.exs
index 33ed17434..11262c523 100644
--- a/test/web/activity_pub/activity_pub_test.exs
+++ b/test/web/activity_pub/activity_pub_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors
+# Copyright © 2017-2019 Pleroma Authors
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
@@ -742,6 +742,37 @@ test "returned pinned statuses" do
assert 3 = length(activities)
end
+ test "it can create a Flag activity" do
+ reporter = insert(:user)
+ target_account = insert(:user)
+ {:ok, activity} = CommonAPI.post(target_account, %{"status" => "foobar"})
+ context = Utils.generate_context_id()
+ content = "foobar"
+
+ reporter_ap_id = reporter.ap_id
+ target_ap_id = target_account.ap_id
+ activity_ap_id = activity.data["id"]
+
+ assert {:ok, activity} =
+ ActivityPub.flag(%{
+ actor: reporter,
+ context: context,
+ account: target_account,
+ statuses: [activity],
+ content: content
+ })
+
+ assert %Activity{
+ actor: ^reporter_ap_id,
+ data: %{
+ "type" => "Flag",
+ "content" => ^content,
+ "context" => ^context,
+ "object" => [^target_ap_id, ^activity_ap_id]
+ }
+ } = activity
+ end
+
describe "publish_one/1" do
test_with_mock "calls `Instances.set_reachable` on successful federation if `unreachable_since` is not specified",
Instances,
diff --git a/test/web/common_api/common_api_test.exs b/test/web/common_api/common_api_test.exs
index 870648fb5..9ba320f59 100644
--- a/test/web/common_api/common_api_test.exs
+++ b/test/web/common_api/common_api_test.exs
@@ -190,4 +190,35 @@ test "check that mutes can't be duplicate", %{user: user, activity: activity} do
{:error, _} = CommonAPI.add_mute(user, activity)
end
end
+
+ describe "reports" do
+ test "creates a report" do
+ reporter = insert(:user)
+ target_user = insert(:user)
+
+ {:ok, activity} = CommonAPI.post(target_user, %{"status" => "foobar"})
+
+ reporter_ap_id = reporter.ap_id
+ target_ap_id = target_user.ap_id
+ activity_ap_id = activity.data["id"]
+ comment = "foobar"
+
+ report_data = %{
+ "account_id" => target_user.id,
+ "comment" => comment,
+ "status_ids" => [activity.id]
+ }
+
+ assert {:ok, flag_activity} = CommonAPI.report(reporter, report_data)
+
+ assert %Activity{
+ actor: ^reporter_ap_id,
+ data: %{
+ "type" => "Flag",
+ "content" => ^comment,
+ "object" => [^target_ap_id, ^activity_ap_id]
+ }
+ } = flag_activity
+ end
+ end
end
diff --git a/test/web/mastodon_api/mastodon_api_controller_test.exs b/test/web/mastodon_api/mastodon_api_controller_test.exs
index 691264135..3dfbc8669 100644
--- a/test/web/mastodon_api/mastodon_api_controller_test.exs
+++ b/test/web/mastodon_api/mastodon_api_controller_test.exs
@@ -1855,4 +1855,69 @@ test "flavours switching (Pleroma Extension)", %{conn: conn} do
assert json_response(set_flavour, 200) == json_response(get_new_flavour, 200)
end
+
+ describe "reports" do
+ setup do
+ reporter = insert(:user)
+ target_user = insert(:user)
+
+ {:ok, activity} = CommonAPI.post(target_user, %{"status" => "foobar"})
+
+ [reporter: reporter, target_user: target_user, activity: activity]
+ end
+
+ test "submit a basic report", %{conn: conn, reporter: reporter, target_user: target_user} do
+ assert %{"action_taken" => false, "id" => _} =
+ conn
+ |> assign(:user, reporter)
+ |> post("/api/v1/reports", %{"account_id" => target_user.id})
+ |> json_response(200)
+ end
+
+ test "submit a report with statuses and comment", %{
+ conn: conn,
+ reporter: reporter,
+ target_user: target_user,
+ activity: activity
+ } do
+ assert %{"action_taken" => false, "id" => _} =
+ conn
+ |> assign(:user, reporter)
+ |> post("/api/v1/reports", %{
+ "account_id" => target_user.id,
+ "status_ids" => [activity.id],
+ "comment" => "bad status!"
+ })
+ |> json_response(200)
+ end
+
+ test "accound_id is required", %{
+ conn: conn,
+ reporter: reporter,
+ activity: activity
+ } do
+ assert %{"error" => "Valid `account_id` required"} =
+ conn
+ |> assign(:user, reporter)
+ |> post("/api/v1/reports", %{"status_ids" => [activity.id]})
+ |> json_response(400)
+ end
+
+ test "comment must be up to the size specified in the config", %{
+ conn: conn,
+ reporter: reporter,
+ target_user: target_user
+ } do
+ max_size = Pleroma.Config.get([:instance, :max_report_comment_size], 1000)
+ comment = String.pad_trailing("a", max_size + 1, "a")
+
+ error = %{"error" => "Comment must be up to #{max_size} characters"}
+
+ assert ^error =
+ conn
+ |> assign(:user, reporter)
+ |> post("/api/v1/reports", %{"account_id" => target_user.id, "comment" => comment})
+ |> json_response(400)
+ end
+ end
end