Add comment ratelimiting, allow any member to open threads

This commit is contained in:
rubenwardy 2019-01-28 19:01:37 +00:00
parent 2691105513
commit 8afe17b984
5 changed files with 64 additions and 27 deletions

View File

@ -20,10 +20,9 @@ from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate from flask_migrate import Migrate
from urllib.parse import urlparse from urllib.parse import urlparse
from app import app, gravatar from app import app, gravatar
from datetime import datetime
from sqlalchemy.orm import validates from sqlalchemy.orm import validates
from flask_user import login_required, UserManager, UserMixin, SQLAlchemyAdapter from flask_user import login_required, UserManager, UserMixin, SQLAlchemyAdapter
import enum import enum, datetime
# Initialise database # Initialise database
db = SQLAlchemy(app) db = SQLAlchemy(app)
@ -129,8 +128,6 @@ class User(db.Model, UserMixin):
replies = db.relationship("ThreadReply", backref="author", lazy="dynamic") replies = db.relationship("ThreadReply", backref="author", lazy="dynamic")
def __init__(self, username, active=False, email=None, password=None): def __init__(self, username, active=False, email=None, password=None):
import datetime
self.username = username self.username = username
self.confirmed_at = datetime.datetime.now() - datetime.timedelta(days=6000) self.confirmed_at = datetime.datetime.now() - datetime.timedelta(days=6000)
self.display_name = username self.display_name = username
@ -172,6 +169,16 @@ class User(db.Model, UserMixin):
else: else:
raise Exception("Permission {} is not related to users".format(perm.name)) raise Exception("Permission {} is not related to users".format(perm.name))
def canCommentRL(self):
hour_ago = datetime.datetime.utcnow() - datetime.timedelta(hours=1)
return ThreadReply.query.filter_by(author=self) \
.filter(ThreadReply.created_at > hour_ago).count() < 4
def canOpenThreadRL(self):
hour_ago = datetime.datetime.utcnow() - datetime.timedelta(hours=1)
return Thread.query.filter_by(author=self) \
.filter(Thread.created_at > hour_ago).count() < 2
class UserEmailVerification(db.Model): class UserEmailVerification(db.Model):
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey("user.id")) user_id = db.Column(db.Integer, db.ForeignKey("user.id"))
@ -347,7 +354,7 @@ class Package(db.Model):
shortDesc = db.Column(db.String(200), nullable=False) shortDesc = db.Column(db.String(200), nullable=False)
desc = db.Column(db.Text, nullable=True) desc = db.Column(db.Text, nullable=True)
type = db.Column(db.Enum(PackageType)) type = db.Column(db.Enum(PackageType))
created_at = db.Column(db.DateTime, nullable=False, default=datetime.utcnow) created_at = db.Column(db.DateTime, nullable=False, default=datetime.datetime.utcnow)
license_id = db.Column(db.Integer, db.ForeignKey("license.id"), nullable=False, default=1) license_id = db.Column(db.Integer, db.ForeignKey("license.id"), nullable=False, default=1)
license = db.relationship("License", foreign_keys=[license_id]) license = db.relationship("License", foreign_keys=[license_id])
@ -496,8 +503,11 @@ class Package(db.Model):
isOwner = user == self.author isOwner = user == self.author
if perm == Permission.CREATE_THREAD:
return user.rank.atLeast(UserRank.MEMBER)
# Members can edit their own packages, and editors can edit any packages # Members can edit their own packages, and editors can edit any packages
if perm == Permission.MAKE_RELEASE or perm == Permission.ADD_SCREENSHOTS or perm == Permission.CREATE_THREAD: if perm == Permission.MAKE_RELEASE or perm == Permission.ADD_SCREENSHOTS:
return isOwner or user.rank.atLeast(UserRank.EDITOR) return isOwner or user.rank.atLeast(UserRank.EDITOR)
if perm == Permission.EDIT_PACKAGE or perm == Permission.APPROVE_CHANGES: if perm == Permission.EDIT_PACKAGE or perm == Permission.APPROVE_CHANGES:
@ -522,8 +532,6 @@ class Package(db.Model):
raise Exception("Permission {} is not related to packages".format(perm.name)) raise Exception("Permission {} is not related to packages".format(perm.name))
def recalcScore(self): def recalcScore(self):
import datetime
self.score = 10 self.score = 10
if self.forums is not None: if self.forums is not None:
@ -630,7 +638,7 @@ class PackageRelease(db.Model):
def __init__(self): def __init__(self):
self.releaseDate = datetime.now() self.releaseDate = datetime.datetime.now()
class PackageReview(db.Model): class PackageReview(db.Model):
@ -762,7 +770,7 @@ class Thread(db.Model):
title = db.Column(db.String(100), nullable=False) title = db.Column(db.String(100), nullable=False)
private = db.Column(db.Boolean, server_default="0") private = db.Column(db.Boolean, server_default="0")
created_at = db.Column(db.DateTime, nullable=False, default=datetime.utcnow) created_at = db.Column(db.DateTime, nullable=False, default=datetime.datetime.utcnow)
replies = db.relationship("ThreadReply", backref="thread", lazy="dynamic") replies = db.relationship("ThreadReply", backref="thread", lazy="dynamic")
@ -800,7 +808,7 @@ class ThreadReply(db.Model):
thread_id = db.Column(db.Integer, db.ForeignKey("thread.id"), nullable=False) thread_id = db.Column(db.Integer, db.ForeignKey("thread.id"), nullable=False)
comment = db.Column(db.String(500), nullable=False) comment = db.Column(db.String(500), nullable=False)
author_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False) author_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False)
created_at = db.Column(db.DateTime, nullable=False, default=datetime.utcnow) created_at = db.Column(db.DateTime, nullable=False, default=datetime.datetime.utcnow)
REPO_BLACKLIST = [".zip", "mediafire.com", "dropbox.com", "weebly.com", \ REPO_BLACKLIST = [".zip", "mediafire.com", "dropbox.com", "weebly.com", \
@ -824,7 +832,7 @@ class ForumTopic(db.Model):
posts = db.Column(db.Integer, nullable=False) posts = db.Column(db.Integer, nullable=False)
views = db.Column(db.Integer, nullable=False) views = db.Column(db.Integer, nullable=False)
created_at = db.Column(db.DateTime, nullable=False, default=datetime.utcnow) created_at = db.Column(db.DateTime, nullable=False, default=datetime.datetime.utcnow)
def getRepoURL(self): def getRepoURL(self):
if self.link is None: if self.link is None:

View File

@ -42,11 +42,18 @@
<a name="reply"></a> <a name="reply"></a>
</div> </div>
<form method="post" action="{{ url_for('thread_page', id=thread.id)}}" class="card-body"> {% if current_user.canCommentRL() %}
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" /> <form method="post" action="{{ url_for('thread_page', id=thread.id)}}" class="card-body">
<textarea class="form-control markdown" required maxlength=500 name="comment"></textarea><br /> <input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<input class="btn btn-primary" type="submit" value="Comment" /> <textarea class="form-control markdown" required maxlength=500 name="comment"></textarea><br />
</form> <input class="btn btn-primary" type="submit" value="Comment" />
</form>
{% else %}
<div class="card-body">
<textarea class="form-control" readonly disabled>Please wait before commenting again.</textarea><br />
<input class="btn btn-primary" type="submit" disabled value="Comment" />
</div>
{% endif %}
</div> </div>
</div> </div>
</div> </div>

View File

@ -21,6 +21,8 @@ from app import app
from app.models import * from app.models import *
from app.utils import triggerNotif, clearNotifications from app.utils import triggerNotif, clearNotifications
import datetime
from flask_wtf import FlaskForm from flask_wtf import FlaskForm
from wtforms import * from wtforms import *
from wtforms.validators import * from wtforms.validators import *
@ -78,6 +80,13 @@ def thread_page(id):
if current_user.is_authenticated and request.method == "POST": if current_user.is_authenticated and request.method == "POST":
comment = request.form["comment"] comment = request.form["comment"]
if not current_user.canCommentRL():
flash("Please wait before commenting again", "danger")
if package:
return redirect(package.getDetailsURL())
else:
return redirect(url_for("home_page"))
if len(comment) <= 500 and len(comment) > 3: if len(comment) <= 500 and len(comment) > 3:
reply = ThreadReply() reply = ThreadReply()
reply.author = current_user reply.author = current_user
@ -126,15 +135,15 @@ def new_thread_page():
if package is None: if package is None:
flash("Unable to find that package!", "error") flash("Unable to find that package!", "error")
# Don't allow making threads on approved packages for now # Don't allow making orphan threads on approved packages for now
if package is None: if package is None:
abort(403) abort(403)
def_is_private = request.args.get("private") or False def_is_private = request.args.get("private") or False
if not package.approved: if package is None or not package.approved:
def_is_private = True def_is_private = True
allow_change = package.approved allow_change = package and package.approved
is_review_thread = package is not None and not package.approved is_review_thread = package and not package.approved
# Check that user can make the thread # Check that user can make the thread
if not package.checkPerm(current_user, Permission.CREATE_THREAD): if not package.checkPerm(current_user, Permission.CREATE_THREAD):
@ -144,8 +153,15 @@ def new_thread_page():
# Only allow creating one thread when not approved # Only allow creating one thread when not approved
elif is_review_thread and package.review_thread is not None: elif is_review_thread and package.review_thread is not None:
flash("A review thread already exists!", "error") flash("A review thread already exists!", "error")
if request.method == "GET": return redirect(url_for("thread_page", id=package.review_thread.id))
return redirect(url_for("thread_page", id=package.review_thread.id))
elif not current_user.canOpenThreadRL():
flash("Please wait before opening another thread", "danger")
if package:
return redirect(package.getDetailsURL())
else:
return redirect(url_for("home_page"))
# Set default values # Set default values
elif request.method == "GET": elif request.method == "GET":
@ -178,9 +194,15 @@ def new_thread_page():
if is_review_thread: if is_review_thread:
package.review_thread = thread package.review_thread = thread
notif_msg = None
if package is not None: if package is not None:
triggerNotif(package.author, current_user, notif_msg = "New thread '{}' on package {}".format(thread.title, package.title)
"New thread '{}' on package {}".format(thread.title, package.title), url_for("thread_page", id=thread.id)) triggerNotif(package.author, current_user, notif_msg, url_for("thread_page", id=thread.id))
else:
notif_msg = "New thread '{}'".format(thread.title)
for user in User.query.filter(User.rank >= UserRank.EDITOR).all():
triggerNotif(user, current_user, notif_msg, url_for("thread_page", id=thread.id))
db.session.commit() db.session.commit()

View File

@ -2,7 +2,6 @@ version: '3'
services: services:
db: db:
image: "postgres:9.6.5" image: "postgres:9.6.5"
restart: always
volumes: volumes:
- "./data/db:/var/lib/postgresql/data" - "./data/db:/var/lib/postgresql/data"
env_file: env_file:
@ -21,6 +20,7 @@ services:
- 5123:5123 - 5123:5123
volumes: volumes:
- "./data/uploads:/home/cdb/app/public/uploads" - "./data/uploads:/home/cdb/app/public/uploads"
- "./app:/home/cdb/app"
depends_on: depends_on:
- db - db
- redis - redis

View File

@ -1,3 +1,3 @@
#!/bin/bash #!/bin/bash
gunicorn -w 4 -b :5123 -e FLASK_APP=app/__init__.py -e FLASK_CONFIG=../config.prod.cfg -e FLASK_DEBUG=0 app:app gunicorn -w 4 -b :5123 -e FLASK_APP=app/__init__.py -e FLASK_CONFIG=../config.prod.cfg -e FLASK_DEBUG=1 app:app