20 KiB
Comments & Moderation System - Complete Documentation
Overview
The Comments System provides threaded discussions with anti-spam protection, user reactions, reporting, and comprehensive moderation tools including user bans and appeals.
Table of Contents
- Core Features
- Database Schema
- Backend API
- Frontend Integration
- Spam Protection
- Moderation Tools
- Ban System
- Reactions
- Admin Management
- Production Checklist
Core Features
Public Commenting
- Multi-target support: Articles, Events, Gallery Albums, YouTube Videos
- Threaded replies: Parent-child comment structure
- Real-time updates: Pagination with React Query
- User profiles: Display username, avatar, and engagement level
Moderation
- Automatic spam detection: Score-based filtering
- Bad word filtering: Censors profanity
- Manual approval: Hidden status for suspicious content
- Admin tools: Bulk actions, user bans, reports queue
User Engagement
- Reactions: 8 reaction types (👍 ❤️ 😊 😂 😢 😠 👎)
- Reports: Users can flag inappropriate comments
- Editing: Own comments editable (marked with timestamp)
- Points integration: Earn XP for comments and reactions
Anti-Abuse
- Rate limiting: Prevents spam flooding
- Daily caps: Limits on comment points earnings
- Ban system: Temporary or permanent blocks
- Appeal process: Users can request unbanning
Database Schema
comments
CREATE TABLE comments (
id BIGSERIAL PRIMARY KEY,
created_at TIMESTAMP WITH TIME ZONE,
updated_at TIMESTAMP WITH TIME ZONE,
target_type VARCHAR(30) NOT NULL,
target_id VARCHAR(128) NOT NULL,
user_id BIGINT NOT NULL,
parent_id BIGINT,
content TEXT NOT NULL,
status VARCHAR(20) DEFAULT 'visible',
spam_score REAL DEFAULT 0,
spam_rules TEXT,
is_edited BOOLEAN DEFAULT FALSE,
edited_at TIMESTAMP WITH TIME ZONE
);
Fields:
target_type- Where comment belongs:article,event,gallery_album,youtube_videotarget_id- ID of the target (can be string for flexibility)user_id- Authorparent_id- NULL for root comments, ID for repliescontent- Comment text (max 2000 chars)status-visibleorhidden(moderation)spam_score- 0.0-1.0 calculated by spam detectorspam_rules- JSON array of triggered spam rulesis_edited- Whether comment was edited after postingedited_at- Timestamp of last edit
Indexes: (target_type, target_id), user_id, parent_id, status, created_at, spam_score
comment_bans
CREATE TABLE comment_bans (
id BIGSERIAL PRIMARY KEY,
created_at TIMESTAMP WITH TIME ZONE,
user_id BIGINT NOT NULL,
reason TEXT,
until TIMESTAMP WITH TIME ZONE,
created_by_id BIGINT NOT NULL
);
Fields:
user_id- Banned userreason- Admin's explanationuntil- NULL = permanent, timestamp = temporarycreated_by_id- Admin who issued the ban
Active Ban Query:
SELECT * FROM comment_bans
WHERE user_id = ? AND (until IS NULL OR until > NOW())
ORDER BY created_at DESC LIMIT 1
unban_requests
CREATE TABLE unban_requests (
id BIGSERIAL PRIMARY KEY,
created_at TIMESTAMP WITH TIME ZONE,
user_id BIGINT NOT NULL,
message TEXT,
status VARCHAR(20) DEFAULT 'pending',
resolved_by_id BIGINT,
resolved_at TIMESTAMP WITH TIME ZONE
);
Statuses: pending, approved (ban lifted), rejected (ban remains)
comment_reports
CREATE TABLE comment_reports (
id BIGSERIAL PRIMARY KEY,
created_at TIMESTAMP WITH TIME ZONE,
comment_id BIGINT NOT NULL,
user_id BIGINT NOT NULL,
reason VARCHAR(255),
UNIQUE (comment_id, user_id)
);
Prevents: Duplicate reports from same user on same comment.
comment_reactions
CREATE TABLE comment_reactions (
id BIGSERIAL PRIMARY KEY,
created_at TIMESTAMP WITH TIME ZONE,
comment_id BIGINT NOT NULL,
user_id BIGINT NOT NULL,
type VARCHAR(24) NOT NULL,
UNIQUE (comment_id, user_id)
);
Types: like, heart, smile, laugh, thumbs_up, thumbs_down, sad, angry
One per user: Changing reaction deletes old one, creates new one.
Backend API
Public Endpoints
GET /api/v1/comments
List comments for a target.
Query Params:
target_type- Required:article,event,gallery_album,youtube_videotarget_id- Required: ID of the targetpage- Page number (default: 1)page_size- Items per page (max 100, default: 20)
Response:
{
"items": [
{
"id": 123,
"target_type": "article",
"target_id": "45",
"parent_id": null,
"content": "Skvělý článek!",
"status": "visible",
"is_edited": false,
"edited_at": null,
"created_at": "2025-11-01T10:00:00Z",
"updated_at": "2025-11-01T10:00:00Z",
"user": {
"id": 78,
"first_name": "Jan",
"last_name": "Novák",
"role": "fan",
"username": "jan-novak",
"avatar_url": "https://api.dicebear.com/..."
},
"reactions": {
"like": 5,
"heart": 2
},
"my_reaction": "like",
"spam_score": 0.1,
"spam_rules": []
}
],
"total": 42,
"page": 1,
"page_size": 20
}
Features:
- Only
visiblecomments returned to public my_reactionincluded if user authenticated- User profile with username + avatar from
user_profiles - Reactions aggregated by type
Protected Endpoints (Require Auth)
POST /api/v1/comments
Create a new comment.
Request:
{
"target_type": "article",
"target_id": "45",
"content": "Skvělý článek!",
"parent_id": null
}
Validation:
- Min 6 characters, max 2000
- Target type must be allowed
- Parent comment must exist if specified
- User not banned
Process:
- Check active ban
- Evaluate spam score
- Filter bad words
- Auto-hide if sensitive words detected
- Create comment record
- Award engagement points (if visible)
- Check achievements
Response: Created comment object
Rate Limit: 20 per minute
PUT /api/v1/comments/:id
Edit own comment (or any if admin).
Request:
{
"content": "Opravený text..."
}
Process:
- Check permission (owner or admin)
- Check not banned
- Re-evaluate spam & filter
- Update content
- Set
is_edited = true,edited_at = now
DELETE /api/v1/comments/:id
Delete own comment (or any if admin).
Cascade: Deletes all child comments, reports, reactions.
POST /api/v1/comments/:id/react
Add or change reaction.
Request:
{
"type": "heart"
}
Process:
- Delete existing reaction (if any)
- Create new reaction
- Award 1 XP point (capped: max 20/day)
Rate Limit: 60 per minute
DELETE /api/v1/comments/:id/react
Remove reaction.
POST /api/v1/comments/:id/report
Report inappropriate comment.
Request:
{
"reason": "Spam nebo urážky"
}
Prevents duplicates: One report per user per comment.
Rate Limit: 10 per hour
POST /api/v1/comments/unban-request
Request to be unbanned.
Request:
{
"message": "Omlouvám se, už se to nebude opakovat..."
}
Rate Limit: 5 per hour
Admin Endpoints
GET /api/v1/admin/comments
List all comments with admin filters.
Query Params:
status-visible|hiddentarget_type- Filter by typetarget_id- Filter by targetuser_id- Filter by authorpage,page_size- Pagination
Response: Includes reports count per comment
PATCH /api/v1/admin/comments/:id/status
Change comment visibility.
Request:
{
"status": "visible" | "hidden"
}
POST /api/v1/admin/comments/ban
Ban a user from commenting.
Request:
{
"user_id": 123,
"reason": "Porušení pravidel diskuse",
"duration_hours": 24
}
Duration:
0= Permanent (until NULL)>0= Temporary (until = now + hours)
Validation:
- Max 8760 hours (1 year)
- Reason required
GET /api/v1/admin/comments/bans
List active bans.
Query: WHERE until IS NULL OR until > NOW()
POST /api/v1/admin/comments/bans/:id/lift
End a ban early.
Process: Sets until = NOW(), making ban expired.
GET /api/v1/admin/comments/unban-requests
List unban appeals.
POST /api/v1/admin/comments/unban-requests/:id/resolve
Approve or reject unban request.
Request:
{
"action": "approve" | "reject"
}
Approve: Sets all user's bans to expired (until = NOW())
Reject: Updates request status only
Frontend Integration
Services
/frontend/src/services/comments.ts
Public comment operations.
Functions:
getComments(targetType, targetId, page, pageSize)createComment(body)updateComment(id, body)deleteComment(id)reactToComment(id, type)unreactToComment(id)reportComment(id, reason)createUnbanRequest(message)
/frontend/src/services/admin/comments.ts
Admin moderation operations.
Functions:
adminListComments(params)adminUpdateCommentStatus(id, status)adminBanUser(user_id, reason, duration_hours)adminListBans()adminLiftBan(id)adminListUnbanRequests()adminResolveUnban(id, action)
Utilities
/frontend/src/utils/commentsHelpers.ts
Key Functions:
formatCommentAge(createdAt)- Human-readable time ("před 5 minutami")getReactionEmoji(type)- Map type to emojigetReactionDisplayName(type)- Localized namecountTotalReactions(reactions)- Sum all reactionsshortenComment(content, maxLength)- Preview textvalidateCommentContent(content)- Client validationgetBanDurationText(until)- Format ban expirysortComments(comments, mode)- Threaded or chronological
Components
Recommended Structure:
/frontend/src/components/comments/
CommentsList.tsx - Main container
CommentItem.tsx - Single comment
CommentForm.tsx - Create/edit form
ReactionPicker.tsx - Reaction selector
ReportModal.tsx - Report dialog
BannedNotice.tsx - Ban notification
Example Usage:
<CommentsList
targetType="article"
targetId={articleId}
enableReplies={true}
enableReactions={true}
/>
Admin Page
/frontend/src/pages/admin/CommentsAdminPage.tsx
Features:
- Filters: Status, target type, user ID, reported-only
- Bulk Actions: Hide/show selected
- Quick Ban: One-click ban with modal
- Reports Queue: Highlighted comments with report count
- Unban Requests: Approve/reject appeals
UI Highlights:
- Spam score badge (color-coded)
- Reports badge (red if >2)
- Inline status toggle
- Delete confirmation
- Ban duration presets (24h, 7d, permanent)
Spam Protection
Spam Score Calculation
Implemented in /internal/services/spam_detection.go (assumed):
Factors:
- Link count: Each link adds 0.1
- Excessive caps: >50% uppercase adds 0.2
- Repeated characters: "aaaaaa" adds 0.15
- Short + links: <20 chars with links adds 0.3
- Blacklisted keywords: Each adds 0.25
Threshold:
- Score > 0.5 → Likely spam
- Score > 0.7 → Auto-hide
Bad Words Filter
Service: /internal/services/bad_words.go
Process:
- Load dictionary (Czech + English)
- Replace with asterisks: "řkv"
- Preserve word length
Example:
Input: "To je pěknej hov*o!"
Output: "To je pěknej ***!"
Sensitive Words Detection
Triggers manual review (auto-hide).
Categories:
- Hate speech
- Threats
- Explicit content
- Harassment
Action: Comment created with status = 'hidden', admin must approve.
Rate Limiting
Applied to comment endpoints:
middleware.RateLimit(20, time.Minute) // 20 comments per minute
middleware.RateLimit(60, time.Minute) // 60 reactions per minute
middleware.RateLimit(10, time.Hour) // 10 reports per hour
Prevents:
- Comment flooding
- Reaction spam
- Report abuse
Moderation Tools
Comment Status
Visible: Public, earns points
Hidden: Only admin sees, no points
Toggle via admin panel or bulk operations.
Spam Score Review
Admin view shows:
- Numeric score (0.00-1.00)
- Color badge (green/yellow/red)
- Triggered rules array
Example Rules:
["excessive_caps", "repeated_chars", "external_link"]
Report Queue
Prioritization:
- Comments with >2 reports highlighted red
- Sort by report count descending
- Show reporter count, not individual reports
Actions:
- Review comment content
- Check spam score
- Review author history
- Decision:
- Hide comment
- Ban user
- Dismiss (do nothing)
Bulk Actions
Future enhancement:
- Select multiple comments
- Apply status change to all
- Delete selected
Ban System
Types of Bans
Temporary:
- Duration in hours
- Auto-expires
- User can appeal early
Permanent:
- No expiry (
until = NULL) - User must appeal
- Admin approval required
Ban Enforcement
Check on Comment Create:
var activeBan models.CommentBan
err := db.Where("user_id = ? AND (until IS NULL OR until > ?)", userID, time.Now()).
First(&activeBan).Error
if err == nil {
return 403, "Your account is restricted from commenting"
}
Check on Comment Edit: Same logic prevents editing while banned.
Ban UI
Admin Panel:
- One-click ban button on comment
- Modal with:
- Reason field (required)
- Duration selector
- Quick presets (1h, 24h, 7d, permanent)
User Notification:
- API error response with ban info
- Frontend shows ban notice with:
- Reason
- Expiry (if temporary)
- Appeal button
Appeal Process
User Flow:
- See ban notice
- Click "Request Unban"
- Write apology/explanation
- Submit (rate-limited: 5/hour)
Admin Flow:
- Review unban requests table
- Check user's comment history
- Decision:
- Approve: Lift all bans
- Reject: Keep ban active
Email Notification (future):
- Notify user of decision
- Include reason for rejection
Reactions
Available Types
| Type | Emoji | Display Name | Use Case |
|---|---|---|---|
| like | 👍 | Líbí se | General agreement |
| heart | ❤️ | Srdíčko | Love/support |
| smile | 😊 | Úsměv | Friendly |
| laugh | 😂 | Smích | Funny |
| thumbs_up | 👍 | Palec nahoru | Approval |
| thumbs_down | 👎 | Palec dolů | Disapproval |
| sad | 😢 | Smutné | Sympathy |
| angry | 😠 | Naštvaný | Frustration |
Implementation
One reaction per user per comment (unique constraint).
Changing Reaction:
- User clicks new reaction
- Frontend calls
POST /comments/:id/reactwith new type - Backend deletes old reaction
- Backend creates new reaction
- Frontend updates UI instantly (optimistic)
Aggregation:
SELECT type, COUNT(*) as cnt
FROM comment_reactions
WHERE comment_id IN (...)
GROUP BY type
Returns:
{
"like": 5,
"heart": 2,
"laugh": 1
}
UI Display:
- Show top 3 reaction types
- Total count
- Highlight user's reaction
- Click to toggle
Admin Management
Dashboard
CommentsAdminPage.tsx provides:
Filters:
- Status (visible/hidden)
- Target type
- Target ID
- User ID
- Reported only toggle
Actions per Comment:
- Toggle visible/hidden
- Delete (with cascade)
- Ban user
- View user profile (future)
Batch Operations (future):
- Select multiple
- Bulk hide/show
- Bulk delete
Bans Management
Active Bans Table:
- User ID
- Reason
- Duration remaining
- Created by
- Actions: Lift ban
Lift Ban:
- Sets
until = NOW() - Comment immediately
Delete Ban:
- Hard delete (not recommended)
- User can comment again
Unban Requests
Queue Display:
- User ID
- Message (appeal text)
- Status (pending/approved/rejected)
- Created date
Actions:
- Approve → Lift all user's bans
- Reject → Update status, ban remains
Best Practices:
- Review user's full comment history
- Consider offense severity
- Check if multiple offenses
- Document decision reason
Production Checklist
Database
- Run migration
20251102000002_create_comments_system.up.sql - Verify indexes created
- Test foreign key constraints
- Confirm unique constraints work
Backend
- Comment controller implemented
- Spam detection service
- Bad words filter
- Ban checking on create/edit
- Rate limiting applied
- Validation helpers
Frontend
- Comments list component
- Comment form
- Reaction picker
- Report modal
- Admin moderation page
- Helpers & utilities
Security
- Input sanitization (XSS prevention)
- SQL injection protection (parameterized queries)
- Rate limiting (spam prevention)
- Ban enforcement
- CSRF protection
- Auth checks on edit/delete
Testing
- Post comment on article
- Reply to comment
- Edit own comment
- Delete own comment
- React to comment (change reaction)
- Report comment
- Admin hide comment
- Admin ban user (temporary)
- User appeal ban
- Admin approve unban
- Test spam detection
- Verify bad words filter
- Load test (pagination, 1000+ comments)
Configuration
- Review spam score thresholds
- Customize bad words dictionary
- Set sensitive words list
- Configure rate limits
- Review ban duration limits
Monitoring
- Track spam scores
- Monitor ban rate
- Review report queue
- Check false positives
- User feedback on filters
Best Practices
For Users
- Be respectful - Follow community guidelines
- No spam - Avoid excessive links, caps
- Report wisely - Use for genuine violations only
- Appeal fairly - Provide honest explanation
For Moderators
- Review context - Read full conversation
- Be consistent - Apply rules uniformly
- Document decisions - Use reason fields
- Respond promptly - Check queue daily
- Communicate - Explain bans clearly
For Developers
- Log everything - Track all moderation actions
- Preserve evidence - Don't hard-delete flagged content
- Monitor metrics - Spam rate, ban appeals, etc.
- Iterate filters - Update based on new patterns
- User feedback - Collect and review regularly
Future Enhancements
Phase 2
- Upvote/downvote separate from reactions
- Comment sorting (newest, oldest, top)
- Notification system (mentions, replies)
- Rich text support (links, formatting)
- Image attachments
Phase 3
- Moderator role (between fan and admin)
- Auto-mod rules (configurable triggers)
- Appeal workflow automation
- Comment analytics dashboard
- User reputation score
Integration Ideas
- Slack/Discord webhooks for reports
- ML-based spam detection
- Sentiment analysis
- Language detection
- Automated translation
Support
For issues or questions:
- Check spam score for false positives
- Review ban table for active restrictions
- Verify rate limits not blocking legitimate use
- Check email logs for notifications
- Consult transaction log for points issues
Migration Files:
database/migrations/20251102000002_create_comments_system.up.sqldatabase/migrations/20251102000002_create_comments_system.down.sql
Key Files:
- Backend:
internal/controllers/comment_controller.go - Backend:
internal/services/spam_detection.go(assumed) - Backend:
internal/services/bad_words.go(assumed) - Frontend:
frontend/src/pages/admin/CommentsAdminPage.tsx - Frontend:
frontend/src/services/comments.ts - Frontend:
frontend/src/services/admin/comments.ts - Utils:
frontend/src/utils/commentsHelpers.ts - Validation:
pkg/validation/comments.go - Helpers:
internal/helpers/comments_helpers.go
Last Updated: November 2, 2025
Status: Production Ready ✅