A community based topic aggregation platform built on atproto
1package adminreports
2
3import (
4 "log"
5 "regexp"
6 "strings"
7 "time"
8 "unicode/utf8"
9)
10
11// Report represents an admin report in the AppView database
12// Reports are created by users to flag serious content for admin review
13type Report struct {
14 ID int64 `json:"id" db:"id"`
15 ReporterDID string `json:"reporterDid" db:"reporter_did"`
16 TargetURI string `json:"targetUri" db:"target_uri"`
17 TargetType TargetType `json:"targetType" db:"target_type"`
18 Reason Reason `json:"reason" db:"reason"`
19 Explanation string `json:"explanation,omitempty" db:"explanation"`
20 Status Status `json:"status" db:"status"`
21 ResolvedBy *string `json:"resolvedBy,omitempty" db:"resolved_by"`
22 ResolutionNotes *string `json:"resolutionNotes,omitempty" db:"resolution_notes"`
23 CreatedAt time.Time `json:"createdAt" db:"created_at"`
24 ResolvedAt *time.Time `json:"resolvedAt,omitempty" db:"resolved_at"`
25}
26
27// Reason represents the category of an admin report
28type Reason string
29
30// Valid reason values for admin reports
31const (
32 ReasonCSAM Reason = "csam"
33 ReasonDoxing Reason = "doxing"
34 ReasonHarassment Reason = "harassment"
35 ReasonSpam Reason = "spam"
36 ReasonIllegal Reason = "illegal"
37 ReasonOther Reason = "other"
38)
39
40// Status represents the processing status of an admin report
41type Status string
42
43// Valid status values for admin reports
44const (
45 StatusOpen Status = "open"
46 StatusReviewing Status = "reviewing"
47 StatusResolved Status = "resolved"
48 StatusDismissed Status = "dismissed"
49)
50
51// TargetType represents the type of content being reported
52type TargetType string
53
54// Valid target types for admin reports
55const (
56 TargetTypePost TargetType = "post"
57 TargetTypeComment TargetType = "comment"
58)
59
60// ValidReasons returns all valid reason values
61func ValidReasons() []Reason {
62 return []Reason{ReasonCSAM, ReasonDoxing, ReasonHarassment, ReasonSpam, ReasonIllegal, ReasonOther}
63}
64
65// ValidStatuses returns all valid status values
66func ValidStatuses() []Status {
67 return []Status{StatusOpen, StatusReviewing, StatusResolved, StatusDismissed}
68}
69
70// ValidTargetTypes returns all valid target type values
71func ValidTargetTypes() []TargetType {
72 return []TargetType{TargetTypePost, TargetTypeComment}
73}
74
75// IsValidReason checks if a reason value is valid
76func IsValidReason(reason string) bool {
77 for _, r := range ValidReasons() {
78 if string(r) == reason {
79 return true
80 }
81 }
82 return false
83}
84
85// IsValidStatus checks if a status value is valid
86func IsValidStatus(status string) bool {
87 for _, s := range ValidStatuses() {
88 if string(s) == status {
89 return true
90 }
91 }
92 return false
93}
94
95// IsValidTargetType checks if a target type value is valid
96func IsValidTargetType(targetType string) bool {
97 for _, t := range ValidTargetTypes() {
98 if string(t) == targetType {
99 return true
100 }
101 }
102 return false
103}
104
105// MaxExplanationLength is the maximum number of characters allowed in an explanation
106const MaxExplanationLength = 1000
107
108// SubmitReportRequest contains the data needed to submit a new report
109type SubmitReportRequest struct {
110 // ReporterDID is the DID of the user submitting the report
111 ReporterDID string
112
113 // TargetURI is the AT Protocol URI of the content being reported
114 TargetURI string
115
116 // Reason is the category of the report
117 Reason string
118
119 // Explanation is an optional description of the issue
120 Explanation string
121}
122
123// atURIPattern validates AT Protocol URIs with proper structure:
124// at://did:plc:xxx/collection/rkey or at://did:web:xxx/collection/rkey
125// Note: This validation focuses on structure rather than strict DID format validation,
126// which is the responsibility of the PDS. The pattern allows alphanumeric DID identifiers.
127var atURIPattern = regexp.MustCompile(`^at://did:(plc:[a-zA-Z0-9]+|web:[a-zA-Z0-9.-]+)/[a-zA-Z0-9.]+/[a-zA-Z0-9_-]+$`)
128
129// Validate validates the SubmitReportRequest and returns an error if invalid
130func (r *SubmitReportRequest) Validate() error {
131 // Validate reporter DID
132 if r.ReporterDID == "" {
133 return ErrReporterRequired
134 }
135
136 // Validate reason is one of the allowed values
137 if !IsValidReason(r.Reason) {
138 return ErrInvalidReason
139 }
140
141 // Validate target URI is a proper AT Protocol URI
142 if !isValidATURI(r.TargetURI) {
143 return ErrInvalidTarget
144 }
145
146 // Validate explanation length (max 1000 characters, using proper character counting)
147 if utf8.RuneCountInString(r.Explanation) > MaxExplanationLength {
148 return ErrExplanationTooLong
149 }
150
151 return nil
152}
153
154// isValidATURI validates that the URI is a proper AT Protocol URI
155// AT Protocol URIs have the format: at://did:plc:xxx/collection/rkey
156// or at://did:web:xxx/collection/rkey
157func isValidATURI(uri string) bool {
158 // Check basic prefix
159 if !strings.HasPrefix(uri, "at://") {
160 return false
161 }
162
163 // Validate the full URI pattern
164 return atURIPattern.MatchString(uri)
165}
166
167// determineTargetType determines whether the target is a post or comment based on the URI
168// AT Protocol URIs have the format: at://did:plc:xxx/collection/rkey
169// For Coves, the collection will contain "post" or "comment"
170func determineTargetType(uri string) TargetType {
171 // Check if the URI contains common post or comment collection patterns
172 lowerURI := strings.ToLower(uri)
173
174 if strings.Contains(lowerURI, "comment") {
175 return TargetTypeComment
176 }
177
178 // Log when defaulting to post for unknown target types
179 if !strings.Contains(lowerURI, "post") {
180 log.Printf("[ADMIN_REPORT] Unknown target type in URI, defaulting to post: %s", uri)
181 }
182
183 return TargetTypePost
184}
185
186// NewReport creates a new Report from a validated SubmitReportRequest
187// This constructor ensures that reports are created with proper defaults and validation
188// The request must be validated before calling this function
189func NewReport(req SubmitReportRequest) (*Report, error) {
190 // Validate the request first
191 if err := req.Validate(); err != nil {
192 return nil, err
193 }
194
195 return &Report{
196 ReporterDID: req.ReporterDID,
197 TargetURI: req.TargetURI,
198 TargetType: determineTargetType(req.TargetURI),
199 Reason: Reason(req.Reason),
200 Explanation: req.Explanation,
201 Status: StatusOpen,
202 CreatedAt: time.Now().UTC(),
203 }, nil
204}