]> git.lizzy.rs Git - cheatdb.git/commitdiff
Add comment ratelimiting, allow any member to open threads
authorrubenwardy <rw@rubenwardy.com>
Mon, 28 Jan 2019 19:01:37 +0000 (19:01 +0000)
committerrubenwardy <rw@rubenwardy.com>
Mon, 28 Jan 2019 19:01:37 +0000 (19:01 +0000)
app/models.py
app/templates/macros/threads.html
app/views/threads.py
docker-compose.yml
runprodguni.sh

index b1cfbb576d8091502f9d5191aed1a4059779bd92..20c3e601accbcaafa388a594435c7c5ede8213f7 100644 (file)
@@ -20,10 +20,9 @@ from flask_sqlalchemy import SQLAlchemy
 from flask_migrate import Migrate
 from urllib.parse import urlparse
 from app import app, gravatar
-from datetime import datetime
 from sqlalchemy.orm import validates
 from flask_user import login_required, UserManager, UserMixin, SQLAlchemyAdapter
-import enum
+import enum, datetime
 
 # Initialise database
 db = SQLAlchemy(app)
@@ -129,8 +128,6 @@ class User(db.Model, UserMixin):
        replies       = db.relationship("ThreadReply", backref="author", lazy="dynamic")
 
        def __init__(self, username, active=False, email=None, password=None):
-               import datetime
-
                self.username = username
                self.confirmed_at = datetime.datetime.now() - datetime.timedelta(days=6000)
                self.display_name = username
@@ -172,6 +169,16 @@ class User(db.Model, UserMixin):
                else:
                        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):
        id      = db.Column(db.Integer, primary_key=True)
        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)
        desc         = db.Column(db.Text, nullable=True)
        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      = db.relationship("License", foreign_keys=[license_id])
@@ -496,8 +503,11 @@ class Package(db.Model):
 
                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
-               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)
 
                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))
 
        def recalcScore(self):
-               import datetime
-
                self.score = 10
 
                if self.forums is not None:
@@ -630,7 +638,7 @@ class PackageRelease(db.Model):
 
 
        def __init__(self):
-               self.releaseDate = datetime.now()
+               self.releaseDate = datetime.datetime.now()
 
 
 class PackageReview(db.Model):
@@ -762,7 +770,7 @@ class Thread(db.Model):
        title      = db.Column(db.String(100), nullable=False)
        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")
 
@@ -800,7 +808,7 @@ class ThreadReply(db.Model):
        thread_id  = db.Column(db.Integer, db.ForeignKey("thread.id"), nullable=False)
        comment    = db.Column(db.String(500), 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", \
@@ -824,7 +832,7 @@ class ForumTopic(db.Model):
        posts     = 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):
                if self.link is None:
index 65f02f4dd6de32911f8a2cf3ce54664c0aeda490..fd7b648175fe17caeac12296cd32e10a64e64f57 100644 (file)
                                <a name="reply"></a>
                        </div>
 
-                       <form method="post" action="{{ url_for('thread_page', id=thread.id)}}" class="card-body">
-                               <input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
-                               <textarea class="form-control markdown" required maxlength=500 name="comment"></textarea><br />
-                               <input class="btn btn-primary" type="submit" value="Comment" />
-                       </form>
+                       {% if current_user.canCommentRL() %}
+                               <form method="post" action="{{ url_for('thread_page', id=thread.id)}}" class="card-body">
+                                       <input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
+                                       <textarea class="form-control markdown" required maxlength=500 name="comment"></textarea><br />
+                                       <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>
index 37ac3d1b7a59b1c57d0c19e2cfcf0b0a27065978..c168a23c425b8945bc0e6ff576134bdb1f9ebc50 100644 (file)
@@ -21,6 +21,8 @@ from app import app
 from app.models import *
 from app.utils import triggerNotif, clearNotifications
 
+import datetime
+
 from flask_wtf import FlaskForm
 from wtforms import *
 from wtforms.validators import *
@@ -78,6 +80,13 @@ def thread_page(id):
        if current_user.is_authenticated and request.method == "POST":
                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:
                        reply = ThreadReply()
                        reply.author = current_user
@@ -126,15 +135,15 @@ def new_thread_page():
                if package is None:
                        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:
                abort(403)
 
        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
-       allow_change     = package.approved
-       is_review_thread = package is not None and not package.approved
+       allow_change     = package and package.approved
+       is_review_thread = package and not package.approved
 
        # Check that user can make the 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
        elif is_review_thread and package.review_thread is not None:
                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
        elif request.method == "GET":
@@ -178,9 +194,15 @@ def new_thread_page():
                if is_review_thread:
                        package.review_thread = thread
 
+               notif_msg = None
                if package is not None:
-                       triggerNotif(package.author, current_user,
-                                       "New thread '{}' on package {}".format(thread.title, package.title), url_for("thread_page", id=thread.id))
+                       notif_msg = "New thread '{}' on package {}".format(thread.title, package.title)
+                       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()
 
index 4c2752edbb5067277710ecd4aeb8bf51fb4e7bbd..93e86bccac799b47829fc4c6a179e644b5667f4d 100644 (file)
@@ -2,7 +2,6 @@ version: '3'
 services:
   db:
     image: "postgres:9.6.5"
-    restart: always
     volumes:
       - "./data/db:/var/lib/postgresql/data"
     env_file:
@@ -21,6 +20,7 @@ services:
       - 5123:5123
     volumes:
       - "./data/uploads:/home/cdb/app/public/uploads"
+      - "./app:/home/cdb/app"
     depends_on:
       - db
       - redis
index fca01c0d51c76ecd38db5878ab9262b18acc7ff7..c7e8bb8855ae7d9eb1c1d24462c2cb64fd0f3257 100644 (file)
@@ -1,3 +1,3 @@
 #!/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