diff --git a/.github/workflows/.pre-commit-config.yaml b/.github/workflows/.pre-commit-config.yaml new file mode 100644 index 0000000..fd5d2ed --- /dev/null +++ b/.github/workflows/.pre-commit-config.yaml @@ -0,0 +1,25 @@ +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 + + - name: Black format + uses: psf/black@stable + + - name: Ruff Check + run: ruff check . + + + diff --git a/cogs/error.py b/cogs/error.py index 5d4031d..dfa12aa 100644 --- a/cogs/error.py +++ b/cogs/error.py @@ -1,6 +1,9 @@ -import logging - +from discord import Interaction +from discord.app_commands import CommandInvokeError from discord.ext import commands +from loguru import logger + +from src.ui.embeds import ErrorMessage class Error(commands.Cog): @@ -8,41 +11,48 @@ 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) + 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 commands.MissingPermissions(): - 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 _: - self.logger.critical( - f"{ctx.message.id}, {ctx.message.content} | {error}", + 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?" + ) ) - print(error) - return None - @commands.Cog.listener() - async def on_command(self, ctx: commands.Context): - self.logger.info(f"{ctx.message.id} {ctx.message.content}") + +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/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 302de02..c332920 100644 --- a/cogs/poll_command.py +++ b/cogs/poll_command.py @@ -2,9 +2,11 @@ import discord from discord import app_commands +from discord.app_commands import Transform, Transformer 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 @@ -12,26 +14,39 @@ from src.ui.poll_view import PollView -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}!" +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 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 = ['"', "”", "“", "„"] +class PollCreate(commands.Cog): def __init__(self, bot: Jachym): self.bot = bot @@ -45,29 +60,19 @@ 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: Transform[list[str, ...], OptionsTransformer], ) -> discord.Message: - await interaction.response.send_message( - embed=PollEmbedBase("Dělám na tom, vydrž!"), - ) + await interaction.response.send_message(embed=PollEmbedBase("Nahrávám anketu...")) message = await interaction.original_response() - 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/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 cfd22d2..d12e56c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,11 +1,23 @@ [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 +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 + +[tool.black] + +line-length = 120 \ No newline at end of file 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..b2f41ee 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) @@ -102,26 +105,21 @@ 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, 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]: - 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/embeds.py b/src/ui/embeds.py index 1280e04..7d05db5 100644 --- a/src/ui/embeds.py +++ b/src/ui/embeds.py @@ -1,33 +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): - def __init__(self, seconds: float): - self.seconds = round(seconds) - formatted_date = discord.utils.format_dt( - datetime.now() + timedelta(seconds=10), - "R", +class ErrorMessage(discord.Embed): + def __init__(self, message: str): + title = "⚠️ Jejda, někde se stala chyba..." + + 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 > 4: - return f"{self.seconds} sekund" - if 4 >= self.seconds > 1: - return f"{self.seconds} sekundy" - return "sekundu" - class PollEmbedBase(discord.Embed): def __init__(self, question) -> None: @@ -41,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): @@ -51,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>" 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