From 4e5c80f1bbb5cc5555986dd24e0cf448696c92b9 Mon Sep 17 00:00:00 2001 From: Riley Flynn Date: Sat, 23 Nov 2024 22:52:40 -0330 Subject: [PATCH] 2024 modernization (#188) * Switch to using uv for package management * Reformat code with ruff * Remove broken plugins * Resolve most linter warnings * First round of plugin pruning * Clean up more plugins * Move DID into FAQ command * Remove prometheus * Clean up dependencies; Remove sentiment analysis command to enable removing of NLTK * WIP migration to module * Re-implement sentry monitoring * Basic plugin system * Port help & LMGTFY * Port number facts * Port remainder of plugins * Plugin shared base class * __init__ * Add missed plugin to plugin list * Update readme * Upgrade dependencies * Standardize on httpx, remove requests * Fix dockerfile * Add env to dockerignore * Remove unused config values --- .dockerignore | 17 +- .env.dist | 3 - .gitignore | 1 + .vscode/settings.json | 11 + Bot.py | 263 -------- Dockerfile | 15 +- Globals.py | 45 -- Plugin.py | 13 - README.md | 99 ++- automata/__init__.py | 0 automata/__main__.py | 39 ++ automata/bot.py | 56 ++ automata/config.py | 33 + automata/mongo.py | 9 + automata/plugins/__init__.py | 36 ++ .../__init__.py => automata/plugins/agenda.py | 27 +- .../__init__.py => automata/plugins/binary.py | 6 +- .../__init__.py => automata/plugins/brainf.py | 8 +- .../plugins/executive_docs.py | 43 +- automata/plugins/faq.py | 141 +++++ .../plugins/fortune_cookie.py | 11 +- .../plugins/instant_answer.py | 7 +- .../__init__.py => automata/plugins/lmgtfy.py | 10 +- .../__init__.py => automata/plugins/man.py | 22 +- .../plugins/mun_identity.py | 112 ++-- .../plugins/number_facts.py | 22 +- .../plugins/starboard.py | 23 +- Utils.py => automata/utils.py | 29 +- docker-compose.yml | 7 +- plugins/3x3 Meme Generator/__init__.py | 211 ------- plugins/3x3 Meme Generator/plugin.json | 6 - plugins/AOC2021/__init__.py | 27 - plugins/AOC2021/plugin.json | 6 - plugins/Agenda/plugin.json | 6 - plugins/Announce/__init__.py | 67 -- plugins/Announce/plugin.json | 6 - plugins/BadFox/__init__.py | 16 - plugins/BadFox/plugin.json | 6 - plugins/Binary/plugin.json | 6 - plugins/Brainf/plugin.json | 6 - plugins/Cards/__init__.py | 27 - plugins/Cards/deck.py | 30 - plugins/Cards/deck_of_cards_api.py | 82 --- plugins/Cards/plugin.json | 6 - plugins/Coinflip/__init__.py | 27 - plugins/Coinflip/plugin.json | 6 - plugins/Continue/__init__.py | 33 - plugins/Continue/gptj.py | 25 - plugins/Continue/plugin.json | 6 - plugins/Course/__init__.py | 126 ---- plugins/Course/bannerScraper.py | 149 ----- plugins/Course/calendarScraper.py | 63 -- plugins/Course/peopleScraper.py | 57 -- plugins/Course/plugin.json | 6 - plugins/Course/rmpScraper.py | 106 ---- plugins/Cowsay/__init__.py | 25 - plugins/Cowsay/plugin.json | 6 - plugins/DID/__init__.py | 22 - plugins/DID/plugin.json | 6 - plugins/Deadly/__init__.py | 19 - plugins/Deadly/plugin.json | 6 - plugins/DisDay/__init__.py | 58 -- plugins/DisDay/plugin.json | 6 - plugins/Ecoji/__init__.py | 25 - plugins/Ecoji/plugin.json | 6 - plugins/ElRater/__init__.py | 16 - plugins/ElRater/plugin.json | 6 - plugins/Emotional Damage/__init__.py | 23 - plugins/Emotional Damage/plugin.json | 6 - plugins/Evil/__init__.py | 18 - plugins/Evil/plugin.json | 6 - plugins/ExecutiveDocs/plugin.json | 6 - plugins/FAQ/__init__.py | 132 ---- plugins/FAQ/plugin.json | 6 - plugins/FRSTT/__init__.py | 16 - plugins/FRSTT/plugin.json | 6 - plugins/FSM/__init__.py | 75 --- plugins/FSM/plugin.json | 6 - plugins/FortuneCookie/plugin.json | 6 - plugins/Halal/__init__.py | 18 - plugins/Halal/plugin.json | 6 - plugins/Hewwo/__init__.py | 19 - plugins/Hewwo/plugin.json | 6 - plugins/HeyGuys/__init__.py | 13 - plugins/HeyGuys/plugin.json | 6 - plugins/InstantAnswer/plugin.json | 6 - plugins/LMGTFY/plugin.json | 6 - plugins/MCWhitelist/__init__.py | 160 ----- plugins/MCWhitelist/mojang_api.py | 69 --- plugins/MCWhitelist/plugin.json | 6 - plugins/MCWhitelist/whitelist_http_api.py | 32 - plugins/MUN Identity/plugin.json | 6 - plugins/Man/plugin.json | 6 - plugins/NFacts/plugin.json | 6 - plugins/Newsline/__init__.py | 119 ---- plugins/Newsline/plugin.json | 6 - plugins/Over9000/__init__.py | 38 -- plugins/Over9000/plugin.json | 6 - plugins/Over9000/quotes.json | 45 -- plugins/Ping/__init__.py | 22 - plugins/Ping/plugin.json | 6 - plugins/Poll/__init__.py | 43 -- plugins/Poll/plugin.json | 6 - plugins/Qwote/__init__.py | 33 - plugins/Qwote/plugin.json | 6 - plugins/SentimentAnalysis/__init__.py | 61 -- plugins/SentimentAnalysis/plugin.json | 6 - plugins/Starboard/plugin.json | 6 - plugins/TodayAtMun/DiaryUtil.py | 118 ---- plugins/TodayAtMun/Tests/test.py | 137 ---- plugins/TodayAtMun/__init__.py | 318 ---------- plugins/TodayAtMun/plugin.json | 6 - plugins/Uptime/__init__.py | 45 -- plugins/Uptime/plugin.json | 6 - plugins/Verilog/__init__.py | 33 - plugins/Verilog/plugin.json | 6 - plugins/Vibe/__init__.py | 25 - plugins/Vibe/plugin.json | 6 - plugins/Weather/__init__.py | 56 -- plugins/Weather/plugin.json | 6 - plugins/WiseQuote/__init__.py | 29 - plugins/WiseQuote/plugin.json | 6 - plugins/robotcheck/__init__.py | 16 - plugins/robotcheck/plugin.json | 6 - pyproject.toml | 19 + requirements.txt | 22 - uv.lock | 583 ++++++++++++++++++ 127 files changed, 1173 insertions(+), 3765 deletions(-) create mode 100644 .vscode/settings.json delete mode 100644 Bot.py delete mode 100644 Globals.py delete mode 100644 Plugin.py create mode 100644 automata/__init__.py create mode 100644 automata/__main__.py create mode 100644 automata/bot.py create mode 100644 automata/config.py create mode 100644 automata/mongo.py create mode 100644 automata/plugins/__init__.py rename plugins/Agenda/__init__.py => automata/plugins/agenda.py (74%) rename plugins/Binary/__init__.py => automata/plugins/binary.py (62%) rename plugins/Brainf/__init__.py => automata/plugins/brainf.py (87%) rename plugins/ExecutiveDocs/__init__.py => automata/plugins/executive_docs.py (75%) create mode 100644 automata/plugins/faq.py rename plugins/FortuneCookie/__init__.py => automata/plugins/fortune_cookie.py (79%) rename plugins/InstantAnswer/__init__.py => automata/plugins/instant_answer.py (88%) rename plugins/LMGTFY/__init__.py => automata/plugins/lmgtfy.py (67%) rename plugins/Man/__init__.py => automata/plugins/man.py (88%) rename plugins/MUN Identity/__init__.py => automata/plugins/mun_identity.py (72%) rename plugins/NFacts/__init__.py => automata/plugins/number_facts.py (79%) rename plugins/Starboard/__init__.py => automata/plugins/starboard.py (88%) rename Utils.py => automata/utils.py (75%) delete mode 100644 plugins/3x3 Meme Generator/__init__.py delete mode 100644 plugins/3x3 Meme Generator/plugin.json delete mode 100644 plugins/AOC2021/__init__.py delete mode 100644 plugins/AOC2021/plugin.json delete mode 100644 plugins/Agenda/plugin.json delete mode 100644 plugins/Announce/__init__.py delete mode 100644 plugins/Announce/plugin.json delete mode 100644 plugins/BadFox/__init__.py delete mode 100644 plugins/BadFox/plugin.json delete mode 100644 plugins/Binary/plugin.json delete mode 100644 plugins/Brainf/plugin.json delete mode 100644 plugins/Cards/__init__.py delete mode 100644 plugins/Cards/deck.py delete mode 100644 plugins/Cards/deck_of_cards_api.py delete mode 100644 plugins/Cards/plugin.json delete mode 100644 plugins/Coinflip/__init__.py delete mode 100644 plugins/Coinflip/plugin.json delete mode 100644 plugins/Continue/__init__.py delete mode 100644 plugins/Continue/gptj.py delete mode 100644 plugins/Continue/plugin.json delete mode 100644 plugins/Course/__init__.py delete mode 100644 plugins/Course/bannerScraper.py delete mode 100644 plugins/Course/calendarScraper.py delete mode 100644 plugins/Course/peopleScraper.py delete mode 100644 plugins/Course/plugin.json delete mode 100644 plugins/Course/rmpScraper.py delete mode 100644 plugins/Cowsay/__init__.py delete mode 100644 plugins/Cowsay/plugin.json delete mode 100644 plugins/DID/__init__.py delete mode 100644 plugins/DID/plugin.json delete mode 100644 plugins/Deadly/__init__.py delete mode 100755 plugins/Deadly/plugin.json delete mode 100644 plugins/DisDay/__init__.py delete mode 100644 plugins/DisDay/plugin.json delete mode 100644 plugins/Ecoji/__init__.py delete mode 100644 plugins/Ecoji/plugin.json delete mode 100644 plugins/ElRater/__init__.py delete mode 100644 plugins/ElRater/plugin.json delete mode 100644 plugins/Emotional Damage/__init__.py delete mode 100644 plugins/Emotional Damage/plugin.json delete mode 100644 plugins/Evil/__init__.py delete mode 100644 plugins/Evil/plugin.json delete mode 100644 plugins/ExecutiveDocs/plugin.json delete mode 100644 plugins/FAQ/__init__.py delete mode 100644 plugins/FAQ/plugin.json delete mode 100644 plugins/FRSTT/__init__.py delete mode 100644 plugins/FRSTT/plugin.json delete mode 100644 plugins/FSM/__init__.py delete mode 100644 plugins/FSM/plugin.json delete mode 100644 plugins/FortuneCookie/plugin.json delete mode 100644 plugins/Halal/__init__.py delete mode 100644 plugins/Halal/plugin.json delete mode 100644 plugins/Hewwo/__init__.py delete mode 100644 plugins/Hewwo/plugin.json delete mode 100644 plugins/HeyGuys/__init__.py delete mode 100644 plugins/HeyGuys/plugin.json delete mode 100644 plugins/InstantAnswer/plugin.json delete mode 100644 plugins/LMGTFY/plugin.json delete mode 100644 plugins/MCWhitelist/__init__.py delete mode 100644 plugins/MCWhitelist/mojang_api.py delete mode 100644 plugins/MCWhitelist/plugin.json delete mode 100644 plugins/MCWhitelist/whitelist_http_api.py delete mode 100644 plugins/MUN Identity/plugin.json delete mode 100644 plugins/Man/plugin.json delete mode 100644 plugins/NFacts/plugin.json delete mode 100644 plugins/Newsline/__init__.py delete mode 100644 plugins/Newsline/plugin.json delete mode 100755 plugins/Over9000/__init__.py delete mode 100755 plugins/Over9000/plugin.json delete mode 100755 plugins/Over9000/quotes.json delete mode 100644 plugins/Ping/__init__.py delete mode 100644 plugins/Ping/plugin.json delete mode 100644 plugins/Poll/__init__.py delete mode 100644 plugins/Poll/plugin.json delete mode 100644 plugins/Qwote/__init__.py delete mode 100644 plugins/Qwote/plugin.json delete mode 100644 plugins/SentimentAnalysis/__init__.py delete mode 100644 plugins/SentimentAnalysis/plugin.json delete mode 100644 plugins/Starboard/plugin.json delete mode 100644 plugins/TodayAtMun/DiaryUtil.py delete mode 100644 plugins/TodayAtMun/Tests/test.py delete mode 100644 plugins/TodayAtMun/__init__.py delete mode 100644 plugins/TodayAtMun/plugin.json delete mode 100644 plugins/Uptime/__init__.py delete mode 100644 plugins/Uptime/plugin.json delete mode 100644 plugins/Verilog/__init__.py delete mode 100644 plugins/Verilog/plugin.json delete mode 100644 plugins/Vibe/__init__.py delete mode 100644 plugins/Vibe/plugin.json delete mode 100644 plugins/Weather/__init__.py delete mode 100644 plugins/Weather/plugin.json delete mode 100644 plugins/WiseQuote/__init__.py delete mode 100644 plugins/WiseQuote/plugin.json delete mode 100644 plugins/robotcheck/__init__.py delete mode 100644 plugins/robotcheck/plugin.json create mode 100644 pyproject.toml delete mode 100644 requirements.txt create mode 100644 uv.lock diff --git a/.dockerignore b/.dockerignore index 2c72390..cd32a31 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,3 +1,16 @@ -data/ +.dockerignore .env -mounted_plugins/ \ No newline at end of file +.env.dist +.git +.github +.gitignore +.python-version +.ruff_cache +.venv +.vscode +**/__pycache__/ +**/.DS_Store +docker-compose.yml +Dockerfile +LICENSE +README.md diff --git a/.env.dist b/.env.dist index d189430..545a9f5 100644 --- a/.env.dist +++ b/.env.dist @@ -5,11 +5,8 @@ AUTOMATA_VERIFIED_ROLE= AUTOMATA_ENABLED_PLUGINS= AUTOMATA_DISABLED_PLUGINS= AUTOMATA_EXECUTIVE_DOCS_CHANNEL= -AUTOMATA_GENERAL_CHANNEL= AUTOMATA_MEMBER_INTENTS_ENABLED= AUTOMATA_STARBOARD_CHANNEL_ID= AUTOMATA_STARBOARD_THRESHOLD= -AUTOMATA_DIARY_DAILY_CHANNEL= -WHITELIST_HTTP_API_BEARER_TOKEN= SENTRY_DSN= DISCORD_AUTH_URI= diff --git a/.gitignore b/.gitignore index 623cdc3..086d795 100644 --- a/.gitignore +++ b/.gitignore @@ -108,6 +108,7 @@ data/mongo/ ### VisualStudioCode ### .vscode/* *.code-workspace +!.vscode/settings.json ### VisualStudioCode Patch ### # Ignore all local history of files diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..b7acb08 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,11 @@ +{ + "editor.formatOnSave": true, + "[python]": { + "editor.defaultFormatter": "charliermarsh.ruff" + }, + "editor.codeActionsOnSave": { + "source.fixAll": "always", + "source.organizeImports": "always", + "source.unusedImports": "always" + } +} diff --git a/Bot.py b/Bot.py deleted file mode 100644 index eede537..0000000 --- a/Bot.py +++ /dev/null @@ -1,263 +0,0 @@ -import asyncio -from dotenv import load_dotenv -import motor.motor_asyncio - -from Utils import CustomHelp - -load_dotenv() - -import os -import logging -import traceback -import contextlib -import sys -from pathlib import Path -from io import StringIO - -from jigsaw.PluginLoader import PluginLoader -import discord -from discord.ext import commands -from prometheus_async.aio.web import start_http_server - -from Plugin import AutomataPlugin -from typing import Optional, Literal - -from Globals import ( - DISABLED_PLUGINS, - ENABLED_PLUGINS, - MONGO_ADDRESS, -) - -IGNORED_LOGGERS = [ - "discord.client", - "discord.gateway", - "discord.state", - "discord.gateway", - "discord.http", - "websockets.protocol", -] - -# Configure logger and silence ignored loggers -logging.basicConfig( - format="{%(asctime)s} (%(name)s) [%(levelname)s]: %(message)s", - datefmt="%x, %X", - level=logging.DEBUG, -) - -for logger in IGNORED_LOGGERS: - logging.getLogger(logger).setLevel(logging.WARNING) - -logger = logging.getLogger("Automata") - -AUTOMATA_TOKEN = os.getenv("AUTOMATA_TOKEN", None) - -if not AUTOMATA_TOKEN: - logger.error( - "AUTOMATA_TOKEN environment variable not set, have you created a .env file and populated it yet?" - ) - exit(1) - -intents = discord.Intents.default() -intents.message_content = True -intents.members = os.getenv("AUTOMATA_MEMBER_INTENTS_ENABLED", "True") == "True" - - -class Automata(commands.Bot): - def __init__(self, *args, **kwargs): - super().__init__(*args, intents=intents, **kwargs) - - async def setup_hook(self) -> None: - self.database = motor.motor_asyncio.AsyncIOMotorClient(MONGO_ADDRESS) - self.loop.create_task(self.enable_plugins()) - - async def enable_plugins(self) -> None: - for plugin in loader.get_all_plugins(): - if plugin["plugin"]: - await plugin["plugin"].enable() - - -bot = Automata( - command_prefix="!", - help_command=CustomHelp(), - description="A custom, multi-purpose moderation bot for the MUN Computer Science Society Discord server.", -) - - -@bot.event -async def on_message(message): - # Log messages - if isinstance(message.channel, discord.DMChannel): - name = message.author.name - else: - name = message.channel.name - logger.info(f"[{name}] {message.author.name}: {message.content}") - await bot.process_commands(message) - - -@bot.event -async def on_ready(): - # When the bot is ready, start the prometheus client - await start_http_server(port=9000) - - -if os.getenv("SENTRY_DSN", None): - - @bot.event - async def on_error(event, *args, **kwargs): - raise - - @bot.event - async def on_command_error(ctx, exception): - raise exception - - -@contextlib.contextmanager -def stdioreader(): - old = (sys.stdout, sys.stderr) - stdout = StringIO() - stderr = StringIO() - sys.stdout = stdout - sys.stderr = stderr - yield stdout, stderr - sys.stdout = old[0] - sys.stderr = old[1] - - -@bot.command(name="eval") -@commands.is_owner() -async def eval_code(ctx: commands.Context, code: str): - """Evaluates code for debugging purposes.""" - try: - result = f"```\n{eval(code)}\n```" - colour = discord.Colour.green() - except: - result = f"```py\n{traceback.format_exc(1)}```" - colour = discord.Colour.red() - - result.replace("\\", "\\\\") - - embed = discord.Embed() - embed.add_field(name=code, value=result) - embed.colour = colour - - await ctx.send(embed=embed) - - -@bot.command(name="exec") -@commands.is_owner() -async def exec_code(ctx: commands.Context, code: str): - """Executes code for debugging purposes.""" - with stdioreader() as (out, err): - try: - exec(code) - result = f"```\n{out.getvalue()}\n```" - colour = discord.Colour.green() - except: - result = f"```py\n{traceback.format_exc(1)}```" - colour = discord.Colour.red() - - result.replace("\\", "\\\\") - - embed = discord.Embed() - embed.add_field(name=code, value=result) - embed.colour = colour - - await ctx.send(embed=embed) - - -@bot.command() -async def plugins(ctx: commands.Context): - """Lists all enabled plugins.""" - embed = discord.Embed() - embed.colour = discord.Colour.blurple() - - for plugin in loader.get_all_plugins(): - if plugin["plugin"]: - embed.add_field( - name=plugin["manifest"]["name"], - value="{}\nVersion: {}\nAuthor: {}".format( - plugin["plugin"].__doc__.rstrip(), - plugin["manifest"]["version"], - plugin["manifest"]["author"], - ), - ) - - await ctx.send(embed=embed) - - -plugins_dir = Path("./plugins") -mounted_plugins_dir = Path("./mounted_plugins") -mounted_plugins_dir.mkdir(exist_ok=True) - -loader = PluginLoader( - plugin_paths=(str(plugins_dir), str(mounted_plugins_dir)), - plugin_class=AutomataPlugin, -) -loader.load_manifests() - -num_of_disabled = 0 - -for plugin in loader.get_all_plugins(): - manifest = plugin["manifest"] - if len(ENABLED_PLUGINS) > 0: - if manifest["main_class"] in ENABLED_PLUGINS: - loader.load_plugin(manifest, bot) - else: - num_of_disabled += 1 - else: - if manifest["main_class"] not in DISABLED_PLUGINS: - loader.load_plugin(manifest, bot) - else: - logger.info(f"{manifest['name']} disabled.") - num_of_disabled += 1 - -logger.info(f"{num_of_disabled} plugins disabled.") - - -@bot.command() -@commands.guild_only() -@commands.has_permissions(view_audit_log=True) -async def sync( - ctx: commands.Context, - guilds: commands.Greedy[discord.Object], - spec: Optional[Literal["~", "*", "^"]] = None, -) -> None: - """Sync application commands to guild(s). - `~` - sync current guild application commands. - `*` - Copy all global application commands to current guild and sync. - `^` - Clear all application commands from current guild and sync. - `` - Sync all guilds application commands globally. - guilds - Sync application commands to guild(s). - """ - if not guilds: - if spec == "~": - # sync current guild - synced = await ctx.bot.tree.sync(guild=ctx.guild) - elif spec == "*": - # copies all global app commands to current guild and syncs - ctx.bot.tree.copy_global_to(guild=ctx.guild) - synced = await ctx.bot.tree.sync(guild=ctx.guild) - elif spec == "^": - # clears all commands from the current guild target and syncs (removes guild commands) - ctx.bot.tree.clear_commands(guild=ctx.guild) - await ctx.bot.tree.sync(guild=ctx.guild) - synced = [] - else: - # global sync - synced = await ctx.bot.tree.sync() - await ctx.send( - f"Synced {len(synced)} commands {'globally' if spec is None else 'to the current guild.'}" - ) - return - # syncs guilds with id 1 and 2 - ret = 0 - for guild in guilds: - try: - await ctx.bot.tree.sync(guild=guild) - except discord.HTTPException: - pass - else: - ret += 1 - await ctx.send(f"Synced the tree to {ret}/{len(guilds)}.") - -bot.run(AUTOMATA_TOKEN) diff --git a/Dockerfile b/Dockerfile index d1df5b4..acb46ef 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,8 +1,11 @@ -FROM python:3.11-alpine3.18 -COPY . /app +FROM ghcr.io/astral-sh/uv:python3.12-alpine + WORKDIR /app -RUN apk --update add --virtual build-dependencies gcc musl-dev libxml2-dev libxslt-dev --no-cache \ - && pip install -r requirements.txt \ - && apk del build-dependencies -CMD ["python", "Bot.py"] +COPY pyproject.toml uv.lock ./ +RUN uv sync --frozen +ENV PATH="/app/.venv/bin:$PATH" + +COPY . . + +CMD ["python", "-m", "automata"] diff --git a/Globals.py b/Globals.py deleted file mode 100644 index 9655547..0000000 --- a/Globals.py +++ /dev/null @@ -1,45 +0,0 @@ -import os - -import motor.motor_asyncio -import sentry_sdk - -ENABLED_PLUGINS = [x for x in os.getenv("AUTOMATA_ENABLED_PLUGINS", "").split(",") if x] -DISABLED_PLUGINS = os.getenv("AUTOMATA_DISABLED_PLUGINS", "").split(",") - -PRIMARY_GUILD = int(os.getenv("AUTOMATA_PRIMARY_GUILD", 514110851016556567)) -VERIFIED_ROLE = int(os.getenv("AUTOMATA_VERIFIED_ROLE", 564672793380388873)) - -GENERAL_CHANNEL = int(os.getenv("AUTOMATA_GENERAL_CHANNEL", 514110851641376809)) - -EXECUTIVE_DOCS_CHANNEL = int( - os.getenv("AUTOMATA_EXECUTIVE_DOCS_CHANNEL", 773246740002373652) -) - -DIARY_DAILY_CHANNEL = int(os.getenv("AUTOMATA_DIARY_DAILY_CHANNEL", 872217138851614750)) - -NEWSLINE_CHANNEL = int(os.getenv("AUTOMATA_NEWSLINE_CHANNEL", 811433377642446869)) - -WHITELIST_HTTP_API_BEARER_TOKEN = os.getenv("WHITELIST_HTTP_API_BEARER_TOKEN", None) - -MONGO_HOST = os.getenv("AUTOMATA_MONGO_HOST", "mongo") -MONGO_ADDRESS = f"mongodb://{MONGO_HOST}/automata" - -DISCORD_AUTH_URI = os.getenv("DISCORD_AUTH_URI", "https://discord.muncompsci.ca") - -WEATHER_API_KEY = os.getenv("AUTOMATA_WEATHER_API_KEY") - -STARBOARD_CHANNEL_ID = int( - os.getenv("AUTOMATA_STARBOARD_CHANNEL_ID", 900883422187253870) -) -STARBOARD_THRESHOLD = int(os.getenv("AUTOMATA_STARBOARD_THRESHOLD", 5)) - -ANNOUNCEMENT_CHANNEL = int( - os.getenv("AUTOMATA_ANNOUNCEMENT_CHANNEL", 752914074504790068) -) - -AOC_LEADERBOARD_CHANNEL = int( - os.getenv("AUTOMATA_ANNOUNCEMENT_CHANNEL", 909857762064871444) -) - -if os.getenv("SENTRY_DSN", None): - sentry_sdk.init(os.environ["SENTRY_DSN"]) diff --git a/Plugin.py b/Plugin.py deleted file mode 100644 index e94af16..0000000 --- a/Plugin.py +++ /dev/null @@ -1,13 +0,0 @@ -from jigsaw import JigsawPlugin - -from discord.ext.commands import Cog, Bot - - -class AutomataPlugin(JigsawPlugin, Cog): - def __init__(self, manifest, bot: Bot): - JigsawPlugin.__init__(self, manifest) - Cog.__init__(self) - self.bot = bot - - async def enable(self): - await self.bot.add_cog(self) diff --git a/README.md b/README.md index 64ae162..606da9a 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ ![Deploy to GHCR](https://github.com/MUNComputerScienceSociety/Automata/workflows/Deploy%20to%20GHCR/badge.svg) [![Code scanning - action](https://github.com/MUNComputerScienceSociety/Automata/actions/workflows/codeql-analysis.yml/badge.svg)](https://github.com/MUNComputerScienceSociety/Automata/actions/workflows/codeql-analysis.yml) -[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) +[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) Discord bot handling the management of the [MUN Computer Science Society](https://muncompsci.ca/) Discord server @@ -10,91 +10,76 @@ For feature requests / help getting the bot running, don't fret to ask questions --- -## **Running locally** +## Running locally 1. Clone the project by running `https://github.com/MUNComputerScienceSociety/Automata.git`, and change into the directory by running `cd ./Automata` 2. Copy `.env.dist` to `.env` 3. Fill out the required information in the `.env` - - At the moment, the only required environment variable required is `AUTOMATA_TOKEN`, which is a Discord token, which you can see how to get [here](https://discordpy.readthedocs.io/en/latest/discord.html) + - At the moment, the required environment variables are: + + - `AUTOMATA_TOKEN`: A Discord token, which you can see how to get [here](https://discordpy.readthedocs.io/en/latest/discord.html) + - `AUTOMATA_PRIMARY_GUILD`: The ID of the Discord server you will be testing in. You can find this by following the instructions [here](https://support.discord.com/hc/en-us/articles/206346498-Where-can-I-find-my-User-Server-Message-ID). + - `AUTOMATA_EXECUTIVE_DOCS_CHANNEL`: The ID of the channel to post executive docs notifications to. This can be any channel in your primary guild, with the warning that it'll be a bit noisy on first start. You can find this by following the instructions [here](https://support.discord.com/hc/en-us/articles/206346498-Where-can-I-find-my-User-Server-Message-ID). + +> [!NOTE] > Note about running locally and avoiding spam: > -> When this bot was smaller, it wasn't so bad to run all the plugins; but since we have 40+, and some of them do some data fetching / loading on startup, so the logs can get a bit loud. +> When this bot was smaller, it wasn't so bad to run all the plugins; but since we have 10+, and some of them do some data fetching / loading on startup, the logs can get a bit loud. > -> If you're working on a plugin, you can add its name to the `AUTOMATA_ENABLED_PLUGINS` env. var (like AUTOMATA_ENABLED_PLUGINS=PluginName), and it will be the _only_ plugin that will be loaded. +> If you're working on a plugin, you can add its name to the `AUTOMATA_ENABLED_PLUGINS` env. var (like `AUTOMATA_ENABLED_PLUGINS=["PluginName"]`), and it will be the _only_ plugin that will be loaded. > -> This is a comma-delimited list as well, so you can enable multiple plugins at once. - -### Locally Using Docker +> This env. var is a JSON-encoded list, so you can enable multiple plugins at once. -1. Create the directory `mounted_plugins` within the project by running `mkdir ./mounted_plugins` -2. Start the containers by running `docker-compose up -d` +Once done, follow the instructions under either of the headings below, depending on how you wish to run the bot. -### Locally Without Docker +### With Docker -1. Run MongoDB +Start the containers by running `docker-compose up -d` - > You can use Docker for running MongoDB (recommended), just add the following to the `docker-compose.yml` file to expose it to your local machine - > - > ```yml - > ... - > mongo: - > ... - > ports: - > - "127.0.0.1:27017:27017" - > ... - > ``` - > - > And start it _only_ by running `docker-compose up -d mongo` +### Without Docker -2. Install the requirements found in the `requirements.txt` file using `pip install -r requirements.txt` +1. Install [uv](https://docs.astral.sh/uv/getting-started/installation/) and Python 3.12 or above. +2. Run MongoDB - - Using a virtual environment is highly recommended, [this section of the Flask documentation explains this well](https://flask.palletsprojects.com/en/1.1.x/installation/#virtual-environments). + - You can use Docker for running MongoDB (recommended), by starting only the `mongo` container with `docker-compose up -d mongo` + - Alternatively, you can install Mongo via whatever means is recommended for your OS -3. Run the bot using `python Bot.py` +3. Install the dependencies with `uv sync` +4. Run the bot using `uv run python -m automata` --- -## **Developing your own plugins** - -- Automata is built around the [discord.py](https://discordpy.readthedocs.io/en/latest/) framework, therefore the plugins make heavy use of its decorators to abstract most of the complexity behind the scenes. - -### Developing using Docker - -1. Create the folder `mounted_plugins` if it doesn't already exist - - `plugins` is baked into the image when it is built, so editing files there won't have an effect -2. Create a new plugin within the `mounted_plugins` folder - 1. Create a new [Jigsaw plugin manifest](https://jigsaw.readthedocs.io/en/latest/plugin.json.html) - - You can use `plugins/Ping/plugin.json` as an example - 2. Create a new plugin - - You can use `plugins/Ping/__init__.py` as an example -3. Start the bot using the instructions from [Running locally](#running-locally) +## Developing your own plugins -When you make changes to your plugins, restart the Automata container using `docker-compose restart automata` +Features are provided to the bot via plugins - if you wish to add your own functionality, you should build your own plugin. To do so: -### Developing without Docker +1. Create a new file in `automata/plugins` for your plugin +2. Add to your file the code for your plugin + - This plugin will be a [discord.py cog](https://discordpy.readthedocs.io/en/stable/ext/commands/cogs.html) - you can refer to their docs for examples of the things you can do and how to do them + - You can use `lmgtfy.py` as a simple example +3. In `automata/plugins/__init__.py`, import your plugin and add it to the `all_plugins` list + - Once again, you can copy this from the `lmgtfy.py` example -1. Create a new plugin, following the directions above within 'Using Docker', but within the `plugins` folder instead -2. Start the bot using the instructions from [Running locally](#running-locally) +Once done, you can run your plugin locally by following the instructions under [Running your changes locally](#running-your-changes-locally). --- -## **Developing the bot core and built-in plugins** +## Running your changes locally -### Developing core using Docker +### Using Docker -1. Edit the `docker-compose.yml`, replacing the `image: muncs/automata` line for the automata container with `build: .` +1. Edit the `docker-compose.yml`, and comment out the `image` line in the `automata` container and uncomment the `build` line 2. Edit the bot core or the plugins as you wish 3. Start the container, forcing a rebuild of the image using `docker-compose up -d --build` -### Developing core without Docker +### Without Docker -1. Just edit the core files / plugins directly :) -2. Start the bot using the instructions from [Running locally](#running-locally) +If you do not want to run the bot in Docker, you can just start the bot using the instructions from [Running locally](#running-locally) - no special steps required. --- -## **Pushing changes to GitHub** +## Pushing changes to GitHub 1. Fork this repository, clone your fork, and commit your changes to a branch on your fork 2. Create a PR to merge your branch into the `master` branch here, and make sure to tag an executive / mention the PR in Discord so we see it @@ -103,11 +88,11 @@ When you make changes to your plugins, restart the Automata container using `doc --- -## **Container responsibilities** +## Container responsibilities Automata is comprised of a number of containers, each with distinct responsibilities. Their responsibilities are as follows: -| Container | Responsibilities | -| --- | --- | -| automata | The Discord bot itself | -| mongo | A MongoDB server used to provide persistent data storage to the `automata` container | +| Container | Responsibilities | +| --------- | ------------------------------------------------------------------------------------ | +| automata | The Discord bot itself | +| mongo | A MongoDB server used to provide persistent data storage to the `automata` container | diff --git a/automata/__init__.py b/automata/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/automata/__main__.py b/automata/__main__.py new file mode 100644 index 0000000..cda2686 --- /dev/null +++ b/automata/__main__.py @@ -0,0 +1,39 @@ +import logging + +import sentry_sdk + +from automata.bot import bot +from automata.config import config + +IGNORED_LOGGERS = [ + "asyncio", + "discord.client", + "discord.gateway", + "discord.gateway", + "discord.http", + "discord.state", + "httpcore.connection", + "httpcore.http11", + "httpx", + "pymongo.command", + "pymongo.connection", + "pymongo.serverSelection", + "pymongo.topology", + "urllib3", + "websockets.protocol", +] + +# Configure logger and silence ignored loggers +logging.basicConfig( + format="{%(asctime)s} (%(name)s) [%(levelname)s]: %(message)s", + datefmt="%x, %X", + level=logging.DEBUG, +) + +for logger in IGNORED_LOGGERS: + logging.getLogger(logger).setLevel(logging.WARNING) + +if config.sentry_dsn: + sentry_sdk.init(config.sentry_dsn) + +bot.run(config.token) diff --git a/automata/bot.py b/automata/bot.py new file mode 100644 index 0000000..610f4cd --- /dev/null +++ b/automata/bot.py @@ -0,0 +1,56 @@ +import logging +from typing import Any, cast + +import discord +from discord.ext import commands + +import automata.plugins as plugins +from automata.config import config +from automata.utils import CommandContext, CustomHelp + +intents = discord.Intents.default() +intents.message_content = True +intents.members = config.member_intents_enabled + + +class Automata(commands.Bot): + async def setup_hook(self) -> None: + for plugin in plugins.enabled_plugins: + await self.add_cog(plugin(self)) + + +bot = Automata( + command_prefix="!", + description="A custom, multi-purpose moderation bot for the MUN Computer Science Society Discord server.", + intents=intents, + help_command=CustomHelp(), +) + +logger = logging.getLogger(__name__) + + +@bot.event +async def on_message(message: discord.Message): + if isinstance(message.channel, discord.DMChannel): + name = message.author.name + else: + channel = cast(discord.TextChannel, message.channel) + name = channel.name + + logger.info(f"[{name}] {message.author.name}: {message.content}") + + await bot.process_commands(message) + + +if config.sentry_dsn: + + @bot.event + async def on_error(event: str, *args: Any, **kwargs: Any): + raise + + @bot.event + async def on_command_error(ctx: CommandContext, exception: commands.CommandError): + raise exception + + +__all__ = ["bot"] diff --git a/automata/config.py b/automata/config.py new file mode 100644 index 0000000..39d4259 --- /dev/null +++ b/automata/config.py @@ -0,0 +1,33 @@ +from pydantic_settings import BaseSettings, SettingsConfigDict + + +class Config(BaseSettings): + model_config = SettingsConfigDict(env_prefix="automata_", env_file=".env") + + discord_auth_uri: str = "https://discord.muncompsci.ca" + executive_docs_channel: int = 773246740002373652 + member_intents_enabled: bool = True + mongo_host: str = "localhost" + primary_guild: int = 514110851016556567 + sentry_dsn: str | None = None + starboard_channel_id: int = 900883422187253870 + starboard_threshold: int = 5 + token: str + verified_role: int = 564672793380388873 + + enabled_plugins: list[str] = [] + disabled_plugins: list[str] = [] + + @property + def mongo_address(self) -> str: + return f"mongodb://{self.mongo_host}/automata" + + def should_enable_plugin(self, plugin: type) -> bool: + if self.enabled_plugins: + return plugin.__name__ in self.enabled_plugins + return plugin.__name__ not in self.disabled_plugins + + +config = Config() # type: ignore - Pydantic and Pyright don't play nice, but Discord.py doesn't work with Mypy + +__all__ = ["config"] diff --git a/automata/mongo.py b/automata/mongo.py new file mode 100644 index 0000000..b26a690 --- /dev/null +++ b/automata/mongo.py @@ -0,0 +1,9 @@ +from typing import Any + +import motor.motor_asyncio + +from automata.config import config + +mongo = motor.motor_asyncio.AsyncIOMotorClient[Any](config.mongo_address) + +__all__ = ["mongo"] diff --git a/automata/plugins/__init__.py b/automata/plugins/__init__.py new file mode 100644 index 0000000..e232f3b --- /dev/null +++ b/automata/plugins/__init__.py @@ -0,0 +1,36 @@ +from automata.config import config +from automata.utils import Plugin + +from .agenda import Agenda +from .binary import Binary +from .brainf import Brainf +from .executive_docs import ExecutiveDocs +from .faq import FAQ +from .fortune_cookie import FortuneCookie +from .instant_answer import InstantAnswer +from .lmgtfy import LMGTFY +from .man import Man +from .mun_identity import MUNIdentity +from .number_facts import NumberFacts +from .starboard import Starboard + +all_plugins: list[type[Plugin]] = [ + Agenda, + Binary, + Brainf, + ExecutiveDocs, + FAQ, + FortuneCookie, + InstantAnswer, + LMGTFY, + Man, + MUNIdentity, + NumberFacts, + Starboard, +] + +enabled_plugins = [ + plugin for plugin in all_plugins if config.should_enable_plugin(plugin) +] + +__all__ = ["enabled_plugins"] diff --git a/plugins/Agenda/__init__.py b/automata/plugins/agenda.py similarity index 74% rename from plugins/Agenda/__init__.py rename to automata/plugins/agenda.py index c614ee2..e84c12d 100644 --- a/plugins/Agenda/__init__.py +++ b/automata/plugins/agenda.py @@ -1,19 +1,19 @@ -from datetime import datetime import logging import uuid +from datetime import datetime from discord.ext import commands -from Plugin import AutomataPlugin -from Utils import send_code_block_maybe_as_file +from automata.mongo import mongo +from automata.utils import CommandContext, Plugin, send_code_block_maybe_as_file logger = logging.getLogger("Agenda") -class Agenda(AutomataPlugin): +class Agenda(Plugin): """Handles tracking agenda items, and exporting them as markdown""" - async def send_agenda_text(self, ctx, variant): + async def send_agenda_text(self, ctx: CommandContext, variant: str | None): items = self.agenda_items.find({}) text = "% MUN Computer Science Society\n% Meeting Agenda\n" @@ -31,20 +31,17 @@ async def send_agenda_text(self, ctx, variant): await send_code_block_maybe_as_file(ctx, text) - def __init__(self, manifest, bot: commands.Bot): - super().__init__(manifest, bot) - async def cog_load(self): - self.agenda_items = self.bot.database.automata.agenda_items + self.agenda_items = mongo.automata.agenda_items @commands.group() - async def agenda(self, ctx): + async def agenda(self, ctx: CommandContext): """Agenda management commands""" pass @agenda.command() @commands.has_permissions(manage_messages=True) - async def add(self, ctx, title: str, description: str): + async def add(self, ctx: CommandContext, title: str, description: str): """Adds an agenda item""" id = str(uuid.uuid4())[:8] @@ -61,23 +58,23 @@ async def add(self, ctx, title: str, description: str): await ctx.send(f"Added item: {title} (`{id}`), with description: {description}") @agenda.command() - async def view(self, ctx, variant: str = None): + async def view(self, ctx: CommandContext, variant: str | None = None): """Views all agenda items""" await self.send_agenda_text(ctx, variant) @agenda.command() @commands.has_permissions(manage_messages=True) - async def clear(self, ctx): + async def clear(self, ctx: CommandContext): """Clears all agenda items""" await self.send_agenda_text(ctx, "clean") self.agenda_items.delete_many({}) - await ctx.send(f"Cleared all agenda items, output of previous items: {text}") + await ctx.send("Cleared all agenda items") @agenda.command() @commands.has_permissions(manage_messages=True) - async def remove(self, ctx, id: str): + async def remove(self, ctx: CommandContext, id: str): """Removes an agenda item by id""" await self.agenda_items.delete_one({"id": id}) diff --git a/plugins/Binary/__init__.py b/automata/plugins/binary.py similarity index 62% rename from plugins/Binary/__init__.py rename to automata/plugins/binary.py index fedc499..b58c026 100644 --- a/plugins/Binary/__init__.py +++ b/automata/plugins/binary.py @@ -1,13 +1,13 @@ from discord.ext import commands -from Plugin import AutomataPlugin +from automata.utils import CommandContext, Plugin -class Binary(AutomataPlugin): +class Binary(Plugin): """Binary""" @commands.command() - async def binary(self, ctx: commands.Context, message: str): + async def binary(self, ctx: CommandContext, message: str): binaryString = "" for char in message: diff --git a/plugins/Brainf/__init__.py b/automata/plugins/brainf.py similarity index 87% rename from plugins/Brainf/__init__.py rename to automata/plugins/brainf.py index 84d2b58..bf434f2 100644 --- a/plugins/Brainf/__init__.py +++ b/automata/plugins/brainf.py @@ -1,11 +1,11 @@ from discord.ext import commands -from Plugin import AutomataPlugin +from automata.utils import CommandContext, Plugin pointer, data, output = None, None, None -def find_end(source): +def find_end(source: str) -> int: loops = 1 for i in range(len(source)): loops += (source[i] == "[") - (source[i] == "]") @@ -41,11 +41,11 @@ def execute_loop(source): pass -class Brainf(AutomataPlugin): +class Brainf(Plugin): """Brainf*ck""" @commands.command() - async def bf(self, ctx: commands.Context, message: str): + async def bf(self, ctx: CommandContext, message: str): """Responds with the output of running the message in brainf*ck""" await ctx.send(execute(message)) diff --git a/plugins/ExecutiveDocs/__init__.py b/automata/plugins/executive_docs.py similarity index 75% rename from plugins/ExecutiveDocs/__init__.py rename to automata/plugins/executive_docs.py index a03c5ae..666c6fb 100644 --- a/plugins/ExecutiveDocs/__init__.py +++ b/automata/plugins/executive_docs.py @@ -1,14 +1,14 @@ import asyncio from datetime import datetime -import httpx +from typing import Any + import discord -from discord.ext import commands, tasks +import httpx +from discord.ext import tasks -from Plugin import AutomataPlugin -from Globals import ( - PRIMARY_GUILD, - EXECUTIVE_DOCS_CHANNEL, -) +from automata.config import config +from automata.mongo import mongo +from automata.utils import Plugin EXECUTIVE_DOCS_BASE_URI = "https://www.cs.mun.ca/~csclub/executive-documents" EXECUTIVE_DOCS_JSON_URI = f"{EXECUTIVE_DOCS_BASE_URI}/docs.json" @@ -20,11 +20,13 @@ "Agendas": discord.Colour.dark_gray(), } +type Doc = Any + -class ExecutiveDocs(AutomataPlugin): +class ExecutiveDocs(Plugin): """Posts new executive documents""" - def doc_embed(self, doc): + def doc_embed(self, doc: Doc) -> discord.Embed: embed = discord.Embed( title=f"Meeting {doc['type']} | {doc['time'].strftime('%A, %B %e, %Y')}", description=doc["url"], @@ -37,17 +39,23 @@ def doc_embed(self, doc): ) return embed - async def post_new_doc(self, doc): + async def post_new_doc(self, doc: Doc): embed = self.doc_embed(doc) - guild = self.bot.get_guild(PRIMARY_GUILD) - channel = guild.get_channel(EXECUTIVE_DOCS_CHANNEL) + + guild = self.bot.get_guild(config.primary_guild) + if guild is None: + await self.cog_unload() + return + + channel = guild.get_channel(config.executive_docs_channel) if channel is None: - self.cog_unload() + await self.cog_unload() return + await channel.send(embed=embed) await self.posted_documents.insert_one(doc) - async def fetch_docs_json(self): + async def fetch_docs_json(self) -> list[Doc]: async with httpx.AsyncClient() as client: resp = await client.get(EXECUTIVE_DOCS_JSON_URI) return resp.json() @@ -68,14 +76,11 @@ async def post_new_docs(self): await self.post_new_doc(doc) await asyncio.sleep(5.0) - def __init__(self, manifest, bot: commands.Bot): - super().__init__(manifest, bot) - async def cog_load(self): - self.posted_documents = self.bot.database.automata.executivedocs_posted_documents + self.posted_documents = mongo.automata.executivedocs_posted_documents self.check_for_new_docs.start() - def cog_unload(self): + async def cog_unload(self): self.check_for_new_docs.cancel() @tasks.loop(seconds=60.0 * 10.0) diff --git a/automata/plugins/faq.py b/automata/plugins/faq.py new file mode 100644 index 0000000..52c7e9e --- /dev/null +++ b/automata/plugins/faq.py @@ -0,0 +1,141 @@ +import discord +from discord.ext import commands + +from automata.utils import CommandContext, Plugin + +# Colors +COLOR_SCIENCE = 0x98FB98 +COLOR_ARTS = 0x9898FB +COLOR_ADMIN = 0xFFFF00 + +# Degree Types +SCIENCE = ("BSC", "B.SC", "SCIENCES", "SCIENCE") +ARTS = ("BA", "B.A", "ART", "ARTS") + + +class FAQ(Plugin): + """Commands to answer some Frequently Asked Questions""" + + async def create_embed_science(self) -> discord.Embed: + """Returns the embed.""" + embed_science = discord.Embed( + title="B.Sc Sample First Year", + url="https://www.mun.ca/undergrad/first-year-information/sample-first-year---st-johns-campus/science/computer-science/", + description="Students pursuing a bachelor of science with a major in computer science will normally take the following courses in their first year:", + color=COLOR_SCIENCE, + ) + embed_science.add_field( + name="**Fall Semester**", + value="> Mathematics 1090 or 1000\n> Computer Science 1001\n> elective\n> English 1090\n> elective\n", + inline=False, + ) + embed_science.add_field( + name="**Winter Semester**", + value="> Mathematics 1000 or 1001\n> Computer Science 1002\n> Computer Science 1003 or elective\n> CRW Course\n> elective\n", + inline=False, + ) + embed_science.add_field( + name="What is an elective?", + value="_Electives can be in any subject, including science courses._", + inline=False, + ) + embed_science.add_field( + name="What is a CRW course?", + value="_CRW courses are Critical Reading and Writing courses_", + inline=False, + ) + embed_science.set_footer( + text="Students who have not completed Computer Science 1003 in their first year will not be able to register for Computer Science 2001/2/3 in the fall of their second year." + ) + + return embed_science + + async def create_embed_arts(self) -> discord.Embed: + """Returns the embed.""" + embed_arts = discord.Embed( + title="B.A Sample First Year", + url="https://www.mun.ca/undergrad/first-year-information/sample-first-year---st-johns-campus/science/computer-science/", + description="Students pursuing a bachelor of arts with a major in computer science will normally take the following courses in their first year::", + color=COLOR_ARTS, + ) + embed_arts.add_field( + name="**Fall Semester**", + value="> English 1090\n> Mathematics 1090 or 1000\n> Computer Science 1001\n> Language Study (LS) course\n> elective (Breathe of Knowledge encouraged) or [minor program](https://www.mun.ca/regoff/calendar/sectionNo=ARTS-0109#ARTS-4701) course\n", + inline=False, + ) + embed_arts.add_field( + name="**Winter Semester**", + value="> CRW course\n> Mathematics 1000 or 1001\n> Computer Science 1002\n> Computer Science 1003 or elective\n> LS course\n", + inline=False, + ) + embed_arts.add_field( + name="What is a Breadth of Knowledge and Language Study elective?", + value='_"More information about [Breadth of Knowledge](https://www.mun.ca/regoff/calendar/sectionNo=ARTS-0109#ARTS-8192) and [Language Study](https://www.mun.ca/regoff/calendar/sectionNo=ARTS-0109#ARTS-8196)_', + inline=False, + ) + embed_arts.set_footer( + text="Students who have not completed Computer Science 1003 in their first year will not be able to register for Computer Science 2001/2/3 in the fall of their second year." + ) + + return embed_arts + + @commands.command() + async def sample(self, ctx: CommandContext, degree: str = "blank"): + """Replies with a sample of the courses you need for your first year. + + Takes type of degree as argument. + """ + + if degree.upper() in SCIENCE: + embed = await self.create_embed_science() + await ctx.send(embed=embed) + + elif degree.upper() in ARTS: + embed = await self.create_embed_arts() + await ctx.send(embed=embed) + + else: + # No type of degree was specified + valid_args = SCIENCE + ARTS + args_str = ", ".join(valid_args) + await ctx.send( + "Specify the type of degree (Valid arguments: " + args_str + ")" + ) + + @commands.command() + async def admission(self, ctx: CommandContext): + """Replies with some FAQ about studying CS at MUN.""" + embed_admission = discord.Embed( + title="Frequently Asked Questions", + url="https://www.mun.ca/computerscience/ugrad/FAQ.php", + description="Some FAQ about studying Computer Science at MUN:", + color=COLOR_ADMIN, + ) + embed_admission.add_field( + name="**How do I get to study CS at MUN?**", + value="After admission to the university you need to complete the required courses\n> 1. COMP 1001 and COMP 1002\n> 2. 6 credit hours in a [CRW](https://www.mun.ca/regoff/calendar/sectionNo=ARTS-0109#ARTS-8194) course, including 3 credit hours in English\n> 3. MATH 1000 and 1001 (or 1090 and 1000)\n> 3. 6 credit hours in other courses\nYou must also have a mean grade of at least 65% in Computer Science 1001 and 1002.", + inline=False, + ) + embed_admission.add_field( + name="**How do I apply to the major?**", + value="Students who wish to major in computer science must submit a completed [online application form](https://www.mun.ca/computerscience/undergraduates/programs/applying-for-admission/) on the [Department of Computer Science](https://www.mun.ca/computerscience/) website. The application form is available from Feb. 1 to June 1 for students applying for fall admission, and from Aug. 1 to Oct. 1 for students applying for winter admission", + inline=False, + ) + embed_admission.add_field( + name="**What is the minimum required average for acceptance?**", + value="Students who fulfill the eligibility requirements compete for a limited number of available spaces. Selection is based on academic performance, normally cumulative average and performance in recent courses. For 2022 applications, students must also have a mean grade of at least 65% in Computer Science 1001 and 1002. Starting Fall 2021 students need an average of 65% in COMP 1001 and COMP 1002 to get in the major", + inline=False, + ) + + await ctx.send(embed=embed_admission) + + @commands.command() + async def DID(self, ctx: CommandContext): + """Replies with informative information on DID.""" + await ctx.send( + """DID and OSDD are mental health issues on the DSM5 list +Pluralkit is an accessibility bot so you know which alter you are talking to +https://did-research.org/comorbid/dd/osdd_udd/did_osdd +http://traumadissociation.com/osdd +Please read these articles before assuming someone is just roleplaying in a server""" + ) diff --git a/plugins/FortuneCookie/__init__.py b/automata/plugins/fortune_cookie.py similarity index 79% rename from plugins/FortuneCookie/__init__.py rename to automata/plugins/fortune_cookie.py index 6df11ad..191b2a1 100644 --- a/plugins/FortuneCookie/__init__.py +++ b/automata/plugins/fortune_cookie.py @@ -1,21 +1,22 @@ -from discord.ext import commands import discord import httpx -from Plugin import AutomataPlugin +from discord.ext import commands + +from automata.utils import CommandContext, Plugin API_BASE = "http://yerkee.com/api" -class FortuneCookie(AutomataPlugin): +class FortuneCookie(Plugin): """A fortune cookie""" @staticmethod - async def _fetch(api_path): + async def _fetch(api_path: str) -> httpx.Response: async with httpx.AsyncClient() as client: return await client.get(f"{API_BASE}{api_path}") @commands.command() - async def fortune(self, ctx: commands.Context): + async def fortune(self, ctx: CommandContext): """Replies with a fortune cookie message""" response = await self._fetch("/fortune/computers") diff --git a/plugins/InstantAnswer/__init__.py b/automata/plugins/instant_answer.py similarity index 88% rename from plugins/InstantAnswer/__init__.py rename to automata/plugins/instant_answer.py index 91d5f90..bf890ee 100644 --- a/plugins/InstantAnswer/__init__.py +++ b/automata/plugins/instant_answer.py @@ -1,15 +1,16 @@ import re + import httpx from discord.ext import commands -from Plugin import AutomataPlugin +from automata.utils import CommandContext, Plugin -class InstantAnswer(AutomataPlugin): +class InstantAnswer(Plugin): """Wrapper for Instant Answer API from DuckDuckGo""" @commands.command() - async def ia(self, ctx, *, argument): + async def ia(self, ctx: CommandContext, *, argument: str): """Replies with Instant Answer from DuckDuckGo""" output_template = ( diff --git a/plugins/LMGTFY/__init__.py b/automata/plugins/lmgtfy.py similarity index 67% rename from plugins/LMGTFY/__init__.py rename to automata/plugins/lmgtfy.py index d105c74..24cee6c 100644 --- a/plugins/LMGTFY/__init__.py +++ b/automata/plugins/lmgtfy.py @@ -1,15 +1,15 @@ -import urllib -import discord +import urllib.parse + from discord.ext import commands -from Plugin import AutomataPlugin +from automata.utils import CommandContext, Plugin -class LMGTFY(AutomataPlugin): +class LMGTFY(Plugin): """Create a LMGTFY link, for people who should have google'd first""" @commands.command() - async def lmgtfy(self, ctx: commands.Context, *, search_terms: str): + async def lmgtfy(self, ctx: CommandContext, *, search_terms: str): """Creates a LMGTFY link with the given search terms""" search_terms = urllib.parse.quote(search_terms) diff --git a/plugins/Man/__init__.py b/automata/plugins/man.py similarity index 88% rename from plugins/Man/__init__.py rename to automata/plugins/man.py index c6da759..6e02845 100644 --- a/plugins/Man/__init__.py +++ b/automata/plugins/man.py @@ -1,24 +1,22 @@ from time import sleep -import requests -from bs4 import BeautifulSoup +import httpx +from bs4 import BeautifulSoup from discord.ext import commands -from Plugin import AutomataPlugin +from automata.utils import CommandContext, Plugin -class Man(AutomataPlugin): +class Man(Plugin): """Linux man command via man7.org""" cached = {} - def __init__(self, manifest, bot: commands.Bot): - super().__init__(manifest, bot) + def __init__(self, bot: commands.Bot): + super().__init__(bot) pages = [] for i in range(9): - r = requests.get( - url=f"https://man7.org/linux/man-pages/dir_section_{i}.html" - ) + r = httpx.get(url=f"https://man7.org/linux/man-pages/dir_section_{i}.html") sleep(0.1) pages.append(BeautifulSoup(r.text, "html.parser")) for i, x in enumerate(pages): @@ -38,6 +36,7 @@ def __init__(self, manifest, bot: commands.Bot): else: self.cached[y.string.split("(")[0]] = [y["href"][5:-5]] + @staticmethod def urlfy(s: str = ""): if s[0] in "012345678": return "https://man7.org/linux/man-pages/man" + s + ".html" @@ -45,7 +44,7 @@ def urlfy(s: str = ""): return s @commands.command() - async def man(self, ctx: commands.Context, search: str = ""): + async def man(self, ctx: CommandContext, search: str = ""): """Searches man7.org for the requested man page""" try: if search == "": @@ -59,7 +58,7 @@ async def man(self, ctx: commands.Context, search: str = ""): else: res = [] for i in range(1, 9): - r = requests.get( + r = httpx.get( url=f"https://man7.org/linux/man-pages/man{i}/{search}.{i}.html" ) sleep(0.1) @@ -69,7 +68,6 @@ async def man(self, ctx: commands.Context, search: str = ""): self.cached[search] = [f"No manual entry for {search}"] await ctx.send(f"No manual entry for {search}") elif len(res) == 1: - self.cached[search] = [f"{res[0]}/{search}.{res[0]}"] await ctx.send( diff --git a/plugins/MUN Identity/__init__.py b/automata/plugins/mun_identity.py similarity index 72% rename from plugins/MUN Identity/__init__.py rename to automata/plugins/mun_identity.py index c472799..daf7cb1 100644 --- a/plugins/MUN Identity/__init__.py +++ b/automata/plugins/mun_identity.py @@ -1,21 +1,20 @@ import asyncio from typing import Dict, List, Optional, Union -import httpx import discord -from Globals import DISCORD_AUTH_URI, PRIMARY_GUILD, VERIFIED_ROLE +import httpx from discord.ext import commands -from Plugin import AutomataPlugin + +from automata.config import config +from automata.mongo import mongo +from automata.utils import CommandContext, Plugin -class MUNIdentity(AutomataPlugin): +class MUNIdentity(Plugin): """Provides identity validation and management services.""" - def __init__(self, manifest, bot: commands.Bot): - super().__init__(manifest, bot) - async def cog_load(self): - self.identities = self.bot.database.automata.munidentity_identities + self.identities = mongo.automata.munidentity_identities async def get_identity( self, *, member: Union[discord.User, int] = None, mun_username: str = None @@ -41,15 +40,8 @@ async def get_identity( identity = await self.identities.find_one(query) return identity - @commands.Cog.listener() - async def on_member_join(self, member: discord.Member): - # await member.send( - # f"Welcome to the MUN Computer Science Society Discord server, {member.mention}.\nIf you have a MUN account, please visit https://discord.muncompsci.ca/auth to verify yourself.\nOtherwise, contact an executive to gain further access." - # ) - pass - @commands.group() - async def identity(self, ctx: commands.Context): + async def identity(self, ctx: CommandContext): """Manage identity validation.""" if not ctx.invoked_subcommand: identity = await self.get_identity(member=ctx.author) @@ -64,20 +56,26 @@ async def identity(self, ctx: commands.Context): ) @identity.command(name="verify") - async def identity_verify(self, ctx: commands.Context, code: str): + async def identity_verify(self, ctx: CommandContext, code: str): """Verify your identity.""" current_identity = await self.get_identity(member=ctx.author) if current_identity is not None: - await self.bot.get_guild(PRIMARY_GUILD).get_member(ctx.author.id).add_roles( - self.bot.get_guild(PRIMARY_GUILD).get_role(VERIFIED_ROLE), - reason=f"Identity verified. MUN username: {current_identity['mun_username']}", + await ( + self.bot.get_guild(config.primary_guild) + .get_member(ctx.author.id) + .add_roles( + self.bot.get_guild(config.primary_guild).get_role( + config.verified_role + ), + reason=f"Identity verified. MUN username: {current_identity['mun_username']}", + ) ) await ctx.send( "Your identity is already verified. If for some reason you need to change your verified username, contact an executive." ) return async with httpx.AsyncClient() as client: - resp = await client.get(f"{DISCORD_AUTH_URI}/identity/{code}") + resp = await client.get(f"{config.discord_auth_uri}/identity/{code}") if resp.status_code == httpx.codes.OK: username = resp.text current_identity = await self.get_identity(mun_username=username) @@ -89,9 +87,15 @@ async def identity_verify(self, ctx: commands.Context, code: str): await self.identities.insert_one( {"discord_id": ctx.author.id, "mun_username": username} ) - await self.bot.get_guild(PRIMARY_GUILD).get_member(ctx.author.id).add_roles( - self.bot.get_guild(PRIMARY_GUILD).get_role(VERIFIED_ROLE), - reason=f"Identity verified. MUN username: {username}", + await ( + self.bot.get_guild(config.primary_guild) + .get_member(ctx.author.id) + .add_roles( + self.bot.get_guild(config.primary_guild).get_role( + config.verified_role + ), + reason=f"Identity verified. MUN username: {username}", + ) ) is_verified_message = await ctx.send("Identity verified!") if type(is_verified_message.channel) is discord.DMChannel: @@ -105,7 +109,7 @@ async def identity_verify(self, ctx: commands.Context, code: str): @identity.command(name="check") @commands.has_permissions(view_audit_log=True) - async def identity_check(self, ctx: commands.Context, user: discord.Member): + async def identity_check(self, ctx: CommandContext, user: discord.Member): """Check the identity verification status of a user.""" identity = await self.identities.find_one({"discord_id": user.id}) if identity is not None: @@ -121,14 +125,20 @@ async def identity_check(self, ctx: commands.Context, user: discord.Member): @identity.command(name="remove") @commands.has_permissions(manage_messages=True) - async def identity_remove(self, ctx: commands.Context, user: discord.Member): + async def identity_remove(self, ctx: CommandContext, user: discord.Member): """Remove the identity from a user.""" identity = await self.get_identity(member=user) if identity is not None: await self.identities.delete_one({"discord_id": user.id}) - await self.bot.get_guild(PRIMARY_GUILD).get_member(user.id).remove_roles( - self.bot.get_guild(PRIMARY_GUILD).get_role(VERIFIED_ROLE), - reason=f"Identity manually removed by {ctx.author.name}#{ctx.author.discriminator}. MUN username: {identity['mun_username']}", + await ( + self.bot.get_guild(config.primary_guild) + .get_member(user.id) + .remove_roles( + self.bot.get_guild(config.primary_guild).get_role( + config.verified_role + ), + reason=f"Identity manually removed by {ctx.author.name}#{ctx.author.discriminator}. MUN username: {identity['mun_username']}", + ) ) embed = discord.Embed() embed.colour = discord.Colour.green() @@ -148,7 +158,7 @@ async def identity_remove(self, ctx: commands.Context, user: discord.Member): @identity.command(name="associate") @commands.has_permissions(manage_messages=True) async def identity_associate( - self, ctx: commands.Context, user: discord.Member, mun_username: str + self, ctx: CommandContext, user: discord.Member, mun_username: str ): """Manually associate a Discord account to a MUN username.""" identity = await self.get_identity(member=user) @@ -168,14 +178,18 @@ async def identity_associate( await self.identities.insert_one( {"discord_id": user.id, "mun_username": mun_username} ) - await self.bot.get_guild(PRIMARY_GUILD).get_member(user.id).add_roles( - self.bot.get_guild(PRIMARY_GUILD).get_role(VERIFIED_ROLE), - reason=f"Identity manually associated by {ctx.author.name}#{ctx.author.discriminator}. MUN username: {mun_username}", + await ( + self.bot.get_guild(config.primary_guild) + .get_member(user.id) + .add_roles( + self.bot.get_guild(config.primary_guild).get_role(config.verified_role), + reason=f"Identity manually associated by {ctx.author.name}#{ctx.author.discriminator}. MUN username: {mun_username}", + ) ) await ctx.send("Identity associated.") @identity.command(name="disassociate") - async def identity_disassociate(self, ctx: commands.Context): + async def identity_disassociate(self, ctx: CommandContext): """Disassociate your own identity from your discord account.""" current_identity = await self.get_identity(member=ctx.author) if current_identity is None: @@ -185,7 +199,7 @@ async def identity_disassociate(self, ctx: commands.Context): embed.colour = discord.Colour.red() embed.add_field( name="Identity Disassociation", - value=f"Are you sure you want to disassociate your identity from your account?", + value="Are you sure you want to disassociate your identity from your account?", ) message = await ctx.reply(embed=embed) confirmed = await self.get_confirmation(ctx, message) @@ -193,20 +207,24 @@ async def identity_disassociate(self, ctx: commands.Context): await ctx.reply("Disassociation cancelled.") return await self.identities.delete_one({"discord_id": ctx.author.id}) - await self.bot.get_guild(PRIMARY_GUILD).get_member(ctx.author.id).remove_roles( - self.bot.get_guild(PRIMARY_GUILD).get_role(VERIFIED_ROLE), - reason=f"Identity manually disassociated by {ctx.author.name}#{ctx.author.discriminator}.", + await ( + self.bot.get_guild(config.primary_guild) + .get_member(ctx.author.id) + .remove_roles( + self.bot.get_guild(config.primary_guild).get_role(config.verified_role), + reason=f"Identity manually disassociated by {ctx.author.name}#{ctx.author.discriminator}.", + ) ) embed = discord.Embed() embed.colour = discord.Colour.green() embed.add_field( name="Disassociation", - value=f"Your identity has been disassociated from your account.", + value="Your identity has been disassociated from your account.", ) await ctx.reply(embed=embed) @staticmethod - async def get_confirmation(ctx: commands.Context, message: discord.Message) -> bool: + async def get_confirmation(ctx: CommandContext, message: discord.Message) -> bool: """Get confirmation from user executing the command by reactions.""" await message.add_reaction("✅") await message.add_reaction("❌") @@ -228,25 +246,27 @@ def check(reaction, user): @identity.command(name="restore_roles") @commands.has_permissions(view_audit_log=True) - async def identity_restore_roles(self, ctx: commands.Context): + async def identity_restore_roles(self, ctx: CommandContext): """Restores VERIFIED_ROLE to users with a registered identity who were not granted it.""" members_restored: List[discord.Member] = [] identities = self.identities.find({}) async with ctx.typing(): while await identities.fetch_next: identity = identities.next_object() - member: discord.Member = self.bot.get_guild(PRIMARY_GUILD).get_member( - identity["discord_id"] - ) + member: discord.Member = self.bot.get_guild( + config.primary_guild + ).get_member(identity["discord_id"]) if member is None: await asyncio.sleep(1) continue - has_role = any(role.id == VERIFIED_ROLE for role in member.roles) + has_role = any(role.id == config.verified_role for role in member.roles) if has_role: await asyncio.sleep(1) continue await member.add_roles( - self.bot.get_guild(PRIMARY_GUILD).get_role(VERIFIED_ROLE), + self.bot.get_guild(config.primary_guild).get_role( + config.verified_role + ), reason=f"Identity restored by {ctx.author.name}#{ctx.author.discriminator}.", ) members_restored.append(member) diff --git a/plugins/NFacts/__init__.py b/automata/plugins/number_facts.py similarity index 79% rename from plugins/NFacts/__init__.py rename to automata/plugins/number_facts.py index 6d509e9..b22cf87 100644 --- a/plugins/NFacts/__init__.py +++ b/automata/plugins/number_facts.py @@ -1,23 +1,21 @@ -from os import name +import discord +import httpx from discord.ext import commands -import httpx -import discord -from Plugin import AutomataPlugin +from automata.utils import CommandContext, Plugin API_BASE = "http://numbersapi.com/" -class NumberFacts(AutomataPlugin): +class NumberFacts(Plugin): """Numbers have a secret facts, check them out!""" @staticmethod - async def fetch(api_path): + async def fetch(api_path: str) -> httpx.Response: async with httpx.AsyncClient() as client: return await client.get(f"{API_BASE}{api_path}") - async def message_embed(self, fact, number): - + async def message_embed(self, fact: str, number: str) -> discord.Embed: embed = discord.Embed(colour=discord.Colour.random()) embed.add_field(name=f"A fact about number {number}", value=fact) embed.set_author(name="NFact") @@ -25,7 +23,7 @@ async def message_embed(self, fact, number): return embed @commands.command(aliases=["nfact"]) - async def numberfact(self, ctx: commands.Context, number: str = "random"): + async def numberfact(self, ctx: CommandContext, number: str = "random"): """Replies with a random fact of random or specified number EX. !nfact | a random number @@ -42,7 +40,7 @@ async def numberfact(self, ctx: commands.Context, number: str = "random"): await ctx.send(embed=embed) @commands.command() - async def yearfact(self, ctx: commands.Context, year: str = "random"): + async def yearfact(self, ctx: CommandContext, year: str = "random"): """ Replies with a fact about a random or specific year """ @@ -57,7 +55,7 @@ async def yearfact(self, ctx: commands.Context, year: str = "random"): await ctx.send(embed=embed) @commands.command() - async def datefact(self, ctx: commands.Context, date: str = "", month: str = ""): + async def datefact(self, ctx: CommandContext, date: str = "", month: str = ""): """ Replies with a fact about a random or specific date !datefact DD MM @@ -76,7 +74,7 @@ async def datefact(self, ctx: commands.Context, date: str = "", month: str = "") await ctx.send(embed=embed) @commands.command() - async def mathfact(self, ctx: commands.Context, number: str = "random"): + async def mathfact(self, ctx: CommandContext, number: str = "random"): """ Numbers Trivia """ diff --git a/plugins/Starboard/__init__.py b/automata/plugins/starboard.py similarity index 88% rename from plugins/Starboard/__init__.py rename to automata/plugins/starboard.py index d8ed6d4..ab8ebc1 100644 --- a/plugins/Starboard/__init__.py +++ b/automata/plugins/starboard.py @@ -1,22 +1,19 @@ from datetime import datetime -from logging import StreamHandler -from typing import Dict, Optional, Union +from typing import Any, Dict, Optional, Union import discord from discord.ext import commands -from Plugin import AutomataPlugin -from Globals import STARBOARD_CHANNEL_ID, STARBOARD_THRESHOLD +from automata.config import config +from automata.mongo import mongo +from automata.utils import Plugin -class Starboard(AutomataPlugin): +class Starboard(Plugin): """React with ⭐'s on a message to add a message to the starboard.""" - def __init__(self, manifest, bot: commands.Bot): - super().__init__(manifest, bot) - async def cog_load(self): - self.starboard = self.bot.database.automata.starboard_starboard + self.starboard = mongo.automata.starboard_starboard async def get_entry( self, @@ -36,7 +33,7 @@ async def get_entry( message_id = self._get_id(message) channel_id = self._get_id(channel) - query = {} + query: Any = {} query["message_id"] = message_id query["channel_id"] = channel_id entry = await self.starboard.find_one(query) @@ -118,8 +115,8 @@ async def on_reaction_add(self, reaction: discord.Reaction, user: discord.Member return if ( - reaction.count != STARBOARD_THRESHOLD - or reaction.message.channel.id == STARBOARD_CHANNEL_ID + reaction.count != config.starboard_threshold + or reaction.message.channel.id == config.starboard_channel_id ): return @@ -130,7 +127,7 @@ async def on_reaction_add(self, reaction: discord.Reaction, user: discord.Member if (await self.get_entry(message=message, channel=channel)) is not None: return - starboard_channel = message.guild.get_channel(STARBOARD_CHANNEL_ID) + starboard_channel = message.guild.get_channel(config.starboard_channel_id) embed = self._format_starboard_embed(message=message) await starboard_channel.send(embed=embed) await self.add_entry(message=message, channel=channel, user=user) diff --git a/Utils.py b/automata/utils.py similarity index 75% rename from Utils.py rename to automata/utils.py index 7389ac1..6529c82 100644 --- a/Utils.py +++ b/automata/utils.py @@ -1,10 +1,20 @@ import io +from typing import Any, Mapping, Optional import discord from discord.ext import commands +type CommandContext = commands.Context[commands.Bot] -async def send_code_block_maybe_as_file(ctx, text): + +class Plugin(commands.Cog): + bot: commands.Bot + + def __init__(self, bot: commands.Bot): + self.bot = bot + + +async def send_code_block_maybe_as_file(ctx: CommandContext, text: str): """ Sends a code block to the current context. @@ -21,10 +31,13 @@ async def send_code_block_maybe_as_file(ctx, text): await ctx.send(f"```{text}```") -class CustomHelp(commands.DefaultHelpCommand): # ( ͡° ͜ʖ ͡°) +class CustomHelp(commands.DefaultHelpCommand): """Custom help command""" - async def send_bot_help(self, mapping): + async def send_bot_help( + self, + mapping: Mapping[Optional[commands.Cog], list[commands.Command[Any, Any, Any]]], + ): """Shows a list of commands""" embed = discord.Embed(title="Commands Help") embed.colour = discord.Colour.blurple() @@ -38,7 +51,7 @@ async def send_bot_help(self, mapping): channel = self.get_destination() await channel.send(embed=embed) - async def send_command_help(self, command): + async def send_command_help(self, command: commands.Command[Any, Any, Any]): """Shows how to use each command""" embed_command = discord.Embed( title=self.get_command_signature(command), description=command.help @@ -47,7 +60,7 @@ async def send_command_help(self, command): channel = self.get_destination() await channel.send(embed=embed_command) - async def send_group_help(self, group: commands.Group): + async def send_group_help(self, group: commands.Group[Any, Any, Any]): """Shows how to use each group of commands""" embed_group = discord.Embed( title=self.get_command_signature(group), description=group.short_doc @@ -58,7 +71,7 @@ async def send_group_help(self, group: commands.Group): channel = self.get_destination() await channel.send(embed=embed_group) - async def send_cog_help(self, cog): + async def send_cog_help(self, cog: commands.Cog): """Shows how to use each category""" embed_cog = discord.Embed(title=cog.qualified_name, description=cog.description) comms = cog.get_commands() @@ -68,8 +81,8 @@ async def send_cog_help(self, cog): channel = self.get_destination() await channel.send(embed=embed_cog) - async def send_error_message(self, error): - "shows if command does not exist" + async def send_error_message(self, error: str): + """Shows if command does not exist""" embed_error = discord.Embed(title="Error", description=error) embed_error.colour = discord.Colour.red() channel = self.get_destination() diff --git a/docker-compose.yml b/docker-compose.yml index 5047e16..8c4e188 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,15 +1,16 @@ -version: '3' +version: "3" services: automata: image: ghcr.io/muncomputersciencesociety/automata + # build: . restart: always - volumes: - - ./mounted_plugins:/app/mounted_plugins depends_on: - mongo env_file: - .env + environment: + - AUTOMATA_MONGO_HOST=mongo mongo: image: mvertes/alpine-mongo diff --git a/plugins/3x3 Meme Generator/__init__.py b/plugins/3x3 Meme Generator/__init__.py deleted file mode 100644 index 250ca70..0000000 --- a/plugins/3x3 Meme Generator/__init__.py +++ /dev/null @@ -1,211 +0,0 @@ -import urllib.parse -import asyncio -from discord import http -from discord.ext import commands -import discord -import httpx -from discord.threads import Thread -from Plugin import AutomataPlugin -from pymongo import collection - - -API = "https://awful-3x3-meme-generator.herokuapp.com" - - -class Generator(AutomataPlugin): - """ - Generates a meme that's probably already dead - """ - - def __init__(self, manifest, bot): - super().__init__(manifest, bot) - - async def cog_load(self): - self.gen3x3_sessions: collection.Collection = ( - self.bot.database.automata.gen3x3_sessions - ) - self.gen3x3_session_counter: collection.Collection = ( - self.bot.database.automata.gen3x3_session_counter - ) - document_count = await self.gen3x3_session_counter.count_documents({}) - if document_count == 0: - self.gen3x3_session_counter.insert_one({"counter": 0}) - - @commands.command() - async def gen3x3( - self, - ctx: commands.Context, - lawful_good, - neutral_good, - chaotic_good, - lawful_neutral, - true_neutral, - chaotic_neutral, - lawful_evil, - neutral_evil, - chaotic_evil, - ): - """Responds with a meme that's probably already dead""" - - params = { - "lg": lawful_good, - "ng": neutral_good, - "cg": chaotic_good, - "ln": lawful_neutral, - "tn": true_neutral, - "cn": chaotic_neutral, - "le": lawful_evil, - "ne": neutral_evil, - "ce": chaotic_evil, - } - - error_check = httpx.get(f"{API}/api", params=params).text - - await ctx.send( - error_check - if error_check[1:16] == "Malformed input" - else f"{API}/api?{urllib.parse.urlencode(params)}" - ) - - @commands.command() - async def gen3x3start(self, ctx: commands.Context): - message = await ctx.send("Creating 3x3 Generation Session") - await self.gen3x3_session_counter.update_one({}, {"$inc": {"counter": 1}}) - counter_obj = await self.gen3x3_session_counter.find_one() - id = counter_obj["counter"] - thread = await message.create_thread(name=f"Gen 3x3 Session {id}") - alignments = { - "lg": None, - "ng": None, - "cg": None, - "ln": None, - "tn": None, - "cn": None, - "le": None, - "ne": None, - "ce": None, - } - await self.gen3x3_sessions.insert_one( - {"id": id, "thread": thread.id, "alignments": alignments} - ) - await thread.typing() - await asyncio.sleep(5) - await thread.send( - "Please use the gen3x3set command followed by one of the following and a link to an image to set the alignments: \nlg, ng, cg, ln, tn, cn, le, ne, ce" - ) - await thread.typing() - await asyncio.sleep(1) - await thread.send('For example "!gen3x3set lg https://i.imgur.com/V73crmb.jpg"') - await thread.typing() - await asyncio.sleep(3) - await thread.send( - "Once all alignments have been set use the gen3x3publish command to publish the image beck to the main channel." - ) - await thread.typing() - await asyncio.sleep(1) - await thread.send( - "You can overwrite any of the alignments at any point before it's published" - ) - - async def set_check(self, content: str): - res = httpx.get(f"{API}/test", params={"image_url": content}).text - return res == "Valid" - - @commands.command() - async def gen3x3set(self, ctx: commands.Context, alignment: str, *, content: str): - res = await self.gen3x3_sessions.find_one({"thread": ctx.message.channel.id}) - if not res: - await ctx.send( - "This command can only be used in a thread created with the gen3x3start command (that is not yet archived)" - ) - return - thread = ctx.message.channel - check = await self.set_check(content) - if not check: - await thread.send("That doesn't seem to be a valid image url") - return - res["alignments"][alignment] = content - await self.gen3x3_sessions.update_one( - {"id": res["id"]}, {"$set": {"alignments": res["alignments"]}} - ) - await thread.send(f"Set {alignment} to: {content}") - - @commands.command() - async def gen3x3get(self, ctx: commands.Context, alignment: str): - res = await self.gen3x3_sessions.find_one({"thread": ctx.message.channel.id}) - if not res: - await ctx.send( - "This command can only be used in a thread created with the gen3x3start command (that is not yet archived)" - ) - return - thread = ctx.message.channel - await thread.send(f"Current {alignment} value: {res['alignments'][alignment]}") - - def publish_check(self, alignments: dict): - errors = [] - keys = alignments.keys() - for key in keys: - if alignments[key] == None: - errors.append(key) - return errors - - @commands.command() - async def gen3x3publish(self, ctx: commands.Context): - res = await self.gen3x3_sessions.find_one({"thread": ctx.message.channel.id}) - if not res: - await ctx.send( - "This command can only be used in a thread created with the gen3x3start command (that is not yet archived)" - ) - return - thread: Thread = ctx.message.channel - empty_alignments = self.publish_check(res["alignments"]) - if len(empty_alignments) != 0: - await thread.send( - f"The following alignments have not been assigned yet: \n{', '.join(empty_alignments)}" - ) - return - await self.gen3x3_sessions.delete_one({"thread": thread.id}) - await thread.edit(archived=True) - channel = thread.parent - image_url = f"{API}/api?{urllib.parse.urlencode(res['alignments'])}" - await channel.send(f"Published image from Session {res['id']}:") - await channel.send(image_url) - - @commands.command() - async def gen3x3preview(self, ctx: commands.Context): - res = await self.gen3x3_sessions.find_one({"thread": ctx.message.channel.id}) - if not res: - await ctx.send( - "This command can only be used in a thread created with the gen3x3start command (that is not yet archived)" - ) - return - temp_grid = {} - keys = res["alignments"].keys() - for key in keys: - if res["alignments"][key] == None: - temp_grid[key] = "https://via.placeholder.com/200" - else: - temp_grid[key] = res["alignments"][key] - image_url = f"{API}/api?{urllib.parse.urlencode(temp_grid)}" - await ctx.send("Preview Image: ") - await ctx.send(image_url) - - # Admin only utility commands - @commands.command() - async def gen3x3count(self, ctx: commands.Context): - if ctx.message.author.guild_permissions.administrator: - active_sessions = await self.gen3x3_sessions.count_documents({}) - await ctx.send( - f"Currently there are {active_sessions} active Gen 3x3 Sessions" - ) - - @commands.command() - async def gen3x3clearall(self, ctx: commands.Context): - if ctx.message.author.guild_permissions.administrator: - count = 0 - async for session in self.gen3x3_sessions.find(): - thread = ctx.channel.get_thread(session["thread"]) - await thread.edit(archived=True) - count += 1 - await self.gen3x3_sessions.delete_many({}) - await ctx.send(f"{count} Sessions archived and deleted") diff --git a/plugins/3x3 Meme Generator/plugin.json b/plugins/3x3 Meme Generator/plugin.json deleted file mode 100644 index ca50a4c..0000000 --- a/plugins/3x3 Meme Generator/plugin.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "3x3 Meme Generator", - "version": "2.0.0", - "author": "Cristopher Yates", - "main_class": "Generator" -} diff --git a/plugins/AOC2021/__init__.py b/plugins/AOC2021/__init__.py deleted file mode 100644 index 61d0162..0000000 --- a/plugins/AOC2021/__init__.py +++ /dev/null @@ -1,27 +0,0 @@ -from discord.ext import commands -from discord import Embed, Colour - -from Plugin import AutomataPlugin -from Globals import AOC_LEADERBOARD_CHANNEL, PRIMARY_GUILD - -AOC_COLOUR = 0x01B204 - - -class AOC(AutomataPlugin): - """AOC 2021 Countdown""" - - @staticmethod - def aoc_embed(): - aoc_embed = Embed() - aoc_embed.colour = Colour(0x01B204) - - return aoc_embed - - @commands.command() - @commands.has_role("Technology Officer") - async def aoccountdown(self, ctx: commands.Context): - countdown_embed = self.aoc_embed() - countdown_embed.title = "Advent Of Count 2021 starts in " - await self.bot.get_guild(PRIMARY_GUILD).get_channel( - AOC_LEADERBOARD_CHANNEL - ).send(embed=countdown_embed) diff --git a/plugins/AOC2021/plugin.json b/plugins/AOC2021/plugin.json deleted file mode 100644 index bb5192e..0000000 --- a/plugins/AOC2021/plugin.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "AOC2021", - "version": "1.0.0", - "author": "Zach Vaters", - "main_class": "AOC" - } \ No newline at end of file diff --git a/plugins/Agenda/plugin.json b/plugins/Agenda/plugin.json deleted file mode 100644 index 5b4e219..0000000 --- a/plugins/Agenda/plugin.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "Agenda", - "version": "0.1.0", - "author": "Jack Arthur Harrhy", - "main_class": "Agenda" -} diff --git a/plugins/Announce/__init__.py b/plugins/Announce/__init__.py deleted file mode 100644 index 30ea313..0000000 --- a/plugins/Announce/__init__.py +++ /dev/null @@ -1,67 +0,0 @@ -from os import name -from discord.ext import commands -from discord.ext.commands import context -from Globals import ANNOUNCEMENT_CHANNEL - -from Plugin import AutomataPlugin -import discord - -PING_EMOTE = "<:ping:842732198444269568>" -NOPING_EMOTE = "<:noping:842732251132854322>" - - -class Announce(AutomataPlugin): - """Announcement feature for a better announcements formatting""" - - @commands.command() - @commands.has_permissions(view_audit_log=True) - async def announce(self, ctx: commands.Context): - """ - Send an embed in #announcements with an @-everyone ping with the current message content. - Before the message is posted it is previewed in the current channel, with a reaction the author can invoke for it to be posted properly. - You can add up to one image by uploading an image with the message. - """ - - message = ctx.message - announcement_channel = await ctx.bot.fetch_channel(ANNOUNCEMENT_CHANNEL) - announcement_message = " ".join( - message.content.split(" ")[1 : len(ctx.message.content)] - ) - - embed = discord.Embed(colour=discord.Colour.blue()) - - embed.set_author(name=message.author.name, icon_url=message.author.avatar.url) - embed.set_footer( - text="MUN Computer Science Society", icon_url=message.guild.icon.url - ) - - if len(announcement_message) > 1: - embed.add_field( - name="Announcement", value=announcement_message, inline=False - ) - - if len(message.attachments) > 0: - attachment = message.attachments[0] - embed.set_image(url=attachment.url) - - announcement_message = await ctx.send(embed=embed) - - await announcement_message.add_reaction(PING_EMOTE) - await announcement_message.add_reaction(NOPING_EMOTE) - - def check(reaction, user): - return user == message.author and reaction.message == announcement_message - - try: - reaction, user = await ctx.bot.wait_for("reaction_add", check=check) - except: - await ctx.send("Discarded") - await announcement_message.delete() - else: - if str(reaction.emoji) == PING_EMOTE: - await announcement_channel.send(embed=embed, content="@everyone") - else: - await announcement_channel.send(embed=embed) - - await ctx.send("Announcement Sent ✅") - await announcement_message.delete() diff --git a/plugins/Announce/plugin.json b/plugins/Announce/plugin.json deleted file mode 100644 index eb5c44d..0000000 --- a/plugins/Announce/plugin.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "Announce", - "version": "1.0.0", - "author": "HaMoOoOd25", - "main_class": "Announce" -} diff --git a/plugins/BadFox/__init__.py b/plugins/BadFox/__init__.py deleted file mode 100644 index 7218a00..0000000 --- a/plugins/BadFox/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -from discord.ext import commands - -from Plugin import AutomataPlugin - - -class BadFox(AutomataPlugin): - """Asks if I'm really a bad fox""" - - @commands.command() - async def BadFox(self, ctx: commands.Context, number_of_times: int = 0): - """Replies with a Am_I_really_a_BadFox? :confused: ), or many!""" - - if number_of_times == 0: - await ctx.send("Am_I_really_a_BadFox? :confused:") - else: - await ctx.send(f"Am_I_really_a_BadFox? :confused: x{number_of_times}") diff --git a/plugins/BadFox/plugin.json b/plugins/BadFox/plugin.json deleted file mode 100644 index ab7ec3e..0000000 --- a/plugins/BadFox/plugin.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "BadFox", - "version": "1.0.0", - "author": "7evnZer0", - "main_class": "BadFox" -} diff --git a/plugins/Binary/plugin.json b/plugins/Binary/plugin.json deleted file mode 100644 index 01d8bc7..0000000 --- a/plugins/Binary/plugin.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "Binary", - "version": "1.0.0", - "author": "Andrew Harris", - "main_class": "Binary" -} diff --git a/plugins/Brainf/plugin.json b/plugins/Brainf/plugin.json deleted file mode 100644 index 731ec06..0000000 --- a/plugins/Brainf/plugin.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "Brainf", - "version": "1.0.0", - "author": "Elisha Hollander", - "main_class": "Brainf" -} diff --git a/plugins/Cards/__init__.py b/plugins/Cards/__init__.py deleted file mode 100644 index 2a59621..0000000 --- a/plugins/Cards/__init__.py +++ /dev/null @@ -1,27 +0,0 @@ -from discord.ext import commands - -from Plugin import AutomataPlugin -from plugins.Cards.deck import Deck - - -class Cards(AutomataPlugin): - """ - Miscellaneous playing card games / tasks - Built using https://deckofcardsapi.com/ - """ - - def __init__(self, manifest, bot: commands.Bot): - super().__init__(manifest, bot) - - @commands.group() - async def cards(self, ctx: commands.Context): - async with ctx.typing(): - if ctx.invoked_subcommand is None: - await ctx.reply(content="Invalid command, check !help cards") - - @cards.command(name="random", aliases=["r"]) - async def cards_random(self, ctx: commands.Context): - """Sends an image of a random card to the channel""" - deck = await Deck.create() - [card] = await deck.draw() - await ctx.reply(card.image) diff --git a/plugins/Cards/deck.py b/plugins/Cards/deck.py deleted file mode 100644 index 32ca4d4..0000000 --- a/plugins/Cards/deck.py +++ /dev/null @@ -1,30 +0,0 @@ -from typing import List - -from .deck_of_cards_api import DeckOfCardsApi, CardResponse - - -class Deck: - @classmethod - async def create(self, *args): - api_deck = await DeckOfCardsApi.new_deck(*args) - return Deck(api_deck) - - def __init__(self, api_deck): - self.deck_id = api_deck.deck_id - self.shuffled = api_deck.shuffled - self.remaining = api_deck.remaining - - async def draw(self, *args) -> List[CardResponse]: - api_deck_draw = await DeckOfCardsApi.deck_draw(self.deck_id, *args) - self.remaining = api_deck_draw.remaining - return api_deck_draw.cards - - -if __name__ == "__main__": - - async def main(): - deck = await Deck.create() - cards = await deck.draw() - print(cards) - - __import__("asyncio").get_event_loop().run_until_complete(main()) diff --git a/plugins/Cards/deck_of_cards_api.py b/plugins/Cards/deck_of_cards_api.py deleted file mode 100644 index 469da98..0000000 --- a/plugins/Cards/deck_of_cards_api.py +++ /dev/null @@ -1,82 +0,0 @@ -from dataclasses import dataclass -from typing import List - -import httpx -from pydantic import BaseModel - -API_BASE = "https://deckofcardsapi.com/api" - - -class NewDeckResponse(BaseModel): - success: bool - deck_id: str - remaining: int - shuffled: bool - - -class CardImagesResponse(BaseModel): - svg: str - png: str - - -class CardResponse(BaseModel): - code: str - image: str - images: CardImagesResponse - value: str - suit: str - - -class DeckDrawResponse(BaseModel): - success: bool - deck_id: str - remaining: int - cards: List[CardResponse] - - -class DeckOfCardsApi(BaseModel): - @staticmethod - async def _fetch(api_path): - async with httpx.AsyncClient() as client: - return await client.get(f"{API_BASE}{api_path}") - - @staticmethod - def _ensure_api_sucess(response_json): - if not response_json["success"]: - raise Exception("Deck of Cards API returned non-sucess") - - @staticmethod - async def new_deck(shuffle=True, count=1) -> NewDeckResponse: - api_path = "/deck/new" - - if shuffle: - api_path += "/shuffle" - - api_path += f"?deck_count={count}" - - response = await DeckOfCardsApi._fetch(api_path) - response_json = response.json() - DeckOfCardsApi._ensure_api_sucess(response_json) - - return NewDeckResponse(**response_json) - - @staticmethod - async def deck_draw(deck_id, count=1) -> DeckDrawResponse: - api_path = f"/deck/{deck_id}/draw/?count={count}" - - response = await DeckOfCardsApi._fetch(api_path) - response_json = response.json() - DeckOfCardsApi._ensure_api_sucess(response_json) - - return DeckDrawResponse(**response_json) - - -if __name__ == "__main__": - - async def main(): - deck = await DeckOfCardsApi.new_deck() - print(deck) - card = await DeckOfCardsApi.deck_draw(deck.deck_id) - print(card) - - __import__("asyncio").get_event_loop().run_until_complete(main()) diff --git a/plugins/Cards/plugin.json b/plugins/Cards/plugin.json deleted file mode 100644 index 4f632fb..0000000 --- a/plugins/Cards/plugin.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "Cards", - "version": "0.1.0", - "author": "jackharrhy", - "main_class": "Cards" -} diff --git a/plugins/Coinflip/__init__.py b/plugins/Coinflip/__init__.py deleted file mode 100644 index 13a79ad..0000000 --- a/plugins/Coinflip/__init__.py +++ /dev/null @@ -1,27 +0,0 @@ -from discord.ext import commands - -from Plugin import AutomataPlugin - -import random - -MAXIMUM_FLIPS = 5 -MINIMUM_FLIPS = 1 - - -class Coinflip(AutomataPlugin): - """Literally a coin flip!""" - - @commands.command() - async def coinflip(self, ctx: commands.Context, number_of_times: int = 1): - """Flips the coin n times!""" - if number_of_times <= MAXIMUM_FLIPS and number_of_times >= MINIMUM_FLIPS: - for i in range(number_of_times): - flip = random.randint(0, 1) - if flip == 0: - await ctx.send("Heads!") - else: - await ctx.send("Tails!") - else: - await ctx.send( - f"Too many flips to handle, try less than {MAXIMUM_FLIPS + 1} and more than {MINIMUM_FLIPS - 1}!" - ) diff --git a/plugins/Coinflip/plugin.json b/plugins/Coinflip/plugin.json deleted file mode 100644 index 6c4569c..0000000 --- a/plugins/Coinflip/plugin.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "Coinflip", - "version": "1.0.0", - "author": "Hking", - "main_class": "Coinflip" -} diff --git a/plugins/Continue/__init__.py b/plugins/Continue/__init__.py deleted file mode 100644 index aebb677..0000000 --- a/plugins/Continue/__init__.py +++ /dev/null @@ -1,33 +0,0 @@ -import httpx -from discord.ext import commands -from Plugin import AutomataPlugin -from plugins.Continue.gptj import gptj_query, gptj_query_simple - - -class Continue(AutomataPlugin): - """Continues a prompt using GPT-J""" - - @commands.command(aliases=["continue"]) - async def gpt(self, ctx: commands.Context, *, prompt: str): - """Continues your prompt with the default parameters""" - res = await gptj_query_simple(prompt) - await ctx.send(f"{prompt}{str(res)}...") - - @commands.command(aliases=["continueadv", "continueadvanced"]) - async def gpt_adv( - self, - ctx: commands.Context, - temperature: float, - top_probability: float, - max_length: int, - *, - prompt: str, - ): - """Continues your prompt with custom creativity, randomness and length \n(Defaults are 0.8, 0.9 and 100 respectively)""" - res = await gptj_query( - prompt, - max_length=max_length, - temperature=temperature, - top_probability=top_probability, - ) - await ctx.send(f"{prompt}{str(res)}...") diff --git a/plugins/Continue/gptj.py b/plugins/Continue/gptj.py deleted file mode 100644 index e7869a0..0000000 --- a/plugins/Continue/gptj.py +++ /dev/null @@ -1,25 +0,0 @@ -import httpx - -GPTJ_API = "http://api.vicgalle.net:5000/generate" - - -async def gptj_query_simple(prompt): - max_length = 100 - temperature = 1.0 - top_probability = 0.9 - return await gptj_query(prompt, max_length, temperature, top_probability) - - -async def gptj_query(prompt, max_length, temperature, top_probability): - async with httpx.AsyncClient() as client: - res = await client.post( - GPTJ_API, - params={ - "context": prompt, - "token_max_length": max_length, - "temperature": temperature, - "top_p": top_probability, - }, - ) - data = res.json() - return data["text"] diff --git a/plugins/Continue/plugin.json b/plugins/Continue/plugin.json deleted file mode 100644 index c48a672..0000000 --- a/plugins/Continue/plugin.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "Continue", - "version": "1.1.0", - "author": "Cristopher Yates", - "main_class": "Continue" -} diff --git a/plugins/Course/__init__.py b/plugins/Course/__init__.py deleted file mode 100644 index 9d5df87..0000000 --- a/plugins/Course/__init__.py +++ /dev/null @@ -1,126 +0,0 @@ -from discord.ext import commands -import discord - -from Plugin import AutomataPlugin -from plugins.Course.calendarScraper import CalendarScraper -from plugins.Course.bannerScraper import BannerScraper -from plugins.Course.peopleScraper import PeopleScraper -from plugins.Course.rmpScraper import RMPScraper - -colors = [discord.Colour.blue(), discord.Colour.red(), discord.Colour.green(), 0] - - -class Course(AutomataPlugin): - """Provides info on a CS course and its listings for the current semester""" - - def __init__(self, manifest, bot): - super().__init__(manifest, bot) - - async def cog_load(self): - self.banner_cache = self.bot.database.automata.banner_scraper_cache - self.calendar_scraper = CalendarScraper(604800, self.banner_cache) # 1 week cache lifetime - await self.calendar_scraper.setup_cache(604800) - self.people_scraper = PeopleScraper(604800, self.banner_cache) # 1 week cache lifetime - await self.people_scraper.setup_cache(604800) - self.rmp_scraper = RMPScraper(604800, self.banner_cache) # 1 week cache lifetime - await self.rmp_scraper.ensure_collection_expiry(604800) - self.banner_scraper = BannerScraper(604800, self.banner_cache) # 1 week cache lifetime - await self.banner_scraper.setup_cache(604800) - - @commands.command() - async def course(self, ctx: commands.Context, course_ID): - """Replies with info on a CS course when given the courses number/ID""" - # Get the course name and info from the calendar - ( - course_name, - course_info, - ) = await self.calendar_scraper.get_name_and_info_from_ID(course_ID) - # If there is no name, tell the user that the course doesn't exist - if not course_name: - await ctx.send("That course doesn't exist!") - return - - # Get the profs that are teaching the course this semester and the campuses where it's being taught - instructor_data = await self.banner_scraper.get_profs_from_course(course_ID) - campuses = list(instructor_data.keys()) - - # Get the year/level of the course - course_year = int(course_ID[0]) - - # Set up the initial embed for the message - embed = discord.Embed( - title=(f"COMP {course_ID}: {course_name}"), - color=colors[course_year - 1], - ) - - # If nobody is teaching the course this semester tell the user - if not campuses: - embed.description = ( - f"{course_info}\n\n**Nobody** is teaching this course this semester" - ) - await ctx.send(embed=embed) - return - - # If this is a course without an insturctor, send the embed with just the course description - if not instructor_data[campuses[0]]: - embed.description = course_info - await ctx.send(embed=embed) - return - - embed.description = ( - f"{course_info}\n\nProfessor(s) teaching this course this semester:\n" - ) - - # For each campus - for i in range(len(campuses)): - prof_strings = [] - # For each prof - for j in range(len(instructor_data[campuses[i]])): - prof_string = "" - prof_name = "" - rmp_string = "" - # Get their info using the dumb Banner name - prof_info = await self.people_scraper.get_prof_info_from_name( - instructor_data[campuses[i]][j] - ) - # If we couldn't get any info - if not prof_info: - # Try to find an RMP profile using the dumb Banner name - ( - rmp_string, - rmp_name, - ) = await self.rmp_scraper.get_rating_from_prof_name( - instructor_data[campuses[i]][j] - ) - # If there is an RMP profile - if rmp_string: - prof_name = rmp_name - # If there's no RMP profile either - else: - prof_name = instructor_data[campuses[i]][j] - prof_string = f"**{prof_name}** (Not a listed MUN Prof) " - # If we found the profs info in the first place - else: - # Get the correct name and then get try to find the RMP profile using it - prof_name = f"{prof_info['fname']} {prof_info['lname']}" - ( - rmp_string, - rmp_name, - ) = await self.rmp_scraper.get_rating_from_prof_name(prof_name) - prof_string = f"{prof_info['title']} **{prof_name}** " - # Let the user know if a profile cannot be found, otherwise add the score to the prof string - prof_string += ( - " - No profile on Rate My Prof\n" - if rmp_string == None - else " - Rate My Prof Score: " + rmp_string + "\n" - ) - prof_strings.append(prof_string) - # Add a field containing the campus name and all of the prof strings - embed.add_field( - name="__" + campuses[i] + "__", - value="\n".join(prof_strings), - inline=False, - ) - - # Send the message - await ctx.send(embed=embed) diff --git a/plugins/Course/bannerScraper.py b/plugins/Course/bannerScraper.py deleted file mode 100644 index c4b2b75..0000000 --- a/plugins/Course/bannerScraper.py +++ /dev/null @@ -1,149 +0,0 @@ -import httpx -from bs4 import BeautifulSoup -import asyncio -from datetime import datetime - -# The stuff between these lines was taken from https://github.com/jackharrhy/muntrunk/blob/master/muntrunk/scrape.py -# Full credit to Jack for this stuff -# -------------------------------------------------------------------------------------------------- -headers = { - "User-Agent": "github.com/MUNComputerScienceSociety/Automata", - "Accept": "text/html", - "Accept-Language": "en-US,en;q=0.5", - "Content-Type": "application/x-www-form-urlencoded", - "Origin": "https://www5.mun.ca", - "Connection": "keep-alive", - "Referer": "https://www5.mun.ca/admit/hwswsltb.P_CourseSearch", - "Upgrade-Insecure-Requests": "1", -} -# -------------------------------------------------------------------------------------------------- - - -class BannerScraper: - def __init__(self, cache_lifetime, cache): - # Get a reference to the cache - self.banner_cache = cache - - # Sets up the cache; deletes whats in it and ensures they expire within the given lifetime - async def setup_cache(self, lifetime): - await self.banner_cache.delete_many({}) - await self.banner_cache.create_index("datetime", expireAfterSeconds=lifetime) - - # -------------------------------------------------------------------------------------------------- - - async def actually_fetch_banner(self, year, term, level): - data = { - "p_term": f"{year}0{term}", - "p_levl": f"0{level}*00", - "campus": "%", - "faculty": "Computer Science", - "prof": "%", - "crn": "%", - } - - async with httpx.AsyncClient() as client: - response = await client.post( - "https://www5.mun.ca/admit/hwswsltb.P_CourseResults", - headers=headers, - data=data, - ) - - soup = BeautifulSoup(response.text, "html.parser") - - h2 = soup.find_all("h2") - if len(h2) >= 2 and h2[1].text == "No matches were found for your search": - return None - - return soup - - # -------------------------------------------------------------------------------------------------- - - async def get_listings_from_ID(self, course_ID): - output = [] - - # If it's before May in a given year, assume it's term 2 of the previous year - current_date = datetime.now() - isTerm2 = current_date.month < 5 - year = (current_date.year - 1) if isTerm2 else current_date.year - term = 2 if isTerm2 else 1 - - # Try to get the listings from the cache - cached = await self.banner_cache.find_one({"year": year, "term": term}) - - search_HTML = None - - # If any were found store them in a variable - if cached is not None: - search_HTML = cached["data"] - else: - # Otherwise get them through webscraping and add them to the cache - banner_resp = await self.actually_fetch_banner(year, term, 1) - search_HTML = banner_resp.text - await self.banner_cache.insert_one( - { - "datetime": datetime.utcnow(), - "year": year, - "term": term, - "data": search_HTML, - } - ) - - # If there is no HTML, panic - if not search_HTML: - return "Something went wrong..." - - # Split the HTML by campuses - courses_by_campus = search_HTML.split("Campus: ") - # Get rid of the first part because it's nonsense - courses_by_campus.pop(0) - - # For each campus - for i in range(len(courses_by_campus)): - # Split its contents into individual courses - courses = courses_by_campus[i].split("\nCOMP") - # Get the name of the campus - campus_name = courses.pop(0).split("\n", 1)[0].strip() - # For each course, if it has the right ID - for j in range(len(courses)): - if courses[j][1:5] == course_ID: - # Append it and its campus name to the output - output.append([(f"COMP{courses[j]}").split("\n"), campus_name]) - - return output - - async def get_profs_from_course(self, course_ID): - - # Get the listing - listing = await self.get_listings_from_ID(course_ID) - - profs_by_campuses = {} - - # For each campus in the listing - for i in range(len(listing)): - # Add an empty list to the dict with the campus name as the key - campus_name = listing[i][1] - profs_by_campuses[campus_name] = [] - - # For each line in the listing for the campus - for j in range(len(listing[i][0])): - # If the line is empty you're at the end - if len(listing[i][0][j]) == 0: - listing[i][0] = listing[i][0][0:j] - break - - # If the line has a "section number" in the space specified, look for a prof name - if listing[i][0][j][38] != " ": - # If the spot where the name should be is not empty - prof_name = listing[i][0][j][148:].strip() - if prof_name: - # Check to see if it is a new prof campus combo - is_new = True - for k in range(len(profs_by_campuses[campus_name])): - if profs_by_campuses[campus_name][k] == prof_name: - is_new = False - - # If it is, add it to the list in the dict for that particular campus - if is_new: - profs_by_campuses[campus_name].append(prof_name) - - return profs_by_campuses diff --git a/plugins/Course/calendarScraper.py b/plugins/Course/calendarScraper.py deleted file mode 100644 index 9780294..0000000 --- a/plugins/Course/calendarScraper.py +++ /dev/null @@ -1,63 +0,0 @@ -import asyncio -from datetime import datetime -from bs4 import BeautifulSoup -from urllib.request import urlopen - -# The URL for MUNs listing of CS courses -calendar_url = "https://www.mun.ca/regoff/calendar/sectionNo=SCI-1023" - - -class CalendarScraper: - def __init__(self, cache_lifetime, cache): - self.calendar_cache = cache - - # Sets up the cache; deletes whats in it and ensures they expire within the given lifetime - async def setup_cache(self, lifetime): - await self.calendar_cache.delete_many({}) - await self.calendar_cache.create_index("datetime", expireAfterSeconds=lifetime) - - async def get_calendar_HTML(self): - # Try to get the HTML data from the cache - cached = await self.calendar_cache.find_one() - - # If any data was found return it - if cached is not None: - return cached["data"] - - # Otherwise, get the data through web scraping - client = urlopen(calendar_url) - page_html = client.read() - client.close() - - # Add the HTML data to the cache and return it - self.calendar_cache.insert_one( - {"datetime": datetime.utcnow(), "data": page_html} - ) - return page_html - - async def get_name_and_info_from_ID(self, course_ID): - # Get the HTML from the url - page_html = await self.get_calendar_HTML() - - # Make soup and get all of the course divs - soup = BeautifulSoup(page_html, "html.parser") - course_divs = soup.find_all("div", {"class": "course"}) - - # Try to find a course with the correct ID - course_index = -1 - for i in range(len(course_divs)): - if ( - course_divs[i].find("p", {"class": "courseNumber"}).text.strip() - == course_ID - ): - course_index = i - break - # If it's not there return Nones - if course_index == -1: - return None, None - - # Otherwise return the courses name and description - course = course_divs[course_index] - course_name = course.find("p", {"class": "courseTitle"}).text.strip() - course_desc = course.div.p.text.strip() - return course_name, f"{course_name} {course_desc}" diff --git a/plugins/Course/peopleScraper.py b/plugins/Course/peopleScraper.py deleted file mode 100644 index c86429b..0000000 --- a/plugins/Course/peopleScraper.py +++ /dev/null @@ -1,57 +0,0 @@ -import asyncio -import httpx -from bs4 import BeautifulSoup -import json -from datetime import datetime - -# The URL for MUNs listing of CS faculty -url = "https://www.mun.ca/appinclude/bedrock/public/api/v1/ua/people.php?type=advanced&nopage=1&department=Computer%20Science" - - -class PeopleScraper: - def __init__(self, cache_lifetime, cache): - self.people_cache = cache - - # Sets up the cache; deletes whats in it and ensures they expire within the given lifetime - async def setup_cache(self, lifetime): - await self.people_cache.delete_many({}) - await self.people_cache.create_index("datetime", expireAfterSeconds=lifetime) - - async def get_faculty_staff(self): - # Try to get the staff data from the cache - cached = await self.people_cache.find_one() - # If any data was found return it - if cached is not None: - return cached["data"] - - # Convert the response to a dict and store the info as a list of dicts - async with httpx.AsyncClient() as client: - resp = await client.get(url) - faculty_staff = eval(resp.text)["results"] - - # Add the faculty staff data to the cache and return it - self.people_cache.insert_one( - {"datetime": datetime.utcnow(), "data": faculty_staff} - ) - return faculty_staff - - async def get_prof_info_from_name(self, prof_name): - - # Get the info of all of the staff in the faculty - faculty_staff = await self.get_faculty_staff() - - # Split the parameter name into individual words - separated_name = prof_name.lower().split(" ") - - # Loop through the staff - for i in range(len(faculty_staff)): - # Assume they are the prof we're looking for - correct_prof = True - for j in range(len(separated_name)): - # If any of the parts of the parameter name aren't present in the profs "displayname" they're not it - if separated_name[j] not in faculty_staff[i]["displayname"].lower(): - correct_prof = False - break - # Otherwise return the dict containing the profs info - if correct_prof: - return faculty_staff[i] diff --git a/plugins/Course/plugin.json b/plugins/Course/plugin.json deleted file mode 100644 index 62865f1..0000000 --- a/plugins/Course/plugin.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "Course", - "version": "1.0.1", - "author": "Cristopher Yates", - "main_class": "Course" -} diff --git a/plugins/Course/rmpScraper.py b/plugins/Course/rmpScraper.py deleted file mode 100644 index 69ea4b5..0000000 --- a/plugins/Course/rmpScraper.py +++ /dev/null @@ -1,106 +0,0 @@ -from bs4 import BeautifulSoup -from urllib.request import urlopen -from datetime import datetime -import asyncio - -# These are the parts that go into a RMP search for a MUN prof (minus the name) -url_parts = [ - "https://www.ratemyprofessors.com/search/teachers?query=", - "&sid=U2Nob29sLTE0NDE=", -] - - -class RMPScraper: - def __init__(self, cache_lifetime, cache): - self.rmp_cache = cache - - # Set up the cache data lifetimes - async def ensure_collection_expiry(self, lifetime): - await self.rmp_cache.create_index("datetime", expireAfterSeconds=lifetime) - - # Get the completed URL for the RMP search - def get_rmp_url(self, separated_prof_name): - - # Append every word from the name together with "%20" in between them - name_with_spaces = "%20".join(separated_prof_name) - - return f"{url_parts[0]}{name_with_spaces}{url_parts[1]}" - - # Get the soup from a URL - def get_soup_from_url(self, url): - client = urlopen(url) - page_html = client.read() - client.close() - return BeautifulSoup(page_html, "html.parser") - - async def get_rating_from_prof_name(self, prof_name): - prof_name = prof_name.lower() - - # Try to get the profs data from the cache - cached = await self.rmp_cache.find_one({"prof_name": prof_name}) - - # If it's found return the profs "rmp string" and name from rmp - if cached is not None: - return cached["rmp_string"], cached["prof_rmp_name"] - - # Get the URL for the search - separated_name = prof_name.split(" ") - url = self.get_rmp_url(separated_name) - # Soup - soup = self.get_soup_from_url(url) - # Get all divs for profs - profs = soup.find_all( - "a", {"class": "TeacherCard__StyledTeacherCard-syjs0d-0 dLJIlx"} - ) - - # If there are no prof divs found we try again, but leave out the first name - if not profs: - url = self.get_rmp_url(separated_name[1:]) - soup = self.get_soup_from_url(url) - profs = soup.find_all( - "a", {"class": "TeacherCard__StyledTeacherCard-syjs0d-0 dLJIlx"} - ) - # Two fails means no profile - if not profs: - return None, None - - probably_the_right_prof = None - # If more than one prof is found from the search find the first one in the CS department - if len(profs) > 1: - found_cs_prof = False - for i in range(len(profs)): - if ( - profs[i] - .find( - "div", {"class": "CardSchool__Department-sc-19lmz2k-0 haUIRO"} - ) - .text - == "Computer Science" - ): - probably_the_right_prof = profs[i] - found_cs_prof = True - break - # If none of the ooptions are in the CS department there is no profile - if not found_cs_prof: - return None, None - else: - # Only one prof means that ones probably the right one - probably_the_right_prof = profs[0] - - # Format and return the output string - score_box = probably_the_right_prof.div.div.div.find_all("div")[1:3] - rmp_string = f"{score_box[0].text} with {score_box[1].text}" - prof_rmp_name = probably_the_right_prof.div.find( - "div", {"class": "TeacherCard__CardInfo-syjs0d-1 fkdYMc"} - ).div.text - - # Add the data to the cache and return it - await self.rmp_cache.insert_one( - { - "datetime": datetime.utcnow(), - "prof_name": prof_name, - "rmp_string": rmp_string, - "prof_rmp_name": prof_rmp_name, - } - ) - return rmp_string, prof_rmp_name diff --git a/plugins/Cowsay/__init__.py b/plugins/Cowsay/__init__.py deleted file mode 100644 index 7195fd4..0000000 --- a/plugins/Cowsay/__init__.py +++ /dev/null @@ -1,25 +0,0 @@ -import urllib.parse - -from discord.ext import commands -import discord -import httpx - -from Plugin import AutomataPlugin - -COWSAY_API = "https://cowsay.morecode.org/say" - - -class Cowsay(AutomataPlugin): - """ - Cowsay - Made using https://cowsay.morecode.org/ - """ - - @commands.command() - async def cowsay(self, ctx: commands.Context, message: str): - """Responds with a cow, saying your message""" - - cow = httpx.get(COWSAY_API, params={"message": message, "format": "text"}).text - - message = await ctx.send(f"```{cow}```") - await message.add_reaction("🐮") diff --git a/plugins/Cowsay/plugin.json b/plugins/Cowsay/plugin.json deleted file mode 100644 index bfc1392..0000000 --- a/plugins/Cowsay/plugin.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "Cowsay", - "version": "0.1.0", - "author": "Jack Arthur Harrhy", - "main_class": "Cowsay" -} diff --git a/plugins/DID/__init__.py b/plugins/DID/__init__.py deleted file mode 100644 index e90430f..0000000 --- a/plugins/DID/__init__.py +++ /dev/null @@ -1,22 +0,0 @@ -from discord.ext import commands - -from Plugin import AutomataPlugin - - -class DID(AutomataPlugin): - """Informative command regarding DID""" - - @commands.command() - async def DID(self, ctx: commands.Context, number_of_times: int = 0): - """Replies with a link""" - - if number_of_times == 0: - await ctx.send( - """DID and OSDD are mental health issues on the DSM5 list -Pluralkit is an accessibility bot so you know which alter you are talking to -https://did-research.org/comorbid/dd/osdd_udd/did_osdd -http://traumadissociation.com/osdd -Please read these articles before assuming someone is just roleplaying in a server""" - ) - else: - await ctx.send(f"HOLD UP! x{number_of_times}") diff --git a/plugins/DID/plugin.json b/plugins/DID/plugin.json deleted file mode 100644 index 2eb2abb..0000000 --- a/plugins/DID/plugin.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "DID", - "version": "1.0.0", - "author": "Josip Bailey/Omega7379", - "main_class": "DID" -} diff --git a/plugins/Deadly/__init__.py b/plugins/Deadly/__init__.py deleted file mode 100644 index ea8fc14..0000000 --- a/plugins/Deadly/__init__.py +++ /dev/null @@ -1,19 +0,0 @@ -from discord.ext import commands -from Plugin import AutomataPlugin - - -# Leigh Trinity -# Oct 12th 2021 -class Deadly(AutomataPlugin): - """deadly""" - - def __init__(self, manifest, bot: commands.Bot): - super().__init__(manifest, bot) - - @commands.command() - async def deadly(self, ctx: commands.Context, number_of_times: int = 0): - - if number_of_times == 0: - await ctx.send("yes by!") - else: - await ctx.send(f"yes by'! x{number_of_times}") diff --git a/plugins/Deadly/plugin.json b/plugins/Deadly/plugin.json deleted file mode 100755 index e851695..0000000 --- a/plugins/Deadly/plugin.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "Deadly", - "version": "1.0.0", - "author": "LeightTrinity", - "main_class": "Deadly" -} diff --git a/plugins/DisDay/__init__.py b/plugins/DisDay/__init__.py deleted file mode 100644 index 7080896..0000000 --- a/plugins/DisDay/__init__.py +++ /dev/null @@ -1,58 +0,0 @@ -from unicodedata import category -from discord.ext import commands - -from Plugin import AutomataPlugin - -import requests -import xml.etree.ElementTree as ET -import datetime -from datetime import timezone -import pytz -import random - -TODAY = "TODAY" -category = ["event", "aviation", "birth", "death"] - - -class DisDay(AutomataPlugin): - """Prints a major event in history\ - - The data retrieved is attributed to http://www.hiztory.org/ - - """ - - @commands.command() - async def disday(self, ctx: commands.Context, today: str = " "): - """Replies with an event in history on this day""" - - # converting system time to newfoundland time - utc_now = datetime.datetime.now(timezone.utc) - newfoundland_timezone = pytz.timezone("America/St_Johns") - current_time = utc_now.astimezone(newfoundland_timezone) - - ind = random.randint(0, 3) - url = "http://api.hiztory.org/random/{0}.xml".format(category[ind]) - - # if it the argument is today, only returns an event happened today - if today.upper() == TODAY: - url = "https://api.hiztory.org/date/event/{0}/{1}/api.xml".format( - current_time.month, current_time.day - ) - - r = requests.get(url) - root = ET.fromstring(r.content) - suffix = "" - for child in root: - # Event tag has the desired info - if child.tag != "event": - continue - formatted_date = datetime.datetime.strptime( - child.attrib["date"], "%Y-%m-%d" - ).strftime("%b %d %Y") - if root.tag == "birth": - suffix = "was born" - await ctx.send( - "On {0}, {1} {2}".format( - formatted_date, child.attrib["content"], suffix - ) - ) diff --git a/plugins/DisDay/plugin.json b/plugins/DisDay/plugin.json deleted file mode 100644 index ed70497..0000000 --- a/plugins/DisDay/plugin.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "DisDay", - "version": "1.0.0", - "author": "Nishant Rathore", - "main_class": "DisDay" -} diff --git a/plugins/Ecoji/__init__.py b/plugins/Ecoji/__init__.py deleted file mode 100644 index c01096c..0000000 --- a/plugins/Ecoji/__init__.py +++ /dev/null @@ -1,25 +0,0 @@ -import httpx -from discord.ext import commands -from Plugin import AutomataPlugin - -URANDOM_ECOJI_URI = "https://jackharrhy.dev/urandom/ecoji/" -MIN = 1 -MAX = 400 - - -class Ecoji(AutomataPlugin): - """Sends randomly generated emojis from https://github.com/jackharrhy/DUaaS""" - - @commands.command() - async def ecoji(self, ctx: commands.Context, amount=MIN): - """Replies with random emojis, amount defaults to 1, and can be set up to 400""" - - if amount < MIN: - await ctx.send(f"Amount must be greater than {MIN - 1}") - elif amount > MAX: - await ctx.send(f"Amount must be less than than {MAX + 1}") - else: - async with httpx.AsyncClient() as client: - resp = await client.get(f"{URANDOM_ECOJI_URI}{amount}") - - await ctx.send(resp.text) diff --git a/plugins/Ecoji/plugin.json b/plugins/Ecoji/plugin.json deleted file mode 100644 index 8a4e732..0000000 --- a/plugins/Ecoji/plugin.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "Ecoji", - "version": "0.1.0", - "author": "Mukto", - "main_class": "Ecoji" -} diff --git a/plugins/ElRater/__init__.py b/plugins/ElRater/__init__.py deleted file mode 100644 index b36aaf3..0000000 --- a/plugins/ElRater/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -from discord.ext import commands - -from Plugin import AutomataPlugin - -import random - - -class Dice(AutomataPlugin): - """Rolls two 6-sided dices""" - - @commands.command() - async def dice(self, ctx: commands.Context): - """Replies with two numbers, rolled between 1 and 6""" - a = random.randint(1, 6) - b = random.randint(1, 6) - await ctx.send(f"Your numbers are {a} and {b}") diff --git a/plugins/ElRater/plugin.json b/plugins/ElRater/plugin.json deleted file mode 100644 index 90701d7..0000000 --- a/plugins/ElRater/plugin.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "Dice", - "version": "1.0.0", - "author": "saraypide", - "main_class": "Dice" -} diff --git a/plugins/Emotional Damage/__init__.py b/plugins/Emotional Damage/__init__.py deleted file mode 100644 index bd982de..0000000 --- a/plugins/Emotional Damage/__init__.py +++ /dev/null @@ -1,23 +0,0 @@ -from discord.ext import commands -from Plugin import AutomataPlugin -import random - -GIF_ONE = "https://leahhynes.github.io/EmotionalDamage/1.gif" -GIF_TWO = "https://leahhynes.github.io/EmotionalDamage/2.gif" -GIF_THREE = "https://leahhynes.github.io/EmotionalDamage/3.gif" - - -class Damage(AutomataPlugin): - """Emotional Damage""" - - @commands.command() - async def damage(self, ctx: commands.Context): - """Replies with a randomized gif of Stephen He saying Emotional Damage""" - - gifint = random.randint(1, 3) - if gifint == 1: - await ctx.send(GIF_ONE) - elif gifint == 2: - await ctx.send(GIF_TWO) - elif gifint == 3: - await ctx.send(GIF_THREE) diff --git a/plugins/Emotional Damage/plugin.json b/plugins/Emotional Damage/plugin.json deleted file mode 100644 index c91ee8b..0000000 --- a/plugins/Emotional Damage/plugin.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "Emotional Damage", - "version": "1.0", - "author": "Leah Hynes", - "main_class": "Damage" -} \ No newline at end of file diff --git a/plugins/Evil/__init__.py b/plugins/Evil/__init__.py deleted file mode 100644 index 3c164de..0000000 --- a/plugins/Evil/__init__.py +++ /dev/null @@ -1,18 +0,0 @@ -from discord.ext import commands -from Plugin import AutomataPlugin - - -class Evil(AutomataPlugin): - """Makes things Evil""" - - @commands.command() - async def evil(self, ctx: commands.Context): - """Reverses the last message of input user""" - mentions = ctx.message.mentions - if len(mentions) == 1: - egassem_live = ctx.message.content.split(">", 1)[1][::-1] - await ctx.send(f"EVIL {mentions[0].name} BE LIKE: {egassem_live}") - elif len(mentions) > 1: - await ctx.send("Too many mentions.") - else: - await ctx.send("Please mention someone.") diff --git a/plugins/Evil/plugin.json b/plugins/Evil/plugin.json deleted file mode 100644 index 2583b77..0000000 --- a/plugins/Evil/plugin.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "Evil", - "version": "0.1.0", - "author": "Mitchell H.", - "main_class": "Evil" -} diff --git a/plugins/ExecutiveDocs/plugin.json b/plugins/ExecutiveDocs/plugin.json deleted file mode 100644 index 112c246..0000000 --- a/plugins/ExecutiveDocs/plugin.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "ExecutiveDocs", - "version": "0.1.0", - "author": "Jack Arthur Harrhy", - "main_class": "ExecutiveDocs" -} diff --git a/plugins/FAQ/__init__.py b/plugins/FAQ/__init__.py deleted file mode 100644 index 7ab6582..0000000 --- a/plugins/FAQ/__init__.py +++ /dev/null @@ -1,132 +0,0 @@ -import discord -from discord.ext import commands - -from Plugin import AutomataPlugin - -# Colors -COLOR_SCIENCE = 0x98FB98 -COLOR_ARTS = 0x9898FB -COLOR_ADMIN = 0xFFFF00 - -# Degree Types -SCIENCE = ("BSC", "B.SC", "SCIENCES", "SCIENCE") -ARTS = ("BA", "B.A", "ART", "ARTS") - - -class FAQ(AutomataPlugin): - """Commands to answer some Frequently Asked Questions""" - - async def create_embed_science(self): - """Returns the embed.""" - embed_science = discord.Embed( - title="B.Sc Sample First Year", - url="https://www.mun.ca/undergrad/first-year-information/sample-first-year---st-johns-campus/science/computer-science/", - description="Students pursuing a bachelor of science with a major in computer science will normally take the following courses in their first year:", - color=COLOR_SCIENCE, - ) - embed_science.add_field( - name=f"**Fall Semester**", - value=f"> Mathematics 1090 or 1000\n> Computer Science 1001\n> elective\n> English 1090\n> elective\n", - inline=False, - ) - embed_science.add_field( - name=f"**Winter Semester**", - value=f"> Mathematics 1000 or 1001\n> Computer Science 1002\n> Computer Science 1003 or elective\n> CRW Course\n> elective\n", - inline=False, - ) - embed_science.add_field( - name=f"What is an elective?", - value=f"_Electives can be in any subject, including science courses._", - inline=False, - ) - embed_science.add_field( - name=f"What is a CRW course?", - value=f"_CRW courses are Critical Reading and Writing courses_", - inline=False, - ) - embed_science.set_footer( - text="Students who have not completed Computer Science 1003 in their first year will not be able to register for Computer Science 2001/2/3 in the fall of their second year." - ) - - return embed_science - - async def create_embed_arts(self): - """Returns the embed.""" - embed_arts = discord.Embed( - title="B.A Sample First Year", - url="https://www.mun.ca/undergrad/first-year-information/sample-first-year---st-johns-campus/science/computer-science/", - description="Students pursuing a bachelor of arts with a major in computer science will normally take the following courses in their first year::", - color=COLOR_ARTS, - ) - embed_arts.add_field( - name=f"**Fall Semester**", - value=f"> English 1090\n> Mathematics 1090 or 1000\n> Computer Science 1001\n> Language Study (LS) course\n> elective (Breathe of Knowledge encouraged) or [minor program](https://www.mun.ca/regoff/calendar/sectionNo=ARTS-0109#ARTS-4701) course\n", - inline=False, - ) - embed_arts.add_field( - name=f"**Winter Semester**", - value=f"> CRW course\n> Mathematics 1000 or 1001\n> Computer Science 1002\n> Computer Science 1003 or elective\n> LS course\n", - inline=False, - ) - embed_arts.add_field( - name=f"What is a Breadth of Knowledge and Language Study elective?", - value=f'_"More information about [Breadth of Knowledge](https://www.mun.ca/regoff/calendar/sectionNo=ARTS-0109#ARTS-8192) and [Language Study](https://www.mun.ca/regoff/calendar/sectionNo=ARTS-0109#ARTS-8196)_', - inline=False, - ) - embed_arts.set_footer( - text="Students who have not completed Computer Science 1003 in their first year will not be able to register for Computer Science 2001/2/3 in the fall of their second year." - ) - - return embed_arts - - @commands.command() - async def sample(self, ctx: commands.Context, degree: str = "blank"): - """Replies with a sample of the courses you need for your first year. - - Takes type of degree as argument. - """ - - if degree.upper() in SCIENCE: - embed = await self.create_embed_science() - await ctx.send(embed=embed) - - elif degree.upper() in ARTS: - embed = await self.create_embed_arts() - await ctx.send(embed=embed) - - else: - # No type of degree was specified - valid_args = SCIENCE + ARTS - args_str = ", ".join(valid_args) - await ctx.send( - "Specify the type of degree (Valid arguments: " + args_str + ")" - ) - - @commands.command() - async def admission(self, ctx: commands.Context): - """Replies with some FAQ about studying CS at MUN.""" - embed_admission = discord.Embed( - title="Frequently Asked Questions", - url="https://www.mun.ca/computerscience/ugrad/FAQ.php", - description="Some FAQ about studying Computer Science at MUN:", - color=COLOR_ADMIN, - ) - embed_admission.add_field( - name=f"**How do I get to study CS at MUN?**", - value=f"After admission to the university you need to complete the required courses\n> 1. COMP 1001 and COMP 1002\n> 2. 6 credit hours in a [CRW](https://www.mun.ca/regoff/calendar/sectionNo=ARTS-0109#ARTS-8194) course, including 3 credit hours in English\n> 3. MATH 1000 and 1001 (or 1090 and 1000)\n> 3. 6 credit hours in other courses\nYou must also have a mean grade of at least 65% in Computer Science 1001 and 1002.", - inline=False, - ) - embed_admission.add_field( - name=f"**How do I apply to the major?**", - value=f"Students who wish to major in computer science must submit a completed [online application form](https://www.mun.ca/computerscience/undergraduates/programs/applying-for-admission/) on the [Department of Computer Science](https://www.mun.ca/computerscience/) website. The application form is available from Feb. 1 to June 1 for students applying for fall admission, and from Aug. 1 to Oct. 1 for students applying for winter admission", - inline=False, - ) - embed_admission.add_field( - name=f"**What is the minimum required average for acceptance?**", - value=f"Students who fulfill the eligibility requirements compete for a limited number of available spaces. Selection is based on academic performance, normally cumulative average and performance in recent courses. For 2022 applications, students must also have a mean grade of at least 65% in Computer Science 1001 and 1002. Starting Fall 2021 students need an average of 65% in COMP 1001 and COMP 1002 to get in the major", - inline=False, - ) - - await ctx.send(embed=embed_admission) - - # ( ͡° ͜ʖ ͡°) diff --git a/plugins/FAQ/plugin.json b/plugins/FAQ/plugin.json deleted file mode 100644 index d34d30f..0000000 --- a/plugins/FAQ/plugin.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "FAQ", - "version": "1.0.0", - "author": "David Chicas", - "main_class": "FAQ" -} diff --git a/plugins/FRSTT/__init__.py b/plugins/FRSTT/__init__.py deleted file mode 100644 index 0891a45..0000000 --- a/plugins/FRSTT/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -from discord.ext import commands - -from Plugin import AutomataPlugin - - -class FRSTT(AutomataPlugin): - """Yoo""" - - @commands.command() - async def FRSTT(self, ctx: commands.Context, number_of_times: int = 0): - """Replies with a yoo, or many!""" - - if number_of_times == 0: - await ctx.send("yoo") - else: - await ctx.send(f"yoo x{number_of_times}") diff --git a/plugins/FRSTT/plugin.json b/plugins/FRSTT/plugin.json deleted file mode 100644 index f67200d..0000000 --- a/plugins/FRSTT/plugin.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "FRSTT", - "version": "1.0.0", - "author": "ColtonFRSTTy", - "main_class": "FRSTT" -} diff --git a/plugins/FSM/__init__.py b/plugins/FSM/__init__.py deleted file mode 100644 index 5c2d142..0000000 --- a/plugins/FSM/__init__.py +++ /dev/null @@ -1,75 +0,0 @@ -from discord.ext import commands - -from Plugin import AutomataPlugin -from Utils import send_code_block_maybe_as_file - - -class FSM(AutomataPlugin): - """ - Emulates a FSM. - """ - - @commands.command() - async def fsm(self, ctx: commands.Context, code: str): - """ - Emulates a FSM. Wrap the code in quotes, with each statement on it's own line. - - Statements: - t x y z: Define a transition from state x to state z when getting an input of y. - i x: Defines state x as the initial state. - a x1 x2 x3...: Sets all the given states to accepting states. - d x1 x2 x3...: Adds all given data to the list of input data to process over. - """ - transitions = {} - data = [] - state = None - accepting = [] - for line in code.split("\n"): - tokens = line.split(" ") - if tokens[0] == "t": - if tokens[1] not in transitions: - transitions[tokens[1]] = {} - transitions[tokens[1]][tokens[2]] = tokens[3] - elif tokens[0] == "d": - for token in tokens[1:]: - data.append(token) - elif tokens[0] == "i": - state = tokens[1] - elif tokens[0] == "a": - for token in tokens[1:]: - accepting.append(token) - - if not state: - await ctx.send("FSM crashed: no initial state specified") - return - - iterations = 0 - - changes = [] - while len(data) != 0 or iterations >= 1000: - iterations += 1 - token = data[0] - data.remove(token) - - try: - changes.append(f"{state} -> {transitions[state][token]} ({token})") - state = transitions[state][token] - except KeyError: - changes.append( - f"FSM crashed: no transition for state {state} and input {token}" - ) - break - - if iterations >= 1000: - changes.append( - "FSM haulted: more than 1000 iterations; maybe infinite loop?" - ) - else: - if state in accepting: - changes.append("FSM accepted") - else: - changes.append("FSM rejected") - - message = "\n".join(changes) - - await send_code_block_maybe_as_file(ctx, message) diff --git a/plugins/FSM/plugin.json b/plugins/FSM/plugin.json deleted file mode 100644 index c975c1e..0000000 --- a/plugins/FSM/plugin.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "FSM", - "version": "1.0.0", - "author": "Riley Flynn", - "main_class": "FSM" -} diff --git a/plugins/FortuneCookie/plugin.json b/plugins/FortuneCookie/plugin.json deleted file mode 100644 index a53fb8b..0000000 --- a/plugins/FortuneCookie/plugin.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "Fortune Cookie", - "version": "1.0.0", - "author": "Ripudaman Singh", - "main_class": "FortuneCookie" -} diff --git a/plugins/Halal/__init__.py b/plugins/Halal/__init__.py deleted file mode 100644 index 4adcfa2..0000000 --- a/plugins/Halal/__init__.py +++ /dev/null @@ -1,18 +0,0 @@ -from discord.ext import commands -import discord -from Plugin import AutomataPlugin - - -class Halal(AutomataPlugin): - """Democratic version of asking Hking""" - - @commands.command() - async def halal(self, ctx: commands.Context): - """Asks if item is Halal""" - message = ctx.message.content - item = discord.utils.escape_mentions(message[7:]) - if len(item) < 1: - item = f"<@{ctx.message.author.id}>" - msg = await ctx.send(f"Is {item} halal?") - await msg.add_reaction("✅") - await msg.add_reaction("❌") diff --git a/plugins/Halal/plugin.json b/plugins/Halal/plugin.json deleted file mode 100644 index ea40eb2..0000000 --- a/plugins/Halal/plugin.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "Halal", - "version": "1.0.0", - "author": "Ripudaman Singh", - "main_class": "Halal" -} diff --git a/plugins/Hewwo/__init__.py b/plugins/Hewwo/__init__.py deleted file mode 100644 index 5651985..0000000 --- a/plugins/Hewwo/__init__.py +++ /dev/null @@ -1,19 +0,0 @@ -import discord -from discord.ext import commands - -from Plugin import AutomataPlugin - - -class Hewwo(AutomataPlugin): - """hewwo thewe, this wiww make youw wife cutew""" - - @commands.command() - async def hewwo(self, ctx: commands.Context, *, arg): - transform = ( - discord.utils.escape_mentions(arg) - .lower() - .replace("r", "w") - .replace("l", "w") - .replace("n", "ny") - ) - await ctx.send(transform) diff --git a/plugins/Hewwo/plugin.json b/plugins/Hewwo/plugin.json deleted file mode 100644 index 426cfae..0000000 --- a/plugins/Hewwo/plugin.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "Hewwo", - "version": "0.0.1", - "author": "Mudkip", - "main_class": "Hewwo" -} diff --git a/plugins/HeyGuys/__init__.py b/plugins/HeyGuys/__init__.py deleted file mode 100644 index 7ff6d12..0000000 --- a/plugins/HeyGuys/__init__.py +++ /dev/null @@ -1,13 +0,0 @@ -from discord.ext import commands - -from Plugin import AutomataPlugin - - -class HeyGuys(AutomataPlugin): - """Someone to say hey guys when you're lonely""" - - @commands.command() - async def heyguys(self, ctx: commands.Context): - """Bot will say hey guys back""" - - await ctx.send("Hey Guys") diff --git a/plugins/HeyGuys/plugin.json b/plugins/HeyGuys/plugin.json deleted file mode 100644 index 8c5339b..0000000 --- a/plugins/HeyGuys/plugin.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "HeyGuys", - "version": "1.0.0", - "author": "PunkBat", - "main_class": "HeyGuys" -} diff --git a/plugins/InstantAnswer/plugin.json b/plugins/InstantAnswer/plugin.json deleted file mode 100644 index 2c167ee..0000000 --- a/plugins/InstantAnswer/plugin.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "InstantAnswer", - "version": "1.0.0", - "author": "S. Parson", - "main_class": "InstantAnswer" -} diff --git a/plugins/LMGTFY/plugin.json b/plugins/LMGTFY/plugin.json deleted file mode 100644 index 75150ac..0000000 --- a/plugins/LMGTFY/plugin.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "LMGTFY", - "version": "0.1.0", - "author": "Andrew Stacey", - "main_class": "LMGTFY" -} diff --git a/plugins/MCWhitelist/__init__.py b/plugins/MCWhitelist/__init__.py deleted file mode 100644 index 51aefa4..0000000 --- a/plugins/MCWhitelist/__init__.py +++ /dev/null @@ -1,160 +0,0 @@ -import discord -from discord.ext import commands -from Globals import PRIMARY_GUILD, VERIFIED_ROLE -from Plugin import AutomataPlugin -from plugins.MCWhitelist.mojang_api import MOJANG_API_BASE, MojangAPI -from plugins.MCWhitelist.whitelist_http_api import WhitelistHttpApi - - -class MCWhitelist(AutomataPlugin): - """Provides Minecraft account whitelisting for MUNCS Craft.""" - - def __init__(self, manifest, bot: commands.Bot): - super().__init__(manifest, bot) - self.whitelist_http_api = WhitelistHttpApi() - - async def cog_load(self): - self.whitelisted_accounts = ( - self.bot.database.automata.mcwhitelist_whitelisted_accounts - ) - self.disallowed_members = self.bot.database.automata.mcwhitelist_disallowed_members - self.mojang_api = MojangAPI(self.bot.database.automata.mojangapi_profile_cache) - await self.mojang_api.ensure_collection_expiry() - - async def get_whitelisted_account(self, member): - query = {"discord_id": member.id} - return await self.whitelisted_accounts.find_one(query) - - async def is_minecraft_account_already_associated(self, username): - whitelist = await self.whitelist_http_api.whitelist() - return any(entry["name"] == username for entry in whitelist) - - async def is_disallowed(self, member): - query = {"discord_id": member.id} - return await self.disallowed_members.find_one(query) is not None - - async def remove_whitelisted_account(self, ctx, whitelisted_account): - username = whitelisted_account["minecraft_username"] - await self.whitelist_http_api.remove(username) - await self.whitelisted_accounts.delete_many({"discord_id": ctx.author.id}) - - async def account_embed(self, whitelisted_account): - embed = discord.Embed() - - profile = await self.mojang_api.profile_from_uuid( - whitelisted_account["minecraft_uuid"] - ) - - if profile is not None: - skin_url = self.mojang_api.skin_url_from_profile(profile) - if skin_url is not None: - embed.set_image(url=skin_url) - - username = whitelisted_account["minecraft_username"] - embed.colour = discord.Colour.dark_green() - embed.add_field(name="Minecraft Username", value=username) - return embed - - @commands.group() - async def whitelist(self, ctx: commands.Context): - """Manage the MUNCS Craft server whitelist.""" - if not ctx.invoked_subcommand: - whitelisted_account = await self.get_whitelisted_account(ctx.author) - if whitelisted_account is not None: - embed = await self.account_embed(whitelisted_account) - await ctx.send(embed=embed) - else: - await ctx.send( - "You don't have a Minecraft account associated with yourself yet, run !whitelist add ." - ) - - @whitelist.command(name="add") - async def whitelist_add(self, ctx: commands.Context, username: str): - """Add users to the MUNCS Craft servers whitelist.""" - if await self.is_disallowed(ctx.author): - await ctx.send( - f"You are disallowed from being added to the MUNCS Craft whitelist." - ) - return - - whitelisted_account = await self.get_whitelisted_account(ctx.author) - if whitelisted_account is not None: - username = whitelisted_account["minecraft_username"] - await ctx.send( - f"You already have the Minecraft account '{username}' associated with your Discord account, run !whitelist remove first to change/add a different one." - ) - return - - verified_role = self.bot.get_guild(PRIMARY_GUILD).get_role(VERIFIED_ROLE) - if verified_role not in ctx.author.roles: - await ctx.send( - "You must first verify your MUN account using !identity before adding yourself to the whitelist." - ) - return - - mojang_resp = await self.mojang_api.info_from_username(username) - if mojang_resp is None: - await ctx.send( - f"Error verifying username '{username}' from Mojang, are you sure you typed it correctly?" - ) - return - - if await self.is_minecraft_account_already_associated(username): - await ctx.send( - f"Username '{username}' is already on the whitelist, reach out to an executive to resolve this issue." - ) - return - - await self.whitelist_http_api.add(username) - - new_whitelisted_account = { - "discord_id": ctx.author.id, - "minecraft_username": username, - "minecraft_uuid": mojang_resp["id"], - } - await self.whitelisted_accounts.insert_one(new_whitelisted_account) - - embed = await self.account_embed(new_whitelisted_account) - await ctx.send("Minecraft account whitelisted!", embed=embed) - - @whitelist.command(name="remove") - async def whitelist_remove(self, ctx: commands.Context): - """Remove users from the MUNCS Craft servers whitelist.""" - whitelisted_account = await self.get_whitelisted_account(ctx.author) - if whitelisted_account is None: - await ctx.send( - f"You don't have the Minecraft account to remove, run !whitelist add first." - ) - return - - await self.remove_whitelisted_account(ctx, whitelisted_account) - username = whitelisted_account["minecraft_username"] - await ctx.send(f"Minecraft account '{username}' removed from the whitelist.") - - @whitelist.command(name="disallow") - @commands.has_permissions(view_audit_log=True) - async def whitelist_disallow(self, ctx: commands.Context, user: discord.Member): - """Disallow users from adding themselves to the MUNCS Craft whitelist.""" - if await self.is_disallowed(user): - await ctx.send("User already disallowed.") - return - - whitelisted_account = await self.get_whitelisted_account(user) - if whitelisted_account is not None: - await self.remove_whitelisted_account(ctx, whitelisted_account) - await ctx.send("User now disallowed, and removed from the whitelist.") - else: - await ctx.send("User now disallowed.") - - await self.disallowed_members.insert_one({"discord_id": user.id}) - - @whitelist.command(name="allow") - @commands.has_permissions(view_audit_log=True) - async def whitelist_allow(self, ctx: commands.Context, user: discord.Member): - """Allows users to add themselves to the MUNCS Craft whitelist, if previously disallowed.""" - if not await self.is_disallowed(user): - await ctx.send("User already allowed.") - return - - await self.disallowed_members.delete_many({"discord_id": user.id}) - await ctx.send("User now allowed.") diff --git a/plugins/MCWhitelist/mojang_api.py b/plugins/MCWhitelist/mojang_api.py deleted file mode 100644 index fd47b67..0000000 --- a/plugins/MCWhitelist/mojang_api.py +++ /dev/null @@ -1,69 +0,0 @@ -import asyncio -import json -from base64 import b64decode -from datetime import datetime - -import httpx - - -MOJANG_API_BASE = "https://api.mojang.com" -MOJANG_SESSIONSERVER_BASE = "https://sessionserver.mojang.com" - - -class MojangAPI: - """https://wiki.vg/Mojang_API""" - - username_base = f"{MOJANG_API_BASE}/users/profiles/minecraft" - profile_base = f"{MOJANG_SESSIONSERVER_BASE}/session/minecraft/profile" - - def __init__(self, profile_cache): - self.profile_cache = profile_cache - - async def ensure_collection_expiry(self): - await self.profile_cache.create_index( - "datetime", expireAfterSeconds=900 # 15 minutes - ) - - async def info_from_username(self, username): - async with httpx.AsyncClient() as client: - resp = await client.get(f"{MojangAPI.username_base}/{username}") - - if resp.status_code == 200: - return resp.json() - - async def profile_from_uuid(self, uuid): - cached = await self.profile_cache.find_one({"uuid": uuid}) - - if cached is not None: - return cached["data"] - - async with httpx.AsyncClient() as client: - resp = await client.get(f"{MojangAPI.profile_base}/{uuid}") - - if resp.status_code == 200: - data = resp.json() - else: - data = None - - await self.profile_cache.insert_one( - {"datetime": datetime.utcnow(), "uuid": uuid, "data": data} - ) - return data - - def skin_url_from_profile(self, profile): - if (profile is None) and (profile["properties"] is None): - return None - - textures_encoded = next( - x for x in profile["properties"] if x["name"] == "textures" - ) - - if textures_encoded is None: - return None - - textures = json.loads(b64decode(textures_encoded["value"])) - - if (textures["textures"] is None) or (textures["textures"]["SKIN"] is None): - return None - - return textures["textures"]["SKIN"]["url"] diff --git a/plugins/MCWhitelist/plugin.json b/plugins/MCWhitelist/plugin.json deleted file mode 100644 index c7825d8..0000000 --- a/plugins/MCWhitelist/plugin.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "MCWhitelist", - "version": "0.1.0", - "author": "Jack Arthur Harrhy", - "main_class": "MCWhitelist" -} diff --git a/plugins/MCWhitelist/whitelist_http_api.py b/plugins/MCWhitelist/whitelist_http_api.py deleted file mode 100644 index 825d223..0000000 --- a/plugins/MCWhitelist/whitelist_http_api.py +++ /dev/null @@ -1,32 +0,0 @@ -import httpx - -from Globals import WHITELIST_HTTP_API_BEARER_TOKEN - -WHITELIST_HTTP_API_BASE = "http://craft.muncompsci.ca:7500" -HEADERS = {"authorization": f"WHA {WHITELIST_HTTP_API_BEARER_TOKEN}"} - - -class WhitelistHttpApi: - async def whitelist(self): - async with httpx.AsyncClient() as client: - resp = await client.get(WHITELIST_HTTP_API_BASE, headers=HEADERS) - - return resp.json() - - async def add(self, username): - data = {"name": username} - - async with httpx.AsyncClient() as client: - resp = await client.post( - WHITELIST_HTTP_API_BASE, json=data, headers=HEADERS - ) - resp.raise_for_status() - - async def remove(self, username): - data = {"name": username} - - async with httpx.AsyncClient() as client: - resp = await client.delete( - WHITELIST_HTTP_API_BASE, json=data, headers=HEADERS - ) - resp.raise_for_status() diff --git a/plugins/MUN Identity/plugin.json b/plugins/MUN Identity/plugin.json deleted file mode 100644 index 9114e10..0000000 --- a/plugins/MUN Identity/plugin.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "MUN Identity", - "version": "1.2.0", - "author": "Riley Flynn", - "main_class": "MUNIdentity" -} diff --git a/plugins/Man/plugin.json b/plugins/Man/plugin.json deleted file mode 100644 index 7fcfca5..0000000 --- a/plugins/Man/plugin.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "Man", - "version": "1.1.0", - "author": "John Marcoux", - "main_class": "Man" -} diff --git a/plugins/NFacts/plugin.json b/plugins/NFacts/plugin.json deleted file mode 100644 index e4f7509..0000000 --- a/plugins/NFacts/plugin.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "NFacts", - "version": "1.0.0", - "author": "HaMoOoOd25", - "main_class": "NumberFacts" -} diff --git a/plugins/Newsline/__init__.py b/plugins/Newsline/__init__.py deleted file mode 100644 index ddfb6f4..0000000 --- a/plugins/Newsline/__init__.py +++ /dev/null @@ -1,119 +0,0 @@ -import asyncio -import re -from urllib.parse import urlencode -from datetime import datetime - -import httpx -import discord -from discord.ext import commands, tasks - -from Plugin import AutomataPlugin -from Globals import ( - PRIMARY_GUILD, - NEWSLINE_CHANNEL, -) - -NEWSLINE_API_BASE_URI = "https://jackharrhy.dev/newsline" -NEWSLINE_API_POSTS = f"{NEWSLINE_API_BASE_URI}/posts" -MUN_LOGO = "https://www.cs.mun.ca/~csclub/assets/logos/others/mun-color.png" - -CHECKING_INTERVAL = 60 * 5 - - -class Newsline(AutomataPlugin): - """Posts from http://cliffy.ucs.mun.ca/archives/newsline.html, using https://github.com/jackharrhy/newsline-api""" - - def __init__(self, manifest, bot: commands.Bot): - super().__init__(manifest, bot) - self.posting = False - - async def cog_load(self): - self.posted_posts = self.bot.database.automata.newsline_posts - self.posted_posts.drop() - self.check_for_new_posts.start() - - def post_embed(self, post, post_detail): - desc = post_detail["text"] - - if len(desc) > 2048: - desc = desc[0 : 2048 - len("...")] + "..." - - url = post["htmlurl"] - if url is None: - url = post["url"] - - embed = discord.Embed( - title=f"{post_detail['subject']} - {post['date'].strftime('%r')}", - description=desc, - colour=discord.Colour.dark_red(), - url=url, - timestamp=post["date"], - ) - embed.set_footer(text="Newsline", icon_url=MUN_LOGO) - return embed - - async def post_new_post(self, post, post_details): - embed = self.post_embed(post, post_details) - await self.bot.get_guild(PRIMARY_GUILD).get_channel(NEWSLINE_CHANNEL).send( - embed=embed - ) - await self.posted_posts.insert_one({"id": post["id"]}) - - async def make_request_to_post_detail(self, post_id): - async with httpx.AsyncClient() as client: - resp = await client.get(f"{NEWSLINE_API_BASE_URI}/posts/{post_id}/detail") - post_detail = resp.json() - post_detail["text"] = re.sub(r"(\n\s*)+\n+", "\n\n", post_detail["text"]) - return post_detail - - async def make_request_to_posts(self, page): - async with httpx.AsyncClient() as client: - resp = await client.get(f"{NEWSLINE_API_POSTS}?{urlencode({'page': page})}") - return resp.json() - - async def fetch_posts(self): - page = 0 - - while len(posts := await self.make_request_to_posts(page)) != 0: - for post in posts: - post["date"] = post["date"].replace("Z", "-03:30") - post["date"] = datetime.fromisoformat(post["date"]) - yield post - - await asyncio.sleep(0.5) - page += 1 - - async def post_new_posts(self): - if self.posting: - return - - self.posting = True - posts_to_post = [] - - async for post in self.fetch_posts(): - potential_post = await self.posted_posts.find_one({"id": post["id"]}) - - if potential_post is None: - posts_to_post.append(post) - else: - break - - posts_to_post.reverse() - - for post in posts_to_post: - post_detail = self.make_request_to_post_detail(post["id"]) - await self.post_new_post(post, post_detail) - await asyncio.sleep(5) - - self.posting = False - - def cog_unload(self): - self.check_for_new_posts.cancel() - - @tasks.loop(seconds=CHECKING_INTERVAL) - async def check_for_new_posts(self): - await self.post_new_posts() - - @check_for_new_posts.before_loop - async def before_checking_for_new_posts(self): - await self.bot.wait_until_ready() diff --git a/plugins/Newsline/plugin.json b/plugins/Newsline/plugin.json deleted file mode 100644 index d521a06..0000000 --- a/plugins/Newsline/plugin.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "Newsline", - "version": "0.1.0", - "author": "Jack Arthur Harrhy", - "main_class": "Newsline" -} diff --git a/plugins/Over9000/__init__.py b/plugins/Over9000/__init__.py deleted file mode 100755 index 2d2ea4f..0000000 --- a/plugins/Over9000/__init__.py +++ /dev/null @@ -1,38 +0,0 @@ -from json import load -from os import path -from random import choice - -from discord.ext import commands - -from Plugin import AutomataPlugin - - -class Over9000(AutomataPlugin): - """Kakarot""" - - def __init__(self, manifest, bot): - super().__init__(manifest, bot) - - with open(path.join(self.manifest["path"], "quotes.json"), "r") as f: - self.dbz_quotes = load(f) - - @commands.command() - async def over9000(self, ctx: commands.Context, dbz_char: str = "goku"): - """Replies with a super saiyan emoji and random - Goku/Vegeta/Frieza quote. - - Takes character name as an argument. - """ - - if dbz_char.upper() in ("GOKU", "KAKAROT"): - await ctx.send(f"ᕙ(⇀‸↼‶)ᕗ\n**Goku:** {choice(self.dbz_quotes['goku'])}") - - elif dbz_char.upper() == "VEGETA": - await ctx.send(f"ᕙ(⇀‸↼‶)ᕗ\n**Vegeta:** {choice(self.dbz_quotes['vegeta'])}") - - elif dbz_char.upper() == "FRIEZA": - await ctx.send(f"ᕙ(⇀‸↼‶)ᕗ\n**Frieza:** {choice(self.dbz_quotes['frieza'])}") - - else: - # An invalid DBZ character was specified. - await ctx.send(f"ᕙ(⇀‸↼‶)ᕗ\n...") diff --git a/plugins/Over9000/plugin.json b/plugins/Over9000/plugin.json deleted file mode 100755 index 27add28..0000000 --- a/plugins/Over9000/plugin.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "Over9000", - "version": "1.0.0", - "author": "Nathan S.C. Stevenson", - "main_class": "Over9000" -} diff --git a/plugins/Over9000/quotes.json b/plugins/Over9000/quotes.json deleted file mode 100755 index 73af89f..0000000 --- a/plugins/Over9000/quotes.json +++ /dev/null @@ -1,45 +0,0 @@ -{ - "goku": [ - "Well, if your friend is stronger than you, I guess you're the third strongest Saiyan now.", - "I'm the Saiyan who came all the way from Earth for the sole purpose of beating you. I am the warrior you've heard of in the legends, pure of heart and awakened by fury - that's what I am.", - "I am the hope of the universe....I am the answer to all living things that cry out for peace...I am the protector of the innocent...I am the light in the darkness...I am truth. Ally to good... Nightmare to you!!!", - "I'm sorry that took so much longer than the others, but I haven't had much occasion to practice this one. This is what I call a Super Saiyan 3.", - "Why'd I have to get this guy? Sure, he's big, but he kinda looks dumb to me.", - "You seem to delight in seeing other people suffer. And you treat life like a disposable commodity! You destroy homes. You take the lives of innocent, peace-loving people. You even take the lives of children. And all of this for your own amusement of personal gain! Well, now, it's your turn!", - "That's enough! Even if it is true and I'm some kind of alien from another planet, or even if you really are my older brother, it doesn't matter, anyone who'd do the horrible things you say is no brother of mine! My name is Goku, and this my home... and you're not welcome here!", - "And, last but not least.... KAAAAAMEEEEEHAAAAMEEEEEEHAAAAAAA!!!!!", - "What's ridiculous is the creature that doesn't believe in respect. There are certain things you don't do, certain things that are understood. You don't ever mess with a man's family.", - "Welcome back Vegeta. I guess it's a good thing I didn't bury you that deep after all.", - "Your energy level is decreasing with every blow. In fact, you're not even a challenge to me anymore. It wouldn't be fair for me to keep fighting you. I'm satisfied now. Your pride has been torn to shreds. You've challenged and lost to a fighter who is superior to you, and to make it worse...he's just a monkey...right?", - "Power comes in response to a need, not a desire.", - "I don't understand why you call Saiyans by such mindless names. The only thing it does it reveal your own fear and ignorance. I guess name calling is your only attack, because you're too weak to challenge me any other way." - ], - "vegeta": [ - "Strength is the only thing that matters in this world. Everything else is just a delusion for the weak.", - "There is only one certainty in life. A strong man stands above and conquers all!", - "Push through the pain. Giving up hurts more.", - "Welcome to the end of your life! And I promise it's going to hurt.", - "Even the mightiest warriors experience fears. What makes them a true warrior is the courage that they possess to overcome their fears.", - "I do not fear this new challenge. Rather like a true warrior I will rise to meet it.", - "Never send a boy to do a man's job.", - "I'll never give in to you circus freaks! Every time I reach a new level of strength, a greater power appears to challenge my authority. It's as if fate is laughing at me with a big stupid grin, just like Kakarot.", - "Are you ready now to witness a power not seen for thousands of years?", - "Don't remind me. I'm mad enough to hurt somebody and pounding you just might be the therapy I need.", - "What's wrong Frieza? Is your brain another one of your weak and under used muscles?", - "It's over 9,000!" - ], - "frieza": [ - "There are three things I refuse to tolerate. Cowardice, bad haircuts, and military insurrection. And it is unfortunate that our friend Vegeta possesses all three of these.", - "I doubt I need an introduction, but just in case, I am the mighty Frieza and yes, all the horrible stories you've heard are true.", - "Before you begin your pathetic struggle to survive, I should warn you. Your chance of winning is nonexistent.", - "Don't look so surprised Vegeta! I'll be right with you, but first I must exterminate the mighty midget.", - "Vegeta, face it. To fight with me is futile and useless. Just wake up. You're blind and delusional. You keep going on about being a Super Saiyan, but it's just a myth Vegeta. I've never seen one, have you? You're such a chump. Heh heh.", - "Miserable Saiyan monkey.", - "Why you insolent tree monkey, I've had enough of this, you're the one who's going to pay. Talk, all you are is a bunch of talk. Allow me to show you something not even what your father saw in his lifetime.", - "Don't worry. It won't hurt too bad. Really. Death is my specialty.", - "Yes, because when one revolts, the others are sure to follow. You know, monkey see…monkey do.", - "The sun is shining, the ocean is a sparkling blue, the mountains can be seen in the background. It's positively perfect. Now all we have to do is decide which one of you gets to die first.", - "Heh heh heh. That's right. Get it all out. Live boldly…even if it is only for a few more moments.", - "Don't be glum. You should actually be flattered. I've never had to summon this much of my power to defeat someone. Fifty percent of my maximum. That's all that's required for this." - ] -} diff --git a/plugins/Ping/__init__.py b/plugins/Ping/__init__.py deleted file mode 100644 index f165daf..0000000 --- a/plugins/Ping/__init__.py +++ /dev/null @@ -1,22 +0,0 @@ -from discord.ext import commands - -from Plugin import AutomataPlugin - - -class PingFlags(commands.FlagConverter): - number_of_times: int = commands.flag( - default=0, name="pings", description="The number of pings to send" - ) - - -class Ping(AutomataPlugin): - """Pong""" - - @commands.hybrid_command() - async def ping(self, ctx: commands.Context, flags: PingFlags): - """Replies with a Pong, or many!""" - - if flags.number_of_times == 0: - await ctx.send("Pong!") - else: - await ctx.send(f"Pong! x{flags.number_of_times}") diff --git a/plugins/Ping/plugin.json b/plugins/Ping/plugin.json deleted file mode 100644 index 05af764..0000000 --- a/plugins/Ping/plugin.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "Ping", - "version": "1.0.0", - "author": "Jack Arthur Harrhy", - "main_class": "Ping" -} diff --git a/plugins/Poll/__init__.py b/plugins/Poll/__init__.py deleted file mode 100644 index d08ffc3..0000000 --- a/plugins/Poll/__init__.py +++ /dev/null @@ -1,43 +0,0 @@ -from discord.ext import commands -import discord - -MAX = 9 - -from Plugin import AutomataPlugin - -reactions = ["1️⃣", "2️⃣", "3️⃣", "4️⃣", "5️⃣", "6️⃣", "7️⃣", "8️⃣", "9️⃣"] - - -class Poll(AutomataPlugin): - """A polling system""" - - @commands.command() - async def poll(self, ctx: commands.Context, *, arg: str): - """Sends a poll as an embed""" - - options = [] - lis = [ - discord.utils.escape_mentions(x.strip()) - for x in arg.split(",") - if len(x.strip()) > 0 - ] - question = lis[0] - options = lis[1:] - if len(question) > 256: - await ctx.send("Poll question is too long") - elif len(options) > MAX: - await ctx.send( - f"Max limit exceeded, please enter less than {MAX + 1} options" - ) - else: - embed = discord.Embed(colour=discord.Colour.blue()) - output = "" - for num, option in enumerate(options): - output += f"{num+1}) {option}\n" - if len(output) > 1024: - await ctx.send("Option length is too long") - else: - embed.add_field(name=question, value=output) - message = await ctx.send(embed=embed) - for reaction, option in zip(reactions, options): - await message.add_reaction(reaction) diff --git a/plugins/Poll/plugin.json b/plugins/Poll/plugin.json deleted file mode 100644 index 1deb181..0000000 --- a/plugins/Poll/plugin.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "Poll", - "version": "1.0.1", - "author": "Ripudaman Singh", - "main_class": "Poll" -} diff --git a/plugins/Qwote/__init__.py b/plugins/Qwote/__init__.py deleted file mode 100644 index 8a402f7..0000000 --- a/plugins/Qwote/__init__.py +++ /dev/null @@ -1,33 +0,0 @@ -import httpx -import json, random -from discord.ext import commands -from Plugin import AutomataPlugin - -QUOTES_ENDPOINT = "https://type.fit/api/quotes" - - -class Qwote(AutomataPlugin): - """Qwotes""" - - def __init__(self, manifest, bot): - super().__init__(manifest, bot) - self.quotes = json.loads(httpx.get(QUOTES_ENDPOINT).text) - - @commands.command() - async def qwote(self, ctx): - """qwote uwu""" - - quote = random.choice(self.quotes) - response = '"{text}" -**{author}**' - - formatted = response.format( - text=quote["text"], - author=quote["author"] - if "author" in quote.keys() and quote["author"] != None - else "Unknown", - ) - - transform = ( - formatted.lower().replace("r", "w").replace("l", "w").replace("n", "ny") - ) - await ctx.send(transform) diff --git a/plugins/Qwote/plugin.json b/plugins/Qwote/plugin.json deleted file mode 100644 index 567caec..0000000 --- a/plugins/Qwote/plugin.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "Qwote", - "version": "1.0.0", - "author": "Jack Arthur Harrhy", - "main_class": "Qwote" -} diff --git a/plugins/SentimentAnalysis/__init__.py b/plugins/SentimentAnalysis/__init__.py deleted file mode 100644 index 6a95c0e..0000000 --- a/plugins/SentimentAnalysis/__init__.py +++ /dev/null @@ -1,61 +0,0 @@ -from nltk.sentiment import SentimentIntensityAnalyzer -import nltk -from discord.ext import commands - -from Plugin import AutomataPlugin - -nltk.downloader.download("vader_lexicon") - - -class SentimentAnalysis(AutomataPlugin): - """NLTK Sentiment Analyzer""" - - sia = SentimentIntensityAnalyzer() - - @commands.command() - async def sentiment(self, ctx, *, argument=None): - """Replies with the sentiment of the sentence""" - - message_to_reply_to = ctx.message - message_to_be_scored = argument - - if argument is None and ctx.message.reference is None: - historical_messages = await ctx.channel.history(limit=2).flatten() - message_to_reply_to = historical_messages[1] - message_to_be_scored = message_to_reply_to.content - - elif argument is None and ctx.message.reference is not None: - message_to_reply_to = await ctx.fetch_message( - ctx.message.reference.message_id - ) - message_to_be_scored = message_to_reply_to.content - - sentiment_text = "" - output_template = "<@{author}>: This text is **{sentiment_text}**." - compound_score = self.sia.polarity_scores(message_to_be_scored)["compound"] - absolute_score = abs(compound_score) - - if absolute_score == 0: - sentiment_text = "absolutely neutral" - elif 0.01 < absolute_score < 0.25: - sentiment_text = "slightly " - elif 0.25 <= absolute_score < 0.50: - sentiment_text = "somewhat " - elif 0.50 <= absolute_score < 0.75: - sentiment_text = "" - elif 0.75 <= absolute_score < 0.90: - sentiment_text = "mostly " - elif 0.90 <= absolute_score < 1.00: - sentiment_text = "overwhelmingly " - elif absolute_score == 1.00: - sentiment_text = "absolutely " - - if compound_score < 0: - sentiment_text += "negative" - elif compound_score > 0: - sentiment_text += "positive" - - output = output_template.format( - author=ctx.message.author.id, sentiment_text=sentiment_text - ) - await message_to_reply_to.reply(output) diff --git a/plugins/SentimentAnalysis/plugin.json b/plugins/SentimentAnalysis/plugin.json deleted file mode 100644 index c0d080d..0000000 --- a/plugins/SentimentAnalysis/plugin.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "Sentiment Analysis", - "version": "1.0.1", - "author": "S. Parson", - "main_class": "SentimentAnalysis" -} diff --git a/plugins/Starboard/plugin.json b/plugins/Starboard/plugin.json deleted file mode 100644 index 792438c..0000000 --- a/plugins/Starboard/plugin.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "Starboard", - "version": "1.0.2", - "author": "Hking and Andrew Stacey", - "main_class": "Starboard" -} diff --git a/plugins/TodayAtMun/DiaryUtil.py b/plugins/TodayAtMun/DiaryUtil.py deleted file mode 100644 index e853aba..0000000 --- a/plugins/TodayAtMun/DiaryUtil.py +++ /dev/null @@ -1,118 +0,0 @@ -from datetime import datetime, timedelta -import pytz - - -class DiaryUtil: - """Provides methods to manipulate dates and lookup/match parsed data.""" - - def __init__(self, diary: dict[str, str]): - self.diary = diary - self.date = DiaryUtil.get_current_time() - - @staticmethod - def get_current_time(timezone=None) -> datetime: - if timezone != None: - tz = pytz.timezone(timezone) - return datetime.now(tz) - else: - return datetime.now() - - @staticmethod - def str_to_datetime(str_date: str) -> datetime: - return datetime.strptime(str_date, "%B %d, %Y, %A") - - @staticmethod - def delta_time(delta_date_one: datetime, delta_date_two: datetime) -> int: - """ - Provides delta time of two dates. - e.g delta_date_two - delta_date_one - """ - return ( - DiaryUtil.truncate_date_time(delta_date_two) - - DiaryUtil.truncate_date_time(delta_date_one) - ).days - - @staticmethod - def delta_event_time(event_date: datetime) -> int: - """Provides delta time of days remaining for a given date to current date.""" - return DiaryUtil.delta_time(datetime.now(), event_date) - - @staticmethod - def truncate_date_time(date: datetime) -> datetime: - return datetime(date.year, date.month, date.day) - - @staticmethod - def time_to_dt_delta(date: str) -> int: - convert_date = DiaryUtil.str_to_datetime(date) - return DiaryUtil.delta_event_time(convert_date) - - def time_delta_emojify(self, next_event_date: str) -> str: - remaining_time = DiaryUtil.delta_event_time( - DiaryUtil.str_to_datetime(next_event_date) - ) - if remaining_time > 1: - return f"⏳ {remaining_time} days" - elif 0 < remaining_time <= 1: - return f"⌛ {remaining_time} day" - else: - return "🔴" - - def set_current_date(self) -> None: - """Sets the current date at that moment.""" - self.date = datetime.now() - - def get_date(self) -> datetime: - """Returns current date.""" - return self.date - - def format_date(self, date: datetime) -> str: - """Provides current date formatted to MUN style.""" - return date.strftime("%B %-d, %Y, %A") - - def next_day(self, date: datetime) -> datetime: - """Increases day by one returns date.""" - date = date + timedelta(days=1) - return date - - def go_to_event(self) -> None: - """Look up key in dict and set it to variable.""" - self.event_desc = self.diary[self.key] - - def find_event(self, date: datetime) -> str: - """Searches for date/event pair in MUN calendar.""" - if (date - DiaryUtil.get_current_time()).days == 365: - return "" - formatted_date = self.format_date(date) - for self.key in self.diary: - if self.key == formatted_date: - return self.key - return self.find_event(self.next_day(date)) - - def next_event(self, date: datetime) -> None: - """Finds the next significant date in diary.""" - self.find_event(date) - self.go_to_event() - - def package_of_events(self, date: datetime, weight: int) -> list[datetime]: - """Creates a package of upcoming events in MUN diary""" - if weight < 0 or weight > 10: - weight = 10 - packaged_items = {} - while len(packaged_items) < weight: - self.find_event(date) - packaged_items[self.key] = self.diary[self.key] - date = self.next_day(date) - return packaged_items - - def today_is_next(self, date: str) -> str: - """Provides an emoji indicator if the next event occurs on current day.""" - today_date = self.format_date(self.get_current_time()) - if today_date == date: - return "🔴" - return "" - - def find_following_event(self): - """Provides date immediately following the next date in the calendar""" - self.set_current_date() - self.find_event(self.date) - self.next_event(DiaryUtil.str_to_datetime(self.key) + timedelta(days=1)) diff --git a/plugins/TodayAtMun/Tests/test.py b/plugins/TodayAtMun/Tests/test.py deleted file mode 100644 index e4899e5..0000000 --- a/plugins/TodayAtMun/Tests/test.py +++ /dev/null @@ -1,137 +0,0 @@ -import sys -import unittest -from unittest.case import TestCase -import pytest - -sys.path.append("../../../") -from datetime import date, datetime, timedelta -from plugins.TodayAtMun.DiaryUtil import DiaryUtil -from plugins.TodayAtMun.__init__ import TodayAtMun - - -class TestDateMethods(unittest.TestCase): - """Testing TodayAtMun Plugin""" - - # parse = TodayAtMun.parse_diary() - parse = {} - diary = DiaryUtil(parse) - - def test1_today_is_next(self): - self.assertEqual(self.diary.today_is_next("May 31, 2020, Monday"), "") - self.assertEqual( - self.diary.today_is_next(self.diary.format_date(datetime.now())), "🔴" - ) - self.assertNotEqual(datetime.now(), "🔴") - - def test2_time_time(self): - self.assertEqual(self.diary.delta_time(now := datetime.now(), now), 0) - self.assertEqual( - self.diary.delta_time(datetime(2022, 10, 1), datetime(2022, 10, 2)), 1 - ) - self.assertEqual( - self.diary.delta_time( - datetime(2022, 10, 2, 22, 59), datetime(2022, 10, 3, 23, 0) - ), - 1, - ) - - -@pytest.fixture -def parsed_diary(): - diary = TodayAtMun.parse_diary() - return DiaryUtil(diary) - - -@pytest.mark.parametrize( - "date, expected", - [(date(2021, 10, 22), ""), (datetime.now(), "🔴")], -) -def test_today_is_next(date, expected): - parse = TodayAtMun.parse_diary() - diary = DiaryUtil(parse) - date = diary.format_date(diary.truncate_date_time(date)) - assert diary.today_is_next(date) == expected - - -@pytest.mark.parametrize( - " today_time, event_time, expected", - [ - (datetime(2022, 10, 2), datetime(2022, 10, 1), 1), - (datetime(2022, 10, 2), datetime(2022, 10, 1), 1), - (datetime(2022, 10, 3, 23, 0), datetime(2022, 10, 2, 22, 59), 1), - (datetime(2000, 8, 2), datetime(2000, 8, 2), 0), - ], -) -def test_time_delta_event(today_time, event_time, expected): - diary = TodayAtMun.parse_diary() - diary = DiaryUtil(diary) - assert diary.delta_time(event_time, today_time) == expected - - -def test_package_of_events(): - diary_data = { - "October 23, 2010, Saturday": "Pizza Party", - "November 20, 2010, Saturday": "Santa Clause parade lol", - "December 10, 2010, Friday": "December is on the go", - "December 30, 2010, Thursday": "NYE EVE", - "December 31, 2010, Friday": "Friday NYE", - "January 1, 2011, Saturday": "NYD", - } - - test1_diary_expected = { - "October 23, 2010, Saturday": "Pizza Party", - "November 20, 2010, Saturday": "Santa Clause parade lol", - "December 10, 2010, Friday": "December is on the go", - "December 30, 2010, Thursday": "NYE EVE", - } - - test2_diary_expected = { - "October 23, 2010, Saturday": "Pizza Party", - "November 20, 2010, Saturday": "Santa Clause parade lol", - "December 10, 2010, Friday": "December is on the go", - "December 30, 2010, Thursday": "NYE EVE", - "December 31, 2010, Friday": "Friday NYE", - } - date = datetime(2010, 10, 22) - diary = DiaryUtil(diary_data) - TestCase().assertDictEqual(diary.package_of_events(date, 4), test1_diary_expected) - TestCase().assertDictEqual(diary.package_of_events(date, 5), test2_diary_expected) - - -def test_find_event(parsed_diary): - - future_date = datetime.now() + timedelta(days=400) - formatted_future_date = parsed_diary.format_date(future_date) - diary_data = {f"{formatted_future_date}": "2 years away"} - diary_util = DiaryUtil(diary_data) - date = datetime.now() - assert diary_util.find_event(date) == "" - - future_date2 = datetime.now() + timedelta(days=30) - formated_date2 = parsed_diary.format_date(future_date2) - diary_data = {f"{formated_date2}": "40 days away"} - diary_util = DiaryUtil(diary_data) - assert diary_data[diary_util.find_event(date)] == "40 days away" - - -def test_daily_time_delta(): - time = datetime(2021, 10, 22) - diary = { - (time + timedelta(days=10)).strftime("%B %-d, %Y, %A"): "First event", - (time + timedelta(days=20)).strftime("%B %-d, %Y, %A"): "Second event", - (time + timedelta(days=30)).strftime("%B %-d, %Y, %A"): "Third event", - (time + timedelta(days=40)).strftime("%B %-d, %Y, %A"): "Fourth event", - } - diary_key_list = list(diary) - diary_util = DiaryUtil(diary) - - assert diary_util.find_event(time) == diary_key_list[0] - next_event_date = diary_util.find_event(time) - assert DiaryUtil.delta_time(time, DiaryUtil.str_to_datetime(next_event_date)) == 10 - time = time + timedelta(days=1) - assert DiaryUtil.delta_time(time, DiaryUtil.str_to_datetime(next_event_date)) == 9 - time = time + timedelta(days=1) - assert DiaryUtil.delta_time(time, DiaryUtil.str_to_datetime(next_event_date)) == 8 - time = datetime(2021, 10, 31) - next_event_date = diary_util.find_event(time) - assert DiaryUtil.delta_time(time, DiaryUtil.str_to_datetime(next_event_date)) == 1 diff --git a/plugins/TodayAtMun/__init__.py b/plugins/TodayAtMun/__init__.py deleted file mode 100644 index c989999..0000000 --- a/plugins/TodayAtMun/__init__.py +++ /dev/null @@ -1,318 +0,0 @@ -import asyncio -from random import choice - -import discord -import httpx -import mechanicalsoup -from bs4 import BeautifulSoup -from discord.ext import commands, tasks -from Globals import DIARY_DAILY_CHANNEL, GENERAL_CHANNEL, PRIMARY_GUILD -from Plugin import AutomataPlugin -from plugins.TodayAtMun.DiaryUtil import DiaryUtil - -MUN_CSS_LOGO = "https://www.cs.mun.ca/~csclub/assets/logos/color-square-trans.png" -MUN_COLOUR_RED = 0x822433 -MUN_COLOUR_WHITE = 0xFFFFFF -MUN_COLOUR_GREY = 0x838486 -DIARY_DATA_SOURCE = "https://www.mun.ca/regoff/calendar/sectionNo=GENINFO-0086" -EXAMS_DATA_SOURCE = "https://selfservice.mun.ca/direct/swkgexm.P_Query_Exam?p_term_code=202102&p_internal_campus_code=CAMP_STJ&p_title=STJ_WINT" - - -class TodayAtMun(AutomataPlugin): - """Provides a utility for MUN diary lookup specifically significant dates.""" - - def __init__(self, manifest, bot: commands.Bot): - super().__init__(manifest, bot) - self.parse = TodayAtMun.parse_diary() - self.diary_util = DiaryUtil(self.parse) - self.days_till_next_event = -1 - - async def cog_load(self): - self.posted_events = self.bot.database.automata.mun_diary - self.check_for_new_event.start() - - def cog_unload(self): - self.check_for_new_event.cancel() - - @staticmethod - def today_embed_template(): - """Provides initial embed attributes.""" - embed = discord.Embed() - mun_colours = [MUN_COLOUR_RED, MUN_COLOUR_WHITE, MUN_COLOUR_GREY] - embed.colour = discord.Colour(choice(mun_colours)) - embed.set_footer( - text="TodayAtMun ● !help TodayAtMun", - icon_url=MUN_CSS_LOGO, - ) - return embed - - def today_embed_next_template(self, next_event_date: str) -> discord.Embed: - embed = self.today_embed_template() - embed.set_author( - name=f"⏳ ~{self.diary_util.delta_event_time(self.diary_util.str_to_datetime(next_event_date))} day(s)" - ) - embed.add_field( - name=f"{self.diary_util.today_is_next(next_event_date)} {next_event_date}", - value=f"{self.diary_util.diary[self.diary_util.key]}.", - inline=False, - ) - return embed - - @commands.group(aliases=["d", "today"]) - async def diary(self, ctx: commands.Context): - """Provides brief info of significant dates on the MUN calendar. - Examples: !d next, !d later, !d bundle 10 - """ - async with ctx.typing(): - if ctx.invoked_subcommand is None: - await ctx.reply(content="Invalid command, check !help diary for more.") - - @diary.command(name="next", aliases=["n"]) - async def today_next(self, ctx: commands.Context): - """Sends next upcoming date on the MUN calendar.""" - self.diary_util.set_current_date() - self.diary_util.find_event(self.diary_util.date) - embed = self.today_embed_next_template(self.diary_util.key) - await ctx.reply(embed=embed) - - @diary.command(name="later", aliases=["l"]) - async def today_after(self, ctx: commands.Context): - """Sends the event after the 'next' event.""" - self.diary_util.find_following_event() - embed = self.today_embed_template() - embed.add_field( - name=f"{self.diary_util.key}, ⌛ ~`{DiaryUtil.time_to_dt_delta(self.diary_util.key)}` days away.", - value=f"{self.diary_util.event_desc}", - inline=False, - ) - await ctx.reply(embed=embed) - - @diary.command(name="date") - async def today_date(self, ctx: commands.Context): - """Sends the current date at that instance.""" - self.diary_util.set_current_date() - await ctx.reply(self.diary_util.format_date(self.diary_util.date)) - - @diary.command(name="bundle", aliases=["b", "nextfive"]) - async def today_bundle(self, ctx: commands.Context, events: int = 5): - """Sends the next n number of events coming up in MUN diary. - Usage: !diary bundle - Example: !diary bundle 10 - """ - self.diary_util.set_current_date() - packaged_events = self.diary_util.package_of_events( - self.diary_util.date, events - ) - packaged_keys = list(packaged_events.keys()) - first_event_date = packaged_keys[0] - last_event_data = packaged_keys[-1] - bundle_size = len(packaged_events) - embed = self.today_embed_template() - embed.add_field( - name=f"__**Showing next {bundle_size} upcoming events in MUN diary**__", - value=f"*{first_event_date}* **-** *{last_event_data}*", - inline=False, - ) - for _, (date, context) in enumerate(packaged_events.items()): - embed.add_field( - name=f"{self.diary_util.today_is_next(date)} **{date}**:", - value=f"{context}", - inline=False, - ) - await ctx.send(embed=embed) - - @today_bundle.error - async def today_next_bundle_handler(self, ctx, error): - error = getattr(error, "original", error) - if isinstance(error, commands.BadArgument): - await ctx.reply("Invalid use of bundle, Usage: !d bundle <1 - 10 : int>") - - async def post_next_event(self, channel: int): - date = DiaryUtil.get_current_time() - self.diary_util.find_event(date) - next_embed = self.today_embed_next_template(self.diary_util.key) - message_id = ( - await self.bot.get_guild(PRIMARY_GUILD) - .get_channel(channel) - .send(embed=next_embed) - ) - - return message_id - - async def notify_new_event(self, message_link): - embed = self.today_embed_template() - embed.add_field( - name="**📅 New MUN Calendar Event**", - value=f"[**Click to view**]({message_link})", - inline=False, - ) - await self.bot.get_guild(PRIMARY_GUILD).get_channel(GENERAL_CHANNEL).send( - embed=embed - ) - - async def post_new_events(self): - date = DiaryUtil.get_current_time() - next_event_date = self.diary_util.find_event(date) - retrieve_event = await self.posted_events.find_one({"date": next_event_date}) - - if retrieve_event is None: - posted_message_id = await self.post_next_event(DIARY_DAILY_CHANNEL) - await self.posted_events.insert_one({"date": next_event_date}) - await self.notify_new_event(posted_message_id.jump_url) - else: - await self.update_event_msg(next_event_date) - await asyncio.sleep(5.0) - - @tasks.loop(minutes=30.0) - async def check_for_new_event(self): - await self.post_new_events() - - @check_for_new_event.before_loop - async def before_check_test(self): - await self.bot.wait_until_ready() - - @diary.command("restart") - @commands.has_permissions(view_audit_log=True) - async def reset_recurrent_events(self, ctx): - """Executive Use Only: Resets automated event posting.""" - await self.bot.database.automata.drop_collection("mun_diary") - await self.bot.database.automata.mun_diary.insert_one({"date": "init"}) - self.check_for_new_event.restart() - - @diary.command("refresh") - @commands.has_permissions(view_audit_log=True) - async def refresh_diary(self, ctx): - """Executive Use Only: Refreshes the MUN calendar data.""" - self.parse = TodayAtMun.parse_diary() - self.diary_util = DiaryUtil(self.parse) - await ctx.reply("MUN calendar refreshed.") - - async def update_event_msg(self, next_event_date: str): - diary_daily_channel = self.bot.get_guild(PRIMARY_GUILD).get_channel( - DIARY_DAILY_CHANNEL - ) - message = await diary_daily_channel.fetch_message( - diary_daily_channel.last_message_id - ) - next_event_date_delta = self.diary_util.time_to_dt_delta(next_event_date) - can_post_to_general = ( - next_event_date_delta != self.days_till_next_event - and self.days_till_next_event != -1 - and next_event_date_delta <= 3 - ) - if can_post_to_general: - self.days_till_next_event = next_event_date_delta - await self.post_next_event(GENERAL_CHANNEL) - message.embeds[0].set_author( - name=self.diary_util.time_delta_emojify(next_event_date) - ) - edit_time = DiaryUtil.get_current_time("Canada/Newfoundland").strftime( - "%-I:%M %p %Z %a %b %-d, %Y" - ) - message.embeds[0].set_footer( - text=f"Last update: {edit_time}", icon_url=MUN_CSS_LOGO - ) - await message.edit(embed=message.embeds[0]) - - @commands.group(aliases=["e"]) - @commands.cooldown(3, 60.0) - async def exam( - self, - ctx: commands.Context, - subj: str = "", - course_num: str = "", - sec_numb: str = "", - crn: str = "", - ) -> None: - """Provides Exam Info for current semester - Usage: !exam - Example: !exam COMP 1003 - """ - sched_heading, table_heading, exams_parsed = TodayAtMun.get_exams( - subj, course_num, sec_numb, crn - ) - embed = self.today_embed_template() - embed.title = sched_heading - embed.add_field(name=table_heading, value="\u200b", inline=False) - for exam in exams_parsed: - embed.add_field(name=" | ".join(exam), value="\u200b", inline=False) - await ctx.send(embed=embed) - - @exam.error - async def exam_handler(self, ctx, error): - error = getattr(error, "original", error) - await ctx.reply(error) - - @staticmethod - def parse_diary() -> dict[str, str]: - diary = {} - resp = httpx.get(DIARY_DATA_SOURCE) - mun_request = resp.text - soup = BeautifulSoup(mun_request, "html.parser") - dates_in_diary = soup.find_all("td", attrs={"align": "left"}) - description_of_date = soup.find_all("td", attrs={"align": "justify"}) - - for left_item, right_item in zip(dates_in_diary, description_of_date): - right_item_parse = right_item.get_text().split() - try: - diary[left_item.find("p").get_text().strip("\n\t")] = " ".join( - right_item_parse - ) - except AttributeError: - diary[left_item.find("li").get_text().strip("\n\t")] = " ".join( - right_item_parse - ) - return diary - - @staticmethod - def submit_form( - subj: str = "", course_num: str = "", sec_numb: str = "", crn: str = "" - ) -> BeautifulSoup: - browser = mechanicalsoup.StatefulBrowser( - soup_config={"features": "html.parser"} - ) - browser.open(EXAMS_DATA_SOURCE) - browser.select_form('form[method="post"]') - - browser["p_subj_code"] = subj - browser["p_crse_numb"] = course_num - browser["p_seq_numb"] = sec_numb - browser["p_crn"] = crn - - browser.submit_selected() - return browser.page - - @staticmethod - def parse_sched_heading(page) -> str: - return page.find("div", class_="infotextdiv").find("b").get_text().strip() - - @staticmethod - def parse_headings(page: BeautifulSoup) -> str: - return " | ".join( - f"{heading.get_text()}" - for heading in page.find_all("td", class_="dbheader") - ) - - @staticmethod - def parse_form(page: BeautifulSoup) -> list[str]: - exam_context = [] - exams = [] - for data_cell, course in enumerate( - page.find_all("td", class_="dbdefault"), start=1 - ): - exam_context.append(course.get_text()) - if data_cell % 6 == 0: - exams.append(exam_context) - exam_context = [] - return exams - - @staticmethod - def get_exams( - subj: str = "", course_num: str = "", sec_numb: str = "", crn: str = "" - ) -> tuple[str, str, list[str]]: - """Provides exam info - schedule brief, table heading and exam details.""" - page = TodayAtMun.submit_form(subj, course_num, sec_numb, crn) - sched_heading = TodayAtMun.parse_sched_heading(page) - headings = TodayAtMun.parse_headings(page) - exams = TodayAtMun.parse_form(page) - return sched_heading, headings, exams diff --git a/plugins/TodayAtMun/plugin.json b/plugins/TodayAtMun/plugin.json deleted file mode 100644 index 97fa885..0000000 --- a/plugins/TodayAtMun/plugin.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "TodayAtMun", - "version": "0.3.0", - "author": "Zach Vaters", - "main_class": "TodayAtMun" -} diff --git a/plugins/Uptime/__init__.py b/plugins/Uptime/__init__.py deleted file mode 100644 index ecfbb07..0000000 --- a/plugins/Uptime/__init__.py +++ /dev/null @@ -1,45 +0,0 @@ -import time - -from discord.ext import commands - -from Plugin import AutomataPlugin - - -class Uptime(AutomataPlugin): - """See how long Automata has been running""" - - startup_time = int(time.time()) - - @commands.command() - async def uptime(self, ctx): - """Replies with the current uptime for this bot""" - - await ctx.message.reply( - f"I have been awake for {self.time_since(self.startup_time)}." - ) - - def time_since(self, comparison_time): - """ - Returns a human-readable representation of the elapsed time since the given time argument. - - Args: - comparison_time: The time being compared to the current time. Expressed as epoch time in seconds. - """ - days = divmod(int(time.time()) - comparison_time, 86400) - hours = divmod(days[1], 3600) - minutes = divmod(hours[1], 60) - seconds = minutes[1] - - output = "" - if days[0] > 0: - output += str(days[0]) + (" day, " if days[0] == 1 else " days, ") - if hours[0] > 0: - output += str(hours[0]) + (" hour, " if hours[0] == 1 else " hours, ") - if minutes[0] > 0: - output += str(minutes[0]) + ( - " minute, " if minutes[0] == 1 else " minutes, " - ) - if seconds > 0: - output += str(seconds) + (" second" if seconds == 1 else " seconds") - - return output diff --git a/plugins/Uptime/plugin.json b/plugins/Uptime/plugin.json deleted file mode 100644 index f031ce9..0000000 --- a/plugins/Uptime/plugin.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "Uptime", - "version": "1.0.0", - "author": "S. Parson", - "main_class": "Uptime" -} diff --git a/plugins/Verilog/__init__.py b/plugins/Verilog/__init__.py deleted file mode 100644 index 7712883..0000000 --- a/plugins/Verilog/__init__.py +++ /dev/null @@ -1,33 +0,0 @@ -import re - -from discord.ext import commands -import httpx - -from Plugin import AutomataPlugin - -BARAB_API = "https://jackharrhy.dev/barab" -CODE_BLOCK_REGEX = "```[a-z]*\n(?P[\s\S]*?)\n```" -HEADERS = {"Content-Type": "text/plain"} - -code_block = re.compile(CODE_BLOCK_REGEX) - - -class Verilog(AutomataPlugin): - """ - Verilog - Made using https://jackharrhy.dev/barab - """ - - @commands.command() - async def verilog(self, ctx: commands.Context): - """Executes all code blocks in your message as verilog""" - - code = "\n\n".join(code_block.findall(ctx.message.content)) - - async with httpx.AsyncClient() as client: - response = await client.post( - BARAB_API, headers=HEADERS, content=code.encode(), timeout=15.0 - ) - - text = response.text.replace("````", "\`\`\`") - await ctx.send(f"```{text}```") diff --git a/plugins/Verilog/plugin.json b/plugins/Verilog/plugin.json deleted file mode 100644 index c50b8ef..0000000 --- a/plugins/Verilog/plugin.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "Verilog", - "version": "0.1.0", - "author": "Jack Arthur Harrhy", - "main_class": "Verilog" -} diff --git a/plugins/Vibe/__init__.py b/plugins/Vibe/__init__.py deleted file mode 100644 index a644dc4..0000000 --- a/plugins/Vibe/__init__.py +++ /dev/null @@ -1,25 +0,0 @@ -import discord -from discord.ext import commands - -from Plugin import AutomataPlugin - -VIBE_IMAGE = "https://hamzahap.github.io/VibeGIFS/Vibe.gif" -VIBIER_IMAGE = "https://hamzahap.github.io/VibeGIFS/HyperVibe.gif" -VIBE_CAR = "https://hamzahap.github.io/VibeGIFS/Vibey.gif" -NO_VIBE = "https://hamzahap.github.io/VibeGIFS/Cry.gif" - - -class Vibe(AutomataPlugin): - """Cat Bop""" - - @commands.command() - async def vibe(self, ctx: commands.Context, vibelevel: int = 1): - """Replies with a Cat Bop Gif! Vibe levels from 1-3 can also be specified.""" - if vibelevel <= 0: - await ctx.send(NO_VIBE) - elif vibelevel == 1: - await ctx.send(VIBE_IMAGE) - elif vibelevel == 69 or vibelevel == 420: - await ctx.send(VIBE_CAR) - else: - await ctx.send(VIBIER_IMAGE) diff --git a/plugins/Vibe/plugin.json b/plugins/Vibe/plugin.json deleted file mode 100644 index 670184a..0000000 --- a/plugins/Vibe/plugin.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "Vibe", - "version": "1.0.0", - "author": "Hking", - "main_class": "Vibe" -} diff --git a/plugins/Weather/__init__.py b/plugins/Weather/__init__.py deleted file mode 100644 index 4adc870..0000000 --- a/plugins/Weather/__init__.py +++ /dev/null @@ -1,56 +0,0 @@ -from os import name -from discord.ext import commands -import discord -import httpx -from Plugin import AutomataPlugin -from Globals import WEATHER_API_KEY -import pytz -from datetime import datetime - -KEY = WEATHER_API_KEY -CALL_URI = f"http://api.weatherapi.com/v1/current.json?key={KEY}&q=A1B 3P7&aqi=no" - - -class Weather(AutomataPlugin): - """A simple weather command to get the current weather cast.""" - - @commands.command() - async def weather(self, ctx: commands.Context): - """Replies an embed of current weather""" - - async with httpx.AsyncClient() as client: - resp = await client.get(CALL_URI) - weather_data = resp.json() - - timezone = pytz.timezone("Canada/Newfoundland") - - embed = discord.Embed( - title="St. John's Weather", - description=weather_data["current"]["condition"]["text"], - colour=discord.Colour.blue(), - timestamp=datetime.now(timezone), - ) - - embed.add_field( - name="Temperature 🌡️", value=str(weather_data["current"]["temp_c"]) + " C" - ) - embed.add_field( - name="Feels Like", value=str(weather_data["current"]["feelslike_c"]) + " C" - ) - embed.add_field( - name="Precipitation 🌧️", - value=str(weather_data["current"]["precip_mm"]) + "mm", - ) - embed.add_field( - name="Humidity 💦", value=str(weather_data["current"]["humidity"]) + "%" - ) - embed.add_field( - name="Cloud ☁️", value=str(weather_data["current"]["cloud"]) + "%" - ) - embed.add_field( - name="Wind Speed 💨", value=str(weather_data["current"]["wind_kph"]) + " KPH" - ) - embed.add_field( - name="Wind Direction 🧭", value=str(weather_data["current"]["wind_dir"]) - ) - await ctx.send(embed=embed) diff --git a/plugins/Weather/plugin.json b/plugins/Weather/plugin.json deleted file mode 100644 index 612cc63..0000000 --- a/plugins/Weather/plugin.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "Weather", - "version": "1.0.0", - "author": "HaMoOoOd25", - "main_class": "Weather" -} diff --git a/plugins/WiseQuote/__init__.py b/plugins/WiseQuote/__init__.py deleted file mode 100644 index 222ec49..0000000 --- a/plugins/WiseQuote/__init__.py +++ /dev/null @@ -1,29 +0,0 @@ -import httpx -import json, random -from discord.ext import commands -from Plugin import AutomataPlugin - -QUOTES_ENDPOINT = "https://type.fit/api/quotes" - - -class WiseQuote(AutomataPlugin): - """Quotes from wise people""" - - def __init__(self, manifest, bot): - super().__init__(manifest, bot) - self.quotes = json.loads(httpx.get(QUOTES_ENDPOINT).text) - - @commands.command() - async def wisequote(self, ctx): - """Replies with a quote from a wise person""" - - quote = random.choice(self.quotes) - response = '"{text}" -**{author}**' - await ctx.send( - response.format( - text=quote["text"], - author=quote["author"] - if "author" in quote.keys() and quote["author"] != None - else "Unknown", - ) - ) diff --git a/plugins/WiseQuote/plugin.json b/plugins/WiseQuote/plugin.json deleted file mode 100644 index 72d3693..0000000 --- a/plugins/WiseQuote/plugin.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "WiseQuote", - "version": "1.0.0", - "author": "S. Parson", - "main_class": "WiseQuote" -} diff --git a/plugins/robotcheck/__init__.py b/plugins/robotcheck/__init__.py deleted file mode 100644 index bb5677f..0000000 --- a/plugins/robotcheck/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -from discord.ext import commands - -from Plugin import AutomataPlugin - - -class robotcheck(AutomataPlugin): - """are you a robot?""" - - @commands.command() - async def robotcheck(self, ctx: commands.Context, number_of_times: int = 0): - """Replies with a no!, or many!""" - - if number_of_times == 0: - await ctx.send("i am not a robot") - else: - await ctx.send(f"no! x{number_of_times}") diff --git a/plugins/robotcheck/plugin.json b/plugins/robotcheck/plugin.json deleted file mode 100644 index 7a02292..0000000 --- a/plugins/robotcheck/plugin.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "robotcheck", - "version": "1.0.0", - "author": "Ben Harris", - "main_class": "robotcheck" -} diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..654678d --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,19 @@ +[project] +name = "automata" +version = "0.1.0" +description = "Discord bot for the MUN Computer Science Society" +readme = "README.md" +requires-python = ">=3.12" +dependencies = [ + "beautifulsoup4>=4.12.3", + "discord-py>=2.4.0", + "httpx>=0.27.2", + "motor>=3.6.0", + "pydantic-settings>=2.6.1", + "pydantic>=2.10.1", + "python-dotenv>=1.0.1", + "sentry-sdk>=2.19.0", +] + +[dependency-groups] +dev = ["ruff>=0.7.3"] diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 053f51f..0000000 --- a/requirements.txt +++ /dev/null @@ -1,22 +0,0 @@ -aiohttp==3.8.5 -aiosignal==1.3.1 -async-timeout==4.0.2 -attrs==23.1.0 -certifi==2023.7.22 -charset-normalizer==3.2.0 -discord==2.3.1 -discord.py==2.3.1 -dnspython==2.4.1 -frozenlist==1.4.0 -idna==3.4 -Jigsaw==3.2.1 -motor==3.2.0 -multidict==6.0.4 -prometheus-async==22.2.0 -prometheus-client==0.17.1 -pymongo==4.4.1 -python-dotenv==1.0.0 -sentry-sdk==1.29.2 -urllib3==2.0.4 -wrapt==1.15.0 -yarl==1.9.2 diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..82c4c5e --- /dev/null +++ b/uv.lock @@ -0,0 +1,583 @@ +version = 1 +requires-python = ">=3.12" +resolution-markers = [ + "python_full_version < '3.13'", + "python_full_version >= '3.13'", +] + +[[package]] +name = "aiohappyeyeballs" +version = "2.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bc/69/2f6d5a019bd02e920a3417689a89887b39ad1e350b562f9955693d900c40/aiohappyeyeballs-2.4.3.tar.gz", hash = "sha256:75cf88a15106a5002a8eb1dab212525c00d1f4c0fa96e551c9fbe6f09a621586", size = 21809 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/d8/120cd0fe3e8530df0539e71ba9683eade12cae103dd7543e50d15f737917/aiohappyeyeballs-2.4.3-py3-none-any.whl", hash = "sha256:8a7a83727b2756f394ab2895ea0765a0a8c475e3c71e98d43d76f22b4b435572", size = 14742 }, +] + +[[package]] +name = "aiohttp" +version = "3.10.10" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohappyeyeballs" }, + { name = "aiosignal" }, + { name = "attrs" }, + { name = "frozenlist" }, + { name = "multidict" }, + { name = "yarl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/17/7e/16e57e6cf20eb62481a2f9ce8674328407187950ccc602ad07c685279141/aiohttp-3.10.10.tar.gz", hash = "sha256:0631dd7c9f0822cc61c88586ca76d5b5ada26538097d0f1df510b082bad3411a", size = 7542993 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ae/99/4c5aefe5ad06a1baf206aed6598c7cdcbc7c044c46801cd0d1ecb758cae3/aiohttp-3.10.10-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:9294bbb581f92770e6ed5c19559e1e99255e4ca604a22c5c6397b2f9dd3ee42c", size = 583536 }, + { url = "https://files.pythonhosted.org/packages/a9/36/8b3bc49b49cb6d2da40ee61ff15dbcc44fd345a3e6ab5bb20844df929821/aiohttp-3.10.10-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:a8fa23fe62c436ccf23ff930149c047f060c7126eae3ccea005f0483f27b2e28", size = 395693 }, + { url = "https://files.pythonhosted.org/packages/e1/77/0aa8660dcf11fa65d61712dbb458c4989de220a844bd69778dff25f2d50b/aiohttp-3.10.10-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5c6a5b8c7926ba5d8545c7dd22961a107526562da31a7a32fa2456baf040939f", size = 390898 }, + { url = "https://files.pythonhosted.org/packages/38/d2/b833d95deb48c75db85bf6646de0a697e7fb5d87bd27cbade4f9746b48b1/aiohttp-3.10.10-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:007ec22fbc573e5eb2fb7dec4198ef8f6bf2fe4ce20020798b2eb5d0abda6138", size = 1312060 }, + { url = "https://files.pythonhosted.org/packages/aa/5f/29fd5113165a0893de8efedf9b4737e0ba92dfcd791415a528f947d10299/aiohttp-3.10.10-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9627cc1a10c8c409b5822a92d57a77f383b554463d1884008e051c32ab1b3742", size = 1350553 }, + { url = "https://files.pythonhosted.org/packages/ad/cc/f835f74b7d344428469200105236d44606cfa448be1e7c95ca52880d9bac/aiohttp-3.10.10-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:50edbcad60d8f0e3eccc68da67f37268b5144ecc34d59f27a02f9611c1d4eec7", size = 1392646 }, + { url = "https://files.pythonhosted.org/packages/bf/fe/1332409d845ca601893bbf2d76935e0b93d41686e5f333841c7d7a4a770d/aiohttp-3.10.10-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a45d85cf20b5e0d0aa5a8dca27cce8eddef3292bc29d72dcad1641f4ed50aa16", size = 1306310 }, + { url = "https://files.pythonhosted.org/packages/e4/a1/25a7633a5a513278a9892e333501e2e69c83e50be4b57a62285fb7a008c3/aiohttp-3.10.10-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0b00807e2605f16e1e198f33a53ce3c4523114059b0c09c337209ae55e3823a8", size = 1260255 }, + { url = "https://files.pythonhosted.org/packages/f2/39/30eafe89e0e2a06c25e4762844c8214c0c0cd0fd9ffc3471694a7986f421/aiohttp-3.10.10-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f2d4324a98062be0525d16f768a03e0bbb3b9fe301ceee99611dc9a7953124e6", size = 1271141 }, + { url = "https://files.pythonhosted.org/packages/5b/fc/33125df728b48391ef1fcb512dfb02072158cc10d041414fb79803463020/aiohttp-3.10.10-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:438cd072f75bb6612f2aca29f8bd7cdf6e35e8f160bc312e49fbecab77c99e3a", size = 1280244 }, + { url = "https://files.pythonhosted.org/packages/3b/61/e42bf2c2934b5caa4e2ec0b5e5fd86989adb022b5ee60c2572a9d77cf6fe/aiohttp-3.10.10-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:baa42524a82f75303f714108fea528ccacf0386af429b69fff141ffef1c534f9", size = 1316805 }, + { url = "https://files.pythonhosted.org/packages/18/32/f52a5e2ae9ad3bba10e026a63a7a23abfa37c7d97aeeb9004eaa98df3ce3/aiohttp-3.10.10-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:a7d8d14fe962153fc681f6366bdec33d4356f98a3e3567782aac1b6e0e40109a", size = 1343930 }, + { url = "https://files.pythonhosted.org/packages/05/be/6a403b464dcab3631fe8e27b0f1d906d9e45c5e92aca97ee007e5a895560/aiohttp-3.10.10-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c1277cd707c465cd09572a774559a3cc7c7a28802eb3a2a9472588f062097205", size = 1306186 }, + { url = "https://files.pythonhosted.org/packages/8e/fd/bb50fe781068a736a02bf5c7ad5f3ab53e39f1d1e63110da6d30f7605edc/aiohttp-3.10.10-cp312-cp312-win32.whl", hash = "sha256:59bb3c54aa420521dc4ce3cc2c3fe2ad82adf7b09403fa1f48ae45c0cbde6628", size = 359289 }, + { url = "https://files.pythonhosted.org/packages/70/9e/5add7e240f77ef67c275c82cc1d08afbca57b77593118c1f6e920ae8ad3f/aiohttp-3.10.10-cp312-cp312-win_amd64.whl", hash = "sha256:0e1b370d8007c4ae31ee6db7f9a2fe801a42b146cec80a86766e7ad5c4a259cf", size = 379313 }, + { url = "https://files.pythonhosted.org/packages/b1/eb/618b1b76c7fe8082a71c9d62e3fe84c5b9af6703078caa9ec57850a12080/aiohttp-3.10.10-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ad7593bb24b2ab09e65e8a1d385606f0f47c65b5a2ae6c551db67d6653e78c28", size = 576114 }, + { url = "https://files.pythonhosted.org/packages/aa/37/3126995d7869f8b30d05381b81a2d4fb4ec6ad313db788e009bc6d39c211/aiohttp-3.10.10-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1eb89d3d29adaf533588f209768a9c02e44e4baf832b08118749c5fad191781d", size = 391901 }, + { url = "https://files.pythonhosted.org/packages/3e/f2/8fdfc845be1f811c31ceb797968523813f8e1263ee3e9120d61253f6848f/aiohttp-3.10.10-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3fe407bf93533a6fa82dece0e74dbcaaf5d684e5a51862887f9eaebe6372cd79", size = 387418 }, + { url = "https://files.pythonhosted.org/packages/60/d5/33d2061d36bf07e80286e04b7e0a4de37ce04b5ebfed72dba67659a05250/aiohttp-3.10.10-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50aed5155f819873d23520919e16703fc8925e509abbb1a1491b0087d1cd969e", size = 1287073 }, + { url = "https://files.pythonhosted.org/packages/00/52/affb55be16a4747740bd630b4c002dac6c5eac42f9bb64202fc3cf3f1930/aiohttp-3.10.10-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4f05e9727ce409358baa615dbeb9b969db94324a79b5a5cea45d39bdb01d82e6", size = 1323612 }, + { url = "https://files.pythonhosted.org/packages/94/f2/cddb69b975387daa2182a8442566971d6410b8a0179bb4540d81c97b1611/aiohttp-3.10.10-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3dffb610a30d643983aeb185ce134f97f290f8935f0abccdd32c77bed9388b42", size = 1368406 }, + { url = "https://files.pythonhosted.org/packages/c1/e4/afba7327da4d932da8c6e29aecaf855f9d52dace53ac15bfc8030a246f1b/aiohttp-3.10.10-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa6658732517ddabe22c9036479eabce6036655ba87a0224c612e1ae6af2087e", size = 1282761 }, + { url = "https://files.pythonhosted.org/packages/9f/6b/364856faa0c9031ea76e24ef0f7fef79cddd9fa8e7dba9a1771c6acc56b5/aiohttp-3.10.10-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:741a46d58677d8c733175d7e5aa618d277cd9d880301a380fd296975a9cdd7bc", size = 1236518 }, + { url = "https://files.pythonhosted.org/packages/46/af/c382846f8356fe64a7b5908bb9b477457aa23b71be7ed551013b7b7d4d87/aiohttp-3.10.10-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e00e3505cd80440f6c98c6d69269dcc2a119f86ad0a9fd70bccc59504bebd68a", size = 1250344 }, + { url = "https://files.pythonhosted.org/packages/87/53/294f87fc086fd0772d0ab82497beb9df67f0f27a8b3dd5742a2656db2bc6/aiohttp-3.10.10-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ffe595f10566f8276b76dc3a11ae4bb7eba1aac8ddd75811736a15b0d5311414", size = 1248956 }, + { url = "https://files.pythonhosted.org/packages/86/30/7d746717fe11bdfefb88bb6c09c5fc985d85c4632da8bb6018e273899254/aiohttp-3.10.10-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:bdfcf6443637c148c4e1a20c48c566aa694fa5e288d34b20fcdc58507882fed3", size = 1293379 }, + { url = "https://files.pythonhosted.org/packages/48/b9/45d670a834458db67a24258e9139ba61fa3bd7d69b98ecf3650c22806f8f/aiohttp-3.10.10-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:d183cf9c797a5291e8301790ed6d053480ed94070637bfaad914dd38b0981f67", size = 1320108 }, + { url = "https://files.pythonhosted.org/packages/72/8c/804bb2e837a175635d2000a0659eafc15b2e9d92d3d81c8f69e141ecd0b0/aiohttp-3.10.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:77abf6665ae54000b98b3c742bc6ea1d1fb31c394bcabf8b5d2c1ac3ebfe7f3b", size = 1281546 }, + { url = "https://files.pythonhosted.org/packages/89/c0/862e6a9de3d6eeb126cd9d9ea388243b70df9b871ce1a42b193b7a4a77fc/aiohttp-3.10.10-cp313-cp313-win32.whl", hash = "sha256:4470c73c12cd9109db8277287d11f9dd98f77fc54155fc71a7738a83ffcc8ea8", size = 357516 }, + { url = "https://files.pythonhosted.org/packages/ae/63/3e1aee3e554263f3f1011cca50d78a4894ae16ce99bf78101ac3a2f0ef74/aiohttp-3.10.10-cp313-cp313-win_amd64.whl", hash = "sha256:486f7aabfa292719a2753c016cc3a8f8172965cabb3ea2e7f7436c7f5a22a151", size = 376785 }, +] + +[[package]] +name = "aiosignal" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "frozenlist" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/67/0952ed97a9793b4958e5736f6d2b346b414a2cd63e82d05940032f45b32f/aiosignal-1.3.1.tar.gz", hash = "sha256:54cd96e15e1649b75d6c87526a6ff0b6c1b0dd3459f43d9ca11d48c339b68cfc", size = 19422 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/ac/a7305707cb852b7e16ff80eaf5692309bde30e2b1100a1fcacdc8f731d97/aiosignal-1.3.1-py3-none-any.whl", hash = "sha256:f8376fb07dd1e86a584e4fcdec80b36b7f81aac666ebc724e2c090300dd83b17", size = 7617 }, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 }, +] + +[[package]] +name = "anyio" +version = "4.6.2.post1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "sniffio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9f/09/45b9b7a6d4e45c6bcb5bf61d19e3ab87df68e0601fa8c5293de3542546cc/anyio-4.6.2.post1.tar.gz", hash = "sha256:4c8bc31ccdb51c7f7bd251f51c609e038d63e34219b44aa86e47576389880b4c", size = 173422 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/f5/f2b75d2fc6f1a260f340f0e7c6a060f4dd2961cc16884ed851b0d18da06a/anyio-4.6.2.post1-py3-none-any.whl", hash = "sha256:6d170c36fba3bdd840c73d3868c1e777e33676a69c3a72cf0a0d5d6d8009b61d", size = 90377 }, +] + +[[package]] +name = "attrs" +version = "24.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fc/0f/aafca9af9315aee06a89ffde799a10a582fe8de76c563ee80bbcdc08b3fb/attrs-24.2.0.tar.gz", hash = "sha256:5cfb1b9148b5b086569baec03f20d7b6bf3bcacc9a42bebf87ffaaca362f6346", size = 792678 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/21/5b6702a7f963e95456c0de2d495f67bf5fd62840ac655dc451586d23d39a/attrs-24.2.0-py3-none-any.whl", hash = "sha256:81921eb96de3191c8258c199618104dd27ac608d9366f5e35d011eae1867ede2", size = 63001 }, +] + +[[package]] +name = "automata" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "beautifulsoup4" }, + { name = "discord-py" }, + { name = "httpx" }, + { name = "motor" }, + { name = "pydantic" }, + { name = "pydantic-settings" }, + { name = "python-dotenv" }, + { name = "sentry-sdk" }, +] + +[package.dev-dependencies] +dev = [ + { name = "ruff" }, +] + +[package.metadata] +requires-dist = [ + { name = "beautifulsoup4", specifier = ">=4.12.3" }, + { name = "discord-py", specifier = ">=2.4.0" }, + { name = "httpx", specifier = ">=0.27.2" }, + { name = "motor", specifier = ">=3.6.0" }, + { name = "pydantic", specifier = ">=2.10.1" }, + { name = "pydantic-settings", specifier = ">=2.6.1" }, + { name = "python-dotenv", specifier = ">=1.0.1" }, + { name = "sentry-sdk", specifier = ">=2.19.0" }, +] + +[package.metadata.requires-dev] +dev = [{ name = "ruff", specifier = ">=0.7.3" }] + +[[package]] +name = "beautifulsoup4" +version = "4.12.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "soupsieve" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b3/ca/824b1195773ce6166d388573fc106ce56d4a805bd7427b624e063596ec58/beautifulsoup4-4.12.3.tar.gz", hash = "sha256:74e3d1928edc070d21748185c46e3fb33490f22f52a3addee9aee0f4f7781051", size = 581181 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/fe/e8c672695b37eecc5cbf43e1d0638d88d66ba3a44c4d321c796f4e59167f/beautifulsoup4-4.12.3-py3-none-any.whl", hash = "sha256:b80878c9f40111313e55da8ba20bdba06d8fa3969fc68304167741bbf9e082ed", size = 147925 }, +] + +[[package]] +name = "certifi" +version = "2024.8.30" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/ee/9b19140fe824b367c04c5e1b369942dd754c4c5462d5674002f75c4dedc1/certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9", size = 168507 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/90/3c9ff0512038035f59d279fddeb79f5f1eccd8859f06d6163c58798b9487/certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8", size = 167321 }, +] + +[[package]] +name = "discord-py" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/39/af/80cab4015722d3bee175509b7249a11d5adf77b5ff4c27f268558079d149/discord_py-2.4.0.tar.gz", hash = "sha256:d07cb2a223a185873a1d0ee78b9faa9597e45b3f6186df21a95cec1e9bcdc9a5", size = 1027707 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/23/10/3c44e9331a5ec3bae8b2919d51f611a5b94e179563b1b89eb6423a8f43eb/discord.py-2.4.0-py3-none-any.whl", hash = "sha256:b8af6711c70f7e62160bfbecb55be699b5cb69d007426759ab8ab06b1bd77d1d", size = 1125988 }, +] + +[[package]] +name = "dnspython" +version = "2.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/4a/263763cb2ba3816dd94b08ad3a33d5fdae34ecb856678773cc40a3605829/dnspython-2.7.0.tar.gz", hash = "sha256:ce9c432eda0dc91cf618a5cedf1a4e142651196bbcd2c80e89ed5a907e5cfaf1", size = 345197 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/1b/e0a87d256e40e8c888847551b20a017a6b98139178505dc7ffb96f04e954/dnspython-2.7.0-py3-none-any.whl", hash = "sha256:b4c34b7d10b51bcc3a5071e7b8dee77939f1e878477eeecc965e9835f63c6c86", size = 313632 }, +] + +[[package]] +name = "frozenlist" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8f/ed/0f4cec13a93c02c47ec32d81d11c0c1efbadf4a471e3f3ce7cad366cbbd3/frozenlist-1.5.0.tar.gz", hash = "sha256:81d5af29e61b9c8348e876d442253723928dce6433e0e76cd925cd83f1b4b817", size = 39930 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/73/fa6d1a96ab7fd6e6d1c3500700963eab46813847f01ef0ccbaa726181dd5/frozenlist-1.5.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:31115ba75889723431aa9a4e77d5f398f5cf976eea3bdf61749731f62d4a4a21", size = 94026 }, + { url = "https://files.pythonhosted.org/packages/ab/04/ea8bf62c8868b8eada363f20ff1b647cf2e93377a7b284d36062d21d81d1/frozenlist-1.5.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7437601c4d89d070eac8323f121fcf25f88674627505334654fd027b091db09d", size = 54150 }, + { url = "https://files.pythonhosted.org/packages/d0/9a/8e479b482a6f2070b26bda572c5e6889bb3ba48977e81beea35b5ae13ece/frozenlist-1.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7948140d9f8ece1745be806f2bfdf390127cf1a763b925c4a805c603df5e697e", size = 51927 }, + { url = "https://files.pythonhosted.org/packages/e3/12/2aad87deb08a4e7ccfb33600871bbe8f0e08cb6d8224371387f3303654d7/frozenlist-1.5.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:feeb64bc9bcc6b45c6311c9e9b99406660a9c05ca8a5b30d14a78555088b0b3a", size = 282647 }, + { url = "https://files.pythonhosted.org/packages/77/f2/07f06b05d8a427ea0060a9cef6e63405ea9e0d761846b95ef3fb3be57111/frozenlist-1.5.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:683173d371daad49cffb8309779e886e59c2f369430ad28fe715f66d08d4ab1a", size = 289052 }, + { url = "https://files.pythonhosted.org/packages/bd/9f/8bf45a2f1cd4aa401acd271b077989c9267ae8463e7c8b1eb0d3f561b65e/frozenlist-1.5.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7d57d8f702221405a9d9b40f9da8ac2e4a1a8b5285aac6100f3393675f0a85ee", size = 291719 }, + { url = "https://files.pythonhosted.org/packages/41/d1/1f20fd05a6c42d3868709b7604c9f15538a29e4f734c694c6bcfc3d3b935/frozenlist-1.5.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30c72000fbcc35b129cb09956836c7d7abf78ab5416595e4857d1cae8d6251a6", size = 267433 }, + { url = "https://files.pythonhosted.org/packages/af/f2/64b73a9bb86f5a89fb55450e97cd5c1f84a862d4ff90d9fd1a73ab0f64a5/frozenlist-1.5.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:000a77d6034fbad9b6bb880f7ec073027908f1b40254b5d6f26210d2dab1240e", size = 283591 }, + { url = "https://files.pythonhosted.org/packages/29/e2/ffbb1fae55a791fd6c2938dd9ea779509c977435ba3940b9f2e8dc9d5316/frozenlist-1.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5d7f5a50342475962eb18b740f3beecc685a15b52c91f7d975257e13e029eca9", size = 273249 }, + { url = "https://files.pythonhosted.org/packages/2e/6e/008136a30798bb63618a114b9321b5971172a5abddff44a100c7edc5ad4f/frozenlist-1.5.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:87f724d055eb4785d9be84e9ebf0f24e392ddfad00b3fe036e43f489fafc9039", size = 271075 }, + { url = "https://files.pythonhosted.org/packages/ae/f0/4e71e54a026b06724cec9b6c54f0b13a4e9e298cc8db0f82ec70e151f5ce/frozenlist-1.5.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:6e9080bb2fb195a046e5177f10d9d82b8a204c0736a97a153c2466127de87784", size = 285398 }, + { url = "https://files.pythonhosted.org/packages/4d/36/70ec246851478b1c0b59f11ef8ade9c482ff447c1363c2bd5fad45098b12/frozenlist-1.5.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9b93d7aaa36c966fa42efcaf716e6b3900438632a626fb09c049f6a2f09fc631", size = 294445 }, + { url = "https://files.pythonhosted.org/packages/37/e0/47f87544055b3349b633a03c4d94b405956cf2437f4ab46d0928b74b7526/frozenlist-1.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:52ef692a4bc60a6dd57f507429636c2af8b6046db8b31b18dac02cbc8f507f7f", size = 280569 }, + { url = "https://files.pythonhosted.org/packages/f9/7c/490133c160fb6b84ed374c266f42800e33b50c3bbab1652764e6e1fc498a/frozenlist-1.5.0-cp312-cp312-win32.whl", hash = "sha256:29d94c256679247b33a3dc96cce0f93cbc69c23bf75ff715919332fdbb6a32b8", size = 44721 }, + { url = "https://files.pythonhosted.org/packages/b1/56/4e45136ffc6bdbfa68c29ca56ef53783ef4c2fd395f7cbf99a2624aa9aaa/frozenlist-1.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:8969190d709e7c48ea386db202d708eb94bdb29207a1f269bab1196ce0dcca1f", size = 51329 }, + { url = "https://files.pythonhosted.org/packages/da/3b/915f0bca8a7ea04483622e84a9bd90033bab54bdf485479556c74fd5eaf5/frozenlist-1.5.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:7a1a048f9215c90973402e26c01d1cff8a209e1f1b53f72b95c13db61b00f953", size = 91538 }, + { url = "https://files.pythonhosted.org/packages/c7/d1/a7c98aad7e44afe5306a2b068434a5830f1470675f0e715abb86eb15f15b/frozenlist-1.5.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:dd47a5181ce5fcb463b5d9e17ecfdb02b678cca31280639255ce9d0e5aa67af0", size = 52849 }, + { url = "https://files.pythonhosted.org/packages/3a/c8/76f23bf9ab15d5f760eb48701909645f686f9c64fbb8982674c241fbef14/frozenlist-1.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1431d60b36d15cda188ea222033eec8e0eab488f39a272461f2e6d9e1a8e63c2", size = 50583 }, + { url = "https://files.pythonhosted.org/packages/1f/22/462a3dd093d11df623179d7754a3b3269de3b42de2808cddef50ee0f4f48/frozenlist-1.5.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6482a5851f5d72767fbd0e507e80737f9c8646ae7fd303def99bfe813f76cf7f", size = 265636 }, + { url = "https://files.pythonhosted.org/packages/80/cf/e075e407fc2ae7328155a1cd7e22f932773c8073c1fc78016607d19cc3e5/frozenlist-1.5.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:44c49271a937625619e862baacbd037a7ef86dd1ee215afc298a417ff3270608", size = 270214 }, + { url = "https://files.pythonhosted.org/packages/a1/58/0642d061d5de779f39c50cbb00df49682832923f3d2ebfb0fedf02d05f7f/frozenlist-1.5.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:12f78f98c2f1c2429d42e6a485f433722b0061d5c0b0139efa64f396efb5886b", size = 273905 }, + { url = "https://files.pythonhosted.org/packages/ab/66/3fe0f5f8f2add5b4ab7aa4e199f767fd3b55da26e3ca4ce2cc36698e50c4/frozenlist-1.5.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ce3aa154c452d2467487765e3adc730a8c153af77ad84096bc19ce19a2400840", size = 250542 }, + { url = "https://files.pythonhosted.org/packages/f6/b8/260791bde9198c87a465224e0e2bb62c4e716f5d198fc3a1dacc4895dbd1/frozenlist-1.5.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9b7dc0c4338e6b8b091e8faf0db3168a37101943e687f373dce00959583f7439", size = 267026 }, + { url = "https://files.pythonhosted.org/packages/2e/a4/3d24f88c527f08f8d44ade24eaee83b2627793fa62fa07cbb7ff7a2f7d42/frozenlist-1.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:45e0896250900b5aa25180f9aec243e84e92ac84bd4a74d9ad4138ef3f5c97de", size = 257690 }, + { url = "https://files.pythonhosted.org/packages/de/9a/d311d660420b2beeff3459b6626f2ab4fb236d07afbdac034a4371fe696e/frozenlist-1.5.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:561eb1c9579d495fddb6da8959fd2a1fca2c6d060d4113f5844b433fc02f2641", size = 253893 }, + { url = "https://files.pythonhosted.org/packages/c6/23/e491aadc25b56eabd0f18c53bb19f3cdc6de30b2129ee0bc39cd387cd560/frozenlist-1.5.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:df6e2f325bfee1f49f81aaac97d2aa757c7646534a06f8f577ce184afe2f0a9e", size = 267006 }, + { url = "https://files.pythonhosted.org/packages/08/c4/ab918ce636a35fb974d13d666dcbe03969592aeca6c3ab3835acff01f79c/frozenlist-1.5.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:140228863501b44b809fb39ec56b5d4071f4d0aa6d216c19cbb08b8c5a7eadb9", size = 276157 }, + { url = "https://files.pythonhosted.org/packages/c0/29/3b7a0bbbbe5a34833ba26f686aabfe982924adbdcafdc294a7a129c31688/frozenlist-1.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7707a25d6a77f5d27ea7dc7d1fc608aa0a478193823f88511ef5e6b8a48f9d03", size = 264642 }, + { url = "https://files.pythonhosted.org/packages/ab/42/0595b3dbffc2e82d7fe658c12d5a5bafcd7516c6bf2d1d1feb5387caa9c1/frozenlist-1.5.0-cp313-cp313-win32.whl", hash = "sha256:31a9ac2b38ab9b5a8933b693db4939764ad3f299fcaa931a3e605bc3460e693c", size = 44914 }, + { url = "https://files.pythonhosted.org/packages/17/c4/b7db1206a3fea44bf3b838ca61deb6f74424a8a5db1dd53ecb21da669be6/frozenlist-1.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:11aabdd62b8b9c4b84081a3c246506d1cddd2dd93ff0ad53ede5defec7886b28", size = 51167 }, + { url = "https://files.pythonhosted.org/packages/c6/c8/a5be5b7550c10858fcf9b0ea054baccab474da77d37f1e828ce043a3a5d4/frozenlist-1.5.0-py3-none-any.whl", hash = "sha256:d994863bba198a4a518b467bb971c56e1db3f180a25c6cf7bb1949c267f748c3", size = 11901 }, +] + +[[package]] +name = "h11" +version = "0.14.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f5/38/3af3d3633a34a3316095b39c8e8fb4853a28a536e55d347bd8d8e9a14b03/h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", size = 100418 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761", size = 58259 }, +] + +[[package]] +name = "httpcore" +version = "1.0.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b6/44/ed0fa6a17845fb033bd885c03e842f08c1b9406c86a2e60ac1ae1b9206a6/httpcore-1.0.6.tar.gz", hash = "sha256:73f6dbd6eb8c21bbf7ef8efad555481853f5f6acdeaff1edb0694289269ee17f", size = 85180 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/06/89/b161908e2f51be56568184aeb4a880fd287178d176fd1c860d2217f41106/httpcore-1.0.6-py3-none-any.whl", hash = "sha256:27b59625743b85577a8c0e10e55b50b5368a4f2cfe8cc7bcfa9cf00829c2682f", size = 78011 }, +] + +[[package]] +name = "httpx" +version = "0.27.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, + { name = "sniffio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/78/82/08f8c936781f67d9e6b9eeb8a0c8b4e406136ea4c3d1f89a5db71d42e0e6/httpx-0.27.2.tar.gz", hash = "sha256:f7c2be1d2f3c3c3160d441802406b206c2b76f5947b11115e6df10c6c65e66c2", size = 144189 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/95/9377bcb415797e44274b51d46e3249eba641711cf3348050f76ee7b15ffc/httpx-0.27.2-py3-none-any.whl", hash = "sha256:7bb2708e112d8fdd7829cd4243970f0c223274051cb35ee80c03301ee29a3df0", size = 76395 }, +] + +[[package]] +name = "idna" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, +] + +[[package]] +name = "motor" +version = "3.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pymongo" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6a/d1/06af0527fd02d49b203db70dba462e47275a3c1094f830fdaf090f0cb20c/motor-3.6.0.tar.gz", hash = "sha256:0ef7f520213e852bf0eac306adf631aabe849227d8aec900a2612512fb9c5b8d", size = 278447 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b4/c2/bba4dce0dc56e49d95c270c79c9330ed19e6b71a2a633aecf53e7e1f04c9/motor-3.6.0-py3-none-any.whl", hash = "sha256:9f07ed96f1754963d4386944e1b52d403a5350c687edc60da487d66f98dbf894", size = 74802 }, +] + +[[package]] +name = "multidict" +version = "6.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/be/504b89a5e9ca731cd47487e91c469064f8ae5af93b7259758dcfc2b9c848/multidict-6.1.0.tar.gz", hash = "sha256:22ae2ebf9b0c69d206c003e2f6a914ea33f0a932d4aa16f236afc049d9958f4a", size = 64002 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/16/92057c74ba3b96d5e211b553895cd6dc7cc4d1e43d9ab8fafc727681ef71/multidict-6.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:b04772ed465fa3cc947db808fa306d79b43e896beb677a56fb2347ca1a49c1fa", size = 48713 }, + { url = "https://files.pythonhosted.org/packages/94/3d/37d1b8893ae79716179540b89fc6a0ee56b4a65fcc0d63535c6f5d96f217/multidict-6.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6180c0ae073bddeb5a97a38c03f30c233e0a4d39cd86166251617d1bbd0af436", size = 29516 }, + { url = "https://files.pythonhosted.org/packages/a2/12/adb6b3200c363062f805275b4c1e656be2b3681aada66c80129932ff0bae/multidict-6.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:071120490b47aa997cca00666923a83f02c7fbb44f71cf7f136df753f7fa8761", size = 29557 }, + { url = "https://files.pythonhosted.org/packages/47/e9/604bb05e6e5bce1e6a5cf80a474e0f072e80d8ac105f1b994a53e0b28c42/multidict-6.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50b3a2710631848991d0bf7de077502e8994c804bb805aeb2925a981de58ec2e", size = 130170 }, + { url = "https://files.pythonhosted.org/packages/7e/13/9efa50801785eccbf7086b3c83b71a4fb501a4d43549c2f2f80b8787d69f/multidict-6.1.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b58c621844d55e71c1b7f7c498ce5aa6985d743a1a59034c57a905b3f153c1ef", size = 134836 }, + { url = "https://files.pythonhosted.org/packages/bf/0f/93808b765192780d117814a6dfcc2e75de6dcc610009ad408b8814dca3ba/multidict-6.1.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55b6d90641869892caa9ca42ff913f7ff1c5ece06474fbd32fb2cf6834726c95", size = 133475 }, + { url = "https://files.pythonhosted.org/packages/d3/c8/529101d7176fe7dfe1d99604e48d69c5dfdcadb4f06561f465c8ef12b4df/multidict-6.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b820514bfc0b98a30e3d85462084779900347e4d49267f747ff54060cc33925", size = 131049 }, + { url = "https://files.pythonhosted.org/packages/ca/0c/fc85b439014d5a58063e19c3a158a889deec399d47b5269a0f3b6a2e28bc/multidict-6.1.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:10a9b09aba0c5b48c53761b7c720aaaf7cf236d5fe394cd399c7ba662d5f9966", size = 120370 }, + { url = "https://files.pythonhosted.org/packages/db/46/d4416eb20176492d2258fbd47b4abe729ff3b6e9c829ea4236f93c865089/multidict-6.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1e16bf3e5fc9f44632affb159d30a437bfe286ce9e02754759be5536b169b305", size = 125178 }, + { url = "https://files.pythonhosted.org/packages/5b/46/73697ad7ec521df7de5531a32780bbfd908ded0643cbe457f981a701457c/multidict-6.1.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:76f364861c3bfc98cbbcbd402d83454ed9e01a5224bb3a28bf70002a230f73e2", size = 119567 }, + { url = "https://files.pythonhosted.org/packages/cd/ed/51f060e2cb0e7635329fa6ff930aa5cffa17f4c7f5c6c3ddc3500708e2f2/multidict-6.1.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:820c661588bd01a0aa62a1283f20d2be4281b086f80dad9e955e690c75fb54a2", size = 129822 }, + { url = "https://files.pythonhosted.org/packages/df/9e/ee7d1954b1331da3eddea0c4e08d9142da5f14b1321c7301f5014f49d492/multidict-6.1.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:0e5f362e895bc5b9e67fe6e4ded2492d8124bdf817827f33c5b46c2fe3ffaca6", size = 128656 }, + { url = "https://files.pythonhosted.org/packages/77/00/8538f11e3356b5d95fa4b024aa566cde7a38aa7a5f08f4912b32a037c5dc/multidict-6.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3ec660d19bbc671e3a6443325f07263be452c453ac9e512f5eb935e7d4ac28b3", size = 125360 }, + { url = "https://files.pythonhosted.org/packages/be/05/5d334c1f2462d43fec2363cd00b1c44c93a78c3925d952e9a71caf662e96/multidict-6.1.0-cp312-cp312-win32.whl", hash = "sha256:58130ecf8f7b8112cdb841486404f1282b9c86ccb30d3519faf301b2e5659133", size = 26382 }, + { url = "https://files.pythonhosted.org/packages/a3/bf/f332a13486b1ed0496d624bcc7e8357bb8053823e8cd4b9a18edc1d97e73/multidict-6.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:188215fc0aafb8e03341995e7c4797860181562380f81ed0a87ff455b70bf1f1", size = 28529 }, + { url = "https://files.pythonhosted.org/packages/22/67/1c7c0f39fe069aa4e5d794f323be24bf4d33d62d2a348acdb7991f8f30db/multidict-6.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d569388c381b24671589335a3be6e1d45546c2988c2ebe30fdcada8457a31008", size = 48771 }, + { url = "https://files.pythonhosted.org/packages/3c/25/c186ee7b212bdf0df2519eacfb1981a017bda34392c67542c274651daf23/multidict-6.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:052e10d2d37810b99cc170b785945421141bf7bb7d2f8799d431e7db229c385f", size = 29533 }, + { url = "https://files.pythonhosted.org/packages/67/5e/04575fd837e0958e324ca035b339cea174554f6f641d3fb2b4f2e7ff44a2/multidict-6.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f90c822a402cb865e396a504f9fc8173ef34212a342d92e362ca498cad308e28", size = 29595 }, + { url = "https://files.pythonhosted.org/packages/d3/b2/e56388f86663810c07cfe4a3c3d87227f3811eeb2d08450b9e5d19d78876/multidict-6.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b225d95519a5bf73860323e633a664b0d85ad3d5bede6d30d95b35d4dfe8805b", size = 130094 }, + { url = "https://files.pythonhosted.org/packages/6c/ee/30ae9b4186a644d284543d55d491fbd4239b015d36b23fea43b4c94f7052/multidict-6.1.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:23bfd518810af7de1116313ebd9092cb9aa629beb12f6ed631ad53356ed6b86c", size = 134876 }, + { url = "https://files.pythonhosted.org/packages/84/c7/70461c13ba8ce3c779503c70ec9d0345ae84de04521c1f45a04d5f48943d/multidict-6.1.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c09fcfdccdd0b57867577b719c69e347a436b86cd83747f179dbf0cc0d4c1f3", size = 133500 }, + { url = "https://files.pythonhosted.org/packages/4a/9f/002af221253f10f99959561123fae676148dd730e2daa2cd053846a58507/multidict-6.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf6bea52ec97e95560af5ae576bdac3aa3aae0b6758c6efa115236d9e07dae44", size = 131099 }, + { url = "https://files.pythonhosted.org/packages/82/42/d1c7a7301d52af79d88548a97e297f9d99c961ad76bbe6f67442bb77f097/multidict-6.1.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57feec87371dbb3520da6192213c7d6fc892d5589a93db548331954de8248fd2", size = 120403 }, + { url = "https://files.pythonhosted.org/packages/68/f3/471985c2c7ac707547553e8f37cff5158030d36bdec4414cb825fbaa5327/multidict-6.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0c3f390dc53279cbc8ba976e5f8035eab997829066756d811616b652b00a23a3", size = 125348 }, + { url = "https://files.pythonhosted.org/packages/67/2c/e6df05c77e0e433c214ec1d21ddd203d9a4770a1f2866a8ca40a545869a0/multidict-6.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:59bfeae4b25ec05b34f1956eaa1cb38032282cd4dfabc5056d0a1ec4d696d3aa", size = 119673 }, + { url = "https://files.pythonhosted.org/packages/c5/cd/bc8608fff06239c9fb333f9db7743a1b2eafe98c2666c9a196e867a3a0a4/multidict-6.1.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b2f59caeaf7632cc633b5cf6fc449372b83bbdf0da4ae04d5be36118e46cc0aa", size = 129927 }, + { url = "https://files.pythonhosted.org/packages/44/8e/281b69b7bc84fc963a44dc6e0bbcc7150e517b91df368a27834299a526ac/multidict-6.1.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:37bb93b2178e02b7b618893990941900fd25b6b9ac0fa49931a40aecdf083fe4", size = 128711 }, + { url = "https://files.pythonhosted.org/packages/12/a4/63e7cd38ed29dd9f1881d5119f272c898ca92536cdb53ffe0843197f6c85/multidict-6.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4e9f48f58c2c523d5a06faea47866cd35b32655c46b443f163d08c6d0ddb17d6", size = 125519 }, + { url = "https://files.pythonhosted.org/packages/38/e0/4f5855037a72cd8a7a2f60a3952d9aa45feedb37ae7831642102604e8a37/multidict-6.1.0-cp313-cp313-win32.whl", hash = "sha256:3a37ffb35399029b45c6cc33640a92bef403c9fd388acce75cdc88f58bd19a81", size = 26426 }, + { url = "https://files.pythonhosted.org/packages/7e/a5/17ee3a4db1e310b7405f5d25834460073a8ccd86198ce044dfaf69eac073/multidict-6.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:e9aa71e15d9d9beaad2c6b9319edcdc0a49a43ef5c0a4c8265ca9ee7d6c67774", size = 28531 }, + { url = "https://files.pythonhosted.org/packages/99/b7/b9e70fde2c0f0c9af4cc5277782a89b66d35948ea3369ec9f598358c3ac5/multidict-6.1.0-py3-none-any.whl", hash = "sha256:48e171e52d1c4d33888e529b999e5900356b9ae588c2f09a52dcefb158b27506", size = 10051 }, +] + +[[package]] +name = "propcache" +version = "0.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a9/4d/5e5a60b78dbc1d464f8a7bbaeb30957257afdc8512cbb9dfd5659304f5cd/propcache-0.2.0.tar.gz", hash = "sha256:df81779732feb9d01e5d513fad0122efb3d53bbc75f61b2a4f29a020bc985e70", size = 40951 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/46/a41ca1097769fc548fc9216ec4c1471b772cc39720eb47ed7e38ef0006a9/propcache-0.2.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:2ee7606193fb267be4b2e3b32714f2d58cad27217638db98a60f9efb5efeccc2", size = 80800 }, + { url = "https://files.pythonhosted.org/packages/75/4f/93df46aab9cc473498ff56be39b5f6ee1e33529223d7a4d8c0a6101a9ba2/propcache-0.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:91ee8fc02ca52e24bcb77b234f22afc03288e1dafbb1f88fe24db308910c4ac7", size = 46443 }, + { url = "https://files.pythonhosted.org/packages/0b/17/308acc6aee65d0f9a8375e36c4807ac6605d1f38074b1581bd4042b9fb37/propcache-0.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2e900bad2a8456d00a113cad8c13343f3b1f327534e3589acc2219729237a2e8", size = 45676 }, + { url = "https://files.pythonhosted.org/packages/65/44/626599d2854d6c1d4530b9a05e7ff2ee22b790358334b475ed7c89f7d625/propcache-0.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f52a68c21363c45297aca15561812d542f8fc683c85201df0bebe209e349f793", size = 246191 }, + { url = "https://files.pythonhosted.org/packages/f2/df/5d996d7cb18df076debae7d76ac3da085c0575a9f2be6b1f707fe227b54c/propcache-0.2.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1e41d67757ff4fbc8ef2af99b338bfb955010444b92929e9e55a6d4dcc3c4f09", size = 251791 }, + { url = "https://files.pythonhosted.org/packages/2e/6d/9f91e5dde8b1f662f6dd4dff36098ed22a1ef4e08e1316f05f4758f1576c/propcache-0.2.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a64e32f8bd94c105cc27f42d3b658902b5bcc947ece3c8fe7bc1b05982f60e89", size = 253434 }, + { url = "https://files.pythonhosted.org/packages/3c/e9/1b54b7e26f50b3e0497cd13d3483d781d284452c2c50dd2a615a92a087a3/propcache-0.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:55346705687dbd7ef0d77883ab4f6fabc48232f587925bdaf95219bae072491e", size = 248150 }, + { url = "https://files.pythonhosted.org/packages/a7/ef/a35bf191c8038fe3ce9a414b907371c81d102384eda5dbafe6f4dce0cf9b/propcache-0.2.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:00181262b17e517df2cd85656fcd6b4e70946fe62cd625b9d74ac9977b64d8d9", size = 233568 }, + { url = "https://files.pythonhosted.org/packages/97/d9/d00bb9277a9165a5e6d60f2142cd1a38a750045c9c12e47ae087f686d781/propcache-0.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6994984550eaf25dd7fc7bd1b700ff45c894149341725bb4edc67f0ffa94efa4", size = 229874 }, + { url = "https://files.pythonhosted.org/packages/8e/78/c123cf22469bdc4b18efb78893e69c70a8b16de88e6160b69ca6bdd88b5d/propcache-0.2.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:56295eb1e5f3aecd516d91b00cfd8bf3a13991de5a479df9e27dd569ea23959c", size = 225857 }, + { url = "https://files.pythonhosted.org/packages/31/1b/fd6b2f1f36d028820d35475be78859d8c89c8f091ad30e377ac49fd66359/propcache-0.2.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:439e76255daa0f8151d3cb325f6dd4a3e93043e6403e6491813bcaaaa8733887", size = 227604 }, + { url = "https://files.pythonhosted.org/packages/99/36/b07be976edf77a07233ba712e53262937625af02154353171716894a86a6/propcache-0.2.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f6475a1b2ecb310c98c28d271a30df74f9dd436ee46d09236a6b750a7599ce57", size = 238430 }, + { url = "https://files.pythonhosted.org/packages/0d/64/5822f496c9010e3966e934a011ac08cac8734561842bc7c1f65586e0683c/propcache-0.2.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3444cdba6628accf384e349014084b1cacd866fbb88433cd9d279d90a54e0b23", size = 244814 }, + { url = "https://files.pythonhosted.org/packages/fd/bd/8657918a35d50b18a9e4d78a5df7b6c82a637a311ab20851eef4326305c1/propcache-0.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4a9d9b4d0a9b38d1c391bb4ad24aa65f306c6f01b512e10a8a34a2dc5675d348", size = 235922 }, + { url = "https://files.pythonhosted.org/packages/a8/6f/ec0095e1647b4727db945213a9f395b1103c442ef65e54c62e92a72a3f75/propcache-0.2.0-cp312-cp312-win32.whl", hash = "sha256:69d3a98eebae99a420d4b28756c8ce6ea5a29291baf2dc9ff9414b42676f61d5", size = 40177 }, + { url = "https://files.pythonhosted.org/packages/20/a2/bd0896fdc4f4c1db46d9bc361c8c79a9bf08ccc08ba054a98e38e7ba1557/propcache-0.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:ad9c9b99b05f163109466638bd30ada1722abb01bbb85c739c50b6dc11f92dc3", size = 44446 }, + { url = "https://files.pythonhosted.org/packages/a8/a7/5f37b69197d4f558bfef5b4bceaff7c43cc9b51adf5bd75e9081d7ea80e4/propcache-0.2.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ecddc221a077a8132cf7c747d5352a15ed763b674c0448d811f408bf803d9ad7", size = 78120 }, + { url = "https://files.pythonhosted.org/packages/c8/cd/48ab2b30a6b353ecb95a244915f85756d74f815862eb2ecc7a518d565b48/propcache-0.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0e53cb83fdd61cbd67202735e6a6687a7b491c8742dfc39c9e01e80354956763", size = 45127 }, + { url = "https://files.pythonhosted.org/packages/a5/ba/0a1ef94a3412aab057bd996ed5f0ac7458be5bf469e85c70fa9ceb43290b/propcache-0.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92fe151145a990c22cbccf9ae15cae8ae9eddabfc949a219c9f667877e40853d", size = 44419 }, + { url = "https://files.pythonhosted.org/packages/b4/6c/ca70bee4f22fa99eacd04f4d2f1699be9d13538ccf22b3169a61c60a27fa/propcache-0.2.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6a21ef516d36909931a2967621eecb256018aeb11fc48656e3257e73e2e247a", size = 229611 }, + { url = "https://files.pythonhosted.org/packages/19/70/47b872a263e8511ca33718d96a10c17d3c853aefadeb86dc26e8421184b9/propcache-0.2.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f88a4095e913f98988f5b338c1d4d5d07dbb0b6bad19892fd447484e483ba6b", size = 234005 }, + { url = "https://files.pythonhosted.org/packages/4f/be/3b0ab8c84a22e4a3224719099c1229ddfdd8a6a1558cf75cb55ee1e35c25/propcache-0.2.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5a5b3bb545ead161be780ee85a2b54fdf7092815995661947812dde94a40f6fb", size = 237270 }, + { url = "https://files.pythonhosted.org/packages/04/d8/f071bb000d4b8f851d312c3c75701e586b3f643fe14a2e3409b1b9ab3936/propcache-0.2.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67aeb72e0f482709991aa91345a831d0b707d16b0257e8ef88a2ad246a7280bf", size = 231877 }, + { url = "https://files.pythonhosted.org/packages/93/e7/57a035a1359e542bbb0a7df95aad6b9871ebee6dce2840cb157a415bd1f3/propcache-0.2.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c997f8c44ec9b9b0bcbf2d422cc00a1d9b9c681f56efa6ca149a941e5560da2", size = 217848 }, + { url = "https://files.pythonhosted.org/packages/f0/93/d1dea40f112ec183398fb6c42fde340edd7bab202411c4aa1a8289f461b6/propcache-0.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2a66df3d4992bc1d725b9aa803e8c5a66c010c65c741ad901e260ece77f58d2f", size = 216987 }, + { url = "https://files.pythonhosted.org/packages/62/4c/877340871251145d3522c2b5d25c16a1690ad655fbab7bb9ece6b117e39f/propcache-0.2.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:3ebbcf2a07621f29638799828b8d8668c421bfb94c6cb04269130d8de4fb7136", size = 212451 }, + { url = "https://files.pythonhosted.org/packages/7c/bb/a91b72efeeb42906ef58ccf0cdb87947b54d7475fee3c93425d732f16a61/propcache-0.2.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1235c01ddaa80da8235741e80815ce381c5267f96cc49b1477fdcf8c047ef325", size = 212879 }, + { url = "https://files.pythonhosted.org/packages/9b/7f/ee7fea8faac57b3ec5d91ff47470c6c5d40d7f15d0b1fccac806348fa59e/propcache-0.2.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3947483a381259c06921612550867b37d22e1df6d6d7e8361264b6d037595f44", size = 222288 }, + { url = "https://files.pythonhosted.org/packages/ff/d7/acd67901c43d2e6b20a7a973d9d5fd543c6e277af29b1eb0e1f7bd7ca7d2/propcache-0.2.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:d5bed7f9805cc29c780f3aee05de3262ee7ce1f47083cfe9f77471e9d6777e83", size = 228257 }, + { url = "https://files.pythonhosted.org/packages/8d/6f/6272ecc7a8daad1d0754cfc6c8846076a8cb13f810005c79b15ce0ef0cf2/propcache-0.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e4a91d44379f45f5e540971d41e4626dacd7f01004826a18cb048e7da7e96544", size = 221075 }, + { url = "https://files.pythonhosted.org/packages/7c/bd/c7a6a719a6b3dd8b3aeadb3675b5783983529e4a3185946aa444d3e078f6/propcache-0.2.0-cp313-cp313-win32.whl", hash = "sha256:f902804113e032e2cdf8c71015651c97af6418363bea8d78dc0911d56c335032", size = 39654 }, + { url = "https://files.pythonhosted.org/packages/88/e7/0eef39eff84fa3e001b44de0bd41c7c0e3432e7648ffd3d64955910f002d/propcache-0.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:8f188cfcc64fb1266f4684206c9de0e80f54622c3f22a910cbd200478aeae61e", size = 43705 }, + { url = "https://files.pythonhosted.org/packages/3d/b6/e6d98278f2d49b22b4d033c9f792eda783b9ab2094b041f013fc69bcde87/propcache-0.2.0-py3-none-any.whl", hash = "sha256:2ccc28197af5313706511fab3a8b66dcd6da067a1331372c82ea1cb74285e036", size = 11603 }, +] + +[[package]] +name = "pydantic" +version = "2.10.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c4/bd/7fc610993f616d2398958d0028d15eaf53bde5f80cb2edb7aa4f1feaf3a7/pydantic-2.10.1.tar.gz", hash = "sha256:a4daca2dc0aa429555e0656d6bf94873a7dc5f54ee42b1f5873d666fb3f35560", size = 783717 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/fc/fda48d347bd50a788dd2a0f318a52160f911b86fc2d8b4c86f4d7c9bceea/pydantic-2.10.1-py3-none-any.whl", hash = "sha256:a8d20db84de64cf4a7d59e899c2caf0fe9d660c7cfc482528e7020d7dd189a7e", size = 455329 }, +] + +[[package]] +name = "pydantic-core" +version = "2.27.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a6/9f/7de1f19b6aea45aeb441838782d68352e71bfa98ee6fa048d5041991b33e/pydantic_core-2.27.1.tar.gz", hash = "sha256:62a763352879b84aa31058fc931884055fd75089cccbd9d58bb6afd01141b235", size = 412785 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/be/51/2e9b3788feb2aebff2aa9dfbf060ec739b38c05c46847601134cc1fed2ea/pydantic_core-2.27.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9cbd94fc661d2bab2bc702cddd2d3370bbdcc4cd0f8f57488a81bcce90c7a54f", size = 1895239 }, + { url = "https://files.pythonhosted.org/packages/7b/9e/f8063952e4a7d0127f5d1181addef9377505dcce3be224263b25c4f0bfd9/pydantic_core-2.27.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5f8c4718cd44ec1580e180cb739713ecda2bdee1341084c1467802a417fe0f02", size = 1805070 }, + { url = "https://files.pythonhosted.org/packages/2c/9d/e1d6c4561d262b52e41b17a7ef8301e2ba80b61e32e94520271029feb5d8/pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:15aae984e46de8d376df515f00450d1522077254ef6b7ce189b38ecee7c9677c", size = 1828096 }, + { url = "https://files.pythonhosted.org/packages/be/65/80ff46de4266560baa4332ae3181fffc4488ea7d37282da1a62d10ab89a4/pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1ba5e3963344ff25fc8c40da90f44b0afca8cfd89d12964feb79ac1411a260ac", size = 1857708 }, + { url = "https://files.pythonhosted.org/packages/d5/ca/3370074ad758b04d9562b12ecdb088597f4d9d13893a48a583fb47682cdf/pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:992cea5f4f3b29d6b4f7f1726ed8ee46c8331c6b4eed6db5b40134c6fe1768bb", size = 2037751 }, + { url = "https://files.pythonhosted.org/packages/b1/e2/4ab72d93367194317b99d051947c071aef6e3eb95f7553eaa4208ecf9ba4/pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0325336f348dbee6550d129b1627cb8f5351a9dc91aad141ffb96d4937bd9529", size = 2733863 }, + { url = "https://files.pythonhosted.org/packages/8a/c6/8ae0831bf77f356bb73127ce5a95fe115b10f820ea480abbd72d3cc7ccf3/pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7597c07fbd11515f654d6ece3d0e4e5093edc30a436c63142d9a4b8e22f19c35", size = 2161161 }, + { url = "https://files.pythonhosted.org/packages/f1/f4/b2fe73241da2429400fc27ddeaa43e35562f96cf5b67499b2de52b528cad/pydantic_core-2.27.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3bbd5d8cc692616d5ef6fbbbd50dbec142c7e6ad9beb66b78a96e9c16729b089", size = 1993294 }, + { url = "https://files.pythonhosted.org/packages/77/29/4bb008823a7f4cc05828198153f9753b3bd4c104d93b8e0b1bfe4e187540/pydantic_core-2.27.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:dc61505e73298a84a2f317255fcc72b710b72980f3a1f670447a21efc88f8381", size = 2001468 }, + { url = "https://files.pythonhosted.org/packages/f2/a9/0eaceeba41b9fad851a4107e0cf999a34ae8f0d0d1f829e2574f3d8897b0/pydantic_core-2.27.1-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:e1f735dc43da318cad19b4173dd1ffce1d84aafd6c9b782b3abc04a0d5a6f5bb", size = 2091413 }, + { url = "https://files.pythonhosted.org/packages/d8/36/eb8697729725bc610fd73940f0d860d791dc2ad557faaefcbb3edbd2b349/pydantic_core-2.27.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:f4e5658dbffe8843a0f12366a4c2d1c316dbe09bb4dfbdc9d2d9cd6031de8aae", size = 2154735 }, + { url = "https://files.pythonhosted.org/packages/52/e5/4f0fbd5c5995cc70d3afed1b5c754055bb67908f55b5cb8000f7112749bf/pydantic_core-2.27.1-cp312-none-win32.whl", hash = "sha256:672ebbe820bb37988c4d136eca2652ee114992d5d41c7e4858cdd90ea94ffe5c", size = 1833633 }, + { url = "https://files.pythonhosted.org/packages/ee/f2/c61486eee27cae5ac781305658779b4a6b45f9cc9d02c90cb21b940e82cc/pydantic_core-2.27.1-cp312-none-win_amd64.whl", hash = "sha256:66ff044fd0bb1768688aecbe28b6190f6e799349221fb0de0e6f4048eca14c16", size = 1986973 }, + { url = "https://files.pythonhosted.org/packages/df/a6/e3f12ff25f250b02f7c51be89a294689d175ac76e1096c32bf278f29ca1e/pydantic_core-2.27.1-cp312-none-win_arm64.whl", hash = "sha256:9a3b0793b1bbfd4146304e23d90045f2a9b5fd5823aa682665fbdaf2a6c28f3e", size = 1883215 }, + { url = "https://files.pythonhosted.org/packages/0f/d6/91cb99a3c59d7b072bded9959fbeab0a9613d5a4935773c0801f1764c156/pydantic_core-2.27.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f216dbce0e60e4d03e0c4353c7023b202d95cbaeff12e5fd2e82ea0a66905073", size = 1895033 }, + { url = "https://files.pythonhosted.org/packages/07/42/d35033f81a28b27dedcade9e967e8a40981a765795c9ebae2045bcef05d3/pydantic_core-2.27.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a2e02889071850bbfd36b56fd6bc98945e23670773bc7a76657e90e6b6603c08", size = 1807542 }, + { url = "https://files.pythonhosted.org/packages/41/c2/491b59e222ec7e72236e512108ecad532c7f4391a14e971c963f624f7569/pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42b0e23f119b2b456d07ca91b307ae167cc3f6c846a7b169fca5326e32fdc6cf", size = 1827854 }, + { url = "https://files.pythonhosted.org/packages/e3/f3/363652651779113189cefdbbb619b7b07b7a67ebb6840325117cc8cc3460/pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:764be71193f87d460a03f1f7385a82e226639732214b402f9aa61f0d025f0737", size = 1857389 }, + { url = "https://files.pythonhosted.org/packages/5f/97/be804aed6b479af5a945daec7538d8bf358d668bdadde4c7888a2506bdfb/pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c00666a3bd2f84920a4e94434f5974d7bbc57e461318d6bb34ce9cdbbc1f6b2", size = 2037934 }, + { url = "https://files.pythonhosted.org/packages/42/01/295f0bd4abf58902917e342ddfe5f76cf66ffabfc57c2e23c7681a1a1197/pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3ccaa88b24eebc0f849ce0a4d09e8a408ec5a94afff395eb69baf868f5183107", size = 2735176 }, + { url = "https://files.pythonhosted.org/packages/9d/a0/cd8e9c940ead89cc37812a1a9f310fef59ba2f0b22b4e417d84ab09fa970/pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c65af9088ac534313e1963443d0ec360bb2b9cba6c2909478d22c2e363d98a51", size = 2160720 }, + { url = "https://files.pythonhosted.org/packages/73/ae/9d0980e286627e0aeca4c352a60bd760331622c12d576e5ea4441ac7e15e/pydantic_core-2.27.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:206b5cf6f0c513baffaeae7bd817717140770c74528f3e4c3e1cec7871ddd61a", size = 1992972 }, + { url = "https://files.pythonhosted.org/packages/bf/ba/ae4480bc0292d54b85cfb954e9d6bd226982949f8316338677d56541b85f/pydantic_core-2.27.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:062f60e512fc7fff8b8a9d680ff0ddaaef0193dba9fa83e679c0c5f5fbd018bc", size = 2001477 }, + { url = "https://files.pythonhosted.org/packages/55/b7/e26adf48c2f943092ce54ae14c3c08d0d221ad34ce80b18a50de8ed2cba8/pydantic_core-2.27.1-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:a0697803ed7d4af5e4c1adf1670af078f8fcab7a86350e969f454daf598c4960", size = 2091186 }, + { url = "https://files.pythonhosted.org/packages/ba/cc/8491fff5b608b3862eb36e7d29d36a1af1c945463ca4c5040bf46cc73f40/pydantic_core-2.27.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:58ca98a950171f3151c603aeea9303ef6c235f692fe555e883591103da709b23", size = 2154429 }, + { url = "https://files.pythonhosted.org/packages/78/d8/c080592d80edd3441ab7f88f865f51dae94a157fc64283c680e9f32cf6da/pydantic_core-2.27.1-cp313-none-win32.whl", hash = "sha256:8065914ff79f7eab1599bd80406681f0ad08f8e47c880f17b416c9f8f7a26d05", size = 1833713 }, + { url = "https://files.pythonhosted.org/packages/83/84/5ab82a9ee2538ac95a66e51f6838d6aba6e0a03a42aa185ad2fe404a4e8f/pydantic_core-2.27.1-cp313-none-win_amd64.whl", hash = "sha256:ba630d5e3db74c79300d9a5bdaaf6200172b107f263c98a0539eeecb857b2337", size = 1987897 }, + { url = "https://files.pythonhosted.org/packages/df/c3/b15fb833926d91d982fde29c0624c9f225da743c7af801dace0d4e187e71/pydantic_core-2.27.1-cp313-none-win_arm64.whl", hash = "sha256:45cf8588c066860b623cd11c4ba687f8d7175d5f7ef65f7129df8a394c502de5", size = 1882983 }, +] + +[[package]] +name = "pydantic-settings" +version = "2.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "python-dotenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b5/d4/9dfbe238f45ad8b168f5c96ee49a3df0598ce18a0795a983b419949ce65b/pydantic_settings-2.6.1.tar.gz", hash = "sha256:e0f92546d8a9923cb8941689abf85d6601a8c19a23e97a34b2964a2e3f813ca0", size = 75646 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5e/f9/ff95fd7d760af42f647ea87f9b8a383d891cdb5e5dbd4613edaeb094252a/pydantic_settings-2.6.1-py3-none-any.whl", hash = "sha256:7fb0637c786a558d3103436278a7c4f1cfd29ba8973238a50c5bb9a55387da87", size = 28595 }, +] + +[[package]] +name = "pymongo" +version = "4.9.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "dnspython" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fb/43/d5e8993bd43e6f9cbe985e8ae1398eb73309e88694ac2ea618eacbc9cea2/pymongo-4.9.2.tar.gz", hash = "sha256:3e63535946f5df7848307b9031aa921f82bb0cbe45f9b0c3296f2173f9283eb0", size = 1889366 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a1/08/7d95aab0463dc5a2c460a0b4e50a45a743afbe20986f47f87a9a88f43c0c/pymongo-4.9.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8083bbe8cb10bb33dca4d93f8223dd8d848215250bb73867374650bac5fe69e1", size = 941617 }, + { url = "https://files.pythonhosted.org/packages/bb/28/40613d8d97fc33bf2b9187446a6746925623aa04a9a27c9b058e97076f7a/pymongo-4.9.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a1b8c636bf557c7166e3799bbf1120806ca39e3f06615b141c88d9c9ceae4d8c", size = 941394 }, + { url = "https://files.pythonhosted.org/packages/df/b2/7f1a0d75f538c0dcaa004ea69e28706fa3ca72d848e0a5a7dafd30939fff/pymongo-4.9.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8aac5dce28454f47576063fbad31ea9789bba67cab86c95788f97aafd810e65b", size = 1907396 }, + { url = "https://files.pythonhosted.org/packages/ba/70/9304bae47a361a4b12adb5be714bad41478c0e5bc3d6cf403b328d6398a0/pymongo-4.9.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d1d5e7123af1fddf15b2b53e58f20bf5242884e671bcc3860f5e954fe13aeddd", size = 1986029 }, + { url = "https://files.pythonhosted.org/packages/ae/51/ac0378d001995c4a705da64a4a2b8e1732f95de5080b752d69f452930cc7/pymongo-4.9.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fe97c847b56d61e533a7af0334193d6b28375b9189effce93129c7e4733794a9", size = 1949088 }, + { url = "https://files.pythonhosted.org/packages/1a/30/e93dc808039dc29fc47acee64f128aa650aacae3e4b57b68e01ff1001cda/pymongo-4.9.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:96ad54433a996e2d1985a9cd8fc82538ca8747c95caae2daf453600cc8c317f9", size = 1910516 }, + { url = "https://files.pythonhosted.org/packages/2b/34/895b9cad3bd5342d5ab51a853ed3a814840ce281d55c6928968e9f3f49f5/pymongo-4.9.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:98b9cade40f5b13e04492a42ae215c3721099be1014ddfe0fbd23f27e4f62c0c", size = 1860499 }, + { url = "https://files.pythonhosted.org/packages/24/7e/167818f324bf2122d45551680671a3c6406a345d3fcace4e737f57bda4e4/pymongo-4.9.2-cp312-cp312-win32.whl", hash = "sha256:dde6068ae7c62ea8ee2c5701f78c6a75618cada7e11f03893687df87709558de", size = 901282 }, + { url = "https://files.pythonhosted.org/packages/12/6b/b7ffa7114177fc1c60ae529512b82629ff7e25d19be88e97f2d0ddd16717/pymongo-4.9.2-cp312-cp312-win_amd64.whl", hash = "sha256:e1ab6cd7cd2d38ffc7ccdc79fdc166c7a91a63f844a96e3e6b2079c054391c68", size = 924925 }, + { url = "https://files.pythonhosted.org/packages/5b/d6/b57ef5f376e2e171218a98b8c30dfd001aa5cac6338aa7f3ca76e6315667/pymongo-4.9.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1ad79d6a74f439a068caf9a1e2daeabc20bf895263435484bbd49e90fbea7809", size = 995233 }, + { url = "https://files.pythonhosted.org/packages/32/80/4ec79e36e99f86a063d297a334883fb5115ad70e9af46142b8dc33f636fa/pymongo-4.9.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:877699e21703717507cbbea23e75b419f81a513b50b65531e1698df08b2d7094", size = 995025 }, + { url = "https://files.pythonhosted.org/packages/c4/fd/8f5464321fdf165700f10aec93b07a75c3537be593291ac2f8c8f5f69bd0/pymongo-4.9.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bc9322ce7cf116458a637ac10517b0c5926a8211202be6dbdc51dab4d4a9afc8", size = 2167429 }, + { url = "https://files.pythonhosted.org/packages/da/42/0f749d805d17f5b17f48f2ee1aaf2a74e67939607b87b245e5ec9b4c1452/pymongo-4.9.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cca029f46acf475504eedb33c7839f030c4bc4f946dcba12d9a954cc48850b79", size = 2258834 }, + { url = "https://files.pythonhosted.org/packages/b8/52/b0c1b8e9cbeae234dd1108a906f30b680755533b7229f9f645d7e7adad25/pymongo-4.9.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2c8c861e77527eec5a4b7363c16030dd0374670b620b08a5300f97594bbf5a40", size = 2216412 }, + { url = "https://files.pythonhosted.org/packages/4d/20/53395473a1023bb6a670b68fbfa937664c75b354c2444463075ff43523e2/pymongo-4.9.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1fc70326ae71b3c7b8d6af82f46bb71dafdba3c8f335b29382ae9cf263ef3a5c", size = 2168891 }, + { url = "https://files.pythonhosted.org/packages/01/b7/fa4030279d8a4a9c0a969a719b6b89da8a59795b5cdf129ef553fce6d1f2/pymongo-4.9.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ba9d2f6df977fee24437f82f7412460b0628cd6b961c4235c9cff71577a5b61f", size = 2109380 }, + { url = "https://files.pythonhosted.org/packages/f3/55/f252972a039fc6bfca748625c5080d6f88801eb61f118fe79cde47342d6a/pymongo-4.9.2-cp313-cp313-win32.whl", hash = "sha256:b3254769e708bc4aa634745c262081d13c841a80038eff3afd15631540a1d227", size = 946962 }, + { url = "https://files.pythonhosted.org/packages/7b/36/88d8438699ba09b714dece00a4a7462330c1d316f5eaa28db450572236f6/pymongo-4.9.2-cp313-cp313-win_amd64.whl", hash = "sha256:169b85728cc17800344ba17d736375f400ef47c9fbb4c42910c4b3e7c0247382", size = 975113 }, +] + +[[package]] +name = "python-dotenv" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bc/57/e84d88dfe0aec03b7a2d4327012c1627ab5f03652216c63d49846d7a6c58/python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca", size = 39115 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/3e/b68c118422ec867fa7ab88444e1274aa40681c606d59ac27de5a5588f082/python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a", size = 19863 }, +] + +[[package]] +name = "ruff" +version = "0.7.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4b/06/09d1276df977eece383d0ed66052fc24ec4550a61f8fbc0a11200e690496/ruff-0.7.3.tar.gz", hash = "sha256:e1d1ba2e40b6e71a61b063354d04be669ab0d39c352461f3d789cac68b54a313", size = 3243664 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/56/933d433c2489e4642487b835f53dd9ff015fb3d8fa459b09bb2ce42d7c4b/ruff-0.7.3-py3-none-linux_armv6l.whl", hash = "sha256:34f2339dc22687ec7e7002792d1f50712bf84a13d5152e75712ac08be565d344", size = 10372090 }, + { url = "https://files.pythonhosted.org/packages/20/ea/1f0a22a6bcdd3fc26c73f63a025d05bd565901b729d56bcb093c722a6c4c/ruff-0.7.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:fb397332a1879b9764a3455a0bb1087bda876c2db8aca3a3cbb67b3dbce8cda0", size = 10190037 }, + { url = "https://files.pythonhosted.org/packages/16/74/aca75666e0d481fe394e76a8647c44ea919087748024924baa1a17371e3e/ruff-0.7.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:37d0b619546103274e7f62643d14e1adcbccb242efda4e4bdb9544d7764782e9", size = 9811998 }, + { url = "https://files.pythonhosted.org/packages/20/a1/cf446a0d7f78ea1f0bd2b9171c11dfe746585c0c4a734b25966121eb4f5d/ruff-0.7.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d59f0c3ee4d1a6787614e7135b72e21024875266101142a09a61439cb6e38a5", size = 10620626 }, + { url = "https://files.pythonhosted.org/packages/cd/c1/82b27d09286ae855f5d03b1ad37cf243f21eb0081732d4d7b0d658d439cb/ruff-0.7.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:44eb93c2499a169d49fafd07bc62ac89b1bc800b197e50ff4633aed212569299", size = 10177598 }, + { url = "https://files.pythonhosted.org/packages/b9/42/c0acac22753bf74013d035a5ef6c5c4c40ad4d6686bfb3fda7c6f37d9b37/ruff-0.7.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6d0242ce53f3a576c35ee32d907475a8d569944c0407f91d207c8af5be5dae4e", size = 11171963 }, + { url = "https://files.pythonhosted.org/packages/43/18/bb0befb7fb9121dd9009e6a72eb98e24f1bacb07c6f3ecb55f032ba98aed/ruff-0.7.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:6b6224af8b5e09772c2ecb8dc9f3f344c1aa48201c7f07e7315367f6dd90ac29", size = 11856157 }, + { url = "https://files.pythonhosted.org/packages/5e/91/04e98d7d6e32eca9d1372be595f9abc7b7f048795e32eb2edbd8794d50bd/ruff-0.7.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c50f95a82b94421c964fae4c27c0242890a20fe67d203d127e84fbb8013855f5", size = 11440331 }, + { url = "https://files.pythonhosted.org/packages/f5/dc/3fe99f2ce10b76d389041a1b9f99e7066332e479435d4bebcceea16caff5/ruff-0.7.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7f3eff9961b5d2644bcf1616c606e93baa2d6b349e8aa8b035f654df252c8c67", size = 12725354 }, + { url = "https://files.pythonhosted.org/packages/43/7b/1daa712de1c5bc6cbbf9fa60e9c41cc48cda962dc6d2c4f2a224d2c3007e/ruff-0.7.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8963cab06d130c4df2fd52c84e9f10d297826d2e8169ae0c798b6221be1d1d2", size = 11010091 }, + { url = "https://files.pythonhosted.org/packages/b6/db/1227a903587432eb569e57a95b15a4f191a71fe315cde4c0312df7bc85da/ruff-0.7.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:61b46049d6edc0e4317fb14b33bd693245281a3007288b68a3f5b74a22a0746d", size = 10610687 }, + { url = "https://files.pythonhosted.org/packages/db/e2/dc41ee90c3085aadad4da614d310d834f641aaafddf3dfbba08210c616ce/ruff-0.7.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:10ebce7696afe4644e8c1a23b3cf8c0f2193a310c18387c06e583ae9ef284de2", size = 10254843 }, + { url = "https://files.pythonhosted.org/packages/6f/09/5f6cac1c91542bc5bd33d40b4c13b637bf64d7bb29e091dadb01b62527fe/ruff-0.7.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:3f36d56326b3aef8eeee150b700e519880d1aab92f471eefdef656fd57492aa2", size = 10730962 }, + { url = "https://files.pythonhosted.org/packages/d3/42/89a4b9a24ef7d00269e24086c417a006f9a3ffeac2c80f2629eb5ce140ee/ruff-0.7.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5d024301109a0007b78d57ab0ba190087b43dce852e552734ebf0b0b85e4fb16", size = 11101907 }, + { url = "https://files.pythonhosted.org/packages/b0/5c/efdb4777686683a8edce94ffd812783bddcd3d2454d38c5ac193fef7c500/ruff-0.7.3-py3-none-win32.whl", hash = "sha256:4ba81a5f0c5478aa61674c5a2194de8b02652f17addf8dfc40c8937e6e7d79fc", size = 8611095 }, + { url = "https://files.pythonhosted.org/packages/bb/b8/28fbc6a4efa50178f973972d1c84b2d0a33cdc731588522ab751ac3da2f5/ruff-0.7.3-py3-none-win_amd64.whl", hash = "sha256:588a9ff2fecf01025ed065fe28809cd5a53b43505f48b69a1ac7707b1b7e4088", size = 9418283 }, + { url = "https://files.pythonhosted.org/packages/3f/77/b587cba6febd5e2003374f37eb89633f79f161e71084f94057c8653b7fb3/ruff-0.7.3-py3-none-win_arm64.whl", hash = "sha256:1713e2c5545863cdbfe2cbce21f69ffaf37b813bfd1fb3b90dc9a6f1963f5a8c", size = 8725228 }, +] + +[[package]] +name = "sentry-sdk" +version = "2.19.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a2/0e/cc0e60f0e0cfd5a9e42622ff5a227301c6475a56bcfa82e8e893bc209f20/sentry_sdk-2.19.0.tar.gz", hash = "sha256:ee4a4d2ae8bfe3cac012dcf3e4607975904c137e1738116549fc3dbbb6ff0e36", size = 298045 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/6b/191ca63f05d3ecc7600b5b3abd493a4c1b8468289c9737a7735ade1fedca/sentry_sdk-2.19.0-py2.py3-none-any.whl", hash = "sha256:7b0b3b709dee051337244a09a30dbf6e95afe0d34a1f8b430d45e0982a7c125b", size = 322158 }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, +] + +[[package]] +name = "soupsieve" +version = "2.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/ce/fbaeed4f9fb8b2daa961f90591662df6a86c1abf25c548329a86920aedfb/soupsieve-2.6.tar.gz", hash = "sha256:e2e68417777af359ec65daac1057404a3c8a5455bb8abc36f1a9866ab1a51abb", size = 101569 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/c2/fe97d779f3ef3b15f05c94a2f1e3d21732574ed441687474db9d342a7315/soupsieve-2.6-py3-none-any.whl", hash = "sha256:e72c4ff06e4fb6e4b5a9f0f55fe6e81514581fca1515028625d0f299c602ccc9", size = 36186 }, +] + +[[package]] +name = "typing-extensions" +version = "4.12.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 }, +] + +[[package]] +name = "urllib3" +version = "2.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ed/63/22ba4ebfe7430b76388e7cd448d5478814d3032121827c12a2cc287e2260/urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9", size = 300677 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/d9/5f4c13cecde62396b0d3fe530a50ccea91e7dfc1ccf0e09c228841bb5ba8/urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac", size = 126338 }, +] + +[[package]] +name = "yarl" +version = "1.17.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "multidict" }, + { name = "propcache" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/54/9c/9c0a9bfa683fc1be7fdcd9687635151544d992cccd48892dc5e0a5885a29/yarl-1.17.1.tar.gz", hash = "sha256:067a63fcfda82da6b198fa73079b1ca40b7c9b7994995b6ee38acda728b64d47", size = 178163 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/af/e25615c7920396219b943b9ff8b34636ae3e1ad30777649371317d7f05f8/yarl-1.17.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:327828786da2006085a4d1feb2594de6f6d26f8af48b81eb1ae950c788d97f61", size = 141839 }, + { url = "https://files.pythonhosted.org/packages/83/5e/363d9de3495c7c66592523f05d21576a811015579e0c87dd38c7b5788afd/yarl-1.17.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cc353841428d56b683a123a813e6a686e07026d6b1c5757970a877195f880c2d", size = 94125 }, + { url = "https://files.pythonhosted.org/packages/e3/a2/b65447626227ebe36f18f63ac551790068bf42c69bb22dfa3ae986170728/yarl-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c73df5b6e8fabe2ddb74876fb82d9dd44cbace0ca12e8861ce9155ad3c886139", size = 92048 }, + { url = "https://files.pythonhosted.org/packages/a1/f5/2ef86458446f85cde10582054fd5113495ef8ce8477da35aaaf26d2970ef/yarl-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0bdff5e0995522706c53078f531fb586f56de9c4c81c243865dd5c66c132c3b5", size = 331472 }, + { url = "https://files.pythonhosted.org/packages/f3/6b/1ba79758ba352cdf2ad4c20cab1b982dd369aa595bb0d7601fc89bf82bee/yarl-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:06157fb3c58f2736a5e47c8fcbe1afc8b5de6fb28b14d25574af9e62150fcaac", size = 341260 }, + { url = "https://files.pythonhosted.org/packages/2d/41/4e07c2afca3f9ed3da5b0e38d43d0280d9b624a3d5c478c425e5ce17775c/yarl-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1654ec814b18be1af2c857aa9000de7a601400bd4c9ca24629b18486c2e35463", size = 340882 }, + { url = "https://files.pythonhosted.org/packages/c3/c0/cd8e94618983c1b811af082e1a7ad7764edb3a6af2bc6b468e0e686238ba/yarl-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f6595c852ca544aaeeb32d357e62c9c780eac69dcd34e40cae7b55bc4fb1147", size = 336648 }, + { url = "https://files.pythonhosted.org/packages/ac/fc/73ec4340d391ffbb8f34eb4c55429784ec9f5bd37973ce86d52d67135418/yarl-1.17.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:459e81c2fb920b5f5df744262d1498ec2c8081acdcfe18181da44c50f51312f7", size = 325019 }, + { url = "https://files.pythonhosted.org/packages/57/48/da3ebf418fc239d0a156b3bdec6b17a5446f8d2dea752299c6e47b143a85/yarl-1.17.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7e48cdb8226644e2fbd0bdb0a0f87906a3db07087f4de77a1b1b1ccfd9e93685", size = 342841 }, + { url = "https://files.pythonhosted.org/packages/5d/79/107272745a470a8167924e353a5312eb52b5a9bb58e22686adc46c94f7ec/yarl-1.17.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:d9b6b28a57feb51605d6ae5e61a9044a31742db557a3b851a74c13bc61de5172", size = 341433 }, + { url = "https://files.pythonhosted.org/packages/30/9c/6459668b3b8dcc11cd061fc53e12737e740fb6b1575b49c84cbffb387b3a/yarl-1.17.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e594b22688d5747b06e957f1ef822060cb5cb35b493066e33ceac0cf882188b7", size = 344927 }, + { url = "https://files.pythonhosted.org/packages/c5/0b/93a17ed733aca8164fc3a01cb7d47b3f08854ce4f957cce67a6afdb388a0/yarl-1.17.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5f236cb5999ccd23a0ab1bd219cfe0ee3e1c1b65aaf6dd3320e972f7ec3a39da", size = 355732 }, + { url = "https://files.pythonhosted.org/packages/9a/63/ead2ed6aec3c59397e135cadc66572330325a0c24cd353cd5c94f5e63463/yarl-1.17.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:a2a64e62c7a0edd07c1c917b0586655f3362d2c2d37d474db1a509efb96fea1c", size = 362123 }, + { url = "https://files.pythonhosted.org/packages/89/bf/f6b75b4c2fcf0e7bb56edc0ed74e33f37fac45dc40e5a52a3be66b02587a/yarl-1.17.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d0eea830b591dbc68e030c86a9569826145df485b2b4554874b07fea1275a199", size = 356355 }, + { url = "https://files.pythonhosted.org/packages/45/1f/50a0257cd07eef65c8c65ad6a21f5fb230012d659e021aeb6ac8a7897bf6/yarl-1.17.1-cp312-cp312-win32.whl", hash = "sha256:46ddf6e0b975cd680eb83318aa1d321cb2bf8d288d50f1754526230fcf59ba96", size = 83279 }, + { url = "https://files.pythonhosted.org/packages/bc/82/fafb2c1268d63d54ec08b3a254fbe51f4ef098211501df646026717abee3/yarl-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:117ed8b3732528a1e41af3aa6d4e08483c2f0f2e3d3d7dca7cf538b3516d93df", size = 89590 }, + { url = "https://files.pythonhosted.org/packages/06/1e/5a93e3743c20eefbc68bd89334d9c9f04f3f2334380f7bbf5e950f29511b/yarl-1.17.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:5d1d42556b063d579cae59e37a38c61f4402b47d70c29f0ef15cee1acaa64488", size = 139974 }, + { url = "https://files.pythonhosted.org/packages/a1/be/4e0f6919013c7c5eaea5c31811c551ccd599d2fc80aa3dd6962f1bbdcddd/yarl-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c0167540094838ee9093ef6cc2c69d0074bbf84a432b4995835e8e5a0d984374", size = 93364 }, + { url = "https://files.pythonhosted.org/packages/73/f0/650f994bc491d0cb85df8bb45392780b90eab1e175f103a5edc61445ff67/yarl-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2f0a6423295a0d282d00e8701fe763eeefba8037e984ad5de44aa349002562ac", size = 91177 }, + { url = "https://files.pythonhosted.org/packages/f3/e8/9945ed555d14b43ede3ae8b1bd73e31068a694cad2b9d3cad0a28486c2eb/yarl-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e5b078134f48552c4d9527db2f7da0b5359abd49393cdf9794017baec7506170", size = 333086 }, + { url = "https://files.pythonhosted.org/packages/a6/c0/7d167e48e14d26639ca066825af8da7df1d2fcdba827e3fd6341aaf22a3b/yarl-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d401f07261dc5aa36c2e4efc308548f6ae943bfff20fcadb0a07517a26b196d8", size = 343661 }, + { url = "https://files.pythonhosted.org/packages/fa/81/80a266517531d4e3553aecd141800dbf48d02e23ebd52909e63598a80134/yarl-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b5f1ac7359e17efe0b6e5fec21de34145caef22b260e978336f325d5c84e6938", size = 345196 }, + { url = "https://files.pythonhosted.org/packages/b0/77/6adc482ba7f2dc6c0d9b3b492e7cd100edfac4cfc3849c7ffa26fd7beb1a/yarl-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f63d176a81555984e91f2c84c2a574a61cab7111cc907e176f0f01538e9ff6e", size = 338743 }, + { url = "https://files.pythonhosted.org/packages/6d/cc/f0c4c0b92ff3ada517ffde2b127406c001504b225692216d969879ada89a/yarl-1.17.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9e275792097c9f7e80741c36de3b61917aebecc08a67ae62899b074566ff8556", size = 326719 }, + { url = "https://files.pythonhosted.org/packages/18/3b/7bfc80d3376b5fa162189993a87a5a6a58057f88315bd0ea00610055b57a/yarl-1.17.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:81713b70bea5c1386dc2f32a8f0dab4148a2928c7495c808c541ee0aae614d67", size = 345826 }, + { url = "https://files.pythonhosted.org/packages/2e/66/cf0b0338107a5c370205c1a572432af08f36ca12ecce127f5b558398b4fd/yarl-1.17.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:aa46dce75078fceaf7cecac5817422febb4355fbdda440db55206e3bd288cfb8", size = 340335 }, + { url = "https://files.pythonhosted.org/packages/2f/52/b084b0eec0fd4d2490e1d33ace3320fad704c5f1f3deaa709f929d2d87fc/yarl-1.17.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1ce36ded585f45b1e9bb36d0ae94765c6608b43bd2e7f5f88079f7a85c61a4d3", size = 345301 }, + { url = "https://files.pythonhosted.org/packages/ef/38/9e2036d948efd3bafcdb4976cb212166fded76615f0dfc6c1492c4ce4784/yarl-1.17.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:2d374d70fdc36f5863b84e54775452f68639bc862918602d028f89310a034ab0", size = 354205 }, + { url = "https://files.pythonhosted.org/packages/81/c1/13dfe1e70b86811733316221c696580725ceb1c46d4e4db852807e134310/yarl-1.17.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:2d9f0606baaec5dd54cb99667fcf85183a7477f3766fbddbe3f385e7fc253299", size = 360501 }, + { url = "https://files.pythonhosted.org/packages/91/87/756e05c74cd8bf9e71537df4a2cae7e8211a9ebe0d2350a3e26949e1e41c/yarl-1.17.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b0341e6d9a0c0e3cdc65857ef518bb05b410dbd70d749a0d33ac0f39e81a4258", size = 359452 }, + { url = "https://files.pythonhosted.org/packages/06/b2/b2bb09c1e6d59e1c9b1b36a86caa473e22c3dbf26d1032c030e9bfb554dc/yarl-1.17.1-cp313-cp313-win32.whl", hash = "sha256:2e7ba4c9377e48fb7b20dedbd473cbcbc13e72e1826917c185157a137dac9df2", size = 308904 }, + { url = "https://files.pythonhosted.org/packages/f3/27/f084d9a5668853c1f3b246620269b14ee871ef3c3cc4f3a1dd53645b68ec/yarl-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:949681f68e0e3c25377462be4b658500e85ca24323d9619fdc41f68d46a1ffda", size = 314637 }, + { url = "https://files.pythonhosted.org/packages/52/ad/1fe7ff5f3e8869d4c5070f47b96bac2b4d15e67c100a8278d8e7876329fc/yarl-1.17.1-py3-none-any.whl", hash = "sha256:f1790a4b1e8e8e028c391175433b9c8122c39b46e1663228158e61e6f915bf06", size = 44352 }, +]