mirror of
https://github.com/minetest/contentdb.git
synced 2025-01-03 19:57:29 +01:00
Add comment ratelimiting, allow any member to open threads
This commit is contained in:
parent
2691105513
commit
8afe17b984
@ -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:
|
||||||
|
@ -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>
|
||||||
|
@ -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()
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user