A community based topic aggregation platform built on atproto
at main 204 lines 6.2 kB view raw
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}