From 129d644a3f08e1c802ce638bdc1445bc9481d520 Mon Sep 17 00:00:00 2001 From: TLSM Date: Mon, 5 Sep 2022 00:41:17 -0400 Subject: [PATCH] Add active user counter logic; add to admin tools. Ports in lightly modified logic from the upstream which tracks active sessions to provide counters and listings to understand site traffic in the admin panel. --- files/__main__.py | 26 +++++++----- files/helpers/const.py | 2 + files/helpers/wrappers.py | 60 ++++++++++++++++++--------- files/routes/admin.py | 19 +++++++++ files/templates/admin/admin_home.html | 8 +++- files/templates/admin/loggedin.html | 21 ++++++++++ files/templates/admin/loggedout.html | 19 +++++++++ requirements.txt | 1 + 8 files changed, 125 insertions(+), 31 deletions(-) create mode 100644 files/templates/admin/loggedin.html create mode 100644 files/templates/admin/loggedout.html diff --git a/files/__main__.py b/files/__main__.py index 2ce76c7d1..0daa072f9 100644 --- a/files/__main__.py +++ b/files/__main__.py @@ -88,25 +88,29 @@ mail = Mail(app) @app.before_request def before_request(): - with open('site_settings.json', 'r') as f: app.config['SETTINGS'] = json.load(f) - if request.host != app.config["SERVER_NAME"]: return {"error":"Unauthorized host provided."}, 401 - if request.headers.get("CF-Worker"): return {"error":"Cloudflare workers are not allowed to access this website."}, 401 + if request.host != app.config["SERVER_NAME"]: + return {"error": "Unauthorized host provided."}, 401 - if not app.config['SETTINGS']['Bots'] and request.headers.get("Authorization"): abort(503) + if not app.config['SETTINGS']['Bots'] and request.headers.get("Authorization"): + abort(503) g.db = db_session() - ua = request.headers.get("User-Agent","").lower() - - if '; wv) ' in ua: g.webview = True - else: g.webview = False - - if 'iphone' in ua or 'ipad' in ua or 'ipod' in ua or 'mac os' in ua or ' firefox/' in ua: g.inferior_browser = True - else: g.inferior_browser = False + g.agent = request.headers.get("User-Agent") + if not g.agent: + return 'Please use a "User-Agent" header!', 403 + ua = g.agent.lower() + 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.timestamp = int(time.time()) diff --git a/files/helpers/const.py b/files/helpers/const.py index b6b217597..7878132f2 100644 --- a/files/helpers/const.py +++ b/files/helpers/const.py @@ -72,6 +72,8 @@ PUSHER_KEY = environ.get("PUSHER_KEY", "").strip() DEFAULT_COLOR = environ.get("DEFAULT_COLOR", "fff").strip() COLORS = {'ff66ac','805ad5','62ca56','38a169','80ffff','2a96f3','eb4963','ff0000','f39731','30409f','3e98a7','e4432d','7b9ae4','ec72de','7f8fa6', 'f8db58','8cdbe6', DEFAULT_COLOR} +LOGGEDIN_ACTIVE_TIME = 15 * 60 + AWARDS = { "ghost": { "kind": "ghost", diff --git a/files/helpers/wrappers.py b/files/helpers/wrappers.py index 8800c37a8..7875061cc 100644 --- a/files/helpers/wrappers.py +++ b/files/helpers/wrappers.py @@ -3,13 +3,19 @@ from .alerts import * from files.helpers.const import * from files.__main__ import db_session from random import randint +import user_agents +import time def get_logged_in_user(): - if not (hasattr(g, 'db') and g.db): g.db = db_session() + if hasattr(g, 'v'): + return g.v + + if not (hasattr(g, 'db') and g.db): + g.db = db_session() v = None - token = request.headers.get("Authorization","").strip() + token = request.headers.get("Authorization", "").strip() if token: client = g.db.query(ClientAuth).filter(ClientAuth.access_token == token).one_or_none() if client: @@ -19,7 +25,7 @@ def get_logged_in_user(): lo_user = session.get("lo_user") if lo_user: id = int(lo_user) - v = g.db.query(User).filter_by(id=id).one_or_none() + v = g.db.query(User).get(id) if v: nonce = session.get("login_nonce", 0) if nonce < v.login_nonce or v.id != id: abort(401) @@ -35,14 +41,40 @@ def get_logged_in_user(): v.client = None - - if request.method.lower() != "get" and app.config['SETTINGS']['Read-only mode'] and not (v and v.admin_level): + if request.method.lower() != "get" \ + and app.config['SETTINGS']['Read-only mode'] \ + and not (v and v.admin_level): abort(403) - if v and v.patron: - if request.content_length and request.content_length > 16 * 1024 * 1024: abort(413) - elif request.content_length and request.content_length > 8 * 1024 * 1024: abort(413) + if request.content_length and request.content_length > 8 * 1024 * 1024: + abort(413) + if not session.get("session_id"): + session.permanent = True + session["session_id"] = secrets.token_hex(49) + + # Active User Counters + loggedin = cache.get(f'{SITE}_loggedin') or {} + loggedout = cache.get(f'{SITE}_loggedout') or {} + + timestamp = int(time.time()) + if v: + if session["session_id"] in loggedout: + del loggedout[session["session_id"]] + loggedin[v.id] = timestamp + else: + ua = str(user_agents.parse(g.agent)) + if 'spider' not in ua.lower() and 'bot' not in ua.lower(): + loggedout[session["session_id"]] = (timestamp, ua) + + g.loggedin_counter = len([x for x in loggedin.values() \ + if (timestamp - x) < LOGGEDIN_ACTIVE_TIME]) + g.loggedout_counter = len([x for x in loggedout.values() \ + if (timestamp - x[0]) < LOGGEDIN_ACTIVE_TIME]) + cache.set(f'{SITE}_loggedin', loggedin) + cache.set(f'{SITE}_loggedout', loggedout) + + g.v = v return v def check_ban_evade(v): @@ -53,12 +85,10 @@ def check_ban_evade(v): def auth_desired(f): def wrapper(*args, **kwargs): - v = get_logged_in_user() check_ban_evade(v) - g.v = v return make_response(f(*args, v=v, **kwargs)) wrapper.__name__ = f.__name__ @@ -68,13 +98,11 @@ def auth_desired(f): def auth_required(f): def wrapper(*args, **kwargs): - v = get_logged_in_user() if not v: abort(401) check_ban_evade(v) - g.v = v return make_response(f(*args, v=v, **kwargs)) wrapper.__name__ = f.__name__ @@ -84,9 +112,7 @@ def auth_required(f): def is_not_permabanned(f): def wrapper(*args, **kwargs): - v = get_logged_in_user() - if not v: abort(401) check_ban_evade(v) @@ -94,7 +120,6 @@ def is_not_permabanned(f): if v.is_suspended_permanently: return {"error": "Forbidden: you are permabanned."}, 403 - g.v = v return make_response(f(*args, v=v, **kwargs)) wrapper.__name__ = f.__name__ @@ -106,14 +131,11 @@ def admin_level_required(x): def wrapper_maker(f): def wrapper(*args, **kwargs): - v = get_logged_in_user() - if not v: abort(401) if v.admin_level < x: abort(403) - - g.v = v + return make_response(f(*args, v=v, **kwargs)) wrapper.__name__ = f.__name__ diff --git a/files/routes/admin.py b/files/routes/admin.py index 25b521bb2..b0ed86d51 100644 --- a/files/routes/admin.py +++ b/files/routes/admin.py @@ -764,6 +764,25 @@ def users_list(v): page=page, ) + +@app.get('/admin/loggedin') +@admin_level_required(2) +def loggedin_list(v): + ids = [x for x, val in cache.get(f'{SITE}_loggedin').items() \ + if (time.time() - val) < LOGGEDIN_ACTIVE_TIME] + users = g.db.query(User).filter(User.id.in_(ids)) \ + .order_by(User.admin_level.desc(), User.truecoins.desc()).all() + return render_template("admin/loggedin.html", v=v, users=users) + + +@app.get('/admin/loggedout') +@admin_level_required(2) +def loggedout_list(v): + users = sorted([val[1] for x, val in cache.get(f'{SITE}_loggedout').items() \ + if (time.time() - val[0]) < LOGGEDIN_ACTIVE_TIME]) + return render_template("admin/loggedout.html", v=v, users=users) + + @app.get("/admin/alt_votes") @admin_level_required(2) def alt_votes_get(v): diff --git a/files/templates/admin/admin_home.html b/files/templates/admin/admin_home.html index 34f83c4f5..17901e7be 100644 --- a/files/templates/admin/admin_home.html +++ b/files/templates/admin/admin_home.html @@ -8,7 +8,13 @@ {% block content %}

 

-

 Admin Tools

+

Admin Tools

+ +
+ Users Here Now: {{g.loggedin_counter + g.loggedout_counter}} — + {{g.loggedin_counter}} Logged-In | + {{g.loggedout_counter}} Logged-Out +

Content