diff --git a/bot.log b/bot.log index 44fed83..c86b3e6 100644 --- a/bot.log +++ b/bot.log @@ -82,3 +82,71 @@ Traceback (most recent call last): File "/Library/Frameworks/Python.framework/Versions/3.13/lib/python3.13/site-packages/aiohttp/connector.py", line 1327, in _wrap_create_connection raise ClientConnectorCertificateError(req.connection_key, exc) from exc aiohttp.client_exceptions.ClientConnectorCertificateError: Cannot connect to host discord.com:443 ssl:True [SSLCertVerificationError: (1, '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:1018)')] +2026-04-22 00:50:03,180 [WARNING] discord.client: PyNaCl is not installed, voice will NOT be supported +2026-04-22 00:50:03,181 [WARNING] discord.client: davey is not installed, voice will NOT be supported +2026-04-22 00:50:03,182 [INFO] discord.client: logging in using static token +2026-04-22 00:50:03,704 [INFO] bot: ✅ Loaded extension: commands.login +2026-04-22 00:50:03,708 [ERROR] bot: ❌ Failed to load commands.profile: Extension 'commands.profile' raised an error: NameError: name 'discord' is not defined +2026-04-22 00:50:03,714 [INFO] bot: ✅ Loaded extension: commands.recent +2026-04-22 00:50:03,910 [INFO] bot: 🔄 Slash commands synced +2026-04-22 00:50:04,352 [WARNING] bot: ⚠️ Bot disconnected from Discord +2026-04-22 00:54:47,504 [WARNING] discord.client: PyNaCl is not installed, voice will NOT be supported +2026-04-22 00:54:47,505 [WARNING] discord.client: davey is not installed, voice will NOT be supported +2026-04-22 00:54:47,506 [INFO] discord.client: logging in using static token +2026-04-22 00:54:47,891 [INFO] bot: ✅ Loaded extension: commands.login +2026-04-22 00:54:47,899 [INFO] bot: ✅ Loaded extension: commands.profile +2026-04-22 00:54:47,900 [INFO] bot: ✅ Loaded extension: commands.recent +2026-04-22 00:54:48,183 [INFO] bot: 🔄 Slash commands synced +2026-04-22 00:54:48,576 [WARNING] bot: ⚠️ Bot disconnected from Discord +2026-04-22 01:02:58,262 [WARNING] discord.client: PyNaCl is not installed, voice will NOT be supported +2026-04-22 01:02:58,263 [WARNING] discord.client: davey is not installed, voice will NOT be supported +2026-04-22 01:02:58,264 [INFO] discord.client: logging in using static token +2026-04-22 01:02:58,661 [INFO] bot: ✅ Loaded extension: commands.login +2026-04-22 01:02:58,661 [INFO] bot: ✅ Loaded extension: commands.profile +2026-04-22 01:02:58,663 [INFO] bot: ✅ Loaded extension: commands.recent +2026-04-22 01:02:58,798 [INFO] bot: 🔄 Slash commands synced +2026-04-22 01:03:01,276 [INFO] discord.gateway: Shard ID None has connected to Gateway (Session ID: 8ffd3e5eaf4edabab0603b0294b5b94f). +2026-04-22 01:03:03,284 [INFO] bot: ✅ Logged in as league of legend bot#3877 (ID: 1494467149186465862) +2026-04-22 01:03:03,284 [INFO] bot: Guilds : 1 +2026-04-22 01:03:03,285 [INFO] bot: Ping : 42ms +2026-04-22 01:03:32,882 [ERROR] discord.app_commands.tree: Ignoring exception in command tree +Traceback (most recent call last): + File "/Library/Frameworks/Python.framework/Versions/3.13/lib/python3.13/site-packages/discord/app_commands/tree.py", line 1138, in wrapper + await self._call(interaction) + File "/Library/Frameworks/Python.framework/Versions/3.13/lib/python3.13/site-packages/discord/app_commands/tree.py", line 1270, in _call + command, options = self._get_app_command_options(data) + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^ + File "/Library/Frameworks/Python.framework/Versions/3.13/lib/python3.13/site-packages/discord/app_commands/tree.py", line 1174, in _get_app_command_options + raise CommandNotFound(name, parents) +discord.app_commands.errors.CommandNotFound: Application command 'ping' not found +2026-04-22 01:03:53,411 [ERROR] discord.app_commands.tree: Ignoring exception in command 'login' +Traceback (most recent call last): + File "/Library/Frameworks/Python.framework/Versions/3.13/lib/python3.13/site-packages/discord/app_commands/tree.py", line 1302, in _call + await command._invoke_with_namespace(interaction, namespace) + File "/Library/Frameworks/Python.framework/Versions/3.13/lib/python3.13/site-packages/discord/app_commands/commands.py", line 883, in _invoke_with_namespace + transformed_values = await self._transform_arguments(interaction, namespace) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/Library/Frameworks/Python.framework/Versions/3.13/lib/python3.13/site-packages/discord/app_commands/commands.py", line 847, in _transform_arguments + raise CommandSignatureMismatch(self) from None +discord.app_commands.errors.CommandSignatureMismatch: The signature for command 'login' is different from the one provided by Discord. This can happen because either your code is out of date or you have not synced the commands with Discord, causing the mismatch in data. It is recommended to sync the command tree to fix this issue. +2026-04-22 03:20:37,191 [WARNING] discord.client: PyNaCl is not installed, voice will NOT be supported +2026-04-22 03:20:37,192 [WARNING] discord.client: davey is not installed, voice will NOT be supported +2026-04-22 03:20:37,193 [INFO] discord.client: logging in using static token +2026-04-22 03:20:37,715 [INFO] bot: ✅ Loaded extension: commands.login +2026-04-22 03:20:37,726 [INFO] bot: ✅ Loaded extension: commands.profile +2026-04-22 03:20:37,731 [INFO] bot: ✅ Loaded extension: commands.recent +2026-04-22 03:20:37,899 [INFO] bot: 🔄 Slash commands synced +2026-04-22 03:20:38,668 [INFO] discord.gateway: Shard ID None has connected to Gateway (Session ID: c6e5a2c0cdf32e5c69cdbabeed40a2d4). +2026-04-22 03:20:40,672 [INFO] bot: ✅ Logged in as league of legend bot#3877 (ID: 1494467149186465862) +2026-04-22 03:20:40,674 [INFO] bot: Guilds : 1 +2026-04-22 03:20:40,674 [INFO] bot: Ping : 45ms +2026-04-22 03:21:03,101 [ERROR] discord.app_commands.tree: Ignoring exception in command 'login' +Traceback (most recent call last): + File "/Library/Frameworks/Python.framework/Versions/3.13/lib/python3.13/site-packages/discord/app_commands/tree.py", line 1302, in _call + await command._invoke_with_namespace(interaction, namespace) + File "/Library/Frameworks/Python.framework/Versions/3.13/lib/python3.13/site-packages/discord/app_commands/commands.py", line 883, in _invoke_with_namespace + transformed_values = await self._transform_arguments(interaction, namespace) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/Library/Frameworks/Python.framework/Versions/3.13/lib/python3.13/site-packages/discord/app_commands/commands.py", line 847, in _transform_arguments + raise CommandSignatureMismatch(self) from None +discord.app_commands.errors.CommandSignatureMismatch: The signature for command 'login' is different from the one provided by Discord. This can happen because either your code is out of date or you have not synced the commands with Discord, causing the mismatch in data. It is recommended to sync the command tree to fix this issue. diff --git a/bot.py b/bot.py index a367973..91165d9 100644 --- a/bot.py +++ b/bot.py @@ -3,7 +3,7 @@ from discord.ext import commands, tasks import asyncio import os import logging -from datetime import datetime +from datetime import datetime, timezone from dotenv import load_dotenv load_dotenv() @@ -52,8 +52,19 @@ class LoLBot(commands.Bot): except Exception as e: log.error(f"❌ Failed to load {ext}: {e}") + # If SERVER_ID is provided, sync specifically to that guild for instant updates + guild_id = os.getenv("SERVER_ID") + if guild_id: + try: + guild = discord.Object(id=int(guild_id)) + self.tree.copy_global_to(guild=guild) + await self.tree.sync(guild=guild) + log.info(f"🔄 Slash commands synced to guild {guild_id}") + except Exception as e: + log.error(f"❌ Failed to sync to guild {guild_id}: {e}") + await self.tree.sync() - log.info("🔄 Slash commands synced") + log.info("🔄 Global slash commands synced") async def on_ready(self): log.info(f"✅ Logged in as {self.user} (ID: {self.user.id})") @@ -127,11 +138,17 @@ bot = LoLBot() @bot.command(name="sync") @commands.is_owner() -async def sync(ctx): - """Force re-sync slash commands (owner only).""" - await bot.tree.sync() - await ctx.send("✅ Slash commands synced.") - log.info("🔄 Manual sync triggered") +async def sync(ctx, guild: str = None): + """Force re-sync slash commands. Use 'guild' to sync to current guild.""" + if guild == "guild": + bot.tree.copy_global_to(guild=ctx.guild) + await bot.tree.sync(guild=ctx.guild) + await ctx.send(f"✅ Slash commands synced to **{ctx.guild.name}**.") + log.info(f"🔄 Manual sync triggered for guild {ctx.guild.id}") + else: + await bot.tree.sync() + await ctx.send("✅ Global slash commands synced (may take an hour).") + log.info("🔄 Manual global sync triggered") @bot.command(name="reload") @@ -147,17 +164,17 @@ async def reload(ctx, extension: str): log.error(f"Reload failed for {extension}: {e}") -@bot.command(name="ping") -async def ping(ctx): +@bot.tree.command(name="ping", description="Check bot latency") +async def ping(interaction: discord.Interaction): """Check bot latency.""" - await ctx.send(f"🏓 Pong! `{round(bot.latency * 1000)}ms`") + await interaction.response.send_message(f"🏓 Pong! `{round(bot.latency * 1000)}ms`") @bot.command(name="status") @commands.is_owner() async def status(ctx): """Show bot status info (owner only).""" - uptime = datetime.utcnow() - bot.start_time if hasattr(bot, "start_time") else "N/A" + uptime = datetime.now(timezone.utc) - bot.start_time if hasattr(bot, "start_time") else "N/A" embed = discord.Embed(title="🤖 Bot Status", color=0x1A78C2) embed.add_field(name="📡 Ping", value=f"{round(bot.latency * 1000)}ms", inline=True) embed.add_field(name="🏠 Guilds", value=str(len(bot.guilds)), inline=True) @@ -175,7 +192,7 @@ async def main(): return async with bot: - bot.start_time = datetime.utcnow() + bot.start_time = datetime.now(timezone.utc) await bot.start(token) diff --git a/bot_output.log b/bot_output.log new file mode 100644 index 0000000..6bb704a --- /dev/null +++ b/bot_output.log @@ -0,0 +1,30 @@ +2026-04-22 00:54:47,504 [WARNING] discord.client: PyNaCl is not installed, voice will NOT be supported +2026-04-22 00:54:47,505 [WARNING] discord.client: davey is not installed, voice will NOT be supported +/Users/simonzheng/Documents/vscode/lolBot/bot.py:178: DeprecationWarning: datetime.datetime.utcnow() is deprecated and scheduled for removal in a future version. Use timezone-aware objects to represent datetimes in UTC: datetime.datetime.now(datetime.UTC). + bot.start_time = datetime.utcnow() +2026-04-22 00:54:47,506 [INFO] discord.client: logging in using static token +2026-04-22 00:54:47,891 [INFO] bot: ✅ Loaded extension: commands.login +2026-04-22 00:54:47,899 [INFO] bot: ✅ Loaded extension: commands.profile +2026-04-22 00:54:47,900 [INFO] bot: ✅ Loaded extension: commands.recent +2026-04-22 00:54:48,183 [INFO] bot: 🔄 Slash commands synced +2026-04-22 00:54:48,576 [WARNING] bot: ⚠️ Bot disconnected from Discord +Traceback (most recent call last): + File "/Users/simonzheng/Documents/vscode/lolBot/bot.py", line 182, in + asyncio.run(main()) + ~~~~~~~~~~~^^^^^^^^ + File "/Library/Frameworks/Python.framework/Versions/3.13/lib/python3.13/asyncio/runners.py", line 194, in run + return runner.run(main) + ~~~~~~~~~~^^^^^^ + File "/Library/Frameworks/Python.framework/Versions/3.13/lib/python3.13/asyncio/runners.py", line 118, in run + return self._loop.run_until_complete(task) + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^ + File "/Library/Frameworks/Python.framework/Versions/3.13/lib/python3.13/asyncio/base_events.py", line 720, in run_until_complete + return future.result() + ~~~~~~~~~~~~~^^ + File "/Users/simonzheng/Documents/vscode/lolBot/bot.py", line 179, in main + await bot.start(token) + File "/Library/Frameworks/Python.framework/Versions/3.13/lib/python3.13/site-packages/discord/client.py", line 851, in start + await self.connect(reconnect=reconnect) + File "/Library/Frameworks/Python.framework/Versions/3.13/lib/python3.13/site-packages/discord/client.py", line 775, in connect + raise PrivilegedIntentsRequired(exc.shard_id) from None +discord.errors.PrivilegedIntentsRequired: Shard ID None is requesting privileged intents that have not been explicitly enabled in the developer portal. It is recommended to go to https://discord.com/developers/applications/ and explicitly enable the privileged intents within your application's page. If this is not possible, then consider disabling the privileged intents instead. diff --git a/commands/__pycache__/login.cpython-313.pyc b/commands/__pycache__/login.cpython-313.pyc new file mode 100644 index 0000000..5cb66b2 Binary files /dev/null and b/commands/__pycache__/login.cpython-313.pyc differ diff --git a/commands/__pycache__/profile.cpython-313.pyc b/commands/__pycache__/profile.cpython-313.pyc new file mode 100644 index 0000000..a712673 Binary files /dev/null and b/commands/__pycache__/profile.cpython-313.pyc differ diff --git a/commands/__pycache__/recent.cpython-313.pyc b/commands/__pycache__/recent.cpython-313.pyc new file mode 100644 index 0000000..c9f9ed0 Binary files /dev/null and b/commands/__pycache__/recent.cpython-313.pyc differ diff --git a/commands/login.py b/commands/login.py index f0706e5..d2761c9 100644 --- a/commands/login.py +++ b/commands/login.py @@ -11,9 +11,7 @@ class LoginCog(commands.Cog): def __init__(self, bot): self.bot = bot - @app_commands.command(name="login", description="Link your Riot account") - @app_commands.describe(summoner="Your Riot ID (e.g. Name#TAG)") - async def login(self, interaction: discord.Interaction, summoner: str): + async def _handle_login(self, interaction: discord.Interaction, summoner: str): await interaction.response.defer(ephemeral=True) if "#" not in summoner: @@ -53,6 +51,16 @@ class LoginCog(commands.Cog): ephemeral=True ) + @app_commands.command(name="login", description="Link your Riot account") + @app_commands.describe(summoner="Your Riot ID (e.g. Name#TAG)") + async def login(self, interaction: discord.Interaction, summoner: str): + await self._handle_login(interaction, summoner) + + @app_commands.command(name="signin", description="Link your Riot account") + @app_commands.describe(summoner="Your Riot ID (e.g. Name#TAG)") + async def signin(self, interaction: discord.Interaction, summoner: str): + await self._handle_login(interaction, summoner) + async def setup(bot): await bot.add_cog(LoginCog(bot)) diff --git a/commands/profile.py b/commands/profile.py index bd08bb7..844ea27 100644 --- a/commands/profile.py +++ b/commands/profile.py @@ -1,3 +1,4 @@ +import discord from discord import app_commands from discord.ext import commands import aiohttp @@ -23,7 +24,7 @@ class ProfileCog(commands.Cog): #check if user login if not user: await interaction.followup.send( - "❌ You haven't linked your Riot account yet. Use `/login` first." + "❌ You haven't linked your Riot account yet. Use `/signin` first." ) return diff --git a/commands/recent.py b/commands/recent.py index 4cc62a4..5ef30b0 100644 --- a/commands/recent.py +++ b/commands/recent.py @@ -30,7 +30,7 @@ class RecentCog(commands.Cog): #check if user if not user: await interaction.followup.send( - "❌ You haven't linked your account yet. Use `/login` first." + "❌ You haven't linked your account yet. Use `/signin` first." ) return diff --git a/database/__pycache__/db.cpython-313.pyc b/database/__pycache__/db.cpython-313.pyc new file mode 100644 index 0000000..a67cf61 Binary files /dev/null and b/database/__pycache__/db.cpython-313.pyc differ diff --git a/services/riot_api.py b/services/riot_api.py index f0fffe2..3b277e3 100644 --- a/services/riot_api.py +++ b/services/riot_api.py @@ -67,7 +67,7 @@ async def get_rank(summoner_id: str) -> dict | None: ) -async def get_match_ids(puuid: str, cont: int = 5) -> dict | None: +async def get_match_ids(puuid: str, count: int = 5) -> dict | None: return await riot_get( f"https://{CLUSTER}.api.riotgames.com" f"/lol/match/v5/matches/by-puuid/{puuid}/ids?count={count}" diff --git a/test_all_commands.py b/test_all_commands.py new file mode 100644 index 0000000..ee50af7 --- /dev/null +++ b/test_all_commands.py @@ -0,0 +1,120 @@ +import unittest +import asyncio +from unittest.mock import AsyncMock, MagicMock, patch +import discord +from commands.login import LoginCog +from commands.profile import ProfileCog +from commands.recent import RecentCog + +class TestCommands(unittest.IsolatedAsyncioTestCase): + def setUp(self): + self.bot = MagicMock() + self.login_cog = LoginCog(self.bot) + self.profile_cog = ProfileCog(self.bot) + self.recent_cog = RecentCog(self.bot) + + async def test_login_success(self): + interaction = AsyncMock(spec=discord.Interaction) + interaction.user.id = 12345 + interaction.response.defer = AsyncMock() + interaction.followup.send = AsyncMock() + + with patch("commands.login.save_user") as mock_save, \ + patch("aiohttp.ClientSession.get") as mock_get: + + # Mock Riot Account API response + mock_resp = AsyncMock() + mock_resp.status = 200 + mock_resp.json.return_value = {"puuid": "fake_puuid"} + mock_get.return_value.__aenter__.return_value = mock_resp + + await self.login_cog.login.callback(self.login_cog, interaction, "TestUser#TAG") + + interaction.response.defer.assert_called_once() + mock_save.assert_called_once_with("12345", "fake_puuid", "TestUser", "TAG") + interaction.followup.send.assert_called_once() + self.assertIn("Linked **TestUser#TAG**", interaction.followup.send.call_args[0][0]) + + async def test_login_not_found(self): + interaction = AsyncMock(spec=discord.Interaction) + interaction.response.defer = AsyncMock() + interaction.followup.send = AsyncMock() + + with patch("aiohttp.ClientSession.get") as mock_get: + mock_resp = AsyncMock() + mock_resp.status = 404 + mock_get.return_value.__aenter__.return_value = mock_resp + + await self.login_cog.login.callback(self.login_cog, interaction, "NonExistent#TAG") + + interaction.followup.send.assert_called_once() + self.assertIn("Summoner not found", interaction.followup.send.call_args[0][0]) + + async def test_profile_not_linked(self): + interaction = AsyncMock(spec=discord.Interaction) + interaction.user.id = 12345 + interaction.response.defer = AsyncMock() + interaction.followup.send = AsyncMock() + + with patch("commands.profile.get_user", return_value=None): + await self.profile_cog.profile.callback(self.profile_cog, interaction) + + interaction.followup.send.assert_called_once() + self.assertIn("haven't linked your Riot account", interaction.followup.send.call_args[0][0]) + + async def test_profile_success(self): + interaction = AsyncMock(spec=discord.Interaction) + interaction.user.id = 12345 + interaction.response.defer = AsyncMock() + interaction.followup.send = AsyncMock() + + # (discord_id, puuid, game_name, tag_line) + mock_user = ("12345", "fake_puuid", "TestUser", "TAG") + + with patch("commands.profile.get_user", return_value=mock_user), \ + patch("commands.profile.fetch") as mock_fetch: + + # Mock Summoner Data + mock_fetch.side_effect = [ + {"id": "summoner_id", "summonerLevel": 100}, # Summoner info + [{"queueType": "RANKED_SOLO_5x5", "tier": "GOLD", "rank": "I", "leaguePoints": 50, "wins": 10, "losses": 5}] # League info + ] + + await self.profile_cog.profile.callback(self.profile_cog, interaction) + + interaction.followup.send.assert_called_once() + embed = interaction.followup.send.call_args[1]["embed"] + self.assertEqual(embed.title, "📊 TestUser#TAG") + self.assertIn("Gold I", embed.fields[1].value) + + async def test_recent_success(self): + interaction = AsyncMock(spec=discord.Interaction) + interaction.user.id = 12345 + interaction.response.defer = AsyncMock() + interaction.followup.send = AsyncMock() + + mock_user = ("12345", "fake_puuid", "TestUser", "TAG") + + with patch("commands.recent.get_user", return_value=mock_user), \ + patch("commands.recent.fetch") as mock_fetch, \ + patch("commands.recent.fetch_all") as mock_fetch_all: + + mock_fetch.return_value = ["match1"] + mock_fetch_all.return_value = [{ + "metadata": {"matchId": "match1"}, + "info": { + "participants": [{"puuid": "fake_puuid", "win": True, "championName": "Lux", "kills": 10, "deaths": 2, "assists": 15, "totalMinionsKilled": 100, "neutralMinionsKilled": 10}], + "gameDuration": 1800, + "queueId": 420 + } + }] + + await self.recent_cog.recent.callback(self.recent_cog, interaction, count=1) + + interaction.followup.send.assert_called_once() + embeds = interaction.followup.send.call_args[1]["embeds"] + self.assertEqual(len(embeds), 1) + self.assertIn("Victory — Lux", embeds[0].title) + +if __name__ == "__main__": + unittest.main() diff --git a/user.db b/user.db new file mode 100644 index 0000000..82db6d1 Binary files /dev/null and b/user.db differ