who_need_help/lib/who_need_help/content_removal.ex

266 lines
7.7 KiB
Elixir

defmodule WhoNeedHelp.ContentRemoval do
@moduledoc "Separate intake and review queue for electronic content-removal notices."
import Ecto.Query
alias WhoNeedHelp.Accounts
alias WhoNeedHelp.Accounts.{Scope, User}
alias WhoNeedHelp.ContentRemoval.{Notice, Notifier}
alias WhoNeedHelp.Pagination
alias WhoNeedHelp.Repo
alias WhoNeedHelp.Trust
alias WhoNeedHelp.Trust.RateLimiter
@access_salt "content-removal-access"
@urgent_categories [
:non_consensual_intimate_media,
:child_sexual_abuse_material,
:threat_to_life_or_safety
]
def change_notice(%Notice{} = notice, attrs \\ %{}) do
Notice.submission_changeset(notice, attrs)
end
def create_notice(scope, regime, attrs) when regime in [:general, :dsa, :take_it_down] do
user = scope_user(scope)
attrs = normalize_keys(attrs)
attrs = maybe_use_user_email(attrs, user)
category = enum_value(Notice, :category, attrs["category"])
status = if category in @urgent_categories, do: :urgent_review, else: :open
response_due_at =
if regime == :take_it_down, do: DateTime.add(DateTime.utc_now(:second), 48, :hour)
with {:ok, _limit} <-
RateLimiter.check(:content_removal_notice, rate_scope(user, attrs["contact_email"])) do
Repo.transact(fn ->
with {:ok, notice} <-
%Notice{
reference: unique_reference(),
regime: regime,
status: status,
response_due_at: response_due_at,
requester_id: user && user.id,
contact_verified_at: user && DateTime.utc_now(:second)
}
|> Notice.submission_changeset(attrs)
|> Repo.insert(),
{:ok, _audit} <-
Trust.audit(
user && user.id,
"content_removal_notice.created",
"content_removal_notice",
notice.id,
%{
"category" => to_string(notice.category),
"regime" => to_string(notice.regime),
"status" => to_string(notice.status)
}
) do
{:ok, notice}
end
end)
|> notify_received()
end
end
def list_for_user(%Scope{user: %User{id: user_id}}) do
Notice
|> where([notice], notice.requester_id == ^user_id)
|> order_by([notice], desc: notice.inserted_at, desc: notice.id)
|> Repo.all()
end
def get_for_user(%Scope{user: %User{id: user_id}}, id) do
with {:ok, id} <- Ecto.UUID.cast(id),
%Notice{} = notice <- Repo.get_by(Notice, id: id, requester_id: user_id) do
{:ok, notice}
else
_ -> {:error, :not_found}
end
end
def get_by_access_token(id, token) when is_binary(token) do
with {:ok, id} <- Ecto.UUID.cast(id),
{:ok, ^id} <-
Phoenix.Token.verify(WhoNeedHelpWeb.Endpoint, @access_salt, token, max_age: 31_536_000),
%Notice{} = notice <- Repo.get(Notice, id) do
verify_contact(notice)
else
_ -> {:error, :not_found}
end
end
def get_by_access_token(_id, _token), do: {:error, :not_found}
def access_token(%Notice{id: id}) do
Phoenix.Token.sign(WhoNeedHelpWeb.Endpoint, @access_salt, id)
end
def status_url(%Notice{} = notice) do
token = access_token(notice)
WhoNeedHelpWeb.Endpoint.url() <>
"/legal/content-removal/#{notice.id}?token=#{URI.encode_www_form(token)}"
end
def paginate_for_staff(%Scope{user: user}, options \\ []) do
if Accounts.moderator_authorized?(user) do
limit = Pagination.limit(options)
cursor = Pagination.cursor(options)
Notice
|> maybe_regime(Keyword.get(options, :regime))
|> before(cursor)
|> order_by([notice], desc: notice.inserted_at, desc: notice.id)
|> limit(^(limit + 1))
|> preload([:requester, :reviewed_by])
|> Repo.all()
|> Pagination.page(limit, &{&1.inserted_at, &1.id})
else
%Pagination.Page{}
end
end
def moderate(%Scope{user: moderator}, id, attrs) do
with {:ok, id} <- Ecto.UUID.cast(id),
true <- Accounts.moderator_authorized?(moderator) do
attrs =
attrs
|> normalize_keys()
|> Map.merge(%{
"reviewed_at" => DateTime.utc_now(:second),
"reviewed_by_id" => moderator.id
})
Repo.transact(fn ->
notice =
Notice
|> where([notice], notice.id == ^id)
|> lock("FOR UPDATE")
|> Repo.one()
if notice do
with {:ok, notice} <- notice |> Notice.moderation_changeset(attrs) |> Repo.update(),
{:ok, _audit} <-
Trust.audit(
moderator.id,
"content_removal_notice.moderated",
"content_removal_notice",
notice.id,
%{
"status" => to_string(notice.status)
}
) do
{:ok, notice}
end
else
{:error, :not_found}
end
end)
|> notify_decision()
else
false -> {:error, :forbidden}
_ -> {:error, :not_found}
end
end
defp notify_received({:ok, %Notice{contact_email: email} = notice}) when email in [nil, ""],
do: notify_operator(notice)
defp notify_received({:ok, notice}) do
result =
case Notifier.deliver_received(notice, status_url(notice)) do
{:ok, _metadata} ->
notice
|> Ecto.Changeset.change(acknowledgement_sent_at: DateTime.utc_now(:second))
|> Repo.update()
_error ->
{:ok, notice}
end
case result do
{:ok, current} -> notify_operator(current)
error -> error
end
end
defp notify_received(result), do: result
defp notify_operator(notice) do
_ = Notifier.deliver_operator_alert(notice)
{:ok, notice}
end
defp notify_decision({:ok, %Notice{contact_verified_at: nil} = notice}), do: {:ok, notice}
defp notify_decision({:ok, notice}) do
case Notifier.deliver_decision(notice, status_url(notice)) do
{:ok, _metadata} ->
notice
|> Ecto.Changeset.change(decision_sent_at: DateTime.utc_now(:second))
|> Repo.update()
_error ->
{:ok, notice}
end
end
defp notify_decision(result), do: result
defp verify_contact(%Notice{contact_verified_at: nil} = notice) do
notice
|> Ecto.Changeset.change(contact_verified_at: DateTime.utc_now(:second))
|> Repo.update()
end
defp verify_contact(%Notice{} = notice), do: {:ok, notice}
defp before(query, nil), do: query
defp before(query, {inserted_at, id}) do
where(
query,
[notice],
notice.inserted_at < ^inserted_at or
(notice.inserted_at == ^inserted_at and notice.id < ^id)
)
end
defp maybe_regime(query, nil), do: query
defp maybe_regime(query, regime), do: where(query, [notice], notice.regime == ^regime)
defp maybe_use_user_email(attrs, %User{email: email}) do
Map.put(attrs, "contact_email", email)
end
defp maybe_use_user_email(attrs, nil), do: attrs
defp rate_scope(%User{id: id}, _email), do: "user:#{id}"
defp rate_scope(nil, email) when is_binary(email),
do: "email:#{String.downcase(String.trim(email))}"
defp rate_scope(nil, _email), do: "missing-email"
defp scope_user(%Scope{user: %User{} = user}), do: user
defp scope_user(_scope), do: nil
defp enum_value(module, field, value) do
Ecto.Enum.cast_value(module, field, value)
|> case do
{:ok, enum} -> enum
:error -> nil
end
end
defp unique_reference do
suffix = 8 |> :crypto.strong_rand_bytes() |> Base.encode32(case: :upper, padding: false)
"REM-#{suffix}"
end
defp normalize_keys(attrs), do: Map.new(attrs, fn {key, value} -> {to_string(key), value} end)
end