package services import ( "context" "regexp" "strings" "github.com/jackc/pgx/v5/pgxpool" ) type UsernameViolation int const ( UsernameOK UsernameViolation = iota UsernameReserved UsernameInappropriate UsernameInvalidFormat ) type UsernameCheckResult struct { Violation UsernameViolation Message string } // ValidateUsernameWithDB checks a handle against the reserved_usernames DB table, // inappropriate words, and format rules. All reserved names are managed via admin console. func ValidateUsernameWithDB(ctx context.Context, pool *pgxpool.Pool, handle string) UsernameCheckResult { h := strings.ToLower(strings.TrimSpace(handle)) // Format check if len(h) < 3 || len(h) > 30 { return UsernameCheckResult{UsernameInvalidFormat, "Username must be between 3 and 30 characters."} } if !validHandleRegex.MatchString(h) { return UsernameCheckResult{UsernameInvalidFormat, "Username can only contain letters, numbers, underscores, and periods."} } // Reserved check — DB only if pool != nil { var count int err := pool.QueryRow(ctx, `SELECT COUNT(*) FROM reserved_usernames WHERE username = $1`, h).Scan(&count) if err == nil && count > 0 { return UsernameCheckResult{ UsernameReserved, "This username is reserved. If you officially represent this brand, company, or public figure, you can submit a verification request at support@sojorn.net to claim it.", } } } // Inappropriate check if reason := isInappropriate(h); reason != "" { return UsernameCheckResult{UsernameInappropriate, "This username is not allowed: " + reason} } return UsernameCheckResult{UsernameOK, ""} } // ValidateDisplayName checks a display name for inappropriate content. func ValidateDisplayName(name string) UsernameCheckResult { n := strings.ToLower(strings.TrimSpace(name)) if len(n) == 0 || len(n) > 50 { return UsernameCheckResult{UsernameInvalidFormat, "Display name must be between 1 and 50 characters."} } if reason := isInappropriate(n); reason != "" { return UsernameCheckResult{UsernameInappropriate, "This display name is not allowed: " + reason} } return UsernameCheckResult{UsernameOK, ""} } var validHandleRegex = regexp.MustCompile(`^[a-z0-9_.]+$`) // ------------------------------------------------------------------- // Inappropriate content filter // ------------------------------------------------------------------- func isInappropriate(text string) string { // Remove common substitutions for bypass attempts normalized := normalizeUsername(text) for _, entry := range inappropriatePatterns { if entry.regex.MatchString(normalized) || entry.regex.MatchString(text) { return entry.reason } } return "" } type inappropriateEntry struct { regex *regexp.Regexp reason string } var inappropriatePatterns []inappropriateEntry func init() { type raw struct { pattern string reason string } entries := []raw{ // Slurs and hate speech {`\bn[i1!|]gg[e3a@][r]?\b`, "contains a racial slur"}, {`\bf[a@]gg?[o0][t]?\b`, "contains a homophobic slur"}, {`\bk[i1!]ke\b`, "contains an antisemitic slur"}, {`\bsp[i1!]c\b`, "contains a racial slur"}, {`\bch[i1!]nk\b`, "contains a racial slur"}, {`\bw[e3]tb[a@]ck\b`, "contains a racial slur"}, {`\bcoon\b`, "contains a racial slur"}, {`\btr[a@]nn[yie]\b`, "contains a transphobic slur"}, {`\bdyke\b`, "contains a homophobic slur"}, {`\bretard(ed)?\b`, "contains an ableist slur"}, // Sexually explicit {`\bp[o0]rn`, "contains sexually explicit content"}, {`\bx{2,}`, "contains sexually explicit content"}, {`\bhentai\b`, "contains sexually explicit content"}, {`\bcum(sl[u]t|dump|bucket)\b`, "contains sexually explicit content"}, {`\bpussy\b`, "contains sexually explicit content"}, {`\bd[i1!]ck(head|face|sucker)`, "contains sexually explicit content"}, {`\bc[o0]ck(sucker)?`, "contains sexually explicit content"}, // Violent / threatening {`\bk[i1!]ll(er)?_(yo)?u`, "contains threatening language"}, {`\bschool.?shoot`, "contains violent content"}, {`\bmass.?murder`, "contains violent content"}, {`\bgenocide\b`, "contains violent content"}, {`\bterroris[tm]`, "contains references to terrorism"}, {`\bisis\b`, "contains references to terrorism"}, {`\bal.?qaeda\b`, "contains references to terrorism"}, {`\bjihad(i|ist)?\b`, "contains references to terrorism"}, // Drugs (hard) {`\bmeth(head|lab)\b`, "contains drug references"}, {`\bcrackhead\b`, "contains drug references"}, {`\bheroin(e)?\b`, "contains drug references"}, {`\bfentanyl\b`, "contains drug references"}, // Impersonation indicators {`\breal_?\b`, "may imply impersonation"}, {`\bthe_?real\b`, "may imply impersonation"}, {`\bofficial_\b`, "may imply impersonation"}, {`\bnot_?fake\b`, "may imply impersonation"}, // Scam / fraud {`\bfree.?money\b`, "suggests fraudulent activity"}, {`\bcrypto.?scam\b`, "suggests fraudulent activity"}, {`\bget.?rich\b`, "suggests fraudulent activity"}, // Self-harm {`\bsu[i1!]c[i1!]de\b`, "contains references to self-harm"}, {`\bkill.?myself\b`, "contains references to self-harm"}, {`\bcut.?myself\b`, "contains references to self-harm"}, // General profanity as usernames (strong) {`\bfuck`, "contains strong profanity"}, {`\bsh[i1!]t(head|face|stain)`, "contains strong profanity"}, {`\bass(hole|wipe|face|hat)`, "contains strong profanity"}, {`\bbitch\b`, "contains strong profanity"}, {`\bwhore\b`, "contains strong profanity"}, {`\bslut\b`, "contains strong profanity"}, {`\bcunt\b`, "contains strong profanity"}, } inappropriatePatterns = make([]inappropriateEntry, 0, len(entries)) for _, e := range entries { re := regexp.MustCompile("(?i)" + e.pattern) inappropriatePatterns = append(inappropriatePatterns, inappropriateEntry{regex: re, reason: e.reason}) } } // normalizeUsername applies common leet-speak substitutions to catch bypass attempts func normalizeUsername(s string) string { replacer := strings.NewReplacer( "0", "o", "1", "i", "3", "e", "4", "a", "5", "s", "7", "t", "@", "a", "$", "s", "!", "i", "|", "l", ) return replacer.Replace(s) }