Skip to main content

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): asc or desc (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:

  1. Age: Comments older than 30 days (configurable) are permanently deleted
  2. 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)

Source distribution for rcomments 0.1.3
File Size Uploaded Provenance
rcomments-0.1.3.tar.gz 21.6 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for rcomments 0.1.3
File Interpreter ABI Platform
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.

Release history Release notifications | RSS feed

This release

0.1.3 This release

2 release files No provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page