alt graph

pull/83/head
Aevann 2022-12-22 22:03:40 +02:00
parent 23faabd467
commit 0e6b144ed7
6 changed files with 47 additions and 117 deletions

View File

@ -1,11 +1,11 @@
import random import random
from operator import * from operator import *
from typing import Any, Union from typing import Callable, Union
import pyotp import pyotp
from sqlalchemy import Column, ForeignKey from sqlalchemy import Column, ForeignKey
from sqlalchemy.orm import aliased, deferred from sqlalchemy.orm import aliased, deferred, Query
from sqlalchemy.sql import func from sqlalchemy.sql import case, func, literal
from sqlalchemy.sql.expression import not_, and_, or_ from sqlalchemy.sql.expression import not_, and_, or_
from sqlalchemy.sql.sqltypes import * from sqlalchemy.sql.sqltypes import *
@ -470,19 +470,43 @@ class User(Base):
def age(self): def age(self):
return int(time.time()) - self.created_utc return int(time.time()) - self.created_utc
@property
@lazy @lazy
def alts_unique(self): def get_alt_graph(self, db:scoped_session, alt_filter:Optional[Callable[[Query], Query]]=None, **kwargs) -> Query:
alts = [] '''
for u in self.alts: Gets the full graph of alts (optionally filtering `Alt` objects by criteria using a callable,
if u not in alts: alts.append(u) such as by a date to only get alts from a certain date) as a query of users that can be filtered
return alts 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 @property
@lazy @lazy
def alts_patron(self): def alts_patron(self):
for u in self.alts_unique: for u in self.get_alt_graph(g.db):
if u.patron: return True if not u._deleted and u.patron: return True
return False return False
@property @property
@ -724,43 +748,10 @@ class User(Base):
def do_reddit(self): def do_reddit(self):
return self.notifications_count == self.reddit_notifications_count 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 @property
@lazy @lazy
def alt_ids(self): 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 @property
@lazy @lazy

View File

@ -458,7 +458,7 @@ def execute_antispam_comment_check(body:str, v:User):
g.db.commit() g.db.commit()
abort(403, "Too much spam!") 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 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']: if v.age < UNDER_SIEGE_AGE_THRESHOLD and not v.admin_level >= PERMS['SITE_BYPASS_UNDER_SIEGE_MODE']:
v.shadowbanned = AUTOJANNY_ID v.shadowbanned = AUTOJANNY_ID

View File

@ -95,67 +95,15 @@ def merge(v:User, id1, id2):
return redirect(user1.url) return redirect(user1.url)
@app.get('/admin/merge_all/<id>')
@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') @app.get('/admin/edit_rules')
@admin_level_required(PERMS['EDIT_RULES']) @admin_level_required(PERMS['EDIT_RULES'])
def edit_rules_get(v): def edit_rules_get(v):
try: try:
with open(f'files/templates/rules_{SITE_NAME}.html', 'r', encoding="utf-8") as f: with open(f'files/templates/rules_{SITE_NAME}.html', 'r', encoding="utf-8") as f:
rules = f.read() rules = f.read()
except: except:
rules = None rules = None
return render_template('admin/edit_rules.html', v=v, rules=rules) return render_template('admin/edit_rules.html', v=v, rules=rules)
@ -174,11 +122,8 @@ def edit_rules_post(v):
user_id=v.id, user_id=v.id,
) )
g.db.add(ma) g.db.add(ma)
return render_template('admin/edit_rules.html', v=v, rules=rules, msg='Rules edited successfully!') return render_template('admin/edit_rules.html', v=v, rules=rules, msg='Rules edited successfully!')
@app.post("/@<username>/make_admin") @app.post("/@<username>/make_admin")
@admin_level_required(PERMS['ADMIN_ADD']) @admin_level_required(PERMS['ADMIN_ADD'])
def make_admin(v:User, username): 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!") send_repeatable_notification(user.id, f"@{v.username} (a site admin) has unbanned you!")
g.db.add(user) g.db.add(user)
for u in user.alts: for u in user.get_alt_graph(g.db):
u.shadowbanned = None u.shadowbanned = None
u.unban_utc = 0 u.unban_utc = 0
u.ban_reason = None u.ban_reason = None
@ -733,7 +678,7 @@ def alt_votes_get(v):
@admin_level_required(PERMS['USER_LINK']) @admin_level_required(PERMS['USER_LINK'])
def admin_view_alts(v:User, username=None): def admin_view_alts(v:User, username=None):
u = get_user(username or request.values.get('username'), graceful=True) 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('/@<username>/alts/') @app.post('/@<username>/alts/')
@limiter.limit(DEFAULT_RATELIMIT_SLOWER) @limiter.limit(DEFAULT_RATELIMIT_SLOWER)
@ -848,10 +793,6 @@ def unagendaposter(user_id, v):
user.agendaposter = 0 user.agendaposter = 0
g.db.add(user) g.db.add(user)
for alt in user.alts:
alt.agendaposter = 0
g.db.add(alt)
ma = ModAction( ma = ModAction(
kind="unchud", kind="unchud",
user_id=v.id, user_id=v.id,
@ -901,7 +842,8 @@ def unshadowban(user_id, v):
user.shadowbanned = None user.shadowbanned = None
if not user.is_banned: user.ban_reason = None if not user.is_banned: user.ban_reason = None
g.db.add(user) g.db.add(user)
for alt in user.alts:
for alt in user.get_alt_graph(g.db):
alt.shadowbanned = None alt.shadowbanned = None
if not alt.is_banned: alt.ban_reason = None if not alt.is_banned: alt.ban_reason = None
g.db.add(alt) g.db.add(alt)
@ -978,7 +920,7 @@ def ban_user(user_id, v):
user.ban(admin=v, reason=reason, days=days) user.ban(admin=v, reason=reason, days=days)
if request.values.get("alts"): 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: if x.admin_level > v.admin_level:
continue continue
x.ban(admin=v, reason=reason, days=days) 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!") send_repeatable_notification(user.id, f"@{v.username} (a site admin) has unbanned you!")
g.db.add(user) 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!") if x.is_banned: send_repeatable_notification(x.id, f"@{v.username} (a site admin) has unbanned you!")
x.is_banned = None x.is_banned = None
x.unban_utc = 0 x.unban_utc = 0

View File

@ -27,9 +27,6 @@ def validate_formkey(u:User, formkey:Optional[str]) -> bool:
def check_for_alts(current:User, include_current_session=True): def check_for_alts(current:User, include_current_session=True):
current_id = current.id 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()] ids = [x[0] for x in g.db.query(User.id).all()]
past_accs = set(session.get("history", [])) if include_current_session else set() 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: if include_current_session:
session["history"] = list(past_accs) session["history"] = list(past_accs)
g.db.flush() 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._alt_deleted: continue
if u.shadowbanned and current.id not in DONT_SHADOWBAN: if u.shadowbanned and current.id not in DONT_SHADOWBAN:
current.shadowbanned = u.shadowbanned current.shadowbanned = u.shadowbanned

View File

@ -49,7 +49,7 @@
{% if v.admin_level >= PERMS['USER_LINK'] %} {% if v.admin_level >= PERMS['USER_LINK'] %}
<h2>Link Accounts</h2> <h2>Link Accounts</h2>
{% if u2 in u1.alts %} {% if u2 in u1.get_alt_graph(g.db) %}
<p>Accounts are <a href="/@{{u1.username}}/alts">known alts</a> of each other.</p> <p>Accounts are <a href="/@{{u1.username}}/alts">known alts</a> of each other.</p>
{% else %} {% else %}

View File

@ -225,7 +225,7 @@
<span id="profile--alts">Alts:</span> <span id="profile--alts">Alts:</span>
{% endif %} {% endif %}
<ul id="profile--alts-list"> <ul id="profile--alts-list">
{% 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) %}
<li><a href="{{account.url}}">@{{account.username}}</a>{% if account._is_manual %} [m]{% endif %}</li> <li><a href="{{account.url}}">@{{account.username}}</a>{% if account._is_manual %} [m]{% endif %}</li>
{% endfor %} {% endfor %}
</ul> </ul>
@ -489,7 +489,7 @@
<span id="profile-mobile--alts">Alts:</span> <span id="profile-mobile--alts">Alts:</span>
{% endif %} {% endif %}
<ul id="profile-mobile--alts-list"> <ul id="profile-mobile--alts-list">
{% 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) %}
<li><a href="{{account.url}}">@{{account.username}}</a>{% if account._is_manual %} [m]{% endif %}</li> <li><a href="{{account.url}}">@{{account.username}}</a>{% if account._is_manual %} [m]{% endif %}</li>
{% endfor %} {% endfor %}
</ul> </ul>