Skip to main content

A minimal Flask + PostgreSQL commenting system with hierarchical threading, soft deletion, rate limiting, and XSS protection

Project description

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.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

rcomments-0.1.3.tar.gz (21.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

rcomments-0.1.3-py3-none-any.whl (15.8 kB view details)

Uploaded Python 3

File details

Details for the file rcomments-0.1.3.tar.gz.

File metadata

  • Download URL: rcomments-0.1.3.tar.gz
  • Upload date:
  • Size: 21.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.3

File hashes

Hashes for rcomments-0.1.3.tar.gz
Algorithm Hash digest
SHA256 ca74b6db5a46bc2694cadf85e65b088edb0f2f18e8205aab940d30e32572a9ea
MD5 b490f001a2c920d8cc42db265aeb16ea
BLAKE2b-256 aa2bffe784a58c4b219cd75075c4e7afe3344541c70f913dd876f5548fc215c2

See more details on using hashes here.

File details

Details for the file rcomments-0.1.3-py3-none-any.whl.

File metadata

  • Download URL: rcomments-0.1.3-py3-none-any.whl
  • Upload date:
  • Size: 15.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.3

File hashes

Hashes for rcomments-0.1.3-py3-none-any.whl
Algorithm Hash digest
SHA256 429e5b4c8982a30995fa85e84c395179d780c583e3c3b6d05dfa7e37de880bc6
MD5 c6eaba68b54ef5df9a04f9a9f54441cf
BLAKE2b-256 2f759d6eb5352c269aadfc4ad6b13dfc7b617f1524b5d5883b15c7475d82c3b9

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page