defmodule GitGudWeb.ModerationActions do
@moduledoc """
The moderation actions shared by the org and repo moderation pages.
Plain functions over the socket rather than a macro: both pages
handle the same five events, and duplicating the bodies is how two
scopes quietly grow different rules about who may do what.
Every function expects `:scope` (an `%Organization{}` or
`%Repository{}`) and `:moderator` in the assigns, plus a reload
callback the page provides to re-read its lists.
Bans carry their own audit trail — who banned whom, when, why, and
who lifted it are all on the row, and lifted rows are kept. There's
no separate log to consult.
"""
import Phoenix.LiveView, only: [put_flash: 3]
alias GitGud.Accounts
alias GitGud.Moderation
alias GitGud.Reports
@doc "Dismiss a report as needing nothing."
def dismiss_report(socket, id, reload) do
with {:ok, report} <- scoped_report(socket, id) do
{:ok, _} = Reports.dismiss(report, socket.assigns.moderator)
socket |> reload.() |> put_flash(:info, "Report dismissed.")
else
_ -> put_flash(socket, :error, "That report isn't yours to act on.")
end
end
@doc "Mark a report handled without changing the content."
def action_report(socket, id, reload) do
with {:ok, report} <- scoped_report(socket, id) do
{:ok, _} = Reports.action(report, socket.assigns.moderator)
socket |> reload.() |> put_flash(:info, "Report marked handled.")
else
_ -> put_flash(socket, :error, "That report isn't yours to act on.")
end
end
@doc """
Replace reported content with a moderation message, and close the
report.
The original is kept and stays visible to admins and its author —
this hides it from everyone else rather than destroying it.
"""
def moderate_report(socket, id, reload) do
moderator = socket.assigns.moderator
with {:ok, report} <- scoped_report(socket, id),
{:ok, _} <- Reports.moderate_target(report, moderator, nil) do
{:ok, _} = Reports.action(report, moderator, "content replaced with mod message")
socket |> reload.() |> put_flash(:info, "Content replaced.")
else
_ -> put_flash(socket, :error, "Could not moderate that content.")
end
end
@doc "Ban the author of a reported item, straight from the queue."
def ban_reported_author(socket, id, reload) do
with {:ok, report} <- scoped_report(socket, id),
%{author_id: author_id} when is_integer(author_id) <- Reports.preview(report),
%{} = user <- GitGud.Repo.get(GitGud.Accounts.User, author_id) do
do_ban(socket, user, nil, nil, reload)
else
_ -> put_flash(socket, :error, "That report has no local author to ban.")
end
end
@doc "Ban someone by handle."
def ban_user(socket, params, reload) do
handle = String.trim(params["handle"] || "")
case Accounts.get_user_by_handle(handle) do
nil ->
put_flash(socket, :error, "No such user.")
user ->
do_ban(
socket,
user,
blank_to_nil(params["reason"]),
parse_expiry(params["expires_on"]),
reload
)
end
end
defp do_ban(socket, user, reason, expires_at, reload) do
scope = socket.assigns.scope
moderator = socket.assigns.moderator
cond do
user.id == moderator.id ->
put_flash(socket, :error, "You can't ban yourself.")
true ->
case Moderation.ban(scope, user,
reason: reason,
expires_at: expires_at,
banned_by: moderator
) do
{:ok, _ban} ->
socket |> reload.() |> put_flash(:info, "#{user.handle} banned.")
{:error, _cs} ->
put_flash(socket, :error, "Could not ban #{user.handle} — already banned here?")
end
end
end
@doc "Lift a ban."
def lift_ban(socket, id, reload) do
ban = Moderation.get_ban(id)
if ban && in_scope?(ban, socket.assigns.scope) do
{:ok, _} = Moderation.lift(ban, socket.assigns.moderator)
socket |> reload.() |> put_flash(:info, "Ban lifted.")
else
put_flash(socket, :error, "That ban isn't yours to lift.")
end
end
# A moderator may only act on reports in their own scope; the id in
# the event is not taken on trust.
defp scoped_report(socket, id) do
report = Reports.get_report!(String.to_integer(id))
if report_in_scope?(report, socket.assigns.scope),
do: {:ok, report},
else: :error
end
defp report_in_scope?(report, %GitGud.Repositories.Repository{id: id}),
do: report.repository_id == id
defp report_in_scope?(report, %GitGud.Organizations.Organization{id: id}),
do: report.organization_id == id
defp in_scope?(ban, %GitGud.Repositories.Repository{id: id}), do: ban.repository_id == id
defp in_scope?(ban, %GitGud.Organizations.Organization{id: id}), do: ban.organization_id == id
defp blank_to_nil(nil), do: nil
defp blank_to_nil(s) when is_binary(s) do
case String.trim(s) do
"" -> nil
trimmed -> trimmed
end
end
# The form offers a date; a ban runs to the end of it.
defp parse_expiry(nil), do: nil
defp parse_expiry(""), do: nil
defp parse_expiry(date_string) do
case Date.from_iso8601(date_string) do
{:ok, date} ->
date
|> DateTime.new!(~T[23:59:59], "Etc/UTC")
|> DateTime.truncate(:second)
_ ->
nil
end
end
end
neiam /gitgud
Git Gud
public · Issues · Pulls · Labels · Forks · Compare · Actions success · Packages
⭐
Log in to mark this repository.
5.3 KiB · text
History
6280797