diff --git a/app/__init__.py b/app/__init__.py index 856adc14..b6b37640 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -13,6 +13,7 @@ # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . + import datetime from flask import * @@ -26,7 +27,6 @@ from flask_login import logout_user, current_user, LoginManager import os, redis from app.markdown import init_markdown, MARKDOWN_EXTENSIONS, MARKDOWN_EXTENSION_CONFIG - app = Flask(__name__, static_folder="public/static") app.config["FLATPAGES_ROOT"] = "flatpages" app.config["FLATPAGES_EXTENSION"] = ".md" @@ -86,27 +86,39 @@ def load_user(user_id): from .blueprints import create_blueprints create_blueprints(app) + @app.route("/uploads/") def send_upload(path): return send_from_directory(app.config["UPLOAD_DIR"], path) + @app.route("//") def flatpage(path): page = pages.get_or_404(path) template = page.meta.get("template", "flatpage.html") return render_template(template, page=page) + @app.before_request def check_for_ban(): if current_user.is_authenticated: - if current_user.rank == models.UserRank.BANNED: - flash(gettext("You have been banned."), "danger") + if current_user.ban and current_user.ban.has_expired: + models.db.session.delete(current_user.ban) + if current_user.rank == models.UserRank.BANNED: + current_user.rank = models.UserRank.MEMBER + models.db.session.commit() + elif current_user.ban or current_user.rank == models.UserRank.BANNED: + if current_user.ban: + flash(gettext("Banned:") + " " + current_user.ban.message, "danger") + else: + flash(gettext("You have been banned."), "danger") logout_user() return redirect(url_for("users.login")) elif current_user.rank == models.UserRank.NOT_JOINED: current_user.rank = models.UserRank.MEMBER models.db.session.commit() + from .utils import clearNotifications, is_safe_url @@ -115,6 +127,7 @@ def check_for_notifications(): if current_user.is_authenticated: clearNotifications(request.path) + @app.errorhandler(404) def page_not_found(e): return render_template("404.html"), 404 @@ -145,7 +158,6 @@ def get_locale(): return locale - @app.route("/set-locale/", methods=["POST"]) @csrf.exempt def set_locale(): diff --git a/app/blueprints/users/settings.py b/app/blueprints/users/settings.py index 22e0da7b..357e9221 100644 --- a/app/blueprints/users/settings.py +++ b/app/blueprints/users/settings.py @@ -358,11 +358,45 @@ def modtools_ban(username): if not user.checkPerm(current_user, Permission.CHANGE_RANK): abort(403) - user.rank = UserRank.BANNED + message = request.form["message"] + expires_at = request.form.get("expires_at") - addAuditLog(AuditSeverity.MODERATION, current_user, f"Banned {user.username}", + user.ban = UserBan() + user.ban.banned_by = current_user + user.ban.message = message + + if expires_at and expires_at != "": + user.ban.expires_at = expires_at + else: + user.rank = UserRank.BANNED + + addAuditLog(AuditSeverity.MODERATION, current_user, f"Banned {user.username}, expires {user.ban.expires_at or '-'}, message: {message}", url_for("users.profile", username=user.username), None) db.session.commit() flash(f"Banned {user.username}", "success") - return redirect(url_for("users.modtools", username=username)) \ No newline at end of file + return redirect(url_for("users.modtools", username=username)) + + +@bp.route("/users//modtools/unban/", methods=["POST"]) +@rank_required(UserRank.MODERATOR) +def modtools_unban(username): + user: User = User.query.filter_by(username=username).first() + if not user: + abort(404) + + if not user.checkPerm(current_user, Permission.CHANGE_RANK): + abort(403) + + if user.ban: + db.session.delete(user.ban) + + if user.rank == UserRank.BANNED: + user.rank = UserRank.MEMBER + + addAuditLog(AuditSeverity.MODERATION, current_user, f"Unbanned {user.username}", + url_for("users.profile", username=user.username), None) + db.session.commit() + + flash(f"Unbanned {user.username}", "success") + return redirect(url_for("users.modtools", username=username)) diff --git a/app/models/users.py b/app/models/users.py index 3af2feca..4df216d2 100644 --- a/app/models/users.py +++ b/app/models/users.py @@ -183,6 +183,8 @@ class User(db.Model, UserMixin): replies = db.relationship("ThreadReply", back_populates="author", lazy="dynamic", cascade="all, delete, delete-orphan", order_by=db.desc("created_at")) forum_topics = db.relationship("ForumTopic", back_populates="author", lazy="dynamic", cascade="all, delete, delete-orphan") + ban = db.relationship("UserBan", foreign_keys="UserBan.user_id", back_populates="user", uselist=False) + def __init__(self, username=None, active=False, email=None, password=None): self.username = username self.display_name = username @@ -482,3 +484,21 @@ class UserNotificationPreferences(db.Model): value = 1 if value else 0 setattr(self, "pref_" + notification_type.toName(), value) + + +class UserBan(db.Model): + user_id = db.Column(db.Integer, db.ForeignKey('user.id'), primary_key=True) + user = db.relationship("User", foreign_keys=[user_id], back_populates="ban") + + message = db.Column(db.UnicodeText, nullable=False) + + banned_by_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) + banned_by = db.relationship("User", foreign_keys=[banned_by_id]) + + created_at = db.Column(db.DateTime, nullable=False, default=datetime.datetime.utcnow) + + expires_at = db.Column(db.DateTime, nullable=True, default=None) + + @property + def has_expired(self): + return self.expires_at and datetime.datetime.now() > self.expires_at diff --git a/app/templates/packages/view.html b/app/templates/packages/view.html index e2d96c30..8da8e4bd 100644 --- a/app/templates/packages/view.html +++ b/app/templates/packages/view.html @@ -434,15 +434,18 @@ {% if package.type == package.type.MOD %}

{{ _("Compatible Games") }}

- {% for support in package.getSortedSupportedGames() %} - - {{ _("%(title)s by %(display_name)s", - title=support.game.title, display_name=support.game.author.display_name) }} - - {% else %} - {{ _("No specific game is required") }} - {% endfor %} +
+ {% for support in package.getSortedSupportedGames() %} + + {{ _("%(title)s by %(display_name)s", + title=support.game.title, display_name=support.game.author.display_name) }} + + {% else %} + {{ _("No specific game is required") }} + {% endfor %} +
+

{{ _("This is an experimental feature.") }} {{ _("Supported games are determined by an algorithm, and may not be correct.") }} diff --git a/app/templates/users/modtools.html b/app/templates/users/modtools.html index 846dff10..4575b40d 100644 --- a/app/templates/users/modtools.html +++ b/app/templates/users/modtools.html @@ -41,13 +41,37 @@ {% if not user.rank.atLeast(current_user.rank) %}

{{ _("Ban") }}

- {% if user.rank.name == "BANNED" %} + {% if user.ban %}

- Banned. + Banned by {{ user.ban.banned_by.display_name }} at {{ user.ban.created_at | full_datetime }} + {% if user.ban.expires_at %} + until {{ user.ban.expires_at | date }} + {% endif %}

+
+ {{ user.ban.message }} +
+
+ + +
{% else %}
+
+ + + + {{ _("Message to display to banned user") }} + +
+
+ + + + {{ _("Expiry date. Leave blank for permanent ban") }} + +
{% endif %} diff --git a/migrations/versions/01f8d5de29e1_.py b/migrations/versions/01f8d5de29e1_.py new file mode 100644 index 00000000..797631c7 --- /dev/null +++ b/migrations/versions/01f8d5de29e1_.py @@ -0,0 +1,33 @@ +"""empty message + +Revision ID: 01f8d5de29e1 +Revises: e571b3498f9e +Create Date: 2022-02-13 10:12:20.150232 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = '01f8d5de29e1' +down_revision = 'e571b3498f9e' +branch_labels = None +depends_on = None + + +def upgrade(): + op.create_table('user_ban', + sa.Column('user_id', sa.Integer(), nullable=False), + sa.Column('message', sa.UnicodeText(), nullable=False), + sa.Column('banned_by_id', sa.Integer(), nullable=False), + sa.Column('created_at', sa.DateTime(), nullable=False), + sa.Column('expires_at', sa.DateTime(), nullable=True), + sa.ForeignKeyConstraint(['banned_by_id'], ['user.id'], ), + sa.ForeignKeyConstraint(['user_id'], ['user.id'], ), + sa.PrimaryKeyConstraint('user_id') + ) + + +def downgrade(): + op.drop_table('user_ban')