From 26e00e76e7a3eb0149402d72472e546405908f41 Mon Sep 17 00:00:00 2001 From: strictshot Date: Mon, 29 Jun 2026 22:58:22 -0400 Subject: [PATCH 1/3] features added, have to test and checkmark acceptance criteria later. --- README.md | 8 +-- src/apps/discord_bot.py | 97 ++++++++++++++++++++++++++++++++-- tests/apps/test_discord_bot.py | 27 ++++++++++ 3 files changed, 126 insertions(+), 6 deletions(-) create mode 100644 tests/apps/test_discord_bot.py diff --git a/README.md b/README.md index 0114494..b7b3a63 100644 --- a/README.md +++ b/README.md @@ -162,11 +162,13 @@ You'll need your own throwaway Discord server and bot to develop against: 1. In the [Discord Developer Portal](https://discord.com/developers/applications), create an application, then under **Bot** click **Reset Token** and copy the token. No privileged intents are required — `/ask` uses default intents. -2. Under **OAuth2 → URL Generator**, select the `bot` and `applications.commands` +2. ADD HERE (rough for now) Scroll down and check the permissions the bot needs: **Send Messages**, + **View Channel** and **Read Message History**. +3. Under **OAuth2 → URL Generator**, select the `bot` and `applications.commands` scopes, open the generated URL, and invite the bot to a server you own. -3. Enable **Settings → Advanced → Developer Mode**, then right-click your server +4. Enable **Settings → Advanced → Developer Mode**, then right-click your server icon → **Copy Server ID**. -4. Add both values to your `.env`: +5. Add both values to your `.env`: ``` DISCORD_BOT_TOKEN=your_token_here diff --git a/src/apps/discord_bot.py b/src/apps/discord_bot.py index a412ea3..e90f4dc 100644 --- a/src/apps/discord_bot.py +++ b/src/apps/discord_bot.py @@ -22,6 +22,41 @@ async def on_ready(self) -> None: await self.tree.sync(guild=guild) log.info("discord_ready", self_user=self.user, guild=settings.discord_guild_id) + async def on_message(self, message) -> None: + # runs only in servers + if message.guild is None: + return + + # ignores other bots and itself + if message.author.bot: + return + + bot_standard_mention = f"<@{self.user.id}>" + bot_nickname_mention = f"<@!{self.user.id}>" + + # triggering on explicit mention + if bot_standard_mention in message.content or bot_nickname_mention in message.content: + question = await question_assembler(message) + + # if question is just empty/non-text + if question == "": + return + + async with message.channel.typing(): + answer = await ask(question=question) + + try: + await message.reply(answer) + except Exception as e: + await message.reply("❌ I couldn't process this request, please try again later.") + log.error("discord_ask_failed", error=str(e)) + return + + content = render_answer_content(answer) + await message.reply(content) + + await self.process_commands(message) + client = CsAssistantBot() @@ -41,12 +76,68 @@ async def ask_command(interaction: discord.Interaction, question: str) -> None: log.error("discord_ask_failed", interaction_id=interaction.id, error=str(e)) return + # content = answer.text + # if answer.sources: + # content += "\n\n**Sources:**\n" + "\n".join(f"• {source.url}" for source in answer.sources) + content = await render_answer_content(answer) + await interaction.followup.send(content) + log.info("discord_ask_completed", interaction_id=interaction.id) + + +async def question_assembler(message) -> str: + # stripping bot mention + # mention_text = await strip_bot_mention(message) + + # if message is reply to a question + referenced_text = "" + if message.reference and message.reference.message_id: + try: + referenced_message = await message.channel.fetch_message(message.reference.message_id) + referenced_text = referenced_message.content + except discord.NotFound: + log.info( + "No referenced text. Not Found.", + channel_id=message.channel.id, + channel_name=message.channel.name, + guild_id=message.guild.id, + target_message_id=message.reference.message_id, + ) + except discord.Forbidden: + log.warning( + "No referenced text. Forbidden.", + channel_id=message.channel.id, + channel_name=message.channel.name, + guild_id=message.guild.id, + target_message_id=message.reference.message_id, + ) + + return question_assembler_helper( + content=message.content, + bot_id=message.guild.me.id, + referenced_text=referenced_text, + ) + + +def question_assembler_helper(*, content: str, bot_id: int, referenced_text: str = "") -> str: + mention_text = strip_bot_mention(content, bot_id) + + if referenced_text: + return referenced_text + "\n\n" + mention_text + return mention_text + + +def strip_bot_mention(content: str, bot_id: int) -> str: + mention_text = content + for x in {f"<@{bot_id}>", f"<@!{bot_id}>"}: + mention_text = mention_text.replace(x, "") + return mention_text.strip() + + +async def render_answer_content(answer) -> str: content = answer.text if answer.sources: content += "\n\n**Sources:**\n" + "\n".join(f"• {source.url}" for source in answer.sources) - - await interaction.followup.send(content) - log.info("discord_ask_completed", interaction_id=interaction.id) + return content if __name__ == "__main__": diff --git a/tests/apps/test_discord_bot.py b/tests/apps/test_discord_bot.py new file mode 100644 index 0000000..9258e2f --- /dev/null +++ b/tests/apps/test_discord_bot.py @@ -0,0 +1,27 @@ +from src.apps.discord_bot import question_assembler_helper, strip_bot_mention + +BOT_ID = 123456789 + + +def test_plain_mention(): + message = f"<@{BOT_ID}> test question" + assert (strip_bot_mention(message, BOT_ID)) == "test question" + + +def test_both_mentions(): + message = f"<@{BOT_ID}> <@!{BOT_ID}> test question" + assert (strip_bot_mention(message, BOT_ID)) == "test question" + + +def test_concatenate_reference_and_original(): + message = f"<@{BOT_ID}> test question" + referenced_text = "Original text" + assert ( + question_assembler_helper(content=message, bot_id=BOT_ID, referenced_text=referenced_text) + == "Original text\n\ntest question" + ) + + +def test_empty_response(): + message = f"<@{BOT_ID}>" + assert (question_assembler_helper(content=message, bot_id=BOT_ID)) == "" From ce2b9c24bf89f76af6dc2d879b058b53b51e4176 Mon Sep 17 00:00:00 2001 From: strictshot Date: Tue, 30 Jun 2026 16:29:23 -0400 Subject: [PATCH 2/3] passed discord testing + test cases --- src/apps/discord_bot.py | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/src/apps/discord_bot.py b/src/apps/discord_bot.py index e90f4dc..480dafc 100644 --- a/src/apps/discord_bot.py +++ b/src/apps/discord_bot.py @@ -45,18 +45,15 @@ async def on_message(self, message) -> None: async with message.channel.typing(): answer = await ask(question=question) + content = await render_answer_content(answer) + try: - await message.reply(answer) + await message.reply(content) except Exception as e: await message.reply("❌ I couldn't process this request, please try again later.") log.error("discord_ask_failed", error=str(e)) return - content = render_answer_content(answer) - await message.reply(content) - - await self.process_commands(message) - client = CsAssistantBot() @@ -76,18 +73,12 @@ async def ask_command(interaction: discord.Interaction, question: str) -> None: log.error("discord_ask_failed", interaction_id=interaction.id, error=str(e)) return - # content = answer.text - # if answer.sources: - # content += "\n\n**Sources:**\n" + "\n".join(f"• {source.url}" for source in answer.sources) content = await render_answer_content(answer) await interaction.followup.send(content) log.info("discord_ask_completed", interaction_id=interaction.id) async def question_assembler(message) -> str: - # stripping bot mention - # mention_text = await strip_bot_mention(message) - # if message is reply to a question referenced_text = "" if message.reference and message.reference.message_id: From 7ec112e8671c0794cbfadd53490e24386b65151b Mon Sep 17 00:00:00 2001 From: strictshot Date: Sat, 4 Jul 2026 21:33:55 +0000 Subject: [PATCH 3/3] Fix typo in bot permissions instructions --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b7b3a63..e8d6697 100644 --- a/README.md +++ b/README.md @@ -162,7 +162,7 @@ You'll need your own throwaway Discord server and bot to develop against: 1. In the [Discord Developer Portal](https://discord.com/developers/applications), create an application, then under **Bot** click **Reset Token** and copy the token. No privileged intents are required — `/ask` uses default intents. -2. ADD HERE (rough for now) Scroll down and check the permissions the bot needs: **Send Messages**, +2. Scroll down and check the permissions the bot needs: **Send Messages**, **View Channel** and **Read Message History**. 3. Under **OAuth2 → URL Generator**, select the `bot` and `applications.commands` scopes, open the generated URL, and invite the bot to a server you own.