alt graph
parent
23faabd467
commit
0e6b144ed7
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Loading…
Reference in New Issue