From c9ecb5d53556e0ac618974b823b935169c8b196b Mon Sep 17 00:00:00 2001 From: justcool393 Date: Mon, 14 Nov 2022 09:32:13 -0800 Subject: [PATCH] account linking improvements (#448) currently account delinking is very messy and can sometimes just not work we do codey stuff so it's not as bad also we create a pretty page for mops to mop up borked account links * alts: allow proper delinking * fix prev commit * url fix * fix 500 * fixes * :pepodrool: * flag * :pepodrool: redux * sdsdsdsds * correct endpoint * fix html page * alts: only adjust session history if flag is set * fix 500 * allow relinking * fsdsds * :pepodrool: redux * alts: don't fail if an alt isn't history * use postToastSwitch + some API changes * remove unnecessary variables * d-none * delink accounts mod action * fa-link-slash * alts: add form to create alt * remove copied and pasted template * rounded section * UI improvement + fix * \n * fix status * admin: remove duplicate route admin: do a permissions check on 2 pages that need it admin: set the manual flag for manually flagged alts * variable change * fix 500 * alts * add shadowban icon to alt link tool * shadowbanned tooltip * add user info section * fix 500, remove unnecessary form, and add alt votes button * trans and also link to page * margin * sdsdsd * stop the count * fix prev commit * with ctx * plural * alts * don't show shadowbanned users to those who can't see them this is... extremely rare and won't ever be seen in production however if perms were ever rearranged in the future, this keeps permissions correct * shadowban check in alt list * let shadow realm enthusiasts see shadowban alts * sdsdsds * test * be graceful where needed * sdsdsdsds * alts: don't allow adding the same account alts: clarify wording * rename and reorder on admin panel * EOL * remove frankly unnecessary check * try with a set * test * Revert "try with a set" This reverts commit 72be353fba5ffa39b37590cc5d3bf584c94ee06e. * Revert "Revert "try with a set"" This reverts commit 81e41890a192e8b46d0463477998e905fddf56ba. * Revert "Revert "Revert "try with a set""" This reverts commit be51592135a3c09848f993f0154bd2ac862ae505. * clean up test --- files/assets/css/main.css | 8 ++ files/classes/alts.py | 1 + files/classes/mod_logs.py | 5 + files/classes/user.py | 3 +- files/helpers/get.py | 2 +- files/routes/admin.py | 78 ++++++++++--- files/routes/login.py | 27 +++-- files/routes/static.py | 2 +- files/templates/admin/admin_home.html | 7 +- files/templates/admin/alt_votes.html | 43 ++----- files/templates/admin/alts.html | 106 ++++++++++++++++++ .../templates/admin/shadowbanned_tooltip.html | 1 + files/templates/userpage.html | 20 +++- 13 files changed, 231 insertions(+), 72 deletions(-) create mode 100644 files/templates/admin/alts.html create mode 100644 files/templates/admin/shadowbanned_tooltip.html diff --git a/files/assets/css/main.css b/files/assets/css/main.css index 569662e0e..e37fc090b 100644 --- a/files/assets/css/main.css +++ b/files/assets/css/main.css @@ -4084,6 +4084,13 @@ ul.comment-section { .rounded { border-radius: 0.35rem; } + +.rounded-section { + margin-bottom: 3rem; + border: .1px solid var(--gray-400); + border-radius: .35rem; + overflow: hidden; +} .rounded-circle { border-radius: 50%; } @@ -5920,6 +5927,7 @@ g { .fa-knife-kitchen:before{content:"\f6f5"} .fa-lights-holiday:before{content:"\f7b2"} .fa-link:before{content:"\f0c1"} +.fa-link-slash:before{content:"\f127"} .fa-lock:before{content:"\f023"} .fa-lock-alt:before{content:"\f30d"} .fa-search:before{content:"\f002"} diff --git a/files/classes/alts.py b/files/classes/alts.py index 1a0c11a3c..c2f84a8c6 100644 --- a/files/classes/alts.py +++ b/files/classes/alts.py @@ -9,6 +9,7 @@ class Alt(Base): user2 = Column(Integer, ForeignKey("users.id"), primary_key=True) is_manual = Column(Boolean, default=False) created_utc = Column(Integer) + deleted = Column(Boolean, default=False) def __init__(self, *args, **kwargs): if "created_utc" not in kwargs: kwargs["created_utc"] = int(time.time()) diff --git a/files/classes/mod_logs.py b/files/classes/mod_logs.py index a70b7a8eb..ac18ca2f9 100644 --- a/files/classes/mod_logs.py +++ b/files/classes/mod_logs.py @@ -226,6 +226,11 @@ ACTIONTYPES = { "icon": 'fa-link', "color": 'bg-success' }, + 'delink_accounts': { + "str": 'delinked {self.target_link}', + "icon": 'fa-link-slash', + "color": 'bg-danger' + }, 'make_admin': { "str": 'made {self.target_link} an admin', "icon": 'fa-user-crown', diff --git a/files/classes/user.py b/files/classes/user.py index 370442fa9..1a0ee79a8 100644 --- a/files/classes/user.py +++ b/files/classes/user.py @@ -739,7 +739,6 @@ class User(Base): @property @lazy def alts(self): - subq = g.db.query(Alt).filter( or_( Alt.user1 == self.id, @@ -764,6 +763,8 @@ class User(Base): 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 diff --git a/files/helpers/get.py b/files/helpers/get.py index 5fe8f1aa2..e8d7938ea 100644 --- a/files/helpers/get.py +++ b/files/helpers/get.py @@ -27,7 +27,7 @@ def get_id(username:str, graceful=False) -> Optional[int]: return user[0] -def get_user(username:str, v:Optional[User]=None, graceful=False, include_blocks=False, include_shadowbanned=True) -> Optional[User]: +def get_user(username:Optional[str], v:Optional[User]=None, graceful=False, include_blocks=False, include_shadowbanned=True) -> Optional[User]: if not username: if graceful: return None abort(404) diff --git a/files/routes/admin.py b/files/routes/admin.py index c5199d29d..8c75ae316 100644 --- a/files/routes/admin.py +++ b/files/routes/admin.py @@ -636,7 +636,6 @@ def users_list(v): @app.get("/admin/alt_votes") @admin_level_required(PERMS['VIEW_ALT_VOTES']) def alt_votes_get(v): - u1 = request.values.get("u1") u2 = request.values.get("u2") @@ -738,35 +737,78 @@ def alt_votes_get(v): data=data ) - -@app.post("/admin/link_accounts") +@app.get("/admin/alts/") +@app.get("/@/alts/") @limiter.limit(DEFAULT_RATELIMIT_SLOWER) @admin_level_required(PERMS['USER_LINK']) -def admin_link_accounts(v): - u1 = get_account(request.values.get("u1")).id - u2 = get_account(request.values.get("u2")).id +def admin_view_alts(v, 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) - new_alt = Alt( - user1=u1, - user2=u2, - is_manual=True - ) +@app.post('/@/alts/') +@limiter.limit(DEFAULT_RATELIMIT_SLOWER) +@admin_level_required(PERMS['USER_LINK']) +def admin_add_alt(v, username): + user1 = get_user(username) + user2 = get_user(request.values.get('other_username')) + if user1.id == user2.id: abort(400, "Can't add the same account as alts of each other") - g.db.add(new_alt) + deleted = request.values.get('deleted', False, bool) or False + ids = [user1.id, user2.id] + a = g.db.query(Alt).filter(Alt.user1.in_(ids), Alt.user2.in_(ids)).one_or_none() + if a: abort(409, f"@{user1.username} and @{user2.username} are already known {'linked' if not a.deleted else 'delinked'} alts") + a = Alt( + user1=user1.id, + user2=user2.id, + manual=True, + deleted=deleted + ) + g.db.add(a) g.db.flush() - check_for_alts(g.db.get(User, u1), include_current_session=False) - check_for_alts(g.db.get(User, u2), include_current_session=False) + check_for_alts(user1, include_current_session=False) + check_for_alts(user2, include_current_session=False) + + word = 'Delinked' if deleted else 'Linked' + ma_word = 'delink' if deleted else 'link' + note = f'from {user2.id}' if deleted else f'with {user2.id}' + ma = ModAction( + kind=f"{ma_word}_accounts", + user_id=v.id, + target_user_id=user1.id, + _note=note + ) + g.db.add(ma) + return {"message": f"{word} @{user1.username} and @{user2.username} successfully!"} + +@app.route('/@/alts//deleted', methods=["PUT", "DELETE"]) +@limiter.limit(DEFAULT_RATELIMIT_SLOWER) +@admin_level_required(PERMS['USER_LINK']) +def admin_delink_relink_alt(v, username, other): + is_delinking = request.method == 'PUT' # we're adding the 'deleted' state if a PUT request + user1 = get_user(username) + user2 = get_account(other) + ids = [user1.id, user2.id] + a = g.db.query(Alt).filter(Alt.user1.in_(ids), Alt.user2.in_(ids)).one_or_none() + if not a: abort(404) + a.deleted = is_delinking + g.db.add(a) + g.db.flush() + check_for_alts(user1, include_current_session=False) + check_for_alts(user2, include_current_session=False) + word = 'Delinked' if is_delinking else 'Relinked' + ma_word = 'delink' if is_delinking else 'link' + note = f'from {user2.id}' if is_delinking else f'with {user2.id} (relinked)' ma = ModAction( - kind="link_accounts", + kind=f"{ma_word}_accounts", user_id=v.id, - target_user_id=u1, - _note=f'with {u2}' + target_user_id=user1.id, + _note=note ) g.db.add(ma) - return redirect(f"/admin/alt_votes?u1={get_account(u1).username}&u2={get_account(u2).username}") + return {"message": f"{word} @{user1.username} and @{user2.username} successfully!"} @app.get("/admin/removed/posts") diff --git a/files/routes/login.py b/files/routes/login.py index 3fdedee54..3af1863ad 100644 --- a/files/routes/login.py +++ b/files/routes/login.py @@ -48,20 +48,24 @@ def check_for_alts(current:User, include_current_session=True): add_alt(past_id, current_id) other_alts = g.db.query(Alt).filter(Alt.user1.in_(li), Alt.user2.in_(li)).all() for a in other_alts: - if a.user1 != past_id: - add_alt(a.user1, past_id) - if a.user1 != current_id: - add_alt(a.user1, current_id) - if a.user2 != past_id: - add_alt(a.user2, past_id) - if a.user2 != current_id: - add_alt(a.user2, current_id) + if a.deleted: + if include_current_session: + try: session["history"].remove(a.user1) + except: pass + try: session["history"].remove(a.user2) + except: pass + continue # don't propagate deleted alt links + if a.user1 != past_id: add_alt(a.user1, past_id) + if a.user1 != current_id: add_alt(a.user1, current_id) + if a.user2 != past_id: add_alt(a.user2, past_id) + if a.user2 != current_id: add_alt(a.user2, current_id) past_accs.add(current_id) if include_current_session: session["history"] = list(past_accs) g.db.flush() for u in current.alts_unique: + if u._alt_deleted: continue if u.shadowbanned: current.shadowbanned = u.shadowbanned if not current.is_banned: current.ban_reason = u.ban_reason @@ -369,11 +373,10 @@ def sign_up_post(v): send_verification_email(new_user) - check_for_alts(new_user) - - send_notification(new_user.id, WELCOME_MSG) - session["lo_user"] = new_user.id + + check_for_alts(new_user) + send_notification(new_user.id, WELCOME_MSG) if SIGNUP_FOLLOW_ID: signup_autofollow = get_account(SIGNUP_FOLLOW_ID) diff --git a/files/routes/static.py b/files/routes/static.py index 7f551f98b..336e9f895 100644 --- a/files/routes/static.py +++ b/files/routes/static.py @@ -145,7 +145,7 @@ def log(v): actions = actions.filter(ModAction.kind.notin_([ "shadowban","unshadowban", "mod_mute_user","mod_unmute_user", - "link_accounts", + "link_accounts","delink_accounts", ])) if admin_id: diff --git a/files/templates/admin/admin_home.html b/files/templates/admin/admin_home.html index 8415ab446..ccd7fd80a 100644 --- a/files/templates/admin/admin_home.html +++ b/files/templates/admin/admin_home.html @@ -69,7 +69,12 @@

Safety

{% if FEATURES['BADGES'] or FEATURES['AWARDS'] -%} diff --git a/files/templates/admin/alt_votes.html b/files/templates/admin/alt_votes.html index 6151df2cc..8c81e1b35 100644 --- a/files/templates/admin/alt_votes.html +++ b/files/templates/admin/alt_votes.html @@ -1,15 +1,9 @@ {% extends "default.html" %} - -{% block title %} -{{SITE_NAME}} - -{% endblock %} - +{% block pagetitle %}Alt Vote Analysis{% endblock %} {% block content %} -
Vote Info
-
+ @@ -17,12 +11,7 @@
{% if u1 and u2 %} - -

Analysis

- - -
@@ -32,7 +21,6 @@ - @@ -58,27 +46,18 @@
@{{u2.username}} only (% unique)
Post Upvotes {{data['u1_only_post_ups']}} ({{data['u1_post_ups_unique']}}%){{data['u2_only_comment_downs']}} ({{data['u2_comment_downs_unique']}}%)
+{% if v.admin_level >= PERMS['USER_LINK'] %} +

Link Accounts

-

Link Accounts

+ {% if u2 in u1.alts %} +

Accounts are known alts of each other.

+ {% else %} -{% if u2 in u1.alts %} -

Accounts are known alts of eachother.

-{% else %} - -

Two accounts controlled by different people should have most uniqueness percentages at or above 70-80%

-

A sockpuppet account will have its uniqueness percentages significantly lower.

- - -
- - - - -
+

Two accounts controlled by different people should have most uniqueness percentages at or above 70-80%

+

A sockpuppet account will have its uniqueness percentages significantly lower.

+ + {% endif %} {% endif %} - {% endif %} - - {% endblock %} diff --git a/files/templates/admin/alts.html b/files/templates/admin/alts.html new file mode 100644 index 000000000..cfb935b64 --- /dev/null +++ b/files/templates/admin/alts.html @@ -0,0 +1,106 @@ +{% extends "settings2.html" %} +{% block content %} +{% if u %} +
Alts for @{{u.username}}
+{% else %} +
Alts
+{% endif %} +{%- import 'util/helpers.html' as help -%} +
+
+ + + +
+
+{% if u %} +{% set count=alts|length %} +
+

@{{u.username}} created their account {{u.created_utc|timestamp}} and has {{count}} known alt{{help.plural(count)}}.
+ They are {% if not u.is_suspended_permanently %}not {% endif %}permanently banned{% if v.admin_level >= PERMS['USER_SHADOWBAN'] %} and they are {% if not u.shadowbanned %}not {% endif %}shadowbanned{% endif %}.

+
+
+ + + + + + + + + + + + + {% for user in alts %} + + + + + + + + + + {% endfor %} +
#NameAccount CreatedAlt Link CreatedManualDelinkedActions
{{loop.index}}{% include "admin/shadowbanned_tooltip.html" %}{% include "user_in_table.html" %}{{user.created_utc|timestamp}}{{user._alt_created_utc|timestamp}}{{user._is_manual}}{{user._alt_deleted}} + + + Alts + {% if v.admin_level >= PERMS['VIEW_ALT_VOTES'] %} + Alt Votes + {% endif %} +
+
+
+
Add Alt
+

This tool allows you to add an alt either linked or delinked.
+ Adding linked will link the two alts together manually, while adding delinked will attempt to delink alts whereever possible.
+ You're on your own for reversing any propagation though. +

+
+ + + + +
+
+{% endif %} + + +{% endblock %} diff --git a/files/templates/admin/shadowbanned_tooltip.html b/files/templates/admin/shadowbanned_tooltip.html new file mode 100644 index 000000000..5b21b5bac --- /dev/null +++ b/files/templates/admin/shadowbanned_tooltip.html @@ -0,0 +1 @@ +{% if v and v.admin_level >= PERMS['USER_SHADOWBAN'] and user.shadowbanned %}{% endif %} diff --git a/files/templates/userpage.html b/files/templates/userpage.html index ce51c9a54..ff14d3b61 100644 --- a/files/templates/userpage.html +++ b/files/templates/userpage.html @@ -230,9 +230,13 @@

User has private mode enabled

{% endif %} {% if v and (v.admin_level >= PERMS['VIEW_ALTS'] or v.alt) %} - Alts: + {% if v.admin_level >= PERMS['USER_LINK'] %} + Alts: + {% else %} + Alts: + {% endif %}
    - {% for account in u.alts_unique %} + {% for account in u.alts_unique if not account._alt_deleted and (v.can_see_shadowbanned or not account.shadowbanned) %}
  • @{{account.username}}{% if account._is_manual %} [m]{% endif %}
  • {% endfor %}
@@ -469,11 +473,15 @@

User has private mode enabled

{% endif %} {% if v and (v.admin_level >= PERMS['VIEW_ALTS'] or v.alt) %} - Alts: + {% if v.admin_level >= PERMS['USER_LINK'] %} + Alts: + {% else %} + Alts: + {% endif %}:
    - {% for account in u.alts_unique %} -
  • @{{account.username}}{% if account._is_manual %} [m]{% endif %}
  • - {% endfor %} + {% for account in u.alts_unique if not account._alt_deleted and (v.can_see_shadowbanned or not account.shadowbanned) %} +
  • @{{account.username}}{% if account._is_manual %} [m]{% endif %}
  • + {% endfor %}
{% endif %}