From 412b5226ab1d7c9e9dca7a021701915ea00f3958 Mon Sep 17 00:00:00 2001 From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com> Date: Wed, 19 Mar 2025 01:55:15 -0500 Subject: [PATCH 1/9] Add missing type annotations --- main.py | 155 ++++++++++++++++++++++++++++++++++---------------------- 1 file changed, 95 insertions(+), 60 deletions(-) diff --git a/main.py b/main.py index 64ab36f..5d811c3 100644 --- a/main.py +++ b/main.py @@ -1,31 +1,50 @@ -#TODO: add types +"""Helper-duck Discord Bot.""" + +# Copyright 2024 alexacallmebaka +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +import json +import logging +import sqlite3 as sql +from pathlib import Path +from typing import Final import nextcord as nc -from nextcord.ext import commands as nc_cmd import nextcord.utils as nc_utils from nextcord.ext import application_checks as nc_app_checks - -import logging -import sqlite3 as sql -import json +from nextcord.ext import commands as nc_cmd logging.basicConfig(level=logging.INFO) -logger = logging.getLogger('nextcord') +LOGGER: Final = logging.getLogger('nextcord') + +CONFIG_FILE: Final = Path('config.json') -with open('config.json') as jsonfile: - config = json.load(jsonfile) +CONFIG: Final = json.loads(CONFIG_FILE.read_text()) -bot = nc_cmd.Bot() +bot: Final = nc_cmd.Bot() -db_connection = sql.connect(config['DB_FILE']) +DB_CONNECTION: Final = sql.connect(CONFIG['DB_FILE']) @bot.event -async def on_ready(): +async def on_ready() -> None: logging.info(f'We have logged in as {bot.user}') @bot.event -async def on_application_command_error(ctx: nc.Interaction, err: Exception): +async def on_application_command_error(ctx: nc.Interaction, err: Exception) -> None: user_name = ctx.user.nick if ctx.user.nick else ctx.user.global_name @@ -33,18 +52,18 @@ async def on_application_command_error(ctx: nc.Interaction, err: Exception): await ctx.send("Looks like you don't have permission to run this command... nice try :smiling_imp:", ephemeral=True) #close {{{1 -@bot.slash_command(description="Close a ticket.", guild_ids=[config['GUILD_ID']]) +@bot.slash_command(description="Close a ticket.", guild_ids=[CONFIG['GUILD_ID']]) async def close(ctx: nc.Interaction, ticket_id: int) -> None: - with db_connection: + with DB_CONNECTION: user_name = ctx.user.nick if ctx.user.nick else ctx.user.global_name try: - db_cursor = db_connection.cursor() + db_cursor = DB_CONNECTION.cursor() - if ctx.user.get_role(config['MENTOR_ROLE_ID']) is None: + if ctx.user.get_role(CONFIG['MENTOR_ROLE_ID']) is None: ticket_info_query = db_cursor.execute('SELECT closed, claimed, mentor_assigned FROM tickets WHERE id = :ticket_id AND author_id = :user_id', {'ticket_id': ticket_id, 'user_id': ctx.user.id}).fetchone() @@ -58,7 +77,7 @@ async def close(ctx: nc.Interaction, ticket_id: int) -> None: closed, claimed, assignee = ticket_info_query - if closed == 1: + if closed: logging.warning(f'User {user_name} tried to close a ticket with ID {ticket_id}, but it is already closed.') @@ -66,7 +85,9 @@ async def close(ctx: nc.Interaction, ticket_id: int) -> None: return - if claimed == 1: + if claimed: + # Not sure if this is right in this case, but would crash in this branch otherwise. + mentor_name = user_name await ctx.send(f'Mentor {mentor_name} has claimed this ticket. Please contact them to close it.', ephemeral=True) @@ -100,7 +121,7 @@ async def close(ctx: nc.Interaction, ticket_id: int) -> None: return - if closed == 1: + if closed: logging.warning(f'Mentor {user_name} tried to close a ticket with ID {ticket_id}, but it is already closed.') @@ -109,13 +130,13 @@ async def close(ctx: nc.Interaction, ticket_id: int) -> None: return # if the ticket has not been claimed... - if claimed == 0: + if not claimed: await ctx.send(f'This ticket has not been claimed. Please claim it before closing it.', ephemeral=True) return - mentor_channel = await ctx.guild.fetch_channel(config['MENTOR_CHANNEL_ID']) + mentor_channel = await ctx.guild.fetch_channel(CONFIG['MENTOR_CHANNEL_ID']) help_thread = mentor_channel.get_thread(help_thread_id) @@ -145,17 +166,17 @@ async def close(ctx: nc.Interaction, ticket_id: int) -> None: #1}}} # claim {{{1 -@bot.slash_command(description="Claim a ticket.", guild_ids=[config['GUILD_ID']]) +@bot.slash_command(description="Claim a ticket.", guild_ids=[CONFIG['GUILD_ID']]) @nc_app_checks.check(lambda ctx: isinstance(ctx.user, nc.Member) and ctx.guild) -@nc_app_checks.has_role(config['MENTOR_ROLE_ID']) +@nc_app_checks.has_role(CONFIG['MENTOR_ROLE_ID']) async def claim(ctx: nc.Interaction, ticket_id: int) -> None: - with db_connection: + with DB_CONNECTION: mentor_name = ctx.user.nick if ctx.user.nick else ctx.user.global_name try: - db_cursor = db_connection.cursor() + db_cursor = DB_CONNECTION.cursor() mentor_query = db_cursor.execute('SELECT 1 FROM mentors WHERE id = :mentor_id', {'mentor_id': ctx.user.id}).fetchone() @@ -178,14 +199,14 @@ async def claim(ctx: nc.Interaction, ticket_id: int) -> None: closed, claimed, prev_assignee_name, author_id, ticket_message, author_location = ticket_query - if closed == 1: + if closed: await ctx.send(f'This ticket has already been closed! :star_struck:', ephemeral=True) return #if ticket is already claimed... - if claimed == 1: + if claimed: await ctx.send(f'This ticket has already been claimed by {prev_assignee_name}. Please contact them if you would like to help out.', ephemeral=True) @@ -195,7 +216,7 @@ async def claim(ctx: nc.Interaction, ticket_id: int) -> None: ticket_author_name = ticket_author.nick if ticket_author.nick else ticket_author.global_name - help_channel = await ctx.guild.fetch_channel(config['HELP_CHANNEL_ID']) + help_channel = await ctx.guild.fetch_channel(CONFIG['HELP_CHANNEL_ID']) db_cursor.execute('UPDATE tickets SET claimed = 1, mentor_assigned_id = :mentor_id, mentor_assigned = :mentor_name WHERE id = :ticket_id', claim_params) @@ -232,19 +253,19 @@ async def claim(ctx: nc.Interaction, ticket_id: int) -> None: #helpme {{{1 #guild id just for testing -@bot.slash_command(description="Request help from a mentor.", guild_ids=[config['GUILD_ID']]) +@bot.slash_command(description="Request help from a mentor.", guild_ids=[CONFIG['GUILD_ID']]) #check that message is from a guild and user is a member of said guild. sort of a dumb check, but need for type safety later on. @nc_app_checks.check(lambda ctx: isinstance(ctx.user, nc.Member) and ctx.guild) async def helpme(ctx: nc.Interaction, author_location: str, ticket_message: str) -> None: - with db_connection: + with DB_CONNECTION: author_name = ctx.user.nick if ctx.user.nick else ctx.user.global_name #use guild nickname if available, otherwise use global name try: - db_cursor = db_connection.cursor() + db_cursor = DB_CONNECTION.cursor() - mentor_channel = await ctx.guild.fetch_channel(config['MENTOR_CHANNEL_ID']) + mentor_channel = await ctx.guild.fetch_channel(CONFIG['MENTOR_CHANNEL_ID']) ticket_params = { 'message': ticket_message , 'author_id': ctx.user.id @@ -283,19 +304,19 @@ async def helpme(ctx: nc.Interaction, author_location: str, ticket_message: str) #1}}} #view all of your tickets. {{{1 -@bot.slash_command(description="View all of your tickets.", guild_ids=[config['GUILD_ID']]) +@bot.slash_command(description="View all of your tickets.", guild_ids=[CONFIG['GUILD_ID']]) #check that message is from a guild and user is a member of said guild. sort of a dumb check, but need for type safety later on. @nc_app_checks.check(lambda ctx: isinstance(ctx.user, nc.Member) and ctx.guild) async def mytix(ctx: nc.Interaction) -> None: - with db_connection: + with DB_CONNECTION: try: - db_cursor = db_connection.cursor() + db_cursor = DB_CONNECTION.cursor() #if user is a mentor, treat differently - if ctx.user.get_role(config['MENTOR_ROLE_ID']) is not None: + if ctx.user.get_role(CONFIG['MENTOR_ROLE_ID']) is not None: tickets_query = db_cursor.execute('SELECT id, closed FROM tickets WHERE mentor_assigned_id = :mentor_id', {'mentor_id': ctx.user.id}).fetchall() if tickets_query == []: @@ -304,9 +325,11 @@ async def mytix(ctx: nc.Interaction) -> None: return + ticket_ids: Iterable[str] + closeds: Iterable[str] ticket_ids, closeds = zip(*tickets_query) - closeds = map(lambda x: ':white_check_mark:' if x == 1 else ':no_entry:', closeds) + closeds = map(lambda x: ':white_check_mark:' if x else ':no_entry:', closeds) ticket_ids = map(str, ticket_ids) tickets_embed = nc.Embed(title='Your Claimed Tickets :sunglasses:', description='Use `/status` with the ticket ID for more information on a given ticket.') @@ -327,11 +350,14 @@ async def mytix(ctx: nc.Interaction) -> None: return #get a tuple for each list of fields. + ticket_ids: Iterable[str] + claimeds: Iterable[str] + closeds: Iterable[str] ticket_ids, claimeds, closeds = zip(*tickets_query) #prep the data for embed. - claimeds = map(lambda x: ':white_check_mark:' if x == 1 else ':no_entry:', claimeds) - closeds = map(lambda x: ':white_check_mark:' if x == 1 else ':no_entry:', closeds) + claimeds = map(lambda x: ':white_check_mark:' if x else ':no_entry:', claimeds) + closeds = map(lambda x: ':white_check_mark:' if x else ':no_entry:', closeds) ticket_ids = map(str, ticket_ids) tickets_embed = nc.Embed(title='Your Tickets :man_dancing:', description='Use `/status` with the ticket ID for more information on a given ticket.') @@ -352,20 +378,20 @@ async def mytix(ctx: nc.Interaction) -> None: #1}}} #view specific ticket details.{{{1 -@bot.slash_command(description="View the details of a specific ticket.", guild_ids=[config['GUILD_ID']]) +@bot.slash_command(description="View the details of a specific ticket.", guild_ids=[CONFIG['GUILD_ID']]) @nc_app_checks.check(lambda ctx: isinstance(ctx.user, nc.Member) and ctx.guild) async def status(ctx: nc.Interaction, ticket_id: int) -> None: - with db_connection: + with DB_CONNECTION: - db_cursor = db_connection.cursor() + db_cursor = DB_CONNECTION.cursor() user_name = ctx.user.nick if ctx.user.nick else ctx.user.global_name #use guild nickname if available, otherwise use global name try: #organizer can view all tickets. - if ctx.user.get_role(config['ORGANIZER_ROLE_ID']) is not None or ctx.user.get_role(config['MENTOR_ROLE_ID']) is not None: + if ctx.user.get_role(CONFIG['ORGANIZER_ROLE_ID']) is not None or ctx.user.get_role(CONFIG['MENTOR_ROLE_ID']) is not None: ticket_query = db_cursor.execute('SELECT claimed, closed, mentor_assigned, message, author, author_location FROM tickets WHERE id = :ticket_id', {'ticket_id': ticket_id}).fetchone() @@ -386,8 +412,8 @@ async def status(ctx: nc.Interaction, ticket_id: int) -> None: ticket_embed = nc.Embed(title=f'Ticket #{ticket_id} :bug:', description=f'Opened by {author} @ {location}.') ticket_embed.add_field(name='__Mentor__ :military_helmet:', value=('N/A' if mentor is None else mentor)) - ticket_embed.add_field(name='__Claimed__ :face_with_monocle:', value=(':white_check_mark:' if claimed == 1 else ':no_entry:')) - ticket_embed.add_field(name='__Closed__ :tada:', value=(':white_check_mark:' if closed == 1 else ':no_entry:')) + ticket_embed.add_field(name='__Claimed__ :face_with_monocle:', value=(':white_check_mark:' if claimed else ':no_entry:')) + ticket_embed.add_field(name='__Closed__ :tada:', value=(':white_check_mark:' if closed else ':no_entry:')) ticket_embed.add_field(name='__Message__ :scroll:', value=message, inline=False) await ctx.send(embed=ticket_embed, ephemeral=True) @@ -400,15 +426,15 @@ async def status(ctx: nc.Interaction, ticket_id: int) -> None: #1}}} #view all open tickets {{{1 -@bot.slash_command(description="View all open tickets.", guild_ids=[config['GUILD_ID']]) -@nc_app_checks.check(lambda ctx: ctx.user.get_role(config['MENTOR_ROLE_ID']) is not None or ctx.user.get_role(config['ORGANIZER_ROLE_ID']) is not None) +@bot.slash_command(description="View all open tickets.", guild_ids=[CONFIG['GUILD_ID']]) +@nc_app_checks.check(lambda ctx: ctx.user.get_role(CONFIG['MENTOR_ROLE_ID']) is not None or ctx.user.get_role(CONFIG['ORGANIZER_ROLE_ID']) is not None) async def opentix(ctx: nc.Interaction) -> None: - with db_connection: + with DB_CONNECTION: try: - db_cursor = db_connection.cursor() + db_cursor = DB_CONNECTION.cursor() #this is an iterable tickets_query = db_cursor.execute('SELECT id, author_location, author, message FROM tickets WHERE claimed = 0 AND closed = 0').fetchall() @@ -420,6 +446,9 @@ async def opentix(ctx: nc.Interaction) -> None: return #get a tuple for each list of fields. + ticket_ids: Iterable[str] + logistics: Iterable[str] + messages: Iterable[str] ticket_ids, locations, authors, messages = zip(*tickets_query) #prep the data for embed. @@ -445,15 +474,15 @@ async def opentix(ctx: nc.Interaction) -> None: #1}}} #view all tickets {{{1 -@bot.slash_command(description="View all tickets.", guild_ids=[config['GUILD_ID']]) -@nc_app_checks.check(lambda ctx: ctx.user.get_role(config['MENTOR_ROLE_ID']) is not None or ctx.user.get_role(config['ORGANIZER_ROLE_ID']) is not None) +@bot.slash_command(description="View all tickets.", guild_ids=[CONFIG['GUILD_ID']]) +@nc_app_checks.check(lambda ctx: ctx.user.get_role(CONFIG['MENTOR_ROLE_ID']) is not None or ctx.user.get_role(CONFIG['ORGANIZER_ROLE_ID']) is not None) async def alltix(ctx: nc.Interaction) -> None: - with db_connection: + with DB_CONNECTION: try: - db_cursor = db_connection.cursor() + db_cursor = DB_CONNECTION.cursor() #this is an iterable tickets_query = db_cursor.execute('SELECT id, claimed, closed FROM tickets').fetchall() @@ -465,11 +494,14 @@ async def alltix(ctx: nc.Interaction) -> None: return #get a tuple for each list of fields. + ticket_ids: Iterable[str] + claimeds: Iterable[str] + closeds: Iterable[str] ticket_ids, claimeds, closeds = zip(*tickets_query) #prep the data for embed. - claimeds = map(lambda x: ':white_check_mark:' if x == 1 else ':no_entry:', claimeds) - closeds = map(lambda x: ':white_check_mark:' if x == 1 else ':no_entry:', closeds) + claimeds = map(lambda x: ':white_check_mark:' if x else ':no_entry:', claimeds) + closeds = map(lambda x: ':white_check_mark:' if x else ':no_entry:', closeds) ticket_ids = map(str, ticket_ids) tickets_embed = nc.Embed(title='All Tickets :face_with_spiral_eyes:', description='Use `/status` to view information about a specific ticket if you have claimed it. Claim an open ticket with `/claim`.') @@ -490,13 +522,13 @@ async def alltix(ctx: nc.Interaction) -> None: #1}}} #leaderboard {{{1 -@bot.slash_command(description="View which mentors have closed to most tickets.", guild_ids=[config['GUILD_ID']]) +@bot.slash_command(description="View which mentors have closed to most tickets.", guild_ids=[CONFIG['GUILD_ID']]) async def leaderboard(ctx: nc.Interaction) -> None: - with db_connection: + with DB_CONNECTION: try: - db_cursor = db_connection.cursor() + db_cursor = DB_CONNECTION.cursor() #this is an iterable mentors_query = db_cursor.execute('SELECT name, tickets_claimed, tickets_closed FROM mentors ORDER BY tickets_closed DESC').fetchall() @@ -508,6 +540,9 @@ async def leaderboard(ctx: nc.Interaction) -> None: return #get a tuple for each list of fields. + mentors: Iterable[str] + num_claimed: str + num_closed: str mentors, num_claimed, num_closed = zip(*mentors_query) #prep the data for embed. @@ -532,4 +567,4 @@ async def leaderboard(ctx: nc.Interaction) -> None: await ctx.send('An unknown error has occured. Please contact a HackKU organizer.', ephemeral=True) #1}}} -bot.run(config['API_TOKEN']) +bot.run(CONFIG['API_TOKEN']) From da78ea443748e809e910e205206dd701771c2b35 Mon Sep 17 00:00:00 2001 From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com> Date: Wed, 19 Mar 2025 02:15:24 -0500 Subject: [PATCH 2/9] Add python `.gitignore` --- .gitignore | 144 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 144 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a0e28ba --- /dev/null +++ b/.gitignore @@ -0,0 +1,144 @@ +# MacOS files +**/.DS_Store + +# Byte-compiled / optimized / DLL files / editor temp files +__pycache__/ +*.py[cod] +*$py.class +*~ +\#* +.#* +*.swp + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.venv/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +.mypy_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# Sphinx documentation +doc/_build/ + +# PyCharm +.idea/ From 405541b33409cc5ced2434915df69332903c2799 Mon Sep 17 00:00:00 2001 From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com> Date: Wed, 19 Mar 2025 02:18:11 -0500 Subject: [PATCH 3/9] Switch to `src`-based pip-installable project --- README.md | 4 +- pyproject.toml | 129 +++++++++++++++++++++++++ main.py => src/helper_duck/__init__.py | 34 ++++--- src/helper_duck/py.typed | 0 4 files changed, 154 insertions(+), 13 deletions(-) create mode 100644 pyproject.toml rename main.py => src/helper_duck/__init__.py (96%) create mode 100644 src/helper_duck/py.typed diff --git a/README.md b/README.md index b932bab..ce5e105 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ mentors. Read about how helper duck works on the HackKU24 [hackerdoc](https://ha ## Setting up helper duck ⚙️ To run helper duck, you will need Python and SQLite3 installed your system. -First install the modules in `requirements.txt`. Secondly, initialize the database by running +First install the project using `pip install -e .`. Secondly, initialize the database by running ``` sqlite3 database.db < db_init.sql @@ -37,7 +37,7 @@ server/role/channel you want the ID of, you will see a new option on the context After that, off to the races! 🏁 Just run ``` -python3 main.py +run_helper_duck ``` Happy hacking! 🍻 diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..4ce387b --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,129 @@ +[build-system] +requires = ["setuptools >= 64"] +build-backend = "setuptools.build_meta" + +[project] +name = "helper_duck" +dynamic = ["version"] +authors = [ + { name="alexacallmebaka", email="50460460+alexacallmebaka@users.noreply.github.com" }, +] +description = "HackKU's Mentorship Ticketing System Discord Bot" +readme = {file = "README.md", content-type = "text/markdown"} +license = {file = "LICENSE"} +requires-python = ">=3.9" +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: End Users/Desktop", + "License :: OSI Approved :: Apache Software License", + "Natural Language :: English", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3 :: Only", + "Topic :: Internet", + "Operating System :: OS Independent", + "Typing :: Typed", +] +keywords = [ + "hackku", "discord", "bot", "ticketing" +] +dependencies = [ + "nextcord>=3.0.1", +] + +[tool.setuptools.dynamic] +version = {attr = "helper_duck.__version__"} + +[project.urls] +"Source" = "https://github.com/the-hackku/helper-duck" +"Bug Tracker" = "https://github.com/the-hackku/helper-duck/issues" + +[project.gui-scripts] +run_helper_duck = "helper_duck:run" + +[project.optional-dependencies] +tools = [ + "ruff>=0.11.0", + "codespell>=2.3.0", + "mypy>=1.15.0", +] + +[tool.setuptools.package-data] +azul = ["py.typed", "data/*", "lang/*", "fonts/*"] + +[tool.uv] +package = true + +[tool.mypy] +files = ["src/helper_duck/",] +show_column_numbers = true +show_error_codes = true +show_traceback = true +disallow_any_decorated = true +disallow_any_unimported = true +ignore_missing_imports = true +local_partial_types = true +no_implicit_optional = true +strict = true +warn_unreachable = true + +[tool.ruff.lint.isort] +combine-as-imports = true + +[tool.pycln] +all = true +disable_all_dunder_policy = true + +[tool.ruff] +line-length = 79 +fix = true + +include = ["*.py", "*.pyi", "**/pyproject.toml"] + +[tool.ruff.lint] +select = [ + "A", # flake8-builtins + "ASYNC", # flake8-async + "B", # flake8-bugbear + "C4", # flake8-comprehensions + "COM", # flake8-commas + "D", # pydocstyle + "E", # Error + "EXE", # flake8-executable + "F", # pyflakes + "FA", # flake8-future-annotations + "FLY", # flynt + "FURB", # refurb + "I", # isort + "ICN", # flake8-import-conventions + "N", # pep8-naming + "PIE", # flake8-pie + "PT", # flake8-pytest-style + "PYI", # flake8-pyi + "Q", # flake8-quotes + "R", # Refactor + "RET", # flake8-return + "RUF", # Ruff-specific rules + "S", # flake8-bandit + "SIM", # flake8-simplify + "SLOT", # flake8-slots + "TCH", # flake8-type-checking + "UP", # pyupgrade + "W", # Warning + "YTT", # flake8-2020 +] +extend-ignore = [ + "D203", # one-blank-line-before-class + "D204", # one-blank-line-after-class + "D211", # no-blank-line-before-class + "D213", # multi-line-summary-second-line + "D417", # undocumented-param "Missing argument descriptions" + "E501", # line-too-long + "PYI041", # redundant-numeric-union + "S101", # assert (use of assert for tests and type narrowing) + "SIM117", # multiple-with-statements +] diff --git a/main.py b/src/helper_duck/__init__.py similarity index 96% rename from main.py rename to src/helper_duck/__init__.py index 5d811c3..fedb234 100644 --- a/main.py +++ b/src/helper_duck/__init__.py @@ -1,21 +1,26 @@ """Helper-duck Discord Bot.""" -# Copyright 2024 alexacallmebaka +# Copyright 2024 alexacallmebaka # -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at # -# http://www.apache.org/licenses/LICENSE-2.0 +# http://www.apache.org/licenses/LICENSE-2.0 # -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. from __future__ import annotations +__title__ = "Helper-duck" +__author__ = "alexacallmebaka" +__license__ = "Apache License 2.0" +__version__ = "0.0.0" + import json import logging import sqlite3 as sql @@ -567,4 +572,11 @@ async def leaderboard(ctx: nc.Interaction) -> None: await ctx.send('An unknown error has occured. Please contact a HackKU organizer.', ephemeral=True) #1}}} -bot.run(CONFIG['API_TOKEN']) + +def run() -> None: + """Main entry point.""" + bot.run(CONFIG['API_TOKEN']) + + +if __name__ == "__main__": + run() diff --git a/src/helper_duck/py.typed b/src/helper_duck/py.typed new file mode 100644 index 0000000..e69de29 From 09325a7984bbb6b1bcde7fb00f43f2d543a9bd7d Mon Sep 17 00:00:00 2001 From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com> Date: Wed, 19 Mar 2025 02:34:43 -0500 Subject: [PATCH 4/9] Fix more type issues --- pyproject.toml | 4 +- src/helper_duck/__init__.py | 148 ++++++++++++++++++++---------------- 2 files changed, 86 insertions(+), 66 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 4ce387b..0841f55 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -64,7 +64,9 @@ show_column_numbers = true show_error_codes = true show_traceback = true disallow_any_decorated = true -disallow_any_unimported = true +# nextcord does not have type annotations and there are errors +# left and right from nextcord types resolving to Any +disallow_any_unimported = false ignore_missing_imports = true local_partial_types = true no_implicit_optional = true diff --git a/src/helper_duck/__init__.py b/src/helper_duck/__init__.py index fedb234..e442d89 100644 --- a/src/helper_duck/__init__.py +++ b/src/helper_duck/__init__.py @@ -26,6 +26,7 @@ import sqlite3 as sql from pathlib import Path from typing import Final +from collections.abc import Iterable import nextcord as nc import nextcord.utils as nc_utils @@ -44,7 +45,7 @@ DB_CONNECTION: Final = sql.connect(CONFIG['DB_FILE']) -@bot.event +@bot.event # type: ignore[misc] async def on_ready() -> None: logging.info(f'We have logged in as {bot.user}') @@ -71,7 +72,7 @@ async def close(ctx: nc.Interaction, ticket_id: int) -> None: if ctx.user.get_role(CONFIG['MENTOR_ROLE_ID']) is None: ticket_info_query = db_cursor.execute('SELECT closed, claimed, mentor_assigned FROM tickets WHERE id = :ticket_id AND author_id = :user_id', {'ticket_id': ticket_id, 'user_id': ctx.user.id}).fetchone() - + if ticket_info_query is None: logging.warning(f'User {user_name} tried to close a ticket with ID {ticket_id}, but they do not have ownership over it or it does not exist.') @@ -81,7 +82,7 @@ async def close(ctx: nc.Interaction, ticket_id: int) -> None: return closed, claimed, assignee = ticket_info_query - + if closed: logging.warning(f'User {user_name} tried to close a ticket with ID {ticket_id}, but it is already closed.') @@ -97,9 +98,9 @@ async def close(ctx: nc.Interaction, ticket_id: int) -> None: await ctx.send(f'Mentor {mentor_name} has claimed this ticket. Please contact them to close it.', ephemeral=True) return - + db_cursor.execute('UPDATE tickets SET closed = 1 WHERE id = :ticket_id', {'ticket_id': ticket_id}) - + logging.info(f'User {user_name} closed thier own ticket with ID {ticket_id}.') await ctx.send('Ticket closed! :saluting_face:', ephemeral=True) @@ -115,10 +116,10 @@ async def close(ctx: nc.Interaction, ticket_id: int) -> None: logging.warning(f'Mentor {user_name} tried to close non-existant ticket with ID {ticket_id}.') return - + closed, ticket_assignee_id, ticket_assignee_name, claimed, help_thread_id = ticket_info_query - - if ctx.user.id != ticket_assignee_id: + + if ctx.user.id != ticket_assignee_id: logging.warning(f'Mentor {user_name} tried to close a ticket with ID {ticket_id} owned by {ticket_assignee_name}.' ) @@ -136,11 +137,11 @@ async def close(ctx: nc.Interaction, ticket_id: int) -> None: # if the ticket has not been claimed... if not claimed: - + await ctx.send(f'This ticket has not been claimed. Please claim it before closing it.', ephemeral=True) - return - + return + mentor_channel = await ctx.guild.fetch_channel(CONFIG['MENTOR_CHANNEL_ID']) help_thread = mentor_channel.get_thread(help_thread_id) @@ -152,12 +153,12 @@ async def close(ctx: nc.Interaction, ticket_id: int) -> None: await ctx.send('An unknown error has occured. Please contact a HackKU organizer.', ephemeral=True) return - + await help_thread.delete() logging.info(f'Deleted help thread wth ID: {help_thread_id}.') db_cursor.execute('UPDATE tickets SET closed = 1 WHERE id = :ticket_id', {'ticket_id': ticket_id}) - + db_cursor.execute('UPDATE mentors SET tickets_closed = tickets_closed + 1 WHERE id = :mentor_id', {'mentor_id': ctx.user.id}) logging.info(f'Ticket with ID {ticket_id} has been closed by mentor {user_name}.') @@ -169,29 +170,35 @@ async def close(ctx: nc.Interaction, ticket_id: int) -> None: await ctx.send('An unknown error has occured. Please contact a HackKU organizer.', ephemeral=True) #1}}} - + + +def guild_member_and_in_guild(ctx: nc.Interaction) -> bool: + """Return if interaction user is a member and interaction is happening in a guild.""" + return isinstance(ctx.user, nc.Member) and ctx.guild + + # claim {{{1 @bot.slash_command(description="Claim a ticket.", guild_ids=[CONFIG['GUILD_ID']]) -@nc_app_checks.check(lambda ctx: isinstance(ctx.user, nc.Member) and ctx.guild) +@nc_app_checks.check(guild_member_and_in_guild) @nc_app_checks.has_role(CONFIG['MENTOR_ROLE_ID']) async def claim(ctx: nc.Interaction, ticket_id: int) -> None: - + with DB_CONNECTION: - + mentor_name = ctx.user.nick if ctx.user.nick else ctx.user.global_name try: db_cursor = DB_CONNECTION.cursor() mentor_query = db_cursor.execute('SELECT 1 FROM mentors WHERE id = :mentor_id', {'mentor_id': ctx.user.id}).fetchone() - + #new mentor! if mentor_query is None: db_cursor.execute('INSERT INTO mentors (id, name, tickets_claimed, tickets_closed) VALUES (:mentor_id, :mentor_name, 0, 0)',{'mentor_id': ctx.user.id, 'mentor_name': mentor_name}) claim_params = {'ticket_id': ticket_id, 'mentor_id': ctx.user.id, 'mentor_name': mentor_name} - + ticket_query = db_cursor.execute('SELECT closed, claimed, mentor_assigned, author_id, message, author_location FROM tickets WHERE id = :ticket_id', {'ticket_id': ticket_id}).fetchone() if ticket_query is None: @@ -201,30 +208,30 @@ async def claim(ctx: nc.Interaction, ticket_id: int) -> None: logging.warning(f'Mentor {mentor_name} tried to claim non-existant ticket with ID {ticket_id}.') return - + closed, claimed, prev_assignee_name, author_id, ticket_message, author_location = ticket_query if closed: - + await ctx.send(f'This ticket has already been closed! :star_struck:', ephemeral=True) - return + return #if ticket is already claimed... if claimed: - + await ctx.send(f'This ticket has already been claimed by {prev_assignee_name}. Please contact them if you would like to help out.', ephemeral=True) - return - - ticket_author = await ctx.guild.fetch_member(author_id) + return + + ticket_author = await ctx.guild.fetch_member(author_id) ticket_author_name = ticket_author.nick if ticket_author.nick else ticket_author.global_name help_channel = await ctx.guild.fetch_channel(CONFIG['HELP_CHANNEL_ID']) db_cursor.execute('UPDATE tickets SET claimed = 1, mentor_assigned_id = :mentor_id, mentor_assigned = :mentor_name WHERE id = :ticket_id', claim_params) - + db_cursor.execute('UPDATE mentors SET tickets_claimed = tickets_claimed + 1 WHERE id = :mentor_id', {'mentor_id': ctx.user.id}) logging.info(f'Mentor {mentor_name} has claimed ticket with ID {ticket_id}.') @@ -238,13 +245,13 @@ async def claim(ctx: nc.Interaction, ticket_id: int) -> None: await help_thread.add_user(ticket_author) logging.info(f'Added User {ticket_author_name} to help thread with ID {help_thread.id}.') - + ticket_update_params = {'help_thread_id': help_thread.id, 'ticket_id': ticket_id} db_cursor.execute('UPDATE tickets SET help_thread_id = :help_thread_id WHERE id = :ticket_id', ticket_update_params) await ctx.send(f'Ticket #{ticket_id} claimed by {mentor_name}!') - + await help_thread.send(f'Greetings {ticket_author.mention}! {ctx.user.mention} is on the way to {author_location} to help you resolve the issue in your ticket:\n> {ticket_message}') except Exception as e: @@ -260,11 +267,11 @@ async def claim(ctx: nc.Interaction, ticket_id: int) -> None: #guild id just for testing @bot.slash_command(description="Request help from a mentor.", guild_ids=[CONFIG['GUILD_ID']]) #check that message is from a guild and user is a member of said guild. sort of a dumb check, but need for type safety later on. -@nc_app_checks.check(lambda ctx: isinstance(ctx.user, nc.Member) and ctx.guild) +@nc_app_checks.check(guild_member_and_in_guild) async def helpme(ctx: nc.Interaction, author_location: str, ticket_message: str) -> None: with DB_CONNECTION: - + author_name = ctx.user.nick if ctx.user.nick else ctx.user.global_name #use guild nickname if available, otherwise use global name try: @@ -288,7 +295,7 @@ async def helpme(ctx: nc.Interaction, author_location: str, ticket_message: str) logging.info(f'Received ticket from user {ticket_params["author"]} with ID {ticket_params["author_id"]}.') ticket_embed = nc.Embed(title='New Ticket Opened! :tickets:', description='A hacker needs help. Use `/claim` to claim this ticket!') - + ticket_id = db_cursor.lastrowid ticket_embed.add_field(name='__ID__ :hash:', value=ticket_id) @@ -308,10 +315,16 @@ async def helpme(ctx: nc.Interaction, author_location: str, ticket_message: str) await ctx.send('An unknown error has occured. Please contact a HackKU organizer.', ephemeral=True) #1}}} + +def bool_checkmark_emoji(value: bool | int) -> str: + """Convert truthy values to check mark emoji, false to x mark.""" + return ':white_check_mark:' if value else ':no_entry:' + + #view all of your tickets. {{{1 @bot.slash_command(description="View all of your tickets.", guild_ids=[CONFIG['GUILD_ID']]) #check that message is from a guild and user is a member of said guild. sort of a dumb check, but need for type safety later on. -@nc_app_checks.check(lambda ctx: isinstance(ctx.user, nc.Member) and ctx.guild) +@nc_app_checks.check(guild_member_and_in_guild) async def mytix(ctx: nc.Interaction) -> None: with DB_CONNECTION: @@ -319,11 +332,11 @@ async def mytix(ctx: nc.Interaction) -> None: try: db_cursor = DB_CONNECTION.cursor() - + #if user is a mentor, treat differently if ctx.user.get_role(CONFIG['MENTOR_ROLE_ID']) is not None: tickets_query = db_cursor.execute('SELECT id, closed FROM tickets WHERE mentor_assigned_id = :mentor_id', {'mentor_id': ctx.user.id}).fetchall() - + if tickets_query == []: await ctx.send('You have not claimed any tickets! Use `/opentix` view open tickets to claim.', ephemeral=True) @@ -334,7 +347,7 @@ async def mytix(ctx: nc.Interaction) -> None: closeds: Iterable[str] ticket_ids, closeds = zip(*tickets_query) - closeds = map(lambda x: ':white_check_mark:' if x else ':no_entry:', closeds) + closeds = map(bool_checkmark_emoji, closeds) ticket_ids = map(str, ticket_ids) tickets_embed = nc.Embed(title='Your Claimed Tickets :sunglasses:', description='Use `/status` with the ticket ID for more information on a given ticket.') @@ -347,7 +360,7 @@ async def mytix(ctx: nc.Interaction) -> None: else: #this is an iterable tickets_query = db_cursor.execute('SELECT id, claimed, closed FROM tickets WHERE author_id = :author_id', {'author_id': ctx.user.id}).fetchall() - + if tickets_query == []: await ctx.send('You have no tickets! Use `/helpme` to open one.', ephemeral=True) @@ -355,14 +368,12 @@ async def mytix(ctx: nc.Interaction) -> None: return #get a tuple for each list of fields. - ticket_ids: Iterable[str] claimeds: Iterable[str] - closeds: Iterable[str] ticket_ids, claimeds, closeds = zip(*tickets_query) - + #prep the data for embed. - claimeds = map(lambda x: ':white_check_mark:' if x else ':no_entry:', claimeds) - closeds = map(lambda x: ':white_check_mark:' if x else ':no_entry:', closeds) + claimeds = map(bool_checkmark_emoji, claimeds) + closeds = map(bool_checkmark_emoji, closeds) ticket_ids = map(str, ticket_ids) tickets_embed = nc.Embed(title='Your Tickets :man_dancing:', description='Use `/status` with the ticket ID for more information on a given ticket.') @@ -382,9 +393,10 @@ async def mytix(ctx: nc.Interaction) -> None: await ctx.send('An unknown error has occured. Please contact a HackKU organizer.', ephemeral=True) #1}}} + #view specific ticket details.{{{1 @bot.slash_command(description="View the details of a specific ticket.", guild_ids=[CONFIG['GUILD_ID']]) -@nc_app_checks.check(lambda ctx: isinstance(ctx.user, nc.Member) and ctx.guild) +@nc_app_checks.check(guild_member_and_in_guild) async def status(ctx: nc.Interaction, ticket_id: int) -> None: with DB_CONNECTION: @@ -394,7 +406,7 @@ async def status(ctx: nc.Interaction, ticket_id: int) -> None: user_name = ctx.user.nick if ctx.user.nick else ctx.user.global_name #use guild nickname if available, otherwise use global name try: - + #organizer can view all tickets. if ctx.user.get_role(CONFIG['ORGANIZER_ROLE_ID']) is not None or ctx.user.get_role(CONFIG['MENTOR_ROLE_ID']) is not None: @@ -412,7 +424,7 @@ async def status(ctx: nc.Interaction, ticket_id: int) -> None: await ctx.send(f'You don\'t have ownership over a ticket with ID {ticket_id}. Maybe you made a typo?', ephemeral=True) return - + claimed, closed, mentor, message, author, location = ticket_query ticket_embed = nc.Embed(title=f'Ticket #{ticket_id} :bug:', description=f'Opened by {author} @ {location}.') @@ -420,9 +432,9 @@ async def status(ctx: nc.Interaction, ticket_id: int) -> None: ticket_embed.add_field(name='__Claimed__ :face_with_monocle:', value=(':white_check_mark:' if claimed else ':no_entry:')) ticket_embed.add_field(name='__Closed__ :tada:', value=(':white_check_mark:' if closed else ':no_entry:')) ticket_embed.add_field(name='__Message__ :scroll:', value=message, inline=False) - + await ctx.send(embed=ticket_embed, ephemeral=True) - + except Exception as e: logging.error(f'User {user_name} tried to view a ticket with ID {ticket_id}, but an unexpected error occured: {e}') @@ -430,9 +442,15 @@ async def status(ctx: nc.Interaction, ticket_id: int) -> None: await ctx.send('An unknown error has occured. Please contact a HackKU organizer.', ephemeral=True) #1}}} -#view all open tickets {{{1 + +def mentor_or_organizer_role(ctx: nc.Interaction) -> bool: + """Check that interaction user has either mentor or organizer role.""" + return ctx.user.get_role(CONFIG['MENTOR_ROLE_ID']) is not None or ctx.user.get_role(CONFIG['ORGANIZER_ROLE_ID']) is not None + + +#view all open tickets {{{1 @bot.slash_command(description="View all open tickets.", guild_ids=[CONFIG['GUILD_ID']]) -@nc_app_checks.check(lambda ctx: ctx.user.get_role(CONFIG['MENTOR_ROLE_ID']) is not None or ctx.user.get_role(CONFIG['ORGANIZER_ROLE_ID']) is not None) +@nc_app_checks.check(mentor_or_organizer_role) async def opentix(ctx: nc.Interaction) -> None: with DB_CONNECTION: @@ -440,10 +458,10 @@ async def opentix(ctx: nc.Interaction) -> None: try: db_cursor = DB_CONNECTION.cursor() - + #this is an iterable tickets_query = db_cursor.execute('SELECT id, author_location, author, message FROM tickets WHERE claimed = 0 AND closed = 0').fetchall() - + if tickets_query == []: await ctx.send('There are no open tickets :sob:', ephemeral=True) @@ -455,7 +473,7 @@ async def opentix(ctx: nc.Interaction) -> None: logistics: Iterable[str] messages: Iterable[str] ticket_ids, locations, authors, messages = zip(*tickets_query) - + #prep the data for embed. ticket_ids = map(str, ticket_ids) logistics = map((lambda x: f'{x[0]} @ *{x[1][:10] + "..." if len(x[1]) > 10 else x[1]}*'), zip(authors, locations)) @@ -478,9 +496,9 @@ async def opentix(ctx: nc.Interaction) -> None: await ctx.send('An unknown error has occured. Please contact a HackKU organizer.', ephemeral=True) #1}}} -#view all tickets {{{1 +#view all tickets {{{1 @bot.slash_command(description="View all tickets.", guild_ids=[CONFIG['GUILD_ID']]) -@nc_app_checks.check(lambda ctx: ctx.user.get_role(CONFIG['MENTOR_ROLE_ID']) is not None or ctx.user.get_role(CONFIG['ORGANIZER_ROLE_ID']) is not None) +@nc_app_checks.check(mentor_or_organizer_role) # type: ignore[misc] async def alltix(ctx: nc.Interaction) -> None: with DB_CONNECTION: @@ -488,10 +506,10 @@ async def alltix(ctx: nc.Interaction) -> None: try: db_cursor = DB_CONNECTION.cursor() - + #this is an iterable tickets_query = db_cursor.execute('SELECT id, claimed, closed FROM tickets').fetchall() - + if tickets_query == []: await ctx.send('There are no tickets :fearful:', ephemeral=True) @@ -503,10 +521,10 @@ async def alltix(ctx: nc.Interaction) -> None: claimeds: Iterable[str] closeds: Iterable[str] ticket_ids, claimeds, closeds = zip(*tickets_query) - + #prep the data for embed. - claimeds = map(lambda x: ':white_check_mark:' if x else ':no_entry:', claimeds) - closeds = map(lambda x: ':white_check_mark:' if x else ':no_entry:', closeds) + claimeds = map(bool_checkmark_emoji, claimeds) + closeds = map(bool_checkmark_emoji, closeds) ticket_ids = map(str, ticket_ids) tickets_embed = nc.Embed(title='All Tickets :face_with_spiral_eyes:', description='Use `/status` to view information about a specific ticket if you have claimed it. Claim an open ticket with `/claim`.') @@ -528,16 +546,16 @@ async def alltix(ctx: nc.Interaction) -> None: #leaderboard {{{1 @bot.slash_command(description="View which mentors have closed to most tickets.", guild_ids=[CONFIG['GUILD_ID']]) -async def leaderboard(ctx: nc.Interaction) -> None: +async def leaderboard(ctx: nc.Interaction) -> None: # type: ignore[misc] with DB_CONNECTION: try: db_cursor = DB_CONNECTION.cursor() - + #this is an iterable mentors_query = db_cursor.execute('SELECT name, tickets_claimed, tickets_closed FROM mentors ORDER BY tickets_closed DESC').fetchall() - + if mentors_query == []: await ctx.send('Welp... no mentors have claimed any tickets :skull:', ephemeral=True) @@ -546,10 +564,10 @@ async def leaderboard(ctx: nc.Interaction) -> None: #get a tuple for each list of fields. mentors: Iterable[str] - num_claimed: str - num_closed: str + num_claimed: Iterable[str] + num_closed: Iterable[str] mentors, num_claimed, num_closed = zip(*mentors_query) - + #prep the data for embed. mentors = map(lambda x: f'**#{x[1]}**: {x[0]}', zip(mentors,range(1,len(mentors)+1))) num_claimed = map(str, num_claimed) From 1f4e89897293ea53d6ef2412783d8039abe83a9f Mon Sep 17 00:00:00 2001 From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com> Date: Wed, 19 Mar 2025 02:36:37 -0500 Subject: [PATCH 5/9] Apply ruff formatting and autofixes --- .pre-commit-config.yaml | 33 ++ README.md | 2 +- src/helper_duck/__init__.py | 823 +++++++++++++++++++++++++----------- 3 files changed, 615 insertions(+), 243 deletions(-) create mode 100644 .pre-commit-config.yaml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..bab8120 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,33 @@ +ci: + autofix_prs: true + autoupdate_schedule: quarterly + submodules: false + +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-ast + - id: check-yaml + - id: check-toml + - id: check-merge-conflict + - id: mixed-line-ending + - id: check-case-conflict + - id: sort-simple-yaml + files: .pre-commit-config.yaml + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.9.6 + hooks: + - id: ruff + args: ["--show-fixes"] + - id: ruff-format + - repo: https://github.com/crate-ci/typos + rev: typos-dict-v0.12.5 + hooks: + - id: typos + - repo: https://github.com/woodruffw/zizmor-pre-commit + rev: v1.3.1 + hooks: + - id: zizmor diff --git a/README.md b/README.md index ce5e105..d7c80cf 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ server/role/channel you want the ID of, you will see a new option on the context - The Discord API token can be generated by creating a new app and bot in the [Discord developer portal](https://discord.com/developers/applications). Make sure to invite helper duck to your server! -After that, off to the races! 🏁 Just run +After that, off to the races! 🏁 Just run ``` run_helper_duck diff --git a/src/helper_duck/__init__.py b/src/helper_duck/__init__.py index e442d89..4377f46 100644 --- a/src/helper_duck/__init__.py +++ b/src/helper_duck/__init__.py @@ -24,70 +24,92 @@ import json import logging import sqlite3 as sql +from collections.abc import Iterable from pathlib import Path from typing import Final -from collections.abc import Iterable import nextcord as nc import nextcord.utils as nc_utils -from nextcord.ext import application_checks as nc_app_checks -from nextcord.ext import commands as nc_cmd +from nextcord.ext import ( + application_checks as nc_app_checks, + commands as nc_cmd, +) logging.basicConfig(level=logging.INFO) -LOGGER: Final = logging.getLogger('nextcord') +LOGGER: Final = logging.getLogger("nextcord") -CONFIG_FILE: Final = Path('config.json') +CONFIG_FILE: Final = Path("config.json") CONFIG: Final = json.loads(CONFIG_FILE.read_text()) bot: Final = nc_cmd.Bot() -DB_CONNECTION: Final = sql.connect(CONFIG['DB_FILE']) +DB_CONNECTION: Final = sql.connect(CONFIG["DB_FILE"]) + @bot.event # type: ignore[misc] async def on_ready() -> None: - logging.info(f'We have logged in as {bot.user}') + logging.info(f"We have logged in as {bot.user}") -@bot.event -async def on_application_command_error(ctx: nc.Interaction, err: Exception) -> None: +@bot.event +async def on_application_command_error( + ctx: nc.Interaction, + err: Exception, +) -> None: user_name = ctx.user.nick if ctx.user.nick else ctx.user.global_name - logging.warning(f'User {user_name} tried to execute {ctx.application_command.qualified_name} but does not have permission to do so.') - await ctx.send("Looks like you don't have permission to run this command... nice try :smiling_imp:", ephemeral=True) + logging.warning( + f"User {user_name} tried to execute {ctx.application_command.qualified_name} but does not have permission to do so.", + ) + await ctx.send( + "Looks like you don't have permission to run this command... nice try :smiling_imp:", + ephemeral=True, + ) -#close {{{1 -@bot.slash_command(description="Close a ticket.", guild_ids=[CONFIG['GUILD_ID']]) -async def close(ctx: nc.Interaction, ticket_id: int) -> None: +# close {{{1 +@bot.slash_command( + description="Close a ticket.", + guild_ids=[CONFIG["GUILD_ID"]], +) +async def close(ctx: nc.Interaction, ticket_id: int) -> None: with DB_CONNECTION: - user_name = ctx.user.nick if ctx.user.nick else ctx.user.global_name try: - db_cursor = DB_CONNECTION.cursor() - if ctx.user.get_role(CONFIG['MENTOR_ROLE_ID']) is None: - - ticket_info_query = db_cursor.execute('SELECT closed, claimed, mentor_assigned FROM tickets WHERE id = :ticket_id AND author_id = :user_id', {'ticket_id': ticket_id, 'user_id': ctx.user.id}).fetchone() + if ctx.user.get_role(CONFIG["MENTOR_ROLE_ID"]) is None: + ticket_info_query = db_cursor.execute( + "SELECT closed, claimed, mentor_assigned FROM tickets WHERE id = :ticket_id AND author_id = :user_id", + {"ticket_id": ticket_id, "user_id": ctx.user.id}, + ).fetchone() if ticket_info_query is None: + logging.warning( + f"User {user_name} tried to close a ticket with ID {ticket_id}, but they do not have ownership over it or it does not exist.", + ) - logging.warning(f'User {user_name} tried to close a ticket with ID {ticket_id}, but they do not have ownership over it or it does not exist.') - - await ctx.send(f'You do not have ownership over a ticket with ID {ticket_id}. Maybe you made a typo?', ephemeral=True) + await ctx.send( + f"You do not have ownership over a ticket with ID {ticket_id}. Maybe you made a typo?", + ephemeral=True, + ) return closed, claimed, assignee = ticket_info_query if closed: + logging.warning( + f"User {user_name} tried to close a ticket with ID {ticket_id}, but it is already closed.", + ) - logging.warning(f'User {user_name} tried to close a ticket with ID {ticket_id}, but it is already closed.') - - await ctx.send('This ticket has already been closed! :star_struck:', ephemeral=True) + await ctx.send( + "This ticket has already been closed! :star_struck:", + ephemeral=True, + ) return @@ -95,81 +117,135 @@ async def close(ctx: nc.Interaction, ticket_id: int) -> None: # Not sure if this is right in this case, but would crash in this branch otherwise. mentor_name = user_name - await ctx.send(f'Mentor {mentor_name} has claimed this ticket. Please contact them to close it.', ephemeral=True) + await ctx.send( + f"Mentor {mentor_name} has claimed this ticket. Please contact them to close it.", + ephemeral=True, + ) return - db_cursor.execute('UPDATE tickets SET closed = 1 WHERE id = :ticket_id', {'ticket_id': ticket_id}) + db_cursor.execute( + "UPDATE tickets SET closed = 1 WHERE id = :ticket_id", + {"ticket_id": ticket_id}, + ) - logging.info(f'User {user_name} closed thier own ticket with ID {ticket_id}.') + logging.info( + f"User {user_name} closed their own ticket with ID {ticket_id}.", + ) - await ctx.send('Ticket closed! :saluting_face:', ephemeral=True) + await ctx.send( + "Ticket closed! :saluting_face:", + ephemeral=True, + ) return - ticket_info_query = db_cursor.execute('SELECT closed, mentor_assigned_id, mentor_assigned, claimed, help_thread_id FROM tickets WHERE id = :ticket_id', {'ticket_id': ticket_id}).fetchone() + ticket_info_query = db_cursor.execute( + "SELECT closed, mentor_assigned_id, mentor_assigned, claimed, help_thread_id FROM tickets WHERE id = :ticket_id", + {"ticket_id": ticket_id}, + ).fetchone() if ticket_info_query is None: + await ctx.send( + f"A ticket with the ID {ticket_id} does not exist. Please try again.", + ephemeral=True, + ) - await ctx.send(f'A ticket with the ID {ticket_id} does not exist. Please try again.', ephemeral=True) - - logging.warning(f'Mentor {user_name} tried to close non-existant ticket with ID {ticket_id}.') + logging.warning( + f"Mentor {user_name} tried to close non-existent ticket with ID {ticket_id}.", + ) return - closed, ticket_assignee_id, ticket_assignee_name, claimed, help_thread_id = ticket_info_query + ( + closed, + ticket_assignee_id, + ticket_assignee_name, + claimed, + help_thread_id, + ) = ticket_info_query if ctx.user.id != ticket_assignee_id: + logging.warning( + f"Mentor {user_name} tried to close a ticket with ID {ticket_id} owned by {ticket_assignee_name}.", + ) - logging.warning(f'Mentor {user_name} tried to close a ticket with ID {ticket_id} owned by {ticket_assignee_name}.' ) - - await ctx.send(f'Woah there! You don\'t own this ticket... {ticket_assignee_name} does. Contact them to close it.', ephemeral=True) + await ctx.send( + f"Woah there! You don't own this ticket... {ticket_assignee_name} does. Contact them to close it.", + ephemeral=True, + ) return if closed: + logging.warning( + f"Mentor {user_name} tried to close a ticket with ID {ticket_id}, but it is already closed.", + ) - logging.warning(f'Mentor {user_name} tried to close a ticket with ID {ticket_id}, but it is already closed.') - - await ctx.send('This ticket has already been closed! :star_struck:', ephemeral=True) + await ctx.send( + "This ticket has already been closed! :star_struck:", + ephemeral=True, + ) - return + return # if the ticket has not been claimed... if not claimed: - - await ctx.send(f'This ticket has not been claimed. Please claim it before closing it.', ephemeral=True) + await ctx.send( + "This ticket has not been claimed. Please claim it before closing it.", + ephemeral=True, + ) return - mentor_channel = await ctx.guild.fetch_channel(CONFIG['MENTOR_CHANNEL_ID']) + mentor_channel = await ctx.guild.fetch_channel( + CONFIG["MENTOR_CHANNEL_ID"], + ) help_thread = mentor_channel.get_thread(help_thread_id) if help_thread is None: + logging.error( + f"Mentor {user_name} tried to close a ticket with ID {ticket_id}, but help thread could not be found.", + ) - logging.error(f'Mentor {user_name} tried to close a ticket with ID {ticket_id}, but help thread could not be found.' ) - - await ctx.send('An unknown error has occured. Please contact a HackKU organizer.', ephemeral=True) + await ctx.send( + "An unknown error has occurred. Please contact a HackKU organizer.", + ephemeral=True, + ) return await help_thread.delete() - logging.info(f'Deleted help thread wth ID: {help_thread_id}.') + logging.info(f"Deleted help thread with ID: {help_thread_id}.") - db_cursor.execute('UPDATE tickets SET closed = 1 WHERE id = :ticket_id', {'ticket_id': ticket_id}) + db_cursor.execute( + "UPDATE tickets SET closed = 1 WHERE id = :ticket_id", + {"ticket_id": ticket_id}, + ) - db_cursor.execute('UPDATE mentors SET tickets_closed = tickets_closed + 1 WHERE id = :mentor_id', {'mentor_id': ctx.user.id}) + db_cursor.execute( + "UPDATE mentors SET tickets_closed = tickets_closed + 1 WHERE id = :mentor_id", + {"mentor_id": ctx.user.id}, + ) - logging.info(f'Ticket with ID {ticket_id} has been closed by mentor {user_name}.') - await ctx.send('Ticket closed!', ephemeral=True) + logging.info( + f"Ticket with ID {ticket_id} has been closed by mentor {user_name}.", + ) + await ctx.send("Ticket closed!", ephemeral=True) except Exception as e: + logging.error( + f"Mentor {user_name} tried to close a ticket with ID {ticket_id}, but an unexpected error occurred: {e}", + ) + + await ctx.send( + "An unknown error has occurred. Please contact a HackKU organizer.", + ephemeral=True, + ) - logging.error(f'Mentor {user_name} tried to close a ticket with ID {ticket_id}, but an unexpected error occured: {e}' ) - await ctx.send('An unknown error has occured. Please contact a HackKU organizer.', ephemeral=True) -#1}}} +# 1}}} def guild_member_and_in_guild(ctx: nc.Interaction) -> bool: @@ -178,168 +254,273 @@ def guild_member_and_in_guild(ctx: nc.Interaction) -> bool: # claim {{{1 -@bot.slash_command(description="Claim a ticket.", guild_ids=[CONFIG['GUILD_ID']]) +@bot.slash_command( + description="Claim a ticket.", + guild_ids=[CONFIG["GUILD_ID"]], +) @nc_app_checks.check(guild_member_and_in_guild) -@nc_app_checks.has_role(CONFIG['MENTOR_ROLE_ID']) +@nc_app_checks.has_role(CONFIG["MENTOR_ROLE_ID"]) async def claim(ctx: nc.Interaction, ticket_id: int) -> None: - with DB_CONNECTION: - mentor_name = ctx.user.nick if ctx.user.nick else ctx.user.global_name try: db_cursor = DB_CONNECTION.cursor() - mentor_query = db_cursor.execute('SELECT 1 FROM mentors WHERE id = :mentor_id', {'mentor_id': ctx.user.id}).fetchone() + mentor_query = db_cursor.execute( + "SELECT 1 FROM mentors WHERE id = :mentor_id", + {"mentor_id": ctx.user.id}, + ).fetchone() - #new mentor! + # new mentor! if mentor_query is None: - - db_cursor.execute('INSERT INTO mentors (id, name, tickets_claimed, tickets_closed) VALUES (:mentor_id, :mentor_name, 0, 0)',{'mentor_id': ctx.user.id, 'mentor_name': mentor_name}) - - claim_params = {'ticket_id': ticket_id, 'mentor_id': ctx.user.id, 'mentor_name': mentor_name} - - ticket_query = db_cursor.execute('SELECT closed, claimed, mentor_assigned, author_id, message, author_location FROM tickets WHERE id = :ticket_id', {'ticket_id': ticket_id}).fetchone() + db_cursor.execute( + "INSERT INTO mentors (id, name, tickets_claimed, tickets_closed) VALUES (:mentor_id, :mentor_name, 0, 0)", + {"mentor_id": ctx.user.id, "mentor_name": mentor_name}, + ) + + claim_params = { + "ticket_id": ticket_id, + "mentor_id": ctx.user.id, + "mentor_name": mentor_name, + } + + ticket_query = db_cursor.execute( + "SELECT closed, claimed, mentor_assigned, author_id, message, author_location FROM tickets WHERE id = :ticket_id", + {"ticket_id": ticket_id}, + ).fetchone() if ticket_query is None: + await ctx.send( + f"A ticket with the ID {ticket_id} does not exist. Please try again.", + ephemeral=True, + ) - await ctx.send(f'A ticket with the ID {ticket_id} does not exist. Please try again.', ephemeral=True) - - logging.warning(f'Mentor {mentor_name} tried to claim non-existant ticket with ID {ticket_id}.') + logging.warning( + f"Mentor {mentor_name} tried to claim non-existent ticket with ID {ticket_id}.", + ) return - closed, claimed, prev_assignee_name, author_id, ticket_message, author_location = ticket_query + ( + closed, + claimed, + prev_assignee_name, + author_id, + ticket_message, + author_location, + ) = ticket_query if closed: - - await ctx.send(f'This ticket has already been closed! :star_struck:', ephemeral=True) + await ctx.send( + "This ticket has already been closed! :star_struck:", + ephemeral=True, + ) return - #if ticket is already claimed... + # if ticket is already claimed... if claimed: - - await ctx.send(f'This ticket has already been claimed by {prev_assignee_name}. Please contact them if you would like to help out.', ephemeral=True) + await ctx.send( + f"This ticket has already been claimed by {prev_assignee_name}. Please contact them if you would like to help out.", + ephemeral=True, + ) return ticket_author = await ctx.guild.fetch_member(author_id) - ticket_author_name = ticket_author.nick if ticket_author.nick else ticket_author.global_name + ticket_author_name = ( + ticket_author.nick + if ticket_author.nick + else ticket_author.global_name + ) - help_channel = await ctx.guild.fetch_channel(CONFIG['HELP_CHANNEL_ID']) + help_channel = await ctx.guild.fetch_channel( + CONFIG["HELP_CHANNEL_ID"], + ) - db_cursor.execute('UPDATE tickets SET claimed = 1, mentor_assigned_id = :mentor_id, mentor_assigned = :mentor_name WHERE id = :ticket_id', claim_params) + db_cursor.execute( + "UPDATE tickets SET claimed = 1, mentor_assigned_id = :mentor_id, mentor_assigned = :mentor_name WHERE id = :ticket_id", + claim_params, + ) - db_cursor.execute('UPDATE mentors SET tickets_claimed = tickets_claimed + 1 WHERE id = :mentor_id', {'mentor_id': ctx.user.id}) + db_cursor.execute( + "UPDATE mentors SET tickets_claimed = tickets_claimed + 1 WHERE id = :mentor_id", + {"mentor_id": ctx.user.id}, + ) - logging.info(f'Mentor {mentor_name} has claimed ticket with ID {ticket_id}.') + logging.info( + f"Mentor {mentor_name} has claimed ticket with ID {ticket_id}.", + ) - help_thread = await help_channel.create_thread(name=f'Ticket #{ticket_id}', reason=f'Ticket #{ticket_id}') + help_thread = await help_channel.create_thread( + name=f"Ticket #{ticket_id}", + reason=f"Ticket #{ticket_id}", + ) - logging.info(f'Created help thread with ID {help_thread.id} for ticket with ID {ticket_id}.') + logging.info( + f"Created help thread with ID {help_thread.id} for ticket with ID {ticket_id}.", + ) await help_thread.add_user(ctx.user) - logging.info(f'Added Mentor {mentor_name} to help thread with ID {help_thread.id}.') + logging.info( + f"Added Mentor {mentor_name} to help thread with ID {help_thread.id}.", + ) await help_thread.add_user(ticket_author) - logging.info(f'Added User {ticket_author_name} to help thread with ID {help_thread.id}.') + logging.info( + f"Added User {ticket_author_name} to help thread with ID {help_thread.id}.", + ) - ticket_update_params = {'help_thread_id': help_thread.id, 'ticket_id': ticket_id} + ticket_update_params = { + "help_thread_id": help_thread.id, + "ticket_id": ticket_id, + } - db_cursor.execute('UPDATE tickets SET help_thread_id = :help_thread_id WHERE id = :ticket_id', ticket_update_params) + db_cursor.execute( + "UPDATE tickets SET help_thread_id = :help_thread_id WHERE id = :ticket_id", + ticket_update_params, + ) - await ctx.send(f'Ticket #{ticket_id} claimed by {mentor_name}!') + await ctx.send(f"Ticket #{ticket_id} claimed by {mentor_name}!") - await help_thread.send(f'Greetings {ticket_author.mention}! {ctx.user.mention} is on the way to {author_location} to help you resolve the issue in your ticket:\n> {ticket_message}') + await help_thread.send( + f"Greetings {ticket_author.mention}! {ctx.user.mention} is on the way to {author_location} to help you resolve the issue in your ticket:\n> {ticket_message}", + ) except Exception as e: + logging.error( + f"Mentor {mentor_name} tried to claim a ticket with ID {ticket_id}, but an unexpected error occurred: {e}", + ) - logging.error(f'Mentor {mentor_name} tried to claim a ticket with ID {ticket_id}, but an unexpected error occured: {e}') + await ctx.send( + "An unknown error has occurred. Please contact a HackKU organizer.", + ephemeral=True, + ) - await ctx.send('An unknown error has occured. Please contact a HackKU organizer.', ephemeral=True) +# 1}}} -#1}}} -#helpme {{{1 -#guild id just for testing -@bot.slash_command(description="Request help from a mentor.", guild_ids=[CONFIG['GUILD_ID']]) -#check that message is from a guild and user is a member of said guild. sort of a dumb check, but need for type safety later on. +# helpme {{{1 +# guild id just for testing +@bot.slash_command( + description="Request help from a mentor.", + guild_ids=[CONFIG["GUILD_ID"]], +) +# check that message is from a guild and user is a member of said guild. sort of a dumb check, but need for type safety later on. @nc_app_checks.check(guild_member_and_in_guild) -async def helpme(ctx: nc.Interaction, author_location: str, ticket_message: str) -> None: - +async def helpme( + ctx: nc.Interaction, + author_location: str, + ticket_message: str, +) -> None: with DB_CONNECTION: - - author_name = ctx.user.nick if ctx.user.nick else ctx.user.global_name #use guild nickname if available, otherwise use global name + author_name = ( + ctx.user.nick if ctx.user.nick else ctx.user.global_name + ) # use guild nickname if available, otherwise use global name try: db_cursor = DB_CONNECTION.cursor() - mentor_channel = await ctx.guild.fetch_channel(CONFIG['MENTOR_CHANNEL_ID']) - - ticket_params = { 'message': ticket_message - , 'author_id': ctx.user.id - , 'author': author_name - , 'author_location': author_location - , 'claimed': False - , 'closed': False - } - - db_cursor.execute(""" + mentor_channel = await ctx.guild.fetch_channel( + CONFIG["MENTOR_CHANNEL_ID"], + ) + + ticket_params = { + "message": ticket_message, + "author_id": ctx.user.id, + "author": author_name, + "author_location": author_location, + "claimed": False, + "closed": False, + } + + db_cursor.execute( + """ INSERT INTO tickets (message, author_id, author, author_location, claimed, closed) VALUES (:message, :author_id, :author, :author_location, :claimed, :closed) - """, ticket_params) + """, + ticket_params, + ) - logging.info(f'Received ticket from user {ticket_params["author"]} with ID {ticket_params["author_id"]}.') + logging.info( + f"Received ticket from user {ticket_params['author']} with ID {ticket_params['author_id']}.", + ) - ticket_embed = nc.Embed(title='New Ticket Opened! :tickets:', description='A hacker needs help. Use `/claim` to claim this ticket!') + ticket_embed = nc.Embed( + title="New Ticket Opened! :tickets:", + description="A hacker needs help. Use `/claim` to claim this ticket!", + ) ticket_id = db_cursor.lastrowid - ticket_embed.add_field(name='__ID__ :hash:', value=ticket_id) - ticket_embed.add_field(name='__Author__ :pen_fountain:', value=author_name) - ticket_embed.add_field(name='__Location__ :round_pushpin:', value=author_location) - ticket_embed.add_field(name='__Message__ :scroll:', value=ticket_message, inline=False) + ticket_embed.add_field(name="__ID__ :hash:", value=ticket_id) + ticket_embed.add_field( + name="__Author__ :pen_fountain:", + value=author_name, + ) + ticket_embed.add_field( + name="__Location__ :round_pushpin:", + value=author_location, + ) + ticket_embed.add_field( + name="__Message__ :scroll:", + value=ticket_message, + inline=False, + ) await mentor_channel.send(embed=ticket_embed) - await ctx.send(f'Ticket submitted with ID {ticket_id}, help will be on the way soon!', ephemeral=True) - + await ctx.send( + f"Ticket submitted with ID {ticket_id}, help will be on the way soon!", + ephemeral=True, + ) except Exception as e: + logging.error( + f"User {author_name} with ID {ctx.user.id} tried to create a ticket, but an unexpected error occurred: {e}", + ) + + await ctx.send( + "An unknown error has occurred. Please contact a HackKU organizer.", + ephemeral=True, + ) - logging.error(f'User {author_name} with ID {ctx.user.id} tried to create a ticket, but an unexpected error occured: {e}' ) - await ctx.send('An unknown error has occured. Please contact a HackKU organizer.', ephemeral=True) -#1}}} +# 1}}} def bool_checkmark_emoji(value: bool | int) -> str: """Convert truthy values to check mark emoji, false to x mark.""" - return ':white_check_mark:' if value else ':no_entry:' + return ":white_check_mark:" if value else ":no_entry:" -#view all of your tickets. {{{1 -@bot.slash_command(description="View all of your tickets.", guild_ids=[CONFIG['GUILD_ID']]) -#check that message is from a guild and user is a member of said guild. sort of a dumb check, but need for type safety later on. +# view all of your tickets. {{{1 +@bot.slash_command( + description="View all of your tickets.", + guild_ids=[CONFIG["GUILD_ID"]], +) +# check that message is from a guild and user is a member of said guild. sort of a dumb check, but need for type safety later on. @nc_app_checks.check(guild_member_and_in_guild) async def mytix(ctx: nc.Interaction) -> None: - with DB_CONNECTION: - try: - db_cursor = DB_CONNECTION.cursor() - #if user is a mentor, treat differently - if ctx.user.get_role(CONFIG['MENTOR_ROLE_ID']) is not None: - tickets_query = db_cursor.execute('SELECT id, closed FROM tickets WHERE mentor_assigned_id = :mentor_id', {'mentor_id': ctx.user.id}).fetchall() + # if user is a mentor, treat differently + if ctx.user.get_role(CONFIG["MENTOR_ROLE_ID"]) is not None: + tickets_query = db_cursor.execute( + "SELECT id, closed FROM tickets WHERE mentor_assigned_id = :mentor_id", + {"mentor_id": ctx.user.id}, + ).fetchall() if tickets_query == []: - - await ctx.send('You have not claimed any tickets! Use `/opentix` view open tickets to claim.', ephemeral=True) + await ctx.send( + "You have not claimed any tickets! Use `/opentix` view open tickets to claim.", + ephemeral=True, + ) return @@ -350,250 +531,408 @@ async def mytix(ctx: nc.Interaction) -> None: closeds = map(bool_checkmark_emoji, closeds) ticket_ids = map(str, ticket_ids) - tickets_embed = nc.Embed(title='Your Claimed Tickets :sunglasses:', description='Use `/status` with the ticket ID for more information on a given ticket.') + tickets_embed = nc.Embed( + title="Your Claimed Tickets :sunglasses:", + description="Use `/status` with the ticket ID for more information on a given ticket.", + ) - tickets_embed.add_field(name='__ID__ :hash:', value='\n'.join(ticket_ids)) - tickets_embed.add_field(name='__Closed__ :tada:', value='\n'.join(closeds)) + tickets_embed.add_field( + name="__ID__ :hash:", + value="\n".join(ticket_ids), + ) + tickets_embed.add_field( + name="__Closed__ :tada:", + value="\n".join(closeds), + ) await ctx.send(embed=tickets_embed, ephemeral=True) else: - #this is an iterable - tickets_query = db_cursor.execute('SELECT id, claimed, closed FROM tickets WHERE author_id = :author_id', {'author_id': ctx.user.id}).fetchall() + # this is an iterable + tickets_query = db_cursor.execute( + "SELECT id, claimed, closed FROM tickets WHERE author_id = :author_id", + {"author_id": ctx.user.id}, + ).fetchall() if tickets_query == []: - - await ctx.send('You have no tickets! Use `/helpme` to open one.', ephemeral=True) + await ctx.send( + "You have no tickets! Use `/helpme` to open one.", + ephemeral=True, + ) return - #get a tuple for each list of fields. + # get a tuple for each list of fields. claimeds: Iterable[str] ticket_ids, claimeds, closeds = zip(*tickets_query) - #prep the data for embed. + # prep the data for embed. claimeds = map(bool_checkmark_emoji, claimeds) closeds = map(bool_checkmark_emoji, closeds) ticket_ids = map(str, ticket_ids) - tickets_embed = nc.Embed(title='Your Tickets :man_dancing:', description='Use `/status` with the ticket ID for more information on a given ticket.') - - tickets_embed.add_field(name='__ID__ :hash:', value='\n'.join(ticket_ids)) - tickets_embed.add_field(name='__Claimed__ :face_with_monocle:', value='\n'.join(claimeds)) - tickets_embed.add_field(name='__Closed__ :tada:', value='\n'.join(closeds)) + tickets_embed = nc.Embed( + title="Your Tickets :man_dancing:", + description="Use `/status` with the ticket ID for more information on a given ticket.", + ) + + tickets_embed.add_field( + name="__ID__ :hash:", + value="\n".join(ticket_ids), + ) + tickets_embed.add_field( + name="__Claimed__ :face_with_monocle:", + value="\n".join(claimeds), + ) + tickets_embed.add_field( + name="__Closed__ :tada:", + value="\n".join(closeds), + ) await ctx.send(embed=tickets_embed, ephemeral=True) except Exception as e: + user_name = ( + ctx.user.nick if ctx.user.nick else ctx.user.global_name + ) # use guild nickname if available, otherwise use global name + + logging.error( + f"User {user_name} tried to view all of their tickets, but an unexpected error occurred: {e}", + ) - user_name = ctx.user.nick if ctx.user.nick else ctx.user.global_name #use guild nickname if available, otherwise use global name + await ctx.send( + "An unknown error has occurred. Please contact a HackKU organizer.", + ephemeral=True, + ) - logging.error(f'User {user_name} tried to view all of thier tickets, but an unexpected error occured: {e}') - await ctx.send('An unknown error has occured. Please contact a HackKU organizer.', ephemeral=True) -#1}}} +# 1}}} -#view specific ticket details.{{{1 -@bot.slash_command(description="View the details of a specific ticket.", guild_ids=[CONFIG['GUILD_ID']]) +# view specific ticket details.{{{1 +@bot.slash_command( + description="View the details of a specific ticket.", + guild_ids=[CONFIG["GUILD_ID"]], +) @nc_app_checks.check(guild_member_and_in_guild) async def status(ctx: nc.Interaction, ticket_id: int) -> None: - with DB_CONNECTION: - db_cursor = DB_CONNECTION.cursor() - user_name = ctx.user.nick if ctx.user.nick else ctx.user.global_name #use guild nickname if available, otherwise use global name + user_name = ( + ctx.user.nick if ctx.user.nick else ctx.user.global_name + ) # use guild nickname if available, otherwise use global name try: - - #organizer can view all tickets. - if ctx.user.get_role(CONFIG['ORGANIZER_ROLE_ID']) is not None or ctx.user.get_role(CONFIG['MENTOR_ROLE_ID']) is not None: - - ticket_query = db_cursor.execute('SELECT claimed, closed, mentor_assigned, message, author, author_location FROM tickets WHERE id = :ticket_id', {'ticket_id': ticket_id}).fetchone() + # organizer can view all tickets. + if ( + ctx.user.get_role(CONFIG["ORGANIZER_ROLE_ID"]) is not None + or ctx.user.get_role(CONFIG["MENTOR_ROLE_ID"]) is not None + ): + ticket_query = db_cursor.execute( + "SELECT claimed, closed, mentor_assigned, message, author, author_location FROM tickets WHERE id = :ticket_id", + {"ticket_id": ticket_id}, + ).fetchone() else: - - #mentor or author can access ticket. - ticket_query = db_cursor.execute('SELECT claimed, closed, mentor_assigned, message, author, author_location FROM tickets WHERE author_id = :author_id AND id = :ticket_id', {'author_id': ctx.user.id, 'ticket_id': ticket_id}).fetchone() + # mentor or author can access ticket. + ticket_query = db_cursor.execute( + "SELECT claimed, closed, mentor_assigned, message, author, author_location FROM tickets WHERE author_id = :author_id AND id = :ticket_id", + {"author_id": ctx.user.id, "ticket_id": ticket_id}, + ).fetchone() if ticket_query is None: + logging.warning( + f"User {user_name} tried to view a ticket with ID {ticket_id}, but database query returned nothing.", + ) - logging.warning(f'User {user_name} tried to view a ticket with ID {ticket_id}, but database query returned nothing.') - - await ctx.send(f'You don\'t have ownership over a ticket with ID {ticket_id}. Maybe you made a typo?', ephemeral=True) + await ctx.send( + f"You don't have ownership over a ticket with ID {ticket_id}. Maybe you made a typo?", + ephemeral=True, + ) return claimed, closed, mentor, message, author, location = ticket_query - ticket_embed = nc.Embed(title=f'Ticket #{ticket_id} :bug:', description=f'Opened by {author} @ {location}.') - ticket_embed.add_field(name='__Mentor__ :military_helmet:', value=('N/A' if mentor is None else mentor)) - ticket_embed.add_field(name='__Claimed__ :face_with_monocle:', value=(':white_check_mark:' if claimed else ':no_entry:')) - ticket_embed.add_field(name='__Closed__ :tada:', value=(':white_check_mark:' if closed else ':no_entry:')) - ticket_embed.add_field(name='__Message__ :scroll:', value=message, inline=False) + ticket_embed = nc.Embed( + title=f"Ticket #{ticket_id} :bug:", + description=f"Opened by {author} @ {location}.", + ) + ticket_embed.add_field( + name="__Mentor__ :military_helmet:", + value=("N/A" if mentor is None else mentor), + ) + ticket_embed.add_field( + name="__Claimed__ :face_with_monocle:", + value=(":white_check_mark:" if claimed else ":no_entry:"), + ) + ticket_embed.add_field( + name="__Closed__ :tada:", + value=(":white_check_mark:" if closed else ":no_entry:"), + ) + ticket_embed.add_field( + name="__Message__ :scroll:", + value=message, + inline=False, + ) await ctx.send(embed=ticket_embed, ephemeral=True) except Exception as e: + logging.error( + f"User {user_name} tried to view a ticket with ID {ticket_id}, but an unexpected error occurred: {e}", + ) - logging.error(f'User {user_name} tried to view a ticket with ID {ticket_id}, but an unexpected error occured: {e}') + await ctx.send( + "An unknown error has occurred. Please contact a HackKU organizer.", + ephemeral=True, + ) - await ctx.send('An unknown error has occured. Please contact a HackKU organizer.', ephemeral=True) -#1}}} + +# 1}}} def mentor_or_organizer_role(ctx: nc.Interaction) -> bool: """Check that interaction user has either mentor or organizer role.""" - return ctx.user.get_role(CONFIG['MENTOR_ROLE_ID']) is not None or ctx.user.get_role(CONFIG['ORGANIZER_ROLE_ID']) is not None + return ( + ctx.user.get_role(CONFIG["MENTOR_ROLE_ID"]) is not None + or ctx.user.get_role(CONFIG["ORGANIZER_ROLE_ID"]) is not None + ) -#view all open tickets {{{1 -@bot.slash_command(description="View all open tickets.", guild_ids=[CONFIG['GUILD_ID']]) +# view all open tickets {{{1 +@bot.slash_command( + description="View all open tickets.", + guild_ids=[CONFIG["GUILD_ID"]], +) @nc_app_checks.check(mentor_or_organizer_role) async def opentix(ctx: nc.Interaction) -> None: - with DB_CONNECTION: - try: - db_cursor = DB_CONNECTION.cursor() - #this is an iterable - tickets_query = db_cursor.execute('SELECT id, author_location, author, message FROM tickets WHERE claimed = 0 AND closed = 0').fetchall() + # this is an iterable + tickets_query = db_cursor.execute( + "SELECT id, author_location, author, message FROM tickets WHERE claimed = 0 AND closed = 0", + ).fetchall() if tickets_query == []: - - await ctx.send('There are no open tickets :sob:', ephemeral=True) + await ctx.send( + "There are no open tickets :sob:", + ephemeral=True, + ) return - #get a tuple for each list of fields. + # get a tuple for each list of fields. ticket_ids: Iterable[str] logistics: Iterable[str] messages: Iterable[str] ticket_ids, locations, authors, messages = zip(*tickets_query) - #prep the data for embed. + # prep the data for embed. ticket_ids = map(str, ticket_ids) - logistics = map((lambda x: f'{x[0]} @ *{x[1][:10] + "..." if len(x[1]) > 10 else x[1]}*'), zip(authors, locations)) - messages = map((lambda x: f'{x[:10]}...' if len(x) > 10 else x), messages) - - tickets_embed = nc.Embed(title='Open Tickets :dancer:', description='Use `/claim` to claim an open ticket. Use `/status` to see the full details of a ticket.') - - tickets_embed.add_field(name='__ID__ :hash:', value='\n'.join(ticket_ids)) - tickets_embed.add_field(name='__Logistics__ :globe_with_meridians:', value='\n'.join(logistics)) - tickets_embed.add_field(name='__Message__ :scroll:', value='\n'.join(messages)) + logistics = map( + ( + lambda x: f"{x[0]} @ *{x[1][:10] + '...' if len(x[1]) > 10 else x[1]}*" + ), + zip(authors, locations), + ) + messages = map( + (lambda x: f"{x[:10]}..." if len(x) > 10 else x), + messages, + ) + + tickets_embed = nc.Embed( + title="Open Tickets :dancer:", + description="Use `/claim` to claim an open ticket. Use `/status` to see the full details of a ticket.", + ) + + tickets_embed.add_field( + name="__ID__ :hash:", + value="\n".join(ticket_ids), + ) + tickets_embed.add_field( + name="__Logistics__ :globe_with_meridians:", + value="\n".join(logistics), + ) + tickets_embed.add_field( + name="__Message__ :scroll:", + value="\n".join(messages), + ) await ctx.send(embed=tickets_embed, ephemeral=True) except Exception as e: + user_name = ( + ctx.user.nick if ctx.user.nick else ctx.user.global_name + ) # use guild nickname if available, otherwise use global name + + logging.error( + f"User {user_name} tried to view all open tickets, but an unexpected error occurred: {e}", + ) + + await ctx.send( + "An unknown error has occurred. Please contact a HackKU organizer.", + ephemeral=True, + ) - user_name = ctx.user.nick if ctx.user.nick else ctx.user.global_name #use guild nickname if available, otherwise use global name - logging.error(f'User {user_name} tried to view all open tickets, but an unexpected error occured: {e}') +# 1}}} - await ctx.send('An unknown error has occured. Please contact a HackKU organizer.', ephemeral=True) -#1}}} -#view all tickets {{{1 -@bot.slash_command(description="View all tickets.", guild_ids=[CONFIG['GUILD_ID']]) +# view all tickets {{{1 +@bot.slash_command( + description="View all tickets.", + guild_ids=[CONFIG["GUILD_ID"]], +) @nc_app_checks.check(mentor_or_organizer_role) # type: ignore[misc] async def alltix(ctx: nc.Interaction) -> None: - with DB_CONNECTION: - try: - db_cursor = DB_CONNECTION.cursor() - #this is an iterable - tickets_query = db_cursor.execute('SELECT id, claimed, closed FROM tickets').fetchall() + # this is an iterable + tickets_query = db_cursor.execute( + "SELECT id, claimed, closed FROM tickets", + ).fetchall() if tickets_query == []: - - await ctx.send('There are no tickets :fearful:', ephemeral=True) + await ctx.send( + "There are no tickets :fearful:", + ephemeral=True, + ) return - #get a tuple for each list of fields. + # get a tuple for each list of fields. ticket_ids: Iterable[str] claimeds: Iterable[str] closeds: Iterable[str] ticket_ids, claimeds, closeds = zip(*tickets_query) - #prep the data for embed. + # prep the data for embed. claimeds = map(bool_checkmark_emoji, claimeds) closeds = map(bool_checkmark_emoji, closeds) ticket_ids = map(str, ticket_ids) - tickets_embed = nc.Embed(title='All Tickets :face_with_spiral_eyes:', description='Use `/status` to view information about a specific ticket if you have claimed it. Claim an open ticket with `/claim`.') - - tickets_embed.add_field(name='__ID__ :hash:', value='\n'.join(ticket_ids)) - tickets_embed.add_field(name='__Claimed__ :face_with_monocle:', value='\n'.join(claimeds)) - tickets_embed.add_field(name='__Closed__ :tada:', value='\n'.join(closeds)) + tickets_embed = nc.Embed( + title="All Tickets :face_with_spiral_eyes:", + description="Use `/status` to view information about a specific ticket if you have claimed it. Claim an open ticket with `/claim`.", + ) + + tickets_embed.add_field( + name="__ID__ :hash:", + value="\n".join(ticket_ids), + ) + tickets_embed.add_field( + name="__Claimed__ :face_with_monocle:", + value="\n".join(claimeds), + ) + tickets_embed.add_field( + name="__Closed__ :tada:", + value="\n".join(closeds), + ) await ctx.send(embed=tickets_embed, ephemeral=True) except Exception as e: + user_name = ( + ctx.user.nick if ctx.user.nick else ctx.user.global_name + ) # use guild nickname if available, otherwise use global name + + logging.error( + f"User {user_name} tried to view all tickets, but an unexpected error occurred: {e}", + ) - user_name = ctx.user.nick if ctx.user.nick else ctx.user.global_name #use guild nickname if available, otherwise use global name + await ctx.send( + "An unknown error has occurred. Please contact a HackKU organizer.", + ephemeral=True, + ) - logging.error(f'User {user_name} tried to view all tickets, but an unexpected error occured: {e}') - await ctx.send('An unknown error has occured. Please contact a HackKU organizer.', ephemeral=True) -#1}}} +# 1}}} -#leaderboard {{{1 -@bot.slash_command(description="View which mentors have closed to most tickets.", guild_ids=[CONFIG['GUILD_ID']]) + +# leaderboard {{{1 +@bot.slash_command( + description="View which mentors have closed to most tickets.", + guild_ids=[CONFIG["GUILD_ID"]], +) async def leaderboard(ctx: nc.Interaction) -> None: # type: ignore[misc] with DB_CONNECTION: - try: - db_cursor = DB_CONNECTION.cursor() - #this is an iterable - mentors_query = db_cursor.execute('SELECT name, tickets_claimed, tickets_closed FROM mentors ORDER BY tickets_closed DESC').fetchall() + # this is an iterable + mentors_query = db_cursor.execute( + "SELECT name, tickets_claimed, tickets_closed FROM mentors ORDER BY tickets_closed DESC", + ).fetchall() if mentors_query == []: - - await ctx.send('Welp... no mentors have claimed any tickets :skull:', ephemeral=True) + await ctx.send( + "Whelp... no mentors have claimed any tickets :skull:", + ephemeral=True, + ) return - #get a tuple for each list of fields. + # get a tuple for each list of fields. mentors: Iterable[str] num_claimed: Iterable[str] num_closed: Iterable[str] mentors, num_claimed, num_closed = zip(*mentors_query) - #prep the data for embed. - mentors = map(lambda x: f'**#{x[1]}**: {x[0]}', zip(mentors,range(1,len(mentors)+1))) + # prep the data for embed. + mentors = map( + lambda x: f"**#{x[1]}**: {x[0]}", + zip(mentors, range(1, len(mentors) + 1)), + ) num_claimed = map(str, num_claimed) num_closed = map(str, num_closed) - tickets_embed = nc.Embed(title='Mentor Leaderboard :fire:', description='Whoever closes the most tickets wins!') - - tickets_embed.add_field(name='__Mentor__ :military_helmet:', value='\n'.join(mentors)) - tickets_embed.add_field(name='__# Claimed__ :face_with_monocle:', value='\n'.join(num_claimed)) - tickets_embed.add_field(name='__# Closed__ :tada:', value='\n'.join(num_closed)) + tickets_embed = nc.Embed( + title="Mentor Leaderboard :fire:", + description="Whoever closes the most tickets wins!", + ) + + tickets_embed.add_field( + name="__Mentor__ :military_helmet:", + value="\n".join(mentors), + ) + tickets_embed.add_field( + name="__# Claimed__ :face_with_monocle:", + value="\n".join(num_claimed), + ) + tickets_embed.add_field( + name="__# Closed__ :tada:", + value="\n".join(num_closed), + ) await ctx.send(embed=tickets_embed, ephemeral=True) except Exception as e: + user_name = ( + ctx.user.nick if ctx.user.nick else ctx.user.global_name + ) # use guild nickname if available, otherwise use global name + + logging.error( + f"User {user_name} tried to view the mentor leaderboard, but an unexpected error occurred: {e}", + ) - user_name = ctx.user.nick if ctx.user.nick else ctx.user.global_name #use guild nickname if available, otherwise use global name + await ctx.send( + "An unknown error has occurred. Please contact a HackKU organizer.", + ephemeral=True, + ) - logging.error(f'User {user_name} tried to view the mentor leaderboard, but an unexpected error occured: {e}') - await ctx.send('An unknown error has occured. Please contact a HackKU organizer.', ephemeral=True) -#1}}} +# 1}}} def run() -> None: """Main entry point.""" - bot.run(CONFIG['API_TOKEN']) + bot.run(CONFIG["API_TOKEN"]) if __name__ == "__main__": From 6a863592f2d9d91442e648c9bdfac6d1097e5522 Mon Sep 17 00:00:00 2001 From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com> Date: Wed, 19 Mar 2025 02:38:03 -0500 Subject: [PATCH 6/9] Ruff automatic unsafe fixes --- src/helper_duck/__init__.py | 26 +++++++++++--------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/src/helper_duck/__init__.py b/src/helper_duck/__init__.py index 4377f46..dc06fbd 100644 --- a/src/helper_duck/__init__.py +++ b/src/helper_duck/__init__.py @@ -24,17 +24,18 @@ import json import logging import sqlite3 as sql -from collections.abc import Iterable from pathlib import Path -from typing import Final +from typing import TYPE_CHECKING, Final import nextcord as nc -import nextcord.utils as nc_utils from nextcord.ext import ( application_checks as nc_app_checks, commands as nc_cmd, ) +if TYPE_CHECKING: + from collections.abc import Iterable + logging.basicConfig(level=logging.INFO) LOGGER: Final = logging.getLogger("nextcord") @@ -733,16 +734,11 @@ async def opentix(ctx: nc.Interaction) -> None: # prep the data for embed. ticket_ids = map(str, ticket_ids) - logistics = map( - ( - lambda x: f"{x[0]} @ *{x[1][:10] + '...' if len(x[1]) > 10 else x[1]}*" - ), - zip(authors, locations), - ) - messages = map( - (lambda x: f"{x[:10]}..." if len(x) > 10 else x), - messages, + logistics = ( + f"{x[0]} @ *{x[1][:10] + '...' if len(x[1]) > 10 else x[1]}*" + for x in zip(authors, locations) ) + messages = (f"{x[:10]}..." if len(x) > 10 else x for x in messages) tickets_embed = nc.Embed( title="Open Tickets :dancer:", @@ -885,9 +881,9 @@ async def leaderboard(ctx: nc.Interaction) -> None: # type: ignore[misc] mentors, num_claimed, num_closed = zip(*mentors_query) # prep the data for embed. - mentors = map( - lambda x: f"**#{x[1]}**: {x[0]}", - zip(mentors, range(1, len(mentors) + 1)), + mentors = ( + f"**#{x[1]}**: {x[0]}" + for x in zip(mentors, range(1, len(mentors) + 1)) ) num_claimed = map(str, num_claimed) num_closed = map(str, num_closed) From f73c88e3a3a90936f521d8b58b5410ca77464ddb Mon Sep 17 00:00:00 2001 From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com> Date: Wed, 19 Mar 2025 02:43:04 -0500 Subject: [PATCH 7/9] Add/fix docstrings for ruff --- src/helper_duck/__init__.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/helper_duck/__init__.py b/src/helper_duck/__init__.py index dc06fbd..b6fd35e 100644 --- a/src/helper_duck/__init__.py +++ b/src/helper_duck/__init__.py @@ -51,6 +51,7 @@ @bot.event # type: ignore[misc] async def on_ready() -> None: + """Handle ready event.""" logging.info(f"We have logged in as {bot.user}") @@ -59,6 +60,7 @@ async def on_application_command_error( ctx: nc.Interaction, err: Exception, ) -> None: + """Handle application command error.""" user_name = ctx.user.nick if ctx.user.nick else ctx.user.global_name logging.warning( @@ -76,6 +78,7 @@ async def on_application_command_error( guild_ids=[CONFIG["GUILD_ID"]], ) async def close(ctx: nc.Interaction, ticket_id: int) -> None: + """Handle closing a ticket.""" with DB_CONNECTION: user_name = ctx.user.nick if ctx.user.nick else ctx.user.global_name @@ -262,6 +265,7 @@ def guild_member_and_in_guild(ctx: nc.Interaction) -> bool: @nc_app_checks.check(guild_member_and_in_guild) @nc_app_checks.has_role(CONFIG["MENTOR_ROLE_ID"]) async def claim(ctx: nc.Interaction, ticket_id: int) -> None: + """Handle claiming a ticket.""" with DB_CONNECTION: mentor_name = ctx.user.nick if ctx.user.nick else ctx.user.global_name @@ -417,6 +421,7 @@ async def helpme( author_location: str, ticket_message: str, ) -> None: + """Handle help request.""" with DB_CONNECTION: author_name = ( ctx.user.nick if ctx.user.nick else ctx.user.global_name @@ -506,6 +511,7 @@ def bool_checkmark_emoji(value: bool | int) -> str: # check that message is from a guild and user is a member of said guild. sort of a dumb check, but need for type safety later on. @nc_app_checks.check(guild_member_and_in_guild) async def mytix(ctx: nc.Interaction) -> None: + """Handle viewing all tickets from the target user.""" with DB_CONNECTION: try: db_cursor = DB_CONNECTION.cursor() @@ -617,6 +623,7 @@ async def mytix(ctx: nc.Interaction) -> None: ) @nc_app_checks.check(guild_member_and_in_guild) async def status(ctx: nc.Interaction, ticket_id: int) -> None: + """Handle viewing the details of a given ticket.""" with DB_CONNECTION: db_cursor = DB_CONNECTION.cursor() @@ -709,6 +716,7 @@ def mentor_or_organizer_role(ctx: nc.Interaction) -> bool: ) @nc_app_checks.check(mentor_or_organizer_role) async def opentix(ctx: nc.Interaction) -> None: + """Handle viewing all open tickets.""" with DB_CONNECTION: try: db_cursor = DB_CONNECTION.cursor() @@ -785,6 +793,7 @@ async def opentix(ctx: nc.Interaction) -> None: ) @nc_app_checks.check(mentor_or_organizer_role) # type: ignore[misc] async def alltix(ctx: nc.Interaction) -> None: + """Handle viewing all tickets.""" with DB_CONNECTION: try: db_cursor = DB_CONNECTION.cursor() @@ -853,10 +862,11 @@ async def alltix(ctx: nc.Interaction) -> None: # leaderboard {{{1 @bot.slash_command( - description="View which mentors have closed to most tickets.", + description="View which mentors have closed the most tickets.", guild_ids=[CONFIG["GUILD_ID"]], ) async def leaderboard(ctx: nc.Interaction) -> None: # type: ignore[misc] + """Handle viewing which mentors have closed the most tickets.""" with DB_CONNECTION: try: db_cursor = DB_CONNECTION.cursor() @@ -927,7 +937,7 @@ async def leaderboard(ctx: nc.Interaction) -> None: # type: ignore[misc] def run() -> None: - """Main entry point.""" + """Run program.""" bot.run(CONFIG["API_TOKEN"]) From b831e685a66c60c2cb53d39e899b8f03068e143a Mon Sep 17 00:00:00 2001 From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com> Date: Wed, 19 Mar 2025 02:46:16 -0500 Subject: [PATCH 8/9] Ignore remaining mypy issues resulting from nextcord being untyped --- src/helper_duck/__init__.py | 48 ++++++++++++++++++------------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/src/helper_duck/__init__.py b/src/helper_duck/__init__.py index b6fd35e..8ffb145 100644 --- a/src/helper_duck/__init__.py +++ b/src/helper_duck/__init__.py @@ -50,13 +50,13 @@ @bot.event # type: ignore[misc] -async def on_ready() -> None: +async def on_ready() -> None: # type: ignore[misc] """Handle ready event.""" logging.info(f"We have logged in as {bot.user}") -@bot.event -async def on_application_command_error( +@bot.event # type: ignore[misc] +async def on_application_command_error( # type: ignore[misc] ctx: nc.Interaction, err: Exception, ) -> None: @@ -73,11 +73,11 @@ async def on_application_command_error( # close {{{1 -@bot.slash_command( +@bot.slash_command( # type: ignore[misc] description="Close a ticket.", guild_ids=[CONFIG["GUILD_ID"]], ) -async def close(ctx: nc.Interaction, ticket_id: int) -> None: +async def close(ctx: nc.Interaction, ticket_id: int) -> None: # type: ignore[misc] """Handle closing a ticket.""" with DB_CONNECTION: user_name = ctx.user.nick if ctx.user.nick else ctx.user.global_name @@ -258,13 +258,13 @@ def guild_member_and_in_guild(ctx: nc.Interaction) -> bool: # claim {{{1 -@bot.slash_command( +@bot.slash_command( # type: ignore[misc] description="Claim a ticket.", guild_ids=[CONFIG["GUILD_ID"]], ) -@nc_app_checks.check(guild_member_and_in_guild) -@nc_app_checks.has_role(CONFIG["MENTOR_ROLE_ID"]) -async def claim(ctx: nc.Interaction, ticket_id: int) -> None: +@nc_app_checks.check(guild_member_and_in_guild) # type: ignore[misc] +@nc_app_checks.has_role(CONFIG["MENTOR_ROLE_ID"]) # type: ignore[misc] +async def claim(ctx: nc.Interaction, ticket_id: int) -> None: # type: ignore[misc] """Handle claiming a ticket.""" with DB_CONNECTION: mentor_name = ctx.user.nick if ctx.user.nick else ctx.user.global_name @@ -410,13 +410,13 @@ async def claim(ctx: nc.Interaction, ticket_id: int) -> None: # helpme {{{1 # guild id just for testing -@bot.slash_command( +@bot.slash_command( # type: ignore[misc] description="Request help from a mentor.", guild_ids=[CONFIG["GUILD_ID"]], ) # check that message is from a guild and user is a member of said guild. sort of a dumb check, but need for type safety later on. -@nc_app_checks.check(guild_member_and_in_guild) -async def helpme( +@nc_app_checks.check(guild_member_and_in_guild) # type: ignore[misc] +async def helpme( # type: ignore[misc] ctx: nc.Interaction, author_location: str, ticket_message: str, @@ -504,13 +504,13 @@ def bool_checkmark_emoji(value: bool | int) -> str: # view all of your tickets. {{{1 -@bot.slash_command( +@bot.slash_command( # type: ignore[misc] description="View all of your tickets.", guild_ids=[CONFIG["GUILD_ID"]], ) # check that message is from a guild and user is a member of said guild. sort of a dumb check, but need for type safety later on. -@nc_app_checks.check(guild_member_and_in_guild) -async def mytix(ctx: nc.Interaction) -> None: +@nc_app_checks.check(guild_member_and_in_guild) # type: ignore[misc] +async def mytix(ctx: nc.Interaction) -> None: # type: ignore[misc] """Handle viewing all tickets from the target user.""" with DB_CONNECTION: try: @@ -617,12 +617,12 @@ async def mytix(ctx: nc.Interaction) -> None: # view specific ticket details.{{{1 -@bot.slash_command( +@bot.slash_command( # type: ignore[misc] description="View the details of a specific ticket.", guild_ids=[CONFIG["GUILD_ID"]], ) -@nc_app_checks.check(guild_member_and_in_guild) -async def status(ctx: nc.Interaction, ticket_id: int) -> None: +@nc_app_checks.check(guild_member_and_in_guild) # type: ignore[misc] +async def status(ctx: nc.Interaction, ticket_id: int) -> None: # type: ignore[misc] """Handle viewing the details of a given ticket.""" with DB_CONNECTION: db_cursor = DB_CONNECTION.cursor() @@ -710,12 +710,12 @@ def mentor_or_organizer_role(ctx: nc.Interaction) -> bool: # view all open tickets {{{1 -@bot.slash_command( +@bot.slash_command( # type: ignore[misc] description="View all open tickets.", guild_ids=[CONFIG["GUILD_ID"]], ) -@nc_app_checks.check(mentor_or_organizer_role) -async def opentix(ctx: nc.Interaction) -> None: +@nc_app_checks.check(mentor_or_organizer_role) # type: ignore[misc] +async def opentix(ctx: nc.Interaction) -> None: # type: ignore[misc] """Handle viewing all open tickets.""" with DB_CONNECTION: try: @@ -787,12 +787,12 @@ async def opentix(ctx: nc.Interaction) -> None: # view all tickets {{{1 -@bot.slash_command( +@bot.slash_command( # type: ignore[misc] description="View all tickets.", guild_ids=[CONFIG["GUILD_ID"]], ) @nc_app_checks.check(mentor_or_organizer_role) # type: ignore[misc] -async def alltix(ctx: nc.Interaction) -> None: +async def alltix(ctx: nc.Interaction) -> None: # type: ignore[misc] """Handle viewing all tickets.""" with DB_CONNECTION: try: @@ -861,7 +861,7 @@ async def alltix(ctx: nc.Interaction) -> None: # leaderboard {{{1 -@bot.slash_command( +@bot.slash_command( # type: ignore[misc] description="View which mentors have closed the most tickets.", guild_ids=[CONFIG["GUILD_ID"]], ) From 2d4bf6baf9f84dc702aaef25352613f0b83b5e5f Mon Sep 17 00:00:00 2001 From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com> Date: Wed, 19 Mar 2025 02:48:36 -0500 Subject: [PATCH 9/9] Remove unused dependency in [tools] extra --- pyproject.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 0841f55..7439257 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,7 +48,6 @@ run_helper_duck = "helper_duck:run" [project.optional-dependencies] tools = [ "ruff>=0.11.0", - "codespell>=2.3.0", "mypy>=1.15.0", ]