227 lines
6.8 KiB
Python
227 lines
6.8 KiB
Python
'''
|
|
Main entry point for the application. Global state among other things are
|
|
stored here.
|
|
'''
|
|
|
|
import gevent.monkey
|
|
|
|
gevent.monkey.patch_all()
|
|
|
|
# ^ special case: in general imports should go
|
|
# stdlib - externals - internals, but gevent does monkey patching for stdlib
|
|
# functions so we want to monkey patch before importing other things
|
|
|
|
import faulthandler
|
|
from os import environ
|
|
from pathlib import Path
|
|
|
|
import flask
|
|
import flask_caching
|
|
import flask_compress
|
|
import flask_limiter
|
|
import flask_mail
|
|
import flask_profiler
|
|
import gevent
|
|
import redis
|
|
from sqlalchemy.engine import Engine, create_engine
|
|
from sqlalchemy.orm import scoped_session, sessionmaker
|
|
from flask_assets import Environment, Bundle
|
|
|
|
from files.helpers.config.const import Service
|
|
from files.helpers.strings import bool_from_string
|
|
|
|
# first, let's parse arguments to find out what type of instance this is...
|
|
|
|
service:Service = Service.from_argv()
|
|
|
|
# ...and then let's create our flask app...
|
|
|
|
app = flask.app.Flask(__name__, template_folder='templates')
|
|
app.url_map.strict_slashes = False
|
|
app.jinja_env.cache = {}
|
|
app.jinja_env.auto_reload = True
|
|
|
|
# set up assets
|
|
assets = Environment(app)
|
|
assets.from_yaml('files/bundles.yaml')
|
|
|
|
faulthandler.enable()
|
|
|
|
# ...then check that debug mode was not accidentally enabled...
|
|
|
|
if bool_from_string(environ.get("ENFORCE_PRODUCTION", True)) and app.debug:
|
|
raise ValueError("Debug mode is not allowed! If this is a dev environment, please set ENFORCE_PRODUCTION to false")
|
|
|
|
# ...and then attempt to load a .env file if the environment is not configured...
|
|
|
|
if environ.get("SITE_ID") is None:
|
|
from dotenv import load_dotenv
|
|
load_dotenv(dotenv_path=Path("bootstrap/site_env"))
|
|
load_dotenv(dotenv_path=Path("env"), override=True)
|
|
|
|
# ...and let's add the flask profiler if it's enabled...
|
|
|
|
if environ.get("FLASK_PROFILER_ENDPOINT"):
|
|
app.config["flask_profiler"] = {
|
|
"enabled": True,
|
|
"storage": {
|
|
"engine": "sqlalchemy",
|
|
},
|
|
"basicAuth": {
|
|
"enabled": True,
|
|
"username": environ.get("FLASK_PROFILER_USERNAME"),
|
|
"password": environ.get("FLASK_PROFILER_PASSWORD"),
|
|
},
|
|
"endpointRoot": environ.get("FLASK_PROFILER_ENDPOINT"),
|
|
}
|
|
|
|
profiler = flask_profiler.Profiler()
|
|
profiler.init_app(app)
|
|
|
|
# ...and then let's set up the easy_profile analysis if it's enabled...
|
|
|
|
if bool_from_string(environ.get('DBG_SQL_ANALYSIS', False)):
|
|
try:
|
|
import inspect as inspectlib
|
|
import linecache
|
|
|
|
from easy_profile import EasyProfileMiddleware
|
|
from jinja2.utils import internal_code
|
|
|
|
def jinja_unmangle_stacktrace():
|
|
rewritten_frames = []
|
|
|
|
for record in inspectlib.stack():
|
|
# Skip jinja internalcode frames
|
|
if record.frame.f_code in internal_code:
|
|
continue
|
|
|
|
filename = record.frame.f_code.co_filename
|
|
lineno = record.frame.f_lineno
|
|
name = record.frame.f_code.co_name
|
|
|
|
template = record.frame.f_globals.get("__jinja_template__")
|
|
if template is not None:
|
|
lineno = template.get_corresponding_lineno(lineno)
|
|
|
|
line = linecache.getline(filename, lineno).strip()
|
|
|
|
rewritten_frames.append(f' File "{filename}", line {lineno}, {name}\n {line}\n')
|
|
|
|
return "".join(rewritten_frames)
|
|
|
|
app.wsgi_app = EasyProfileMiddleware(
|
|
app.wsgi_app,
|
|
stack_callback = jinja_unmangle_stacktrace)
|
|
except ModuleNotFoundError:
|
|
# failed to import, just keep on going
|
|
pass
|
|
|
|
# ...and let's load up app config...
|
|
|
|
from files.helpers.config.const import (DEFAULT_THEME, MAX_CONTENT_LENGTH,
|
|
PERMANENT_SESSION_LIFETIME,
|
|
SESSION_COOKIE_SAMESITE)
|
|
from files.helpers.config.environment import *
|
|
|
|
app.config.update({
|
|
"SITE_ID": SITE_ID,
|
|
"SITE_TITLE": SITE_TITLE,
|
|
"SQLALCHEMY_TRACK_MODIFICATIONS": SQLALCHEMY_TRACK_MODIFICATIONS,
|
|
"DATABASE_URL": DATABASE_URL,
|
|
"SECRET_KEY": SECRET_KEY,
|
|
"SERVER_NAME": SERVER_NAME,
|
|
"SEND_FILE_MAX_AGE_DEFAULT": 0 if app.debug else 3153600,
|
|
"SESSION_COOKIE_NAME": f'session_{SITE_ID.lower()}',
|
|
"VERSION": "1.0.0",
|
|
"MAX_CONTENT_LENGTH": MAX_CONTENT_LENGTH,
|
|
"SESSION_COOKIE_SECURE": SESSION_COOKIE_SECURE,
|
|
"SESSION_COOKIE_SAMESITE": SESSION_COOKIE_SAMESITE,
|
|
"PERMANENT_SESSION_LIFETIME": PERMANENT_SESSION_LIFETIME,
|
|
"DEFAULT_COLOR": DEFAULT_COLOR,
|
|
"DEFAULT_THEME": DEFAULT_THEME,
|
|
"FORCE_HTTPS": 1,
|
|
"UserAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.107 Safari/537.36",
|
|
"HCAPTCHA_SITEKEY": HCAPTCHA_SITEKEY,
|
|
"HCAPTCHA_SECRET": HCAPTCHA_SECRET,
|
|
"SPAM_SIMILARITY_THRESHOLD": SPAM_SIMILARITY_THRESHOLD,
|
|
"SPAM_URL_SIMILARITY_THRESHOLD": SPAM_URL_SIMILARITY_THRESHOLD,
|
|
"SPAM_SIMILAR_COUNT_THRESHOLD": SPAM_SIMILAR_COUNT_THRESHOLD,
|
|
"COMMENT_SPAM_SIMILAR_THRESHOLD": COMMENT_SPAM_SIMILAR_THRESHOLD,
|
|
"COMMENT_SPAM_COUNT_THRESHOLD": COMMENT_SPAM_COUNT_THRESHOLD,
|
|
"CACHE_TYPE": "RedisCache",
|
|
"CACHE_REDIS_URL": CACHE_REDIS_URL,
|
|
"MAIL_SERVER": MAIL_SERVER,
|
|
"MAIL_PORT": MAIL_PORT,
|
|
"MAIL_USE_TLS": MAIL_USE_TLS,
|
|
"DESCRIPTION": DESCRIPTION,
|
|
"MAIL_USERNAME": MAIL_USERNAME,
|
|
"MAIL_PASSWORD": MAIL_PASSWORD,
|
|
"DESCRIPTION": DESCRIPTION,
|
|
"SETTINGS": {},
|
|
"SQLALCHEMY_DATABASE_URI": DATABASE_URL,
|
|
"MENTION_LIMIT": MENTION_LIMIT,
|
|
"MULTIMEDIA_EMBEDDING_ENABLED": MULTIMEDIA_EMBEDDING_ENABLED,
|
|
"RESULTS_PER_PAGE_COMMENTS": RESULTS_PER_PAGE_COMMENTS,
|
|
"SCORE_HIDING_TIME_HOURS": SCORE_HIDING_TIME_HOURS,
|
|
"ENABLE_SERVICES": ENABLE_SERVICES,
|
|
"RATE_LIMITER_ENABLED": RATE_LIMITER_ENABLED,
|
|
|
|
"DBG_VOLUNTEER_PERMISSIVE": DBG_VOLUNTEER_PERMISSIVE,
|
|
"VOLUNTEER_JANITOR_ENABLE": VOLUNTEER_JANITOR_ENABLE,
|
|
})
|
|
|
|
# ...and then let's load redis so that...
|
|
|
|
r = redis.Redis(
|
|
host=CACHE_REDIS_URL,
|
|
decode_responses=True,
|
|
ssl_cert_reqs=None
|
|
)
|
|
|
|
# ...we can configure our ratelimiter...
|
|
|
|
def get_remote_addr():
|
|
with app.app_context():
|
|
return request.headers.get('X-Real-IP', default='127.0.0.1')
|
|
|
|
if service.enable_services and not RATE_LIMITER_ENABLED:
|
|
print("Rate limiter disabled in debug mode!")
|
|
|
|
limiter = flask_limiter.Limiter(
|
|
key_func=get_remote_addr,
|
|
app=app,
|
|
default_limits=["3/second;30/minute;200/hour;1000/day"],
|
|
application_limits=["10/second;200/minute;5000/hour;10000/day"],
|
|
storage_uri=CACHE_REDIS_URL,
|
|
auto_check=False,
|
|
enabled=RATE_LIMITER_ENABLED,
|
|
)
|
|
|
|
# ...and then after that we can load the database.
|
|
|
|
engine: Engine = create_engine(DATABASE_URL)
|
|
db_session_factory: sessionmaker = sessionmaker(
|
|
bind=engine,
|
|
autoflush=False,
|
|
future=True,
|
|
)
|
|
db_session: scoped_session = scoped_session(db_session_factory)
|
|
|
|
# now that we've that, let's add the cache, compression, and mail extensions to our app...
|
|
|
|
cache = flask_caching.Cache(app)
|
|
flask_compress.Compress(app)
|
|
mail = flask_mail.Mail(app)
|
|
|
|
# ...and then import the before and after request handlers if this we will import routes.
|
|
|
|
if service.enable_services:
|
|
from files.routes.allroutes import *
|
|
|
|
# setup is done. let's conditionally import the rest of the routes.
|
|
|
|
if service == Service.THEMOTTE:
|
|
from files.routes import *
|
|
elif service == Service.CHAT:
|
|
from files.routes.chat import *
|