Class: Moderate::Services::ResolveReport
- Inherits:
-
Object
- Object
- Moderate::Services::ResolveReport
- Defined in:
- lib/moderate/services/resolve_report.rb
Overview
ResolveReport — the audited decision engine behind ‘report.resolve!` / `report.dismiss!`.
This is where a moderator’s decision actually happens, and it’s the most legally-loaded path in the gem, so it’s built defensively:
1. ATOMIC + IDEMPOTENT. The whole transition runs inside `report.with_lock`
(a SELECT ... FOR UPDATE row lock). Two moderators clicking "resolve" at
once must not both run enforcement (double-ban, double-remove) — the lock
serializes them, and we RE-CHECK `open?` *inside* the lock after reload so
the second one sees the closed record and bails with a clean error rather
than re-applying actions.
2. A NOTE IS MANDATORY. DSA Art. 17 requires a "clear and specific statement
of reasons"; the moderator's note is the human-readable ground. No note,
no decision — we raise RecordInvalid with the note error attached.
See: https://eur-lex.europa.eu/eli/reg/2022/2065/oj (Article 17).
3. ENFORCEMENT IS HOST-AGNOSTIC. Content removal calls the reportable's own
`remove_reported_field!` (the host decides what "remove" means for its
content); a ban calls `Moderate.apply_ban` → the host's `ban_handler`
(the host decides what "banned" means). The gem never reaches into a
host's domain — it only invokes the contracts.
4. TWO DECISION EVENTS, ON PURPOSE. `report_decision` tells the *reporter*
"we handled it" (Art. 16(5)); `affected_user_decision` gives the *content
owner* the Art. 17 statement of reasons (action taken, ground, automated-
means disclosure, appeal path). Different people, different rights — see
docs/notifications.md ("Why two decision events").
Instance Method Summary collapse
-
#dismiss!(note:) ⇒ Object
Dismiss (no violation found).
-
#initialize(report, by:) ⇒ ResolveReport
constructor
A new instance of ResolveReport.
-
#resolve!(note:, remove_content: false, ban_user: false, resolution_basis: "terms") ⇒ Object
Resolve WITH action (the report was valid; we acted on the content/account).
Constructor Details
#initialize(report, by:) ⇒ ResolveReport
Returns a new instance of ResolveReport.
35 36 37 38 |
# File 'lib/moderate/services/resolve_report.rb', line 35 def initialize(report, by:) @report = report @moderator = by end |
Instance Method Details
#dismiss!(note:) ⇒ Object
Dismiss (no violation found). Still requires a note (Art. 17 applies to the reporter’s right to know the outcome too) and still opens an appeal window —the reporter can appeal a dismissal.
60 61 62 63 |
# File 'lib/moderate/services/resolve_report.rb', line 60 def dismiss!(note:) note = require_note!(note) transition!("dismissed", note: note, actions: { resolution_basis: "no_violation" }) end |
#resolve!(note:, remove_content: false, ban_user: false, resolution_basis: "terms") ⇒ Object
Resolve WITH action (the report was valid; we acted on the content/account). ‘resolution_basis` records the legal/contractual ground bucket; it’s validated against the migration’s check-constraint list by the model.
43 44 45 46 47 48 49 50 51 52 53 54 55 |
# File 'lib/moderate/services/resolve_report.rb', line 43 def resolve!(note:, remove_content: false, ban_user: false, resolution_basis: "terms") note = require_note!(note) actions = { remove_content: truthy?(remove_content), ban_user: truthy?(ban_user), resolution_basis: resolution_basis.to_s.strip.presence || "terms" } transition!("actioned", note: note, actions: actions) do remove_reported_content! if actions[:remove_content] ban_reported_user!(note: note) if actions[:ban_user] end end |