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: +

    +

    + """ + 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