diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..18bee40 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,102 @@ +# Bidragsguide + +Heisann sveisann! Er du interessert i å bidra? Kult! + +Det er derimot noen kodestilsregler jeg ønsker at skal følges. Dette dokumentet fungerer som en defacto regelbok for både meg og/eller andre. + +## Sjekk koden din + +Viktigst av alt er at koden din består flake8 sin PEP8-test. Dette kan du teste på egen maskin ved å installere flake8: + +``` +pip install flake8 +``` + +for å så kjøre følgende kommando: + +``` +flake8 --max-line-length=120 --show-source +``` + +## Regler + +### Typing + +All kode skal være typet til beste evne. Dette betyr at man for eksempel også gjerne bør spesifisere typene innad i en dict eller lignende. + +### Docstrings + +Alle funksjoner skal ha docsstrings som følger denne stilen: + +```python +def greet(self, navn: str) -> str: + """ + Greets the invoking user using the inputted name + + Parameters + ---------- + navn (str): The user's inputted name + + Returns + ---------- + (str): A greeting + """ + + return f'Hello, {name}' +``` + +Sløyf alt som ikke brukes. + +### Sortering av imports + +Alle imports skal være gruppert på følgende måte: + +1. Standardbibliotek +2. Tredjepartsbibliotek +3. Lokal kode + +Innad i hver gruppe skal importeringer sortert alfabetisk med vekt på forelderpakken. Det vil si at hvis du importerer bare en del av en pakke skal den fortsatt være sortert i henhold til forelderpakkens navn. + +### Rekkefølge på kommandosjekker (decorators) + +Hver kommando er en funksjon som har et eller flere decorators over seg. Dette er sjekker som utføres før kommandoen kjøres. En av disse konverterer også funksjonen til en faktisk kommando som vi kan kjøre gjennom Discord. + +Decorators skal sorteres som følgende: + +1. is owner +2. Guild only +3. Bot permissions +4. User permissions +5. Cooldown +6. Kommando (denne må alltid være med) + +Merk deg at ikke alle disse trenger å være med, og ikke alle sjekker som finnes er nevnt her. Bruk hodet og tenk hva som er logisk for deg om du møter på en slik situasjon. + +### Quotation marks + +Ta i bruk enkle quotation marks `'` der det er mulig. Multiline comments skal skrives med dobble quotation marks `"`. + +### Newline mellom docstring og kode samt mellom siste kodelinje returstatement + +Ja, det tittelen sier. Det er et unntak for returstatements hvis det er tatt i bruk early returning. + +### Norske parameternavn i kommandoer + +Selv om Discord.py 2.0 støtter oversettelser, er ikke dette noe vi implementerer. Med tanke på at botten er norsk og for norske brukere må dette reflekteres i parameternavn siden dette er hva Discord bruker som grensesnitt for kommandoer. + +### Engelsk for alt annet + +All annen kode holdes på engelsk. Dette inkluderer kommentarer. + +Et tips er å ha et engelsk funksjonsnavn, men spesifisere i decoratoren over med navnparameteren. + +### Avhengigheter + +Hvis en ny avhengighet trengs, sørg for å spesifisere major og minor versjon i requirementsfila. + + +## Ønsket utforming av ny funksjonalitet + +- Bruk embeds til feilmeldinger og som respons om mulig. +- Sett cooldown til 5 sekunder for kommandoer som krever eksterne API kall eller særlig mye prosessering. + - Ellers, settes denne til 2 sekunder. diff --git a/LICENSE b/LICENSE index e72bfdd..2a15844 100644 --- a/LICENSE +++ b/LICENSE @@ -631,8 +631,8 @@ to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. - - Copyright (C) + UiO Gaming's multi-purpose Discord bot + Copyright (C) 2022 Leander West Furumo This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -652,7 +652,7 @@ Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: - Copyright (C) + uiogaming-bot Copyright (C) 2022 Leander West Furumo This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. diff --git a/README.md b/README.md index ff2f805..f98c6d7 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,41 @@ # UiO Gaming Discord Bot -UiO Gaming Discord Bot er en chattebot for ulike formål. Den skal hjelpe oss med ulike oppgaver og ha mange ulike funksjoner +UiO Gaming Discord Bot er en chattebot for ulike formål. Hvilke formål det er, er jeg ikke helt sikker på. -Mer dokumentasjon kommer snart™ +## Installasjon + +1. Fjern `.example` fra [config.yaml.example](src/config/config.yaml) i configmappen + +2. Fyll inn verdiene i denne configfilen + +Du kan så bevege deg videre til ett av to alternativer. + +### Valgmulighet 1 - Manuelt + +Du bør ha gjort følgende: + +- Installert Python 3.10+ +- Ha en Postgresql database satt opp + + +1. Installer avhengigheter + +``` +pip install -r requirements.txt -U +``` + +2. Kjør bot + +``` +python src/run.py +``` + +### Valgmulighet 2 - Docker + +``` +docker-compose up +``` + +## Bidra + +Se [CONTRIBUTING.md](CONTRIBUTING.md) diff --git a/requirements.txt b/requirements.txt index 0c1b8e4..e768269 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,9 +1,9 @@ -discord.py -pyyaml -requests -asyncio -psutil -pillow -aiocron -psycopg2 -mcrcon +aiocron==1.8.* +asyncio==3.4.* +discord.py==2.1.* +mcrcon==0.7.* +pillow==9.3.* +psutil==5.9.* +psycopg2==2.9.* +pyyaml==6.0.* +requests==2.28.* diff --git a/src/cogs/anime.py b/src/cogs/anime.py index a65d9b0..3269fa1 100644 --- a/src/cogs/anime.py +++ b/src/cogs/anime.py @@ -1,651 +1,822 @@ -from discord.ext import commands -import discord +from datetime import datetime +import re -from requests import post -from re import compile, sub +import discord +from discord import app_commands +from discord.ext import commands +import requests from cogs.utils import embed_templates -async def set_color(color: str) -> int: - """ - Converts anilist colors to hext - - Parameters - ---------- - color: The color string returned from the Anilist API - - Returns - ---------- - (int): The color in hex for use in discord embeds - """ - - colors = { - 'blue': 0x3db4f2, - 'purple': 0xc063ff, - 'pink': 0xFC9DD6, - 'orange': 0xEF881A, - 'red': 0xE13333, - 'green': 0x4CCA51, - 'gray': 0x677B94 - } - if color in colors: - color = colors[color] - else: - color = color.replace('#', '0x') - color = int(color, 0) - return color - - -async def convert_media_format(media_format: str): - """ - Translates media format names into norwegian - - Parameters - ---------- - media_format: 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', - 'MOVIE': 'Film', - 'SPECIAL': 'Ekstramateriale', - 'MUSIC': 'Musikk', - 'MANGA': 'Manga', - 'NOVEL': 'Novelle', - 'ONE_SHOT': 'Kort Manga' - } - try: - return media_formats[media_format] - except KeyError: - return media_format - - -async def convert_language_names(language_name: str): - """ - Translates language names into norwegian - - Parameters - ---------- - language_name: 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', - 'KOREAN': 'Koreansk', - 'ITALIAN': 'Italiensk', - 'SPANISH': 'Spansk', - 'PORTUGUESE': 'Portugisisk', - 'FRENCH': 'Fransk', - 'GERMAN': 'Tysk', - 'HEBREW': 'Hebraisk', - 'HUNGARIAN': 'Ungarsk' - } - try: - return languages[language_name] - except KeyError: - return language_name - - -async def convert_role_names(role_name: str): - """ - Translates role names into norwegian - - Parameters - ---------- - role_name: The english role name returned from the Anilist API - - Returns - ---------- - (str): The role name in norwegian. If a translation doesn't exist it returns the english name - """ - - roles = { - 'MAIN': 'Hovedkarakter', - 'SUPPORTING': 'Bikarakter/Biperson', - 'BACKGROUND': 'Statist' - } - try: - return roles[role_name] - except KeyError: - return role_name - - -async def convert_status(status: str): - """ - Translates role names into norwegian - - Parameters - ---------- - status: 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' - } - try: - return statuses[status] - except KeyError: - return status - - -async def remove_html(text): - """ - Removes HTML tag elements - - Parameters - ---------- - text: The raw HTML - - Returns - ---------- - (str): The input string without HTML tags - """ - - compiled = compile(r'<.*?>') - clean_text = sub(compiled, '', text) - return clean_text - - class Anime(commands.Cog): - def __init__(self, bot): - self.bot = bot + """View information about different media on Anilist""" - @commands.bot_has_permissions(embed_links=True) - @commands.cooldown(1, 5, commands.BucketType.guild) - @commands.group() - async def anilist(self, ctx): + 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') + + def __convert_color(self, color: str) -> int: """ - Se informasjon om noe fra Anilist + Converts anilist colors to hext + + Parameters + ---------- + color (str): The color string returned from the Anilist API + + Returns + ---------- + (int): The color in hex for use in discord embeds """ - if ctx.invoked_subcommand is None: - await ctx.send_help(ctx.command) + colors = { + 'blue': 0x3db4f2, + 'purple': 0xc063ff, + 'pink': 0xFC9DD6, + 'orange': 0xEF881A, + 'red': 0xE13333, + 'green': 0x4CCA51, + 'gray': 0x677B94 + } - @anilist.command(aliases=['profile']) - async def profil(self, ctx, *, bruker: str): + return colors.get(color, 0x677B94) + + def __convert_media_format(self, media_format: str) -> str: """ - Viser informasjon om en Anilist-profil + Translates media format names into norwegian + + Parameters + ---------- + 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 """ - async with ctx.channel.typing(): + media_formats = { + 'TV': 'TV-Serie', + 'TV_SHORT': 'Kort TV-Serie', + 'MOVIE': 'Film', + 'SPECIAL': 'Ekstramateriale', + 'MUSIC': 'Musikk', + 'MANGA': 'Manga', + 'NOVEL': 'Novelle', + 'ONE_SHOT': 'Kort Manga' + } + return media_formats.get(media_format, media_format) - # GraphQL - query = """ - query ($name: String) { - User (name: $name) { - name - siteUrl - avatar { - large + def __convert_language_names(self, language_name: str) -> str: + """ + Translates language names into norwegian + + Parameters + ---------- + 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', + 'KOREAN': 'Koreansk', + 'ITALIAN': 'Italiensk', + 'SPANISH': 'Spansk', + 'PORTUGUESE': 'Portugisisk', + 'FRENCH': 'Fransk', + 'GERMAN': 'Tysk', + 'HEBREW': 'Hebraisk', + 'HUNGARIAN': 'Ungarsk' + } + + return languages.get(language_name, language_name) + + def __convert_role_names(self, role_name: str) -> str: + """ + Translates role names into norwegian + + Parameters + ---------- + role_name (str): The english role name returned from the Anilist API + + Returns + ---------- + (str): The role name in norwegian. If a translation doesn't exist it returns the english name + """ + + roles = { + 'MAIN': 'Hovedkarakter', + 'SUPPORTING': 'Bikarakter/Biperson', + 'BACKGROUND': 'Statist' + } + try: + return roles[role_name] + except KeyError: + return role_name + + def __convert_status(self, status: str) -> str: + """ + Translates role names into norwegian + + Parameters + ---------- + 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: + """ + Removes HTML tag elements + + Parameters + ---------- + text: The raw HTML + + Returns + ---------- + (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]: + """ + Makes a request to the Anilist API + + Parameters + ---------- + 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 + ---------- + (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) + response = response.json() + data = response['data'][key] + url = data['siteUrl'] + except TypeError: + embed = embed_templates.error_fatal(interaction, text='Kunne ikke finne det du søkte etter!') + await interaction.response.send_message(embed=embed) + return None, None + + return data, url + + def __construct_favorite_media_string(self, media: list) -> str: + """ + Constructs a string with the user's favorite media + + Parameters + ---------- + media (list): The list of media returned from the Anilist API + + Returns + ---------- + (str): The constructed string + """ + + favorite_media = [] + for node in media: + try: + nsfwtag = '🔞' if node['isAdult'] else '' + anime_name = node['title']['romaji'] + anime_url = node['siteUrl'] + 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: + """ + Constructs a string with the user's favorite entities + + Parameters + ---------- + 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_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: + """ + Constructs a string with the media's release schedule + + Parameters + ---------- + numbers (dict): dict containing release date and end date + + Returns + ---------- + (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') + 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') + else: + end_date = f'{numbers["end_day"]}.{numbers["end_month"]}.{numbers["end_year"]}' + + if release_date == end_date: + date = release_date + else: + date = f'{release_date} - {end_date}' + + return date + + @app_commands.checks.bot_has_permissions(embed_links=True) + @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) { + User (name: $name) { + name + siteUrl + avatar { + large + } + options { + profileColor + } + statistics { + anime { + minutesWatched + statuses { + count + status + } } - options { - profileColor + manga { + chaptersRead + statuses { + count + status + } } - statistics { - anime { + } + favourites { + anime (perPage: 3) { + nodes { + isAdult + title { + romaji + } + siteUrl + } + } + manga (perPage: 3) { + nodes { + isAdult + title { + romaji + } + siteUrl + } + } + studios (perPage: 3) { + nodes { + name + siteUrl + } + } + characters (perPage: 3) { + nodes { + name { + full + } + siteUrl + } + } + staff (perPage: 3) { + nodes { + name { + full + } + siteUrl + } + } + } + } + } + """ + variables = { + 'name': bruker + } + data, url = await self.__request_anilist(interaction, query, variables, 'User') + + if not data: + return + + user_name = data['name'] + profile_pic = data['avatar']['large'] + days_watched = round(data['statistics']['anime']['minutesWatched'] / 1440, 1) + chapters_read = data['statistics']['manga']['chaptersRead'] + color = self.__convert_color(data['options']['profileColor']) + + anime_statuses = {status['status']: {'count': status['count']} for status in data['statistics']['anime']['statuses']} + manga_statuses = {status['status']: {'count': status['count']} for status in data['statistics']['manga']['statuses']} + + try: + anime_completed = anime_statuses['COMPLETED']['count'] + except KeyError: + anime_completed = 0 + try: + manga_completed = manga_statuses['COMPLETED']['count'] + except KeyError: + manga_completed = 0 + + favourite_anime = self.__construct_favorite_media_string(data['favourites']['anime']['nodes']) + favourite_manga = self.__construct_favorite_media_string(data['favourites']['manga']['nodes']) + favourite_character = self.__construct_favorite_entity_string(data['favourites']['characters']['nodes']) + favourite_staff = self.__construct_favorite_entity_string(data['favourites']['staff']['nodes']) + favourite_studio = self.__construct_favorite_entity_string(data['favourites']['studios']['nodes'], studio=True) + + embed = discord.Embed(title=user_name, color=color, url=url) + embed.set_author(name='Anilist', icon_url='https://anilist.co/img/logo_al.png') + embed.set_thumbnail(url=profile_pic) + embed.add_field(name='Antall dager sett', value=days_watched) + embed.add_field(name='Antall anime sett', value=anime_completed) + embed.add_field(name='Antall kapitler lest', value=chapters_read) + embed.add_field(name='Antall manga lest', value=manga_completed) + if favourite_anime: + embed.add_field(name='Noen favorittanime', value=favourite_anime, inline=False) + if favourite_manga: + embed.add_field(name='Noen favorittmanga', value=favourite_manga, inline=False) + if favourite_character: + embed.add_field(name='Noen favorittkarakterer', value=favourite_character, inline=False) + if favourite_studio: + embed.add_field(name='Noen favorittstudioer', value=favourite_studio, inline=False) + if favourite_studio: + embed.add_field(name='Noen favorittskapere', value=favourite_staff, inline=False) + 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) + @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) { + User (name: $name) { + name + siteUrl + avatar { + large + } + options { + profileColor + } + statistics { + anime { + minutesWatched + episodesWatched + meanScore + statuses { + count + status minutesWatched - statuses { - count - status - } } - manga { - chaptersRead - statuses { - count - status - } - } - } - favourites { - anime (perPage: 3) { - nodes { - isAdult - title { - romaji - } - siteUrl - } - } - manga (perPage: 3) { - nodes { - isAdult - title { - romaji - } - siteUrl - } - } - studios (perPage: 3) { - nodes { + studios (limit: 3, sort: COUNT_DESC) { + studio { name siteUrl } } - characters (perPage: 3) { - nodes { - name { - full - } - siteUrl - } - } - staff (perPage: 3) { - nodes { - name { - full - } - siteUrl - } + genres (limit: 3, sort: COUNT_DESC) { + genre } } } } - """ - variables = { - 'name': bruker - } - try: - data = post( - 'https://graphql.anilist.co', - json={'query': query, 'variables': variables}).json() - data = data['data']['User'] - url = data['siteUrl'] - except TypeError: - embed = embed_templates.error_fatal(ctx, text='Kunne ikke finne brukeren!') - return await ctx.send(embed=embed) - - user_name = data['name'] - profile_pic = data['avatar']['large'] - days_watched = round(data['statistics']['anime']['minutesWatched'] / 1440, 1) - chapters_read = data['statistics']['manga']['chaptersRead'] - color = await set_color(data['options']['profileColor']) - - anime_statuses = {} - for status in data['statistics']['anime']['statuses']: - status_name = status['status'] - status_count = status['count'] - anime_statuses.update({f'{status_name}': {'count': status_count}}) - - manga_statuses = {} - for status in data['statistics']['manga']['statuses']: - status_name = status['status'] - status_count = status['count'] - manga_statuses.update({f'{status_name}': {'count': status_count}}) - - try: - anime_completed = anime_statuses['COMPLETED']['count'] - except KeyError: - anime_completed = 0 - try: - manga_completed = manga_statuses['COMPLETED']['count'] - except KeyError: - manga_completed = 0 - - favourite_anime = [] - for anime in data['favourites']['anime']['nodes']: - try: - if anime['isAdult'] is True: - nsfwtag = ' (NSFW)' - else: - nsfwtag = '' - anime_name = anime['title']['romaji'] - anime_url = anime['siteUrl'] - favourite_anime.append(f'[{anime_name}]({anime_url}){nsfwtag}') - except KeyError: - pass - favourite_anime = ' | '.join(favourite_anime) - - favourite_manga = [] - for manga in data['favourites']['manga']['nodes']: - try: - if manga['isAdult'] is True: - nsfwtag = ' (NSFW)' - else: - nsfwtag = '' - manga_name = manga['title']['romaji'] - manga_url = manga['siteUrl'] - favourite_manga.append(f'[{manga_name}]({manga_url}){nsfwtag}') - except KeyError: - pass - favourite_manga = ' | '.join(favourite_manga) - - favourite_character = [] - for character in data['favourites']['characters']['nodes']: - try: - character_name = character['name']['full'] - character_url = character['siteUrl'] - favourite_character.append(f'[{character_name}]({character_url})') - except KeyError: - pass - favourite_character = ' | '.join(favourite_character) - - favourite_staff = [] - for staff in data['favourites']['staff']['nodes']: - try: - staff_name = staff['name']['full'] - staff_url = staff['siteUrl'] - favourite_staff.append(f'[{staff_name}]({staff_url})') - except KeyError: - pass - favourite_staff = ' | '.join(favourite_staff) - - favourite_studio = [] - for studio in data['favourites']['studios']['nodes']: - try: - studio_name = studio['name'] - studio_url = studio['siteUrl'] - favourite_studio.append(f'[{studio_name}]({studio_url})') - except KeyError: - pass - favourite_studio = ' | '.join(favourite_studio) - - embed = discord.Embed(title=user_name, color=color, url=url) - embed.set_author(name='Anilist', icon_url='https://anilist.co/img/logo_al.png') - embed.set_thumbnail(url=profile_pic) - embed.add_field(name='Antall dager sett', value=days_watched) - embed.add_field(name='Antall anime sett', value=anime_completed) - embed.add_field(name='Antall kapitler lest', value=chapters_read) - embed.add_field(name='Antall manga lest', value=manga_completed) - if favourite_anime != '': - embed.add_field(name='Noen favorittanime', value=favourite_anime, inline=False) - if favourite_manga != '': - embed.add_field(name='Noen favorittmanga', value=favourite_manga, inline=False) - if favourite_character != '': - embed.add_field(name='Noen favorittkarakterer', value=favourite_character, inline=False) - if favourite_studio != '': - embed.add_field(name='Noen favorittstudioer', value=favourite_studio, inline=False) - if favourite_studio != '': - embed.add_field(name='Noen favorittskapere', value=favourite_staff, inline=False) - embed = embed_templates.default_footer(ctx, embed) - await ctx.send(embed=embed) - - - @anilist.command(aliases=['anime']) - async def animestats(self, ctx, *, bruker: str): + } """ - Viser animeinformasjon til en Anilist-profil + variables = { + 'name': bruker + } + data, url = await self.__request_anilist(interaction, query, variables, 'User') + + if not data: + return + + user_name = data['name'] + profile_pic = data['avatar']['large'] + days_watched = round(data['statistics']['anime']['minutesWatched'] / 1440, 1) + episodes_watched = data['statistics']['anime']['episodesWatched'] + color = self.__convert_color(data['options']['profileColor']) + + statuses = {status['status']: {'count': status['count'], 'minutes': status['minutesWatched']} for status in data['statistics']['anime']['statuses']} + + try: + completed = statuses['COMPLETED']['count'] + except KeyError: + completed = 0 + try: + watching = statuses['CURRENT']['count'] + except KeyError: + watching = 0 + try: + planning = statuses['PLANNING']['count'] + planning_days = round(statuses['PLANNING']['minutes'] / 1440, 1) + except KeyError: + planning = 0 + planning_days = 0 + try: + dropped = statuses['DROPPED']['count'] + except KeyError: + dropped = 0 + + anime_mean_score = data['statistics']['anime']['meanScore'] + if not anime_mean_score: + anime_mean_score = '**Ingen**' + + most_watched_genres = ', '.join([genre['genre'] for genre in data['statistics']['anime']['genres']]) + most_watched_studios = self.__construct_favorite_entity_string(data['statistics']['anime']['studios']) + + embed = discord.Embed(title=user_name, color=color, url=url) + embed.set_author(name='Anilist', icon_url='https://anilist.co/img/logo_al.png') + embed.set_thumbnail(url=profile_pic) + embed.add_field(name='Gj.snittsvurdering gitt', value=anime_mean_score) + embed.add_field(name='Antall dager sett', value=days_watched) + embed.add_field(name='Antall episoder sett', value=episodes_watched) + embed.add_field(name='Antall anime sett', value=completed) + embed.add_field(name='Ser på nå', value=watching) + embed.add_field(name='Planlegger å se', value=f'{planning}\n({planning_days} dager)') + embed.add_field(name='Droppet', value=dropped) + if most_watched_genres: + embed.add_field(name='Mest sette sjangere', value=most_watched_genres, inline=False) + if most_watched_studios: + embed.add_field(name='Mest sette studioer', value=most_watched_studios, inline=False) + 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) + @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 """ - async with ctx.channel.typing(): - - # GraphQL - query = """ - query ($name: String) { - User (name: $name) { - name - siteUrl - avatar { - large - } - options { - profileColor - } - statistics { - anime { - minutesWatched - episodesWatched - meanScore - statuses { - count - status - minutesWatched - } - studios (limit: 3, sort: COUNT_DESC) { - studio { - name - siteUrl - } - } - genres (limit: 3, sort: COUNT_DESC) { - genre - } - } - } + # GraphQL + query = """ + query ($name: String) { + User(name: $name) { + name + siteUrl + avatar { + large } - } - """ - variables = { - 'name': bruker - } - try: - data = post( - 'https://graphql.anilist.co', - json={'query': query, 'variables': variables}).json() - data = data['data']['User'] - url = data['siteUrl'] - except TypeError: - embed = embed_templates.error_fatal(ctx, text='Kunne ikke finne brukeren!') - return await ctx.send(embed=embed) - - user_name = data['name'] - profile_pic = data['avatar']['large'] - days_watched = round(data['statistics']['anime']['minutesWatched'] / 1440, 1) - episodes_watched = data['statistics']['anime']['episodesWatched'] - color = await set_color(data['options']['profileColor']) - - statuses = {} - for status in data['statistics']['anime']['statuses']: - status_name = status['status'] - status_count = status['count'] - status_minutes = status['minutesWatched'] - statuses.update({f'{status_name}': {'count': status_count, 'minutes': status_minutes}}) - - try: - completed = statuses['COMPLETED']['count'] - except KeyError: - completed = 0 - try: - watching = statuses['CURRENT']['count'] - except KeyError: - watching = 0 - try: - planning = statuses['PLANNING']['count'] - planning_days = round(statuses['PLANNING']['minutes'] / 1440, 1) - except KeyError: - planning = 0 - planning_days = 0 - try: - dropped = statuses['DROPPED']['count'] - except KeyError: - dropped = 0 - - anime_mean_score = data['statistics']['anime']['meanScore'] - if anime_mean_score == 0: - anime_mean_score = '**Ingen**' - - genres = [] - for genre in data['statistics']['anime']['genres']: - genres.append(genre['genre']) - most_watched_genres = ', '.join(genres) - - studios = [] - for studio in data['statistics']['anime']['studios']: - studio_name = studio['studio']['name'] - studio_url = studio['studio']['siteUrl'] - studios.append(f'[{studio_name}]({studio_url})') - most_watched_studios = ' | '.join(studios) - - embed = discord.Embed(title=user_name, color=color, url=url) - embed.set_author(name='Anilist', icon_url='https://anilist.co/img/logo_al.png') - embed.set_thumbnail(url=profile_pic) - embed.add_field(name='Gj.snittsvurdering gitt', value=anime_mean_score) - embed.add_field(name='Antall dager sett', value=days_watched) - embed.add_field(name='Antall episoder sett', value=episodes_watched) - embed.add_field(name='Antall anime sett', value=completed) - embed.add_field(name='Ser på nå', value=watching) - embed.add_field(name='Planlegger å se', value=f'{planning}\n({planning_days} dager)') - embed.add_field(name='Droppet', value=dropped) - if most_watched_genres != '': - embed.add_field(name='Mest sette sjangere', value=most_watched_genres, inline=False) - if most_watched_studios != '': - embed.add_field(name='Mest sette studioer', value=most_watched_studios, inline=False) - embed = embed_templates.default_footer(ctx, embed) - await ctx.send(embed=embed) - - @anilist.command(aliases=['manga']) - async def mangastats(self, ctx, *, bruker: str): - """ - Viser mnagainformasjonen til en Anilist-profil - """ - - async with ctx.channel.typing(): - - # GraphQL - query = """ - query ($name: String) { - User(name: $name) { - name - siteUrl - avatar { - large - } - options { - profileColor - } - statistics { - manga { + options { + profileColor + } + statistics { + manga { + chaptersRead + volumesRead + meanScore + statuses { + count + status chaptersRead - volumesRead - meanScore - statuses { - count - status - chaptersRead - } - staff (limit: 3, sort: COUNT_DESC) { - staff { - name { - full - } - siteUrl + } + staff (limit: 3, sort: COUNT_DESC) { + staff { + name { + full } + siteUrl } - genres (limit: 3, sort: COUNT_DESC) { - genre + } + genres (limit: 3, sort: COUNT_DESC) { + genre + } + } + } + } + } + """ + variables = { + 'name': bruker + } + data, url = await self.__request_anilist(interaction, query, variables, 'User') + + if not data: + return + + user_name = data['name'] + profile_pic = data['avatar']['large'] + chapters_read = round(data['statistics']['manga']['chaptersRead'] / 1440, 1) + volumes_read = data['statistics']['manga']['volumesRead'] + color = self.__convert_color(data['options']['profileColor']) + + statuses = {status['status']: {'count': status['count'], 'minutes': status['chaptersRead']} for status in data['statistics']['manga']['statuses']} + + try: + completed = statuses['COMPLETED']['count'] + except KeyError: + completed = 0 + try: + reading = statuses['CURRENT']['count'] + except KeyError: + reading = 0 + try: + planning = statuses['PLANNING']['count'] + planning_days = statuses['PLANNING']['chaptersRead'] + except KeyError: + planning = 0 + planning_days = 0 + try: + dropped = statuses['DROPPED']['count'] + except KeyError: + dropped = 0 + + manga_mean_score = data['statistics']['manga']['meanScore'] + if manga_mean_score == 0: + manga_mean_score = '**Ingen**' + + most_read_genres = ', '.join([genre['genre'] for genre in data['statistics']['manga']['genres']]) + most_read_staff = self.__construct_favorite_entity_string(data['statistics']['manga']['staff']) + + embed = discord.Embed(title=user_name, color=color, url=url) + embed.set_author(name='Anilist', icon_url='https://anilist.co/img/logo_al.png') + embed.set_thumbnail(url=profile_pic) + embed.add_field(name='Gj.snittsvurdering gitt', value=manga_mean_score) + embed.add_field(name='Antall kapitler lest', value=chapters_read) + embed.add_field(name='Antall volum lest', value=volumes_read) + embed.add_field(name='Antall manga lest', value=completed) + embed.add_field(name='Leser nå', value=reading) + embed.add_field(name='Planlegger å lese', value=f'{planning}\n({planning_days} kapitler)') + embed.add_field(name='Droppet', value=dropped) + if most_read_genres: + embed.add_field(name='Mest leste sjangere', value=most_read_genres, inline=False) + if most_read_staff: + embed.add_field(name='Mest leste skapere', value=most_read_staff, inline=False) + 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) + @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 ($search: String, $isMain: Boolean) { + Media (search: $search, type: ANIME) { + siteUrl + format + status + description (asHtml: false) + episodes + duration + genres + isAdult + bannerImage + meanScore + coverImage { + large + color + } + startDate { + year + month + day + } + endDate { + year + month + day + } + title { + romaji + english + native + } + studios (isMain: $isMain) { + nodes { + name + siteUrl + } + } + staff (sort: ROLE) { + edges { + role + node { + siteUrl + name { + first + last + native } } } } } - """ - variables = { - 'name': bruker - } - - try: - data = post( - 'https://graphql.anilist.co', - json={'query': query, 'variables': variables}).json() - data = data['data']['User'] - url = data['siteUrl'] - except TypeError: - embed = embed_templates.error_fatal(ctx, text='Kunne ikke finne brukeren!') - return await ctx.send(embed=embed) - - user_name = data['name'] - profile_pic = data['avatar']['large'] - chapters_read = round(data['statistics']['manga']['chaptersRead'] / 1440, 1) - volumes_read = data['statistics']['manga']['volumesRead'] - color = await set_color(data['options']['profileColor']) - - statuses = {} - for status in data['statistics']['manga']['statuses']: - status_name = status['status'] - status_count = status['count'] - status_minutes = status['chaptersRead'] - statuses.update({f'{status_name}': {'count': status_count, 'minutes': status_minutes}}) - - try: - completed = statuses['COMPLETED']['count'] - except KeyError: - completed = 0 - try: - reading = statuses['CURRENT']['count'] - except KeyError: - reading = 0 - try: - planning = statuses['PLANNING']['count'] - planning_days = statuses['PLANNING']['chaptersRead'] - except KeyError: - planning = 0 - planning_days = 0 - try: - dropped = statuses['DROPPED']['count'] - except KeyError: - dropped = 0 - - manga_mean_score = data['statistics']['manga']['meanScore'] - if manga_mean_score == 0: - manga_mean_score = '**Ingen**' - - genres = [] - for genre in data['statistics']['manga']['genres']: - genres.append(genre['genre']) - most_read_genres = ', '.join(genres) - - staff = [] - for staffmember in data['statistics']['manga']['staff']: - staff_name = staffmember['staff']['name']['full'] - staff_url = staffmember['staff']['siteUrl'] - staff.append(f'[{staff_name}]({staff_url})') - most_read_staff = ' | '.join(staff) - - embed = discord.Embed(title=user_name, color=color, url=url) - embed.set_author(name='Anilist', icon_url='https://anilist.co/img/logo_al.png') - embed.set_thumbnail(url=profile_pic) - embed.add_field(name='Gj.snittsvurdering gitt', value=manga_mean_score) - embed.add_field(name='Antall kapitler lest', value=chapters_read) - embed.add_field(name='Antall volum lest', value=volumes_read) - embed.add_field(name='Antall manga lest', value=completed) - embed.add_field(name='Leser nå', value=reading) - embed.add_field(name='Planlegger å lese', value=f'{planning}\n({planning_days} kapitler)') - embed.add_field(name='Droppet', value=dropped) - if most_read_genres != '': - embed.add_field(name='Mest leste sjangere', value=most_read_genres, inline=False) - if most_read_staff != '': - embed.add_field(name='Mest leste skapere', value=most_read_staff, inline=False) - embed = embed_templates.default_footer(ctx, embed) - await ctx.send(embed=embed) - - @commands.bot_has_permissions(embed_links=True) - @commands.cooldown(1, 5, commands.BucketType.guild) - @commands.command() - async def anime(self, ctx, *, animenavn: str): + } """ - Viser informasjon om en anime + variables = { + 'search': navn, + 'isMain': True + } + data, url = await self.__request_anilist(interaction, query, variables, 'Media') + + if not data: + return + + nsfw = data['isAdult'] + if nsfw: + embed = embed_templates.error_fatal(interaction, text='Animen du søkte på er NSFW. ' + + 'Gjør kommandoen i en NSFW-kanal i stedet') + return await interaction.response.send_message(embed=embed) + + cover_image = data['coverImage']['large'] + banner_image = data['bannerImage'] + mean_score = data['meanScore'] + + color = self.__convert_color(data['coverImage']['color']) + + title_romaji = data['title']['romaji'] + title_native = data['title']['native'] + title_english = data['title']['english'] + titles = [title_romaji, title_native, title_english] + title_romaji, title_native, title_english = [title if title else '' for title in titles] + + description = self.__remove_html(data['description']) if data['description'] else '' + genres = ', '.join(data['genres']) + studios_string = self.__construct_favorite_entity_string(data['studios']['nodes'], studio=True) + + staff = data['staff']['edges'] + director_string = '' + for staff_member in staff: + if staff_member['role'] == 'Director': + director_first_name = staff_member['node']['name']['first'] + director_last_name = staff_member['node']['name']['last'] + director_native_name = staff_member['node']['name']['native'] + director_url = staff_member['node']['siteUrl'] + director_string += f'[{director_first_name} {director_last_name} ' +\ + f'({director_native_name})]({director_url})' + + episodes = data['episodes'] + duration = data['duration'] + + numbers = { + 'episodes': episodes, + 'duration': duration, + 'start_day': data['startDate'].get('day') if data['startDate'].get('day') else '?', + 'start_month': data['startDate'].get('month') if data['startDate'].get('month') else '?', + 'start_year': data['startDate'].get('year') if data['startDate'].get('year') else '?', + 'end_day': data['endDate'].get('day') if data['endDate'].get('day') else '?', + 'end_month': data['endDate'].get('month') if data['endDate'].get('month') else '?', + 'end_year': data['endDate'].get('year') if data['endDate'].get('year') else '?' + } + + date = self.__construct_release_schedule_string(numbers) + + length_string = '' + if numbers['duration'] == 1: + length_string += 'minutt ' + else: + length_string += 'minutter ' + if numbers['episodes'] == 1: + length_string += 'lang' + else: + length_string += 'lange' + + # Status + status = data['status'] + status = self.__convert_status(status) + + # Format + media_format = data['format'] + media_format = self.__convert_media_format(media_format) + + embed = discord.Embed(color=color, title=title_romaji, url=url, + description=f'{title_native}\n{title_english}') + embed.set_thumbnail(url=cover_image) + embed.add_field(name='Format', value=media_format) + embed.add_field(name='Status', value=status) + if studios_string: + embed.add_field(name='Studio', value=studios_string) + if director_string: + embed.add_field(name='Regissør', value=director_string) + embed.add_field(name='Utgivelsesdato', value=date) + if media_format == 'Film': + embed.add_field(name='Lengde', value=f'{duration} minutter') + else: + embed.add_field(name='Episoder', value=f'{episodes} ({duration} {length_string})') + if mean_score: + embed.add_field(name='Gj.snittsvurdering', value=f'{mean_score}/100') + embed.add_field(name='Sjangere', value=genres) + if len(description) < 1024 and description: + embed.add_field(name='Sammendrag', value=description, inline=False) + if banner_image: + embed.set_image(url=banner_image) + 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) + @anilist.command(name='manga', description='Hent informasjon om en manga') + async def anilist_manga(self, interaction: discord.Interaction, navn: str): + """ + Shows information about a manga + + Parameters + ---------- + interaction (discord.Interaction): Slash command context object + navn (str): The name of the manga to fetch information about """ - async with ctx.channel.typing(): - - # GraphQL - query = ''' - query ($search: String, $isMain: Boolean) { - Media (search: $search, type: ANIME) { + query = """ + query ($search: String) { + Media (search: $search, type: MANGA) { siteUrl format status description (asHtml: false) - episodes - duration + volumes + chapters genres isAdult bannerImage @@ -669,13 +840,7 @@ class Anime(commands.Cog): english native } - studios (isMain: $isMain) { - nodes { - name - siteUrl - } - } - staff (sort: ROLE) { + staff(sort: ROLE) { edges { role node { @@ -690,590 +855,391 @@ class Anime(commands.Cog): } } } - ''' - variables = { - 'search': animenavn, - 'isMain': True + """ + variables = { + 'search': navn + } + data, url = await self.__request_anilist(interaction, query, variables, 'Media') + + if not data: + return + + nsfw = data['isAdult'] + if nsfw: + embed = embed_templates.error_fatal(interaction, text='Mangaen du søkte på er NSFW. ' + + 'Gjør kommandoen i en NSFW-kanal i stedet') + return await interaction.response.send_message(embed=embed) + + cover_image = data['coverImage']['large'] + banner_image = data['bannerImage'] + mean_score = data['meanScore'] + + color = self.__convert_color(data['coverImage']['color']) + + title_romaji = data['title']['romaji'] + title_native = data['title']['native'] + title_english = data['title']['english'] + titles = [title_romaji, title_native, title_english] + title_romaji, title_native, title_english = [title if title else '' for title in titles] + + description = self.__remove_html(data['description']) if data.get('description') else '' + genres = ', '.join(data['genres']) + + staff = data['staff']['edges'] + staff_string = '' + for staff_member in staff: + staff_first_name = staff_member['node']['name']['first'] + staff_last_name = staff_member['node']['name']['last'] + staff_native_name = staff_member['node']['name']['native'] + staff_url = staff_member['node']['siteUrl'] + staff_string += f'[{staff_first_name} {staff_last_name} ({staff_native_name})]({staff_url})\n' + + numbers = { + 'chapters': data['chapters'], + 'volumes': data['volumes'], + 'start_day': data['startDate'].get('day') if data['startDate'].get('day') else '?', + 'start_month': data['startDate'].get('month') if data['startDate'].get('month') else '?', + 'start_year': data['startDate'].get('year') if data['startDate'].get('year') else '?', + 'end_day': data['endDate'].get('day') if data['endDate'].get('day') else '?', + 'end_month': data['endDate'].get('month') if data['endDate'].get('month') else '?', + 'end_year': data['endDate'].get('year') if data['endDate'].get('year') else '?' + } + date = self.__construct_release_schedule_string(numbers) + + chapters_string = '' + if numbers['chapters'] == 1: + chapters_string += 'kapittel' + else: + chapters_string += 'kapitler' + + status = data['status'] + status = self.__convert_status(status) + + media_format = data['format'] + media_format = self.__convert_media_format(media_format) + + embed = discord.Embed(color=color, title=title_romaji, url=url, + description=f'{title_native}\n{title_english}') + embed.set_thumbnail(url=cover_image) + embed.add_field(name='Format', value=media_format) + embed.add_field(name='Status', value=status) + if staff_string: + embed.add_field(name='Laget av', value=staff_string) + embed.add_field(name='Utgivelsesdato', value=date) + embed.add_field(name='Lengde', value=f'{numbers["chapters"]} {chapters_string}\n{numbers["volumes"]} volum') + if mean_score: + embed.add_field(name='Gj.snittsvurdering', value=f'{mean_score}/100') + embed.add_field(name='Sjangere', value=genres) + if len(description) < 1024 and description: + embed.add_field(name='Sammendrag', value=description, inline=False) + if banner_image: + embed.set_image(url=banner_image) + 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) + @anilist.command(name='karakter', description='Hent informasjon om en karakter') + async def anilist_character(self, interaction: discord.Interaction, navn: str): + """ + 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 { + full + native + } + siteUrl + favourites + image { + large + } + description (asHtml: false) + media (sort: POPULARITY_DESC, perPage: 1) { + edges { + node { + siteUrl + isAdult + title { + romaji + } + } + characterRole + voiceActors { + siteUrl + language + name { + full + native + } + } + } + } + } } - try: - data = post('https://graphql.anilist.co', json={'query': query, 'variables': variables}).json() - data = data['data']['Media'] - url = data['siteUrl'] - except TypeError: - embed = embed_templates.error_fatal('Kunne ikke finne animen!') - return await ctx.send(embed=embed) + """ + variables = { + 'search': navn + } + data, url = await self.__request_anilist(interaction, query, variables, 'Character') - nsfw = data['isAdult'] - if nsfw: - embed = embed_templates.error_fatal(ctx, text='Animen du søkte på er NSFW. ' + - 'Gjør kommandoen i en NSFW-kanal i stedet') - return await ctx.send(embed=embed) + if not data: + return - cover_image = data['coverImage']['large'] - banner_image = data['bannerImage'] - mean_score = data['meanScore'] + name_romaji = data['name']['full'] + name_native = data['name']['native'] + image = data['image']['large'] + favourites = data['favourites'] - color = data['coverImage']['color'] - if not color: - color = 0x02A9FF - else: - color = color.replace('#', '0x') - color = int(color, 0) + if data['description']: + description = self.__remove_html(data['description']) + if len(description) > 1024: + description = description[0:1020] + '...' + else: + description = '*Ingen biografi funnet* 😔' - title_romaji = data['title']['romaji'] - title_native = data['title']['native'] - title_english = data['title']['english'] - titles = [title_romaji, title_native, title_english] - for i, title in enumerate(titles): - if title is None: - titles[i] = '' - title_romaji, title_native, title_english = titles + featured_in = [] + voice_actors = [] + for media in data['media']['edges']: + nsfwtag = '🔞' if media['node']['isAdult'] else '' + media_name = media['node']['title']['romaji'] + media_url = media['node']['siteUrl'] + media_role = self.__convert_role_names(media['characterRole']) + for voice_actor in media['voiceActors']: + voice_actor_url = voice_actor['siteUrl'] + voice_actor_language = self.__convert_language_names(voice_actor['language']) + voice_actor_name = voice_actor['name']['full'] + if voice_actor['name']['native'] != ' ' and voice_actor['name']['native'] is not None: + voice_actor_name += f' ({voice_actor["name"]["native"]}' + voice_actors.append(f'{voice_actor_language} stemme: [{voice_actor_name}]({voice_actor_url})') - description = data['description'] - if description: - description = await remove_html(data['description']) - else: - description = '' - genres = ', '.join(data['genres']) + voice_actors_string = '\n'.join(voice_actors) + featured_in.append(f'[{media_name}]({media_url}){nsfwtag} som {media_role}\n{voice_actors_string}') - studios = data['studios']['nodes'] - studios_string = '' - for studio in studios: - studio_name = studio['name'] - studio_url = studio['siteUrl'] - studios_string += f'[{studio_name}]({studio_url})\n' + featured_in = '\n\n'.join(featured_in) - staff = data['staff']['edges'] - director_string = '' - for staff_member in staff: - if staff_member['role'] == 'Director': - director_first_name = staff_member['node']['name']['first'] - director_last_name = staff_member['node']['name']['last'] - director_native_name = staff_member['node']['name']['native'] - director_url = staff_member['node']['siteUrl'] - director_string += f'[{director_first_name} {director_last_name} ' +\ - f'({director_native_name})]({director_url})' + embed = discord.Embed(color=0x02A9FF, title=name_romaji, url=url, description=name_native) + embed.set_thumbnail(url=image) + embed.add_field(name='Antall favoritter på Anilist', value=favourites, inline=False) + embed.add_field(name='Biografi', value=description, inline=False) + embed.add_field(name='Er med i bl.a.', value=featured_in, inline=False) + embed_templates.default_footer(interaction, embed) + await interaction.response.send_message(embed=embed) - episodes = data['episodes'] - duration = data['duration'] - - release_day = data['startDate']['day'] - release_month = data['startDate']['month'] - release_year = data['startDate']['year'] - end_day = data['endDate']['day'] - end_month = data['endDate']['month'] - end_year = data['endDate']['year'] - - numbers = [episodes, duration, release_day, release_month, release_year, end_day, end_month, end_year] - for i, number in enumerate(numbers): - if number is None: - numbers[i] = '?' - episodes, duration, release_day, release_month, release_year, end_day, end_month, end_year = numbers - length_string = '' - if duration == 1: - length_string += 'minutt ' - else: - length_string += 'minutter ' - if episodes == 1: - length_string += 'lang' - else: - length_string += 'lange' - - release_date = f'{release_day}.{release_month}.{release_year}' - end_date = f'{end_day}.{end_month}.{end_year}' - if release_date == end_date: - date = release_date - else: - date = f'{release_date} - {end_date}' - - status = data['status'] - status = await convert_status(status) - - media_format = data['format'] - media_format = await convert_media_format(media_format) - - embed = discord.Embed(color=color, title=title_romaji, url=url, - description=f'{title_native}\n{title_english}') - embed.set_thumbnail(url=cover_image) - embed.add_field(name='Format', value=media_format) - embed.add_field(name='Status', value=status) - if studios_string: - embed.add_field(name='Studio', value=studios_string) - if director_string: - embed.add_field(name='Regissør', value=director_string) - embed.add_field(name='Utgivelsesdato', value=date) - if media_format == 'Film': - embed.add_field(name='Lengde', value=f'{duration} minutter') - else: - embed.add_field(name='Episoder', value=f'{episodes} ({duration} {length_string})') - if mean_score: - embed.add_field(name='Gj.snittsvurdering', value=f'{mean_score}/100') - embed.add_field(name='Sjangere', value=genres) - if len(description) < 1024 and description != '': - embed.add_field(name='Sammendrag', value=description, inline=False) - if banner_image: - embed.set_image(url=banner_image) - embed = embed_templates.default_footer(ctx, embed) - await ctx.send(embed=embed) - - @commands.bot_has_permissions(embed_links=True) - @commands.cooldown(1, 5, commands.BucketType.guild) - @commands.command() - async def manga(self, ctx, *, manganavn: str): + @app_commands.checks.bot_has_permissions(embed_links=True) + @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): """ - Viser informasjon om en manga + Shows information about a creator + + Parameters + ---------- + interaction (discord.Interaction): Slash command context object + navn (str): The name of the creator to fetch information about """ - async with ctx.channel.typing(): + query = """ + query ($search: String) { + Staff(search: $search) { + name { + full + native + } + siteUrl + image { + large + } + language + favourites + description(asHtml: false) + staffMedia(sort: POPULARITY_DESC, perPage: 2) { + edges { + staffRole + node { + siteUrl + isAdult + title { + romaji + } + } + } + } + characters(sort: FAVOURITES_DESC, perPage: 2) { + edges { + node { + name { + full + native + } + siteUrl + } + } + } + } + } + """ + variables = { + 'search': navn + } + data, url = await self.__request_anilist(interaction, query, variables, 'Staff') - query = ''' - query ($search: String) { - Media (search: $search, type: MANGA) { + if not data: + return + + name_romaji = data['name']['full'] + name_native = data['name']['native'] + image = data['image']['large'] + favourites = data['favourites'] + + if data['description']: + description = self.__remove_html(data['description']) + if len(description) > 1024: + description = description[0:1020] + '...' + else: + description = '*Ingen biografi funnet* 😔' + + featured_in = [] + for media in data['staffMedia']['edges']: + nsfwtag = '🔞' if media['node']['isAdult'] else '' + media_name = media['node']['title']['romaji'] + media_url = media['node']['siteUrl'] + media_role = media['staffRole'] + featured_in.append(f'[{media_name}]({media_url}){nsfwtag}\n{media_role}') + featured_in = '\n\n'.join(featured_in) + + characters = [] + for character in data['characters']['edges']: + character_url = character['node']['siteUrl'] + character_name = character['node']['name']['full'] + if character['node']['name']['native'] != ' ' and character['node']['name']['native'] is not None: + character_name += f' ({character["node"]["name"]["native"]}' + characters.append(f'[{character_name}]({character_url})') + characters = '\n\n'.join(characters) + + embed = discord.Embed(color=0x02A9FF, title=name_romaji, url=url, description=name_native) + embed.set_thumbnail(url=image) + embed.add_field(name='Antall favoritter på Anilist', value=favourites, inline=False) + embed.add_field(name='Biografi', value=description, inline=False) + if characters: + embed.add_field(name='Har vært stemmen til bl.a.', value=characters, inline=False) + if featured_in: + embed.add_field(name='Har deltatt i produksjonen av bl.a.', value=featured_in, inline=False) + 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) + @anilist.command(name='studio', description='Hent informasjon om et studio') + async def anilist_studio(self, interaction: discord.Interaction, navn: str): + """ + 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 + siteUrl + favourites + media(sort: POPULARITY_DESC, perPage: 3) { + nodes { siteUrl - format - status - description (asHtml: false) - volumes - chapters - genres - isAdult - bannerImage - meanScore coverImage { large - color - } - startDate { - year - month - day - } - endDate { - year - month - day } + isAdult title { - romaji - english - native - } - staff(sort: ROLE) { - edges { - role - node { - siteUrl - name { - first - last - native - } - } - } + romaji } } + } } - ''' - variables = { - 'search': manganavn - } - try: - data = post('https://graphql.anilist.co', json={'query': query, 'variables': variables}).json() - data = data['data']['Media'] - url = data['siteUrl'] - except TypeError: - embed = embed_templates.error_fatal('Kunne ikke finne Mangaen!') - return await ctx.send(embed=embed) + } + """ + variables = { + 'search': navn + } + data, url = await self.__request_anilist(interaction, query, variables, 'Studio') - nsfw = data['isAdult'] - if nsfw: - embed = embed_templates.error_fatal(ctx, text='Mangaen du søkte på er NSFW. ' + - 'Gjør kommandoen i en NSFW-kanal i stedet') - return await ctx.send(embed=embed) + if not data: + return - cover_image = data['coverImage']['large'] - banner_image = data['bannerImage'] - mean_score = data['meanScore'] + name = data['name'] + favourites = data['favourites'] - color = data['coverImage']['color'] - if not color: - color = 0x02A9FF - else: - color = color.replace('#', '0x') - color = int(color, 0) + popular_media = [] + for media in data['media']['nodes']: + nsfwtag = '🔞' if media['isAdult'] else '' + media_name = media['title']['romaji'] + media_url = media['siteUrl'] + popular_media.append(f'- [{media_name}]({media_url}){nsfwtag}') + popular_media = '\n'.join(popular_media) - title_romaji = data['title']['romaji'] - title_native = data['title']['native'] - title_english = data['title']['english'] - titles = [title_romaji, title_native, title_english] - for i, title in enumerate(titles): - if title is None: - titles[i] = '' - title_romaji, title_native, title_english = titles - - description = data['description'] - if description: - description = await remove_html(data['description']) - else: - description = '' - genres = ', '.join(data['genres']) - - staff = data['staff']['edges'] - staff_string = '' - for staff_member in staff: - staff_first_name = staff_member['node']['name']['first'] - staff_last_name = staff_member['node']['name']['last'] - staff_native_name = staff_member['node']['name']['native'] - staff_url = staff_member['node']['siteUrl'] - staff_string += f'[{staff_first_name} {staff_last_name} ({staff_native_name})]({staff_url})\n' - - chapters = data['chapters'] - volumes = data['volumes'] - - release_day = data['startDate']['day'] - release_month = data['startDate']['month'] - release_year = data['startDate']['year'] - - numbers = [chapters, volumes, release_day, release_month, release_year] - for i, number in enumerate(numbers): - if number is None: - numbers[i] = '?' - chapters, volumes, release_day, release_month, release_year = numbers - release_date = f'{release_day}.{release_month}.{release_year}' - - chapters_string = '' - if chapters == 1: - chapters_string += 'kapittel' - else: - chapters_string += 'kapitler' - - status = data['status'] - status = await convert_status(status) - - media_format = data['format'] - media_format = await convert_media_format(media_format) - - embed = discord.Embed(color=color, title=title_romaji, url=url, - description=f'{title_native}\n{title_english}') - embed.set_thumbnail(url=cover_image) - embed.add_field(name='Format', value=media_format) - embed.add_field(name='Status', value=status) - if staff_string: - embed.add_field(name='Laget av', value=staff_string) - embed.add_field(name='Utgivelsesdato', value=release_date) - embed.add_field(name='Lengde', value=f'{chapters} {chapters_string}\n{volumes} volum') - if mean_score: - embed.add_field(name='Gj.snittsvurdering', value=f'{mean_score}/100') - embed.add_field(name='Sjangere', value=genres) - if len(description) < 1024 and description != '': - embed.add_field(name='Sammendrag', value=description, inline=False) - if banner_image: - embed.set_image(url=banner_image) - embed = embed_templates.default_footer(ctx, embed) - await ctx.send(embed=embed) - - @commands.bot_has_permissions(embed_links=True) - @commands.cooldown(1, 5, commands.BucketType.guild) - @commands.command(aliases=['character']) - async def karakter(self, ctx, *, karakternavn: str): - """Viser informasjon om en karakter""" - - async with ctx.channel.typing(): - - query = ''' - query ($search: String) { - Character (search: $search) { - name { - full - native - } + query2 = """ + query ($search: String) { + Studio(search: $search) { + media(sort: START_DATE_DESC) { + nodes { siteUrl - favourites - image { - large - } - description (asHtml: false) - media (sort: POPULARITY_DESC, perPage: 1) { - edges { - node { - siteUrl - isAdult - title { - romaji - } - } - characterRole - voiceActors { - siteUrl - language - name { - full - native - } - } - } + status + isAdult + title { + romaji } } + } } - ''' - variables = { - 'search': karakternavn - } - try: - data = post('https://graphql.anilist.co', json={'query': query, 'variables': variables}).json() - data = data['data']['Character'] - url = data['siteUrl'] - except TypeError: - embed = embed_templates.error_fatal('Kunne ikke finne karakteren!') - return await ctx.send(embed=embed) - - name_romaji = data['name']['full'] - name_native = data['name']['native'] - image = data['image']['large'] - favourites = data['favourites'] - - if data['description']: - description = await remove_html(data['description']) - if len(description) > 1024: - description = description[0:1020] + '...' - else: - description = '*Ingen biografi funnet* 😔' - - featured_in = [] - voice_actors = [] - for media in data['media']['edges']: - if media['node']['isAdult'] is True: - nsfwtag = ' (NSFW)' - else: - nsfwtag = '' - media_name = media['node']['title']['romaji'] - media_url = media['node']['siteUrl'] - media_role = await convert_role_names(media['characterRole']) - for voice_actor in media['voiceActors']: - voice_actor_url = voice_actor['siteUrl'] - voice_actor_language = await convert_language_names(voice_actor['language']) - voice_actor_name = voice_actor['name']['full'] - if voice_actor['name']['native'] != ' ' and voice_actor['name']['native'] is not None: - voice_actor_name += ' (' + voice_actor['name']['native'] + ')' - voice_actors.append(f'{voice_actor_language} stemme: [{voice_actor_name}]({voice_actor_url})') - - voice_actors_string = '\n'.join(voice_actors) - featured_in.append(f'[{media_name}]({media_url}){nsfwtag} som {media_role}\n{voice_actors_string}') - - featured_in = '\n\n'.join(featured_in) - - embed = discord.Embed(color=0x02A9FF, title=name_romaji, url=url, description=name_native) - embed.set_thumbnail(url=image) - embed.add_field(name='Antall favoritter på Anilist', value=favourites, inline=False) - embed.add_field(name='Biografi', value=description, inline=False) - embed.add_field(name='Er med i bl.a.', value=featured_in, inline=False) - embed = embed_templates.default_footer(ctx, embed) - await ctx.send(embed=embed) - - @commands.bot_has_permissions(embed_links=True) - @commands.cooldown(1, 5, commands.BucketType.guild) - @commands.command(aliases=['staff']) - async def skaper(self, ctx, *, skapernavn: str): - """Viser informasjon om en skaper""" - - async with ctx.channel.typing(): - - query = ''' - query ($search: String) { - Staff(search: $search) { - name { - full - native - } - siteUrl - image { - large - } - language - favourites - description(asHtml: false) - staffMedia(sort: POPULARITY_DESC, perPage: 2) { - edges { - staffRole - node { - siteUrl - isAdult - title { - romaji - } - } - } - } - characters(sort: FAVOURITES_DESC, perPage: 2) { - edges { - node { - name { - full - native - } - siteUrl - } - } - } - } } - ''' - variables = { - 'search': skapernavn - } - try: - data = post('https://graphql.anilist.co', json={'query': query, 'variables': variables}).json() - data = data['data']['Staff'] - url = data['siteUrl'] - except TypeError: - embed = embed_templates.error_fatal('Kunne ikke finne skaperen!') - return await ctx.send(embed=embed) + """ + data = requests.post('https://graphql.anilist.co', json={'query': query2, 'variables': variables}, timeout=10).json() + data = data['data']['Studio'] - name_romaji = data['name']['full'] - name_native = data['name']['native'] - image = data['image']['large'] - language = data['language'] - favourites = data['favourites'] - - if data['description']: - description = await remove_html(data['description']) - if len(description) > 1024: - description = description[0:1020] + '...' + recent_media = [] + upcoming_media = [] + for media in data['media']['nodes']: + nsfwtag = '🔞' if media['isAdult'] else '' + media_name = media['title']['romaji'] + media_url = media['siteUrl'] + if media['status'] == 'NOT_YET_RELEASED': + upcoming_media.append(f'- [{media_name}]({media_url}){nsfwtag}') else: - description = '*Ingen biografi funnet* 😔' + recent_media.append(f'- [{media_name}]({media_url}){nsfwtag}') + recent_media = '\n'.join(recent_media[0:3]) + upcoming_media = '\n'.join(upcoming_media[0:3]) - featured_in = [] - for media in data['staffMedia']['edges']: - if media['node']['isAdult'] is True: - nsfwtag = ' (NSFW)' - else: - nsfwtag = '' - media_name = media['node']['title']['romaji'] - media_url = media['node']['siteUrl'] - media_role = media['staffRole'] - featured_in.append(f'[{media_name}]({media_url}){nsfwtag}\n{media_role}') - featured_in = '\n\n'.join(featured_in) - - characters = [] - for character in data['characters']['edges']: - character_url = character['node']['siteUrl'] - character_name = character['node']['name']['full'] - if character['node']['name']['native'] != ' ' and character['node']['name']['native'] is not None: - character_name += ' (' + character['node']['name']['native'] + ')' - characters.append(f'[{character_name}]({character_url})') - characters = '\n\n'.join(characters) - - embed = discord.Embed(color=0x02A9FF, title=name_romaji, url=url, description=name_native) - embed.set_thumbnail(url=image) - embed.add_field(name='Antall favoritter på Anilist', value=favourites, inline=False) - embed.add_field(name='Biografi', value=description, inline=False) - if characters != '': - embed.add_field(name='Har vært stemmen til bl.a.', value=characters, inline=False) - if featured_in != '': - embed.add_field(name='Har deltatt i produksjonen av bl.a.', value=featured_in, inline=False) - embed = embed_templates.default_footer(ctx, embed) - await ctx.send(embed=embed) - - @commands.bot_has_permissions(embed_links=True) - @commands.cooldown(1, 5, commands.BucketType.guild) - @commands.command() - async def studio(self, ctx, *, studionavn: str): - """Viser informasjon om et animestudio""" - - async with ctx.channel.typing(): - - query = ''' - query ($search: String) { - Studio(search: $search) { - name - siteUrl - favourites - media(sort: POPULARITY_DESC, perPage: 3) { - nodes { - siteUrl - coverImage { - large - } - isAdult - title { - romaji - } - } - } - } - } - ''' - variables = { - 'search': studionavn - } - try: - data = post('https://graphql.anilist.co', json={'query': query, 'variables': variables}).json() - data = data['data']['Studio'] - url = data['siteUrl'] - except TypeError: - embed = embed_templates.error_fatal('Kunne ikke finne studioet!') - return await ctx.send(embed=embed) - - name = data['name'] - favourites = data['favourites'] - - popular_media = [] - for media in data['media']['nodes']: - if media['isAdult'] is True: - nsfwtag = ' (NSFW)' - else: - nsfwtag = '' - media_name = media['title']['romaji'] - media_url = media['siteUrl'] - popular_media.append(f'- [{media_name}]({media_url}){nsfwtag}') - popular_media = '\n'.join(popular_media) - - query2 = ''' - query ($search: String) { - Studio(search: $search) { - media(sort: START_DATE_DESC) { - nodes { - siteUrl - status - isAdult - title { - romaji - } - } - } - } - } - ''' - data = post('https://graphql.anilist.co', json={'query': query2, 'variables': variables}).json() - data = data['data']['Studio'] - - recent_media = [] - upcoming_media = [] - for media in data['media']['nodes']: - if media['isAdult'] is True: - nsfwtag = ' (NSFW)' - else: - nsfwtag = '' - media_name = media['title']['romaji'] - media_url = media['siteUrl'] - if media['status'] == 'NOT_YET_RELEASED': - upcoming_media.append(f'- [{media_name}]({media_url}){nsfwtag}') - else: - recent_media.append(f'- [{media_name}]({media_url}){nsfwtag}') - recent_media = '\n'.join(recent_media[0:3]) - upcoming_media = '\n'.join(upcoming_media[0:3]) - - embed = discord.Embed(color=0x02A9FF, title=name, url=url) - embed.add_field(name='Antall favoritter på Anilist', value=favourites, inline=False) - if popular_media != '': - embed.add_field(name='Mest Populære Anime', value=popular_media, inline=False) - if recent_media != '': - embed.add_field(name='Nyeste Utgitte Anime', value=recent_media, inline=False) - if upcoming_media != '': - embed.add_field(name='Kommende Anime', value=upcoming_media, inline=False) - embed = embed_templates.default_footer(ctx, embed) - await ctx.send(embed=embed) + embed = discord.Embed(color=0x02A9FF, title=name, url=url) + embed.add_field(name='Antall favoritter på Anilist', value=favourites, inline=False) + if popular_media: + embed.add_field(name='Mest Populære Anime', value=popular_media, inline=False) + if recent_media: + embed.add_field(name='Nyeste Utgitte Anime', value=recent_media, inline=False) + if upcoming_media: + embed.add_field(name='Kommende Anime', value=upcoming_media, inline=False) + embed_templates.default_footer(interaction, embed) + await interaction.response.send_message(embed=embed) -def setup(bot): - bot.add_cog(Anime(bot)) +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 905d691..bf1f99b 100644 --- a/src/cogs/birthday.py +++ b/src/cogs/birthday.py @@ -1,21 +1,30 @@ -from discord.ext import commands -import discord - -import psycopg2 -import aiocron -import asyncio from datetime import datetime -from dateutil.relativedelta import relativedelta -from cogs.utils import embed_templates, discord_utils +from dateutil.relativedelta import relativedelta +import discord +from discord import app_commands +from discord.ext import commands +import psycopg2 + +from cogs.utils import discord_utils, embed_templates class Birthday(commands.Cog): - def __init__(self, bot): + """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""" + self.cursor.execute( """ CREATE TABLE IF NOT EXISTS birthdays ( @@ -26,22 +35,61 @@ class Birthday(commands.Cog): ) self.bot.db_connection.commit() - def _fetch_user_birthday(self, user_id): - self.cursor.execute("SELECT birthday FROM birthdays WHERE discord_id = (%s)", (user_id,)) - return self.cursor.fetchone() + def __fetch_user_birthday(self, user_id: int) -> datetime | None: + """ + 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 for a user + + Parameters + ---------- + user_id (int): The Discord user ID + + Returns + ---------- + (datetime): The date of the next birthday + """ - def _fetch_user_next_birthday(self, user_id): 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,) ) - return self.cursor.fetchone() + results = self.cursor.fetchone() + if results: + birthday = results[2] + return datetime(birthday.year, birthday.month, birthday.day) + else: + 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 + + Returns + ---------- + (list[tuple[int, datetime, datetime]]): A list of tuples containing the user ID, the date of birth and the next birthday + """ - def _fetch_next_birthdays(self): self.cursor.execute( """ SELECT *, CAST(birthday + ((EXTRACT(YEAR FROM AGE(birthday)) + 1) * interval '1' YEAR) AS DATE) AS next_birthday @@ -50,124 +98,205 @@ class Birthday(commands.Cog): LIMIT 5 """ ) - return self.cursor.fetchall() + results = self.cursor.fetchall() + + # Convert date objects to datetime objects + for result in results: + _, birthday, next_birthday = result + birthday = datetime(birthday.year, birthday.month, birthday.day) + next_birthday = datetime(next_birthday.year, next_birthday.month, next_birthday.day) + + return results + + 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 + """ - def _set_user_birthday(self, user_id, birthday): try: - self.cursor.execute("INSERT INTO birthdays (discord_id, birthday) VALUES (%s, %s)", (user_id, birthday)) + self.cursor.execute( + """ + INSERT INTO birthdays (discord_id, birthday) + VALUES (%s, %s) + """, (user_id, birthday)) 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 * * *") - async def _check_birthdays(self): - await asyncio.sleep(1) - self.cursor.execute( - """ - SELECT * - FROM birthdays - WHERE EXTRACT(MONTH FROM birthday) = EXTRACT(MONTH FROM current_date) - AND EXTRACT(DAY FROM birthday) = EXTRACT(DAY FROM current_date); - """ - ) + # @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 + # + # self.cursor.execute( + # """ + # SELECT * + # FROM birthdays + # WHERE EXTRACT(MONTH FROM birthday) = EXTRACT(MONTH FROM current_date) + # AND EXTRACT(DAY FROM birthday) = EXTRACT(DAY FROM current_date); + # """ + # ) + # + # birthdays = self.cursor.fetchall() + # if birthdays: + # guild = self.bot.get_guild(747542543750660178) + # channel = guild.get_channel(747542544291987597) + # for birthday in birthdays: + # user = self.bot.get_user(birthday[0]) + # if user: + # await channel.send(f'Gratulrer med dagen {user.mention}!') - birthdays = self.cursor.fetchall() - if birthdays: - guild = self.bot.get_guild(747542543750660178) - channel = guild.get_channel(747542544291987597) - for birthday in birthdays: - user = self.bot.get_user(birthday[0]) - if user: - 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(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. - @commands.cooldown(1, 5, commands.BucketType.user) - @commands.command() - async def settbursdag(self, ctx, date): + 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.strptime(date, "%d.%m.%Y") + date = datetime(år, måned, dag) except ValueError: - return await ctx.send(embed=embed_templates.error_fatal(ctx, text="Feil format, gjøk!")) + return await interaction.response.send_message( + embed=embed_templates.error_fatal(interaction, text='ikke en gyldig dato, gjøk!'), + ephemeral=True + ) if date.year < 1970: - return await ctx.send( - embed=embed_templates.error_fatal(ctx, text="Ok boomer, men nei. Hvorfor? Unix timestamps. Google it") + return await interaction.response.send_message( + embed=embed_templates.error_fatal(interaction, text='Ok boomer, men nei. Hvorfor? Unix timestamps. Google it'), + ephemeral=True ) - self._set_user_birthday(ctx.author.id, date) + if date > datetime.now(): + return await interaction.response.send_message( + embed=embed_templates.error_fatal(interaction, text='Du kan ikke sette en fremtidig dato!'), + ephemeral=True + ) - embed = embed_templates.success(ctx, text=f"Bursdag satt til ") - await ctx.send(embed=embed) + self.__set_user_birthday(interaction.user.id, date) - @commands.cooldown(1, 5, commands.BucketType.user) - @commands.command() - async def fjernbursdag(self, ctx): + embed = embed_templates.success(interaction, text=f'Bursdag satt til {discord.utils.format_dt(date, style="D")}') + await interaction.response.send_message(embed=embed) - self.cursor.execute("DELETE FROM birthdays WHERE discord_id = (%s)", (ctx.author.id,)) + @app_commands.checks.bot_has_permissions(embed_links=True) + @app_commands.checks.cooldown(1, 5) + @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(ctx, text="Bursdag fjernet") - await ctx.send(embed=embed) + embed = embed_templates.success(interaction, text='Bursdag fjernet') + await interaction.response.send_message(embed=embed) - @commands.cooldown(1, 5, commands.BucketType.user) - @commands.command() - async def bursdag(self, ctx, *, bruker: discord.Member = None): + @app_commands.checks.bot_has_permissions(embed_links=True) + @app_commands.checks.cooldown(1, 5) + @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 = ctx.author + bruker = interaction.user - user = self._fetch_user_birthday(bruker.id) + user = self.__fetch_user_birthday(bruker.id) if not user: - return await ctx.send( - embed=embed_templates.error_fatal(ctx, text="Brukeren har ikke registrert bursdagen sin") + return await interaction.response.send_message( + embed=embed_templates.error_fatal(interaction, text='Brukeren har ikke registrert bursdagen sin') ) - birthday = datetime(user[0].year, user[0].month, user[0].day) + # Current age information + birthday = datetime(user.year, user.month, user.day) days_old = (datetime.now() - birthday).days years_old = relativedelta(datetime.now(), birthday).years - # Next birthday - birthday_current_year = birthday.replace(year=datetime.now().year) - if (datetime.now() - birthday_current_year).days < 0: - next_birthday = birthday_current_year - else: - next_birthday = birthday_current_year.replace(year=datetime.now().year + 1) + # Next birthday information + next_birthday = self.__fetch_user_next_birthday(bruker.id) next_birthday_days = (next_birthday - datetime.now()).days - embed = discord.Embed(description=bruker.mention, color=discord_utils.get_user_color(bruker)) - embed.set_thumbnail(url=bruker.avatar_url) - embed.set_author(name=bruker.name, icon_url=bruker.avatar_url) - embed.add_field(name="Bursdag", value=f"") - 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(ctx, embed) - await ctx.send(embed=embed) + 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_templates.default_footer(interaction, embed) + await interaction.response.send_message(embed=embed) - @commands.command() - async def kommendebursdager(self, ctx): + @app_commands.guild_only() + @app_commands.checks.bot_has_permissions(embed_links=True) + @app_commands.checks.cooldown(1, 5) + @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. - next_birthdays = self._fetch_next_birthdays() + Parameters + ---------- + interaction (discord.Interaction): Slash command context object + """ + + next_birthdays = self.__fetch_next_birthdays() if not next_birthdays: - return await ctx.send(embed=embed_templates.error_fatal(ctx, 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: - discord_user = ctx.guild.get_member(user[0]) - if not discord_user: + user_id, _, next_birthday = user + try: + discord_user = await interaction.guild.fetch_member(user_id) # Guild should always be available because of guild_only + except (discord.NotFound, discord.HTTPException): continue - next_birthday = datetime(user[2].year, user[2].month, user[2].day) - next_birthday_days = (next_birthday - datetime.now()).days - birthday_string += f"{discord_user.name}#{discord_user.discriminator} - ({next_birthday_days} dager)\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' - embed = discord.Embed(title="Kommende bursdager", description=birthday_string) - embed_templates.default_footer(ctx, embed) - await ctx.send(embed=embed) + if not birthday_string: + 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_templates.default_footer(interaction, embed) + await interaction.response.send_message(embed=embed) -def setup(bot): - bot.add_cog(Birthday(bot)) +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 f72d115..5e940b5 100644 --- a/src/cogs/botinfo.py +++ b/src/cogs/botinfo.py @@ -1,48 +1,55 @@ -from discord.ext import commands -import discord - -from time import time, perf_counter +from os import environ, getpid import platform -from os import getpid, environ +from time import time + +import discord +from discord import app_commands +from discord.ext import commands from psutil import Process from cogs.utils import embed_templates class BotInfo(commands.Cog): - def __init__(self, bot): + """View information about the bot""" + + def __init__(self, bot: commands.Bot): + """ + Parameters + ---------- + bot (commands.Bot): The bot instance + """ + self.bot = bot - @commands.bot_has_permissions(embed_links=True, external_emojis=True) - @commands.cooldown(1, 2, commands.BucketType.guild) - @commands.command(aliases=['info', 'about', 'om']) - async def botinfo(self, ctx): + @app_commands.checks.bot_has_permissions(embed_links=True, external_emojis=True) + @app_commands.checks.cooldown(1, 2) + @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) - # Ping - start = perf_counter() - status_msg = await ctx.send('Beregner ping...') - end = perf_counter() - ping = int((end - start) * 1000) - # Memory usage process = Process(getpid()) memory_usage = round(process.memory_info().rss / 1000000, 1) # Member stats - """ - This is a pretty stupid way of doing things but it's the best I can come up with. - Using curly brackets for both dicts and sets is retarded becuase - I have to do this shit in order to create a fucking empty set. - - Apparently you can't access user presences, only member presences - Because of that I have to use sets instead of ints in order to not count duplicate users. - Fucking hell discord... - """ + # + # This is a pretty stupid way of doing things but it's the best I can come up with. + # Using curly brackets for both dicts and sets is retarded becuase + # I have to do this shit in order to create a fucking empty set. + # + # Apparently you can't access user presences, only member presences + # Because of that I have to use sets instead of ints in order to not count duplicate users. + # Fucking hell discord... total_members = set([]) online_members = set([]) idle_members = set([]) @@ -61,12 +68,12 @@ class BotInfo(commands.Cog): offline_members.add(member.id) # Build embed - embed = discord.Embed(color=ctx.me.color, url=self.bot.misc['website']) - embed.set_author(name=dev.name, icon_url=dev.avatar_url) - embed.set_thumbnail(url=self.bot.user.avatar_url) + embed = discord.Embed(color=interaction.client.user.color, url=self.bot.misc['website']) + embed.set_author(name=dev.name, icon_url=dev.display_avatar) + embed.set_thumbnail(url=self.bot.user.display_avatar) embed.add_field(name='Dev', value=f'{dev.mention}\n{dev.name}#{dev.discriminator}') - embed.add_field(name='Oppetid', value=self.get_uptime()) - embed.add_field(name='Ping', value=f'Ekte ping: {ping} ms\nWebsocket ping: {int(self.bot.latency * 1000)} ms') + embed.add_field(name='Oppetid', value=self.__get_uptime()) + embed.add_field(name='Ping', value=f'Websocket ping: {self.__get_ping()} ms') embed.add_field(name='Servere', value=len(self.bot.guilds)) embed.add_field(name='Discord.py', value=discord.__version__) embed.add_field(name='Python', value=platform.python_version()) @@ -81,49 +88,60 @@ class BotInfo(commands.Cog): f'{self.bot.emoji["offline"]}{len(offline_members)}') embed.add_field(name='Lenker', value=f'[Nettside]({self.bot.misc["website"]}) | ' + f'[Kildekode]({self.bot.misc["source_code"]})') - embed_templates.default_footer(ctx, embed) - await status_msg.edit(embed=embed, content=None) + embed_templates.default_footer(interaction, embed) + await interaction.response.send_message(embed=embed) - @commands.bot_has_permissions(embed_links=True) - @commands.cooldown(1, 2, commands.BucketType.guild) - @commands.command(aliases=['uptime']) - async def oppetid(self, ctx): + @app_commands.checks.bot_has_permissions(embed_links=True) + @app_commands.checks.cooldown(1, 2) + @app_commands.command(name='oppetid', description='Se hvor lenge botten har kjørt') + async def uptime(self, interaction: discord.Interaction): """ - Viser hvor lenge bot har vært kjørende - """ - - embed = discord.Embed(color=ctx.me.color) - embed.add_field(name='🔌 Oppetid', value=self.get_uptime()) - embed_templates.default_footer(ctx, embed) - await ctx.send(embed=embed) - - @commands.bot_has_permissions(embed_links=True) - @commands.cooldown(1, 2, commands.BucketType.guild) - @commands.command() - async def ping(self, ctx): - """ - Viser bot sin ping - """ - - start = perf_counter() - status_msg = await ctx.send('Pinging...') - end = perf_counter() - ping = int((end - start) * 1000) - - embed = discord.Embed(color=ctx.me.color) - embed.add_field( - name='📶 Ping', - value=f'**Ekte ping:** {ping} ms\n**Websocket ping:** {int(self.bot.latency * 1000)} ms' - ) - embed_templates.default_footer(ctx, embed) - await status_msg.edit(embed=embed, content=None) - - def get_uptime(self) -> str: - """ - Returns the current uptime of the bot in string format + 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()) + 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, 2) + @app_commands.command(name='ping', description='Se botten sin nåværende ping') + async def ping(self, interaction: discord.Interaction): + """ + 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', + value=f'**Websocket ping:** {self.__get_ping()} ms' + ) + embed_templates.default_footer(interaction, embed) + await interaction.response.send_message(embed=embed, content=None) + + def __get_ping(self) -> int: + """ + Get the bot's ping in milliseconds + + Returns + ---------- + (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 Returns ---------- @@ -139,5 +157,13 @@ class BotInfo(commands.Cog): return f'{days}d, {hours}t, {minutes}m, {seconds}s' -def setup(bot): - bot.add_cog(BotInfo(bot)) +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/camera_channel.py b/src/cogs/deprecated/camera_channel.py index 5c4a1c4..11e9889 100644 --- a/src/cogs/deprecated/camera_channel.py +++ b/src/cogs/deprecated/camera_channel.py @@ -4,7 +4,7 @@ import asyncio class CameraChannel(commands.Cog): - def __init__(self, bot): + def __init__(self, bot: commands.Bot): self.bot = bot async def on_voice_state_update(self, member, before, after): @@ -20,7 +20,7 @@ class CameraChannel(commands.Cog): await member.move_to(channel=None) -def setup(bot): +async def setup(bot: commands.Bot): #bot.add_listener(CameraChannel(bot).on_voice_state_update, 'on_voice_state_update') - #bot.add_cog(CameraChannel(bot)) + #await bot.add_cog(CameraChannel(bot)) pass diff --git a/src/cogs/deprecated/social_credit.py b/src/cogs/deprecated/social_credit.py index 29afed7..4791b08 100644 --- a/src/cogs/deprecated/social_credit.py +++ b/src/cogs/deprecated/social_credit.py @@ -47,7 +47,7 @@ class CreditUser: class SocialCredit(commands.Cog): - def __init__(self, bot): + def __init__(self, bot: commands.Bot): self.bot = bot self.fuck_uwu.start() @@ -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,15 +259,15 @@ 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) -def setup(bot): - bot.add_cog(SocialCredit(bot)) +async def setup(bot: commands.Bot): + await bot.add_cog(SocialCredit(bot)) diff --git a/src/cogs/deprecated/vær.py b/src/cogs/deprecated/vær.py index 03d6eab..b21499a 100644 --- a/src/cogs/deprecated/vær.py +++ b/src/cogs/deprecated/vær.py @@ -7,10 +7,10 @@ import json class Vær(commands.Cog): - def __init__(self, bot): + 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,9 +19,9 @@ 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.set_author(name=dev.name, icon_url=dev.avatar_url) + 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 klokkeslett = 0 @@ -35,12 +35,12 @@ 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) -def setup(bot): - bot.add_cog(Vær(bot)) +async def setup(bot: commands.Bot): + await bot.add_cog(Vær(bot)) diff --git a/src/cogs/deprecated/wiki_search.py b/src/cogs/deprecated/wiki_search.py index 3d2cdab..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,11 +15,11 @@ 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) await ctx.send(embed=embed) -def setup(bot): - bot.add_cog(Wiki(bot)) \ No newline at end of file +async def setup(bot: commands.Bot): + await bot.add_cog(Wiki(bot)) \ No newline at end of file diff --git a/src/cogs/dev_tools.py b/src/cogs/dev_tools.py index 66e95fa..dcabf09 100644 --- a/src/cogs/dev_tools.py +++ b/src/cogs/dev_tools.py @@ -1,251 +1,63 @@ -from discord.ext import commands -import discord - import asyncio -from sys import exit -from os import listdir, system -import socket -from requests import get +from os import listdir +import requests -from cogs.utils import misc_utils, embed_templates +import discord +from discord.ext import commands + +from cogs.utils import embed_templates class DevTools(commands.Cog): - def __init__(self, bot): + """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.bot_has_permissions(embed_links=True) @commands.is_owner() - @commands.command(aliases=['shutdown', 'stoppbått']) - async def stoppbot(self, ctx): - """ - Log out and shut down the bot - """ - - embed = discord.Embed(color=ctx.me.color, description='Logger av...') - await ctx.send(embed=embed) - await self.bot.logout() - exit('Bot stopped through command') - @commands.bot_has_permissions(embed_links=True) - @commands.is_owner() - @commands.command() - async def custommsg(self, ctx, channel: int, *text): + @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) await channel.send(custommessage) + # Send confirmation message to the invoker embed = discord.Embed(color=ctx.me.color) embed.add_field(name='Sent', value=custommessage) await ctx.send(embed=embed) + @commands.is_owner() @commands.bot_has_permissions(embed_links=True) - @commands.is_owner() - @commands.command(aliases=['listservers']) - async def listguilds(self, ctx, page=0): - """ - Lists all guilds the bot is a member of - """ - - guild_list = [f'{guild.name} - {guild.id}' for guild in self.bot.guilds] - - page_data = misc_utils.paginator(guild_list, int(page)) - page = page_data['page'] - pagecount = page_data['pagecount'] - guilds = '\n'.join(page_data['page_content']) - - embed = discord.Embed(color=ctx.me.color) - embed.add_field(name='Guilds', value=guilds) - embed.set_footer(text=f'page: {page}/{pagecount}') - await ctx.send(embed=embed) - - @commands.bot_has_permissions(embed_links=True) - @commands.is_owner() - @commands.command() - async def listusers(self, ctx, page=0): - """ - Lists all users the bot has access to - """ - - user_list = [f'{user.name}#{user.discriminator} - {user.id}' for user in self.bot.users] - page_data = misc_utils.paginator(user_list, int(page)) - - pagecount = page_data['pagecount'] - page = page_data['page'] - users = '\n'.join(page_data['page_content']) - - embed = discord.Embed(color=ctx.me.color) - embed.add_field(name='Users', value=users) - embed.set_footer(text=f'page: {page}/{pagecount}') - await ctx.send(embed=embed) - - @commands.bot_has_permissions(embed_links=True) - @commands.is_owner() - @commands.command(alises=['unloadcog', 'disablecog']) - async def unload(self, ctx, cog: str): - """ - Disables a specified cog - """ - - for file in listdir('./src/cogs'): - if file.endswith('.py'): - name = file[:-3] - if name == cog: - self.bot.unload_extension(f'cogs.{name}') - embed = discord.Embed(color=ctx.me.color, description=f'{cog} has been disabled') - return await ctx.send(embed=embed) - - embed = embed_templates.error_fatal(ctx, text=f'{cog} does not exist') - await ctx.send(embed=embed) - - @commands.bot_has_permissions(embed_links=True) - @commands.is_owner() - @commands.command(aliases=['loadcog', 'enablecog']) - async def load(self, ctx, cog): - """ - Enables a speicifed cog - """ - - for file in listdir('./src/cogs'): - if file.endswith('.py'): - name = file[:-3] - if name == cog: - self.bot.load_extension(f'cogs.{name}') - embed = discord.Embed(color=ctx.me.color, description=f'{cog} loaded') - return await ctx.send(embed=embed) - - embed = embed_templates.error_fatal(ctx, text=f'{cog} does not exist') - await ctx.send(embed=embed) - - @commands.bot_has_permissions(embed_links=True) - @commands.is_owner() - @commands.command(aliases=['reloadcog']) - async def reload(self, ctx, cog): - """ - Reloads a specified cog - """ - - for file in listdir('./src/cogs'): - if file.endswith('.py'): - name = file[:-3] - if name == cog: - self.bot.reload_extension(f'cogs.{name}') - embed = discord.Embed(color=ctx.me.color, description=f'{cog} has been reloaded') - return await ctx.send(embed=embed) - - embed = embed_templates.error_fatal(ctx, text=f'{cog} does not exist') - await ctx.send(embed=embed) - - @commands.bot_has_permissions(embed_links=True) - @commands.is_owner() - @commands.command() - async def reloadcommand(self, ctx, cog): - """ - Reloads a specified cog - """ - - for file in listdir('./src/cogs'): - if file.endswith('.py'): - name = file[:-3] - if name == cog: - self.bot.reload_extension(f'cogs.{name}') - embed = discord.Embed(color=ctx.me.color, description=f'{cog} has been reloaded') - return await ctx.send(embed=embed) - - embed = embed_templates.error_fatal(ctx, text=f'{cog} does not exist') - await ctx.send(embed=embed) - - @commands.bot_has_permissions(embed_links=True) - @commands.is_owner() - @commands.command(aliases=['reloadunloadedcogs']) - async def reloadunloaded(self, ctx): - """ - Reloads all cogs, including previously disabled ones - """ - - for file in listdir('./src/cogs'): - if file.endswith('.py'): - name = file[:-3] - self.bot.unload_extension(f'cogs.{name}') - - for file in listdir('./src/cogs'): - if file.endswith('.py'): - name = file[:-3] - self.bot.load_extension(f'cogs.{name}') - - embed = discord.Embed(color=ctx.me.color, description='Reloaded all cogs') - await ctx.send(embed=embed) - - @commands.bot_has_permissions(embed_links=True) - @commands.is_owner() - @commands.command(aliases=['reloadallcogs']) - async def reloadall(self, ctx): - """ - Reloads all previously enabled cogs - """ - - for file in listdir('./src/cogs'): - if file.endswith('.py'): - name = file[:-3] - self.bot.reload_extension(f'cogs.{name}') - - embed = discord.Embed(color=ctx.me.color, description='Reloaded all previously enabled cogs') - await ctx.send(embed=embed) - - @commands.is_owner() - @commands.command() - async def cmd(self, ctx, *, command: str): - """ - Execute terminal commands - """ - - system(command) - await ctx.send('Done!') - - @commands.bot_has_permissions(embed_links=True) - @commands.is_owner() - @commands.command() - async def localip(self, ctx): - """ - Sends LAN IP-address - """ - - s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - s.connect(('8.8.8.8', 80)) - embed = discord.Embed(color=ctx.me.color) - embed.add_field(name='LAN IP-address', value=s.getsockname()[0]) - await ctx.send(embed=embed) - s.close() - - @commands.bot_has_permissions(embed_links=True) - @commands.is_owner() - @commands.command() - async def publicip(self, ctx): - """ - Sends WAN IP-address - inb4 I leak my IP-address - """ - - data = get('https://wtfismyip.com/json').json() - ip = data['YourFuckingIPAddress'] - location = data['YourFuckingLocation'] - isp = data['YourFuckingISP'] - - embed = discord.Embed(color=ctx.me.color) - embed.add_field(name='WAN IP-address', value=f'{ip}\n{location}\n{isp}') - await ctx.send(embed=embed) - - @commands.bot_has_permissions(embed_links=True) - @commands.is_owner() @commands.cooldown(1, 10, commands.BucketType.guild) - @commands.command() - async def changepresence(self, ctx, activity_type, message, status_type): + @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 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 = { @@ -253,10 +65,7 @@ class DevTools(commands.Cog): 'listening': 2, 'watching': 3 } - if activity_type in activities: - activity_type = activities[activity_type] - else: - activity_type = 0 + activity_type = activities.get(activity_type, 0) status_types = { 'online': discord.Status.online, @@ -264,45 +73,49 @@ class DevTools(commands.Cog): 'idle': discord.Status.idle, 'offline': discord.Status.offline } - if status_type in status_types: - status_type = status_types[status_type] - else: - status_type = discord.Status.online + status_type = status_types.get(status_type, discord.Status.online) await self.bot.change_presence( 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.bot_has_permissions(embed_links=True, add_reactions=True) @commands.is_owner() - @commands.command() - async def leave(self, ctx, *guild_id: int): + @commands.bot_has_permissions(embed_links=True, add_reactions=True) + @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 guild_id == (): - guild_id = ctx.guild.id - else: - guild_id = guild_id + # If no guild id is specified, leave the current guild + guild_id = guild_id if guild_id else ctx.guild.id + # Get guild try: guild = await self.bot.fetch_guild(guild_id) except discord.errors.Forbidden: embed = embed_templates.error_fatal(ctx, text='Bot is not a member of this guild') return await ctx.send(embed=embed) + # Send confirmation message for leaving confirmation_msg = await ctx.send(f'Do you want to leave {guild.name} (`{guild.id}`)?') await confirmation_msg.add_reaction('✅') - def comfirm(reaction, user): + # Check confirmation + def comfirm(reaction: discord.Reaction, user: discord.Member): return user == ctx.author and str(reaction.emoji) == '✅' try: - reaction, user = await self.bot.wait_for('reaction_add', timeout=15.0, check=comfirm) + await self.bot.wait_for('reaction_add', timeout=15.0, check=comfirm) except asyncio.TimeoutError: await ctx.message.delete() await confirmation_msg.delete() @@ -314,46 +127,158 @@ class DevTools(commands.Cog): except discord.errors.Forbidden: pass + @commands.is_owner() @commands.bot_has_permissions(embed_links=True) - @commands.is_owner() - @commands.command() - async def resetcooldown(self, ctx, command: str): + @commands.command(name='publicip', description='Hent den offentlige IP-adressen til serveren botten kjører på') + async def publicip(self, ctx: commands.Context): """ - Resets the cooldown period for a specified command + Sends WAN IP-address. Inb4 I leak my IP-address + + Parameters + ---------- + ctx (commands.Context): Context object """ - try: - self.bot.get_command(command).reset_cooldown(ctx) - except AttributeError: - embed = embed_templates.error_fatal(ctx, text=f'{command} is not a command') - return await ctx.send(embed=embed) + data = requests.get('https://wtfismyip.com/json', timeout=10).json() + ip = data['YourFuckingIPAddress'] + location = data['YourFuckingLocation'] + isp = data['YourFuckingISP'] - embed = discord.Embed(color=ctx.me.color, description='Cooldown reset!') + embed = discord.Embed(color=ctx.me.color) + embed.add_field(name='WAN IP-address', value=f'{ip}\n{location}\n{isp}') await ctx.send(embed=embed) - @commands.bot_has_permissions(embed_links=True, external_emojis=True) @commands.is_owner() - @commands.command() - async def allemoji(self, ctx): + @commands.bot_has_permissions(embed_links=True) + @commands.group(name='cogs', description='Administrer cogs') + async def cogs(self, ctx: commands.Context): """ - Sends all emoji the bot has access to + Cog management commands + + Parameters + ---------- + ctx (commands.Context): Context object """ - embed = discord.Embed(colour=ctx.me.color) + if not ctx.invoked_subcommand: + await ctx.send_help(ctx.command) - emoji_string = '' - for guild in self.bot.guilds: - emoji_string += f'\n**{guild.name}**\n' - for emoji in guild.emojis: - if len(emoji_string) > 2000: - embed.description = emoji_string - await ctx.send(embed=embed) - emoji_string = f'\n**{guild.name}**\n' - emoji_string += f'{emoji} ' + @cogs.command(name='unload', description='Avslutt en cog') + async def cogs_unload(self, ctx: commands.Context, cog: str): + """ + Disables a specified cog - embed.description = emoji_string + 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] + if name == cog: + await self.bot.unload_extension(f'cogs.{name}') + embed = discord.Embed(color=ctx.me.color, description=f'{cog} has been disabled') + return await ctx.send(embed=embed) + + embed = embed_templates.error_fatal(ctx, text=f'{cog} does not exist') + await ctx.send(embed=embed) + + @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] + if name == cog: + await self.bot.load_extension(f'cogs.{name}') + embed = discord.Embed(color=ctx.me.color, description=f'{cog} loaded') + return await ctx.send(embed=embed) + + embed = embed_templates.error_fatal(ctx, text=f'{cog} does not exist') + await ctx.send(embed=embed) + + @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] + if name == cog: + await self.bot.reload_extension(f'cogs.{name}') + embed = discord.Embed(color=ctx.me.color, description=f'{cog} has been reloaded') + return await ctx.send(embed=embed) + + embed = embed_templates.error_fatal(ctx, text=f'{cog} does not exist') + await ctx.send(embed=embed) + + @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'): + name = file[:-3] + await self.bot.unload_extension(f'cogs.{name}') + + # Load all cogs + for file in listdir('./src/cogs'): + if file.endswith('.py'): + name = file[:-3] + await self.bot.load_extension(f'cogs.{name}') + + embed = discord.Embed(color=ctx.me.color, description='Reloaded all cogs') + await ctx.send(embed=embed) + + @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] + await self.bot.reload_extension(f'cogs.{name}') + + embed = discord.Embed(color=ctx.me.color, description='Reloaded all previously enabled cogs') await ctx.send(embed=embed) -def setup(bot): - bot.add_cog(DevTools(bot)) +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 1db2701..60d6ae4 100644 --- a/src/cogs/errors.py +++ b/src/cogs/errors.py @@ -1,38 +1,71 @@ -from discord.ext import commands -import discord - import traceback -import sys -from datetime import datetime + +import discord +from discord import app_commands +from discord.ext import commands from cogs.utils import embed_templates from cogs.utils.misc_utils import ignore_exception class Errors(commands.Cog): - def __init__(self, bot): + """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 # Set error handler for slash commands @commands.Cog.listener() - async def on_command(self, ctx): + async def on_command(self, ctx: commands.Context): """ - Prints command execution metadata + Logs prefix command execution metadata + + Parameters + ---------- + ctx (commands.Context): Context object """ - print( - f'{datetime.now().strftime("%Y-%m-%d %H:%M:%S")} | ' + - f'{"❌ " if ctx.command_failed else "✔ "} {ctx.command} - ' + + self.bot.logger.info( + f'{"❌ " if ctx.command_failed else "✔ "} {ctx.command} | ' + f'{ctx.author.name}#{ctx.author.discriminator} ({ctx.author.id}) | ' + f'{ctx.guild.id}-{ctx.channel.id}-{ctx.message.id}' ) @commands.Cog.listener() - async def on_command_error(self, ctx, error): + async def on_app_command_completion(self, interaction: discord.Interaction, command: app_commands.Command | app_commands.ContextMenu): """ - Returns something based on the type of command error + Logs slash command execution metadata + + Parameters + ---------- + interaction (discord.Interaction): Slash command context object + command (app_commands.Command | app_commands.ContextMenu): Command object """ - # Reset cooldown if command throws an error + self.bot.logger.info( + f'{"❌ " if interaction.command_failed else "✔ "} {command.name} | ' + + f'{interaction.user.name}#{interaction.user.discriminator} ({interaction.user.id}) | ' + + f'{interaction.guild_id}-{interaction.channel_id}-{interaction.id}' + ) + + @commands.Cog.listener() + async def on_command_error(self, ctx: commands.Context, error: commands.CommandError): + """ + 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) @@ -57,31 +90,31 @@ class Errors(commands.Cog): elif isinstance(error, commands.BotMissingPermissions): permissions = ', '.join(error.missing_perms) - embed = embed_templates.error_warning(ctx, text='Jeg mangler følgende tillatelser:\n\n' + - f'```\n{permissions}\n```') - return await ctx.send(embed=embed) + text = 'Jeg mangler følgende tillatelser:\n\n' + \ + f'```\n{permissions}\n```' + return await ctx.send(text) elif isinstance(error, commands.MissingPermissions): permissions = ', '.join(error.missing_perms) - embed = embed_templates.error_warning(ctx, text='Du mangler følgende tillatelser\n\n' + - f'```\n{permissions}\n```') - return await ctx.send(embed=embed) + text = 'Du mangler følgende tillatelser\n\n' + \ + f'```\n{permissions}\n```' + return await ctx.send(text) elif isinstance(error, commands.NotOwner): - embed = embed_templates.error_fatal(ctx, text='Bare boteieren kan gjøre dette') - return await ctx.send(embed=embed) + text = 'Bare boteieren kan gjøre dette' + return await ctx.send(text) elif isinstance(error, commands.CommandOnCooldown): - embed = embed_templates.error_warning(ctx, text='Kommandoen har nettopp blitt brukt' + - f'Prøv igjen om `{error.retry_after:.1f}` sekunder.') - return await ctx.send(embed=embed) + text = 'Kommandoen har nettopp blitt brukt' + \ + f'Prøv igjen om `{error.retry_after:.1f}` sekunder.' + return await ctx.send(text) elif isinstance(error, commands.NoPrivateMessage): try: - embed = embed_templates.error_fatal(ctx, text='Denne kommandoen kan bare utføres i servere') - return await ctx.send(embed=embed) + 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 @@ -92,15 +125,57 @@ class Errors(commands.Cog): embed = embed_templates.error_fatal(ctx, text='En ukjent feil oppstod!') await ctx.send(embed=embed) - # Print full exception to console - print(f'Ignoring exception in command {ctx.command}:', file=sys.stderr) - traceback.print_exception( - type(error), - error, - error.__traceback__, - file=sys.stderr - ) + # Log full exception to file + 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 + + 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) + + if isinstance(error, commands.BotMissingPermissions): + permissions = ', '.join(error.missing_perms) + embed = embed_templates.error_warning(interaction, text='Jeg mangler følgende tillatelser:\n\n' + + f'```\n{permissions}\n```') + return await interaction.response.send_message(embed=embed) + + elif isinstance(error, commands.MissingPermissions): + permissions = ', '.join(error.missing_perms) + embed = embed_templates.error_warning(interaction, text='Du mangler følgende tillatelser\n\n' + + f'```\n{permissions}\n```') + return await interaction.response.send_message(embed=embed) + + elif isinstance(error, commands.NotOwner): + embed = embed_templates.error_fatal(interaction, text='Bare boteieren kan gjøre dette') + return await interaction.response.send_message(embed=embed) + + elif isinstance(error, commands.CommandOnCooldown): + embed = embed_templates.error_warning(interaction, text='Kommandoen har nettopp blitt brukt' + + f'Prøv igjen om `{error.retry_after:.1f}` sekunder.') + return await interaction.response.send_message(embed=embed) + + embed = embed_templates.error_fatal(interaction, text='En ukjent feil oppstod!') + await interaction.response.send_message(embed=embed) + + # Log full exception to file + self.bot.logger.error(''.join(traceback.format_exception(type(error), error, error.__traceback__))) -def setup(bot): - bot.add_cog(Errors(bot)) +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 10f7809..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): - self.bot = bot + """Simple commands that return short strings for the memes""" @commands.cooldown(1, 2, commands.BucketType.guild) - @commands.command() - async def norskeuniversiteter(self, ctx): + @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() - async def informatikkuio(self, ctx): + @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() - async def informatikkuio2(self, ctx): + @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 informatikkuio3(self, ctx): + 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): + 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): + 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): + 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') -def setup(bot): - bot.add_cog(Fun(bot)) +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 b00d62e..e233087 100644 --- a/src/cogs/funreplies.py +++ b/src/cogs/funreplies.py @@ -1,31 +1,89 @@ -from discord.ext import commands - +import random import re +from datetime import datetime + +import discord +from discord.ext import commands class FunReplies(commands.Cog): - def __init__(self, bot): + """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 - async def react(self, message): + # Cooldowns for trigger words + # + # This is kind of a shitty way to do it but I'm too lazy to implement anything good right now + self.cooldown_seconds = 120 + initial_datetime = datetime(2000, 9, 11) # Set initial datetime far in the past to allow triggering right after boot + self.previous_invokations = { + 'borgerlønn': initial_datetime, + 'olof palme': initial_datetime, + 'lesgo': initial_datetime, + 'neuf': initial_datetime, + 'yeet': initial_datetime + } + + async def reply_to_triggers(self, message: discord.Message): + """ + 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 - if re.findall(r'(^|\W)borgerlønn(\W|$)', message.clean_content.lower()) != []: - await message.channel.send('<@267415183931080715> DE SNAKKER OM BORGERLØNN') + message_content = message.content.lower() - elif re.findall(r'(^|\W)olof palme(\W|$)', message.clean_content.lower()) != []: - await message.channel.send('Jeg vet hvem som drepte Olof Palme 👀') + if re.search(r'(^|\W)borgerlønn(\W|$)', message_content, flags=re.IGNORECASE): + if (datetime.now() - self.previous_invokations['borgerlønn']).seconds > self.cooldown_seconds: + await message.channel.send('<@267415183931080715> DE SNAKKER OM BORGERLØNN') + self.previous_invokations['borgerlønn'] = datetime.now() - # The Ultimate spaghetti - elif ( - re.match(r'(\W|\s)*lesgo{1}(\s|\W)*', message.clean_content.lower()) or - message.clean_content.lower() == 'lesgo' - ): - await message.channel.send('https://cdn.discordapp.com/attachments/750052141346979850/' + - '824764933513281596/3xhpwbakz2361.png') + elif re.search(r'(^|\W)olof palme(\W|$)', message_content, flags=re.IGNORECASE): + if (datetime.now() - self.previous_invokations['olof palme']).seconds > self.cooldown_seconds: + await message.channel.send('Jeg vet hvem som drepte Olof Palme 👀') + self.previous_invokations['olof palme'] = datetime.now() + + elif re.search(r'(\W|\s)*le+s+go+(\s|\W)*', message_content): + if (datetime.now() - self.previous_invokations['lesgo']).seconds > self.cooldown_seconds: + await message.channel.send('https://cdn.discordapp.com/attachments/750052141346979850/' + + '824764933513281596/3xhpwbakz2361.png') + self.previous_invokations['lesgo'] = datetime.now() + + elif re.search(r'^((er det|hvor)\s+)*(noen|folk|mange)\s+på\s+(rommet|neuf|kontoret)\?*$', message_content, flags=re.IGNORECASE): + if (datetime.now() - self.previous_invokations['neuf']).seconds > self.cooldown_seconds: + # Choose a random user to ping + pingable = ["<@554977854971183125>", "<@276061121776058370>"] + to_be_pinged = random.choice(pingable) + await message.channel.send(f'Ja, {to_be_pinged} bor der') + + self.previous_invokations['neuf'] = datetime.now() + + elif re.search(r'(^|\W)+ye+et($|\W)+', message_content, flags=re.IGNORECASE): + if (datetime.now() - self.previous_invokations['yeet']).seconds > self.cooldown_seconds: + await message.channel.send('<:Nei:826593267642662912>') + self.previous_invokations['yeet'] = datetime.now() -def setup(bot): - bot.add_listener(FunReplies(bot).react, 'on_message') - bot.add_cog(FunReplies(bot)) +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 f53753b..f35ae28 100644 --- a/src/cogs/info.py +++ b/src/cogs/info.py @@ -1,39 +1,143 @@ -from discord.ext import commands -import discord - from os import remove -from cogs.utils import embed_templates, misc_utils +import discord +from discord import app_commands +from discord.ext import commands + +from cogs.utils import discord_utils, embed_templates, misc_utils class Info(commands.Cog): - def __init__(self, bot): + """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 - @commands.bot_has_permissions(embed_links=True, external_emojis=True) - @commands.guild_only() - @commands.cooldown(1, 2, commands.BucketType.guild) - @commands.command(aliases=['guildinfo', 'server', 'serverinfo', 'si', 'gi']) - async def guild(self, ctx): + 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: """ - Viser info om guilden + Puts all roles except @everyone into a list and joins them to a string + + Parameters + ---------- + roles (list[discord.Role]): List of roles + + Returns + ------- + (str): String of roles """ - guild_created_date = ctx.guild.created_at.strftime('%d. %b. %Y - %H:%M') - since_created_days = (ctx.message.created_at - ctx.guild.created_at).days + if not roles: + return '**Ingen roller**' - if since_created_days == 1: - since_created_days_string = 'dag' - else: - since_created_days_string = 'dager' + roles = [role.name for role in roles if role.name != '@everyone'] + roles.reverse() - total_members = ctx.guild.member_count + return ', '.join(roles) + + def __construct_booster_string(self, interaction: discord.Interaction, join_method: callable = ', '.join) -> str: + """ + 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**' + + # Sort boosters by boost date and put name#discrim as well as date of boost into a list of stirngs + # Use map instead of list comprehension because I can divide the code into 2 lines + boosters = list(map(lambda booster: f'{booster.name}#{booster.discriminator} - {discord.utils.format_dt(booster.premium_since)}', + sorted(interaction.guild.premium_subscribers, key=lambda m: m.premium_since))) + boosters = [f'{booster.name}#{booster.discriminator}' for booster in boosters] + boosters.reverse() + + return join_method(boosters) + + def __construct_member_string(self, members: list[discord.Member]) -> str: + """ + 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**' + + members = [f'{member.name}#{member.discriminator}' for member in members] + + 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 + + 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) + + # Send file + txt_file = discord.File(file_path) + await interaction.response.send_message(file=txt_file) + + # Delete file + with misc_utils.ignore_exception(OSError): + remove(file_path) + + @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') + 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') + 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' + + # Member count and status counts + total_members = interaction.guild.member_count bot_members = 0 online_members = 0 idle_members = 0 dnd_members = 0 offline_members = 0 - for member in ctx.guild.members: + for member in interaction.guild.members: if member.bot: bot_members += 1 if str(member.status) == 'online': @@ -45,111 +149,57 @@ class Info(commands.Cog): elif str(member.status) == 'offline': offline_members += 1 - roles = [] - for role in ctx.guild.roles: - if role.name != '@everyone': - roles.append(role.name) - roles.reverse() - roles = ', '.join(roles) - if len(roles) > 1024: - roles = f'Skriv `{self.bot.prefix}guildroller` for å se rollene' - if roles == '': - roles = '**Ingen roller**' + # Roles + roles = self.__construct_role_string(interaction.guild.roles) + roles = roles if len(roles) < 1024 else f'Bruk /{self.bot.tree.get_command("guild").get_command("roller").qualified_name} for å se rollene' - boosters = [] - premium_subscribers = sorted( - ctx.guild.premium_subscribers, key=lambda m: m.premium_since) - for booster in premium_subscribers: - boosters.append(f'{booster.name}#{booster.discriminator}') - boosters = ', '.join(boosters) - if len(boosters) > 1024: - boosters = f'Skriv `{self.bot.prefix}boosters` for å se boosters' - if boosters == '': - boosters = '**Ingen boosters**' + # Boosts + boosters = self.__construct_booster_string(interaction) + boosters = boosters if len(boosters) < 1024 else f'Bruk /{self.bot.tree.get_command("guild").get_command("boosters").qualified_name} for å se boostere' - text_channels = len(ctx.guild.text_channels) - voice_channels = len(ctx.guild.voice_channels) - categories = len(ctx.guild.categories) + # Channels counts + text_channels = len(interaction.guild.text_channels) + voice_channels = len(interaction.guild.voice_channels) + categories = len(interaction.guild.categories) total_channels = text_channels + voice_channels - flags = { - 'us': ':flag_us:', - 'eu': ':flag_eu:', - 'singapore': ':flag_sg:', - 'london': ':flag_gb:', - 'sydney': ':flag_au:', - 'amsterdam': ':flag_nl:', - 'frankfurt': ':flag_de:', - 'brazil': ':flag_br:', - 'dubai': ':flag_ae:', - 'japan': ':flag_jp:', - 'russia': ':flag_ru:', - 'southafrica': ':flag_za:', - 'hongkong': ':flag_hk:', - 'india': ':flag_in:' - } - region = str(ctx.guild.region) - if region.startswith('us'): - region = 'us' - elif region.startswith('eu'): - region = 'eu' - elif region.startswith('amsterdam'): - region = 'amsterdam' - try: - flag = flags[region] - except KeyError: - flag = ':question:' - - region_names = { - 'eu-central': 'Sentral-Europa', - 'eu-west': 'Vest-Europa', - 'europe': 'Europa', - 'hongkong': 'Hong Kong', - 'russia': 'Russland', - 'southafrica': 'Sør-Afrika', - 'us-central': 'Midt-USA', - 'us-east': 'New Jersey', - 'us-south': 'Sør-USA', - 'us-west': 'California', - 'vip-amsterdam': 'Amsterdam (VIP)', - 'vip-us-east': 'Øst-USA (VIP)', - 'vip-us-west': 'Vest-USA (VIP)', - } - try: - region_name = region_names[str(ctx.guild.region)] - except KeyError: - region_name = str(ctx.guild.region).title() - + # Features features_string = '' - if ctx.guild.features is not []: + if interaction.guild.features is not []: features = { - 'VIP_REGIONS': 'VIP', - 'VANITY_URL': 'Egen URL', + 'ANIMATED_BANNER': 'Animer serverbanner', + 'ANIMATED_ICON': 'Animert serverikon', + 'BANNER': 'Serverbanner', + 'COMMERCE': 'E-handel', + 'COMMUNITY': 'Samfunnsserver', + 'DISCOVERABLE': 'På utforsksiden', + 'FEATURABLE': '', 'INVITE_SPLASH': 'Invitasjonsbilde', - 'VERIFIED': 'Verifisert', - 'PARTNERED': 'Discord Partner', - 'MORE_EMOJI': 'Ekstra emoji', - 'DISCOVERABLE': 'Fremhevet', - 'FEATURABLE': 'Kan fremheves', - 'COMMUNITY': 'Sammfunsguild', - 'COMMERCE': 'Butikkanaler', - 'PUBLIC': 'Offentlig guild', + 'MEMBER_VERIFICATION_GATE_ENABLED': 'Medlemsverifisering', + 'MONETIZATION_ENABLED': 'Betalingskanal', + 'MORE_EMOJI': 'Flere emoji', + 'MORE_STICKERS': 'Flere stickers', 'NEWS': 'Nyhetskanaler', - 'BANNER': 'Banner', - 'ANIMATED_ICON': 'Animert ikon', - 'PUBLIC_DISABLED': 'Ikke offentlig', - 'WELCOME_SCREEN_ENABLED': 'Velkomstvindu', + 'PARTNERED': 'Partner', 'PREVIEW_ENABLED': 'Forhåndsvisning', - 'MEMBER_VERIFICATION_GATE_ENABLED': 'Medlemsverifisering' + 'PRIVATE_THREADS': 'Private tråder', + 'ROLE_ICONS': 'Rolleikon', + 'TICKETED_EVENTS_ENABLED': 'Arrangmenter med billettsalg', + 'VANITY_URL': 'Egendefinert URL', + 'VERIFIED': 'Verifisert', + 'VIP_REGIONS': 'Høyere bitrate i stemmekanaler', + 'WELCOME_SCREEN_ENABLED': 'Velkomstskjerm', + 'INVITES_DISABLED': 'Invitasjoner deaktivert' } - for feature in ctx.guild.features: - features_string += f'{features[feature]}\n' + for feature in interaction.guild.features: + if translation := features.get(feature): + features_string += f'{translation}\n' photos = {} - if ctx.guild.splash_url: - photos['Invitasjonsbilde'] = ctx.guild.splash_url_as(format='png') - if ctx.guild.banner_url: - photos['Banner'] = ctx.guild.banner_url_as(format='png') + if interaction.guild.discovery_splash: + photos['Invitasjonsbilde'] = interaction.guild.discovery_splash + if interaction.guild.banner: + photos['Banner'] = interaction.guild.banner verification_level = { 'none': 'ingen', @@ -158,26 +208,25 @@ class Info(commands.Cog): 'high': 'e-post, registrert i 5 min, medlem i 10 min', 'extreme': 'telefon' } - verification = verification_level[str(ctx.guild.verification_level)] + verification = verification_level[str(interaction.guild.verification_level)] content_filter = { 'disabled': 'nei', 'no_role': 'for alle uten rolle', 'all_members': 'ja' } - content = content_filter[str(ctx.guild.explicit_content_filter)] + content = content_filter[str(interaction.guild.explicit_content_filter)] - embed = discord.Embed(color=ctx.me.color, description=f'**Verifiseringskrav:** {verification}\n' + - f'**Innholdsfilter:** {content}\n' + - f'**Boost Tier:** {ctx.guild.premium_tier}\n' + - f'**Emoji:** {len(ctx.guild.emojis)}') - embed.set_author(name=ctx.guild.name, icon_url=ctx.guild.icon_url) - embed.set_thumbnail(url=ctx.guild.icon_url_as(static_format='png')) - embed.add_field(name='ID', value=ctx.guild.id) - embed.add_field(name='Eier', value=ctx.guild.owner.mention) - embed.add_field(name='Region', value=f'{flag} {region_name}') - embed.add_field(name='Opprettet', value=f'{guild_created_date}\n{since_created_days} ' + - f'{since_created_days_string} siden') + embed = discord.Embed(color=interaction.guild.me.color, description=f'**Verifiseringskrav:** {verification}\n' + + f'**Innholdsfilter:** {content}\n' + + f'**Boost Tier:** {interaction.guild.premium_tier}\n' + + f'**Emoji:** {len(interaction.guild.emojis)}') + embed.set_author(name=interaction.guild.name, icon_url=interaction.guild.icon) + embed.set_thumbnail(url=interaction.guild.icon) + embed.add_field(name='ID', value=interaction.guild.id) + embed.add_field(name='Eier', value=interaction.guild.owner.mention) + embed.add_field(name='Opprettet', value=f'{creation_date_timestamp}\n{since_created_days} ' + + f'{days_ago} siden') embed.add_field(name=f'Kanaler ({total_channels})', value=f'💬 Tekst: **{text_channels}**\n' + f'🔊 Tale: **{voice_channels}**\n' + f'🗃️ Kategorier: **{categories}**') @@ -188,158 +237,356 @@ class Info(commands.Cog): f'{self.bot.emoji["idle"]}{idle_members} ' + f'{self.bot.emoji["dnd"]}{dnd_members} ' + f'{self.bot.emoji["offline"]}{offline_members}') - embed.add_field(name=f'Roller ({len(ctx.guild.roles) - 1})', value=roles, inline=False) - if ctx.guild.premium_tier != 0: - embed.add_field(name=f'Boosters ({ctx.guild.premium_subscription_count})', value=boosters, inline=False) + embed.add_field(name=f'Roller ({len(interaction.guild.roles) - 1})', value=roles, inline=False) + if interaction.guild.premium_tier != 0: + embed.add_field(name=f'Boosters ({interaction.guild.premium_subscription_count})', value=boosters, inline=False) - if features_string != '': + if not features_string: embed.add_field(name='Tillegsfunksjoner', value=features_string) - if photos != {}: + if photos: photos_string = '' for key, value in photos.items(): photos_string += f'[{key}]({value})\n' embed.add_field(name='Bilder', value=photos_string) - await ctx.send(embed=embed) + await interaction.response.send_message(embed=embed) - @commands.bot_has_permissions(embed_links=True) - @commands.guild_only() - @commands.cooldown(1, 2, commands.BucketType.guild) - @commands.command(aliases=['serverroller', 'guildroles', 'serverroles']) - async def guildroller(self, ctx): + @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') + async def guild_roles(self, interaction: discord.Interaction): """ - Viser rollene i en guild + List all roles in the guild + + Parameters + ---------- + interaction (discord.Interaction): Slash command context object """ - roles = [] - for role in ctx.guild.roles: - if role.name != '@everyone': - roles.append(role.name) - if roles is []: - roles = ['**Ingen Roller**'] - roles.reverse() - roles = ', '.join(roles) - - roles = roles.replace(', --', '\n--') - roles = roles.replace('--, ', '--\n') + roles = self.__construct_role_string(interaction.guild.roles) + # IF roles list is longer than 2048, create text file and send it if len(roles) > 2048: - with open(f'./assets/temp/{ctx.guild.id}_roles.txt', 'w') as f: - f.seek(0) - f.write(roles) + self.__send_as_txt_file(interaction, f'./assets/temp/{interaction.guild.id}_roles.txt', roles) + else: + embed = discord.Embed(color=interaction.guild.me.color, description=roles) + embed.set_author(name=f'Roller ({len(interaction.guild.roles)})', icon_url=interaction.guild.icon) + embed.set_footer(text=interaction.guild.name, icon_url=interaction.guild.icon) + await interaction.response.send_message(embed=embed) - txt_file = discord.File(f'./assets/temp/{ctx.guild.id}_roles.txt') - await ctx.send(file=txt_file) + @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') + async def guild_boosters(self, interaction: discord.Interaction): + """ + List all boosters in the guild - with misc_utils.ignore_exception(OSError): - remove(f'./assets/temp/{ctx.guild.id}_roles.txt') + Parameters + ---------- + interaction (discord.Interaction): Slash command context object + """ - return + 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) - if roles == '': - roles = '**Ingen roller**' + boosters = self.__construct_booster_string(interaction, '\n'.join) - embed = discord.Embed(color=ctx.me.color, description=roles) - embed.set_author(name=f'Roller ({len(ctx.guild.roles) - 1})', icon_url=ctx.guild.icon_url) - embed.set_footer(text=ctx.guild.name, icon_url=ctx.guild.icon_url) - await ctx.send(embed=embed) + embed = discord.Embed(color=interaction.guild.me.color, description=boosters) + embed.set_author(name=f'Boosters ({interaction.guild.premium_subscription_count})', icon_url=interaction.guild.icon) + embed.set_footer(text=interaction.guild.name, icon_url=interaction.guild.icon) + 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, 2) + @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) + 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, 2) + @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) + + embed = discord.Embed(color=interaction.guild.me.color, description=f'[Lenke]({interaction.guild.discovery_splash})') + embed.set_author(name=interaction.guild.name, icon_url=interaction.guild.icon) + embed.set_image(url=interaction.guild.discovery_splash) + 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, 2) + @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) + + embed = discord.Embed(color=interaction.me.guild.color, description=f'[Lenke]({interaction.guild.banner})') + embed.set_author(name=interaction.guild.name, icon_url=interaction.guild.icon) + embed.set_image(url=interaction.guild.banner) + 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, 2) + @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') + since_created_days = (interaction.created_at - rolle.created_at).days + + # List of members with the role + members = self.__construct_member_string(rolle.members) + members = members if len(members) < 1024 else 'For mange medlemmer for å vise her' + + # List of permissions + permissions = ', '.join([permission for permission, value in iter(rolle.permissions) if value is True]) + + embed = discord.Embed(title=rolle.name, description=f'{rolle.mention}\n**ID:** {rolle.id}', color=discord_utils.get_color(rolle)) + embed.set_author(name=rolle.guild.name, icon_url=rolle.guild.icon) + embed.add_field(name='Fargekode', value=str(rolle.color)) + embed.add_field(name='Opprettet', value=f'{created_at_timestamp}\n{since_created_days} ' + + f'{"dager" if since_created_days > 1 else "dag"} siden') + embed.add_field(name='Posisjon', value=rolle.position) + embed.add_field(name='Nevnbar', value='Ja' if rolle.mentionable else 'Nei') + embed.add_field(name='Vises separat i medlemsliste', value='Ja' if rolle.hoist else 'Nei') + if permissions: + embed.add_field(name='Tillatelser', value=permissions, inline=False) + embed.add_field(name=f'Brukere med rollen ({len(rolle.members)})', value=members, 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, 2) + @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' + + embed = discord.Embed(color=interaction.guild.me.color, title=kanal.name, description=f'{kanal.mention}\nID: {kanal.id}') + embed.set_author(name=kanal.guild.name, icon_url=kanal.guild.icon) + embed.add_field(name='Beskrivelse', value=kanal.topic if kanal.topic else '**Ingen**', inline=False) + embed.add_field(name='Opprettet', value=discord.utils.format_dt(kanal.created_at, style='F')) + embed.add_field(name='NSFW', value='Ja' if kanal.is_nsfw() else 'Nei') + embed.add_field(name='Saktemodus', value=f'Ja ({kanal.slowmode_delay} sekunder)' if kanal.slowmode_delay else 'Nei') + if kanal.category: + embed.add_field(name='Kategori', value=kanal.category.name) + embed.add_field(name=f'Antall med tilgang ({len(kanal.members)})', value=members) + 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, 2) + @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')) + embed.add_field(name='Bitrate', value=f'{int(kanal.bitrate / 1000)}kbps') + embed.add_field(name='Maksgrense', value=f'{kanal.user_limit} personer' if kanal.user_limit != 0 else '∞ personer') + if kanal.category: + embed.add_field(name='Kategori', value=kanal.category.name) + embed.add_field(name='Antall koblet til', value=len(kanal.members)) + embed_templates.default_footer(interaction, embed) + await interaction.response.send_message(embed=embed) + + # NOTE: This command is implemented using the old command framework + # This is due to lack of emoji support in the new framework + # TODO: take a look at this command when/if the new framework supports emojis + @commands.guild_only() @commands.bot_has_permissions(embed_links=True) - @commands.guild_only() - @commands.cooldown(1, 2, commands.BucketType.guild) - @commands.command(aliases=['guildboosters', 'serverboosters', 'boosts']) - async def boosters(self, ctx): + @commands.cooldown(1, 2) + @commands.command(name='emoji', description='Hent informasjon om en emoji i serveren') + async def emoji(self, ctx: commands.Context, emoji: discord.Emoji): """ - Viser boosters i en guild + Get information about an emoji in the server + + Parameters + ---------- + ctx (commands.Context): Command context object + emoji (discord.Emoji): Emoji to fetch information about """ - if len(ctx.guild.premium_subscribers) == 0: - embed = embed_templates.error_warning(ctx, text='Serveren har ikke noen boosts :(') - return await ctx.send(embed=embed) + emoji = await emoji.guild.fetch_emoji(emoji.id) + try: + emoji_creator = f'{emoji.user.mention}\n{emoji.user.name}#{emoji.user.discriminator}' + except AttributeError: + emoji_creator = 'Jeg trenger `manage_emojis`-tillatelsen på serveren den er fra for å hente dette' - boosters = [] - premium_subscribers = sorted(ctx.guild.premium_subscribers, key=lambda m: m.premium_since) - for booster in premium_subscribers: - date = booster.premium_since.strftime('%d.%m.%Y %H:%M') - boosters.append(f'{booster.name}#{booster.discriminator} - {date}') - boosters = '\n'.join(boosters) - - embed = discord.Embed(color=ctx.me.color, description=boosters) - embed.set_author(name=f'Boosters ({ctx.guild.premium_subscription_count})', icon_url=ctx.guild.icon_url) - embed.set_footer(text=ctx.guild.name, icon_url=ctx.guild.icon_url) + embed = discord.Embed(color=ctx.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='Animert', value='Ja' if emoji.animated else 'Nei') + embed.add_field(name='Lagt til av', value=emoji_creator) + embed.set_image(url=emoji.url) await ctx.send(embed=embed) - @commands.bot_has_permissions(embed_links=True) + 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 + + 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 + @commands.guild_only() - @commands.cooldown(1, 2, commands.BucketType.guild) - @commands.command(aliases=['serverikon', 'servericon', 'guildicon', 'icon', 'guildikon']) - async def ikon(self, ctx): + @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') + async def guild_user_created_oldest(self, interaction: discord.Interaction): """ - Viser ikonet til serveren du er i + List the oldest users on the server based on when they were created + + Parameters + ---------- + interaction (discord.Interaction): Slash command context object """ - url = ctx.guild.icon_url_as(static_format='png') + # Sort members by creation date + members = sorted(interaction.guild.members, key=lambda m: m.created_at) - embed = discord.Embed(color=ctx.me.color, description=f'[Lenke]({url})') - embed.set_author(name=ctx.guild.name, icon_url=url) - embed.set_image(url=url) - embed = embed_templates.default_footer(ctx, embed) - await ctx.send(embed=embed) + # Create list of members with index, name#discriminator and creation date + members_formatted = list(map( + lambda m: f'**#{(m[0] + 1)}** {m[1].name}#{m[1].discriminator} - {discord.utils.format_dt(m[1].created_at, style="F")}', + enumerate(members) + )) + + paginator = misc_utils.Paginator(members_formatted) + view = discord_utils.Scroller(paginator, self.__construct_oldest_embed) + + # Send first page + embed = discord.Embed(color=interaction.guild.me.color, title='Eldste brukere på serveren basert på når de ble lagd') + embed = self.__construct_oldest_embed(paginator, paginator.get_current_page(), embed) + await interaction.response.send_message(embed=embed, view=view) - @commands.bot_has_permissions(embed_links=True) @commands.guild_only() - @commands.cooldown(1, 2, commands.BucketType.guild) - @commands.command(aliases=['guildsplash', 'serversplash']) - async def splash(self, ctx): + @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') + async def guild_user_joined_oldest(self, interaction: discord.Interaction): """ - Viser invite splash til serveren + List the oldest users on the server based on when they joined + + Parameters + ---------- + interaction (discord.Interaction): Slash command context object """ - if not ctx.guild.splash_url: - embed = embed_templates.error_warning(ctx, text='Serveren har ikke en splash') - return await ctx.send(embed=embed) + # Sort members by creation date + members = sorted(interaction.guild.members, key=lambda m: m.joined_at) - url = ctx.guild.splash_url_as(format='png') + # Create list of members with index, name#discriminator and creation date + members_formatted = list(map( + lambda m: f'**#{(m[0] + 1)}** {m[1].name}#{m[1].discriminator} - {discord.utils.format_dt(m[1].joined_at, style="F")}', + enumerate(members) + )) - embed = discord.Embed(color=ctx.me.color, description=f'[Lenke]({url})') - embed.set_author(name=ctx.guild.name, icon_url=ctx.guild.icon_url) - embed.set_image(url=url) - embed = embed_templates.default_footer(ctx, embed) - await ctx.send(embed=embed) + paginator = misc_utils.Paginator(members_formatted) + view = discord_utils.Scroller(paginator, self.__construct_oldest_embed) - @commands.bot_has_permissions(embed_links=True) - @commands.guild_only() - @commands.cooldown(1, 2, commands.BucketType.guild) - @commands.command(aliases=['guildbanner', 'serverbanner']) - async def banner(self, ctx): - """ - Viser invite splash til serveren + # Send first page + embed = discord.Embed(color=interaction.guild.me.color, title='Eldste brukere på serveren basert på når de ble med') + embed = self.__construct_oldest_embed(paginator, paginator.get_current_page(), embed) + await interaction.response.send_message(embed=embed, view=view) + + @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') + async def user_info(self, interaction: discord.Interaction, *, bruker: discord.Member | None = None): """ + Get information about a user - if not ctx.guild.banner_url: - embed = embed_templates.error_warning(ctx, text='Serveren har ikke et banner :(') - return await ctx.send(embed=embed) - - url = ctx.guild.banner_url_as(format='png') - - embed = discord.Embed(color=ctx.me.color, description=f'[Lenke]({url})') - embed.set_author(name=ctx.guild.name, icon_url=ctx.guild.icon_url) - embed.set_image(url=url) - embed = embed_templates.default_footer(ctx, embed) - await ctx.send(embed=embed) - - @commands.bot_has_permissions(embed_links=True, external_emojis=True) - @commands.guild_only() - @commands.cooldown(1, 2, commands.BucketType.guild) - @commands.command(aliases=['userinfo', 'ui', 'brukerinfo', 'user']) - async def bruker(self, ctx, *, bruker: discord.Member = None): - """ - Viser info om en bruker + 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 = ctx.author + bruker = interaction.user + # Get device(s) that is currently used by the user app = '' if str(bruker.mobile_status) != 'offline': app += '📱 ' @@ -348,48 +595,27 @@ class Info(commands.Cog): if str(bruker.desktop_status) != 'offline': app += '💻' - join_index = sorted(ctx.guild.members, key=lambda m: m.joined_at).index(bruker) + 1 - creation_index = sorted(ctx.guild.members, key=lambda m: m.created_at).index(bruker) + 1 + # Get index of join, creation and/or boost date in comparison to other users in the guild + join_index = sorted(interaction.guild.members, key=lambda m: m.joined_at).index(bruker) + 1 + creation_index = sorted(interaction.guild.members, key=lambda m: m.created_at).index(bruker) + 1 if bruker.premium_since: - premium_index = sorted(ctx.guild.premium_subscribers, key=lambda m: m.premium_since).index(bruker) + 1 + premium_index = sorted(interaction.guild.premium_subscribers, key=lambda m: m.premium_since).index(bruker) + 1 - bruker_joined_date = bruker.joined_at.strftime('%d. %b. %Y - %H:%M') - bruker_created_date = bruker.created_at.strftime('%d. %b. %Y - %H:%M') - since_joined_days = (ctx.message.created_at - bruker.joined_at).days - since_created_days = (ctx.message.created_at - bruker.created_at).days - if since_created_days == 1: - since_created_days_string = 'dag' - else: - since_created_days_string = 'dager' - if since_joined_days == 1: - since_joined_days_string = 'dag' - else: - since_joined_days_string = 'dager' + # Creation/join date & days ago + created_timestamp = discord.utils.format_dt(bruker.created_at, style='F') + joined_timestamp = discord.utils.format_dt(bruker.joined_at, style='F') + joined_days = (interaction.created_at - bruker.joined_at).days + created_days = (interaction.created_at - bruker.created_at).days + # Boost date & days ago if bruker.premium_since: - premium_since = bruker.premium_since.strftime('%d. %b. %Y - %H:%M') - premium_since_days = (ctx.message.created_at - bruker.premium_since).days - if since_joined_days == 1: - premium_since_days_string = 'dag' - else: - premium_since_days_string = 'dager' + premium_since = discord.utils.format_dt(bruker.premium_since, style='F') + premium_since_days = (interaction.message.created_at - bruker.premium_since).days - roles = [] - for role in bruker.roles: - if role.name != '@everyone': - roles.append(role.name) - roles.reverse() - roles = ', '.join(roles) + # Get user roles + roles = self.__construct_role_string(bruker.roles) - if len(roles) > 1024: - roles = f'Skriv `{self.bot.prefix}{ctx.command}` for å se rollene' - if roles == '': - roles = '**Ingen roller**' - - if str(bruker.color) != '#000000': - color = bruker.color - else: - color = discord.Colour(0x99AAB5) + roles = roles if len(roles) < 1024 else f'Bruk /{self.bot.tree.get_command("bruker").get_command("roller").qualified_name} for å se rollene' statuses = { 'online': f'{self.bot.emoji["online"]} Pålogget', @@ -399,333 +625,92 @@ class Info(commands.Cog): } status = statuses[str(bruker.status)] - embed = discord.Embed(color=color, description=f'{bruker.mention}\nID: {bruker.id}\n{status}\n{app}') + embed = discord.Embed(color=discord_utils.get_color(bruker), description=f'{bruker.mention}\nID: {bruker.id}\n{status}\n{app}') if bruker.display_name == bruker.name: - embed.set_author(name=f'{bruker.name}#{bruker.discriminator}', icon_url=bruker.avatar_url) + embed.set_author(name=f'{bruker.name}#{bruker.discriminator}', icon_url=bruker.display_avatar) else: embed.set_author(name=f'{bruker.name}#{bruker.discriminator} | {bruker.display_name}', - icon_url=bruker.avatar_url) - embed.set_thumbnail(url=bruker.avatar_url_as(static_format='png')) - embed.add_field(name='Opprettet', value=f'{bruker_created_date}\n{since_created_days} ' + - f'{since_created_days_string} siden') - embed.add_field(name='Ble med i serveren', value=f'{bruker_joined_date}\n{since_joined_days} ' + - f'{since_joined_days_string} siden') + icon_url=bruker.display_avatar) + embed.set_thumbnail(url=bruker.display_avatar) + embed.add_field(name='Opprettet', value=f'{created_timestamp}\n{created_days} ' + + f'{"dag" if created_days == 1 else "dager"} siden') + embed.add_field(name='Ble med i serveren', value=f'{joined_timestamp}\n{joined_days} ' + + f'{"dag" if joined_days == 1 else "dager"} siden') if bruker.premium_since: embed.add_field(name='Boost', value=f'{premium_since}\n{premium_since_days} ' + - f'{premium_since_days_string} siden\n' + + f'{"dag" if premium_since_days == 1 else "dager"} siden\n' + f'Booster #{premium_index} av serveren', inline=False) - embed.add_field(name=f'Roller ({len(bruker.roles) - 1})', value=roles, inline=False) + embed.add_field(name=f'Roller ({len(bruker.roles)})', value=roles, inline=False) embed.set_footer(text=f'#{join_index} Medlem av serveren | #{creation_index} Eldste brukeren på serveren') + # Playing activity if bruker.activities: games = '' for activity in bruker.activities: games += f'{activity.name}\n' if games: embed.add_field(name='Spiller', value=games, inline=False) - await ctx.send(embed=embed) - @commands.bot_has_permissions(embed_links=True) - @commands.guild_only() - @commands.cooldown(1, 2, commands.BucketType.guild) - @commands.command(aliases=['userroles']) - async def brukerroller(self, ctx, bruker: discord.Member = None): + 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, 2) + @user_group.command(name='roller', description='Hent roller til en bruker') + async def user_roles(self, interaction: discord.Interaction, bruker: discord.Member | None = None): """ - Viser rollene til en bruker + 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 = ctx.author + bruker = interaction.user - roles = [] - for role in bruker.roles: - if role.name != '@everyone': - roles.append(role.name) - roles.reverse() - roles = ', '.join(roles) + roles = self.__construct_role_string(bruker.roles) + # If the list of roles is too long, send it as a file if len(roles) > 2048: - txt_file = discord.File(f'./assets/temp/{ctx.guild.id}_{ctx.author.id}_roles.txt') - await ctx.send(file=txt_file) - - with misc_utils.ignore_exception(OSError): - remove(f'./assets/temp/{ctx.guild.id}_{ctx.author.id}_roles.txt') - - return - - if roles == '': - roles = '**Ingen roller**' - - if str(bruker.color) != '#000000': - color = bruker.color + self.__send_as_txt_file(interaction, f'./assets/temp/{interaction.guild.id}_{interaction.user.id}_roles.txt', roles) else: - color = discord.Colour(0x99AAB5) + embed = discord.Embed(color=discord_utils.get_color(bruker), description=roles) + embed.set_author(name=f'Roller ({len(bruker.roles)})', icon_url=bruker.display_avatar) + embed.set_footer(text=f'{bruker.name}#{bruker.discriminator}', icon_url=bruker.display_avatar) + await interaction.response.send_message(embed=embed) - embed = discord.Embed(color=color, description=roles) - embed.set_author(name=f'Roller ({len(bruker.roles) - 1})', icon_url=bruker.avatar_url) - embed.set_footer(text=f'{bruker.name}#{bruker.discriminator}', icon_url=bruker.avatar_url) - await ctx.send(embed=embed) - - @commands.bot_has_permissions(embed_links=True) - @commands.cooldown(1, 2, commands.BucketType.guild) - @commands.command(aliases=['profilbilde', 'brukeravatar', 'useravatar']) - async def avatar(self, ctx, bruker: discord.Member = None): + @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') + async def user_avatar(self, interaction: discord.Interaction, bruker: discord.User | discord.Member | None = None): """ - Viser avataren til en bruker + 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 = ctx.author + bruker = interaction.user - if str(bruker.color) != '#000000': - color = bruker.color - else: - color = discord.Colour(0x99AAB5) - - url = bruker.avatar_url_as(static_format='png') - - embed = discord.Embed(color=color, description=f'[Lenke]({url})') - embed.set_author(name=f'{bruker.name}#{bruker.discriminator}', icon_url=url) - embed.set_image(url=url) - embed = embed_templates.default_footer(ctx, embed) - await ctx.send(embed=embed) - - @commands.bot_has_permissions(embed_links=True) - @commands.guild_only() - @commands.cooldown(1, 2, commands.BucketType.guild) - @commands.command(aliases=['roleinfo', 'rolleinfo']) - async def rolle(self, ctx, *, rolle: discord.Role): - """ - Viser info om en rolle - """ - - if rolle.name == '@everyone': - embed = embed_templates.fatal(ctx, text='Skriv inn en annen rolle enn @everyone') - return await ctx.send(embed=embed) - - if str(rolle.color) != '#000000': - color = rolle.color - else: - color = discord.Colour(0x99AAB5) - - if rolle.mentionable: - mentionable = 'Ja' - else: - mentionable = 'Nei' - - if rolle.hoist: - hoisted = 'Ja' - else: - hoisted = 'Nei' - - rolle_created_date = rolle.created_at.strftime('%d. %b. %Y - %H:%M') - since_created_days = (ctx.message.created_at - rolle.created_at).days - - if since_created_days == 1: - since_created_days_string = 'dag' - else: - since_created_days_string = 'dager' - - members = [] - for member in rolle.members: - members.append(f'{member.name}#{member.discriminator}') - members = ', '.join(members) - - if len(members) > 1024: - members = 'For mange medlemmer for å vise her' - if len(members) == 0: - members = '**Ingen**' - - permissions = ', '.join([permission for permission, value in iter(rolle.permissions) if value is True]) - - embed = discord.Embed(title=rolle.name, description=f'{rolle.mention}\n**ID:** {rolle.id}', color=color) - embed.set_author(name=rolle.guild.name, icon_url=rolle.guild.icon_url) - embed.add_field(name='Fargekode', value=str(rolle.color)) - embed.add_field(name='Opprettet', value=f'{rolle_created_date}\n{since_created_days} ' + - f'{since_created_days_string} siden') - embed.add_field(name='Posisjon', value=rolle.position) - embed.add_field(name='Nevnbar', value=mentionable) - embed.add_field(name='Vises separat i medlemsliste', value=hoisted) - if permissions: - embed.add_field(name='Tillatelser', value=permissions, inline=False) - embed.add_field(name=f'Brukere med rollen ({len(rolle.members)})', value=members, inline=False) - embed = embed_templates.default_footer(ctx, embed) - await ctx.send(embed=embed) - - @commands.bot_has_permissions(embed_links=True) - @commands.guild_only() - @commands.cooldown(1, 5, commands.BucketType.guild) - @commands.command(aliases=['kanal', 'channel', 'channelinfo', 'textchannel']) - async def tekstkanal(self, ctx, *, kanal: discord.TextChannel): - """ - Viser info om en tekstkanal - """ - - nsfw = 'Nei' - if kanal.is_nsfw(): - nsfw = 'Ja' - - if kanal.slowmode_delay == 0: - saktemodus = 'Nei' - else: - saktemodus = f'Ja ({kanal.slowmode_delay} sekunder)' - - description = '**Ingen**' - if kanal.topic: - description = kanal.topic - - members = [] - for member in kanal.members: - members.append(f'{member.name}#{member.discriminator}') - members = ', '.join(members) - if len(members) > 1024: - members = 'For mange for å vise her' - - embed = discord.Embed(color=ctx.me.color, title=kanal.name, description=f'{kanal.mention}\nID: {kanal.id}') - embed.set_author(name=kanal.guild.name, icon_url=kanal.guild.icon_url) - embed.add_field(name='Beskrivelse', value=description, inline=False) - embed.add_field(name='Opprettet', value=kanal.created_at.strftime('%d. %b. %Y - %H:%M')) - embed.add_field(name='NSFW', value=nsfw) - embed.add_field(name='Saktemodus', value=saktemodus) - if kanal.category: - embed.add_field(name='Kategori', value=kanal.category.name) - embed.add_field(name=f'Antall med tilgang ({len(kanal.members)})', value=members) - embed = embed_templates.default_footer(ctx, embed) - await ctx.send(embed=embed) - - @commands.bot_has_permissions(embed_links=True) - @commands.guild_only() - @commands.cooldown(1, 5, commands.BucketType.guild) - @commands.command(aliases=['talekanalinfo', 'voicechannel']) - async def talekanal(self, ctx, *, kanal: discord.VoiceChannel): - """ - Viser info om en talekanal - """ - - if kanal.user_limit == 0: - limit = '∞ personer' - else: - limit = f'{kanal.user_limit} personer' - - embed = discord.Embed(color=ctx.me.color, title=kanal.name, description=f'ID: {kanal.id}') - embed.set_author(name=kanal.guild.name, icon_url=kanal.guild.icon_url) - embed.add_field(name='Opprettet', value=kanal.created_at.strftime('%d. %b. %Y - %H:%M')) - embed.add_field(name='Bitrate', value=f'{int(kanal.bitrate / 1000)}kbps') - embed.add_field(name='Maksgrense', value=limit) - if kanal.category: - embed.add_field(name='Kategori', value=kanal.category.name) - embed.add_field(name='Antall koblet til', value=len(kanal.members)) - embed = embed_templates.default_footer(ctx, embed) - await ctx.send(embed=embed) - - @commands.bot_has_permissions(embed_links=True) - @commands.guild_only() - @commands.cooldown(1, 5, commands.BucketType.guild) - @commands.command(aliases=['emote']) - async def emoji(self, ctx, emoji: discord.Emoji): - """ - Viser info om en CUSTOM emoji som tilhører serveren - """ - - animated = 'Nei' - if emoji.animated: - animated = 'Ja' - - emoji = await emoji.guild.fetch_emoji(emoji.id) - try: - emoji_creator = f'{emoji.user.mention}\n{emoji.user.name}#{emoji.user.discriminator}' - except AttributeError: - emoji_creator = 'Jeg trenger `manage_emojis`-tillatelsen på serveren den er fra for å hente dette' - - embed = discord.Embed(color=ctx.me.color, title=emoji.name, description=f'ID: {emoji.id}') - embed.set_author(name=emoji.guild.name, icon_url=emoji.guild.icon_url) - embed.add_field(name='Opprettet', value=emoji.created_at.strftime('%d. %b. %Y - %H:%M')) - embed.add_field(name='Animert', value=animated) - embed.add_field(name='Lagt til av', value=emoji_creator) - embed.set_image(url=emoji.url) - embed = embed_templates.default_footer(ctx, embed) - await ctx.send(embed=embed) - - @commands.bot_has_permissions(embed_links=True) - @commands.guild_only() - @commands.cooldown(1, 5, commands.BucketType.guild) - @commands.command(aliases=['serveremoji', 'serveremotes']) - async def guildemoji(self, ctx): - """ - Viser alle emoji som serveren har - """ - - embed = discord.Embed(colour=ctx.me.color) - embed.set_author(name=ctx.guild.name, icon_url=ctx.guild.icon_url) - - emoji_string = '' - for emoji in ctx.guild.emojis: - if len(emoji_string) > 2000: - embed.description = emoji_string - await ctx.send(embed=embed) - emoji_string = '' - emoji_string += f'{emoji} ' - - embed.description = emoji_string - await ctx.send(embed=embed) - - @commands.bot_has_permissions(embed_links=True) - @commands.guild_only() - @commands.cooldown(1, 5, commands.BucketType.guild) - @commands.command(aliases=['oldest']) - async def eldst(self, ctx, side=0): - """ - Liste over de eldste brukerene på serveren - """ - - members = sorted(ctx.guild.members, key=lambda m: m.created_at) - - formatted_members = [] - for member in members: - member_created_date = member.created_at.strftime('%d. %b. %Y - %H:%M') - member_index = (members.index(member) + 1) - formatted_members.append( - f'**#{member_index}** {member.name}#{member.discriminator} - {member_created_date}' - ) - - page_data = misc_utils.paginator(formatted_members, int(side)) - side = page_data['page'] - pagecount = page_data['pagecount'] - members = '\n'.join(page_data['page_content']) - - embed = discord.Embed(color=ctx.me.color) - embed.add_field(name='Eldste Discordbrukerene på serveren', value=members) - embed.set_footer(text=f'Side: {side}/{pagecount}') - await ctx.send(embed=embed) - - @commands.bot_has_permissions(embed_links=True) - @commands.guild_only() - @commands.cooldown(1, 5, commands.BucketType.guild) - @commands.command() - async def joinorder(self, ctx, side=0): - """ - Liste over de første medlemmene av serveren - """ - - members = sorted(ctx.guild.members, key=lambda m: m.joined_at) - - formatted_members = [] - for member in members: - member_joined_date = member.joined_at.strftime('%d. %b. %Y - %H:%M') - member_index = (members.index(member) + 1) - formatted_members.append(f'**#{member_index}** {member.name}#{member.discriminator} - {member_joined_date}') - - page_data = misc_utils.paginator(formatted_members, int(side)) - side = page_data['page'] - pagecount = page_data['pagecount'] - members = '\n'.join(page_data['page_content']) - - embed = discord.Embed(color=ctx.me.color) - embed.set_author(name=ctx.guild.name, icon_url=ctx.guild.icon_url) - embed.add_field(name='Første Discordbrukerene på serveren', value=members) - embed.set_footer(text=f'Side: {side}/{pagecount}') - await ctx.send(embed=embed) + embed = discord.Embed(color=discord_utils.get_color(bruker), description=f'[Lenke]({bruker.display_avatar})') + embed.set_author(name=f'{bruker.name}#{bruker.discriminator}', icon_url=bruker.display_avatar) + embed.set_image(url=bruker.display_avatar) + embed_templates.default_footer(interaction, embed) + await interaction.response.send_message(embed=embed) -def setup(bot): - bot.add_cog(Info(bot)) +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 4232b6d..f773ddc 100644 --- a/src/cogs/mc_whitelist.py +++ b/src/cogs/mc_whitelist.py @@ -1,19 +1,29 @@ +import discord +from discord import app_commands from discord.ext import commands - -import requests -import json from mcrcon import MCRcon +import requests from cogs.utils import embed_templates class MCWhitelist(commands.Cog): - def __init__(self, bot): + """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 ( @@ -24,49 +34,75 @@ class MCWhitelist(commands.Cog): ) self.bot.db_connection.commit() - @commands.cooldown(1, 10, commands.BucketType.user) - @commands.command() - async def whitelist(self, ctx, minecraftbrukernavn): + @app_commands.checks.bot_has_permissions(embed_links=True) + @app_commands.checks.cooldown(1, 5) + @app_commands.command(name='whitelist', description='Whitelist minecraftbrukeren din på serveren vår') + async def whitelist(self, interaction: discord.Interaction, minecraftbrukernavn: str): + """ + Whitelist a minecraft user on the Minecraft server - # fetch minecraft uuid from api - data = requests.get(f"https://api.mojang.com/users/profiles/minecraft/{minecraftbrukernavn}") + 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() if data.status_code != 200: - return await ctx.send( - embed=embed_templates.error_fatal(ctx, text=f"Brukeren `{minecraftbrukernavn}` finnes ikk på minecraft") + return await interaction.response.send_message( + embed=embed_templates.error_fatal(interaction, text=f'Brukeren `{minecraftbrukernavn}` finnes ikke på minecraft'), + ephemeral=True ) data = data.json() # check if the discord user or minecraft user is in the db self.cursor.execute( - "SELECT * FROM mc_whitelist WHERE minecraft_id = %s OR discord_id = %s", (data["id"], ctx.author.id) + """ + SELECT * + FROM mc_whitelist + WHERE minecraft_id = %s OR discord_id = %s + """, (data['id'], interaction.user.id) ) if self.cursor.fetchone(): - return await ctx.send( + return await interaction.response.send_message( embed=embed_templates.error_fatal( - ctx, text="Du har allerede whitelisted en bruker eller så er brukeren du oppga whitelisted" + interaction, + text='Du har allerede whitelisted en bruker eller så er brukeren du oppga whitelisted' ) ) - with MCRcon(host="192.168.0.24", password="hallaballayeet", port=25575) as mcr: - mcr.command(f"whitelist add {data['name']}") - mcr.command("whitelist reload") + # 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') + # Add user to db self.cursor.execute( """ INSERT INTO mc_whitelist (discord_id, minecraft_id) VALUES (%s, %s) """, - (ctx.author.id, data["id"]), + (interaction.user.id, data['id']), ) self.bot.db_connection.commit() - await ctx.send( + await interaction.response.send_message( embed=embed_templates.success( - ctx, text=f"`{data['name']}` er nå tilknyttet din discordbruker og whitelisted!" + interaction, text=f'`{data["name"]}` er nå tilknyttet din discordbruker og whitelisted!' ) ) -def setup(bot): - bot.add_cog(MCWhitelist(bot)) +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 6891fbc..7ede72c 100644 --- a/src/cogs/misc.py +++ b/src/cogs/misc.py @@ -1,271 +1,304 @@ -from discord.ext import commands -import discord - -from re import sub from datetime import datetime -import requests -import json -from asyncio import sleep -from PIL import Image, ImageDraw, ImageFont +import re from os import remove import urllib +import discord +from discord import app_commands +from discord.ext import commands +from PIL import Image, ImageDraw, ImageFont +import requests + from cogs.utils import embed_templates from cogs.utils.misc_utils import ignore_exception class Misc(commands.Cog): - def __init__(self, bot): + """Miscellaneous commands that don't fit anywhere else""" + + def __init__(self, bot: commands.Bot): + """ + Parameters + ---------- + bot (commands.Bot): The bot instance + """ + self.bot = bot - @commands.bot_has_permissions(embed_links=True) - @commands.cooldown(1, 2, commands.BucketType.guild) - @commands.command() - async def weeb(self, ctx): + @app_commands.checks.bot_has_permissions(embed_links=True) + @app_commands.checks.cooldown(1, 2) + @app_commands.command(name='weeb', description='Kjeft på weebs') + async def weeb(self, interaction: discord.Interaction): """ - Kjeft på weebs + Call out weebs + + Parameters + ---------- + interaction (discord.Interaction): Slash command context object """ - await ctx.send("<:sven:762725919604473866> Weebs 👉 <#803596668129509417>") + await interaction.response.send_message('<:sven:762725919604473866> Weebs 👉 <#803596668129509417>') - @commands.bot_has_permissions(embed_links=True) - @commands.cooldown(1, 2, commands.BucketType.guild) - @commands.command(aliases=["owoify", "uwu"]) - async def owo(self, ctx, *, tekst: str): + @app_commands.checks.bot_has_permissions(embed_links=True) + @app_commands.checks.cooldown(1, 2) + @app_commands.command(name='kweeb', description='김정운, 최고도') + async def kweeb(self, interaction: discord.Interaction): """ - Oversetter teksten din til owo + Call out kweebs + + Parameters + ---------- + interaction (discord.Interaction): Slash command context object """ - owo_rules = {"r": "w", "l": "w", "R": "W", "L": "W", "n": "ny", "N": "Ny", "ove": "uv"} + await interaction.response.send_message( + """ +Melding du nettopp sendte ga muligens følgende vibber for brukeren som brukte denne kommandoen: + +> KOREA NUMBER ONE! +> OMG I LUUUUV JUNGKOK!!! 🥰😍🥰 STAN JUNGKOOK! 🤞🤞🤞 사랑해!!! ♥♥♥ +> JEG EEEEELSKER 김치 +> OP OP OP OPPAN GANGNAM STYLE! + +Så da kan man spørre seg selv? Hva faen skjedde med god gammeldags weebdom? +Til vår skuffelse finnes den fortsatt, men ble det for mainstream å være weeb. +Så da beveger massen seg til det neste hyperkapitalistiske øst-asiatiske staten med økonomisk boom. +Så hvor ble da av weebsa? Denne videoen svarer ganske godt på det +https://cdn.discordapp.com/attachments/448502155184439297/1027355652059832351/THEY_WENT_TO_KOREA.mp4 + +Du har nå blitt kalt en kweeb. Det er over for deg. +Våkne! +https://cdn.discordapp.com/attachments/674726496878723082/1042631569396990004/kweeb_er_den_nye_weeb.png + """ + ) + + @app_commands.checks.bot_has_permissions(embed_links=True) + @app_commands.checks.cooldown(1, 2) + @app_commands.command(name='owo', description='Oversetter teksten din til owo (we_eb)') + async def owo(self, interaction: discord.Interaction, tekst: str): + """ + Translate text into owo (we_eb) + + 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 = sub(key, value, tekst) + tekst = re.sub(key, value, tekst) # https://kaomoji.moe/ - if not tekst or len(tekst) >= 1000: - embed = embed_templates.error_warning(ctx, text="Teksten er for lang") - return await ctx.send(embed=embed) + if len(tekst) >= 1000: + embed = embed_templates.error_warning(interaction, text='Teksten er for lang') + return await interaction.response.send_message(embed=embed) - embed = discord.Embed(color=ctx.me.color, description=tekst) - embed = embed_templates.default_footer(ctx, embed) - await ctx.send(embed=embed) + embed = discord.Embed(color=interaction.client.user.color, description=tekst) + embed_templates.default_footer(interaction, embed) + await interaction.response.send_message(embed=embed) - @commands.command() - async def emne(self, ctx, emnekode: str): + @app_commands.checks.cooldown(1, 5) + @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 - data = requests.get(f"https://data.uio.no/studies/v1/course/{emnekode}") + 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 ctx.send(embed=embed_templates.error_fatal(ctx, text="Fant ikke emnekode")) + return await interaction.response.send_message(embed=embed_templates.error_fatal(interaction, text='Fant ikke emnekode')) - emne = data.json()["info"]["name"] - await ctx.send(emne) + await interaction.response.send_message(data.json()['info']['name']) - @commands.bot_has_permissions(embed_links=True) - @commands.cooldown(1, 2, commands.BucketType.guild) - @commands.command(aliases=["clapify"]) - async def klappifiser(self, ctx, *, tekst): + @app_commands.checks.bot_has_permissions(embed_links=True) + @app_commands.checks.cooldown(1, 2) + @app_commands.command(name='klappifiser', description='Klappifiser tekst') + async def clapify(self, interation: discord.Interaction, tekst: str): """ - Klapppifiserer teksten din + Add clap emoji between every word of a string + + Parameters + ---------- + interaction (discord.Interaction): Slash command context object + tekst (str): Text to clapify """ - if not tekst or len(tekst) >= 1000: - embed = embed_templates.error_warning(ctx, text="Teksten er for lang") - return await ctx.send(embed=embed) + if len(tekst) >= 1000: + embed = embed_templates.error_warning(interation, text='Teksten er for lang') + return await interation.response.send_message(embed=embed) - tekst = sub(" ", "👏", tekst) + tekst = re.sub(' ', '👏', tekst) - embed = discord.Embed(color=ctx.me.color, description=f"**{tekst.upper()}**") - embed = embed_templates.default_footer(ctx, embed) - await ctx.send(embed=embed) + 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) - @commands.cooldown(1, 5, commands.BucketType.user) - @commands.command(aliases=["filmsøk"]) - async def imdb(self, ctx, *, film): + @app_commands.checks.bot_has_permissions(embed_links=True) + @app_commands.checks.cooldown(1, 5) + @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 """ - url = "http://www.omdbapi.com/?" + urllib.parse.urlencode({"s": film, "apikey": self.bot.api_keys["omdb"]}) - search = requests.get(url).json() + # Search for movie/show + 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(ctx, text="Fant ikke filmen!") - return await ctx.send(embed=embed) + embed = embed_templates.error_fatal(interaction, text='Fant ikke filmen!') + return await interaction.response.send_message(embed=embed) - data = requests.get(f'http://www.omdbapi.com/?i={best_result_id}&apikey={self.bot.api_keys["omdb"]}').json() - - acceptable_media_types = ["movie", "series", "episode"] - if data["Type"] not in acceptable_media_types: - embed = embed_templates.error_fatal(ctx, text="Fant ikke filmen!") - return await ctx.send(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!') + return await interaction.response.send_message(embed=embed) embed = discord.Embed( title=f'{data["Title"]} ({data["Year"]})', color=0xF5C518, url=f'https://www.imdb.com/title/{data["imdbID"]}/', ) - embed.set_author(name=ctx.author.name, icon_url=ctx.author.avatar_url) - 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') - embed.set_footer(text=f'Utgitt: {data["Released"]}') + 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_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 ctx.send(embed=embed) + await interaction.response.send_message(embed=embed) - @commands.bot_has_permissions(embed_links=True) - @commands.cooldown(1, 5, commands.BucketType.guild) - @commands.command(aliases=["identify"]) - async def identifiser(self, ctx, bilde=None): + @app_commands.checks.bot_has_permissions(embed_links=True) + @app_commands.checks.cooldown(1, 5) + @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): """ - Beskriver hva som er på bildet + Get holidays for a country for a given year + + Parameters + ---------- + interaction (discord.Interaction): Slash command context object + land (str): Country code + år (int): Year """ - if ctx.message.attachments != []: - bilde = ctx.message.attachments[0].url + land = land.upper() + aar = datetime.now().year if not år else int(år) - if bilde is None: - embed = embed_templates.error_fatal(ctx, text="Du må gi meg et bilde!") - - async with ctx.channel.typing(): - - payload = {"inputs": [{"data": {"image": {"url": bilde}}}]} - header = {"Authorization": f'Key {self.bot.api_keys["clarifai"]}'} - url = ( - "https://api.clarifai.com/v2/models/aaa03c23b3724a16a56b629203edc62c" - + "/versions/aa7f35c01e0642fda5cf400f543e7c40/outputs" - ) - data = requests.post(url, data=json.dumps(payload), headers=header) - if data.status_code != 200: - embed = embed_templates.default_footer("API request feilet") - return await ctx.send(embed=embed) - - data = data.json() - - words = [] - for i, concepts in enumerate(data["outputs"][0]["data"]["concepts"]): - words.append(concepts["name"]) - if i >= 5: - break - - words = ", ".join(words) - - await sleep(2) - - embed = discord.Embed(color=ctx.me.color) - embed.set_author(name="Clarifai AI", icon_url="https://github.com/Clarifai.png") - embed.description = f"**Ord som beskriver dette bildet:**\n\n{words}" - embed.set_image(url=bilde) - embed = embed_templates.default_footer(ctx, embed) - await ctx.send(embed=embed) - - @commands.bot_has_permissions(embed_links=True) - @commands.cooldown(1, 5, commands.BucketType.guild) - @commands.command(aliases=["holidays", "fridager", "ferie"]) - async def helligdager(self, ctx, land=None, aar=None): - """ - Se hellidagene i et land - """ - - if not land: - land = "NO" - else: - land = land.upper() - - if not aar: - aar = datetime.now().year - else: - int(aar) - - data = requests.get(f"https://date.nager.at/api/v2/publicholidays/{aar}/{land}") + 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( - ctx, text="Ugyldig land\nHusk å bruke landskoder\n" + "For eksempel: `NO`" + interaction, text='Ugyldig land\nHusk å bruke landskoder\n' + 'For eksempel: `NO`' ) - return await ctx.send(embed=embed) + return await interaction.response.send_message(embed=embed) data = data.json() - country = data[0]["countryCode"].lower() - holiday_str = "" + country = data[0]['countryCode'].lower() + + # Construct output + holiday_str = '' for day in data: - date = day["date"] - date = datetime.strptime(date, "%Y-%m-%d").strftime("%d. %B") + 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=ctx.me.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 = embed_templates.default_footer(ctx, embed) - await ctx.send(embed=embed) + embed_templates.default_footer(interaction, embed) + await interaction.response.send_message(embed=embed) - @commands.bot_has_permissions(embed_links=True) - @commands.cooldown(1, 5, commands.BucketType.guild) - @commands.command() - async def match(self, ctx, *, bruker: discord.Member = None): + @app_commands.checks.bot_has_permissions(embed_links=True) + @app_commands.checks.cooldown(1, 5) + @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 not bruker: - embed = embed_templates.error_warning(ctx, text="Du må gi meg en bruker") - return await ctx.send(embed=embed) - if bruker == ctx.author: + if bruker == interaction.user: embed = embed_templates.error_warning( - ctx, 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 ctx.send(embed=embed) + return await interaction.response.send_message(embed=embed) - async with ctx.channel.typing(): + invoker_id = int(str(interaction.user.id)[11:14]) + user_id = int(str(bruker.id)[11:14]) - invoker_id = int(str(ctx.author.id)[11:14]) - user_id = int(str(bruker.id)[11:14]) + match_percent = int((invoker_id + user_id) % 100) - match_percent = int((invoker_id + user_id) % 100) + if bruker.id == self.bot.user.id: + match_percent = 100 - 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 ctx.author.avatar_url_as(format="png").save(fp=f"./src/assets/temp/{ctx.author.id}_raw.png") - await bruker.avatar_url_as(format="png").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 = invoker.resize((389, 389), Image.ANTIALIAS) + 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') - invoker = Image.open(f"./src/assets/temp/{ctx.author.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 = user.resize((389, 389), Image.ANTIALIAS) - heart = Image.open("./src/assets/misc/heart.png") - mask = Image.open("./src/assets/misc/heart.png", "r") + 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_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)) - 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_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)) + image.save(f'./src/assets/temp/{interaction.user.id}_{bruker.id}_edit.png') - image.save(f"./src/assets/temp/{ctx.author.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_templates.default_footer(interaction, embed) + await interaction.response.send_message(embed=embed, file=f) - f = discord.File(f"./src/assets/temp/{ctx.author.id}_{bruker.id}_edit.png") - embed = discord.Embed() - embed.set_image(url=f"attachment://{ctx.author.id}_{bruker.id}_edit.png") - embed = embed_templates.default_footer(ctx, embed) - await ctx.send(embed=embed, file=f) - - with ignore_exception(OSError): - remove(f"./src/assets/temp/{bruker.id}_raw.png") - remove(f"./src/assets/temp/{ctx.author.id}_raw.png") - remove(f"./src/assets/temp/{ctx.author.id}_{bruker.id}_edit.png") + 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') -def setup(bot): - bot.add_cog(Misc(bot)) +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/mod.py b/src/cogs/mod.py deleted file mode 100644 index 40f2976..0000000 --- a/src/cogs/mod.py +++ /dev/null @@ -1,67 +0,0 @@ -from discord.ext import commands -import discord - -from datetime import datetime - -from cogs.utils import embed_templates - - -class Mod(commands.Cog): - def __init__(self, bot) -> None: - self.bot = bot - - @commands.bot_has_permissions(ban_members=True) - @commands.has_permissions(ban_members=True) - @commands.guild_only() - @commands.command() - async def hackban(self, ctx, brukerid, *, begrunnelse=None): - """ - Bannlys en bruker før de i det hele tatt har blitt med i serveren - """ - - try: - brukerid = int(brukerid) - except ValueError: - embed = embed_templates.error_warning(ctx, text='Du må gi meg en bruker-id') - return await ctx.send(embed=embed) - - user = discord.Object(id=brukerid) - - for banentry in await ctx.guild.bans(): - if banentry.user.id == user.id: - embed = embed_templates.error_warning(ctx, text='Denne brukeren er allerede bannlyst fra serveren!') - return await ctx.send(embed=embed) - - if begrunnelse is None: - begrunnelse = 'Ingen grunn angitt' - - date = datetime.now().strftime('%d. %b. %Y - %H:%M') - - try: - await ctx.guild.ban( - user, - reason=f'{date} | {ctx.author.name}#{ctx.author.discriminator} ({ctx.author.id}): {begrunnelse}' - ) - except discord.errors.NotFound: - embed = embed_templates.error_warning(ctx, text='Bruker finnes ikke!') - return await ctx.send(embed=embed) - - user_object = self.bot.get_user(brukerid) - - embed = discord.Embed(color=discord.Color.red(), title='🔨 Bruker utestengt!') - if begrunnelse != 'Ingen grunn angitt': - embed.description = f'`{begrunnelse}`' - if user_object: - user = user_object - embed.set_thumbnail(url=user.avatar_url) - embed.set_author(name=f'{user.name}#{user.discriminator}', icon_url=user.avatar_url) - embed.add_field(name='Bruker', value=f'{user.name}#{user.discriminator}\n{user.mention}', inline=False) - else: - embed.add_field(name='Bruker', value=f'`{user.id}`', inline=False) - embed.add_field(name='Av', value=f'{ctx.author.name}#{ctx.author.discriminator}\n{ctx.author.mention}') - embed = embed_templates.default_footer(ctx, embed) - await ctx.send(embed=embed) - - -def setup(bot): - bot.add_cog(Mod(bot)) diff --git a/src/cogs/repeated_messages.py b/src/cogs/repeated_messages.py index ebb8488..e048c38 100644 --- a/src/cogs/repeated_messages.py +++ b/src/cogs/repeated_messages.py @@ -1,38 +1,51 @@ -from discord.ext import commands - import aiocron +from discord.ext import commands class RepeatedMessages(commands.Cog): - def __init__(self, bot): + """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(): - guild = self.bot.get_guild(747542543750660178) - channel = guild.get_channel(747542544291987597) + """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(): - guild = self.bot.get_guild(747542543750660178) - channel = guild.get_channel(747542544291987597) + """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(): - guild = self.bot.get_guild(747542543750660178) - channel = guild.get_channel(747542544291987597) + """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>\nhttps://youtu.be/5qXkEvHrZ-0" + 'GODT NYTTÅR!\n\nNå, meld dere inn i UiO Gaming igjen <:ThisIsAThreat:874998234072903710>' ) -def setup(bot): - bot.add_cog(RepeatedMessages(bot)) +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 e76ccde..4d02ebf 100644 --- a/src/cogs/utils/discord_utils.py +++ b/src/cogs/utils/discord_utils.py @@ -1,18 +1,73 @@ import discord +from .misc_utils import Paginator -def get_user_color(user: discord.User) -> discord.Color: + +def get_color(discord_object: discord.User | discord.Member | discord.Role) -> discord.Color: """ Returns the user's top role color + Parameters ----------- - user (discord.User): A discord user or member object + discord_object (discord.User|discord.Member|discord.Role): A discord object that has a color attribute + Returns ----------- (discord.Color): The user's displayed color """ - if str(user.color) != '#000000': - return user.color + 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 + + self.add_item(ScrollerButton(self.paginator, self.paginator.first_page, content_constructor, label='<<', disabled=self.paginator.current_page == 1)) + self.add_item(ScrollerButton(self.paginator, self.paginator.previous_page, content_constructor, label='<', disabled=self.paginator.current_page == 1)) + self.add_item(ScrollerButton(self.paginator, self.paginator.next_page, content_constructor, label='>', disabled=self.paginator.current_page == self.paginator.total_page_count)) + self.add_item(ScrollerButton(self.paginator, self.paginator.last_page, content_constructor, label='>>', disabled=self.paginator.current_page == self.paginator.total_page_count)) diff --git a/src/cogs/utils/embed_templates.py b/src/cogs/utils/embed_templates.py index 64e743d..ce41af3 100644 --- a/src/cogs/utils/embed_templates.py +++ b/src/cogs/utils/embed_templates.py @@ -1,70 +1,81 @@ import discord -def default_footer(ctx: discord.ext.commands.Context, embed: discord.Embed) -> discord.Embed: +def default_footer(interaction: discord.Interaction, embed: discord.Embed) -> discord.Embed: """ Sets a footer containing the command invoker's name, discriminator and avatar + Parameters ----------- - ctx (discord.ext.commands.Context): The current Discord context + interaction (discord.Interaction): Slash command context object embed (discord.Embed): An embed object to add the footer to + Returns ----------- - (discord.Embed): The sent in embed + footer + (discord.Embed): The passed in embed + footer """ - return embed.set_footer(icon_url=ctx.author.avatar_url, text=f"{ctx.author.name}#{ctx.author.discriminator}") + return embed.set_footer( + icon_url=interaction.user.avatar, + text=f'{interaction.user.name}#{interaction.user.discriminator}' + ) -def error_warning(ctx: discord.ext.commands.Context, text: str) -> discord.Embed: +def error_warning(interaction: discord.Interaction, text: str) -> discord.Embed: """ Creates an embed with a specified error message based on a warning template + Parameters ----------- - ctx (discord.ext.commands.Context): The current Discord 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}") - default_footer(ctx, embed) + embed = discord.Embed(color=discord.Color.gold(), description=f'⚠️ {text}') + default_footer(interaction, embed) return embed -def error_fatal(ctx: discord.ext.commands.Context, text: str) -> discord.Embed: +def error_fatal(interaction: discord.Interaction, text: str) -> discord.Embed: """ Creates an embed with a specified error message based on a warning template + Parameters ----------- - ctx (discord.ext.commands.Context): The current Discord 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}") - default_footer(ctx, embed) + embed = discord.Embed(color=discord.Color.red(), description=f'❌ {text}') + default_footer(interaction, embed) return embed -def success(ctx: discord.ext.commands.Context, text: str) -> discord.Embed: +def success(interaction: discord.Interaction, text: str) -> discord.Embed: """ Creates an embed with a specified message using a template signifying success. + Parameters ----------- - ctx (discord.ext.commands.Context): The current Discord 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}") - default_footer(ctx, embed) + 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 4d5fa4a..868bb8f 100644 --- a/src/cogs/utils/misc_utils.py +++ b/src/cogs/utils/misc_utils.py @@ -1,56 +1,99 @@ -from typing import Union, Dict -from math import ceil from contextlib import contextmanager +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): + except exceptions: pass -def paginator(content: list, page: int) -> Dict[str, Union[int, str]]: - """ - Divides content into 10 element pages +class Paginator: + def __init__(self, content: list): + self.content = content + self.total_page_count = ceil(len(content) / 10) + self.current_page = 1 - Parameters - ----------- - content: A list of strings - page: The page + def get_page(self, page: int) -> list | None: + """ + Returns the page of the paginator - Returns - ----------- - (dict): A dictionay containing content and metadata - keys: pagecount, page, page_content - """ + Parameters + ---------- + page (int): The page you want to get - pagecount = ceil(len(content) / 10) + Returns + ---------- + (list|None): The page you requested + """ + if page < 1 or page > self.total_page_count: + return None - if not page or page <= 0 or page > pagecount: - page = 1 + start_index = (page - 1) * 10 + end_index = page * 10 - start_index = (page - 1) * 10 - end_index = page * 10 + return self.content[start_index:end_index] - page_content = content[start_index:end_index] + def get_current_page(self) -> list: + """ + Returns the current page of the paginator - page_data = { - 'pagecount': pagecount, - 'page': page, - 'page_content': page_content - } - return page_data + Returns + ---------- + (list): The current page of the paginator + """ + return self.get_page(self.current_page) + + def next_page(self) -> list | None: + """ + Returns the next page of the paginator + + Returns + ---------- + (list): The next page of the paginator + """ + self.current_page += 1 + return self.get_page(self.current_page) + + def previous_page(self) -> list | None: + """ + Returns the previous page of the paginator + + Returns + ---------- + (list): The previous page of the paginator + """ + self.current_page -= 1 + return self.get_page(self.current_page) + + def first_page(self) -> list | None: + """ + Returns the first page of the paginator + + Returns + ---------- + (list): The first page of the paginator + """ + self.current_page = 1 + return self.get_page(self.current_page) + + def last_page(self) -> list | None: + """ + Returns the last page of the paginator + + Returns + ---------- + (list): The last page of the paginator + """ + self.current_page = self.total_page_count + return self.get_page(self.current_page) diff --git a/src/config/config.yaml.example b/src/config/config.yaml.example index 563b588..d2343c9 100644 --- a/src/config/config.yaml.example +++ b/src/config/config.yaml.example @@ -1,3 +1,5 @@ +config_mode: dev + # Bot settings bot: token: @@ -7,6 +9,17 @@ bot: activity: listening type: dnd +# Database +database: + host: + dbname: + username: + password: + +# Minecraft server +minecraft: + rcon_password: + # Api keys api: clarifai: diff --git a/src/logger.py b/src/logger.py new file mode 100644 index 0000000..0d0d493 --- /dev/null +++ b/src/logger.py @@ -0,0 +1,16 @@ +import logging +import logging.handlers + + +class BotLogger: + """Logging config for the bot""" + + def __init__(self): + logger = logging.getLogger('discord') + logger.setLevel(logging.INFO) + handler = logging.FileHandler(filename='discord.log', encoding='utf-8', mode='w') + formatter = logging.Formatter('[{asctime}] [{levelname:<8}] {name}: {message}', '%Y-%m-%d %H:%M:%S', style='{') + handler.setFormatter(formatter) + logger.addHandler(handler) + + self.logger = logger diff --git a/src/run.py b/src/run.py index 9385e32..9017af1 100644 --- a/src/run.py +++ b/src/run.py @@ -1,84 +1,97 @@ -from discord.ext import commands -import discord +import codecs +from os import listdir +from time import time -from codecs import open +import discord +from discord.ext import commands +import psycopg2 import yaml -from os import listdir -import locale -from time import time -import psycopg2 +from logger import BotLogger -locale.setlocale(locale.LC_ALL, "") - -with open("./src/config/config.yaml", "r", encoding="utf8") as f: +# Load config file +with codecs.open('./src/config/config.yaml', 'r', encoding='utf8') as f: config = yaml.load(f, Loader=yaml.SafeLoader) -intents = discord.Intents.all() - class Bot(commands.Bot): + """UiO Gaming Bot""" + def __init__(self): super().__init__( - command_prefix=commands.when_mentioned_or(config["bot"]["prefix"]), case_insensitive=True, intents=intents + command_prefix=commands.when_mentioned_or(config['bot']['prefix']), + case_insensitive=True, + intents=discord.Intents.all() ) - self.prefix = config["bot"]["prefix"] - self.presence = config["bot"].get("presence", {}) + self.logger = BotLogger().logger # Initialize logger - self.api_keys = config.get("api", {}) - - db = config.get("database", {}) + # Connect to 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'], ) - self.emoji = config.get("emoji", {}) - self.misc = config.get("misc", {}) + # 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') + + async def setup_hook(self): + # Load cogs + for file in listdir('./src/cogs'): + if file.endswith('.py'): + name = file[:-3] + await bot.load_extension(f'cogs.{name}') + + # Sync slash commands to Discord + if self.config_mode == 'prod': + await self.tree.sync() + else: + self.tree.copy_global_to(guild=discord.Object(id=412646636771344395)) + await self.tree.sync(guild=discord.Object(id=412646636771344395)) +# Create bot instance bot = Bot() -activities = {"playing": 0, "listening": 2, "watching": 3} -if bot.presence["activity"].lower() in activities: - activity_type = activities[bot.presence["activity"].lower()] -else: - activity_type = 0 - -status_types = { - "online": discord.Status.online, - "dnd": discord.Status.dnd, - "idle": discord.Status.idle, - "offline": discord.Status.offline, -} -if bot.presence["type"].lower() in status_types: - status_type = status_types[bot.presence["type"].lower()] -else: - status_type = discord.Status.online - - @bot.event async def on_ready(): - if not hasattr(bot, "uptime"): + if not hasattr(bot, 'uptime'): bot.uptime = time() - for file in listdir("./src/cogs"): - if file.endswith(".py"): - name = file[:-3] - bot.load_extension(f"cogs.{name}") + # Print bot info + 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, + } + 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) - print(f"Username: {bot.user.name}") - print(f"ID: {bot.user.id}") - print(f"Version: {discord.__version__}") - print("." * 50 + "\n") await bot.change_presence( - activity=discord.Activity(type=activity_type, name=bot.presence["message"]), status=status_type + activity=discord.Activity(type=activity_type, name=bot.presence['message']), + status=status_type ) -bot.run(config["bot"]["token"], bot=True, reconnect=True) +bot.run(config['bot']['token'], reconnect=True, log_handler=None)