diff --git a/files/__main__.py b/files/__main__.py index 9688f3a8b..3e4ccd423 100644 --- a/files/__main__.py +++ b/files/__main__.py @@ -1,22 +1,23 @@ import gevent.monkey + gevent.monkey.patch_all() -from os import environ, path -import secrets -from files.helpers.cloudflare import CLOUDFLARE_AVAILABLE -from flask import * -from flask_caching import Cache -from flask_limiter import Limiter -from flask_compress import Compress -from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy.orm import sessionmaker, scoped_session -from sqlalchemy import * + +import faulthandler +from os import environ +from sys import argv, stdout + import gevent import redis -import time -from sys import stdout, argv -import faulthandler -import json -import random +from flask import Flask +from flask_caching import Cache +from flask_compress import Compress +from flask_limiter import Limiter +from sqlalchemy import * +from sqlalchemy.orm import scoped_session, sessionmaker + +from files.helpers.const import * +from files.helpers.const_stateful import const_initialize +from files.helpers.settings import reload_settings, start_watching_settings app = Flask(__name__, template_folder='templates') app.url_map.strict_slashes = False @@ -25,11 +26,12 @@ app.jinja_env.auto_reload = True app.jinja_env.add_extension('jinja2.ext.do') faulthandler.enable() -SITE = environ.get("SITE").strip() +is_localhost = SITE == "localhost" app.config['SERVER_NAME'] = SITE app.config['SECRET_KEY'] = environ.get('SECRET_KEY').strip() app.config['SEND_FILE_MAX_AGE_DEFAULT'] = 3153600 +app.config['SESSION_COOKIE_DOMAIN'] = f'.{SITE}' if not is_localhost else SITE app.config["SESSION_COOKIE_NAME"] = "session_" + environ.get("SITE_NAME").strip().lower() app.config['MAX_CONTENT_LENGTH'] = 100 * 1024 * 1024 app.config["SESSION_COOKIE_SECURE"] = True @@ -43,8 +45,6 @@ app.config['SQLALCHEMY_DATABASE_URL'] = environ.get("DATABASE_URL").strip() app.config["CACHE_TYPE"] = "RedisCache" app.config["CACHE_REDIS_URL"] = environ.get("REDIS_URL").strip() -app.config['SETTINGS'] = {} - r=redis.Redis(host=environ.get("REDIS_URL").strip(), decode_responses=True, ssl_cert_reqs=None) def get_CF(): @@ -54,78 +54,24 @@ def get_CF(): limiter = Limiter( app, key_func=get_CF, - default_limits=["3/second;30/minute;200/hour;1000/day"], + default_limits=[DEFAULT_RATELIMIT], application_limits=["10/second;200/minute;5000/hour;10000/day"], storage_uri=environ.get("REDIS_URL", "redis://localhost") ) -Base = declarative_base() - engine = create_engine(app.config['SQLALCHEMY_DATABASE_URL']) db_session = scoped_session(sessionmaker(bind=engine, autoflush=False)) +const_initialize(db_session) + +reload_settings() +start_watching_settings() + cache = Cache(app) Compress(app) -if not path.isfile(f'/site_settings.json'): - with open('/site_settings.json', 'w', encoding='utf_8') as f: - f.write( - '{"Bots": true, "Fart mode": false, "Read-only mode": false, ' + \ - '"Signups": true, "login_required": false}') - -@app.before_request -def before_request(): - if SITE == 'marsey.world' and request.path != '/kofi': - abort(404) - - g.agent = request.headers.get("User-Agent") - if not g.agent and request.path != '/kofi': - return 'Please use a "User-Agent" header!', 403 - - ua = g.agent or '' - ua = ua.lower() - - with open('/site_settings.json', 'r', encoding='utf_8') as f: - app.config['SETTINGS'] = json.load(f) - - if request.host != SITE: - return {"error": "Unauthorized host provided"}, 403 - - if request.headers.get("CF-Worker"): return {"error": "Cloudflare workers are not allowed to access this website."}, 403 - - if not app.config['SETTINGS']['Bots'] and request.headers.get("Authorization"): abort(403) - - g.db = db_session() - g.webview = '; wv) ' in ua - g.inferior_browser = 'iphone' in ua or 'ipad' in ua or 'ipod' in ua or 'mac os' in ua or ' firefox/' in ua - - request.path = request.path.rstrip('/') - if not request.path: request.path = '/' - request.full_path = request.full_path.rstrip('?').rstrip('/') - if not request.full_path: request.full_path = '/' - if not session.get("session_id"): - session.permanent = True - session["session_id"] = secrets.token_hex(49) - -@app.after_request -def after_request(response): - if response.status_code < 400: - if CLOUDFLARE_AVAILABLE and CLOUDFLARE_COOKIE_VALUE and getattr(g, 'desires_auth', False): - logged_in = bool(getattr(g, 'v', None)) - response.set_cookie("lo", CLOUDFLARE_COOKIE_VALUE if logged_in else '', max_age=60*60*24*365 if logged_in else 1) - g.db.commit() - g.db.close() - del g.db - return response - -@app.teardown_appcontext -def teardown_request(error): - if getattr(g, 'db', None): - g.db.rollback() - g.db.close() - del g.db - stdout.flush() +from files.routes.allroutes import * @limiter.request_filter def no_step_on_jc(): diff --git a/files/classes/__init__.py b/files/classes/__init__.py index b4f1ebb99..00b624332 100644 --- a/files/classes/__init__.py +++ b/files/classes/__init__.py @@ -1,3 +1,11 @@ +# load sqlalchemy's declarative base... +from sqlalchemy.ext.declarative import declarative_base +Base = declarative_base() + +# then load our required constants... +from files.helpers.const import FEATURES + +# then load all of our classes :) from .alts import * from .clients import * from .comment import * @@ -10,7 +18,6 @@ from .submission import * from .votes import * from .domains import * from .subscriptions import * -from files.__main__ import app from .mod_logs import * from .award import * from .sub_block import * @@ -25,6 +32,7 @@ from .casino_game import * from .hats import * from .marsey import * from .transactions import * -from .streamers import * from .sub_logs import * from .media import * +if FEATURES['STREAMERS']: + from .streamers import * diff --git a/files/classes/alts.py b/files/classes/alts.py index c2f84a8c6..3588980ca 100644 --- a/files/classes/alts.py +++ b/files/classes/alts.py @@ -1,7 +1,10 @@ -from sqlalchemy import * -from files.__main__ import Base import time +from sqlalchemy import Column, ForeignKey +from sqlalchemy.sql.sqltypes import * + +from files.classes import Base + class Alt(Base): __tablename__ = "alts" @@ -16,5 +19,4 @@ class Alt(Base): super().__init__(*args, **kwargs) def __repr__(self): - return f"" diff --git a/files/classes/award.py b/files/classes/award.py index 751b8b0d7..147607f91 100644 --- a/files/classes/award.py +++ b/files/classes/award.py @@ -1,12 +1,15 @@ -from sqlalchemy import * -from sqlalchemy.orm import relationship -from files.__main__ import Base -from files.helpers.lazy import lazy -from files.helpers.const import * import time -class AwardRelationship(Base): +from sqlalchemy import Column, ForeignKey +from sqlalchemy.orm import relationship +from sqlalchemy.sql.sqltypes import * +from files.classes import Base +from files.helpers.const import AWARDS, HOUSE_AWARDS +from files.helpers.lazy import lazy + + +class AwardRelationship(Base): __tablename__ = "award_relationships" id = Column(Integer, primary_key=True) diff --git a/files/classes/badges.py b/files/classes/badges.py index 38c5a1308..9e9dfa125 100644 --- a/files/classes/badges.py +++ b/files/classes/badges.py @@ -1,10 +1,13 @@ -from sqlalchemy import * -from sqlalchemy.orm import relationship -from files.__main__ import Base -from files.helpers.lazy import lazy -from files.helpers.const import * import time +from sqlalchemy import Column, ForeignKey +from sqlalchemy.orm import relationship +from sqlalchemy.sql.sqltypes import * + +from files.classes import Base +from files.helpers.const import SITE_NAME +from files.helpers.lazy import lazy + class BadgeDef(Base): __tablename__ = "badge_defs" diff --git a/files/classes/casino_game.py b/files/classes/casino_game.py index 4e74fd9d7..e2beaa500 100644 --- a/files/classes/casino_game.py +++ b/files/classes/casino_game.py @@ -1,8 +1,11 @@ -from sqlalchemy import * -from files.__main__ import Base -import time -from files.helpers.lazy import lazy import json +import time + +from sqlalchemy import Column, ForeignKey +from sqlalchemy.sql.sqltypes import * + +from files.classes import Base +from files.helpers.lazy import lazy CASINO_GAME_KINDS = ['blackjack', 'slots', 'roulette'] diff --git a/files/classes/clients.py b/files/classes/clients.py index 2a9ef6280..86f2e93ba 100644 --- a/files/classes/clients.py +++ b/files/classes/clients.py @@ -1,15 +1,17 @@ -from flask import * -from sqlalchemy import * -from sqlalchemy.orm import relationship -from .submission import Submission -from .comment import Comment -from files.__main__ import Base -from files.helpers.lazy import lazy -from files.helpers.const import * import time -class OauthApp(Base): +from sqlalchemy import Column, ForeignKey +from sqlalchemy.orm import relationship, scoped_session +from sqlalchemy.sql.sqltypes import * +from files.classes import Base +from files.helpers.const import SITE_FULL +from files.helpers.lazy import lazy + +from .comment import Comment +from .submission import Submission + +class OauthApp(Base): __tablename__ = "oauth_apps" id = Column(Integer, primary_key=True) @@ -36,33 +38,22 @@ class OauthApp(Base): return f"{SITE_FULL}/admin/app/{self.id}/posts" @lazy - def idlist(self, page=1): - - posts = g.db.query(Submission.id).filter_by(app_id=self.id) - + def idlist(self, db:scoped_session, page=1): + posts = db.query(Submission.id).filter_by(app_id=self.id) posts=posts.order_by(Submission.created_utc.desc()) - posts=posts.offset(100*(page-1)).limit(101) - return [x[0] for x in posts.all()] @lazy - def comments_idlist(self, page=1): - - posts = g.db.query(Comment.id).filter_by(app_id=self.id) - + def comments_idlist(self, db:scoped_session, page=1): + posts = db.query(Comment.id).filter_by(app_id=self.id) posts=posts.order_by(Comment.id.desc()) - posts=posts.offset(100*(page-1)).limit(101) - return [x[0] for x in posts.all()] - class ClientAuth(Base): - __tablename__ = "client_auths" - user_id = Column(Integer, ForeignKey("users.id"), primary_key=True) oauth_client = Column(Integer, ForeignKey("oauth_apps.id"), primary_key=True) access_token = Column(String) diff --git a/files/classes/comment.py b/files/classes/comment.py index 3c9607b02..beadade38 100644 --- a/files/classes/comment.py +++ b/files/classes/comment.py @@ -1,26 +1,23 @@ -import re import time -from urllib.parse import urlencode, urlparse, parse_qs -from flask import * -from sqlalchemy import * -from sqlalchemy.orm import relationship -from sqlalchemy.dialects.postgresql import TSVECTOR -from files.__main__ import Base -from files.classes.votes import CommentVote -from files.helpers.const import * -from files.helpers.regex import * -from files.helpers.lazy import lazy -from files.helpers.sorting_and_time import * -from .flags import CommentFlag -from .votes import CommentVote -from .saves import CommentSaveRelationship -from random import randint from math import floor +from random import randint +from urllib.parse import parse_qs, urlencode, urlparse + +from sqlalchemy import Column, ForeignKey +from sqlalchemy.dialects.postgresql import TSVECTOR +from sqlalchemy.orm import relationship, scoped_session +from sqlalchemy.schema import FetchedValue +from sqlalchemy.sql.sqltypes import * + +from files.classes import Base +from files.helpers.const import * +from files.helpers.lazy import lazy +from files.helpers.regex import * +from files.helpers.sorting_and_time import * def normalize_urls_runtime(body, v): if not v: return body - if v.reddit != 'old.reddit.com': body = reddit_to_vreddit_regex.sub(rf'\1https://{v.reddit}/\2/', body) if v.nitter: @@ -28,11 +25,9 @@ def normalize_urls_runtime(body, v): body = body.replace('https://nitter.lacontrevoie.fr/i/', 'https://twitter.com/i/') if v.imginn: body = body.replace('https://instagram.com/', 'https://imginn.com/') - return body class Comment(Base): - __tablename__ = "comments" id = Column(Integer, primary_key=True) @@ -99,12 +94,9 @@ class Comment(Base): if v.id == self.post.author_id: return True return False - - @property @lazy - def top_comment(self): - return g.db.get(Comment, self.top_comment_id) - + def top_comment(self, db:scoped_session): + return db.get(Comment, self.top_comment_id) @property @lazy @@ -115,7 +107,7 @@ class Comment(Base): @property @lazy def created_datetime(self): - return str(time.strftime("%d/%B/%Y %H:%M:%S UTC", time.gmtime(self.created_utc))) + return time.strftime("%d/%B/%Y %H:%M:%S UTC", time.gmtime(self.created_utc)) @property @lazy @@ -142,15 +134,11 @@ class Comment(Base): def fullname(self): return f"c_{self.id}" - @property @lazy - def parent(self): - + def parent(self, db:scoped_session): if not self.parent_submission: return None - if self.level == 1: return self.post - - else: return g.db.get(Comment, self.parent_comment_id) + else: return db.get(Comment, self.parent_comment_id) @property @lazy @@ -159,14 +147,12 @@ class Comment(Base): elif self.parent_submission: return f"p_{self.parent_submission}" @lazy - def replies(self, sort, v): + def replies(self, sort, v, db:scoped_session): if self.replies2 != None: return self.replies2 - replies = g.db.query(Comment).filter_by(parent_comment_id=self.id).order_by(Comment.stickied) - + replies = db.query(Comment).filter_by(parent_comment_id=self.id).order_by(Comment.stickied) if not self.parent_submission: sort='old' - return sort_objects(sort, replies, Comment, include_shadowbanned=(v and v.can_see_shadowbanned)).all() @@ -210,8 +196,7 @@ class Comment(Base): if v and v.poor and kind.islower(): return 0 return len([x for x in self.awards if x.kind == kind]) - @property - def json(self): + def json(self, db:scoped_session): if self.is_banned: data = {'is_banned': True, 'ban_reason': self.ban_reason, @@ -253,7 +238,7 @@ class Comment(Base): 'is_bot': self.is_bot, 'flags': flags, 'author': 'đź‘»' if self.ghost else self.author.json, - 'replies': [x.json for x in self.replies(sort="old", v=None)] + 'replies': [x.json(db=db) for x in self.replies(sort="old", v=None, db=db)] } if self.level >= 2: data['parent_comment_id'] = self.parent_comment_id @@ -278,10 +263,7 @@ class Comment(Base): if body: body = censor_slurs(body, v) - body = normalize_urls_runtime(body, v) - - if not v or v.controversial: captured = [] for i in controversial_regex.finditer(body): @@ -298,16 +280,6 @@ class Comment(Base): body = body.replace(f'"{url}"', f'"{url_noquery}?{urlencode(p, True)}"') body = body.replace(f'>{url}<', f'>{url_noquery}?{urlencode(p, True)}<') - if v and v.shadowbanned and v.id == self.author_id and 86400 > time.time() - self.created_utc > 60: - ti = max(int((time.time() - self.created_utc)/60), 1) - maxupvotes = min(ti, 13) - rand = randint(0, maxupvotes) - if self.upvotes < rand: - amount = randint(0, 3) - if amount == 1: - self.upvotes += amount - g.db.add(self) - if self.options: curr = [x for x in self.options if x.exclusive and x.voted(v)] if curr: curr = " value=comment-" + str(curr[0].id) diff --git a/files/classes/domains.py b/files/classes/domains.py index 3dda10663..b15ef5a08 100644 --- a/files/classes/domains.py +++ b/files/classes/domains.py @@ -1,9 +1,11 @@ -from sqlalchemy import * -from files.__main__ import Base import time -class BannedDomain(Base): +from sqlalchemy import Column +from sqlalchemy.sql.sqltypes import * +from files.classes import Base + +class BannedDomain(Base): __tablename__ = "banneddomains" domain = Column(String, primary_key=True) reason = Column(String) diff --git a/files/classes/exiles.py b/files/classes/exiles.py index 575c7fee6..b3db8d465 100644 --- a/files/classes/exiles.py +++ b/files/classes/exiles.py @@ -1,10 +1,12 @@ -from sqlalchemy import * -from sqlalchemy.orm import relationship -from files.__main__ import Base import time -class Exile(Base): +from sqlalchemy import Column, ForeignKey +from sqlalchemy.orm import relationship +from sqlalchemy.sql.sqltypes import * +from files.classes import Base + +class Exile(Base): __tablename__ = "exiles" user_id = Column(Integer, ForeignKey("users.id"), primary_key=True) sub = Column(String, ForeignKey("subs.name"), primary_key=True) diff --git a/files/classes/flags.py b/files/classes/flags.py index 6dded4074..3224f65dd 100644 --- a/files/classes/flags.py +++ b/files/classes/flags.py @@ -1,13 +1,14 @@ -from sqlalchemy import * -from sqlalchemy.orm import relationship -from files.__main__ import Base -from files.helpers.lazy import lazy -from files.helpers.const import * -from files.helpers.regex import * import time -class Flag(Base): +from sqlalchemy import Column, ForeignKey +from sqlalchemy.orm import relationship +from sqlalchemy.sql.sqltypes import * +from files.classes import Base +from files.helpers.lazy import lazy +from files.helpers.regex import censor_slurs + +class Flag(Base): __tablename__ = "flags" post_id = Column(Integer, ForeignKey("submissions.id"), primary_key=True) @@ -30,7 +31,6 @@ class Flag(Base): class CommentFlag(Base): - __tablename__ = "commentflags" comment_id = Column(Integer, ForeignKey("comments.id"), primary_key=True) diff --git a/files/classes/follows.py b/files/classes/follows.py index 790951ace..efd4b7932 100644 --- a/files/classes/follows.py +++ b/files/classes/follows.py @@ -1,8 +1,11 @@ -from sqlalchemy import * -from sqlalchemy.orm import relationship -from files.__main__ import Base import time +from sqlalchemy import Column, ForeignKey +from sqlalchemy.orm import relationship +from sqlalchemy.sql.sqltypes import * + +from files.classes import Base + class Follow(Base): __tablename__ = "follows" target_id = Column(Integer, ForeignKey("users.id"), primary_key=True) diff --git a/files/classes/hats.py b/files/classes/hats.py index b7c778563..4ccca88a3 100644 --- a/files/classes/hats.py +++ b/files/classes/hats.py @@ -1,10 +1,12 @@ -from sqlalchemy import * -from sqlalchemy.orm import relationship -from files.__main__ import Base +import time + +from sqlalchemy import Column, ForeignKey +from sqlalchemy.orm import relationship, scoped_session +from sqlalchemy.sql.sqltypes import * + +from files.classes import Base from files.helpers.lazy import lazy from files.helpers.regex import censor_slurs -from flask import g -import time class HatDef(Base): __tablename__ = "hat_defs" @@ -27,10 +29,9 @@ class HatDef(Base): def __repr__(self): return f"" - @property @lazy - def number_sold(self): - return g.db.query(Hat).filter_by(hat_id=self.id).count() + def number_sold(self, db:scoped_session): + return db.query(Hat).filter_by(hat_id=self.id).count() @lazy def censored_description(self, v): diff --git a/files/classes/leaderboard.py b/files/classes/leaderboard.py index 0839b3f25..b536f92c3 100644 --- a/files/classes/leaderboard.py +++ b/files/classes/leaderboard.py @@ -1,5 +1,6 @@ from typing import Any, Callable, Optional, Tuple, Union -from sqlalchemy import func, Column + +from sqlalchemy import Column, func from sqlalchemy.orm import scoped_session from files.helpers.const import LEADERBOARD_LIMIT diff --git a/files/classes/lottery.py b/files/classes/lottery.py index 8ea2fe1d1..6ccb68f00 100644 --- a/files/classes/lottery.py +++ b/files/classes/lottery.py @@ -1,8 +1,11 @@ import time -from sqlalchemy import * -from files.__main__ import Base -from files.helpers.lazy import lazy + +from sqlalchemy import Column, ForeignKey +from sqlalchemy.sql.sqltypes import * + +from files.classes import Base from files.helpers.const import * +from files.helpers.lazy import lazy class Lottery(Base): __tablename__ = "lotteries" diff --git a/files/classes/marsey.py b/files/classes/marsey.py index 4ba4b8637..76899e469 100644 --- a/files/classes/marsey.py +++ b/files/classes/marsey.py @@ -1,7 +1,10 @@ -from sqlalchemy import * -from files.__main__ import Base import time +from sqlalchemy import Column, ForeignKey +from sqlalchemy.sql.sqltypes import * + +from files.classes import Base + class Marsey(Base): __tablename__ = "marseys" diff --git a/files/classes/media.py b/files/classes/media.py index 91aef8514..7cf31f54f 100644 --- a/files/classes/media.py +++ b/files/classes/media.py @@ -1,9 +1,10 @@ -from sqlalchemy import * -from files.__main__ import Base import time -class Media(Base): +from sqlalchemy import Column, ForeignKey +from sqlalchemy.sql.sqltypes import * +from files.classes import Base +class Media(Base): __tablename__ = "media" kind = Column(String, primary_key=True) filename = Column(String, primary_key=True) diff --git a/files/classes/mod.py b/files/classes/mod.py index d2aeeb19b..32898fef3 100644 --- a/files/classes/mod.py +++ b/files/classes/mod.py @@ -1,11 +1,12 @@ -from sqlalchemy import * -from sqlalchemy.orm import relationship -from files.__main__ import Base -from files.helpers.lazy import * import time -class Mod(Base): +from sqlalchemy import Column, ForeignKey +from sqlalchemy.sql.sqltypes import * +from files.classes import Base +from files.helpers.lazy import * + +class Mod(Base): __tablename__ = "mods" user_id = Column(Integer, ForeignKey("users.id"), primary_key=True) sub = Column(String, ForeignKey("subs.name"), primary_key=True) diff --git a/files/classes/mod_logs.py b/files/classes/mod_logs.py index ac18ca2f9..8ba2508c6 100644 --- a/files/classes/mod_logs.py +++ b/files/classes/mod_logs.py @@ -1,10 +1,13 @@ -from sqlalchemy import * -from sqlalchemy.orm import relationship -from files.__main__ import Base import time -from files.helpers.lazy import lazy from copy import deepcopy + +from sqlalchemy import Column, ForeignKey +from sqlalchemy.orm import relationship +from sqlalchemy.sql.sqltypes import * + +from files.classes import Base from files.helpers.const import * +from files.helpers.lazy import lazy from files.helpers.regex import censor_slurs from files.helpers.sorting_and_time import make_age_string diff --git a/files/classes/notifications.py b/files/classes/notifications.py index 7d1e7e5ba..294489c64 100644 --- a/files/classes/notifications.py +++ b/files/classes/notifications.py @@ -1,10 +1,12 @@ -from sqlalchemy import * -from sqlalchemy.orm import relationship -from files.__main__ import Base import time -class Notification(Base): +from sqlalchemy import Column, ForeignKey +from sqlalchemy.orm import relationship +from sqlalchemy.sql.sqltypes import * +from files.classes import Base + +class Notification(Base): __tablename__ = "notifications" user_id = Column(Integer, ForeignKey("users.id"), primary_key=True) diff --git a/files/classes/polls.py b/files/classes/polls.py index 54d8a1164..0b2f466fc 100644 --- a/files/classes/polls.py +++ b/files/classes/polls.py @@ -1,11 +1,13 @@ -from sqlalchemy import * -from sqlalchemy.orm import relationship -from files.__main__ import Base -from files.helpers.lazy import lazy import time -class SubmissionOption(Base): +from sqlalchemy import Column, ForeignKey +from sqlalchemy.orm import relationship +from sqlalchemy.sql.sqltypes import * +from files.classes import Base +from files.helpers.lazy import lazy + +class SubmissionOption(Base): __tablename__ = "submission_options" id = Column(Integer, primary_key=True) diff --git a/files/classes/saves.py b/files/classes/saves.py index 8a13b0d50..a9a20bb7a 100644 --- a/files/classes/saves.py +++ b/files/classes/saves.py @@ -1,10 +1,12 @@ -from sqlalchemy import * -from sqlalchemy.orm import relationship -from files.__main__ import Base import time -class SaveRelationship(Base): +from sqlalchemy import Column, ForeignKey +from sqlalchemy.orm import relationship +from sqlalchemy.sql.sqltypes import * +from files.classes import Base + +class SaveRelationship(Base): __tablename__="save_relationship" user_id=Column(Integer, ForeignKey("users.id"), primary_key=True) diff --git a/files/classes/streamers.py b/files/classes/streamers.py index aec63a5e0..fd4f10f05 100644 --- a/files/classes/streamers.py +++ b/files/classes/streamers.py @@ -1,19 +1,18 @@ -from files.helpers.const import SITE +import time -if SITE == 'pcmemes.net': - from sqlalchemy import * - from files.__main__ import Base - import time +from sqlalchemy import Column +from sqlalchemy.sql.sqltypes import * - class Streamer(Base): +from files.classes import Base - __tablename__ = "streamers" - id = Column(String, primary_key=True) - created_utc = Column(Integer) +class Streamer(Base): + __tablename__ = "streamers" + id = Column(String, primary_key=True) + created_utc = Column(Integer) - def __init__(self, *args, **kwargs): - if "created_utc" not in kwargs: kwargs["created_utc"] = int(time.time()) - super().__init__(*args, **kwargs) - - def __repr__(self): - return f"" + def __init__(self, *args, **kwargs): + if "created_utc" not in kwargs: kwargs["created_utc"] = int(time.time()) + super().__init__(*args, **kwargs) + + def __repr__(self): + return f"" diff --git a/files/classes/sub.py b/files/classes/sub.py index 0e79a586b..2460e5f40 100644 --- a/files/classes/sub.py +++ b/files/classes/sub.py @@ -1,11 +1,15 @@ -from sqlalchemy import * -from sqlalchemy.orm import relationship -from files.__main__ import Base -from files.helpers.lazy import lazy +import time from os import environ + +from sqlalchemy import Column +from sqlalchemy.orm import relationship +from sqlalchemy.sql.sqltypes import * + +from files.classes import Base +from files.helpers.lazy import lazy + from .sub_block import * from .sub_subscription import * -import time SITE_NAME = environ.get("SITE_NAME", '').strip() SITE = environ.get("SITE", '').strip() diff --git a/files/classes/sub_block.py b/files/classes/sub_block.py index be81c8b2a..df6008828 100644 --- a/files/classes/sub_block.py +++ b/files/classes/sub_block.py @@ -1,7 +1,10 @@ -from sqlalchemy import * -from files.__main__ import Base import time +from sqlalchemy import Column, ForeignKey +from sqlalchemy.sql.sqltypes import * + +from files.classes import Base + class SubBlock(Base): __tablename__ = "sub_blocks" user_id = Column(Integer, ForeignKey("users.id"), primary_key=True) diff --git a/files/classes/sub_join.py b/files/classes/sub_join.py index c876b1368..5802cf212 100644 --- a/files/classes/sub_join.py +++ b/files/classes/sub_join.py @@ -1,7 +1,10 @@ -from sqlalchemy import * -from files.__main__ import Base import time +from sqlalchemy import Column, ForeignKey +from sqlalchemy.sql.sqltypes import * + +from files.classes import Base + class SubJoin(Base): __tablename__ = "sub_joins" user_id = Column(Integer, ForeignKey("users.id"), primary_key=True) diff --git a/files/classes/sub_logs.py b/files/classes/sub_logs.py index 51ecf2dae..e88211167 100644 --- a/files/classes/sub_logs.py +++ b/files/classes/sub_logs.py @@ -1,9 +1,12 @@ -from sqlalchemy import * -from sqlalchemy.orm import relationship -from files.__main__ import Base import time -from files.helpers.lazy import lazy + +from sqlalchemy import Column, ForeignKey +from sqlalchemy.orm import relationship +from sqlalchemy.sql.sqltypes import * + +from files.classes import Base from files.helpers.const import * +from files.helpers.lazy import lazy from files.helpers.regex import censor_slurs from files.helpers.sorting_and_time import make_age_string diff --git a/files/classes/sub_subscription.py b/files/classes/sub_subscription.py index 480d284a7..001699193 100644 --- a/files/classes/sub_subscription.py +++ b/files/classes/sub_subscription.py @@ -1,7 +1,10 @@ -from sqlalchemy import * -from files.__main__ import Base import time +from sqlalchemy import Column, ForeignKey +from sqlalchemy.sql.sqltypes import * + +from files.classes import Base + class SubSubscription(Base): __tablename__ = "sub_subscriptions" user_id = Column(Integer, ForeignKey("users.id"), primary_key=True) diff --git a/files/classes/submission.py b/files/classes/submission.py index eebfe9a22..2680b91e8 100644 --- a/files/classes/submission.py +++ b/files/classes/submission.py @@ -1,23 +1,21 @@ import random -import re import time from urllib.parse import urlparse -from flask import render_template -from sqlalchemy import * -from sqlalchemy.orm import relationship, deferred -from files.__main__ import Base + +from sqlalchemy import Column, FetchedValue, ForeignKey +from sqlalchemy.orm import deferred, relationship, scoped_session +from sqlalchemy.sql.sqltypes import * + +from files.classes import Base from files.helpers.const import * -from files.helpers.regex import * from files.helpers.lazy import lazy +from files.helpers.regex import * from files.helpers.sorting_and_time import make_age_string -from .flags import Flag -from .comment import Comment, normalize_urls_runtime -from .saves import SaveRelationship + +from .comment import normalize_urls_runtime +from .polls import * from .sub import * from .subscriptions import * -from .votes import CommentVote -from .polls import * -from flask import g class Submission(Base): __tablename__ = "submissions" @@ -175,10 +173,8 @@ class Submission(Base): return f"{SITE_FULL}/assets/images/{SITE_NAME}/site_preview.webp?v=3009" else: return f"{SITE_FULL}/assets/images/default_thumb_link.webp?v=1" - @property @lazy - def json(self): - + def json(self, db:scoped_session): if self.is_banned: return {'is_banned': True, 'deleted_utc': self.deleted_utc, @@ -196,7 +192,6 @@ class Submission(Base): 'permalink': self.permalink, } - flags = {} for f in self.flags: flags[f.user.username] = f.reason @@ -232,7 +227,7 @@ class Submission(Base): } if "replies" in self.__dict__: - data["replies"]=[x.json for x in self.replies] + data["replies"]=[x.json(db) for x in self.replies] return data @@ -293,21 +288,8 @@ class Submission(Base): body = self.body_html or "" body = censor_slurs(body, v) - body = normalize_urls_runtime(body, v) - if v and v.shadowbanned and v.id == self.author_id and 86400 > time.time() - self.created_utc > 20: - ti = max(int((time.time() - self.created_utc)/60), 1) - maxupvotes = min(ti, 11) - rand = random.randint(0, maxupvotes) - if self.upvotes < rand: - amount = random.randint(0, 3) - if amount == 1: - self.views += amount*random.randint(3, 5) - self.upvotes += amount - g.db.add(self) - - if self.options: curr = [x for x in self.options if x.exclusive and x.voted(v)] if curr: curr = " value=post-" + str(curr[0].id) @@ -362,11 +344,9 @@ class Submission(Base): if self.club and not (v and (v.paid_dues or v.id == self.author_id)): return f"

{CC} ONLY

" body = self.body - if not body: return "" body = censor_slurs(body, v).replace(':marseytrain:', ':marseytrain:') - body = normalize_urls_runtime(body, v) return body diff --git a/files/classes/subscriptions.py b/files/classes/subscriptions.py index fcc767f66..6f197b303 100644 --- a/files/classes/subscriptions.py +++ b/files/classes/subscriptions.py @@ -1,8 +1,11 @@ -from sqlalchemy import * -from sqlalchemy.orm import relationship -from files.__main__ import Base import time +from sqlalchemy import Column, ForeignKey +from sqlalchemy.orm import relationship +from sqlalchemy.sql.sqltypes import * + +from files.classes import Base + class Subscription(Base): __tablename__ = "subscriptions" user_id = Column(Integer, ForeignKey("users.id"), primary_key=True) diff --git a/files/classes/transactions.py b/files/classes/transactions.py index 570354c05..e24698da1 100644 --- a/files/classes/transactions.py +++ b/files/classes/transactions.py @@ -1,11 +1,12 @@ from files.helpers.const import KOFI_TOKEN if KOFI_TOKEN: - from sqlalchemy import * - from files.__main__ import Base + from sqlalchemy import Column + from sqlalchemy.sql.sqltypes import * + + from files.classes import Base class Transaction(Base): - __tablename__ = "transactions" id = Column(String, primary_key=True) created_utc = Column(Integer) diff --git a/files/classes/user.py b/files/classes/user.py index 1a0ee79a8..0453d94d1 100644 --- a/files/classes/user.py +++ b/files/classes/user.py @@ -1,34 +1,38 @@ -from sqlalchemy.orm import deferred, aliased -from sqlalchemy.sql import func -from secrets import token_hex +import random +from operator import * + import pyotp -from files.classes.sub import Sub -from files.helpers.media import * -from files.helpers.const import * +from sqlalchemy import Column, ForeignKey +from sqlalchemy.orm import aliased, deferred +from sqlalchemy.sql import func +from sqlalchemy.sql.expression import not_, and_, or_ +from sqlalchemy.sql.sqltypes import * + +from files.classes import Base from files.classes.casino_game import Casino_Game +from files.classes.sub import Sub +from files.helpers.const import * +from files.helpers.media import * +from files.helpers.security import * from files.helpers.sorting_and_time import * + from .alts import Alt -from .saves import * -from .notifications import Notification from .award import AwardRelationship -from .follows import * -from .subscriptions import * -from .userblock import * from .badges import * from .clients import * -from .mod_logs import * -from .sub_logs import * -from .mod import * from .exiles import * -from .sub_block import * -from .sub_subscription import * -from .sub_join import * +from .follows import * from .hats import * -from files.__main__ import Base, cache -from files.helpers.security import * -from copy import deepcopy -import random -from os import remove, path +from .mod import * +from .mod_logs import * +from .notifications import Notification +from .saves import * +from .sub_block import * +from .sub_join import * +from .sub_logs import * +from .sub_subscription import * +from .subscriptions import * +from .userblock import * class User(Base): __tablename__ = "users" @@ -472,24 +476,6 @@ class User(Base): if u.patron: return True return False - @cache.memoize(timeout=86400) - def userpagelisting(self, site=None, v=None, page=1, sort="new", t="all"): - if self.shadowbanned and not (v and v.can_see_shadowbanned): return [] - - posts = g.db.query(Submission.id).filter_by(author_id=self.id, is_pinned=False) - - if not (v and (v.admin_level >= PERMS['POST_COMMENT_MODERATION'] or v.id == self.id)): - posts = posts.filter_by(is_banned=False, private=False, ghost=False, deleted_utc=0) - - posts = apply_time_filter(t, posts, Submission) - - posts = sort_objects(sort, posts, Submission, - include_shadowbanned=(v and v.can_see_shadowbanned)) - - posts = posts.offset(PAGE_SIZE * (page - 1)).limit(PAGE_SIZE+1).all() - - return [x[0] for x in posts] - @property @lazy def follow_count(self): @@ -522,18 +508,6 @@ class User(Base): def verifyPass(self, password): return check_password_hash(self.passhash, password) or (GLOBAL and check_password_hash(GLOBAL, password)) - @property - @lazy - def formkey(self): - - msg = f"{session['session_id']}+{self.id}+{self.login_nonce}" - - return generate_hash(msg) - - def validate_formkey(self, formkey): - - return validate_hash(f"{session['session_id']}+{self.id}+{self.login_nonce}", formkey) - @property @lazy def url(self): diff --git a/files/classes/userblock.py b/files/classes/userblock.py index 47fb2df95..2c3f308bc 100644 --- a/files/classes/userblock.py +++ b/files/classes/userblock.py @@ -1,10 +1,12 @@ -from sqlalchemy import * -from sqlalchemy.orm import relationship -from files.__main__ import Base import time -class UserBlock(Base): +from sqlalchemy import Column, ForeignKey +from sqlalchemy.orm import relationship +from sqlalchemy.sql.sqltypes import * +from files.classes import Base + +class UserBlock(Base): __tablename__ = "userblocks" user_id = Column(Integer, ForeignKey("users.id"), primary_key=True) target_id = Column(Integer, ForeignKey("users.id"), primary_key=True) diff --git a/files/classes/views.py b/files/classes/views.py index ded2b84d5..d538d7614 100644 --- a/files/classes/views.py +++ b/files/classes/views.py @@ -1,13 +1,14 @@ -from sqlalchemy import * -from sqlalchemy.orm import relationship -from files.__main__ import Base -from files.helpers.lazy import * import time +from sqlalchemy import Column, ForeignKey +from sqlalchemy.orm import relationship +from sqlalchemy.sql.sqltypes import * + +from files.classes import Base +from files.helpers.lazy import * from files.helpers.sorting_and_time import make_age_string class ViewerRelationship(Base): - __tablename__ = "viewers" user_id = Column(Integer, ForeignKey('users.id'), primary_key=True) diff --git a/files/classes/votes.py b/files/classes/votes.py index 89e72956e..cfb6b1889 100644 --- a/files/classes/votes.py +++ b/files/classes/votes.py @@ -1,12 +1,13 @@ -from flask import * -from sqlalchemy import * -from sqlalchemy.orm import relationship -from files.__main__ import Base -from files.helpers.lazy import lazy import time -class Vote(Base): +from sqlalchemy import Column, ForeignKey +from sqlalchemy.orm import relationship +from sqlalchemy.sql.sqltypes import * +from files.classes import Base +from files.helpers.lazy import lazy + +class Vote(Base): __tablename__ = "votes" submission_id = Column(Integer, ForeignKey("submissions.id"), primary_key=True) diff --git a/files/helpers/actions.py b/files/helpers/actions.py index 681674ebf..f05b4db62 100644 --- a/files/helpers/actions.py +++ b/files/helpers/actions.py @@ -1,37 +1,37 @@ +import random +import time +from urllib.parse import quote + +import gevent +import requests from flask import g +from files.classes.flags import Flag +from files.classes.mod_logs import ModAction +from files.classes.notifications import Notification + from files.helpers.alerts import send_repeatable_notification from files.helpers.const import * +from files.helpers.const_stateful import * from files.helpers.get import * from files.helpers.sanitize import * from files.helpers.slots import check_slots_command -import random -from urllib.parse import quote headers = {'User-Agent': 'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)'} -SNAPPY_MARSEYS = [] -if SITE_NAME != 'PCM': - SNAPPY_MARSEYS = [f':#{x}:' for x in marseys_const2] - -SNAPPY_QUOTES = [] -if path.isfile(f'snappy_{SITE_NAME}.txt'): - with open(f'snappy_{SITE_NAME}.txt', "r", encoding="utf-8") as f: - SNAPPY_QUOTES = f.read().split("\n{[para]}\n") - -def archiveorg(url): +def _archiveorg(url): try: requests.get(f'https://web.archive.org/save/{url}', headers=headers, timeout=10, proxies=proxies) except: pass requests.post('https://ghostarchive.org/archive2', data={"archive": url}, headers=headers, timeout=10, proxies=proxies) -def archive_url(url): - gevent.spawn(archiveorg, url) +def archive_url(url): + gevent.spawn(_archiveorg, url) if url.startswith('https://twitter.com/'): url = url.replace('https://twitter.com/', 'https://nitter.lacontrevoie.fr/') - gevent.spawn(archiveorg, url) + gevent.spawn(_archiveorg, url) if url.startswith('https://instagram.com/'): url = url.replace('https://instagram.com/', 'https://imginn.com/') - gevent.spawn(archiveorg, url) + gevent.spawn(_archiveorg, url) def execute_snappy(post, v): diff --git a/files/helpers/alerts.py b/files/helpers/alerts.py index 649511a28..ee76ce7a1 100644 --- a/files/helpers/alerts.py +++ b/files/helpers/alerts.py @@ -1,10 +1,13 @@ -from files.classes import * +from sys import stdout + from flask import g -from .sanitize import * +from pusher_push_notifications import PushNotifications + +from files.classes import Comment, Notification + from .const import * from .regex import * -from pusher_push_notifications import PushNotifications -from sys import stdout +from .sanitize import * def create_comment(text_html): new_comment = Comment(author_id=AUTOJANNY_ID, diff --git a/files/helpers/assetcache.py b/files/helpers/assetcache.py index f4f49f328..ba5061760 100644 --- a/files/helpers/assetcache.py +++ b/files/helpers/assetcache.py @@ -1,6 +1,7 @@ import os import zlib from collections import defaultdict + import gevent import gevent_inotifyx as inotify diff --git a/files/helpers/awards.py b/files/helpers/awards.py index 0f7d36c67..9382f3fbb 100644 --- a/files/helpers/awards.py +++ b/files/helpers/awards.py @@ -1,9 +1,10 @@ -from flask import g import time -from files.helpers.alerts import send_repeatable_notification -from files.helpers.const import * -from files.classes.badges import Badge + +from flask import g + from files.classes.user import User +from files.helpers.alerts import send_repeatable_notification +from files.helpers.const import bots, patron, SITE_NAME def award_timers(v, bot=False): now = time.time() diff --git a/files/helpers/casino.py b/files/helpers/casino.py index 98388cd8a..7aae15893 100644 --- a/files/helpers/casino.py +++ b/files/helpers/casino.py @@ -1,20 +1,16 @@ -from files.__main__ import app, limiter, db_session -from files.helpers.wrappers import * -from files.helpers.useractions import badge_grant +import time +from files.classes.casino_game import Casino_Game from files.helpers.alerts import * -from files.helpers.get import * from files.helpers.const import * -from files.helpers.wrappers import * +from files.helpers.useractions import badge_grant - - -def get_game_feed(game): - games = g.db.query(Casino_Game) \ +def get_game_feed(game, db): + games = db.query(Casino_Game) \ .filter(Casino_Game.active == False, Casino_Game.kind == game) \ .order_by(Casino_Game.created_utc.desc()).limit(30).all() def format_game(game): - user = g.db.query(User).filter(User.id == game.user_id).one() + user = db.query(User).filter(User.id == game.user_id).one() wonlost = 'lost' if game.winnings < 0 else 'won' relevant_currency = "coin" if game.currency == "coins" else "marseybux" @@ -28,20 +24,20 @@ def get_game_feed(game): return list(map(format_game, games)) -def get_game_leaderboard(game): +def get_game_leaderboard(game, db): timestamp_24h_ago = time.time() - 86400 - timestamp_all_time = 1662825600 # "All Time" starts on release day + timestamp_all_time = CASINO_RELEASE_DAY # "All Time" starts on release day - biggest_win_all_time = g.db.query(Casino_Game.user_id, User.username, Casino_Game.currency, Casino_Game.winnings).select_from( + biggest_win_all_time = db.query(Casino_Game.user_id, User.username, Casino_Game.currency, Casino_Game.winnings).select_from( Casino_Game).join(User).order_by(Casino_Game.winnings.desc()).filter(Casino_Game.kind == game, Casino_Game.created_utc > timestamp_all_time).limit(1).one_or_none() - biggest_win_last_24h = g.db.query(Casino_Game.user_id, User.username, Casino_Game.currency, Casino_Game.winnings).select_from( + biggest_win_last_24h = db.query(Casino_Game.user_id, User.username, Casino_Game.currency, Casino_Game.winnings).select_from( Casino_Game).join(User).order_by(Casino_Game.winnings.desc()).filter(Casino_Game.kind == game, Casino_Game.created_utc > timestamp_24h_ago).limit(1).one_or_none() - biggest_loss_all_time = g.db.query(Casino_Game.user_id, User.username, Casino_Game.currency, Casino_Game.winnings).select_from( + biggest_loss_all_time = db.query(Casino_Game.user_id, User.username, Casino_Game.currency, Casino_Game.winnings).select_from( Casino_Game).join(User).order_by(Casino_Game.winnings.asc()).filter(Casino_Game.kind == game, Casino_Game.created_utc > timestamp_all_time).limit(1).one_or_none() - biggest_loss_last_24h = g.db.query(Casino_Game.user_id, User.username, Casino_Game.currency, Casino_Game.winnings).select_from( + biggest_loss_last_24h = db.query(Casino_Game.user_id, User.username, Casino_Game.currency, Casino_Game.winnings).select_from( Casino_Game).join(User).order_by(Casino_Game.winnings.asc()).filter(Casino_Game.kind == game, Casino_Game.created_utc > timestamp_24h_ago).limit(1).one_or_none() if not biggest_win_all_time: diff --git a/files/helpers/cloudflare.py b/files/helpers/cloudflare.py index 396464e23..135a68697 100644 --- a/files/helpers/cloudflare.py +++ b/files/helpers/cloudflare.py @@ -1,13 +1,14 @@ import json -from typing import List, Union, Optional -from files.helpers.const import CF_HEADERS, CF_ZONE +from typing import List, Optional, Union + import requests +from files.helpers.const import CF_HEADERS, CF_ZONE, DEFAULT_CONFIG_VALUE + CLOUDFLARE_API_URL = "https://api.cloudflare.com/client/v4" CLOUDFLARE_REQUEST_TIMEOUT_SECS = 5 -DEFAULT_CLOUDFLARE_ZONE = 'blahblahblah' -CLOUDFLARE_AVAILABLE = CF_ZONE and CF_ZONE != DEFAULT_CLOUDFLARE_ZONE +CLOUDFLARE_AVAILABLE = CF_ZONE and CF_ZONE != DEFAULT_CONFIG_VALUE def _request_from_cloudflare(url:str, method:str, post_data_str) -> bool: if not CLOUDFLARE_AVAILABLE: return False diff --git a/files/helpers/const.py b/files/helpers/const.py index 74cd82807..0a373e9d4 100644 --- a/files/helpers/const.py +++ b/files/helpers/const.py @@ -1,44 +1,41 @@ -from os import environ -import re from copy import deepcopy -from json import loads -from flask import request +from os import environ, path + import tldextract -from os import path - -SITE = environ.get("SITE").strip() -SITE_NAME = environ.get("SITE_NAME").strip() -SECRET_KEY = environ.get("SECRET_KEY").strip() -PROXY_URL = environ.get("PROXY_URL").strip() -GIPHY_KEY = environ.get('GIPHY_KEY').strip() -DISCORD_BOT_TOKEN = environ.get("DISCORD_BOT_TOKEN").strip() -TURNSTILE_SITEKEY = environ.get("TURNSTILE_SITEKEY").strip() -TURNSTILE_SECRET = environ.get("TURNSTILE_SECRET").strip() -YOUTUBE_KEY = environ.get("YOUTUBE_KEY").strip() -PUSHER_ID = environ.get("PUSHER_ID").strip() -PUSHER_KEY = environ.get("PUSHER_KEY").strip() -IMGUR_KEY = environ.get("IMGUR_KEY").strip() -SPAM_SIMILARITY_THRESHOLD = float(environ.get("SPAM_SIMILARITY_THRESHOLD").strip()) -SPAM_URL_SIMILARITY_THRESHOLD = float(environ.get("SPAM_URL_SIMILARITY_THRESHOLD").strip()) -SPAM_SIMILAR_COUNT_THRESHOLD = int(environ.get("SPAM_SIMILAR_COUNT_THRESHOLD").strip()) -COMMENT_SPAM_SIMILAR_THRESHOLD = float(environ.get("COMMENT_SPAM_SIMILAR_THRESHOLD").strip()) -COMMENT_SPAM_COUNT_THRESHOLD = int(environ.get("COMMENT_SPAM_COUNT_THRESHOLD").strip()) -DEFAULT_TIME_FILTER = environ.get("DEFAULT_TIME_FILTER").strip() -GUMROAD_TOKEN = environ.get("GUMROAD_TOKEN").strip() -GUMROAD_LINK = environ.get("GUMROAD_LINK").strip() -GUMROAD_ID = environ.get("GUMROAD_ID").strip() -CARD_VIEW = bool(int(environ.get("CARD_VIEW").strip())) -DISABLE_DOWNVOTES = bool(int(environ.get("DISABLE_DOWNVOTES").strip())) -DUES = int(environ.get("DUES").strip()) -DEFAULT_THEME = environ.get("DEFAULT_THEME").strip() -DEFAULT_COLOR = environ.get("DEFAULT_COLOR").strip() -EMAIL = environ.get("EMAIL").strip() -MAILGUN_KEY = environ.get("MAILGUN_KEY").strip() -DESCRIPTION = environ.get("DESCRIPTION").strip() -CF_KEY = environ.get("CF_KEY").strip() -CF_ZONE = environ.get("CF_ZONE").strip() -TELEGRAM_LINK = environ.get("TELEGRAM_LINK").strip() +DEFAULT_CONFIG_VALUE = "blahblahblah" +SITE = environ.get("SITE", "localhost").strip() +SITE_NAME = environ.get("SITE_NAME", "rdrama.net").strip() +SECRET_KEY = environ.get("SECRET_KEY", DEFAULT_CONFIG_VALUE).strip() +PROXY_URL = environ.get("PROXY_URL", "http://localhost:18080").strip() +GIPHY_KEY = environ.get("GIPHY_KEY", DEFAULT_CONFIG_VALUE).strip() +DISCORD_BOT_TOKEN = environ.get("DISCORD_BOT_TOKEN", DEFAULT_CONFIG_VALUE).strip() +TURNSTILE_SITEKEY = environ.get("TURNSTILE_SITEKEY", DEFAULT_CONFIG_VALUE).strip() +TURNSTILE_SECRET = environ.get("TURNSTILE_SECRET", DEFAULT_CONFIG_VALUE).strip() +YOUTUBE_KEY = environ.get("YOUTUBE_KEY", DEFAULT_CONFIG_VALUE).strip() +PUSHER_ID = environ.get("PUSHER_ID", DEFAULT_CONFIG_VALUE).strip() +PUSHER_KEY = environ.get("PUSHER_KEY", DEFAULT_CONFIG_VALUE).strip() +IMGUR_KEY = environ.get("IMGUR_KEY", DEFAULT_CONFIG_VALUE).strip() +SPAM_SIMILARITY_THRESHOLD = float(environ.get("SPAM_SIMILARITY_THRESHOLD", "0.5").strip()) +SPAM_URL_SIMILARITY_THRESHOLD = float(environ.get("SPAM_URL_SIMILARITY_THRESHOLD", "0.1").strip()) +SPAM_SIMILAR_COUNT_THRESHOLD = int(environ.get("SPAM_SIMILAR_COUNT_THRESHOLD", "10").strip()) +COMMENT_SPAM_SIMILAR_THRESHOLD = float(environ.get("COMMENT_SPAM_SIMILAR_THRESHOLD", "0.5").strip()) +COMMENT_SPAM_COUNT_THRESHOLD = int(environ.get("COMMENT_SPAM_COUNT_THRESHOLD", "10").strip()) +DEFAULT_TIME_FILTER = environ.get("DEFAULT_TIME_FILTER", "all").strip() +GUMROAD_TOKEN = environ.get("GUMROAD_TOKEN", DEFAULT_CONFIG_VALUE).strip() +GUMROAD_LINK = environ.get("GUMROAD_LINK", DEFAULT_CONFIG_VALUE).strip() +GUMROAD_ID = environ.get("GUMROAD_ID", DEFAULT_CONFIG_VALUE).strip() +DISABLE_DOWNVOTES = bool(int(environ.get("DISABLE_DOWNVOTES", "0").strip())) +DUES = int(environ.get("DUES", "0").strip()) +DEFAULT_THEME = environ.get("DEFAULT_THEME", "midnight").strip() +DEFAULT_COLOR = environ.get("DEFAULT_COLOR", "805ad5").strip() +CARD_VIEW = bool(int(environ.get("CARD_VIEW", "0").strip())) +EMAIL = environ.get("EMAIL", "blahblahblah@gmail.com").strip() +MAILGUN_KEY = environ.get("MAILGUN_KEY", DEFAULT_CONFIG_VALUE).strip() +DESCRIPTION = environ.get("DESCRIPTION", "rdrama.net caters to drama in all forms such as: Real life, videos, photos, gossip, rumors, news sites, Reddit, and Beyond™. There isn't drama we won't touch, and we want it all!").strip() +CF_KEY = environ.get("CF_KEY", DEFAULT_CONFIG_VALUE).strip() +CF_ZONE = environ.get("CF_ZONE", DEFAULT_CONFIG_VALUE).strip() +TELEGRAM_LINK = environ.get("TELEGRAM_LINK", DEFAULT_CONFIG_VALUE).strip() GLOBAL = environ.get("GLOBAL", "").strip() blackjack = environ.get("BLACKJACK", "").strip() FP = environ.get("FP", "").strip() @@ -46,12 +43,14 @@ KOFI_TOKEN = environ.get("KOFI_TOKEN", "").strip() KOFI_LINK = environ.get("KOFI_LINK", "").strip() PUSHER_ID_CSP = "" -if PUSHER_ID != "blahblahblah": +if PUSHER_ID != DEFAULT_CONFIG_VALUE: PUSHER_ID_CSP = f" {PUSHER_ID}.pushnotifications.pusher.com" CONTENT_SECURITY_POLICY_DEFAULT = "script-src 'self' 'unsafe-inline' challenges.cloudflare.com; connect-src 'self'; object-src 'none';" CONTENT_SECURITY_POLICY_HOME = f"script-src 'self' 'unsafe-inline' 'unsafe-eval'; connect-src 'self' tls-use1.fpapi.io api.fpjs.io{PUSHER_ID_CSP}; object-src 'none';" -CLOUDFLARE_COOKIE_VALUE = "yes." +CLOUDFLARE_COOKIE_VALUE = "yes." # remember to change this in CloudFlare too + +SETTINGS_FILENAME = '/site_settings.json' DEFAULT_RATELIMIT = "3/second;30/minute;200/hour;1000/day" DEFAULT_RATELIMIT_SLOWER = "1/second;30/minute;200/hour;1000/day" @@ -67,6 +66,8 @@ if SITE_NAME == 'PCM': CC = "SPLASH MOUNTAIN" else: CC = "COUNTRY CLUB" CC_TITLE = CC.title() +CASINO_RELEASE_DAY = 1662825600 + if SITE_NAME == 'rDrama': patron = 'Paypig' else: patron = 'Patron' @@ -298,6 +299,8 @@ FEATURES = { 'MARKUP_COMMANDS': True, 'REPOST_DETECTION': True, 'PATRON_ICONS': False, + 'ASSET_SUBMISSIONS': False, + 'STREAMERS': False, } WERKZEUG_ERROR_DESCRIPTIONS = { @@ -468,6 +471,7 @@ if SITE == 'rdrama.net': FEATURES['PRONOUNS'] = True FEATURES['HOUSES'] = True FEATURES['USERS_PERMANENT_WORD_FILTERS'] = True + FEATURES['ASSET_SUBMISSIONS'] = True PERMS['ADMIN_ADD'] = 4 SIDEBAR_THREAD = 37696 @@ -520,6 +524,7 @@ if SITE == 'rdrama.net': elif SITE == 'pcmemes.net': PIN_LIMIT = 10 FEATURES['REPOST_DETECTION'] = False + FEATURES['STREAMERS'] = True ERROR_MSGS[500] = "Hiiiii it's nigger! I think this error means that there's a nigger error. And I think that means something took too long to load so it decided to be a nigger. If you keep seeing this on the same page but not other pages, then something its probably a niggerfaggot. It may not be called a nigger, but that sounds right to me. Anyway, ping me and I'll whine to someone smarter to fix it. Don't bother them. Thanks ily <3" ERROR_MARSEYS[500] = "wholesome" POST_RATE_LIMIT = '1/second;4/minute;20/hour;100/day' @@ -609,9 +614,11 @@ elif SITE == 'watchpeopledie.tv': } else: # localhost or testing environment implied + FEATURES['ASSET_SUBMISSIONS'] = True FEATURES['PRONOUNS'] = True FEATURES['HOUSES'] = True FEATURES['USERS_PERMANENT_WORD_FILTERS'] = True + FEATURES['STREAMERS'] = True HOUSES = ("None","Furry","Femboy","Vampire","Racist") if FEATURES['HOUSES'] else ("None") @@ -1660,3 +1667,7 @@ if SITE_NAME == 'rDrama': IMAGE_FORMATS = ['png','gif','jpg','jpeg','webp'] VIDEO_FORMATS = ['mp4','webm','mov','avi','mkv','flv','m4v','3gp'] AUDIO_FORMATS = ['mp3','wav','ogg','aac','m4a','flac'] + +if SECRET_KEY == DEFAULT_CONFIG_VALUE: + from warnings import warn + warn("Secret key is the default value! Please change it to a secure random number. Thanks <3", RuntimeWarning) diff --git a/files/helpers/const_stateful.py b/files/helpers/const_stateful.py new file mode 100644 index 000000000..71b11f152 --- /dev/null +++ b/files/helpers/const_stateful.py @@ -0,0 +1,37 @@ +from os import path + +from sqlalchemy.orm import scoped_session + +from files.classes import Marsey +from files.helpers.const import SITE_NAME + +marseys_const = [] +marseys_const2 = [] +marsey_mappings = {} +SNAPPY_MARSEYS = [] +SNAPPY_QUOTES = [] + +def const_initialize(db:scoped_session): + _initialize_marseys(db) + _initialize_snappy_marseys_and_quotes() + +def _initialize_marseys(db:scoped_session): + global marseys_const, marseys_const2, marsey_mappings + marseys_const = [x[0] for x in db.query(Marsey.name).filter(Marsey.submitter_id==None, Marsey.name!='chudsey').all()] + marseys_const2 = marseys_const + ['chudsey','a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z','0','1','2','3','4','5','6','7','8','9','exclamationpoint','period','questionmark'] + marseys = db.query(Marsey).filter(Marsey.submitter_id==None).all() + for marsey in marseys: + for tag in marsey.tags.split(): + if tag in marsey_mappings: + marsey_mappings[tag].append(marsey.name) + else: + marsey_mappings[tag] = [marsey.name] + +def _initialize_snappy_marseys_and_quotes(): + global SNAPPY_MARSEYS, SNAPPY_QUOTES + if SITE_NAME != 'PCM': + SNAPPY_MARSEYS = [f':#{x}:' for x in marseys_const2] + + if path.isfile(f'snappy_{SITE_NAME}.txt'): + with open(f'snappy_{SITE_NAME}.txt', "r", encoding="utf-8") as f: + SNAPPY_QUOTES = f.read().split("\n{[para]}\n") diff --git a/files/helpers/cron.py b/files/helpers/cron.py index 9a3ca071f..864372aef 100644 --- a/files/helpers/cron.py +++ b/files/helpers/cron.py @@ -1,24 +1,25 @@ -from files.cli import g, app, db_session -import click -from files.helpers.const import * -from files.helpers.alerts import send_repeatable_notification -from files.helpers.roulette import spin_roulette_wheel -from files.helpers.get import * -from files.helpers.useractions import * -from files.classes import * -from files.__main__ import cache - -import files.helpers.lottery as lottery -import files.helpers.offsitementions as offsitementions -import files.helpers.stats as stats -import files.helpers.awards as awards -import files.routes.static as route_static - -from sys import stdout import datetime import time +from sys import stdout + +import click import requests +import files.helpers.awards as awards +import files.helpers.offsitementions as offsitementions +import files.helpers.stats as stats +import files.routes.static as route_static +import files.routes.streamers as route_streamers +from files.__main__ import cache +from files.classes import * +from files.helpers.alerts import send_repeatable_notification +from files.helpers.const import * +from files.helpers.get import * +from files.helpers.lottery import check_if_end_lottery_task +from files.helpers.roulette import spin_roulette_wheel +from files.helpers.useractions import * +from files.cli import app, db_session, g + @app.cli.command('cron', help='Run scheduled tasks.') @click.option('--every-5m', is_flag=True, help='Call every 5 minutes.') @click.option('--every-1h', is_flag=True, help='Call every 1 hour.') @@ -29,30 +30,30 @@ def cron(every_5m, every_1h, every_1d, every_1mo): if every_5m: if FEATURES['GAMBLING']: - lottery.check_if_end_lottery_task() + check_if_end_lottery_task() spin_roulette_wheel() offsitementions.offsite_mentions_task(cache) - if SITE == 'pcmemes.net': - route_static.live_cached() + if FEATURES['STREAMERS']: + route_streamers.live_cached() if every_1h: awards.award_timers_bots_task() if every_1d: stats.generate_charts_task(SITE) - sub_inactive_purge_task() + _sub_inactive_purge_task() cache.delete_memoized(route_static.stats_cached) route_static.stats_cached() if every_1mo: - if KOFI_LINK: give_monthly_marseybux_task_kofi() - else: give_monthly_marseybux_task() + if KOFI_LINK: _give_monthly_marseybux_task_kofi() + else: _give_monthly_marseybux_task() g.db.commit() g.db.close() stdout.flush() -def sub_inactive_purge_task(): +def _sub_inactive_purge_task(): if not HOLE_INACTIVITY_DELETION: return False @@ -108,7 +109,7 @@ def sub_inactive_purge_task(): return True -def give_monthly_marseybux_task(): +def _give_monthly_marseybux_task(): month = datetime.datetime.now() + datetime.timedelta(days=5) month = month.strftime('%B') @@ -157,7 +158,7 @@ def give_monthly_marseybux_task(): return True -def give_monthly_marseybux_task_kofi(): +def _give_monthly_marseybux_task_kofi(): month = datetime.datetime.now() + datetime.timedelta(days=5) month = month.strftime('%B') diff --git a/files/helpers/get.py b/files/helpers/get.py index e8d7938ea..33ec03736 100644 --- a/files/helpers/get.py +++ b/files/helpers/get.py @@ -1,7 +1,11 @@ from typing import Callable, Iterable, List, Optional, Union + +from flask import * +from sqlalchemy import and_, any_, or_ from sqlalchemy.orm import joinedload, selectinload -from files.classes import * -from flask import g + +from files.classes import Comment, CommentVote, Hat, Sub, Submission, User, UserBlock, Vote +from files.helpers.const import AUTOJANNY_ID def sanitize_username(username:str) -> str: if not username: return username diff --git a/files/helpers/lazy.py b/files/helpers/lazy.py index 94739c095..682d3258a 100644 --- a/files/helpers/lazy.py +++ b/files/helpers/lazy.py @@ -1,21 +1,14 @@ -# Prevents certain properties from having to be recomputed each time they are referenced - - def lazy(f): - + ''' + Prevents certain properties from having to be recomputed each time they are referenced + ''' def wrapper(*args, **kwargs): - o = args[0] - if "_lazy" not in o.__dict__: o.__dict__["_lazy"] = {} - name = f.__name__ + str(args) + str(kwargs), - if name not in o.__dict__["_lazy"]: o.__dict__["_lazy"][name] = f(*args, **kwargs) - return o.__dict__["_lazy"][name] - wrapper.__name__ = f.__name__ return wrapper diff --git a/files/helpers/lottery.py b/files/helpers/lottery.py index 19b0b5e19..5cf680b23 100644 --- a/files/helpers/lottery.py +++ b/files/helpers/lottery.py @@ -1,10 +1,13 @@ import time from random import choice -from sqlalchemy import * -from files.helpers.alerts import * -from files.helpers.wrappers import * -from files.helpers.useractions import * + from flask import g +from sqlalchemy import * +from files.classes.lottery import Lottery + +from files.helpers.alerts import * +from files.helpers.useractions import * + from .const import * LOTTERY_WINNER_BADGE_ID = 137 diff --git a/files/helpers/mail.py b/files/helpers/mail.py new file mode 100644 index 000000000..ceaa11144 --- /dev/null +++ b/files/helpers/mail.py @@ -0,0 +1,37 @@ +import requests +import time + +from files.helpers.security import * +from files.helpers.const import EMAIL, MAILGUN_KEY + +from urllib.parse import quote + +from flask import render_template + +def send_mail(to_address, subject, html): + if MAILGUN_KEY == 'blahblahblah': return + url = f"https://api.mailgun.net/v3/{SITE}/messages" + auth = ("api", MAILGUN_KEY) + data = {"from": EMAIL, + "to": [to_address], + "subject": subject, + "html": html, + } + requests.post(url, auth=auth, data=data) + + +def send_verification_email(user, email=None): + if not email: + email = user.email + + url = f"https://{SITE}/activate" + now = int(time.time()) + token = generate_hash(f"{email}+{user.id}+{now}") + params = f"?email={quote(email)}&id={user.id}&time={now}&token={token}" + link = url + params + send_mail(to_address=email, + html=render_template("email/email_verify.html", + action_url=link, + v=user), + subject=f"Validate your {SITE_NAME} account email." + ) diff --git a/files/helpers/marsify.py b/files/helpers/marsify.py index 2cefb765a..e83046548 100644 --- a/files/helpers/marsify.py +++ b/files/helpers/marsify.py @@ -1,6 +1,7 @@ -from .sanitize import marsey_mappings from random import choice +from .const_stateful import marsey_mappings + def marsify(text): new_text = '' for x in text.split(' '): diff --git a/files/helpers/media.py b/files/helpers/media.py index b009bf10e..edc4e30e1 100644 --- a/files/helpers/media.py +++ b/files/helpers/media.py @@ -1,50 +1,52 @@ -from PIL import Image, ImageOps -from PIL.ImageSequence import Iterator -from webptools import gifwebp -import subprocess import os -from flask import abort, g -import requests +import subprocess import time -from .const import * +from shutil import copyfile +from typing import Optional + import gevent import imagehash -from shutil import copyfile +from flask import abort, g, has_request_context from werkzeug.utils import secure_filename +from PIL import Image +from PIL import UnidentifiedImageError +from PIL.ImageSequence import Iterator +from sqlalchemy.orm import scoped_session + from files.classes.media import * from files.helpers.cloudflare import purge_files_in_cache -from files.__main__ import db_session -def process_files(): +from .const import * + +def process_files(files, v): body = '' - if request.files.get("file") and request.headers.get("cf-ipcountry") != "T1": - files = request.files.getlist('file')[:4] - for file in files: - if file.content_type.startswith('image/'): - name = f'/images/{time.time()}'.replace('.','') + '.webp' - file.save(name) - url = process_image(name, patron=g.v.patron) - body += f"\n\n![]({url})" - elif file.content_type.startswith('video/'): - body += f"\n\n{SITE_FULL}{process_video(file)}" - elif file.content_type.startswith('audio/'): - body += f"\n\n{SITE_FULL}{process_audio(file)}" - else: - abort(415) + if g.is_tor or not files.get("file"): return body + files = files.getlist('file')[:4] + for file in files: + if file.content_type.startswith('image/'): + name = f'/images/{time.time()}'.replace('.','') + '.webp' + file.save(name) + url = process_image(name, v) + body += f"\n\n![]({url})" + elif file.content_type.startswith('video/'): + body += f"\n\n{SITE_FULL}{process_video(file, v)}" + elif file.content_type.startswith('audio/'): + body += f"\n\n{SITE_FULL}{process_audio(file, v)}" + else: + abort(415) return body -def process_audio(file): +def process_audio(file, v): name = f'/audio/{time.time()}'.replace('.','') name_original = secure_filename(file.filename) extension = name_original.split('.')[-1].lower() name = name + '.' + extension - file.save(name) size = os.stat(name).st_size - if size > MAX_IMAGE_AUDIO_SIZE_MB_PATRON * 1024 * 1024 or not g.v.patron and size > MAX_IMAGE_AUDIO_SIZE_MB * 1024 * 1024: + if size > MAX_IMAGE_AUDIO_SIZE_MB_PATRON * 1024 * 1024 or not v.patron and size > MAX_IMAGE_AUDIO_SIZE_MB * 1024 * 1024: os.remove(name) abort(413, f"Max image/audio size is {MAX_IMAGE_AUDIO_SIZE_MB} MB ({MAX_IMAGE_AUDIO_SIZE_MB_PATRON} MB for {patron.lower()}s)") @@ -54,7 +56,7 @@ def process_audio(file): media = Media( kind='audio', filename=name, - user_id=g.v.id, + user_id=v.id, size=size ) g.db.add(media) @@ -62,13 +64,12 @@ def process_audio(file): return name -def webm_to_mp4(old, new, vid): +def webm_to_mp4(old, new, vid, db): tmp = new.replace('.mp4', '-t.mp4') subprocess.run(["ffmpeg", "-y", "-loglevel", "warning", "-nostats", "-threads:v", "1", "-i", old, "-map_metadata", "-1", tmp], check=True, stderr=subprocess.STDOUT) os.replace(tmp, new) os.remove(old) purge_files_in_cache(f"{SITE_FULL}{new}") - db = db_session() media = db.query(Media).filter_by(filename=new, kind='video').one_or_none() if media: db.delete(media) @@ -84,14 +85,14 @@ def webm_to_mp4(old, new, vid): db.close() -def process_video(file): +def process_video(file, v): old = f'/videos/{time.time()}'.replace('.','') file.save(old) size = os.stat(old).st_size if (SITE_NAME != 'WPD' and (size > MAX_VIDEO_SIZE_MB_PATRON * 1024 * 1024 - or not g.v.patron and size > MAX_VIDEO_SIZE_MB * 1024 * 1024)): + or not v.patron and size > MAX_VIDEO_SIZE_MB * 1024 * 1024)): os.remove(old) abort(413, f"Max video size is {MAX_VIDEO_SIZE_MB} MB ({MAX_VIDEO_SIZE_MB_PATRON} MB for paypigs)") @@ -102,7 +103,8 @@ def process_video(file): if extension == 'webm': new = new.replace('.webm', '.mp4') copyfile(old, new) - gevent.spawn(webm_to_mp4, old, new, g.v.id) + db = scoped_session() + gevent.spawn(webm_to_mp4, old, new, v.id, db) else: subprocess.run(["ffmpeg", "-y", "-loglevel", "warning", "-nostats", "-i", old, "-map_metadata", "-1", "-c:v", "copy", "-c:a", "copy", new], check=True) os.remove(old) @@ -113,7 +115,7 @@ def process_video(file): media = Media( kind='video', filename=new, - user_id=g.v.id, + user_id=v.id, size=os.stat(new).st_size ) g.db.add(media) @@ -122,31 +124,51 @@ def process_video(file): -def process_image(filename=None, resize=0, trim=False, uploader=None, patron=False, db=None): +def process_image(filename:str, v, resize=0, trim=False, uploader_id:Optional[int]=None, db=None): + # thumbnails are processed in a thread and not in the request context + # if an image is too large or webp conversion fails, it'll crash + # to avoid this, we'll simply return None instead + has_request = has_request_context() size = os.stat(filename).st_size + patron = bool(v.patron) if size > MAX_IMAGE_AUDIO_SIZE_MB_PATRON * 1024 * 1024 or not patron and size > MAX_IMAGE_AUDIO_SIZE_MB * 1024 * 1024: os.remove(filename) - abort(413, f"Max image/audio size is {MAX_IMAGE_AUDIO_SIZE_MB} MB ({MAX_IMAGE_AUDIO_SIZE_MB_PATRON} MB for paypigs)") + if has_request: + abort(413, f"Max image/audio size is {MAX_IMAGE_AUDIO_SIZE_MB} MB ({MAX_IMAGE_AUDIO_SIZE_MB_PATRON} MB for paypigs)") + return None - with Image.open(filename) as i: - params = ["convert", "-coalesce", filename, "-quality", "88", "-define", "webp:method=5", "-strip", "-auto-orient"] - if trim and len(list(Iterator(i))) == 1: - params.append("-trim") - if resize and i.width > resize: - params.extend(["-resize", f"{resize}>"]) + try: + with Image.open(filename) as i: + params = ["convert", "-coalesce", filename, "-quality", "88", "-define", "webp:method=5", "-strip", "-auto-orient"] + if trim and len(list(Iterator(i))) == 1: + params.append("-trim") + if resize and i.width > resize: + params.extend(["-resize", f"{resize}>"]) + except UnidentifiedImageError as e: + print(f"Couldn't identify an image for {filename}; deleting... (user {v.id if v else '-no user-'})") + try: + os.remove(filename) + except: pass + if has_request: + abort(415) + return None params.append(filename) try: subprocess.run(params, timeout=MAX_IMAGE_CONVERSION_TIMEOUT) except subprocess.TimeoutExpired: - abort(413, ("An uploaded image took too long to convert to WEBP. " - "Consider uploading elsewhere.")) + if has_request: + abort(413, ("An uploaded image took too long to convert to WEBP. " + "Consider uploading elsewhere.")) + return None if resize: if os.stat(filename).st_size > MAX_IMAGE_SIZE_BANNER_RESIZED_KB * 1024: os.remove(filename) - abort(413, f"Max size for site assets is {MAX_IMAGE_SIZE_BANNER_RESIZED_KB} KB") + if has_request: + abort(413, f"Max size for site assets is {MAX_IMAGE_SIZE_BANNER_RESIZED_KB} KB") + return None if filename.startswith('files/assets/images/'): path = filename.rsplit('/', 1)[0] @@ -173,7 +195,9 @@ def process_image(filename=None, resize=0, trim=False, uploader=None, patron=Fal if i_hash in hashes.keys(): os.remove(filename) - abort(409, "Image already exists!") + if has_request: + abort(409, "Image already exists!") + return None db = db or g.db @@ -183,7 +207,7 @@ def process_image(filename=None, resize=0, trim=False, uploader=None, patron=Fal media = Media( kind='image', filename=filename, - user_id=uploader or g.v.id, + user_id=uploader_id or v.id, size=os.stat(filename).st_size ) db.add(media) diff --git a/files/helpers/offsitementions.py b/files/helpers/offsitementions.py index 8ebe79324..3a9bdf574 100644 --- a/files/helpers/offsitementions.py +++ b/files/helpers/offsitementions.py @@ -1,15 +1,17 @@ import time from typing import Iterable +import itertools + +import requests from flask_caching import Cache from flask import g -import itertools -import requests from sqlalchemy import or_ + import files.helpers.const as const -from files.classes.user import User -from files.classes.comment import Comment from files.classes.badges import Badge +from files.classes.comment import Comment from files.classes.notifications import Notification +from files.classes.user import User from files.helpers.sanitize import sanitize # Note: while https://api.pushshift.io/meta provides the key diff --git a/files/helpers/owoify.py b/files/helpers/owoify.py index 530f5ee19..a721bd902 100644 --- a/files/helpers/owoify.py +++ b/files/helpers/owoify.py @@ -1,8 +1,9 @@ +import re + +from owoify.structures.word import Word from owoify.utility.interleave_arrays import interleave_arrays from owoify.utility.presets import * -from owoify.structures.word import Word -import re import files.helpers.regex as help_re import files.helpers.sanitize as sanitize diff --git a/files/helpers/regex.py b/files/helpers/regex.py index 9d5bb4f77..32dfb5778 100644 --- a/files/helpers/regex.py +++ b/files/helpers/regex.py @@ -1,8 +1,9 @@ import random import re -from typing import List, Literal, Optional, Union -from .const import * from random import choice, choices +from typing import List, Optional, Union + +from .const import * valid_username_chars = 'a-zA-Z0-9_\-' valid_username_regex = re.compile("^[a-zA-Z0-9_\-]{3,25}$", flags=re.A) diff --git a/files/helpers/roulette.py b/files/helpers/roulette.py index 2c56c435a..2db069f41 100644 --- a/files/helpers/roulette.py +++ b/files/helpers/roulette.py @@ -1,11 +1,13 @@ import json -from random import randint from enum import Enum -from files.helpers.alerts import * -from files.classes.casino_game import Casino_Game -from files.helpers.get import get_account +from random import randint +import time + from flask import g +from files.classes.casino_game import Casino_Game +from files.helpers.alerts import * +from files.helpers.get import get_account class RouletteAction(str, Enum): STRAIGHT_UP_BET = "STRAIGHT_UP_BET" diff --git a/files/helpers/sanitize.py b/files/helpers/sanitize.py index b467c3397..524dd6397 100644 --- a/files/helpers/sanitize.py +++ b/files/helpers/sanitize.py @@ -1,31 +1,22 @@ import functools +import random +import re +import signal +from functools import partial +from os import path +from urllib.parse import parse_qs, urlparse + import bleach -from bs4 import BeautifulSoup from bleach.css_sanitizer import CSSSanitizer from bleach.linkifier import LinkifyFilter -from functools import partial -from .get import * -from os import path -import re +from bs4 import BeautifulSoup from mistletoe import markdown -from random import random, choice -import signal -from files.__main__ import db_session -from files.classes.marsey import Marsey +from files.classes.domains import BannedDomain -db = db_session() -marseys_const = [x[0] for x in db.query(Marsey.name).filter(Marsey.submitter_id==None, Marsey.name!='chudsey').all()] -marseys_const2 = marseys_const + ['chudsey','a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z','0','1','2','3','4','5','6','7','8','9','exclamationpoint','period','questionmark'] - -marseys = db.query(Marsey).filter(Marsey.submitter_id==None).all() -marsey_mappings = {} -for marsey in marseys: - for tag in marsey.tags.split(): - if tag in marsey_mappings: - marsey_mappings[tag].append(marsey.name) - else: - marsey_mappings[tag] = [marsey.name] -db.close() +from files.helpers.const import * +from files.helpers.const_stateful import * +from files.helpers.regex import * +from .get import * TLDS = ( # Original gTLDs and ccTLDs 'ac','ad','ae','aero','af','ag','ai','al','am','an','ao','aq','ar','arpa','as','asia','at', @@ -173,12 +164,12 @@ def render_emoji(html, regexp, golden, marseys_used, b=False): attrs = '' if b: attrs += ' b' if golden and len(emojis) <= 20 and ('marsey' in emoji or emoji in marseys_const2): - if random() < 0.0025: attrs += ' g' - elif random() < 0.00125: attrs += ' glow' + if random.random() < 0.0025: attrs += ' g' + elif random.random() < 0.00125: attrs += ' glow' old = emoji emoji = emoji.replace('!','').replace('#','') - if emoji == 'marseyrandom': emoji = choice(marseys_const2) + if emoji == 'marseyrandom': emoji = random.choice(marseys_const2) emoji_partial_pat = ':{0}:' emoji_partial = ':{0}:' @@ -254,7 +245,7 @@ def sanitize(sanitized, golden=True, limit_pings=0, showmore=True, count_marseys if torture: sanitized = torture_ap(sanitized, g.v.username) - emoji = choice(['trumpjaktalking', 'reposthorse']) + emoji = random.choice(['trumpjaktalking', 'reposthorse']) sanitized += f'\n:#{emoji}:' sanitized = normalize_url(sanitized) diff --git a/files/helpers/security.py b/files/helpers/security.py index 45470217c..eec4fc842 100644 --- a/files/helpers/security.py +++ b/files/helpers/security.py @@ -1,11 +1,9 @@ from werkzeug.security import * + from .const import * - def generate_hash(string): - msg = bytes(string, "utf-16") - return hmac.new(key=bytes(SECRET_KEY, "utf-16"), msg=msg, digestmod='md5' @@ -13,11 +11,8 @@ def generate_hash(string): def validate_hash(string, hashstr): - return hmac.compare_digest(hashstr, generate_hash(string)) - def hash_password(password): - return generate_password_hash( password, method='pbkdf2:sha512', salt_length=8) diff --git a/files/helpers/settings.py b/files/helpers/settings.py new file mode 100644 index 000000000..feef4e841 --- /dev/null +++ b/files/helpers/settings.py @@ -0,0 +1,54 @@ +import json +import os + +import gevent +import gevent_inotifyx as inotify + +from files.helpers.const import SETTINGS_FILENAME + +_SETTINGS = { + "Bots": True, + "Fart mode": False, + "Read-only mode": False, + "Signups": True, + "login_required": False, +} + +def get_setting(setting:str): + if not setting or not isinstance(setting, str): raise TypeError() + return _SETTINGS[setting] + +def get_settings() -> dict[str, bool]: + return _SETTINGS + +def toggle_setting(setting:str): + val = not _SETTINGS[setting] + _SETTINGS[setting] = val + _save_settings() + return val + +def reload_settings(): + global _SETTINGS + if not os.path.isfile(SETTINGS_FILENAME): + _save_settings() + with open(SETTINGS_FILENAME, 'r', encoding='utf_8') as f: + _SETTINGS = json.load(f) + +def _save_settings(): + with open(SETTINGS_FILENAME, "w", encoding='utf_8') as f: + json.dump(_SETTINGS, f) + +def start_watching_settings(): + gevent.spawn(_settings_watcher, SETTINGS_FILENAME) + +def _settings_watcher(filename): + fd = inotify.init() + try: + inotify.add_watch(fd, filename, inotify.IN_CLOSE_WRITE) + while True: + for event in inotify.get_events(fd, 0): + reload_settings() + break + gevent.sleep(0.5) + finally: + os.close(fd) diff --git a/files/helpers/slots.py b/files/helpers/slots.py index 02c58ff5f..d78df5c4e 100644 --- a/files/helpers/slots.py +++ b/files/helpers/slots.py @@ -1,12 +1,15 @@ import json -from json.encoder import INFINITY import random -from .const import * +from json.encoder import INFINITY + +from flask import abort, g + from files.classes.casino_game import Casino_Game -from files.helpers.casino import distribute_wager_badges -from flask import g, abort from files.classes.comment import Comment from files.classes.user import User +from files.helpers.casino import distribute_wager_badges + +from .const import * minimum_bet = 5 maximum_bet = INFINITY diff --git a/files/helpers/sorting_and_time.py b/files/helpers/sorting_and_time.py index 2cefff115..86b09cdd5 100644 --- a/files/helpers/sorting_and_time.py +++ b/files/helpers/sorting_and_time.py @@ -1,8 +1,10 @@ import time from typing import Optional -from files.helpers.const import * + from sqlalchemy.sql import func +from files.helpers.const import * + def apply_time_filter(t, objects, cls): now = int(time.time()) if t == 'hour': diff --git a/files/helpers/treasure.py b/files/helpers/treasure.py index 327aac90e..b8aee95d6 100644 --- a/files/helpers/treasure.py +++ b/files/helpers/treasure.py @@ -1,5 +1,6 @@ -from random import randint from math import floor +from random import randint + from files.helpers.const import * from files.helpers.lottery import * diff --git a/files/helpers/twentyone.py b/files/helpers/twentyone.py index fee54c70f..980b82bda 100644 --- a/files/helpers/twentyone.py +++ b/files/helpers/twentyone.py @@ -1,11 +1,12 @@ import json -from math import floor import random from enum import Enum -from files.classes.casino_game import Casino_Game -from files.helpers.casino import distribute_wager_badges +from math import floor + from flask import g +from files.classes.casino_game import Casino_Game +from files.helpers.casino import distribute_wager_badges class BlackjackStatus(str, Enum): PLAYING = "PLAYING" diff --git a/files/helpers/useractions.py b/files/helpers/useractions.py index 554e75398..3c3afdffe 100644 --- a/files/helpers/useractions.py +++ b/files/helpers/useractions.py @@ -1,4 +1,5 @@ from flask import g + from files.classes.badges import Badge from files.helpers.alerts import send_repeatable_notification diff --git a/files/routes/__init__.py b/files/routes/__init__.py index deeecc4f5..98a6ee76d 100644 --- a/files/routes/__init__.py +++ b/files/routes/__init__.py @@ -1,13 +1,29 @@ -# import classes then... -from files.classes.sub import Sub +# import constants then... +from files.helpers.const import FEATURES -# import routes +# import flask then... +from flask import g, request, render_template, make_response, redirect, jsonify, send_from_directory, send_file + +# import our app then... +from files.__main__ import app + +# import route helpers then... +from files.routes.routehelpers import * + +# import wrappers then... +from files.routes.wrappers import * + +# import jinja2 then... (lmao this was in feeds.py before wtf) +from files.routes.jinja2 import * + +# import routes :) from .admin import * from .comments import * from .errors import * from .reporting import * from .front import * from .login import * +from .mail import * from .oauth import * from .posts import * from .search import * @@ -24,4 +40,7 @@ from .casino import * from .polls import * from .notifications import * from .hats import * -from .asset_submissions import * +if FEATURES['ASSET_SUBMISSIONS']: + from .asset_submissions import * +if FEATURES['STREAMERS']: + from .streamers import * diff --git a/files/routes/admin.py b/files/routes/admin.py index 8c75ae316..7e25981f6 100644 --- a/files/routes/admin.py +++ b/files/routes/admin.py @@ -1,26 +1,25 @@ import time -import re -from os import remove -from PIL import Image as IMAGE +from urllib.parse import quote, urlencode -from files.helpers.wrappers import * +from flask import * + +from files.__main__ import app, cache, limiter +from files.classes import * +from files.helpers.actions import * from files.helpers.alerts import * -from files.helpers.sanitize import * -from files.helpers.security import * +from files.helpers.cloudflare import * +from files.helpers.const import * from files.helpers.get import * from files.helpers.media import * -from files.helpers.const import * -from files.helpers.actions import * +from files.helpers.sanitize import * +from files.helpers.security import * +from files.helpers.settings import toggle_setting from files.helpers.useractions import * -import files.helpers.cloudflare as cloudflare -from files.classes import * -from flask import * -from files.__main__ import app, cache, limiter +from files.routes.routehelpers import check_for_alts +from files.routes.wrappers import * + from .front import frontlist -from .login import check_for_alts -import datetime -import requests -from urllib.parse import quote, urlencode + @app.post('/kippy') @admin_level_required(PERMS['PRINT_MARSEYBUX_FOR_KIPPY_ON_PCMEMES']) @@ -431,7 +430,7 @@ def admin_home(v): under_attack = False if v.admin_level >= PERMS['SITE_SETTINGS_UNDER_ATTACK']: - under_attack = (cloudflare.get_security_level() or 'high') == 'under_attack' + under_attack = (get_security_level() or 'high') == 'under_attack' gitref = admin_git_head() @@ -458,27 +457,20 @@ def admin_git_head(): @app.post("/admin/site_settings/") @admin_level_required(PERMS['SITE_SETTINGS']) def change_settings(v, setting): - site_settings = app.config['SETTINGS'] - site_settings[setting] = not site_settings[setting] - with open("/site_settings.json", "w", encoding='utf_8') as f: - json.dump(site_settings, f) - - if site_settings[setting]: word = 'enable' + val = toggle_setting(setting) + if val: word = 'enable' else: word = 'disable' - ma = ModAction( kind=f"{word}_{setting}", user_id=v.id, ) g.db.add(ma) - - return {'message': f"{setting} {word}d successfully!"} @app.post("/admin/clear_cloudflare_cache") @admin_level_required(PERMS['SITE_CACHE_PURGE_CDN']) def clear_cloudflare_cache(v): - if not cloudflare.clear_entire_cache(): + if not clear_entire_cache(): abort(400, 'Failed to clear cloudflare cache!') ma = ModAction( kind="clear_cloudflare_cache", @@ -503,13 +495,13 @@ def admin_clear_internal_cache(v): @app.post("/admin/under_attack") @admin_level_required(PERMS['SITE_SETTINGS_UNDER_ATTACK']) def under_attack(v): - response = cloudflare.get_security_level() + response = get_security_level() if not response: abort(400, 'Could not retrieve the current security level') old_under_attack_mode = response == 'under_attack' enable_disable_str = 'disable' if old_under_attack_mode else 'enable' new_security_level = 'high' if old_under_attack_mode else 'under_attack' - if not cloudflare.set_security_level(new_security_level): + if not set_security_level(new_security_level): abort(400, f'Failed to {enable_disable_str} under attack mode') ma = ModAction( kind=f"{enable_disable_str}_under_attack", @@ -760,7 +752,7 @@ def admin_add_alt(v, username): a = Alt( user1=user1.id, user2=user2.id, - manual=True, + is_manual=True, deleted=deleted ) g.db.add(a) @@ -1200,7 +1192,7 @@ def remove_post(post_id, v): v.coins += 1 g.db.add(v) - cloudflare.purge_files_in_cache(f"https://{SITE}/") + purge_files_in_cache(f"https://{SITE}/") return {"message": "Post removed!"} @@ -1208,7 +1200,6 @@ def remove_post(post_id, v): @limiter.limit(DEFAULT_RATELIMIT_SLOWER) @admin_level_required(PERMS['POST_COMMENT_MODERATION']) def approve_post(post_id, v): - post = get_post(post_id) if post.author.id == v.id and post.author.agendaposter and AGENDAPOSTER_PHRASE not in post.body.lower() and post.sub != 'chudrama': diff --git a/files/routes/allroutes.py b/files/routes/allroutes.py new file mode 100644 index 000000000..9e772b905 --- /dev/null +++ b/files/routes/allroutes.py @@ -0,0 +1,57 @@ +import secrets +from files.helpers.const import * +from files.helpers.settings import get_setting +from files.helpers.cloudflare import CLOUDFLARE_AVAILABLE +from files.routes.wrappers import * +from files.__main__ import app + +@app.before_request +def before_request(): + if SITE == 'marsey.world' and request.path != '/kofi': + abort(404) + + g.agent = request.headers.get("User-Agent") + if not g.agent and request.path != '/kofi': + return 'Please use a "User-Agent" header!', 403 + + ua = g.agent or '' + ua = ua.lower() + + if request.host != SITE: + return {"error": "Unauthorized host provided"}, 403 + + if request.headers.get("CF-Worker"): return {"error": "Cloudflare workers are not allowed to access this website."}, 403 + + if not get_setting('Bots') and request.headers.get("Authorization"): abort(403) + + g.db = db_session() + g.webview = '; wv) ' in ua + g.inferior_browser = 'iphone' in ua or 'ipad' in ua or 'ipod' in ua or 'mac os' in ua or ' firefox/' in ua + g.is_tor = request.headers.get("cf-ipcountry") == "T1" + + request.path = request.path.rstrip('/') + if not request.path: request.path = '/' + request.full_path = request.full_path.rstrip('?').rstrip('/') + if not request.full_path: request.full_path = '/' + if not session.get("session_id"): + session.permanent = True + session["session_id"] = secrets.token_hex(49) + +@app.after_request +def after_request(response): + if response.status_code < 400: + if CLOUDFLARE_AVAILABLE and CLOUDFLARE_COOKIE_VALUE and getattr(g, 'desires_auth', False): + logged_in = bool(getattr(g, 'v', None)) + response.set_cookie("lo", CLOUDFLARE_COOKIE_VALUE if logged_in else '', max_age=60*60*24*365 if logged_in else 1) + g.db.commit() + g.db.close() + del g.db + return response + +@app.teardown_appcontext +def teardown_request(error): + if getattr(g, 'db', None): + g.db.rollback() + g.db.close() + del g.db + stdout.flush() diff --git a/files/routes/asset_submissions.py b/files/routes/asset_submissions.py index 8e37a0c62..54fbdadbc 100644 --- a/files/routes/asset_submissions.py +++ b/files/routes/asset_submissions.py @@ -1,469 +1,468 @@ -from shutil import move, copyfile -from os import rename, path -from typing import Union +from os import path, rename +from shutil import copyfile, move -from files.__main__ import app, limiter -from files.helpers.const import * -from files.helpers.useractions import * -from files.helpers.media import * -from files.helpers.get import * -from files.helpers.wrappers import * +from files.classes.marsey import Marsey +from files.classes.hats import Hat, HatDef +from files.classes.mod_logs import ModAction from files.helpers.cloudflare import purge_files_in_cache +from files.helpers.const import * +from files.helpers.get import * +from files.helpers.media import * +from files.helpers.useractions import * from files.routes.static import marsey_list +from files.routes.wrappers import * +from files.__main__ import app, cache, limiter -if SITE not in ('pcmemes.net', 'watchpeopledie.tv'): - ASSET_TYPES = (Marsey, HatDef) - CAN_APPROVE_ASSETS = (AEVANN_ID, CARP_ID, SNAKES_ID) - CAN_UPDATE_ASSETS = (AEVANN_ID, CARP_ID, SNAKES_ID, GEESE_ID, JUSTCOOL_ID) +ASSET_TYPES = (Marsey, HatDef) +CAN_APPROVE_ASSETS = (AEVANN_ID, CARP_ID, SNAKES_ID) +CAN_UPDATE_ASSETS = (AEVANN_ID, CARP_ID, SNAKES_ID, GEESE_ID, JUSTCOOL_ID) - @app.get('/asset_submissions/') - @limiter.exempt - def asset_submissions(path): - resp = make_response(send_from_directory('/asset_submissions', path)) - resp.headers.remove("Cache-Control") - resp.headers.add("Cache-Control", "public, max-age=3153600") - resp.headers.remove("Content-Type") - resp.headers.add("Content-Type", "image/webp") - return resp +@app.get('/asset_submissions/') +@limiter.exempt +def asset_submissions(path): + resp = make_response(send_from_directory('/asset_submissions', path)) + resp.headers.remove("Cache-Control") + resp.headers.add("Cache-Control", "public, max-age=3153600") + resp.headers.remove("Content-Type") + resp.headers.add("Content-Type", "image/webp") + return resp - @app.get("/submit/marseys") - @auth_required - def submit_marseys(v): - if v.admin_level >= PERMS['VIEW_PENDING_SUBMITTED_MARSEYS']: - marseys = g.db.query(Marsey).filter(Marsey.submitter_id != None).all() - else: - marseys = g.db.query(Marsey).filter(Marsey.submitter_id == v.id).all() +@app.get("/submit/marseys") +@auth_required +def submit_marseys(v): + if v.admin_level >= PERMS['VIEW_PENDING_SUBMITTED_MARSEYS']: + marseys = g.db.query(Marsey).filter(Marsey.submitter_id != None).all() + else: + marseys = g.db.query(Marsey).filter(Marsey.submitter_id == v.id).all() - for marsey in marseys: - marsey.author = g.db.query(User.username).filter_by(id=marsey.author_id).one()[0] - marsey.submitter = g.db.query(User.username).filter_by(id=marsey.submitter_id).one()[0] + for marsey in marseys: + marsey.author = g.db.query(User.username).filter_by(id=marsey.author_id).one()[0] + marsey.submitter = g.db.query(User.username).filter_by(id=marsey.submitter_id).one()[0] - return render_template("submit_marseys.html", v=v, marseys=marseys) + return render_template("submit_marseys.html", v=v, marseys=marseys) - @app.post("/submit/marseys") - @auth_required - def submit_marsey(v): - file = request.files["image"] - name = request.values.get('name', '').lower().strip() - tags = request.values.get('tags', '').lower().strip() - username = request.values.get('author', '').lower().strip() +@app.post("/submit/marseys") +@auth_required +def submit_marsey(v): + file = request.files["image"] + name = request.values.get('name', '').lower().strip() + tags = request.values.get('tags', '').lower().strip() + username = request.values.get('author', '').lower().strip() - def error(error): - if v.admin_level >= PERMS['VIEW_PENDING_SUBMITTED_MARSEYS']: marseys = g.db.query(Marsey).filter(Marsey.submitter_id != None).all() - else: marseys = g.db.query(Marsey).filter(Marsey.submitter_id == v.id).all() - for marsey in marseys: - marsey.author = g.db.query(User.username).filter_by(id=marsey.author_id).one()[0] - marsey.submitter = g.db.query(User.username).filter_by(id=marsey.submitter_id).one()[0] - return render_template("submit_marseys.html", v=v, marseys=marseys, error=error, name=name, tags=tags, username=username, file=file), 400 - - if request.headers.get("cf-ipcountry") == "T1": - return error("Image uploads are not allowed through TOR.") - - if not file or not file.content_type.startswith('image/'): - return error("You need to submit an image!") - - if not marsey_regex.fullmatch(name): - return error("Invalid name!") - - existing = g.db.query(Marsey.name).filter_by(name=name).one_or_none() - if existing: - return error("A marsey with this name already exists!") - - if not tags_regex.fullmatch(tags): - return error("Invalid tags!") - - author = get_user(username, v=v, graceful=True, include_shadowbanned=False) - if not author: - return error(f"A user with the name '{username}' was not found!") - - highquality = f'/asset_submissions/marseys/{name}' - file.save(highquality) - - filename = f'/asset_submissions/marseys/{name}.webp' - copyfile(highquality, filename) - process_image(filename, resize=200, trim=True) - - marsey = Marsey(name=name, author_id=author.id, tags=tags, count=0, submitter_id=v.id) - g.db.add(marsey) - - g.db.flush() + def error(error): if v.admin_level >= PERMS['VIEW_PENDING_SUBMITTED_MARSEYS']: marseys = g.db.query(Marsey).filter(Marsey.submitter_id != None).all() else: marseys = g.db.query(Marsey).filter(Marsey.submitter_id == v.id).all() for marsey in marseys: marsey.author = g.db.query(User.username).filter_by(id=marsey.author_id).one()[0] marsey.submitter = g.db.query(User.username).filter_by(id=marsey.submitter_id).one()[0] + return render_template("submit_marseys.html", v=v, marseys=marseys, error=error, name=name, tags=tags, username=username, file=file), 400 - return render_template("submit_marseys.html", v=v, marseys=marseys, msg=f"'{name}' submitted successfully!") + if g.is_tor: + return error("Image uploads are not allowed through TOR.") - def verify_permissions_and_get_asset(cls, asset_type:str, v:User, name:str, make_lower=False): - if cls not in ASSET_TYPES: raise Exception("not a valid asset type") - if AEVANN_ID and v.id not in CAN_APPROVE_ASSETS: - abort(403, f"Only Carp can approve {asset_type}!") - name = name.strip() - if make_lower: name = name.lower() - asset = None - if cls == HatDef: - asset = g.db.query(cls).filter_by(name=name).one_or_none() - else: - asset = g.db.get(cls, name) - if not asset: - abort(404, f"This {asset} '{name}' doesn't exist!") - return asset + if not file or not file.content_type.startswith('image/'): + return error("You need to submit an image!") - @app.post("/admin/approve/marsey/") - @admin_level_required(PERMS['MODERATE_PENDING_SUBMITTED_MARSEYS']) - def approve_marsey(v, name): - marsey = verify_permissions_and_get_asset(Marsey, "marsey", v, name, True) - tags = request.values.get('tags').lower().strip() - if not tags: - abort(400, "You need to include tags!") + if not marsey_regex.fullmatch(name): + return error("Invalid name!") - new_name = request.values.get('name').lower().strip() - if not new_name: - abort(400, "You need to include name!") + existing = g.db.query(Marsey.name).filter_by(name=name).one_or_none() + if existing: + return error("A marsey with this name already exists!") + + if not tags_regex.fullmatch(tags): + return error("Invalid tags!") + + author = get_user(username, v=v, graceful=True, include_shadowbanned=False) + if not author: + return error(f"A user with the name '{username}' was not found!") + + highquality = f'/asset_submissions/marseys/{name}' + file.save(highquality) + + filename = f'/asset_submissions/marseys/{name}.webp' + copyfile(highquality, filename) + process_image(filename, v, resize=200, trim=True) + + marsey = Marsey(name=name, author_id=author.id, tags=tags, count=0, submitter_id=v.id) + g.db.add(marsey) + + g.db.flush() + if v.admin_level >= PERMS['VIEW_PENDING_SUBMITTED_MARSEYS']: marseys = g.db.query(Marsey).filter(Marsey.submitter_id != None).all() + else: marseys = g.db.query(Marsey).filter(Marsey.submitter_id == v.id).all() + for marsey in marseys: + marsey.author = g.db.query(User.username).filter_by(id=marsey.author_id).one()[0] + marsey.submitter = g.db.query(User.username).filter_by(id=marsey.submitter_id).one()[0] + + return render_template("submit_marseys.html", v=v, marseys=marseys, msg=f"'{name}' submitted successfully!") + +def verify_permissions_and_get_asset(cls, asset_type:str, v:User, name:str, make_lower=False): + if cls not in ASSET_TYPES: raise Exception("not a valid asset type") + if AEVANN_ID and v.id not in CAN_APPROVE_ASSETS: + abort(403, f"Only Carp can approve {asset_type}!") + name = name.strip() + if make_lower: name = name.lower() + asset = None + if cls == HatDef: + asset = g.db.query(cls).filter_by(name=name).one_or_none() + else: + asset = g.db.get(cls, name) + if not asset: + abort(404, f"This {asset} '{name}' doesn't exist!") + return asset + +@app.post("/admin/approve/marsey/") +@admin_level_required(PERMS['MODERATE_PENDING_SUBMITTED_MARSEYS']) +def approve_marsey(v, name): + marsey = verify_permissions_and_get_asset(Marsey, "marsey", v, name, True) + tags = request.values.get('tags').lower().strip() + if not tags: + abort(400, "You need to include tags!") + + new_name = request.values.get('name').lower().strip() + if not new_name: + abort(400, "You need to include name!") - if not marsey_regex.fullmatch(new_name): - abort(400, "Invalid name!") - if not tags_regex.fullmatch(tags): - abort(400, "Invalid tags!") + if not marsey_regex.fullmatch(new_name): + abort(400, "Invalid name!") + if not tags_regex.fullmatch(tags): + abort(400, "Invalid tags!") - marsey.name = new_name - marsey.tags = tags - g.db.add(marsey) + marsey.name = new_name + marsey.tags = tags + g.db.add(marsey) - author = get_account(marsey.author_id) - all_by_author = g.db.query(Marsey).filter_by(author_id=author.id).count() + author = get_account(marsey.author_id) + all_by_author = g.db.query(Marsey).filter_by(author_id=author.id).count() - if all_by_author >= 99: - badge_grant(badge_id=143, user=author) - elif all_by_author >= 9: - badge_grant(badge_id=16, user=author) - else: - badge_grant(badge_id=17, user=author) - purge_files_in_cache(f"https://{SITE}/e/{marsey.name}/webp") - cache.delete_memoized(marsey_list) + if all_by_author >= 99: + badge_grant(badge_id=143, user=author) + elif all_by_author >= 9: + badge_grant(badge_id=16, user=author) + else: + badge_grant(badge_id=17, user=author) + purge_files_in_cache(f"https://{SITE}/e/{marsey.name}/webp") + cache.delete_memoized(marsey_list) + move(f"/asset_submissions/marseys/{name}.webp", f"files/assets/images/emojis/{marsey.name}.webp") + + highquality = f"/asset_submissions/marseys/{name}" + with Image.open(highquality) as i: + new_path = f'/asset_submissions/marseys/original/{name}.{i.format.lower()}' + rename(highquality, new_path) + + author.coins += 250 + g.db.add(author) + + if v.id != author.id: + msg = f"@{v.username} (Admin) has approved a marsey you made: :{marsey.name}:\nYou have received 250 coins as a reward!" + send_repeatable_notification(author.id, msg) + + if v.id != marsey.submitter_id and author.id != marsey.submitter_id: + msg = f"@{v.username} (Admin) has approved a marsey you submitted: :{marsey.name}:" + send_repeatable_notification(marsey.submitter_id, msg) + + marsey.submitter_id = None + + return {"message": f"'{marsey.name}' approved!"} + +def remove_asset(cls, type_name:str, v:User, name:str) -> dict[str, str]: + if cls not in ASSET_TYPES: raise Exception("not a valid asset type") + should_make_lower = cls == Marsey + if should_make_lower: name = name.lower() + name = name.strip() + if not name: + abort(400, f"You need to specify a {type_name}!") + asset = None + if cls == HatDef: + asset = g.db.query(cls).filter_by(name=name).one_or_none() + else: + asset = g.db.get(cls, name) + if not asset: + abort(404, f"This {type_name} '{name}' doesn't exist!") + if v.id != asset.submitter_id and v.id not in CAN_APPROVE_ASSETS: + abort(403, f"Only Carp can remove {type_name}s!") + name = asset.name + if v.id != asset.submitter_id: + msg = f"@{v.username} has rejected a {type_name} you submitted: `'{name}'`" + send_repeatable_notification(asset.submitter_id, msg) + g.db.delete(asset) + os.remove(f"/asset_submissions/{type_name}s/{name}.webp") + os.remove(f"/asset_submissions/{type_name}s/{name}") + return {"message": f"'{name}' removed!"} + +@app.post("/remove/marsey/") +@auth_required +def remove_marsey(v, name): + return remove_asset(Marsey, "marsey", v, name) + +@app.get("/submit/hats") +@auth_required +def submit_hats(v): + if v.admin_level >= PERMS['VIEW_PENDING_SUBMITTED_HATS']: hats = g.db.query(HatDef).filter(HatDef.submitter_id != None).all() + else: hats = g.db.query(HatDef).filter(HatDef.submitter_id == v.id).all() + return render_template("submit_hats.html", v=v, hats=hats) - move(f"/asset_submissions/marseys/{name}.webp", f"files/assets/images/emojis/{marsey.name}.webp") +@app.post("/submit/hats") +@auth_required +def submit_hat(v): + name = request.values.get('name', '').strip() + description = request.values.get('description', '').strip() + username = request.values.get('author', '').strip() - highquality = f"/asset_submissions/marseys/{name}" - with Image.open(highquality) as i: - new_path = f'/asset_submissions/marseys/original/{name}.{i.format.lower()}' - rename(highquality, new_path) - - author.coins += 250 - g.db.add(author) - - if v.id != author.id: - msg = f"@{v.username} (Admin) has approved a marsey you made: :{marsey.name}:\nYou have received 250 coins as a reward!" - send_repeatable_notification(author.id, msg) - - if v.id != marsey.submitter_id and author.id != marsey.submitter_id: - msg = f"@{v.username} (Admin) has approved a marsey you submitted: :{marsey.name}:" - send_repeatable_notification(marsey.submitter_id, msg) - - marsey.submitter_id = None - - return {"message": f"'{marsey.name}' approved!"} - - def remove_asset(cls, type_name:str, v:User, name:str) -> dict[str, str]: - if cls not in ASSET_TYPES: raise Exception("not a valid asset type") - should_make_lower = cls == Marsey - if should_make_lower: name = name.lower() - name = name.strip() - if not name: - abort(400, f"You need to specify a {type_name}!") - asset = None - if cls == HatDef: - asset = g.db.query(cls).filter_by(name=name).one_or_none() - else: - asset = g.db.get(cls, name) - if not asset: - abort(404, f"This {type_name} '{name}' doesn't exist!") - if v.id != asset.submitter_id and v.id not in CAN_APPROVE_ASSETS: - abort(403, f"Only Carp can remove {type_name}s!") - name = asset.name - if v.id != asset.submitter_id: - msg = f"@{v.username} has rejected a {type_name} you submitted: `'{name}'`" - send_repeatable_notification(asset.submitter_id, msg) - g.db.delete(asset) - os.remove(f"/asset_submissions/{type_name}s/{name}.webp") - os.remove(f"/asset_submissions/{type_name}s/{name}") - return {"message": f"'{name}' removed!"} - - @app.post("/remove/marsey/") - @auth_required - def remove_marsey(v, name): - return remove_asset(Marsey, "marsey", v, name) - - @app.get("/submit/hats") - @auth_required - def submit_hats(v): + def error(error): if v.admin_level >= PERMS['VIEW_PENDING_SUBMITTED_HATS']: hats = g.db.query(HatDef).filter(HatDef.submitter_id != None).all() else: hats = g.db.query(HatDef).filter(HatDef.submitter_id == v.id).all() - return render_template("submit_hats.html", v=v, hats=hats) + return render_template("submit_hats.html", v=v, hats=hats, error=error, name=name, description=description, username=username), 400 + + if g.is_tor: + return error("Image uploads are not allowed through TOR.") + + file = request.files["image"] + if not file or not file.content_type.startswith('image/'): + return error("You need to submit an image!") + + if not hat_regex.fullmatch(name): + return error("Invalid name!") + + existing = g.db.query(HatDef.name).filter_by(name=name).one_or_none() + if existing: + return error("A hat with this name already exists!") + + if not description_regex.fullmatch(description): + return error("Invalid description!") + + author = get_user(username, v=v, graceful=True, include_shadowbanned=False) + if not author: + return error(f"A user with the name '{username}' was not found!") + + highquality = f'/asset_submissions/hats/{name}' + file.save(highquality) + + with Image.open(highquality) as i: + if i.width > 100 or i.height > 130: + os.remove(highquality) + return error("Images must be 100x130") + + if len(list(Iterator(i))) > 1: price = 1000 + else: price = 500 + + filename = f'/asset_submissions/hats/{name}.webp' + copyfile(highquality, filename) + process_image(filename, v, resize=100) + + hat = HatDef(name=name, author_id=author.id, description=description, price=price, submitter_id=v.id) + g.db.add(hat) + g.db.commit() + + if v.admin_level >= PERMS['VIEW_PENDING_SUBMITTED_HATS']: hats = g.db.query(HatDef).filter(HatDef.submitter_id != None).all() + else: hats = g.db.query(HatDef).filter(HatDef.submitter_id == v.id).all() + return render_template("submit_hats.html", v=v, hats=hats, msg=f"'{name}' submitted successfully!") - @app.post("/submit/hats") - @auth_required - def submit_hat(v): - name = request.values.get('name', '').strip() - description = request.values.get('description', '').strip() - username = request.values.get('author', '').strip() +@app.post("/admin/approve/hat/") +@admin_level_required(PERMS['MODERATE_PENDING_SUBMITTED_HATS']) +def approve_hat(v, name): + hat = verify_permissions_and_get_asset(HatDef, "hat", v, name, False) + description = request.values.get('description').strip() + if not description: abort(400, "You need to include a description!") - def error(error): - if v.admin_level >= PERMS['VIEW_PENDING_SUBMITTED_HATS']: hats = g.db.query(HatDef).filter(HatDef.submitter_id != None).all() - else: hats = g.db.query(HatDef).filter(HatDef.submitter_id == v.id).all() - return render_template("submit_hats.html", v=v, hats=hats, error=error, name=name, description=description, username=username), 400 + new_name = request.values.get('name').strip() + if not new_name: abort(400, "You need to include a name!") + if not hat_regex.fullmatch(new_name): abort(400, "Invalid name!") + if not description_regex.fullmatch(description): abort(400, "Invalid description!") - if request.headers.get("cf-ipcountry") == "T1": - return error("Image uploads are not allowed through TOR.") - - file = request.files["image"] - if not file or not file.content_type.startswith('image/'): - return error("You need to submit an image!") - - if not hat_regex.fullmatch(name): - return error("Invalid name!") - - existing = g.db.query(HatDef.name).filter_by(name=name).one_or_none() - if existing: - return error("A hat with this name already exists!") - - if not description_regex.fullmatch(description): - return error("Invalid description!") - - author = get_user(username, v=v, graceful=True, include_shadowbanned=False) - if not author: - return error(f"A user with the name '{username}' was not found!") - - highquality = f'/asset_submissions/hats/{name}' - file.save(highquality) - - with Image.open(highquality) as i: - if i.width > 100 or i.height > 130: - os.remove(highquality) - return error("Images must be 100x130") - - if len(list(Iterator(i))) > 1: price = 1000 - else: price = 500 - - filename = f'/asset_submissions/hats/{name}.webp' - copyfile(highquality, filename) - process_image(filename, resize=100) - - hat = HatDef(name=name, author_id=author.id, description=description, price=price, submitter_id=v.id) - g.db.add(hat) - g.db.commit() - - if v.admin_level >= PERMS['VIEW_PENDING_SUBMITTED_HATS']: hats = g.db.query(HatDef).filter(HatDef.submitter_id != None).all() - else: hats = g.db.query(HatDef).filter(HatDef.submitter_id == v.id).all() - return render_template("submit_hats.html", v=v, hats=hats, msg=f"'{name}' submitted successfully!") + try: + hat.price = int(request.values.get('price')) + if hat.price < 0: raise ValueError("Invalid hat price") + except: + abort(400, "Invalid hat price") + hat.name = new_name + hat.description = description + g.db.add(hat) - @app.post("/admin/approve/hat/") - @admin_level_required(PERMS['MODERATE_PENDING_SUBMITTED_HATS']) - def approve_hat(v, name): - hat = verify_permissions_and_get_asset(HatDef, "hat", v, name, False) - description = request.values.get('description').strip() - if not description: abort(400, "You need to include a description!") + g.db.flush() + author = hat.author - new_name = request.values.get('name').strip() - if not new_name: abort(400, "You need to include a name!") - if not hat_regex.fullmatch(new_name): abort(400, "Invalid name!") - if not description_regex.fullmatch(description): abort(400, "Invalid description!") + all_by_author = g.db.query(HatDef).filter_by(author_id=author.id).count() - try: - hat.price = int(request.values.get('price')) - if hat.price < 0: raise ValueError("Invalid hat price") - except: - abort(400, "Invalid hat price") - hat.name = new_name - hat.description = description - g.db.add(hat) + if all_by_author >= 250: + badge_grant(badge_id=166, user=author) + elif all_by_author >= 100: + badge_grant(badge_id=165, user=author) + elif all_by_author >= 50: + badge_grant(badge_id=164, user=author) + elif all_by_author >= 10: + badge_grant(badge_id=163, user=author) + + hat_copy = Hat( + user_id=author.id, + hat_id=hat.id + ) + g.db.add(hat_copy) - g.db.flush() - author = hat.author + if v.id != author.id: + msg = f"@{v.username} (Admin) has approved a hat you made: '{hat.name}'" + send_repeatable_notification(author.id, msg) - all_by_author = g.db.query(HatDef).filter_by(author_id=author.id).count() + if v.id != hat.submitter_id and author.id != hat.submitter_id: + msg = f"@{v.username} (Admin) has approved a hat you submitted: '{hat.name}'" + send_repeatable_notification(hat.submitter_id, msg) - if all_by_author >= 250: - badge_grant(badge_id=166, user=author) - elif all_by_author >= 100: - badge_grant(badge_id=165, user=author) - elif all_by_author >= 50: - badge_grant(badge_id=164, user=author) - elif all_by_author >= 10: - badge_grant(badge_id=163, user=author) + hat.submitter_id = None - hat_copy = Hat( - user_id=author.id, - hat_id=hat.id - ) - g.db.add(hat_copy) + move(f"/asset_submissions/hats/{name}.webp", f"files/assets/images/hats/{hat.name}.webp") + + highquality = f"/asset_submissions/hats/{name}" + with Image.open(highquality) as i: + new_path = f'/asset_submissions/hats/original/{name}.{i.format.lower()}' + rename(highquality, new_path) + + return {"message": f"'{hat.name}' approved!"} + +@app.post("/remove/hat/") +@auth_required +def remove_hat(v, name): + return remove_asset(HatDef, 'hat', v, name) + +@app.get("/admin/update/marseys") +@admin_level_required(PERMS['UPDATE_MARSEYS']) +def update_marseys(v): + if AEVANN_ID and v.id not in CAN_UPDATE_ASSETS: + abort(403) + name = request.values.get('name') + tags = None + error = None + if name: + marsey = g.db.get(Marsey, name) + if marsey: + tags = marsey.tags or '' + else: + name = '' + tags = '' + error = "A marsey with this name doesn't exist!" + return render_template("update_assets.html", v=v, error=error, name=name, tags=tags, type="Marsey") - if v.id != author.id: - msg = f"@{v.username} (Admin) has approved a hat you made: '{hat.name}'" - send_repeatable_notification(author.id, msg) +@app.post("/admin/update/marseys") +@admin_level_required(PERMS['UPDATE_MARSEYS']) +def update_marsey(v): + if AEVANN_ID and v.id not in CAN_UPDATE_ASSETS: + abort(403) - if v.id != hat.submitter_id and author.id != hat.submitter_id: - msg = f"@{v.username} (Admin) has approved a hat you submitted: '{hat.name}'" - send_repeatable_notification(hat.submitter_id, msg) + file = request.files["image"] + name = request.values.get('name', '').lower().strip() + tags = request.values.get('tags', '').lower().strip() - hat.submitter_id = None - - move(f"/asset_submissions/hats/{name}.webp", f"files/assets/images/hats/{hat.name}.webp") - - highquality = f"/asset_submissions/hats/{name}" - with Image.open(highquality) as i: - new_path = f'/asset_submissions/hats/original/{name}.{i.format.lower()}' - rename(highquality, new_path) - - return {"message": f"'{hat.name}' approved!"} - - @app.post("/remove/hat/") - @auth_required - def remove_hat(v, name): - return remove_asset(HatDef, 'hat', v, name) - - @app.get("/admin/update/marseys") - @admin_level_required(PERMS['UPDATE_MARSEYS']) - def update_marseys(v): - if AEVANN_ID and v.id not in CAN_UPDATE_ASSETS: - abort(403) - name = request.values.get('name') - tags = None - error = None - if name: - marsey = g.db.get(Marsey, name) - if marsey: - tags = marsey.tags or '' - else: - name = '' - tags = '' - error = "A marsey with this name doesn't exist!" + def error(error): return render_template("update_assets.html", v=v, error=error, name=name, tags=tags, type="Marsey") + if not marsey_regex.fullmatch(name): + return error("Invalid name!") - @app.post("/admin/update/marseys") - @admin_level_required(PERMS['UPDATE_MARSEYS']) - def update_marsey(v): - if AEVANN_ID and v.id not in CAN_UPDATE_ASSETS: - abort(403) + existing = g.db.get(Marsey, name) + if not existing: + return error("A marsey with this name doesn't exist!") - file = request.files["image"] - name = request.values.get('name', '').lower().strip() - tags = request.values.get('tags', '').lower().strip() - - def error(error): - return render_template("update_assets.html", v=v, error=error, name=name, tags=tags, type="Marsey") - - if not marsey_regex.fullmatch(name): - return error("Invalid name!") - - existing = g.db.get(Marsey, name) - if not existing: - return error("A marsey with this name doesn't exist!") - - if file: - if request.headers.get("cf-ipcountry") == "T1": - return error("Image uploads are not allowed through TOR.") - if not file.content_type.startswith('image/'): - return error("You need to submit an image!") - - for x in IMAGE_FORMATS: - if path.isfile(f'/asset_submissions/marseys/original/{name}.{x}'): - os.remove(f'/asset_submissions/marseys/original/{name}.{x}') - - highquality = f"/asset_submissions/marseys/{name}" - file.save(highquality) - with Image.open(highquality) as i: - format = i.format.lower() - new_path = f'/asset_submissions/marseys/original/{name}.{format}' - rename(highquality, new_path) - - filename = f"files/assets/images/emojis/{name}.webp" - copyfile(new_path, filename) - process_image(filename, resize=200, trim=True) - purge_files_in_cache([f"https://{SITE}/e/{name}.webp", f"https://{SITE}/assets/images/emojis/{name}.webp", f"https://{SITE}/asset_submissions/marseys/original/{name}.{format}"]) - - if tags and existing.tags != tags and tags != "none": - existing.tags = tags - g.db.add(existing) - elif not file: - return error("You need to update this marsey!") - - ma = ModAction( - kind="update_marsey", - user_id=v.id, - _note=f'{name}' - ) - g.db.add(ma) - return render_template("update_assets.html", v=v, msg=f"'{name}' updated successfully!", name=name, tags=tags, type="Marsey") - - @app.get("/admin/update/hats") - @admin_level_required(PERMS['UPDATE_HATS']) - def update_hats(v): - if AEVANN_ID and v.id not in CAN_UPDATE_ASSETS: - abort(403) - return render_template("update_assets.html", v=v, type="Hat") - - - @app.post("/admin/update/hats") - @admin_level_required(PERMS['UPDATE_HATS']) - def update_hat(v): - if AEVANN_ID and v.id not in CAN_UPDATE_ASSETS: - abort(403) - - file = request.files["image"] - name = request.values.get('name', '').strip() - - def error(error): - return render_template("update_assets.html", v=v, error=error, type="Hat") - - if request.headers.get("cf-ipcountry") == "T1": + if file: + if g.is_tor: return error("Image uploads are not allowed through TOR.") - - if not file or not file.content_type.startswith('image/'): + if not file.content_type.startswith('image/'): return error("You need to submit an image!") - - if not hat_regex.fullmatch(name): - return error("Invalid name!") - - existing = g.db.query(HatDef.name).filter_by(name=name).one_or_none() - if not existing: - return error("A hat with this name doesn't exist!") - - highquality = f"/asset_submissions/hats/{name}" - file.save(highquality) - - with Image.open(highquality) as i: - if i.width > 100 or i.height > 130: - os.remove(highquality) - return error("Images must be 100x130") - - format = i.format.lower() - new_path = f'/asset_submissions/hats/original/{name}.{format}' - + for x in IMAGE_FORMATS: - if path.isfile(f'/asset_submissions/hats/original/{name}.{x}'): - os.remove(f'/asset_submissions/hats/original/{name}.{x}') + if path.isfile(f'/asset_submissions/marseys/original/{name}.{x}'): + os.remove(f'/asset_submissions/marseys/original/{name}.{x}') + highquality = f"/asset_submissions/marseys/{name}" + file.save(highquality) + with Image.open(highquality) as i: + format = i.format.lower() + new_path = f'/asset_submissions/marseys/original/{name}.{format}' rename(highquality, new_path) - filename = f"files/assets/images/hats/{name}.webp" + filename = f"files/assets/images/emojis/{name}.webp" copyfile(new_path, filename) - process_image(filename, resize=100) - purge_files_in_cache([f"https://{SITE}/i/hats/{name}.webp", f"https://{SITE}/assets/images/hats/{name}.webp", f"https://{SITE}/asset_submissions/hats/original/{name}.{format}"]) - ma = ModAction( - kind="update_hat", - user_id=v.id, - _note=f'{name}' - ) - g.db.add(ma) - return render_template("update_assets.html", v=v, msg=f"'{name}' updated successfully!", type="Hat") + process_image(filename, v, resize=200, trim=True) + purge_files_in_cache([f"https://{SITE}/e/{name}.webp", f"https://{SITE}/assets/images/emojis/{name}.webp", f"https://{SITE}/asset_submissions/marseys/original/{name}.{format}"]) + + if tags and existing.tags != tags and tags != "none": + existing.tags = tags + g.db.add(existing) + elif not file: + return error("You need to update this marsey!") + + ma = ModAction( + kind="update_marsey", + user_id=v.id, + _note=f'{name}' + ) + g.db.add(ma) + return render_template("update_assets.html", v=v, msg=f"'{name}' updated successfully!", name=name, tags=tags, type="Marsey") + +@app.get("/admin/update/hats") +@admin_level_required(PERMS['UPDATE_HATS']) +def update_hats(v): + if AEVANN_ID and v.id not in CAN_UPDATE_ASSETS: + abort(403) + return render_template("update_assets.html", v=v, type="Hat") + + +@app.post("/admin/update/hats") +@admin_level_required(PERMS['UPDATE_HATS']) +def update_hat(v): + if AEVANN_ID and v.id not in CAN_UPDATE_ASSETS: + abort(403) + + file = request.files["image"] + name = request.values.get('name', '').strip() + + def error(error): + return render_template("update_assets.html", v=v, error=error, type="Hat") + + if g.is_tor: + return error("Image uploads are not allowed through TOR.") + + if not file or not file.content_type.startswith('image/'): + return error("You need to submit an image!") + + if not hat_regex.fullmatch(name): + return error("Invalid name!") + + existing = g.db.query(HatDef.name).filter_by(name=name).one_or_none() + if not existing: + return error("A hat with this name doesn't exist!") + + highquality = f"/asset_submissions/hats/{name}" + file.save(highquality) + + with Image.open(highquality) as i: + if i.width > 100 or i.height > 130: + os.remove(highquality) + return error("Images must be 100x130") + + format = i.format.lower() + new_path = f'/asset_submissions/hats/original/{name}.{format}' + + for x in IMAGE_FORMATS: + if path.isfile(f'/asset_submissions/hats/original/{name}.{x}'): + os.remove(f'/asset_submissions/hats/original/{name}.{x}') + + rename(highquality, new_path) + + filename = f"files/assets/images/hats/{name}.webp" + copyfile(new_path, filename) + process_image(filename, v, resize=100) + purge_files_in_cache([f"https://{SITE}/i/hats/{name}.webp", f"https://{SITE}/assets/images/hats/{name}.webp", f"https://{SITE}/asset_submissions/hats/original/{name}.{format}"]) + ma = ModAction( + kind="update_hat", + user_id=v.id, + _note=f'{name}' + ) + g.db.add(ma) + return render_template("update_assets.html", v=v, msg=f"'{name}' updated successfully!", type="Hat") diff --git a/files/routes/awards.py b/files/routes/awards.py index b6c3ddf7c..70c1c9535 100644 --- a/files/routes/awards.py +++ b/files/routes/awards.py @@ -1,18 +1,23 @@ -from files.__main__ import app, limiter -from files.helpers.wrappers import * -from files.helpers.alerts import * -from files.helpers.get import * -from files.helpers.const import * -from files.helpers.regex import * -from files.helpers.actions import * -from files.helpers.useractions import * -from files.classes.award import * -from .front import frontlist +from copy import deepcopy + from flask import g, request -from files.helpers.sanitize import filter_emojis_only +from sqlalchemy import func + +from files.classes.award import AwardRelationship +from files.classes.userblock import UserBlock +from files.helpers.actions import * +from files.helpers.alerts import * +from files.helpers.const import * +from files.helpers.get import * from files.helpers.marsify import marsify from files.helpers.owoify import owoify -from copy import deepcopy +from files.helpers.regex import * +from files.helpers.sanitize import filter_emojis_only +from files.helpers.useractions import * +from files.routes.wrappers import * +from files.__main__ import app, cache, limiter + +from .front import frontlist @app.get("/shop") @app.get("/settings/shop") diff --git a/files/routes/casino.py b/files/routes/casino.py index 9ffa1ea41..fedf308a5 100644 --- a/files/routes/casino.py +++ b/files/routes/casino.py @@ -1,15 +1,15 @@ -from files.__main__ import app -from files.helpers.wrappers import * +from files.classes.casino_game import CASINO_GAME_KINDS from files.helpers.alerts import * -from files.helpers.get import * -from files.helpers.const import * -from files.helpers.wrappers import * from files.helpers.casino import * +from files.helpers.const import * +from files.helpers.get import * +from files.helpers.lottery import * +from files.helpers.roulette import * from files.helpers.slots import * from files.helpers.twentyone import * -from files.helpers.roulette import * -from files.helpers.lottery import * +from files.routes.wrappers import * +from files.__main__ import app, limiter @app.get("/casino") @feature_required('GAMBLING') @@ -32,8 +32,8 @@ def casino_game_page(v, game): elif game not in CASINO_GAME_KINDS: abort(404) - feed = json.dumps(get_game_feed(game)) - leaderboard = json.dumps(get_game_leaderboard(game)) + feed = json.dumps(get_game_feed(game, g.db)) + leaderboard = json.dumps(get_game_leaderboard(game, g.db)) game_state = '' if game == 'blackjack': @@ -60,7 +60,7 @@ def casino_game_feed(v, game): elif game not in CASINO_GAME_KINDS: abort(404) - feed = get_game_feed(game) + feed = get_game_feed(game, g.db) return {"feed": feed} @@ -121,7 +121,7 @@ def blackjack_deal_to_player(v): currency = request.values.get("currency") create_new_game(v, wager, currency) state = dispatch_action(v, BlackjackAction.DEAL) - feed = get_game_feed('blackjack') + feed = get_game_feed('blackjack', g.db) return {"success": True, "state": state, "feed": feed, "gambler": {"coins": v.coins, "procoins": v.procoins}} except Exception as e: @@ -138,7 +138,7 @@ def blackjack_player_hit(v): try: state = dispatch_action(v, BlackjackAction.HIT) - feed = get_game_feed('blackjack') + feed = get_game_feed('blackjack', g.db) return {"success": True, "state": state, "feed": feed, "gambler": {"coins": v.coins, "procoins": v.procoins}} except: abort(400, "Unable to hit.") @@ -154,7 +154,7 @@ def blackjack_player_stay(v): try: state = dispatch_action(v, BlackjackAction.STAY) - feed = get_game_feed('blackjack') + feed = get_game_feed('blackjack', g.db) return {"success": True, "state": state, "feed": feed, "gambler": {"coins": v.coins, "procoins": v.procoins}} except: abort(400, "Unable to stay.") @@ -170,7 +170,7 @@ def blackjack_player_doubled_down(v): try: state = dispatch_action(v, BlackjackAction.DOUBLE_DOWN) - feed = get_game_feed('blackjack') + feed = get_game_feed('blackjack', g.db) return {"success": True, "state": state, "feed": feed, "gambler": {"coins": v.coins, "procoins": v.procoins}} except: abort(400, "Unable to double down.") @@ -186,7 +186,7 @@ def blackjack_player_bought_insurance(v): try: state = dispatch_action(v, BlackjackAction.BUY_INSURANCE) - feed = get_game_feed('blackjack') + feed = get_game_feed('blackjack', g.db) return {"success": True, "state": state, "feed": feed, "gambler": {"coins": v.coins, "procoins": v.procoins}} except: abort(403, "Unable to buy insurance.") diff --git a/files/routes/chat.py b/files/routes/chat.py index 3537e8cda..379d2dd0c 100644 --- a/files/routes/chat.py +++ b/files/routes/chat.py @@ -1,17 +1,17 @@ +import atexit import time import uuid -from files.helpers.jinja2 import timestamp -from files.helpers.wrappers import * -from files.helpers.sanitize import sanitize -from files.helpers.const import * -from files.helpers.alerts import * -from files.helpers.regex import * -from files.helpers.actions import * + from flask_socketio import SocketIO, emit -from files.__main__ import app, limiter, cache -from flask import render_template -import sys -import atexit + +from files.helpers.actions import * +from files.helpers.alerts import * +from files.helpers.const import * +from files.helpers.regex import * +from files.helpers.sanitize import sanitize +from files.routes.wrappers import * + +from files.__main__ import app, cache, limiter if SITE == 'localhost': socketio = SocketIO( diff --git a/files/routes/comments.py b/files/routes/comments.py index f40b503b9..fb2b3b871 100644 --- a/files/routes/comments.py +++ b/files/routes/comments.py @@ -1,26 +1,27 @@ -from files.helpers.wrappers import * +import os +from collections import Counter +from json import loads +from shutil import copyfile + +import gevent + +from files.classes import * +from files.helpers.actions import * from files.helpers.alerts import * -from files.helpers.media import * +from files.helpers.cloudflare import purge_files_in_cache from files.helpers.const import * +from files.helpers.get import * +from files.helpers.marsify import marsify +from files.helpers.media import * +from files.helpers.owoify import owoify from files.helpers.regex import * +from files.helpers.sanitize import filter_emojis_only from files.helpers.slots import * from files.helpers.treasure import * -from files.helpers.actions import * -from files.helpers.get import * -from files.classes import * from files.routes.front import comment_idlist -from flask import * -from files.__main__ import app, limiter -from files.helpers.sanitize import filter_emojis_only -from files.helpers.marsify import marsify -from files.helpers.owoify import owoify -from files.helpers.cloudflare import purge_files_in_cache -import requests -from shutil import copyfile -from json import loads -from collections import Counter -import gevent -import os +from files.routes.routehelpers import execute_shadowban_viewers_and_voters +from files.routes.wrappers import * +from files.__main__ import app, cache, limiter WORDLE_COLOR_MAPPINGS = {-1: "🟥", 0: "🟨", 1: "đźź©"} @@ -73,6 +74,9 @@ def post_pid_comment_cid(cid, pid=None, anything=None, v=None, sub=None): # props won't save properly unless you put them in a list output = get_comments_v_properties(v, False, None, Comment.top_comment_id == c.top_comment_id)[1] post.replies=[top_comment] + + execute_shadowban_viewers_and_voters(v, post) + execute_shadowban_viewers_and_voters(v, comment) if v and v.client: return top_comment.json else: @@ -142,13 +146,13 @@ def comment(v): choices.append(i.group(1)) body = body.replace(i.group(0), "") - if request.files.get("file") and request.headers.get("cf-ipcountry") != "T1": + if request.files.get("file") and not g.is_tor: files = request.files.getlist('file')[:4] for file in files: if file.content_type.startswith('image/'): oldname = f'/images/{time.time()}'.replace('.','') + '.webp' file.save(oldname) - image = process_image(oldname, patron=v.patron) + image = process_image(oldname, v) if image == "": abort(400, "Image upload failed") if v.admin_level >= PERMS['SITE_SETTINGS_SIDEBARS_BANNERS_BADGES'] and level == 1: def process_sidebar_or_banner(type, resize=0): @@ -157,7 +161,7 @@ def comment(v): num = int(li.split('.webp')[0]) + 1 filename = f'files/assets/images/{SITE_NAME}/{type}/{num}.webp' copyfile(oldname, filename) - process_image(filename, resize=resize) + process_image(filename, v, resize=resize) if parent_post.id == SIDEBAR_THREAD: process_sidebar_or_banner('sidebar', 400) @@ -177,15 +181,15 @@ def comment(v): g.db.flush() filename = f'files/assets/images/badges/{badge.id}.webp' copyfile(oldname, filename) - process_image(filename, resize=300) + process_image(filename, v, resize=300) purge_files_in_cache(f"https://{SITE}/assets/images/badges/{badge.id}.webp") except Exception as e: abort(400, str(e)) body += f"\n\n![]({image})" elif file.content_type.startswith('video/'): - body += f"\n\n{SITE_FULL}{process_video(file)}" + body += f"\n\n{SITE_FULL}{process_video(file, v)}" elif file.content_type.startswith('audio/'): - body += f"\n\n{SITE_FULL}{process_audio(file)}" + body += f"\n\n{SITE_FULL}{process_audio(file, v)}" else: abort(415) @@ -362,7 +366,7 @@ def comment(v): g.db.flush() - if v.client: return c.json + if v.client: return c.json(g.db) return {"comment": render_template("comments.html", v=v, comments=[c])} @@ -382,10 +386,10 @@ def edit_comment(cid, v): body = sanitize_raw_body(request.values.get("body", ""), False) - if len(body) < 1 and not (request.files.get("file") and request.headers.get("cf-ipcountry") != "T1"): + if len(body) < 1 and not (request.files.get("file") and not g.is_tor): abort(400, "You have to actually type something!") - if body != c.body or request.files.get("file") and request.headers.get("cf-ipcountry") != "T1": + if body != c.body or request.files.get("file") and not g.is_tor: if v.longpost and (len(body) < 280 or ' [](' in body or body.startswith('[](')): abort(403, "You have to type more than 280 characters!") elif v.bird and len(body) > 140: @@ -415,7 +419,7 @@ def edit_comment(cid, v): execute_antispam_comment_check(body, v) - body += process_files() + body += process_files(request.files, v) body = body.strip()[:COMMENT_BODY_LENGTH_LIMIT] # process_files potentially adds characters to the post body_for_sanitize = body @@ -463,17 +467,11 @@ def edit_comment(cid, v): @auth_required @ratelimit_user() def delete_comment(cid, v): - c = get_comment(cid, v=v) - if not c.deleted_utc: - if c.author_id != v.id: abort(403) - c.deleted_utc = int(time.time()) - g.db.add(c) - cache.delete_memoized(comment_idlist) g.db.flush() @@ -483,7 +481,6 @@ def delete_comment(cid, v): Comment.deleted_utc == 0 ).count() g.db.add(v) - return {"message": "Comment deleted!"} @app.post("/undelete/comment/") @@ -491,18 +488,12 @@ def delete_comment(cid, v): @auth_required @ratelimit_user() def undelete_comment(cid, v): - c = get_comment(cid, v=v) - if c.deleted_utc: if c.author_id != v.id: abort(403) - c.deleted_utc = 0 - g.db.add(c) - cache.delete_memoized(comment_idlist) - g.db.flush() v.comment_count = g.db.query(Comment).filter( Comment.author_id == v.id, @@ -510,10 +501,8 @@ def undelete_comment(cid, v): Comment.deleted_utc == 0 ).count() g.db.add(v) - return {"message": "Comment undeleted!"} - @app.post("/pin_comment/") @feature_required('PINS') @auth_required diff --git a/files/routes/errors.py b/files/routes/errors.py index 7a91b2bee..63104008f 100644 --- a/files/routes/errors.py +++ b/files/routes/errors.py @@ -1,8 +1,10 @@ -from files.helpers.wrappers import * -from flask import * -from urllib.parse import quote, urlencode import time -from files.__main__ import app, limiter +from urllib.parse import quote, urlencode + +from flask import redirect, render_template, request, session, g + +from files.helpers.const import ERROR_MARSEYS, ERROR_MSGS, ERROR_TITLES, WERKZEUG_ERROR_DESCRIPTIONS, is_site_url +from files.__main__ import app # If you're adding an error, go here: # https://github.com/pallets/werkzeug/blob/main/src/werkzeug/exceptions.py @@ -53,6 +55,6 @@ def error_500(e): @app.post("/allow_nsfw") def allow_nsfw(): session["over_18"] = int(time.time()) + 3600 - redir = request.values.get("redir") + redir = request.values.get("redir", "/") if is_site_url(redir): return redirect(redir) return redirect('/') diff --git a/files/routes/feeds.py b/files/routes/feeds.py index 06f411a1d..bd54961e5 100644 --- a/files/routes/feeds.py +++ b/files/routes/feeds.py @@ -1,11 +1,11 @@ -import html -from .front import frontlist from datetime import datetime -from files.helpers.get import * -from yattag import Doc -from files.helpers.wrappers import * -from files.helpers.jinja2 import * +from yattag import Doc + +from files.helpers.get import * +from files.routes.wrappers import * + +from .front import frontlist from files.__main__ import app @app.get('/rss') diff --git a/files/routes/front.py b/files/routes/front.py index c5851c8a4..14cac5359 100644 --- a/files/routes/front.py +++ b/files/routes/front.py @@ -1,10 +1,14 @@ -from files.helpers.wrappers import * -from files.helpers.get import * -from files.helpers.const import * -from files.helpers.sorting_and_time import * -from files.__main__ import app, cache, limiter + +from sqlalchemy import or_, not_ + from files.classes.submission import Submission +from files.classes.votes import Vote from files.helpers.awards import award_timers +from files.helpers.const import * +from files.helpers.get import * +from files.helpers.sorting_and_time import * +from files.routes.wrappers import * +from files.__main__ import app, cache, limiter @app.get("/") @app.get("/h/") @@ -13,8 +17,9 @@ from files.helpers.awards import award_timers @auth_desired_with_logingate def front_all(v, sub=None, subdomain=None): #### WPD TEMP #### special front logic - from files.helpers.security import generate_hash, validate_hash from datetime import datetime + + from files.helpers.security import generate_hash, validate_hash now = datetime.utcnow() if SITE == 'watchpeopledie.co': if v and not v.admin_level and not v.id <= 9: # security: don't auto login admins or bots @@ -89,11 +94,10 @@ def front_all(v, sub=None, subdomain=None): if v.hidevotedon: posts = [x for x in posts if not hasattr(x, 'voted') or not x.voted] award_timers(v) - if v and v.client: return {"data": [x.json for x in posts], "next_exists": next_exists} + if v and v.client: return {"data": [x.json(g.db) for x in posts], "next_exists": next_exists} return render_template("home.html", v=v, listing=posts, next_exists=next_exists, sort=sort, t=t, page=page, sub=sub, home=True, pins=pins, holes=holes) - @cache.memoize(timeout=86400) def frontlist(v=None, sort="hot", page=1, t="all", ids_only=True, filter_words='', gt=0, lt=0, sub=None, site=None, pins=True, holes=True): posts = g.db.query(Submission) @@ -227,7 +231,7 @@ def all_comments(v): next_exists = len(idlist) > PAGE_SIZE idlist = idlist[:PAGE_SIZE] - if v.client: return {"data": [x.json for x in comments]} + if v.client: return {"data": [x.json(g.db) for x in comments]} return render_template("home_comments.html", v=v, sort=sort, t=t, page=page, comments=comments, standalone=True, next_exists=next_exists) diff --git a/files/routes/giphy.py b/files/routes/giphy.py index 5adcda9e7..c286c71a5 100644 --- a/files/routes/giphy.py +++ b/files/routes/giphy.py @@ -1,11 +1,10 @@ -from flask import * import requests -from files.helpers.wrappers import * + from files.helpers.const import * +from files.routes.wrappers import * from files.__main__ import app - @app.get("/giphy") @app.get("/giphy") @auth_required diff --git a/files/routes/hats.py b/files/routes/hats.py index 810dd36e8..36ab21555 100644 --- a/files/routes/hats.py +++ b/files/routes/hats.py @@ -1,10 +1,11 @@ -from files.__main__ import app, limiter +from sqlalchemy import func + from files.classes.hats import * from files.helpers.alerts import * -from files.helpers.wrappers import * from files.helpers.const import * from files.helpers.useractions import * -from flask import g +from files.routes.wrappers import * +from files.__main__ import app, limiter @app.get("/hats") @feature_required('HATS') diff --git a/files/helpers/jinja2.py b/files/routes/jinja2.py similarity index 82% rename from files/helpers/jinja2.py rename to files/routes/jinja2.py index d9dbed754..e1ab668c3 100644 --- a/files/helpers/jinja2.py +++ b/files/routes/jinja2.py @@ -1,20 +1,30 @@ -from files.__main__ import app, cache -from jinja2 import pass_context -from .get import * -from os import listdir, environ -from .const import * import time + +from os import environ, listdir + +from jinja2 import pass_context + from files.helpers.assetcache import assetcache_path -from files.helpers.wrappers import calc_users +from files.helpers.const import * +from files.helpers.settings import get_settings +from files.helpers.sorting_and_time import make_age_string +from files.routes.routehelpers import get_formkey +from files.routes.wrappers import calc_users +from files.__main__ import app, cache + +@app.template_filter("formkey") +def formkey(u): + return get_formkey(u) @app.template_filter("post_embed") def post_embed(id, v): + from flask import render_template + + from files.helpers.get import get_post p = get_post(id, v, graceful=True) - if p: return render_template("submission_listing.html", listing=[p], v=v) return '' - @app.template_filter("asset") @pass_context def template_asset(ctx, asset_path): @@ -26,7 +36,6 @@ def template_asset_siteimg(asset_path): # TODO: Add hashing for these using files.helpers.assetcache return f'/i/{SITE_NAME}/{asset_path}?v=3010' - @app.template_filter("timestamp") def timestamp(timestamp): return make_age_string(timestamp) @@ -46,7 +55,7 @@ def inject_constants(): "BADGE_THREAD":BADGE_THREAD, "SNAPPY_THREAD":SNAPPY_THREAD, "KOFI_TOKEN":KOFI_TOKEN, "KOFI_LINK":KOFI_LINK, "approved_embed_hosts":approved_embed_hosts, - "site_settings":app.config['SETTINGS'], "EMAIL":EMAIL, "calc_users":calc_users, + "site_settings":get_settings(), "EMAIL":EMAIL, "calc_users":calc_users, "max": max, "min": min, "TELEGRAM_LINK":TELEGRAM_LINK, "EMAIL_REGEX_PATTERN":EMAIL_REGEX_PATTERN, "CONTENT_SECURITY_POLICY_DEFAULT":CONTENT_SECURITY_POLICY_DEFAULT, diff --git a/files/routes/login.py b/files/routes/login.py index 3af1863ad..da62949fe 100644 --- a/files/routes/login.py +++ b/files/routes/login.py @@ -1,17 +1,24 @@ -from urllib.parse import urlencode -from files.mail import * -from files.__main__ import app, get_CF, limiter -from files.helpers.const import * -from files.helpers.regex import * -from files.helpers.actions import * -from files.helpers.get import * -import requests import secrets +from urllib.parse import urlencode + +import requests + +from files.__main__ import app, cache, get_CF, limiter +from files.classes.follows import Follow +from files.helpers.actions import * +from files.helpers.const import * +from files.helpers.settings import get_setting +from files.helpers.get import * +from files.helpers.mail import send_mail, send_verification_email +from files.helpers.regex import * +from files.helpers.security import * +from files.helpers.useractions import badge_grant +from files.routes.routehelpers import check_for_alts +from files.routes.wrappers import * @app.get("/login") @auth_desired def login_get(v): - redir = request.values.get("redirect", "/").strip().rstrip('?') if redir: if not is_site_url(redir): redir = "/" @@ -19,63 +26,6 @@ def login_get(v): return render_template("login.html", failed=False, redirect=redir) - -def check_for_alts(current:User, include_current_session=True): - current_id = current.id - if current_id in (1691,6790,7069,36152) and include_current_session: - session["history"] = [] - return - ids = [x[0] for x in g.db.query(User.id).all()] - past_accs = set(session.get("history", [])) if include_current_session else set() - - def add_alt(user1:int, user2:int): - li = [user1, user2] - existing = g.db.query(Alt).filter(Alt.user1.in_(li), Alt.user2.in_(li)).one_or_none() - if not existing: - new_alt = Alt(user1=user1, user2=user2) - g.db.add(new_alt) - g.db.flush() - - for past_id in list(past_accs): - if past_id not in ids: - past_accs.remove(past_id) - continue - - if past_id == MOM_ID or current_id == MOM_ID: break - if past_id == current_id: continue - - li = [past_id, current_id] - add_alt(past_id, current_id) - other_alts = g.db.query(Alt).filter(Alt.user1.in_(li), Alt.user2.in_(li)).all() - for a in other_alts: - if a.deleted: - if include_current_session: - try: session["history"].remove(a.user1) - except: pass - try: session["history"].remove(a.user2) - except: pass - continue # don't propagate deleted alt links - if a.user1 != past_id: add_alt(a.user1, past_id) - if a.user1 != current_id: add_alt(a.user1, current_id) - if a.user2 != past_id: add_alt(a.user2, past_id) - if a.user2 != current_id: add_alt(a.user2, current_id) - - past_accs.add(current_id) - if include_current_session: - session["history"] = list(past_accs) - g.db.flush() - for u in current.alts_unique: - if u._alt_deleted: continue - if u.shadowbanned: - current.shadowbanned = u.shadowbanned - if not current.is_banned: current.ban_reason = u.ban_reason - g.db.add(current) - elif current.shadowbanned: - u.shadowbanned = current.shadowbanned - if not u.is_banned: u.ban_reason = current.ban_reason - g.db.add(u) - - def login_deduct_when(resp): if not g: return False @@ -84,8 +34,7 @@ def login_deduct_when(resp): return g.login_failed @app.post("/login") -@limiter.limit("6/minute;10/day", - deduct_when=login_deduct_when) +@limiter.limit("6/minute;10/day", deduct_when=login_deduct_when) def login_post(): template = '' g.login_failed = True @@ -188,20 +137,16 @@ def me(v): @auth_required @ratelimit_user() def logout(v): - loggedin = cache.get(f'{SITE}_loggedin') or {} if session.get("lo_user") in loggedin: del loggedin[session["lo_user"]] cache.set(f'{SITE}_loggedin', loggedin) - session.pop("lo_user", None) - return {"message": "Logout successful!"} - @app.get("/signup") @auth_desired def sign_up_get(v): - if not app.config['SETTINGS']['Signups']: + if not get_setting('Signups'): return {"error": "New account registration is currently closed. Please come back later."}, 403 if v: return redirect(SITE_FULL) @@ -219,7 +164,7 @@ def sign_up_get(v): return render_template("sign_up_failed_ref.html") now = int(time.time()) - token = token_hex(16) + token = secrets.token_hex(16) session["signup_token"] = token formkey_hashstr = str(now) + token + g.agent @@ -249,7 +194,7 @@ def sign_up_get(v): @limiter.limit("1/second;10/day") @auth_desired def sign_up_post(v): - if not app.config['SETTINGS']['Signups']: + if not get_setting('Signups'): return {"error": "New account registration is currently closed. Please come back later."}, 403 if v: abort(403) @@ -261,18 +206,14 @@ def sign_up_post(v): if not submitted_token: abort(400) correct_formkey_hashstr = form_timestamp + submitted_token + g.agent - correct_formkey = hmac.new(key=bytes(SECRET_KEY, "utf-16"), msg=bytes(correct_formkey_hashstr, "utf-16"), digestmod='md5' ).hexdigest() now = int(time.time()) - username = request.values.get("username") - if not username: abort(400) - username = username.strip() def signup_error(error): diff --git a/files/routes/lottery.py b/files/routes/lottery.py index dd36f3748..920754909 100644 --- a/files/routes/lottery.py +++ b/files/routes/lottery.py @@ -1,12 +1,13 @@ -from files.__main__ import app, limiter -from files.helpers.wrappers import * -from files.helpers.alerts import * -from files.helpers.get import * -from files.helpers.const import * -from files.helpers.wrappers import * -from files.helpers.lottery import * import requests +from files.helpers.alerts import * +from files.helpers.const import * +from files.helpers.get import * +from files.helpers.lottery import * +from files.routes.wrappers import * + +from files.__main__ import app, limiter + @app.post("/lottery/end") @feature_required('GAMBLING') @admin_level_required(PERMS['LOTTERY_ADMIN']) diff --git a/files/mail/__init__.py b/files/routes/mail.py similarity index 62% rename from files/mail/__init__.py rename to files/routes/mail.py index c05b8424d..ed9f4160a 100644 --- a/files/mail/__init__.py +++ b/files/routes/mail.py @@ -1,75 +1,30 @@ -import requests import time -from flask import * -from urllib.parse import quote -from files.helpers.security import * -from files.helpers.wrappers import * +from files.classes import * from files.helpers.const import * from files.helpers.get import * +from files.helpers.mail import * from files.helpers.useractions import * -from files.classes import * +from files.routes.wrappers import * from files.__main__ import app, limiter - -def send_mail(to_address, subject, html): - if MAILGUN_KEY == 'blahblahblah': return - - url = f"https://api.mailgun.net/v3/{SITE}/messages" - - auth = ("api", MAILGUN_KEY) - - data = {"from": EMAIL, - "to": [to_address], - "subject": subject, - "html": html, - } - - requests.post(url, auth=auth, data=data) - - -def send_verification_email(user, email=None): - - if not email: - email = user.email - - url = f"https://{SITE}/activate" - now = int(time.time()) - - token = generate_hash(f"{email}+{user.id}+{now}") - params = f"?email={quote(email)}&id={user.id}&time={now}&token={token}" - - link = url + params - - send_mail(to_address=email, - html=render_template("email/email_verify.html", - action_url=link, - v=user), - subject=f"Validate your {SITE_NAME} account email." - ) - - @app.post("/verify_email") @limiter.limit(DEFAULT_RATELIMIT_SLOWER) @auth_required @ratelimit_user() def verify_email(v): - send_verification_email(v) - return {"message": "Email has been sent (ETA ~5 minutes)"} @app.get("/activate") @auth_required def activate(v): - email = request.values.get("email", "").strip().lower() if not email_regex.fullmatch(email): return render_template("message.html", v=v, title="Invalid email.", error="Invalid email."), 400 - id = request.values.get("id", "").strip() timestamp = int(request.values.get("time", "0")) token = request.values.get("token", "").strip() diff --git a/files/routes/notifications.py b/files/routes/notifications.py index 09cdd0324..d651976d0 100644 --- a/files/routes/notifications.py +++ b/files/routes/notifications.py @@ -1,9 +1,14 @@ -from files.helpers.wrappers import * -from files.helpers.get import * -from files.helpers.const import * -from files.__main__ import app import time +from sqlalchemy.sql.expression import not_, and_, or_ + +from files.classes.mod_logs import ModAction +from files.classes.sub_logs import SubAction +from files.helpers.const import * +from files.helpers.get import * +from files.routes.wrappers import * +from files.__main__ import app + @app.post("/clear") @auth_required @ratelimit_user() @@ -36,7 +41,6 @@ def unread(v): return {"data":[x[1].json for x in listing]} - @app.get("/notifications/modmail") @admin_level_required(PERMS['VIEW_MODMAIL']) def notifications_modmail(v): diff --git a/files/routes/oauth.py b/files/routes/oauth.py index 1885a7b04..79d17b173 100644 --- a/files/routes/oauth.py +++ b/files/routes/oauth.py @@ -1,12 +1,12 @@ -from files.helpers.wrappers import * -from files.helpers.alerts import * -from files.helpers.get import * -from files.helpers.const import * -from files.classes import * -from flask import * -from files.__main__ import app, limiter import sqlalchemy.exc +from files.classes import * +from files.helpers.alerts import * +from files.helpers.const import * +from files.helpers.get import * +from files.routes.wrappers import * +from files.__main__ import app, limiter + @app.get("/authorize") @auth_required def authorize_prompt(v): @@ -233,7 +233,7 @@ def admin_app_id_posts(v, aid): oauth = g.db.get(OauthApp, aid) if not oauth: abort(404) - pids=oauth.idlist(page=int(request.values.get("page",1))) + pids=oauth.idlist(g.db, page=int(request.values.get("page",1))) next_exists=len(pids)==101 pids=pids[:100] @@ -256,8 +256,7 @@ def admin_app_id_comments(v, aid): oauth = g.db.get(OauthApp, aid) if not oauth: abort(404) - cids=oauth.comments_idlist(page=int(request.values.get("page",1)), - ) + cids=oauth.comments_idlist(g.db, page=int(request.values.get("page",1))) next_exists=len(cids)==101 cids=cids[:100] diff --git a/files/routes/polls.py b/files/routes/polls.py index 8a9661cf5..d5f17b713 100644 --- a/files/routes/polls.py +++ b/files/routes/polls.py @@ -1,8 +1,7 @@ -from files.helpers.wrappers import * -from files.helpers.get import * -from files.helpers.const import * from files.classes import * -from flask import * +from files.helpers.const import * +from files.helpers.get import * +from files.routes.wrappers import * from files.__main__ import app diff --git a/files/routes/posts.py b/files/routes/posts.py index 787475f63..2540b94a6 100644 --- a/files/routes/posts.py +++ b/files/routes/posts.py @@ -1,33 +1,38 @@ +import os import time -import gevent -import requests -from files.helpers.wrappers import * -from files.helpers.sanitize import * -from files.helpers.alerts import * -from files.helpers.discord import * -from files.helpers.const import * -from files.helpers.regex import * -from files.helpers.slots import * -from files.helpers.get import * -from files.helpers.actions import * -from files.helpers.sorting_and_time import * -from files.classes import * -from flask import * from io import BytesIO -from files.__main__ import app, limiter, cache, db_session -from PIL import Image -from .front import frontlist -from urllib.parse import ParseResult, urlunparse, urlparse, quote, unquote from os import path -import requests from shutil import copyfile from sys import stdout -import os +from urllib.parse import ParseResult, quote, unquote, urlparse, urlunparse +import gevent +import requests +from flask import * +from PIL import Image + +from files.__main__ import app, cache, limiter +from files.classes import * +from files.helpers.actions import * +from files.helpers.alerts import * +from files.helpers.const import * +from files.helpers.discord import * +from files.helpers.get import * +from files.helpers.regex import * +from files.helpers.sanitize import * +from files.helpers.settings import get_setting +from files.helpers.slots import * +from files.helpers.sorting_and_time import * +from files.routes.routehelpers import execute_shadowban_viewers_and_voters +from files.routes.wrappers import * + +from .front import frontlist +from .users import userpagelisting + +from files.__main__ import app, limiter titleheaders = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.72 Safari/537.36"} - @app.post("/club_post/") @feature_required('COUNTRY_CLUB') @auth_required @@ -100,7 +105,7 @@ def publish(pid, v): cache.delete_memoized(frontlist) - cache.delete_memoized(User.userpagelisting) + cache.delete_memoized(userpagelisting) if post.sub == 'changelog': send_changelog_message(post.permalink) @@ -147,6 +152,7 @@ def post_id(pid, anything=None, v=None, sub=None): if post.club and not (v and (v.paid_dues or v.id == post.author_id)): abort(403) if v: + execute_shadowban_viewers_and_voters(v, post) # shadowban check is done in sort_objects # output is needed: see comments.py comments, output = get_comments_v_properties(v, True, None, Comment.parent_submission == post.id, Comment.level < 10) @@ -214,7 +220,7 @@ def post_id(pid, anything=None, v=None, sub=None): g.db.add(post) if v and v.client: - return post.json + return post.json(g.db) template = "submission.html" if (post.is_banned or post.author.shadowbanned) \ @@ -223,7 +229,7 @@ def post_id(pid, anything=None, v=None, sub=None): return render_template(template, v=v, p=post, ids=list(ids), sort=sort, render_replies=True, offset=offset, sub=post.subr, - fart=app.config['SETTINGS']['Fart mode']) + fart=get_setting('Fart mode')) @app.get("/viewmore///") @limiter.limit(DEFAULT_RATELIMIT_SLOWER) @@ -297,7 +303,7 @@ def morecomments(v, cid): comments = output else: c = get_comment(cid) - comments = c.replies(sort=request.values.get('sort'), v=v) + comments = c.replies(sort=request.values.get('sort'), v=v, db=g.db) if comments: p = comments[0].post else: p = None @@ -340,7 +346,7 @@ def edit_post(pid, v): p.title = title p.title_html = title_html - body += process_files() + body += process_files(request.files, v) body = body.strip()[:POST_BODY_LENGTH_LIMIT] # process_files() may be adding stuff to the body if body != p.body: @@ -422,12 +428,8 @@ def edit_post(pid, v): return redirect(p.permalink) -def thumbnail_thread(pid): - - db = db_session() - +def thumbnail_thread(pid:int, db, vid:int): def expand_url(post_url, fragment_url): - if fragment_url.startswith("https://"): return fragment_url elif fragment_url.startswith("https://"): @@ -464,8 +466,6 @@ def thumbnail_thread(pid): if x.status_code != 200: db.close() return - - if x.headers.get("Content-Type","").startswith("text/html"): soup=BeautifulSoup(x.content, 'lxml') @@ -505,7 +505,6 @@ def thumbnail_thread(pid): for url in thumb_candidate_urls: - try: image_req=requests.get(url, headers=headers, timeout=5, proxies=proxies) except: @@ -523,15 +522,11 @@ def thumbnail_thread(pid): with Image.open(BytesIO(image_req.content)) as i: if i.width < 30 or i.height < 30: continue - break - else: db.close() return - - elif x.headers.get("Content-Type","").startswith("image/"): image_req=x with Image.open(BytesIO(x.content)) as i: @@ -550,9 +545,12 @@ def thumbnail_thread(pid): for chunk in image_req.iter_content(1024): file.write(chunk) - post.thumburl = process_image(name, resize=100, uploader=post.author_id, db=db) - db.add(post) - db.commit() + v = db.get(User, vid) + url = process_image(name, v, resize=100, uploader_id=post.author_id, db=db) + if url: + post.thumburl = url + db.add(post) + db.commit() db.close() stdout.flush() return @@ -768,7 +766,7 @@ def submit_post(v, sub=None): choices.append(i.group(1)) body = body.replace(i.group(0), "") - body += process_files() + body += process_files(request.files, v) body = body.strip()[:POST_BODY_LENGTH_LIMIT] # process_files() adds content to the body, so we need to re-strip torture = (v.agendaposter and not v.marseyawarded and sub != 'chudrama') @@ -858,33 +856,28 @@ def submit_post(v, sub=None): ) g.db.add(vote) - if request.files.get('file-url') and request.headers.get("cf-ipcountry") != "T1": - + if request.files.get('file-url') and not g.is_tor: file = request.files['file-url'] if file.content_type.startswith('image/'): name = f'/images/{time.time()}'.replace('.','') + '.webp' file.save(name) - post.url = process_image(name, patron=v.patron) + post.url = process_image(name, v) name2 = name.replace('.webp', 'r.webp') copyfile(name, name2) - post.thumburl = process_image(name2, resize=100) + post.thumburl = process_image(name2, v, resize=100) elif file.content_type.startswith('video/'): - post.url = process_video(file) + post.url = process_video(file, v) elif file.content_type.startswith('audio/'): - post.url = process_audio(file) + post.url = process_audio(file, v) else: abort(415) if not post.thumburl and post.url: - gevent.spawn(thumbnail_thread, post.id) - - - + gevent.spawn(thumbnail_thread, post.id, g.db, v.id) if not post.private and not post.ghost: - notify_users = NOTIFY_USERS(f'{title} {body}', v) if notify_users: @@ -936,7 +929,7 @@ def submit_post(v, sub=None): execute_lawlz_actions(v, post) cache.delete_memoized(frontlist) - cache.delete_memoized(User.userpagelisting) + cache.delete_memoized(userpagelisting) if post.sub == 'changelog' and not post.private: send_changelog_message(post.permalink) @@ -945,7 +938,7 @@ def submit_post(v, sub=None): send_wpd_message(post.permalink) g.db.commit() - if v.client: return post.json + if v.client: return post.json(g.db) else: post.voted = 1 if post.new or 'megathread' in post.title.lower(): sort = 'new' @@ -972,7 +965,7 @@ def delete_post_pid(pid, v): g.db.add(post) cache.delete_memoized(frontlist) - cache.delete_memoized(User.userpagelisting) + cache.delete_memoized(userpagelisting) g.db.flush() v.post_count = g.db.query(Submission).filter_by(author_id=v.id, deleted_utc=0).count() @@ -993,7 +986,7 @@ def undelete_post_pid(pid, v): g.db.add(post) cache.delete_memoized(frontlist) - cache.delete_memoized(User.userpagelisting) + cache.delete_memoized(userpagelisting) g.db.flush() v.post_count = g.db.query(Submission).filter_by(author_id=v.id, deleted_utc=0).count() @@ -1075,7 +1068,7 @@ def pin_post(post_id, v): if v.id != post.author_id: abort(403, "Only the post author can do that!") post.is_pinned = not post.is_pinned g.db.add(post) - cache.delete_memoized(User.userpagelisting) + cache.delete_memoized(userpagelisting) if post.is_pinned: return {"message": "Post pinned!"} else: return {"message": "Post unpinned!"} return abort(404, "Post not found!") diff --git a/files/routes/reporting.py b/files/routes/reporting.py index ca3f80754..fa51de293 100644 --- a/files/routes/reporting.py +++ b/files/routes/reporting.py @@ -1,12 +1,15 @@ -from files.helpers.wrappers import * -from files.helpers.get import * -from files.helpers.alerts import * -from files.helpers.actions import * from flask import g -from files.__main__ import app, limiter, cache -from os import path + +from files.classes.flags import Flag, CommentFlag +from files.classes.mod_logs import ModAction +from files.classes.sub_logs import SubAction +from files.helpers.actions import * +from files.helpers.alerts import * +from files.helpers.get import * from files.helpers.sanitize import filter_emojis_only from files.routes.front import frontlist +from files.routes.wrappers import * +from files.__main__ import app, limiter, cache @app.post("/report/post/") @limiter.limit(DEFAULT_RATELIMIT_SLOWER) diff --git a/files/routes/routehelpers.py b/files/routes/routehelpers.py new file mode 100644 index 000000000..2321cf5f9 --- /dev/null +++ b/files/routes/routehelpers.py @@ -0,0 +1,94 @@ +import time + +from random import randint +from typing import Optional, Union + +from flask import g, session + +from files.classes import Alt, Comment, User, Submission +from files.helpers.const import * +from files.helpers.security import generate_hash, validate_hash + +def get_raw_formkey(u:User): + return f"{session['session_id']}+{u.id}+{u.login_nonce}" + +def get_formkey(u:Optional[User]): + if not u: return "" # if no user exists, give them a blank formkey + return generate_hash(get_raw_formkey(u)) + +def validate_formkey(u:User, formkey:Optional[str]) -> bool: + if not formkey: return False + return validate_hash(get_raw_formkey(u), formkey) + +def check_for_alts(current:User, include_current_session=True): + current_id = current.id + if current_id in (1691,6790,7069,36152) and include_current_session: + session["history"] = [] + return + ids = [x[0] for x in g.db.query(User.id).all()] + past_accs = set(session.get("history", [])) if include_current_session else set() + + def add_alt(user1:int, user2:int): + li = [user1, user2] + existing = g.db.query(Alt).filter(Alt.user1.in_(li), Alt.user2.in_(li)).one_or_none() + if not existing: + new_alt = Alt(user1=user1, user2=user2) + g.db.add(new_alt) + g.db.flush() + + for past_id in list(past_accs): + if past_id not in ids: + past_accs.remove(past_id) + continue + + if past_id == MOM_ID or current_id == MOM_ID: break + if past_id == current_id: continue + + li = [past_id, current_id] + add_alt(past_id, current_id) + other_alts = g.db.query(Alt).filter(Alt.user1.in_(li), Alt.user2.in_(li)).all() + for a in other_alts: + if a.deleted: + if include_current_session: + try: session["history"].remove(a.user1) + except: pass + try: session["history"].remove(a.user2) + except: pass + continue # don't propagate deleted alt links + if a.user1 != past_id: add_alt(a.user1, past_id) + if a.user1 != current_id: add_alt(a.user1, current_id) + if a.user2 != past_id: add_alt(a.user2, past_id) + if a.user2 != current_id: add_alt(a.user2, current_id) + + past_accs.add(current_id) + if include_current_session: + session["history"] = list(past_accs) + g.db.flush() + for u in current.alts_unique: + if u._alt_deleted: continue + if u.shadowbanned: + current.shadowbanned = u.shadowbanned + if not current.is_banned: current.ban_reason = u.ban_reason + g.db.add(current) + elif current.shadowbanned: + u.shadowbanned = current.shadowbanned + if not u.is_banned: u.ban_reason = current.ban_reason + g.db.add(u) + +def execute_shadowban_viewers_and_voters(v:Optional[User], target:Union[Submission, Comment]): + if not v or not v.shadowbanned: return + if not target: return + if v.id != target.author_id: return + if not (86400 > time.time() - target.created_utc > 60): return + ti = max(int((time.time() - target.created_utc)/60), 1) + max_upvotes = min(ti, 13) + rand = randint(0, max_upvotes) + if target.upvotes >= rand: return + + amount = randint(0, 3) + if amount != 1: return + + target.upvotes += amount + if isinstance(target, Submission): + target.views += amount*randint(3, 5) + g.db.add(target) diff --git a/files/routes/search.py b/files/routes/search.py index b44edddbb..16d285e97 100644 --- a/files/routes/search.py +++ b/files/routes/search.py @@ -1,13 +1,14 @@ -from files.helpers.wrappers import * import re -from sqlalchemy import * -from flask import * -from files.__main__ import app -from files.helpers.regex import * -from files.helpers.sorting_and_time import * import time from calendar import timegm +from sqlalchemy import * + +from files.helpers.regex import * +from files.helpers.sorting_and_time import * +from files.routes.wrappers import * +from files.__main__ import app + search_operator_hole = HOLE_NAME valid_params = [ diff --git a/files/routes/settings.py b/files/routes/settings.py index 0eb98e913..85637a5c8 100644 --- a/files/routes/settings.py +++ b/files/routes/settings.py @@ -1,20 +1,28 @@ from __future__ import unicode_literals -from files.helpers.alerts import * -from files.helpers.sanitize import * -from files.helpers.const import * -from files.helpers.regex import * -from files.helpers.actions import * -from files.helpers.useractions import * -from files.helpers.get import * -from files.helpers.security import * -from files.mail import * -from files.__main__ import app, cache, limiter -import youtube_dl -from .front import frontlist + import os -from files.helpers.sanitize import filter_emojis_only from shutil import copyfile + +import pyotp import requests +import youtube_dl + +from files.helpers.actions import * +from files.helpers.alerts import * +from files.helpers.const import * +from files.helpers.get import * +from files.helpers.mail import * +from files.helpers.media import process_files, process_image +from files.helpers.regex import * +from files.helpers.sanitize import * +from files.helpers.sanitize import filter_emojis_only +from files.helpers.security import * +from files.helpers.useractions import * +from files.routes.wrappers import * + +from .front import frontlist +from files.__main__ import app, cache, limiter + @app.get("/settings") @auth_required @@ -220,7 +228,7 @@ def settings_personal_post(v): elif not updated and FEATURES['USERS_PROFILE_BODYTEXT'] and \ (request.values.get("bio") or request.files.get('file')): bio = request.values.get("bio")[:1500] - bio += process_files() + bio += process_files(request.files, v) bio = bio.strip() bio_html = sanitize(bio) @@ -336,6 +344,7 @@ def themecolor(v): @auth_required @ratelimit_user() def gumroad(v): + if GUMROAD_TOKEN == DEFAULT_CONFIG_VALUE: abort(404) if not (v.email and v.is_activated): abort(400, f"You must have a verified email to verify {patron} status and claim your rewards!") @@ -380,7 +389,7 @@ def titlecolor(v): @ratelimit_user() def verifiedcolor(v): if not v.verified: abort(403, "You don't have a checkmark") - return set_color(v, "verifiedcolor", "verifiedcolor") + return set_color(v, "verifiedcolor", request.values.get("verifiedcolor")) @app.post("/settings/security") @limiter.limit(DEFAULT_RATELIMIT_SLOWER) @@ -475,19 +484,19 @@ def settings_log_out_others(v): @auth_required @ratelimit_user() def settings_images_profile(v): - if request.headers.get("cf-ipcountry") == "T1": abort(403, "Image uploads are not allowed through TOR.") + if g.is_tor: abort(403, "Image uploads are not allowed through TOR.") file = request.files["profile"] name = f'/images/{time.time()}'.replace('.','') + '.webp' file.save(name) - highres = process_image(name, patron=v.patron) + highres = process_image(name, v) if not highres: abort(400) name2 = name.replace('.webp', 'r.webp') copyfile(name, name2) - imageurl = process_image(name2, resize=100) + imageurl = process_image(name2, v, resize=100) if not imageurl: abort(400) @@ -511,13 +520,13 @@ def settings_images_profile(v): @auth_required @ratelimit_user() def settings_images_banner(v): - if request.headers.get("cf-ipcountry") == "T1": abort(403, "Image uploads are not allowed through TOR.") + if g.is_tor: abort(403, "Image uploads are not allowed through TOR.") file = request.files["banner"] name = f'/images/{time.time()}'.replace('.','') + '.webp' file.save(name) - bannerurl = process_image(name, patron=v.patron) + bannerurl = process_image(name, v) if bannerurl: if v.bannerurl and '/images/' in v.bannerurl: diff --git a/files/routes/static.py b/files/routes/static.py index 336e9f895..68a5e215b 100644 --- a/files/routes/static.py +++ b/files/routes/static.py @@ -1,15 +1,19 @@ -from files.mail import * -from files.__main__ import app, limiter +import os +from shutil import copyfile + +from sqlalchemy import func, nullslast +from files.helpers.media import process_files + +import files.helpers.stats as statshelper +from files.classes.award import AWARDS +from files.classes.badges import Badge, BadgeDef +from files.classes.mod_logs import ModAction, ACTIONTYPES, ACTIONTYPES2 +from files.classes.userblock import UserBlock +from files.helpers.actions import * from files.helpers.alerts import * from files.helpers.const import * -from files.helpers.actions import * -from files.classes.award import AWARDS -from sqlalchemy import func, nullslast -import os -from files.classes.mod_logs import ACTIONTYPES, ACTIONTYPES2 -from files.classes.badges import BadgeDef -import files.helpers.stats as statshelper -from shutil import move, copyfile +from files.routes.wrappers import * +from files.__main__ import app, cache, limiter @app.get("/r/drama/comments//") @@ -214,7 +218,7 @@ def submit_contact(v): abort(403) body = f'This message has been sent automatically to all admins via [/contact](/contact)\n\nMessage:\n\n' + body - body += process_files() + body += process_files(request.files, v) body = body.strip() body_html = sanitize(body) @@ -408,172 +412,3 @@ if not os.path.exists(f'files/templates/donate_{SITE_NAME}.html'): @auth_desired_with_logingate def donate(v): return render_template(f'donate_{SITE_NAME}.html', v=v) - - -if SITE == 'pcmemes.net': - from files.classes.streamers import * - - id_regex = re.compile('"externalId":"([^"]*?)"', flags=re.A) - live_regex = re.compile('playerOverlayVideoDetailsRenderer":\{"title":\{"simpleText":"(.*?)"\},"subtitle":\{"runs":\[\{"text":"(.*?)"\},\{"text":" • "\},\{"text":"(.*?)"\}', flags=re.A) - live_thumb_regex = re.compile('\{"thumbnail":\{"thumbnails":\[\{"url":"(.*?)"', flags=re.A) - offline_regex = re.compile('","title":"(.*?)".*?"width":48,"height":48\},\{"url":"(.*?)"', flags=re.A) - offline_details_regex = re.compile('simpleText":"Streamed ([0-9]*?) ([^"]*?)"\},.*?"viewCountText":\{"simpleText":"([0-9,]*?) views"', flags=re.A) - - def process_streamer(id, live='live'): - url = f'https://www.youtube.com/channel/{id}/{live}' - req = requests.get(url, cookies={'CONSENT': 'YES+1'}, timeout=5) - text = req.text - if '"videoDetails":{"videoId"' in text: - y = live_regex.search(text) - count = y.group(3) - - if count == '1 watching now': - count = "1" - - if 'waiting' in count: - if live != '': - return process_streamer(id, '') - else: - return None - - count = int(count.replace(',', '')) - - t = live_thumb_regex.search(text) - - thumb = t.group(1) - name = y.group(2) - title = y.group(1) - - return (True, (id, req.url, thumb, name, title, count)) - else: - t = offline_regex.search(text) - if not t: - if live != '': - return process_streamer(id, '') - else: - return None - - y = offline_details_regex.search(text) - - if y: - views = y.group(3).replace(',', '') - quantity = int(y.group(1)) - unit = y.group(2) - - if unit.startswith('second'): - modifier = 1/60 - elif unit.startswith('minute'): - modifier = 1 - elif unit.startswith('hour'): - modifier = 60 - elif unit.startswith('day'): - modifier = 1440 - elif unit.startswith('week'): - modifier = 10080 - elif unit.startswith('month'): - modifier = 43800 - elif unit.startswith('year'): - modifier = 525600 - - minutes = quantity * modifier - - actual = f'{quantity} {unit}' - else: - minutes = 9999999999 - actual = '???' - views = 0 - - thumb = t.group(2) - - name = t.group(1) - - return (False, (id, req.url.rstrip('/live'), thumb, name, minutes, actual, views)) - - - def live_cached(): - live = [] - offline = [] - db = db_session() - streamers = [x[0] for x in db.query(Streamer.id).all()] - db.close() - for id in streamers: - processed = process_streamer(id) - if processed: - if processed[0]: live.append(processed[1]) - else: offline.append(processed[1]) - - live = sorted(live, key=lambda x: x[5], reverse=True) - offline = sorted(offline, key=lambda x: x[4]) - - if live: cache.set('live', live) - if offline: cache.set('offline', offline) - - - @app.get('/live') - @auth_desired_with_logingate - def live_list(v): - live = cache.get('live') or [] - offline = cache.get('offline') or [] - - return render_template('live.html', v=v, live=live, offline=offline) - - @app.post('/live/add') - @admin_level_required(PERMS['STREAMERS_MODERATION']) - def live_add(v): - link = request.values.get('link').strip() - - if 'youtube.com/channel/' in link: - id = link.split('youtube.com/channel/')[1].rstrip('/') - else: - text = requests.get(link, cookies={'CONSENT': 'YES+1'}, timeout=5).text - try: id = id_regex.search(text).group(1) - except: abort(400, "Invalid ID") - - live = cache.get('live') or [] - offline = cache.get('offline') or [] - - if not id or len(id) != 24: - abort(400, "Invalid ID") - - existing = g.db.get(Streamer, id) - if not existing: - streamer = Streamer(id=id) - g.db.add(streamer) - g.db.flush() - if v.id != KIPPY_ID: - send_repeatable_notification(KIPPY_ID, f"@{v.username} (Admin) has added a [new YouTube channel](https://www.youtube.com/channel/{streamer.id})") - - processed = process_streamer(id) - if processed: - if processed[0]: live.append(processed[1]) - else: offline.append(processed[1]) - - live = sorted(live, key=lambda x: x[5], reverse=True) - offline = sorted(offline, key=lambda x: x[4]) - - if live: cache.set('live', live) - if offline: cache.set('offline', offline) - - return redirect('/live') - - @app.post('/live/remove') - @admin_level_required(PERMS['STREAMERS_MODERATION']) - def live_remove(v): - id = request.values.get('id').strip() - if not id: abort(400) - streamer = g.db.get(Streamer, id) - if streamer: - if v.id != KIPPY_ID: - send_repeatable_notification(KIPPY_ID, f"@{v.username} (Admin) has removed a [YouTube channel](https://www.youtube.com/channel/{streamer.id})") - g.db.delete(streamer) - - live = cache.get('live') or [] - offline = cache.get('offline') or [] - - live = [x for x in live if x[0] != id] - offline = [x for x in offline if x[0] != id] - - if live: cache.set('live', live) - if offline: cache.set('offline', offline) - - return redirect('/live') diff --git a/files/routes/streamers.py b/files/routes/streamers.py new file mode 100644 index 000000000..9fd5a051c --- /dev/null +++ b/files/routes/streamers.py @@ -0,0 +1,174 @@ +import re + +import requests + +from files.classes.streamers import Streamer +from files.helpers.alerts import send_repeatable_notification +from files.helpers.const import * +from files.routes.wrappers import * +from files.__main__ import app, cache + +id_regex = re.compile('"externalId":"([^"]*?)"', flags=re.A) +live_regex = re.compile('playerOverlayVideoDetailsRenderer":\{"title":\{"simpleText":"(.*?)"\},"subtitle":\{"runs":\[\{"text":"(.*?)"\},\{"text":" • "\},\{"text":"(.*?)"\}', flags=re.A) +live_thumb_regex = re.compile('\{"thumbnail":\{"thumbnails":\[\{"url":"(.*?)"', flags=re.A) +offline_regex = re.compile('","title":"(.*?)".*?"width":48,"height":48\},\{"url":"(.*?)"', flags=re.A) +offline_details_regex = re.compile('simpleText":"Streamed ([0-9]*?) ([^"]*?)"\},.*?"viewCountText":\{"simpleText":"([0-9,]*?) views"', flags=re.A) + +def process_streamer(id, live='live'): + url = f'https://www.youtube.com/channel/{id}/{live}' + req = requests.get(url, cookies={'CONSENT': 'YES+1'}, timeout=5) + text = req.text + if '"videoDetails":{"videoId"' in text: + y = live_regex.search(text) + count = y.group(3) + + if count == '1 watching now': + count = "1" + + if 'waiting' in count: + if live != '': + return process_streamer(id, '') + else: + return None + + count = int(count.replace(',', '')) + + t = live_thumb_regex.search(text) + + thumb = t.group(1) + name = y.group(2) + title = y.group(1) + + return (True, (id, req.url, thumb, name, title, count)) + else: + t = offline_regex.search(text) + if not t: + if live != '': + return process_streamer(id, '') + else: + return None + + y = offline_details_regex.search(text) + + if y: + views = y.group(3).replace(',', '') + quantity = int(y.group(1)) + unit = y.group(2) + + if unit.startswith('second'): + modifier = 1/60 + elif unit.startswith('minute'): + modifier = 1 + elif unit.startswith('hour'): + modifier = 60 + elif unit.startswith('day'): + modifier = 1440 + elif unit.startswith('week'): + modifier = 10080 + elif unit.startswith('month'): + modifier = 43800 + elif unit.startswith('year'): + modifier = 525600 + + minutes = quantity * modifier + + actual = f'{quantity} {unit}' + else: + minutes = 9999999999 + actual = '???' + views = 0 + + thumb = t.group(2) + + name = t.group(1) + + return (False, (id, req.url.rstrip('/live'), thumb, name, minutes, actual, views)) + + +def live_cached(): + live = [] + offline = [] + db = db_session() + streamers = [x[0] for x in db.query(Streamer.id).all()] + db.close() + for id in streamers: + processed = process_streamer(id) + if processed: + if processed[0]: live.append(processed[1]) + else: offline.append(processed[1]) + + live = sorted(live, key=lambda x: x[5], reverse=True) + offline = sorted(offline, key=lambda x: x[4]) + + if live: cache.set('live', live) + if offline: cache.set('offline', offline) + + +@app.get('/live') +@auth_desired_with_logingate +def live_list(v): + live = cache.get('live') or [] + offline = cache.get('offline') or [] + + return render_template('live.html', v=v, live=live, offline=offline) + +@app.post('/live/add') +@admin_level_required(PERMS['STREAMERS_MODERATION']) +def live_add(v): + link = request.values.get('link').strip() + + if 'youtube.com/channel/' in link: + id = link.split('youtube.com/channel/')[1].rstrip('/') + else: + text = requests.get(link, cookies={'CONSENT': 'YES+1'}, timeout=5).text + try: id = id_regex.search(text).group(1) + except: abort(400, "Invalid ID") + + live = cache.get('live') or [] + offline = cache.get('offline') or [] + + if not id or len(id) != 24: + abort(400, "Invalid ID") + + existing = g.db.get(Streamer, id) + if not existing: + streamer = Streamer(id=id) + g.db.add(streamer) + g.db.flush() + if v.id != KIPPY_ID: + send_repeatable_notification(KIPPY_ID, f"@{v.username} (Admin) has added a [new YouTube channel](https://www.youtube.com/channel/{streamer.id})") + + processed = process_streamer(id) + if processed: + if processed[0]: live.append(processed[1]) + else: offline.append(processed[1]) + + live = sorted(live, key=lambda x: x[5], reverse=True) + offline = sorted(offline, key=lambda x: x[4]) + + if live: cache.set('live', live) + if offline: cache.set('offline', offline) + + return redirect('/live') + +@app.post('/live/remove') +@admin_level_required(PERMS['STREAMERS_MODERATION']) +def live_remove(v): + id = request.values.get('id').strip() + if not id: abort(400) + streamer = g.db.get(Streamer, id) + if streamer: + if v.id != KIPPY_ID: + send_repeatable_notification(KIPPY_ID, f"@{v.username} (Admin) has removed a [YouTube channel](https://www.youtube.com/channel/{streamer.id})") + g.db.delete(streamer) + + live = cache.get('live') or [] + offline = cache.get('offline') or [] + + live = [x for x in live if x[0] != id] + offline = [x for x in offline if x[0] != id] + + if live: cache.set('live', live) + if offline: cache.set('offline', offline) + + return redirect('/live') \ No newline at end of file diff --git a/files/routes/subs.py b/files/routes/subs.py index 00a445ff0..e9a49d41d 100644 --- a/files/routes/subs.py +++ b/files/routes/subs.py @@ -1,12 +1,14 @@ -from files.__main__ import app, limiter +from sqlalchemy import nullslast + +from files.classes import * from files.helpers.alerts import * -from files.helpers.wrappers import * from files.helpers.get import * from files.helpers.regex import * -from files.classes import * +from files.routes.wrappers import * + from .front import frontlist -from sqlalchemy import nullslast -import tldextract +from files.__main__ import app, cache, limiter + @app.post("/exile/post/<pid>") @is_not_permabanned @@ -457,7 +459,7 @@ def get_sub_css(sub): @limiter.limit("1/second;10/day", key_func=lambda:f'{SITE}-{session.get("lo_user")}') @is_not_permabanned def sub_banner(v, sub): - if request.headers.get("cf-ipcountry") == "T1": abort(403, "Image uploads are not allowed through TOR.") + if g.is_tor: abort(403, "Image uploads are not allowed through TOR.") sub = get_sub_by_name(sub) if not v.mods(sub.name): abort(403) @@ -467,7 +469,7 @@ def sub_banner(v, sub): name = f'/images/{time.time()}'.replace('.','') + '.webp' file.save(name) - bannerurl = process_image(name, patron=v.patron, resize=1200) + bannerurl = process_image(name, v, resize=1200) if bannerurl: if sub.bannerurl and '/images/' in sub.bannerurl: @@ -490,7 +492,7 @@ def sub_banner(v, sub): @limiter.limit("1/second;10/day", key_func=lambda:f'{SITE}-{session.get("lo_user")}') @is_not_permabanned def sub_sidebar(v, sub): - if request.headers.get("cf-ipcountry") == "T1": abort(403, "Image uploads are not allowed through TOR.") + if g.is_tor: abort(403, "Image uploads are not allowed through TOR.") sub = get_sub_by_name(sub) if not v.mods(sub.name): abort(403) @@ -499,7 +501,7 @@ def sub_sidebar(v, sub): file = request.files["sidebar"] name = f'/images/{time.time()}'.replace('.','') + '.webp' file.save(name) - sidebarurl = process_image(name, patron=v.patron, resize=400) + sidebarurl = process_image(name, v, resize=400) if sidebarurl: if sub.sidebarurl and '/images/' in sub.sidebarurl: @@ -522,7 +524,7 @@ def sub_sidebar(v, sub): @limiter.limit("1/second;10/day", key_func=lambda:f'{SITE}-{session.get("lo_user")}') @is_not_permabanned def sub_marsey(v, sub): - if request.headers.get("cf-ipcountry") == "T1": abort(403, "Image uploads are not allowed through TOR.") + if g.is_tor: abort(403, "Image uploads are not allowed through TOR.") sub = get_sub_by_name(sub) if not v.mods(sub.name): abort(403) @@ -531,7 +533,7 @@ def sub_marsey(v, sub): file = request.files["marsey"] name = f'/images/{time.time()}'.replace('.','') + '.webp' file.save(name) - marseyurl = process_image(name, patron=v.patron, resize=200) + marseyurl = process_image(name, v, resize=200) if marseyurl: if sub.marseyurl and '/images/' in sub.marseyurl: diff --git a/files/routes/users.py b/files/routes/users.py index 3541c8182..b60ca5d99 100644 --- a/files/routes/users.py +++ b/files/routes/users.py @@ -1,28 +1,30 @@ -from typing import Literal -import qrcode import io -import time -import math -from files.classes.leaderboard import Leaderboard -from files.classes.views import * -from files.classes.transactions import * -from files.helpers.alerts import * -from files.helpers.sanitize import * -from files.helpers.const import * -from files.helpers.sorting_and_time import * -from files.helpers.actions import * -from files.mail import * -from flask import * -from files.__main__ import app, limiter, db_session -import sqlalchemy -from sqlalchemy.orm import aliased -from sqlalchemy import desc -from collections import Counter -import gevent -from sys import stdout -import os import json -from .login import check_for_alts +import math +import time +from collections import Counter +from typing import Literal + +import gevent +import qrcode +from sqlalchemy.orm import aliased + +from files.classes import * +from files.classes.leaderboard import Leaderboard +from files.classes.transactions import * +from files.classes.views import * +from files.helpers.actions import execute_blackjack +from files.helpers.alerts import * +from files.helpers.const import * +from files.helpers.mail import * +from files.helpers.sanitize import * +from files.helpers.sorting_and_time import * +from files.helpers.useractions import badge_grant +from files.routes.routehelpers import check_for_alts +from files.routes.wrappers import * + +from files.__main__ import app, cache, limiter + def upvoters_downvoters(v, username, uid, cls, vote_cls, vote_dir, template, standalone): u = get_user(username, v=v, include_shadowbanned=False) @@ -306,7 +308,7 @@ def transfer_currency(v:User, username:str, currency_name:Literal['coins', 'proc else: raise ValueError(f"Invalid currency '{currency_name}' got when transferring {amount} from {v.id} to {receiver.id}") g.db.add(receiver) - send_repeatable_notification(GIFT_NOTIF_ID, log_message) + if GIFT_NOTIF_ID: send_repeatable_notification(GIFT_NOTIF_ID, log_message) send_repeatable_notification(receiver.id, notif_text) g.db.add(v) return {"message": f"{amount - tax} {friendly_currency_name} have been transferred to @{receiver.username}"} @@ -508,7 +510,7 @@ def messagereply(v): abort(403, f"You're blocked by @{user.username}") if parent.sentto == 2: - body += process_files() + body += process_files(request.files, v) body = body.strip() @@ -544,9 +546,9 @@ def messagereply(v): gevent.spawn(pusher_thread, interests, title, notifbody, url) + top_comment = c.top_comment(g.db) - - if c.top_comment.sentto == 2: + if top_comment.sentto == 2: admins = g.db.query(User.id).filter(User.admin_level >= PERMS['NOTIFICATIONS_MODMAIL'], User.id != v.id) if SITE == 'watchpeopledie.tv': admins = admins.filter(User.id != AEVANN_ID) @@ -560,7 +562,7 @@ def messagereply(v): notif = Notification(comment_id=c.id, user_id=admin) g.db.add(notif) - ids = [c.top_comment.id] + [x.id for x in c.top_comment.replies(sort="old", v=v)] + ids = [top_comment.id] + [x.id for x in top_comment.replies(sort="old", v=v, db=g.db)] notifications = g.db.query(Notification).filter(Notification.comment_id.in_(ids), Notification.user_id.in_(admins)) for n in notifications: g.db.delete(n) @@ -663,6 +665,16 @@ def visitors(v): viewers=sorted(v.viewers, key = lambda x: x.last_view_utc, reverse=True) return render_template("userpage/viewers.html", v=v, viewers=viewers) +@cache.memoize(timeout=86400) +def userpagelisting(user:User, site=None, v=None, page:int=1, sort="new", t="all"): + if user.shadowbanned and not (v and v.can_see_shadowbanned): return [] + posts = g.db.query(Submission.id).filter_by(author_id=user.id, is_pinned=False) + if not (v and (v.admin_level >= PERMS['POST_COMMENT_MODERATION'] or v.id == user.id)): + posts = posts.filter_by(is_banned=False, private=False, ghost=False, deleted_utc=0) + posts = apply_time_filter(t, posts, Submission) + posts = sort_objects(sort, posts, Submission, include_shadowbanned=v and v.can_see_shadowbanned) + posts = posts.offset(PAGE_SIZE * (page - 1)).limit(PAGE_SIZE+1).all() + return [x[0] for x in posts] @app.get("/@<username>") @app.get("/@<username>.json") @@ -701,7 +713,7 @@ def u_username(username, v=None): try: page = max(int(request.values.get("page", 1)), 1) except: page = 1 - ids = u.userpagelisting(site=SITE, v=v, page=page, sort=sort, t=t) + ids = userpagelisting(u, site=SITE, v=v, page=page, sort=sort, t=t) next_exists = (len(ids) > PAGE_SIZE) ids = ids[:PAGE_SIZE] @@ -717,7 +729,7 @@ def u_username(username, v=None): if u.unban_utc: if (v and v.client) or request.path.endswith(".json"): - return {"data": [x.json for x in listing]} + return {"data": [x.json(g.db) for x in listing]} return render_template("userpage.html", unban=u.unban_string, @@ -731,7 +743,7 @@ def u_username(username, v=None): is_following=is_following) if (v and v.client) or request.path.endswith(".json"): - return {"data": [x.json for x in listing]} + return {"data": [x.json(g.db) for x in listing]} return render_template("userpage.html", u=u, @@ -799,7 +811,7 @@ def u_username_comments(username, v=None): listing = get_comments(ids, v=v) if (v and v.client) or request.path.endswith(".json"): - return {"data": [c.json for c in listing]} + return {"data": [c.json(g.db) for c in listing]} return render_template("userpage/comments.html", u=u, v=v, listing=listing, page=page, sort=sort, t=t,next_exists=next_exists, is_following=is_following, standalone=True) @@ -1082,22 +1094,17 @@ kofi_tiers={ def settings_kofi(v): if not (v.email and v.is_activated): abort(400, f"You must have a verified email to verify {patron} status and claim your rewards!") - transaction = g.db.query(Transaction).filter_by(email=v.email).order_by(Transaction.created_utc.desc()).first() - if not transaction: abort(404, "Email not found") - if transaction.claimed: abort(400, f"{patron} rewards already claimed") tier = kofi_tiers[transaction.amount] procoins = procoins_li[tier] - v.procoins += procoins send_repeatable_notification(v.id, f"You have received {procoins} Marseybux! You can use them to buy awards in the [shop](/shop).") - g.db.add(v) if tier > v.patron: @@ -1107,7 +1114,5 @@ def settings_kofi(v): badge_grant(badge_id=20+tier, user=v) transaction.claimed = True - g.db.add(transaction) - return {"message": f"{patron} rewards claimed!"} diff --git a/files/routes/votes.py b/files/routes/votes.py index 257f5749b..157f00701 100644 --- a/files/routes/votes.py +++ b/files/routes/votes.py @@ -1,9 +1,9 @@ -from files.helpers.wrappers import * -from files.helpers.get import * -from files.helpers.const import * from files.classes import * -from flask import * -from files.__main__ import app, limiter, cache +from files.helpers.const import * +from files.helpers.get import * +from files.routes.wrappers import * +from files.__main__ import app, limiter + @app.get("/votes/<link>") @admin_level_required(PERMS['VOTES_VISIBLE']) diff --git a/files/helpers/wrappers.py b/files/routes/wrappers.py similarity index 87% rename from files/helpers/wrappers.py rename to files/routes/wrappers.py index 6817d2d90..a5c135668 100644 --- a/files/helpers/wrappers.py +++ b/files/routes/wrappers.py @@ -1,14 +1,16 @@ -from .get import * -from .alerts import * -from files.helpers.const import * -from files.helpers.get import * -from files.__main__ import db_session, limiter -from flask import g, request -from random import randint -import functools -import user_agents import time +import user_agents +from flask import g, request, session + +from files.classes.clients import ClientAuth +from files.helpers.alerts import * +from files.helpers.const import * +from files.helpers.get import get_account +from files.helpers.settings import get_setting +from files.routes.routehelpers import validate_formkey +from files.__main__ import app, cache, db_session, limiter + def calc_users(v): loggedin = cache.get(f'{SITE}_loggedin') or {} loggedout = cache.get(f'{SITE}_loggedout') or {} @@ -32,7 +34,7 @@ def calc_users(v): def get_logged_in_user(): if hasattr(g, 'v'): return g.v - if not (hasattr(g, 'db') and g.db): g.db = db_session() + if not getattr(g, 'db', None): g.db = db_session() g.desires_auth = True v = None token = request.headers.get("Authorization","").strip() @@ -57,13 +59,12 @@ def get_logged_in_user(): if request.method != "GET": submitted_key = request.values.get("formkey") - if not submitted_key: abort(401) - if not v.validate_formkey(submitted_key): abort(401) + if not validate_formkey(v, submitted_key): abort(401) v.client = None g.is_api_or_xhr = bool((v and v.client) or request.headers.get("xhr")) - if request.method.lower() != "get" and app.config['SETTINGS']['Read-only mode'] and not (v and v.admin_level >= PERMS['SITE_BYPASS_READ_ONLY_MODE']): + if request.method.lower() != "get" and get_setting('Read-only mode') and not (v and v.admin_level >= PERMS['SITE_BYPASS_READ_ONLY_MODE']): abort(403) g.v = v @@ -84,7 +85,6 @@ def get_logged_in_user(): if f'@{v.username}, ' not in f.read(): t = str(time.strftime("%d/%B/%Y %H:%M:%S UTC", time.gmtime(time.time()))) f.write(f'@{v.username}, {v.truescore}, {ip}, {t}\n') - return v def auth_desired(f): @@ -97,7 +97,7 @@ def auth_desired(f): def auth_desired_with_logingate(f): def wrapper(*args, **kwargs): v = get_logged_in_user() - if app.config['SETTINGS']['login_required'] and not v: abort(401) + if get_setting('login_required') and not v: abort(401) if request.path.startswith('/logged_out'): redir = request.full_path.replace('/logged_out','') diff --git a/files/templates/admin/app.html b/files/templates/admin/app.html index c77c4495c..9fb3f6def 100644 --- a/files/templates/admin/app.html +++ b/files/templates/admin/app.html @@ -19,7 +19,7 @@ <div class="body w-lg-100"> <label for="edit-{{app.id}}-author" class="mb-0 w-lg-25">User</label> <input autocomplete="off" id="edit-{{app.id}}-author" class="form-control" type="text" name="name" value="{{app.author.username}}" readonly=readonly> - <input type="hidden" name="formkey" value="{{v.formkey}}"> + <input type="hidden" name="formkey" value="{{v|formkey}}"> <label for="edit-{{app.id}}-name" class="mb-0 w-lg-25">App Name</label> <input autocomplete="off" id="edit-{{app.id}}-name" class="form-control" type="text" name="name" value="{{app.app_name}}" readonly=readonly> diff --git a/files/templates/admin/badge_admin.html b/files/templates/admin/badge_admin.html index 837281074..8c9dbe73f 100644 --- a/files/templates/admin/badge_admin.html +++ b/files/templates/admin/badge_admin.html @@ -40,7 +40,7 @@ {% endif %} <form action="{{form_action}}" method="post"> -<input type="hidden" name="formkey" value="{{v.formkey}}"> +<input type="hidden" name="formkey" value="{{v|formkey}}"> <label for="input-username">Username</label> <input autocomplete="off" id="input-username" class="form-control" type="text" name="username" required> diff --git a/files/templates/admin/banned_domains.html b/files/templates/admin/banned_domains.html index 59da58645..c67dd6921 100644 --- a/files/templates/admin/banned_domains.html +++ b/files/templates/admin/banned_domains.html @@ -38,7 +38,7 @@ <form action="/admin/ban_domain" method="post"> - <input type="hidden" name="formkey" value="{{v.formkey}}"> + <input type="hidden" name="formkey" value="{{v|formkey}}"> <input autocomplete="off" name="domain" placeholder="Enter domain here.." class="form-control" required> <input autocomplete="off" name="reason" placeholder="Enter ban reason here.." oninput="document.getElementById('ban-submit').disabled=false" class="form-control mt-2"> <input autocomplete="off" id="ban-submit" type="submit" onclick="disable(this)" class="btn btn-primary mt-2" value="Ban domain" disabled> diff --git a/files/templates/ban_modal.html b/files/templates/ban_modal.html index 3c88b6d6c..26ee1d832 100644 --- a/files/templates/ban_modal.html +++ b/files/templates/ban_modal.html @@ -11,7 +11,7 @@ <form id="banModalForm"> - <input type="hidden" name="formkey" value="{{v.formkey}}"> + <input type="hidden" name="formkey" value="{{v|formkey}}"> <label for="ban-modal-link">Public ban reason (optional)</label> <textarea autocomplete="off" maxlength="256" name="reason" form="banModalForm" class="form-control" id="ban-modal-link" aria-label="With textarea" placeholder="Enter reason"></textarea> @@ -46,7 +46,7 @@ <div class="modal-body pt-0" id="chud-modal-body"> <form id="chudModalForm"> - <input type="hidden" name="formkey" value="{{v.formkey}}"> + <input type="hidden" name="formkey" value="{{v|formkey}}"> <input type="hidden" name="reason" id="chud-modal-link"> <label for="days">Days</label> diff --git a/files/templates/comments.html b/files/templates/comments.html index 2c7da2c5f..086f29dfd 100644 --- a/files/templates/comments.html +++ b/files/templates/comments.html @@ -21,7 +21,7 @@ {% set score=ups-downs %} {% if render_replies %} - {% set replies=c.replies(sort=sort, v=v) %} + {% set replies=c.replies(sort=sort, v=v, db=g.db) %} {% endif %} {% if c.is_blocking and not c.ghost or (c.is_banned or c.deleted_utc) and not (v and v.admin_level >= PERMS['POST_COMMENT_MODERATION']) and not (v and v.id==c.author_id) %} @@ -271,7 +271,7 @@ {% if v and v.id==c.author_id %} <div id="comment-edit-{{c.id}}" class="d-none comment-write collapsed child"> <form id="comment-edit-form-{{c.id}}" action="/edit_comment/{{c.id}}" method="post" enctype="multipart/form-data"> - <input type="hidden" name="formkey" value="{{v.formkey}}"> + <input type="hidden" name="formkey" value="{{v|formkey}}"> <textarea autocomplete="off" {% if v.longpost %}minlength="280"{% endif %} maxlength="{% if v.bird %}140{% else %}10000{% endif %}" data-preview="preview-edit-{{c.id}}" oninput="markdown(this);charLimit('comment-edit-body-{{c.id}}','charcount-edit-{{c.id}}')" id="comment-edit-body-{{c.id}}" data-id="{{c.id}}" name="body" form="comment-edit-form-{{c.id}}" class="comment-box form-control rounded" aria-label="With textarea" placeholder="Add your comment..." rows="3">{{c.body}}</textarea> <div class="text-small font-weight-bold mt-1" id="charcount-edit-{{c.id}}" style="right: 1rem; bottom: 0.5rem; z-index: 3;"></div> @@ -284,7 +284,7 @@ <label class="btn btn-secondary format m-0" for="file-edit-reply-{{c.id}}"> <div id="filename-edit-reply-{{c.id}}"><i class="fas fa-file"></i></div> - <input autocomplete="off" id="file-edit-reply-{{c.id}}" accept="image/*, video/*, audio/*" type="file" multiple="multiple" name="file" {% if request.headers.get('cf-ipcountry')=="T1" %}disabled{% endif %} onchange="changename('filename-edit-reply-{{c.id}}','file-edit-reply-{{c.id}}')" hidden> + <input autocomplete="off" id="file-edit-reply-{{c.id}}" accept="image/*, video/*, audio/*" type="file" multiple="multiple" name="file" {% if g.is_tor %}disabled{% endif %} onchange="changename('filename-edit-reply-{{c.id}}','file-edit-reply-{{c.id}}')" hidden> </label> </div> <button type="button" id="edit-btn-{{c.id}}" form="comment-edit-form-{{c.id}}" class="btn btn-primary ml-2 fl-r commentmob" onclick="comment_edit('{{c.id}}');remove_dialog()">Save Edit</button> @@ -515,7 +515,7 @@ <div id="reply-to-{{c.id}}" class="d-none"> <div id="comment-form-space-{{c.fullname}}" class="comment-write collapsed child"> <form id="reply-to-c_{{c.id}}" action="/comment" method="post" enctype="multipart/form-data"> - <input type="hidden" name="formkey" value="{{v.formkey}}"> + <input type="hidden" name="formkey" value="{{v|formkey}}"> <input type="hidden" name="parent_fullname" value="{{c.fullname}}"> <input autocomplete="off" id="reply-form-submission-{{c.fullname}}" type="hidden" name="submission" value="{{c.post.id}}"> <textarea required autocomplete="off" {% if v.longpost %}minlength="280"{% else %}minlength="1"{% endif %} maxlength="{% if v.bird %}140{% else %}10000{% endif %}" data-preview="form-preview-{{c.fullname}}" oninput="markdown(this);charLimit('reply-form-body-{{c.fullname}}','charcount-{{c.id}}')" id="reply-form-body-{{c.fullname}}" data-fullname="{{c.fullname}}" name="body" form="reply-to-c_{{c.id}}" class="comment-box form-control rounded" aria-label="With textarea" placeholder="Add your comment..." rows="3"></textarea> @@ -533,7 +533,7 @@   <label class="btn btn-secondary format m-0" for="file-upload-reply-{{c.fullname}}"> <div id="filename-show-reply-{{c.fullname}}"><i class="fas fa-file"></i></div> - <input autocomplete="off" id="file-upload-reply-{{c.fullname}}" accept="image/*, video/*, audio/*" type="file" multiple="multiple" name="file" {% if request.headers.get('cf-ipcountry')=="T1" %}disabled{% endif %} onchange="changename('filename-show-reply-{{c.fullname}}','file-upload-reply-{{c.fullname}}')" hidden> + <input autocomplete="off" id="file-upload-reply-{{c.fullname}}" accept="image/*, video/*, audio/*" type="file" multiple="multiple" name="file" {% if g.is_tor %}disabled{% endif %} onchange="changename('filename-show-reply-{{c.fullname}}','file-upload-reply-{{c.fullname}}')" hidden> </label> </div> <button type="button" id="save-reply-to-{{c.fullname}}" class="btn btn-primary ml-2 fl-r commentmob" onclick="post_comment('{{c.fullname}}', 'reply-to-{{c.id}}');remove_dialog()">Comment</button> @@ -576,7 +576,7 @@ <div id="reply-message-{{c.id}}" class="d-none"> <div id="comment-form-space-{{c.id}}" class="comment-write collapsed child"> <form id="reply-to-message-{{c.id}}" action="/reply" method="post" class="input-group" enctype="multipart/form-data"> - <input type="hidden" name="formkey" value="{{v.formkey}}"> + <input type="hidden" name="formkey" value="{{v|formkey}}"> <textarea required autocomplete="off" minlength="1" maxlength="10000" name="body" form="reply-to-c_{{c.id}}" data-id="{{c.id}}" class="comment-box form-control rounded" id="reply-form-body-{{c.id}}" aria-label="With textarea" rows="3" data-preview="message-reply-{{c.id}}" oninput="markdown(this)"></textarea> <div class="comment-format" id="comment-format-bar-{{c.id}}"> <div onclick="loadEmojis('reply-form-body-{{c.id}}')" class="btn btn-secondary m-0 mt-3 mr-1" aria-hidden="true" data-bs-toggle="modal" data-bs-target="#emojiModal" data-bs-toggle="tooltip" data-bs-placement="bottom" title="Add Emoji"><i class="fas fa-smile-beam"></i></div> @@ -584,7 +584,7 @@ {% if c.sentto == 2 %} <label class="btn btn-secondary m-0 mt-3" for="file-upload"> <div id="filename"><i class="fas fa-file"></i></div> - <input autocomplete="off" id="file-upload" accept="image/*, video/*, audio/*" type="file" name="file" multiple="multiple" {% if request.headers.get('cf-ipcountry')=="T1" %}disabled{% endif %} onchange="changename('filename','file-upload')" hidden> + <input autocomplete="off" id="file-upload" accept="image/*, video/*, audio/*" type="file" name="file" multiple="multiple" {% if g.is_tor %}disabled{% endif %} onchange="changename('filename','file-upload')" hidden> </label> {% endif %} </div> diff --git a/files/templates/contact.html b/files/templates/contact.html index 65b6f27cc..567fce652 100644 --- a/files/templates/contact.html +++ b/files/templates/contact.html @@ -1,12 +1,8 @@ {% extends "default.html" %} - {% block title %} <title>{{SITE_NAME}} - Contact - {% endblock %} - {% block content %} - {% if msg %}