diff --git a/litecord/blueprints/users.py b/litecord/blueprints/users.py index 8fbb270..969a043 100644 --- a/litecord/blueprints/users.py +++ b/litecord/blueprints/users.py @@ -477,13 +477,14 @@ def rand_hex(length: int = 8) -> str: return urandom(length).hex()[:length] -async def _del_from_table(table: str, user_id: int): +async def _del_from_table(db, table: str, user_id: int): + """Delete a row from a table.""" column = { 'channel_overwrites': 'target_user', 'user_settings': 'id' }.get(table, 'user_id') - res = await app.db.execute(f""" + res = await db.execute(f""" DELETE FROM {table} WHERE {column} = $1 """, user_id) @@ -492,6 +493,74 @@ async def _del_from_table(table: str, user_id: int): user_id, table, res) +async def delete_user(user_id, *, db=None): + """Delete a user. Does not disconnect the user.""" + if db is None: + db = app.db + + new_username = f'Deleted User {rand_hex()}' + + # by using a random hex in password_hash + # we break attempts at using the default '123' password hash + # to issue valid tokens for deleted users. + + await db.execute(""" + UPDATE users + SET + username = $1, + email = NULL, + mfa_enabled = false, + verified = false, + avatar = NULL, + flags = 0, + premium_since = NULL, + phone = '', + password_hash = $2 + WHERE + id = $3 + """, new_username, rand_hex(32), user_id) + + # remove the user from various tables + await _del_from_table(db, 'user_settings', user_id) + await _del_from_table(db, 'user_payment_sources', user_id) + await _del_from_table(db, 'user_subscriptions', user_id) + await _del_from_table(db, 'user_payments', user_id) + await _del_from_table(db, 'user_read_state', user_id) + await _del_from_table(db, 'guild_settings', user_id) + await _del_from_table(db, 'guild_settings_channel_overrides', user_id) + + await db.execute(""" + DELETE FROM relationships + WHERE user_id = $1 OR peer_id = $1 + """, user_id) + + # DMs are still maintained, but not the state. + await _del_from_table(db, 'dm_channel_state', user_id) + + # TODO: group DMs + + await _del_from_table(db, 'members', user_id) + await _del_from_table(db, 'member_roles', user_id) + await _del_from_table(db, 'channel_overwrites', user_id) + + +async def _force_disconnect(user_id): + # after removing the user from all tables, we need to force + # all known user states to reconnect, causing the user to not + # be online anymore. + user_states = app.state_manager.user_states(user_id) + + for state in user_states: + # make it unable to resume + app.state_manager.remove(state) + + if not state.ws: + continue + + # force a close, 4000 should make the client reconnect. + await state.ws.close(4000) + + @bp.route('/@me/delete', methods=['POST']) async def delete_account(): """Delete own account. @@ -526,64 +595,7 @@ async def delete_account(): if not await check_password(pwd_hash, password): raise Unauthorized('password does not match') - new_username = f'Deleted User {rand_hex()}' - - # by using a random hex in password_hash - # we break attempts at using the default '123' password hash - # to issue valid tokens for deleted users. - - await app.db.execute(""" - UPDATE users - SET - username = $1, - email = NULL, - mfa_enabled = false, - verified = false, - avatar = NULL, - flags = 0, - premium_since = NULL, - phone = '', - password_hash = $2 - WHERE - id = $3 - """, new_username, rand_hex(32), user_id) - - # remove the user from various tables - await _del_from_table('user_settings', user_id) - await _del_from_table('user_payment_sources', user_id) - await _del_from_table('user_subscriptions', user_id) - await _del_from_table('user_payments', user_id) - await _del_from_table('user_read_state', user_id) - await _del_from_table('guild_settings', user_id) - await _del_from_table('guild_settings_channel_overrides', user_id) - - await app.db.execute(""" - DELETE FROM relationships - WHERE user_id = $1 OR peer_id = $1 - """, user_id) - - # DMs are still maintained, but not the state. - await _del_from_table('dm_channel_state', user_id) - - # TODO: group DMs - - await _del_from_table('members', user_id) - await _del_from_table('member_roles', user_id) - await _del_from_table('channel_overwrites', user_id) - - # after removing the user from all tables, we need to force - # all known user states to reconnect, causing the user to not - # be online anymore. - user_states = app.state_manager.user_states(user_id) - - for state in user_states: - # make it unable to resume - app.state_manager.remove(state) - - if not state.ws: - continue - - # force a close, 4000 should make the client reconnect. - await state.ws.close(4000) + await delete_user(user_id) + await _force_disconnect(user_id) return '', 204 diff --git a/manage/cmd/users.py b/manage/cmd/users.py index f8f4ee8..1516673 100644 --- a/manage/cmd/users.py +++ b/manage/cmd/users.py @@ -21,10 +21,12 @@ import base64 import itsdangerous import bcrypt from litecord.blueprints.auth import create_user, make_token +from litecord.blueprints.users import delete_user from litecord.enums import UserFlags -async def find_user(username, discrim, ctx): +async def find_user(username, discrim, ctx) -> int: + """Get a user ID via the username/discrim pair.""" return await ctx.db.fetchval(""" SELECT id FROM users @@ -53,8 +55,12 @@ async def adduser(ctx, args): uid, _ = await create_user(args.username, args.email, args.password, ctx.db, ctx.loop) + user = await ctx.storage.get_user(uid) + print('created!') print(f'\tuid: {uid}') + print(f'\tusername: {user["username"]}') + print(f'\tdiscrim: {user["discriminator"]}') async def make_staff(ctx, args): @@ -88,6 +94,31 @@ async def generate_bot_token(ctx, args): print(make_token(args.user_id, password_hash)) +async def del_user(ctx, args): + """Delete a user.""" + uid = await find_user(args.username, args.discrim, ctx) + + if uid is None: + print('user not found') + return + + user = await ctx.storage.get_user(uid) + + print(f'\tuid: {user["user_id"]}') + print(f'\tuname: {user["username"]}') + print(f'\tdiscrim: {user["discriminator"]}') + + print('\n you sure you want to delete user? press Y (uppercase)') + confirm = input() + + if confirm != 'Y': + print('not confirmed') + return + + await delete_user(uid) + print('ok') + + def setup(subparser): setup_test_parser = subparser.add_parser( 'adduser', @@ -109,23 +140,25 @@ def setup(subparser): description=make_staff.__doc__ ) + staff_parser.add_argument('username') staff_parser.add_argument( - 'username' - ) - staff_parser.add_argument( - 'discrim', help='the discriminator of the user' - ) + 'discrim', help='the discriminator of the user') staff_parser.set_defaults(func=make_staff) + del_user_parser = subparser.add_parser( + 'deluser', help='delete a single user') + + del_user_parser.add_argument('username') + del_user_parser.add_argument('discriminator') + + del_user_parser.set_defaults(func=del_user) + token_parser = subparser.add_parser( 'generate_token', help='generate a token for specified bot', - description=generate_bot_token.__doc__ - ) + description=generate_bot_token.__doc__) - token_parser.add_argument( - 'user_id' - ) + token_parser.add_argument('user_id') token_parser.set_defaults(func=generate_bot_token)