diff --git a/bases/bot_detector/discord_bot/cogs/rsn_linking_commands.py b/bases/bot_detector/discord_bot/cogs/rsn_linking_commands.py index 98b2f81..09a3b3a 100644 --- a/bases/bot_detector/discord_bot/cogs/rsn_linking_commands.py +++ b/bases/bot_detector/discord_bot/cogs/rsn_linking_commands.py @@ -72,6 +72,28 @@ async def install_plugin_msg(self) -> discord.Embed: ) return embed + async def set_primary_msg(self, name: str) -> discord.Embed: + embed = discord.Embed(title=f"Setting '{name}' as Primary:", color=0x00FF00) + embed.add_field( + name="STATUS", + inline=False, + value=cleandoc( + f""" + '{name}' is now your primary account. + """ + ), + ) + embed.add_field( + name="INFO", + inline=False, + value=cleandoc( + """ + You can change your primary at any time by typing '/set_primary '. + """ + ), + ) + return embed + async def unlink_msg(self, name: str, role_removed: bool) -> discord.Embed: embed = discord.Embed(title=f"Unlinking '{name}':", color=0xFFA500) embed.add_field( @@ -349,3 +371,59 @@ async def unlink(self, ctx: Context, *, name: str): embed = await self.unlink_msg(name=name, role_removed=role_removed) await ctx.reply(embed=embed) + + @commands.hybrid_command(name="set_primary") + async def set_primary(self, ctx: Context, *, name: str): + logger.debug( + f"{ctx.author.name=}, {ctx.author.id=}, Requesting set_primary, {name=}" + ) + + if not name: + await ctx.reply( + "Please specify the RSN of the account you'd wish to set as primary. /set_primary " + ) + return + + if not string_processing.is_valid_rsn(name): + await ctx.reply(f"{name} isn't a valid Runescape user name.") + return + + assert self.deps.legacy_api is not None + player = await self.deps.legacy_api.get_player(player_name=name) + player_id = player.get("id") if player else None + if player_id is None: + await ctx.reply(f"No player found for '{name}'.") + return + + session_factory = self.deps.get_session_factory() + discord_id = str(ctx.author.id) + async with session_factory() as session: + linked = await self.verification_repo.get_linked_accounts( + async_session=session, + discord_id=discord_id, + ) + match = [ + account for account in linked if account.Player_id == int(player_id) + ] + + if not match: + await ctx.reply(f"'{name}' is not linked to your Discord account.") + return + + if match[0].primary_rsn == 1: + await ctx.reply(f"'{name}' is already your primary account.") + return + + updated = await self.verification_repo.set_primary_rsn( + async_session=session, + discord_id=discord_id, + player_id=int(player_id), + is_primary=True, + ) + + if not updated: + await ctx.reply(f"Failed to set '{name}' as your primary account.") + return + + embed = await self.set_primary_msg(name=name) + await ctx.reply(embed=embed) diff --git a/test/bases/bot_detector/discord_bot/test_rsn_linking_commands.py b/test/bases/bot_detector/discord_bot/test_rsn_linking_commands.py index ef55bb8..258a5d7 100644 --- a/test/bases/bot_detector/discord_bot/test_rsn_linking_commands.py +++ b/test/bases/bot_detector/discord_bot/test_rsn_linking_commands.py @@ -53,6 +53,10 @@ async def _invoke(cog: rsnLinkingCommands, ctx: AsyncMock, name: str): return await type(cog).unlink.callback(cog, ctx, name=name) +async def _invoke_set_primary(cog: rsnLinkingCommands, ctx: AsyncMock, name: str): + return await type(cog).set_primary.callback(cog, ctx, name=name) + + def _verified_role() -> MagicMock: role = MagicMock(spec=discord.Role) role.id = VERIFIED_ROLE_ID @@ -193,3 +197,125 @@ async def test_unlink_skips_role_lookup_outside_guild(): cog.verification_repo.get_linked_accounts.assert_awaited_once() ctx.author.remove_roles.assert_not_awaited() + + +def _linked_account(player_id: int, primary_rsn: int = 0) -> MagicMock: + account = MagicMock() + account.Player_id = player_id + account.primary_rsn = primary_rsn + return account + + +@pytest.mark.asyncio +async def test_set_primary_success_replies_embed(): + cog = _make_cog() + ctx = _make_ctx() + + cog.deps.legacy_api.get_player = AsyncMock( + return_value={"id": 42, "name": "Zezima"} + ) + cog.deps.get_session_factory = MagicMock(return_value=_make_session_factory()) + cog.verification_repo.get_linked_accounts = AsyncMock( + return_value=[_linked_account(player_id=42)] + ) + cog.verification_repo.set_primary_rsn = AsyncMock(return_value=True) + + await _invoke_set_primary(cog, ctx, "Zezima") + + cog.verification_repo.set_primary_rsn.assert_awaited_once() + kwargs = cog.verification_repo.set_primary_rsn.call_args.kwargs + assert kwargs["discord_id"] == "12345" + assert kwargs["player_id"] == 42 + assert kwargs["is_primary"] is True + ctx.reply.assert_awaited_once() + embed = ctx.reply.call_args.kwargs["embed"] + assert embed.title == "Setting 'Zezima' as Primary:" + fields = {f.name for f in embed.fields} + assert "STATUS" in fields + + +@pytest.mark.asyncio +async def test_set_primary_invalid_rsn_rejected(): + cog = _make_cog() + ctx = _make_ctx() + + await _invoke_set_primary(cog, ctx, "this name is way too long") + + ctx.reply.assert_awaited_once_with( + "this name is way too long isn't a valid Runescape user name." + ) + cog.deps.legacy_api.get_player.assert_not_called() + + +@pytest.mark.asyncio +async def test_set_primary_player_not_found(): + cog = _make_cog() + ctx = _make_ctx() + + cog.deps.legacy_api.get_player = AsyncMock(return_value=None) + + await _invoke_set_primary(cog, ctx, "Zezima") + + ctx.reply.assert_awaited_once_with("No player found for 'Zezima'.") + cog.verification_repo.set_primary_rsn.assert_not_called() + + +@pytest.mark.asyncio +async def test_set_primary_not_linked_rejected(): + cog = _make_cog() + ctx = _make_ctx() + + cog.deps.legacy_api.get_player = AsyncMock( + return_value={"id": 42, "name": "Zezima"} + ) + cog.deps.get_session_factory = MagicMock(return_value=_make_session_factory()) + cog.verification_repo.get_linked_accounts = AsyncMock( + return_value=[_linked_account(player_id=7)] + ) + + await _invoke_set_primary(cog, ctx, "Zezima") + + ctx.reply.assert_awaited_once_with( + "'Zezima' is not linked to your Discord account." + ) + cog.verification_repo.set_primary_rsn.assert_not_called() + + +@pytest.mark.asyncio +async def test_set_primary_already_primary_rejected(): + cog = _make_cog() + ctx = _make_ctx() + + cog.deps.legacy_api.get_player = AsyncMock( + return_value={"id": 42, "name": "Zezima"} + ) + cog.deps.get_session_factory = MagicMock(return_value=_make_session_factory()) + cog.verification_repo.get_linked_accounts = AsyncMock( + return_value=[_linked_account(player_id=42, primary_rsn=1)] + ) + + await _invoke_set_primary(cog, ctx, "Zezima") + + ctx.reply.assert_awaited_once_with("'Zezima' is already your primary account.") + cog.verification_repo.set_primary_rsn.assert_not_called() + + +@pytest.mark.asyncio +async def test_set_primary_update_failure_replies_error(): + cog = _make_cog() + ctx = _make_ctx() + + cog.deps.legacy_api.get_player = AsyncMock( + return_value={"id": 42, "name": "Zezima"} + ) + cog.deps.get_session_factory = MagicMock(return_value=_make_session_factory()) + cog.verification_repo.get_linked_accounts = AsyncMock( + return_value=[_linked_account(player_id=42)] + ) + cog.verification_repo.set_primary_rsn = AsyncMock(return_value=False) + + await _invoke_set_primary(cog, ctx, "Zezima") + + ctx.reply.assert_awaited_once_with( + "Failed to set 'Zezima' as your primary account." + )