diff --git a/CHANGELOG.md b/CHANGELOG.md index a15077f..157b5af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,18 +1,5 @@ # Changelog -## 0.4.0 - -- Added dependency to [SUOU](https://github.com/sakuragasaki46/suou) -- Added user strikes, memberships and user blocks -- Added ✨color themes✨ -- Users can now set their display name and biography in `/settings` - -## 0.3.3 - -- Fixed bugs in templates introduced in 0.3.2 -- Improved karma management -- Fixed og: meta tags missing - ## 0.3.2 - Fixed administrator users not being able to create +guilds diff --git a/alembic/versions/29a8d663c7ce_.py b/alembic/versions/29a8d663c7ce_.py deleted file mode 100644 index f9e7cae..0000000 --- a/alembic/versions/29a8d663c7ce_.py +++ /dev/null @@ -1,92 +0,0 @@ -"""upgrade to 0.4.0 - -NOTICE: REVISIONS BEFORE 0.3.1 ARE LOST FOR GOOD - -get over it and move on: the recommended way to upgrade is via -python3 -m freak -U - -Revision ID: 29a8d663c7ce -Revises: -Create Date: 2025-06-17 21:55:16.145111 - -""" -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision: str = '29a8d663c7ce' -down_revision: Union[str, None] = '7122c8715ff9' -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - """Upgrade schema.""" - # ### commands auto generated by Alembic - please adjust! ### - op.create_table('freak_user_block', - sa.Column('actor_id', sa.BigInteger(), nullable=False), - sa.Column('target_id', sa.BigInteger(), nullable=False), - sa.ForeignKeyConstraint(['actor_id'], ['freak_user.id'], ), - sa.ForeignKeyConstraint(['target_id'], ['freak_user.id'], ), - sa.PrimaryKeyConstraint('actor_id', 'target_id') - ) - op.create_table('freak_user_strike', - sa.Column('id', sa.LargeBinary(length=16), nullable=False), - sa.Column('user_id', sa.BigInteger(), nullable=False), - sa.Column('target_type', sa.SmallInteger(), nullable=False), - sa.Column('target_id', sa.BigInteger(), nullable=False), - sa.Column('target_content', sa.String(length=4096), nullable=True), - sa.Column('reason_code', sa.SmallInteger(), nullable=False), - sa.Column('issued_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True), - sa.Column('issued_by_id', sa.BigInteger(), nullable=True), - sa.ForeignKeyConstraint(['issued_by_id'], ['freak_user.id'], ), - sa.ForeignKeyConstraint(['user_id'], ['freak_user.id'], ondelete='cascade'), - sa.PrimaryKeyConstraint('id') - ) - op.create_table('freak_member', - sa.Column('id', sa.LargeBinary(length=16), nullable=False), - sa.Column('user_id', sa.BigInteger(), nullable=True), - sa.Column('guild_id', sa.BigInteger(), nullable=True), - sa.Column('is_approved', sa.Boolean(), server_default=sa.text('false'), nullable=False), - sa.Column('is_subscribed', sa.Boolean(), server_default=sa.text('false'), nullable=False), - sa.Column('is_moderator', sa.Boolean(), server_default=sa.text('false'), nullable=False), - sa.Column('banned_at', sa.DateTime(), nullable=True), - sa.Column('banned_by_id', sa.BigInteger(), nullable=True), - sa.Column('banned_reason', sa.SmallInteger(), server_default=sa.text('0'), nullable=True), - sa.Column('banned_until', sa.DateTime(), nullable=True), - sa.Column('banned_message', sa.String(length=256), nullable=True), - sa.ForeignKeyConstraint(['banned_by_id'], ['freak_user.id'], name='user_banner_id'), - sa.ForeignKeyConstraint(['guild_id'], ['freak_topic.id'], ), - sa.ForeignKeyConstraint(['user_id'], ['freak_user.id'], ), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('user_id', 'guild_id', name='member_user_topic') - ) - op.add_column('freak_topic', sa.Column('is_restricted', sa.Boolean(), server_default=sa.text('false'), nullable=False)) - op.add_column('freak_topic', sa.Column('is_public', sa.Boolean(), server_default=sa.text('true'), nullable=False)) - op.drop_column('freak_topic', 'privacy') - op.add_column('freak_user', sa.Column('pronouns', sa.Integer(), server_default=sa.text('0'), nullable=False)) - op.add_column('freak_user', sa.Column('biography', sa.String(length=1024), nullable=True)) - op.add_column('freak_user', sa.Column('is_approved', sa.Boolean(), server_default=sa.text('false'), nullable=False)) - op.add_column('freak_user', sa.Column('invited_by_id', sa.BigInteger(), nullable=True)) - op.create_foreign_key('user_inviter_id', 'freak_user', 'freak_user', ['invited_by_id'], ['id']) - # ### end Alembic commands ### - - -def downgrade() -> None: - """Downgrade schema.""" - # ### commands auto generated by Alembic - please adjust! ### - op.drop_constraint('user_inviter_id', 'freak_user', type_='foreignkey') - op.drop_column('freak_user', 'invited_by_id') - op.drop_column('freak_user', 'is_approved') - op.drop_column('freak_user', 'biography') - op.drop_column('freak_user', 'pronouns') - op.add_column('freak_topic', sa.Column('privacy', sa.SMALLINT(), server_default=sa.text('0'), autoincrement=False, nullable=True)) - op.drop_column('freak_topic', 'is_public') - op.drop_column('freak_topic', 'is_restricted') - op.drop_table('freak_member') - op.drop_table('freak_user_strike') - op.drop_table('freak_user_block') - # ### end Alembic commands ### diff --git a/alembic/versions/7122c8715ff9_.py b/alembic/versions/7122c8715ff9_.py deleted file mode 100644 index 67eb85a..0000000 --- a/alembic/versions/7122c8715ff9_.py +++ /dev/null @@ -1,28 +0,0 @@ -"""autogenerated to allow downgrade to nothing as a bugfix - -Revision ID: 7122c8715ff9 -Revises: 29a8d663c7ce -Create Date: 2025-06-17 22:05:14.803669 - -""" -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision: str = '7122c8715ff9' -down_revision: Union[str, None] = None -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - """Upgrade schema.""" - pass - - -def downgrade() -> None: - """Downgrade schema.""" - pass diff --git a/alembic/versions/90c7d0098efe_.py b/alembic/versions/90c7d0098efe_.py deleted file mode 100644 index 07390d9..0000000 --- a/alembic/versions/90c7d0098efe_.py +++ /dev/null @@ -1,32 +0,0 @@ -"""empty message - -Revision ID: 90c7d0098efe -Revises: 29a8d663c7ce -Create Date: 2025-06-19 01:16:41.120290 - -""" -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision: str = '90c7d0098efe' -down_revision: Union[str, None] = '29a8d663c7ce' -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - """Upgrade schema.""" - # ### commands auto generated by Alembic - please adjust! ### - op.add_column('freak_user', sa.Column('color_theme', sa.SmallInteger(), server_default=sa.text('0'), nullable=False)) - # ### end Alembic commands ### - - -def downgrade() -> None: - """Downgrade schema.""" - # ### commands auto generated by Alembic - please adjust! ### - op.drop_column('freak_user', 'color_theme') - # ### end Alembic commands ### diff --git a/freak/__init__.py b/freak/__init__.py index 44416e0..9c32442 100644 --- a/freak/__init__.py +++ b/freak/__init__.py @@ -1,11 +1,9 @@ -import re 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 @@ -13,38 +11,22 @@ import dotenv from flask_login import LoginManager from flask_wtf.csrf import CSRFProtect from sqlalchemy import select -from sqlalchemy.exc import SQLAlchemyError -from suou import Snowflake, ssv_list from werkzeug.routing import BaseConverter from sassutils.wsgi import SassMiddleware -from suou.configparse import ConfigOptions, ConfigValue - -from freak.colors import color_themes, theme_classes - -__version__ = '0.4.0-dev24' +__version__ = '0.3.2' APP_BASE_DIR = os.path.dirname(os.path.dirname(__file__)) -if not dotenv.load_dotenv(): - warnings.warn('.env not loaded; application may break!', RuntimeWarning) - -class AppConfig(ConfigOptions): - secret_key = ConfigValue(required=True) - database_url = ConfigValue(required=True) - app_name = ConfigValue() - domain_name = ConfigValue() - private_assets = ConfigValue(cast=ssv_list) - jquery_url = ConfigValue(default='https://ajax.googleapis.com/ajax/libs/jquery/3.7.1/jquery.min.js') - -app_config = AppConfig() +dotenv.load_dotenv() app = Flask(__name__) -app.secret_key = app_config.secret_key -app.config['SQLALCHEMY_DATABASE_URI'] = app_config.database_url +app.secret_key = os.getenv('SECRET_KEY') +app.config['SQLALCHEMY_DATABASE_URI'] = os.getenv('DATABASE_URL') app.config['JSONIFY_PRETTYPRINT_REGULAR'] = False from .models import db, User, Post +from .iding import id_from_b32l, id_to_b32l # SASS app.wsgi_app = SassMiddleware(app.wsgi_app, dict( @@ -57,9 +39,9 @@ class SlugConverter(BaseConverter): class B32lConverter(BaseConverter): regex = r'_?[a-z2-7]+' def to_url(self, value): - return Snowflake(value).to_b32l() + return id_to_b32l(value) def to_python(self, value): - return Snowflake.from_b32l(value) + return id_from_b32l(value) app.url_map.converters['slug'] = SlugConverter app.url_map.converters['b32l'] = B32lConverter @@ -79,40 +61,32 @@ PRIVATE_ASSETS = os.getenv('PRIVATE_ASSETS', '').split() @app.context_processor def _inject_variables(): return { - 'app_name': app_config.app_name, - 'app_version': __version__, - 'domain_name': app_config.domain_name, + 'app_name': os.getenv('APP_NAME'), + 'domain_name': os.getenv('DOMAIN_NAME'), 'url_for_css': (lambda name, **kwargs: url_for('static', filename=f'css/{name}.css', **kwargs)), - 'private_scripts': [x for x in app_config.private_assets if x.endswith('.js')], + 'private_scripts': [x for x in PRIVATE_ASSETS if x.endswith('.js')], 'private_styles': [x for x in PRIVATE_ASSETS if x.endswith('.css')], - 'jquery_url': app_config.jquery_url, + 'jquery_url': os.getenv('JQUERY_URL') or 'https://ajax.googleapis.com/ajax/libs/jquery/3.7.1/jquery.min.js', 'post_count': Post.count(), - 'user_count': User.active_count(), - 'colors': color_themes, - 'theme_classes': theme_classes + 'user_count': User.active_count() } @login_manager.user_loader def _inject_user(userid): try: - u = db.session.execute(select(User).where(User.id == userid)).scalar() - if u is None or u.is_disabled: - return None - return u - except SQLAlchemyError as e: - warnings.warn(f'cannot retrieve user {userid} from db (exception: {e})', RuntimeWarning) + return db.session.execute(select(User).where(User.id == userid)).scalar() + except Exception: + warnings.warn(f'cannot retrieve user {userid} from db', RuntimeWarning) g.no_user = True return None -def redact_url_password(u: str | Any) -> str | Any: - if not isinstance(u, str): - return u - return re.sub(r':[^@:/ ]+@', ':***@', u) - @app.errorhandler(ProgrammingError) def error_db(body): g.no_user = True - warnings.warn(f'No database access! (url is {redact_url_password(app.config['SQLALCHEMY_DATABASE_URI'])!r})', RuntimeWarning) + warnings.warn(f'No database access! (url is {app.config['SQLALCHEMY_DATABASE_URI']})', RuntimeWarning) + fix_database_url() + if request.method in ('HEAD', 'GET') and not 'retry' in request.args: + return redirect(request.url + ('&' if '?' in request.url else '?') + 'retry=1'), 307, {'cache-control': 'private,no-cache,must-revalidate,max-age=0'} return render_template('500.html'), 500 @app.errorhandler(400) diff --git a/freak/ajax.py b/freak/ajax.py index 82abb9d..83a4185 100644 --- a/freak/ajax.py +++ b/freak/ajax.py @@ -7,8 +7,7 @@ AJAX hooks for the website. import re from flask import Blueprint, request -from sqlalchemy import delete, insert, select -from .models import Guild, db, User, Post, PostUpvote +from .models import Topic, db, User, Post, PostUpvote from flask_login import current_user, login_required bp = Blueprint('ajax', __name__) @@ -19,7 +18,7 @@ def username_availability(username: str): is_valid = re.fullmatch('[a-z0-9_-]+', username) is not None if is_valid: - user = db.session.execute(select(User).where(User.username == username)).scalar() + user = db.session.execute(db.select(User).where(User.username == username)).scalar() is_available = user is None or user == current_user else: @@ -33,10 +32,10 @@ def username_availability(username: str): @bp.route('/guild_name_availability/') def guild_name_availability(name: str): - is_valid = re.fullmatch('[a-z0-9_-]+', name) is not None + is_valid = re.fullmatch('[a-z0-9_-]+', username) is not None if is_valid: - gd = db.session.execute(select(Guild).where(Guild.name == name)).scalar() + gd = db.session.execute(db.select(Topic).where(Topic.name == name)).scalar() is_available = gd is None else: @@ -52,19 +51,19 @@ def guild_name_availability(name: str): @login_required def post_upvote(id): o = request.form['o'] - p: Post | None = db.session.execute(select(Post).where(Post.id == id)).scalar() + p: Post | None = db.session.execute(db.select(Post).where(Post.id == id)).scalar() if p is None: return { 'status': 'fail', 'message': 'Post not found' }, 404 if o == '1': - db.session.execute(delete(PostUpvote).where(PostUpvote.c.post_id == p.id, PostUpvote.c.voter_id == current_user.id, PostUpvote.c.is_downvote == True)) - db.session.execute(insert(PostUpvote).values(post_id = p.id, voter_id = current_user.id, is_downvote = False)) + db.session.execute(db.delete(PostUpvote).where(PostUpvote.c.post_id == p.id, PostUpvote.c.voter_id == current_user.id, PostUpvote.c.is_downvote == True)) + db.session.execute(db.insert(PostUpvote).values(post_id = p.id, voter_id = current_user.id, is_downvote = False)) elif o == '0': - db.session.execute(delete(PostUpvote).where(PostUpvote.c.post_id == p.id, PostUpvote.c.voter_id == current_user.id)) + db.session.execute(db.delete(PostUpvote).where(PostUpvote.c.post_id == p.id, PostUpvote.c.voter_id == current_user.id)) elif o == '-1': - db.session.execute(delete(PostUpvote).where(PostUpvote.c.post_id == p.id, PostUpvote.c.voter_id == current_user.id, PostUpvote.c.is_downvote == False)) - db.session.execute(insert(PostUpvote).values(post_id = p.id, voter_id = current_user.id, is_downvote = True)) + db.session.execute(db.delete(PostUpvote).where(PostUpvote.c.post_id == p.id, PostUpvote.c.voter_id == current_user.id, PostUpvote.c.is_downvote == False)) + db.session.execute(db.insert(PostUpvote).values(post_id = p.id, voter_id = current_user.id, is_downvote = True)) else: return { 'status': 'fail', 'message': 'Invalid score' }, 400 diff --git a/freak/algorithms.py b/freak/algorithms.py index bd43525..efc7bf6 100644 --- a/freak/algorithms.py +++ b/freak/algorithms.py @@ -2,33 +2,30 @@ from flask_login import current_user from sqlalchemy import func, select -from .models import db, Post, Guild, User +from .models import db, Post, Topic, User def cuser() -> User: return current_user if current_user.is_authenticated else None -def cuser_id() -> int: - return current_user.id if current_user.is_authenticated else None - def public_timeline(): return select(Post).join(User, User.id == Post.author_id).where( - Post.privacy == 0, User.not_suspended(), Post.not_removed(), User.has_not_blocked(Post.author_id, cuser_id()) + Post.privacy == 0, User.not_suspended(), Post.not_removed() ).order_by(Post.created_at.desc()) -def topic_timeline(gname): - return select(Post).join(Guild).join(User, User.id == Post.author_id).where( - Post.privacy == 0, Guild.name == gname, User.not_suspended(), Post.not_removed(), User.has_not_blocked(Post.author_id, cuser_id()) +def topic_timeline(topic_name): + return select(Post).join(Topic).join(User, User.id == Post.author_id).where( + Post.privacy == 0, Topic.name == topic_name, User.not_suspended(), Post.not_removed() ).order_by(Post.created_at.desc()) def user_timeline(user_id): return select(Post).join(User, User.id == Post.author_id).where( - Post.visible_by(cuser()), User.id == user_id, User.not_suspended(), Post.not_removed(), User.has_not_blocked(Post.author_id, cuser_id()) + Post.visible_by(cuser()), User.id == user_id, User.not_suspended(), Post.not_removed() ).order_by(Post.created_at.desc()) def top_guilds_query(): q_post_count = func.count().label('post_count') - qr = select(Guild, q_post_count)\ - .join(Post, Post.topic_id == Guild.id).group_by(Guild)\ + qr = select(Topic, q_post_count)\ + .join(Post, Post.topic_id == Topic.id).group_by(Topic)\ .having(q_post_count > 5).order_by(q_post_count.desc()) return qr diff --git a/freak/cli.py b/freak/cli.py index 34a1959..9fd7930 100644 --- a/freak/cli.py +++ b/freak/cli.py @@ -4,40 +4,22 @@ import argparse import os import subprocess -from sqlalchemy import create_engine, select -from sqlalchemy.orm import Session -from . import __version__ as version, app -from .models import User, db +from sqlalchemy import create_engine +from . import __version__ as version +from .models import db def make_parser(): parser = argparse.ArgumentParser() parser.add_argument('--version', '-v', action='version', version=version) parser.add_argument('--upgrade', '-U', action='store_true', help='create or upgrade schema') - parser.add_argument('--flush', '-H', action='store_true', help='recompute karma for all users') return parser def main(): args = make_parser().parse_args() - - engine = create_engine(os.getenv('DATABASE_URL')) if args.upgrade: - ret_code = subprocess.Popen(['alembic', 'upgrade', 'head']).wait() - if ret_code != 0: - print(f'Schema upgrade failed (code: {ret_code})') - exit(ret_code) - # if the alembic/versions folder is empty - db.metadata.create_all(engine) + db.metadata.create_all(create_engine(os.getenv('DATABASE_URL'))) + subprocess.Popen(['alembic', 'upgrade', 'head']).wait() print('Schema upgraded!') - if args.flush: - cnt = 0 - with app.app_context(): - for u in db.session.execute(select(User)).scalars(): - u.recompute_karma() - cnt += 1 - db.session.add(u) - db.session.commit() - print(f'Recomputed karma of {cnt} users') - print(f'Visit ') diff --git a/freak/colors.py b/freak/colors.py deleted file mode 100644 index 2391f87..0000000 --- a/freak/colors.py +++ /dev/null @@ -1,39 +0,0 @@ - - -from collections import namedtuple - - -ColorTheme = namedtuple('ColorTheme', 'code name') - -## actual color codes are set in CSS - -color_themes = [ - ColorTheme(0, 'Default'), - ColorTheme(1, 'Rei'), - ColorTheme(2, 'Ai'), - ColorTheme(3, 'Aqua'), - ColorTheme(4, 'Neru'), - ColorTheme(5, 'Gumi'), - ColorTheme(6, 'Emu'), - ColorTheme(7, 'Spacegray'), - ColorTheme(8, 'Haku'), - ColorTheme(9, 'Miku'), - ColorTheme(10, 'Defoko'), - ColorTheme(11, 'Kaito'), - ColorTheme(12, 'Meiko'), - ColorTheme(13, 'Leek'), - ColorTheme(14, 'Teto'), - ColorTheme(15, 'Ruby') -] - -def theme_classes(color_code: int): - cl = [] - sch, th = divmod(color_code, 256) - if sch == 1: - cl.append('color-scheme-light') - if sch == 2: - cl.append('color-scheme-dark') - if 1 <= th <= 15: - cl.append(f'color-theme-{th}') - - return ' '.join(cl) diff --git a/freak/dei.py b/freak/dei.py deleted file mode 100644 index 8ddebb4..0000000 --- a/freak/dei.py +++ /dev/null @@ -1,77 +0,0 @@ -""" -Utilities for Diversity, Equity, Inclusion -""" - -from __future__ import annotations - - -BRICKS = '@abcdefghijklmnopqrstuvwxyz+?-\'/' -# legend @: space, -: literal, +: suffix (i.e. ae+r expands to ae/aer), ': literal, ?: unknown, /: separator - -class Pronoun(int): - PRESETS = { - 'hh': 'he/him', - 'sh': 'she/her', - 'tt': 'they/them', - 'ii': 'it/its', - 'hs': 'he/she', - 'ht': 'he/they', - 'hi': 'he/it', - 'shh': 'she/he', - 'st': 'she/they', - 'si': 'she/it', - 'th': 'they/he', - 'ts': 'they/she', - 'ti': 'they/it', - } - - UNSPECIFIED = 0 - - ## presets from PronounDB - ## DO NOT TOUCH the values unless you know their exact correspondence!! - ## hint: Pronoun.from_short() - HE = HE_HIM = 264 - SHE = SHE_HER = 275 - THEY = THEY_THEM = 660 - IT = IT_ITS = 297 - HE_SHE = 616 - HE_THEY = 648 - HE_IT = 296 - SHE_HE = 8467 - SHE_THEY = 657 - SHE_IT = 307 - THEY_HE = 276 - THEY_SHE = 628 - THEY_IT = 308 - ANY = 26049 - OTHER = 19047055 - ASK = 11873 - AVOID = NAME_ONLY = 4505281 - - def short(self) -> str: - i = self - s = '' - while i > 0: - s += BRICKS[i % 32] - i >>= 5 - return s - - def full(self): - s = self.short() - - if s in self.PRESETS: - return self.PRESETS[s] - - if '+' in s: - s1, s2 = s.rsplit('+') - s = s1 + '/' + s1 + s2 - - return s - __str__ = full - - @classmethod - def from_short(self, s: str) -> Pronoun: - i = 0 - for j, ch in enumerate(s): - i += BRICKS.index(ch) << (5 * j) - return Pronoun(i) diff --git a/freak/filters.py b/freak/filters.py index 378cfc5..4c1d0be 100644 --- a/freak/filters.py +++ b/freak/filters.py @@ -1,36 +1,78 @@ -import markdown +import re, markdown +from markdown.inlinepatterns import InlineProcessor, SimpleTagInlineProcessor +import xml.etree.ElementTree as etree from markupsafe import Markup -from suou import Snowflake -from suou.markdown import StrikethroughExtension, SpoilerExtension, PingExtension from . import app -# make spoilers prevail over blockquotes +from .iding import id_to_b32l + +#### MARKDOWN EXTENSIONS #### + +class StrikethroughExtension(markdown.extensions.Extension): + def extendMarkdown(self, md: markdown.Markdown, md_globals=None): + postprocessor = StrikethroughPostprocessor(md) + md.postprocessors.register(postprocessor, 'strikethrough', 0) + +class StrikethroughPostprocessor(markdown.postprocessors.Postprocessor): + pattern = re.compile(r"~~(((?!~~).)+)~~", re.DOTALL) + + def run(self, html): + return re.sub(self.pattern, self.convert, html) + + def convert(self, match): + return '' + match.group(1) + '' + + +### XXX it currently only detects spoilers that are not at the beginning of the line. To be fixed. +class SpoilerExtension(markdown.extensions.Extension): + def extendMarkdown(self, md: markdown.Markdown, md_globals=None): + md.inlinePatterns.register(SimpleTagInlineProcessor(r'()>!(.*?)!<', 'span class="spoiler"'), 'spoiler', 14) + + @classmethod + def patch_blockquote_processor(cls): + """Patch BlockquoteProcessor to make Spoiler prevail over blockquotes.""" + from markdown.blockprocessors import BlockQuoteProcessor + BlockQuoteProcessor.RE = re.compile(r'(^|\n)[ ]{0,3}>(?!!)[ ]?(.*)') + +# make spoilers prevail over blockquotes SpoilerExtension.patch_blockquote_processor() +class MentionPattern(InlineProcessor): + def __init__(self, regex, url_prefix: str): + super().__init__(regex) + self.url_prefix = url_prefix + def handleMatch(self, m, data): + el = etree.Element('a') + el.attrib['href'] = self.url_prefix + m.group(1) + el.text = m.group(0) + return el, m.start(0), m.end(0) + +class PingExtension(markdown.extensions.Extension): + def extendMarkdown(self, md: markdown.Markdown, md_globals=None): + md.inlinePatterns.register(MentionPattern(r'@([a-zA-Z0-9_-]{2,32})', '/@'), 'ping_mention', 14) + md.inlinePatterns.register(MentionPattern(r'\+([a-zA-Z0-9_-]{2,32})', '/+'), 'ping_mention', 14) + @app.template_filter() def to_markdown(text, toc = False): extensions = [ 'tables', 'footnotes', 'fenced_code', 'sane_lists', StrikethroughExtension(), SpoilerExtension(), - PingExtension({'@': '/@', '+': '/+'}) + ## XXX untested + PingExtension() ] if toc: extensions.append('toc') return Markup(markdown.Markdown(extensions=extensions).convert(text)) -app.template_filter('markdown')(to_markdown) - @app.template_filter() def to_b32l(n): - return Snowflake(n).to_b32l() + return id_to_b32l(n) -app.template_filter('b32l')(to_b32l) @app.template_filter() -def append(text, l: list): +def append(text, l): l.append(text) return None - diff --git a/freak/iding.py b/freak/iding.py index 3295393..b028f53 100644 --- a/freak/iding.py +++ b/freak/iding.py @@ -1,24 +1,17 @@ """ -DEPRECATED use suou.snowflake instead. - PSA: this module is for the LEGACY (v2) iding. -For the SIQ-based ID's, see suou.iding . - -The suou library also provides snowflake support. +For the SIQ-based ID's (upcoming 0.4), see suou.iding """ import base64 import os import time -from suou.functools import deprecated - epoch = 1577833200000 machine_id = int(os.getenv("MACHINE_ID", "0")) machine_counter = 0 -@deprecated('use SnowflakeGen(). Planned for removal in 0.5') def new_id(*, from_date = None): global machine_counter @@ -35,16 +28,14 @@ def new_id(*, from_date = None): ((machine_counter := machine_counter + 1) % 1024) ) -@deprecated('use suou.Snowflake.to_b32l() instead') -def id_to_b32l(n: int) -> str: +def id_to_b32l(n): return ( '_' if n < 0 else '' ) + base64.b32encode( (-n if n < 0 else n).to_bytes(10, 'big') ).decode().lstrip('A').lower() -@deprecated('use suou.Snowflake.from_b32l() instead') -def id_from_b32l(s: str) -> int: +def id_from_b32l(s, *, n_bytes=10): return (-1 if s.startswith('_') else 1) * int.from_bytes( base64.b32decode(s.lstrip('_').upper().rjust(16, 'A').encode()), 'big' ) diff --git a/freak/models.py b/freak/models.py index 3f0b480..99872f4 100644 --- a/freak/models.py +++ b/freak/models.py @@ -4,20 +4,18 @@ from __future__ import annotations from collections import namedtuple import datetime -from functools import partial +from functools import lru_cache from operator import or_ from threading import Lock -from sqlalchemy import Column, Integer, String, ForeignKey, UniqueConstraint, text, \ +from sqlalchemy import Column, String, ForeignKey, and_, text, \ CheckConstraint, Date, DateTime, Boolean, func, BigInteger, \ - SmallInteger, select, update, Table -from sqlalchemy.orm import Relationship, relationship + SmallInteger, select, insert, update, create_engine, Table +from sqlalchemy.orm import Relationship, declarative_base, relationship from flask_sqlalchemy import SQLAlchemy from flask_login import AnonymousUserMixin -from suou import SiqType, Snowflake, Wanted, deprecated, not_implemented -from suou.sqlalchemy import create_session, declarative_base, id_column, parent_children, snowflake_column from werkzeug.security import check_password_hash - -from freak import app_config +import os +from .iding import new_id, id_to_b32l from .utils import age_and_days, get_remote_addr, timed_cache @@ -27,27 +25,23 @@ USER_ACTIVE = 0 USER_INACTIVE = 1 USER_BANNED = 2 -ReportReason = namedtuple('ReportReason', 'num_code code description extra', defaults=dict(extra=None)) +ReportReason = namedtuple('ReportReason', 'num_code code description') post_report_reasons = [ - ## emergency ReportReason(110, 'hate_speech', 'Extreme hate speech / terrorism'), - ReportReason(121, 'csam', 'Child abuse or endangerment', extra=dict(suspend=True)), + ReportReason(121, 'csam', 'Child abuse or endangerment'), ReportReason(142, 'revenge_sxm', 'Revenge porn'), ReportReason(122, 'black_market', 'Sale or promotion of regulated goods (e.g. firearms)'), - ## urgent ReportReason(171, 'xxx', 'Pornography'), ReportReason(111, 'tasteless', 'Extreme violence / gore'), ReportReason(180, 'impersonation', 'Impersonation'), ReportReason(141, 'doxing', 'Diffusion of PII (personally identifiable information)'), - ## less urgent + ReportReason(123, 'copyviol', 'This is my creation and someone else is using it without my permission/license'), ReportReason(140, 'bullying', 'Harassment, bullying, or suicide incitement'), ReportReason(112, 'lgbt_hate', 'Hate speech against LGBTQ+ or women'), ReportReason(150, 'security_exploit', 'Dangerous security exploit or violation'), ReportReason(190, 'false_information', 'False or deceiving information'), - ReportReason(123, 'copyviol', 'This is my creation and someone else is using it without my permission/license'), - ## minor (unironically) - ReportReason(210, 'underage', 'Presence in violation of age limits (i.e. under 13, or minor in adult spaces)', extra=dict(suspend=True)) + ReportReason(210, 'underage', 'Presence in violation of age limits (i.e. under 13, or minor in adult spaces)') ] REPORT_REASON_STRINGS = { **{x.num_code: x.description for x in post_report_reasons}, **{x.code: x.description for x in post_report_reasons} } @@ -64,18 +58,20 @@ REPORT_UPDATE_ON_HOLD = 3 ## END constants and enums -Base = declarative_base(app_config.domain_name, app_config.secret_key, - snowflake_epoch=1577833200) +Base = declarative_base() db = SQLAlchemy(model_class=Base) -CSI = create_session_interactively = partial(create_session, app_config.database_url) +def create_session_interactively(): + '''Create a session for querying the database in Python REPL.''' + engine = create_engine(os.getenv('DATABASE_URL')) + return db.Session(bind = engine) +CSI = create_session_interactively -# the BaseModel() class will be removed in 0.5 -from .iding import new_id -@deprecated('id_column() and explicit id column are better. Will be removed in 0.5') +## TODO replace with suou.declarative_base() - upcoming 0.4 class BaseModel(Base): __abstract__ = True + id = Column(BigInteger, primary_key=True, default=new_id) ## Many-to-many relationship keys for some reasons have to go @@ -90,22 +86,10 @@ PostUpvote = Table( Column('is_downvote', Boolean, server_default=text('false')) ) -UserBlock = Table( - 'freak_user_block', - Base.metadata, - Column('actor_id', BigInteger, ForeignKey('freak_user.id'), primary_key=True), - Column('target_id', BigInteger, ForeignKey('freak_user.id'), primary_key=True) -) - - -class User(Base): +class User(BaseModel): __tablename__ = 'freak_user' - __table_args__ = ( - ## XXX this constraint (and the other three at Post, Guild and Comment) cannot be removed!! - UniqueConstraint('id', name='user_id_uniq'), - ) - id = snowflake_column() + id = Column(BigInteger, primary_key=True, default=new_id, unique=True) username = Column(String(32), CheckConstraint(text("username = lower(username) and username ~ '^[a-z0-9_-]+$'"), name="user_username_valid"), unique=True, nullable=False) display_name = Column(String(64), nullable=False) @@ -118,10 +102,7 @@ class User(Base): is_disabled_by_user = Column(Boolean, server_default=text('false'), nullable=False) karma = Column(BigInteger, server_default=text('0'), nullable=False) legacy_id = Column(BigInteger, nullable=True) - - pronouns = Column(Integer, server_default=text('0'), nullable=False) - biography = Column(String(1024), nullable=True) - color_theme = Column(SmallInteger, nullable=False, server_default=text('0')) + # TODO add pronouns and biography (upcoming 0.4) # moderation banned_at = Column(DateTime, nullable=True) @@ -129,22 +110,18 @@ class User(Base): banned_reason = Column(SmallInteger, server_default=text('0'), nullable=True) banned_until = Column(DateTime, nullable=True) banned_message = Column(String(256), nullable=True) - - # invites - is_approved = Column(Boolean, server_default=text('false'), nullable=False) - invited_by_id = Column(BigInteger, ForeignKey('freak_user.id', name='user_inviter_id'), nullable=True) # utilities - ## XXX posts and comments relationships are temporarily disabled because they make - ## SQLAlchemy fail initialization of models — bricking the app. - ## Posts are queried manually anyway #posts = relationship("Post", back_populates='author', ) upvoted_posts = relationship("Post", secondary=PostUpvote, back_populates='upvoters') #comments = relationship("Comment", back_populates='author') - + ## XXX posts and comments relationships are temporarily disabled because they make + ## SQLAlchemy fail initialization of models — bricking the app. + ## Posts are queried manually anyway + @property def is_disabled(self): - return (self.banned_at is not None and (self.banned_until is None or self.banned_until <= datetime.datetime.now())) or self.is_disabled_by_user + return self.banned_at is not None or self.is_disabled_by_user @property def is_active(self): @@ -174,7 +151,7 @@ class User(Base): """ ## XXX change func name? return dict( - id = Snowflake(self.id).to_b32l(), + id = id_to_b32l(self.id), username = self.username, display_name = self.display_name, age = self.age() @@ -182,18 +159,15 @@ class User(Base): ) def reward(self, points=1): - """ - Manipulate a user's karma on the fly - """ with Lock(): db.session.execute(update(User).where(User.id == self.id).values(karma = self.karma + points)) db.session.commit() def can_create_guild(self): - ## TODO make guild creation requirements configurable return self.karma > 15 or self.is_administrator - can_create_community = deprecated('use .can_create_guild()')(can_create_guild) + ## deprecated alias! + can_create_community = can_create_guild def handle(self): return f'@{self.username}' @@ -214,43 +188,10 @@ class User(Base): def not_suspended(cls): return or_(User.banned_at == None, User.banned_until <= datetime.datetime.now()) - @classmethod - def has_not_blocked(cls, actor, target): - """ - Filter out a content if the author has blocked current user. - - XXX untested. - """ - - # TODO add recognition - actor_id = actor - target_id = target - - qq= ~select(UserBlock).where(UserBlock.c.actor_id == actor_id, UserBlock.c.target_id == target_id).exists() - print(qq) - return qq - - def recompute_karma(self): - c = 0 - c += db.session.execute(select(func.count('*')).select_from(Post).where(Post.author == self)).scalar() - c += db.session.execute(select(func.count('*')).select_from(PostUpvote).join(Post).where(Post.author == self, PostUpvote.c.is_downvote == False)).scalar() - c -= db.session.execute(select(func.count('*')).select_from(PostUpvote).join(Post).where(Post.author == self, PostUpvote.c.is_downvote == True)).scalar() - - self.karma = c - - @timed_cache(60) - def strike_count(self) -> int: - return db.session.execute(select(func.count('*')).select_from(UserStrike).where(UserStrike.user_id == self.id)).scalar() - -## END User - -class Guild(Base): +class Topic(BaseModel): __tablename__ = 'freak_topic' - __table_args__ = ( - UniqueConstraint('id', name='topic_id_uniq'), - ) - id = snowflake_column() + id = Column(BigInteger, primary_key=True, default=new_id, unique=True) name = Column(String(32), CheckConstraint(text("name = lower(name) AND name ~ '^[a-z0-9_-]+$'"), name='topic_name_valid'), unique=True, nullable=False) display_name = Column(String(64), nullable=False) @@ -258,12 +199,8 @@ class Guild(Base): created_at = Column(DateTime, server_default=func.current_timestamp(), index=True, nullable=False) owner_id = Column(BigInteger, ForeignKey('freak_user.id', name='topic_owner_id'), nullable=True) language = Column(String(16), server_default=text("'en-US'")) - # true: prevent non-members from participating - is_restricted = Column(Boolean, server_default=text('false'), nullable=False) - # false: make the guild invite-only - is_public = Column(Boolean, server_default=text('true'), nullable=False) + privacy = Column(SmallInteger, server_default=text('0')) - # MUST NOT be filled in on post-0.2 instances legacy_id = Column(BigInteger, nullable=True) def url(self): @@ -273,56 +210,16 @@ class Guild(Base): return f'+{self.name}' # utilities - posts = relationship('Post', back_populates='guild') - - -Topic = deprecated('renamed to Guild')(Guild) - -## END Guild - -class Member(Base): - """ - User-Guild relationship. NEW in 0.4.0. - """ - __tablename__ = 'freak_member' - __table_args__ = ( - UniqueConstraint('user_id', 'guild_id', name='member_user_topic'), - ) - - ## Newer tables use SIQ. Older tables will gradually transition to SIQ as well. - id = id_column(SiqType.MANYTOMANY) - user_id = Column(BigInteger, ForeignKey('freak_user.id')) - guild_id = Column(BigInteger, ForeignKey('freak_topic.id')) - is_approved = Column(Boolean, server_default=text('false'), nullable=False) - is_subscribed = Column(Boolean, server_default=text('false'), nullable=False) - is_moderator = Column(Boolean, server_default=text('false'), nullable=False) - - # moderation - banned_at = Column(DateTime, nullable=True) - banned_by_id = Column(BigInteger, ForeignKey('freak_user.id', name='user_banner_id'), nullable=True) - banned_reason = Column(SmallInteger, server_default=text('0'), nullable=True) - banned_until = Column(DateTime, nullable=True) - banned_message = Column(String(256), nullable=True) - - user = relationship(User, primaryjoin = lambda: User.id == Member.user_id) - guild = relationship(Guild) - banned_by = relationship(User, primaryjoin= lambda: User.id == Member.banned_by_id) - - @property - def is_banned(self): - return self.banned_at is not None and (self.banned_until is None or self.banned_until <= datetime.datetime.now()) + posts = relationship('Post', back_populates='topic') POST_TYPE_DEFAULT = 0 POST_TYPE_LINK = 1 -class Post(Base): +class Post(BaseModel): __tablename__ = 'freak_post' - __table_args__ = ( - UniqueConstraint('id', name='post_id_uniq'), - ) - id = snowflake_column() + id = Column(BigInteger, primary_key=True, default=new_id, unique=True) slug = Column(String(64), CheckConstraint("slug IS NULL OR (slug = lower(slug) AND slug ~ '^[a-z0-9_-]+$')", name='post_slug_valid'), nullable=True) title = Column(String(256), nullable=False) @@ -346,17 +243,16 @@ class Post(Base): # utilities author: Relationship[User] = relationship("User", lazy='selectin', foreign_keys=[author_id])#, back_populates="posts") - guild = relationship("Guild", back_populates="posts", lazy='selectin') + topic = relationship("Topic", back_populates="posts", lazy='selectin') comments = relationship("Comment", back_populates="parent_post") upvoters = relationship("User", secondary=PostUpvote, back_populates='upvoted_posts') - def topic_or_user(self) -> Guild | User: - return self.guild or self.author + def topic_or_user(self) -> Topic | User: + return self.topic or self.author def url(self): - return self.topic_or_user().url() + '/comments/' + Snowflake(self.id).to_b32l() + '/' + (self.slug or '') + return self.topic_or_user().url() + '/comments/' + id_to_b32l(self.id) + '/' + (self.slug or '') - @not_implemented def generate_slug(self): return slugify.slugify(self.title, max_length=64) @@ -367,7 +263,7 @@ class Post(Base): def upvoted_by(self, user: User | AnonymousUserMixin | None): if not user or not user.is_authenticated: return 0 - v: PostUpvote | None = db.session.execute(select(PostUpvote.c).where(PostUpvote.c.voter_id == user.id, PostUpvote.c.post_id == self.id)).fetchone() + v = db.session.execute(db.select(PostUpvote.c).where(PostUpvote.c.voter_id == user.id, PostUpvote.c.post_id == self.id)).fetchone() if v: if v.is_downvote: return -1 @@ -378,7 +274,7 @@ class Post(Base): return db.session.execute(select(Comment).where(Comment.parent_comment == None, Comment.parent_post == self).order_by(Comment.created_at.desc()).limit(limit)).scalars() def report_url(self) -> str: - return f'/report/post/{Snowflake(self.id):l}' + return '/report/post/' + id_to_b32l(self.id) def report_count(self) -> int: return db.session.execute(select(func.count('*')).select_from(PostReport).where(PostReport.target_id == self.id, ~PostReport.update_status.in_((1, 2)))).scalar() @@ -401,13 +297,12 @@ class Post(Base): return or_(Post.author_id == user.id, Post.privacy.in_((0, 1))) -class Comment(Base): +class Comment(BaseModel): __tablename__ = 'freak_comment' - __table_args__ = ( - UniqueConstraint('id', name='comment_id_uniq'), - ) - id = snowflake_column() + # tweak to allow remote_side to work + ## XXX will be changed in 0.4 to suou.id_column() + id = Column(BigInteger, primary_key=True, default=new_id, unique=True) author_id = Column(BigInteger, ForeignKey('freak_user.id', name='comment_author_id'), nullable=True) parent_post_id = Column(BigInteger, ForeignKey('freak_post.id', name='comment_parent_post_id'), nullable=False) @@ -418,7 +313,6 @@ class Comment(Base): updated_at = Column(DateTime, nullable=True) is_locked = Column(Boolean, server_default=text('false')) - ## DO NOT FILL IN! intended for 0.2 or earlier legacy_id = Column(BigInteger, nullable=True) removed_at = Column(DateTime, nullable=True) @@ -426,14 +320,15 @@ class Comment(Base): removed_reason = Column(SmallInteger, nullable=True) author = relationship('User', foreign_keys=[author_id])#, back_populates='comments') - parent_post: Relationship[Post] = relationship("Post", back_populates="comments", foreign_keys=[parent_post_id]) - parent_comment, child_comments = parent_children('comment', parent_remote_side=Wanted('id')) + parent_post = relationship("Post", back_populates="comments", foreign_keys=[parent_post_id]) + parent_comment = relationship("Comment", back_populates="child_comments", remote_side=[id]) + child_comments = relationship("Comment", back_populates="parent_comment") def url(self): - return self.parent_post.url() + f'/comment/{Snowflake(self.id):l}' + return self.parent_post.url() + '/comment/' + id_to_b32l(self.id) def report_url(self) -> str: - return f'/report/comment/{Snowflake(self.id):l}' + return '/report/comment/' + id_to_b32l(self.id) def report_count(self) -> int: return db.session.execute(select(func.count('*')).select_from(PostReport).where(PostReport.target_id == self.id, ~PostReport.update_status.in_((1, 2)))).scalar() @@ -446,10 +341,8 @@ class Comment(Base): def not_removed(cls): return Post.removed_at == None -class PostReport(Base): +class PostReport(BaseModel): __tablename__ = 'freak_postreport' - - id = snowflake_column() author_id = Column(BigInteger, ForeignKey('freak_user.id', name='report_author_id'), nullable=True) target_type = Column(SmallInteger, nullable=False) @@ -460,7 +353,7 @@ class PostReport(Base): created_ip = Column(String(64), default=get_remote_addr, nullable=False) author = relationship('User') - + def target(self): if self.target_type == REPORT_TARGET_POST: return db.session.execute(select(Post).where(Post.id == self.target_id)).scalar() @@ -469,27 +362,6 @@ class PostReport(Base): else: return self.target_id - def is_critical(self): - return self.reason_code in ( - 121, 142, 210 - ) - -class UserStrike(Base): - __tablename__ = 'freak_user_strike' - - id = id_column(SiqType.MULTI) - - user_id = Column(BigInteger, ForeignKey('freak_user.id', ondelete='cascade'), nullable=False) - target_type = Column(SmallInteger, nullable=False) - target_id = Column(BigInteger, nullable=False) - target_content = Column(String(4096), nullable=True) - reason_code = Column(SmallInteger, nullable=False) - issued_at = Column(DateTime, server_default=func.current_timestamp()) - issued_by_id = Column(BigInteger, ForeignKey('freak_user.id'), nullable=True) - - user = relationship(User, primaryjoin= lambda: User.id == UserStrike.user_id) - issued_by = relationship(User, primaryjoin= lambda: User.id == UserStrike.issued_by_id) - # PostUpvote table is at the top !! diff --git a/freak/rest/__init__.py b/freak/rest/__init__.py index b203afa..3e3013c 100644 --- a/freak/rest/__init__.py +++ b/freak/rest/__init__.py @@ -2,8 +2,8 @@ from flask import Blueprint from flask_restx import Resource, Api -from sqlalchemy import select -from suou import Snowflake + +from freak.iding import id_to_b32l from ..models import Post, User, db @@ -21,31 +21,31 @@ class Nurupo(Resource): @rest.route('/user/') class UserInfo(Resource): def get(self, id: int): - u: User | None = db.session.execute(select(User).where(User.id == id)).scalar() + u: User | None = db.session.execute(db.select(User).where(User.id == id)).scalar() if u is None: return dict(error='User not found'), 404 uj = dict( - id = f'{Snowflake(u.id):l}', + id = id_to_b32l(u.id), username = u.username, display_name = u.display_name, joined_at = u.joined_at.isoformat('T'), karma = u.karma, age = u.age() ) - return dict(users={f'{Snowflake(id):l}': uj}) + return dict(users={id_to_b32l(id): uj}) @rest.route('/post/') class SinglePost(Resource): def get(self, id: int): - p: Post | None = db.session.execute(select(Post).where(Post.id == id)).scalar() + p: Post | None = db.session.execute(db.select(Post).where(Post.id == id)).scalar() if p is None: return dict(error='Not found'), 404 pj = dict( - id = f'{Snowflake(p.id):l}', + id = id_to_b32l(p.id), title = p.title, author = p.author.simple_info(), to = p.topic_or_user().handle(), created_at = p.created_at.isoformat('T') ) - return dict(posts={f'{Snowflake(id):l}': pj}) \ No newline at end of file + return dict(posts={id_to_b32l(id): pj}) \ No newline at end of file diff --git a/freak/static/js/lib.js b/freak/static/js/lib.js index 0e6f824..7bebaed 100644 --- a/freak/static/js/lib.js +++ b/freak/static/js/lib.js @@ -1,6 +1,14 @@ (function(){ - "use strict"; - + // UNUSED! Period is disallowed regardless now + function checkUsername(u){ + return ( + /^\./.test(u)? 'You cannot start username with a period.': + /\.$/.test(u)? 'You cannot end username with a period.': + /\.\./.test(u)? 'You cannot have more than one period in a row.': + u.match(/\.(com|net|org|txt)$/)? 'Your username cannot end with .' + forbidden_extensions[1]: + 'ok' + ); + } function attachUsernameInput(){ @@ -132,36 +140,9 @@ }).then(e => e.json()); } - function enableThemeChange() { - let schemeItems = document.querySelectorAll('.apply-theme [name="color_scheme"]'); - - for (let ii of schemeItems) { - ii.addEventListener('change', function(e) { - let removed_classes = Array.from(document.body.classList).filter((x) => /^color-scheme-/.test(x)); - document.body.classList.remove(...removed_classes); - if (e.target.value !== 'unset') { - document.body.classList.add(`color-scheme-${e.target.value}`); - } - console.log(`Color scheme changed to ${e.target.value}`) - }) - } - - let themeItems = document.querySelectorAll('.apply-theme [name="color_theme"]'); - - for (let ii of themeItems) { - ii.addEventListener('change', function(e) { - let removed_classes = Array.from(document.body.classList).filter((x) => /^color-theme-/.test(x)); - document.body.classList.remove(...removed_classes); - document.body.classList.add(`color-theme-${e.target.value}`); - console.log(`Color theme changed to ${e.target.value}`) - }) - } - } - function main() { attachUsernameInput(); enablePostVotes(); - enableThemeChange(); } main(); diff --git a/freak/static/sass/base.sass b/freak/static/sass/base.sass index a45f049..71772ab 100644 --- a/freak/static/sass/base.sass +++ b/freak/static/sass/base.sass @@ -5,26 +5,10 @@ box-sizing: border-box \:root - --c0-accent: #ff7300 - --c1-accent: #ff7300 - --c2-accent: #f837ce - --c3-accent: #38b8ff - --c4-accent: #ffe338 - --c5-accent: #78f038 - --c6-accent: #ff9aae - --c7-accent: #606080 - --c8-accent: #aeaac0 - --c9-accent: #3ae0b8 - --c10-accent: #a828ba - --c11-accent: #1871d8 - --c12-accent: #885a18 - --c13-accent: #38a856 - --c14-accent: #ff3018 - --c15-accent: #ff1668 - --light-text-primary: #181818 --light-text-alt: #444 --light-border: #999 + --light-accent: #ff7300 --light-success: #73af00 --light-error: #e04433 --light-canvas: #eaecee @@ -34,21 +18,17 @@ --dark-text-primary: #e8e8e8 --dark-text-alt: #c0cad3 --dark-border: #777 + --dark-accent: #ff7300 --dark-success: #93cf00 --dark-error: #e04433 --dark-canvas: #0a0a0e --dark-background: #181a21 --dark-bg-sharp: #080808 - --accent: var(--c0-accent) - - // the following are DEPRECATED // - --light-accent: var(--accent) - --dark-accent: var(--accent) - --text-primary: var(--light-text-primary) --text-alt: var(--light-text-alt) --border: var(--light-border) + --accent: var(--light-accent) --success: var(--light-success) --error: var(--light-error) --canvas: var(--light-canvas) @@ -60,77 +40,35 @@ --text-primary: var(--dark-text-primary) --text-alt: var(--dark-text-alt) --border: var(--dark-border) + --accent: var(--dark-accent) --success: var(--dark-success) --error: var(--dark-error) --canvas: var(--dark-canvas) --background: var(--dark-background) --bg-sharp: var(--dark-bg-sharp) -.color-scheme-light +body.color-scheme-light --text-primary: var(--light-text-primary) --text-alt: var(--light-text-alt) --border: var(--light-border) + --accent: var(--light-accent) --success: var(--light-success) --error: var(--light-error) --canvas: var(--light-canvas) --background: var(--light-background) --bg-sharp: var(--light-bg-sharp) -.color-scheme-dark +body.color-scheme-dark --text-primary: var(--dark-text-primary) --text-alt: var(--dark-text-alt) --border: var(--dark-border) + --accent: var(--dark-accent) --success: var(--dark-success) --error: var(--dark-error) --canvas: var(--dark-canvas) --background: var(--dark-background) --bg-sharp: var(--dark-bg-sharp) -.color-theme-1 - --accent: var(--c1-accent) - -.color-theme-2 - --accent: var(--c2-accent) - -.color-theme-3 - --accent: var(--c3-accent) - -.color-theme-4 - --accent: var(--c4-accent) - -.color-theme-5 - --accent: var(--c5-accent) - -.color-theme-6 - --accent: var(--c6-accent) - -.color-theme-7 - --accent: var(--c7-accent) - -.color-theme-8 - --accent: var(--c8-accent) - -.color-theme-9 - --accent: var(--c9-accent) - -.color-theme-10 - --accent: var(--c10-accent) - -.color-theme-11 - --accent: var(--c11-accent) - -.color-theme-12 - --accent: var(--c12-accent) - -.color-theme-13 - --accent: var(--c13-accent) - -.color-theme-14 - --accent: var(--c14-accent) - -.color-theme-15 - --accent: var(--c15-accent) - body, input, select, button font-family: $ui-fonts diff --git a/freak/static/sass/content.sass b/freak/static/sass/content.sass index 56354ec..5b0b4b3 100644 --- a/freak/static/sass/content.sass +++ b/freak/static/sass/content.sass @@ -29,7 +29,6 @@ blockquote ul margin: 4px 0 padding: 0 - padding-inline-start: 1.5em > li margin: 0 diff --git a/freak/static/sass/layout.sass b/freak/static/sass/layout.sass index efb58ef..ff7e034 100644 --- a/freak/static/sass/layout.sass +++ b/freak/static/sass/layout.sass @@ -54,11 +54,6 @@ header.header &, > ul, > ul > li:has(.mini-search-bar) flex: 1 - - ul > li span - color: var(--text-primary) - font-size: .6em - .header-username > * display: block @@ -108,9 +103,6 @@ aside.card padding: 12px margin: -12px -12px 0 -12px position: relative - a - color: inherit - text-decoration: underline > ul list-style: none margin: 0 @@ -140,7 +132,6 @@ ul.inline list-style: none padding: 0 margin: 0 - display: inline > li display: inline &::before @@ -148,31 +139,13 @@ ul.inline margin: 0 .5em &:first-child::before content: '' - margin: 0 - -ul.grid - list-style: none - padding: 0 - display: grid - grid-template-columns: 1fr 1fr 1fr 1fr - grid-template-rows: auto - > li - border: 1px solid var(--border) - border-radius: .5em - padding: .5em - margin: 1em .5em - text-align: center - small - display: block - ul.message-options color: var(--text-alt) list-style: none padding: 0 font-size: smaller - .comment-frame & - margin-bottom: -4px + margin-bottom: -4px .post-frame margin-left: 3em @@ -183,9 +156,6 @@ ul.message-options margin-left: 0 margin-right: 3em - .message-options - margin-bottom: 1em - .message-stats position: absolute left: -3em @@ -302,7 +272,7 @@ button, [type="submit"], [type="reset"], [type="button"] &.primary background-color: var(--accent) - color: var(--background) + color: var(--bg-main) &[disabled] opacity: .5 @@ -328,27 +298,5 @@ button, [type="submit"], [type="reset"], [type="button"] width: 0 margin-right: auto -.border-accent - border: var(--accent) 1px solid - display: inline-flex - align-items: center - padding: 0 4px -.round - border-radius: 1em - -.done - opacity: .5 - -button.card - width: 100% - padding: .5em 1em - background-color: transparent - border-color: var(--accent) - color: var(--accent) - border-radius: 1em - - &.primary - background-color: var(--accent) - color: var(--background) diff --git a/freak/static/sass/mobile.sass b/freak/static/sass/mobile.sass index 2ed1831..25a8e02 100644 --- a/freak/static/sass/mobile.sass +++ b/freak/static/sass/mobile.sass @@ -6,19 +6,11 @@ .content-nav, .content-main width: 100% - ul.grid - grid-template-columns: 1fr 1fr - - .nomobile - display: none - @media screen and (max-width: 960px) .header-username display: none header.header - padding: .5em .5em - .mini-search-bar display: none @@ -26,11 +18,4 @@ display: inline-block ul > li:has(.mini-search-bar) - flex: unset - - -// not mobile: // - -@media screen and (min-width: 801px) - .mobileonly - display: none \ No newline at end of file + flex: unset \ No newline at end of file diff --git a/freak/templates/admin/admin_base.html b/freak/templates/admin/admin_base.html index a61de60..11a0306 100644 --- a/freak/templates/admin/admin_base.html +++ b/freak/templates/admin/admin_base.html @@ -6,13 +6,11 @@ - {% for private_style in private_styles %} - - {% endfor %} + - +
{% for message in get_flashed_messages() %} diff --git a/freak/templates/admin/admin_home.html b/freak/templates/admin/admin_home.html index bdaa7ae..ad49860 100644 --- a/freak/templates/admin/admin_home.html +++ b/freak/templates/admin/admin_home.html @@ -1,12 +1,9 @@ {% extends "admin/admin_base.html" %} {% block content %} -
    + {% endblock %} \ No newline at end of file diff --git a/freak/templates/admin/admin_report_detail.html b/freak/templates/admin/admin_report_detail.html index 370134e..8844c5a 100644 --- a/freak/templates/admin/admin_report_detail.html +++ b/freak/templates/admin/admin_report_detail.html @@ -1,6 +1,5 @@ {% extends "admin/admin_base.html" %} {% from "macros/embed.html" import embed_post with context %} -{% from "macros/icon.html" import icon, callout with context %} {% block content %}

    Report detail #{{ report.id }}

    @@ -15,20 +14,10 @@ {% else %}

    Unknown media type

    {% endif %} - {% if report.is_critical() %} - {% call callout('nsfw_language') %} - This is a critical offense. “Strike” will immediately suspend the offender's account. - {% endcall %} - {% endif %}
    - {% if report.is_critical() %} - - {% else %} - - {% endif %}
    {% endblock %} diff --git a/freak/templates/admin/admin_strikes.html b/freak/templates/admin/admin_strikes.html deleted file mode 100644 index 87b71a1..0000000 --- a/freak/templates/admin/admin_strikes.html +++ /dev/null @@ -1,21 +0,0 @@ -{% extends "admin/admin_base.html" %} -{% from "macros/feed.html" import stop_scrolling, no_more_scrolling with context %} - -{% block content %} -
      - {% for strike in strike_list %} -
    • -

      #{{ strike.id }} to {{ strike.user.handle() }}

      -
        -
      • Reason: {{ report_reasons[strike.reason_code] }}
      • - -
      -
    • - {% endfor %} - {% if strike_list.has_next %} - {{ stop_scrolling(strike_list.page) }} - {% else %} - {{ no_more_scrolling(strike_list.page) }} - {% endif %} -
    -{% endblock %} \ No newline at end of file diff --git a/freak/templates/base.html b/freak/templates/base.html index 579cee4..0038452 100644 --- a/freak/templates/base.html +++ b/freak/templates/base.html @@ -4,7 +4,6 @@ - {% from "macros/icon.html" import icon with context %} {% block title %} {{ app_name }} {% endblock %} @@ -15,8 +14,6 @@ This service is age-restricted; do not access if underage. More info: https://{{ domain_name }}/terms --> - - {# psa: icons url MUST be supplied by .env via PRIVATE_ASSETS= #} @@ -26,7 +23,7 @@ - +

    {{ app_name }}

    @@ -46,28 +43,30 @@ {% if g.no_user %} {% elif current_user.is_authenticated %} -
  • - +
  • create - New post - -
  • -
  • {{ icon('profile')}}profile +
  • + + profile +
  • - {{ current_user.handle() }} - {{ icon('karma') }} {{ current_user.karma }} karma -
  • + @{{ current_user.username }} + {{ current_user.karma }} karma +
    +
  • - {{ icon('logout') }} log out + log out
  • {% else %}
  • - {{ icon('logout') }}log in -
  • -
  • - {{ icon('join') }}register -
  • + + log in +
  • + + register +
  • {% endif %}
@@ -103,9 +102,9 @@ function changeAccentColorTime() { let hours = (new Date).getHours(); if (hours < 6 || hours >= 19) { - document.body.classList.add('night'); + document.body.style.setProperty('--accent', '#1871d8'); } else { - document.body.classList.remove('night'); + document.body.style.removeProperty('--accent'); } } changeAccentColorTime(); diff --git a/freak/templates/feed.html b/freak/templates/feed.html index 281de92..01057a4 100644 --- a/freak/templates/feed.html +++ b/freak/templates/feed.html @@ -1,7 +1,6 @@ {% extends "base.html" %} {% from "macros/feed.html" import feed_post, stop_scrolling, no_more_scrolling with context %} {% 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 #} @@ -17,11 +16,13 @@ {% block nav %} {% if top_communities %} - {{ nav_top_communities(top_communities) }} + {% from "macros/nav.html" import nav_top_communities with context %} + {{ nav_top_communities(top_communities) }} {% endif %} - {% if feed_type == 'guild' %} - {{ nav_guild(guild) }} + {% if feed_type == 'topic' %} + {% from "macros/nav.html" import nav_topic with context %} + {{ nav_topic(topic) }} {% endif %}