rcomments 0.1.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 RComments Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,6 @@
1
+ include LICENSE
2
+ include README.md
3
+ include .env.example
4
+ recursive-include rcomments *.py
5
+ recursive-include tests *.py
6
+ global-exclude *.py[cod] __pycache__ *.so *.dylib .DS_Store
@@ -0,0 +1,471 @@
1
+ Metadata-Version: 2.4
2
+ Name: rcomments
3
+ Version: 0.1.2
4
+ Summary: A minimal Flask + PostgreSQL commenting system with hierarchical threading, soft deletion, rate limiting, and XSS protection
5
+ Author-email: Rahul Naik <rnnutube@gmail.com>
6
+ License: MIT License
7
+
8
+ Copyright (c) 2024 RComments Contributors
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+
28
+ Project-URL: Homepage, https://github.com
29
+ Project-URL: Repository, https://github.com
30
+ Project-URL: Issues, https://github.com
31
+ Keywords: flask,postgresql,comments,forum,discussion
32
+ Classifier: Development Status :: 4 - Beta
33
+ Classifier: Intended Audience :: Developers
34
+ Classifier: License :: OSI Approved :: MIT License
35
+ Classifier: Programming Language :: Python :: 3
36
+ Classifier: Programming Language :: Python :: 3.8
37
+ Classifier: Programming Language :: Python :: 3.9
38
+ Classifier: Programming Language :: Python :: 3.10
39
+ Classifier: Programming Language :: Python :: 3.11
40
+ Classifier: Framework :: Flask
41
+ Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
42
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
43
+ Requires-Python: >=3.8
44
+ Description-Content-Type: text/markdown
45
+ License-File: LICENSE
46
+ Requires-Dist: Flask>=2.0.0
47
+ Requires-Dist: Flask-SQLAlchemy>=3.0.0
48
+ Requires-Dist: Flask-Migrate>=4.0.0
49
+ Requires-Dist: psycopg2-binary>=2.9.0
50
+ Requires-Dist: python-dotenv>=0.21.0
51
+ Requires-Dist: email-validator>=2.0.0
52
+ Requires-Dist: bleach>=6.0.0
53
+ Requires-Dist: limits>=3.0.0
54
+ Requires-Dist: flask-limiter>=3.0.0
55
+ Provides-Extra: dev
56
+ Requires-Dist: pytest>=7.0.0; extra == "dev"
57
+ Requires-Dist: pytest-flask>=1.2.0; extra == "dev"
58
+ Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
59
+ Requires-Dist: black>=23.0.0; extra == "dev"
60
+ Requires-Dist: flake8>=6.0.0; extra == "dev"
61
+ Requires-Dist: mypy>=1.0.0; extra == "dev"
62
+ Dynamic: license-file
63
+
64
+ # RComments
65
+
66
+ A minimal, secure, and feature-rich commenting system for Flask + PostgreSQL applications.
67
+
68
+ ## Features
69
+
70
+ - **Hierarchical Threading**: Support for nested replies with unlimited depth
71
+ - **Flexible Identity**: Registered usernames or anonymous commenting
72
+ - **Metadata Tracking**: Automatic UTC timestamps and IP logging for moderation
73
+ - **XSS Protection**: HTML sanitization to prevent injection attacks
74
+ - **Rate Limiting**: Configurable limits to prevent spam
75
+ - **Soft Deletion**: Comments can be marked as deleted without permanent removal
76
+ - **Automatic Cleanup**: Permanently removes old comments (30 days or 150 comments limit)
77
+ - **Easy Integration**: Simple Flask blueprint registration
78
+
79
+ ## Installation
80
+
81
+ ```bash
82
+ pip install rcomments
83
+ ```
84
+
85
+
86
+
87
+ ## Quick Start
88
+
89
+ ### 1. Configuration
90
+
91
+ Create a `.env` file in your project root:
92
+
93
+ ```env
94
+ DATABASE_URL=postgresql://username:password@localhost/yourdb
95
+ SECRET_KEY=your-flask-secret-key
96
+ RATE_LIMIT=10 per minute
97
+ MAX_COMMENT_AGE_DAYS=30
98
+ MAX_TOTAL_COMMENTS=150
99
+ ALLOW_ANONYMOUS=true
100
+ ```
101
+
102
+ ### 2. Flask Application Setup
103
+
104
+ ```python
105
+ from flask import Flask
106
+ from flask_sqlalchemy import SQLAlchemy
107
+ from flask_limiter import Limiter
108
+ from rcomments import init_config, init_routes, db as rcomments_db
109
+
110
+ app = Flask(__name__)
111
+ app.config.from_prefixed_env() # Loads from .env
112
+
113
+ # Initialize RComments
114
+ init_config()
115
+
116
+ # Setup database (use your existing SQLAlchemy instance or create a new one)
117
+ db = SQLAlchemy(app)
118
+ rcomments_db.init_app(app)
119
+
120
+ # Setup rate limiter
121
+ limiter = Limiter(
122
+ app,
123
+ key_func=lambda: request.headers.get("X-Forwarded-For", request.remote_addr)
124
+ )
125
+
126
+ # Initialize routes
127
+ init_routes(app, limiter)
128
+
129
+ if __name__ == "__main__":
130
+ app.run(debug=True)
131
+ ```
132
+
133
+ ### 3. Database Migration
134
+
135
+ Initialize the database:
136
+
137
+ ```bash
138
+ # Using Flask-Migrate (recommended)
139
+ flask db init
140
+ flask db migrate -m "Initial RComments tables"
141
+ flask db upgrade
142
+
143
+ # Or using the CLI
144
+ rcomments-cli init-db
145
+ ```
146
+
147
+ ## API Endpoints
148
+
149
+ All endpoints are prefixed with `/api/comments`.
150
+
151
+ ### GET `/api/comments`
152
+
153
+ Fetch all top-level comments with nested replies.
154
+
155
+ **Query Parameters:**
156
+ - `sort` (optional): `asc` or `desc` (default: `desc`)
157
+ - `limit` (optional): Maximum number of top-level comments
158
+
159
+ **Response:**
160
+ ```json
161
+ {
162
+ "success": true,
163
+ "data": [
164
+ {
165
+ "id": 1,
166
+ "content": "This is a comment",
167
+ "author_name": "John",
168
+ "is_anonymous": false,
169
+ "status": "ACTIVE",
170
+ "created_at": "2024-01-15T10:30:00Z",
171
+ "parent_id": null,
172
+ "replies": []
173
+ }
174
+ ],
175
+ "count": 1
176
+ }
177
+ ```
178
+
179
+ ### POST `/api/comments`
180
+
181
+ Create a new comment.
182
+
183
+ **Request Body:**
184
+ ```json
185
+ {
186
+ "content": "Your comment text",
187
+ "author_name": "Your Name",
188
+ "author_email": "email@example.com", // optional
189
+ "parent_id": 1, // optional, for replies
190
+ "is_anonymous": false // optional, default: false
191
+ }
192
+ ```
193
+
194
+ **Response:**
195
+ ```json
196
+ {
197
+ "success": true,
198
+ "data": {
199
+ "id": 2,
200
+ "content": "Your comment text",
201
+ "author_name": "Your Name",
202
+ "is_anonymous": false,
203
+ "status": "ACTIVE",
204
+ "created_at": "2024-01-15T10:35:00Z",
205
+ "parent_id": 1
206
+ },
207
+ "message": "Comment created successfully"
208
+ }
209
+ ```
210
+
211
+ ### GET `/api/comments/<id>`
212
+
213
+ Fetch a single comment with its replies.
214
+
215
+ ### DELETE `/api/comments/<id>`
216
+
217
+ Soft delete a comment (marks as `[Comment removed]`).
218
+
219
+ ### GET `/api/comments/<id>/replies`
220
+
221
+ Fetch direct replies for a comment.
222
+
223
+ ### GET `/api/comments/stats`
224
+
225
+ Get comment statistics.
226
+
227
+ ### POST `/api/comments/cleanup`
228
+
229
+ Trigger cleanup of old comments (admin only - secure this in production!).
230
+
231
+ **Query Parameters:**
232
+ - `dry_run=true` - Preview what would be deleted
233
+
234
+ ## Security Features
235
+
236
+ ### XSS Protection
237
+
238
+ All comment content is automatically sanitized using `bleach`. Only basic formatting tags are allowed:
239
+ - `<b>`, `<i>`, `<u>`, `<em>`, `<strong>`, `<br>`, `<p>`
240
+
241
+ ### Rate Limiting
242
+
243
+ Configurable rate limits prevent automated spamming. Default: 10 comments per minute per IP.
244
+
245
+ ### IP Logging
246
+
247
+ IP addresses are hashed (SHA-256) for privacy while maintaining moderation capabilities.
248
+
249
+ ### Soft Deletion
250
+
251
+ 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.
252
+
253
+ ### Automatic Cleanup
254
+
255
+ The cleanup process permanently removes comments from the database based on two limits:
256
+ 1. **Age**: Comments older than 30 days (configurable) are **permanently deleted**
257
+ 2. **Count**: If total active comments exceed 150 (configurable), oldest comments are **permanently deleted**
258
+
259
+ **Note**: Cleanup removes both top-level comments and replies. The `dry_run` mode allows previewing what would be deleted without making changes.
260
+
261
+ ## Integration with React/TypeScript
262
+
263
+ Example React component:
264
+
265
+ ```tsx
266
+ import React, { useState, useEffect } from 'react';
267
+ import axios from 'axios';
268
+
269
+ interface Comment {
270
+ id: number;
271
+ content: string;
272
+ author_name: string;
273
+ is_anonymous: boolean;
274
+ created_at: string;
275
+ parent_id: number | null;
276
+ replies: Comment[];
277
+ }
278
+
279
+ const CommentSection: React.FC = () => {
280
+ const [comments, setComments] = useState<Comment[]>([]);
281
+ const [newComment, setNewComment] = useState({
282
+ content: '',
283
+ author_name: '',
284
+ author_email: '',
285
+ is_anonymous: false,
286
+ parent_id: null as number | null
287
+ });
288
+
289
+ useEffect(() => {
290
+ fetchComments();
291
+ }, []);
292
+
293
+ const fetchComments = async () => {
294
+ const response = await axios.get('/api/comments');
295
+ setComments(response.data.data);
296
+ };
297
+
298
+ const submitComment = async (e: React.FormEvent) => {
299
+ e.preventDefault();
300
+ await axios.post('/api/comments', newComment);
301
+ setNewComment({ content: '', author_name: '', author_email: '', is_anonymous: false, parent_id: null });
302
+ fetchComments();
303
+ };
304
+
305
+ const deleteComment = async (id: number) => {
306
+ await axios.delete(`/api/comments/${id}`);
307
+ fetchComments();
308
+ };
309
+
310
+ return (
311
+ <div className="comment-section">
312
+ <h2>Comments</h2>
313
+
314
+ <form onSubmit={submitComment}>
315
+ <textarea
316
+ value={newComment.content}
317
+ onChange={(e) => setNewComment({...newComment, content: e.target.value})}
318
+ placeholder="Your comment..."
319
+ required
320
+ />
321
+ <input
322
+ type="text"
323
+ value={newComment.author_name}
324
+ onChange={(e) => setNewComment({...newComment, author_name: e.target.value})}
325
+ placeholder="Your name"
326
+ required
327
+ />
328
+ <input
329
+ type="email"
330
+ value={newComment.author_email}
331
+ onChange={(e) => setNewComment({...newComment, author_email: e.target.value})}
332
+ placeholder="Email (optional)"
333
+ />
334
+ <label>
335
+ <input
336
+ type="checkbox"
337
+ checked={newComment.is_anonymous}
338
+ onChange={(e) => setNewComment({...newComment, is_anonymous: e.target.checked})}
339
+ />
340
+ Post anonymously
341
+ </label>
342
+ <button type="submit">Post Comment</button>
343
+ </form>
344
+
345
+ <div className="comments-list">
346
+ {comments.map(comment => (
347
+ <CommentItem
348
+ key={comment.id}
349
+ comment={comment}
350
+ onReply={(parentId) => setNewComment({
351
+ ...newComment,
352
+ parent_id: parentId,
353
+ content: '',
354
+ })}
355
+ onDelete={deleteComment}
356
+ />
357
+ ))}
358
+ </div>
359
+ </div>
360
+ );
361
+ };
362
+
363
+ const CommentItem: React.FC<{ comment: Comment; onReply: (id: number) => void; onDelete: (id: number) => void }> = ({
364
+ comment,
365
+ onReply,
366
+ onDelete
367
+ }) => {
368
+ const [showReplyForm, setShowReplyForm] = useState(false);
369
+
370
+ return (
371
+ <div className="comment" style={{ marginLeft: comment.parent_id ? '20px' : '0' }}>
372
+ <div className="comment-header">
373
+ <strong>{comment.author_name}</strong>
374
+ {comment.is_anonymous && <span> (Anonymous)</span>}
375
+ <small>{new Date(comment.created_at).toLocaleString()}</small>
376
+ </div>
377
+ <div className="comment-content" dangerouslySetInnerHTML={{ __html: comment.content }} />
378
+ <div className="comment-actions">
379
+ <button onClick={() => setShowReplyForm(!showReplyForm)}>Reply</button>
380
+ <button onClick={() => onDelete(comment.id)}>Delete</button>
381
+ </div>
382
+
383
+ {showReplyForm && (
384
+ <form onSubmit={(e) => { e.preventDefault(); onReply(comment.id); }}>
385
+ <textarea placeholder="Your reply..." required />
386
+ <button type="submit">Post Reply</button>
387
+ </form>
388
+ )}
389
+
390
+ {comment.replies && comment.replies.length > 0 && (
391
+ <div className="replies">
392
+ {comment.replies.map(reply => (
393
+ <CommentItem
394
+ key={reply.id}
395
+ comment={reply}
396
+ onReply={onReply}
397
+ onDelete={onDelete}
398
+ />
399
+ ))}
400
+ </div>
401
+ )}
402
+ </div>
403
+ );
404
+ };
405
+
406
+ export default CommentSection;
407
+ ```
408
+
409
+ ## Command Line Interface
410
+
411
+ RComments includes a CLI for database management:
412
+
413
+ ```bash
414
+ # Initialize database and migrations
415
+ rcomments-cli init-db
416
+
417
+ # Create and apply migrations
418
+ rcomments-cli migrate-db
419
+
420
+ # Clean up old comments
421
+ rcomments-cli cleanup [--dry-run]
422
+
423
+ # Show statistics
424
+ rcomments-cli stats
425
+ ```
426
+
427
+ ## Configuration Options
428
+
429
+ | Variable | Default | Description |
430
+ |----------|---------|-------------|
431
+ | `DATABASE_URL` | `postgresql://localhost/rcomments` | PostgreSQL connection string |
432
+ | `SECRET_KEY` | `dev-secret-key-change-in-production` | Flask secret key |
433
+ | `RATE_LIMIT` | `10 per minute` | Rate limit format |
434
+ | `MAX_COMMENT_AGE_DAYS` | `30` | Maximum age of comments in days |
435
+ | `MAX_TOTAL_COMMENTS` | `150` | Maximum total active comments |
436
+ | `ALLOW_ANONYMOUS` | `true` | Allow anonymous comments |
437
+ | `REQUIRE_EMAIL_ANONYMOUS` | `false` | Require email for anonymous users |
438
+
439
+ ## Development
440
+
441
+ ### Setup
442
+
443
+ ```bash
444
+
445
+ cd rcomments
446
+ python -m venv venv
447
+ source venv/bin/activate # On Windows: venv\Scripts\activate
448
+ pip install -e ".[dev]"
449
+ ```
450
+
451
+ ### Running Tests
452
+
453
+ ```bash
454
+ pytest tests/ -v
455
+ ```
456
+
457
+ ### Code Formatting
458
+
459
+ ```bash
460
+ black rcomments/
461
+ flake8 rcomments/
462
+ mypy rcomments/
463
+ ```
464
+
465
+ ## License
466
+
467
+ MIT License. See [LICENSE](LICENSE) for details.
468
+
469
+
470
+
471
+