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
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

View File

@ -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

View File

@ -95,67 +95,15 @@ def merge(v:User, id1, id2):
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')
@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("/@<username>/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('/@<username>/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

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):
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

View File

@ -49,7 +49,7 @@
{% if v.admin_level >= PERMS['USER_LINK'] %}
<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>
{% else %}

View File

@ -225,7 +225,7 @@
<span id="profile--alts">Alts:</span>
{% endif %}
<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>
{% endfor %}
</ul>
@ -489,7 +489,7 @@
<span id="profile-mobile--alts">Alts:</span>
{% endif %}
<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>
{% endfor %}
</ul>