From b0c815ea0a8c37dd96c02b142bacf99c0488eae7 Mon Sep 17 00:00:00 2001 From: Yusur Princeps Date: Tue, 8 Jul 2025 01:01:50 +0200 Subject: [PATCH] improved 404 handling, added mod tools page (stub), CSS .warning, outlawed some usernames --- CHANGELOG.md | 5 ++-- README.md | 43 +++++++++++++++++++++++++++++- freak/__init__.py | 14 ++++++++-- freak/ajax.py | 8 +++--- freak/models.py | 36 +++++++++++++++++++++++++ freak/search.py | 25 +++++++++++++++++ freak/static/sass/base.sass | 26 +++++++++++++++--- freak/static/sass/content.sass | 9 ------- freak/static/sass/layout.sass | 9 +++++++ freak/templates/feed.html | 10 ++----- freak/templates/guildsettings.html | 29 ++++++++++++++++++++ freak/templates/macros/nav.html | 3 +++ freak/templates/singledelete.html | 2 +- freak/templates/usersettings.html | 2 -- freak/website/__init__.py | 3 +++ freak/website/accounts.py | 10 +++++-- freak/website/moderation.py | 36 +++++++++++++++++++++++++ 17 files changed, 236 insertions(+), 34 deletions(-) create mode 100644 freak/templates/guildsettings.html create mode 100644 freak/website/moderation.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 10f4493..8924a33 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,11 +3,12 @@ ## 0.4.0 - Added dependency to [SUOU](https://github.com/sakuragasaki46/suou) library -- Added user blocks +- Users can now block each other + + Blocking a user prevents them from seeing your comments, posts (standalone or in feed) and profile - Added user strikes: a strike logs the content of a removed message for future use - Posts may now be deleted by author. If it has comments, comments are not spared +- Moderators (and admins) have now access to mod tools - Implemented guild subscriptions - + Blocking a user prevents them from seeing your comments, posts (standalone or in feed) and profile - Added ✨color themes✨ - Users can now set their display name, biography and color theme in `/settings` diff --git a/README.md b/README.md index 347ffd9..20c781e 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,44 @@ # Freak -(´ω\`) \ No newline at end of file +> \~(´ω\`)\~ +> (Josip Broz Tito, possibly) + +**Freak** (as in extremely interested into something, NOT as in predator) is a in-development FOSS and sovereign alternative to Reddit (and an attempt to revive Ruqqus from scratch). The socio-moral reasons are beyond the scope of this README. + +## Installation + +* First make sure you have these requirements: + * Unix-like OS (Docker container, Linux or MacOS are all good). + * **Python** >=3.10. Recommended to use a virtualenv (unless in Docker lol). + * **PostgreSQL** at least 16. + * **Redis**/Valkey (as of 0.4.0 unused in codebase). + * A server machine with a public IP address and shell access (mandatory for production, optional for development/staging). + * A reverse proxy listening on ports 80 and 443. Reminder to set `APP_IS_BEHIND_PROXY=1` in `.env` !!! + * Electricity. + * Will to not give up. +* Clone this repository. +* Fill in `.env` with the necessary information. + * `DOMAIN_NAME` (you must own it. Don't have? `.xyz` are like $2 or $3 on Namecheap[^1]) + * `APP_NAME` + * `DATABASE_URL` (hint: `postgresql://username:password@localhost/dbname`) + * `SECRET_KEY` (you can generate one with the command `cat /dev/random | tr -dc A-Za-z0-9_. | head -c 56`) + * `PRIVATE_ASSETS` (you must provide the icon stylesheets here. Useful for custom CSS / scripts as well) + * `APP_IS_BEHIND_PROXY` (mandatory if behind reverse proxy or NAT) +* ... + +[^1]: Namecheap is an American company. Don't trust American companies. + +## FAQ + +... + +## License + +Licensed under the [Apache License, Version 2.0](LICENSE), a non-copyleft free and open source license. + +This is a hobby project, made available “AS IS”, with __no warranty__ express or implied. + +I (sakuragasaki46) may NOT be held accountable for Your use of my code. + +> It's pointless to file a lawsuit because you feel damaged, and it's only going to turn against you. What a waste of money you could have spent on a vacation or charity, or invested in stocks. + diff --git a/freak/__init__.py b/freak/__init__.py index ba98759..7590ada 100644 --- a/freak/__init__.py +++ b/freak/__init__.py @@ -5,7 +5,7 @@ from sqlite3 import ProgrammingError from typing import Any import warnings from flask import ( - Flask, g, render_template, + Flask, g, redirect, render_template, request, send_from_directory, url_for ) import os @@ -21,7 +21,7 @@ from werkzeug.middleware.proxy_fix import ProxyFix from suou.configparse import ConfigOptions, ConfigValue -from freak.colors import color_themes, theme_classes +from .colors import color_themes, theme_classes __version__ = '0.4.0-dev27' @@ -131,8 +131,18 @@ def error_400(body): def error_403(body): return render_template('403.html'), 403 +from .search import find_guild_or_user + @app.errorhandler(404) def error_404(body): + try: + if mo := re.match(r'/([a-z0-9_-]+)/?', request.path): + alternative = find_guild_or_user(mo.group(1)) + if alternative is not None: + return redirect(alternative), 302 + except Exception as e: + warnings.warn(f'Exception in find_guild_or_user: {e}') + pass return render_template('404.html'), 404 @app.errorhandler(405) diff --git a/freak/ajax.py b/freak/ajax.py index d0da1e5..5fe2d04 100644 --- a/freak/ajax.py +++ b/freak/ajax.py @@ -8,7 +8,7 @@ AJAX hooks for the website. import re from flask import Blueprint, abort, flash, redirect, request from sqlalchemy import delete, insert, select -from .models import Guild, Member, UserBlock, db, User, Post, PostUpvote +from .models import Guild, Member, UserBlock, db, User, Post, PostUpvote, username_is_legal from flask_login import current_user, login_required current_user: User @@ -18,7 +18,7 @@ bp = Blueprint('ajax', __name__) @bp.route('/username_availability/') @bp.route('/ajax/username_availability/') def username_availability(username: str): - is_valid = re.fullmatch('[a-z0-9_-]+', username) is not None + is_valid = username_is_legal(username) if is_valid: user = db.session.execute(select(User).where(User.username == username)).scalar() @@ -30,7 +30,7 @@ def username_availability(username: str): return { 'status': 'ok', 'is_valid': is_valid, - 'is_available': is_available, + 'is_available': is_available } @bp.route('/guild_name_availability/') @@ -47,7 +47,7 @@ def guild_name_availability(name: str): return { 'status': 'ok', 'is_valid': is_valid, - 'is_available': is_available, + 'is_available': is_available } @bp.route('/comments//upvote', methods=['POST']) diff --git a/freak/models.py b/freak/models.py index a478595..dd807e4 100644 --- a/freak/models.py +++ b/freak/models.py @@ -6,6 +6,7 @@ from collections import namedtuple import datetime from functools import partial from operator import or_ +import re from threading import Lock from sqlalchemy import Column, Integer, String, ForeignKey, UniqueConstraint, text, \ CheckConstraint, Date, DateTime, Boolean, func, BigInteger, \ @@ -62,6 +63,26 @@ REPORT_UPDATE_COMPLETE = 1 REPORT_UPDATE_REJECTED = 2 REPORT_UPDATE_ON_HOLD = 3 +USERNAME_RE = r'[a-z2-9_-][a-z0-9_-]+' + +ILLEGAL_USERNAMES = ( + ## reserved for masspings and administrative claims + 'me', 'everyone', 'here', 'admin', 'mod', 'modteam', 'moderator', 'sysop', 'room', 'all', 'any', 'nobody', 'deleted', 'suspended', 'owner', 'administrator', 'ai', + ## law enforcement corps and slurs because yes + 'pedo', 'rape', 'rapist', 'nigger', 'retard', 'ncmec', 'police', 'cops', '911', 'childsafety', 'report', 'dmca' +) + +def username_is_legal(username: str) -> bool: + if len(username) < 2 or len(username) > 100: + return False + + if re.fullmatch(USERNAME_RE, username) is None: + return False + + if username in ILLEGAL_USERNAMES: + return False + return True + ## END constants and enums Base = declarative_base(app_config.domain_name, app_config.secret_key, @@ -259,6 +280,21 @@ class User(Base): def strike_count(self) -> int: return db.session.execute(select(func.count('*')).select_from(UserStrike).where(UserStrike.user_id == self.id)).scalar() + def moderates(self, gu: Guild) -> bool: + ## owner + if gu.owner_id == self.id: + return True + ## admin or global mod + if self.is_administrator: + return True + memb = db.session.execute(select(Member).where(Member.user_id == self.id, Member.guild_id == gu.id)).scalar() + + if memb is None: + return False + return memb.is_moderator + + ## TODO check banship? + # UserBlock table is at the top !! ## END User diff --git a/freak/search.py b/freak/search.py index b1f46f6..b4b7c27 100644 --- a/freak/search.py +++ b/freak/search.py @@ -2,8 +2,11 @@ from typing import Iterable +from flask import flash, g from sqlalchemy import Column, Select, select, or_ +from .models import Guild, User, db + class SearchQuery: keywords: Iterable[str] @@ -23,3 +26,25 @@ class SearchQuery: sq = sq.where(or_(*or_cond) if len(or_cond) > 1 else or_cond[0]) return sq + +def find_guild_or_user(name: str) -> str | None: + """ + Used in 404 error handler. + + Returns an URL to redirect or None for no redirect. + """ + + if hasattr(g, 'no_user'): + return None + + gu = db.session.execute(select(Guild).where(Guild.name == name)).scalar() + if gu is not None: + flash(f'There is nothing at /{name}. Luckily, a guild with name {gu.handle()} happens to exist. Next time, remember to add + before!') + return gu.url() + + user = db.session.execute(select(User).where(User.username == name)).scalar() + if user is not None: + flash(f'There is nothing at /{name}. Luckily, a user named {user.handle()} happens to exist. Next time, remember to add @ before!') + return user.url() + + return None \ No newline at end of file diff --git a/freak/static/sass/base.sass b/freak/static/sass/base.sass index 32cfe22..28dd1b9 100644 --- a/freak/static/sass/base.sass +++ b/freak/static/sass/base.sass @@ -26,7 +26,8 @@ --light-text-alt: #444 --light-border: #999 --light-success: #73af00 - --light-error: #e04433 + --light-error: #e04830 + --light-warning: #dea800 --light-canvas: #eaecee --light-background: #f9f9f9 --light-bg-sharp: #fdfdff @@ -35,7 +36,8 @@ --dark-text-alt: #c0cad3 --dark-border: #777 --dark-success: #93cf00 - --dark-error: #e04433 + --dark-error: #e04830 + --dark-warning: #dea800 --dark-canvas: #0a0a0e --dark-background: #181a21 --dark-bg-sharp: #080808 @@ -51,6 +53,7 @@ --border: var(--light-border) --success: var(--light-success) --error: var(--light-error) + --warning: var(--light-warning) --canvas: var(--light-canvas) --background: var(--light-background) --bg-sharp: var(--light-bg-sharp) @@ -62,6 +65,7 @@ --border: var(--dark-border) --success: var(--dark-success) --error: var(--dark-error) + --warning: var(--dark-warning) --canvas: var(--dark-canvas) --background: var(--dark-background) --bg-sharp: var(--dark-bg-sharp) @@ -72,6 +76,7 @@ --border: var(--light-border) --success: var(--light-success) --error: var(--light-error) + --warning: var(--light-warning) --canvas: var(--light-canvas) --background: var(--light-background) --bg-sharp: var(--light-bg-sharp) @@ -82,6 +87,7 @@ --border: var(--dark-border) --success: var(--dark-success) --error: var(--dark-error) + --warning: var(--dark-warning) --canvas: var(--dark-canvas) --background: var(--dark-background) --bg-sharp: var(--dark-bg-sharp) @@ -185,4 +191,18 @@ img max-height: 100vh .faint - opacity: .75 \ No newline at end of file + opacity: .75 + strong & + font-weight: 400 + +.callout + color: var(--text-alt) + +.success + color: var(--success) + +.error + color: var(--error) + +.warning + color: var(--warning) diff --git a/freak/static/sass/content.sass b/freak/static/sass/content.sass index e30a6c1..6aaf1fa 100644 --- a/freak/static/sass/content.sass +++ b/freak/static/sass/content.sass @@ -14,15 +14,6 @@ blockquote padding-right: 1em border-right: 4px solid var(--border) -.callout - color: var(--text-alt) - -.success - color: var(--success) - -.error - color: var(--error) - .message-content p margin: 4px 0 diff --git a/freak/static/sass/layout.sass b/freak/static/sass/layout.sass index c042424..71754f0 100644 --- a/freak/static/sass/layout.sass +++ b/freak/static/sass/layout.sass @@ -378,3 +378,12 @@ button.card margin: 12px auto font-size: 36px text-align: center + + +textarea.create_text + min-height: 8em + + // specificity ew // + form.boundaryless & + min-height: 8em + diff --git a/freak/templates/feed.html b/freak/templates/feed.html index 281de92..4737ad5 100644 --- a/freak/templates/feed.html +++ b/freak/templates/feed.html @@ -3,10 +3,6 @@ {% from "macros/title.html" import title_tag with context %} {% from "macros/nav.html" import nav_guild, nav_top_communities with context %} -{# set feed_title = 'For you' if feed_type == 'foryou' and not feed_title %} -{% set feed_title = 'Explore' if feed_type == 'explore' and not feed_title #} - - {% block title %} {{ title_tag(feed_title) }} {% endblock %} @@ -24,7 +20,7 @@ {{ nav_guild(guild) }} {% endif %} -