Reports
authorEgor <egor@kislitsyn.com>
Wed, 20 Feb 2019 16:51:25 +0000 (16:51 +0000)
committerkaniini <nenolod@gmail.com>
Wed, 20 Feb 2019 16:51:25 +0000 (16:51 +0000)
18 files changed:
config/config.exs
docs/config.md
lib/pleroma/activity.ex
lib/pleroma/emails/admin_email.ex [new file with mode: 0644]
lib/pleroma/emails/mailer.ex
lib/pleroma/user.ex
lib/pleroma/web/activity_pub/activity_pub.ex
lib/pleroma/web/activity_pub/utils.ex
lib/pleroma/web/common_api/common_api.ex
lib/pleroma/web/common_api/utils.ex
lib/pleroma/web/mastodon_api/mastodon_api_controller.ex
lib/pleroma/web/mastodon_api/views/report_view.ex [new file with mode: 0644]
lib/pleroma/web/router.ex
lib/pleroma/web/twitter_api/twitter_api.ex
priv/repo/migrations/20190123092341_users_add_is_admin_index.exs [new file with mode: 0644]
test/web/activity_pub/activity_pub_test.exs
test/web/common_api/common_api_test.exs
test/web/mastodon_api/mastodon_api_controller_test.exs

index 317299bf196cdaeec3589c2745447b212535db30..6119aaea111b3a33d322ffa6312ed4a09394d569 100644 (file)
@@ -164,7 +164,8 @@ config :pleroma, :instance,
   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.Web.Federator.RetryQueue,
 
 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.
index 6647549a254b2c4e5c53f907aeba92a0eaee71e6..14723b727ff5a014e32a279f4b7e7946123c6b6a 100644 (file)
@@ -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
index cdfe7ea9e482c9a36b332ce312801af7615a352b..66854dc2dd2920208261f4da1849420bd60c874a 100644 (file)
@@ -113,4 +113,14 @@ defmodule Pleroma.Activity do
   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 (file)
index 0000000..9b20c7e
--- /dev/null
@@ -0,0 +1,63 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# 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
+        "<p>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)
+            "<li><a href=\"#{status_url}\">#{status_url}</li>"
+          end)
+          |> Enum.join("\n")
+
+        """
+        <p> Statuses:
+          <ul>
+            #{statuses_list_html}
+          </ul>
+        </p>
+        """
+      else
+        ""
+      end
+
+    html_body = """
+    <p>Reported by: <a href="#{user_url(reporter)}">#{reporter.nickname}</a></p>
+    <p>Reported Account: <a href="#{user_url(account)}">#{account.nickname}</a></p>
+    #{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
index 8d12641f20bd91ca713bd1e9fbd4ba296955d2e9..f7e3aa78b3ccc0f7b219dfbdce46d1019c5e482f 100644 (file)
@@ -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
index 35ba4ad995246a7362a8a05cb14ca6f53610d714..c98b942ff317335e47fe05d4d44233467480ccbe 100644 (file)
@@ -273,7 +273,7 @@ defmodule Pleroma.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 @@ defmodule Pleroma.User 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
index cb8a2139e0d286ead368385b7111f393a597f352..d1ac8172e81b56da6e56d319b06716eca2ff9bda 100644 (file)
@@ -353,6 +353,31 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub 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"]
 
index 6a89374d0126d792bd37d18e8a326e60766730cd..88f4779c8abe1aad455b24a5fc3714f5ce7f481a 100644 (file)
@@ -598,4 +598,20 @@ defmodule Pleroma.Web.ActivityPub.Utils 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
index 90b208e540107bb19e5e5a0ccd757d418cc45aa0..e788337cccdfadd85838dbc2f78f37b48470b3b1 100644 (file)
@@ -243,4 +243,31 @@ defmodule Pleroma.Web.CommonAPI 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
index abdeee947516214e472e2daa70361c2789d7131d..1d3a314ce3ba69776993f7fc626551a0a687e881 100644 (file)
@@ -322,4 +322,22 @@ defmodule Pleroma.Web.CommonAPI.Utils 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
index 17b95eb44817301e8d0bd88ec27657d533edae88..60738301b592b7a3bf5ca041c16af5ec62accc48 100644 (file)
@@ -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 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController 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 (file)
index 0000000..a16e7ff
--- /dev/null
@@ -0,0 +1,14 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# 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
index 559d3aa0c280c848df8cd75171c8c4978ca402ed..357ed7843625a223fff6ad431c58311599a4aade 100644 (file)
@@ -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
index db521a3ad09599568d52dc597c2b1e469e8dc6e4..efdd0bf435ed853fd5d4640d6d4b0da02a340fc2 100644 (file)
@@ -216,7 +216,7 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI 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 (file)
index 0000000..ba6ff78
--- /dev/null
@@ -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
index 33ed17434feccb69de800cf4533239b1e773aa66..11262c5237d4da84bddea54dc41eae9c13657e46 100644 (file)
@@ -1,5 +1,5 @@
 # Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
 # SPDX-License-Identifier: AGPL-3.0-only
 
 defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
@@ -742,6 +742,37 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest 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,
index 870648fb54aa3591751879931d2dc7f69d116eee..9ba320f5924ed06ed401640abd8cec5bb528d28b 100644 (file)
@@ -190,4 +190,35 @@ defmodule Pleroma.Web.CommonAPITest 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
index 691264135e3db414036bca744bce592a3d77fb1f..3dfbc8669fb270a1fa34a6b08d3cd0c9fa4090a0 100644 (file)
@@ -1855,4 +1855,69 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest 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