who_need_help/lib/who_need_help/trust.ex

1094 lines
33 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.Pagination
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
if Accounts.eligible_user_id?(user.id) do
case RateLimiter.check(action, user.id) do
{:error, :rate_limited} = error ->
create_velocity_signal_once(user.id, action)
error
result ->
result
end
else
{:error, :account_not_eligible}
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
paginate_visible_reviews(user_id).entries
end
def paginate_visible_reviews(user_id, options \\ []) do
limit = Pagination.limit(options)
cursor = Pagination.cursor(options)
public_user = Accounts.public_user_query()
Review
|> where([review], review.reviewee_id == ^user_id and not is_nil(review.revealed_at))
|> before_review(cursor)
|> order_by([review], desc: review.inserted_at, desc: review.id)
|> limit(^(limit + 1))
|> preload([review], reviewer: ^public_user)
|> Repo.all()
|> Pagination.page(limit, &{&1.inserted_at, &1.id})
end
@empty_reputation %{
completed: 0,
unique_people: 0,
verified_handovers: 0,
location_supported: 0,
rating: nil
}
def reputation(user_id), do: Map.fetch!(reputations([user_id]), user_id)
def reputations([]), do: %{}
def reputations(user_ids) when is_list(user_ids) do
user_ids = Enum.uniq(user_ids)
helper_rows =
Assignment
|> join(:inner, [assignment], request in HelpRequest,
on: request.id == assignment.request_id
)
|> where(
[assignment],
assignment.status == :completed and assignment.helper_id in ^user_ids
)
|> select([assignment, request], %{
id: assignment.id,
user_id: assignment.helper_id,
counterpart_id: request.requester_id,
verified: not is_nil(assignment.handover_verified_at),
location_supported:
not is_nil(assignment.proximity_observed_at) and
not is_nil(assignment.helper_movement_observed_at)
})
requester_rows =
Assignment
|> join(:inner, [assignment], request in HelpRequest,
on: request.id == assignment.request_id
)
|> where(
[assignment, request],
assignment.status == :completed and request.requester_id in ^user_ids and
assignment.helper_id != request.requester_id
)
|> select([assignment, request], %{
id: assignment.id,
user_id: request.requester_id,
counterpart_id: assignment.helper_id,
verified: not is_nil(assignment.handover_verified_at),
location_supported: not is_nil(assignment.proximity_observed_at)
})
aggregates =
helper_rows
|> union_all(^requester_rows)
|> subquery()
|> group_by([row], row.user_id)
|> select([row], %{
user_id: row.user_id,
completed: count(row.id),
unique_people: count(row.counterpart_id, :distinct),
verified_handovers: filter(count(row.id), row.verified),
location_supported: filter(count(row.id), row.location_supported)
})
ratings =
Review
|> where(
[review],
review.reviewee_id in ^user_ids and not is_nil(review.revealed_at)
)
|> group_by([review], review.reviewee_id)
|> select([review], %{user_id: review.reviewee_id, rating: avg(review.rating)})
rows =
aggregates
|> subquery()
|> join(:left, [aggregate], rating in subquery(ratings),
on: rating.user_id == aggregate.user_id
)
|> select([aggregate, rating], %{
user_id: aggregate.user_id,
completed: aggregate.completed,
unique_people: aggregate.unique_people,
verified_handovers: aggregate.verified_handovers,
location_supported: aggregate.location_supported,
rating: rating.rating
})
|> Repo.all()
Enum.reduce(rows, Map.new(user_ids, &{&1, @empty_reputation}), fn row, reputations ->
Map.put(
reputations,
row.user_id,
row
|> Map.delete(:user_id)
|> Map.update!(:rating, &decimal_average/1)
)
end)
end
def leaderboard do
paginate_leaderboard().entries
end
def paginate_leaderboard(options \\ []) do
limit = Pagination.limit(options)
cursor = decode_leaderboard_cursor(Keyword.get(options, :after))
rows = leaderboard_rows(cursor, limit)
page_rows = Enum.take(rows, limit)
users_by_id = users_by_id(Enum.map(page_rows, & &1.helper_id))
entries =
Enum.map(page_rows, fn row ->
%{
user: Map.fetch!(users_by_id, row.helper_id),
completed: row.completed,
unique_people: row.unique_people,
location_supported_people: row.location_supported_people,
verified_people: row.verified_people,
rating: decimal_average(row.rating)
}
end)
next_cursor =
if length(rows) > limit do
page_rows
|> List.last()
|> encode_leaderboard_cursor()
end
%Pagination.Page{entries: entries, next_cursor: next_cursor}
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
paginate_reports(%Scope{user: user}, status).entries
end
def paginate_reports(%Scope{user: user}, status \\ nil, options \\ []) do
if Accounts.moderator_authorized?(user) do
limit = Pagination.limit(options)
cursor = Pagination.cursor(options)
Report
|> maybe_status(status)
|> before_report(cursor)
|> order_by([report], desc: report.inserted_at, desc: report.id)
|> limit(^(limit + 1))
|> preload([
:reporter,
:reviewed_by,
request: :requester,
assignment: :helper,
message: :sender,
activity: :creator,
activity_message: :sender
])
|> Repo.all()
|> Pagination.page(limit, &{&1.inserted_at, &1.id})
else
%Pagination.Page{}
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], desc: message.inserted_at, desc: message.id)
|> limit(200)
|> preload(:sender)
|> Repo.all()
|> Enum.reverse()
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], desc: message.inserted_at, desc: message.id)
|> limit(200)
|> preload(:sender)
|> Repo.all()
|> Enum.reverse()
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
paginate_blocks(%Scope{user: user}).entries
end
def paginate_blocks(%Scope{user: user}, options \\ []) do
limit = Pagination.limit(options)
cursor = Pagination.cursor(options)
public_user = Accounts.public_user_query()
Block
|> where([block], block.blocker_id == ^user.id)
|> before_block(cursor)
|> order_by([block], desc: block.inserted_at, desc: block.id)
|> limit(^(limit + 1))
|> preload([block], blocked: ^public_user)
|> Repo.all()
|> Pagination.page(limit, &{&1.inserted_at, &1.id})
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
paginate_abuse_signals(%Scope{user: user}, status).entries
end
def paginate_abuse_signals(%Scope{user: user}, status \\ :open, options \\ []) do
if Accounts.moderator_authorized?(user) do
limit = Pagination.limit(options)
cursor = Pagination.cursor(options)
AbuseSignal
|> where([signal], signal.status == ^status)
|> before_abuse_signal(cursor)
|> order_by([signal], desc: signal.inserted_at, desc: signal.id)
|> limit(^(limit + 1))
|> preload([:subject, :assignment, :reviewed_by])
|> Repo.all()
|> Pagination.page(limit, &{&1.inserted_at, &1.id})
else
%Pagination.Page{}
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 users_by_id([]), do: %{}
defp users_by_id(ids) do
User
|> where([user], user.id in ^ids)
|> Repo.all()
|> Map.new(&{&1.id, &1})
end
defp leaderboard_rows(cursor, limit) do
stats =
Assignment
|> join(:inner, [assignment], request in HelpRequest,
on: request.id == assignment.request_id
)
|> where([assignment], assignment.status == :completed)
|> group_by([assignment], assignment.helper_id)
|> select([assignment, request], %{
helper_id: assignment.helper_id,
completed: count(assignment.id),
unique_people: count(request.requester_id, :distinct),
location_supported_people:
filter(
count(request.requester_id, :distinct),
not is_nil(assignment.proximity_observed_at) and
not is_nil(assignment.helper_movement_observed_at)
),
verified_people:
filter(
count(request.requester_id, :distinct),
not is_nil(assignment.handover_verified_at)
)
})
ratings =
Review
|> where([review], not is_nil(review.revealed_at))
|> group_by([review], review.reviewee_id)
|> select([review], %{reviewee_id: review.reviewee_id, rating: avg(review.rating)})
stats
|> subquery()
|> join(:inner, [stats], helper in User, on: helper.id == stats.helper_id)
|> join(:left, [stats, _helper], rating in subquery(ratings),
on: rating.reviewee_id == stats.helper_id
)
|> where(^leaderboard_after(cursor))
|> order_by(
[stats, helper],
desc: stats.location_supported_people,
desc: stats.verified_people,
desc: stats.unique_people,
desc: stats.completed,
asc: fragment("coalesce(lower(?), '')", helper.display_name),
asc: helper.id
)
|> limit(^(limit + 1))
|> select([stats, helper, rating], %{
helper_id: helper.id,
completed: stats.completed,
unique_people: stats.unique_people,
location_supported_people: stats.location_supported_people,
verified_people: stats.verified_people,
rating: rating.rating,
normalized_name: fragment("coalesce(lower(?), '')", helper.display_name)
})
|> Repo.all()
end
defp leaderboard_after(nil), do: dynamic(true)
defp leaderboard_after(cursor) do
dynamic(
[stats, helper],
stats.location_supported_people < ^cursor.location_supported_people or
(stats.location_supported_people == ^cursor.location_supported_people and
stats.verified_people < ^cursor.verified_people) or
(stats.location_supported_people == ^cursor.location_supported_people and
stats.verified_people == ^cursor.verified_people and
stats.unique_people < ^cursor.unique_people) or
(stats.location_supported_people == ^cursor.location_supported_people and
stats.verified_people == ^cursor.verified_people and
stats.unique_people == ^cursor.unique_people and stats.completed < ^cursor.completed) or
(stats.location_supported_people == ^cursor.location_supported_people and
stats.verified_people == ^cursor.verified_people and
stats.unique_people == ^cursor.unique_people and stats.completed == ^cursor.completed and
fragment("coalesce(lower(?), '')", helper.display_name) > ^cursor.normalized_name) or
(stats.location_supported_people == ^cursor.location_supported_people and
stats.verified_people == ^cursor.verified_people and
stats.unique_people == ^cursor.unique_people and stats.completed == ^cursor.completed and
fragment("coalesce(lower(?), '')", helper.display_name) == ^cursor.normalized_name and
helper.id > ^cursor.helper_id)
)
end
defp encode_leaderboard_cursor(row) do
%{
"location_supported_people" => row.location_supported_people,
"verified_people" => row.verified_people,
"unique_people" => row.unique_people,
"completed" => row.completed,
"normalized_name" => row.normalized_name,
"helper_id" => row.helper_id
}
|> Jason.encode!()
|> Base.url_encode64(padding: false)
end
defp decode_leaderboard_cursor(value) when value in [nil, ""], do: nil
defp decode_leaderboard_cursor(value) when is_binary(value) do
with {:ok, json} <- Base.url_decode64(value, padding: false),
{:ok,
%{
"location_supported_people" => location_supported_people,
"verified_people" => verified_people,
"unique_people" => unique_people,
"completed" => completed,
"normalized_name" => normalized_name,
"helper_id" => helper_id
}} <- Jason.decode(json),
true <-
Enum.all?(
[location_supported_people, verified_people, unique_people, completed],
&(is_integer(&1) and &1 >= 0)
),
true <- is_binary(normalized_name) and byte_size(normalized_name) <= 80,
{:ok, helper_id} <- Ecto.UUID.cast(helper_id) do
%{
location_supported_people: location_supported_people,
verified_people: verified_people,
unique_people: unique_people,
completed: completed,
normalized_name: normalized_name,
helper_id: helper_id
}
else
_ -> nil
end
end
defp decode_leaderboard_cursor(_value), do: nil
defp decimal_average(nil), do: nil
defp decimal_average(value), do: value |> Decimal.to_float() |> Float.round(1)
defp before_review(query, nil), do: query
defp before_review(query, {inserted_at, id}) do
where(
query,
[review],
review.inserted_at < ^inserted_at or
(review.inserted_at == ^inserted_at and review.id < ^id)
)
end
defp before_report(query, nil), do: query
defp before_report(query, {inserted_at, id}) do
where(
query,
[report],
report.inserted_at < ^inserted_at or
(report.inserted_at == ^inserted_at and report.id < ^id)
)
end
defp before_block(query, nil), do: query
defp before_block(query, {inserted_at, id}) do
where(
query,
[block],
block.inserted_at < ^inserted_at or
(block.inserted_at == ^inserted_at and block.id < ^id)
)
end
defp before_abuse_signal(query, nil), do: query
defp before_abuse_signal(query, {inserted_at, id}) do
where(
query,
[signal],
signal.inserted_at < ^inserted_at or
(signal.inserted_at == ^inserted_at and signal.id < ^id)
)
end
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