From a11f9d841a6eb6aa24577e770343db8b7029ec92 Mon Sep 17 00:00:00 2001 From: robertsokola Date: Thu, 1 Jun 2023 23:41:54 +0200 Subject: [PATCH 01/35] Ruff fixes Signed-off-by: robertsokola --- cogs/error.py | 27 +++++++++++------ cogs/morserovka.py | 2 +- cogs/poll_command.py | 27 ++++++++++++----- cogs/sync_command.py | 14 ++++----- cogs/utility.py | 21 ++++++++----- main.py | 4 ++- pyproject.toml | 11 +++++++ readme.md | 2 +- src/__init__.py | 0 src/db_folder/databases.py | 49 ++++++++++++------------------- src/helpers.py | 5 ++-- src/jachym.py | 23 +++++++++------ src/ui/__init__.py | 0 src/ui/button.py | 44 +++++++++++++-------------- src/ui/embeds.py | 24 +++++++++------ src/ui/poll.py | 29 +++++++++--------- src/ui/poll_view.py | 5 ---- tests/manual_testing_generator.py | 3 +- 18 files changed, 162 insertions(+), 128 deletions(-) create mode 100644 pyproject.toml create mode 100644 src/__init__.py create mode 100644 src/ui/__init__.py diff --git a/cogs/error.py b/cogs/error.py index 73e5b8e..5d4031d 100644 --- a/cogs/error.py +++ b/cogs/error.py @@ -10,26 +10,35 @@ def __init__(self, bot): self.bot = bot self.logger = logging.getLogger("discord") - self.logger.setLevel(logging.WARN) - handler = logging.FileHandler(filename="discord.log", encoding="utf-8", mode="w") - handler.setFormatter(logging.Formatter("%(asctime)s:%(levelname)s:%(name)s: %(message)s")) + handler = logging.FileHandler( + filename="discord.log", + encoding="utf-8", + mode="w", + ) + handler.setFormatter( + logging.Formatter("%(asctime)s:%(levelname)s:%(name)s: %(message)s"), + ) self.logger.addHandler(handler) @commands.Cog.listener() - async def on_command_error(self, ctx: commands.Context, error: commands.CommandError): + async def on_command_error( + self, + ctx: commands.Context, + error: commands.CommandError, + ): match error: - case commands.ExpectedClosingQuoteError(): - return await ctx.send(f"Pozor! Chybí tady: {error} uvozovka!") - case commands.MissingPermissions(): return await ctx.send("Chybí ti požadovaná práva!") case commands.CommandNotFound(): - pass + return None case _: - self.logger.critical(f"{ctx.message.id}, {ctx.message.content} | {error}") + self.logger.critical( + f"{ctx.message.id}, {ctx.message.content} | {error}", + ) print(error) + return None @commands.Cog.listener() async def on_command(self, ctx: commands.Context): diff --git a/cogs/morserovka.py b/cogs/morserovka.py index 8c40813..f84e6cc 100644 --- a/cogs/morserovka.py +++ b/cogs/morserovka.py @@ -69,7 +69,7 @@ def __init__(self, bot): async def zasifruj(self, interaction: discord.Interaction, message: str) -> Message: cipher = "" for letter in message: - if letter.upper() in self.MORSE_CODE_DICT.keys(): + if letter.upper() in self.MORSE_CODE_DICT: cipher += self.MORSE_CODE_DICT[letter.upper()] + "/" else: return await interaction.response.send_message( diff --git a/cogs/poll_command.py b/cogs/poll_command.py index 5a5354c..302de02 100644 --- a/cogs/poll_command.py +++ b/cogs/poll_command.py @@ -15,8 +15,7 @@ def error_handling(answer: list[str]) -> str: if len(answer) > Poll.MAX_OPTIONS: return f"Zadal jsi příliš mnoho odpovědí, můžeš maximálně {Poll.MAX_OPTIONS}!" - elif len(answer) < Poll.MIN_OPTIONS: - return f"Zadal jsi příliš málo odpovědí, můžeš alespoň {Poll.MIN_OPTIONS}!" + return f"Zadal jsi příliš málo odpovědí, můžeš alespoň {Poll.MIN_OPTIONS}!" class PollCreate(commands.Cog): @@ -26,7 +25,7 @@ class PollCreate(commands.Cog): "question": "Otázka, na kterou potřebuješ vědět odpověď", "answer": 'Odpovědi, rozděluješ odpovědi uvozovkou ("), maximálně pouze 10 možností', "help": """ - Jednoduchá anketa, která obsahuje otázku a odpovědi. Povoleno je 10 možností. + Jednoduchá anketa, která obsahuje otázku a odpovědi. Povoleno je 10 možností. """, } @@ -36,17 +35,31 @@ class PollCreate(commands.Cog): def __init__(self, bot: Jachym): self.bot = bot - @app_commands.command(name="anketa", description="Anketa pro hlasování. Jsou vidět všichni hlasovatelé.") + @app_commands.command( + name="anketa", + description="Anketa pro hlasování. Jsou vidět všichni hlasovatelé.", + ) @app_commands.rename(question="otázka", answer="odpovědi") @app_commands.describe( question="Otázka, kterou chceš položit.", answer='Odpovědi, rozděluješ odpovědi uvozovkou ("), maximálně pouze 10 možností', ) - async def pool(self, interaction: discord.Interaction, question: str, answer: str) -> discord.Message: - await interaction.response.send_message(embed=PollEmbedBase("Dělám na tom, vydrž!")) + async def pool( + self, + interaction: discord.Interaction, + question: str, + answer: str, + ) -> discord.Message: + await interaction.response.send_message( + embed=PollEmbedBase("Dělám na tom, vydrž!"), + ) message = await interaction.original_response() - answers = re.split("|".join(self.REGEX_PATTERN), answer) + answers = [ + answer + for answer in re.split("|".join(self.REGEX_PATTERN), answer) + if answer.strip() + ] if error_handling(answers): return await message.edit(embed=PollEmbedBase(error_handling(answers))) diff --git a/cogs/sync_command.py b/cogs/sync_command.py index 33b6813..7789b38 100644 --- a/cogs/sync_command.py +++ b/cogs/sync_command.py @@ -1,8 +1,8 @@ -from typing import Literal, Optional, TYPE_CHECKING +from typing import TYPE_CHECKING, Literal import discord from discord.ext import commands -from discord.ext.commands import Greedy, Context +from discord.ext.commands import Context, Greedy if TYPE_CHECKING: from src.jachym import Jachym @@ -16,10 +16,10 @@ def __init__(self, bot: "Jachym"): @commands.guild_only() @commands.is_owner() async def sync( - self, - ctx: Context, - guilds: Greedy[discord.Guild], - spec: Optional[Literal["-", "*", "^"]] = None, + self, + ctx: Context, + guilds: Greedy[discord.Guild], + spec: Literal["-", "*", "^"] | None = None, ) -> None: """ A command to sync all slash commands to servers user requires. Works like this: @@ -57,7 +57,7 @@ async def sync( synced = await self.bot.tree.sync() await ctx.send( - f"Synced {len(synced)} commands {'globally' if spec is None else 'to the current guild.'}" + f"Synced {len(synced)} commands {'globally' if spec is None else 'to the current guild.'}", ) return diff --git a/cogs/utility.py b/cogs/utility.py index a565324..724d8fc 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -14,12 +14,15 @@ def __init__(self, bot): self.bot = bot @app_commands.command( - name="pomoc", description="Pomocníček, který ti pomůže s různými věcmi." + name="pomoc", + description="Pomocníček, který ti pomůže s různými věcmi.", ) async def pomoc(self, interaction: discord.Interaction) -> Message: embed = EmbedFromJSON().add_fields_from_json("help") return await interaction.response.send_message( - embed=embed, file=EmbedFromJSON.PICTURE, ephemeral=True + embed=embed, + file=EmbedFromJSON.PICTURE, + ephemeral=True, ) @app_commands.command( @@ -29,7 +32,9 @@ async def pomoc(self, interaction: discord.Interaction) -> Message: async def rozcestnik(self, interaction: discord.Interaction) -> Message: embed = EmbedFromJSON().add_fields_from_json("rozcestnik") return await interaction.response.send_message( - embed=embed, file=EmbedFromJSON.PICTURE, ephemeral=True + embed=embed, + file=EmbedFromJSON.PICTURE, + ephemeral=True, ) @app_commands.command( @@ -54,10 +59,9 @@ async def clear(self, ctx: commands.Context, limit: int) -> Message: if 1 < limit < 100: deleted = await ctx.channel.purge(limit=limit) return await ctx.send( - "Smazáno {deleted} zpráv.".format(deleted=len(deleted)) + f"Smazáno {len(deleted)} zpráv.", ) - else: - return await ctx.send("Limit musí být někde mezi 1 nebo 99!") + return await ctx.send("Limit musí být někde mezi 1 nebo 99!") @commands.command() async def time(self, ctx: commands.Context): @@ -67,11 +71,12 @@ async def time(self, ctx: commands.Context): async def birthday(self, ctx): today = datetime.date.today() bot_birthday = datetime.datetime.strptime( - self.bot.MY_BIRTHDAY, "%d.%m.%Y" + self.bot.MY_BIRTHDAY, + "%d.%m.%Y", ).replace(year=today.year) days_until_birthday = (bot_birthday.date() - today).days await ctx.send( - f"Moje narozeniny jsou 27. prosince 2020 a zbývá přesně {days_until_birthday} dní do mých narozenin!" + f"Moje narozeniny jsou 27. prosince 2020 a zbývá přesně {days_until_birthday} dní do mých narozenin!", ) diff --git a/main.py b/main.py index 846b86c..fca8ff6 100644 --- a/main.py +++ b/main.py @@ -1,12 +1,14 @@ import asyncio from os import getenv + from dotenv import load_dotenv + from src.jachym import Jachym load_dotenv("password.env") -async def main(): +async def main() -> None: bot = Jachym() async with bot: await bot.load_extensions() diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..cfd22d2 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,11 @@ +[tool.ruff] + +select = ["ALL"] + +ignore = [ + "D", # Ignore docstrings + "E501", # Line too long, let Black handle this + "ANN", # typing, let mypy handle tis + "INP001" # Namespace package eeeeh +] + diff --git a/readme.md b/readme.md index 4d11bfb..404abb0 100644 --- a/readme.md +++ b/readme.md @@ -1,4 +1,4 @@ - +r

Logo Jáchyma
diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/db_folder/databases.py b/src/db_folder/databases.py index 4ac343a..32ebce9 100644 --- a/src/db_folder/databases.py +++ b/src/db_folder/databases.py @@ -1,17 +1,15 @@ -from abc import ABC -from typing import TYPE_CHECKING, AsyncIterator, Optional +from collections.abc import AsyncIterator import aiomysql import discord.errors from discord import Message +from loguru import logger +from src.jachym import Jachym from src.ui.poll import Poll -if TYPE_CHECKING: - from ..jachym import Jachym - -class Crud(ABC): +class Crud: def __init__(self, poll: aiomysql.pool.Pool): self.poll = poll @@ -21,21 +19,21 @@ async def commit_value(self, sql: str, value: tuple) -> None: await cursor.execute(sql, value) await conn.commit() - async def commit_many_values(self, sql: str, values: list[tuple]) -> None: + async def commit_many_values(self, sql: str, values: list[tuple[int, str]]) -> None: async with self.poll.acquire() as conn: cursor = await conn.cursor() await cursor.executemany(sql, values) await conn.commit() async def fetch_all_values( - self, sql: str, value: Optional[tuple] = None + self, + sql: str, + value: tuple | None = None, ) -> list[tuple]: async with self.poll.acquire() as conn: cursor = await conn.cursor() await cursor.execute(sql, value) - values = await cursor.fetchall() - - return values + return await cursor.fetchall() class PollDatabase(Crud): @@ -68,24 +66,21 @@ async def fetch_all_answers(self, message_id) -> list[str]: value = (message_id,) tuple_of_tuples_db = await self.fetch_all_values(sql, value) + return [answer for tupl in tuple_of_tuples_db for answer in tupl] - answers = [answer for tupl in tuple_of_tuples_db for answer in tupl] - - return answers - - async def fetch_all_polls(self, bot: "Jachym") -> AsyncIterator[Poll | Message]: + async def fetch_all_polls(self, bot: Jachym) -> AsyncIterator[Poll and Message]: sql = "SELECT * FROM `Poll`" polls = await self.fetch_all_values(sql) for message_id, channel_id, question, date, _ in polls: try: message = await bot.get_partial_messageable(channel_id).fetch_message( - message_id + message_id, ) except (discord.errors.NotFound, discord.errors.Forbidden): await self.remove(message_id) - print(f"Removed a Pool: {message_id, question}") + logger.warning(f"Removed a Pool: {message_id, question}") continue options = await self.fetch_all_answers(message_id) @@ -111,31 +106,23 @@ async def add_options(self, discord_poll: Poll): (discord_poll.message_id, vote_option) for vote_option in discord_poll.options ] - await self.commit_many_values(sql, values) - async def add_user(self, message_id: Poll.message_id, user: int, index: int): + async def add_user(self, message_id: Poll, user: int, index: int): sql = "INSERT INTO `Answers`(message_id, vote_user, iter_index) VALUES (%s, %s, %s)" values = (message_id, user, index) - await self.commit_value(sql, values) - async def remove_user(self, message_id: Poll.message_id, user, index): + async def remove_user(self, message_id: Poll, user: int, index: int): sql = "DELETE FROM `Answers` WHERE message_id = %s AND vote_user = %s AND iter_index = %s" value = (message_id, user, index) - await self.commit_value(sql, value) - async def fetch_all_users(self, message_id: Poll.message_id, index) -> set[int]: + async def fetch_all_users(self, poll: Poll, index: int) -> set[int]: sql = ( "SELECT vote_user FROM `Answers` WHERE message_id = %s AND iter_index = %s" ) - values = (message_id, index) + values = (poll.message_id, index) users_voted_for = await self.fetch_all_values(sql, values) - - clean_users_voted_for = set( - user for user_tuple in users_voted_for for user in user_tuple - ) - - return clean_users_voted_for + return {user for user_tuple in users_voted_for for user in user_tuple} diff --git a/src/helpers.py b/src/helpers.py index 75a260f..b3d5129 100644 --- a/src/helpers.py +++ b/src/helpers.py @@ -1,12 +1,13 @@ import time +from collections.abc import Callable from functools import wraps from loguru import logger -def timeit(func: callable): +def timeit(func: Callable): @wraps(func) - async def async_wrapper(*args, **kwargs): + async def async_wrapper(*args, **kwargs) -> Callable: logger.info(f"{func.__name__} starting...") start = time.time() result = await func(*args, **kwargs) diff --git a/src/jachym.py b/src/jachym.py index a05ca3a..37f0781 100644 --- a/src/jachym.py +++ b/src/jachym.py @@ -1,18 +1,22 @@ from os import getenv, listdir -from typing import Optional +from typing import TYPE_CHECKING -import aiomysql.pool import discord from aiomysql import create_pool from discord.ext import commands +from discord.ext.commands import ExtensionFailed, ExtensionNotFound from dotenv import load_dotenv from loguru import logger from src.db_folder.databases import PollDatabase from src.helpers import timeit -from src.ui.poll import Poll from src.ui.poll_view import PollView +if TYPE_CHECKING: + import aiomysql.pool + + from src.ui.poll import Poll + load_dotenv("password.env") @@ -22,7 +26,7 @@ class Jachym(commands.Bot): def __init__(self) -> None: # https://discordpy.readthedocs.io/en/stable/intents.html - self.pool: Optional[aiomysql.pool.Pool] = None + self.pool: aiomysql.pool.Pool | None = None self.active_discord_polls: set[Poll] = set() super().__init__( @@ -37,7 +41,7 @@ async def _fetch_pools_from_database(self) -> None: async for poll, message in poll_database.fetch_all_polls(self): self.add_view( - PollView(poll=poll, embed=message.embeds[0], db_poll=self.pool) + PollView(poll=poll, embed=message.embeds[0], db_poll=self.pool), ) self.active_discord_polls.add(poll) @@ -47,17 +51,18 @@ async def set_presence(self) -> None: activity_name = f"Jsem na {len(self.guilds)} serverech a mám spuštěno {len(self.active_discord_polls)} anket!" await self.change_presence(activity=discord.Game(name=activity_name)) - async def load_extensions(self): + async def load_extensions(self) -> None: for filename in listdir("cogs/"): if filename.endswith(".py"): try: await self.load_extension(f"cogs.{filename[:-3]}") logger.success(f"{filename[:-3]} has loaded successfully") - except Exception as error: + except (ExtensionNotFound, ExtensionFailed) as error: logger.error(error) - async def setup_hook(self): + async def setup_hook(self) -> None: logger.info("Getting setup ready...") + self.pool = await create_pool( user=getenv("USER_DATABASE"), password=getenv("PASSWORD"), @@ -72,6 +77,6 @@ async def setup_hook(self): logger.success("Setup ready!") @commands.Cog.listener() - async def on_ready(self): + async def on_ready(self) -> None: await self.set_presence() logger.success("Bot online!") diff --git a/src/ui/__init__.py b/src/ui/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/ui/button.py b/src/ui/button.py index e0d92fc..f09c9a3 100644 --- a/src/ui/button.py +++ b/src/ui/button.py @@ -11,18 +11,22 @@ class ButtonBackend(discord.ui.Button): Button class to edit a poll embed with """ + LENTGH_STRING = 30 + def __init__( - self, - custom_id: str, - poll: Poll, - emoji: str, - embed: PollEmbed, - index: int, - label: str, - db_poll: aiomysql.pool.Pool, + self, + custom_id: str, + poll: Poll, + emoji: str, + embed: PollEmbed, + index: int, + label: str, + db_poll: aiomysql.pool.Pool, ) -> None: super().__init__( - label=label if len(label) <= 30 else "", emoji=emoji, custom_id=custom_id + label=label if len(label) <= self.LENTGH_STRING else "", + emoji=emoji, + custom_id=custom_id, ) self.poll = poll self.embed = embed @@ -37,22 +41,18 @@ async def toggle_vote(self, interaction: discord.Interaction) -> set[str]: vote_button_db = VoteButtonDatabase(self.db_poll) user = interaction.user.id - users_ID = await vote_button_db.fetch_all_users( - self.poll.message_id, self.index - ) + users_id = await vote_button_db.fetch_all_users(self.poll, self.index) - if user not in users_ID: - await vote_button_db.add_user(self.poll.message_id, user, self.index) - users_ID.add(user) + if user not in users_id: + await vote_button_db.add_user(self.poll, user, self.index) + users_id.add(user) else: - await vote_button_db.remove_user(self.poll.message_id, user, self.index) - users_ID.remove(user) - - members = set( - interaction.guild.get_member(user_id).display_name for user_id in users_ID - ) + await vote_button_db.remove_user(self.poll, user, self.index) + users_id.remove(user) - return members + return { + interaction.guild.get_member(user_id).display_name for user_id in users_id + } async def edit_embed(self, members: set[str]) -> discord.Embed: return self.embed.set_field_at( diff --git a/src/ui/embeds.py b/src/ui/embeds.py index a0c3305..1280e04 100644 --- a/src/ui/embeds.py +++ b/src/ui/embeds.py @@ -11,7 +11,10 @@ class CooldownErrorEmbed(discord.Embed): def __init__(self, seconds: float): self.seconds = round(seconds) - formatted_date = discord.utils.format_dt(datetime.now() + timedelta(seconds=10), "R") + formatted_date = discord.utils.format_dt( + datetime.now() + timedelta(seconds=10), + "R", + ) super().__init__( title=f"⚠️ Vydrž! Další anketu můžeš založit {formatted_date}! ⚠️", @@ -21,10 +24,9 @@ def __init__(self, seconds: float): def correct_czech_writing(self) -> str: if self.seconds > 4: return f"{self.seconds} sekund" - elif 4 >= self.seconds > 1: + if 4 >= self.seconds > 1: return f"{self.seconds} sekundy" - else: - return "sekundu" + return "sekundu" class PollEmbedBase(discord.Embed): @@ -43,7 +45,11 @@ def __init__(self, poll: Poll): def _add_options(self): for index, option in enumerate(self.answers): - self.add_field(name=f"{self.REACTIONS[index]} {option}", value="**0** |", inline=False) + self.add_field( + name=f"{self.REACTIONS[index]} {option}", + value="**0** |", + inline=False, + ) def _add_timestamp(self): self.add_field( @@ -62,8 +68,8 @@ def __init__(self): @classmethod def add_fields_from_json(cls, root_path): - with open(cls.PATH, "r") as f: + with pathlib.Path.open(cls.PATH) as f: text = json.load(f)[root_path] - em = EmbedFromJSON().from_dict(text) - em.set_thumbnail(url="attachment://LogoPotkani.png") - return em + em = EmbedFromJSON().from_dict(text) + em.set_thumbnail(url="attachment://LogoPotkani.png") + return em diff --git a/src/ui/poll.py b/src/ui/poll.py index 2daea56..397cf3b 100644 --- a/src/ui/poll.py +++ b/src/ui/poll.py @@ -1,5 +1,4 @@ -from datetime import datetime -from typing import Optional, Union +from datetime import date, datetime class Poll: @@ -20,21 +19,23 @@ class Poll: ] def __init__( - self, - message_id: int, - channel_id: int, - question: str, - options: list[str], - user_id: Optional[int] = None, - date_created: Optional[Union[datetime, str]] = datetime.now().strftime( - "%Y-%m-%d" - ), + self, + message_id: int, + channel_id: int, + question: str, + options: list[str], + user_id: int | None = None, + date_created: datetime | None = None, ): self._message_id = message_id self._channel_id = channel_id self._question = question self._options = options - self._date_created_at = date_created + self._date_created_at = ( + datetime.now().strftime("%Y-%m-%d") + if date_created is None + else date_created + ) self._user_id = user_id @property @@ -54,7 +55,7 @@ def options(self) -> list[str]: return self._options @property - def created_at(self) -> datetime.date: + def created_at(self) -> datetime | date | None: if isinstance(self._date_created_at, str): return datetime.fromisoformat(self._date_created_at).date() if isinstance(self._date_created_at, datetime): @@ -62,5 +63,5 @@ def created_at(self) -> datetime.date: return self._date_created_at @property - def user_id(self) -> int: + def user_id(self) -> int | None: return self._user_id diff --git a/src/ui/poll_view.py b/src/ui/poll_view.py index 33da530..2095353 100644 --- a/src/ui/poll_view.py +++ b/src/ui/poll_view.py @@ -1,4 +1,3 @@ -import datetime import aiomysql.pool import discord @@ -29,7 +28,3 @@ def add_buttons(self): ) self.add_item(button) - - def check_date_difference(self) -> bool: - date_deletion = self.poll.created_at + datetime.timedelta(days=25) - return datetime.date.today() > date_deletion diff --git a/tests/manual_testing_generator.py b/tests/manual_testing_generator.py index 834b5a9..aa6b6e8 100644 --- a/tests/manual_testing_generator.py +++ b/tests/manual_testing_generator.py @@ -1,5 +1,4 @@ -from datetime import datetime -from datetime import timedelta +from datetime import datetime, timedelta """ I'd like to test using the pytest, unfortunately I have to use manual testing through discord to check the correct From cae11de4fcae8d2aa3f10f814f65edf4df60f787 Mon Sep 17 00:00:00 2001 From: robertsokola Date: Mon, 5 Jun 2023 22:47:48 +0200 Subject: [PATCH 02/35] Some more Ruff fixes Signed-off-by: robertsokola --- cogs/poll_command.py | 12 +++++++----- readme.md | 1 - src/db_folder/databases.py | 7 +++++-- src/ui/button.py | 16 ++++++++-------- 4 files changed, 20 insertions(+), 16 deletions(-) diff --git a/cogs/poll_command.py b/cogs/poll_command.py index 302de02..92925da 100644 --- a/cogs/poll_command.py +++ b/cogs/poll_command.py @@ -15,7 +15,8 @@ def error_handling(answer: list[str]) -> str: if len(answer) > Poll.MAX_OPTIONS: return f"Zadal jsi příliš mnoho odpovědí, můžeš maximálně {Poll.MAX_OPTIONS}!" - return f"Zadal jsi příliš málo odpovědí, můžeš alespoň {Poll.MIN_OPTIONS}!" + if len(answer) < Poll.MIN_OPTIONS: + return f"Zadal jsi příliš málo odpovědí, můžeš alespoň {Poll.MIN_OPTIONS}!" class PollCreate(commands.Cog): @@ -45,16 +46,17 @@ def __init__(self, bot: Jachym): answer='Odpovědi, rozděluješ odpovědi uvozovkou ("), maximálně pouze 10 možností', ) async def pool( - self, - interaction: discord.Interaction, - question: str, - answer: str, + self, + interaction: discord.Interaction, + question: str, + answer: str, ) -> discord.Message: await interaction.response.send_message( embed=PollEmbedBase("Dělám na tom, vydrž!"), ) message = await interaction.original_response() + # bugfix for answers that were empty answers = [ answer for answer in re.split("|".join(self.REGEX_PATTERN), answer) diff --git a/readme.md b/readme.md index 404abb0..8e17c8c 100644 --- a/readme.md +++ b/readme.md @@ -1,4 +1,3 @@ -r

Logo Jáchyma
diff --git a/src/db_folder/databases.py b/src/db_folder/databases.py index 32ebce9..240fef7 100644 --- a/src/db_folder/databases.py +++ b/src/db_folder/databases.py @@ -1,11 +1,14 @@ from collections.abc import AsyncIterator +from typing import TYPE_CHECKING import aiomysql import discord.errors from discord import Message from loguru import logger -from src.jachym import Jachym +if TYPE_CHECKING: + from src.jachym import Jachym + from src.ui.poll import Poll @@ -68,7 +71,7 @@ async def fetch_all_answers(self, message_id) -> list[str]: tuple_of_tuples_db = await self.fetch_all_values(sql, value) return [answer for tupl in tuple_of_tuples_db for answer in tupl] - async def fetch_all_polls(self, bot: Jachym) -> AsyncIterator[Poll and Message]: + async def fetch_all_polls(self, bot: "Jachym") -> AsyncIterator[Poll and Message]: sql = "SELECT * FROM `Poll`" polls = await self.fetch_all_values(sql) diff --git a/src/ui/button.py b/src/ui/button.py index f09c9a3..51ce2f3 100644 --- a/src/ui/button.py +++ b/src/ui/button.py @@ -14,14 +14,14 @@ class ButtonBackend(discord.ui.Button): LENTGH_STRING = 30 def __init__( - self, - custom_id: str, - poll: Poll, - emoji: str, - embed: PollEmbed, - index: int, - label: str, - db_poll: aiomysql.pool.Pool, + self, + custom_id: str, + poll: Poll, + emoji: str, + embed: PollEmbed, + index: int, + label: str, + db_poll: aiomysql.pool.Pool, ) -> None: super().__init__( label=label if len(label) <= self.LENTGH_STRING else "", From 80808c29df3856412df6d3ebe738cd4c00701dd3 Mon Sep 17 00:00:00 2001 From: robertsokola Date: Mon, 5 Jun 2023 23:16:47 +0200 Subject: [PATCH 03/35] bugfix where database couldn't work Signed-off-by: robertsokola --- pyproject.toml | 4 +++- src/db_folder/databases.py | 8 ++++---- src/ui/embeds.py | 6 ++++-- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index cfd22d2..a03c7d5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,6 +6,8 @@ ignore = [ "D", # Ignore docstrings "E501", # Line too long, let Black handle this "ANN", # typing, let mypy handle tis - "INP001" # Namespace package eeeeh + "INP001", # Namespace package eeeeh + "PLR0913", #Too many args to func call + "DTZ005", # datetime object without "tz" is not allowed ] diff --git a/src/db_folder/databases.py b/src/db_folder/databases.py index 240fef7..c30decf 100644 --- a/src/db_folder/databases.py +++ b/src/db_folder/databases.py @@ -111,14 +111,14 @@ async def add_options(self, discord_poll: Poll): ] await self.commit_many_values(sql, values) - async def add_user(self, message_id: Poll, user: int, index: int): + async def add_user(self, discord_poll: Poll, user: int, index: int): sql = "INSERT INTO `Answers`(message_id, vote_user, iter_index) VALUES (%s, %s, %s)" - values = (message_id, user, index) + values = (discord_poll.message_id, user, index) await self.commit_value(sql, values) - async def remove_user(self, message_id: Poll, user: int, index: int): + async def remove_user(self, discord_poll: Poll, user: int, index: int): sql = "DELETE FROM `Answers` WHERE message_id = %s AND vote_user = %s AND iter_index = %s" - value = (message_id, user, index) + value = (discord_poll.message_id, user, index) await self.commit_value(sql, value) async def fetch_all_users(self, poll: Poll, index: int) -> set[int]: diff --git a/src/ui/embeds.py b/src/ui/embeds.py index 1280e04..add4c01 100644 --- a/src/ui/embeds.py +++ b/src/ui/embeds.py @@ -9,6 +9,8 @@ class CooldownErrorEmbed(discord.Embed): + LIMIT = 4 + def __init__(self, seconds: float): self.seconds = round(seconds) formatted_date = discord.utils.format_dt( @@ -22,9 +24,9 @@ def __init__(self, seconds: float): ) def correct_czech_writing(self) -> str: - if self.seconds > 4: + if self.seconds > self.LIMIT: return f"{self.seconds} sekund" - if 4 >= self.seconds > 1: + if self.LIMIT >= self.seconds > 1: return f"{self.seconds} sekundy" return "sekundu" From b95a0a0e07a6b9ae637f0fe064eaa5d81d522b07 Mon Sep 17 00:00:00 2001 From: robertsokola Date: Mon, 5 Jun 2023 23:25:35 +0200 Subject: [PATCH 04/35] commit hooks Signed-off-by: robertsokola --- .github/workflows/pre-commit-config.yml | 5 +++++ .github/workflows/pre-commit.yml | 14 ++++++++++++++ 2 files changed, 19 insertions(+) create mode 100644 .github/workflows/pre-commit-config.yml create mode 100644 .github/workflows/pre-commit.yml diff --git a/.github/workflows/pre-commit-config.yml b/.github/workflows/pre-commit-config.yml new file mode 100644 index 0000000..1898689 --- /dev/null +++ b/.github/workflows/pre-commit-config.yml @@ -0,0 +1,5 @@ +- repo: https://github.com/astral-sh/ruff-pre-commit + # Ruff version. + rev: v0.0.270 + hooks: + - id: ruff \ No newline at end of file diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml new file mode 100644 index 0000000..39483b0 --- /dev/null +++ b/.github/workflows/pre-commit.yml @@ -0,0 +1,14 @@ +name: pre-commit + +on: + pull_request: + push: + branches: [master, dev-branch, main] + +jobs: + pre-commit: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-python@v3 + - uses: pre-commit/action@v3.0.0 \ No newline at end of file From aa240d88cf31bb99618bdf097f40a9d3a2b63ec9 Mon Sep 17 00:00:00 2001 From: robertsokola Date: Mon, 5 Jun 2023 23:26:11 +0200 Subject: [PATCH 05/35] test Signed-off-by: robertsokola --- src/ui/embeds.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ui/embeds.py b/src/ui/embeds.py index add4c01..998e66f 100644 --- a/src/ui/embeds.py +++ b/src/ui/embeds.py @@ -9,7 +9,7 @@ class CooldownErrorEmbed(discord.Embed): - LIMIT = 4 + LIMIT = 4x def __init__(self, seconds: float): self.seconds = round(seconds) From b191eb3a3f584b783f46646fc8d3065ae2c8adc4 Mon Sep 17 00:00:00 2001 From: robertsokola Date: Mon, 5 Jun 2023 23:30:29 +0200 Subject: [PATCH 06/35] test Signed-off-by: robertsokola --- .../workflows/{pre-commit-config.yml => pre-commit-config.yaml} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .github/workflows/{pre-commit-config.yml => pre-commit-config.yaml} (100%) diff --git a/.github/workflows/pre-commit-config.yml b/.github/workflows/pre-commit-config.yaml similarity index 100% rename from .github/workflows/pre-commit-config.yml rename to .github/workflows/pre-commit-config.yaml From 9a582f894cbdfea3b745db26c559d04417cbf366 Mon Sep 17 00:00:00 2001 From: robertsokola Date: Mon, 5 Jun 2023 23:46:29 +0200 Subject: [PATCH 07/35] Test Signed-off-by: robertsokola --- .github/workflows/.pre-commit-config.yaml | 5 +++++ .github/workflows/pre-commit-config.yaml | 5 ----- src/ui/embeds.py | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) create mode 100644 .github/workflows/.pre-commit-config.yaml delete mode 100644 .github/workflows/pre-commit-config.yaml diff --git a/.github/workflows/.pre-commit-config.yaml b/.github/workflows/.pre-commit-config.yaml new file mode 100644 index 0000000..f16a370 --- /dev/null +++ b/.github/workflows/.pre-commit-config.yaml @@ -0,0 +1,5 @@ +repos: + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.0.270 + hooks: + - id: ruff \ No newline at end of file diff --git a/.github/workflows/pre-commit-config.yaml b/.github/workflows/pre-commit-config.yaml deleted file mode 100644 index 1898689..0000000 --- a/.github/workflows/pre-commit-config.yaml +++ /dev/null @@ -1,5 +0,0 @@ -- repo: https://github.com/astral-sh/ruff-pre-commit - # Ruff version. - rev: v0.0.270 - hooks: - - id: ruff \ No newline at end of file diff --git a/src/ui/embeds.py b/src/ui/embeds.py index 998e66f..add4c01 100644 --- a/src/ui/embeds.py +++ b/src/ui/embeds.py @@ -9,7 +9,7 @@ class CooldownErrorEmbed(discord.Embed): - LIMIT = 4x + LIMIT = 4 def __init__(self, seconds: float): self.seconds = round(seconds) From 815d1f829530165814db0a8086de50368659779a Mon Sep 17 00:00:00 2001 From: robertsokola Date: Tue, 6 Jun 2023 00:00:59 +0200 Subject: [PATCH 08/35] Test Signed-off-by: robertsokola --- .github/workflows/pre-commit.yml | 14 -------------- 1 file changed, 14 deletions(-) delete mode 100644 .github/workflows/pre-commit.yml diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml deleted file mode 100644 index 39483b0..0000000 --- a/.github/workflows/pre-commit.yml +++ /dev/null @@ -1,14 +0,0 @@ -name: pre-commit - -on: - pull_request: - push: - branches: [master, dev-branch, main] - -jobs: - pre-commit: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - uses: actions/setup-python@v3 - - uses: pre-commit/action@v3.0.0 \ No newline at end of file From 859fa2633464a175bf2317684d5e5462940f733b Mon Sep 17 00:00:00 2001 From: robertsokola Date: Tue, 6 Jun 2023 00:02:34 +0200 Subject: [PATCH 09/35] Test Signed-off-by: robertsokola --- .github/workflows/.pre-commit-config.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/.pre-commit-config.yaml b/.github/workflows/.pre-commit-config.yaml index f16a370..c25dc01 100644 --- a/.github/workflows/.pre-commit-config.yaml +++ b/.github/workflows/.pre-commit-config.yaml @@ -2,4 +2,5 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.0.270 hooks: - - id: ruff \ No newline at end of file + - id: ruff + on: push \ No newline at end of file From 5236069f09436a45b82e34ff0d143838f0f8b241 Mon Sep 17 00:00:00 2001 From: z004rmyh Date: Tue, 13 Jun 2023 15:00:13 +0200 Subject: [PATCH 10/35] Formatting, added new file for handling error embeds with using raise --- cogs/error.py | 6 +++--- cogs/morserovka.py | 6 +----- cogs/poll_command.py | 7 ++----- cogs/sync_command.py | 8 ++++---- pyproject.toml | 5 +++++ src/db_folder/databases.py | 9 ++------- src/ui/button.py | 4 +--- src/ui/error_handling.py | 0 src/ui/poll.py | 20 ++++++++------------ tests/manual_testing_generator.py | 4 +--- 10 files changed, 27 insertions(+), 42 deletions(-) create mode 100644 src/ui/error_handling.py diff --git a/cogs/error.py b/cogs/error.py index 5d4031d..2ba44ff 100644 --- a/cogs/error.py +++ b/cogs/error.py @@ -22,9 +22,9 @@ def __init__(self, bot): @commands.Cog.listener() async def on_command_error( - self, - ctx: commands.Context, - error: commands.CommandError, + self, + ctx: commands.Context, + error: commands.CommandError, ): match error: case commands.MissingPermissions(): diff --git a/cogs/morserovka.py b/cogs/morserovka.py index f84e6cc..19225b8 100644 --- a/cogs/morserovka.py +++ b/cogs/morserovka.py @@ -4,7 +4,6 @@ from discord import Message, app_commands from discord.ext import commands - # Klasická morseovka @@ -81,10 +80,7 @@ async def zasifruj(self, interaction: discord.Interaction, message: str) -> Mess @app_commands.command(name="desifruj", description="Dešifruj text z morserovky!") @app_commands.describe(message="Věta nebo slovo pro dešifrování") async def desifruj(self, interaction: discord.Interaction, message: str) -> Message: - decipher = "".join( - self.REVERSED_MORSE_CODE_DICT.get(letter) - for letter in re.split(r"\/|\\|\|", message) - ) + decipher = "".join(self.REVERSED_MORSE_CODE_DICT.get(letter) for letter in re.split(r"\/|\\|\|", message)) return await interaction.response.send_message(decipher) diff --git a/cogs/poll_command.py b/cogs/poll_command.py index 92925da..534f91f 100644 --- a/cogs/poll_command.py +++ b/cogs/poll_command.py @@ -17,6 +17,7 @@ def error_handling(answer: list[str]) -> str: return f"Zadal jsi příliš mnoho odpovědí, můžeš maximálně {Poll.MAX_OPTIONS}!" if len(answer) < Poll.MIN_OPTIONS: return f"Zadal jsi příliš málo odpovědí, můžeš alespoň {Poll.MIN_OPTIONS}!" + return None class PollCreate(commands.Cog): @@ -57,11 +58,7 @@ async def pool( message = await interaction.original_response() # bugfix for answers that were empty - answers = [ - answer - for answer in re.split("|".join(self.REGEX_PATTERN), answer) - if answer.strip() - ] + answers = [answer for answer in re.split("|".join(self.REGEX_PATTERN), answer) if answer.strip()] if error_handling(answers): return await message.edit(embed=PollEmbedBase(error_handling(answers))) diff --git a/cogs/sync_command.py b/cogs/sync_command.py index 7789b38..45ce82b 100644 --- a/cogs/sync_command.py +++ b/cogs/sync_command.py @@ -16,10 +16,10 @@ def __init__(self, bot: "Jachym"): @commands.guild_only() @commands.is_owner() async def sync( - self, - ctx: Context, - guilds: Greedy[discord.Guild], - spec: Literal["-", "*", "^"] | None = None, + self, + ctx: Context, + guilds: Greedy[discord.Guild], + spec: Literal["-", "*", "^"] | None = None, ) -> None: """ A command to sync all slash commands to servers user requires. Works like this: diff --git a/pyproject.toml b/pyproject.toml index a03c7d5..faae938 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,3 +11,8 @@ ignore = [ "DTZ005", # datetime object without "tz" is not allowed ] +line-length = 120 + +[tool.black] + +line-length = 120 \ No newline at end of file diff --git a/src/db_folder/databases.py b/src/db_folder/databases.py index c30decf..b2f41ee 100644 --- a/src/db_folder/databases.py +++ b/src/db_folder/databases.py @@ -105,10 +105,7 @@ def __init__(self, pool: aiomysql.pool.Pool): async def add_options(self, discord_poll: Poll): sql = "INSERT INTO `VoteButtons`(message_id, answers) VALUES (%s, %s)" - values = [ - (discord_poll.message_id, vote_option) - for vote_option in discord_poll.options - ] + values = [(discord_poll.message_id, vote_option) for vote_option in discord_poll.options] await self.commit_many_values(sql, values) async def add_user(self, discord_poll: Poll, user: int, index: int): @@ -122,9 +119,7 @@ async def remove_user(self, discord_poll: Poll, user: int, index: int): await self.commit_value(sql, value) async def fetch_all_users(self, poll: Poll, index: int) -> set[int]: - sql = ( - "SELECT vote_user FROM `Answers` WHERE message_id = %s AND iter_index = %s" - ) + sql = "SELECT vote_user FROM `Answers` WHERE message_id = %s AND iter_index = %s" values = (poll.message_id, index) users_voted_for = await self.fetch_all_values(sql, values) diff --git a/src/ui/button.py b/src/ui/button.py index 51ce2f3..4b83cc7 100644 --- a/src/ui/button.py +++ b/src/ui/button.py @@ -50,9 +50,7 @@ async def toggle_vote(self, interaction: discord.Interaction) -> set[str]: await vote_button_db.remove_user(self.poll, user, self.index) users_id.remove(user) - return { - interaction.guild.get_member(user_id).display_name for user_id in users_id - } + return {interaction.guild.get_member(user_id).display_name for user_id in users_id} async def edit_embed(self, members: set[str]) -> discord.Embed: return self.embed.set_field_at( diff --git a/src/ui/error_handling.py b/src/ui/error_handling.py new file mode 100644 index 0000000..e69de29 diff --git a/src/ui/poll.py b/src/ui/poll.py index 397cf3b..834c153 100644 --- a/src/ui/poll.py +++ b/src/ui/poll.py @@ -19,23 +19,19 @@ class Poll: ] def __init__( - self, - message_id: int, - channel_id: int, - question: str, - options: list[str], - user_id: int | None = None, - date_created: datetime | None = None, + self, + message_id: int, + channel_id: int, + question: str, + options: list[str], + user_id: int | None = None, + date_created: datetime | None = None, ): self._message_id = message_id self._channel_id = channel_id self._question = question self._options = options - self._date_created_at = ( - datetime.now().strftime("%Y-%m-%d") - if date_created is None - else date_created - ) + self._date_created_at = datetime.now().strftime("%Y-%m-%d") if date_created is None else date_created self._user_id = user_id @property diff --git a/tests/manual_testing_generator.py b/tests/manual_testing_generator.py index aa6b6e8..dbe4b7f 100644 --- a/tests/manual_testing_generator.py +++ b/tests/manual_testing_generator.py @@ -18,9 +18,7 @@ def test_pools(count=2) -> str: def test_events(): date = datetime.now() + timedelta(minutes=1) - test_string = ( - f'!udalost create "Name" "Description" "{date.strftime("%d.%m.%Y %H:%M")}"' - ) + test_string = f'!udalost create "Name" "Description" "{date.strftime("%d.%m.%Y %H:%M")}"' return test_string From afd2b88a52f8897233e06abee880ab31cf9b3d5d Mon Sep 17 00:00:00 2001 From: z004rmyh Date: Thu, 15 Jun 2023 14:30:56 +0200 Subject: [PATCH 11/35] Basic Exception classes, test if they work as intended --- cogs/error.py | 24 ++++++++---------------- cogs/poll_command.py | 8 ++++---- src/ui/error_handling.py | 24 ++++++++++++++++++++++++ 3 files changed, 36 insertions(+), 20 deletions(-) diff --git a/cogs/error.py b/cogs/error.py index 2ba44ff..9843050 100644 --- a/cogs/error.py +++ b/cogs/error.py @@ -1,7 +1,7 @@ -import logging - from discord.ext import commands +from src.ui.error_handling import EmbedBaseError + class Error(commands.Cog): """Basic class for catching errors and sending a message""" @@ -9,24 +9,16 @@ class Error(commands.Cog): def __init__(self, bot): self.bot = bot - self.logger = logging.getLogger("discord") - handler = logging.FileHandler( - filename="discord.log", - encoding="utf-8", - mode="w", - ) - handler.setFormatter( - logging.Formatter("%(asctime)s:%(levelname)s:%(name)s: %(message)s"), - ) - self.logger.addHandler(handler) - @commands.Cog.listener() async def on_command_error( - self, - ctx: commands.Context, - error: commands.CommandError, + self, + ctx: commands.Context, + error: commands.CommandError, ): match error: + case EmbedBaseError(): + return await error.send() + case commands.MissingPermissions(): return await ctx.send("Chybí ti požadovaná práva!") diff --git a/cogs/poll_command.py b/cogs/poll_command.py index 534f91f..bf43378 100644 --- a/cogs/poll_command.py +++ b/cogs/poll_command.py @@ -1,5 +1,5 @@ import re - +from src.ui.error_handling import TooFewOptionsError, TooManyOptionsError import discord from discord import app_commands from discord.ext import commands @@ -12,11 +12,11 @@ from src.ui.poll_view import PollView -def error_handling(answer: list[str]) -> str: +def error_handling(answer: list[str]) -> TooFewOptionsError | TooManyOptionsError | None: if len(answer) > Poll.MAX_OPTIONS: - return f"Zadal jsi příliš mnoho odpovědí, můžeš maximálně {Poll.MAX_OPTIONS}!" + raise TooManyOptionsError(f"Zadal jsi příliš mnoho odpovědí, můžeš maximálně {Poll.MAX_OPTIONS}!") if len(answer) < Poll.MIN_OPTIONS: - return f"Zadal jsi příliš málo odpovědí, můžeš alespoň {Poll.MIN_OPTIONS}!" + raise TooFewOptionsError(f"Zadal jsi příliš málo odpovědí, můžeš alespoň {Poll.MIN_OPTIONS}!") return None diff --git a/src/ui/error_handling.py b/src/ui/error_handling.py index e69de29..bc66b51 100644 --- a/src/ui/error_handling.py +++ b/src/ui/error_handling.py @@ -0,0 +1,24 @@ + +class EmbedBaseError(Exception): + def __init__(self, message, interaction, inner): + super().__init__(message, inner) + self.message = message + self.interaction = interaction + + async def send(self): + args = { + "content": self.message, + "ephemeral": True + } + if not self.interaction.response.is_done(): + await self.interaction.response.send_message(**args) + else: + self.interaction.followup.send(**args) + +class TooManyOptionsError(EmbedBaseError): + pass + +class TooFewOptionsError(EmbedBaseError): + pass + + From 728be46b51ddad981910ccf0c3030c89499ae659 Mon Sep 17 00:00:00 2001 From: Robert Sokola <76487619+TheXer@users.noreply.github.com> Date: Fri, 16 Jun 2023 10:13:47 +0200 Subject: [PATCH 12/35] Update .pre-commit-config.yaml --- .github/workflows/.pre-commit-config.yaml | 27 ++++++++++++++++++----- 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/.github/workflows/.pre-commit-config.yaml b/.github/workflows/.pre-commit-config.yaml index c25dc01..5adc2cc 100644 --- a/.github/workflows/.pre-commit-config.yaml +++ b/.github/workflows/.pre-commit-config.yaml @@ -1,6 +1,21 @@ -repos: - - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.0.270 - hooks: - - id: ruff - on: push \ No newline at end of file +on: push + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v1 + + - name: Set up CPython + uses: actions/setup-python@v4 + + - name: Install dependencies + id: install-deps + run: | + python -m pip install --upgrade pip setuptools wheel ruff + pip install -U -r requirements.txt + + - name: Ruff Check + uses: jpetrucciani/ruff-check@0.0.239 + From b71b713d4c0a33b3e6ecfe46c9d23cc3328be504 Mon Sep 17 00:00:00 2001 From: z004rmyh Date: Fri, 16 Jun 2023 10:26:29 +0200 Subject: [PATCH 13/35] Repaired pyproject.toml, formatting --- cogs/error.py | 18 +++++++----------- cogs/poll_command.py | 9 ++++++--- pyproject.toml | 23 ++++++++++++++--------- src/ui/error_handling.py | 7 +++---- 4 files changed, 30 insertions(+), 27 deletions(-) diff --git a/cogs/error.py b/cogs/error.py index 9843050..bfc76d7 100644 --- a/cogs/error.py +++ b/cogs/error.py @@ -1,4 +1,5 @@ from discord.ext import commands +from loguru import logger from src.ui.error_handling import EmbedBaseError @@ -11,31 +12,26 @@ def __init__(self, bot): @commands.Cog.listener() async def on_command_error( - self, - ctx: commands.Context, - error: commands.CommandError, + self, + ctx: commands.Context, + error: commands.CommandError, ): match error: case EmbedBaseError(): + logger.error(error) return await error.send() case commands.MissingPermissions(): + logger.error(f"Missing Permissions: {error}") return await ctx.send("Chybí ti požadovaná práva!") case commands.CommandNotFound(): return None case _: - self.logger.critical( - f"{ctx.message.id}, {ctx.message.content} | {error}", - ) - print(error) + logger.critical(f"Catched an error: {error}") return None - @commands.Cog.listener() - async def on_command(self, ctx: commands.Context): - self.logger.info(f"{ctx.message.id} {ctx.message.content}") - async def setup(bot): await bot.add_cog(Error(bot)) diff --git a/cogs/poll_command.py b/cogs/poll_command.py index bf43378..f18a9db 100644 --- a/cogs/poll_command.py +++ b/cogs/poll_command.py @@ -1,5 +1,5 @@ import re -from src.ui.error_handling import TooFewOptionsError, TooManyOptionsError + import discord from discord import app_commands from discord.ext import commands @@ -8,15 +8,18 @@ from src.db_folder.databases import PollDatabase, VoteButtonDatabase from src.jachym import Jachym from src.ui.embeds import PollEmbed, PollEmbedBase +from src.ui.error_handling import TooFewOptionsError, TooManyOptionsError from src.ui.poll import Poll from src.ui.poll_view import PollView def error_handling(answer: list[str]) -> TooFewOptionsError | TooManyOptionsError | None: if len(answer) > Poll.MAX_OPTIONS: - raise TooManyOptionsError(f"Zadal jsi příliš mnoho odpovědí, můžeš maximálně {Poll.MAX_OPTIONS}!") + msg = f"Zadal jsi příliš mnoho odpovědí, můžeš maximálně {Poll.MAX_OPTIONS}!" + raise TooManyOptionsError(msg) if len(answer) < Poll.MIN_OPTIONS: - raise TooFewOptionsError(f"Zadal jsi příliš málo odpovědí, můžeš alespoň {Poll.MIN_OPTIONS}!") + msg = f"Zadal jsi příliš málo odpovědí, můžeš alespoň {Poll.MIN_OPTIONS}!" + raise TooFewOptionsError(msg) return None diff --git a/pyproject.toml b/pyproject.toml index faae938..d12e56c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,14 +1,19 @@ [tool.ruff] -select = ["ALL"] - -ignore = [ - "D", # Ignore docstrings - "E501", # Line too long, let Black handle this - "ANN", # typing, let mypy handle tis - "INP001", # Namespace package eeeeh - "PLR0913", #Too many args to func call - "DTZ005", # datetime object without "tz" is not allowed +select = [ + "E", # pycodestyle + "F", # pyflakes + "UP", # pyupgrade, + "I", # isort + "UP", # pyupgrade + "ASYNC", + "BLE", # Blind Exception + "T20", # Found a print! + "RET", # Unnecessary return + "SIM", # Simplify +] +exclude = [ + "tests", ] line-length = 120 diff --git a/src/ui/error_handling.py b/src/ui/error_handling.py index bc66b51..1af1413 100644 --- a/src/ui/error_handling.py +++ b/src/ui/error_handling.py @@ -1,4 +1,3 @@ - class EmbedBaseError(Exception): def __init__(self, message, interaction, inner): super().__init__(message, inner) @@ -8,17 +7,17 @@ def __init__(self, message, interaction, inner): async def send(self): args = { "content": self.message, - "ephemeral": True + "ephemeral": True, } if not self.interaction.response.is_done(): await self.interaction.response.send_message(**args) else: self.interaction.followup.send(**args) + class TooManyOptionsError(EmbedBaseError): pass + class TooFewOptionsError(EmbedBaseError): pass - - From 68bd73ed214c452ec3e084a98bc814713344ad45 Mon Sep 17 00:00:00 2001 From: Robert Sokola <76487619+TheXer@users.noreply.github.com> Date: Fri, 16 Jun 2023 10:31:07 +0200 Subject: [PATCH 14/35] Update .pre-commit-config.yaml --- .github/workflows/.pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/.pre-commit-config.yaml b/.github/workflows/.pre-commit-config.yaml index 5adc2cc..9a858b9 100644 --- a/.github/workflows/.pre-commit-config.yaml +++ b/.github/workflows/.pre-commit-config.yaml @@ -17,5 +17,5 @@ jobs: pip install -U -r requirements.txt - name: Ruff Check - uses: jpetrucciani/ruff-check@0.0.239 + run: ruff check . From a79b86de1bf6c300457af22928e26a6439da3673 Mon Sep 17 00:00:00 2001 From: z004rmyh Date: Fri, 16 Jun 2023 10:32:11 +0200 Subject: [PATCH 15/35] test --- cogs/poll_command.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cogs/poll_command.py b/cogs/poll_command.py index f18a9db..e64fec7 100644 --- a/cogs/poll_command.py +++ b/cogs/poll_command.py @@ -71,7 +71,7 @@ async def pool( question=question, options=answers, user_id=interaction.user.id, - ) + embed = PollEmbed(poll) view = PollView(poll, embed, db_poll=self.bot.pool) From 297eb5d487a38ae614472117fcdbec3c425ea2ee Mon Sep 17 00:00:00 2001 From: z004rmyh Date: Fri, 16 Jun 2023 10:33:31 +0200 Subject: [PATCH 16/35] OK, everything with precommit hooks works now, horray --- cogs/poll_command.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cogs/poll_command.py b/cogs/poll_command.py index e64fec7..f18a9db 100644 --- a/cogs/poll_command.py +++ b/cogs/poll_command.py @@ -71,7 +71,7 @@ async def pool( question=question, options=answers, user_id=interaction.user.id, - + ) embed = PollEmbed(poll) view = PollView(poll, embed, db_poll=self.bot.pool) From 97d6d077b829b8f8a25ba49519bee379fa41379d Mon Sep 17 00:00:00 2001 From: Robert Sokola <76487619+TheXer@users.noreply.github.com> Date: Fri, 16 Jun 2023 10:41:37 +0200 Subject: [PATCH 17/35] Update .pre-commit-config.yaml Added Black formatter --- .github/workflows/.pre-commit-config.yaml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/.pre-commit-config.yaml b/.github/workflows/.pre-commit-config.yaml index 9a858b9..768b1da 100644 --- a/.github/workflows/.pre-commit-config.yaml +++ b/.github/workflows/.pre-commit-config.yaml @@ -13,9 +13,14 @@ jobs: - name: Install dependencies id: install-deps run: | - python -m pip install --upgrade pip setuptools wheel ruff + python -m pip install --upgrade pip setuptools wheel ruff black pip install -U -r requirements.txt + + - name: Black check + run: black . - name: Ruff Check run: ruff check . + + From 6433590a15457864a6030e40126eba298a33b972 Mon Sep 17 00:00:00 2001 From: z004rmyh Date: Fri, 16 Jun 2023 10:42:12 +0200 Subject: [PATCH 18/35] Test Black --- cogs/poll_command.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cogs/poll_command.py b/cogs/poll_command.py index f18a9db..4b15524 100644 --- a/cogs/poll_command.py +++ b/cogs/poll_command.py @@ -61,7 +61,9 @@ async def pool( message = await interaction.original_response() # bugfix for answers that were empty - answers = [answer for answer in re.split("|".join(self.REGEX_PATTERN), answer) if answer.strip()] + answers = [answer + for answer in re.split("|".join(self.REGEX_PATTERN), answer) + if answer.strip()] if error_handling(answers): return await message.edit(embed=PollEmbedBase(error_handling(answers))) From d487ec8c62ca199535441b5d22ae4fb0dff5af0a Mon Sep 17 00:00:00 2001 From: Robert Sokola <76487619+TheXer@users.noreply.github.com> Date: Fri, 16 Jun 2023 10:52:52 +0200 Subject: [PATCH 19/35] Update .pre-commit-config.yaml --- .github/workflows/.pre-commit-config.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/.pre-commit-config.yaml b/.github/workflows/.pre-commit-config.yaml index 768b1da..824eb7d 100644 --- a/.github/workflows/.pre-commit-config.yaml +++ b/.github/workflows/.pre-commit-config.yaml @@ -15,9 +15,9 @@ jobs: run: | python -m pip install --upgrade pip setuptools wheel ruff black pip install -U -r requirements.txt - - - name: Black check - run: black . + + - name: Black format + uses: psf/black@stable - name: Ruff Check run: ruff check . From fbdbcd1b99e910c0b62de57fd909b92d311ff491 Mon Sep 17 00:00:00 2001 From: z004rmyh Date: Fri, 16 Jun 2023 10:59:03 +0200 Subject: [PATCH 20/35] Testing success --- cogs/poll_command.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/cogs/poll_command.py b/cogs/poll_command.py index 4b15524..f18a9db 100644 --- a/cogs/poll_command.py +++ b/cogs/poll_command.py @@ -61,9 +61,7 @@ async def pool( message = await interaction.original_response() # bugfix for answers that were empty - answers = [answer - for answer in re.split("|".join(self.REGEX_PATTERN), answer) - if answer.strip()] + answers = [answer for answer in re.split("|".join(self.REGEX_PATTERN), answer) if answer.strip()] if error_handling(answers): return await message.edit(embed=PollEmbedBase(error_handling(answers))) From 550987fb0b1943aa981a18e1c153898f504491db Mon Sep 17 00:00:00 2001 From: Robert Sokola <76487619+TheXer@users.noreply.github.com> Date: Fri, 16 Jun 2023 11:00:12 +0200 Subject: [PATCH 21/35] Update .pre-commit-config.yaml --- .github/workflows/.pre-commit-config.yaml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/.pre-commit-config.yaml b/.github/workflows/.pre-commit-config.yaml index 824eb7d..fd5d2ed 100644 --- a/.github/workflows/.pre-commit-config.yaml +++ b/.github/workflows/.pre-commit-config.yaml @@ -13,8 +13,7 @@ jobs: - name: Install dependencies id: install-deps run: | - python -m pip install --upgrade pip setuptools wheel ruff black - pip install -U -r requirements.txt + python -m pip install --upgrade pip setuptools wheel ruff - name: Black format uses: psf/black@stable From 7d4b3e984d9f15dc3148a0afb8e24286ed94b6a1 Mon Sep 17 00:00:00 2001 From: robertsokola Date: Mon, 19 Jun 2023 23:36:40 +0200 Subject: [PATCH 22/35] Functional errors using raise keyword. Signed-off-by: robertsokola --- cogs/error.py | 54 +++++++++++++++++++++------------ cogs/poll_command.py | 64 +++++++++++++++++++++------------------- src/ui/error_handling.py | 23 --------------- 3 files changed, 69 insertions(+), 72 deletions(-) delete mode 100644 src/ui/error_handling.py diff --git a/cogs/error.py b/cogs/error.py index bfc76d7..98804bb 100644 --- a/cogs/error.py +++ b/cogs/error.py @@ -1,7 +1,32 @@ +from discord import Interaction +from discord.app_commands import CommandInvokeError from discord.ext import commands from loguru import logger -from src.ui.error_handling import EmbedBaseError + +class PrettyError(CommandInvokeError): + def __init__(self, message: str, interaction: Interaction, inner_exception: Exception | None = None): + super().__init__(interaction.command, inner_exception) + self.message = message + self.interaction = interaction + + async def send(self): + send_args = { + "content": f"{self.message}", + "ephemeral": False, + } + if not self.interaction.response.is_done(): + await self.interaction.response.send_message(**send_args) + else: + await self.interaction.followup.send(**send_args) + + +class TooManyOptionsError(PrettyError): + pass + + +class TooFewOptionsError(PrettyError): + pass class Error(commands.Cog): @@ -9,28 +34,19 @@ class Error(commands.Cog): def __init__(self, bot): self.bot = bot + tree = self.bot.tree + self._old_tree_error = tree.on_error + tree.on_error = self.on_app_command_error @commands.Cog.listener() - async def on_command_error( - self, - ctx: commands.Context, - error: commands.CommandError, - ): + async def on_app_command_error(self, interaction: Interaction, error: Exception): match error: - case EmbedBaseError(): - logger.error(error) - return await error.send() - - case commands.MissingPermissions(): - logger.error(f"Missing Permissions: {error}") - return await ctx.send("Chybí ti požadovaná práva!") - - case commands.CommandNotFound(): - return None - + case PrettyError(): + # if I use only 'error', gives me NoneType. Solved by this + logger.error(f"{error.__class__.__name__}: {interaction.command.name}") + await error.send() case _: - logger.critical(f"Catched an error: {error}") - return None + logger.critical(error) async def setup(bot): diff --git a/cogs/poll_command.py b/cogs/poll_command.py index f18a9db..4724b6d 100644 --- a/cogs/poll_command.py +++ b/cogs/poll_command.py @@ -5,38 +5,47 @@ from discord.ext import commands from loguru import logger +from cogs.error import TooFewOptionsError, TooManyOptionsError from src.db_folder.databases import PollDatabase, VoteButtonDatabase from src.jachym import Jachym from src.ui.embeds import PollEmbed, PollEmbedBase -from src.ui.error_handling import TooFewOptionsError, TooManyOptionsError from src.ui.poll import Poll from src.ui.poll_view import PollView -def error_handling(answer: list[str]) -> TooFewOptionsError | TooManyOptionsError | None: - if len(answer) > Poll.MAX_OPTIONS: - msg = f"Zadal jsi příliš mnoho odpovědí, můžeš maximálně {Poll.MAX_OPTIONS}!" - raise TooManyOptionsError(msg) - if len(answer) < Poll.MIN_OPTIONS: - msg = f"Zadal jsi příliš málo odpovědí, můžeš alespoň {Poll.MIN_OPTIONS}!" - raise TooFewOptionsError(msg) - return None +class OptionsTransformer(app_commands.Transformer): + async def transform( + self, interaction: discord.Interaction, option: str + ) -> TooManyOptionsError | TooFewOptionsError | list[str]: + """ + Transformer method to transformate a single string to multiple options. If they are not within parameters, + raises an error, else returns options. + Parameters + ---------- + interaction: discord.Interaction + option: str -class PollCreate(commands.Cog): - POLL_PARAMETERS = { - "name": "anketa", - "description": "Anketa pro hlasování. Jsou vidět všichni hlasovatelé.", - "question": "Otázka, na kterou potřebuješ vědět odpověď", - "answer": 'Odpovědi, rozděluješ odpovědi uvozovkou ("), maximálně pouze 10 možností', - "help": """ - Jednoduchá anketa, která obsahuje otázku a odpovědi. Povoleno je 10 možností. - """, - } - - # Bugfix for iPhone users who have different font for aposthrofe - REGEX_PATTERN = ['"', "”", "“", "„"] + Returns + ------- + List of strings + + Raises: + ------- + TooManyOptionsError, TooFewOptionsError + + """ + answers = [option for option in re.split('"|"|“|„', option) if option.strip()] + if len(answers) > Poll.MAX_OPTIONS: + msg = f"Zadal jsi příliš mnoho odpovědí, můžeš maximálně {Poll.MAX_OPTIONS}!" + raise TooManyOptionsError(msg, interaction) + if len(answers) < Poll.MIN_OPTIONS: + msg = f"Zadal jsi příliš málo odpovědí, můžeš alespoň {Poll.MIN_OPTIONS}!" + raise TooFewOptionsError(msg, interaction) + return answers + +class PollCreate(commands.Cog): def __init__(self, bot: Jachym): self.bot = bot @@ -53,23 +62,18 @@ async def pool( self, interaction: discord.Interaction, question: str, - answer: str, + answer: app_commands.Transform[list[str, ...], OptionsTransformer], ) -> discord.Message: await interaction.response.send_message( - embed=PollEmbedBase("Dělám na tom, vydrž!"), + embed=PollEmbedBase("Nahrávám anketu..."), ) message = await interaction.original_response() - # bugfix for answers that were empty - answers = [answer for answer in re.split("|".join(self.REGEX_PATTERN), answer) if answer.strip()] - if error_handling(answers): - return await message.edit(embed=PollEmbedBase(error_handling(answers))) - poll = Poll( message_id=message.id, channel_id=message.channel.id, question=question, - options=answers, + options=answer, user_id=interaction.user.id, ) diff --git a/src/ui/error_handling.py b/src/ui/error_handling.py deleted file mode 100644 index 1af1413..0000000 --- a/src/ui/error_handling.py +++ /dev/null @@ -1,23 +0,0 @@ -class EmbedBaseError(Exception): - def __init__(self, message, interaction, inner): - super().__init__(message, inner) - self.message = message - self.interaction = interaction - - async def send(self): - args = { - "content": self.message, - "ephemeral": True, - } - if not self.interaction.response.is_done(): - await self.interaction.response.send_message(**args) - else: - self.interaction.followup.send(**args) - - -class TooManyOptionsError(EmbedBaseError): - pass - - -class TooFewOptionsError(EmbedBaseError): - pass From 965bffa50d4131226e9ded93138de1f871060e18 Mon Sep 17 00:00:00 2001 From: robertsokola Date: Thu, 29 Jun 2023 23:08:17 +0200 Subject: [PATCH 23/35] Added Emojis, rewritten error embed, repaired error handling. Signed-off-by: robertsokola --- cogs/error.py | 54 ++++++++++++++++++++++++-------------------- cogs/poll_command.py | 9 ++++---- src/ui/embeds.py | 45 ++++++++++++++++-------------------- src/ui/emojis.py | 9 ++++++++ 4 files changed, 62 insertions(+), 55 deletions(-) create mode 100644 src/ui/emojis.py diff --git a/cogs/error.py b/cogs/error.py index 98804bb..dfa12aa 100644 --- a/cogs/error.py +++ b/cogs/error.py @@ -3,30 +3,7 @@ from discord.ext import commands from loguru import logger - -class PrettyError(CommandInvokeError): - def __init__(self, message: str, interaction: Interaction, inner_exception: Exception | None = None): - super().__init__(interaction.command, inner_exception) - self.message = message - self.interaction = interaction - - async def send(self): - send_args = { - "content": f"{self.message}", - "ephemeral": False, - } - if not self.interaction.response.is_done(): - await self.interaction.response.send_message(**send_args) - else: - await self.interaction.followup.send(**send_args) - - -class TooManyOptionsError(PrettyError): - pass - - -class TooFewOptionsError(PrettyError): - pass +from src.ui.embeds import ErrorMessage class Error(commands.Cog): @@ -47,6 +24,35 @@ async def on_app_command_error(self, interaction: Interaction, error: Exception) await error.send() case _: logger.critical(error) + await interaction.response.send_message( + embed=ErrorMessage( + "Tato zpráva by se nikdy zobrazit správně neměla. " + "Jsi borec, že jsi mi dokázal rozbít Jáchyma, nechceš mi o tom napsat do issues na githubu?" + ) + ) + + +class PrettyError(CommandInvokeError): + """Pretty errors useful for raise keyword""" + + def __init__(self, message: str, interaction: Interaction, inner_exception: Exception | None = None): + super().__init__(interaction.command, inner_exception) + self.message = message + self.interaction = interaction + + async def send(self): + if not self.interaction.response.is_done(): + await self.interaction.response.send_message(embed=ErrorMessage(self.message)) + else: + await self.interaction.followup.send(embed=ErrorMessage(self.message)) + + +class TooManyOptionsError(PrettyError): + pass + + +class TooFewOptionsError(PrettyError): + pass async def setup(bot): diff --git a/cogs/poll_command.py b/cogs/poll_command.py index 4724b6d..c332920 100644 --- a/cogs/poll_command.py +++ b/cogs/poll_command.py @@ -2,6 +2,7 @@ import discord from discord import app_commands +from discord.app_commands import Transform, Transformer from discord.ext import commands from loguru import logger @@ -13,7 +14,7 @@ from src.ui.poll_view import PollView -class OptionsTransformer(app_commands.Transformer): +class OptionsTransformer(Transformer): async def transform( self, interaction: discord.Interaction, option: str ) -> TooManyOptionsError | TooFewOptionsError | list[str]: @@ -62,11 +63,9 @@ async def pool( self, interaction: discord.Interaction, question: str, - answer: app_commands.Transform[list[str, ...], OptionsTransformer], + answer: Transform[list[str, ...], OptionsTransformer], ) -> discord.Message: - await interaction.response.send_message( - embed=PollEmbedBase("Nahrávám anketu..."), - ) + await interaction.response.send_message(embed=PollEmbedBase("Nahrávám anketu...")) message = await interaction.original_response() poll = Poll( diff --git a/src/ui/embeds.py b/src/ui/embeds.py index add4c01..7d05db5 100644 --- a/src/ui/embeds.py +++ b/src/ui/embeds.py @@ -1,35 +1,33 @@ import json import pathlib -from datetime import datetime, timedelta +from datetime import datetime import discord -from discord.colour import Color +from discord.colour import Color, Colour +from src.ui.emojis import ScoutEmojis from src.ui.poll import Poll -class CooldownErrorEmbed(discord.Embed): - LIMIT = 4 +class ErrorMessage(discord.Embed): + def __init__(self, message: str): + title = "⚠️ Jejda, někde se stala chyba..." - def __init__(self, seconds: float): - self.seconds = round(seconds) - formatted_date = discord.utils.format_dt( - datetime.now() + timedelta(seconds=10), - "R", + description = ( + f"{message}\n\n" + f"{ScoutEmojis.FLEUR_DE_LIS} *Pokud máš pocit, že tohle by chyba být neměla, " + f"napiš [sem](https://github.com/TheXer/Jachym/issues/new/choose)*" ) + self.set_footer(text="Uděláno s ♥!") + super().__init__( - title=f"⚠️ Vydrž! Další anketu můžeš založit {formatted_date}! ⚠️", - colour=Color.red(), + title=title, + description=description, + colour=Colour.red(), + timestamp=datetime.now(), ) - def correct_czech_writing(self) -> str: - if self.seconds > self.LIMIT: - return f"{self.seconds} sekund" - if self.LIMIT >= self.seconds > 1: - return f"{self.seconds} sekundy" - return "sekundu" - class PollEmbedBase(discord.Embed): def __init__(self, question) -> None: @@ -43,7 +41,9 @@ def __init__(self, poll: Poll): super().__init__(poll.question) self.answers = poll.options self._add_options() - self._add_timestamp() + + self.set_footer(text="Uděláno s ♥!") + self.timestamp = datetime.now() def _add_options(self): for index, option in enumerate(self.answers): @@ -53,13 +53,6 @@ def _add_options(self): inline=False, ) - def _add_timestamp(self): - self.add_field( - name="", - value=f"Anketa byla vytvořena {discord.utils.format_dt(datetime.now(), 'R')}", - inline=False, - ) - class EmbedFromJSON(discord.Embed): PATH = pathlib.Path("src/text_json/cz_text.json") diff --git a/src/ui/emojis.py b/src/ui/emojis.py new file mode 100644 index 0000000..32b84a6 --- /dev/null +++ b/src/ui/emojis.py @@ -0,0 +1,9 @@ +from dataclasses import dataclass + + +@dataclass() +class ScoutEmojis: + FLEUR_DE_LIS = "<:lilie:814103053208649778>" + SCOUT_SCARF = "<:satek:814103053614972938>" + POTKANI = "<:Potkani:803954899250839582>" + SCOUTS = "<:skauti:814103051878531112>" From 8a2cf62f3e06b936696243debcf07b437702ed24 Mon Sep 17 00:00:00 2001 From: robertsokola Date: Thu, 29 Jun 2023 23:13:49 +0200 Subject: [PATCH 24/35] Created a new branch from main, where all the changes should arrive before commiting to main Signed-off-by: robertsokola --- src/ui/button.py | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/src/ui/button.py b/src/ui/button.py index f09c9a3..eb9940e 100644 --- a/src/ui/button.py +++ b/src/ui/button.py @@ -8,20 +8,20 @@ class ButtonBackend(discord.ui.Button): """ - Button class to edit a poll embed with + Button class to edit a poll embed with. """ LENTGH_STRING = 30 def __init__( - self, - custom_id: str, - poll: Poll, - emoji: str, - embed: PollEmbed, - index: int, - label: str, - db_poll: aiomysql.pool.Pool, + self, + custom_id: str, + poll: Poll, + emoji: str, + embed: PollEmbed, + index: int, + label: str, + db_poll: aiomysql.pool.Pool, ) -> None: super().__init__( label=label if len(label) <= self.LENTGH_STRING else "", @@ -50,9 +50,7 @@ async def toggle_vote(self, interaction: discord.Interaction) -> set[str]: await vote_button_db.remove_user(self.poll, user, self.index) users_id.remove(user) - return { - interaction.guild.get_member(user_id).display_name for user_id in users_id - } + return {interaction.guild.get_member(user_id).display_name for user_id in users_id} async def edit_embed(self, members: set[str]) -> discord.Embed: return self.embed.set_field_at( From 7f7a459207c4c4c44d651e1db42d0f9ead3522b6 Mon Sep 17 00:00:00 2001 From: robertsokola Date: Sun, 2 Jul 2023 23:46:37 +0200 Subject: [PATCH 25/35] Additions: - New emoji class, inherited from Enum this time - Corrected embeds and errors for using the new emoji style - main.py has debug logger now - Now you can add new options using new button known bugs: - Raising errors in buttons/modals doesn't work properly due to a bug in attributes. Maybe raise an issue on discord.py pages Signed-off-by: robertsokola --- cogs/error.py | 8 +++-- main.py | 2 ++ src/db_folder/databases.py | 5 ++++ src/ui/button.py | 23 ++++++++++++++ src/ui/embeds.py | 2 +- src/ui/emojis.py | 9 ++++-- src/ui/modals.py | 61 ++++++++++++++++++++++++++++++++++++++ src/ui/poll_view.py | 10 +++++-- 8 files changed, 112 insertions(+), 8 deletions(-) create mode 100644 src/ui/modals.py diff --git a/cogs/error.py b/cogs/error.py index dfa12aa..fbded10 100644 --- a/cogs/error.py +++ b/cogs/error.py @@ -1,5 +1,5 @@ from discord import Interaction -from discord.app_commands import CommandInvokeError +from discord.app_commands import AppCommandError from discord.ext import commands from loguru import logger @@ -32,7 +32,7 @@ async def on_app_command_error(self, interaction: Interaction, error: Exception) ) -class PrettyError(CommandInvokeError): +class PrettyError(AppCommandError): """Pretty errors useful for raise keyword""" def __init__(self, message: str, interaction: Interaction, inner_exception: Exception | None = None): @@ -55,5 +55,9 @@ class TooFewOptionsError(PrettyError): pass +class NoPermissionError(PrettyError): + pass + + async def setup(bot): await bot.add_cog(Error(bot)) diff --git a/main.py b/main.py index fca8ff6..d085fb0 100644 --- a/main.py +++ b/main.py @@ -1,6 +1,7 @@ import asyncio from os import getenv +import discord.utils from dotenv import load_dotenv from src.jachym import Jachym @@ -11,6 +12,7 @@ async def main() -> None: bot = Jachym() async with bot: + discord.utils.setup_logging() await bot.load_extensions() await bot.start(getenv("DISCORD_TOKEN")) diff --git a/src/db_folder/databases.py b/src/db_folder/databases.py index b2f41ee..64bdfd0 100644 --- a/src/db_folder/databases.py +++ b/src/db_folder/databases.py @@ -108,6 +108,11 @@ async def add_options(self, discord_poll: Poll): values = [(discord_poll.message_id, vote_option) for vote_option in discord_poll.options] await self.commit_many_values(sql, values) + async def add_option(self, discord_poll: Poll, option: str): + sql = "INSERT INTO `VoteButtons`(message_id, answers) VALUES (%s, %s)" + value = discord_poll.message_id, option + await self.commit_value(sql, value) + async def add_user(self, discord_poll: Poll, user: int, index: int): sql = "INSERT INTO `Answers`(message_id, vote_user, iter_index) VALUES (%s, %s, %s)" values = (discord_poll.message_id, user, index) diff --git a/src/ui/button.py b/src/ui/button.py index eb9940e..f8a34ab 100644 --- a/src/ui/button.py +++ b/src/ui/button.py @@ -1,8 +1,11 @@ import aiomysql.pool import discord +from cogs.error import TooManyOptionsError from src.db_folder.databases import VoteButtonDatabase from src.ui.embeds import PollEmbed +from src.ui.emojis import EssentialEmojis, ScoutEmojis +from src.ui.modals import NewOptionModal from src.ui.poll import Poll @@ -66,3 +69,23 @@ async def callback(self, interaction: discord.Interaction): edited_embed = await self.edit_embed(members) await interaction.response.edit_message(embed=edited_embed) + + +class NewOptionButton(discord.ui.Button): + LABEL = "Přidat novou možnost" + + def __init__(self, embed: PollEmbed, poll: Poll, db_pool: aiomysql.pool.Pool): + self.embed = embed + self.poll = poll + self.db_pool = db_pool + + super().__init__( + label=self.LABEL, + emoji=ScoutEmojis.FLEUR_DE_LIS.value, + custom_id=f"option_button::{poll.message_id}", + row=4, + ) + + async def callback(self, interaction: discord.Interaction): + modal = NewOptionModal(self.embed, self.db_pool, self.poll, self.view) + await interaction.response.send_modal(modal) diff --git a/src/ui/embeds.py b/src/ui/embeds.py index 7d05db5..59f6690 100644 --- a/src/ui/embeds.py +++ b/src/ui/embeds.py @@ -15,7 +15,7 @@ def __init__(self, message: str): description = ( f"{message}\n\n" - f"{ScoutEmojis.FLEUR_DE_LIS} *Pokud máš pocit, že tohle by chyba být neměla, " + f"{ScoutEmojis.FLEUR_DE_LIS.value} *Pokud máš pocit, že tohle by chyba být neměla, " f"napiš [sem](https://github.com/TheXer/Jachym/issues/new/choose)*" ) diff --git a/src/ui/emojis.py b/src/ui/emojis.py index 32b84a6..f5ce470 100644 --- a/src/ui/emojis.py +++ b/src/ui/emojis.py @@ -1,9 +1,12 @@ -from dataclasses import dataclass +from enum import Enum -@dataclass() -class ScoutEmojis: +class ScoutEmojis(Enum): FLEUR_DE_LIS = "<:lilie:814103053208649778>" SCOUT_SCARF = "<:satek:814103053614972938>" POTKANI = "<:Potkani:803954899250839582>" SCOUTS = "<:skauti:814103051878531112>" + + +class EssentialEmojis(Enum): + NUMBER_EMOJIS = ["1️⃣", "2️⃣", "3️⃣", "4️⃣", "5️⃣", "6️⃣", "7️⃣", "8️⃣", "9️⃣", "🔟"] diff --git a/src/ui/modals.py b/src/ui/modals.py new file mode 100644 index 0000000..eac532b --- /dev/null +++ b/src/ui/modals.py @@ -0,0 +1,61 @@ +import discord + +from cogs.error import TooManyOptionsError, NoPermissionError +from src.db_folder.databases import VoteButtonDatabase + + +class NewOptionModal(discord.ui.Modal): + EMOJIS = ["1️⃣", "2️⃣", "3️⃣", "4️⃣", "5️⃣", "6️⃣", "7️⃣", "8️⃣", "9️⃣", "🔟"] + + def __init__(self, embed, db_poll, poll, view): + super().__init__(title="Nová možnost") + + self.new_option = discord.ui.TextInput( + label="Jméno nové možnosti", + max_length=255, + required=True, + placeholder="Vymysli príma otázku!", + style=discord.TextStyle.short, + ) + + self.add_item(self.new_option) + + self.embed = embed + self.db_poll = db_poll + self.poll = poll + self.view = view + + async def on_submit(self, interaction: discord.Interaction): + em = await self.add_item_to_embed() + await interaction.response.edit_message(embed=em, view=self.view) + + async def interaction_check(self, interaction: discord.Interaction): + if not self.poll.user_id == interaction.user.id: + raise NoPermissionError( + "Nejsi uživatel, kdo vytvořil tuto anketu. Nemáš tedy nárok ji upravovat.", + interaction, + ) + if len(self.embed.fields) > 10: + raise TooManyOptionsError("Nemůžeš mít víc jak 10 možností!", interaction) + + async def add_item_to_embed(self): + from src.ui.button import ButtonBackend + + self.view.add_item( + ButtonBackend( + label=self.new_option.value, + emoji=self.EMOJIS[len(self.embed.fields)], + index=len(self.embed.fields), + poll=self.poll, + custom_id=f"{len(self.embed.fields)}:{self.poll.message_id}", + embed=self.embed, + db_poll=self.db_poll, + ) + ) + await VoteButtonDatabase(self.db_poll).add_option(self.poll, self.new_option.value) + + return self.embed.add_field( + name=f"{self.EMOJIS[len(self.embed.fields)]} {self.new_option.value}", + value="**0** | ", + inline=False, + ) diff --git a/src/ui/poll_view.py b/src/ui/poll_view.py index 2095353..1b86fc3 100644 --- a/src/ui/poll_view.py +++ b/src/ui/poll_view.py @@ -1,19 +1,21 @@ import aiomysql.pool import discord -from src.ui.button import ButtonBackend +from src.ui.button import ButtonBackend, NewOptionButton +from src.ui.embeds import PollEmbed from src.ui.poll import Poll class PollView(discord.ui.View): REACTIONS = ["1️⃣", "2️⃣", "3️⃣", "4️⃣", "5️⃣", "6️⃣", "7️⃣", "8️⃣", "9️⃣", "🔟"] - def __init__(self, poll: Poll, embed, db_poll: aiomysql.pool.Pool): + def __init__(self, poll: Poll, embed: PollEmbed, db_poll: aiomysql.pool.Pool): super().__init__(timeout=None) self.poll = poll self.embed = embed self.db_poll = db_poll self.add_buttons() + self.add_option_button() def add_buttons(self): for index, option in enumerate(self.poll.options): @@ -28,3 +30,7 @@ def add_buttons(self): ) self.add_item(button) + + def add_option_button(self): + button = NewOptionButton(self.embed, self.poll, self.db_poll) + self.add_item(button) From 3a605c12f37117c86c2ccc486edae5c0e6c54c30 Mon Sep 17 00:00:00 2001 From: robertsokola Date: Tue, 4 Jul 2023 16:22:26 +0200 Subject: [PATCH 26/35] Error messages now functions as intended, emojis functions as intended. Try to make a embed that displays whether a user added a new option Signed-off-by: robertsokola --- cogs/error.py | 29 +-------------------------- cogs/poll_command.py | 2 +- src/db_folder/databases.py | 3 ++- src/ui/button.py | 31 +++++++++++++++++++++++++---- src/ui/embeds.py | 6 ++---- src/ui/emojis.py | 3 +-- src/ui/error_view.py | 40 ++++++++++++++++++++++++++++++++++++++ src/ui/modals.py | 23 +++++++--------------- src/ui/poll_view.py | 9 ++++----- 9 files changed, 85 insertions(+), 61 deletions(-) create mode 100644 src/ui/error_view.py diff --git a/cogs/error.py b/cogs/error.py index fbded10..8b7c008 100644 --- a/cogs/error.py +++ b/cogs/error.py @@ -1,9 +1,9 @@ from discord import Interaction -from discord.app_commands import AppCommandError from discord.ext import commands from loguru import logger from src.ui.embeds import ErrorMessage +from src.ui.error_view import PrettyError class Error(commands.Cog): @@ -32,32 +32,5 @@ async def on_app_command_error(self, interaction: Interaction, error: Exception) ) -class PrettyError(AppCommandError): - """Pretty errors useful for raise keyword""" - - def __init__(self, message: str, interaction: Interaction, inner_exception: Exception | None = None): - super().__init__(interaction.command, inner_exception) - self.message = message - self.interaction = interaction - - async def send(self): - if not self.interaction.response.is_done(): - await self.interaction.response.send_message(embed=ErrorMessage(self.message)) - else: - await self.interaction.followup.send(embed=ErrorMessage(self.message)) - - -class TooManyOptionsError(PrettyError): - pass - - -class TooFewOptionsError(PrettyError): - pass - - -class NoPermissionError(PrettyError): - pass - - async def setup(bot): await bot.add_cog(Error(bot)) diff --git a/cogs/poll_command.py b/cogs/poll_command.py index c332920..9b4b6c8 100644 --- a/cogs/poll_command.py +++ b/cogs/poll_command.py @@ -6,10 +6,10 @@ from discord.ext import commands from loguru import logger -from cogs.error import TooFewOptionsError, TooManyOptionsError from src.db_folder.databases import PollDatabase, VoteButtonDatabase from src.jachym import Jachym from src.ui.embeds import PollEmbed, PollEmbedBase +from src.ui.error_view import TooFewOptionsError, TooManyOptionsError from src.ui.poll import Poll from src.ui.poll_view import PollView diff --git a/src/db_folder/databases.py b/src/db_folder/databases.py index 64bdfd0..7588c78 100644 --- a/src/db_folder/databases.py +++ b/src/db_folder/databases.py @@ -75,7 +75,7 @@ async def fetch_all_polls(self, bot: "Jachym") -> AsyncIterator[Poll and Message sql = "SELECT * FROM `Poll`" polls = await self.fetch_all_values(sql) - for message_id, channel_id, question, date, _ in polls: + for message_id, channel_id, question, date, user_id in polls: try: message = await bot.get_partial_messageable(channel_id).fetch_message( message_id, @@ -94,6 +94,7 @@ async def fetch_all_polls(self, bot: "Jachym") -> AsyncIterator[Poll and Message question=question, date_created=date, options=options, + user_id=user_id, ) yield pool, message diff --git a/src/ui/button.py b/src/ui/button.py index f8a34ab..9eb79b2 100644 --- a/src/ui/button.py +++ b/src/ui/button.py @@ -1,10 +1,9 @@ import aiomysql.pool import discord -from cogs.error import TooManyOptionsError from src.db_folder.databases import VoteButtonDatabase from src.ui.embeds import PollEmbed -from src.ui.emojis import EssentialEmojis, ScoutEmojis +from src.ui.emojis import ScoutEmojis from src.ui.modals import NewOptionModal from src.ui.poll import Poll @@ -53,7 +52,7 @@ async def toggle_vote(self, interaction: discord.Interaction) -> set[str]: await vote_button_db.remove_user(self.poll, user, self.index) users_id.remove(user) - return {interaction.guild.get_member(user_id).display_name for user_id in users_id} + return {interaction.guild.get_member(user_id).mention for user_id in users_id} async def edit_embed(self, members: set[str]) -> discord.Embed: return self.embed.set_field_at( @@ -67,7 +66,6 @@ async def callback(self, interaction: discord.Interaction): members = await self.toggle_vote(interaction) edited_embed = await self.edit_embed(members) - await interaction.response.edit_message(embed=edited_embed) @@ -87,5 +85,30 @@ def __init__(self, embed: PollEmbed, poll: Poll, db_pool: aiomysql.pool.Pool): ) async def callback(self, interaction: discord.Interaction): + await self.interaction_check(interaction) + modal = NewOptionModal(self.embed, self.db_pool, self.poll, self.view) await interaction.response.send_modal(modal) + + async def interaction_check(self, interaction: discord.Interaction) -> PermissionError | ValueError | None: + """ + This function does error handling for pressing the button, before anything shows. Unfortunately it can't + use PrettyError() class, because components derived from Item() class has errors silently passed. This means + that we should use errors derived from Exception() class instead. + + Parameters + ---------- + interaction: discord.Interaction + + Raises + ------- + PermissionError, ValueError + """ + if not self.poll.user_id == interaction.user.id: + raise PermissionError( + "Nejsi uživatel, kdo vytvořil tuto anketu. Nemáš tedy nárok ji upravovat.", + ) + if len(self.embed.fields) >= 10: + raise ValueError("Nemůžeš mít víc jak 10 možností!") + + return None diff --git a/src/ui/embeds.py b/src/ui/embeds.py index 59f6690..43d38de 100644 --- a/src/ui/embeds.py +++ b/src/ui/embeds.py @@ -5,7 +5,7 @@ import discord from discord.colour import Color, Colour -from src.ui.emojis import ScoutEmojis +from src.ui.emojis import NUMBER_EMOJIS, ScoutEmojis from src.ui.poll import Poll @@ -35,8 +35,6 @@ def __init__(self, question) -> None: class PollEmbed(PollEmbedBase): - REACTIONS = ["1️⃣", "2️⃣", "3️⃣", "4️⃣", "5️⃣", "6️⃣", "7️⃣", "8️⃣", "9️⃣", "🔟"] - def __init__(self, poll: Poll): super().__init__(poll.question) self.answers = poll.options @@ -48,7 +46,7 @@ def __init__(self, poll: Poll): def _add_options(self): for index, option in enumerate(self.answers): self.add_field( - name=f"{self.REACTIONS[index]} {option}", + name=f"{NUMBER_EMOJIS[index]} {option}", value="**0** |", inline=False, ) diff --git a/src/ui/emojis.py b/src/ui/emojis.py index f5ce470..7e38c59 100644 --- a/src/ui/emojis.py +++ b/src/ui/emojis.py @@ -8,5 +8,4 @@ class ScoutEmojis(Enum): SCOUTS = "<:skauti:814103051878531112>" -class EssentialEmojis(Enum): - NUMBER_EMOJIS = ["1️⃣", "2️⃣", "3️⃣", "4️⃣", "5️⃣", "6️⃣", "7️⃣", "8️⃣", "9️⃣", "🔟"] +NUMBER_EMOJIS = ["1️⃣", "2️⃣", "3️⃣", "4️⃣", "5️⃣", "6️⃣", "7️⃣", "8️⃣", "9️⃣", "🔟"] diff --git a/src/ui/error_view.py b/src/ui/error_view.py new file mode 100644 index 0000000..b6e4dfd --- /dev/null +++ b/src/ui/error_view.py @@ -0,0 +1,40 @@ +import discord +from discord import Interaction +from discord.app_commands import CommandInvokeError +from discord.ui import Item +from loguru import logger + +from src.ui.embeds import ErrorMessage + + +class PrettyError(CommandInvokeError): + """Pretty errors useful for raise keyword""" + + def __init__(self, message: str, interaction: Interaction, inner_exception: Exception | None = None): + super().__init__(interaction.command, inner_exception) + self.message = message + self.interaction = interaction + + async def send(self): + if not self.interaction.response.is_done(): + await self.interaction.response.send_message(embed=ErrorMessage(self.message), ephemeral=True) + else: + await self.interaction.followup.send(embed=ErrorMessage(self.message), ephemeral=True) + + +class ErrorView(discord.ui.View): + async def on_error(self, interaction: Interaction, error: Exception, item: Item): + logger.error(f"{item.__class__.__name__} raised an error: {str(error)}") + await interaction.response.send_message(embed=ErrorMessage(str(error)), ephemeral=True) + + +class TooManyOptionsError(PrettyError): + pass + + +class TooFewOptionsError(PrettyError): + pass + + +class NoPermissionError(PrettyError): + pass diff --git a/src/ui/modals.py b/src/ui/modals.py index eac532b..c46a25b 100644 --- a/src/ui/modals.py +++ b/src/ui/modals.py @@ -1,17 +1,16 @@ import discord -from cogs.error import TooManyOptionsError, NoPermissionError from src.db_folder.databases import VoteButtonDatabase +from src.ui.emojis import NUMBER_EMOJIS class NewOptionModal(discord.ui.Modal): - EMOJIS = ["1️⃣", "2️⃣", "3️⃣", "4️⃣", "5️⃣", "6️⃣", "7️⃣", "8️⃣", "9️⃣", "🔟"] - def __init__(self, embed, db_poll, poll, view): super().__init__(title="Nová možnost") self.new_option = discord.ui.TextInput( label="Jméno nové možnosti", + min_length=2, max_length=255, required=True, placeholder="Vymysli príma otázku!", @@ -26,25 +25,17 @@ def __init__(self, embed, db_poll, poll, view): self.view = view async def on_submit(self, interaction: discord.Interaction): - em = await self.add_item_to_embed() + em = await self.add_item_to_embed(interaction) await interaction.response.edit_message(embed=em, view=self.view) - async def interaction_check(self, interaction: discord.Interaction): - if not self.poll.user_id == interaction.user.id: - raise NoPermissionError( - "Nejsi uživatel, kdo vytvořil tuto anketu. Nemáš tedy nárok ji upravovat.", - interaction, - ) - if len(self.embed.fields) > 10: - raise TooManyOptionsError("Nemůžeš mít víc jak 10 možností!", interaction) - - async def add_item_to_embed(self): + async def add_item_to_embed(self, interaction: discord.Interaction): + # To avoid circular import from src.ui.button import ButtonBackend self.view.add_item( ButtonBackend( label=self.new_option.value, - emoji=self.EMOJIS[len(self.embed.fields)], + emoji=NUMBER_EMOJIS[len(self.embed.fields)], index=len(self.embed.fields), poll=self.poll, custom_id=f"{len(self.embed.fields)}:{self.poll.message_id}", @@ -55,7 +46,7 @@ async def add_item_to_embed(self): await VoteButtonDatabase(self.db_poll).add_option(self.poll, self.new_option.value) return self.embed.add_field( - name=f"{self.EMOJIS[len(self.embed.fields)]} {self.new_option.value}", + name=f"{NUMBER_EMOJIS[len(self.embed.fields)]} {self.new_option.value}", value="**0** | ", inline=False, ) diff --git a/src/ui/poll_view.py b/src/ui/poll_view.py index 1b86fc3..85da5a8 100644 --- a/src/ui/poll_view.py +++ b/src/ui/poll_view.py @@ -1,14 +1,13 @@ import aiomysql.pool -import discord from src.ui.button import ButtonBackend, NewOptionButton from src.ui.embeds import PollEmbed +from src.ui.emojis import NUMBER_EMOJIS +from src.ui.error_view import ErrorView from src.ui.poll import Poll -class PollView(discord.ui.View): - REACTIONS = ["1️⃣", "2️⃣", "3️⃣", "4️⃣", "5️⃣", "6️⃣", "7️⃣", "8️⃣", "9️⃣", "🔟"] - +class PollView(ErrorView): def __init__(self, poll: Poll, embed: PollEmbed, db_poll: aiomysql.pool.Pool): super().__init__(timeout=None) self.poll = poll @@ -22,7 +21,7 @@ def add_buttons(self): button = ButtonBackend( custom_id=f"{index}:{self.poll.message_id}", label=f"{option}", - emoji=self.REACTIONS[index], + emoji=NUMBER_EMOJIS[index], poll=self.poll, embed=self.embed, index=index, From d3cd4c41eeca912e1c51b140499c5fb2a85c6ce2 Mon Sep 17 00:00:00 2001 From: robertsokola Date: Wed, 5 Jul 2023 10:51:08 +0200 Subject: [PATCH 27/35] Added type-hints Signed-off-by: robertsokola --- src/ui/button.py | 28 ++++++++++++++-------------- src/ui/embeds.py | 4 ++++ src/ui/error_view.py | 16 +++++++++------- src/ui/modals.py | 13 ++++++++----- src/ui/poll.py | 4 +--- src/ui/poll_view.py | 2 ++ 6 files changed, 38 insertions(+), 29 deletions(-) diff --git a/src/ui/button.py b/src/ui/button.py index 9eb79b2..027c1dc 100644 --- a/src/ui/button.py +++ b/src/ui/button.py @@ -1,5 +1,6 @@ import aiomysql.pool import discord +from discord import InteractionResponse, Member from src.db_folder.databases import VoteButtonDatabase from src.ui.embeds import PollEmbed @@ -39,7 +40,7 @@ def __init__( def index(self): return self._index - async def toggle_vote(self, interaction: discord.Interaction) -> set[str]: + async def toggle_vote(self, interaction: discord.Interaction) -> set[Member]: vote_button_db = VoteButtonDatabase(self.db_poll) user = interaction.user.id @@ -52,21 +53,21 @@ async def toggle_vote(self, interaction: discord.Interaction) -> set[str]: await vote_button_db.remove_user(self.poll, user, self.index) users_id.remove(user) - return {interaction.guild.get_member(user_id).mention for user_id in users_id} + return {interaction.guild.get_member(user_id) for user_id in users_id} - async def edit_embed(self, members: set[str]) -> discord.Embed: + async def edit_embed(self, members: set[Member]) -> discord.Embed: return self.embed.set_field_at( index=self.index, name=self.embed.fields[self.index].name, - value=f"**{len(members)}** | {', '.join(members)}", + value=f"**{len(members)}** | {', '.join(member.mention for member in members)}", inline=False, ) - async def callback(self, interaction: discord.Interaction): + async def callback(self, interaction: discord.Interaction) -> InteractionResponse: members = await self.toggle_vote(interaction) edited_embed = await self.edit_embed(members) - await interaction.response.edit_message(embed=edited_embed) + return await interaction.response.edit_message(embed=edited_embed) class NewOptionButton(discord.ui.Button): @@ -84,11 +85,11 @@ def __init__(self, embed: PollEmbed, poll: Poll, db_pool: aiomysql.pool.Pool): row=4, ) - async def callback(self, interaction: discord.Interaction): + async def callback(self, interaction: discord.Interaction) -> InteractionResponse: await self.interaction_check(interaction) modal = NewOptionModal(self.embed, self.db_pool, self.poll, self.view) - await interaction.response.send_modal(modal) + return await interaction.response.send_modal(modal) async def interaction_check(self, interaction: discord.Interaction) -> PermissionError | ValueError | None: """ @@ -104,11 +105,10 @@ async def interaction_check(self, interaction: discord.Interaction) -> Permissio ------- PermissionError, ValueError """ - if not self.poll.user_id == interaction.user.id: - raise PermissionError( - "Nejsi uživatel, kdo vytvořil tuto anketu. Nemáš tedy nárok ji upravovat.", - ) + if self.poll.user_id != interaction.user.id: + msg = "Nejsi uživatel, kdo vytvořil tuto anketu. Nemáš tedy nárok ji upravovat." + raise PermissionError(msg) if len(self.embed.fields) >= 10: - raise ValueError("Nemůžeš mít víc jak 10 možností!") - + msg = "Nemůžeš mít víc jak 10 možností!" + raise ValueError(msg) return None diff --git a/src/ui/embeds.py b/src/ui/embeds.py index 43d38de..adbd818 100644 --- a/src/ui/embeds.py +++ b/src/ui/embeds.py @@ -10,6 +10,8 @@ class ErrorMessage(discord.Embed): + """Whether an error occurs, this embed is sent.""" + def __init__(self, message: str): title = "⚠️ Jejda, někde se stala chyba..." @@ -35,6 +37,8 @@ def __init__(self, question) -> None: class PollEmbed(PollEmbedBase): + """Base Embed view for Poll objects.""" + def __init__(self, poll: Poll): super().__init__(poll.question) self.answers = poll.options diff --git a/src/ui/error_view.py b/src/ui/error_view.py index b6e4dfd..b6725f9 100644 --- a/src/ui/error_view.py +++ b/src/ui/error_view.py @@ -1,5 +1,5 @@ import discord -from discord import Interaction +from discord import Interaction, InteractionResponse from discord.app_commands import CommandInvokeError from discord.ui import Item from loguru import logger @@ -15,26 +15,28 @@ def __init__(self, message: str, interaction: Interaction, inner_exception: Exce self.message = message self.interaction = interaction - async def send(self): + async def send(self) -> InteractionResponse: if not self.interaction.response.is_done(): - await self.interaction.response.send_message(embed=ErrorMessage(self.message), ephemeral=True) - else: - await self.interaction.followup.send(embed=ErrorMessage(self.message), ephemeral=True) + return await self.interaction.response.send_message(embed=ErrorMessage(self.message), ephemeral=True) + return await self.interaction.followup.send(embed=ErrorMessage(self.message), ephemeral=True) class ErrorView(discord.ui.View): - async def on_error(self, interaction: Interaction, error: Exception, item: Item): + async def on_error(self, interaction: Interaction, error: Exception, item: Item) -> InteractionResponse: logger.error(f"{item.__class__.__name__} raised an error: {str(error)}") - await interaction.response.send_message(embed=ErrorMessage(str(error)), ephemeral=True) + return await interaction.response.send_message(embed=ErrorMessage(str(error)), ephemeral=True) class TooManyOptionsError(PrettyError): + "Whether the view has too many options." pass class TooFewOptionsError(PrettyError): + "Whether the view has too few options." pass class NoPermissionError(PrettyError): + "Whether the user has no permissions to edit." pass diff --git a/src/ui/modals.py b/src/ui/modals.py index c46a25b..00aa0f1 100644 --- a/src/ui/modals.py +++ b/src/ui/modals.py @@ -1,16 +1,19 @@ +import aiomysql import discord from src.db_folder.databases import VoteButtonDatabase +from src.ui.embeds import PollEmbed from src.ui.emojis import NUMBER_EMOJIS +from src.ui.poll import Poll class NewOptionModal(discord.ui.Modal): - def __init__(self, embed, db_poll, poll, view): - super().__init__(title="Nová možnost") + def __init__(self, embed: PollEmbed, db_poll: aiomysql.pool.Pool, poll: Poll, view): + super().__init__(title="Přidání nové možnosti do ankety") self.new_option = discord.ui.TextInput( label="Jméno nové možnosti", - min_length=2, + min_length=1, max_length=255, required=True, placeholder="Vymysli príma otázku!", @@ -25,10 +28,10 @@ def __init__(self, embed, db_poll, poll, view): self.view = view async def on_submit(self, interaction: discord.Interaction): - em = await self.add_item_to_embed(interaction) + em = await self.add_item_to_embed() await interaction.response.edit_message(embed=em, view=self.view) - async def add_item_to_embed(self, interaction: discord.Interaction): + async def add_item_to_embed(self) -> PollEmbed: # To avoid circular import from src.ui.button import ButtonBackend diff --git a/src/ui/poll.py b/src/ui/poll.py index 834c153..4512df9 100644 --- a/src/ui/poll.py +++ b/src/ui/poll.py @@ -2,9 +2,7 @@ class Poll: - """ - Slot class for each Pool object. - """ + """Slot class for each Pool object.""" MAX_OPTIONS = 10 MIN_OPTIONS = 2 diff --git a/src/ui/poll_view.py b/src/ui/poll_view.py index 85da5a8..5b21694 100644 --- a/src/ui/poll_view.py +++ b/src/ui/poll_view.py @@ -8,6 +8,8 @@ class PollView(ErrorView): + """Poll View to add buttons to.""" + def __init__(self, poll: Poll, embed: PollEmbed, db_poll: aiomysql.pool.Pool): super().__init__(timeout=None) self.poll = poll From 3958343f41aeb487458e8840ff43b573f75691d9 Mon Sep 17 00:00:00 2001 From: robertsokola Date: Thu, 27 Jul 2023 15:28:07 +0200 Subject: [PATCH 28/35] Added a function that converts a plain text into datetime object. Next commit should aim for tasks.loop and database overlay fixes. Signed-off-by: robertsokola --- cogs/poll_command.py | 28 ++++++++++++++++++++++++++-- requirements.txt | 3 ++- src/ui/error_view.py | 5 +++++ 3 files changed, 33 insertions(+), 3 deletions(-) diff --git a/cogs/poll_command.py b/cogs/poll_command.py index 9b4b6c8..341a119 100644 --- a/cogs/poll_command.py +++ b/cogs/poll_command.py @@ -1,5 +1,7 @@ +import datetime import re +import dateparser import discord from discord import app_commands from discord.app_commands import Transform, Transformer @@ -9,7 +11,7 @@ from src.db_folder.databases import PollDatabase, VoteButtonDatabase from src.jachym import Jachym from src.ui.embeds import PollEmbed, PollEmbedBase -from src.ui.error_view import TooFewOptionsError, TooManyOptionsError +from src.ui.error_view import DatetimeNotRecognizedError, TooFewOptionsError, TooManyOptionsError from src.ui.poll import Poll from src.ui.poll_view import PollView @@ -46,6 +48,21 @@ async def transform( return answers +class DatetimeTransformer(Transformer): + async def transform(self, interaction: discord.Interaction, date_time: str) -> datetime.datetime: + parsed_datetime = dateparser.parse( + date_time, + languages=["cs", "en", "sk"], + ) + if not parsed_datetime: + msg = "Daný datum jsem bohužel nerozpoznal, zkusíš to znova?" + raise DatetimeNotRecognizedError(msg, interaction) + if parsed_datetime < datetime.datetime.now(): + msg = "Datum nemůžeš zakládat v minulosti!" + raise DatetimeNotRecognizedError(msg, interaction) + return parsed_datetime + + class PollCreate(commands.Cog): def __init__(self, bot: Jachym): self.bot = bot @@ -54,16 +71,22 @@ def __init__(self, bot: Jachym): name="anketa", description="Anketa pro hlasování. Jsou vidět všichni hlasovatelé.", ) - @app_commands.rename(question="otázka", answer="odpovědi") + @app_commands.rename( + question="otázka", + answer="odpovědi", + date_time="datum", + ) @app_commands.describe( question="Otázka, kterou chceš položit.", answer='Odpovědi, rozděluješ odpovědi uvozovkou ("), maximálně pouze 10 možností', + date_time="Den, na který anketa skončí.", ) async def pool( self, interaction: discord.Interaction, question: str, answer: Transform[list[str, ...], OptionsTransformer], + date_time: Transform[datetime.datetime, DatetimeTransformer] | None, ) -> discord.Message: await interaction.response.send_message(embed=PollEmbedBase("Nahrávám anketu...")) message = await interaction.original_response() @@ -74,6 +97,7 @@ async def pool( question=question, options=answer, user_id=interaction.user.id, + date_created=date_time, ) embed = PollEmbed(poll) diff --git a/requirements.txt b/requirements.txt index 79fb711..86f16d1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,4 +2,5 @@ python-dotenv==0.17.1 aiomysql>=0.0.22 pytest>=7.3.1 -loguru>=0.7.0 \ No newline at end of file +loguru>=0.7.0 +dateparser>=1.1.8 \ No newline at end of file diff --git a/src/ui/error_view.py b/src/ui/error_view.py index b6725f9..2d2df06 100644 --- a/src/ui/error_view.py +++ b/src/ui/error_view.py @@ -40,3 +40,8 @@ class TooFewOptionsError(PrettyError): class NoPermissionError(PrettyError): "Whether the user has no permissions to edit." pass + + +class DatetimeNotRecognizedError(PrettyError): + "Whether the datetime is not recognized." + pass From 1904c99ee282756defd69f6a1cf380fafe24d5c0 Mon Sep 17 00:00:00 2001 From: Martin <81148675+Martian-0007@users.noreply.github.com> Date: Thu, 27 Jul 2023 16:17:24 +0200 Subject: [PATCH 29/35] Updated poll texts --- cogs/poll_command.py | 24 +++++++++++++++++------ src/UNKNOWN.egg-info/PKG-INFO | 4 ++++ src/UNKNOWN.egg-info/SOURCES.txt | 19 ++++++++++++++++++ src/UNKNOWN.egg-info/dependency_links.txt | 1 + src/UNKNOWN.egg-info/top_level.txt | 6 ++++++ 5 files changed, 48 insertions(+), 6 deletions(-) create mode 100644 src/UNKNOWN.egg-info/PKG-INFO create mode 100644 src/UNKNOWN.egg-info/SOURCES.txt create mode 100644 src/UNKNOWN.egg-info/dependency_links.txt create mode 100644 src/UNKNOWN.egg-info/top_level.txt diff --git a/cogs/poll_command.py b/cogs/poll_command.py index 341a119..718a062 100644 --- a/cogs/poll_command.py +++ b/cogs/poll_command.py @@ -43,7 +43,7 @@ async def transform( msg = f"Zadal jsi příliš mnoho odpovědí, můžeš maximálně {Poll.MAX_OPTIONS}!" raise TooManyOptionsError(msg, interaction) if len(answers) < Poll.MIN_OPTIONS: - msg = f"Zadal jsi příliš málo odpovědí, můžeš alespoň {Poll.MIN_OPTIONS}!" + msg = f"Zadal jsi příliš málo odpovědí, zadej alespoň {Poll.MIN_OPTIONS}!" raise TooFewOptionsError(msg, interaction) return answers @@ -64,12 +64,24 @@ async def transform(self, interaction: discord.Interaction, date_time: str) -> d class PollCreate(commands.Cog): + POLL_PARAMETERS = { + "name": "anketa", + "description": "Anketa pro hlasování. Jsou vidět všichni, kteří se zapojili.", + "question": "Otázka, na kterou potřebuješ znát odpověď", + "answers": f'Odpovědi, odpovědi rozděluj uvozovkami ("), maximálně až {Poll.MAX_OPTIONS} možností', + "date_time": "Datum, kdy anketa skončí", + "help": + f""" + Jednoduchá anketa, která obsahuje otázku a odpovědi. Povoleno je až {Poll.MAX_OPTIONS} možností. + """ + } + def __init__(self, bot: Jachym): self.bot = bot @app_commands.command( - name="anketa", - description="Anketa pro hlasování. Jsou vidět všichni hlasovatelé.", + name=POLL_PARAMETERS["name"], + description=POLL_PARAMETERS["description"], ) @app_commands.rename( question="otázka", @@ -77,9 +89,9 @@ def __init__(self, bot: Jachym): date_time="datum", ) @app_commands.describe( - question="Otázka, kterou chceš položit.", - answer='Odpovědi, rozděluješ odpovědi uvozovkou ("), maximálně pouze 10 možností', - date_time="Den, na který anketa skončí.", + question=POLL_PARAMETERS["question"], + answer=POLL_PARAMETERS["answers"], + date_time=POLL_PARAMETERS["date_time"], ) async def pool( self, diff --git a/src/UNKNOWN.egg-info/PKG-INFO b/src/UNKNOWN.egg-info/PKG-INFO new file mode 100644 index 0000000..527a06f --- /dev/null +++ b/src/UNKNOWN.egg-info/PKG-INFO @@ -0,0 +1,4 @@ +Metadata-Version: 2.1 +Name: UNKNOWN +Version: 0.0.0 +License-File: LICENSE diff --git a/src/UNKNOWN.egg-info/SOURCES.txt b/src/UNKNOWN.egg-info/SOURCES.txt new file mode 100644 index 0000000..b92166a --- /dev/null +++ b/src/UNKNOWN.egg-info/SOURCES.txt @@ -0,0 +1,19 @@ +LICENSE +pyproject.toml +src/__init__.py +src/helpers.py +src/jachym.py +src/UNKNOWN.egg-info/PKG-INFO +src/UNKNOWN.egg-info/SOURCES.txt +src/UNKNOWN.egg-info/dependency_links.txt +src/UNKNOWN.egg-info/top_level.txt +src/db_folder/databases.py +src/ui/__init__.py +src/ui/button.py +src/ui/embeds.py +src/ui/emojis.py +src/ui/error_view.py +src/ui/modals.py +src/ui/poll.py +src/ui/poll_view.py +tests/test_pool.py \ No newline at end of file diff --git a/src/UNKNOWN.egg-info/dependency_links.txt b/src/UNKNOWN.egg-info/dependency_links.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/UNKNOWN.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/src/UNKNOWN.egg-info/top_level.txt b/src/UNKNOWN.egg-info/top_level.txt new file mode 100644 index 0000000..8e47b64 --- /dev/null +++ b/src/UNKNOWN.egg-info/top_level.txt @@ -0,0 +1,6 @@ +__init__ +db_folder +helpers +jachym +text_json +ui From 3791384e22360bcf6902c86f8db45a232f7f7fe7 Mon Sep 17 00:00:00 2001 From: Martin <81148675+Martian-0007@users.noreply.github.com> Date: Thu, 27 Jul 2023 16:25:39 +0200 Subject: [PATCH 30/35] Update poll_command.py oprava pomoci black --- cogs/poll_command.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/cogs/poll_command.py b/cogs/poll_command.py index 718a062..e339412 100644 --- a/cogs/poll_command.py +++ b/cogs/poll_command.py @@ -70,10 +70,9 @@ class PollCreate(commands.Cog): "question": "Otázka, na kterou potřebuješ znát odpověď", "answers": f'Odpovědi, odpovědi rozděluj uvozovkami ("), maximálně až {Poll.MAX_OPTIONS} možností', "date_time": "Datum, kdy anketa skončí", - "help": - f""" + "help": f""" Jednoduchá anketa, která obsahuje otázku a odpovědi. Povoleno je až {Poll.MAX_OPTIONS} možností. - """ + """, } def __init__(self, bot: Jachym): From a2a992d57ff9978753f5ec3fbb622ae406ce4c3c Mon Sep 17 00:00:00 2001 From: robertsokola Date: Fri, 25 Aug 2023 23:52:45 +0200 Subject: [PATCH 31/35] Changes: - fixed bug with databases where two people clicking at the same time could break entire bot - added new functionality with looping, where the bot can check if the embed is due date - added new transformer for transferring dates, it's a little more dumbproof than previous versions - added new error handling... more beautiful! Signed-off-by: robertsokola --- .gitignore | 2 + cogs/poll_command.py | 102 +++++++++++++---------------------------- src/jachym.py | 4 +- src/ui/button.py | 7 ++- src/ui/embeds.py | 13 +++++- src/ui/poll.py | 6 +-- src/ui/transformers.py | 67 +++++++++++++++++++++++++++ tests/test_pool.py | 2 +- 8 files changed, 123 insertions(+), 80 deletions(-) create mode 100644 src/ui/transformers.py diff --git a/.gitignore b/.gitignore index 6c92f07..0718b0e 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,5 @@ __pycache__ /discord.log .DS_STORE .vscode +.ruff_cache +.pytest_cache diff --git a/cogs/poll_command.py b/cogs/poll_command.py index e339412..5932ac9 100644 --- a/cogs/poll_command.py +++ b/cogs/poll_command.py @@ -1,86 +1,27 @@ +import asyncio import datetime -import re -import dateparser import discord from discord import app_commands -from discord.app_commands import Transform, Transformer -from discord.ext import commands +from discord.app_commands import Transform +from discord.ext import commands, tasks from loguru import logger from src.db_folder.databases import PollDatabase, VoteButtonDatabase from src.jachym import Jachym from src.ui.embeds import PollEmbed, PollEmbedBase -from src.ui.error_view import DatetimeNotRecognizedError, TooFewOptionsError, TooManyOptionsError from src.ui.poll import Poll from src.ui.poll_view import PollView - - -class OptionsTransformer(Transformer): - async def transform( - self, interaction: discord.Interaction, option: str - ) -> TooManyOptionsError | TooFewOptionsError | list[str]: - """ - Transformer method to transformate a single string to multiple options. If they are not within parameters, - raises an error, else returns options. - - Parameters - ---------- - interaction: discord.Interaction - option: str - - Returns - ------- - List of strings - - Raises: - ------- - TooManyOptionsError, TooFewOptionsError - - """ - answers = [option for option in re.split('"|"|“|„', option) if option.strip()] - if len(answers) > Poll.MAX_OPTIONS: - msg = f"Zadal jsi příliš mnoho odpovědí, můžeš maximálně {Poll.MAX_OPTIONS}!" - raise TooManyOptionsError(msg, interaction) - if len(answers) < Poll.MIN_OPTIONS: - msg = f"Zadal jsi příliš málo odpovědí, zadej alespoň {Poll.MIN_OPTIONS}!" - raise TooFewOptionsError(msg, interaction) - return answers - - -class DatetimeTransformer(Transformer): - async def transform(self, interaction: discord.Interaction, date_time: str) -> datetime.datetime: - parsed_datetime = dateparser.parse( - date_time, - languages=["cs", "en", "sk"], - ) - if not parsed_datetime: - msg = "Daný datum jsem bohužel nerozpoznal, zkusíš to znova?" - raise DatetimeNotRecognizedError(msg, interaction) - if parsed_datetime < datetime.datetime.now(): - msg = "Datum nemůžeš zakládat v minulosti!" - raise DatetimeNotRecognizedError(msg, interaction) - return parsed_datetime +from src.ui.transformers import DatetimeTransformer, OptionsTransformer class PollCreate(commands.Cog): - POLL_PARAMETERS = { - "name": "anketa", - "description": "Anketa pro hlasování. Jsou vidět všichni, kteří se zapojili.", - "question": "Otázka, na kterou potřebuješ znát odpověď", - "answers": f'Odpovědi, odpovědi rozděluj uvozovkami ("), maximálně až {Poll.MAX_OPTIONS} možností', - "date_time": "Datum, kdy anketa skončí", - "help": f""" - Jednoduchá anketa, která obsahuje otázku a odpovědi. Povoleno je až {Poll.MAX_OPTIONS} možností. - """, - } - def __init__(self, bot: Jachym): self.bot = bot @app_commands.command( - name=POLL_PARAMETERS["name"], - description=POLL_PARAMETERS["description"], + name="anketa", + description="Anketa pro hlasování. Jsou vidět všichni hlasovatelé.", ) @app_commands.rename( question="otázka", @@ -88,9 +29,9 @@ def __init__(self, bot: Jachym): date_time="datum", ) @app_commands.describe( - question=POLL_PARAMETERS["question"], - answer=POLL_PARAMETERS["answers"], - date_time=POLL_PARAMETERS["date_time"], + question="Otázka, kterou chceš položit.", + answer='Odpovědi, rozděluješ odpovědi uvozovkou ("), maximálně pouze 10 možností', + date_time="Den, na který anketa skončí.", ) async def pool( self, @@ -116,11 +57,34 @@ async def pool( await PollDatabase(self.bot.pool).add(poll) await VoteButtonDatabase(self.bot.pool).add_options(poll) - self.bot.active_discord_polls.add(poll) + self.bot.active_discord_polls.add((poll, message)) await self.bot.set_presence() + logger.info(f"Successfully added Pool - {message.id}") return await message.edit(embed=embed, view=view) +class PollTaskLoops(commands.Cog): + def __init__(self, bot: Jachym): + self.bot = bot + self.send_completed_pool.start() + + @tasks.loop(seconds=5) + async def send_completed_pool(self): + for poll, message in self.bot.active_discord_polls.copy(): + if poll.created_at is None or datetime.datetime.now() < poll.created_at: + continue + + embed = message.embeds[0] + embed.title = f"{embed.title[0]} [UZAVŘENO] {embed.title[1:]}" + channel = self.bot.get_channel(poll.channel_id) + await channel.send(embed=embed) + + asyncio.create_task(PollDatabase(self.bot.pool).remove(poll.message_id)) + asyncio.create_task(message.delete()) + self.bot.active_discord_polls.remove((poll, message)) + + async def setup(bot): await bot.add_cog(PollCreate(bot)) + await bot.add_cog(PollTaskLoops(bot)) diff --git a/src/jachym.py b/src/jachym.py index 37f0781..82dd17d 100644 --- a/src/jachym.py +++ b/src/jachym.py @@ -27,7 +27,7 @@ class Jachym(commands.Bot): def __init__(self) -> None: # https://discordpy.readthedocs.io/en/stable/intents.html self.pool: aiomysql.pool.Pool | None = None - self.active_discord_polls: set[Poll] = set() + self.active_discord_polls: set[tuple[Poll, discord.Message]] = set() super().__init__( command_prefix=commands.when_mentioned_or("!"), @@ -43,7 +43,7 @@ async def _fetch_pools_from_database(self) -> None: self.add_view( PollView(poll=poll, embed=message.embeds[0], db_poll=self.pool), ) - self.active_discord_polls.add(poll) + self.active_discord_polls.add((poll, message)) logger.success(f"There are now {len(self.active_discord_polls)} active pools!") diff --git a/src/ui/button.py b/src/ui/button.py index 027c1dc..3a1b5fb 100644 --- a/src/ui/button.py +++ b/src/ui/button.py @@ -1,3 +1,5 @@ +from functools import cached_property + import aiomysql.pool import discord from discord import InteractionResponse, Member @@ -7,6 +9,7 @@ from src.ui.emojis import ScoutEmojis from src.ui.modals import NewOptionModal from src.ui.poll import Poll +import asyncio class ButtonBackend(discord.ui.Button): @@ -47,10 +50,10 @@ async def toggle_vote(self, interaction: discord.Interaction) -> set[Member]: users_id = await vote_button_db.fetch_all_users(self.poll, self.index) if user not in users_id: - await vote_button_db.add_user(self.poll, user, self.index) + asyncio.create_task(vote_button_db.add_user(self.poll, user, self.index)) users_id.add(user) else: - await vote_button_db.remove_user(self.poll, user, self.index) + asyncio.create_task(vote_button_db.remove_user(self.poll, user, self.index)) users_id.remove(user) return {interaction.guild.get_member(user_id) for user_id in users_id} diff --git a/src/ui/embeds.py b/src/ui/embeds.py index adbd818..d04c8ee 100644 --- a/src/ui/embeds.py +++ b/src/ui/embeds.py @@ -1,6 +1,6 @@ import json import pathlib -from datetime import datetime +from datetime import datetime, timedelta import discord from discord.colour import Color, Colour @@ -47,6 +47,9 @@ def __init__(self, poll: Poll): self.set_footer(text="Uděláno s ♥!") self.timestamp = datetime.now() + if poll.created_at is not None: + self._add_timestamp(poll.created_at) + def _add_options(self): for index, option in enumerate(self.answers): self.add_field( @@ -55,6 +58,14 @@ def _add_options(self): inline=False, ) + def _add_timestamp(self, timestamp: datetime): + unix_time = discord.utils.format_dt(timestamp, "R") + self.add_field( + name="", + value=f"Anketa vyprší {unix_time}", + inline=False, + ) + class EmbedFromJSON(discord.Embed): PATH = pathlib.Path("src/text_json/cz_text.json") diff --git a/src/ui/poll.py b/src/ui/poll.py index 4512df9..e34c8b5 100644 --- a/src/ui/poll.py +++ b/src/ui/poll.py @@ -29,7 +29,7 @@ def __init__( self._channel_id = channel_id self._question = question self._options = options - self._date_created_at = datetime.now().strftime("%Y-%m-%d") if date_created is None else date_created + self._date_created_at = date_created self._user_id = user_id @property @@ -50,10 +50,6 @@ def options(self) -> list[str]: @property def created_at(self) -> datetime | date | None: - if isinstance(self._date_created_at, str): - return datetime.fromisoformat(self._date_created_at).date() - if isinstance(self._date_created_at, datetime): - return self._date_created_at.date() return self._date_created_at @property diff --git a/src/ui/transformers.py b/src/ui/transformers.py new file mode 100644 index 0000000..ac4aed9 --- /dev/null +++ b/src/ui/transformers.py @@ -0,0 +1,67 @@ +import datetime +import re + +import dateparser +import discord +from discord.app_commands import Transformer + +from src.ui.error_view import DatetimeNotRecognizedError, TooFewOptionsError, TooManyOptionsError +from src.ui.poll import Poll + + +class OptionsTransformer(Transformer): + async def transform( + self, interaction: discord.Interaction, option: str + ) -> TooManyOptionsError | TooFewOptionsError | list[str]: + """ + Transformer method to transformate a single string to multiple options. If they are not within parameters, + raises an error, else returns options. + + Parameters + ---------- + interaction: discord.Interaction + option: str + + Returns + ------- + List of strings + + Raises: + ------- + TooManyOptionsError, TooFewOptionsError + + """ + answers = [option for option in re.split('"|"|“|„', option) if option.strip()] + + if len(answers) > Poll.MAX_OPTIONS: + msg = f"Zadal jsi příliš mnoho odpovědí, můžeš maximálně {Poll.MAX_OPTIONS}!" + raise TooManyOptionsError(msg, interaction) + + if len(answers) < Poll.MIN_OPTIONS: + msg = f"Zadal jsi příliš málo odpovědí, můžeš alespoň {Poll.MIN_OPTIONS}!" + raise TooFewOptionsError(msg, interaction) + + return answers + + +class DatetimeTransformer(Transformer): + async def transform( + self, interaction: discord.Interaction, date_time: str + ) -> DatetimeNotRecognizedError | datetime.datetime: + parsed_datetime: datetime.datetime = dateparser.parse( + date_time, + languages=["cs", "en", "sk"], + ) + + if not parsed_datetime: + msg = "Daný datum jsem bohužel nerozpoznal, zkusíš to znova?" + raise DatetimeNotRecognizedError(msg, interaction) + + if parsed_datetime < datetime.datetime.now(): + msg = "Datum nemůžeš zakládat v minulosti!" + raise DatetimeNotRecognizedError(msg, interaction) + + if parsed_datetime.time() == "00:00:00": + parsed_datetime = parsed_datetime + datetime.timedelta(hours=12, minutes=00) + + return parsed_datetime diff --git a/tests/test_pool.py b/tests/test_pool.py index 33ff976..d27c84d 100644 --- a/tests/test_pool.py +++ b/tests/test_pool.py @@ -19,6 +19,6 @@ datetime.datetime.now().strftime("%Y-%m-%d"), ], ) -def test_datetime(date_test): +def test_datetime_now(date_test): pool = Poll(MESSAGE_ID, CHANNEL_ID, QUESTION, OPTIONS, USER_ID, date_test) assert pool.created_at == datetime.date.today() From 2ec8425d8488627bf674f2ddf1ff126f9dc23c95 Mon Sep 17 00:00:00 2001 From: robertsokola Date: Fri, 25 Aug 2023 23:54:10 +0200 Subject: [PATCH 32/35] Fixes from ruff lint. Signed-off-by: robertsokola --- src/ui/button.py | 4 ++-- src/ui/embeds.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ui/button.py b/src/ui/button.py index 3a1b5fb..65ea575 100644 --- a/src/ui/button.py +++ b/src/ui/button.py @@ -1,4 +1,5 @@ -from functools import cached_property + +import asyncio import aiomysql.pool import discord @@ -9,7 +10,6 @@ from src.ui.emojis import ScoutEmojis from src.ui.modals import NewOptionModal from src.ui.poll import Poll -import asyncio class ButtonBackend(discord.ui.Button): diff --git a/src/ui/embeds.py b/src/ui/embeds.py index d04c8ee..bf6c9f9 100644 --- a/src/ui/embeds.py +++ b/src/ui/embeds.py @@ -1,6 +1,6 @@ import json import pathlib -from datetime import datetime, timedelta +from datetime import datetime import discord from discord.colour import Color, Colour From 9f6c9ad0b09d91451ea2a64981a81ac13bd65cb3 Mon Sep 17 00:00:00 2001 From: robertsokola Date: Fri, 25 Aug 2023 23:55:41 +0200 Subject: [PATCH 33/35] black lint Signed-off-by: robertsokola --- src/ui/button.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/ui/button.py b/src/ui/button.py index 65ea575..808dce4 100644 --- a/src/ui/button.py +++ b/src/ui/button.py @@ -1,4 +1,3 @@ - import asyncio import aiomysql.pool From deb38c030bf5cba135c4da736b0d7d15dc7e37a6 Mon Sep 17 00:00:00 2001 From: robertsokola Date: Sun, 3 Sep 2023 21:19:48 +0200 Subject: [PATCH 34/35] Fixed error with tasks, fixed adding new field formatting issues. Signed-off-by: robertsokola --- cogs/poll_command.py | 15 +++++++++------ src/ui/error_view.py | 12 ++++++++---- src/ui/modals.py | 22 ++++++++++++++++------ 3 files changed, 33 insertions(+), 16 deletions(-) diff --git a/cogs/poll_command.py b/cogs/poll_command.py index 5932ac9..14ed7e2 100644 --- a/cogs/poll_command.py +++ b/cogs/poll_command.py @@ -39,7 +39,7 @@ async def pool( question: str, answer: Transform[list[str, ...], OptionsTransformer], date_time: Transform[datetime.datetime, DatetimeTransformer] | None, - ) -> discord.Message: + ): await interaction.response.send_message(embed=PollEmbedBase("Nahrávám anketu...")) message = await interaction.original_response() @@ -57,11 +57,11 @@ async def pool( await PollDatabase(self.bot.pool).add(poll) await VoteButtonDatabase(self.bot.pool).add_options(poll) - self.bot.active_discord_polls.add((poll, message)) await self.bot.set_presence() - logger.info(f"Successfully added Pool - {message.id}") - return await message.edit(embed=embed, view=view) + + await message.edit(embed=embed, view=view) + self.bot.active_discord_polls.add((poll, message)) class PollTaskLoops(commands.Cog): @@ -76,9 +76,12 @@ async def send_completed_pool(self): continue embed = message.embeds[0] - embed.title = f"{embed.title[0]} [UZAVŘENO] {embed.title[1:]}" + embed_copy = embed.copy() + embed_copy.title = f"{embed.title[0]} [UZAVŘENO] {embed.title[1:]}" + embed_copy.remove_field(len(embed_copy.fields) - 1) + channel = self.bot.get_channel(poll.channel_id) - await channel.send(embed=embed) + await channel.send(embed=embed_copy) asyncio.create_task(PollDatabase(self.bot.pool).remove(poll.message_id)) asyncio.create_task(message.delete()) diff --git a/src/ui/error_view.py b/src/ui/error_view.py index 2d2df06..cda2762 100644 --- a/src/ui/error_view.py +++ b/src/ui/error_view.py @@ -28,20 +28,24 @@ async def on_error(self, interaction: Interaction, error: Exception, item: Item) class TooManyOptionsError(PrettyError): - "Whether the view has too many options." + """Whether the view has too many options.""" + pass class TooFewOptionsError(PrettyError): - "Whether the view has too few options." + """Whether the view has too few options.""" + pass class NoPermissionError(PrettyError): - "Whether the user has no permissions to edit." + """Whether the user has no permissions to edit.""" + pass class DatetimeNotRecognizedError(PrettyError): - "Whether the datetime is not recognized." + """Whether the datetime is not recognized.""" + pass diff --git a/src/ui/modals.py b/src/ui/modals.py index 00aa0f1..b887ae0 100644 --- a/src/ui/modals.py +++ b/src/ui/modals.py @@ -35,11 +35,25 @@ async def add_item_to_embed(self) -> PollEmbed: # To avoid circular import from src.ui.button import ButtonBackend + options = { + "name": f"{NUMBER_EMOJIS[len(self.embed.fields)]} {self.new_option.value}", + "value": "**0** | ", + "inline": False, + } + + # Lmao, not ideal, but it is what it is. + if self.embed.fields[len(self.embed.fields) - 1].value.startswith("Anketa vyprší"): + index = len(self.embed.fields) - 1 + else: + index = len(self.embed.fields) + + options["index"] = index + self.view.add_item( ButtonBackend( label=self.new_option.value, emoji=NUMBER_EMOJIS[len(self.embed.fields)], - index=len(self.embed.fields), + index=index, poll=self.poll, custom_id=f"{len(self.embed.fields)}:{self.poll.message_id}", embed=self.embed, @@ -48,8 +62,4 @@ async def add_item_to_embed(self) -> PollEmbed: ) await VoteButtonDatabase(self.db_poll).add_option(self.poll, self.new_option.value) - return self.embed.add_field( - name=f"{NUMBER_EMOJIS[len(self.embed.fields)]} {self.new_option.value}", - value="**0** | ", - inline=False, - ) + return self.embed.insert_field_at(**options) From 24212d5b4bd123d48daccff94bd9eae0c34fa921 Mon Sep 17 00:00:00 2001 From: robertsokola Date: Sun, 3 Sep 2023 21:22:26 +0200 Subject: [PATCH 35/35] deleted some files, tasks optimization Signed-off-by: robertsokola --- cogs/poll_command.py | 4 ++++ src/UNKNOWN.egg-info/PKG-INFO | 4 ---- src/UNKNOWN.egg-info/SOURCES.txt | 19 ------------------- src/UNKNOWN.egg-info/dependency_links.txt | 1 - src/UNKNOWN.egg-info/top_level.txt | 6 ------ 5 files changed, 4 insertions(+), 30 deletions(-) delete mode 100644 src/UNKNOWN.egg-info/PKG-INFO delete mode 100644 src/UNKNOWN.egg-info/SOURCES.txt delete mode 100644 src/UNKNOWN.egg-info/dependency_links.txt delete mode 100644 src/UNKNOWN.egg-info/top_level.txt diff --git a/cogs/poll_command.py b/cogs/poll_command.py index 14ed7e2..a85b059 100644 --- a/cogs/poll_command.py +++ b/cogs/poll_command.py @@ -87,6 +87,10 @@ async def send_completed_pool(self): asyncio.create_task(message.delete()) self.bot.active_discord_polls.remove((poll, message)) + @send_completed_pool.before_loop + async def prepare_loop(self): + await self.bot.wait_until_ready() + async def setup(bot): await bot.add_cog(PollCreate(bot)) diff --git a/src/UNKNOWN.egg-info/PKG-INFO b/src/UNKNOWN.egg-info/PKG-INFO deleted file mode 100644 index 527a06f..0000000 --- a/src/UNKNOWN.egg-info/PKG-INFO +++ /dev/null @@ -1,4 +0,0 @@ -Metadata-Version: 2.1 -Name: UNKNOWN -Version: 0.0.0 -License-File: LICENSE diff --git a/src/UNKNOWN.egg-info/SOURCES.txt b/src/UNKNOWN.egg-info/SOURCES.txt deleted file mode 100644 index b92166a..0000000 --- a/src/UNKNOWN.egg-info/SOURCES.txt +++ /dev/null @@ -1,19 +0,0 @@ -LICENSE -pyproject.toml -src/__init__.py -src/helpers.py -src/jachym.py -src/UNKNOWN.egg-info/PKG-INFO -src/UNKNOWN.egg-info/SOURCES.txt -src/UNKNOWN.egg-info/dependency_links.txt -src/UNKNOWN.egg-info/top_level.txt -src/db_folder/databases.py -src/ui/__init__.py -src/ui/button.py -src/ui/embeds.py -src/ui/emojis.py -src/ui/error_view.py -src/ui/modals.py -src/ui/poll.py -src/ui/poll_view.py -tests/test_pool.py \ No newline at end of file diff --git a/src/UNKNOWN.egg-info/dependency_links.txt b/src/UNKNOWN.egg-info/dependency_links.txt deleted file mode 100644 index 8b13789..0000000 --- a/src/UNKNOWN.egg-info/dependency_links.txt +++ /dev/null @@ -1 +0,0 @@ - diff --git a/src/UNKNOWN.egg-info/top_level.txt b/src/UNKNOWN.egg-info/top_level.txt deleted file mode 100644 index 8e47b64..0000000 --- a/src/UNKNOWN.egg-info/top_level.txt +++ /dev/null @@ -1,6 +0,0 @@ -__init__ -db_folder -helpers -jachym -text_json -ui