833 lines
24 KiB
Elixir
833 lines
24 KiB
Elixir
defmodule WhoNeedHelp.Trust do
|
|
@moduledoc "Reviews, reports, blocks, reputation, abuse signals, and auditable moderation."
|
|
|
|
import Ecto.Query
|
|
|
|
alias WhoNeedHelp.Accounts
|
|
alias WhoNeedHelp.Accounts.{Scope, User}
|
|
alias WhoNeedHelp.Activities
|
|
alias WhoNeedHelp.Activities.{Activity, Participant}
|
|
alias WhoNeedHelp.Activities.Message, as: ActivityMessage
|
|
alias WhoNeedHelp.Help
|
|
alias WhoNeedHelp.Help.{Assignment, HelpRequest}
|
|
alias WhoNeedHelp.Messaging.Message
|
|
alias WhoNeedHelp.Repo
|
|
alias WhoNeedHelp.Tracking.{Position, TrackingSession}
|
|
|
|
alias WhoNeedHelp.Trust.{
|
|
AbuseSignal,
|
|
AuditEvent,
|
|
Block,
|
|
RateLimiter,
|
|
Report,
|
|
Review
|
|
}
|
|
|
|
def authorize_action(%Scope{user: %User{} = user}, action) do
|
|
user = Repo.get(User, user.id)
|
|
|
|
cond do
|
|
not Accounts.eligible_for_trust_actions?(user) ->
|
|
{:error, :account_not_eligible}
|
|
|
|
true ->
|
|
case RateLimiter.check(action, user.id) do
|
|
{:error, :rate_limited} = error ->
|
|
create_velocity_signal_once(user.id, action)
|
|
error
|
|
|
|
result ->
|
|
result
|
|
end
|
|
end
|
|
end
|
|
|
|
def authorize_action(_, _action), do: {:error, :account_not_eligible}
|
|
|
|
def eligible?(%Scope{user: %User{id: id}}), do: Accounts.eligible_user_id?(id)
|
|
def eligible?(_scope), do: false
|
|
|
|
def submit_review(%Scope{user: user} = scope, %Assignment{} = assignment, attrs) do
|
|
attrs = stringify_keys(attrs)
|
|
|
|
request =
|
|
case assignment.request do
|
|
%HelpRequest{} = request -> request
|
|
_ -> Repo.get!(HelpRequest, assignment.request_id)
|
|
end
|
|
|
|
with {:ok, _limit} <- authorize_action(scope, :review),
|
|
true <- assignment.status == :completed,
|
|
true <- user.id in [assignment.helper_id, request.requester_id] do
|
|
reviewee_id =
|
|
if user.id == assignment.helper_id, do: request.requester_id, else: assignment.helper_id
|
|
|
|
Repo.transact(fn ->
|
|
with {:ok, review} <-
|
|
%Review{}
|
|
|> Review.changeset(
|
|
Map.merge(attrs, %{
|
|
"assignment_id" => assignment.id,
|
|
"reviewer_id" => user.id,
|
|
"reviewee_id" => reviewee_id
|
|
})
|
|
)
|
|
|> Repo.insert(),
|
|
reviews <- Repo.all(from r in Review, where: r.assignment_id == ^assignment.id),
|
|
true <- length(reviews) <= 2 do
|
|
if length(reviews) == 2 do
|
|
now = DateTime.utc_now(:second)
|
|
|
|
Repo.update_all(from(r in Review, where: r.assignment_id == ^assignment.id),
|
|
set: [revealed_at: now]
|
|
)
|
|
end
|
|
|
|
audit(user.id, "review.submitted", "assignment", assignment.id, %{
|
|
"revealed" => length(reviews) == 2
|
|
})
|
|
|
|
{:ok, review}
|
|
else
|
|
false -> {:error, :invalid_review_count}
|
|
other -> other
|
|
end
|
|
end)
|
|
else
|
|
false -> {:error, :forbidden}
|
|
other -> other
|
|
end
|
|
end
|
|
|
|
def visible_reviews(user_id) do
|
|
Review
|
|
|> where([review], review.reviewee_id == ^user_id and not is_nil(review.revealed_at))
|
|
|> order_by([review], desc: review.inserted_at)
|
|
|> preload(:reviewer)
|
|
|> Repo.all()
|
|
end
|
|
|
|
def reputation(user_id) do
|
|
rows = completed_rows() |> Enum.filter(&participant?(&1, user_id))
|
|
counterparts = Enum.map(rows, &counterpart(&1, user_id))
|
|
ratings = ratings(user_id)
|
|
|
|
%{
|
|
completed: length(rows),
|
|
unique_people: counterparts |> Enum.uniq() |> length(),
|
|
verified_handovers: Enum.count(rows, &(not is_nil(&1.handover_verified_at))),
|
|
location_supported:
|
|
Enum.count(
|
|
rows,
|
|
&(not is_nil(&1.proximity_observed_at) and movement_for_user?(&1, user_id))
|
|
),
|
|
rating: average(ratings)
|
|
}
|
|
end
|
|
|
|
def leaderboard do
|
|
ratings_by_user =
|
|
Review
|
|
|> where([review], not is_nil(review.revealed_at))
|
|
|> group_by([review], review.reviewee_id)
|
|
|> select([review], {review.reviewee_id, avg(review.rating)})
|
|
|> Repo.all()
|
|
|> Map.new()
|
|
|
|
completed_rows()
|
|
|> Enum.group_by(& &1.helper_id)
|
|
|> Enum.map(fn {helper_id, rows} ->
|
|
helper = rows |> hd() |> Map.fetch!(:helper)
|
|
|
|
supported_people =
|
|
rows
|
|
|> Enum.filter(
|
|
&(not is_nil(&1.proximity_observed_at) and
|
|
not is_nil(&1.helper_movement_observed_at))
|
|
)
|
|
|> Enum.map(& &1.requester_id)
|
|
|> Enum.uniq()
|
|
|> length()
|
|
|
|
verified_people =
|
|
rows
|
|
|> Enum.filter(&(not is_nil(&1.handover_verified_at)))
|
|
|> Enum.map(& &1.requester_id)
|
|
|> Enum.uniq()
|
|
|> length()
|
|
|
|
%{
|
|
user: helper,
|
|
completed: length(rows),
|
|
unique_people: rows |> Enum.map(& &1.requester_id) |> Enum.uniq() |> length(),
|
|
location_supported_people: supported_people,
|
|
verified_people: verified_people,
|
|
rating: ratings_by_user |> Map.get(helper_id) |> decimal_average()
|
|
}
|
|
end)
|
|
|> Enum.sort_by(fn entry ->
|
|
{
|
|
-entry.location_supported_people,
|
|
-entry.verified_people,
|
|
-entry.unique_people,
|
|
-entry.completed,
|
|
String.downcase(entry.user.display_name || "")
|
|
}
|
|
end)
|
|
end
|
|
|
|
def report(%Scope{user: user} = scope, attrs) do
|
|
attrs = stringify_keys(attrs)
|
|
|
|
with {:ok, _limit} <- authorize_action(scope, :report),
|
|
:ok <- authorize_report_target(scope, attrs) do
|
|
Repo.transact(fn ->
|
|
with {:ok, report} <-
|
|
%Report{reporter_id: user.id}
|
|
|> Report.changeset(attrs)
|
|
|> Repo.insert() do
|
|
audit(user.id, "report.created", "report", report.id, %{
|
|
"reason" => to_string(report.reason)
|
|
})
|
|
|
|
{:ok, report}
|
|
end
|
|
end)
|
|
end
|
|
end
|
|
|
|
def list_reports(%Scope{user: user}, status \\ nil) do
|
|
if Accounts.moderator_authorized?(user) do
|
|
Report
|
|
|> maybe_status(status)
|
|
|> order_by([report], asc: report.status, desc: report.inserted_at)
|
|
|> preload([
|
|
:reporter,
|
|
:reviewed_by,
|
|
request: :requester,
|
|
assignment: :helper,
|
|
message: :sender,
|
|
activity: :creator,
|
|
activity_message: :sender
|
|
])
|
|
|> Repo.all()
|
|
else
|
|
[]
|
|
end
|
|
end
|
|
|
|
def moderate_report(%Scope{user: moderator}, report_id, attrs) do
|
|
if Accounts.moderator_authorized?(moderator) do
|
|
attrs =
|
|
attrs
|
|
|> stringify_keys()
|
|
|> Map.merge(%{
|
|
"reviewed_at" => DateTime.utc_now(:second),
|
|
"reviewed_by_id" => moderator.id
|
|
})
|
|
|
|
Repo.transact(fn ->
|
|
report =
|
|
Report |> where([report], report.id == ^report_id) |> lock("FOR UPDATE") |> Repo.one!()
|
|
|
|
with {:ok, report} <- report |> Report.moderation_changeset(attrs) |> Repo.update() do
|
|
audit(moderator.id, "report.moderated", "report", report.id, %{
|
|
"status" => to_string(report.status)
|
|
})
|
|
|
|
{:ok, report}
|
|
end
|
|
end)
|
|
else
|
|
{:error, :forbidden}
|
|
end
|
|
end
|
|
|
|
def report_evidence(%Scope{user: moderator}, report_id) do
|
|
if Accounts.moderator_authorized?(moderator) do
|
|
report =
|
|
Report
|
|
|> Repo.get(report_id)
|
|
|> Repo.preload([
|
|
:reporter,
|
|
:request,
|
|
:assignment,
|
|
:activity,
|
|
message: :assignment,
|
|
activity_message: :activity
|
|
])
|
|
|
|
if report do
|
|
assignment_id =
|
|
cond do
|
|
report.assignment_id -> report.assignment_id
|
|
report.message -> report.message.assignment_id
|
|
true -> nil
|
|
end
|
|
|
|
messages =
|
|
if assignment_id do
|
|
Message
|
|
|> where([message], message.assignment_id == ^assignment_id)
|
|
|> order_by([message], asc: message.inserted_at)
|
|
|> preload(:sender)
|
|
|> Repo.all()
|
|
else
|
|
[]
|
|
end
|
|
|
|
activity_id =
|
|
case report.activity_message do
|
|
%ActivityMessage{activity_id: activity_id} -> activity_id
|
|
_ -> nil
|
|
end
|
|
|
|
activity_messages =
|
|
if activity_id do
|
|
ActivityMessage
|
|
|> where([message], message.activity_id == ^activity_id)
|
|
|> order_by([message], asc: message.inserted_at)
|
|
|> preload(:sender)
|
|
|> Repo.all()
|
|
else
|
|
[]
|
|
end
|
|
|
|
with {:ok, _audit} <-
|
|
audit(moderator.id, "report.evidence_viewed", "report", report.id, %{
|
|
"assignment_id" => assignment_id,
|
|
"activity_id" => activity_id,
|
|
"message_count" => length(messages),
|
|
"activity_message_count" => length(activity_messages)
|
|
}) do
|
|
{:ok, %{report: report, messages: messages, activity_messages: activity_messages}}
|
|
end
|
|
else
|
|
{:error, :not_found}
|
|
end
|
|
else
|
|
{:error, :forbidden}
|
|
end
|
|
end
|
|
|
|
def block(%Scope{user: user} = scope, blocked_id) do
|
|
with {:ok, _limit} <- authorize_action(scope, :block),
|
|
%User{} <- Repo.get(User, blocked_id) do
|
|
Repo.transact(fn ->
|
|
with {:ok, block} <-
|
|
%Block{}
|
|
|> Block.changeset(%{blocker_id: user.id, blocked_id: blocked_id})
|
|
|> Repo.insert() do
|
|
audit(user.id, "user.blocked", "user", blocked_id)
|
|
clear_pair_tracking(user.id, blocked_id)
|
|
{:ok, block}
|
|
end
|
|
end)
|
|
else
|
|
nil -> {:error, :not_found}
|
|
other -> other
|
|
end
|
|
end
|
|
|
|
def unblock(%Scope{user: user}, blocked_id) do
|
|
case Repo.get_by(Block, blocker_id: user.id, blocked_id: blocked_id) do
|
|
nil ->
|
|
{:ok, :already_unblocked}
|
|
|
|
block ->
|
|
Repo.transact(fn ->
|
|
with {:ok, _block} <- Repo.delete(block) do
|
|
audit(user.id, "user.unblocked", "user", blocked_id)
|
|
{:ok, :unblocked}
|
|
end
|
|
end)
|
|
end
|
|
end
|
|
|
|
def list_blocks(%Scope{user: user}) do
|
|
Block
|
|
|> where([block], block.blocker_id == ^user.id)
|
|
|> order_by([block], desc: block.inserted_at)
|
|
|> preload(:blocked)
|
|
|> Repo.all()
|
|
end
|
|
|
|
def blocked_between?(first_user_id, second_user_id) do
|
|
Block
|
|
|> where(
|
|
[block],
|
|
(block.blocker_id == ^first_user_id and block.blocked_id == ^second_user_id) or
|
|
(block.blocker_id == ^second_user_id and block.blocked_id == ^first_user_id)
|
|
)
|
|
|> Repo.exists?()
|
|
end
|
|
|
|
def blocked_by?(blocker_id, blocked_id) do
|
|
Repo.exists?(
|
|
from block in Block,
|
|
where: block.blocker_id == ^blocker_id and block.blocked_id == ^blocked_id
|
|
)
|
|
end
|
|
|
|
def record_completion_signals(%Assignment{} = assignment) do
|
|
request =
|
|
case assignment.request do
|
|
%HelpRequest{} = request -> request
|
|
_ -> Repo.get!(HelpRequest, assignment.request_id)
|
|
end
|
|
|
|
repeated_pair? =
|
|
Assignment
|
|
|> join(:inner, [candidate], req in HelpRequest, on: req.id == candidate.request_id)
|
|
|> where(
|
|
[candidate, req],
|
|
candidate.status == :completed and candidate.id != ^assignment.id and
|
|
candidate.helper_id == ^assignment.helper_id and
|
|
req.requester_id == ^request.requester_id
|
|
)
|
|
|> Repo.exists?()
|
|
|
|
if repeated_pair? do
|
|
create_signal_once(:repeated_pair, assignment.helper_id, assignment.id, %{
|
|
"counterpart_id" => request.requester_id
|
|
})
|
|
end
|
|
|
|
reciprocal? =
|
|
Assignment
|
|
|> join(:inner, [candidate], req in HelpRequest, on: req.id == candidate.request_id)
|
|
|> where(
|
|
[candidate, req],
|
|
candidate.status == :completed and candidate.helper_id == ^request.requester_id and
|
|
req.requester_id == ^assignment.helper_id
|
|
)
|
|
|> Repo.exists?()
|
|
|
|
if reciprocal? do
|
|
create_signal_once(:reciprocal_ring, assignment.helper_id, assignment.id, %{
|
|
"counterpart_id" => request.requester_id,
|
|
"factual_signal_only" => true
|
|
})
|
|
end
|
|
|
|
if is_nil(assignment.proximity_observed_at) do
|
|
create_signal_once(
|
|
:handover_without_location_evidence,
|
|
assignment.helper_id,
|
|
assignment.id,
|
|
%{"tracking_optional" => true}
|
|
)
|
|
end
|
|
|
|
if is_nil(assignment.helper_movement_observed_at) do
|
|
create_signal_once(:location_without_movement, assignment.helper_id, assignment.id, %{
|
|
"tracking_optional" => true
|
|
})
|
|
end
|
|
|
|
:ok
|
|
end
|
|
|
|
def list_abuse_signals(%Scope{user: user}, status \\ :open) do
|
|
if Accounts.moderator_authorized?(user) do
|
|
AbuseSignal
|
|
|> where([signal], signal.status == ^status)
|
|
|> order_by([signal], desc: signal.inserted_at)
|
|
|> preload([:subject, :assignment, :reviewed_by])
|
|
|> Repo.all()
|
|
else
|
|
[]
|
|
end
|
|
end
|
|
|
|
def moderate_signal(%Scope{user: moderator}, signal_id, attrs) do
|
|
if Accounts.moderator_authorized?(moderator) do
|
|
attrs =
|
|
attrs
|
|
|> stringify_keys()
|
|
|> Map.merge(%{
|
|
"reviewed_at" => DateTime.utc_now(:second),
|
|
"reviewed_by_id" => moderator.id
|
|
})
|
|
|
|
Repo.transact(fn ->
|
|
signal =
|
|
AbuseSignal
|
|
|> where([signal], signal.id == ^signal_id)
|
|
|> lock("FOR UPDATE")
|
|
|> Repo.one!()
|
|
|
|
with {:ok, signal} <- signal |> AbuseSignal.moderation_changeset(attrs) |> Repo.update() do
|
|
audit(moderator.id, "abuse_signal.moderated", "abuse_signal", signal.id, %{
|
|
"status" => to_string(signal.status)
|
|
})
|
|
|
|
{:ok, signal}
|
|
end
|
|
end)
|
|
else
|
|
{:error, :forbidden}
|
|
end
|
|
end
|
|
|
|
def hide_request(%Scope{user: moderator}, request_id, reason) do
|
|
result =
|
|
if Accounts.moderator_authorized?(moderator) do
|
|
Repo.transact(fn ->
|
|
request =
|
|
HelpRequest
|
|
|> where([request], request.id == ^request_id)
|
|
|> lock("FOR UPDATE")
|
|
|> Repo.one!()
|
|
|
|
with {:ok, request} <-
|
|
request
|
|
|> HelpRequest.moderation_changeset(%{
|
|
hidden_at: DateTime.utc_now(:second),
|
|
hidden_reason: reason
|
|
})
|
|
|> Repo.update() do
|
|
audit(moderator.id, "request.hidden", "request", request.id, %{"reason" => reason})
|
|
{:ok, request}
|
|
end
|
|
end)
|
|
else
|
|
{:error, :forbidden}
|
|
end
|
|
|
|
with {:ok, request} <- result do
|
|
Help.notify_request_updated(request.id)
|
|
{:ok, request}
|
|
end
|
|
end
|
|
|
|
def restore_request(%Scope{user: moderator}, request_id) do
|
|
result =
|
|
if Accounts.moderator_authorized?(moderator) do
|
|
Repo.transact(fn ->
|
|
request =
|
|
HelpRequest
|
|
|> where([request], request.id == ^request_id)
|
|
|> lock("FOR UPDATE")
|
|
|> Repo.one!()
|
|
|
|
with {:ok, request} <-
|
|
request
|
|
|> HelpRequest.moderation_changeset(%{hidden_at: nil, hidden_reason: nil})
|
|
|> Repo.update() do
|
|
audit(moderator.id, "request.restored", "request", request.id)
|
|
{:ok, request}
|
|
end
|
|
end)
|
|
else
|
|
{:error, :forbidden}
|
|
end
|
|
|
|
with {:ok, request} <- result do
|
|
Help.notify_request_updated(request.id)
|
|
{:ok, request}
|
|
end
|
|
end
|
|
|
|
def hide_activity(%Scope{user: moderator}, activity_id, reason) do
|
|
result =
|
|
if Accounts.moderator_authorized?(moderator) do
|
|
Repo.transact(fn ->
|
|
activity =
|
|
Activity
|
|
|> where([activity], activity.id == ^activity_id)
|
|
|> lock("FOR UPDATE")
|
|
|> Repo.one!()
|
|
|
|
with {:ok, activity} <-
|
|
activity
|
|
|> Activity.moderation_changeset(%{
|
|
hidden_at: DateTime.utc_now(:second),
|
|
hidden_reason: reason
|
|
})
|
|
|> Repo.update() do
|
|
audit(moderator.id, "activity.hidden", "activity", activity.id, %{
|
|
"reason" => reason
|
|
})
|
|
|
|
{:ok, activity}
|
|
end
|
|
end)
|
|
else
|
|
{:error, :forbidden}
|
|
end
|
|
|
|
with {:ok, activity} <- result do
|
|
Activities.notify_activity_updated(activity.id)
|
|
{:ok, activity}
|
|
end
|
|
end
|
|
|
|
def restore_activity(%Scope{user: moderator}, activity_id) do
|
|
result =
|
|
if Accounts.moderator_authorized?(moderator) do
|
|
Repo.transact(fn ->
|
|
activity =
|
|
Activity
|
|
|> where([activity], activity.id == ^activity_id)
|
|
|> lock("FOR UPDATE")
|
|
|> Repo.one!()
|
|
|
|
with {:ok, activity} <-
|
|
activity
|
|
|> Activity.moderation_changeset(%{hidden_at: nil, hidden_reason: nil})
|
|
|> Repo.update() do
|
|
audit(moderator.id, "activity.restored", "activity", activity.id)
|
|
{:ok, activity}
|
|
end
|
|
end)
|
|
else
|
|
{:error, :forbidden}
|
|
end
|
|
|
|
with {:ok, activity} <- result do
|
|
Activities.notify_activity_updated(activity.id)
|
|
{:ok, activity}
|
|
end
|
|
end
|
|
|
|
def moderate_user(%Scope{user: moderator}, user_id, attrs) do
|
|
Repo.transact(fn ->
|
|
with {:ok, user} <- Accounts.moderate_user(moderator, user_id, attrs),
|
|
{:ok, _audit} <-
|
|
audit(moderator.id, "user.moderated", "user", user.id, %{
|
|
"moderation_status" => to_string(user.moderation_status)
|
|
}) do
|
|
{:ok, user}
|
|
end
|
|
end)
|
|
end
|
|
|
|
def moderate_role(%Scope{user: admin}, user_id, attrs) do
|
|
Repo.transact(fn ->
|
|
with {:ok, user} <- Accounts.change_user_role(admin, user_id, attrs),
|
|
{:ok, _audit} <-
|
|
audit(admin.id, "user.role_changed", "user", user.id, %{
|
|
"role" => to_string(user.role)
|
|
}) do
|
|
{:ok, user}
|
|
end
|
|
end)
|
|
end
|
|
|
|
def audit(actor_id, action, target_type, target_id, metadata \\ %{}) do
|
|
%AuditEvent{}
|
|
|> AuditEvent.changeset(%{
|
|
actor_id: actor_id,
|
|
action: action,
|
|
target_type: target_type,
|
|
target_id: target_id,
|
|
metadata: metadata
|
|
})
|
|
|> Repo.insert()
|
|
end
|
|
|
|
defp completed_rows do
|
|
Assignment
|
|
|> join(:inner, [assignment], request in HelpRequest, on: request.id == assignment.request_id)
|
|
|> join(:inner, [assignment, _request], helper in User, on: helper.id == assignment.helper_id)
|
|
|> where([assignment], assignment.status == :completed)
|
|
|> select([assignment, request, helper], %{
|
|
id: assignment.id,
|
|
helper_id: assignment.helper_id,
|
|
requester_id: request.requester_id,
|
|
handover_verified_at: assignment.handover_verified_at,
|
|
proximity_observed_at: assignment.proximity_observed_at,
|
|
helper_movement_observed_at: assignment.helper_movement_observed_at,
|
|
helper: helper
|
|
})
|
|
|> Repo.all()
|
|
end
|
|
|
|
defp participant?(row, user_id), do: row.helper_id == user_id or row.requester_id == user_id
|
|
|
|
defp counterpart(row, user_id),
|
|
do: if(row.helper_id == user_id, do: row.requester_id, else: row.helper_id)
|
|
|
|
defp movement_for_user?(row, user_id) do
|
|
if row.helper_id == user_id, do: not is_nil(row.helper_movement_observed_at), else: true
|
|
end
|
|
|
|
defp ratings(user_id) do
|
|
Review
|
|
|> where([review], review.reviewee_id == ^user_id and not is_nil(review.revealed_at))
|
|
|> select([review], review.rating)
|
|
|> Repo.all()
|
|
end
|
|
|
|
defp average([]), do: nil
|
|
defp average(values), do: Float.round(Enum.sum(values) / length(values), 1)
|
|
defp decimal_average(nil), do: nil
|
|
defp decimal_average(value), do: value |> Decimal.to_float() |> Float.round(1)
|
|
|
|
defp authorize_report_target(%Scope{user: user}, %{"request_id" => request_id})
|
|
when is_binary(request_id) do
|
|
case Repo.get(HelpRequest, request_id) do
|
|
%HelpRequest{requester_id: requester_id} when requester_id != user.id -> :ok
|
|
%HelpRequest{} -> {:error, :cannot_report_self}
|
|
nil -> {:error, :not_found}
|
|
end
|
|
end
|
|
|
|
defp authorize_report_target(scope, %{"assignment_id" => assignment_id})
|
|
when is_binary(assignment_id) do
|
|
case Repo.get(Assignment, assignment_id) do
|
|
%Assignment{} = assignment ->
|
|
if Help.participant?(scope, assignment), do: :ok, else: {:error, :forbidden}
|
|
|
|
nil ->
|
|
{:error, :not_found}
|
|
end
|
|
end
|
|
|
|
defp authorize_report_target(scope, %{"message_id" => message_id}) when is_binary(message_id) do
|
|
case Message |> Repo.get(message_id) |> Repo.preload(:assignment) do
|
|
%Message{sender_id: sender_id, assignment: assignment} ->
|
|
cond do
|
|
sender_id == scope.user.id -> {:error, :cannot_report_self}
|
|
Help.participant?(scope, assignment) -> :ok
|
|
true -> {:error, :forbidden}
|
|
end
|
|
|
|
nil ->
|
|
{:error, :not_found}
|
|
end
|
|
end
|
|
|
|
defp authorize_report_target(%Scope{user: user}, %{"activity_id" => activity_id})
|
|
when is_binary(activity_id) do
|
|
case Repo.get(Activity, activity_id) do
|
|
%Activity{creator_id: creator_id} when creator_id == user.id ->
|
|
{:error, :cannot_report_self}
|
|
|
|
%Activity{} = activity ->
|
|
if is_nil(activity.hidden_at) or activity_participant?(activity.id, user.id),
|
|
do: :ok,
|
|
else: {:error, :not_found}
|
|
|
|
nil ->
|
|
{:error, :not_found}
|
|
end
|
|
end
|
|
|
|
defp authorize_report_target(
|
|
%Scope{user: user},
|
|
%{"activity_message_id" => message_id}
|
|
)
|
|
when is_binary(message_id) do
|
|
case Repo.get(ActivityMessage, message_id) do
|
|
%ActivityMessage{sender_id: sender_id} when sender_id == user.id ->
|
|
{:error, :cannot_report_self}
|
|
|
|
%ActivityMessage{activity_id: activity_id} ->
|
|
if activity_participant?(activity_id, user.id),
|
|
do: :ok,
|
|
else: {:error, :forbidden}
|
|
|
|
nil ->
|
|
{:error, :not_found}
|
|
end
|
|
end
|
|
|
|
defp authorize_report_target(_scope, _attrs), do: {:error, :invalid_target}
|
|
|
|
defp activity_participant?(activity_id, user_id) do
|
|
Repo.exists?(
|
|
from participant in Participant,
|
|
where:
|
|
participant.activity_id == ^activity_id and participant.user_id == ^user_id and
|
|
participant.status == :approved
|
|
)
|
|
end
|
|
|
|
defp create_signal_once(kind, subject_id, assignment_id, metadata) do
|
|
query =
|
|
from signal in AbuseSignal,
|
|
where:
|
|
signal.kind == ^kind and signal.subject_id == ^subject_id and
|
|
signal.assignment_id == ^assignment_id
|
|
|
|
if Repo.exists?(query) do
|
|
{:ok, :already_recorded}
|
|
else
|
|
%AbuseSignal{}
|
|
|> AbuseSignal.changeset(%{
|
|
kind: kind,
|
|
subject_id: subject_id,
|
|
assignment_id: assignment_id,
|
|
metadata: metadata
|
|
})
|
|
|> Repo.insert()
|
|
end
|
|
end
|
|
|
|
defp create_velocity_signal_once(subject_id, action) do
|
|
query =
|
|
from signal in AbuseSignal,
|
|
where:
|
|
signal.kind == :velocity and signal.subject_id == ^subject_id and
|
|
signal.status == :open
|
|
|
|
case Repo.one(query) do
|
|
nil ->
|
|
%AbuseSignal{}
|
|
|> AbuseSignal.changeset(%{
|
|
kind: :velocity,
|
|
subject_id: subject_id,
|
|
metadata: %{"action" => to_string(action)}
|
|
})
|
|
|> Repo.insert()
|
|
|
|
signal ->
|
|
signal
|
|
|> Ecto.Changeset.change(
|
|
metadata: Map.put(signal.metadata, "latest_action", to_string(action))
|
|
)
|
|
|> Repo.update()
|
|
end
|
|
end
|
|
|
|
defp maybe_status(query, nil), do: query
|
|
defp maybe_status(query, status), do: where(query, [report], report.status == ^status)
|
|
|
|
defp clear_pair_tracking(first_user_id, second_user_id) do
|
|
assignment_ids =
|
|
Assignment
|
|
|> join(:inner, [assignment], request in HelpRequest,
|
|
on: request.id == assignment.request_id
|
|
)
|
|
|> where(
|
|
[assignment, request],
|
|
(assignment.helper_id == ^first_user_id and request.requester_id == ^second_user_id) or
|
|
(assignment.helper_id == ^second_user_id and request.requester_id == ^first_user_id)
|
|
)
|
|
|> where([assignment], assignment.status in [:accepted, :in_progress])
|
|
|> select([assignment], assignment.id)
|
|
|
|
session_ids =
|
|
TrackingSession
|
|
|> where([session], session.active and session.assignment_id in subquery(assignment_ids))
|
|
|> select([session], session.id)
|
|
|
|
Repo.delete_all(
|
|
from position in Position, where: position.tracking_session_id in subquery(session_ids)
|
|
)
|
|
|
|
Repo.update_all(
|
|
from(session in TrackingSession, where: session.id in subquery(session_ids)),
|
|
set: [active: false, ended_at: DateTime.utc_now(:second)]
|
|
)
|
|
|
|
:ok
|
|
end
|
|
|
|
defp stringify_keys(attrs) do
|
|
Map.new(attrs, fn {key, value} -> {to_string(key), value} end)
|
|
end
|
|
end
|