diff --git a/files/assets/css/main.css b/files/assets/css/main.css index 569662e0ed..e37fc090b4 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 1a0c11a3c4..c2f84a8c62 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 a70b7a8eb9..ac18ca2f9e 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 370442fa9e..1a0ee79a86 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 5fe8f1aa29..e8d7938ea9 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 c5199d29de..8c75ae3165 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 3fdedee549..3af1863ade 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 7f551f98be..336e9f8955 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 8415ab4464..ccd7fd80ac 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 6151df2cce..8c81e1b35b 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 0000000000..cfb935b645 --- /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 0000000000..5b21b5bacb --- /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 ce51c9a542..ff14d3b615 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 %}