From ed7714efdb2aa92f9c5f2d6670555529618c6d44 Mon Sep 17 00:00:00 2001 From: UnitedAirforce Date: Mon, 16 Mar 2026 17:55:09 +0800 Subject: [PATCH] Discord stuff --- new_server_7003/api/account.py | 22 +-- new_server_7003/api/discord_hook.py | 35 +++- new_server_7003/api/misc.py | 11 +- new_server_7003/discord_server.py | 280 ++++++++++++++++++++++++++++ 4 files changed, 333 insertions(+), 15 deletions(-) create mode 100644 new_server_7003/discord_server.py diff --git a/new_server_7003/api/account.py b/new_server_7003/api/account.py index a0f25e0..d8c4c42 100644 --- a/new_server_7003/api/account.py +++ b/new_server_7003/api/account.py @@ -4,7 +4,7 @@ from starlette.routing import Route from datetime import datetime import secrets -from api.misc import is_alphanumeric, inform_page, verify_password, hash_password, crc32_decimal, should_serve, generate_salt +from api.misc import is_alphanumeric, inform_page, verify_password, hash_password, crc32_decimal, should_serve, generate_salt, validate_password from api.database import check_blacklist, user_name_to_user_info, decrypt_fields_to_user_info, set_user_data_using_decrypted_fields, get_user_from_save_id, create_user, logout_user, login_user, get_bind, read_user_save_file, write_user_save_file from api.crypt import decrypt_fields from config import AUTHORIZATION_MODE @@ -69,11 +69,9 @@ async def password_reset(request: Request): user_info, device_info = await decrypt_fields_to_user_info(decrypted_fields) if user_info: - username = user_info['username'] - if username == new_password: - return inform_page("FAILED:
Username cannot be the same as password.", 0) - if len(new_password) < 6: - return inform_page("FAILED:
Password must have 6 or more characters.", 0) + result, str = validate_password(user_info['username'], new_password) + if not result: + return inform_page("FAILED:
" + str, 0) old_hash = user_info['password_hash'] if old_hash: @@ -172,18 +170,16 @@ async def register(request: Request): if not username or not password: return inform_page("FAILED:
Missing username or password.", 0) - - if username == password: - return inform_page("FAILED:
Username cannot be the same as password.", 0) - + if len(username) < 6 or len(username) > 20: return inform_page("FAILED:
Username must be between 6 and 20
characters long.", 0) - if len(password) < 6: - return inform_page("FAILED:
Password must have
6 or above characters.", 0) - if not is_alphanumeric(username): return inform_page("FAILED:
Username must consist entirely of
alphanumeric characters.", 0) + + result, str = validate_password(username, password) + if not result: + return inform_page("FAILED:
" + str, 0) decrypted_fields, _ = await decrypt_fields(request) if not decrypted_fields: diff --git a/new_server_7003/api/discord_hook.py b/new_server_7003/api/discord_hook.py index 2edc663..37c54aa 100644 --- a/new_server_7003/api/discord_hook.py +++ b/new_server_7003/api/discord_hook.py @@ -4,7 +4,7 @@ from starlette.requests import Request from starlette.routing import Route from datetime import datetime -from api.misc import is_alphanumeric, inform_page, generate_salt, check_email, generate_otp +from api.misc import is_alphanumeric, inform_page, generate_salt, check_email, generate_otp, validate_password, hash_password from api.database import player_database, accounts, binds, decrypt_fields_to_user_info, get_bind, verify_user_code, user_name_to_user_info from api.crypt import decrypt_fields from api.email_hook import send_email_to_user @@ -139,6 +139,38 @@ async def discord_get_bind(request: Request, form): return JSONResponse({"state": 1, "message": "Your account is binded to: " + user_record['username']}) +@require_authorization(mode_required=[2]) +@validate_form_fields(["discord_id", "password"]) +@check_discord_api_key() +async def discord_reset_password(request: Request, form): + discord_id = form.get("discord_id") + password = form.get("password") + + query = binds.select().where(binds.c.bind_account == discord_id).where(binds.c.is_verified == 1) + bind_record = await player_database.fetch_one(query) + bind_record = dict(bind_record) if bind_record else None + if not bind_record: + return JSONResponse({"state": 0, "message": "No verified bind found for this Discord ID."}, status_code=404) + + user_query = accounts.select().where(accounts.c.id == bind_record['user_id']) + user_record = await player_database.fetch_one(user_query) + + user_record = dict(user_record) if user_record else None + if not user_record: + return JSONResponse({"state": 0, "message": "User associated with this bind does not exist."}, status_code=405) + + result, str = validate_password(user_record['username'], password) + if not result: + return JSONResponse({"state": 0, "message": str}, status_code=406) + + hashed_new_password = hash_password(password) + query = accounts.update().where(accounts.c.id == user_record['id']).values( + password_hash=hashed_new_password + ) + await player_database.execute(query) + + return JSONResponse({"state": 1, "message": "Your password is reset."}) + @require_authorization(mode_required=[2]) @validate_form_fields(["discord_id"]) @check_discord_api_key() @@ -183,6 +215,7 @@ routes = [ Route('/send_email', send_email, methods=['POST']), Route('/discord_get_token', discord_get_token, methods=['POST']), Route('/discord_get_bind', discord_get_bind, methods=['POST']), + Route('/discord_reset_password', discord_reset_password, methods=['POST']), Route('/discord_ban', discord_ban, methods=['POST']), Route('/discord_unban', discord_unban, methods=['POST']), Route('/verify', verify_user, methods=['POST']) diff --git a/new_server_7003/api/misc.py b/new_server_7003/api/misc.py index 4cda206..2b282bd 100644 --- a/new_server_7003/api/misc.py +++ b/new_server_7003/api/misc.py @@ -318,4 +318,13 @@ async def get_start_xml(): for child in response_root: root.append(child) - return root \ No newline at end of file + return root + +def validate_password(username, password): + if username == password: + return 0, "Username cannot be the same as password." + + if len(password) < 6: + return 0, "Password must have
6 or above characters." + + return 1, "" \ No newline at end of file diff --git a/new_server_7003/discord_server.py b/new_server_7003/discord_server.py new file mode 100644 index 0000000..e4fd705 --- /dev/null +++ b/new_server_7003/discord_server.py @@ -0,0 +1,280 @@ +import discord +from discord.ext import commands +from discord import Interaction, TextStyle, app_commands +from discord.ui import Modal, TextInput, View, Button +import httpx + +from config import DISCORD_BOT_SECRET, DISCORD_BOT_API_KEY, OVERRIDE_HOST, HOST, PORT + +intents = discord.Intents.default() +intents.members = True +intents.message_content = True +bot = commands.Bot(command_prefix="/", intents=intents) + +BIND_CHANNEL_ID = 1439460843015372971 + +URL_HOST = OVERRIDE_HOST if OVERRIDE_HOST is not None else ("http://" + HOST + ":" + str(PORT) + "/") + +def only_in_bind_channel(): + def predicate(interaction: discord.Interaction) -> bool: + if isinstance(interaction.channel, discord.DMChannel): + return False + return interaction.channel_id == BIND_CHANNEL_ID + return app_commands.check(predicate) + +@bot.event +async def on_ready(): + print(f"[Discord] Bot connected as {bot.user}") + + try: + await bot.tree.sync() + except Exception as e: + print("[Discord] Error syncing commands:", e) + + # Register persistent view + bot.add_view(BindButtons()) + + channel = bot.get_channel(BIND_CHANNEL_ID) + if channel: + # Delete old messages from the bot + async for message in channel.history(limit=100): + if message.author == bot.user: + try: + await message.delete() + except: + pass + + # Send the new message with buttons + await channel.send("Use the buttons below:", view=BindButtons()) + + +async def get_account_info(discord_id: str): + request_url = URL_HOST + "discord_get_bind" + request_post = {"discord_id": discord_id} + request_headers = {"X-API-KEY": DISCORD_BOT_API_KEY} + + async with httpx.AsyncClient() as client: + response = await client.post(request_url, data=request_post, headers=request_headers) + response_data = response.json() + print("get_account_info called by ", discord_id, " response payload:", response.text) + + prefix = "✅ " if response_data.get("state") == 1 else "❌ " + return prefix + response_data.get("message", "No message provided.") + +async def get_otp(username: str, bind_token: str, discord_id: str): + request_url = URL_HOST + "discord_get_token" + request_post = { + "username": username, + "bind_token": bind_token, + "discord_id": discord_id + } + request_headers = {"X-API-KEY": DISCORD_BOT_API_KEY} + + async with httpx.AsyncClient() as client: + response = await client.post(request_url, data=request_post, headers=request_headers) + print("get_otp called by ", discord_id, " response payload:", response.text) + response_data = response.json() + + prefix = "✅ " if response_data.get("state") == 1 else "❌ " + return prefix + response_data.get("message", "No message provided.") + +async def reset_password(password: str, discord_id: str): + request_url = URL_HOST + "discord_reset_password" + request_post = { + "password": password, + "discord_id": discord_id + } + request_headers = {"X-API-KEY": DISCORD_BOT_API_KEY} + + async with httpx.AsyncClient() as client: + response = await client.post(request_url, data=request_post, headers=request_headers) + print("get_otp called by ", discord_id, " response payload:", response.text) + response_data = response.json() + + prefix = "✅ " if response_data.get("state") == 1 else "❌ " + return prefix + response_data.get("message", "No message provided.") + +async def ban_user_from_game_service(discord_id: str): + try: + request_url = URL_HOST + "discord_ban" + request_post = {"discord_id": discord_id} + request_headers = {"X-API-KEY": DISCORD_BOT_API_KEY} + + async with httpx.AsyncClient() as client: + response = await client.post(request_url, data=request_post, headers=request_headers) + data = response.json() + print("ban_user_from_game_service called by ", discord_id, " response payload:", response.text) + + prefix = "✅ " if data.get("state") == 1 else "❌ " + return prefix + data.get("message", "No message provided.") + + except Exception as e: + return f"❌ Error: {str(e)}" + +async def try_unban_user_from_game_service(discord_id: str): + try: + request_url = URL_HOST + "discord_unban" + request_post = {"discord_id": discord_id} + request_headers = {"X-API-KEY": DISCORD_BOT_API_KEY} + + async with httpx.AsyncClient() as client: + response = await client.post(request_url, data=request_post, headers=request_headers) + data = response.json() + print("try_unban_user_from_game_service called by ", discord_id, " response payload:", response.text) + + prefix = "✅ " if data.get("state") == 1 else "❌ " + return prefix + data.get("message", "No message provided.") + + except Exception as e: + return f"❌ Error: {str(e)}" + +# ------------------------------- +# Modal Definition +# ------------------------------- + +class BindModal(Modal, title="Bind Your Account"): + username = TextInput( + label="Username", + placeholder="Enter your in-game username", + style=TextStyle.short, + required=True, + ) + + bind_token = TextInput( + label="Bind Token", + placeholder="Enter your bind token", + style=TextStyle.short, + required=True, + ) + + async def on_submit(self, interaction: Interaction): + username = self.username.value + bind_token = self.bind_token.value + + if not username or not username.strip(): + return await interaction.response.send_message( + "❌ Username cannot be empty.", ephemeral=True + ) + + if len(username) < 6 or len(username) > 20: + return await interaction.response.send_message( + "❌ Username length not valid.", ephemeral=True + ) + + if not bind_token or not bind_token.strip(): + return await interaction.response.send_message( + "❌ Bind token cannot be empty.", ephemeral=True + ) + + if len(bind_token) > 10: + return await interaction.response.send_message( + "❌ Bind token length not valid.", ephemeral=True + ) + + discord_id = str(interaction.user.id) + + response = await get_otp(username, bind_token, discord_id) + await interaction.response.send_message(response, ephemeral=True) + +class PasswordResetModal(Modal, title="Password Reset"): + password = TextInput( + label="New Password (Requirement: )", + placeholder="Your New Password", + style=TextStyle.short, + required=True, + ) + + repeat_password = TextInput( + label="Repeat New Password", + placeholder="Repeat The Password", + style=TextStyle.short, + required=True, + ) + + async def on_submit(self, interaction: Interaction): + password = self.password.value + repeat_password = self.repeat_password.value + + if not password or not password.strip(): + return await interaction.response.send_message( + "❌ Password cannot be empty.", ephemeral=True + ) + + if not repeat_password or not repeat_password.strip(): + return await interaction.response.send_message( + "❌ Repeat Password cannot be empty.", ephemeral=True + ) + + if len(password) < 6: + return await interaction.response.send_message( + "❌ Password must have 6 or more characters.", ephemeral=True + ) + + if password != repeat_password: + return await interaction.response.send_message( + "❌ Repeat Password does not match.", ephemeral=True + ) + + discord_id = str(interaction.user.id) + + response = await reset_password(password, discord_id) + await interaction.response.send_message(response, ephemeral=True) + +class BindButtons(View): + def __init__(self): + super().__init__(timeout=None) # Persistent buttons + + @discord.ui.button(label="Bind Account", style=discord.ButtonStyle.primary, custom_id="btn_bind") + async def bind_button(self, interaction: Interaction, button: Button): + await interaction.response.send_modal(BindModal()) + + @discord.ui.button(label="Who Am I?", style=discord.ButtonStyle.secondary, custom_id="btn_whoami") + async def whoami_button(self, interaction: Interaction, button: Button): + discord_id = str(interaction.user.id) + response = await get_account_info(discord_id) + await interaction.response.send_message(response, ephemeral=True) + + @discord.ui.button(label="Reset Password", style=discord.ButtonStyle.danger, custom_id="btn_password_reset") + async def password_reset_button(self, interaction: Interaction, button: Button): + await interaction.response.send_modal(PasswordResetModal()) + +# ------------------------------- +# Member Events +# ------------------------------- + +@bot.event +async def on_member_remove(member): + print(f"{member.name} left the server → banning in-game.") + await ban_user_from_game_service(str(member.id)) + +@bot.event +async def on_member_join(member): + print(f"{member.name} joined the server → unbanning in-game if needed.") + await try_unban_user_from_game_service(str(member.id)) + +# ------------------------------- +# Prefix command error handler +# ------------------------------- + +@bot.event +async def on_command_error(ctx, error): + if isinstance(error, commands.MissingRequiredArgument): + await ctx.send(f"❌ Missing argument: {error.param.name}.", ephemeral=True) + else: + await ctx.send(f"❌ Error: {str(error)}", ephemeral=True) + +@bot.event +async def on_message(message): + if message.author == bot.user: + return + + if isinstance(message.channel, discord.DMChannel): + await message.channel.send("❌ This bot does not accept direct messages. Please use it in a server.") + return + + if message.channel.id == BIND_CHANNEL_ID: + # Delete user messages + await message.delete() + return + +bot.run(DISCORD_BOT_SECRET) \ No newline at end of file