From 0e6b144ed7d5581e80504eb2ac8fcd690efac0f7 Mon Sep 17 00:00:00 2001 From: Aevann Date: Thu, 22 Dec 2022 22:03:40 +0200 Subject: [PATCH] alt graph --- files/classes/user.py | 81 +++++++++++++--------------- files/helpers/actions.py | 2 +- files/routes/admin.py | 70 +++--------------------- files/routes/routehelpers.py | 5 +- files/templates/admin/alt_votes.html | 2 +- files/templates/userpage/banner.html | 4 +- 6 files changed, 47 insertions(+), 117 deletions(-) diff --git a/files/classes/user.py b/files/classes/user.py index 0f170c7e2..76171d87b 100644 --- a/files/classes/user.py +++ b/files/classes/user.py @@ -1,11 +1,11 @@ import random from operator import * -from typing import Any, Union +from typing import Callable, Union import pyotp from sqlalchemy import Column, ForeignKey -from sqlalchemy.orm import aliased, deferred -from sqlalchemy.sql import func +from sqlalchemy.orm import aliased, deferred, Query +from sqlalchemy.sql import case, func, literal from sqlalchemy.sql.expression import not_, and_, or_ from sqlalchemy.sql.sqltypes import * @@ -470,19 +470,43 @@ class User(Base): def age(self): return int(time.time()) - self.created_utc - @property @lazy - def alts_unique(self): - alts = [] - for u in self.alts: - if u not in alts: alts.append(u) - return alts + def get_alt_graph(self, db:scoped_session, alt_filter:Optional[Callable[[Query], Query]]=None, **kwargs) -> Query: + ''' + Gets the full graph of alts (optionally filtering `Alt` objects by criteria using a callable, + such as by a date to only get alts from a certain date) as a query of users that can be filtered + further. This function filters alts marked as deleted by default, pass `include_deleted=True` to + disable this behavior and include delinked alts. + ''' + if not alt_filter: + alt_filter = lambda q:q + + if not kwargs.get('include_deleted', False): + deleted_filter = lambda q:q.filter(Alt.deleted == False) + else: + deleted_filter = lambda q:q + + combined_filter = lambda q:deleted_filter(alt_filter(q)) + + alt_graph_cte = db.query(literal(self.id).label('user_id')).select_from(Alt).cte('alt_graph', recursive=True) + + alt_graph_cte_inner = combined_filter(db.query( + case( + (Alt.user1 == alt_graph_cte.c.user_id, Alt.user2), + (Alt.user2 == alt_graph_cte.c.user_id, Alt.user1), + ) + ).select_from(Alt, alt_graph_cte).filter( + or_(alt_graph_cte.c.user_id == Alt.user1, alt_graph_cte.c.user_id == Alt.user2) + )) + + alt_graph_cte = alt_graph_cte.union(alt_graph_cte_inner) + return db.query(User).filter(User.id == alt_graph_cte.c.user_id) @property @lazy def alts_patron(self): - for u in self.alts_unique: - if u.patron: return True + for u in self.get_alt_graph(g.db): + if not u._deleted and u.patron: return True return False @property @@ -724,43 +748,10 @@ class User(Base): def do_reddit(self): return self.notifications_count == self.reddit_notifications_count - @property - @lazy - def alts(self): - subq = g.db.query(Alt).filter( - or_( - Alt.user1 == self.id, - Alt.user2 == self.id - ) - ).subquery() - - data = g.db.query( - User, - aliased(Alt, alias=subq) - ).join( - subq, - or_( - subq.c.user1 == User.id, - subq.c.user2 == User.id - ) - ).filter( - User.id != self.id - ).order_by(User.username).all() - - output = [] - for x in data: - user = x[0] - user._is_manual = x[1].is_manual - user._alt_deleted = x[1].deleted - user._alt_created_utc = x[1].created_utc - output.append(user) - - return output - @property @lazy def alt_ids(self): - return [x.id for x in self.alts if not x._alt_deleted] + return [x.id for x in self.get_alt_graph(g.db) if not x._alt_deleted] @property @lazy diff --git a/files/helpers/actions.py b/files/helpers/actions.py index 369c34238..30f298793 100644 --- a/files/helpers/actions.py +++ b/files/helpers/actions.py @@ -458,7 +458,7 @@ def execute_antispam_comment_check(body:str, v:User): g.db.commit() abort(403, "Too much spam!") -def execute_under_siege(v:User, target:Optional[Union[Submission, Comment]], body, type:str): +def execute_under_siege(v:User, target:Optional[Union[Submission, Comment]], body, type:str) -> bool: if not get_setting("under_siege"): return True if v.age < UNDER_SIEGE_AGE_THRESHOLD and not v.admin_level >= PERMS['SITE_BYPASS_UNDER_SIEGE_MODE']: v.shadowbanned = AUTOJANNY_ID diff --git a/files/routes/admin.py b/files/routes/admin.py index 3383ca815..b4f039a0b 100644 --- a/files/routes/admin.py +++ b/files/routes/admin.py @@ -95,67 +95,15 @@ def merge(v:User, id1, id2): return redirect(user1.url) -@app.get('/admin/merge_all/') -@admin_level_required(PERMS['USER_MERGE']) -def merge_all(v:User, id): - if v.id != AEVANN_ID: abort(403) - - if time.time() - session.get('verified', 0) > 3: - session.pop("lo_user", None) - path = request.path - qs = urlencode(dict(request.values)) - argval = quote(f"{path}?{qs}", safe='') - return redirect(f"/login?redirect={argval}") - - user = get_account(id) - - alt_ids = [x.id for x in user.alts_unique] - - things = g.db.query(AwardRelationship).filter(AwardRelationship.user_id.in_(alt_ids)).all() + g.db.query(Mod).filter(Mod.user_id.in_(alt_ids)).all() + g.db.query(Exile).filter(Exile.user_id.in_(alt_ids)).all() - for thing in things: - thing.user_id = user.id - g.db.add(thing) - - things = g.db.query(Submission).filter(Submission.author_id.in_(alt_ids)).all() + g.db.query(Comment).filter(Comment.author_id.in_(alt_ids)).all() - for thing in things: - thing.author_id = user.id - g.db.add(thing) - - - badges = g.db.query(Badge).filter(Badge.user_id.in_(alt_ids)).all() - for badge in badges: - if not user.has_badge(badge.badge_id): - badge.user_id = user.id - g.db.add(badge) - g.db.flush() - - for alt in user.alts_unique: - for kind in ('comment_count', 'post_count', 'winnings', 'received_award_count', 'coins_spent', 'lootboxes_bought', 'coins', 'truescore', 'marseybux'): - amount = getattr(user, kind) + getattr(alt, kind) - setattr(user, kind, amount) - setattr(alt, kind, 0) - g.db.add(alt) - - g.db.add(user) - - online = cache.get(CHAT_ONLINE_CACHE_KEY) - cache.clear() - cache.set(CHAT_ONLINE_CACHE_KEY, online) - - return redirect(user.url) - - @app.get('/admin/edit_rules') @admin_level_required(PERMS['EDIT_RULES']) def edit_rules_get(v): - try: with open(f'files/templates/rules_{SITE_NAME}.html', 'r', encoding="utf-8") as f: rules = f.read() except: rules = None - return render_template('admin/edit_rules.html', v=v, rules=rules) @@ -174,11 +122,8 @@ def edit_rules_post(v): user_id=v.id, ) g.db.add(ma) - return render_template('admin/edit_rules.html', v=v, rules=rules, msg='Rules edited successfully!') - - @app.post("/@/make_admin") @admin_level_required(PERMS['ADMIN_ADD']) def make_admin(v:User, username): @@ -316,7 +261,7 @@ def revert_actions(v:User, username): send_repeatable_notification(user.id, f"@{v.username} (a site admin) has unbanned you!") g.db.add(user) - for u in user.alts: + for u in user.get_alt_graph(g.db): u.shadowbanned = None u.unban_utc = 0 u.ban_reason = None @@ -733,7 +678,7 @@ def alt_votes_get(v): @admin_level_required(PERMS['USER_LINK']) def admin_view_alts(v:User, username=None): u = get_user(username or request.values.get('username'), graceful=True) - return render_template('admin/alts.html', v=v, u=u, alts=u.alts_unique if u else None) + return render_template('admin/alts.html', v=v, u=u, alts=u.get_alt_graph(g.db) if u else None) @app.post('/@/alts/') @limiter.limit(DEFAULT_RATELIMIT_SLOWER) @@ -848,10 +793,6 @@ def unagendaposter(user_id, v): user.agendaposter = 0 g.db.add(user) - for alt in user.alts: - alt.agendaposter = 0 - g.db.add(alt) - ma = ModAction( kind="unchud", user_id=v.id, @@ -901,7 +842,8 @@ def unshadowban(user_id, v): user.shadowbanned = None if not user.is_banned: user.ban_reason = None g.db.add(user) - for alt in user.alts: + + for alt in user.get_alt_graph(g.db): alt.shadowbanned = None if not alt.is_banned: alt.ban_reason = None g.db.add(alt) @@ -978,7 +920,7 @@ def ban_user(user_id, v): user.ban(admin=v, reason=reason, days=days) if request.values.get("alts"): - for x in user.alts: + for x in user.get_alt_graph(g.db): if x.admin_level > v.admin_level: continue x.ban(admin=v, reason=reason, days=days) @@ -1110,7 +1052,7 @@ def unban_user(user_id, v): send_repeatable_notification(user.id, f"@{v.username} (a site admin) has unbanned you!") g.db.add(user) - for x in user.alts: + for x in user.get_alt_graph(g.db): if x.is_banned: send_repeatable_notification(x.id, f"@{v.username} (a site admin) has unbanned you!") x.is_banned = None x.unban_utc = 0 diff --git a/files/routes/routehelpers.py b/files/routes/routehelpers.py index b07633046..7d1cc6a99 100644 --- a/files/routes/routehelpers.py +++ b/files/routes/routehelpers.py @@ -27,9 +27,6 @@ def validate_formkey(u:User, formkey:Optional[str]) -> bool: 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() @@ -68,7 +65,7 @@ def check_for_alts(current:User, include_current_session=True): if include_current_session: session["history"] = list(past_accs) g.db.flush() - for u in current.alts_unique: + for u in current.get_alt_graph(g.db): if u._alt_deleted: continue if u.shadowbanned and current.id not in DONT_SHADOWBAN: current.shadowbanned = u.shadowbanned diff --git a/files/templates/admin/alt_votes.html b/files/templates/admin/alt_votes.html index 8c81e1b35..8a54e26a6 100644 --- a/files/templates/admin/alt_votes.html +++ b/files/templates/admin/alt_votes.html @@ -49,7 +49,7 @@ {% if v.admin_level >= PERMS['USER_LINK'] %}

Link Accounts

- {% if u2 in u1.alts %} + {% if u2 in u1.get_alt_graph(g.db) %}

Accounts are known alts of each other.

{% else %} diff --git a/files/templates/userpage/banner.html b/files/templates/userpage/banner.html index d1fcf8614..3b964b9a3 100644 --- a/files/templates/userpage/banner.html +++ b/files/templates/userpage/banner.html @@ -225,7 +225,7 @@ Alts: {% endif %}
    - {% for account in u.alts_unique if not account._alt_deleted and (v.can_see_shadowbanned or not account.shadowbanned) %} + {% for account in u.get_alt_graph(g.db) if not account._alt_deleted and (v.can_see_shadowbanned or not account.shadowbanned) %}
  • @{{account.username}}{% if account._is_manual %} [m]{% endif %}
  • {% endfor %}
@@ -489,7 +489,7 @@ Alts: {% endif %}
    - {% for account in u.alts_unique if not account._alt_deleted and (v.can_see_shadowbanned or not account.shadowbanned) %} + {% for account in u.get_alt_graph(g.db) if not account._alt_deleted and (v.can_see_shadowbanned or not account.shadowbanned) %}
  • @{{account.username}}{% if account._is_manual %} [m]{% endif %}
  • {% endfor %}