RComments
A minimal, secure, and feature-rich commenting system for Flask + PostgreSQL applications.
Features
- Hierarchical Threading: Support for nested replies with unlimited depth
- Flexible Identity: Registered usernames or anonymous commenting
- Metadata Tracking: Automatic UTC timestamps and IP logging for moderation
- XSS Protection: HTML sanitization to prevent injection attacks
- Rate Limiting: Configurable limits to prevent spam
- Soft Deletion: Comments can be marked as deleted without permanent removal
- Automatic Cleanup: Permanently removes old comments (30 days or 150 comments limit)
- Easy Integration: Simple Flask blueprint registration
Installation
pip install rcomments
Quick Start
1. Configuration
Create a .env file in your project root:
DATABASE_URL=postgresql://username:password@localhost/yourdb
SECRET_KEY=your-flask-secret-key
RATE_LIMIT=10 per minute
MAX_COMMENT_AGE_DAYS=30
MAX_TOTAL_COMMENTS=150
ALLOW_ANONYMOUS=true
2. Flask Application Setup
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_limiter import Limiter
from rcomments import init_config, init_routes, db as rcomments_db
app = Flask(__name__)
app.config.from_prefixed_env() # Loads from .env
# Initialize RComments
init_config()
# Setup database - rcomments uses its own db instance
# Configure your database via DATABASE_URL in .env
rcomments_db.init_app(app)
# Setup rate limiter
limiter = Limiter(
app,
key_func=lambda: request.headers.get("X-Forwarded-For", request.remote_addr)
)
# Initialize routes
init_routes(app, limiter)
if __name__ == "__main__":
app.run(debug=True)
3. Database Migration
Initialize the database:
# Using Flask-Migrate (recommended)
flask db init
flask db migrate -m "Initial RComments tables"
flask db upgrade
# Or using the CLI
rcomments-cli init-db
API Endpoints
All endpoints are prefixed with /api/comments.
GET /api/comments
Fetch all top-level comments with nested replies.
Query Parameters:
sort(optional):ascordesc(default:desc)limit(optional): Maximum number of top-level comments
Response:
{
"success": true,
"data": [
{
"id": 1,
"content": "This is a comment",
"author_name": "John",
"is_anonymous": false,
"status": "ACTIVE",
"created_at": "2024-01-15T10:30:00Z",
"parent_id": null,
"replies": []
}
],
"count": 1
}
POST /api/comments
Create a new comment.
Request Body:
{
"content": "Your comment text",
"author_name": "Your Name",
"author_email": "email@example.com", // optional
"parent_id": 1, // optional, for replies
"is_anonymous": false // optional, default: false
}
Response:
{
"success": true,
"data": {
"id": 2,
"content": "Your comment text",
"author_name": "Your Name",
"is_anonymous": false,
"status": "ACTIVE",
"created_at": "2024-01-15T10:35:00Z",
"parent_id": 1
},
"message": "Comment created successfully"
}
GET /api/comments/<id>
Fetch a single comment with its replies.
DELETE /api/comments/<id>
Soft delete a comment (marks as [Comment removed]).
GET /api/comments/<id>/replies
Fetch direct replies for a comment.
GET /api/comments/stats
Get comment statistics.
POST /api/comments/cleanup
Trigger cleanup of old comments (admin only - secure this in production!).
Query Parameters:
dry_run=true- Preview what would be deleted
Security Features
XSS Protection
All comment content is automatically sanitized using bleach. Only basic formatting tags are allowed:
<b>,<i>,<u>,<em>,<strong>,<br>,<p>
Rate Limiting
Configurable rate limits prevent automated spamming. Default: 10 comments per minute per IP.
IP Logging
IP addresses are hashed (SHA-256) for privacy while maintaining moderation capabilities.
Soft Deletion
Comments can be soft-deleted via the DELETE endpoint, which marks them with status DELETED and replaces content with [Comment removed]. This preserves thread structure while hiding content.
Automatic Cleanup
The cleanup process permanently removes comments from the database based on two limits:
- Age: Comments older than 30 days (configurable) are permanently deleted
- Count: If total active comments exceed 150 (configurable), oldest comments are permanently deleted
Note: Cleanup removes both top-level comments and replies. The dry_run mode allows previewing what would be deleted without making changes.
Integration with React/TypeScript
Example React component:
import React, { useState, useEffect } from 'react';
import axios from 'axios';
interface Comment {
id: number;
content: string;
author_name: string;
is_anonymous: boolean;
created_at: string;
parent_id: number | null;
replies: Comment[];
}
const CommentSection: React.FC = () => {
const [comments, setComments] = useState<Comment[]>([]);
const [newComment, setNewComment] = useState({
content: '',
author_name: '',
author_email: '',
is_anonymous: false,
parent_id: null as number | null
});
useEffect(() => {
fetchComments();
}, []);
const fetchComments = async () => {
const response = await axios.get('/api/comments');
setComments(response.data.data);
};
const submitComment = async (e: React.FormEvent) => {
e.preventDefault();
await axios.post('/api/comments', newComment);
setNewComment({ content: '', author_name: '', author_email: '', is_anonymous: false, parent_id: null });
fetchComments();
};
const deleteComment = async (id: number) => {
await axios.delete(`/api/comments/${id}`);
fetchComments();
};
return (
<div className="comment-section">
<h2>Comments</h2>
<form onSubmit={submitComment}>
<textarea
value={newComment.content}
onChange={(e) => setNewComment({...newComment, content: e.target.value})}
placeholder="Your comment..."
required
/>
<input
type="text"
value={newComment.author_name}
onChange={(e) => setNewComment({...newComment, author_name: e.target.value})}
placeholder="Your name"
required
/>
<input
type="email"
value={newComment.author_email}
onChange={(e) => setNewComment({...newComment, author_email: e.target.value})}
placeholder="Email (optional)"
/>
<label>
<input
type="checkbox"
checked={newComment.is_anonymous}
onChange={(e) => setNewComment({...newComment, is_anonymous: e.target.checked})}
/>
Post anonymously
</label>
<button type="submit">Post Comment</button>
</form>
<div className="comments-list">
{comments.map(comment => (
<CommentItem
key={comment.id}
comment={comment}
onReply={(parentId) => setNewComment({
...newComment,
parent_id: parentId,
content: '',
})}
onDelete={deleteComment}
/>
))}
</div>
</div>
);
};
const CommentItem: React.FC<{ comment: Comment; onReply: (id: number) => void; onDelete: (id: number) => void }> = ({
comment,
onReply,
onDelete
}) => {
const [showReplyForm, setShowReplyForm] = useState(false);
return (
<div className="comment" style={{ marginLeft: comment.parent_id ? '20px' : '0' }}>
<div className="comment-header">
<strong>{comment.author_name}</strong>
{comment.is_anonymous && <span> (Anonymous)</span>}
<small>{new Date(comment.created_at).toLocaleString()}</small>
</div>
<div className="comment-content" dangerouslySetInnerHTML={{ __html: comment.content }} />
<div className="comment-actions">
<button onClick={() => setShowReplyForm(!showReplyForm)}>Reply</button>
<button onClick={() => onDelete(comment.id)}>Delete</button>
</div>
{showReplyForm && (
<form onSubmit={(e) => { e.preventDefault(); onReply(comment.id); }}>
<textarea placeholder="Your reply..." required />
<button type="submit">Post Reply</button>
</form>
)}
{comment.replies && comment.replies.length > 0 && (
<div className="replies">
{comment.replies.map(reply => (
<CommentItem
key={reply.id}
comment={reply}
onReply={onReply}
onDelete={onDelete}
/>
))}
</div>
)}
</div>
);
};
export default CommentSection;
Command Line Interface
RComments includes a CLI for database management:
# Initialize database and migrations
rcomments-cli init-db
# Create and apply migrations
rcomments-cli migrate-db
# Clean up old comments
rcomments-cli cleanup [--dry-run]
# Show statistics
rcomments-cli stats
Configuration Options
| Variable | Default | Description |
|---|---|---|
DATABASE_URL |
postgresql://localhost/rcomments |
PostgreSQL connection string |
SECRET_KEY |
dev-secret-key-change-in-production |
Flask secret key |
RATE_LIMIT |
10 per minute |
Rate limit format |
MAX_COMMENT_AGE_DAYS |
30 |
Maximum age of comments in days |
MAX_TOTAL_COMMENTS |
150 |
Maximum total active comments |
ALLOW_ANONYMOUS |
true |
Allow anonymous comments |
REQUIRE_EMAIL_ANONYMOUS |
false |
Require email for anonymous users |
Development
Setup
cd rcomments
python -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activate
pip install -e ".[dev]"
Running Tests
pytest tests/ -v
Code Formatting
black rcomments/
flake8 rcomments/
mypy rcomments/
License
MIT License. See LICENSE for details.
Metadata
Release files for rcomments 0.1.3
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| rcomments-0.1.3.tar.gz | 21.6 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| rcomments-0.1.3-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 37.4 kB
Release files / rcomments-0.1.3.tar.gz
| Download URL | rcomments-0.1.3.tar.gz |
|---|---|
| Size | 21.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
ca74b6db5a46bc2694cadf85e65b088edb0f2f18e8205aab940d30e32572a9ea
|
|
BLAKE2b-256 checksum How to use checksums |
aa2bffe784a58c4b219cd75075c4e7afe3344541c70f913dd876f5548fc215c2
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.14.3
|
Release files / rcomments-0.1.3-py3-none-any.whl
| Download URL | rcomments-0.1.3-py3-none-any.whl |
|---|---|
| Size | 15.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
429e5b4c8982a30995fa85e84c395179d780c583e3c3b6d05dfa7e37de880bc6
|
|
BLAKE2b-256 checksum How to use checksums |
2f759d6eb5352c269aadfc4ad6b13dfc7b617f1524b5d5883b15c7475d82c3b9
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.14.3
|
Security for rcomments 0.1.3
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.