diff --git a/src/cogs/anime.py b/src/cogs/anime.py index b8dab21..3269fa1 100644 --- a/src/cogs/anime.py +++ b/src/cogs/anime.py @@ -10,8 +10,7 @@ from cogs.utils import embed_templates class Anime(commands.Cog): - def __init__(self, bot: commands.Bot): - self.bot = bot + """View information about different media on Anilist""" anilist = app_commands.Group(name='anilist', description='Hent informasjon fra Anilist') anilist_profile = app_commands.Group(parent=anilist, name='profil', description='Hent informasjon om en bruker på Anilist') @@ -22,12 +21,13 @@ class Anime(commands.Cog): Parameters ---------- - color: The color string returned from the Anilist API + color (str): The color string returned from the Anilist API Returns ---------- (int): The color in hex for use in discord embeds """ + colors = { 'blue': 0x3db4f2, 'purple': 0xc063ff, @@ -37,6 +37,7 @@ class Anime(commands.Cog): 'green': 0x4CCA51, 'gray': 0x677B94 } + return colors.get(color, 0x677B94) def __convert_media_format(self, media_format: str) -> str: @@ -45,12 +46,13 @@ class Anime(commands.Cog): Parameters ---------- - media_format: The english media format name returned from the Anilist API + media_format (str): The english media format name returned from the Anilist API Returns ---------- (str): The media format name in norwegian. If a translation doesn't exist it returns the english name """ + media_formats = { 'TV': 'TV-Serie', 'TV_SHORT': 'Kort TV-Serie', @@ -69,12 +71,13 @@ class Anime(commands.Cog): Parameters ---------- - language_name: The english language name returned from the Anilist API + language_name (str): The english language name returned from the Anilist API Returns ---------- (str): The language name in norwegian. If a translation doesn't exist it returns the english name """ + languages = { 'JAPANESE': 'Japansk', 'ENGLISH': 'Engelsk', @@ -87,6 +90,7 @@ class Anime(commands.Cog): 'HEBREW': 'Hebraisk', 'HUNGARIAN': 'Ungarsk' } + return languages.get(language_name, language_name) def __convert_role_names(self, role_name: str) -> str: @@ -95,7 +99,7 @@ class Anime(commands.Cog): Parameters ---------- - role_name: The english role name returned from the Anilist API + role_name (str): The english role name returned from the Anilist API Returns ---------- @@ -118,18 +122,20 @@ class Anime(commands.Cog): Parameters ---------- - status: The english status name returned from the Anilist API + status (str): The english status name returned from the Anilist API Returns ---------- (str): The status name in norwegian. If a translation doesn't exist it returns the english name """ + statuses = { 'FINISHED': 'Fullt utgitt', 'RELEASING': 'Pågående', 'NOT_YET_RELEASED': 'Ikke utgitt enda', 'CANCELLED': 'Kansellert' } + return statuses.get(status, status) def __remove_html(self, text: str) -> str: @@ -144,8 +150,10 @@ class Anime(commands.Cog): ---------- (str): The input string without HTML tags """ + compiled = re.compile(r'<.*?>') clean_text = re.sub(compiled, '', text) + return clean_text async def __request_anilist(self, interaction: discord.Interaction, query: str, variables: dict, key: str) -> tuple[dict, str] | tuple[None, None]: @@ -154,14 +162,16 @@ class Anime(commands.Cog): Parameters ---------- - query: The GraphQL query - variables: The variables for the query - key: The key to return from the response + interaction (discord.Interaction): Slash command context object + query (str): The GraphQL query + variables (str): The variables for the query + key (str): The key to return from the response Returns ---------- - (dict): The response from the Anilist API + (tuple[dict, str] | tuple[None, None]): The response from the Anilist API """ + try: url = 'https://graphql.anilist.co' response = requests.post(url, json={'query': query, 'variables': variables}, timeout=10) @@ -181,12 +191,13 @@ class Anime(commands.Cog): Parameters ---------- - media: The list of media returned from the Anilist API + media (list): The list of media returned from the Anilist API Returns ---------- (str): The constructed string """ + favorite_media = [] for node in media: try: @@ -196,6 +207,7 @@ class Anime(commands.Cog): favorite_media.append(f'[{anime_name}]({anime_url}){nsfwtag}') except KeyError: pass + return ' | '.join(favorite_media) def __construct_favorite_entity_string(self, entities: list, studio: bool = False) -> str: @@ -204,23 +216,26 @@ class Anime(commands.Cog): Parameters ---------- - entity: The list of entities returned from the Anilist API + entities (list): The list of entities returned from the Anilist API + studio (bool): Whether the entities are studios or not Returns ---------- (str): The constructed string """ + favortie_entities = [] for entity in entities: try: if studio: entity_name = entity['name'] else: - entity_name = entity['name']["full"] + entity_name = entity['name']['full'] entity_url = entity['siteUrl'] favortie_entities.append(f'[{entity_name}]({entity_url})') except KeyError: pass + return ' | '.join(favortie_entities) def __construct_release_schedule_string(self, numbers: dict) -> str: @@ -235,16 +250,17 @@ class Anime(commands.Cog): ---------- (str): The constructed string """ + # Release and end date formatting if numbers['start_day'] != '?' and numbers['start_month'] != '?' and numbers['start_year'] != '?': release_date = datetime(numbers['start_year'], numbers['start_month'], numbers['start_day']) - release_date = discord.utils.format_dt(release_date, style="d") + release_date = discord.utils.format_dt(release_date, style='d') else: release_date = f'{numbers["start_day"]}.{numbers["start_month"]}.{numbers["start_year"]}' - if numbers['end_day'] != '?' and numbers['end_month'] != '?' and numbers['end_year'] != '?': - end_date = datetime(numbers['end_year'], numbers['end_month'], numbers['end_day']) - end_date = discord.utils.format_dt(end_date, style="d") + if numbers["end_day"] != '?' and numbers["end_month"] != '?' and numbers["end_year"] != '?': + end_date = datetime(numbers["end_year"], numbers["end_month"], numbers["end_day"]) + end_date = discord.utils.format_dt(end_date, style='d') else: end_date = f'{numbers["end_day"]}.{numbers["end_month"]}.{numbers["end_year"]}' @@ -259,6 +275,15 @@ class Anime(commands.Cog): @app_commands.checks.cooldown(1, 5) @anilist_profile.command(name='generelt', description='Viser informasjon om en Anilistprofil') async def anilist_profile_general(self, interaction: discord.Interaction, bruker: str): + """ + Shows general information about a user's Anilist profile + + Parameters + ---------- + interaction (discord.Interaction): Slash command context object + bruker (str): The user to get information about + """ + # GraphQL query = """ query ($name: String) { @@ -388,6 +413,15 @@ class Anime(commands.Cog): @app_commands.checks.cooldown(1, 5) @anilist_profile.command(name='anime', description='Se statistikk om animetittingen til en bruker') async def anilist_profile_anime_stats(self, interaction: discord.Interaction, bruker: str): + """ + Shows anime viewing statistics from a user's Anilist profile + + Parameters + ---------- + interaction (discord.Interaction): Slash command context object + bruker (str): The user to get information about + """ + # GraphQL query = """ query ($name: String) { @@ -487,6 +521,15 @@ class Anime(commands.Cog): @app_commands.checks.cooldown(1, 5) @anilist_profile.command(name='manga', description='Se statistikk om mangalesingen til en bruker') async def anilist_profile_manga_stats(self, interaction: discord.Interaction, bruker: str): + """ + Shows manga reading statistics from a user's Anilist profile + + Parameters + ---------- + interaction (discord.Interaction): Slash command context object + bruker (str): The user to get information about + """ + # GraphQL query = """ query ($name: String) { @@ -588,8 +631,17 @@ class Anime(commands.Cog): @app_commands.checks.cooldown(1, 5) @anilist.command(name='anime', description='Hent informasjon om en anime') async def anilist_anime(self, interaction: discord.Interaction, navn: str): + """ + Shows information about an anime + + Parameters + ---------- + interaction (discord.Interaction): Slash command context object + navn (str): The name of the anime to fetch information about + """ + # GraphQL - query = ''' + query = """ query ($search: String, $isMain: Boolean) { Media (search: $search, type: ANIME) { siteUrl @@ -642,7 +694,7 @@ class Anime(commands.Cog): } } } - ''' + """ variables = { 'search': navn, 'isMain': True @@ -747,7 +799,16 @@ class Anime(commands.Cog): @app_commands.checks.cooldown(1, 5) @anilist.command(name='manga', description='Hent informasjon om en manga') async def anilist_manga(self, interaction: discord.Interaction, navn: str): - query = ''' + """ + Shows information about a manga + + Parameters + ---------- + interaction (discord.Interaction): Slash command context object + navn (str): The name of the manga to fetch information about + """ + + query = """ query ($search: String) { Media (search: $search, type: MANGA) { siteUrl @@ -794,7 +855,7 @@ class Anime(commands.Cog): } } } - ''' + """ variables = { 'search': navn } @@ -880,7 +941,16 @@ class Anime(commands.Cog): @app_commands.checks.cooldown(1, 5) @anilist.command(name='karakter', description='Hent informasjon om en karakter') async def anilist_character(self, interaction: discord.Interaction, navn: str): - query = ''' + """ + Shows information about a character + + Parameters + ---------- + interaction (discord.Interaction): Slash command context object + navn (str): The name of the character to fetch information about + """ + + query = """ query ($search: String) { Character (search: $search) { name { @@ -915,7 +985,7 @@ class Anime(commands.Cog): } } } - ''' + """ variables = { 'search': navn } @@ -968,7 +1038,16 @@ class Anime(commands.Cog): @app_commands.checks.cooldown(1, 5) @anilist.command(name='skaper', description='Hent informasjon om en skaper') async def anilist_creator(self, interaction: discord.Interaction, navn: str): - query = ''' + """ + Shows information about a creator + + Parameters + ---------- + interaction (discord.Interaction): Slash command context object + navn (str): The name of the creator to fetch information about + """ + + query = """ query ($search: String) { Staff(search: $search) { name { @@ -1007,7 +1086,7 @@ class Anime(commands.Cog): } } } - ''' + """ variables = { 'search': navn } @@ -1061,7 +1140,16 @@ class Anime(commands.Cog): @app_commands.checks.cooldown(1, 5) @anilist.command(name='studio', description='Hent informasjon om et studio') async def anilist_studio(self, interaction: discord.Interaction, navn: str): - query = ''' + """ + Shows information about a studio + + Parameters + ---------- + interaction (discord.Interaction): Slash command context object + navn (str): The name of the studio to fetch information about + """ + + query = """ query ($search: String) { Studio(search: $search) { name @@ -1081,7 +1169,7 @@ class Anime(commands.Cog): } } } - ''' + """ variables = { 'search': navn } @@ -1101,7 +1189,7 @@ class Anime(commands.Cog): popular_media.append(f'- [{media_name}]({media_url}){nsfwtag}') popular_media = '\n'.join(popular_media) - query2 = ''' + query2 = """ query ($search: String) { Studio(search: $search) { media(sort: START_DATE_DESC) { @@ -1116,7 +1204,7 @@ class Anime(commands.Cog): } } } - ''' + """ data = requests.post('https://graphql.anilist.co', json={'query': query2, 'variables': variables}, timeout=10).json() data = data['data']['Studio'] @@ -1146,4 +1234,12 @@ class Anime(commands.Cog): async def setup(bot: commands.Bot): + """ + Add the cog to the bot on extension load + + Parameters + ---------- + bot (commands.Bot): Bot instance + """ + await bot.add_cog(Anime(bot)) diff --git a/src/cogs/birthday.py b/src/cogs/birthday.py index 9ee3450..bf1f99b 100644 --- a/src/cogs/birthday.py +++ b/src/cogs/birthday.py @@ -10,12 +10,21 @@ from cogs.utils import discord_utils, embed_templates class Birthday(commands.Cog): + """Save the birthday of Discord users and allow them to see the birthdays of other users""" + def __init__(self, bot: commands.Bot): + """ + Parameters + ---------- + bot (commands.Bot): The bot instance + """ + self.bot = bot self.cursor = self.bot.db_connection.cursor() def init_db(self): - """Create the necessary tables for the birthday cog to work.""" + """Create the necessary tables for the birthday cog to work""" + self.cursor.execute( """ CREATE TABLE IF NOT EXISTS birthdays ( @@ -27,22 +36,43 @@ class Birthday(commands.Cog): self.bot.db_connection.commit() def __fetch_user_birthday(self, user_id: int) -> datetime | None: - """Fetch the birthday of a user from the database""" - self.cursor.execute("SELECT birthday FROM birthdays WHERE discord_id = (%s)", (user_id,)) + """ + Fetch the birthday of a user in the database + + Parameters + ---------- + user_id (int): The Discord user ID + + Returns + ---------- + (datetime | None): The birthday of the user + """ + + self.cursor.execute('SELECT birthday FROM birthdays WHERE discord_id = (%s)', (user_id,)) result = self.cursor.fetchone() if result: birthday = result[0] return datetime(birthday.year, birthday.month, birthday.day) def __fetch_user_next_birthday(self, user_id: int) -> datetime: - """Get date of next birthday of a user""" + """ + Get date of next birthday for a user + + Parameters + ---------- + user_id (int): The Discord user ID + + Returns + ---------- + (datetime): The date of the next birthday + """ + self.cursor.execute( """ SELECT *, CAST(birthday + ((EXTRACT(YEAR FROM AGE(birthday)) + 1) * interval '1' YEAR) AS DATE) AS next_birthday FROM birthdays WHERE discord_id = %s - """, - (user_id,), + """, (user_id,) ) results = self.cursor.fetchone() if results: @@ -52,7 +82,14 @@ class Birthday(commands.Cog): return datetime.now() # This will never happen unless a database error occurs. Keeping the type checker happy def __fetch_next_birthdays(self) -> list[tuple[int, datetime, datetime]]: - """Fetch the 5 first future birthdays of all users in the database""" + """ + Fetch the 5 first future birthdays of all users in the database + + Returns + ---------- + (list[tuple[int, datetime, datetime]]): A list of tuples containing the user ID, the date of birth and the next birthday + """ + self.cursor.execute( """ SELECT *, CAST(birthday + ((EXTRACT(YEAR FROM AGE(birthday)) + 1) * interval '1' YEAR) AS DATE) AS next_birthday @@ -71,8 +108,16 @@ class Birthday(commands.Cog): return results - def __set_user_birthday(self, user_id: int, birthday: datetime) -> None: - """Iset the birthday of a user in the database. If it already exists, update it.""" + def __set_user_birthday(self, user_id: int, birthday: datetime): + """ + Set the birthday of a user in the database. If it already exists, update it. + + Parameters + ---------- + user_id (int): The Discord user ID + birthday (datetime): The birthday of the user + """ + try: self.cursor.execute( """ @@ -82,10 +127,10 @@ class Birthday(commands.Cog): self.bot.db_connection.commit() except psycopg2.errors.UniqueViolation: self.bot.db_connection.rollback() - self.cursor.execute("UPDATE birthdays SET birthday = %s WHERE discord_id = %s", (birthday, user_id)) + self.cursor.execute('UPDATE birthdays SET birthday = %s WHERE discord_id = %s', (birthday, user_id)) self.bot.db_connection.commit() - # @aiocron.crontab("0 0 * * *") + # @aiocron.crontab('0 0 * * *') # async def __check_birthdays(self): # """Check if it's someone's birthday every day at midgnight and send a greeting if it is.""" # await asyncio.sleep(1) # Prevent sending message before date has really changed @@ -106,54 +151,79 @@ class Birthday(commands.Cog): # for birthday in birthdays: # user = self.bot.get_user(birthday[0]) # if user: - # await channel.send(f"Gratulrer med dagen {user.mention}!") + # await channel.send(f'Gratulrer med dagen {user.mention}!') @app_commands.checks.bot_has_permissions(embed_links=True) @app_commands.checks.cooldown(1, 5) - @app_commands.command() - async def settbursdag(self, interaction: discord.Interaction, dag: int, måned: int, år: int): - """Lagrer din bursdag i databasen""" + @app_commands.command(name='settbursdag', description='Lagrer din bursdag i databasen') + async def birthday_set(self, interaction: discord.Interaction, dag: int, måned: int, år: int): + """ + Allows the user to set their birthday in the database. + + Parameters + ---------- + interaction (discord.Interaction): Slash command context object + dag (int): Day of the month + måned (int): Month of the year + år (int): Year + """ + try: date = datetime(år, måned, dag) except ValueError: return await interaction.response.send_message( - embed=embed_templates.error_fatal(interaction, text="ikke en gyldig dato, gjøk!"), + embed=embed_templates.error_fatal(interaction, text='ikke en gyldig dato, gjøk!'), ephemeral=True ) if date.year < 1970: return await interaction.response.send_message( - embed=embed_templates.error_fatal(interaction, text="Ok boomer, men nei. Hvorfor? Unix timestamps. Google it"), + embed=embed_templates.error_fatal(interaction, text='Ok boomer, men nei. Hvorfor? Unix timestamps. Google it'), ephemeral=True ) if date > datetime.now(): return await interaction.response.send_message( - embed=embed_templates.error_fatal(interaction, text="Du kan ikke sette en fremtidig dato!"), + embed=embed_templates.error_fatal(interaction, text='Du kan ikke sette en fremtidig dato!'), ephemeral=True ) self.__set_user_birthday(interaction.user.id, date) - embed = embed_templates.success(interaction, text=f"Bursdag satt til {discord.utils.format_dt(date, style='D')}") + embed = embed_templates.success(interaction, text=f'Bursdag satt til {discord.utils.format_dt(date, style="D")}') await interaction.response.send_message(embed=embed) @app_commands.checks.bot_has_permissions(embed_links=True) @app_commands.checks.cooldown(1, 5) - @app_commands.command() - async def fjernbursdag(self, interaction: discord.Interaction): - """Fjerner bursdagen din fra databasen""" - self.cursor.execute("DELETE FROM birthdays WHERE discord_id = (%s)", (interaction.user.id,)) + @app_commands.command(name='fjernbursdag', description='Fjerner bursdagen din fra databasen') + async def birthday_remove(self, interaction: discord.Interaction): + """ + Removes the invoking user's birthday from the database + + Parameters + ---------- + interaction (discord.Interaction): Slash command context object + """ + + self.cursor.execute('DELETE FROM birthdays WHERE discord_id = (%s)', (interaction.user.id,)) self.bot.db_connection.commit() - embed = embed_templates.success(interaction, text="Bursdag fjernet") + embed = embed_templates.success(interaction, text='Bursdag fjernet') await interaction.response.send_message(embed=embed) @app_commands.checks.bot_has_permissions(embed_links=True) @app_commands.checks.cooldown(1, 5) - @app_commands.command() - async def bursdag(self, interaction: discord.Interaction, bruker: discord.Member | discord.User | None = None): - """Viser bursdagen til en bruker. Om ingen er gitt, vises din egen bursdag.""" + @app_commands.command(name='bursdag', description='Viser bursdagen til en bruker. Om ingen er gitt, vises din egen') + async def birthday_show(self, interaction: discord.Interaction, bruker: discord.Member | discord.User | None = None): + """ + Shows the birthday of a user. If none is given, shows the invoking user's one. + + Parameters + ---------- + interaction (discord.Interaction): Slash command context object + bruker (discord.Member | discord.User | None): The user to show the birthday of. Defaults to None. + """ + if not bruker: bruker = interaction.user @@ -161,7 +231,7 @@ class Birthday(commands.Cog): if not user: return await interaction.response.send_message( - embed=embed_templates.error_fatal(interaction, text="Brukeren har ikke registrert bursdagen sin") + embed=embed_templates.error_fatal(interaction, text='Brukeren har ikke registrert bursdagen sin') ) # Current age information @@ -176,24 +246,31 @@ class Birthday(commands.Cog): embed = discord.Embed(description=bruker.mention, color=discord_utils.get_color(bruker)) embed.set_thumbnail(url=bruker.display_avatar) embed.set_author(name=bruker.name, icon_url=bruker.display_avatar) - embed.add_field(name="Bursdag", value=discord.utils.format_dt(birthday, style="D")) - embed.add_field(name="Hvor gammel?", value=f"{years_old} år\n({days_old} dager)", inline=False) - embed.add_field(name="Neste bursdag om", value=f"{next_birthday_days} dager", inline=False) + embed.add_field(name='Bursdag', value=discord.utils.format_dt(birthday, style='D')) + embed.add_field(name='Hvor gammel?', value=f'{years_old} år\n({days_old} dager)', inline=False) + embed.add_field(name='Neste bursdag om', value=f'{next_birthday_days} dager', inline=False) embed_templates.default_footer(interaction, embed) await interaction.response.send_message(embed=embed) @app_commands.guild_only() @app_commands.checks.bot_has_permissions(embed_links=True) @app_commands.checks.cooldown(1, 5) - @app_commands.command() - async def kommendebursdager(self, interaction: discord.Interaction): - """Se opptil de 5 neste bursdagene i UiO Gaming""" + @app_commands.command(name='kommendebursdager', description='Viser en liste over de opptil 5 kommende bursdager i serveren') + async def birthdays_upcoming(self, interaction: discord.Interaction): + """ + Lists up to 5 upcoming birthdays of users in the server. + + Parameters + ---------- + interaction (discord.Interaction): Slash command context object + """ + next_birthdays = self.__fetch_next_birthdays() if not next_birthdays: - return await interaction.response.send_message(embed=embed_templates.error_fatal(interaction, text="Ingen bursdager")) + return await interaction.response.send_message(embed=embed_templates.error_fatal(interaction, text='Ingen bursdager')) - birthday_string = "" + birthday_string = '' for user in next_birthdays: user_id, _, next_birthday = user try: @@ -201,17 +278,25 @@ class Birthday(commands.Cog): except (discord.NotFound, discord.HTTPException): continue - timestamp = discord.utils.format_dt(next_birthday, style="D") - relative_timestamp = discord.utils.format_dt(next_birthday, style="R") - birthday_string += f"{discord_user.name}#{discord_user.discriminator} - {timestamp} ({relative_timestamp})\n" + timestamp = discord.utils.format_dt(next_birthday, style='D') + relative_timestamp = discord.utils.format_dt(next_birthday, style='R') + birthday_string += f'{discord_user.name}#{discord_user.discriminator} - {timestamp} ({relative_timestamp})\n' if not birthday_string: - return await interaction.response.send_message(embed=embed_templates.error_fatal(interaction, text="Ingen bursdager")) + return await interaction.response.send_message(embed=embed_templates.error_fatal(interaction, text='Ingen bursdager')) - embed = discord.Embed(title="Kommende bursdager", description=birthday_string) + embed = discord.Embed(title='Kommende bursdager', description=birthday_string) embed_templates.default_footer(interaction, embed) await interaction.response.send_message(embed=embed) async def setup(bot: commands.Bot): + """ + Add the cog to the bot on extension load + + Parameters + ---------- + bot (commands.Bot): Bot instance + """ + await bot.add_cog(Birthday(bot)) diff --git a/src/cogs/botinfo.py b/src/cogs/botinfo.py index 7e238df..5e940b5 100644 --- a/src/cogs/botinfo.py +++ b/src/cogs/botinfo.py @@ -11,14 +11,29 @@ from cogs.utils import embed_templates class BotInfo(commands.Cog): + """View information about the bot""" + def __init__(self, bot: commands.Bot): + """ + Parameters + ---------- + bot (commands.Bot): The bot instance + """ + self.bot = bot @app_commands.checks.bot_has_permissions(embed_links=True, external_emojis=True) @app_commands.checks.cooldown(1, 2) - @app_commands.command() + @app_commands.command(name='botinfo', description='Se informajson om botten') async def botinfo(self, interaction: discord.Interaction): - """Viser informasjon om bot""" + """ + View information about the bot + + Parameters + ---------- + interaction (discord.Interaction): Slash command context object + """ + # Dev user info dev = await self.bot.fetch_user(170506717140877312) @@ -78,9 +93,15 @@ class BotInfo(commands.Cog): @app_commands.checks.bot_has_permissions(embed_links=True) @app_commands.checks.cooldown(1, 2) - @app_commands.command() - async def oppetid(self, interaction: discord.Interaction): - """Viser hvor lenge bot har vært kjørende""" + @app_commands.command(name='oppetid', description='Se hvor lenge botten har kjørt') + async def uptime(self, interaction: discord.Interaction): + """ + View the bot's uptime + + Parameters + ---------- + interaction (discord.Interaction): Slash command context object + """ embed = discord.Embed(color=interaction.client.user.color) embed.add_field(name='🔌 Oppetid', value=self.__get_uptime()) @@ -89,9 +110,16 @@ class BotInfo(commands.Cog): @app_commands.checks.bot_has_permissions(embed_links=True) @app_commands.checks.cooldown(1, 2) - @app_commands.command() + @app_commands.command(name='ping', description='Se botten sin nåværende ping') async def ping(self, interaction: discord.Interaction): - """Viser bot sin ping""" + """ + View the bot's ping + + Parameters + ---------- + interaction (discord.Interaction): Slash command context object + """ + embed = discord.Embed(color=interaction.client.user.color) embed.add_field( name='📶 Ping', @@ -108,19 +136,18 @@ class BotInfo(commands.Cog): ---------- (int) The bot's ping in milliseconds """ + return int(self.bot.latency * 1000) def __get_uptime(self) -> str: """ Returns the current uptime of the bot in string format - Parameters - ---------- - Returns ---------- (str) The bot's uptime in human readable format """ + now = time() diff = int(now - self.bot.uptime) days, remainder = divmod(diff, 24 * 60 * 60) @@ -131,4 +158,12 @@ class BotInfo(commands.Cog): async def setup(bot: commands.Bot): + """ + Add the cog to the bot on extension load + + Parameters + ---------- + bot (commands.Bot): Bot instance + """ + await bot.add_cog(BotInfo(bot)) diff --git a/src/cogs/deprecated/social_credit.py b/src/cogs/deprecated/social_credit.py index 048d619..4791b08 100644 --- a/src/cogs/deprecated/social_credit.py +++ b/src/cogs/deprecated/social_credit.py @@ -69,7 +69,7 @@ class SocialCredit(commands.Cog): def add_new_citizen(func): async def wrapper(*args, **kwargs): self = args[0] - self.cursor.execute("SELECT user_id FROM social_credit WHERE user_id = %s", (args[1],)) + self.cursor.execute('SELECT user_id FROM social_credit WHERE user_id = %s', (args[1],)) if not self.cursor.fetchone(): self._add_citizen(args[1]) await func(*args, **kwargs) @@ -100,9 +100,9 @@ class SocialCredit(commands.Cog): async def get_leaderboard(self, ctx, page, worst=False): if worst: - keyword = "ASC" + keyword = 'ASC' else: - keyword = "DESC" + keyword = 'DESC' self.cursor.execute( f""" @@ -113,13 +113,13 @@ class SocialCredit(commands.Cog): result = self.cursor.fetchall() if not result: - return await ctx.send(embed=embed_templates.error_fatal(ctx, "Ingen brukere er registrert i databasen")) + return await ctx.send(embed=embed_templates.error_fatal(ctx, 'Ingen brukere er registrert i databasen')) output_list = [] for i, row in enumerate(result): user = ctx.guild.get_member(row[0]) if user: - output_list.append(f"{i+1}. {user.name}#{user.discriminator} - `{row[1]}` poeng") + output_list.append(f'{i+1}. {user.name}#{user.discriminator} - `{row[1]}` poeng') return misc_utils.paginator(output_list, int(page)) @@ -134,7 +134,7 @@ class SocialCredit(commands.Cog): @add_new_citizen async def social_punishment(self, user_id, points): - print(f"{points} points deducted from {user_id}") + print(f'{points} points deducted from {user_id}') self.cursor.execute( """ UPDATE social_credit @@ -147,7 +147,7 @@ class SocialCredit(commands.Cog): @add_new_citizen async def social_reward(self, user_id, points): - print(f"{points} points given to {user_id}") + print(f'{points} points given to {user_id}') self.cursor.execute( """ UPDATE social_credit @@ -164,17 +164,17 @@ class SocialCredit(commands.Cog): if not bruker: bruker = ctx.author - self.cursor.execute("SELECT * FROM social_credit WHERE user_id = %s", (bruker.id,)) + self.cursor.execute('SELECT * FROM social_credit WHERE user_id = %s', (bruker.id,)) result = self.cursor.fetchone() if not result: return await ctx.send( - embed=embed_templates.error_fatal(ctx, f"{bruker.mention} er ikke registrert i databasen") + embed=embed_templates.error_fatal(ctx, f'{bruker.mention} er ikke registrert i databasen') ) db_user = CreditUser(*result) - embed = discord.Embed(description=(f"{bruker.mention} har `{db_user.credit_score}` social credits")) + embed = discord.Embed(description=(f'{bruker.mention} har `{db_user.credit_score}` social credits')) await ctx.send(embed=embed) @commands.command() @@ -185,13 +185,13 @@ class SocialCredit(commands.Cog): page = 0 page_data = await self.get_leaderboard(ctx, page) - output_content = "\n".join(page_data["page_content"]) + output_content = '\n'.join(page_data['page_content']) - embed = discord.Embed(title="Social credit leaderboard", description=output_content) - embed.set_footer(text=f"Side {page_data['page']}/{page_data['pagecount']}") + embed = discord.Embed(title='Social credit leaderboard', description=output_content) + embed.set_footer(text=f'Side {page_data['page']}/{page_data['pagecount']}') await ctx.send(embed=embed) - @commands.command(aliases=["skammeliste"]) + @commands.command(aliases=['skammeliste']) async def shamelist(self, ctx, page=0): try: page = int(page) @@ -199,13 +199,13 @@ class SocialCredit(commands.Cog): page = 0 page_data = await self.get_leaderboard(ctx, page, worst=True) - output_content = "\n".join(page_data["page_content"]) + output_content = '\n'.join(page_data['page_content']) - embed = discord.Embed(title="Social credit skammeliste", description=output_content) - embed.set_footer(text=f"Side {page_data['page']}/{page_data['pagecount']}") + embed = discord.Embed(title='Social credit skammeliste', description=output_content) + embed.set_footer(text=f'Side {page_data['page']}/{page_data['pagecount']}') await ctx.send(embed=embed) - @commands.Cog.listener("on_message") + @commands.Cog.listener('on_message') async def on_message(self, message): if message.author.bot: return @@ -246,12 +246,12 @@ class SocialCredit(commands.Cog): for mention in message.mentions: await self.social_punishment(mention.id, 10) - @commands.Cog.listener("on_reaction_add") + @commands.Cog.listener('on_reaction_add') async def on_star_add(self, reaction, user): if user.bot: return - if reaction.emoji == "⭐": + if reaction.emoji == '⭐': if reaction.message.author == user: await self.social_punishment(user.id, 100) else: @@ -259,12 +259,12 @@ class SocialCredit(commands.Cog): await self.social_punishment(user.id, (len(reaction.message.reactions) - 1) * 25) await self.social_reward(user.id, 25 * len(reaction.message.reactions)) - @commands.Cog.listener("on_reaction_remove") + @commands.Cog.listener('on_reaction_remove') async def on_star_remove(self, reaction, user): if user.bot: return - if reaction.emoji == "⭐": + if reaction.emoji == '⭐': if len(reaction.message.reactions) >= 3: await self.social_punishment(user.id, 25) diff --git a/src/cogs/deprecated/vær.py b/src/cogs/deprecated/vær.py index 84bc9ce..b21499a 100644 --- a/src/cogs/deprecated/vær.py +++ b/src/cogs/deprecated/vær.py @@ -10,7 +10,7 @@ class Vær(commands.Cog): def __init__(self, bot: commands.Bot): self.bot = bot - @aiocron.crontab("0 6 * * *") + @aiocron.crontab('0 6 * * *') async def forecast(): dev = await self.bot.fetch_user(162337748781367296) @@ -19,8 +19,8 @@ class Vær(commands.Cog): weather = Yr(location_name='Norway/Oslo/Oslo/Oslo', forecast_link='forecast_hour_by_hour') - embed = discord.Embed(title=":white_sun_small_cloud: Værmelding for Oslo :white_sun_small_cloud:") - embed.description = "*Værmeldingen levert til deg av Petter*" + embed = discord.Embed(title=':white_sun_small_cloud: Værmelding for Oslo :white_sun_small_cloud:') + embed.description = '*Værmeldingen levert til deg av Petter*' embed.set_author(name=dev.name, icon_url=dev.display_avatar) half_hour = False @@ -35,11 +35,11 @@ class Vær(commands.Cog): rain = data['precipitation'] regn = rain['@value'] temperatur = temp['@value'] - embed.add_field(name=f"kl {klokkeslett}", value=f"{temperatur}°C - {regn}mm") + embed.add_field(name=f'kl {klokkeslett}', value=f'{temperatur}°C - {regn}mm') klokkeslett += 1 half_hour = True - await channel.send("RISE AND SHINE GAMERS!", embed=embed) + await channel.send('RISE AND SHINE GAMERS!', embed=embed) async def setup(bot: commands.Bot): diff --git a/src/cogs/deprecated/wiki_search.py b/src/cogs/deprecated/wiki_search.py index 5ce9bdc..d594782 100644 --- a/src/cogs/deprecated/wiki_search.py +++ b/src/cogs/deprecated/wiki_search.py @@ -6,7 +6,7 @@ class Wiki(commands.Cog): @commands.command() async def wiki_search(self, ctx, *search_term): - search_term = " ".join(search_term) + search_term = ' '.join(search_term) search = wikipedia.search(search_term) search = search[0] @@ -15,7 +15,7 @@ class Wiki(commands.Cog): summary = wikipedia.summary(search_term, sentences=5) url = search.url - embed = discord.Embed(title=f"Wikipediasøk for {search_term}", url=url) + embed = discord.Embed(title=f'Wikipediasøk for {search_term}', url=url) embed.add_field(name=search_term, value=summary) embed_templates.default_footer(ctx,embed) diff --git a/src/cogs/dev_tools.py b/src/cogs/dev_tools.py index 11137da..dcabf09 100644 --- a/src/cogs/dev_tools.py +++ b/src/cogs/dev_tools.py @@ -9,14 +9,31 @@ from cogs.utils import embed_templates class DevTools(commands.Cog): + """Commands for developers to mangage cogs and other functionality of the bot""" + def __init__(self, bot: commands.Bot): + """ + Parameters + ---------- + bot (commands.Bot): The bot instance + """ + self.bot = bot @commands.is_owner() @commands.bot_has_permissions(embed_links=True) - @commands.command() + @commands.command(name='custommsg', description='Send en melding til hvilken som helst kanal på hvilken som helst server botten er i') async def custommsg(self, ctx: commands.Context, channel: int, *text: tuple[str]): - """Send a message to a specified channel""" + """ + Send a message to any channel on any server the bot is in + + Parameters + ---------- + ctx (commands.Context): Context object + channel (int): The channel ID to send the message to + text (tuple[str]): The text to send + """ + # Send message to the requested channel channel = self.bot.get_channel(channel) custommessage = ' '.join(text) @@ -30,9 +47,19 @@ class DevTools(commands.Cog): @commands.is_owner() @commands.bot_has_permissions(embed_links=True) @commands.cooldown(1, 10, commands.BucketType.guild) - @commands.command() + @commands.command(name='changepresence', description='Endre botten sin nåværende status') async def changepresence(self, ctx: commands.Context, activity_type: str, message: str, status_type: str): - """Changes the bot's presence status""" + """ + Change the bot's presence status + + Parameters + ---------- + ctx (commands.Context): Context object + activity_type (str): The type of activity to set the bot's status to + message (str): The message to set the bot's status to + status_type (str): The type of status to set the bot's status to + """ + activities = { 'playing': 0, 'listening': 2, @@ -52,14 +79,23 @@ class DevTools(commands.Cog): status=status_type, activity=discord.Activity(type=activity_type, name=message) ) + embed = discord.Embed(color=ctx.me.color, description='Endret Presence!') await ctx.send(embed=embed) @commands.is_owner() @commands.bot_has_permissions(embed_links=True, add_reactions=True) - @commands.command() + @commands.command(name='leave', description='Forlater en server. Om ingen er gitt, forlat der det ble sendt fra') async def leave(self, ctx: commands.Context, *guild_id: int): - """Leaves a specified guild""" + """ + Make bot leave a specified guild. If no guild is specified, leave the guild the command was sent from + + Parameters + ---------- + ctx (commands.Context): Context object + guild_id (int): The ID of the guild to leave + """ + # If no guild id is specified, leave the current guild guild_id = guild_id if guild_id else ctx.guild.id @@ -75,7 +111,7 @@ class DevTools(commands.Cog): await confirmation_msg.add_reaction('✅') # Check confirmation - def comfirm(reaction, user): + def comfirm(reaction: discord.Reaction, user: discord.Member): return user == ctx.author and str(reaction.emoji) == '✅' try: @@ -93,9 +129,16 @@ class DevTools(commands.Cog): @commands.is_owner() @commands.bot_has_permissions(embed_links=True) - @commands.command() + @commands.command(name='publicip', description='Hent den offentlige IP-adressen til serveren botten kjører på') async def publicip(self, ctx: commands.Context): - """Sends WAN IP-address. inb4 I leak my IP-address""" + """ + Sends WAN IP-address. Inb4 I leak my IP-address + + Parameters + ---------- + ctx (commands.Context): Context object + """ + data = requests.get('https://wtfismyip.com/json', timeout=10).json() ip = data['YourFuckingIPAddress'] location = data['YourFuckingLocation'] @@ -107,15 +150,30 @@ class DevTools(commands.Cog): @commands.is_owner() @commands.bot_has_permissions(embed_links=True) - @commands.group() + @commands.group(name='cogs', description='Administrer cogs') async def cogs(self, ctx: commands.Context): - """Cog management commands""" + """ + Cog management commands + + Parameters + ---------- + ctx (commands.Context): Context object + """ + if not ctx.invoked_subcommand: await ctx.send_help(ctx.command) - @cogs.command() - async def unload(self, ctx: commands.Context, cog: str): - """Disables a specified cog""" + @cogs.command(name='unload', description='Avslutt en cog') + async def cogs_unload(self, ctx: commands.Context, cog: str): + """ + Disables a specified cog + + Parameters + ---------- + ctx (commands.Context): Context object + cog (str): The name of the cog to disable + """ + for file in listdir('./src/cogs'): if file.endswith('.py'): name = file[:-3] @@ -127,9 +185,17 @@ class DevTools(commands.Cog): embed = embed_templates.error_fatal(ctx, text=f'{cog} does not exist') await ctx.send(embed=embed) - @cogs.command() - async def load(self, ctx: commands.Context, cog: str): - """Enables a speicifed cog""" + @cogs.command(name='load', description='Aktiver en cog') + async def cogs_load(self, ctx: commands.Context, cog: str): + """ + Enables a speicifed cog + + Parameters + ---------- + ctx (commands.Context): Context object + cog (str): The name of the cog to enable + """ + for file in listdir('./src/cogs'): if file.endswith('.py'): name = file[:-3] @@ -141,9 +207,16 @@ class DevTools(commands.Cog): embed = embed_templates.error_fatal(ctx, text=f'{cog} does not exist') await ctx.send(embed=embed) - @cogs.command() - async def reload(self, ctx: commands.Context, cog: str): - """Reloads a specified cog""" + @cogs.command(name='reload', description='Last inn en cog på nytt') + async def cogs_reload(self, ctx: commands.Context, cog: str): + """ + Reloads a specified cog + + Parameters + ---------- + ctx (commands.Context): Context object + cog (str): The name of the cog to reload + """ for file in listdir('./src/cogs'): if file.endswith('.py'): name = file[:-3] @@ -155,9 +228,16 @@ class DevTools(commands.Cog): embed = embed_templates.error_fatal(ctx, text=f'{cog} does not exist') await ctx.send(embed=embed) - @cogs.command() - async def reloadunloaded(self, ctx: commands.Context): - """Reloads all cogs, including previously disabled ones""" + @cogs.command(name='reloadunloaded', description='Last inn alle cogs på nytt, selv de som ikke er lastet inn') + async def cogs_reloadunloaded(self, ctx: commands.Context): + """ + Reloads all cogs, including previously disabled ones + + Parameters + ---------- + ctx (commands.Context): Context object + """ + # Unload all cogs for file in listdir('./src/cogs'): if file.endswith('.py'): @@ -173,9 +253,16 @@ class DevTools(commands.Cog): embed = discord.Embed(color=ctx.me.color, description='Reloaded all cogs') await ctx.send(embed=embed) - @cogs.command() - async def reloadall(self, ctx: commands.Context): - """Reloads all previously enabled cogs""" + @cogs.command(name='reloadall', description='Last inn alle cogs på nytt') + async def cogs_reloadall(self, ctx: commands.Context): + """ + Reloads all previously enabled cogs + + Parameters + ---------- + ctx (commands.Context): Context object + """ + for file in listdir('./src/cogs'): if file.endswith('.py'): name = file[:-3] @@ -186,4 +273,12 @@ class DevTools(commands.Cog): async def setup(bot: commands.Bot): + """ + Add the cog to the bot on extension load + + Parameters + ---------- + bot (commands.Bot): Bot instance + """ + await bot.add_cog(DevTools(bot)) diff --git a/src/cogs/errors.py b/src/cogs/errors.py index d11dd93..60d6ae4 100644 --- a/src/cogs/errors.py +++ b/src/cogs/errors.py @@ -9,13 +9,28 @@ from cogs.utils.misc_utils import ignore_exception class Errors(commands.Cog): + """Error handlers for commands""" + def __init__(self, bot: commands.Bot): + """ + Parameters + ---------- + bot (commands.Bot): The bot instance + """ + self.bot = bot - bot.tree.on_error = self.on_app_command_error + bot.tree.on_error = self.on_app_command_error # Set error handler for slash commands @commands.Cog.listener() async def on_command(self, ctx: commands.Context): - """Logs prefix command execution metadata""" + """ + Logs prefix command execution metadata + + Parameters + ---------- + ctx (commands.Context): Context object + """ + self.bot.logger.info( f'{"❌ " if ctx.command_failed else "✔ "} {ctx.command} | ' + f'{ctx.author.name}#{ctx.author.discriminator} ({ctx.author.id}) | ' + @@ -24,7 +39,15 @@ class Errors(commands.Cog): @commands.Cog.listener() async def on_app_command_completion(self, interaction: discord.Interaction, command: app_commands.Command | app_commands.ContextMenu): - """Logs slash command execution metadata""" + """ + Logs slash command execution metadata + + Parameters + ---------- + interaction (discord.Interaction): Slash command context object + command (app_commands.Command | app_commands.ContextMenu): Command object + """ + self.bot.logger.info( f'{"❌ " if interaction.command_failed else "✔ "} {command.name} | ' + f'{interaction.user.name}#{interaction.user.discriminator} ({interaction.user.id}) | ' + @@ -33,7 +56,15 @@ class Errors(commands.Cog): @commands.Cog.listener() async def on_command_error(self, ctx: commands.Context, error: commands.CommandError): - """Handle prefix command errors""" + """ + Handle prefix command errors + + Parameters + ---------- + ctx (commands.Context): Context object + error (commands.CommandError): Eror context object + """ + # Reset cooldown if command throws AttributeError with ignore_exception(AttributeError): self.bot.get_command(f'{ctx.command}').reset_cooldown(ctx) @@ -83,7 +114,7 @@ class Errors(commands.Cog): text = 'Denne kommandoen kan bare utføres i servere' return await ctx.send(text) except discord.errors.Forbidden: # Thrown if bot is blocked by the user or if the user has closed their DMs - print("DM Blocked!") + print('DM Blocked!') elif isinstance(error, commands.DisabledCommand): pass @@ -98,7 +129,15 @@ class Errors(commands.Cog): self.bot.logger.error(''.join(traceback.format_exception(type(error), error, error.__traceback__))) async def on_app_command_error(self, interaction: discord.Interaction, error: app_commands.AppCommandError): - """Handle slash command errors""" + """ + Handle slash command errors + + Parameters + ---------- + interaction (discord.Interaction): Slash command context object + error (app_commands.AppCommandError): Eror context object + """ + # Log command usage, just in case await self.on_app_command_completion(interaction, interaction.command) @@ -131,4 +170,12 @@ class Errors(commands.Cog): async def setup(bot: commands.Bot): + """ + Add the cog to the bot on extension load + + Parameters + ---------- + bot (commands.Bot): Bot instance + """ + await bot.add_cog(Errors(bot)) diff --git a/src/cogs/fun.py b/src/cogs/fun.py index b59d3c9..d96cfeb 100644 --- a/src/cogs/fun.py +++ b/src/cogs/fun.py @@ -2,44 +2,107 @@ from discord.ext import commands class Fun(commands.Cog): - def __init__(self, bot: commands.Bot): - self.bot = bot + """Simple commands that return short strings for the memes""" @commands.cooldown(1, 2, commands.BucketType.guild) - @commands.command() + @commands.command(name='norskeuniversiteter') async def norskeuniversiteter(self, ctx: commands.Context): + """ + Sends a link to university meme image album + + Parameters + ---------- + ctx (commands.Context): Context object + """ + await ctx.send('') @commands.cooldown(1, 2, commands.BucketType.guild) - @commands.command() + @commands.command(name='ifi') async def ifi(self, ctx: commands.Context): + """ + ifi meme 1 + + Parameters + ---------- + ctx (commands.Context): Context object + """ + await ctx.send('https://i.imgur.com/ypyK1mi.jpg') @commands.cooldown(1, 2, commands.BucketType.guild) - @commands.command() + @commands.command(name='ifi2') async def ifi2(self, ctx: commands.Context): + """ + ifi meme 2 + + Parameters + ---------- + ctx (commands.Context): Context object + """ + await ctx.send('https://i.imgur.com/ZqgZEEA.jpg') @commands.cooldown(1, 2, commands.BucketType.guild) @commands.command() async def ifi3(self, ctx: commands.Context): + """ + ifi meme 3 + + Parameters + ---------- + ctx (commands.Context): Context object + """ + await ctx.send('https://i.imgur.com/Gx9DQE5.jpg') @commands.cooldown(1, 2, commands.BucketType.guild) @commands.command() async def uio(self, ctx: commands.Context): + """ + uio meme 1 + + Parameters + ---------- + ctx (commands.Context): Context object + """ + await ctx.send('https://i.imgur.com/188MoIV.jpg') @commands.cooldown(1, 2, commands.BucketType.guild) @commands.command() async def ntnu(self, ctx: commands.Context): + """ + ntnu meme 1 + + Parameters + ---------- + ctx (commands.Context): Context object + """ + await ctx.send('https://twitter.com/NTNU/status/970667413564993536') @commands.cooldown(1, 2, commands.BucketType.guild) @commands.command() async def ntnu2(self, ctx: commands.Context): + """ + ntnu meme 2 + + Parameters + ---------- + ctx (commands.Context): Context object + """ + await ctx.send('https://i.imgur.com/h84fknj.jpg') async def setup(bot: commands.Bot): + """ + Add the cog to the bot on extension load + + Parameters + ---------- + bot (commands.Bot): Bot instance + """ + await bot.add_cog(Fun(bot)) diff --git a/src/cogs/funreplies.py b/src/cogs/funreplies.py index fcc42ce..e233087 100644 --- a/src/cogs/funreplies.py +++ b/src/cogs/funreplies.py @@ -7,7 +7,15 @@ from discord.ext import commands class FunReplies(commands.Cog): + """Reply to messages that trigger certain key words/phrases""" + def __init__(self, bot: commands.Bot): + """ + Parameters + ---------- + bot (commands.Bot): The bot instance + """ + self.bot = bot # Cooldowns for trigger words @@ -24,7 +32,14 @@ class FunReplies(commands.Cog): } async def reply_to_triggers(self, message: discord.Message): - """Replies to messages that trigger certain key words/phrases""" + """ + Replies to messages that trigger certain key words/phrases + + Parameters + ---------- + message (discord.Message): Message object to check for triggers to + """ + if message.author.bot: return @@ -62,5 +77,13 @@ class FunReplies(commands.Cog): async def setup(bot: commands.Bot): + """ + Add the cog to the bot on extension load + + Parameters + ---------- + bot (commands.Bot): Bot instance + """ + bot.add_listener(FunReplies(bot).reply_to_triggers, 'on_message') await bot.add_cog(FunReplies(bot)) diff --git a/src/cogs/info.py b/src/cogs/info.py index c989d78..38e0842 100644 --- a/src/cogs/info.py +++ b/src/cogs/info.py @@ -8,24 +8,55 @@ from cogs.utils import discord_utils, embed_templates, misc_utils class Info(commands.Cog): + """View information about Discord object such as guilds, users, roles and channels""" + def __init__(self, bot: commands.Bot): + """ + Parameters + ---------- + bot (commands.Bot): The bot instance + """ + self.bot = bot - guild_group = app_commands.Group(name="guild", description="Se ting om serveren") - user_group = app_commands.Group(name="bruker", description="Se ting om brukeren") + guild_group = app_commands.Group(name='guild', description='Se ting om serveren') + user_group = app_commands.Group(name='bruker', description='Se ting om brukeren') def __construct_role_string(self, roles: list[discord.Role]) -> str: - """Puts all roles except @everyone into a list and joins them to a string""" - if not roles: - return "**Ingen roller**" + """ + Puts all roles except @everyone into a list and joins them to a string - roles = [role.name for role in roles if role.name != "@everyone"] + Parameters + ---------- + roles (list[discord.Role]): List of roles + + Returns + ------- + (str): String of roles + """ + + if not roles: + return '**Ingen roller**' + + roles = [role.name for role in roles if role.name != '@everyone'] roles.reverse() return ', '.join(roles) def __construct_booster_string(self, interaction: discord.Interaction, join_method: callable = ', '.join) -> str: - """Joins all boosters into a string""" + """ + Joins all boosters into a string + + Parameters + ---------- + interaction (discord.Interaction): Slash command context object + join_method (callable): Function to join the boosters with + + Returns + ------- + (str): String of boosters + """ + if not interaction.guild.premium_subscribers: return '**Ingen boostere**' @@ -39,7 +70,18 @@ class Info(commands.Cog): return join_method(boosters) def __construct_member_string(self, members: list[discord.Member]) -> str: - """Joins all name#discrim into a string""" + """ + Joins all name#discrim into a string + + Parameters + ---------- + members (list[discord.Member]): List of members + + Returns + ------- + (str): String of members + """ + if not members: return '**Ingen**' @@ -48,7 +90,16 @@ class Info(commands.Cog): return ', '.join(members) async def __send_as_txt_file(self, interaction: discord.Interaction, content: str, file_path: str): - """Sends a string as a txt file and deletes the file afterwards""" + """ + Sends a string as a txt file and deletes the file afterwards + + Parameters + ---------- + interaction (discord.Interaction): Slash command context object + content (str): String that's too long to send + file_path (str): Path to file + """ + # Create file with open(file_path, 'w', encoding='utf-8') as file: file.write(content) @@ -64,10 +115,18 @@ class Info(commands.Cog): @app_commands.guild_only() @app_commands.checks.bot_has_permissions(embed_links=True, external_emojis=True) @app_commands.checks.cooldown(1, 2) - @guild_group.command(name="info", description="Hent informasjon om en server") + @guild_group.command(name='info', description='Hent informasjon om en server') async def guild_info(self, interaction: discord.Interaction): + """ + Sends general information about the guild + + Parameters + ---------- + interaction (discord.Interaction): Slash command context object + """ + # Days since guild was created - creation_date_timestamp = discord.utils.format_dt(interaction.guild.created_at, style="F") + creation_date_timestamp = discord.utils.format_dt(interaction.guild.created_at, style='F') since_created_days = (interaction.created_at - interaction.guild.created_at).days days_ago = f'{since_created_days} dager' if since_created_days != 1 else f'{since_created_days} dag' @@ -195,8 +254,16 @@ class Info(commands.Cog): @app_commands.guild_only() @app_commands.checks.bot_has_permissions(embed_links=True) @app_commands.checks.cooldown(1, 2) - @guild_group.command(name="roller", description="Se rollene på serveren") + @guild_group.command(name='roller', description='Se rollene på serveren') async def guild_roles(self, interaction: discord.Interaction): + """ + List all roles in the guild + + Parameters + ---------- + interaction (discord.Interaction): Slash command context object + """ + roles = self.__construct_role_string(interaction.guild.roles) # IF roles list is longer than 2048, create text file and send it @@ -211,8 +278,16 @@ class Info(commands.Cog): @app_commands.guild_only() @app_commands.checks.bot_has_permissions(embed_links=True) @app_commands.checks.cooldown(1, 2) - @guild_group.command(name="boosters", description="Se boostere på serveren") + @guild_group.command(name='boosters', description='Se boostere på serveren') async def guild_boosters(self, interaction: discord.Interaction): + """ + List all boosters in the guild + + Parameters + ---------- + interaction (discord.Interaction): Slash command context object + """ + if len(interaction.guild.premium_subscribers) == 0: embed = embed_templates.error_warning(interaction, text='Serveren har ikke noen boosts :(') return await interaction.response.send_message(embed=embed) @@ -227,8 +302,16 @@ class Info(commands.Cog): @app_commands.guild_only() @app_commands.checks.bot_has_permissions(embed_links=True) @app_commands.checks.cooldown(1, 2) - @guild_group.command(name="ikon", description="Hent serverens ikon") + @guild_group.command(name='ikon', description='Hent serverens ikon') async def guild_icon(self, interaction: discord.Interaction): + """ + Get the guild icon + + Parameters + ---------- + interaction (discord.Interaction): Slash command context object + """ + embed = discord.Embed(color=interaction.guild.me.color, description=f'[Lenke]({interaction.guild.icon})') embed.set_author(name=interaction.guild.name, icon_url=interaction.guild.icon) embed.set_image(url=interaction.guild.icon) @@ -238,8 +321,16 @@ class Info(commands.Cog): @app_commands.guild_only() @app_commands.checks.bot_has_permissions(embed_links=True) @app_commands.checks.cooldown(1, 2) - @guild_group.command(name="splash", description="Hent serverens splash (bakgrunnsbilde på invitasjonsside)") + @guild_group.command(name='splash', description='Hent serverens splash (bakgrunnsbilde på invitasjonsside)') async def guild_splash(self, interaction: discord.Interaction): + """ + Get the guild splash + + Parameters + ---------- + interaction (discord.Interaction): Slash command context object + """ + if not interaction.guild.discovery_splash: embed = embed_templates.error_warning(interaction, text='Serveren har ikke en splash') return await interaction.response.send_message(embed=embed) @@ -253,8 +344,16 @@ class Info(commands.Cog): @app_commands.guild_only() @app_commands.checks.bot_has_permissions(embed_links=True) @app_commands.checks.cooldown(1, 2) - @guild_group.command(name="banner", description="Hen serverens banner") + @guild_group.command(name='banner', description='Hen serverens banner') async def guild_banner(self, interaction: discord.Interaction): + """ + Get the guild banner + + Parameters + ---------- + interaction (discord.Interaction): Slash command context object + """ + if not interaction.guild.banner: embed = embed_templates.error_warning(interaction, text='Serveren har ikke et banner :(') return await interaction.response.send_message(embed=embed) @@ -268,14 +367,23 @@ class Info(commands.Cog): @app_commands.guild_only() @app_commands.checks.bot_has_permissions(embed_links=True) @app_commands.checks.cooldown(1, 2) - @guild_group.command(name="rolle", description="Hent informasjon om en rolle på serveren") + @guild_group.command(name='rolle', description='Hent informasjon om en rolle på serveren') async def guild_role(self, interaction: discord.Interaction, rolle: discord.Role): + """ + Get information about a role + + Parameters + ---------- + interaction (discord.Interaction): Slash command context object + rolle (discord.Role): Role to fetch information about + """ + if rolle.name == '@everyone': embed = embed_templates.error_fatal(interaction, text='Skriv inn en annen rolle enn @everyone') return await interaction.response.send_message(embed=embed) # Timestamp and days since creation - created_at_timestamp = discord.utils.format_dt(rolle.created_at, style="F") + created_at_timestamp = discord.utils.format_dt(rolle.created_at, style='F') since_created_days = (interaction.created_at - rolle.created_at).days # List of members with the role @@ -302,8 +410,17 @@ class Info(commands.Cog): @app_commands.guild_only() @app_commands.checks.bot_has_permissions(embed_links=True) @app_commands.checks.cooldown(1, 2) - @guild_group.command(name="tekstkanal", description="Hent informasjon om en tekstkanal") + @guild_group.command(name='tekstkanal', description='Hent informasjon om en tekstkanal') async def guild_text_channel(self, interaction: discord.Interaction, kanal: discord.TextChannel): + """ + Get information about a text channel + + Parameters + ---------- + interaction (discord.Interaction): Slash command context object + kanal (discord.TextChannel): Text channel to fetch information about + """ + members = self.__construct_member_string(kanal.members) if len(members) > 1024: members = 'For mange for å vise her' @@ -323,8 +440,17 @@ class Info(commands.Cog): @app_commands.guild_only() @app_commands.checks.bot_has_permissions(embed_links=True) @app_commands.checks.cooldown(1, 2) - @guild_group.command(name="talekanal", description="Hent informasjon om en talekanal") + @guild_group.command(name='talekanal', description='Hent informasjon om en talekanal') async def guild_voice_channel(self, interaction: discord.Interaction, kanal: discord.VoiceChannel): + """ + Get information about a voice channel + + Parameters + ---------- + interaction (discord.Interaction): Slash command context object + kanal (discord.VoiceChannel): Voice channel to fetch information about + """ + embed = discord.Embed(color=interaction.guild.me.color, title=kanal.name, description=f'ID: {kanal.id}') embed.set_author(name=kanal.guild.name, icon_url=kanal.guild.icon) embed.add_field(name='Opprettet', value=discord.utils.format_dt(kanal.created_at, style='F')) @@ -352,17 +478,25 @@ class Info(commands.Cog): # embed = discord.Embed(color=interaction.guild.me.color, title=emoji.name, description=f'ID: {emoji.id}') # embed.set_author(name=emoji.guild.name, icon_url=emoji.guild.icon) - # embed.add_field(name='Opprettet', value=discord.utils.format_dt(emoji.created_at, style="F")) + # embed.add_field(name='Opprettet', value=discord.utils.format_dt(emoji.created_at, style='F')) # embed.add_field(name='Animert', value='Ja' if emoji.animated else 'Nei') # embed.add_field(name='Lagt til av', value=emoji_creator) # embed.set_image(url=emoji.url) # embed_templates.default_footer(interaction, embed) # await interaction.response.send_message(embed=embed) - guild_oldest_group = app_commands.Group(name="eldst", description="Viser de eldste medlemmene på serveren", parent=guild_group) + guild_oldest_group = app_commands.Group(name='eldst', description='Viser de eldste medlemmene på serveren', parent=guild_group) def __construct_oldest_embed(self, paginator: misc_utils.Paginator, page: list, embed: discord.Embed) -> discord.Embed: - """Constructs the embed for the oldest commands""" + """ + Constructs the embed for the oldest commands + + Parameters + ---------- + paginator (misc_utils.Paginator): Paginator dataclass + page (list): List of members to display + embed (discord.Embed): Embed to add fields to + """ embed.description = '\n'.join(page) embed.set_footer(text=f'Side {paginator.current_page}/{paginator.total_page_count}') return embed @@ -370,8 +504,16 @@ class Info(commands.Cog): @commands.guild_only() @app_commands.checks.bot_has_permissions(embed_links=True) @app_commands.checks.cooldown(1, 5) - @guild_oldest_group.command(name="lagd", description="Liste over de eldste brukerene på serveren basert på når de ble lagd") + @guild_oldest_group.command(name='lagd', description='Liste over de eldste brukerene på serveren basert på når de ble lagd') async def guild_user_created_oldest(self, interaction: discord.Interaction): + """ + List the oldest users on the server based on when they were created + + Parameters + ---------- + interaction (discord.Interaction): Slash command context object + """ + # Sort members by creation date members = sorted(interaction.guild.members, key=lambda m: m.created_at) @@ -392,8 +534,16 @@ class Info(commands.Cog): @commands.guild_only() @app_commands.checks.bot_has_permissions(embed_links=True) @app_commands.checks.cooldown(1, 5) - @guild_oldest_group.command(name="joined", description="Liste over de eldste brukerene på serveren basert på når de ble med") + @guild_oldest_group.command(name='joined', description='Liste over de eldste brukerene på serveren basert på når de ble med') async def guild_user_joined_oldest(self, interaction: discord.Interaction): + """ + List the oldest users on the server based on when they joined + + Parameters + ---------- + interaction (discord.Interaction): Slash command context object + """ + # Sort members by creation date members = sorted(interaction.guild.members, key=lambda m: m.joined_at) @@ -414,8 +564,17 @@ class Info(commands.Cog): @app_commands.guild_only() @app_commands.checks.bot_has_permissions(embed_links=True, external_emojis=True) @app_commands.checks.cooldown(1, 2) - @user_group.command(name="info", description="Hent informasjon om en bruker") + @user_group.command(name='info', description='Hent informasjon om en bruker') async def user_info(self, interaction: discord.Interaction, *, bruker: discord.Member | None = None): + """ + Get information about a user + + Parameters + ---------- + interaction (discord.Interaction): Slash command context object + bruker (discord.Member, optional): User to get information about. Defaults to None (invoker). + """ + if not bruker: bruker = interaction.user @@ -489,8 +648,17 @@ class Info(commands.Cog): @app_commands.guild_only() @app_commands.checks.bot_has_permissions(embed_links=True) @app_commands.checks.cooldown(1, 2) - @user_group.command(name="roller", description="Hent roller til en bruker") + @user_group.command(name='roller', description='Hent roller til en bruker') async def user_roles(self, interaction: discord.Interaction, bruker: discord.Member | None = None): + """ + Get roles of a user + + Parameters + ---------- + interaction (discord.Interaction): Slash command context object + bruker (discord.Member, optional): User to get roles of. Defaults to None (invoker). + """ + if not bruker: bruker = interaction.user @@ -507,8 +675,17 @@ class Info(commands.Cog): @app_commands.checks.bot_has_permissions(embed_links=True) @app_commands.checks.cooldown(1, 2) - @user_group.command(name="avatar", description="Hent avatar til en bruker") + @user_group.command(name='avatar', description='Hent avatar til en bruker') async def user_avatar(self, interaction: discord.Interaction, bruker: discord.User | discord.Member | None = None): + """ + Get avatar of a user + + Parameters + ---------- + interaction (discord.Interaction): Slash command context object + bruker (discord.User, optional): User to get avatar of. Defaults to None (invoker). + """ + if not bruker: bruker = interaction.user @@ -520,4 +697,12 @@ class Info(commands.Cog): async def setup(bot: commands.Bot): + """ + Add the cog to the bot on extension load + + Parameters + ---------- + bot (commands.Bot): Bot instance + """ + await bot.add_cog(Info(bot)) diff --git a/src/cogs/mc_whitelist.py b/src/cogs/mc_whitelist.py index ab6fbcc..f773ddc 100644 --- a/src/cogs/mc_whitelist.py +++ b/src/cogs/mc_whitelist.py @@ -8,13 +8,22 @@ from cogs.utils import embed_templates class MCWhitelist(commands.Cog): + """Stand alone cog for handling whitelisting of Discord users on the Minecraft server""" + def __init__(self, bot: commands.Bot): + """ + Parameters + ---------- + bot (commands.Bot): The bot instance + """ + self.bot = bot self.cursor = self.bot.db_connection.cursor() self.init_db() def init_db(self): """Create the necessary tables for the mc_whitelist cog to work.""" + self.cursor.execute( """ CREATE TABLE IF NOT EXISTS mc_whitelist ( @@ -27,15 +36,22 @@ class MCWhitelist(commands.Cog): @app_commands.checks.bot_has_permissions(embed_links=True) @app_commands.checks.cooldown(1, 5) - @app_commands.command() + @app_commands.command(name='whitelist', description='Whitelist minecraftbrukeren din på serveren vår') async def whitelist(self, interaction: discord.Interaction, minecraftbrukernavn: str): - """Whitelist Minecraftbrukeren din på serveren vår""" + """ + Whitelist a minecraft user on the Minecraft server + + Parameters + ---------- + interaction (discord.Interaction): Slash command context object + minecraftbrukernavn (str): Minecraft username + """ # Fetch minecraft uuid from api - data = requests.get(f"https://api.mojang.com/users/profiles/minecraft/{minecraftbrukernavn}", timeout=10).json() + data = requests.get(f'https://api.mojang.com/users/profiles/minecraft/{minecraftbrukernavn}', timeout=10).json() if data.status_code != 200: return await interaction.response.send_message( - embed=embed_templates.error_fatal(interaction, text=f"Brukeren `{minecraftbrukernavn}` finnes ikke på minecraft"), + embed=embed_templates.error_fatal(interaction, text=f'Brukeren `{minecraftbrukernavn}` finnes ikke på minecraft'), ephemeral=True ) @@ -47,21 +63,21 @@ class MCWhitelist(commands.Cog): SELECT * FROM mc_whitelist WHERE minecraft_id = %s OR discord_id = %s - """, (data["id"], interaction.user.id) + """, (data['id'], interaction.user.id) ) if self.cursor.fetchone(): return await interaction.response.send_message( embed=embed_templates.error_fatal( interaction, - text="Du har allerede whitelisted en bruker eller så er brukeren du oppga whitelisted" + text='Du har allerede whitelisted en bruker eller så er brukeren du oppga whitelisted' ) ) # Whitelist user on minecraft server # Unfortunately, this requires an active connection to the server, with correct credentials - with MCRcon(host="127.0.0.1", password=self.bot.mc_rcon_password, port=25575) as mcr: - mcr.command(f"whitelist add {data['name']}") - mcr.command("whitelist reload") + with MCRcon(host='127.0.0.1', password=self.bot.mc_rcon_password, port=25575) as mcr: + mcr.command(f'whitelist add {data["name"]}') + mcr.command('whitelist reload') # Add user to db self.cursor.execute( @@ -69,16 +85,24 @@ class MCWhitelist(commands.Cog): INSERT INTO mc_whitelist (discord_id, minecraft_id) VALUES (%s, %s) """, - (interaction.user.id, data["id"]), + (interaction.user.id, data['id']), ) self.bot.db_connection.commit() await interaction.response.send_message( embed=embed_templates.success( - interaction, text=f"`{data['name']}` er nå tilknyttet din discordbruker og whitelisted!" + interaction, text=f'`{data["name"]}` er nå tilknyttet din discordbruker og whitelisted!' ) ) async def setup(bot: commands.Bot): + """ + Add the cog to the bot on extension load + + Parameters + ---------- + bot (commands.Bot): Bot instance + """ + await bot.add_cog(MCWhitelist(bot)) diff --git a/src/cogs/misc.py b/src/cogs/misc.py index 73cbc24..7ede72c 100644 --- a/src/cogs/misc.py +++ b/src/cogs/misc.py @@ -14,21 +14,43 @@ from cogs.utils.misc_utils import ignore_exception class Misc(commands.Cog): + """Miscellaneous commands that don't fit anywhere else""" + def __init__(self, bot: commands.Bot): + """ + Parameters + ---------- + bot (commands.Bot): The bot instance + """ + self.bot = bot @app_commands.checks.bot_has_permissions(embed_links=True) @app_commands.checks.cooldown(1, 2) - @app_commands.command() + @app_commands.command(name='weeb', description='Kjeft på weebs') async def weeb(self, interaction: discord.Interaction): - """Kjeft på weebs""" - await interaction.response.send_message("<:sven:762725919604473866> Weebs 👉 <#803596668129509417>") + """ + Call out weebs + + Parameters + ---------- + interaction (discord.Interaction): Slash command context object + """ + + await interaction.response.send_message('<:sven:762725919604473866> Weebs 👉 <#803596668129509417>') @app_commands.checks.bot_has_permissions(embed_links=True) @app_commands.checks.cooldown(1, 2) - @app_commands.command() + @app_commands.command(name='kweeb', description='김정운, 최고도') async def kweeb(self, interaction: discord.Interaction): - """김정운, 최고도""" + """ + Call out kweebs + + Parameters + ---------- + interaction (discord.Interaction): Slash command context object + """ + await interaction.response.send_message( """ Melding du nettopp sendte ga muligens følgende vibber for brukeren som brukte denne kommandoen: @@ -52,17 +74,24 @@ https://cdn.discordapp.com/attachments/674726496878723082/1042631569396990004/kw @app_commands.checks.bot_has_permissions(embed_links=True) @app_commands.checks.cooldown(1, 2) - @app_commands.command() + @app_commands.command(name='owo', description='Oversetter teksten din til owo (we_eb)') async def owo(self, interaction: discord.Interaction, tekst: str): - """Oversetter teksten din til owo (we_eb)""" + """ + Translate text into owo (we_eb) - owo_rules = {"r": "w", "l": "w", "R": "W", "L": "W", "n": "ny", "N": "Ny", "ove": "uv"} + Parameters + ---------- + interaction (discord.Interaction): Slash command context object + tekst (str): Text to translate + """ + + owo_rules = {'r': 'w', 'l': 'w', 'R': 'W', 'L': 'W', 'n': 'ny', 'N': 'Ny', 'ove': 'uv'} for key, value in owo_rules.items(): tekst = re.sub(key, value, tekst) # https://kaomoji.moe/ if len(tekst) >= 1000: - embed = embed_templates.error_warning(interaction, text="Teksten er for lang") + embed = embed_templates.error_warning(interaction, text='Teksten er for lang') return await interaction.response.send_message(embed=embed) embed = discord.Embed(color=interaction.client.user.color, description=tekst) @@ -70,50 +99,74 @@ https://cdn.discordapp.com/attachments/674726496878723082/1042631569396990004/kw await interaction.response.send_message(embed=embed) @app_commands.checks.cooldown(1, 5) - @app_commands.command() - async def emne(self, interaction: discord.Interaction, emnekode: str): - """For NTNU'ere som ikke kan emnekoder på UiO""" - data = requests.get(f"https://data.uio.no/studies/v1/course/{emnekode}", timeout=10) - if data.status_code != 200: - return await interaction.response.send_message(embed=embed_templates.error_fatal(interaction, text="Fant ikke emnekode")) + @app_commands.command(name='emne', description='For NTNU\'ere som ikke kan emnekoder på UiO') + async def course_code(self, interaction: discord.Interaction, emnekode: str): + """ + Translate UiO course codes to their respective course names - await interaction.response.send_message(data.json()["info"]["name"]) + Parameters + ---------- + interaction (discord.Interaction): Slash command context object + emnekode (str): UiO course code + """ + + data = requests.get(f'https://data.uio.no/studies/v1/course/{emnekode}', timeout=10) + if data.status_code != 200: + return await interaction.response.send_message(embed=embed_templates.error_fatal(interaction, text='Fant ikke emnekode')) + + await interaction.response.send_message(data.json()['info']['name']) @app_commands.checks.bot_has_permissions(embed_links=True) @app_commands.checks.cooldown(1, 2) - @app_commands.command() - async def klappifiser(self, interation: discord.Interaction, tekst: str): - """Klapppifiserer teksten din""" + @app_commands.command(name='klappifiser', description='Klappifiser tekst') + async def clapify(self, interation: discord.Interaction, tekst: str): + """ + Add clap emoji between every word of a string + + Parameters + ---------- + interaction (discord.Interaction): Slash command context object + tekst (str): Text to clapify + """ + if len(tekst) >= 1000: - embed = embed_templates.error_warning(interation, text="Teksten er for lang") + embed = embed_templates.error_warning(interation, text='Teksten er for lang') return await interation.response.send_message(embed=embed) - tekst = re.sub(" ", "👏", tekst) + tekst = re.sub(' ', '👏', tekst) - embed = discord.Embed(color=interation.client.user.color, description=f"**{tekst.upper()}**") + embed = discord.Embed(color=interation.client.user.color, description=f'**{tekst.upper()}**') embed_templates.default_footer(interation, embed) await interation.response.send_message(embed=embed) @app_commands.checks.bot_has_permissions(embed_links=True) @app_commands.checks.cooldown(1, 5) - @app_commands.command() + @app_commands.command(name='imdb', description='Søk etter filmer på IMDB') async def imdb(self, interaction: discord.Interaction, tittel: str): - """Se informasjon om en film eller serie""" + """ + Search for movies on IMDB and displays information about the first result + + Parameters + ---------- + interaction (discord.Interaction): Slash command context object + tittel (str): Movie title + """ + # Search for movie/show - url = "http://www.omdbapi.com/?" + urllib.parse.urlencode({"s": tittel, "apikey": self.bot.api_keys["omdb"]}) + url = 'http://www.omdbapi.com/?' + urllib.parse.urlencode({'s': tittel, 'apikey': self.bot.api_keys['omdb']}) search = requests.get(url, timeout=10).json() try: - best_result_id = search["Search"][0]["imdbID"] + best_result_id = search['Search'][0]['imdbID'] except KeyError: - embed = embed_templates.error_fatal(interaction, text="Fant ikke filmen!") + embed = embed_templates.error_fatal(interaction, text='Fant ikke filmen!') return await interaction.response.send_message(embed=embed) # Get movie/show info data = requests.get(f'http://www.omdbapi.com/?i={best_result_id}&apikey={self.bot.api_keys["omdb"]}', timeout=10).json() - acceptable_media_types = ["movie", "series", "episode"] - if data["Type"] not in acceptable_media_types: - embed = embed_templates.error_fatal(interaction, text="Fant ikke filmen!") + acceptable_media_types = ['movie', 'series', 'episode'] + if data['Type'] not in acceptable_media_types: + embed = embed_templates.error_fatal(interaction, text='Fant ikke filmen!') return await interaction.response.send_message(embed=embed) embed = discord.Embed( @@ -122,61 +175,78 @@ https://cdn.discordapp.com/attachments/674726496878723082/1042631569396990004/kw url=f'https://www.imdb.com/title/{data["imdbID"]}/', ) embed.set_author(name=interaction.user.name, icon_url=interaction.user.display_avatar) - embed.add_field(name="Type", value=data["Type"].title()) - embed.add_field(name="Sjanger", value=data["Genre"]) - embed.add_field(name="Spilletid", value=data["Runtime"]) - embed.add_field(name="Vurdering på IMDb", value=f'{data["imdbRating"]}/10') - release_date = datetime.strptime(data["Released"], "%d %b %Y") - embed.add_field(name="Utgitt", value=discord.utils.format_dt(release_date, style="D")) + embed.add_field(name='Type', value=data['Type'].title()) + embed.add_field(name='Sjanger', value=data['Genre']) + embed.add_field(name='Spilletid', value=data['Runtime']) + embed.add_field(name='Vurdering på IMDb', value=f'{data["imdbRating"]}/10') + release_date = datetime.strptime(data['Released'], '%d %b %Y') + embed.add_field(name='Utgitt', value=discord.utils.format_dt(release_date, style='D')) embed_templates.default_footer(interaction, embed) - if data["Poster"] != "N/A": - embed.set_thumbnail(url=data["Poster"]) - if data["Director"] != "N/A": - embed.description = data["Director"] - if data["Plot"] != "N/A" and len(data["Plot"]) < 1024: - embed.add_field(name="Sammendrag", value=data["Plot"], inline=False) + if data['Poster'] != 'N/A': + embed.set_thumbnail(url=data['Poster']) + if data['Director'] != 'N/A': + embed.description = data['Director'] + if data['Plot'] != 'N/A' and len(data['Plot']) < 1024: + embed.add_field(name='Sammendrag', value=data['Plot'], inline=False) await interaction.response.send_message(embed=embed) @app_commands.checks.bot_has_permissions(embed_links=True) @app_commands.checks.cooldown(1, 5) - @app_commands.command() - async def helligdager(self, interaction: discord.Interaction, *, land: str = "NO", år: int = None): - """Se hellidagene i et land""" + @app_commands.command(name='helligdager', description='Se helligdager i et land for et år') + async def holidays(self, interaction: discord.Interaction, *, land: str = 'NO', år: int = None): + """ + Get holidays for a country for a given year + + Parameters + ---------- + interaction (discord.Interaction): Slash command context object + land (str): Country code + år (int): Year + """ + land = land.upper() aar = datetime.now().year if not år else int(år) - data = requests.get(f"https://date.nager.at/api/v2/publicholidays/{aar}/{land}", timeout=10) + data = requests.get(f'https://date.nager.at/api/v2/publicholidays/{aar}/{land}', timeout=10) if data.status_code != 200: embed = embed_templates.error_fatal( - interaction, text="Ugyldig land\nHusk å bruke landskoder\n" + "For eksempel: `NO`" + interaction, text='Ugyldig land\nHusk å bruke landskoder\n' + 'For eksempel: `NO`' ) return await interaction.response.send_message(embed=embed) data = data.json() - country = data[0]["countryCode"].lower() + country = data[0]['countryCode'].lower() # Construct output - holiday_str = "" + holiday_str = '' for day in data: - date = discord.utils.format_dt(datetime.strptime(day["date"], "%Y-%m-%d"), style="D") + date = discord.utils.format_dt(datetime.strptime(day['date'], '%Y-%m-%d'), style='D') holiday_str += f'**{date}**: {day["localName"]}\n' - embed = discord.Embed(color=interaction.client.user.color, title=f":flag_{country}: Helligdager {aar} :flag_{country}:") + embed = discord.Embed(color=interaction.client.user.color, title=f':flag_{country}: Helligdager {aar} :flag_{country}:') embed.description = holiday_str embed_templates.default_footer(interaction, embed) await interaction.response.send_message(embed=embed) @app_commands.checks.bot_has_permissions(embed_links=True) @app_commands.checks.cooldown(1, 5) - @app_commands.command() + @app_commands.command(name='match', description='Se hvor mye du matcher med en annen') async def match(self, interaction: discord.Interaction, bruker: discord.Member): - """Se hvor mye du matcher med en annen""" + """ + Get dating compatibility with another user + + Parameters + ---------- + interaction (discord.Interaction): Slash command context object + bruker (discord.Member): User to match with + """ + if bruker == interaction.user: embed = embed_templates.error_warning( - interaction, text="Jeg vet du er ensom, men du kan " "ikke matche med deg selv" + interaction, text='Jeg vet du er ensom, men du kan ' 'ikke matche med deg selv' ) return await interaction.response.send_message(embed=embed) @@ -188,39 +258,47 @@ https://cdn.discordapp.com/attachments/674726496878723082/1042631569396990004/kw if bruker.id == self.bot.user.id: match_percent = 100 - await interaction.user.display_avatar.save(fp=f"./src/assets/temp/{interaction.user.id}_raw.png") - await bruker.display_avatar.save(fp=f"./src/assets/temp/{bruker.id}_raw.png") + await interaction.user.display_avatar.save(fp=f'./src/assets/temp/{interaction.user.id}_raw.png') + await bruker.display_avatar.save(fp=f'./src/assets/temp/{bruker.id}_raw.png') - invoker = Image.open(f"./src/assets/temp/{interaction.user.id}_raw.png").convert("RGBA") + invoker = Image.open(f'./src/assets/temp/{interaction.user.id}_raw.png').convert('RGBA') invoker = invoker.resize((389, 389), Image.ANTIALIAS) - user = Image.open(f"./src/assets/temp/{bruker.id}_raw.png").convert("RGBA") + user = Image.open(f'./src/assets/temp/{bruker.id}_raw.png').convert('RGBA') user = user.resize((389, 389), Image.ANTIALIAS) - heart = Image.open("./src/assets/misc/heart.png") - mask = Image.open("./src/assets/misc/heart.png", "r") + heart = Image.open('./src/assets/misc/heart.png') + mask = Image.open('./src/assets/misc/heart.png', 'r') - image = Image.new("RGBA", (1024, 576)) + image = Image.new('RGBA', (1024, 576)) image.paste(invoker, (0, 94)) image.paste(user, (635, 94)) image.paste(heart, (311, 94), mask=mask) draw = ImageDraw.Draw(image) - font = ImageFont.truetype("./src/assets/fonts/RobotoMono-Medium.ttf", 86) - font_size = font.getsize(f"{match_percent}%") + font = ImageFont.truetype('./src/assets/fonts/RobotoMono-Medium.ttf', 86) + font_size = font.getsize(f'{match_percent}%') font_size = ((image.size[0] - font_size[0]) / 2, (image.size[1] - font_size[1]) / 2) - draw.text(font_size, f"{match_percent}%", font=font, fill=(255, 255, 255, 255)) + draw.text(font_size, f'{match_percent}%', font=font, fill=(255, 255, 255, 255)) - image.save(f"./src/assets/temp/{interaction.user.id}_{bruker.id}_edit.png") + image.save(f'./src/assets/temp/{interaction.user.id}_{bruker.id}_edit.png') - f = discord.File(f"./src/assets/temp/{interaction.user.id}_{bruker.id}_edit.png") + f = discord.File(f'./src/assets/temp/{interaction.user.id}_{bruker.id}_edit.png') embed = discord.Embed() - embed.set_image(url=f"attachment://{interaction.user.id}_{bruker.id}_edit.png") + embed.set_image(url=f'attachment://{interaction.user.id}_{bruker.id}_edit.png') embed_templates.default_footer(interaction, embed) await interaction.response.send_message(embed=embed, file=f) with ignore_exception(OSError): - remove(f"./src/assets/temp/{bruker.id}_raw.png") - remove(f"./src/assets/temp/{interaction.user.id}_raw.png") - remove(f"./src/assets/temp/{interaction.user.id}_{bruker.id}_edit.png") + remove(f'./src/assets/temp/{bruker.id}_raw.png') + remove(f'./src/assets/temp/{interaction.user.id}_raw.png') + remove(f'./src/assets/temp/{interaction.user.id}_{bruker.id}_edit.png') async def setup(bot: commands.Bot): + """ + Add the cog to the bot on extension load + + Parameters + ---------- + bot (commands.Bot): Bot instance + """ + await bot.add_cog(Misc(bot)) diff --git a/src/cogs/repeated_messages.py b/src/cogs/repeated_messages.py index 7fa6bc9..e048c38 100644 --- a/src/cogs/repeated_messages.py +++ b/src/cogs/repeated_messages.py @@ -3,34 +3,49 @@ from discord.ext import commands class RepeatedMessages(commands.Cog): + """Send messages at specific points of time""" + def __init__(self, bot: commands.Bot): self.bot = bot - self.uiog_server = self.bot.get_guild(747542543750660178) - @aiocron.crontab("0 0 * * 5") + @aiocron.crontab('0 0 * * 5') async def fredag(): + """Sends a message on Friday at 00:00""" + channel = await self.uiog_server.fetch_channel(747542544291987597) await channel.send( - "NU ÄR DET FREDAG!\n" + - "https://cdn.discordapp.com/attachments/750052141346979850/851216786259181578/video0.mp4" + 'NU ÄR DET FREDAG!\n' + + 'https://cdn.discordapp.com/attachments/750052141346979850/851216786259181578/video0.mp4' ) - @aiocron.crontab("0 0 * * 1") + @aiocron.crontab('0 0 * * 1') async def mandag(): + """Sends a message on Monday at 00:00""" + channel = await self.uiog_server.fetch_channel(747542544291987597) await channel.send( - "ENDELIG MANDAG!\n\n" + - "https://cdn.discordapp.com/attachments/678396498089738250/862853827278929940/hvorfor_de_rike_br_spises.mp4" + 'ENDELIG MANDAG!\n\n' + + 'https://cdn.discordapp.com/attachments/678396498089738250/862853827278929940/hvorfor_de_rike_br_spises.mp4' ) - @aiocron.crontab("0 0 1 1 *") + @aiocron.crontab('0 0 1 1 *') async def new_year(): + """Sends a message on New Year's Day at 00:00""" + channel = await self.uiog_server.fetch_channel(747542544291987597) await channel.send( - "GODT NYTTÅR!\n\nNå, meld dere inn i UiO Gaming igjen <:ThisIsAThreat:874998234072903710>" + 'GODT NYTTÅR!\n\nNå, meld dere inn i UiO Gaming igjen <:ThisIsAThreat:874998234072903710>' ) async def setup(bot: commands.Bot): + """ + Add the cog to the bot on extension load + + Parameters + ---------- + bot (commands.Bot): Bot instance + """ + await bot.add_cog(RepeatedMessages(bot)) diff --git a/src/cogs/utils/discord_utils.py b/src/cogs/utils/discord_utils.py index ae107ae..1dd9847 100644 --- a/src/cogs/utils/discord_utils.py +++ b/src/cogs/utils/discord_utils.py @@ -15,27 +15,55 @@ def get_color(discord_object: discord.User | discord.Member | discord.Role) -> d ----------- (discord.Color): The user's displayed color """ - if hasattr(discord_object, "color") and str(discord_object.color) != '#000000': + + if hasattr(discord_object, 'color') and str(discord_object.color) != '#000000': return discord_object.color return discord.Colour(0x99AAB5) class ScrollerButton(discord.ui.Button): + """Button that scrolls through pages in a scroller view""" + def __init__(self, paginator: Paginator, button_action: callable, content_constructor: callable, label: str, disabled: bool = False): + """ + Parameters + ----------- + paginator (Paginator): The paginator object that contains the data to be paginated + button_action (callable): The function that returns the requested page + content_constructor (callable): A function that takes a paginator object and a page number and returns an embed + """ + super().__init__(label=label, disabled=disabled) self.paginator = paginator self.button_action = button_action self.content_constructor = content_constructor async def callback(self, interaction: discord.Interaction): + """ + What to do when the button is pressed + + Parameters + ----------- + interaction (discord.Interaction): Slash command context object + """ + await interaction.response.defer() content = self.content_constructor(self.paginator, self.button_action(), interaction.message.embeds[0]) await interaction.message.edit(embed=content, view=Scroller(self.paginator, self.content_constructor)) class Scroller(discord.ui.View): + """View that allows scrolling through pages of data using the pagination module""" + def __init__(self, paginatior: Paginator, content_constructor: callable): + """ + Parameters + ----------- + paginator (Paginator): The paginator object that contains the data to be paginated + content_constructor (callable): A function that takes a paginator object and a page number and returns an embed + """ + super().__init__() self.paginator = paginatior diff --git a/src/cogs/utils/embed_templates.py b/src/cogs/utils/embed_templates.py index 21afa99..ce41af3 100644 --- a/src/cogs/utils/embed_templates.py +++ b/src/cogs/utils/embed_templates.py @@ -7,16 +7,17 @@ def default_footer(interaction: discord.Interaction, embed: discord.Embed) -> di Parameters ----------- - interaction (discord.Interaction): Discord interaction context + interaction (discord.Interaction): Slash command context object embed (discord.Embed): An embed object to add the footer to Returns ----------- (discord.Embed): The passed in embed + footer """ + return embed.set_footer( icon_url=interaction.user.avatar, - text=f"{interaction.user.name}#{interaction.user.discriminator}" + text=f'{interaction.user.name}#{interaction.user.discriminator}' ) @@ -26,14 +27,15 @@ def error_warning(interaction: discord.Interaction, text: str) -> discord.Embed: Parameters ----------- - interaction (discord.Interaction): Discord interaction context + interaction (discord.Interaction): Slash command context object text (str): The error message Returns ----------- (discord.Embed): An embed object based on the template with the specified text """ - embed = discord.Embed(color=discord.Color.gold(), description=f"⚠️ {text}") + + embed = discord.Embed(color=discord.Color.gold(), description=f'⚠️ {text}') default_footer(interaction, embed) return embed @@ -45,14 +47,15 @@ def error_fatal(interaction: discord.Interaction, text: str) -> discord.Embed: Parameters ----------- - interaction (discord.Interaction): Discord interaction context + interaction (discord.Interaction): Slash command context object text (str): The error message Returns ----------- (discord.Embed): An embed object based on the template with the specified text """ - embed = discord.Embed(color=discord.Color.red(), description=f"❌ {text}") + + embed = discord.Embed(color=discord.Color.red(), description=f'❌ {text}') default_footer(interaction, embed) return embed @@ -64,14 +67,15 @@ def success(interaction: discord.Interaction, text: str) -> discord.Embed: Parameters ----------- - interaction (discord.Interaction): Discord interaction context + interaction (discord.Interaction): Slash command context object text (str): The message Returns ----------- discord.Embed: An embed object based on the template with the specified text """ - embed = discord.Embed(color=discord.Color.green(), description=f"✅ {text}") + + embed = discord.Embed(color=discord.Color.green(), description=f'✅ {text}') default_footer(interaction, embed) return embed diff --git a/src/cogs/utils/misc_utils.py b/src/cogs/utils/misc_utils.py index bb97244..868bb8f 100644 --- a/src/cogs/utils/misc_utils.py +++ b/src/cogs/utils/misc_utils.py @@ -3,18 +3,15 @@ from math import ceil @contextmanager -def ignore_exception(*exceptions) -> None: +def ignore_exception(*exceptions: Exception): """ Ignores the given exceptions Parameters ---------- - *exceptions: The exceptions you want to ignore - - Returns - ---------- - None + *exceptions tuple[Exception]: The exceptions you want to ignore """ + try: yield except exceptions: diff --git a/src/logger.py b/src/logger.py index d0eb52a..0d0d493 100644 --- a/src/logger.py +++ b/src/logger.py @@ -3,6 +3,8 @@ import logging.handlers class BotLogger: + """Logging config for the bot""" + def __init__(self): logger = logging.getLogger('discord') logger.setLevel(logging.INFO) diff --git a/src/run.py b/src/run.py index 25f3bf9..9017af1 100644 --- a/src/run.py +++ b/src/run.py @@ -11,14 +11,16 @@ from logger import BotLogger # Load config file -with codecs.open("./src/config/config.yaml", "r", encoding="utf8") as f: +with codecs.open('./src/config/config.yaml', 'r', encoding='utf8') as f: config = yaml.load(f, Loader=yaml.SafeLoader) class Bot(commands.Bot): - def __init__(self) -> None: + """UiO Gaming Bot""" + + def __init__(self): super().__init__( - command_prefix=commands.when_mentioned_or(config["bot"]["prefix"]), + command_prefix=commands.when_mentioned_or(config['bot']['prefix']), case_insensitive=True, intents=discord.Intents.all() ) @@ -26,28 +28,28 @@ class Bot(commands.Bot): self.logger = BotLogger().logger # Initialize logger # Connect to database - db = config.get("database", {}) + db = config.get('database', {}) self.db_connection = psycopg2.connect( - host=db["host"], - dbname=db["dbname"], - user=db["username"], - password=db["password"], + host=db['host'], + dbname=db['dbname'], + user=db['username'], + password=db['password'], ) # Fetch misc config values - self.mc_rcon_password = config["minecraft"]["rcon_password"] - self.presence = config["bot"].get("presence", {}) - self.api_keys = config.get("api", {}) - self.emoji = config.get("emoji", {}) - self.misc = config.get("misc", {}) - self.config_mode = config.get("mode") + self.mc_rcon_password = config['minecraft']['rcon_password'] + self.presence = config['bot'].get('presence', {}) + self.api_keys = config.get('api', {}) + self.emoji = config.get('emoji', {}) + self.misc = config.get('misc', {}) + self.config_mode = config.get('mode') - async def setup_hook(self) -> None: + async def setup_hook(self): # Load cogs - for file in listdir("./src/cogs"): - if file.endswith(".py"): + for file in listdir('./src/cogs'): + if file.endswith('.py'): name = file[:-3] - await bot.load_extension(f"cogs.{name}") + await bot.load_extension(f'cogs.{name}') # Sync slash commands to Discord if self.config_mode == 'prod': @@ -63,33 +65,33 @@ bot = Bot() @bot.event async def on_ready(): - if not hasattr(bot, "uptime"): + if not hasattr(bot, 'uptime'): bot.uptime = time() # Print bot info - print(f"Username: {bot.user.name}") - print(f"ID: {bot.user.id}") - print(f"Version: {discord.__version__}") - print("." * 50 + "\n") + print(f'Username: {bot.user.name}') + print(f'ID: {bot.user.id}') + print(f'Version: {discord.__version__}') + print('.' * 50 + '\n') # Set initial presence # Presence status status_types = { - "online": discord.Status.online, - "dnd": discord.Status.dnd, - "idle": discord.Status.idle, - "offline": discord.Status.offline, + 'online': discord.Status.online, + 'dnd': discord.Status.dnd, + 'idle': discord.Status.idle, + 'offline': discord.Status.offline, } - status_type = status_types.get(bot.presence["type"].lower(), discord.Status.online) + status_type = status_types.get(bot.presence['type'].lower(), discord.Status.online) # Presence actitivity - activities = {"playing": 0, "listening": 2, "watching": 3} - activity_type = activities.get(bot.presence["activity"].lower(), 0) + activities = {'playing': 0, 'listening': 2, 'watching': 3} + activity_type = activities.get(bot.presence['activity'].lower(), 0) await bot.change_presence( - activity=discord.Activity(type=activity_type, name=bot.presence["message"]), + activity=discord.Activity(type=activity_type, name=bot.presence['message']), status=status_type ) -bot.run(config["bot"]["token"], reconnect=True, log_handler=None) +bot.run(config['bot']['token'], reconnect=True, log_handler=None)