Merge branch 'feature/reports' into 'develop'

Reports

Closes #83

See merge request pleroma/pleroma!701
This commit is contained in:
kaniini 2019-02-20 16:51:25 +00:00
commit 54f9e3d382
18 changed files with 347 additions and 5 deletions

View file

@ -164,7 +164,8 @@
max_pinned_statuses: 1, max_pinned_statuses: 1,
no_attachment_links: false, no_attachment_links: false,
welcome_user_nickname: nil, welcome_user_nickname: nil,
welcome_message: nil welcome_message: nil,
max_report_comment_size: 1000
config :pleroma, :markup, config :pleroma, :markup,
# XXX - unfortunately, inline images must be enabled by default right now, because # XXX - unfortunately, inline images must be enabled by default right now, because
@ -340,7 +341,8 @@
config :pleroma, Pleroma.Jobs, config :pleroma, Pleroma.Jobs,
federator_incoming: [max_jobs: 50], 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 # Import environment specific config. This must remain at the bottom
# of this file so it overrides the configuration defined above. # of this file so it overrides the configuration defined above.

View file

@ -100,6 +100,7 @@ config :pleroma, Pleroma.Mailer,
* `no_attachment_links`: Set to true to disable automatically adding attachment link text to statuses * `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_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. * `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 ## :logger
* `backends`: `:console` is used to send logs to stdout, `{ExSyslogger, :ex_syslogger}` to log to syslog * `backends`: `:console` is used to send logs to stdout, `{ExSyslogger, :ex_syslogger}` to log to syslog

View file

@ -113,4 +113,14 @@ def mastodon_notification_type(%Activity{data: %{"type" => unquote(ap_type)}}),
end end
def mastodon_notification_type(%Activity{}), do: nil 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 end

View file

@ -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

View file

@ -4,4 +4,10 @@
defmodule Pleroma.Mailer do defmodule Pleroma.Mailer do
use Swoosh.Mailer, otp_app: :pleroma 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 end

View file

@ -273,7 +273,7 @@ def try_send_confirmation_email(%User{} = user) do
Pleroma.Config.get([:instance, :account_activation_required]) do Pleroma.Config.get([:instance, :account_activation_required]) do
user user
|> Pleroma.UserEmail.account_confirmation_email() |> Pleroma.UserEmail.account_confirmation_email()
|> Pleroma.Mailer.deliver() |> Pleroma.Mailer.deliver_async()
else else
{:ok, :noop} {:ok, :noop}
end end
@ -1284,4 +1284,13 @@ def error_user(ap_id) do
inserted_at: NaiveDateTime.utc_now() inserted_at: NaiveDateTime.utc_now()
} }
end 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 end

View file

@ -353,6 +353,31 @@ def unblock(blocker, blocked, activity_id \\ nil, local \\ true) do
end end
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 def fetch_activities_for_context(context, opts \\ %{}) do
public = ["https://www.w3.org/ns/activitystreams#Public"] public = ["https://www.w3.org/ns/activitystreams#Public"]

View file

@ -598,4 +598,20 @@ def make_create_data(params, additional) do
} }
|> Map.merge(additional) |> Map.merge(additional)
end 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 end

View file

@ -243,4 +243,31 @@ def thread_muted?(user, activity) do
_ -> true _ -> true
end end
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 end

View file

@ -322,4 +322,22 @@ def maybe_extract_mentions(%{"tag" => tag}) do
end end
def maybe_extract_mentions(_), do: [] 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 end

View file

@ -24,6 +24,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
alias Pleroma.Web.MastodonAPI.MastodonView alias Pleroma.Web.MastodonAPI.MastodonView
alias Pleroma.Web.MastodonAPI.PushSubscriptionView alias Pleroma.Web.MastodonAPI.PushSubscriptionView
alias Pleroma.Web.MastodonAPI.StatusView alias Pleroma.Web.MastodonAPI.StatusView
alias Pleroma.Web.MastodonAPI.ReportView
alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.OAuth.App alias Pleroma.Web.OAuth.App
@ -1533,6 +1534,20 @@ def status_card(conn, %{"id" => status_id}) do
end end
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) def try_render(conn, target, params)
when is_binary(target) do when is_binary(target) do
res = render(conn, target, params) res = render(conn, target, params)

View file

@ -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

View file

@ -275,6 +275,8 @@ defmodule Pleroma.Web.Router do
delete("/filters/:id", MastodonAPIController, :delete_filter) delete("/filters/:id", MastodonAPIController, :delete_filter)
post("/pleroma/flavour/:flavour", MastodonAPIController, :set_flavour) post("/pleroma/flavour/:flavour", MastodonAPIController, :set_flavour)
post("/reports", MastodonAPIController, :reports)
end end
scope [] do scope [] do

View file

@ -216,7 +216,7 @@ def password_reset(nickname_or_email) do
{:ok, token_record} <- Pleroma.PasswordResetToken.create_token(user) do {:ok, token_record} <- Pleroma.PasswordResetToken.create_token(user) do
user user
|> UserEmail.password_reset_email(token_record.token) |> UserEmail.password_reset_email(token_record.token)
|> Mailer.deliver() |> Mailer.deliver_async()
else else
false -> false ->
{:error, "bad user identifier"} {:error, "bad user identifier"}

View file

@ -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

View file

@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server # 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 # SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.ActivityPubTest do defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
@ -742,6 +742,37 @@ test "returned pinned statuses" do
assert 3 = length(activities) assert 3 = length(activities)
end 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 describe "publish_one/1" do
test_with_mock "calls `Instances.set_reachable` on successful federation if `unreachable_since` is not specified", test_with_mock "calls `Instances.set_reachable` on successful federation if `unreachable_since` is not specified",
Instances, Instances,

View file

@ -190,4 +190,35 @@ test "check that mutes can't be duplicate", %{user: user, activity: activity} do
{:error, _} = CommonAPI.add_mute(user, activity) {:error, _} = CommonAPI.add_mute(user, activity)
end end
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 end

View file

@ -1855,4 +1855,69 @@ test "flavours switching (Pleroma Extension)", %{conn: conn} do
assert json_response(set_flavour, 200) == json_response(get_new_flavour, 200) assert json_response(set_flavour, 200) == json_response(get_new_flavour, 200)
end 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 end