Merge pull request #42 from UiO-Gaming/2024-q1-refactor

2024 q1 refactor
This commit is contained in:
Leander Furumo authored and GitHub committed 2024-04-05 02:43:16 +02:00
commit 9531797a53
40 files changed
+862 -1204

No files matched your search

+12
View File
@@ -62,6 +62,18 @@ All annen kode holdes på engelsk. Dette inkluderer kommentarer.
Et tips er å ha et engelsk funksjonsnavn, men spesifisere i decoratoren over med navnparameteren.
### Bruk av embed templates
`error_warning` skal brukes om det er feil brukerinput, mangel på data eller generelt sett en feil som er forventet/ikke av utenforstående årsåker
`error_fatal` brukes når en uventet feil skjer. Ofte på grun av utenforstående årsaker som et API som er nede osv.
`success` kan brukes til å gi brukeren en bekreftelse på at operasjonen som ble forespurt gikk fint. Det vil si at den ikke skal brukes til å vise spesifikk data, men heller brukes når det ikke er noe å vise.
### Bruk av private variabler/metoder
Generelt sett unngår vi bruk av leading underscores for variabelnavn og metoder. Ja, de har sin bruk, men Python var aldri ment til å være objektorientert på samme måte som f.eks Java. Uansett skal en ikke bruke metoder fra andre cogs. Dermed er regelen: greit overalt utenom i cogs.
### Avhengigheter
Hvis en ny avhengighet trengs, sørg for å spesifisere major og minor versjon i requirementsfila.
+1 -1
View File
@@ -10,7 +10,7 @@ RUN apt-get update && apt-get install -y imagemagick ffmpeg libsm6 libxext6 grap
COPY . .
RUN mv /app/src/assets/misc/policy.xml /etc/ImageMagick-6/policy.xml
RUN mv /app/src/assets/policy.xml /etc/ImageMagick-6/policy.xml
VOLUME logs
+1
View File
@@ -14,4 +14,5 @@ pypandoc==1.13.*
pyyaml==6.0.*
regex==2023.12.25
requests==2.31.*
tzdata # This only needed if run on windows. I haven't set a version either to ensure it's up to date.
wordcloud==1.9.*
File renamed without changes.
Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.
File renamed without changes.

Before

Width:  |  Height:  |  Size: 7.9 KiB

After

Width:  |  Height:  |  Size: 7.9 KiB

File renamed without changes.
File renamed without changes.

Before

Width:  |  Height:  |  Size: 129 KiB

After

Width:  |  Height:  |  Size: 129 KiB

File renamed without changes.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 49 KiB

+60 -54
View File
@@ -12,12 +12,21 @@ from cogs.utils import embed_templates
class Anime(commands.Cog):
"""View information about different media on Anilist"""
def __init__(self, bot: commands.Bot):
"""
Parameters
----------
bot (commands.Bot): The bot instance
"""
self.anilist_logo = "https://anilist.co/img/logo_al.png"
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:
def convert_color(self, color: str) -> int:
"""
Converts anilist colors to hext
@@ -39,10 +48,9 @@ class Anime(commands.Cog):
"green": 0x4CCA51,
"gray": 0x677B94,
}
return colors.get(color, 0x677B94)
def __convert_media_format(self, media_format: str) -> str:
def convert_media_format(self, media_format: str) -> str:
"""
Translates media format names into norwegian
@@ -67,7 +75,7 @@ class Anime(commands.Cog):
}
return media_formats.get(media_format, media_format)
def __convert_language_names(self, language_name: str) -> str:
def convert_language_names(self, language_name: str) -> str:
"""
Translates language names into norwegian
@@ -92,10 +100,9 @@ class Anime(commands.Cog):
"HEBREW": "Hebraisk",
"HUNGARIAN": "Ungarsk",
}
return languages.get(language_name, language_name)
def __convert_role_names(self, role_name: str) -> str:
def onvert_role_names(self, role_name: str) -> str:
"""
Translates role names into norwegian
@@ -114,7 +121,7 @@ class Anime(commands.Cog):
except KeyError:
return role_name
def __convert_status(self, status: str) -> str:
def convert_status(self, status: str) -> str:
"""
Translates role names into norwegian
@@ -133,10 +140,9 @@ class Anime(commands.Cog):
"NOT_YET_RELEASED": "Ikke utgitt enda",
"CANCELLED": "Kansellert",
}
return statuses.get(status, status)
def __remove_html(self, text: str) -> str:
def remove_html(self, text: str) -> str:
"""
Removes HTML tag elements
@@ -154,7 +160,7 @@ class Anime(commands.Cog):
return clean_text
async def __request_anilist(
async def request_anilist(
self, interaction: discord.Interaction, query: str, variables: dict, key: str
) -> tuple[dict, str] | tuple[None, None]:
"""
@@ -179,13 +185,13 @@ class Anime(commands.Cog):
data = response["data"][key]
url = data["siteUrl"]
except TypeError:
embed = embed_templates.error_fatal(interaction, text="Kunne ikke finne det du søkte etter!")
embed = embed_templates.error_fatal("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:
def construct_favorite_media_string(self, media: list) -> str:
"""
Constructs a string with the user's favorite media
@@ -210,7 +216,7 @@ class Anime(commands.Cog):
return "\n".join(favorite_media)
def __construct_favorite_entity_string(self, entities: list, studio: bool = False) -> str:
def construct_favorite_entity_string(self, entities: list, studio: bool = False) -> str:
"""
Constructs a string with the user's favorite entities
@@ -238,7 +244,7 @@ class Anime(commands.Cog):
return "\n".join(favortie_entities)
def __construct_release_schedule_string(self, numbers: dict) -> str:
def construct_release_schedule_string(self, numbers: dict) -> str:
"""
Constructs a string with the media's release schedule
@@ -358,7 +364,7 @@ class Anime(commands.Cog):
}
"""
variables = {"name": bruker}
data, url = await self.__request_anilist(interaction, query, variables, "User")
data, url = await self.request_anilist(interaction, query, variables, "User")
if not data:
return
@@ -367,7 +373,7 @@ class Anime(commands.Cog):
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"])
color = self.convert_color(data["options"]["profileColor"])
anime_statuses = {
status["status"]: {"count": status["count"]} for status in data["statistics"]["anime"]["statuses"]
@@ -385,14 +391,14 @@ class Anime(commands.Cog):
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)
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_author(name="Anilist", icon_url=self.anilist_logo)
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)
@@ -460,7 +466,7 @@ class Anime(commands.Cog):
}
"""
variables = {"name": bruker}
data, url = await self.__request_anilist(interaction, query, variables, "User")
data, url = await self.request_anilist(interaction, query, variables, "User")
if not data:
return
@@ -469,7 +475,7 @@ class Anime(commands.Cog):
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"])
color = self.convert_color(data["options"]["profileColor"])
statuses = {
status["status"]: {"count": status["count"], "minutes": status["minutesWatched"]}
@@ -500,10 +506,10 @@ class Anime(commands.Cog):
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"])
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_author(name="Anilist", icon_url=self.anilist_logo)
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)
@@ -570,7 +576,7 @@ class Anime(commands.Cog):
}
"""
variables = {"name": bruker}
data, url = await self.__request_anilist(interaction, query, variables, "User")
data, url = await self.request_anilist(interaction, query, variables, "User")
if not data:
return
@@ -579,7 +585,7 @@ class Anime(commands.Cog):
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"])
color = self.convert_color(data["options"]["profileColor"])
statuses = {
status["status"]: {"count": status["count"], "minutes": status["chaptersRead"]}
@@ -610,10 +616,10 @@ class Anime(commands.Cog):
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"])
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_author(name="Anilist", icon_url=self.anilist_logo)
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)
@@ -697,15 +703,15 @@ class Anime(commands.Cog):
}
"""
variables = {"search": navn, "isMain": True}
data, url = await self.__request_anilist(interaction, query, variables, "Media")
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"
embed = embed_templates.error_warning(
"Animen du søkte på er NSFW. Gjør kommandoen i en NSFW-kanal i stedet"
)
return await interaction.response.send_message(embed=embed)
@@ -713,7 +719,7 @@ class Anime(commands.Cog):
banner_image = data["bannerImage"]
mean_score = data["meanScore"]
color = self.__convert_color(data["coverImage"]["color"])
color = self.convert_color(data["coverImage"]["color"])
title_romaji = data["title"]["romaji"]
title_native = data["title"]["native"]
@@ -721,9 +727,9 @@ class Anime(commands.Cog):
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 ""
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)
studios_string = self.construct_favorite_entity_string(data["studios"]["nodes"], studio=True)
staff = data["staff"]["edges"]
director_string = ""
@@ -751,7 +757,7 @@ class Anime(commands.Cog):
"end_year": data["endDate"].get("year") if data["endDate"].get("year") else "?",
}
date = self.__construct_release_schedule_string(numbers)
date = self.construct_release_schedule_string(numbers)
length_string = ""
if numbers["duration"] == 1:
@@ -765,11 +771,11 @@ class Anime(commands.Cog):
# Status
status = data["status"]
status = self.__convert_status(status)
status = self.convert_status(status)
# Format
media_format = data["format"]
media_format = self.__convert_media_format(media_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)
@@ -855,15 +861,15 @@ class Anime(commands.Cog):
}
"""
variables = {"search": navn}
data, url = await self.__request_anilist(interaction, query, variables, "Media")
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"
embed = embed_templates.error_warning(
"Mangaen du søkte på er NSFW. Gjør kommandoen i en NSFW-kanal i stedet"
)
return await interaction.response.send_message(embed=embed)
@@ -871,7 +877,7 @@ class Anime(commands.Cog):
banner_image = data["bannerImage"]
mean_score = data["meanScore"]
color = self.__convert_color(data["coverImage"]["color"])
color = self.convert_color(data["coverImage"]["color"])
title_romaji = data["title"]["romaji"]
title_native = data["title"]["native"]
@@ -879,7 +885,7 @@ class Anime(commands.Cog):
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 ""
description = self.remove_html(data["description"]) if data.get("description") else ""
genres = ", ".join(data["genres"])
staff = data["staff"]["edges"]
@@ -901,7 +907,7 @@ class Anime(commands.Cog):
"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)
date = self.construct_release_schedule_string(numbers)
chapters_string = ""
if numbers["chapters"] == 1:
@@ -910,10 +916,10 @@ class Anime(commands.Cog):
chapters_string += "kapitler"
status = data["status"]
status = self.__convert_status(status)
status = self.convert_status(status)
media_format = data["format"]
media_format = self.__convert_media_format(media_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)
@@ -982,7 +988,7 @@ class Anime(commands.Cog):
}
"""
variables = {"search": navn}
data, url = await self.__request_anilist(interaction, query, variables, "Character")
data, url = await self.request_anilist(interaction, query, variables, "Character")
if not data:
return
@@ -993,7 +999,7 @@ class Anime(commands.Cog):
favourites = data["favourites"]
if data["description"]:
description = self.__remove_html(data["description"])
description = self.remove_html(data["description"])
if len(description) > 1024:
description = description[0:1020] + "..."
else:
@@ -1005,10 +1011,10 @@ class Anime(commands.Cog):
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"])
media_role = self.onvert_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_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"]}'
@@ -1080,7 +1086,7 @@ class Anime(commands.Cog):
}
"""
variables = {"search": navn}
data, url = await self.__request_anilist(interaction, query, variables, "Staff")
data, url = await self.request_anilist(interaction, query, variables, "Staff")
if not data:
return
@@ -1091,7 +1097,7 @@ class Anime(commands.Cog):
favourites = data["favourites"]
if data["description"]:
description = self.__remove_html(data["description"])
description = self.remove_html(data["description"])
if len(description) > 1024:
description = description[0:1020] + "..."
else:
@@ -1160,7 +1166,7 @@ class Anime(commands.Cog):
}
"""
variables = {"search": navn}
data, url = await self.__request_anilist(interaction, query, variables, "Studio")
data, url = await self.request_anilist(interaction, query, variables, "Studio")
if not data:
return
+4 -4
View File
@@ -21,7 +21,8 @@ class Aurora(commands.Cog):
self.bot = bot
self.notified = datetime(2000, 9, 11) # Used to prevent spamming aurora alerts
# self.aurora_alarm.start()
self.aurora_alarm.start()
self.AURORA_CHANNEL = 747542544291987599
async def get_forecast(self) -> dict | None:
"""
@@ -41,7 +42,6 @@ class Aurora(commands.Cog):
for interval in data["shortIntervals"]:
# Convert to datetime objects, convert timezone to utc and strip timezone info
start = datetime.fromisoformat(interval["start"]).astimezone(pytz.utc).replace(tzinfo=None)
# end = datetime.fromisoformat(interval["end"]).astimezone(pytz.utc).replace(tzinfo=None)
# If sighting is not in within the next 12 hours ignore
if (start - datetime.now()) > timedelta(hours=12):
@@ -58,7 +58,7 @@ class Aurora(commands.Cog):
most_likely_sighting = max(valid_sightings, key=lambda x: x["auroraValue"])
return most_likely_sighting
@tasks.loop(minutes=10)
@tasks.loop(minutes=60)
async def aurora_alarm(self):
"""
Checks if the aurora forecast is above 50% and sends a message to the aurora channel if it is
@@ -76,7 +76,7 @@ class Aurora(commands.Cog):
self.bot.logger.info("aurora_alarm: Fetch forecast")
return
channel = self.bot.get_channel(747542544291987599)
channel = self.bot.get_channel(self.AURORA_CHANNEL)
if not channel:
self.bot.logger.warning("aurora_alarm: Failed to get aurora channel")
return
+37 -43
View File
@@ -30,7 +30,9 @@ class Birthday(commands.Cog):
self.birthday_check.start()
def init_db(self):
"""Create the necessary tables for the birthday cog to work"""
"""
Create the necessary tables for the birthday cog to work
"""
self.cursor.execute(
"""
@@ -45,9 +47,11 @@ class Birthday(commands.Cog):
name="bursdag", description="Se, endre eller fjern bursdager for brukere på serveren"
)
@tasks.loop(hours=24)
@tasks.loop(time=misc_utils.MIDNIGHT)
async def birthday_check(self):
"""Check if it's someone's birthday every day at midgnight and send a greeting if it is."""
"""
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
@@ -64,27 +68,25 @@ class Birthday(commands.Cog):
birthdays = self.cursor.fetchall()
self.bot.logger.info(f"Found the following birthdays: {birthdays}") # TODO: Temporary. Remove
if birthdays:
guild = self.bot.get_guild(747542543750660178)
channel = guild.get_channel(747542544291987597)
for birthday in birthdays:
user = await guild.fetch_member(birthday[0])
if user:
await channel.send(f"Gratulerer med dagen {user.mention}! 🥳")
else:
self.bot.logger.warning(f"Could not find user with ID {birthday[0]}")
@birthday_check.before_loop
async def before_birthday_check(self):
"""Syncs loop with the time of day"""
if not birthdays:
return
self.bot.logger.info("Postponing birthday check until midnight")
await discord_utils.sleep_until_midnight(self.bot)
guild = self.bot.get_guild(self.bot.UIO_GAMING_GUILD_ID)
channel = guild.get_channel(747542544291987597)
for birthday in birthdays:
user = await guild.fetch_member(birthday[0])
if user:
await channel.send(f"Gratulerer med dagen {user.mention}! 🥳")
else:
self.bot.logger.warning(f"Could not find user with ID {birthday[0]}")
def cog_unload(self):
async def cog_unload(self):
self.bot.logger.info("Unloading cog")
self.birthday_check.cancel()
self.cursor.close()
def __fetch_user_birthday(self, user_id: int) -> datetime | None:
def fetch_user_birthday(self, user_id: int) -> datetime | None:
"""
Fetch the birthday of a user in the database
@@ -103,7 +105,7 @@ class Birthday(commands.Cog):
birthday = result[0]
return datetime(birthday.year, birthday.month, birthday.day)
def __fetch_user_next_birthday(self, user_id: int) -> datetime:
def fetch_user_next_birthday(self, user_id: int) -> datetime:
"""
Get date of next birthday for a user
@@ -132,7 +134,7 @@ class Birthday(commands.Cog):
# This will never happen unless a database error occurs. Keeping the type checker happy
return datetime.now()
def __fetch_next_birthdays(self) -> list[tuple[int, datetime, datetime]]:
def fetch_next_birthdays(self) -> list[tuple[int, datetime, datetime]]:
"""
Fetch the 5 first future birthdays of all users in the database
@@ -160,7 +162,7 @@ class Birthday(commands.Cog):
return birthdays
def __set_user_birthday(self, user_id: int, birthday: datetime):
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.
@@ -201,28 +203,24 @@ class Birthday(commands.Cog):
date = datetime(år, måned, dag)
except ValueError:
return await interaction.response.send_message(
embed=embed_templates.error_fatal(interaction, text="ikke en gyldig dato, gjøk!"), ephemeral=True
embed=embed_templates.error_warning("ikke en gyldig dato, gjøk!"), ephemeral=True
)
if date.year < 1970:
return await interaction.response.send_message(
embed=embed_templates.error_fatal(
interaction, text="Ok boomer, men nei. Hvorfor? Unix timestamps. Google it"
),
embed=embed_templates.error_warning("Ok boomer, men nei. Hvorfor? Unix timestamps. Google it"),
ephemeral=True,
)
if date > datetime.now():
return await interaction.response.send_message(
embed=embed_templates.error_fatal(interaction, text="Du kan ikke sette en fremtidig dato!"),
embed=embed_templates.error_warning("Du kan ikke sette en fremtidig dato!"),
ephemeral=True,
)
self.__set_user_birthday(interaction.user.id, date)
self.set_user_birthday(interaction.user.id, date)
embed = embed_templates.success(
interaction, text=f'Bursdag satt til {discord.utils.format_dt(date, style="D")}'
)
embed = embed_templates.success(f"Bursdag satt til {discord.utils.format_dt(date, style='D')}")
await interaction.response.send_message(embed=embed)
@app_commands.checks.bot_has_permissions(embed_links=True)
@@ -239,7 +237,7 @@ class Birthday(commands.Cog):
self.cursor.execute("DELETE FROM birthdays WHERE discord_id = (%s)", (interaction.user.id,))
embed = embed_templates.success(interaction, text="Bursdag fjernet")
embed = embed_templates.success("Bursdag fjernet")
await interaction.response.send_message(embed=embed)
@app_commands.checks.bot_has_permissions(embed_links=True)
@@ -260,11 +258,11 @@ class Birthday(commands.Cog):
if not bruker:
bruker = interaction.user
user = self.__fetch_user_birthday(bruker.id)
user = self.fetch_user_birthday(bruker.id)
if not user:
return await interaction.response.send_message(
embed=embed_templates.error_fatal(interaction, text="Brukeren har ikke registrert bursdagen sin")
embed=embed_templates.error_warning("Brukeren har ikke registrert bursdagen sin")
)
# Current age information
@@ -273,10 +271,10 @@ class Birthday(commands.Cog):
years_old = relativedelta(datetime.now(), birthday).years
# Next birthday information
next_birthday = self.__fetch_user_next_birthday(bruker.id)
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_color(bruker))
embed = discord.Embed(description=bruker.mention, color=bruker.color)
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"))
@@ -303,12 +301,10 @@ class Birthday(commands.Cog):
await interaction.response.defer()
next_birthdays = self.__fetch_next_birthdays()
next_birthdays = self.fetch_next_birthdays()
if not next_birthdays:
return await interaction.followup.send(
embed=embed_templates.error_fatal(interaction, text="Ingen bursdager")
)
return await interaction.followup.send(embed=embed_templates.error_warning("Ingen bursdager"))
birthday_strings = []
for user in next_birthdays:
@@ -325,9 +321,7 @@ class Birthday(commands.Cog):
birthday_strings.append(f"* {discord_user.name} - {timestamp} ({relative_timestamp})")
if not birthday_strings:
return await interaction.followup.send(
embed=embed_templates.error_fatal(interaction, text="Ingen bursdager")
)
return await interaction.followup.send(embed=embed_templates.error_warning("Ingen bursdager"))
paginator = misc_utils.Paginator(birthday_strings)
view = discord_utils.Scroller(paginator, interaction.user)
+6 -6
View File
@@ -71,8 +71,8 @@ class BotInfo(commands.Cog):
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.display_name}")
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="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())
@@ -106,7 +106,7 @@ class BotInfo(commands.Cog):
"""
embed = discord.Embed(color=interaction.client.user.color)
embed.add_field(name="🔌 Oppetid", value=self.__get_uptime())
embed.add_field(name="🔌 Oppetid", value=self.get_uptime())
await interaction.response.send_message(embed=embed)
@app_commands.checks.bot_has_permissions(embed_links=True)
@@ -122,10 +122,10 @@ class BotInfo(commands.Cog):
"""
embed = discord.Embed(color=interaction.client.user.color)
embed.add_field(name="📶 Ping", value=f"* **Websocket ping:** {self.__get_ping()} ms\n* **I hodet:** ∞")
embed.add_field(name="📶 Ping", value=f"* **Websocket ping:** {self.get_ping()} ms\n* **I hodet:** ∞")
await interaction.response.send_message(embed=embed, content=None)
def __get_ping(self) -> int:
def get_ping(self) -> int:
"""
Get the bot's ping in milliseconds
@@ -136,7 +136,7 @@ class BotInfo(commands.Cog):
return int(self.bot.latency * 1000)
def __get_uptime(self) -> str:
def get_uptime(self) -> str:
"""
Returns the current uptime of the bot in string format
+9 -91
View File
@@ -1,7 +1,6 @@
import asyncio
import json
import random
import textwrap
from datetime import datetime
from datetime import timedelta
from typing import override
@@ -11,12 +10,9 @@ import discord
import numpy as np
from discord import app_commands
from discord.ext import commands
from PIL import Image
from PIL import ImageDraw
from PIL import ImageFont
from cogs.utils import discord_utils
from cogs.utils import embed_templates
from cogs.utils import misc_utils
from cogs.utils.discord_utils import Lobby
from cogs.utils.discord_utils import LobbyView
@@ -53,19 +49,13 @@ class CS2Bingo(commands.Cog):
self.active_lobbies.pop(str(interaction.user.id))
else:
return await interaction.response.send_message(
embed=embed_templates.error_warning(
interaction,
text="Du hoster allerede en lobby",
)
embed=embed_templates.error_warning("Du hoster allerede en lobby")
)
for lobby in self.active_lobbies.values():
if interaction.user.id in lobby.players:
return await interaction.response.send_message(
embed=embed_templates.error_warning(
interaction,
text="Du er allerede i en lobby",
)
embed=embed_templates.error_warning("Du er allerede i en lobby")
)
self.active_lobbies[str(interaction.user.id)] = Lobby(
@@ -80,7 +70,7 @@ class CS2Bingo(commands.Cog):
embed = discord.Embed(
title="Bingolobby",
description=f"Lobbyen stenger {time_left}. Bli med innen da!",
color=discord_utils.get_color(interaction.user),
color=interaction.user.color,
)
embed.set_author(name=interaction.user.global_name, icon_url=interaction.user.avatar)
embed.add_field(name="Spillere", value=f"* {interaction.user.mention}", inline=False)
@@ -103,77 +93,6 @@ class BingoGenerator:
with open(f"{BINGO_PATH}/names.json") as f:
NAMES = json.load(f)
@classmethod
async def put_text_in_box(
cls,
image: np.ndarray,
text: str,
top_left: tuple,
bottom_right: tuple,
font_path: str,
font_size: int = 10,
color: tuple = (0, 0, 0),
):
"""
Puts text inside a bounding box on the cv2 image with automatic text wrapping, using PIL for text rendering.
:param image: Source cv2 image (numpy array).
:param text: Text string to be put.
:param top_left: Tuple (x, y) defining the top-left corner of the bounding box.
:param bottom_right: Tuple (x, y) defining the bottom-right corner of the bounding box.
:param font_path: Path to a .ttf font file.
:param font_size: Initial font size.
:param color: Text color in RGB.
:return: Image with text (numpy array).
"""
# TODO: Dry. This algorithm is also seen in the memes cog
# Convert the cv2 image to a PIL image
image_pil = Image.fromarray(cv2.cvtColor(image, cv2.COLOR_BGR2RGB))
draw = ImageDraw.Draw(image_pil)
# Get coordinates of the bounding box
x1, y1 = top_left
x2, y2 = bottom_right
# Get the width and height of the bounding box
# We multiply by 0.9 to give some padding
w, h = (x2 - x1) * 0.9, (y2 - y1) * 0.9
# Initialize font width/height to 0 in order to run the while loops below at least once
font_width = 0
font_height = 0
# Add newlines to the text. Here we have gone with 15 characters per line
# This is arbitrary, but seems to work well
text = textwrap.fill(text, width=15)
# Increase the font size until the longest line of text does not fit in the bounding box anymore
# We also have to check the height, since the text might be too tall for the box
while font_width < w and font_height < h:
font = ImageFont.truetype(font_path, font_size)
longest_line = max(text.split("\n"), key=lambda x: font.getlength(x))
font_width = font.getlength(longest_line)
# bbox doesn't work with multiline text, so we have to calculate the height manually
# take the height of the bounding box, multiply by 1.5 (our assumed line dividor height)
# after that, we multiply by the number of lines
font_box = font.getbbox(text)
font_height = ((font_box[3] - font_box[1]) * 1.5) * text.count("\n")
font_size += 1
# Actually draw the text
draw.multiline_textbbox((w, h), text=text, font=font, align="center", anchor="mm")
center_of_box = ((x1 + x2) // 2, (y1 + y2) // 2)
draw.multiline_text(center_of_box, text, font=font, fill=color, align="center", anchor="mm")
# Convert back to cv2 image format and update the original image
cv2_image = cv2.cvtColor(np.array(image_pil), cv2.COLOR_RGB2BGR)
np.copyto(image, cv2_image)
@classmethod
async def sample_cells(cls, players: list[str], bot: commands.Bot) -> dict[str, list[str]]:
assert 0 < len(players) <= 10
@@ -238,11 +157,11 @@ class BingoGenerator:
for i in range(0, 5):
for j in range(0, 5):
width = 123
await cls.put_text_in_box(
await misc_utils.put_text_in_box(
sliced,
sample_space.pop(),
(i * width + 1, j * width + 1),
((i + 1) * width, (j + 1) * width),
top_left=(i * width + 1, j * width + 1),
bottom_right=((i + 1) * width, (j + 1) * width),
font_path=cls.FONT_PATH,
)
@@ -257,7 +176,7 @@ class BingoView(LobbyView):
@override
async def start_lobby(self, interaction: discord.Interaction, button: discord.ui.Button):
if interaction.user != self.lobby.host:
embed = embed_templates.error_warning(interaction, text="Bare hosten kan starte bingoen")
embed = embed_templates.error_warning("Bare hosten kan starte bingoen")
return await interaction.response.send_message(embed=embed, ephemeral=True, delete_after=10)
await self.end_lobby()
@@ -268,8 +187,7 @@ class BingoView(LobbyView):
mention_players = " ".join([f"{p.mention}" for p in self.lobby.players])
embed = embed_templates.success(
interaction,
text="Bingoen har startet\n\nDere vil nå få tilsendt bingobrettene deres på DM. "
"Bingoen har startet\n\nDere vil nå få tilsendt bingobrettene deres på DM. "
+ "Sørg for at jeg kan slide inn i dem :smirk:",
)
await interaction.response.send_message(content=mention_players, embed=embed)
-26
View File
@@ -1,26 +0,0 @@
import asyncio
from discord.ext import commands
class CameraChannel(commands.Cog):
def __init__(self, bot: commands.Bot):
self.bot = bot
async def on_voice_state_update(self, member, before, after):
if after.channel is None:
return
if member.voice.channel.id != 817125310491131949:
return
if not member.voice.self_video:
await asyncio.sleep(10)
if not member.voice.self_video and member.voice.channel.id == 817125310491131949:
await member.move_to(channel=None)
async def setup(bot: commands.Bot):
# bot.add_listener(CameraChannel(bot).on_voice_state_update, 'on_voice_state_update')
# await bot.add_cog(CameraChannel(bot))
pass
-45
View File
@@ -1,45 +0,0 @@
import json
import aiocron
import discord
from discord.ext import commands
from yr.libyr import Yr
class Vær(commands.Cog):
def __init__(self, bot: commands.Bot):
self.bot = bot
@aiocron.crontab("0 6 * * *")
async def forecast():
dev = await self.bot.fetch_user(162337748781367296)
guild = self.bot.get_guild(747542543750660178)
channel = guild.get_channel(750052141346979850)
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.display_avatar)
half_hour = False
klokkeslett = 0
for forecast in weather.forecast(str):
if half_hour:
half_hour = False
continue
data = json.loads(forecast)
temp = data["temperature"]
rain = data["precipitation"]
regn = rain["@value"]
temperatur = temp["@value"]
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)
async def setup(bot: commands.Bot):
await bot.add_cog(Vær(bot))
-27
View File
@@ -1,27 +0,0 @@
import discord
import wikipedia
from discord.ext import commands
from cogs.utils import embed_templates
class Wiki(commands.Cog):
@commands.command()
async def wiki_search(self, ctx, *search_term):
search_term = " ".join(search_term)
search = wikipedia.search(search_term)
search = search[0]
search = wikipedia.page(search)
summary = wikipedia.summary(search_term, sentences=5)
url = search.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)
async def setup(bot: commands.Bot):
await bot.add_cog(Wiki(bot))
+34 -28
View File
@@ -42,7 +42,7 @@ class DevTools(commands.Cog):
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]):
async def custommsg(self, ctx: commands.Context, channel: int, text: str):
"""
Send a message to any channel on any server the bot is in
@@ -50,17 +50,16 @@ class DevTools(commands.Cog):
----------
ctx (commands.Context): Context object
channel (int): The channel ID to send the message to
text (tuple[str]): The text to send
text (str): The text to send
"""
# Send message to the requested channel
channel = self.bot.get_channel(channel)
custommessage = " ".join(text)
await channel.send(custommessage)
await channel.send(text)
# Send confirmation message to the invoker
embed = discord.Embed(color=ctx.me.color)
embed.add_field(name="Sent", value=custommessage)
embed.add_field(name="Sent", value=text)
await ctx.reply(embed=embed)
@commands.is_owner()
@@ -115,7 +114,7 @@ class DevTools(commands.Cog):
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")
embed = embed_templates.error_warning("Bot is not a member of this guild")
return await ctx.reply(embed=embed)
# Send confirmation message for leaving
@@ -187,14 +186,16 @@ class DevTools(commands.Cog):
"""
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.reply(embed=embed)
if not file.endswith(".py"):
continue
embed = embed_templates.error_fatal(ctx, text=f"{cog} does not exist")
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.reply(embed=embed)
embed = embed_templates.error_warning(f"{cog} does not exist")
await ctx.reply(embed=embed)
@cogs.command(name="load", description="Aktiver en cog")
@@ -209,14 +210,16 @@ class DevTools(commands.Cog):
"""
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.reply(embed=embed)
if not file.endswith(".py"):
continue
embed = embed_templates.error_fatal(ctx, text=f"{cog} does not exist")
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.reply(embed=embed)
embed = embed_templates.error_warning(f"{cog} does not exist")
await ctx.reply(embed=embed)
@cogs.command(name="reload", description="Last inn en cog på nytt")
@@ -229,15 +232,18 @@ class DevTools(commands.Cog):
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.reply(embed=embed)
embed = embed_templates.error_fatal(ctx, text=f"{cog} does not exist")
for file in listdir("./src/cogs"):
if not file.endswith(".py"):
continue
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.reply(embed=embed)
embed = embed_templates.error_warning(f"{cog} does not exist")
await ctx.reply(embed=embed)
@cogs.command(name="reloadunloaded", description="Last inn alle cogs på nytt, selv de som ikke er lastet inn")
+69 -54
View File
@@ -76,50 +76,50 @@ class Errors(commands.Cog):
if hasattr(ctx.command, "on_error"):
return
# Ignored errors
ignored = commands.CommandNotFound
error = getattr(error, "original", error)
if isinstance(error, ignored):
# Ignored errors
ignored_errors = (commands.CommandNotFound, commands.DisabledCommand, commands.CheckFailure)
if isinstance(error, ignored_errors):
self.bot.logger.info("Ignored error", error)
return
# Errors that should prompt a help message
send_help = (commands.MissingRequiredArgument, commands.TooManyArguments, commands.BadArgument)
if isinstance(error, send_help):
self.bot.get_command(f"{ctx.command}").reset_cooldown(ctx)
return await ctx.send_help(ctx.command)
elif isinstance(error, commands.BotMissingPermissions):
permissions = ", ".join(error.missing_permissions)
text = "Jeg mangler følgende tillatelser:\n\n" + f"```\n{permissions}\n```"
return await ctx.reply(text)
embed = self.error_missing_perms(error.missing_permissions, "Jeg")
elif isinstance(error, commands.MissingPermissions):
permissions = ", ".join(error.missing_permissions)
text = "Du mangler følgende tillatelser\n\n" + f"```\n{permissions}\n```"
return await ctx.reply(text)
embed = self.error_missing_perms(error.missing_permissions, "Du")
elif isinstance(error, commands.NotOwner):
text = "Bare boteieren kan gjøre dette"
return await ctx.reply(text)
embed = embed_templates.error_warning("Bare boteieren kan gjøre dette")
elif isinstance(error, commands.CommandOnCooldown):
text = "Kommandoen har nettopp blitt brukt\n" + f"Prøv igjen om `{error.retry_after:.1f}` sekunder."
return await ctx.reply(text)
embed = embed_templates.error_warning(
f"Kommandoen har nettopp blitt brukt\nPrøv igjen om `{error.retry_after:.1f}` sekunder.",
)
elif isinstance(error, commands.NoPrivateMessage):
try:
text = "Denne kommandoen kan bare utføres i servere"
return await ctx.reply(text)
except discord.errors.Forbidden: # Thrown if bot is blocked by the user or if the user has closed their DMs
self.bot.logger.info("DM Blocked!")
embed = embed_templates.error_warning("Denne kommandoen kan bare utføres it servere")
else:
embed = embed_templates.error_fatal("En ukjent feil oppstod!")
f = discord.File("./src/assets/edb.png")
embed.set_image(url="attachment://edb.png")
elif isinstance(error, commands.DisabledCommand):
pass
# Yeah, this fucking sucks but I want my beautiful edb picture :_;
try:
f
except NameError:
f = None
elif isinstance(error, commands.CheckFailure):
return
embed = embed_templates.error_fatal(ctx, text="En ukjent feil oppstod!")
await ctx.reply(embed=embed)
try:
await ctx.reply(embed=embed, file=f)
except discord.errors.Forbidden:
self.bot.logger.warning("Could not send error message to user")
except app_commands.BotMissingPermissions:
await ctx.reply(
"Jeg mangler `embed_links` og/eller `attach_files` tillatelsen(e). Ingenting funker uten de"
)
# Log full exception to file
self.bot.logger.error("".join(traceback.format_exception(type(error), error, error.__traceback__)))
@@ -134,45 +134,60 @@ class Errors(commands.Cog):
error (app_commands.AppCommandError): Eror context object
"""
# TODO: add support for defered interactions
await interaction.response.defer()
# Log command usage, just in case
await self.on_app_command_completion(interaction, interaction.command)
if isinstance(error, app_commands.BotMissingPermissions):
permissions = ", ".join(error.missing_permissions)
embed = embed_templates.error_warning(
interaction, text="Jeg mangler følgende tillatelser:\n\n" + f"```\n{permissions}\n```"
)
embed = self.error_missing_perms(error.missing_permissions, "Jeg")
elif isinstance(error, app_commands.MissingPermissions):
permissions = ", ".join(error.missing_permissions)
embed = embed_templates.error_warning(
interaction, text="Du mangler følgende tillatelser\n\n" + f"```\n{permissions}\n```"
)
# TODO: figure this shit out. app_commands does not support this check
# elif isinstance(error, app_commands.NotOwner):
# embed = embed_templates.error_fatal(interaction, text='Bare boteieren kan gjøre dette')
# return await interaction.response.send_message(embed=embed)
embed = self.error_missing_perms(error.missing_permissions, "Du")
elif isinstance(error, app_commands.CommandOnCooldown):
embed = embed_templates.error_warning(
interaction,
text="Kommandoen har nettopp blitt brukt\n" + f"Prøv igjen om `{error.retry_after:.1f}` sekunder.",
f"Kommandoen har nettopp blitt brukt\nPrøv igjen om `{error.retry_after:.1f}` sekunder."
)
else:
embed = embed_templates.error_fatal(interaction, text="En ukjent feil oppstod!")
embed = embed_templates.error_fatal("En ukjent feil oppstod!")
f = discord.File("./src/assets/edb.png")
embed.set_image(url="attachment://edb.png")
if interaction.response.is_done():
await interaction.followup.send(embed=embed)
else:
await interaction.response.send_message(embed=embed)
# Yeah, this fucking sucks but I want my beautiful edb picture :_;
try:
f
except NameError:
f = None
try:
await interaction.followup.send(embed=embed, file=f)
except discord.errors.Forbidden:
self.bot.logger.warning("Could not send error message to user")
except app_commands.BotMissingPermissions:
await interaction.followup.send(
"Jeg mangler `embed_links` og/eller `attach_files` tillatelsen(e). Ingenting funker uten de"
)
# Log full exception to file
self.bot.logger.error("".join(traceback.format_exception(type(error), error, error.__traceback__)))
def error_missing_perms(self, permissions: list[str], client: str) -> discord.Embed:
"""
Missing permissions embed template.
Since it only applies to this cog it has been put here instead of the util file
Parameters
----------
permissions (list[str]): The missing permissions
client (str): Who is missing the permissions? Typically "Du" (user) or "Jeg" (bot client)
Returns
----------
(discord.Embed): The final embed
"""
permissions = ", ".join(permissions)
return embed_templates.error_warning(f"{client} mangler følgende tillatelser\n\n```{permissions}\n```")
async def setup(bot: commands.Bot):
"""
+42 -25
View File
@@ -12,11 +12,9 @@ class FunReplies(commands.Cog):
"""
Parameters
----------
bot (commands.Bot): The bot instance
bot (commands.Bot): The bot instance. In this case not used
"""
self.bot = bot
# 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
@@ -32,6 +30,7 @@ class FunReplies(commands.Cog):
"borgerlønn": initial_datetime,
}
@commands.Cog.listener("on_message")
async def reply_to_triggers(self, message: discord.Message):
"""
Replies to messages that trigger certain key words/phrases
@@ -44,32 +43,51 @@ class FunReplies(commands.Cog):
if message.author.bot:
return
message_content = message.content.lower()
# TODO: add ability to disable single triggers?
# Auto assign cooldown_key?
triggers = [
(r"(^|\W)borgerlønn(\W|$)", "@ sivert DE SNAKKER OM BORGERLØNN", "borgerlønn"),
(r"(^|\W)olof palme(\W|$)", "Jeg vet hvem som drepte Olof Palme 👀", "olof palme"),
(r"(^|\W)+ye+et($|\W)+", "<:Nei:826593267642662912>", "yeet"),
(r"(^|\W)skal? aldri drikke?[\w\s]*igjen($|\W)+", ":billed_cap:", "drikke"),
(r"(^|\W)(jeg?|(e|æ)(g|j)?|i) er? sivert arntzen($|\W)+", "Nei, jeg er Sivert Arntzen", "sivert"),
]
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.reply("@ sivert DE SNAKKER OM BORGERLØNN")
self.previous_invokations["borgerlønn"] = datetime.now()
for trigger in triggers:
regex, reply, cooldown_key = trigger
if await self.trigger(
message=message, regex_match=regex, reply=reply, cooldown_key=cooldown_key, regex_flags=re.IGNORECASE
):
return
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.reply("Jeg vet hvem som drepte Olof Palme 👀")
self.previous_invokations["olof palme"] = datetime.now()
async def trigger(
self, message: discord.Message, regex_match: str, reply: str, cooldown_key: str, regex_flags=None
) -> bool:
"""
Add a trigger to the bot
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.reply("<:Nei:826593267642662912>")
self.previous_invokations["yeet"] = datetime.now()
Parameters
----------
message (discord.Message): The message object to check for triggers
regex_match (str): The regex pattern to match
reply (str): The reply to send
cooldown_key (str): The key to use for cooldown tracking
regex_flags (int): The regex flags to use
elif re.search(r"(^|\W)skal? aldri drikke?[\w\s]*igjen($|\W)+", message_content, flags=re.IGNORECASE):
if (datetime.now() - self.previous_invokations["drikke"]).seconds > self.cooldown_seconds:
await message.reply(":billed_cap:")
self.previous_invokations["drikke"] = datetime.now()
Returns
----------
bool: Whether or not the reply was triggered
"""
elif re.search(r"(^|\W)(jeg?|(e|æ)(g|j)?|i) er? sivert arntzen($|\W)+", message_content, flags=re.IGNORECASE):
if (datetime.now() - self.previous_invokations["sivert"]).seconds > self.cooldown_seconds:
await message.reply("Nei, jeg er Sivert Arntzen!")
self.previous_invokations["sivert"] = datetime.now()
if (datetime.now() - self.previous_invokations[cooldown_key]).seconds < self.cooldown_seconds:
return False
if re.search(regex_match, message.content, flags=regex_flags):
await message.reply(reply)
self.previous_invokations[cooldown_key] = datetime.now()
return True
return False
async def setup(bot: commands.Bot):
@@ -81,5 +99,4 @@ async def setup(bot: commands.Bot):
bot (commands.Bot): Bot instance
"""
bot.add_listener(FunReplies(bot).reply_to_triggers, "on_message")
await bot.add_cog(FunReplies(bot))
+17 -16
View File
@@ -2,12 +2,11 @@ import discord
from discord import app_commands
from discord.ext import commands
from cogs.utils import discord_utils
from cogs.utils import embed_templates
class Gullkorn(commands.Cog):
"""Stand alone cog for handling whitelisting of Discord users on the Minecraft server"""
"""Tracks stats for gullkorn"""
def __init__(self, bot: commands.Bot):
"""
@@ -21,7 +20,9 @@ class Gullkorn(commands.Cog):
self.init_db()
def init_db(self):
"""Create the necessary tables for the gullkorn cog to work."""
"""
Create the necessary tables for the gullkorn cog to work
"""
self.cursor.execute(
"""
@@ -49,7 +50,7 @@ class Gullkorn(commands.Cog):
"""
)
def __construct_data_string(self, data: list[tuple[int, int, int]]) -> str:
def construct_data_string(self, data: list[tuple[int, int, int]]) -> str:
"""
Constructs a formatted string displaying lists of gullkorn data
@@ -72,6 +73,7 @@ class Gullkorn(commands.Cog):
return formatted_string
@commands.Cog.listener("on_message")
async def gullkorn_listener(self, message: discord.Message):
"""
Listens for messages in the gullkorn channel and updates the database accordingly
@@ -122,8 +124,9 @@ class Gullkorn(commands.Cog):
bruker (discord.Member, optional): Discord user to fetch stats for. Defaults to None.
"""
# This looks kinda ugly, ngl
if bruker:
result = self.cursor.execute(
self.cursor.execute(
"""
SELECT *
FROM gullkorn
@@ -131,25 +134,24 @@ class Gullkorn(commands.Cog):
""",
(bruker.id,),
)
result = self.cursor.fetchone()
if not result:
return await interaction.response.send_message(
embed=embed_templates.error_fatal(interaction, text="Ingen data om denne brukeren funnet"),
embed=embed_templates.error_warning("Ingen data om denne brukeren funnet"),
ephemeral=False,
)
embed = discord.Embed(
title=f"Gullkornstatistikk for `{bruker.name}`",
color=discord_utils.get_color(bruker),
color=bruker.color,
)
embed.set_thumbnail(url=bruker.avatar)
embed.add_field(name="Antall gullkorn", value=result[1])
embed.add_field(name="Antall gullkorn postet", value=result[2])
return await interaction.response.send_message(embed=embed, ephemeral=False)
summary = self.cursor.execute(
self.cursor.execute(
"""
SELECT SUM(citations_posted)
FROM gullkorn;
@@ -157,7 +159,7 @@ class Gullkorn(commands.Cog):
)
summary = self.cursor.fetchone()
most_cited = self.cursor.execute(
self.cursor.execute(
"""
SELECT *
FROM most_cited
@@ -166,7 +168,7 @@ class Gullkorn(commands.Cog):
)
most_cited = self.cursor.fetchall()
citations_posted = self.cursor.execute(
self.cursor.execute(
"""
SELECT *
FROM most_frequent_posters
@@ -175,13 +177,13 @@ class Gullkorn(commands.Cog):
)
citations_posted = self.cursor.fetchall()
most_cited_string = self.__construct_data_string(most_cited)
citations_posted_string = self.__construct_data_string(citations_posted)
most_cited_string = self.construct_data_string(most_cited)
citations_posted_string = self.construct_data_string(citations_posted)
gullkorn_first_msg = "https://canary.discord.com/channels/747542543750660178/865970753748074576/1034587913285025912" # noqa: E501
GULLKORN_FIRST_MSG = "https://canary.discord.com/channels/747542543750660178/865970753748074576/1034587913285025912" # noqa: E501
embed = discord.Embed(title="Gullkornstatistikk for serveren")
embed.description = f"Antall gullkorn siden [denne meldingen]({gullkorn_first_msg}): *{summary[0]}*"
embed.description = f"Antall gullkorn siden [denne meldingen]({GULLKORN_FIRST_MSG}): *{summary[0]}*"
embed.add_field(name="Mest sitert", value=most_cited_string, inline=False)
embed.add_field(name="Postet mest", value=citations_posted_string, inline=False)
await interaction.response.send_message(embed=embed, ephemeral=False)
@@ -196,5 +198,4 @@ async def setup(bot: commands.Bot):
bot (commands.Bot): Bot instance
"""
bot.add_listener(Gullkorn(bot).gullkorn_listener, "on_message")
await bot.add_cog(Gullkorn(bot))
+53 -88
View File
@@ -1,5 +1,3 @@
from os import remove
import discord
from discord import app_commands
from discord.ext import commands
@@ -24,7 +22,7 @@ class Info(commands.Cog):
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:
def construct_role_string(self, roles: list[discord.Role]) -> str:
"""
Puts all roles except @everyone into a list and joins them to a string
@@ -45,7 +43,7 @@ class Info(commands.Cog):
return ", ".join(roles)
def __construct_booster_string(self, interaction: discord.Interaction, join_method: callable = ", ".join) -> str:
def construct_booster_string(self, interaction: discord.Interaction, join_method: callable = ", ".join) -> str:
"""
Joins all boosters into a string
@@ -62,20 +60,17 @@ class Info(commands.Cog):
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 b: f"* {b.name} - {discord.utils.format_dt(b.premium_since)}",
sorted(interaction.guild.premium_subscribers, key=lambda m: m.premium_since),
)
)
# Sort boosters by boost date and put name as well as date of boost into a list of stirngs
boosters = [
f"* {b.name} - {discord.utils.format_dt(b.premium_since)}"
for b in sorted(interaction.guild.premium_subscribers, key=lambda m: m.premium_since)
]
return join_method(boosters)
def __construct_member_string(self, members: list[discord.Member]) -> str:
def construct_member_string(self, members: list[discord.Member]) -> str:
"""
Joins all name#discrim into a string
Joins all names into a string
Parameters
----------
@@ -93,31 +88,6 @@ class Info(commands.Cog):
return ", ".join(members)
async def __send_as_txt_file(self, interaction: discord.Interaction, content: str, file_path: str):
"""
Sends a string as a txt file and deletes the file afterwards
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
try:
remove(file_path)
except (FileNotFoundError, OSError, PermissionError):
pass
@app_commands.guild_only()
@app_commands.checks.bot_has_permissions(embed_links=True, external_emojis=True)
@app_commands.checks.cooldown(1, 2)
@@ -156,7 +126,7 @@ class Info(commands.Cog):
offline_members += 1
# Roles
roles = self.__construct_role_string(interaction.guild.roles)
roles = self.construct_role_string(interaction.guild.roles)
roles = (
roles
if len(roles) < 1024
@@ -164,7 +134,7 @@ class Info(commands.Cog):
)
# Boosts
boosters = self.__construct_booster_string(interaction, join_method="\n".join)
boosters = self.construct_booster_string(interaction, join_method="\n".join)
boosters = (
boosters
if len(boosters) < 1024
@@ -179,31 +149,35 @@ class Info(commands.Cog):
# Features
features_string = ""
if interaction.guild.features is not []:
if interaction.guild.features != []:
features = {
"ANIMATED_BANNER": "Animer serverbanner",
"ANIMATED_ICON": "Animert serverikon",
"APPLICATION_COMMAND_PERMISSIONS_V2": "Gamle slash command tillatelser",
"AUTO_MODERATION": "Automoderering",
"BANNER": "Serverbanner",
"COMMERCE": "E-handel",
"COMMUNITY": "Samfunnsserver",
"CREATOR_MONETIZABLE_PROVISIONAL": "Betalingsmuligheter",
"CREATOR_STORE_PAGE": "Abonnomentsside",
"DEVELOPER_SUPPORT_SERVER": "Støtteserver for utviklere",
"DISCOVERABLE": "På utforsksiden",
"FEATURABLE": "",
"FEATURABLE": "Fremhevbar",
"INVITES_DISABLED": "Invitasjoner deaktivert",
"INVITE_SPLASH": "Invitasjonsbilde",
"MEMBER_VERIFICATION_GATE_ENABLED": "Medlemsverifisering",
"MONETIZATION_ENABLED": "Betalingskanal",
"MORE_EMOJI": "Flere emoji",
"MORE_STICKERS": "Flere stickers",
"NEWS": "Nyhetskanaler",
"PARTNERED": "Partner",
"PREVIEW_ENABLED": "Forhåndsvisning",
"PRIVATE_THREADS": "Private tråder",
"RAID_ALERTS_DISABLED": "Raidvarsler deaktivert",
"ROLE_ICONS": "Rolleikon",
"ROLE_SUBSCRIPTIONS_AVAILABLE_FOR_PURCHASE": "Abonomentsroller tilgjengelig",
"ROLE_SUBSCRIPTIONS_ENABLED": "Abonomentsroller aktivert",
"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 interaction.guild.features:
if translation := features.get(feature):
@@ -232,7 +206,8 @@ class Info(commands.Cog):
description=f"* **Verifiseringskrav:** {verification}\n"
+ f"* **Innholdsfilter:** {content}\n"
+ f"* **Boost Tier:** {interaction.guild.premium_tier}\n"
+ f"* **Emoji:** {len(interaction.guild.emojis)}",
+ f"* **Emoji:** {len(interaction.guild.emojis)}\n"
+ f"* **Stickers:** {len(interaction.guild.stickers)}\n",
)
embed.set_author(name=interaction.guild.name, icon_url=interaction.guild.icon)
embed.set_thumbnail(url=interaction.guild.icon)
@@ -271,7 +246,7 @@ class Info(commands.Cog):
await interaction.response.send_message(embed=embed)
@app_commands.guild_only()
@app_commands.checks.bot_has_permissions(embed_links=True)
@app_commands.checks.bot_has_permissions(embed_links=True, attach_files=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):
@@ -283,11 +258,11 @@ class Info(commands.Cog):
interaction (discord.Interaction): Slash command context object
"""
roles = self.__construct_role_string(interaction.guild.roles)
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:
self.__send_as_txt_file(interaction, f"./assets/temp/{interaction.guild.id}_roles.txt", roles)
await discord_utils.send_as_txt_file(interaction, roles, f"./assets/temp/{interaction.guild.id}_roles.txt")
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)
@@ -308,10 +283,10 @@ class Info(commands.Cog):
"""
if len(interaction.guild.premium_subscribers) == 0:
embed = embed_templates.error_warning(interaction, text="Serveren har ikke noen boosts :(")
embed = embed_templates.error_warning("Serveren har ikke noen boosts :(")
return await interaction.response.send_message(embed=embed)
boosters = self.__construct_booster_string(interaction, "\n".join)
boosters = self.construct_booster_string(interaction, "\n".join)
embed = discord.Embed(color=interaction.guild.me.color, description=boosters)
embed.set_author(
@@ -352,7 +327,7 @@ class Info(commands.Cog):
"""
if not interaction.guild.splash:
embed = embed_templates.error_warning(interaction, text="Serveren har ikke en splash")
embed = embed_templates.error_warning("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.splash})")
@@ -374,7 +349,7 @@ class Info(commands.Cog):
"""
if not interaction.guild.banner:
embed = embed_templates.error_warning(interaction, text="Serveren har ikke et banner :(")
embed = embed_templates.error_warning("Serveren har ikke et banner :(")
return await interaction.response.send_message(embed=embed)
embed = discord.Embed(color=interaction.guild.me.color, description=f"[Lenke]({interaction.guild.banner})")
@@ -397,7 +372,7 @@ class Info(commands.Cog):
"""
if rolle.name == "@everyone":
embed = embed_templates.error_fatal(interaction, text="Skriv inn en annen rolle enn @everyone")
embed = embed_templates.error_warning("Skriv inn en annen rolle enn `@everyone`")
return await interaction.response.send_message(embed=embed)
# Timestamp and days since creation
@@ -405,15 +380,13 @@ class Info(commands.Cog):
since_created_days = (interaction.created_at - rolle.created_at).days
# List of members with the role
members = self.__construct_member_string(rolle.members)
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 = discord.Embed(title=rolle.name, description=f"{rolle.mention}\n**ID:** {rolle.id}", color=rolle.color)
embed.set_author(name=rolle.guild.name, icon_url=rolle.guild.icon)
embed.add_field(name="Fargekode", value=str(rolle.color))
embed.add_field(
@@ -443,7 +416,7 @@ class Info(commands.Cog):
kanal (discord.TextChannel): Text channel to fetch information about
"""
members = self.__construct_member_string(kanal.members)
members = self.construct_member_string(kanal.members)
if len(members) > 1024:
members = "For mange for å vise her"
@@ -501,9 +474,10 @@ class Info(commands.Cog):
interaction (discord.Interaction): Slash command context object
"""
roles = list(filter(lambda r: r.name != "@everyone", interaction.guild.roles))
roles = sorted(roles, key=lambda x: len(x.members), reverse=True)
roles_formatted = list(map(lambda r: f"**#{r[0] + 1}** {r[1].mention} - {len(r[1].members)}", enumerate(roles)))
roles = sorted(
[r for r in interaction.guild.roles if r.name != "@everyone"], key=lambda x: len(x.members), reverse=True
)
roles_formatted = [f"**#{i + 1}** {r.mention} - {len(r.members)}" for i, r in enumerate(roles)]
paginator = misc_utils.Paginator(roles_formatted)
view = discord_utils.Scroller(paginator, interaction.user)
@@ -566,18 +540,14 @@ class Info(commands.Cog):
# Sort members by creation date
members = sorted(interaction.guild.members, key=lambda m: m.created_at)
# Create list of members with index, name#discriminator and creation date
members_formatted = list(
map(
lambda m: f'**#{(m[0] + 1)}** {m[1].name} - {discord.utils.format_dt(m[1].created_at, style="F")}', # noqa: E501
enumerate(members),
)
)
# Create list of members with index, name and creation date
members_formatted = [
f"**#{i+1}** {m.name} - {discord.utils.format_dt(m.created_at, style='F')}" for i, m in enumerate(members)
]
paginator = misc_utils.Paginator(members_formatted)
view = discord_utils.Scroller(paginator, interaction.user)
# Send first page
embed = view.construct_embed(
discord.Embed(
color=interaction.guild.me.color, title="Eldste brukere på serveren basert på når de ble lagd"
@@ -604,12 +574,9 @@ class Info(commands.Cog):
members = sorted(interaction.guild.members, key=lambda m: m.joined_at)
# Create list of members with index, name#discriminator and creation date
members_formatted = list(
map(
lambda m: f'**#{(m[0] + 1)}** {m[1].name} - {discord.utils.format_dt(m[1].joined_at, style="F")}', # noqa: E501
enumerate(members),
)
)
members_formatted = [
f"**#{i+1}** {m.name} - {discord.utils.format_dt(m.joined_at, style='F')}" for i, m in enumerate(members)
]
paginator = misc_utils.Paginator(members_formatted)
view = discord_utils.Scroller(paginator, interaction.user)
@@ -666,7 +633,7 @@ class Info(commands.Cog):
premium_since_days = (interaction.created_at - bruker.premium_since).days
# Get user roles
roles = self.__construct_role_string(bruker.roles)
roles = self.construct_role_string(bruker.roles)
roles = (
roles
@@ -682,9 +649,7 @@ class Info(commands.Cog):
}
status = statuses[str(bruker.status)]
embed = discord.Embed(
color=discord_utils.get_color(bruker), description=f"{bruker.mention}\nID: {bruker.id}\n{status}\n{app}"
)
embed = discord.Embed(color=bruker.color, description=f"{bruker.mention}\nID: {bruker.id}\n{status}\n{app}")
if bruker.display_name == bruker.name:
embed.set_author(name=bruker.name, icon_url=bruker.display_avatar)
else:
@@ -736,15 +701,15 @@ class Info(commands.Cog):
if not bruker:
bruker = interaction.user
roles = self.__construct_role_string(bruker.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:
self.__send_as_txt_file(
interaction, f"./assets/temp/{interaction.guild.id}_{interaction.user.id}_roles.txt", roles
await discord_utils.send_as_txt_file(
interaction, roles, f"./assets/temp/{interaction.guild.id}_{interaction.user.id}_roles.txt"
)
else:
embed = discord.Embed(color=discord_utils.get_color(bruker), description=roles)
embed = discord.Embed(color=bruker.color, description=roles)
embed.set_author(name=f"Roller ({len(bruker.roles)})", icon_url=bruker.display_avatar)
embed.set_footer(text=bruker.name, icon_url=bruker.display_avatar)
await interaction.response.send_message(embed=embed)
@@ -765,7 +730,7 @@ class Info(commands.Cog):
if not bruker:
bruker = interaction.user
embed = discord.Embed(color=discord_utils.get_color(bruker), description=f"[Lenke]({bruker.display_avatar})")
embed = discord.Embed(color=bruker.color, description=f"[Lenke]({bruker.display_avatar})")
embed.set_author(name=bruker.name, icon_url=bruker.display_avatar)
embed.set_image(url=bruker.display_avatar)
await interaction.response.send_message(embed=embed)
+21 -12
View File
@@ -22,7 +22,9 @@ class MCWhitelist(commands.Cog):
self.init_db()
def init_db(self):
"""Create the necessary tables for the mc_whitelist cog to work."""
"""
Create the necessary tables for the mc_whitelist cog to work
"""
self.cursor.execute(
"""
@@ -50,9 +52,7 @@ class MCWhitelist(commands.Cog):
data = requests.get(f"https://api.mojang.com/users/profiles/minecraft/{minecraftbrukernavn}", timeout=10)
if data.status_code != 200:
return await interaction.response.send_message(
embed=embed_templates.error_fatal(
interaction, text=f"Brukeren `{minecraftbrukernavn}` finnes ikke på minecraft"
),
embed=embed_templates.error_warning(f"Brukeren `{minecraftbrukernavn}` finnes ikke på minecraft"),
ephemeral=True,
)
@@ -69,16 +69,25 @@ class MCWhitelist(commands.Cog):
)
if self.cursor.fetchone():
return await interaction.response.send_message(
embed=embed_templates.error_fatal(
interaction, text="Du har allerede whitelisted en bruker eller så er brukeren du oppga whitelisted"
embed=embed_templates.error_warning(
"Du har allerede whitelisted en bruker eller så er brukeren du oppga whitelisted"
)
)
# Whitelist user on minecraft server
# Unfortunately, this requires an active connection to the server, with correct credentials
with MCRcon(host="127.0.0.1", password=self.bot.mc_rcon_password, port=25575) as mcr:
mcr.command(f'whitelist add {data["name"]}')
mcr.command("whitelist reload")
# Also unfortunate, we seemingly need to wrap the context manager like this to catch any exceptions
try:
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")
except Exception as e:
self.bot.logger.error(f"Failed to use RCON: {e}")
return await interaction.response.send_message(
embed=embed_templates.error_fatal(
"Klarte ikke å koble til minecraftserveren. Ta kontakt med din lokale teknisk ansvarlige",
)
)
# Add user to db
self.cursor.execute(
@@ -89,10 +98,10 @@ class MCWhitelist(commands.Cog):
(interaction.user.id, data["id"]),
)
self.bot.logger.info(f"Whitelisted {data['name']} for {interaction.user.name}")
await interaction.response.send_message(
embed=embed_templates.success(
interaction, text=f'`{data["name"]}` er nå tilknyttet din discordbruker og whitelisted!'
)
embed=embed_templates.success(f'`{data["name"]}` er nå tilknyttet din discordbruker og whitelisted!')
)
+17 -90
View File
@@ -1,22 +1,22 @@
import functools
import os
import textwrap
from io import BytesIO
import cv2
import discord
import numpy as np
from discord import app_commands
from discord.ext import commands
from moviepy.editor import CompositeVideoClip
from moviepy.editor import TextClip
from moviepy.editor import VideoFileClip
from PIL import Image
from PIL import ImageDraw
from PIL import ImageEnhance
from PIL import ImageFont
from PIL import UnidentifiedImageError
from cogs.utils import discord_utils
from cogs.utils import embed_templates
from cogs.utils import misc_utils
class Meme(commands.Cog):
@@ -31,7 +31,7 @@ class Meme(commands.Cog):
self.bot = bot
@app_commands.checks.bot_has_permissions(embed_links=True)
@app_commands.checks.bot_has_permissions(attach_files=True)
@app_commands.checks.cooldown(1, 5)
@app_commands.command(name="deepfry", description="Friter et bilde til hundre og helvete")
async def deepfry(self, interaction: discord.Interaction, bilde: discord.Attachment):
@@ -47,14 +47,12 @@ class Meme(commands.Cog):
await interaction.response.defer()
# Fetch image
input = await discord_utils.get_file_bytesio(bilde)
input_image = await discord_utils.get_file_bytesio(bilde)
# Open image
# Check if image is valid
try:
image = Image.open(input)
image = Image.open(input_image)
except UnidentifiedImageError:
await interaction.followup.send(embed=embed_templates.error_warning(interaction, text="Bildet er ugyldig"))
await interaction.followup.send(embed=embed_templates.error_warning("Bildet er ugyldig"))
return
if image.mode != "RGB":
@@ -76,79 +74,7 @@ class Meme(commands.Cog):
await interaction.followup.send(file=discord.File(output, "deepfry.jpg"))
@staticmethod
def __draw_text(image: Image.Image, text: str, offset: tuple = (0, 0)):
"""
Draws meme text on preferred meme template.
Parameters
----------
image (Image.Image): The meme template.
text (str): The text to be drawn.
font (ImageFont.FreeTypeFont): The font, with its size set, to be used.
offset (tuple): The offset of the text from the top left corner of the image. Defaults to (0, 0).
"""
box_size = (540, 540)
box = Image.new("RGB", box_size, (255, 255, 255))
draw = ImageDraw.Draw(box)
font_path = "./src/assets/fonts/RobotoMono-Medium.ttf"
# Calculate initial font size
# This is done by increasing the font size until the text is too wide
max_width, max_height = box_size[0] * 0.9, box_size[1] * 0.9 # 90% of the box size
font_size = 20 # Initiale size. I consider this the smallest readable size
font_width = 0 # Initiale width. This is just to make sure the while loop runs at least once
while font_width < max_width:
font = ImageFont.truetype(font_path, font_size)
font_box = font.getbbox(text)
font_width = font_box[2] - font_box[0]
font_size += 2
# Wrap the text based on our current font size
text = textwrap.fill(text, width=font_size)
# We then have to calculate the font size again
#
# The problem is, font.getbbox() doesn't work with multiline text so it returns the size without the linebreaks
# We can get around this by calculating the size of the longest line in the text
# and then increasing the font size until that line is too wide
#
# We also have to make sure it doesn't get too tall
longest_line = max(text.split("\n"), key=len)
longest_line_font_box = font.getbbox(longest_line)
font_width = longest_line_font_box[2] - longest_line_font_box[0]
# Estimate font height
# This is done by multiplying the height of the original non-wrapped text with the number of lines
# and then adding 20 to account for the line spacing.
# This of course is dependant on font size itself, but it's good enough for now
font_height = (font_box[3] - font_box[1] + 20) * text.count("\n")
max_height = box_size[1] * 0.9
while font_width < max_width and font_height < max_height:
font = ImageFont.truetype(font_path, font_size)
longest_line_font_box = font.getbbox(longest_line)
font_width = longest_line_font_box[2] - longest_line_font_box[0]
font_box = font.getbbox(text)
font_height = (font_box[3] - font_box[1] + 20) * text.count("\n")
font_size += 2
# Draw textbox and text
text_box = draw.multiline_textbbox(box_size, text=text, font=font, align="center", anchor="mm")
text_box = box.size[0] // 2, box.size[1] // 2
draw.multiline_text(text_box, text, font=font, fill=(0, 0, 0, 255), align="center", anchor="mm")
image.paste(box, offset)
@app_commands.checks.bot_has_permissions(embed_links=True)
@app_commands.checks.bot_has_permissions(attach_files=True)
@app_commands.checks.cooldown(1, 5)
@app_commands.command(name="preferansemem", description="Generer en drake-aktig mem basert på tekst")
async def prefer_meme(self, interaction: discord.Interaction, dårlig_tekst: str, bra_tekst: str):
@@ -165,21 +91,23 @@ class Meme(commands.Cog):
await interaction.response.defer()
# Fetch meme template
template = Image.open("./src/assets/misc/sivert_goodbad.jpg")
template = cv2.imread("./src/assets/sivert_goodbad.jpg")
# Draw text
Meme.__draw_text(template, dårlig_tekst, offset=(540, 0))
Meme.__draw_text(template, bra_tekst, offset=(540, 540))
font = "./src/assets/fonts/comic.ttf"
await misc_utils.put_text_in_box(template, dårlig_tekst, (540, 0), (1080, 540), font)
await misc_utils.put_text_in_box(template, bra_tekst, (540, 540), (1080, 1080), font)
# Save image to buffer
output = BytesIO()
template.save(output, format="jpeg")
_, buffer = cv2.imencode(".jpg", template)
output = BytesIO(buffer)
cv2.imdecode(np.frombuffer(output.getbuffer(), np.uint8), -1)
output.seek(0)
# Send image
await interaction.followup.send(file=discord.File(output, "sivert_goodbad.jpg"))
@app_commands.checks.bot_has_permissions(embed_links=True)
@app_commands.checks.bot_has_permissions(attach_files=True)
@app_commands.checks.cooldown(1, 60)
@app_commands.command(name="crabrave", description="Generer en crab rave video basert på tekst")
async def crab_rave(self, interaction: discord.Interaction, topptekst: str, bunntekst: str):
@@ -204,7 +132,6 @@ class Meme(commands.Cog):
os.remove(temp_file)
except (FileNotFoundError, PermissionError):
self.bot.logger.warn(f"Failed to remove temporary file {temp_file}")
pass
@staticmethod
def make_crab(top_text: str, bottom_text: str, user_id: int):
@@ -220,7 +147,7 @@ class Meme(commands.Cog):
"""
font_path = "DejaVu-Sans-Bold"
clip = VideoFileClip("./src/assets/misc/crab.mp4")
clip = VideoFileClip("./src/assets/crab.mp4")
top_part = (
TextClip(top_text, fontsize=60, color="white", stroke_width=2, stroke_color="black", font=font_path)
+28 -84
View File
@@ -1,6 +1,5 @@
import random
import re
import urllib
from datetime import datetime
from hashlib import md5
from io import BytesIO
@@ -29,7 +28,6 @@ class Misc(commands.Cog):
self.bot = bot
@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):
@@ -56,16 +54,24 @@ class Misc(commands.Cog):
tekst (str): Text to translate
"""
owo_rules = {"r": "w", "l": "w", "R": "W", "L": "W", "n": "ny", "N": "Ny", "ove": "uv"}
for key, value in owo_rules.items():
tekst = re.sub(key, value, tekst)
# https://kaomoji.moe/
# Decades of computer sciencetist optimizng every little detail to make
# our computers as fast as possible. Only for me to ruin it by doing this shit.
# I am sorry to our forefathers whose shoulders we stand upon.
# I have sinned
tekst = re.sub(r"[rl]", "w", tekst)
tekst = re.sub(r"[RL]", "W", tekst)
tekst = re.sub(r"n([aeiou])", r"ny\1", tekst, flags=re.IGNORECASE)
tekst = re.sub(r"N([aeiou])", r"Ny\1", tekst)
tekst = re.sub(r"th", "d", tekst, flags=re.IGNORECASE)
tekst = tekst.replace("ove", "uv")
# TODO: add kaomoji? https://kaomoji.moe/
if len(tekst) >= 1000:
embed = embed_templates.error_warning(interaction, text="Teksten er for lang")
embed = embed_templates.error_warning("Teksten er for lang")
return await interaction.response.send_message(embed=embed)
embed = discord.Embed(color=interaction.client.user.color, description=tekst)
embed = discord.Embed(description=tekst)
await interaction.response.send_message(embed=embed)
@app_commands.checks.bot_has_permissions(embed_links=True)
@@ -117,15 +123,11 @@ class Misc(commands.Cog):
params = {"action": "autocomplete", "service": "emner", "scope": "/studier/emner", "q": emnekode, "limit": 1}
data = requests.get("https://www.uio.no/studier/", params=params, timeout=10)
if data.status_code != 200:
return await interaction.response.send_message(
embed=embed_templates.error_fatal(interaction, text="Kunne ikke nå API")
)
return await interaction.response.send_message(embed=embed_templates.error_fatal("Kunne ikke nå API"))
results = data.text.strip("\n").split("\n")[:-1]
if not results:
return await interaction.response.send_message(
embed=embed_templates.error_fatal(interaction, text="Fant ikke emnekode")
)
return await interaction.response.send_message(embed=embed_templates.error_warning("Fant ikke emnekode"))
for result in results:
code, name, _ = result.split(";")
@@ -148,10 +150,10 @@ class Misc(commands.Cog):
"""
if len(tekst) >= 1000:
embed = embed_templates.error_warning(interation, text="Teksten er for lang")
embed = embed_templates.error_warning("Teksten er for lang")
return await interation.response.send_message(embed=embed)
tekst = re.sub(" ", "👏", tekst)
tekst = tekst.replace(" ", "👏")
embed = discord.Embed(color=interation.client.user.color, description=f"**{tekst.upper()}**")
await interation.response.send_message(embed=embed)
@@ -169,6 +171,7 @@ class Misc(commands.Cog):
tekst (str): Text to clapify
"""
# TODO: make this not horrible. I can't be botherd to right now
words = tekst.split(" ")
for i, word in enumerate(words):
karakter = 0
@@ -188,60 +191,6 @@ class Misc(commands.Cog):
embed = discord.Embed(color=interation.client.user.color, description=text)
await interation.response.send_message(content=mention, embed=embed)
@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):
"""
Search for movies on IMDB and displays information about the first result
Parameters
----------
interaction (discord.Interaction): Slash command context object
tittel (str): Movie title
"""
# Search for movie/show
url = "http://www.omdbapi.com/?" + urllib.parse.urlencode({"s": tittel, "apikey": self.bot.api_keys["omdb"]})
search = requests.get(url, timeout=10).json()
try:
best_result_id = search["Search"][0]["imdbID"]
except KeyError:
embed = embed_templates.error_fatal(interaction, text="Fant ikke filmen!")
return await interaction.response.send_message(embed=embed)
# Get movie/show info
data = requests.get(
f'http://www.omdbapi.com/?i={best_result_id}&apikey={self.bot.api_keys["omdb"]}', timeout=10
).json()
acceptable_media_types = ["movie", "series", "episode"]
if data["Type"] not in acceptable_media_types:
embed = embed_templates.error_fatal(interaction, text="Fant ikke filmen!")
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=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"))
if data["Poster"] != "N/A":
embed.set_thumbnail(url=data["Poster"])
if data["Director"] != "N/A":
embed.description = data["Director"]
if data["Plot"] != "N/A" and len(data["Plot"]) < 1024:
embed.add_field(name="Sammendrag", value=data["Plot"], inline=False)
await interaction.response.send_message(embed=embed)
@app_commands.checks.bot_has_permissions(embed_links=True)
@app_commands.checks.cooldown(1, 5)
@app_commands.command(name="helligdager", description="Se helligdager i et land for et år")
@@ -257,13 +206,11 @@ class Misc(commands.Cog):
"""
land = land.upper()
aar = datetime.now().year if not år else int(år)
år = datetime.now().year if not år else int(år)
data = requests.get(f"https://date.nager.at/api/v2/publicholidays/{aar}/{land}", timeout=10)
data = requests.get(f"https://date.nager.at/api/v2/publicholidays/{år}/{land}", timeout=10)
if data.status_code != 200:
embed = embed_templates.error_fatal(
interaction, text="Ugyldig land\nHusk å bruke landskoder\n" + "For eksempel: `NO`"
)
embed = embed_templates.error_warning("Ugyldig land\nHusk å bruke landskoder\n" + "For eksempel: `NO`")
return await interaction.response.send_message(embed=embed)
data = data.json()
@@ -277,12 +224,12 @@ class Misc(commands.Cog):
holiday_str += f'* **{date}**: {day["localName"]}\n'
embed = discord.Embed(
color=interaction.client.user.color, title=f":flag_{country}: Helligdager {aar} :flag_{country}:"
color=interaction.client.user.color, title=f":flag_{country}: Helligdager {år} :flag_{country}:"
)
embed.description = holiday_str
await interaction.response.send_message(embed=embed)
@app_commands.checks.bot_has_permissions(embed_links=True)
@app_commands.checks.bot_has_permissions(embed_links=True, attach_files=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):
@@ -296,9 +243,7 @@ class Misc(commands.Cog):
"""
if bruker == interaction.user:
embed = embed_templates.error_warning(
interaction, text="Jeg vet du er ensom, men du kan ikke matche med deg selv"
)
embed = embed_templates.error_warning("Jeg vet du er ensom, men du kan ikke matche med deg selv")
return await interaction.response.send_message(embed=embed)
invoker_id = int(str(interaction.user.id)[11:14])
@@ -320,8 +265,8 @@ class Misc(commands.Cog):
user = user.resize((389, 389))
# Prepare heart image
heart = Image.open("./src/assets/misc/heart.png")
mask = Image.open("./src/assets/misc/heart.png", "r")
heart = Image.open("./src/assets/heart.png")
mask = Image.open("./src/assets/heart.png", "r")
# Putting it all together
image = Image.new("RGBA", (1024, 576))
@@ -329,7 +274,7 @@ class Misc(commands.Cog):
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 = ImageFont.truetype("./src/assets/fonts/comic.ttf", 86)
font_size = font.getbbox(f"{match_percent}%")
font_size = ((image.size[0] - font_size[2]) / 2, (image.size[1] - font_size[3]) / 2)
draw.text(font_size, f"{match_percent}%", font=font, fill=(255, 255, 255, 255))
@@ -345,7 +290,6 @@ class Misc(commands.Cog):
embed.set_image(url=f"attachment://{interaction.user.id}_{bruker.id}_match.png")
await interaction.response.send_message(embed=embed, file=f)
@app_commands.checks.bot_has_permissions(embed_links=True)
@app_commands.checks.cooldown(1, 2)
@app_commands.command(name="ifonlyyouknew", description="How bad things really are")
async def howbadthingsreallyare(self, interaction: discord.Interaction):
+19 -18
View File
@@ -10,23 +10,20 @@ class RepeatedMessages(commands.Cog):
def __init__(self, bot: commands.Bot):
self.bot = bot
self.general_channel = 747542544291987597
# @aiocron.crontab('37 13 * * *')
# async def leet():
# """Sends a message at leet gamer hours"""
# guild = self.bot.get_guild(747542543750660178)
# channel = guild.get_channel(747542544291987597)
# sticker = await guild.fetch_sticker(1046948904471633951)
# await asyncio.sleep(1)
# await channel.send(stickers=[sticker])
# Yes we have to have these inside the constructor
# Probably could've implemented the waiting functions myself
# but that sounds like work and I don't like to work
@aiocron.crontab("0 0 * * 5")
async def fredag():
"""Sends a message on Friday at 00:00"""
"""
Sends a message on Friday at 00:00
"""
guild = self.bot.get_guild(747542543750660178)
channel = guild.get_channel(747542544291987597)
guild = self.bot.get_guild(self.bot.UIO_GAMING_GUILD_ID)
channel = guild.get_channel(self.general_channel)
videos = [
"https://cdn.discordapp.com/attachments/750052141346979850/851216786259181578/video0.mp4",
@@ -48,10 +45,12 @@ class RepeatedMessages(commands.Cog):
@aiocron.crontab("0 0 * * 1")
async def mandag():
"""Sends a message on Monday at 00:00"""
"""
Sends a message on Monday at 00:00
"""
guild = self.bot.get_guild(747542543750660178)
channel = guild.get_channel(747542544291987597)
guild = self.bot.get_guild(self.bot.UIO_GAMING_GUILD_ID)
channel = guild.get_channel(self.general_channel)
await channel.send(
"ENDELIG MANDAG!\n\n"
# + "https://cdn.discordapp.com/attachments/678396498089738250/862853827278929940/hvorfor_de_rike_br_spises.mp4" # noqa: E501
@@ -60,10 +59,12 @@ class RepeatedMessages(commands.Cog):
@aiocron.crontab("0 0 1 1 *")
async def new_year():
"""Sends a message on New Year's Day at 00:00"""
"""
Sends a message on New Year's Day at 00:00
"""
guild = self.bot.get_guild(747542543750660178)
channel = guild.get_channel(747542544291987597)
guild = self.bot.get_guild(self.bot.UIO_GAMING_GUILD_ID)
channel = guild.get_channel(self.general_channel)
await channel.send(
"GODT NYTTÅR!\n\nNå, meld dere inn i UiO Gaming igjen <:ThisIsAThreat:874998234072903710>"
)
+64 -45
View File
@@ -19,23 +19,10 @@ from cogs.utils import misc_utils
- score for å bli pinged i uio gullkorn
+ score for å få melding på stjernetavla/bli stjerna. multiplier per stjerne
- score hver dag med weebrolle (lite)
"""
"""
------ TO-DO ------
+ score for guten tag
- score sitte i afk
+ score for å sitte i voice per time
---------------
- score for winiie the pooh
- score for memes i general
- score for å snakke negativt om borgerlønn
- score for å snakke negativt om styret
* hvis lav score, spiller av kinesisk nasjonalsang på voicechannel når joiner
daglig uthengig av laveste score
"""
@@ -55,7 +42,9 @@ class SocialCredit(commands.Cog):
self.init_db()
def init_db(self):
"""Create the necessary tables for the social credit cog to work"""
"""
Create the necessary tables for the social credit cog to work
"""
self.cursor.execute(
"""
@@ -76,13 +65,15 @@ class SocialCredit(commands.Cog):
self = args[0]
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])
self.add_citizen(args[1])
await func(*args, **kwargs)
return wrapper
def roll(percent: int = 50):
"""Decorator that executes the function with a given percent chance"""
"""
Decorator that executes the function with a given percent chance
"""
def decorator(func):
async def wrapper(*args, **kwargs):
@@ -93,7 +84,7 @@ class SocialCredit(commands.Cog):
return decorator
def _add_citizen(self, user_id: int):
def add_citizen(self, user_id: int):
"""
Adds a user to the database
@@ -111,21 +102,45 @@ class SocialCredit(commands.Cog):
(user_id, self.START_POINTS),
)
@tasks.loop(hours=24.0, reconnect=True)
@tasks.loop(time=misc_utils.MIDNIGHT, reconnect=True)
async def fuck_uwu(self):
"""Deducts points from people with the weeb role every 24 hours"""
"""
Deducts points from people with the weeb role every 24 hours
"""
await self.bot.wait_until_ready()
weeb_role = self.bot.get_guild(747542543750660178).get_role(803629993539403826)
# We need to do these checks in case the bot user does not have access to the UiO Gaming Discord server
if not (guild := self.bot.get_guild(self.bot.UIO_GAMING_GUILD_ID)):
self.bot.logger.warning(
"Could not fetch UiO Gaming guild."
+ "If the bot does not have access to the UiO Gaming server, this function won't work. "
+ "If it is, ignore this."
)
return
if not (weeb_role := guild.get_role(803629993539403826)):
self.bot.logger.warning(
"Could not fetch UiO Gaming's weeb role."
+ "If the bot does not have access to the UiO Gaming server, this function won't work. "
+ "If it is, ignore this."
)
return
for weeb in weeb_role.members:
await self.social_punishment(weeb.id, 1, "weeb")
@fuck_uwu.before_loop
async def before_fuck_uwu(self):
"""Syncs loop with the time of day"""
"""
Wait until bot cache is ready
"""
await discord_utils.sleep_until_midnight(self.bot)
await self.bot.wait_until_ready()
async def cog_unload(self):
self.bot.logger.info("Unloading cog")
self.fuck_uwu.cancel()
self.cursor.close()
@add_new_citizen
async def social_punishment(self, user_id: int, points: int, reason: str):
@@ -173,6 +188,8 @@ class SocialCredit(commands.Cog):
social_credit_group = app_commands.Group(name="socialcredit", description="Trenger dette å forklares?")
@app_commands.checks.bot_has_permissions(embed_links=True)
@app_commands.checks.cooldown(1, 2)
@social_credit_group.command(name="credits", description="Sjekk hvor dårlig menneske du er")
async def credits(self, interaction: discord.Interaction, *, bruker: discord.Member | None = None):
"""
@@ -187,12 +204,18 @@ class SocialCredit(commands.Cog):
if not bruker:
bruker = interaction.user
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 interaction.response.send_message(
embed=embed_templates.error_fatal(interaction, f"{bruker.mention} er ikke registrert i databasen")
embed=embed_templates.error_warning(f"{bruker.mention} er ikke registrert i databasen")
)
db_user = CreditUser(*result)
@@ -200,6 +223,8 @@ class SocialCredit(commands.Cog):
embed = discord.Embed(description=(f"{bruker.mention} har `{db_user.credit_score}` social credits"))
await interaction.response.send_message(embed=embed)
@app_commands.checks.bot_has_permissions(embed_links=True)
@app_commands.checks.cooldown(1, 2)
@social_credit_group.command(name="leaderboard", description="Sjekk hvem som er de beste og verste borgerne")
async def leaderboard(self, interaction: discord.Interaction):
"""
@@ -218,18 +243,14 @@ class SocialCredit(commands.Cog):
ORDER BY credit_score DESC
"""
)
result = self.cursor.fetchall()
if not (result := self.cursor.fetchall()):
if not result:
return await interaction.send(
embed=embed_templates.error_fatal(interaction, "Ingen brukere er registrert i databasen")
embed=embed_templates.error_warning("Ingen brukere er registrert i databasen")
)
leaderboard_formatted = list(
map(
lambda s: f"**#{s[0]+1}** <@{s[1][0]}> - `{s[1][1]}` poeng",
enumerate(result),
)
)
leaderboard_formatted = [f"**#{s[0]+1}** <@{s[1][0]}> - `{s[1][1]}` poeng" for s in enumerate(result)]
paginator = misc_utils.Paginator(leaderboard_formatted)
view = discord_utils.Scroller(paginator, interaction.user)
@@ -321,10 +342,10 @@ class SocialCredit(commands.Cog):
message (discord.Message): The message object
"""
if message.channel.id == 865970753748074576:
if message.mentions:
for mention in message.mentions:
await self.social_punishment(mention.id, 10, "gullkorn")
# This only works in the UiO Gaming server as well
if message.channel.id == 865970753748074576 and message.mentions:
for mention in message.mentions:
await self.social_punishment(mention.id, 10, "gullkorn")
@commands.Cog.listener("on_reaction_add")
async def on_star_add(self, reaction: discord.Reaction, user: discord.User | discord.Member):
@@ -343,12 +364,11 @@ class SocialCredit(commands.Cog):
if reaction.emoji == "⭐":
if reaction.message.author == user:
await self.social_punishment(user.id, 100, "self-star")
else:
if len(reaction.message.reactions) >= 3:
await self.social_punishment(
user.id, (len(reaction.message.reactions) - 1) * 25, "remove already accumulated stars"
)
await self.social_reward(user.id, 25 * len(reaction.message.reactions), "add new stars")
elif reaction.count == 3:
await self.social_punishment(
user.id, (len(reaction.message.reactions) - 1) * 25, "remove already accumulated stars"
)
await self.social_reward(user.id, 25 * len(reaction.message.reactions), "add new stars")
@commands.Cog.listener("on_reaction_remove")
async def on_star_remove(self, reaction: discord.Reaction, user: discord.User | discord.Member):
@@ -359,9 +379,8 @@ class SocialCredit(commands.Cog):
if user.bot:
return
if reaction.emoji == "⭐":
if len(reaction.message.reactions) >= 3:
await self.social_punishment(user.id, 25, "remove star")
if reaction.emoji == "⭐" and reaction.count >= 3:
await self.social_punishment(user.id, 25, "remove star")
async def setup(bot: commands.Bot):
+39 -35
View File
@@ -26,14 +26,17 @@ class Streak(commands.Cog):
self.cursor = self.bot.db_connection.cursor()
self.init_db()
# Cache to avoid having to insert to the db for every message
self.streak_cache = {}
self.populate_cache()
self.streak_check.start()
self.streak_update_loop.start()
self.insert_cache_loop.start()
def init_db(self):
"""Create the necessary tables for the streak cog to work."""
"""
Create the necessary tables for the streak cog to work
"""
self.cursor.execute(
"""
@@ -47,11 +50,9 @@ class Streak(commands.Cog):
)
async def cog_unload(self):
"""Stop the loops and close the database connection"""
await self.streak_update()
self.bot.logger.info("Unloading cog")
self.insert_cache_loop.cancel()
self.streak_check.cancel()
self.streak_update_loop.cancel()
self.cursor.close()
def populate_cache(self):
@@ -98,17 +99,25 @@ class Streak(commands.Cog):
}
@tasks.loop(minutes=10)
async def streak_update_loop(self):
async def insert_cache_loop(self):
"""
Inserts cache into the database every 10 minutes
"""
await self.insert_cache()
@insert_cache_loop.after_loop
async def on_insert_cache_loop_cancel(self):
self.bot.logger.info("Insert cache loop cancelled")
if self.insert_cache_loop.is_being_cancelled() and self.streak_cache:
await self.insert_cache()
async def insert_cache(self):
"""
Inserts cache into the database
"""
await self.streak_update()
async def streak_update(self):
"""
Inserts cache into the database
"""
self.bot.logger.info("Inserting cache into database")
# Insert into database
for user_id, user_data in self.streak_cache.items():
@@ -128,14 +137,15 @@ class Streak(commands.Cog):
),
)
@tasks.loop(hours=24)
@tasks.loop(time=misc_utils.MIDNIGHT)
async def streak_check(self):
"""
Check if anyone has lost their streak
"""
# Clear cache to make sure all streaks are up to date
await self.streak_update()
if self.streak_cache:
await self.insert_cache()
self.cursor.execute(
"""
@@ -156,15 +166,16 @@ class Streak(commands.Cog):
(streak[1],),
)
@streak_check.before_loop
async def before_streak_check(self):
"""Syncs loop to the time of day"""
self.bot.logger.info("Postponing streak check until midnight")
await discord_utils.sleep_until_midnight(self.bot)
@streak_check.after_loop
async def on_streak_check_cancel(self):
self.bot.logger.info("Streak check loop cancelled")
if self.streak_check.is_being_cancelled():
await self.streak_check()
streak_group = app_commands.Group(name="streak", description="Snapchat streaks, men for Discord")
@app_commands.checks.bot_has_permissions(embed_links=True)
@app_commands.checks.cooldown(1, 2)
@streak_group.command(name="se", description="Se din eller en annen brukers streak")
async def streak_user(self, interaction: discord.Interaction, bruker: discord.Member | discord.User | None = None):
"""
@@ -189,13 +200,10 @@ class Streak(commands.Cog):
""",
(bruker.id,),
)
streak = self.cursor.fetchone()
if not streak:
return await interaction.followup.send(
embed=embed_templates.error_warning(interaction, "Brukeren har ikke noen streak")
)
return await interaction.followup.send(embed=embed_templates.error_warning("Brukeren har ikke noen streak"))
streak_msg_channel, streak_msg_id = streak[0].split("-")
streak_msg_channel, streak_msg_id = int(streak_msg_channel), int(streak_msg_id)
@@ -225,6 +233,8 @@ class Streak(commands.Cog):
)
await interaction.followup.send(embed=embed)
@app_commands.checks.bot_has_permissions(embed_links=True)
@app_commands.checks.cooldown(1, 2)
@streak_group.command(name="topp", description="Se hvem som har høyest streak på serveren")
async def streak_top(self, interaction: discord.Interaction):
"""
@@ -244,20 +254,14 @@ class Streak(commands.Cog):
ORDER BY streak_start_time;
"""
)
streaks = self.cursor.fetchall()
if not streaks:
return await interaction.followup.send(
embed=embed_templates.error_warning(interaction, "Ingen har noen streak enda")
)
return await interaction.followup.send(embed=embed_templates.error_warning("Ingen har noen streak enda"))
streaks_formatted = list(
map(
lambda s: f"**#{s[0]+1}** <@{s[1][0]}> - {(datetime.now() - s[1][1]).days} dager",
enumerate(streaks),
)
)
streaks_formatted = [
f"**#{s[0]+1}** <@{s[1][0]}> - {(datetime.now() - s[1][1]).days} dager" for s in enumerate(streaks)
]
paginator = misc_utils.Paginator(streaks_formatted)
view = discord_utils.Scroller(paginator, interaction.user)
+2 -3
View File
@@ -16,7 +16,6 @@ class TempVoice(commands.Cog):
bot (commands.Bot): The bot instance
"""
self.bot = bot
self.temp_voice_helper = TempVoiceHelper(bot)
@commands.Cog.listener()
@@ -29,6 +28,7 @@ class TempVoice(commands.Cog):
await self.temp_voice_helper.on_voice_state_update(member, before, after)
@app_commands.checks.bot_has_permissions(manage_channels=True, embed_links=True)
@app_commands.checks.cooldown(1, 300)
@app_commands.command(name="tempvoice")
async def tempvoice(self, interaction: discord.Interaction, name: str, limit: int = 0):
@@ -46,8 +46,7 @@ class TempVoice(commands.Cog):
await interaction.response.send_message(
embed=embed_templates.success(
interaction,
text=f"{channel.mention} ble opprettet. Den vil bli slettet etter cirka 1 minutt uten aktivitet.",
f"{channel.mention} ble opprettet. Den vil bli slettet etter cirka 1 minutt uten aktivitet.",
)
)
+15 -19
View File
@@ -14,7 +14,6 @@ import discord
from discord import app_commands
from discord.ext import commands
from cogs.utils import discord_utils
from cogs.utils import embed_templates
from cogs.utils.discord_utils import Lobby
from cogs.utils.discord_utils import LobbyView
@@ -54,8 +53,7 @@ class TenMan(commands.Cog):
else:
return await interaction.response.send_message(
embed=embed_templates.error_warning(
interaction,
text="Du hoster allerede en lobby",
"Du hoster allerede en lobby",
)
)
@@ -63,8 +61,7 @@ class TenMan(commands.Cog):
if interaction.user.id in lobby.players:
return await interaction.response.send_message(
embed=embed_templates.error_warning(
interaction,
text="Du er allerede i en lobby",
"Du er allerede i en lobby",
)
)
@@ -80,7 +77,7 @@ class TenMan(commands.Cog):
embed = discord.Embed(
title="10 Man Lobby",
description=f"Lobbyen stenger {time_left}. Bli med innen da!",
color=discord_utils.get_color(interaction.user),
color=interaction.user.color,
)
embed.set_author(name=interaction.user.global_name, icon_url=interaction.user.avatar)
embed.add_field(name="Spillere", value=f"* {interaction.user.mention}", inline=False)
@@ -97,11 +94,11 @@ class TenManView(LobbyView):
@override
async def start_lobby(self, interaction: discord.Interaction, button: discord.ui.Button):
if interaction.user != self.lobby.host:
embed = embed_templates.error_warning(interaction, text="Bare hosten kan starte")
embed = embed_templates.error_warning("Bare hosten kan starte")
return await interaction.response.send_message(embed=embed, ephemeral=True, delete_after=10)
if len(self.lobby.players) < 3:
embed = embed_templates.error_warning(interaction, text="Det må være mer enn 2 i lobbyen")
embed = embed_templates.error_warning("Det må være mer enn 2 i lobbyen")
return await interaction.response.send_message(embed=embed, ephemeral=True, delete_after=10)
await self.end_lobby()
@@ -136,7 +133,7 @@ class TeamLeaderView(discord.ui.View):
async def on_error(self, interaction: discord.Interaction, error: Exception, item: discord.ui.Item):
self.bot.logger.error("TeamLeaderView error", exc_info=error)
embed = embed_templates.error_warning(interaction, text="Oopsie woopsie, we made a fucky wucky!")
embed = embed_templates.error_fatal("Oopsie woopsie, we made a fucky wucky!")
await interaction.response.send_message(embed=embed, delete_after=10)
@@ -151,7 +148,7 @@ class TeamLeaderSelectMenu(discord.ui.Select):
async def callback(self, interaction: discord.Interaction):
if interaction.user != self.parent_view.lobby.host:
embed = embed_templates.error_warning(interaction, text="Bare hosten kan velge lagledere")
embed = embed_templates.error_warning("Bare hosten kan velge lagledere")
return await interaction.response.send_message(embed=embed, ephemeral=True, delete_after=10)
team_leaders = []
@@ -203,7 +200,7 @@ class TeamSelectView(discord.ui.View):
async def on_error(self, interaction: discord.Interaction, error: Exception, item: discord.ui.Item):
self.bot.logger.error("TeamSelectView error", exc_info=error)
embed = embed_templates.error_warning(interaction, text="Oopsie woopsie, we made a fucky wucky")
embed = embed_templates.error_warning("Oopsie woopsie, we made a fucky wucky")
await interaction.response.send_message(embed=embed, delete_after=10)
def teams_ready(self, interaction):
@@ -246,11 +243,11 @@ class TeamSelectMenu(discord.ui.Select):
async def callback(self, interaction: discord.Interaction):
if interaction.user != self.parent_view.teams[0][0] and interaction.user != self.parent_view.teams[1][0]:
embed = embed_templates.error_warning(interaction, text="Bare lagledere kan velge spillere")
embed = embed_templates.error_warning("Bare lagledere kan velge spillere")
return await interaction.response.send_message(embed=embed, ephemeral=True, delete_after=10)
if interaction.user != self.parent_view.teams[self.parent_view.turn][0]:
embed = embed_templates.error_warning(interaction, text="Det er ikke din tur enda")
embed = embed_templates.error_warning("Det er ikke din tur enda")
return await interaction.response.send_message(embed=embed, ephemeral=True, delete_after=10)
turn_in_question = self.parent_view.turn
@@ -264,7 +261,7 @@ class TeamSelectMenu(discord.ui.Select):
await self.rerender_players(interaction, turn_in_question)
embed = embed_templates.success(interaction, text="Bruker valgt!")
embed = embed_templates.success("Bruker valgt!")
await interaction.response.send_message(embed=embed, ephemeral=True, delete_after=5)
@@ -279,7 +276,7 @@ class MoveTeamVoiceButton(discord.ui.Button):
async def callback(self, interaction: discord.Interaction):
if interaction.user != self.parent_view.teams[0][0] and interaction.user != self.parent_view.teams[1][0]:
embed = embed_templates.error_warning(interaction, text="Bare lagleder kan flytte laget!")
embed = embed_templates.error_warning("Bare lagleder kan flytte laget!")
return await interaction.response.send_message(embed=embed, ephemeral=True, delete_after=10)
if interaction.user == self.parent_view.teams[0][0]:
@@ -295,21 +292,20 @@ class MoveTeamVoiceButton(discord.ui.Button):
failed_users = await temp_voice.move_players(interaction, self.clicked[team["number"]], team["players"])
else:
embed = embed_templates.error_warning(
interaction, text=f"Kanal for lag {team['number']} allerede opprettet og spillere flyttet!"
f"Kanal for lag {team['number']} allerede opprettet og spillere flyttet!"
)
return await interaction.response.send_message(embed=embed, ephemeral=True, delete_after=10)
if failed_users:
failed_users = "\n".join([f"* {user.mention}" for user in failed_users])
embed = embed_templates.error_warning(
interaction,
text=f"Klarte ikke å flytte følgende brukere til kanalen:\n{failed_users}\n\n"
f"Klarte ikke å flytte følgende brukere til kanalen:\n{failed_users}\n\n"
+ "Dette kan være fordi brukere(ne) ikke er koblet til en kanal fra før"
+ "eller at jeg ikke har tillatelse til å flytte",
)
return await interaction.response.send_message(embed=embed, delete_after=10)
embed = embed_templates.success(interaction, text=f"Flyttet lag {team['number']} til voice!")
embed = embed_templates.success(f"Flyttet lag {team['number']} til voice!")
await interaction.response.send_message(embed=embed, delete_after=10)
-163
View File
@@ -1,163 +0,0 @@
import io
import traceback
import aiohttp
import discord
from discord import app_commands
from discord.ext import commands
from PIL import Image
from PIL import ImageDraw
from PIL import ImageFont
from PIL import ImageSequence
from cogs.utils import embed_templates
class The(commands.Cog):
"""
This cog lets you make a "the" barnacle boy laser eyes meme
"""
def __init__(self, bot: commands.Bot):
self.bot = bot
self.background_url = "https://i.kym-cdn.com/photos/images/newsfeed/001/777/063/d42.png"
self.x = 30
self.y = 340
async def fetch_image(self, url: str) -> io.BytesIO:
"""
Fetch the image from the URL
Parameters
----------
url (str): url
Returns
----------
(io.BytesIO): data buffer
"""
if not url:
return None
async with aiohttp.ClientSession() as session:
async with session.get(url) as resp:
if resp.status != 200:
return None
data = io.BytesIO(await resp.read())
return data
def outline_text(
self,
draw: ImageDraw,
text: str,
font: ImageFont.FreeTypeFont,
x: int,
y: int,
thickness: int,
) -> None:
"""
Outlines given text
"""
for dx in range(-thickness, thickness + 1):
for dy in range(-thickness, thickness + 1):
if dx != 0 or dy != 0:
draw.text((x + dx, y + dy), text, font=font, fill="black")
draw.text((x, y), text, font=font, fill="white")
@app_commands.command()
async def the(
self, interaction: discord.Interaction, top_text: str = "", image_url: str = "", bottom_text: str = ""
):
"""
Generer en barnacle boy laser eyes THE mem basert på gitt bilde/tekst
Parameters
----------
interaction (discord.Interaction): Slash command context object
caption (str): top text
image_url (str): image url
bottom_text (str): bottom text
Returns
----------
(discord.File): an image
"""
await interaction.response.defer()
gif = False
try:
bg_data = await self.fetch_image(self.background_url)
background = Image.open(bg_data)
image_data = await self.fetch_image(image_url)
if image_data:
image = Image.open(image_data)
if image.format.lower() == "gif":
gif = True
frames = []
for frame in ImageSequence.Iterator(image):
frame = frame.convert("RGBA").resize((200, 200), Image.ANTIALIAS)
new_frame = background.copy().convert("RGBA")
new_frame.alpha_composite(frame, (self.x, self.y))
frames.append(new_frame)
draw = ImageDraw.Draw(frames[0], "RGBA")
else:
image = image.convert("RGBA").resize((200, 200), Image.ANTIALIAS)
background = background.convert("RGBA")
background.alpha_composite(image, (self.x, self.y))
draw = ImageDraw.Draw(background, "RGBA")
font = ImageFont.truetype("./src/assets/fonts/impact.ttf", 120)
if top_text:
text_x, text_y = (210, -10)
self.outline_text(draw, top_text.upper(), font, text_x, text_y, thickness=5)
if bottom_text:
text_x, text_y = (150, 550)
self.outline_text(draw, bottom_text.upper(), font, text_x, text_y, thickness=5)
if gif:
with io.BytesIO() as output:
frames[0].save(
output,
format="GIF",
save_all=True,
append_images=frames[1:],
duration=image.info["duration"],
loop=0,
transparency=image.info.get("transparency", 0),
disposal=2, # Use 'restore to background color' disposal method
)
output.seek(0)
await interaction.followup.send(file=discord.File(output, "result.gif"))
else:
with io.BytesIO() as output:
background.save(output, format="PNG")
output.seek(0)
await interaction.followup.send(file=discord.File(output, "result.png"))
except Exception as e:
self.bot.logger.warning(f"An error occurred while processing the images: {e}")
traceback.print_exc()
await interaction.followup.send(
embed=embed_templates.error_fatal(
interaction, "Klarte ikke å generere mem! Har du gitt en ordentlig bildelenke?"
)
)
async def setup(bot):
"""
Add the cog to the bot on extension load
Parameters
----------
bot (commands.Bot): Bot instance
"""
await bot.add_cog(The(bot))
+36 -28
View File
@@ -34,10 +34,12 @@ class UserFacts(commands.Cog):
"ESFP",
}
self.mbti_list = list(self.mbti_codes)
self.similarity_matrix = self._create_similarity_matrix()
self.similarity_matrix = self.create_similarity_matrix()
def init_db(self):
"""Create the necessary tables for the birthday cog to work"""
"""
Create the necessary tables for the birthday cog to work
"""
self.cursor.execute(
"""
@@ -51,6 +53,8 @@ class UserFacts(commands.Cog):
height_group = app_commands.Group(name="høyde", description="Se, endre eller fjern høyde for brukere på serveren")
@app_commands.checks.bot_has_permissions(embed_links=True)
@app_commands.checks.cooldown(1, 2)
@height_group.command(name="se", description="Se høyden til en bruker")
async def height_see(self, interaction: discord.Interaction, bruker: discord.Member = None):
"""
@@ -76,7 +80,7 @@ class UserFacts(commands.Cog):
if not height:
return await interaction.response.send_message(
embed=embed_templates.error_warning(interaction, text="Brukeren har ikke lagt inn høyden sin")
embed=embed_templates.error_warning("Brukeren har ikke lagt inn høyden sin")
)
height = height[0]
@@ -92,6 +96,8 @@ class UserFacts(commands.Cog):
await interaction.response.send_message(embed=embed)
@app_commands.checks.bot_has_permissions(embed_links=True)
@app_commands.checks.cooldown(1, 2)
@height_group.command(name="sett", description="Sett høyden din")
@app_commands.rename(height_cm="høyde_cm")
async def height_set(self, interaction: discord.Interaction, height_cm: app_commands.Range[int, 50, 250]):
@@ -114,8 +120,10 @@ class UserFacts(commands.Cog):
(interaction.user.id, height_cm, height_cm),
)
await interaction.response.send_message(embed=embed_templates.success(interaction, text="Høyde satt!"))
await interaction.response.send_message(embed=embed_templates.success("Høyde satt!"))
@app_commands.checks.bot_has_permissions(embed_links=True)
@app_commands.checks.cooldown(1, 2)
@height_group.command(name="fjern", description="Fjern høyden din")
async def height_remove(self, interaction: discord.Interaction):
"""
@@ -137,13 +145,13 @@ class UserFacts(commands.Cog):
if self.cursor.rowcount == 0:
return await interaction.response.send_message(
embed=embed_templates.success(
interaction, text="Du hadde ikke lagt inn høyden din fra før av men ok :)"
)
embed=embed_templates.success("Du hadde ikke lagt inn høyden din fra før av men ok :)")
)
await interaction.response.send_message(embed=embed_templates.success(interaction, text="Høyde fjernet!"))
await interaction.response.send_message(embed=embed_templates.success("Høyde fjernet!"))
@app_commands.checks.bot_has_permissions(embed_links=True)
@app_commands.checks.cooldown(1, 2)
@height_group.command(name="leaderboard", description="Se en leaderboard over høyder")
async def height_leaderboard(self, interaction: discord.Interaction):
"""
@@ -164,17 +172,12 @@ class UserFacts(commands.Cog):
if not (result := self.cursor.fetchall()):
return await interaction.response.send_message(
embed=embed_templates.error_warning(interaction, text="Ingen har lagt inn høyden sin enda")
embed=embed_templates.error_warning("Ingen har lagt inn høyden sin enda")
)
result_formatted = list(
map(
lambda s: f"**#{s[0]+1}** <@{s[1][0]}> - `{s[1][1]}` cm",
enumerate(result),
)
)
results_formatted = [f"**#{i+1}** <@{row[0]}> - `{row[1]}` cm" for i, row in enumerate(result)]
paginator = misc_utils.Paginator(result_formatted)
paginator = misc_utils.Paginator(results_formatted)
view = discord_utils.Scroller(paginator, interaction.user)
embed = view.construct_embed(discord.Embed(title="Våre høyeste og laveste (👑)"))
@@ -182,6 +185,8 @@ class UserFacts(commands.Cog):
mbti_group = app_commands.Group(name="mbti", description="Se, endre eller fjern MBTI for brukere på serveren")
@app_commands.checks.bot_has_permissions(embed_links=True, attach_files=True)
@app_commands.checks.cooldown(1, 2)
@mbti_group.command(name="se", description="Se MBTI-en til en bruker")
async def mbti_see(self, interaction: discord.Interaction, bruker: discord.Member = None):
"""
@@ -207,7 +212,7 @@ class UserFacts(commands.Cog):
if not user_mbti:
return await interaction.response.send_message(
embed=embed_templates.error_warning(interaction, text="Brukeren har ikke lagt inn MBTI-en sin")
embed=embed_templates.error_warning("Brukeren har ikke lagt inn MBTI-en sin")
)
user_mbti = user_mbti[0]
@@ -235,7 +240,7 @@ class UserFacts(commands.Cog):
else:
others.append((discord_id, mbti))
self._create_mbti_graph((bruker, user_mbti), others)
self.create_mbti_graph((bruker, user_mbti), others)
with open(f"src/assets/temp/{bruker.id}_mbti.png", "rb") as f:
image = discord.File(f, filename=f"{bruker.id}_mbti.png")
@@ -243,6 +248,8 @@ class UserFacts(commands.Cog):
embed.set_image(url=f"attachment://{bruker.id}_mbti.png")
await interaction.response.send_message(embed=embed, file=image)
@app_commands.checks.bot_has_permissions(embed_links=True)
@app_commands.checks.cooldown(1, 2)
@mbti_group.command(name="sett", description="Sett MBTI-en din")
async def mbti_set(self, interaction: discord.Interaction, mbti: str):
"""
@@ -255,9 +262,7 @@ class UserFacts(commands.Cog):
"""
if mbti.upper() not in self.mbti_codes:
return await interaction.response.send_message(
embed=embed_templates.error_warning(interaction, text="Ugyldig MBTI")
)
return await interaction.response.send_message(embed=embed_templates.error_warning("Ugyldig MBTI"))
self.cursor.execute(
"""
@@ -269,7 +274,7 @@ class UserFacts(commands.Cog):
(interaction.user.id, mbti.upper(), mbti.upper()),
)
await interaction.response.send_message(embed=embed_templates.success(interaction, text="MBTI satt!"))
await interaction.response.send_message(embed=embed_templates.success("MBTI satt!"))
@mbti_set.autocomplete("mbti")
async def mbti_set_autocomplete_callback(self, interaction: discord.Interaction, current: str):
@@ -286,6 +291,8 @@ class UserFacts(commands.Cog):
app_commands.Choice(name=mbti, value=mbti) for mbti in self.mbti_codes if mbti.startswith(current.upper())
]
@app_commands.checks.bot_has_permissions(embed_links=True)
@app_commands.checks.cooldown(1, 2)
@mbti_group.command(name="fjern", description="Fjern MBTI-en din")
async def mbti_remove(self, interaction: discord.Interaction):
"""
@@ -307,13 +314,13 @@ class UserFacts(commands.Cog):
if self.cursor.rowcount == 0:
return await interaction.response.send_message(
embed=embed_templates.success(
interaction, text="Du hadde ikke lagt inn MBTIen din fra før av men ok :)"
)
embed=embed_templates.success("Du hadde ikke lagt inn MBTIen din fra før av men ok :)")
)
await interaction.response.send_message(embed=embed_templates.success(interaction, text="MBTI fjernet!"))
await interaction.response.send_message(embed=embed_templates.success("MBTI fjernet!"))
@app_commands.checks.bot_has_permissions(embed_links=True)
@app_commands.checks.cooldown(1, 2)
@mbti_group.command(name="forklaring", description="Skjønner du ikke hva MBTI er? Her er en forklaring")
async def mbti_explanation(self, interaction: discord.Interaction):
"""
@@ -334,7 +341,7 @@ class UserFacts(commands.Cog):
)
await interaction.response.send_message(embed=embed)
def _create_similarity_matrix(self):
def create_similarity_matrix(self):
def similarity(mbti_1, mbti_2):
similarity = 0
for i in range(4):
@@ -352,7 +359,8 @@ class UserFacts(commands.Cog):
return similarity_matrix
def _create_mbti_graph(self, user_mbti: tuple[discord.Member, str], others: list[tuple[discord.Member, str]]):
# TODO: fix this. The distances are wrong
def create_mbti_graph(self, user_mbti: tuple[discord.Member, str], others: list[tuple[discord.Member, str]]):
user, mbti = user_mbti
# Create a Graphviz graph
+34 -43
View File
@@ -1,3 +1,4 @@
import os
from dataclasses import dataclass
from datetime import datetime
from io import BytesIO
@@ -10,40 +11,30 @@ from . import embed_templates
from .misc_utils import Paginator
def get_color(discord_object: discord.User | discord.Member | discord.Role) -> discord.Color:
async def send_as_txt_file(interaction: discord.Interaction, content: str, file_path: str):
"""
Returns the user's top role color
Sends a string as a txt file and deletes the file afterwards
Parameters
-----------
discord_object (discord.User|discord.Member|discord.Role): A discord object that has a color attribute
Returns
-----------
(discord.Color): The user's displayed color
----------
interaction (discord.Interaction): Slash command context object
content (str): String that's too long to send
file_path (str): Path to file
"""
if hasattr(discord_object, "color") and str(discord_object.color) != "#000000":
return discord_object.color
# Create file
with open(file_path, "w", encoding="utf-8") as file:
file.write(content)
return discord.Colour(0x99AAB5)
# Send file
txt_file = discord.File(file_path)
await interaction.response.send_message(file=txt_file)
async def sleep_until_midnight(bot):
"""Syncs loop with the time of day"""
await bot.wait_until_ready()
now = datetime.datetime.now()
if now.hour > 0:
sleep_until = now + datetime.timedelta(days=1)
sleep_until = sleep_until.replace(hour=0, minute=0, second=0, microsecond=0)
else:
sleep_until = now.replace(hour=0, minute=0, second=0, microsecond=0)
bot.logger.info(f"Loop sleeping until {sleep_until}")
await discord.utils.sleep_until(sleep_until)
# Delete file
try:
os.remove(file_path)
except OSError:
pass
async def get_file_bytesio(file: discord.Attachment | discord.Asset) -> BytesIO:
@@ -240,37 +231,37 @@ class LobbyView(discord.ui.View):
@discord.ui.button(label="Bli med", style=discord.ButtonStyle.blurple)
async def join_lobby(self, interaction: discord.Interaction, button: discord.ui.Button):
if datetime.now() > self.lobby.ends:
embed = embed_templates.error_warning(interaction, text="Lobbyen har allerede startet")
embed = embed_templates.error_warning("Lobbyen har allerede startet")
return await interaction.response.send_message(embed=embed, ephemeral=True, delete_after=10)
if (
interaction.user.id in self.lobby.kicked_players
): # We have to use the ID here to avoid fetching the member object in the selection menu
embed = embed_templates.error_warning(interaction, text="Du er blitt kastet ut av lobbyen")
embed = embed_templates.error_warning("Du er blitt kastet ut av lobbyen")
return await interaction.response.send_message(embed=embed, ephemeral=True, delete_after=10)
if interaction.user in self.lobby.players:
embed = embed_templates.error_warning(interaction, text="Du er allerede i lobbyen")
embed = embed_templates.error_warning("Du er allerede i lobbyen")
return await interaction.response.send_message(embed=embed, ephemeral=True, delete_after=10)
if len(self.lobby.players) >= 10:
embed = embed_templates.error_warning(interaction, text="Lobbyen er full")
embed = embed_templates.error_warning("Lobbyen er full")
return await interaction.response.send_message(embed=embed, ephemeral=True, delete_after=10)
self.lobby.players.append(interaction.user)
await self.rerender_players(interaction)
embed = embed_templates.success(interaction, text="Du har blitt med i lobbyen")
embed = embed_templates.success("Du har blitt med i lobbyen")
await interaction.response.send_message(embed=embed, ephemeral=True, delete_after=5)
@discord.ui.button(label="Start", style=discord.ButtonStyle.green)
async def start_lobby(self, interaction: discord.Interaction, button: discord.ui.Button):
if interaction.user != self.lobby.host:
embed = embed_templates.error_warning(interaction, text="Bare hosten kan starte")
embed = embed_templates.error_warning("Bare hosten kan starte")
return await interaction.response.send_message(embed=embed, ephemeral=True, delete_after=10)
if len(self.lobby.players) < 3:
embed = embed_templates.error_warning(interaction, text="Det må være mer enn 2 i lobbyen")
embed = embed_templates.error_warning("Det må være mer enn 2 i lobbyen")
return await interaction.response.send_message(embed=embed, ephemeral=True, delete_after=10)
await self.end_lobby()
@@ -283,30 +274,30 @@ class LobbyView(discord.ui.View):
async def leave_lobby(self, interaction: discord.Interaction, button: discord.ui.Button):
if interaction.user == self.lobby.host:
embed = embed_templates.error_warning(
interaction, text="Du kan ikke forlate lobbyen som host. Slett lobbyen i stedet om ønskelig!"
"Du kan ikke forlate lobbyen som host. Slett lobbyen i stedet om ønskelig!"
)
return await interaction.response.send_message(embed=embed, ephemeral=True, delete_after=10)
if interaction.user not in self.lobby.players:
embed = embed_templates.error_warning(interaction, text="Du er ikke i lobbyen")
embed = embed_templates.error_warning("Du er ikke i lobbyen")
return await interaction.response.send_message(embed=embed, ephemeral=True, delete_after=10)
self.lobby.players.remove(interaction.user)
await self.rerender_players(interaction)
embed = embed_templates.success(interaction, text="Du har forlatt lobbyen")
embed = embed_templates.success("Du har forlatt lobbyen")
await interaction.response.send_message(embed=embed, ephemeral=True, delete_after=5)
@discord.ui.button(label="Slett", style=discord.ButtonStyle.red)
async def delete_lobby(self, interaction: discord.Interaction, button: discord.ui.Button):
if interaction.user != self.lobby.host:
embed = embed_templates.error_warning(interaction, text="Bare hosten kan slette lobbyen")
embed = embed_templates.error_warning("Bare hosten kan slette lobbyen")
return await interaction.response.send_message(embed=embed, ephemeral=True, delete_after=10)
self.lobby.ends = datetime.now()
await interaction.message.delete()
embed = embed_templates.success(interaction, text="Lobbyen har blitt slettet")
embed = embed_templates.success("Lobbyen har blitt slettet")
await interaction.response.send_message(embed=embed, delete_after=5)
@@ -332,11 +323,11 @@ class _KickSelectMenu(discord.ui.Select):
async def callback(self, interaction: discord.Interaction):
if interaction.user != self.parent_view.lobby.host:
embed = embed_templates.error_warning(interaction, text="Bare hosten kan kicke spillere")
embed = embed_templates.error_warning("Bare hosten kan kicke spillere")
return await interaction.response.send_message(embed=embed, ephemeral=True, delete_after=10)
if str(interaction.user.id) in interaction.data["values"][0]:
embed = embed_templates.error_warning(interaction, text="Du kan ikke kicke deg selv")
embed = embed_templates.error_warning("Du kan ikke kicke deg selv")
return await interaction.response.send_message(embed=embed, ephemeral=True, delete_after=10)
self.parent_view.lobby.kicked_players.append(int(interaction.data["values"][0]))
@@ -352,7 +343,7 @@ class _KickSelectMenu(discord.ui.Select):
await self.rerender_players(interaction)
embed = embed_templates.success(interaction, text="Brukeren har blitt sparket fra lobbyen")
embed = embed_templates.success("Brukeren har blitt sparket fra lobbyen")
await interaction.response.send_message(embed=embed, ephemeral=True, delete_after=5)
@@ -438,7 +429,7 @@ class TempVoiceHelper:
except discord.Forbidden:
self.bot.logger.error(f"Failed to create temporary voice channel in {interaction.guild}")
await interaction.response.send_message(
embed=embed_templates.error_fatal(interaction, text="Jeg har ikke tilgang til å opprette en talekanal")
embed=embed_templates.error_fatal("Jeg har ikke tilgang til å opprette en talekanal")
)
return
+3 -29
View File
@@ -1,30 +1,12 @@
import discord
def default_footer(interaction: discord.Interaction, embed: discord.Embed) -> discord.Embed:
"""
Sets a footer containing the command invoker's name, discriminator and avatar
Parameters
-----------
interaction (discord.Interaction): Slash command context object
embed (discord.Embed): An embed object to add the footer to
Returns
-----------
(discord.Embed): The passed in embed + footer
"""
return embed.set_footer(icon_url=interaction.user.avatar, text=interaction.user.name)
def error_warning(interaction: discord.Interaction, text: str) -> discord.Embed:
def error_warning(text: str) -> discord.Embed:
"""
Creates an embed with a specified error message based on a warning template
Parameters
-----------
interaction (discord.Interaction): Slash command context object
text (str): The error message
Returns
@@ -33,18 +15,15 @@ def error_warning(interaction: discord.Interaction, text: str) -> discord.Embed:
"""
embed = discord.Embed(color=discord.Color.gold(), description=f"⚠️ {text}")
default_footer(interaction, embed)
return embed
def error_fatal(interaction: discord.Interaction, text: str) -> discord.Embed:
def error_fatal(text: str) -> discord.Embed:
"""
Creates an embed with a specified error message based on a warning template
Parameters
-----------
interaction (discord.Interaction): Slash command context object
text (str): The error message
Returns
@@ -53,18 +32,15 @@ def error_fatal(interaction: discord.Interaction, text: str) -> discord.Embed:
"""
embed = discord.Embed(color=discord.Color.red(), description=f"❌ {text}")
default_footer(interaction, embed)
return embed
def success(interaction: discord.Interaction, text: str) -> discord.Embed:
def success(text: str) -> discord.Embed:
"""
Creates an embed with a specified message using a template signifying success.
Parameters
-----------
interaction (discord.Interaction): Slash command context object
text (str): The message
Returns
@@ -73,6 +49,4 @@ def success(interaction: discord.Interaction, text: str) -> discord.Embed:
"""
embed = discord.Embed(color=discord.Color.green(), description=f"✅ {text}")
default_footer(interaction, embed)
return embed
+94
View File
@@ -1,8 +1,27 @@
import datetime
import textwrap
from math import ceil
from zoneinfo import ZoneInfo
import cv2
import numpy as np
from PIL import Image
from PIL import ImageDraw
from PIL import ImageFont
MIDNIGHT = datetime.time(hour=19, minute=48, tzinfo=ZoneInfo("Europe/Oslo"))
class Paginator:
"""Interface for managing content. Divides your content into pages of 10 items each"""
def __init__(self, content: list):
"""
Parameters
----------
content (list): The content you want to paginate
"""
self.content = content
self.total_page_count = ceil(len(content) / 10)
self.current_page = 1
@@ -19,6 +38,7 @@ class Paginator:
----------
(list|None): The page you requested
"""
if page < 1 or page > self.total_page_count:
return None
@@ -35,6 +55,7 @@ class Paginator:
----------
(list): The current page of the paginator
"""
return self.get_page(self.current_page)
def next_page(self) -> list | None:
@@ -45,6 +66,7 @@ class Paginator:
----------
(list): The next page of the paginator
"""
self.current_page += 1
return self.get_page(self.current_page)
@@ -56,6 +78,7 @@ class Paginator:
----------
(list): The previous page of the paginator
"""
self.current_page -= 1
return self.get_page(self.current_page)
@@ -67,6 +90,7 @@ class Paginator:
----------
(list): The first page of the paginator
"""
self.current_page = 1
return self.get_page(self.current_page)
@@ -78,5 +102,75 @@ class Paginator:
----------
(list): The last page of the paginator
"""
self.current_page = self.total_page_count
return self.get_page(self.current_page)
async def put_text_in_box(
image: np.ndarray,
text: str,
top_left: tuple,
bottom_right: tuple,
font_path: str = "./src/assets/fonts/impact.ttf",
font_size: int = 10,
color: tuple = (0, 0, 0),
):
"""
Puts text inside a bounding box on the cv2 image with automatic text wrapping, using PIL for text rendering.
Parameters
----------
image (np.ndarray): Source cv2 image.
text (str): Text to be put on the image.
top_left (tuple): (x, y) Top-left corner of the bounding box.
bottom_right (tuple): (x, y) Bottom-right corner of the bounding box.
font_path (str): Path to the font file. Defaults to Impact font.
font_size (int): Initial font size. Defaults to 10.
color (tuple): Text color in RGB. Defaults to (0, 0, 0).
"""
# Convert the cv2 image to a PIL image
image_pil = Image.fromarray(cv2.cvtColor(image, cv2.COLOR_BGR2RGB))
draw = ImageDraw.Draw(image_pil)
# Get coordinates of the bounding box
x1, y1 = top_left
x2, y2 = bottom_right
# Get the width and height of the bounding box
# We multiply by 0.9 to give some padding
w, h = (x2 - x1) * 0.9, (y2 - y1) * 0.9
# Initialize font width/height to 0 in order to run the while loops below at least once
font_width = 0
font_height = 0
# Add newlines to the text. Here we have gone with 15 characters per line
# This is arbitrary, but seems to work well
text = textwrap.fill(text, width=15)
# Increase the font size until the longest line of text does not fit in the bounding box anymore
# We also have to check the height, since the text might be too tall for the box
while font_width < w and font_height < h:
font = ImageFont.truetype(font_path, font_size)
longest_line = max(text.split("\n"), key=lambda x: font.getlength(x))
font_width = font.getlength(longest_line)
# bbox doesn't work with multiline text, so we have to calculate the height manually
# take the height of the bounding box, multiply by 1.5 (our assumed line dividor height)
# after that, we multiply by the number of lines
font_box = font.getbbox(text)
font_height = ((font_box[3] - font_box[1]) * 1.5) * text.count("\n")
font_size += 1
# Actually draw the text
draw.multiline_textbbox((w, h), text=text, font=font, align="center", anchor="mm")
center_of_box = ((x1 + x2) // 2, (y1 + y2) // 2)
draw.multiline_text(center_of_box, text, font=font, fill=color, align="center", anchor="mm")
# Convert back to cv2 image format and update the original image
cv2_image = cv2.cvtColor(np.array(image_pil), cv2.COLOR_RGB2BGR)
np.copyto(image, cv2_image)
+9 -19
View File
@@ -53,9 +53,8 @@ async def fetch_article(title: str):
# This is wildly inefficent but I don't. fucking. care. Just make it work
text = regex.sub(
r"(?=\{)(\{([^{}]|(?1))*\})|\[\[(Kategori|Fil):.+?\]\]|\{.+?\}", "", text, flags=regex.MULTILINE | regex.DOTALL
) # Remove templates and categories
) # Remove templates, categories and images
text = pypandoc.convert_text(text, "markdown", format="mediawiki")
print(text)
text = regex.sub(
r"\[(.+?)\]\(.+? \"wikilink\"\)", r"\1", text, flags=regex.MULTILINE | regex.DOTALL
) # Pandoc adds alt text to links. discord doesn't support that
@@ -107,7 +106,7 @@ class Viteboka(commands.Cog):
viteboka_group = app_commands.Group(name="viteboka", description="Søk i Viteboka etter informasjon")
@app_commands.checks.bot_has_permissions(embed_links=True)
@app_commands.checks.bot_has_permissions(embed_links=True, attach_files=True)
@app_commands.checks.cooldown(1, 10)
@viteboka_group.command(name="søk", description="Søk etter en artikkel i Viteboka")
async def search(self, interaction: discord.Interaction, søkestreng: str):
@@ -131,7 +130,7 @@ class Viteboka(commands.Cog):
response = requests.get(url=API_URL, params=params)
if response.status_code != 200:
self.bot.logger.error(f"Failed to search for articles with query {søkestreng}: {response.status_code}")
embed = embed_templates.error_fatal(interaction, text="Klarte ikke å søke etter artikler")
embed = embed_templates.error_fatal("Klarte ikke å søke etter artikler")
return await interaction.followup.send(embed=embed)
data = response.json()
@@ -139,7 +138,7 @@ class Viteboka(commands.Cog):
if not search_results:
self.bot.logger.info(f"No articles found with query {søkestreng}")
embed = embed_templates.error_fatal(interaction, text="Fant ingen artikler som matcher søket")
embed = embed_templates.error_warning("Fant ingen artikler som matcher søket")
return await interaction.followup.send(embed=embed)
if len(search_results) == 1:
@@ -160,16 +159,13 @@ class Viteboka(commands.Cog):
title, url, text, image = await fetch_article(søkestreng)
except VitebokaException as e:
self.bot.logger.error(f"Failed to fetch article {søkestreng}: {e}")
embed = embed_templates.error_fatal(
interaction,
text=str(e),
)
embed = embed_templates.error_fatal(str(e))
return await interaction.followup.send(embed=embed)
embed = viteboka_embed(title, url, text, image)
await interaction.followup.send(embed=embed)
@app_commands.checks.bot_has_permissions(embed_links=True)
@app_commands.checks.bot_has_permissions(embed_links=True, attach_files=True)
@app_commands.checks.cooldown(1, 5)
@viteboka_group.command(name="tilfeldig", description="Få en tilfeldig artikkel fra Viteboka")
async def random(self, interaction: discord.Interaction):
@@ -194,7 +190,7 @@ class Viteboka(commands.Cog):
response = requests.get(url=API_URL, params=params)
if response.status_code != 200:
self.bot.logger.error(f"Failed to fetch random article: {response.status_code}")
embed = embed_templates.error_fatal(interaction, text="Klarte ikke å søke etter en tilfeldig artikkel")
embed = embed_templates.error_fatal("Klarte ikke å søke etter en tilfeldig artikkel")
return await interaction.followup.send(embed=embed)
data = response.json()
@@ -204,10 +200,7 @@ class Viteboka(commands.Cog):
title, url, text, image = await fetch_article(random_article_title)
except VitebokaException as e:
self.bot.logger.error(f"Failed to fetch article {random_article_title}: {e}")
embed = embed_templates.error_fatal(
interaction,
text=str(e),
)
embed = embed_templates.error_fatal(str(e))
return await interaction.followup.send(embed=embed)
embed = viteboka_embed(title, url, text, image)
@@ -254,10 +247,7 @@ class ArticleButton(discord.ui.Button):
try:
title, url, text, image = await fetch_article(self.article_title)
except VitebokaException as e:
embed = embed_templates.error_fatal(
interaction,
text=str(e),
)
embed = embed_templates.error_fatal(str(e))
return await interaction.followup.send(embed=embed)
else:
embed = viteboka_embed(title, url, text, image)
+7 -6
View File
@@ -8,7 +8,7 @@ from discord.ext import commands
class WebsiteEvents(commands.Cog):
"""Miscellaneous commands that don"t fit anywhere else"""
"""Uploads Discord events to Sanity CMS"""
def __init__(self, bot: commands.Bot):
"""
@@ -57,6 +57,7 @@ class WebsiteEvents(commands.Cog):
self.bot.logger.info("Finished syncing events! Note errors may have occured")
@commands.Cog.listener("on_scheduled_event_create")
async def create_event(self, event: discord.ScheduledEvent):
"""
Creates an event in Sanity CMS when an event is added on Discord
@@ -67,8 +68,9 @@ class WebsiteEvents(commands.Cog):
"""
# Convert timezone to Europe/Oslo
time = event.start_time.replace(tzinfo=pytz.utc) # Make sure the datetime object contains the timezone info
time = event.start_time.astimezone(pytz.timezone("Europe/Oslo"))
# We need to set a timezone unit before being able to convert it to another timezone
# That's why this line is so fucking weird
time = event.start_time.replace(tzinfo=pytz.utc).astimezone(pytz.timezone("Europe/Oslo"))
document = {
"_type": "event",
@@ -94,6 +96,7 @@ class WebsiteEvents(commands.Cog):
else:
self.bot.logger.error(f"Failed to create event in Sanity with ID: {event.id}. Response: {response.text}")
@commands.Cog.listener("on_scheduled_event_delete")
async def delete_event(self, event: discord.ScheduledEvent):
"""
Deletes an event in Sanity CMS when an event is deleted on Discord
@@ -111,6 +114,7 @@ class WebsiteEvents(commands.Cog):
else:
self.bot.logger.error(f"Failed to delete event in Sanity with ID: {event.id}. Response: {response.text}")
@commands.Cog.listener("on_scheduled_event_update")
async def update_event(self, before: discord.ScheduledEvent, after: discord.ScheduledEvent):
"""
Updates an event in Sanity when edited on discord
@@ -141,7 +145,4 @@ async def setup(bot: commands.Bot):
bot (commands.Bot): Bot instance
"""
bot.add_listener(WebsiteEvents(bot).create_event, "on_scheduled_event_create")
bot.add_listener(WebsiteEvents(bot).update_event, "on_scheduled_event_update")
bot.add_listener(WebsiteEvents(bot).delete_event, "on_scheduled_event_delete")
await bot.add_cog(WebsiteEvents(bot))
+91 -54
View File
@@ -20,8 +20,6 @@ from wordcloud import WordCloud as WCloud # Avoid naming conflicts with cog cla
from cogs.utils import embed_templates
# from wordcloud import ImageColorGenerator
class WordCloud(commands.Cog):
"""Generate a wordcloud based on the most frequent words posted"""
@@ -41,12 +39,16 @@ class WordCloud(commands.Cog):
# The user's defaultdict has a default value of 0
self.word_freq_cache = defaultdict(lambda: defaultdict(int))
# We cache the consenting users to avoid querying the database every 16:36
# It's frail but it works unless you have a skill issue
self.consenting_users = []
self.populate_consenting_users()
nltk.download("stopwords")
self.batch_update_word_freqs_loop.start()
self.insert_cache_loop.start()
self.MSG_NO_DATA = "Fant ingen data om deg" # SonarCloud recommended this LMAO. Makes sense but also not
def init_db(self):
"""
@@ -78,8 +80,8 @@ class WordCloud(commands.Cog):
Insert cache to db, stop tasks and close the database connection on cog unload
"""
await self.batch_update_word_freqs()
self.batch_update_word_freqs_loop.cancel()
self.bot.logger.info("Unloading cog")
self.insert_cache_loop.cancel()
self.cursor.close()
def populate_consenting_users(self):
@@ -95,7 +97,7 @@ class WordCloud(commands.Cog):
)
self.consenting_users = [row[0] for row in self.cursor.fetchall()]
async def batch_update_word_freqs(self):
async def insert_cache(self):
"""
Inserts cached word frequencies into the database.
We do this in order to prevent excess database writes
@@ -126,20 +128,25 @@ class WordCloud(commands.Cog):
word_freqs,
)
except psycopg2.Error as err:
self.bot.db_connection.rollback()
self.bot.logger.error(f"Failed to insert wordcloud cache into database - {err}")
self.word_freq_cache.clear()
@tasks.loop(minutes=20)
async def batch_update_word_freqs_loop(self):
async def insert_cache_loop(self):
"""
Clears and inserts word frequency cache into the database every 20 minutes
"""
self.bot.logger.info("Dumping word cloud cache to database...")
await self.batch_update_word_freqs()
await self.insert_cache()
@insert_cache_loop.after_loop
async def on_insert_cache_loop_cancel(self):
if self.insert_cache_loop.is_being_cancelled() and self.word_freq_cache:
await self.insert_cache()
@commands.Cog.listener("on_message")
async def word_freq_listener(self, message: discord.Message):
"""
Listens for all messages of consenting users
@@ -149,17 +156,52 @@ class WordCloud(commands.Cog):
message (discord.Message): Message to count
"""
if message.author.id not in self.consenting_users:
if not self.can_count_message(message):
return
# Ignore bot commands
# This is a very naive approach but it works for our use case
if not message.clean_content[:2].isalpha():
return
tokens = self.tokenize(message.clean_content)
for token in tokens:
# Enter into cache
# This may take a performance hit but who tf cares. We're using python anyway
self.word_freq_cache[message.author.id][token] += 1
def can_count_message(self, message: discord.Message) -> bool:
"""
Check if a message can be counted
Parameters
----------
message (discord.Message): Message to check
Returns
----------
bool: Whether the message can be counted
"""
return (
message.author.id in self.consenting_users
and not message.clean_content[:2].isalpha() # Naive check for traditional bot commands
)
def tokenize(self, text: str) -> list[str]:
"""
Tokenize a text into words
Parameters
----------
text (str): Text to tokenize
Returns
----------
list[str]: List of words
"""
tokens = []
# Divide into words
# Also very naive but luckily we only speak norwegian and english :)
words = message.clean_content.split(" ")
# Very naive but luckily we only speak norwegian and english :)
words = text.split(" ")
for word in words:
# Filter urls
@@ -172,9 +214,9 @@ class WordCloud(commands.Cog):
if not word:
continue
# Enter into cache
# This may take a performance hit but who tf cares. We're using python anyway
self.word_freq_cache[message.author.id][word] += 1
tokens.append(word)
return tokens
@staticmethod
def generate_wordcloud(text: str, max_words: int = 4000, allow_bigrams: bool = False) -> BytesIO:
@@ -193,7 +235,7 @@ class WordCloud(commands.Cog):
"""
filter_words = set(stopwords.words("norwegian") + stopwords.words("english"))
mask = np.array(Image.open("./src/assets/word_cloud/mask.png"))
mask = np.array(Image.open("./src/assets/word_cloud_mask.png"))
wc = WCloud(
max_words=max_words,
@@ -223,6 +265,7 @@ class WordCloud(commands.Cog):
)
@app_commands.checks.bot_has_permissions(embed_links=True)
@app_commands.checks.cooldown(1, 2)
@wordcloud_group.command(name="samtykke", description="Gi samtykke til å samle meldingsdataen din for ordskyer")
async def consent(self, interaction: discord.Interaction):
"""
@@ -235,7 +278,7 @@ class WordCloud(commands.Cog):
if interaction.user.id in self.consenting_users:
return await interaction.response.send_message(
embed=embed_templates.error_warning(interaction, text="Du har allerede samtykket"), ephemeral=False
embed=embed_templates.error_warning("Du har allerede samtykket"), ephemeral=False
)
try:
@@ -250,7 +293,7 @@ class WordCloud(commands.Cog):
self.bot.db_connection.rollback()
self.bot.logger.error(f"Failed to insert wordcloud metadata into database - {err}")
return await interaction.response.send_message(
embed=embed_templates.error_warning(interaction, text="Klarte ikke å skrive til database"),
embed=embed_templates.error_fatal("Klarte ikke å skrive til database"),
ephemeral=False,
)
@@ -260,6 +303,7 @@ class WordCloud(commands.Cog):
await interaction.response.send_message(embed=embed, ephemeral=False)
@app_commands.checks.bot_has_permissions(embed_links=True)
@app_commands.checks.cooldown(1, 2)
@wordcloud_group.command(name="slett", description="Fjern samtykke og slett alle dine ordskydata")
async def consent_remove(self, interaction: discord.Interaction):
"""
@@ -274,7 +318,7 @@ class WordCloud(commands.Cog):
self.consenting_users.remove(interaction.user.id)
except ValueError:
return await interaction.response.send_message(
embed=embed_templates.error_warning(interaction, text="Fant ingen data om deg"), ephemeral=False
embed=embed_templates.error_warning(self.MSG_NO_DATA), ephemeral=False
)
self.word_freq_cache.pop(f"{interaction.user.id}", None)
@@ -291,11 +335,11 @@ class WordCloud(commands.Cog):
self.bot.db_connection.rollback()
self.bot.logger.error(f"Failed to delete wordcloud metadata from database - {err}")
return await interaction.response.send_message(
embed=embed_templates.error_warning(interaction, text="Klarte ikke å slette fra database"),
embed=embed_templates.error_fatal("Klarte ikke å slette fra database"),
ephemeral=False,
)
embed = embed_templates.success(interaction, "Meldingsdata er slettet!")
embed = embed_templates.success("Meldingsdata er slettet!")
await interaction.response.send_message(embed=embed)
@app_commands.checks.bot_has_permissions(embed_links=True, attach_files=True)
@@ -323,16 +367,18 @@ class WordCloud(commands.Cog):
if not result:
return await interaction.response.send_message(
embed=embed_templates.error_warning(interaction, text="Fant ingen data om deg"), ephemeral=False
embed=embed_templates.error_warning(self.MSG_NO_DATA), ephemeral=False
)
freq_list = {f"{word}": freq for word, freq in result}
embed = discord.Embed(description="Her er dataen jeg har lagret om deg")
# Create Json file
buffer = StringIO()
buffer.write(json.dumps(freq_list, indent=4))
buffer.seek(0)
file = discord.File(buffer, filename=f"uiog_word_freqs_{interaction.user.id}.json")
embed = discord.Embed(description="Her er dataen jeg har lagret om deg")
await interaction.response.send_message(embed=embed, file=file, ephemeral=True)
@app_commands.checks.bot_has_permissions(embed_links=True, attach_files=True)
@@ -352,7 +398,7 @@ class WordCloud(commands.Cog):
await interaction.response.defer()
# Clear cache first to ensure correct count
await self.batch_update_word_freqs()
await self.insert_cache()
# Fetch word count from database
self.cursor.execute(
@@ -367,10 +413,12 @@ class WordCloud(commands.Cog):
if not results:
return await interaction.followup.send(
embed=embed_templates.error_warning(interaction, text="Fant ingen data om deg"), ephemeral=False
embed=embed_templates.error_warning(self.MSG_NO_DATA), ephemeral=False
)
# Fetch tracking start time metadata
# This doesn't work most of the time so consider using the API call instead of cache call
# Or consider removing this feature entirely
self.cursor.execute(
"""
SELECT tracked_since_message_channel_id, tracked_since_message_id
@@ -400,13 +448,11 @@ class WordCloud(commands.Cog):
word_cloud_file = discord.File(word_cloud, filename=f"wordcloud_{interaction.user.id}.png")
embed = discord.Embed(title="☁️ Her er ordskyen din! ☁️")
embed.description = "Basert på de 4000 mest frekvente ordene dine siden "
if origin_found:
embed.description = (
f"Basert på de 4000 mest frekvente ordene dine siden {origin_msg_timestamp}\n"
+ f"[Se melding]({origin_msg.jump_url})"
)
embed.description += f"{origin_msg_timestamp}\n[Se melding]({origin_msg.jump_url})"
else:
embed.description = "Basert på de 4000 mest frekvente ordene dine siden `ukjent dato`"
embed.description += "`ukjent dato`"
embed.set_image(url=f"attachment://wordcloud_{interaction.user.id}.png")
await interaction.followup.send(embed=embed, file=word_cloud_file)
@@ -428,9 +474,10 @@ class WordCloud(commands.Cog):
await interaction.response.defer()
# Get member role for future channel permission checking
member_role = interaction.guild.get_role(779849617651138601)
# This shouldn't be hard coded but eh
board_member_role = interaction.guild.get_role(779849617651138601)
# Fetch last 1000 messages
# This is horrible but there's a lot of edge cases to handle so spare me pls
all_messages = ""
for channel in interaction.guild.text_channels:
if not channel.permissions_for(interaction.user).send_messages:
@@ -438,36 +485,27 @@ class WordCloud(commands.Cog):
# Avoid reading secret board member chats
if (
not channel.permissions_for(member_role).send_messages
board_member_role # None if invoked outside of UiO Gaming's server. Avoiding crash due to AND check
and not channel.permissions_for(board_member_role).send_messages
and not channel.permissions_for(interaction.guild.default_role).send_messages
):
continue
try:
async for message in channel.history(
limit=int(antall / 2)
): # Limit search in each channel to half of requested
if message.author != interaction.user:
# Limit search to half of requested messages to avoid rate limits
# We assume we can still fetch enough tokens to reach the requested amount
async for message in channel.history(limit=int(antall / 2)):
if not self.can_count_message(message):
continue
# Filter bot command messages
if not message.clean_content[:2].isalpha():
continue
# Filter URLs
filtered_msg = re.sub(
r"https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)", # noqa: E501
"",
message.clean_content,
)
filtered_msg = " ".join(self.tokenize(message.clean_content))
all_messages += f"{filtered_msg} "
except discord.errors.Forbidden:
continue
if not all_messages:
return await interaction.followup.send(
embed=embed_templates.error_warning(interaction, text="Fant ingen data om deg"), ephemeral=False
embed=embed_templates.error_warning(self.MSG_NO_DATA), ephemeral=False
)
# Generate word cloud
@@ -495,5 +533,4 @@ async def setup(bot: commands.Bot):
bot (commands.Bot): Bot instance
"""
bot.add_listener(WordCloud(bot).word_freq_listener, "on_message")
await bot.add_cog(WordCloud(bot))
+38 -48
View File
@@ -41,77 +41,67 @@ class Bot(commands.Bot):
self.logger = BotLogger().logger # Initialize logger
self.guild_id = config.get("dev_guild", UIO_GAMING_GUILD_ID)
self.cog_files = set(listdir("./src/cogs"))
# Connect to database
# We assume the config file is valid and contains the needed keys, even if they are empty
db = config.get("database")
for db_credential in db.values():
if not db_credential:
self.db_connection = None
self.logger.warning(
f"No database credentials specified. Disabling db reliant cogs:\n{DATABASE_RELIANT_COGS}"
)
break
else:
# Check for missing credentials
if self.check_credentials(config["database"], DATABASE_RELIANT_COGS):
self.db_connection = psycopg2.connect(
host=db["host"],
dbname=db["dbname"],
user=db["username"],
password=db["password"],
host=config["database"]["host"],
dbname=config["database"]["dbname"],
user=config["database"]["username"],
password=config["database"]["password"],
)
self.db_connection.autocommit = True
self.db_connection.autocommit = True # Scary
for sanity_credential in config.get("sanity").values():
if not sanity_credential:
self.has_sanity_credentials = False
self.logger.warning(
f"No Sanity credentials specified. Disabling sanity reliant cogs\n{SANITY_RELIANT_COGS}"
)
break
else:
self.has_sanity_credentials = True
if self.check_credentials(config["sanity"], SANITY_RELIANT_COGS):
self.sanity = config["sanity"]
if not config.get("minecraft").get("rcon_password"):
self.has_minecraft_credentials = False
self.logger.warning(
f"No Minecraft credentials specified. Disabling minecraft reliatn cogs\n{MINECRAFT_RELIANT_COGS}"
)
else:
self.has_minecraft_credentials = True
if self.check_credentials(config["minecraft"], MINECRAFT_RELIANT_COGS):
self.mc_rcon_password = config["minecraft"]["rcon_password"]
# Fetch misc config values
self.mc_rcon_password = config["minecraft"]["rcon_password"]
self.sanity = config["sanity"]
self.UIO_GAMING_GUILD_ID = UIO_GAMING_GUILD_ID
self.guild_id = config.get("dev_guild", self.UIO_GAMING_GUILD_ID)
self.config_mode = config.get("config_mode")
self.presence = config["bot"].get("presence", {})
self.emoji = config.get("emoji", {})
self.misc = config.get("misc", {})
self.config_mode = config.get("config_mode")
async def setup_hook(self):
cog_files = listdir("./src/cogs")
if self.db_connection is None:
cog_files = set(cog_files) - DATABASE_RELIANT_COGS
if not self.has_sanity_credentials:
cog_files = set(cog_files) - SANITY_RELIANT_COGS
if not self.has_minecraft_credentials:
cog_files = set(cog_files) - MINECRAFT_RELIANT_COGS
# Load cogs
for file in cog_files:
for file in self.cog_files:
if file.endswith(".py"):
name = file[:-3]
await bot.load_extension(f"cogs.{name}")
# Sync slash commands to Discord
# Sync slash commands
if self.config_mode == "prod":
await self.tree.sync()
else:
self.tree.copy_global_to(guild=discord.Object(id=self.guild_id))
await self.tree.sync(guild=discord.Object(id=self.guild_id))
def check_credentials(self, credentials: dict, dependant_cogs: set[str]) -> bool:
"""
Check if the credentials are valid. Removes cogs from the class' `cog_files` attribute if not
Parameters
-----------
credentials (dict): The credentials to check
dependant_cogs (set): The cogs that depend on the credentials
Returns
----------
bool: Whether the credentials are valid. If not, the dependant cogs are disabled.
"""
for credential in credentials.values():
if not credential:
self.logger.warning(f"Missing credentials. Disabling reliant cogs:\n{dependant_cogs}")
self.cog_files -= dependant_cogs
return False
return True
# Create bot instance
bot = Bot()