From 643b2de45bc5239ae6fd791abde9a0b86962c152 Mon Sep 17 00:00:00 2001 From: Corban-Lee <77944149+Corban-Lee@users.noreply.github.com> Date: Mon, 12 Jun 2023 21:13:30 +0100 Subject: [PATCH] basic bot --- .gitignore | 3 + src/bot.py | 217 ++++++++++++++++++++++++++++++++++++++++++++++++++++ src/main.py | 16 ++++ 3 files changed, 236 insertions(+) create mode 100644 src/bot.py create mode 100644 src/main.py diff --git a/.gitignore b/.gitignore index d9005f2..0e26039 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +# Stores the Bot token +TOKEN + # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] diff --git a/src/bot.py b/src/bot.py new file mode 100644 index 0000000..34470f6 --- /dev/null +++ b/src/bot.py @@ -0,0 +1,217 @@ +"""The discord bot for the application""" + +import time +from datetime import datetime + +import aiohttp +import discord +from bs4 import BeautifulSoup as bs4 +from discord import Intents, Interaction, app_commands +from discord.ext import commands +from bbc_feeds import news + + +class DiscordBot(commands.Bot): + + def __init__(self): + super().__init__(command_prefix="-", intents=Intents.all()) + + async def sync_app_commands(self): + """Sync application commands""" + + await self.wait_until_ready() + await self.tree.sync() + print("app commands synced") + + async def on_ready(self): + """When the bot is ready""" + + await self.add_cog(CommandsCog(self)) + await self.add_cog(ErrorCog(self)) + await self.sync_app_commands() + +class CommandsCog(commands.Cog): + + def __init__(self, bot): + self.bot = bot + super().__init__() + + async def story_to_embed(self, story) -> discord.Embed: + """ + Returns a discord.Embed object representing the given story. + + Parameters + ---------- + story : _type_ + _description_ + + Returns + ------- + discord.Embed + _description_ + """ + async with aiohttp.ClientSession() as session: + async with session.get(story.link) as response: + html = await response.text() + + soup = bs4(html, "html.parser") + image_src = soup.select_one("picture > img").get("src") + + embed = discord.Embed( + colour=discord.Colour.from_rgb(255, 0, 0), + title=story.title, + description=story.summary, + url=story.link, + timestamp=datetime.fromtimestamp(time.mktime(story.published_parsed)), + ) + embed.set_image(url=image_src) + return embed + + group = app_commands.Group( + name="news", + description="BBC News" + ) + + @group.command(name="latest") + async def get_latest_news(self, inter: Interaction, limit: int = 1): + """ + Provides the latest developments from BBC News. + """ + + await inter.response.defer() + stories = news().all(limit=limit) + for story in stories: + embed = await self.story_to_embed(story) + await inter.followup.send(embed=embed) + + +class ErrorCog(commands.Cog): + """Error handling cog.""" + + __slots__ = () + default_err_msg = "I'm sorry, but I've encountered an " \ + "error while processing your command." + + def __init__(self, bot): + super().__init__() + self.bot = bot + + # Register the error handler + bot.tree.error(coro = self._dispatch_to_app_command_handler) + + def trace_error(self, error: Exception): + print(f"{type(error).__name__} {error}") + raise error + + async def _dispatch_to_app_command_handler( + self, + inter: Interaction, + error: app_commands.AppCommandError + ): + """Dispatches the error to the app command handler""" + + self.bot.dispatch("app_command_error", inter, error) + + async def _respond_to_interaction(self, inter: Interaction) -> bool: + """Respond to an interaction with an error message""" + + try: + await inter.response.send_message( + self.default_err_msg, + ephemeral=True + ) + except discord.InteractionResponded: + return + + @commands.Cog.listener("on_app_command_error") + async def get_app_command_error( + self, + inter: Interaction, + error: app_commands.AppCommandError + ): + """Handles the application command error. + + Responds with the appropriate error message. + """ + + try: + # Send the default error message and create an edit + # shorthand to add more details to the message once + # we've figured out what the error is. + print(error.with_traceback(None)) + await self._respond_to_interaction(inter) + edit = lambda x: inter.edit_original_response(content=x) + + raise error + + except app_commands.CommandInvokeError as _err: + + # The interaction has already been responded to. + if isinstance( + _err.original, + discord.InteractionResponded + ): + await edit(_err.original) + return + + # Some other error occurred while invoking the command. + await edit( + f"`{type(_err.original).__name__}` " \ + f": {_err.original}" + ) + + except app_commands.CheckFailure as _err: + + # The command is still on cooldown. + if isinstance( + _err, + app_commands.CommandOnCooldown + ): + await edit( + f"Woah, slow down! This command is on cooldown, " \ + f"wait `{str(_err).split(' ')[7]}` !" + ) + return + + if isinstance( + _err, + app_commands.MissingPermissions + ): + await edit( + "You don't have the required permissions to " \ + "run this command!" + ) + return + + if isinstance( + _err, + app_commands.BotMissingPermissions + ): + await edit( + "I don't have the required permissions to " \ + "run this command! Please ask an admin to " \ + "grant me the required permissions." + ) + return + + # A different check has failed. + await edit(f"`{type(_err).__name__}` : {_err}") + + except app_commands.CommandNotFound: + + # The command could not be found. + await edit( + f"I couldn't find the command you were looking for... " + "\nThis is probably a discord bug related to " \ + "desynchronization between my commands and discord's " \ + "servers. Please try again later." + ) + + except Exception as _err: + # Caught here: + # app_commands.TransformerError + # app_commands.CommandLimitReached + # app_commands.CommandAlreadyRegistered + # app_commands.CommandSignatureMismatch + + self.trace_error(_err) diff --git a/src/main.py b/src/main.py new file mode 100644 index 0000000..2b944ef --- /dev/null +++ b/src/main.py @@ -0,0 +1,16 @@ +"""Entry point for the application.""" + +import asyncio +from bot import DiscordBot + + +async def main(): + + with open("TOKEN", "r") as token_file: + token = token_file.read() + + await DiscordBot().start(token) + + +if __name__ == "__main__": + asyncio.run(main())