diff --git a/jarvis/__init__.py b/jarvis/__init__.py index 2b0804c..703bc1a 100644 --- a/jarvis/__init__.py +++ b/jarvis/__init__.py @@ -1,42 +1,53 @@ """Main JARVIS package.""" import logging -from importlib.metadata import version as _v -from dis_snek import Intents +import aioredis +import jurigged +import rook from jarvis_core.db import connect from jarvis_core.log import get_logger +from naff import Intents -from jarvis import utils +from jarvis import const from jarvis.client import Jarvis +from jarvis.cogs import __path__ as cogs_path from jarvis.config import JarvisConfig +from jarvis.utils import get_extensions -try: - __version__ = _v("jarvis") -except Exception: - __version__ = "0.0.0" - -jconfig = JarvisConfig.from_yaml() -logger = get_logger("jarvis") -logger.setLevel(jconfig.log_level) -file_handler = logging.FileHandler(filename="jarvis.log", encoding="UTF-8", mode="w") -file_handler.setFormatter( - logging.Formatter("[%(asctime)s] [%(name)s] [%(levelname)8s] %(message)s") -) -logger.addHandler(file_handler) - -intents = Intents.DEFAULT | Intents.MESSAGES | Intents.GUILD_MEMBERS | Intents.GUILD_MESSAGES -restart_ctx = None - -jarvis = Jarvis( - intents=intents, - sync_interactions=jconfig.sync, - delete_unused_application_cmds=True, - send_command_tracebacks=False, -) +__version__ = const.__version__ async def run() -> None: """Run JARVIS""" + jconfig = JarvisConfig.from_yaml() + logger = get_logger("jarvis", show_locals=jconfig.log_level == "DEBUG") + logger.setLevel(jconfig.log_level) + file_handler = logging.FileHandler(filename="jarvis.log", encoding="UTF-8", mode="w") + file_handler.setFormatter( + logging.Formatter("[%(asctime)s] [%(name)s] [%(levelname)8s] %(message)s") + ) + logger.addHandler(file_handler) + + intents = ( + Intents.DEFAULT | Intents.MESSAGES | Intents.GUILD_MEMBERS | Intents.GUILD_MESSAGE_CONTENT + ) + redis_config = jconfig.redis.copy() + redis_host = redis_config.pop("host") + + redis = await aioredis.from_url(redis_host, decode_responses=True, **redis_config) + + jarvis = Jarvis( + intents=intents, + sync_interactions=jconfig.sync, + delete_unused_application_cmds=True, + send_command_tracebacks=False, + redis=redis, + ) + + if jconfig.log_level == "DEBUG": + jurigged.watch() + if jconfig.rook_token: + rook.start(token=jconfig.rook_token, labels={"env": "dev"}) logger.info("Starting JARVIS") logger.debug("Connecting to database") connect(**jconfig.mongo["connect"], testing=jconfig.mongo["database"] != "jarvis") @@ -44,9 +55,9 @@ async def run() -> None: # jconfig.get_db_config() logger.debug("Loading extensions") - for extension in utils.get_extensions(): + for extension in get_extensions(cogs_path): jarvis.load_extension(extension) - logger.debug(f"Loaded {extension}") + logger.debug("Loaded %s", extension) jarvis.max_messages = jconfig.max_messages logger.debug("Running JARVIS") diff --git a/jarvis/client.py b/jarvis/client.py index f054764..65065a8 100644 --- a/jarvis/client.py +++ b/jarvis/client.py @@ -1,34 +1,48 @@ """Custom JARVIS client.""" +import asyncio import logging import re import traceback -from datetime import datetime, timezone +from datetime import datetime, timedelta, timezone from aiohttp import ClientSession -from dis_snek import Snake, listen -from dis_snek.api.events.discord import ( +from jarvis_core.db import q +from jarvis_core.db.models import ( + Action, + Autopurge, + Autoreact, + Modlog, + Note, + Roleping, + Setting, + Warning, +) +from jarvis_core.filters import invites, url +from jarvis_core.util.ansi import RESET, Fore, Format, fmt +from naff import Client, listen +from naff.api.events.discord import ( MemberAdd, MemberRemove, + MemberUpdate, MessageCreate, MessageDelete, MessageUpdate, ) -from dis_snek.client.errors import CommandCheckFailure, CommandOnCooldown -from dis_snek.client.utils.misc_utils import find_all -from dis_snek.models.discord.channel import DMChannel -from dis_snek.models.discord.embed import EmbedField -from dis_snek.models.discord.enums import Permissions -from dis_snek.models.discord.message import Message -from dis_snek.models.snek.context import Context, InteractionContext, MessageContext -from dis_snek.models.snek.tasks.task import Task -from dis_snek.models.snek.tasks.triggers import IntervalTrigger -from jarvis_core.db import q -from jarvis_core.db.models import Autopurge, Autoreact, Roleping, Setting, Warning -from jarvis_core.filters import invites, url -from jarvis_core.util import build_embed -from jarvis_core.util.ansi import RESET, Fore, Format, fmt +from naff.api.events.internal import Button +from naff.client.errors import CommandCheckFailure, CommandOnCooldown, HTTPException +from naff.client.utils.misc_utils import find_all, get +from naff.models.discord.channel import DMChannel +from naff.models.discord.embed import Embed, EmbedField +from naff.models.discord.enums import AuditLogEventType, Permissions +from naff.models.discord.message import Message +from naff.models.discord.user import Member +from naff.models.naff.context import Context, InteractionContext, PrefixedContext +from naff.models.naff.tasks.task import Task +from naff.models.naff.tasks.triggers import IntervalTrigger from pastypy import AsyncPaste as Paste +from jarvis import const +from jarvis.utils import build_embed from jarvis.utils.embeds import warning_embed DEFAULT_GUILD = 862402786116763668 @@ -54,9 +68,11 @@ VAL_FMT = fmt(Fore.WHITE) CMD_FMT = fmt(Fore.GREEN, Format.BOLD) -class Jarvis(Snake): +class Jarvis(Client): def __init__(self, *args, **kwargs): # noqa: ANN002 ANN003 + redis = kwargs.pop("redis") super().__init__(*args, **kwargs) + self.redis = redis self.logger = logging.getLogger(__name__) self.phishing_domains = [] self.pre_run_callback = self._prerun @@ -71,19 +87,29 @@ class Jarvis(Snake): self.logger.debug(f"Found {len(data)} changes to phishing domains") + add = 0 + sub = 0 + for update in data: if update["type"] == "add": - if update["domain"] not in self.phishing_domains: - self.phishing_domains.append(update["domain"]) + for domain in update["domains"]: + if domain not in self.phishing_domains: + add += 1 + self.phishing_domains.append(domain) elif update["type"] == "delete": - if update["domain"] in self.phishing_domains: - self.phishing_domains.remove(update["domain"]) + for domain in update["domains"]: + if domain in self.phishing_domains: + sub -= 1 + self.phishing_domains.remove(domain) + self.logger.debug(f"{add} additions, {sub} removals") async def _prerun(self, ctx: Context, *args, **kwargs) -> None: - name = ctx.invoked_name + name = ctx.invoke_target if isinstance(ctx, InteractionContext) and ctx.target_id: kwargs["context target"] = ctx.target - args = " ".join(f"{k}:{v}" for k, v in kwargs.items()) + args = " ".join(f"{k}:{v}" for k, v in kwargs.items()) + elif isinstance(ctx, PrefixedContext): + args = " ".join(args) self.logger.debug(f"Running command `{name}` with args: {args or 'None'}") async def _sync_domains(self) -> None: @@ -96,21 +122,35 @@ class Jarvis(Snake): @listen() async def on_ready(self) -> None: - """Lepton on_ready override.""" - await self._sync_domains() - self._update_domains.start() + """NAFF on_ready override.""" + try: + await self._sync_domains() + self._update_domains.start() + except Exception as e: + self.logger.error("Failed to load anti-phishing", exc_info=e) self.logger.info("Logged in as {}".format(self.user)) # noqa: T001 self.logger.info("Connected to {} guild(s)".format(len(self.guilds))) # noqa: T001 + self.logger.info("Current version: {}".format(const.__version__)) self.logger.info( # noqa: T001 "https://discord.com/api/oauth2/authorize?client_id=" "{}&permissions=8&scope=bot%20applications.commands".format(self.user.id) ) + async def on_error(self, source: str, error: Exception, *args, **kwargs) -> None: + """NAFF on_error override.""" + if isinstance(error, HTTPException): + errors = error.search_for_message(error.errors) + out = f"HTTPException: {error.status}|{error.response.reason}: " + "\n".join(errors) + self.logger.error(out, exc_info=error) + else: + self.logger.error(f"Ignoring exception in {source}", exc_info=error) + async def on_command_error( self, ctx: Context, error: Exception, *args: list, **kwargs: dict ) -> None: - """Lepton on_command_error override.""" - self.logger.debug(f"Handling error in {ctx.invoked_name}: {error}") + """NAFF on_command_error override.""" + name = ctx.invoke_target + self.logger.debug(f"Handling error in {name}: {error}") if isinstance(error, CommandOnCooldown): await ctx.send(str(error), ephemeral=True) return @@ -131,7 +171,7 @@ class Jarvis(Snake): if isinstance(v, str) and len(v) > 100: v = v[97] + "..." arg_str += f"{v}\n" - elif isinstance(ctx, MessageContext): + elif isinstance(ctx, PrefixedContext): for v in ctx.args: if isinstance(v, str) and len(v) > 100: v = v[97] + "..." @@ -143,11 +183,15 @@ class Jarvis(Snake): full_message = ERROR_MSG.format( guild_name=ctx.guild.name, error_time=error_time, - invoked_name=ctx.invoked_name, + invoked_name=name, arg_str=arg_str, callback_args=callback_args, callback_kwargs=callback_kwargs, ) + tb = traceback.format_exception(error) + if isinstance(error, HTTPException): + errors = error.search_for_message(error.errors) + tb[-1] = f"HTTPException: {error.status}|{error.response.reason}: " + "\n".join(errors) error_message = "".join(traceback.format_exception(error)) if len(full_message + error_message) >= 1800: error_message = "\n ".join(error_message.split("\n")) @@ -167,12 +211,16 @@ class Jarvis(Snake): f"\nException:\n```py\n{error_message}\n```" ) await ctx.send("Whoops! Encountered an error. The error has been logged.", ephemeral=True) - return await super().on_command_error(ctx, error, *args, **kwargs) + try: + return await super().on_command_error(ctx, error, *args, **kwargs) + except Exception as e: + self.logger.error("Uncaught exception", exc_info=e) # Modlog async def on_command(self, ctx: Context) -> None: - """Lepton on_command override.""" - if not isinstance(ctx.channel, DMChannel) and ctx.invoked_name not in ["pw"]: + """NAFF on_command override.""" + name = ctx.invoke_target + if not isinstance(ctx.channel, DMChannel) and name not in ["pw"]: modlog = await Setting.find_one(q(guild=ctx.guild.id, setting="activitylog")) if modlog: channel = await ctx.guild.fetch_channel(modlog.value) @@ -186,7 +234,7 @@ class Jarvis(Snake): if len(v) > 100: v = v[:97] + "..." args.append(f"{KEY_FMT}{k}:{VAL_FMT}{v}{RESET}") - elif isinstance(ctx, MessageContext): + elif isinstance(ctx, PrefixedContext): for v in ctx.args: if isinstance(v, str) and len(v) > 100: v = v[97] + "..." @@ -195,7 +243,7 @@ class Jarvis(Snake): fields = [ EmbedField( name="Command", - value=f"```ansi\n{CMD_FMT}{ctx.invoked_name}{RESET} {args}\n```", + value=f"```ansi\n{CMD_FMT}{ctx.invoke_target}{RESET} {args}\n```", inline=False, ), ] @@ -242,13 +290,131 @@ class Jarvis(Snake): channel = await guild.fetch_channel(log.channel) embed = build_embed( title="Member Left", - desciption=f"{user.username}#{user.discriminator} left {guild.name}", + description=f"{user.username}#{user.discriminator} left {guild.name}", fields=[], ) embed.set_author(name=user.username, icon_url=user.avatar.url) embed.set_footer(text=f"{user.username}#{user.discriminator} | {user.id}") await channel.send(embed=embed) + async def process_verify(self, before: Member, after: Member) -> Embed: + """Process user verification.""" + auditlog = await after.guild.fetch_audit_log( + user_id=before.id, action_type=AuditLogEventType.MEMBER_ROLE_UPDATE + ) + audit_event = get(auditlog.events, reason="Verification passed") + if audit_event: + admin_mention = "[N/A]" + admin_text = "[N/A]" + if admin := await after.guild.fet_member(audit_event.user_id): + admin_mention = admin.mention + admin_text = f"{admin.username}#{admin.discriminator}" + fields = ( + EmbedField(name="Moderator", value=f"{admin_mention} ({admin_text})"), + EmbedField(name="Reason", value=audit_event.reason), + ) + embed = build_embed( + title="User Verified", + description=f"{after.mention} was verified", + fields=fields, + color="#fc9e3f", + ) + embed.set_author(name=after.display_name, icon_url=after.display_avatar.url) + embed.set_footer(text=f"{after.username}#{after.discriminator} | {after.id}") + return embed + + async def process_rolechange(self, before: Member, after: Member) -> Embed: + """Process role changes.""" + if before.roles == after.roles: + return + + new_roles = [] + removed_roles = [] + + for role in before.roles: + if role not in after.roles: + removed_roles.append(role) + for role in after.roles: + if role not in before.roles: + new_roles.append(role) + + new_text = "\n".join(role.mention for role in new_roles) or "None" + removed_text = "\n".join(role.mention for role in removed_roles) or "None" + + fields = ( + EmbedField(name="Added Roles", value=new_text), + EmbedField(name="Removed Roles", value=removed_text), + ) + embed = build_embed( + title="User Roles Changed", + description=f"{after.mention} had roles changed", + fields=fields, + color="#fc9e3f", + ) + embed.set_author(name=after.display_name, icon_url=after.display_avatar.url) + embed.set_footer(text=f"{after.username}#{after.discriminator} | {after.id}") + return embed + + async def process_rename(self, before: Member, after: Member) -> None: + """Process name change.""" + if ( + before.nickname == after.nickname + and before.discriminator == after.discriminator + and before.username == after.username + ): + return + + fields = ( + EmbedField( + name="Before", + value=f"{before.display_name} ({before.username}#{before.discriminator})", + ), + EmbedField( + name="After", value=f"{after.display_name} ({after.username}#{after.discriminator})" + ), + ) + embed = build_embed( + title="User Renamed", + description=f"{after.mention} changed their name", + fields=fields, + color="#fc9e3f", + ) + embed.set_author(name=after.display_name, icon_url=after.display_avatar.url) + embed.set_footer(text=f"{after.username}#{after.discriminator} | {after.id}") + return embed + + @listen() + async def on_member_update(self, event: MemberUpdate) -> None: + """Handle on_member_update event.""" + before = event.before + after = event.after + + if (before.display_name == after.display_name and before.roles == after.roles) or ( + not after or not before + ): + return + + log = await Setting.find_one(q(guild=before.guild.id, setting="activitylog")) + if log: + channel = await before.guild.fetch_channel(log.value) + await asyncio.sleep(0.5) # Wait for audit log + embed = None + if before._role_ids != after._role_ids: + verified = await Setting.find_one(q(guild=before.guild.id, setting="verified")) + v_role = None + if verified: + v_role = await before.guild.fetch_role(verified.value) + if not v_role: + self.logger.debug(f"Guild {before.guild.id} verified role no longer exists") + await verified.delete() + else: + if not before.has_role(v_role) and after.has_role(v_role): + embed = await self.process_verify(before, after) + embed = embed or await self.process_rolechange(before, after) + embed = embed or await self.process_rename(before, after) + if embed: + await channel.send(embed=embed) + # Message async def autopurge(self, message: Message) -> None: """Handle autopurge events.""" @@ -308,10 +474,13 @@ class Jarvis(Snake): await message.delete() except Exception: self.logger.debug("Message deleted before action taken") + + expires_at = datetime.now(tz=timezone.utc) + timedelta(hours=24) await Warning( active=True, admin=self.user.id, duration=24, + expires_at=expires_at, guild=message.guild.id, reason="Sent an invite link", user=message.author.id, @@ -338,10 +507,12 @@ class Jarvis(Snake): self.logger.debug( f"Massmention threshold on {message.guild.id}/{message.channel.id}/{message.id}" ) + expires_at = datetime.now(tz=timezone.utc) + timedelta(hours=24) await Warning( active=True, admin=self.user.id, duration=24, + expires_at=expires_at, guild=message.guild.id, reason="Mass Mention", user=message.author.id, @@ -351,19 +522,19 @@ class Jarvis(Snake): async def roleping(self, message: Message) -> None: """Handle roleping events.""" - if message.author.has_permission(Permissions.MANAGE_GUILD): - return + try: + if message.author.has_permission(Permissions.MANAGE_GUILD): + return + except Exception as e: + self.logger.error("Failed to get permissions, pretending check failed", exc_info=e) if await Roleping.collection.count_documents(q(guild=message.guild.id, active=True)) == 0: return rolepings = await Roleping.find(q(guild=message.guild.id, active=True)).to_list(None) # Get all role IDs involved with message - roles = [] - async for mention in message.mention_roles: - roles.append(mention.id) + roles = [x.id async for x in message.mention_roles] async for mention in message.mention_users: - for role in mention.roles: - roles.append(role.id) + roles += [x.id for x in mention.roles] if not roles: return @@ -381,12 +552,15 @@ class Jarvis(Snake): user_is_admin = message.author.has_permission(Permissions.ADMINISTRATOR) # Check if user in a bypass list + def check_has_role(roleping: Roleping) -> bool: + return any(role.id in roleping.bypass["roles"] for role in message.author.roles) + user_has_bypass = False for roleping in rolepings: if message.author.id in roleping.bypass["users"]: user_has_bypass = True break - if any(role.id in roleping.bypass["roles"] for role in message.author.roles): + if check_has_role(roleping): user_has_bypass = True break @@ -394,10 +568,12 @@ class Jarvis(Snake): self.logger.debug( f"Rolepinged role in {message.guild.id}/{message.channel.id}/{message.id}" ) + expires_at = datetime.now(tz=timezone.utc) + timedelta(hours=24) await Warning( active=True, admin=self.user.id, duration=24, + expires_at=expires_at, guild=message.guild.id, reason="Pinged a blocked role/user with a blocked role", user=message.author.id, @@ -412,10 +588,12 @@ class Jarvis(Snake): self.logger.debug( f"Phishing url `{m}` detected in {message.guild.id}/{message.channel.id}/{message.id}" ) + expires_at = datetime.now(tz=timezone.utc) + timedelta(hours=24) await Warning( active=True, admin=self.user.id, duration=24, + expires_at=expires_at, guild=message.guild.id, reason="Phishing URL", user=message.author.id, @@ -441,10 +619,12 @@ class Jarvis(Snake): self.logger.debug( f"Scam url `{match.string}` detected in {message.guild.id}/{message.channel.id}/{message.id}" ) + expires_at = datetime.now(tz=timezone.utc) + timedelta(hours=24) await Warning( active=True, admin=self.user.id, duration=24, + expires_at=expires_at, guild=message.guild.id, reason="Unsafe URL", user=message.author.id, @@ -511,7 +691,7 @@ class Jarvis(Snake): ) await channel.send(embed=embed) except Exception as e: - self.logger.warn( + self.logger.warning( f"Failed to process edit {before.guild.id}/{before.channel.id}/{before.id}: {e}" ) if not isinstance(after.channel, DMChannel) and not after.author.bot: @@ -587,6 +767,72 @@ class Jarvis(Snake): ) await channel.send(embed=embed) except Exception as e: - self.logger.warn( + self.logger.warning( f"Failed to process edit {message.guild.id}/{message.channel.id}/{message.id}: {e}" ) + + @listen() + async def on_button(self, event: Button) -> None: + """Process button events.""" + context = event.context + if not context.deferred and not context.responded: + await context.defer(ephemeral=True) + if not context.custom_id.startswith("modcase|"): + return await super().on_button(event) + + if not context.author.has_permission(Permissions.MODERATE_MEMBERS): + return + + user_key = f"msg|{context.message.id}" + action_key = "" + + if context.custom_id == "modcase|yes": + if user_id := await self.redis.get(user_key): + action_key = f"{user_id}|{context.guild.id}" + if (user := await context.guild.fetch_member(user_id)) and ( + action_data := await self.redis.get(action_key) + ): + name, parent = action_data.split("|")[:2] + action = Action(action_type=name, parent=parent) + note = Note( + admin=context.author.id, content="Moderation case opened via message" + ) + modlog = Modlog( + user=user.id, + admin=context.author.id, + guild=context.guild.id, + actions=[action], + notes=[note], + ) + await modlog.commit() + + fields = ( + EmbedField(name="Admin", value=context.author.mention), + EmbedField(name="Opening Action", value=f"{name} {parent}"), + ) + embed = build_embed( + title="Moderation Case Opened", + description=f"Moderation case opened against {user.mention}", + fields=fields, + ) + embed.set_author( + name=user.username + "#" + user.discriminator, + icon_url=user.display_avatar.url, + ) + + await context.message.edit(embed=embed) + elif not user: + self.logger.debug("User no longer in guild") + await context.send("User no longer in guild", ephemeral=True) + else: + self.logger.warn("Unable to get action data ( %s )", action_key) + await context.send("Unable to get action data", ephemeral=True) + + for row in context.message.components: + for component in row.components: + component.disabled = True + await context.message.edit(components=context.message.components) + msg = "Cancelled" if context.custom_id == "modcase|no" else "Moderation case opened" + await context.send(msg) + await self.redis.delete(user_key) + await self.redis.delete(action_key) diff --git a/jarvis/cogs/admin/__init__.py b/jarvis/cogs/admin/__init__.py index f0fb1af..e524d89 100644 --- a/jarvis/cogs/admin/__init__.py +++ b/jarvis/cogs/admin/__init__.py @@ -1,12 +1,22 @@ """JARVIS Admin Cogs.""" import logging -from dis_snek import Snake +from naff import Client -from jarvis.cogs.admin import ban, kick, lock, lockdown, mute, purge, roleping, warning +from jarvis.cogs.admin import ( + ban, + kick, + lock, + lockdown, + modcase, + mute, + purge, + roleping, + warning, +) -def setup(bot: Snake) -> None: +def setup(bot: Client) -> None: """Add admin cogs to JARVIS""" logger = logging.getLogger(__name__) msg = "Loaded jarvis.cogs.admin.{}" @@ -17,7 +27,9 @@ def setup(bot: Snake) -> None: lock.LockCog(bot) logger.debug(msg.format("lock")) lockdown.LockdownCog(bot) - logger.debug(msg.format("ban")) + logger.debug(msg.format("lockdown")) + modcase.CaseCog(bot) + logger.debug(msg.format("modcase")) mute.MuteCog(bot) logger.debug(msg.format("mute")) purge.PurgeCog(bot) diff --git a/jarvis/cogs/admin/ban.py b/jarvis/cogs/admin/ban.py index 223b37e..2cbe0c6 100644 --- a/jarvis/cogs/admin/ban.py +++ b/jarvis/cogs/admin/ban.py @@ -1,22 +1,21 @@ """JARVIS BanCog.""" -import logging import re -from dis_snek import InteractionContext, Permissions, Snake -from dis_snek.client.utils.misc_utils import find, find_all -from dis_snek.ext.paginators import Paginator -from dis_snek.models.discord.embed import EmbedField -from dis_snek.models.discord.user import User -from dis_snek.models.snek.application_commands import ( +from jarvis_core.db import q +from jarvis_core.db.models import Ban, Unban +from naff import InteractionContext, Permissions +from naff.client.utils.misc_utils import find, find_all +from naff.ext.paginators import Paginator +from naff.models.discord.embed import EmbedField +from naff.models.discord.user import User +from naff.models.naff.application_commands import ( OptionTypes, SlashCommand, SlashCommandChoice, slash_command, slash_option, ) -from dis_snek.models.snek.command import check -from jarvis_core.db import q -from jarvis_core.db.models import Ban, Unban +from naff.models.naff.command import check from jarvis.utils import build_embed from jarvis.utils.cogs import ModcaseCog @@ -26,10 +25,6 @@ from jarvis.utils.permissions import admin_or_permissions class BanCog(ModcaseCog): """JARVIS BanCog.""" - def __init__(self, bot: Snake): - super().__init__(bot) - self.logger = logging.getLogger(__name__) - async def discord_apply_ban( self, ctx: InteractionContext, diff --git a/jarvis/cogs/admin/kick.py b/jarvis/cogs/admin/kick.py index 618e61b..820b777 100644 --- a/jarvis/cogs/admin/kick.py +++ b/jarvis/cogs/admin/kick.py @@ -1,16 +1,14 @@ """JARVIS KickCog.""" -import logging - -from dis_snek import InteractionContext, Permissions, Snake -from dis_snek.models.discord.embed import EmbedField -from dis_snek.models.discord.user import User -from dis_snek.models.snek.application_commands import ( +from jarvis_core.db.models import Kick +from naff import InteractionContext, Permissions +from naff.models.discord.embed import EmbedField +from naff.models.discord.user import User +from naff.models.naff.application_commands import ( OptionTypes, slash_command, slash_option, ) -from dis_snek.models.snek.command import check -from jarvis_core.db.models import Kick +from naff.models.naff.command import check from jarvis.utils import build_embed from jarvis.utils.cogs import ModcaseCog @@ -20,10 +18,6 @@ from jarvis.utils.permissions import admin_or_permissions class KickCog(ModcaseCog): """JARVIS KickCog.""" - def __init__(self, bot: Snake): - super().__init__(bot) - self.logger = logging.getLogger(__name__) - @slash_command(name="kick", description="Kick a user") @slash_option(name="user", description="User to kick", opt_type=OptionTypes.USER, required=True) @slash_option( diff --git a/jarvis/cogs/admin/lock.py b/jarvis/cogs/admin/lock.py index fb18a8b..602959d 100644 --- a/jarvis/cogs/admin/lock.py +++ b/jarvis/cogs/admin/lock.py @@ -2,26 +2,26 @@ import logging from typing import Union -from dis_snek import InteractionContext, Scale, Snake -from dis_snek.client.utils.misc_utils import get -from dis_snek.models.discord.channel import GuildText, GuildVoice -from dis_snek.models.discord.enums import Permissions -from dis_snek.models.snek.application_commands import ( +from jarvis_core.db import q +from jarvis_core.db.models import Lock, Permission +from naff import Client, Cog, InteractionContext +from naff.client.utils.misc_utils import get +from naff.models.discord.channel import GuildText, GuildVoice +from naff.models.discord.enums import Permissions +from naff.models.naff.application_commands import ( OptionTypes, slash_command, slash_option, ) -from dis_snek.models.snek.command import check -from jarvis_core.db import q -from jarvis_core.db.models import Lock, Permission +from naff.models.naff.command import check from jarvis.utils.permissions import admin_or_permissions -class LockCog(Scale): +class LockCog(Cog): """JARVIS LockCog.""" - def __init__(self, bot: Snake): + def __init__(self, bot: Client): self.bot = bot self.logger = logging.getLogger(__name__) diff --git a/jarvis/cogs/admin/lockdown.py b/jarvis/cogs/admin/lockdown.py index 8ee7ac5..d06b3e3 100644 --- a/jarvis/cogs/admin/lockdown.py +++ b/jarvis/cogs/admin/lockdown.py @@ -1,25 +1,27 @@ """JARVIS LockdownCog.""" import logging -from dis_snek import InteractionContext, Scale, Snake -from dis_snek.client.utils.misc_utils import find_all, get -from dis_snek.models.discord.channel import GuildCategory, GuildChannel -from dis_snek.models.discord.enums import Permissions -from dis_snek.models.discord.guild import Guild -from dis_snek.models.discord.user import Member -from dis_snek.models.snek.application_commands import ( +from jarvis_core.db import q +from jarvis_core.db.models import Lock, Lockdown, Permission +from naff import Client, Cog, InteractionContext +from naff.client.utils.misc_utils import find_all, get +from naff.models.discord.channel import GuildCategory, GuildChannel +from naff.models.discord.enums import Permissions +from naff.models.discord.guild import Guild +from naff.models.discord.user import Member +from naff.models.naff.application_commands import ( OptionTypes, SlashCommand, slash_option, ) -from dis_snek.models.snek.command import check -from jarvis_core.db import q -from jarvis_core.db.models import Lock, Lockdown, Permission +from naff.models.naff.command import check from jarvis.utils.permissions import admin_or_permissions -async def lock(bot: Snake, target: GuildChannel, admin: Member, reason: str, duration: int) -> None: +async def lock( + bot: Client, target: GuildChannel, admin: Member, reason: str, duration: int +) -> None: """ Lock an existing channel @@ -44,7 +46,7 @@ async def lock(bot: Snake, target: GuildChannel, admin: Member, reason: str, dur ).commit() -async def lock_all(bot: Snake, guild: Guild, admin: Member, reason: str, duration: int) -> None: +async def lock_all(bot: Client, guild: Guild, admin: Member, reason: str, duration: int) -> None: """ Lock all channels @@ -64,7 +66,7 @@ async def lock_all(bot: Snake, guild: Guild, admin: Member, reason: str, duratio await lock(bot, channel, admin, reason, duration) -async def unlock_all(bot: Snake, guild: Guild, admin: Member) -> None: +async def unlock_all(bot: Client, guild: Guild, admin: Member) -> None: """ Unlock all locked channels @@ -92,10 +94,10 @@ async def unlock_all(bot: Snake, guild: Guild, admin: Member) -> None: await lockdown.commit() -class LockdownCog(Scale): +class LockdownCog(Cog): """JARVIS LockdownCog.""" - def __init__(self, bot: Snake): + def __init__(self, bot: Client): self.bot = bot self.logger = logging.getLogger(__name__) diff --git a/jarvis/cogs/admin/modcase.py b/jarvis/cogs/admin/modcase.py new file mode 100644 index 0000000..3cc446f --- /dev/null +++ b/jarvis/cogs/admin/modcase.py @@ -0,0 +1,332 @@ +"""JARVIS Moderation Case management.""" +from typing import TYPE_CHECKING, List, Optional + +from jarvis_core.db import q +from jarvis_core.db.models import Modlog, Note, actions +from naff import Cog, InteractionContext, Permissions +from naff.ext.paginators import Paginator +from naff.models.discord.embed import Embed, EmbedField +from naff.models.discord.user import Member +from naff.models.naff.application_commands import ( + OptionTypes, + SlashCommand, + slash_option, +) +from naff.models.naff.command import check +from rich.console import Console +from rich.table import Table + +from jarvis.utils import build_embed +from jarvis.utils.permissions import admin_or_permissions + +if TYPE_CHECKING: + from naff.models.discord.guild import Guild + +ACTIONS_LOOKUP = { + "ban": actions.Ban, + "kick": actions.Kick, + "mute": actions.Mute, + "unban": actions.Unban, + "warning": actions.Warning, +} + + +class CaseCog(Cog): + """JARVIS CaseCog.""" + + async def get_summary_embed(self, mod_case: Modlog, guild: "Guild") -> Embed: + """ + Get Moderation case summary embed. + + Args: + mod_case: Moderation case + guild: Originating guild + """ + action_table = Table() + action_table.add_column(header="Type", justify="left", style="orange4", no_wrap=True) + action_table.add_column(header="Admin", justify="left", style="cyan", no_wrap=True) + action_table.add_column(header="Reason", justify="left", style="white") + + note_table = Table() + note_table.add_column(header="Admin", justify="left", style="cyan", no_wrap=True) + note_table.add_column(header="Content", justify="left", style="white") + + console = Console() + + action_output = "" + action_output_extra = "" + for idx, action in enumerate(mod_case.actions): + parent_action = await ACTIONS_LOOKUP[action.action_type].find_one(q(id=action.parent)) + if not parent_action: + action.orphaned = True + action_table.add_row(action.action_type.title(), "[N/A]", "[N/A]") + else: + admin = await self.bot.fetch_user(parent_action.admin) + admin_text = "[N/A]" + if admin: + admin_text = f"{admin.username}#{admin.discriminator}" + action_table.add_row(action.action_type.title(), admin_text, parent_action.reason) + with console.capture() as cap: + console.print(action_table) + + tmp_output = cap.get() + if len(tmp_output) >= 800: + action_output_extra = f"... and {len(mod_case.actions[idx:])} more actions" + break + + action_output = tmp_output + + note_output = "" + note_output_extra = "" + notes = sorted(mod_case.notes, key=lambda x: x.created_at) + for idx, note in enumerate(notes): + admin = await self.bot.fetch_user(note.admin) + admin_text = "[N/A]" + if admin: + admin_text = f"{admin.username}#{admin.discriminator}" + note_table.add_row(admin_text, note.content) + + with console.capture() as cap: + console.print(note_table) + + tmp_output = cap.get() + if len(tmp_output) >= 1000: + note_output_extra = f"... and {len(notes[idx:])} more notes" + break + + note_output = tmp_output + + status = "Open" if mod_case.open else "Closed" + + user = await self.bot.fetch_user(mod_case.user) + username = "[N/A]" + user_text = "[N/A]" + if user: + username = f"{user.username}#{user.discriminator}" + user_text = user.mention + + admin = await self.bot.fetch_user(mod_case.admin) + admin_text = "[N/A]" + if admin: + admin_text = admin.mention + + action_output = f"```ansi\n{action_output}\n{action_output_extra}\n```" + note_output = f"```ansi\n{note_output}\n{note_output_extra}\n```" + + fields = ( + EmbedField( + name="Actions", value=action_output if mod_case.actions else "No Actions Found" + ), + EmbedField(name="Notes", value=note_output if mod_case.notes else "No Notes Found"), + ) + + embed = build_embed( + title=f"Moderation Case [`{mod_case.nanoid}`]", + description=f"{status} case against {user_text} [**opened by {admin_text}**]", + fields=fields, + timestamp=mod_case.created_at, + ) + icon_url = None + if user: + icon_url = user.avatar.url + + embed.set_author(name=username, icon_url=icon_url) + embed.set_footer(text=str(mod_case.user)) + + await mod_case.commit() + return embed + + async def get_action_embeds(self, mod_case: Modlog, guild: "Guild") -> List[Embed]: + """ + Get Moderation case action embeds. + + Args: + mod_case: Moderation case + guild: Originating guild + """ + embeds = [] + user = await self.bot.fetch_user(mod_case.user) + username = "[N/A]" + user_mention = "[N/A]" + avatar_url = None + if user: + username = f"{user.username}#{user.discriminator}" + avatar_url = user.avatar.url + user_mention = user.mention + + for action in mod_case.actions: + if action.orphaned: + continue + parent_action = await ACTIONS_LOOKUP[action.action_type].find_one(q(id=action.parent)) + if not parent_action: + action.orphaned = True + continue + + admin = await self.bot.fetch_user(parent_action.admin) + admin_text = "[N/A]" + if admin: + admin_text = admin.mention + + fields = (EmbedField(name=action.action_type.title(), value=parent_action.reason),) + embed = build_embed( + title="Moderation Case Action", + description=f"{admin_text} initiated an action against {user_mention}", + fields=fields, + timestamp=parent_action.created_at, + ) + embed.set_author(name=username, icon_url=avatar_url) + embeds.append(embed) + + await mod_case.commit() + return embeds + + cases = SlashCommand(name="cases", description="Manage moderation cases") + + @cases.subcommand(sub_cmd_name="list", sub_cmd_description="List moderation cases") + @slash_option( + name="user", + description="User to filter cases to", + opt_type=OptionTypes.USER, + required=False, + ) + @slash_option( + name="closed", + description="Include closed cases", + opt_type=OptionTypes.BOOLEAN, + required=False, + ) + @check(admin_or_permissions(Permissions.BAN_MEMBERS)) + async def _cases_list( + self, ctx: InteractionContext, user: Optional[Member] = None, closed: bool = False + ) -> None: + query = q(guild=ctx.guild.id) + if not closed: + query.update(q(open=True)) + if user: + query.update(q(user=user.id)) + cases = await Modlog.find(query).sort("created_at", -1).to_list(None) + + if len(cases) == 0: + await ctx.send("No cases to view", ephemeral=True) + return + + pages = [await self.get_summary_embed(c, ctx.guild) for c in cases] + paginator = Paginator.create_from_embeds(self.bot, *pages, timeout=300) + await paginator.send(ctx) + + case = SlashCommand(name="case", description="Manage a moderation case") + show = case.group(name="show", description="Show information about a specific case") + + @show.subcommand(sub_cmd_name="summary", sub_cmd_description="Summarize a specific case") + @slash_option(name="cid", description="Case ID", opt_type=OptionTypes.STRING, required=True) + @check(admin_or_permissions(Permissions.BAN_MEMBERS)) + async def _case_show_summary(self, ctx: InteractionContext, cid: str) -> None: + case = await Modlog.find_one(q(guild=ctx.guild.id, nanoid=cid)) + if not case: + await ctx.send(f"Could not find case with ID {cid}", ephemeral=True) + return + + embed = await self.get_summary_embed(case, ctx.guild) + await ctx.send(embed=embed) + + @show.subcommand(sub_cmd_name="actions", sub_cmd_description="Get case actions") + @slash_option(name="cid", description="Case ID", opt_type=OptionTypes.STRING, required=True) + @check(admin_or_permissions(Permissions.BAN_MEMBERS)) + async def _case_show_actions(self, ctx: InteractionContext, cid: str) -> None: + case = await Modlog.find_one(q(guild=ctx.guild.id, nanoid=cid)) + if not case: + await ctx.send(f"Could not find case with ID {cid}", ephemeral=True) + return + + pages = await self.get_action_embeds(case, ctx.guild) + paginator = Paginator.create_from_embeds(self.bot, *pages, timeout=300) + await paginator.send(ctx) + + @case.subcommand(sub_cmd_name="close", sub_cmd_description="Show a specific case") + @slash_option(name="cid", description="Case ID", opt_type=OptionTypes.STRING, required=True) + @check(admin_or_permissions(Permissions.BAN_MEMBERS)) + async def _case_close(self, ctx: InteractionContext, cid: str) -> None: + case = await Modlog.find_one(q(guild=ctx.guild.id, nanoid=cid)) + if not case: + await ctx.send(f"Could not find case with ID {cid}", ephemeral=True) + return + + case.open = False + await case.commit() + + embed = await self.get_summary_embed(case, ctx.guild) + await ctx.send(embed=embed) + + @case.subcommand(sub_cmd_name="repoen", sub_cmd_description="Reopen a specific case") + @slash_option(name="cid", description="Case ID", opt_type=OptionTypes.STRING, required=True) + @check(admin_or_permissions(Permissions.BAN_MEMBERS)) + async def _case_reopen(self, ctx: InteractionContext, cid: str) -> None: + case = await Modlog.find_one(q(guild=ctx.guild.id, nanoid=cid)) + if not case: + await ctx.send(f"Could not find case with ID {cid}", ephemeral=True) + return + + case.open = True + await case.commit() + + embed = await self.get_summary_embed(case, ctx.guild) + await ctx.send(embed=embed) + + @case.subcommand(sub_cmd_name="note", sub_cmd_description="Add a note to a specific case") + @slash_option(name="cid", description="Case ID", opt_type=OptionTypes.STRING, required=True) + @slash_option( + name="note", description="Note to add", opt_type=OptionTypes.STRING, required=True + ) + @check(admin_or_permissions(Permissions.BAN_MEMBERS)) + async def _case_note(self, ctx: InteractionContext, cid: str, note: str) -> None: + case = await Modlog.find_one(q(guild=ctx.guild.id, nanoid=cid)) + if not case: + await ctx.send(f"Could not find case with ID {cid}", ephemeral=True) + return + + if not case.open: + await ctx.send("Case is closed, please re-open to add a new comment", ephemeral=True) + return + + if len(note) > 50: + await ctx.send("Note must be <= 50 characters", ephemeral=True) + return + + note = Note(admin=ctx.author.id, content=note) + + case.notes.append(note) + await case.commit() + + embed = await self.get_summary_embed(case, ctx.guild) + await ctx.send(embed=embed) + + @case.subcommand(sub_cmd_name="new", sub_cmd_description="Open a new case") + @slash_option(name="user", description="Target user", opt_type=OptionTypes.USER, required=True) + @slash_option( + name="note", description="Note to add", opt_type=OptionTypes.STRING, required=True + ) + @check(admin_or_permissions(Permissions.BAN_MEMBERS)) + async def _case_new(self, ctx: InteractionContext, user: Member, note: str) -> None: + case = await Modlog.find_one(q(guild=ctx.guild.id, user=user.id, open=True)) + if case: + await ctx.send(f"Case already open with ID `{case.nanoid}`", ephemeral=True) + return + + if not isinstance(user, Member): + await ctx.send("User must be in this guild", ephemeral=True) + return + + if len(note) > 50: + await ctx.send("Note must be <= 50 characters", ephemeral=True) + return + + note = Note(admin=ctx.author.id, content=note) + + case = Modlog( + user=user.id, guild=ctx.guild.id, admin=ctx.author.id, notes=[note], actions=[] + ) + await case.commit() + await case.reload() + + embed = await self.get_summary_embed(case, ctx.guild) + await ctx.send(embed=embed) diff --git a/jarvis/cogs/admin/mute.py b/jarvis/cogs/admin/mute.py index a8df321..cbcfab3 100644 --- a/jarvis/cogs/admin/mute.py +++ b/jarvis/cogs/admin/mute.py @@ -1,15 +1,16 @@ """JARVIS MuteCog.""" import asyncio -import logging from datetime import datetime, timedelta, timezone from dateparser import parse from dateparser_data.settings import default_parsers -from dis_snek import InteractionContext, Permissions, Snake -from dis_snek.models.discord.embed import EmbedField -from dis_snek.models.discord.modal import InputText, Modal, TextStyles -from dis_snek.models.discord.user import Member -from dis_snek.models.snek.application_commands import ( +from jarvis_core.db.models import Mute +from naff import InteractionContext, Permissions +from naff.client.errors import Forbidden +from naff.models.discord.embed import EmbedField +from naff.models.discord.modal import InputText, Modal, TextStyles +from naff.models.discord.user import Member +from naff.models.naff.application_commands import ( CommandTypes, OptionTypes, SlashCommandChoice, @@ -17,8 +18,7 @@ from dis_snek.models.snek.application_commands import ( slash_command, slash_option, ) -from dis_snek.models.snek.command import check -from jarvis_core.db.models import Mute +from naff.models.naff.command import check from jarvis.utils import build_embed from jarvis.utils.cogs import ModcaseCog @@ -28,10 +28,6 @@ from jarvis.utils.permissions import admin_or_permissions class MuteCog(ModcaseCog): """JARVIS MuteCog.""" - def __init__(self, bot: Snake): - super().__init__(bot) - self.logger = logging.getLogger(__name__) - async def _apply_timeout( self, ctx: InteractionContext, user: Member, reason: str, until: datetime ) -> None: @@ -125,8 +121,11 @@ class MuteCog(ModcaseCog): f"`{old_until}` is in the past, which isn't allowed", ephemeral=True ) return - embed = await self._apply_timeout(ctx, ctx.target, reason, until) - await response.send(embed=embed) + try: + embed = await self._apply_timeout(ctx, ctx.target, reason, until) + await response.send(embed=embed) + except Forbidden: + await response.send("Unable to mute this user", ephemeral=True) @slash_command(name="mute", description="Mute a user") @slash_option(name="user", description="User to mute", opt_type=OptionTypes.USER, required=True) @@ -179,8 +178,11 @@ class MuteCog(ModcaseCog): return until = datetime.now(tz=timezone.utc) + timedelta(minutes=duration) - embed = await self._apply_timeout(ctx, user, reason, until) - await ctx.send(embed=embed) + try: + embed = await self._apply_timeout(ctx, user, reason, until) + await ctx.send(embed=embed) + except Forbidden: + await ctx.send("Unable to mute this user", ephemeral=True) @slash_command(name="unmute", description="Unmute a user") @slash_option( diff --git a/jarvis/cogs/admin/purge.py b/jarvis/cogs/admin/purge.py index 4036eb9..abcb588 100644 --- a/jarvis/cogs/admin/purge.py +++ b/jarvis/cogs/admin/purge.py @@ -1,24 +1,24 @@ """JARVIS PurgeCog.""" import logging -from dis_snek import InteractionContext, Permissions, Scale, Snake -from dis_snek.models.discord.channel import GuildText -from dis_snek.models.snek.application_commands import ( +from jarvis_core.db import q +from jarvis_core.db.models import Autopurge, Purge +from naff import Client, Cog, InteractionContext, Permissions +from naff.models.discord.channel import GuildText +from naff.models.naff.application_commands import ( OptionTypes, slash_command, slash_option, ) -from dis_snek.models.snek.command import check -from jarvis_core.db import q -from jarvis_core.db.models import Autopurge, Purge +from naff.models.naff.command import check from jarvis.utils.permissions import admin_or_permissions -class PurgeCog(Scale): +class PurgeCog(Cog): """JARVIS PurgeCog.""" - def __init__(self, bot: Snake): + def __init__(self, bot: Client): self.bot = bot self.logger = logging.getLogger(__name__) diff --git a/jarvis/cogs/admin/roleping.py b/jarvis/cogs/admin/roleping.py index 6fda235..3bbe1da 100644 --- a/jarvis/cogs/admin/roleping.py +++ b/jarvis/cogs/admin/roleping.py @@ -1,29 +1,29 @@ """JARVIS RolepingCog.""" import logging -from dis_snek import InteractionContext, Permissions, Scale, Snake -from dis_snek.client.utils.misc_utils import find_all -from dis_snek.ext.paginators import Paginator -from dis_snek.models.discord.embed import EmbedField -from dis_snek.models.discord.role import Role -from dis_snek.models.discord.user import Member -from dis_snek.models.snek.application_commands import ( +from jarvis_core.db import q +from jarvis_core.db.models import Roleping +from naff import Client, Cog, InteractionContext, Permissions +from naff.client.utils.misc_utils import find_all +from naff.ext.paginators import Paginator +from naff.models.discord.embed import EmbedField +from naff.models.discord.role import Role +from naff.models.discord.user import Member +from naff.models.naff.application_commands import ( OptionTypes, SlashCommand, slash_option, ) -from dis_snek.models.snek.command import check -from jarvis_core.db import q -from jarvis_core.db.models import Roleping +from naff.models.naff.command import check from jarvis.utils import build_embed from jarvis.utils.permissions import admin_or_permissions -class RolepingCog(Scale): +class RolepingCog(Cog): """JARVIS RolepingCog.""" - def __init__(self, bot: Snake): + def __init__(self, bot: Client): self.bot = bot self.logger = logging.getLogger(__name__) diff --git a/jarvis/cogs/admin/warning.py b/jarvis/cogs/admin/warning.py index 97e2f7b..c118ee0 100644 --- a/jarvis/cogs/admin/warning.py +++ b/jarvis/cogs/admin/warning.py @@ -1,19 +1,19 @@ """JARVIS WarningCog.""" -import logging +from datetime import datetime, timedelta, timezone -from dis_snek import InteractionContext, Permissions, Snake -from dis_snek.client.utils.misc_utils import get_all -from dis_snek.ext.paginators import Paginator -from dis_snek.models.discord.embed import EmbedField -from dis_snek.models.discord.user import Member -from dis_snek.models.snek.application_commands import ( +from jarvis_core.db import q +from jarvis_core.db.models import Warning +from naff import InteractionContext, Permissions +from naff.client.utils.misc_utils import get_all +from naff.ext.paginators import Paginator +from naff.models.discord.embed import EmbedField +from naff.models.discord.user import Member +from naff.models.naff.application_commands import ( OptionTypes, slash_command, slash_option, ) -from dis_snek.models.snek.command import check -from jarvis_core.db import q -from jarvis_core.db.models import Warning +from naff.models.naff.command import check from jarvis.utils import build_embed from jarvis.utils.cogs import ModcaseCog @@ -24,10 +24,6 @@ from jarvis.utils.permissions import admin_or_permissions class WarningCog(ModcaseCog): """JARVIS WarningCog.""" - def __init__(self, bot: Snake): - super().__init__(bot) - self.logger = logging.getLogger(__name__) - @slash_command(name="warn", description="Warn a user") @slash_option(name="user", description="User to warn", opt_type=OptionTypes.USER, required=True) @slash_option( @@ -59,12 +55,14 @@ class WarningCog(ModcaseCog): await ctx.send("User not in guild", ephemeral=True) return await ctx.defer() + expires_at = datetime.now(tz=timezone.utc) + timedelta(hours=duration) await Warning( user=user.id, reason=reason, admin=ctx.author.id, guild=ctx.guild.id, duration=duration, + expires_at=expires_at, active=True, ).commit() embed = warning_embed(user, reason) diff --git a/jarvis/cogs/autoreact.py b/jarvis/cogs/autoreact.py index db1a41f..751474d 100644 --- a/jarvis/cogs/autoreact.py +++ b/jarvis/cogs/autoreact.py @@ -3,26 +3,26 @@ import logging import re from typing import Optional, Tuple -from dis_snek import InteractionContext, Permissions, Scale, Snake -from dis_snek.client.utils.misc_utils import find -from dis_snek.models.discord.channel import GuildText -from dis_snek.models.snek.application_commands import ( +from jarvis_core.db import q +from jarvis_core.db.models import Autoreact +from naff import Client, Cog, InteractionContext, Permissions +from naff.client.utils.misc_utils import find +from naff.models.discord.channel import GuildText +from naff.models.naff.application_commands import ( OptionTypes, SlashCommand, slash_option, ) -from dis_snek.models.snek.command import check -from jarvis_core.db import q -from jarvis_core.db.models import Autoreact +from naff.models.naff.command import check from jarvis.data.unicode import emoji_list from jarvis.utils.permissions import admin_or_permissions -class AutoReactCog(Scale): +class AutoReactCog(Cog): """JARVIS Autoreact Cog.""" - def __init__(self, bot: Snake): + def __init__(self, bot: Client): self.bot = bot self.logger = logging.getLogger(__name__) self.custom_emote = re.compile(r"^<:\w+:(\d+)>$") @@ -206,6 +206,6 @@ class AutoReactCog(Scale): await ctx.send(message) -def setup(bot: Snake) -> None: +def setup(bot: Client) -> None: """Add AutoReactCog to JARVIS""" AutoReactCog(bot) diff --git a/jarvis/cogs/botutil.py b/jarvis/cogs/botutil.py index 2341c0e..aaeb377 100644 --- a/jarvis/cogs/botutil.py +++ b/jarvis/cogs/botutil.py @@ -1,24 +1,33 @@ """JARVIS bot utility commands.""" import logging +import platform from io import BytesIO +import psutil from aiofile import AIOFile, LineReader -from dis_snek import MessageContext, Scale, Snake -from dis_snek.models.discord.file import File -from molter import msg_command +from naff import Client, Cog, PrefixedContext, prefixed_command +from naff.models.discord.embed import EmbedField +from naff.models.discord.file import File +from rich.console import Console + +from jarvis.utils import build_embed +from jarvis.utils.updates import update -class BotutilCog(Scale): +class BotutilCog(Cog): """JARVIS Bot Utility Cog.""" - def __init__(self, bot: Snake): + def __init__(self, bot: Client): self.bot = bot self.logger = logging.getLogger(__name__) + self.add_cog_check(self.is_owner) - @msg_command(name="tail") - async def _tail(self, ctx: MessageContext, count: int = 10) -> None: - if ctx.author.id != self.bot.owner.id: - return + async def is_owner(self, ctx: PrefixedContext) -> bool: + """Checks if author is bot owner.""" + return ctx.author.id == self.bot.owner.id + + @prefixed_command(name="tail") + async def _tail(self, ctx: PrefixedContext, count: int = 10) -> None: lines = [] async with AIOFile("jarvis.log", "r") as af: async for line in LineReader(af): @@ -35,11 +44,8 @@ class BotutilCog(Scale): else: await ctx.reply(content=f"```\n{log}\n```") - @msg_command(name="log") - async def _log(self, ctx: MessageContext) -> None: - if ctx.author.id != self.bot.owner.id: - return - + @prefixed_command(name="log") + async def _log(self, ctx: PrefixedContext) -> None: async with AIOFile("jarvis.log", "r") as af: with BytesIO() as file_bytes: raw = await af.read_bytes() @@ -48,7 +54,67 @@ class BotutilCog(Scale): log = File(file_bytes, file_name="jarvis.log") await ctx.reply(content="Here's the latest log", file=log) + @prefixed_command(name="crash") + async def _crash(self, ctx: PrefixedContext) -> None: + raise Exception("As you wish") -def setup(bot: Snake) -> None: + @prefixed_command(name="sysinfo") + async def _sysinfo(self, ctx: PrefixedContext) -> None: + st_ts = int(self.bot.start_time.timestamp()) + ut_ts = int(psutil.boot_time()) + fields = ( + EmbedField(name="Operation System", value=platform.system() or "Unknown", inline=False), + EmbedField(name="Version", value=platform.release() or "N/A", inline=False), + EmbedField(name="System Start Time", value=f" ()"), + EmbedField(name="Python Version", value=platform.python_version()), + EmbedField(name="Bot Start Time", value=f" ()"), + ) + embed = build_embed(title="System Info", description="", fields=fields) + embed.set_image(url=self.bot.user.avatar.url) + await ctx.send(embed=embed) + + @prefixed_command(name="update") + async def _update(self, ctx: PrefixedContext) -> None: + status = await update(self.bot) + if status: + console = Console() + with console.capture() as capture: + console.print(status.table) + self.logger.debug(capture.get()) + self.logger.debug(len(capture.get())) + added = "\n".join(status.added) + removed = "\n".join(status.removed) + changed = "\n".join(status.changed) + + fields = [ + EmbedField(name="Old Commit", value=status.old_hash), + EmbedField(name="New Commit", value=status.new_hash), + ] + if added: + fields.append(EmbedField(name="New Modules", value=f"```\n{added}\n```")) + if removed: + fields.append(EmbedField(name="Removed Modules", value=f"```\n{removed}\n```")) + if changed: + fields.append(EmbedField(name="Changed Modules", value=f"```\n{changed}\n```")) + + embed = build_embed( + "Update Status", description="Updates have been applied", fields=fields + ) + embed.set_thumbnail(url="https://dev.zevaryx.com/git.png") + + self.logger.info("Updates applied") + content = f"```ansi\n{capture.get()}\n```" + if len(content) < 3000: + await ctx.reply(content, embed=embed) + else: + await ctx.reply(f"Total Changes: {status.lines['total_lines']}", embed=embed) + + else: + embed = build_embed(title="Update Status", description="No changes applied", fields=[]) + embed.set_thumbnail(url="https://dev.zevaryx.com/git.png") + await ctx.reply(embed=embed) + + +def setup(bot: Client) -> None: """Add BotutilCog to JARVIS""" BotutilCog(bot) diff --git a/jarvis/cogs/ctc2.py b/jarvis/cogs/ctc2.py index 90519dc..2a20c0f 100644 --- a/jarvis/cogs/ctc2.py +++ b/jarvis/cogs/ctc2.py @@ -3,20 +3,20 @@ import logging import re import aiohttp -from dis_snek import InteractionContext, Scale, Snake -from dis_snek.ext.paginators import Paginator -from dis_snek.models.discord.components import ActionRow, Button, ButtonStyles -from dis_snek.models.discord.embed import EmbedField -from dis_snek.models.discord.user import Member, User -from dis_snek.models.snek.application_commands import ( +from jarvis_core.db import q +from jarvis_core.db.models import Guess +from naff import Client, Cog, InteractionContext +from naff.ext.paginators import Paginator +from naff.models.discord.components import ActionRow, Button, ButtonStyles +from naff.models.discord.embed import EmbedField +from naff.models.discord.user import Member, User +from naff.models.naff.application_commands import ( OptionTypes, SlashCommand, slash_option, ) -from dis_snek.models.snek.command import cooldown -from dis_snek.models.snek.cooldowns import Buckets -from jarvis_core.db import q -from jarvis_core.db.models import Guess +from naff.models.naff.command import cooldown +from naff.models.naff.cooldowns import Buckets from jarvis.utils import build_embed @@ -29,10 +29,10 @@ invites = re.compile( ) -class CTCCog(Scale): +class CTCCog(Cog): """JARVIS Complete the Code 2 Cog.""" - def __init__(self, bot: Snake): + def __init__(self, bot: Client): self.bot = bot self.logger = logging.getLogger(__name__) self._session = aiohttp.ClientSession() @@ -112,13 +112,11 @@ class CTCCog(Scale): guesses = Guess.find().sort("correct", -1).sort("id", -1) fields = [] async for guess in guesses: - user = await ctx.guild.get_member(guess["user"]) + user = await self.bot.fetch_user(guess["user"]) if not user: - user = await self.bot.fetch_user(guess["user"]) - if not user: - user = "[redacted]" + user = "[redacted]" if isinstance(user, (Member, User)): - user = user.name + "#" + user.discriminator + user = user.username + "#" + user.discriminator name = "Correctly" if guess["correct"] else "Incorrectly" name += " guessed by: " + user fields.append( @@ -132,9 +130,9 @@ class CTCCog(Scale): for i in range(0, len(fields), 5): embed = build_embed( title="completethecodetwo.cards guesses", - description=f"{len(fields)} guesses so far", + description=f"**{len(fields)} guesses so far**", fields=fields[i : i + 5], - url="https://completethecodetwo.cards", + url="https://ctc2.zevaryx.com/gueses", ) embed.set_thumbnail(url="https://dev.zevaryx.com/db_logo.png") embed.set_footer( @@ -148,6 +146,6 @@ class CTCCog(Scale): await paginator.send(ctx) -def setup(bot: Snake) -> None: +def setup(bot: Client) -> None: """Add CTCCog to JARVIS""" CTCCog(bot) diff --git a/jarvis/cogs/dbrand.py b/jarvis/cogs/dbrand.py index 8fe1c98..f42bfae 100644 --- a/jarvis/cogs/dbrand.py +++ b/jarvis/cogs/dbrand.py @@ -3,37 +3,37 @@ import logging import re import aiohttp -from dis_snek import InteractionContext, Scale, Snake -from dis_snek.models.discord.embed import EmbedField -from dis_snek.models.snek.application_commands import ( +from naff import Client, Cog, InteractionContext +from naff.models.discord.embed import EmbedField +from naff.models.naff.application_commands import ( OptionTypes, SlashCommand, slash_option, ) -from dis_snek.models.snek.command import cooldown -from dis_snek.models.snek.cooldowns import Buckets +from naff.models.naff.command import cooldown +from naff.models.naff.cooldowns import Buckets -from jarvis.config import get_config +from jarvis.config import JarvisConfig from jarvis.data.dbrand import shipping_lookup from jarvis.utils import build_embed guild_ids = [862402786116763668] # [578757004059738142, 520021794380447745, 862402786116763668] -class DbrandCog(Scale): +class DbrandCog(Cog): """ dbrand functions for JARVIS Mostly support functions. Credit @cpixl for the shipping API """ - def __init__(self, bot: Snake): + def __init__(self, bot: Client): self.bot = bot self.logger = logging.getLogger(__name__) self.base_url = "https://dbrand.com/" self._session = aiohttp.ClientSession() self._session.headers.update({"Content-Type": "application/json"}) - self.api_url = get_config().urls["dbrand_shipping"] + self.api_url = JarvisConfig.from_yaml().urls["dbrand_shipping"] self.cache = {} def __del__(self): @@ -197,6 +197,6 @@ class DbrandCog(Scale): await ctx.send(embed=embed) -def setup(bot: Snake) -> None: +def setup(bot: Client) -> None: """Add dbrandcog to JARVIS""" DbrandCog(bot) diff --git a/jarvis/cogs/dev.py b/jarvis/cogs/dev.py index 4d2b743..bc4cb42 100644 --- a/jarvis/cogs/dev.py +++ b/jarvis/cogs/dev.py @@ -8,20 +8,20 @@ import uuid as uuidpy import ulid as ulidpy from bson import ObjectId -from dis_snek import InteractionContext, Scale, Snake -from dis_snek.models.discord.embed import EmbedField -from dis_snek.models.discord.message import Attachment -from dis_snek.models.snek.application_commands import ( +from jarvis_core.filters import invites, url +from jarvis_core.util import convert_bytesize, hash +from jarvis_core.util.http import get_size +from naff import Client, Cog, InteractionContext +from naff.models.discord.embed import EmbedField +from naff.models.discord.message import Attachment +from naff.models.naff.application_commands import ( OptionTypes, SlashCommandChoice, slash_command, slash_option, ) -from dis_snek.models.snek.command import cooldown -from dis_snek.models.snek.cooldowns import Buckets -from jarvis_core.filters import invites, url -from jarvis_core.util import convert_bytesize, hash -from jarvis_core.util.http import get_size +from naff.models.naff.command import cooldown +from naff.models.naff.cooldowns import Buckets from jarvis.utils import build_embed @@ -45,10 +45,10 @@ UUID_GET = {3: uuidpy.uuid3, 5: uuidpy.uuid5} MAX_FILESIZE = 5 * (1024**3) # 5GB -class DevCog(Scale): +class DevCog(Cog): """JARVIS Developer Cog.""" - def __init__(self, bot: Snake): + def __init__(self, bot: Client): self.bot = bot self.logger = logging.getLogger(__name__) @@ -272,9 +272,9 @@ class DevCog(Scale): output = subprocess.check_output( # noqa: S603, S607 ["tokei", "-C", "--sort", "code"] ).decode("UTF-8") - await ctx.send(f"```\n{output}\n```") + await ctx.send(f"```haskell\n{output}\n```") -def setup(bot: Snake) -> None: +def setup(bot: Client) -> None: """Add DevCog to JARVIS""" DevCog(bot) diff --git a/jarvis/cogs/gl.py b/jarvis/cogs/gl.py index 7fa80b2..cde44af 100644 --- a/jarvis/cogs/gl.py +++ b/jarvis/cogs/gl.py @@ -4,20 +4,20 @@ import logging from datetime import datetime import gitlab -from dis_snek import InteractionContext, Scale, Snake -from dis_snek.ext.paginators import Paginator -from dis_snek.models.discord.embed import Embed, EmbedField -from dis_snek.models.discord.modal import InputText, Modal, TextStyles -from dis_snek.models.discord.user import Member -from dis_snek.models.snek.application_commands import ( +from naff import Client, Cog, InteractionContext +from naff.ext.paginators import Paginator +from naff.models.discord.embed import Embed, EmbedField +from naff.models.discord.modal import InputText, Modal, TextStyles +from naff.models.discord.user import Member +from naff.models.naff.application_commands import ( OptionTypes, SlashCommand, SlashCommandChoice, slash_command, slash_option, ) -from dis_snek.models.snek.command import cooldown -from dis_snek.models.snek.cooldowns import Buckets +from naff.models.naff.command import cooldown +from naff.models.naff.cooldowns import Buckets from jarvis.config import JarvisConfig from jarvis.utils import build_embed @@ -25,10 +25,10 @@ from jarvis.utils import build_embed guild_ids = [862402786116763668] -class GitlabCog(Scale): +class GitlabCog(Cog): """JARVIS GitLab Cog.""" - def __init__(self, bot: Snake): + def __init__(self, bot: Client): self.bot = bot self.logger = logging.getLogger(__name__) config = JarvisConfig.from_yaml() @@ -62,15 +62,17 @@ class GitlabCog(Scale): labels = issue.labels if labels: labels = "\n".join(issue.labels) - if not labels: + else: labels = "None" fields = [ - EmbedField(name="State", value=issue.state[0].upper() + issue.state[1:]), + EmbedField(name="State", value=issue.state.title()), EmbedField(name="Assignee", value=assignee), EmbedField(name="Labels", value=labels), ] - color = self.project.labels.get(issue.labels[0]).color + color = "#FC6D27" + if issue.labels: + color = self.project.labels.get(issue.labels[0]).color fields.append(EmbedField(name="Created At", value=created_at)) if issue.state == "closed": closed_at = datetime.strptime(issue.closed_at, "%Y-%m-%dT%H:%M:%S.%fZ").strftime( @@ -463,7 +465,7 @@ class GitlabCog(Scale): await resp.send(embed=embed) -def setup(bot: Snake) -> None: +def setup(bot: Client) -> None: """Add GitlabCog to JARVIS if Gitlab token exists.""" if JarvisConfig.from_yaml().gitlab_token: GitlabCog(bot) diff --git a/jarvis/cogs/image.py b/jarvis/cogs/image.py index 0dac7f8..f44c9a7 100644 --- a/jarvis/cogs/image.py +++ b/jarvis/cogs/image.py @@ -6,28 +6,30 @@ from io import BytesIO import aiohttp import cv2 import numpy as np -from dis_snek import InteractionContext, Scale, Snake -from dis_snek.models.discord.embed import EmbedField -from dis_snek.models.discord.file import File -from dis_snek.models.discord.message import Attachment -from dis_snek.models.snek.application_commands import ( +from jarvis_core.util import convert_bytesize, unconvert_bytesize +from naff import Client, Cog, InteractionContext +from naff.models.discord.embed import EmbedField +from naff.models.discord.file import File +from naff.models.discord.message import Attachment +from naff.models.naff.application_commands import ( OptionTypes, slash_command, slash_option, ) -from jarvis_core.util import build_embed, convert_bytesize, unconvert_bytesize + +from jarvis.utils import build_embed MIN_ACCURACY = 0.80 -class ImageCog(Scale): +class ImageCog(Cog): """ Image processing functions for JARVIS May be categorized under util later """ - def __init__(self, bot: Snake): + def __init__(self, bot: Client): self.bot = bot self.logger = logging.getLogger(__name__) self._session = aiohttp.ClientSession() @@ -83,7 +85,7 @@ class ImageCog(Scale): if tgt_size > unconvert_bytesize(8, "MB"): await ctx.send("Target too large to send. Please make target < 8MB", ephemeral=True) return - elif tgt_size < 1024: + if tgt_size < 1024: await ctx.send("Sizes < 1KB are extremely unreliable and are disabled", ephemeral=True) return @@ -151,6 +153,6 @@ class ImageCog(Scale): ) -def setup(bot: Snake) -> None: +def setup(bot: Client) -> None: """Add ImageCog to JARVIS""" ImageCog(bot) diff --git a/jarvis/cogs/reddit.py b/jarvis/cogs/reddit.py new file mode 100644 index 0000000..f292f78 --- /dev/null +++ b/jarvis/cogs/reddit.py @@ -0,0 +1,425 @@ +"""JARVIS Reddit cog.""" +import asyncio +import logging +from typing import List, Optional + +from asyncpraw import Reddit +from asyncpraw.models.reddit.submission import Submission +from asyncpraw.models.reddit.submission import Subreddit as Sub +from asyncprawcore.exceptions import Forbidden, NotFound, Redirect +from jarvis_core.db import q +from jarvis_core.db.models import Subreddit, SubredditFollow +from naff import Client, Cog, InteractionContext, Permissions +from naff.client.utils.misc_utils import get +from naff.models.discord.channel import ChannelTypes, GuildText +from naff.models.discord.components import ActionRow, Select, SelectOption +from naff.models.discord.embed import Embed, EmbedField +from naff.models.naff.application_commands import ( + OptionTypes, + SlashCommand, + SlashCommandChoice, + slash_option, +) +from naff.models.naff.command import check + +from jarvis import const +from jarvis.config import JarvisConfig +from jarvis.utils import build_embed +from jarvis.utils.permissions import admin_or_permissions + +DEFAULT_USER_AGENT = f"python:JARVIS:{const.__version__} (by u/zevaryx)" + + +class RedditCog(Cog): + """JARVIS Reddit Cog.""" + + def __init__(self, bot: Client): + self.bot = bot + self.logger = logging.getLogger(__name__) + config = JarvisConfig.from_yaml() + config.reddit["user_agent"] = config.reddit.get("user_agent", DEFAULT_USER_AGENT) + self.api = Reddit(**config.reddit) + + async def post_embeds(self, sub: Sub, post: Submission) -> Optional[List[Embed]]: + """ + Build a post embeds. + + Args: + post: Post to build embeds + """ + url = "https://reddit.com" + post.permalink + await post.author.load() + author_url = f"https://reddit.com/u/{post.author.name}" + author_icon = post.author.icon_img + images = [] + title = f"{post.title}" + fields = [] + content = "" + og_post = None + if not post.is_self: + og_post = post # noqa: F841 + post = await self.api.submission(post.crosspost_parent_list[0]["id"]) + await post.load() + fields.append(EmbedField(name="Crossposted From", value=post.subreddit_name_prefixed)) + content = f"> **{post.title}**" + if "url" in vars(post): + if any(post.url.endswith(x) for x in ["jpeg", "jpg", "png", "gif"]): + images = [post.url] + if "media_metadata" in vars(post): + for k, v in post.media_metadata.items(): + if v["status"] != "valid" or v["m"] not in ["image/jpg", "image/png", "image/gif"]: + continue + ext = v["m"].split("/")[-1] + i_url = f"https://i.redd.it/{k}.{ext}" + images.append(i_url) + if len(images) == 4: + break + + if "selftext" in vars(post) and post.selftext: + content += "\n\n" + post.selftext + if len(content) > 900: + content = content[:900] + "..." + content += f"\n\n[View this post]({url})" + + if not images and not content: + self.logger.debug(f"Post {post.id} had neither content nor images?") + return None + + color = "#FF4500" + if "primary_color" in vars(sub): + color = sub.primary_color + base_embed = build_embed( + title=title, + description=content, + fields=fields, + timestamp=post.created_utc, + url=url, + color=color, + ) + base_embed.set_author( + name="u/" + post.author.name, url=author_url, icon_url=author_icon + ) + base_embed.set_footer( + text="Reddit", icon_url="https://www.redditinc.com/assets/images/site/reddit-logo.png" + ) + + embeds = [base_embed] + + if len(images) > 0: + embeds[0].set_image(url=images[0]) + for image in images[1:4]: + embed = Embed(url=url) + embed.set_image(url=image) + embeds.append(embed) + + return embeds + + reddit = SlashCommand(name="reddit", description="Manage Reddit follows") + + @reddit.subcommand(sub_cmd_name="follow", sub_cmd_description="Follow a Subreddit") + @slash_option( + name="name", + description="Subreddit display name", + opt_type=OptionTypes.STRING, + required=True, + ) + @slash_option( + name="channel", + description="Channel to post to", + opt_type=OptionTypes.CHANNEL, + channel_types=[ChannelTypes.GUILD_TEXT], + required=True, + ) + @check(admin_or_permissions(Permissions.MANAGE_GUILD)) + async def _reddit_follow(self, ctx: InteractionContext, name: str, channel: GuildText) -> None: + name = name.replace("r/", "") + if len(name) > 20 or len(name) < 3: + await ctx.send("Invalid Subreddit name", ephemeral=True) + return + + if not isinstance(channel, GuildText): + await ctx.send("Channel must be a text channel", ephemeral=True) + return + + try: + subreddit = await self.api.subreddit(name) + await subreddit.load() + except (NotFound, Forbidden, Redirect) as e: + self.logger.debug(f"Subreddit {name} raised {e.__class__.__name__} on add") + await ctx.send("Subreddit may be private, quarantined, or nonexistent.", ephemeral=True) + return + + exists = await SubredditFollow.find_one( + q(display_name=subreddit.display_name, guild=ctx.guild.id) + ) + if exists: + await ctx.send("Subreddit already being followed in this guild", ephemeral=True) + return + + count = len([i async for i in SubredditFollow.find(q(guild=ctx.guild.id))]) + if count >= 12: + await ctx.send("Cannot follow more than 12 Subreddits", ephemeral=True) + return + + if subreddit.over18 and not channel.nsfw: + await ctx.send( + "Subreddit is nsfw, but channel is not. Mark the channel NSFW first.", + ephemeral=True, + ) + return + + sr = await Subreddit.find_one(q(display_name=subreddit.display_name)) + if not sr: + sr = Subreddit(display_name=subreddit.display_name, over18=subreddit.over18) + await sr.commit() + + srf = SubredditFollow( + display_name=subreddit.display_name, + channel=channel.id, + guild=ctx.guild.id, + admin=ctx.author.id, + ) + await srf.commit() + + await ctx.send(f"Now following `r/{name}` in {channel.mention}") + + @reddit.subcommand(sub_cmd_name="unfollow", sub_cmd_description="Unfollow Subreddits") + @check(admin_or_permissions(Permissions.MANAGE_GUILD)) + async def _subreddit_unfollow(self, ctx: InteractionContext) -> None: + subs = SubredditFollow.find(q(guild=ctx.guild.id)) + subreddits = [] + async for sub in subs: + subreddits.append(sub) + if not subreddits: + await ctx.send("You need to follow a Subreddit first", ephemeral=True) + return + + options = [] + names = [] + for idx, subreddit in enumerate(subreddits): + sub = await Subreddit.find_one(q(display_name=subreddit.display_name)) + names.append(sub.display_name) + option = SelectOption(label=sub.display_name, value=str(idx)) + options.append(option) + + select = Select( + options=options, custom_id="to_delete", min_values=1, max_values=len(subreddits) + ) + + components = [ActionRow(select)] + block = "\n".join(x for x in names) + message = await ctx.send( + content=f"You are following the following subreddits:\n```\n{block}\n```\n\n" + "Please choose subreddits to unfollow", + components=components, + ) + + try: + context = await self.bot.wait_for_component( + check=lambda x: ctx.author.id == x.context.author.id, + messages=message, + timeout=60 * 5, + ) + for to_delete in context.context.values: + follow = get(subreddits, guild=ctx.guild.id, display_name=names[int(to_delete)]) + try: + await follow.delete() + except Exception: + self.logger.debug("Ignoring deletion error") + for row in components: + for component in row.components: + component.disabled = True + + block = "\n".join(names[int(x)] for x in context.context.values) + await context.context.edit_origin( + content=f"Unfollowed the following:\n```\n{block}\n```", components=components + ) + except asyncio.TimeoutError: + for row in components: + for component in row.components: + component.disabled = True + await message.edit(components=components) + + @reddit.subcommand(sub_cmd_name="hot", sub_cmd_description="Get the hot post of a subreddit") + @slash_option( + name="name", description="Subreddit name", opt_type=OptionTypes.STRING, required=True + ) + async def _subreddit_hot(self, ctx: InteractionContext, name: str) -> None: + await ctx.defer() + name = name.replace("r/", "") + if len(name) > 20 or len(name) < 3: + await ctx.send("Invalid Subreddit name", ephemeral=True) + return + try: + subreddit = await self.api.subreddit(name) + await subreddit.load() + except (NotFound, Forbidden, Redirect) as e: + self.logger.debug(f"Subreddit {name} raised {e.__class__.__name__} in hot") + await ctx.send("Subreddit may be private, quarantined, or nonexistent.", ephemeral=True) + return + try: + post = [x async for x in subreddit.hot(limit=1)][0] + except Exception as e: + self.logger.error(f"Failed to get post from {name}", exc_info=e) + await ctx.send("Well, this is awkward. Something went wrong", ephemeral=True) + return + + embeds = await self.post_embeds(subreddit, post) + if post.over_18 and not ctx.channel.nsfw: + try: + await ctx.author.send(embeds=embeds) + await ctx.send("Hey! Due to content, I had to DM the result to you") + except Exception: + await ctx.send("Hey! Due to content, I cannot share the result") + else: + await ctx.send(embeds=embeds) + + @reddit.subcommand(sub_cmd_name="top", sub_cmd_description="Get the top post of a subreddit") + @slash_option( + name="name", description="Subreddit name", opt_type=OptionTypes.STRING, required=True + ) + @slash_option( + name="time", + description="Top time", + opt_type=OptionTypes.STRING, + required=False, + choices=[ + SlashCommandChoice(name="All", value="all"), + SlashCommandChoice(name="Day", value="day"), + SlashCommandChoice(name="Hour", value="hour"), + SlashCommandChoice(name="Month", value="month"), + SlashCommandChoice(name="Week", value="week"), + SlashCommandChoice(name="Year", value="year"), + ], + ) + async def _subreddit_top(self, ctx: InteractionContext, name: str, time: str = "all") -> None: + await ctx.defer() + name = name.replace("r/", "") + if len(name) > 20 or len(name) < 3: + await ctx.send("Invalid Subreddit name", ephemeral=True) + return + try: + subreddit = await self.api.subreddit(name) + await subreddit.load() + except (NotFound, Forbidden, Redirect) as e: + self.logger.debug(f"Subreddit {name} raised {e.__class__.__name__} in top") + await ctx.send("Subreddit may be private, quarantined, or nonexistent.", ephemeral=True) + return + try: + post = [x async for x in subreddit.top(time_filter=time, limit=1)][0] + except Exception as e: + self.logger.error(f"Failed to get post from {name}", exc_info=e) + await ctx.send("Well, this is awkward. Something went wrong", ephemeral=True) + return + + embeds = await self.post_embeds(subreddit, post) + if post.over_18 and not ctx.channel.nsfw: + try: + await ctx.author.send(embeds=embeds) + await ctx.send("Hey! Due to content, I had to DM the result to you") + except Exception: + await ctx.send("Hey! Due to content, I cannot share the result") + else: + await ctx.send(embeds=embeds) + + @reddit.subcommand( + sub_cmd_name="random", sub_cmd_description="Get a random post of a subreddit" + ) + @slash_option( + name="name", description="Subreddit name", opt_type=OptionTypes.STRING, required=True + ) + async def _subreddit_random(self, ctx: InteractionContext, name: str) -> None: + await ctx.defer() + name = name.replace("r/", "") + if len(name) > 20 or len(name) < 3: + await ctx.send("Invalid Subreddit name", ephemeral=True) + return + try: + subreddit = await self.api.subreddit(name) + await subreddit.load() + except (NotFound, Forbidden, Redirect) as e: + self.logger.debug(f"Subreddit {name} raised {e.__class__.__name__} in random") + await ctx.send("Subreddit may be private, quarantined, or nonexistent.", ephemeral=True) + return + try: + post = await subreddit.random() + except Exception as e: + self.logger.error(f"Failed to get post from {name}", exc_info=e) + await ctx.send("Well, this is awkward. Something went wrong", ephemeral=True) + return + + embeds = await self.post_embeds(subreddit, post) + if post.over_18 and not ctx.channel.nsfw: + try: + await ctx.author.send(embeds=embeds) + await ctx.send("Hey! Due to content, I had to DM the result to you") + except Exception: + await ctx.send("Hey! Due to content, I cannot share the result") + else: + await ctx.send(embeds=embeds) + + @reddit.subcommand( + sub_cmd_name="rising", sub_cmd_description="Get a rising post of a subreddit" + ) + @slash_option( + name="name", description="Subreddit name", opt_type=OptionTypes.STRING, required=True + ) + async def _subreddit_rising(self, ctx: InteractionContext, name: str) -> None: + await ctx.defer() + name = name.replace("r/", "") + if len(name) > 20 or len(name) < 3: + await ctx.send("Invalid Subreddit name", ephemeral=True) + return + try: + subreddit = await self.api.subreddit(name) + await subreddit.load() + except (NotFound, Forbidden, Redirect) as e: + self.logger.debug(f"Subreddit {name} raised {e.__class__.__name__} in rising") + await ctx.send("Subreddit may be private, quarantined, or nonexistent.", ephemeral=True) + return + try: + post = [x async for x in subreddit.rising(limit=1)][0] + except Exception as e: + self.logger.error(f"Failed to get post from {name}", exc_info=e) + await ctx.send("Well, this is awkward. Something went wrong", ephemeral=True) + return + + embeds = await self.post_embeds(subreddit, post) + if post.over_18 and not ctx.channel.nsfw: + try: + await ctx.author.send(embeds=embeds) + await ctx.send("Hey! Due to content, I had to DM the result to you") + except Exception: + await ctx.send("Hey! Due to content, I cannot share the result") + else: + await ctx.send(embeds=embeds) + + @reddit.subcommand(sub_cmd_name="post", sub_cmd_description="Get a specific submission") + @slash_option( + name="sid", description="Submission ID", opt_type=OptionTypes.STRING, required=True + ) + async def _reddit_post(self, ctx: InteractionContext, sid: str) -> None: + await ctx.defer() + try: + post = await self.api.submission(sid) + await post.load() + except (NotFound, Forbidden, Redirect) as e: + self.logger.debug(f"Submission {sid} raised {e.__class__.__name__} in post") + await ctx.send("Subreddit may be private, quarantined, or nonexistent.", ephemeral=True) + return + + embeds = await self.post_embeds(post.subreddit, post) + if post.over_18 and not ctx.channel.nsfw: + try: + await ctx.author.send(embeds=embeds) + await ctx.send("Hey! Due to content, I had to DM the result to you") + except Exception: + await ctx.send("Hey! Due to content, I cannot share the result") + else: + await ctx.send(embeds=embeds) + + +def setup(bot: Client) -> None: + """Add RedditCog to JARVIS""" + if JarvisConfig.from_yaml().reddit: + RedditCog(bot) diff --git a/jarvis/cogs/remindme.py b/jarvis/cogs/remindme.py index 7225412..ef70d42 100644 --- a/jarvis/cogs/remindme.py +++ b/jarvis/cogs/remindme.py @@ -8,20 +8,20 @@ from typing import List from bson import ObjectId from dateparser import parse from dateparser_data.settings import default_parsers -from dis_snek import InteractionContext, Scale, Snake -from dis_snek.client.utils.misc_utils import get -from dis_snek.models.discord.channel import GuildChannel -from dis_snek.models.discord.components import ActionRow, Select, SelectOption -from dis_snek.models.discord.embed import Embed, EmbedField -from dis_snek.models.discord.modal import InputText, Modal, TextStyles -from dis_snek.models.snek.application_commands import ( +from jarvis_core.db import q +from jarvis_core.db.models import Reminder +from naff import Client, Cog, InteractionContext +from naff.client.utils.misc_utils import get +from naff.models.discord.channel import GuildChannel +from naff.models.discord.components import ActionRow, Select, SelectOption +from naff.models.discord.embed import Embed, EmbedField +from naff.models.discord.modal import InputText, Modal, TextStyles +from naff.models.naff.application_commands import ( OptionTypes, SlashCommand, slash_command, slash_option, ) -from jarvis_core.db import q -from jarvis_core.db.models import Reminder from jarvis.utils import build_embed @@ -33,10 +33,10 @@ invites = re.compile( ) -class RemindmeCog(Scale): +class RemindmeCog(Cog): """JARVIS Remind Me Cog.""" - def __init__(self, bot: Snake): + def __init__(self, bot: Client): self.bot = bot self.logger = logging.getLogger(__name__) @@ -329,13 +329,13 @@ class RemindmeCog(Scale): embed.set_thumbnail(url=ctx.author.display_avatar) await ctx.send(embed=embed, ephemeral=reminder.private) - if reminder.remind_at <= datetime.now(tz=timezone.utc): + if reminder.remind_at <= datetime.now(tz=timezone.utc) and not reminder.active: try: await reminder.delete() except Exception: self.logger.debug("Ignoring deletion error") -def setup(bot: Snake) -> None: +def setup(bot: Client) -> None: """Add RemindmeCog to JARVIS""" RemindmeCog(bot) diff --git a/jarvis/cogs/rolegiver.py b/jarvis/cogs/rolegiver.py index d934de8..0200a6e 100644 --- a/jarvis/cogs/rolegiver.py +++ b/jarvis/cogs/rolegiver.py @@ -2,29 +2,29 @@ import asyncio import logging -from dis_snek import InteractionContext, Permissions, Scale, Snake -from dis_snek.client.utils.misc_utils import get -from dis_snek.models.discord.components import ActionRow, Select, SelectOption -from dis_snek.models.discord.embed import EmbedField -from dis_snek.models.discord.role import Role -from dis_snek.models.snek.application_commands import ( +from jarvis_core.db import q +from jarvis_core.db.models import Rolegiver +from naff import Client, Cog, InteractionContext, Permissions +from naff.client.utils.misc_utils import get +from naff.models.discord.components import ActionRow, Select, SelectOption +from naff.models.discord.embed import EmbedField +from naff.models.discord.role import Role +from naff.models.naff.application_commands import ( OptionTypes, SlashCommand, slash_option, ) -from dis_snek.models.snek.command import check, cooldown -from dis_snek.models.snek.cooldowns import Buckets -from jarvis_core.db import q -from jarvis_core.db.models import Rolegiver +from naff.models.naff.command import check, cooldown +from naff.models.naff.cooldowns import Buckets from jarvis.utils import build_embed from jarvis.utils.permissions import admin_or_permissions -class RolegiverCog(Scale): +class RolegiverCog(Cog): """JARVIS Role Giver Cog.""" - def __init__(self, bot: Snake): + def __init__(self, bot: Client): self.bot = bot self.logger = logging.getLogger(__name__) @@ -41,14 +41,23 @@ class RolegiverCog(Scale): await ctx.send("Cannot add `@everyone` to rolegiver", ephemeral=True) return + if role.bot_managed or not role.is_assignable: + await ctx.send( + "Cannot assign this role, try lowering it below my role or using a different role", + ephemeral=True, + ) + return + setting = await Rolegiver.find_one(q(guild=ctx.guild.id)) - if setting and role.id in setting.roles: + if setting and setting.roles and role.id in setting.roles: await ctx.send("Role already in rolegiver", ephemeral=True) return if not setting: setting = Rolegiver(guild=ctx.guild.id, roles=[]) + setting.roles = setting.roles or [] + if len(setting.roles) >= 20: await ctx.send("You can only have 20 roles in the rolegiver", ephemeral=True) return @@ -378,6 +387,6 @@ class RolegiverCog(Scale): await ctx.send("Rolegiver cleanup finished") -def setup(bot: Snake) -> None: +def setup(bot: Client) -> None: """Add RolegiverCog to JARVIS""" RolegiverCog(bot) diff --git a/jarvis/cogs/settings.py b/jarvis/cogs/settings.py index d5e5086..db2672b 100644 --- a/jarvis/cogs/settings.py +++ b/jarvis/cogs/settings.py @@ -3,29 +3,29 @@ import asyncio import logging from typing import Any -from dis_snek import InteractionContext, Scale, Snake -from dis_snek.models.discord.channel import GuildText -from dis_snek.models.discord.components import ActionRow, Button, ButtonStyles -from dis_snek.models.discord.embed import EmbedField -from dis_snek.models.discord.enums import Permissions -from dis_snek.models.discord.role import Role -from dis_snek.models.snek.application_commands import ( +from jarvis_core.db import q +from jarvis_core.db.models import Setting +from naff import Client, Cog, InteractionContext +from naff.models.discord.channel import GuildText +from naff.models.discord.components import ActionRow, Button, ButtonStyles +from naff.models.discord.embed import EmbedField +from naff.models.discord.enums import Permissions +from naff.models.discord.role import Role +from naff.models.naff.application_commands import ( OptionTypes, SlashCommand, slash_option, ) -from dis_snek.models.snek.command import check -from jarvis_core.db import q -from jarvis_core.db.models import Setting +from naff.models.naff.command import check from jarvis.utils import build_embed from jarvis.utils.permissions import admin_or_permissions -class SettingsCog(Scale): +class SettingsCog(Cog): """JARVIS Settings Management Cog.""" - def __init__(self, bot: Snake): + def __init__(self, bot: Client): self.bot = bot self.logger = logging.getLogger(__name__) @@ -105,6 +105,12 @@ class SettingsCog(Scale): if role.id == ctx.guild.id: await ctx.send("Cannot set verified to `@everyone`", ephemeral=True) return + if role.bot_managed or not role.is_assignable: + await ctx.send( + "Cannot assign this role, try lowering it below my role or using a different role", + ephemeral=True, + ) + return await ctx.defer() await self.update_settings("verified", role.id, ctx.guild.id) await ctx.send(f"Settings applied. New verified role is `{role.name}`") @@ -118,6 +124,12 @@ class SettingsCog(Scale): if role.id == ctx.guild.id: await ctx.send("Cannot set unverified to `@everyone`", ephemeral=True) return + if role.bot_managed or not role.is_assignable: + await ctx.send( + "Cannot assign this role, try lowering it below my role or using a different role", + ephemeral=True, + ) + return await ctx.defer() await self.update_settings("unverified", role.id, ctx.guild.id) await ctx.send(f"Settings applied. New unverified role is `{role.name}`") @@ -169,14 +181,11 @@ class SettingsCog(Scale): await ctx.send("Setting `massmention` unset") @unset.subcommand(sub_cmd_name="verified", sub_cmd_description="Unset verified role") - @slash_option( - name="role", description="Verified role", opt_type=OptionTypes.ROLE, required=True - ) @check(admin_or_permissions(Permissions.MANAGE_GUILD)) async def _unset_verified(self, ctx: InteractionContext) -> None: await ctx.defer() await self.delete_settings("verified", ctx.guild.id) - await ctx.send("Setting `massmention` unset") + await ctx.send("Setting `verified` unset") @unset.subcommand(sub_cmd_name="unverified", sub_cmd_description="Unset unverified role") @check(admin_or_permissions(Permissions.MANAGE_GUILD)) @@ -210,7 +219,11 @@ class SettingsCog(Scale): async for setting in settings: value = setting.value if setting.setting in ["unverified", "verified", "mute"]: - value = await ctx.guild.fetch_role(value) + try: + value = await ctx.guild.fetch_role(value) + except KeyError: + await setting.delete() + continue if value: value = value.mention else: @@ -269,6 +282,6 @@ class SettingsCog(Scale): await message.edit(content="Guild settings not cleared", components=components) -def setup(bot: Snake) -> None: +def setup(bot: Client) -> None: """Add SettingsCog to JARVIS""" SettingsCog(bot) diff --git a/jarvis/cogs/starboard.py b/jarvis/cogs/starboard.py index 85383c7..a87267c 100644 --- a/jarvis/cogs/starboard.py +++ b/jarvis/cogs/starboard.py @@ -1,20 +1,20 @@ """JARVIS Starboard Cog.""" import logging -from dis_snek import InteractionContext, Permissions, Scale, Snake -from dis_snek.models.discord.channel import GuildText -from dis_snek.models.discord.components import ActionRow, Select, SelectOption -from dis_snek.models.discord.message import Message -from dis_snek.models.snek.application_commands import ( +from jarvis_core.db import q +from jarvis_core.db.models import Star, Starboard +from naff import Client, Cog, InteractionContext, Permissions +from naff.models.discord.channel import GuildText +from naff.models.discord.components import ActionRow, Select, SelectOption +from naff.models.discord.message import Message +from naff.models.naff.application_commands import ( CommandTypes, OptionTypes, SlashCommand, context_menu, slash_option, ) -from dis_snek.models.snek.command import check -from jarvis_core.db import q -from jarvis_core.db.models import Star, Starboard +from naff.models.naff.command import check from jarvis.utils import build_embed from jarvis.utils.permissions import admin_or_permissions @@ -28,10 +28,10 @@ supported_images = [ ] -class StarboardCog(Scale): +class StarboardCog(Cog): """JARVIS Starboard Cog.""" - def __init__(self, bot: Snake): + def __init__(self, bot: Client): self.bot = bot self.logger = logging.getLogger(__name__) @@ -135,7 +135,7 @@ class StarboardCog(Scale): if c and isinstance(c, GuildText): channel_list.append(c) else: - self.logger.warn( + self.logger.warning( f"Starboard {starboard.channel} no longer valid in {ctx.guild.name}" ) to_delete.append(starboard) @@ -318,6 +318,6 @@ class StarboardCog(Scale): await ctx.send(f"Star {id} deleted from {starboard.mention}") -def setup(bot: Snake) -> None: +def setup(bot: Client) -> None: """Add StarboardCog to JARVIS""" StarboardCog(bot) diff --git a/jarvis/cogs/temprole.py b/jarvis/cogs/temprole.py new file mode 100644 index 0000000..c2a443e --- /dev/null +++ b/jarvis/cogs/temprole.py @@ -0,0 +1,126 @@ +"""JARVIS temporary role handler.""" +import logging +from datetime import datetime, timezone + +from dateparser import parse +from dateparser_data.settings import default_parsers +from jarvis_core.db.models import Temprole +from naff import Client, Cog, InteractionContext, Permissions +from naff.models.discord.embed import EmbedField +from naff.models.discord.role import Role +from naff.models.discord.user import Member +from naff.models.naff.application_commands import ( + OptionTypes, + slash_command, + slash_option, +) +from naff.models.naff.command import check + +from jarvis.utils import build_embed +from jarvis.utils.permissions import admin_or_permissions + + +class TemproleCog(Cog): + """JARVIS Temporary Role Cog.""" + + def __init__(self, bot: Client): + self.bot = bot + self.logger = logging.getLogger(__name__) + + @slash_command(name="temprole", description="Give a user a temporary role") + @slash_option( + name="user", description="User to grant role", opt_type=OptionTypes.USER, required=True + ) + @slash_option( + name="role", description="Role to grant", opt_type=OptionTypes.ROLE, required=True + ) + @slash_option( + name="duration", + description="Duration of temp role (i.e. 2 hours)", + opt_type=OptionTypes.STRING, + required=True, + ) + @slash_option( + name="reason", + description="Reason for temporary role", + opt_type=OptionTypes.STRING, + required=False, + ) + @check(admin_or_permissions(Permissions.MANAGE_ROLES)) + async def _temprole( + self, ctx: InteractionContext, user: Member, role: Role, duration: str, reason: str = None + ) -> None: + await ctx.defer() + if not isinstance(user, Member): + await ctx.send("User not in guild", ephemeral=True) + return + + if role.id == ctx.guild.id: + await ctx.send("Cannot add `@everyone` to users", ephemeral=True) + return + + if role.bot_managed or not role.is_assignable: + await ctx.send( + "Cannot assign this role, try lowering it below my role or using a different role", + ephemeral=True, + ) + return + + base_settings = { + "PREFER_DATES_FROM": "future", + "TIMEZONE": "UTC", + "RETURN_AS_TIMEZONE_AWARE": True, + } + rt_settings = base_settings.copy() + rt_settings["PARSERS"] = [ + x for x in default_parsers if x not in ["absolute-time", "timestamp"] + ] + + rt_duration = parse(duration, settings=rt_settings) + + at_settings = base_settings.copy() + at_settings["PARSERS"] = [x for x in default_parsers if x != "relative-time"] + at_duration = parse(duration, settings=at_settings) + + if rt_duration: + duration = rt_duration + elif at_duration: + duration = at_duration + else: + self.logger.debug(f"Failed to parse duration: {duration}") + await ctx.send(f"`{duration}` is not a parsable date, please try again", ephemeral=True) + return + + if duration < datetime.now(tz=timezone.utc): + await ctx.send( + f"`{duration}` is in the past. Past durations aren't allowed", ephemeral=True + ) + return + + await user.add_role(role, reason=reason) + await Temprole( + guild=ctx.guild.id, user=user.id, role=role.id, admin=ctx.author.id, expires_at=duration + ).commit() + + ts = int(duration.timestamp()) + + fields = ( + EmbedField(name="Role", value=role.mention), + EmbedField(name="Valid Until", value=f" ()"), + ) + + embed = build_embed( + title="Role granted", + description=f"Role temporarily granted to {user.mention}", + fields=fields, + ) + embed.set_author( + name=f"{user.username}#{user.discriminator}", icon_url=user.display_avatar.url + ) + + await ctx.send(embed=embed) + + +def setup(bot: Client) -> None: + """Add TemproleCog to JARVIS""" + TemproleCog(bot) diff --git a/jarvis/cogs/twitter.py b/jarvis/cogs/twitter.py index a043d16..16a650d 100644 --- a/jarvis/cogs/twitter.py +++ b/jarvis/cogs/twitter.py @@ -3,27 +3,27 @@ import asyncio import logging import tweepy -from dis_snek import InteractionContext, Permissions, Scale, Snake -from dis_snek.client.utils.misc_utils import get -from dis_snek.models.discord.channel import GuildText -from dis_snek.models.discord.components import ActionRow, Select, SelectOption -from dis_snek.models.snek.application_commands import ( +from jarvis_core.db import q +from jarvis_core.db.models import TwitterAccount, TwitterFollow +from naff import Client, Cog, InteractionContext, Permissions +from naff.client.utils.misc_utils import get +from naff.models.discord.channel import GuildText +from naff.models.discord.components import ActionRow, Select, SelectOption +from naff.models.naff.application_commands import ( OptionTypes, SlashCommand, slash_option, ) -from dis_snek.models.snek.command import check -from jarvis_core.db import q -from jarvis_core.db.models import TwitterAccount, TwitterFollow +from naff.models.naff.command import check from jarvis.config import JarvisConfig from jarvis.utils.permissions import admin_or_permissions -class TwitterCog(Scale): +class TwitterCog(Cog): """JARVIS Twitter Cog.""" - def __init__(self, bot: Snake): + def __init__(self, bot: Client): self.bot = bot self.logger = logging.getLogger(__name__) config = JarvisConfig.from_yaml() @@ -63,7 +63,7 @@ class TwitterCog(Scale): self, ctx: InteractionContext, handle: str, channel: GuildText, retweets: bool = True ) -> None: handle = handle.lower() - if len(handle) > 15: + if len(handle) > 15 or len(handle) < 4: await ctx.send("Invalid Twitter handle", ephemeral=True) return @@ -80,16 +80,16 @@ class TwitterCog(Scale): ) return - count = len([i async for i in TwitterFollow.find(q(guild=ctx.guild.id))]) - if count >= 12: - await ctx.send("Cannot follow more than 12 Twitter accounts", ephemeral=True) - return - exists = await TwitterFollow.find_one(q(twitter_id=account.id, guild=ctx.guild.id)) if exists: await ctx.send("Twitter account already being followed in this guild", ephemeral=True) return + count = len([i async for i in TwitterFollow.find(q(guild=ctx.guild.id))]) + if count >= 12: + await ctx.send("Cannot follow more than 12 Twitter accounts", ephemeral=True) + return + ta = await TwitterAccount.find_one(q(twitter_id=account.id)) if not ta: ta = TwitterAccount( @@ -243,6 +243,7 @@ class TwitterCog(Scale): await message.edit(components=components) -def setup(bot: Snake) -> None: +def setup(bot: Client) -> None: """Add TwitterCog to JARVIS""" - TwitterCog(bot) + if JarvisConfig.from_yaml().twitter: + TwitterCog(bot) diff --git a/jarvis/cogs/util.py b/jarvis/cogs/util.py index ff5d7b7..e709479 100644 --- a/jarvis/cogs/util.py +++ b/jarvis/cogs/util.py @@ -1,6 +1,5 @@ """JARVIS Utility Cog.""" import logging -import platform import re import secrets import string @@ -9,14 +8,14 @@ from io import BytesIO import numpy as np from dateparser import parse -from dis_snek import InteractionContext, Scale, Snake, const -from dis_snek.models.discord.channel import GuildCategory, GuildText, GuildVoice -from dis_snek.models.discord.embed import EmbedField -from dis_snek.models.discord.file import File -from dis_snek.models.discord.guild import Guild -from dis_snek.models.discord.role import Role -from dis_snek.models.discord.user import Member, User -from dis_snek.models.snek.application_commands import ( +from naff import Client, Cog, InteractionContext, const +from naff.models.discord.channel import GuildCategory, GuildText, GuildVoice +from naff.models.discord.embed import EmbedField +from naff.models.discord.file import File +from naff.models.discord.guild import Guild +from naff.models.discord.role import Role +from naff.models.discord.user import Member, User +from naff.models.naff.application_commands import ( CommandTypes, OptionTypes, SlashCommandChoice, @@ -24,12 +23,12 @@ from dis_snek.models.snek.application_commands import ( slash_command, slash_option, ) -from dis_snek.models.snek.command import cooldown -from dis_snek.models.snek.cooldowns import Buckets +from naff.models.naff.command import cooldown +from naff.models.naff.cooldowns import Buckets from PIL import Image from tzlocal import get_localzone -import jarvis +from jarvis import const as jconst from jarvis.data import pigpen from jarvis.data.robotcamo import emotes, hk, names from jarvis.utils import build_embed, get_repo_hash @@ -37,14 +36,14 @@ from jarvis.utils import build_embed, get_repo_hash JARVIS_LOGO = Image.open("jarvis_small.png").convert("RGBA") -class UtilCog(Scale): +class UtilCog(Cog): """ Utility functions for JARVIS Mostly system utility functions, but may change over time """ - def __init__(self, bot: Snake): + def __init__(self, bot: Client): self.bot = bot self.logger = logging.getLogger(__name__) @@ -55,10 +54,12 @@ class UtilCog(Scale): desc = f"All systems online\nConnected to **{len(self.bot.guilds)}** guilds" color = "#3498db" fields = [] + uptime = int(self.bot.start_time.timestamp()) - fields.append(EmbedField(name="dis-snek", value=const.__version__)) - fields.append(EmbedField(name="Version", value=jarvis.__version__, inline=False)) - fields.append(EmbedField(name="Git Hash", value=get_repo_hash()[:7], inline=False)) + fields.append(EmbedField(name="Version", value=jconst.__version__, inline=True)) + fields.append(EmbedField(name="naff", value=const.__version__, inline=True)) + fields.append(EmbedField(name="Git Hash", value=get_repo_hash()[:7], inline=True)) + fields.append(EmbedField(name="Online Since", value=f"", inline=False)) num_domains = len(self.bot.phishing_domains) fields.append( EmbedField( @@ -153,6 +154,7 @@ class UtilCog(Scale): EmbedField(name="Position", value=str(role.position), inline=True), EmbedField(name="Mentionable", value="Yes" if role.mentionable else "No", inline=True), EmbedField(name="Member Count", value=str(len(role.members)), inline=True), + EmbedField(name="Created At", value=f""), ] embed = build_embed( title="", @@ -190,18 +192,15 @@ class UtilCog(Scale): user_roles = user.roles if user_roles: user_roles = sorted(user.roles, key=lambda x: -x.position) - format_string = "%a, %b %-d, %Y %-I:%M %p" - if platform.system() == "Windows": - format_string = "%a, %b %#d, %Y %#I:%M %p" fields = [ EmbedField( name="Joined", - value=user.joined_at.strftime(format_string), + value=f"", ), EmbedField( name="Registered", - value=user.created_at.strftime(format_string), + value=f"", ), EmbedField( name=f"Roles [{len(user_roles)}]", @@ -267,6 +266,7 @@ class UtilCog(Scale): EmbedField(name="Threads", value=str(threads), inline=True), EmbedField(name="Members", value=str(members), inline=True), EmbedField(name="Roles", value=str(roles), inline=True), + EmbedField(name="Created At", value=f""), ] if len(role_list) < 1024: fields.append(EmbedField(name="Role List", value=role_list, inline=False)) @@ -375,6 +375,6 @@ class UtilCog(Scale): await ctx.send(embed=embed, ephemeral=private) -def setup(bot: Snake) -> None: +def setup(bot: Client) -> None: """Add UtilCog to JARVIS""" UtilCog(bot) diff --git a/jarvis/cogs/verify.py b/jarvis/cogs/verify.py index 602f691..d921a60 100644 --- a/jarvis/cogs/verify.py +++ b/jarvis/cogs/verify.py @@ -3,13 +3,13 @@ import asyncio import logging from random import randint -from dis_snek import InteractionContext, Scale, Snake -from dis_snek.models.discord.components import Button, ButtonStyles, spread_to_rows -from dis_snek.models.snek.application_commands import slash_command -from dis_snek.models.snek.command import cooldown -from dis_snek.models.snek.cooldowns import Buckets from jarvis_core.db import q from jarvis_core.db.models import Setting +from naff import Client, Cog, InteractionContext +from naff.models.discord.components import Button, ButtonStyles, spread_to_rows +from naff.models.naff.application_commands import slash_command +from naff.models.naff.command import cooldown +from naff.models.naff.cooldowns import Buckets def create_layout() -> list: @@ -30,10 +30,10 @@ def create_layout() -> list: return spread_to_rows(*buttons, max_in_row=3) -class VerifyCog(Scale): +class VerifyCog(Cog): """JARVIS Verify Cog.""" - def __init__(self, bot: Snake): + def __init__(self, bot: Client): self.bot = bot self.logger = logging.getLogger(__name__) @@ -45,7 +45,13 @@ class VerifyCog(Scale): if not role: message = await ctx.send("This guild has not enabled verification", ephemeral=True) return - if await ctx.guild.fetch_role(role.value) in ctx.author.roles: + verified_role = await ctx.guild.fetch_role(role.value) + if not verified_role: + await ctx.send("This guild has not enabled verification", ephemeral=True) + await role.delete() + return + + if verified_role in ctx.author.roles: await ctx.send("You are already verified.", ephemeral=True) return components = create_layout() @@ -69,12 +75,20 @@ class VerifyCog(Scale): for component in row.components: component.disabled = True setting = await Setting.find_one(q(guild=ctx.guild.id, setting="verified")) - role = await ctx.guild.fetch_role(setting.value) - await ctx.author.add_role(role, reason="Verification passed") + try: + role = await ctx.guild.fetch_role(setting.value) + await ctx.author.add_role(role, reason="Verification passed") + except AttributeError: + self.logger.warning("Verified role deleted before verification finished") setting = await Setting.find_one(q(guild=ctx.guild.id, setting="unverified")) if setting: - role = await ctx.guild.fetch_role(setting.value) - await ctx.author.remove_role(role, reason="Verification passed") + try: + role = await ctx.guild.fetch_role(setting.value) + await ctx.author.remove_role(role, reason="Verification passed") + except AttributeError: + self.logger.warning( + "Unverified role deleted before verification finished" + ) await response.context.edit_origin( content=f"Welcome, {ctx.author.mention}. Please enjoy your stay.", @@ -94,6 +108,6 @@ class VerifyCog(Scale): self.logger.debug(f"User {ctx.author.id} failed to verify before timeout") -def setup(bot: Snake) -> None: +def setup(bot: Client) -> None: """Add VerifyCog to JARVIS""" VerifyCog(bot) diff --git a/jarvis/config.py b/jarvis/config.py index 2aec603..5159b7c 100644 --- a/jarvis/config.py +++ b/jarvis/config.py @@ -1,96 +1,17 @@ """Load the config for JARVIS""" -import os - from jarvis_core.config import Config as CConfig -from pymongo import MongoClient -from yaml import load - -try: - from yaml import CLoader as Loader -except ImportError: - from yaml import Loader class JarvisConfig(CConfig): - REQUIRED = ["token", "mongo", "urls"] + REQUIRED = ("token", "mongo", "urls", "redis") OPTIONAL = { "sync": False, "log_level": "WARNING", - "scales": None, + "cogs": None, "events": True, "gitlab_token": None, "max_messages": 1000, "twitter": None, + "reddit": None, + "rook_token": None, } - - -class Config(object): - """Config singleton object for JARVIS""" - - def __new__(cls, *args: list, **kwargs: dict): - """Get the singleton config, or creates a new one.""" - it = cls.__dict__.get("it") - if it is not None: - return it - cls.__it__ = it = object.__new__(cls) - it.init(*args, **kwargs) - return it - - def init( - self, - token: str, - mongo: dict, - urls: dict, - sync: bool = False, - log_level: str = "WARNING", - cogs: list = None, - events: bool = True, - gitlab_token: str = None, - max_messages: int = 1000, - twitter: dict = None, - ) -> None: - """Initialize the config object.""" - self.token = token - self.mongo = mongo - self.urls = urls - self.log_level = log_level - self.cogs = cogs - self.events = events - self.max_messages = max_messages - self.gitlab_token = gitlab_token - self.twitter = twitter - self.sync = sync or os.environ.get("SYNC_COMMANDS", False) - self.__db_loaded = False - self.__mongo = MongoClient(**self.mongo["connect"]) - - def get_db_config(self) -> None: - """Load the database config objects.""" - if not self.__db_loaded: - db = self.__mongo[self.mongo["database"]] - items = db.config.find() - for item in items: - setattr(self, item["key"], item["value"]) - self.__db_loaded = True - - @classmethod - def from_yaml(cls, y: dict) -> "Config": - """Load the yaml config file.""" - return cls(**y) - - -def get_config(path: str = "config.yaml") -> Config: - """Get the config from the specified yaml file.""" - if Config.__dict__.get("it"): - return Config() - with open(path) as f: - raw = f.read() - y = load(raw, Loader=Loader) - config = Config.from_yaml(y) - config.get_db_config() - return config - - -def reload_config() -> None: - """Force reload of the config singleton on next call.""" - if "it" in Config.__dict__: - Config.__dict__.pop("it") diff --git a/jarvis/const.py b/jarvis/const.py new file mode 100644 index 0000000..d1f2869 --- /dev/null +++ b/jarvis/const.py @@ -0,0 +1,7 @@ +"""JARVIS constants.""" +from importlib.metadata import version as _v + +try: + __version__ = _v("jarvis") +except Exception: + __version__ = "0.0.0" diff --git a/jarvis/data/robotcamo.py b/jarvis/data/robotcamo.py index 3c1647e..3577833 100644 --- a/jarvis/data/robotcamo.py +++ b/jarvis/data/robotcamo.py @@ -50,32 +50,32 @@ emotes = { } names = { - 852317928572715038: "rcA", - 852317954975727679: "rcB", - 852317972424818688: "rcC", - 852317990238421003: "rcD", - 852318044503539732: "rcE", - 852318058353786880: "rcF", - 852318073994477579: "rcG", - 852318105832259614: "rcH", - 852318122278125580: "rcI", - 852318145074167818: "rcJ", - 852318159952412732: "rcK", - 852318179358408704: "rcL", - 852318241555873832: "rcM", - 852318311115128882: "rcN", - 852318329951223848: "rcO", - 852318344643477535: "rcP", - 852318358920757248: "rcQ", - 852318385638211594: "rcR", - 852318401166311504: "rcS", - 852318421524938773: "rcT", - 852318435181854742: "rcU", - 852318453204647956: "rcV", - 852318470267731978: "rcW", - 852318484749877278: "rcX", - 852318504564555796: "rcY", - 852318519449092176: "rcZ", + 852317928572715038: "rcLetterA", + 852317954975727679: "rcLetterB", + 852317972424818688: "rcLetterC", + 852317990238421003: "rcLetterD", + 852318044503539732: "rcLetterE", + 852318058353786880: "rcLetterF", + 852318073994477579: "rcLetterG", + 852318105832259614: "rcLetterH", + 852318122278125580: "rcLetterI", + 852318145074167818: "rcLetterJ", + 852318159952412732: "rcLetterK", + 852318179358408704: "rcLetterL", + 852318241555873832: "rcLetterM", + 852318311115128882: "rcLetterN", + 852318329951223848: "rcLetterO", + 852318344643477535: "rcLetterP", + 852318358920757248: "rcLetterQ", + 852318385638211594: "rcLetterR", + 852318401166311504: "rcLetterS", + 852318421524938773: "rcLetterT", + 852318435181854742: "rcLetterU", + 852318453204647956: "rcLetterV", + 852318470267731978: "rcLetterW", + 852318484749877278: "rcLetterX", + 852318504564555796: "rcLetterY", + 852318519449092176: "rcLetterZ", 860663352740151316: "rc1", 860662785243348992: "rc2", 860662950011469854: "rc3", diff --git a/jarvis/utils/__init__.py b/jarvis/utils/__init__.py index 751bfe6..2bece3a 100644 --- a/jarvis/utils/__init__.py +++ b/jarvis/utils/__init__.py @@ -3,14 +3,11 @@ from datetime import datetime, timezone from pkgutil import iter_modules import git -from dis_snek.models.discord.embed import Embed, EmbedField -from dis_snek.models.discord.guild import AuditLogEntry -from dis_snek.models.discord.user import Member +from naff.models.discord.embed import Embed, EmbedField +from naff.models.discord.guild import AuditLogEntry +from naff.models.discord.user import Member -import jarvis.cogs -from jarvis.config import get_config - -__all__ = ["cachecog", "permissions"] +from jarvis.config import JarvisConfig def build_embed( @@ -64,11 +61,11 @@ def modlog_embed( return embed -def get_extensions(path: str = jarvis.cogs.__path__) -> list: +def get_extensions(path: str) -> list: """Get JARVIS cogs.""" - config = get_config() + config = JarvisConfig.from_yaml() vals = config.cogs or [x.name for x in iter_modules(path)] - return ["jarvis.cogs.{}".format(x) for x in vals] + return [f"jarvis.cogs.{x}" for x in vals] def update() -> int: diff --git a/jarvis/utils/cogs.py b/jarvis/utils/cogs.py index 7994290..c18ac38 100644 --- a/jarvis/utils/cogs.py +++ b/jarvis/utils/cogs.py @@ -1,64 +1,28 @@ """Cog wrapper for command caching.""" -from datetime import datetime, timedelta, timezone +import logging +from datetime import timedelta -from dis_snek import InteractionContext, Scale, Snake -from dis_snek.client.utils.misc_utils import find -from dis_snek.models.discord.embed import EmbedField -from dis_snek.models.snek.tasks.task import Task -from dis_snek.models.snek.tasks.triggers import IntervalTrigger from jarvis_core.db import q -from jarvis_core.db.models import ( - Action, - Ban, - Kick, - Modlog, - Mute, - Note, - Setting, - Warning, -) +from jarvis_core.db.models import Action, Ban, Kick, Modlog, Mute, Setting, Warning +from naff import Client, Cog, InteractionContext +from naff.models.discord.components import ActionRow, Button, ButtonStyles +from naff.models.discord.embed import EmbedField from jarvis.utils import build_embed MODLOG_LOOKUP = {"Ban": Ban, "Kick": Kick, "Mute": Mute, "Warning": Warning} +IGNORE_COMMANDS = {"Ban": ["bans"], "Kick": [], "Mute": ["unmute"], "Warning": ["warnings"]} -class CacheCog(Scale): - """Cog wrapper for command caching.""" - - def __init__(self, bot: Snake): - self.bot = bot - self.cache = {} - self._expire_interaction.start() - - def check_cache(self, ctx: InteractionContext, **kwargs: dict) -> dict: - """Check the cache.""" - if not kwargs: - kwargs = {} - return find( - lambda x: x["command"] == ctx.subcommand_name # noqa: W503 - and x["user"] == ctx.author.id # noqa: W503 - and x["guild"] == ctx.guild.id # noqa: W503 - and all(x[k] == v for k, v in kwargs.items()), # noqa: W503 - self.cache.values(), - ) - - @Task.create(IntervalTrigger(minutes=1)) - async def _expire_interaction(self) -> None: - keys = list(self.cache.keys()) - for key in keys: - if self.cache[key]["timeout"] <= datetime.now(tz=timezone.utc) + timedelta(minutes=1): - del self.cache[key] - - -class ModcaseCog(Scale): +class ModcaseCog(Cog): """Cog wrapper for moderation case logging.""" - def __init__(self, bot: Snake): + def __init__(self, bot: Client): self.bot = bot - self.add_scale_postrun(self.log) + self.logger = logging.getLogger(__name__) + self.add_cog_postrun(self.log) - async def log(self, ctx: InteractionContext, *args: list, **kwargs: dict) -> None: + async def log(self, ctx: InteractionContext, *_args: list, **kwargs: dict) -> None: """ Log a moderation activity in a moderation case. @@ -67,34 +31,33 @@ class ModcaseCog(Scale): """ name = self.__name__.replace("Cog", "") - if name not in ["Lock", "Lockdown", "Purge", "Roleping"]: + if name in MODLOG_LOOKUP and ctx.invoke_target not in IGNORE_COMMANDS[name]: user = kwargs.pop("user", None) if not user and not ctx.target_id: - self.logger.warn(f"Admin action {name} missing user, exiting") + self.logger.warning("Admin action %s missing user, exiting", name) return - elif ctx.target_id: + if ctx.target_id: user = ctx.target coll = MODLOG_LOOKUP.get(name, None) if not coll: - self.logger.warn(f"Unsupported action {name}, exiting") + self.logger.warning("Unsupported action %s, exiting", name) return - action = await coll.find_one(q(user=user.id, guild=ctx.guild_id, active=True)) + action = await coll.find_one( + q(user=user.id, guild=ctx.guild_id, active=True), sort=[("_id", -1)] + ) if not action: - self.logger.warn(f"Missing action {name}, exiting") + self.logger.warning("Missing action %s, exiting", name) return - action = Action(action_type=name.lower(), parent=action.id) - note = Note(admin=self.bot.user.id, content="Moderation case opened automatically") - await Modlog(user=user.id, admin=ctx.author.id, actions=[action], notes=[note]).commit() notify = await Setting.find_one(q(guild=ctx.guild.id, setting="notify", value=True)) - if notify and name not in ["Kick", "Ban"]: # Ignore Kick and Ban, as these are unique - fields = [ + if notify and name not in ("Kick", "Ban"): # Ignore Kick and Ban, as these are unique + fields = ( EmbedField(name="Action Type", value=name, inline=False), EmbedField( name="Reason", value=kwargs.get("reason", None) or "N/A", inline=False ), - ] + ) embed = build_embed( title="Admin action taken", description=f"Admin action has been taken against you in {ctx.guild.name}", @@ -103,4 +66,54 @@ class ModcaseCog(Scale): guild_url = f"https://discord.com/channels/{ctx.guild.id}" embed.set_author(name=ctx.guild.name, icon_url=ctx.guild.icon.url, url=guild_url) embed.set_thumbnail(url=ctx.guild.icon.url) - await user.send(embed=embed) + try: + await user.send(embed=embed) + except Exception: + self.logger.debug("User not warned of action due to closed DMs") + + modlog = await Modlog.find_one(q(user=user.id, guild=ctx.guild.id, open=True)) + if modlog: + m_action = Action(action_type=name.lower(), parent=action.id) + modlog.actions.append(m_action) + await modlog.commit() + return + + lookup_key = f"{user.id}|{ctx.guild.id}" + + async with self.bot.redis.lock("lock|" + lookup_key): + if await self.bot.redis.get(lookup_key): + self.logger.debug(f"User {user.id} in {ctx.guild.id} already has pending case") + return + + modlog = await Setting.find_one(q(guild=ctx.guild.id, setting="modlog")) + if not modlog: + return + + channel = await ctx.guild.fetch_channel(modlog.value) + if not channel: + self.logger.warn( + f"Guild {ctx.guild.id} modlog channel no longer exists, deleting" + ) + await modlog.delete() + return + + embed = build_embed( + title="Recent Action Taken", + description=f"Would you like to open a moderation case for {user.mention}?", + fields=[], + ) + embed.set_author( + name=user.username + "#" + user.discriminator, icon_url=user.display_avatar.url + ) + components = [ + ActionRow( + Button(style=ButtonStyles.RED, emoji="✖️", custom_id="modcase|no"), + Button(style=ButtonStyles.GREEN, emoji="✔️", custom_id="modcase|yes"), + ) + ] + message = await channel.send(embed=embed, components=components) + + await self.bot.redis.set( + lookup_key, f"{name.lower()}|{action.id}", ex=timedelta(days=7) + ) + await self.bot.redis.set(f"msg|{message.id}", user.id, ex=timedelta(days=7)) diff --git a/jarvis/utils/embeds.py b/jarvis/utils/embeds.py index 31c9fbc..3d9e929 100644 --- a/jarvis/utils/embeds.py +++ b/jarvis/utils/embeds.py @@ -1,7 +1,8 @@ """JARVIS bot-specific embeds.""" -from dis_snek.models.discord.embed import Embed, EmbedField -from dis_snek.models.discord.user import Member -from jarvis_core.util import build_embed +from naff.models.discord.embed import Embed, EmbedField +from naff.models.discord.user import Member + +from jarvis.utils import build_embed def warning_embed(user: Member, reason: str) -> Embed: @@ -12,7 +13,7 @@ def warning_embed(user: Member, reason: str) -> Embed: user: User to warn reason: Warning reason """ - fields = [EmbedField(name="Reason", value=reason, inline=False)] + fields = (EmbedField(name="Reason", value=reason, inline=False),) embed = build_embed( title="Warning", description=f"{user.mention} has been warned", fields=fields ) diff --git a/jarvis/utils/permissions.py b/jarvis/utils/permissions.py index 1eacbc7..cac2ff2 100644 --- a/jarvis/utils/permissions.py +++ b/jarvis/utils/permissions.py @@ -1,7 +1,7 @@ """Permissions wrappers.""" -from dis_snek import InteractionContext, Permissions +from naff import InteractionContext, Permissions -from jarvis.config import get_config +from jarvis.config import JarvisConfig def user_is_bot_admin() -> bool: @@ -9,8 +9,9 @@ def user_is_bot_admin() -> bool: async def predicate(ctx: InteractionContext) -> bool: """Command check predicate.""" - if getattr(get_config(), "admins", None): - return ctx.author.id in get_config().admins + cfg = JarvisConfig.from_yaml() + if getattr(cfg, "admins", None): + return ctx.author.id in cfg.admins else: return False diff --git a/jarvis/utils/updates.py b/jarvis/utils/updates.py new file mode 100644 index 0000000..3dd52f5 --- /dev/null +++ b/jarvis/utils/updates.py @@ -0,0 +1,217 @@ +"""JARVIS update handler.""" +import asyncio +import logging +from dataclasses import dataclass +from importlib import import_module +from inspect import getmembers, isclass +from pkgutil import iter_modules +from types import FunctionType, ModuleType +from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional + +import git +from naff.client.errors import ExtensionNotFound +from naff.client.utils.misc_utils import find, find_all +from naff.models.naff.application_commands import SlashCommand +from naff.models.naff.cog import Cog +from rich.table import Table + +import jarvis.cogs + +if TYPE_CHECKING: + from naff.client.client import Client + +_logger = logging.getLogger(__name__) + + +@dataclass +class UpdateResult: + """JARVIS update result.""" + + old_hash: str + new_hash: str + table: Table + added: List[str] + removed: List[str] + changed: List[str] + lines: Dict[str, int] + + +def get_all_commands(module: ModuleType = jarvis.cogs) -> Dict[str, Callable]: + """Get all SlashCommands from a specified module.""" + commands = {} + + def validate_ires(entry: Any) -> bool: + return isclass(entry) and issubclass(entry, Cog) and entry is not Cog + + def validate_cog(cog: FunctionType) -> bool: + return isinstance(cog, SlashCommand) + + for item in iter_modules(module.__path__): + new_module = import_module(f"{module.__name__}.{item.name}") + if item.ispkg: + if cmds := get_all_commands(new_module): + commands.update(cmds) + else: + inspect_result = getmembers(new_module) + cogs = [] + for _, val in inspect_result: + if validate_ires(val): + cogs.append(val) + for cog in cogs: + values = cog.__dict__.values() + commands[cog.__module__] = find_all(lambda x: isinstance(x, SlashCommand), values) + return {k: v for k, v in commands.items() if v} + + +def get_git_changes(repo: git.Repo) -> dict: + """Get all Git changes""" + logger = _logger + logger.debug("Getting all git changes") + current_hash = repo.head.ref.object.hexsha + tracking = repo.head.ref.tracking_branch() + + file_changes = {} + for commit in tracking.commit.iter_items(repo, f"{repo.head.ref.path}..{tracking.path}"): + if commit.hexsha == current_hash: + break + files = commit.stats.files + file_changes.update( + {key: {"insertions": 0, "deletions": 0, "lines": 0} for key in files.keys()} + ) + for file, stats in files.items(): + if file not in file_changes: + file_changes[file] = {"insertions": 0, "deletions": 0, "lines": 0} + for key, val in stats.items(): + file_changes[file][key] += val + logger.debug("Found %i changed files", len(file_changes)) + + table = Table(title="File Changes") + + table.add_column("File", justify="left", style="white", no_wrap=True) + table.add_column("Insertions", justify="center", style="green") + table.add_column("Deletions", justify="center", style="red") + table.add_column("Lines", justify="center", style="magenta") + + i_total = 0 + d_total = 0 + l_total = 0 + for file, stats in file_changes.items(): + i_total += stats["insertions"] + d_total += stats["deletions"] + l_total += stats["lines"] + table.add_row( + file, + str(stats["insertions"]), + str(stats["deletions"]), + str(stats["lines"]), + ) + logger.debug("%i insertions, %i deletions, %i total", i_total, d_total, l_total) + + table.add_row("Total", str(i_total), str(d_total), str(l_total)) + return { + "table": table, + "lines": {"inserted_lines": i_total, "deleted_lines": d_total, "total_lines": l_total}, + } + + +async def update(bot: "Client") -> Optional[UpdateResult]: + """ + Update JARVIS and return an UpdateResult. + + Args: + bot: Bot instance + + Returns: + UpdateResult object + """ + logger = _logger + repo = git.Repo(".") + current_hash = repo.head.object.hexsha + origin = repo.remotes.origin + origin.fetch() + remote_hash = origin.refs[repo.active_branch.name].object.hexsha + + if current_hash != remote_hash: + logger.info("Updating from %s to %s", current_hash, remote_hash) + current_commands = get_all_commands() + changes = get_git_changes(repo) + + origin.pull() + await asyncio.sleep(3) + + new_commands = get_all_commands() + + logger.info("Checking if any modules need reloaded...") + + reloaded = [] + loaded = [] + unloaded = [] + + logger.debug("Checking for removed cogs") + for module in current_commands.keys(): + if module not in new_commands: + logger.debug("Module %s removed after update", module) + bot.drop_cog(module) + unloaded.append(module) + + logger.debug("Checking for new/modified commands") + for module, commands in new_commands.items(): + logger.debug("Processing %s", module) + if module not in current_commands: + bot.load_cog(module) + loaded.append(module) + elif len(current_commands[module]) != len(commands): + try: + bot.reload_cog(module) + except ExtensionNotFound: + bot.load_cog(module) + reloaded.append(module) + else: + for command in commands: + old_command = find( + lambda x: x.resolved_name == command.resolved_name, current_commands[module] + ) + + # Extract useful info + old_args = old_command.options + if old_args: + old_arg_names = [x.name for x in old_args] + new_args = command.options + if new_args: + new_arg_names = [x.name for x in new_args] + + # No changes + if not old_args and not new_args: + continue + + # Check if number arguments have changed + if len(old_args) != len(new_args): + try: + bot.reload_cog(module) + except ExtensionNotFound: + bot.load_cog(module) + reloaded.append(module) + elif any(x not in old_arg_names for x in new_arg_names) or any( + x not in new_arg_names for x in old_arg_names + ): + try: + bot.reload_cog(module) + except ExtensionNotFound: + bot.load_cog(module) + reloaded.append(module) + elif any(new_args[idx].type != x.type for idx, x in enumerate(old_args)): + try: + bot.reload_cog(module) + except ExtensionNotFound: + bot.load_cog(module) + reloaded.append(module) + + return UpdateResult( + old_hash=current_hash, + new_hash=remote_hash, + added=loaded, + removed=unloaded, + changed=reloaded, + **changes, + ) + return None diff --git a/poetry.lock b/poetry.lock index c1b3a01..2697614 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,3 +1,25 @@ +[[package]] +name = "aiofile" +version = "3.7.4" +description = "Asynchronous file operations." +category = "main" +optional = false +python-versions = ">3.4.*, <4" + +[package.dependencies] +caio = ">=0.9.0,<0.10.0" + +[package.extras] +develop = ["aiomisc", "asynctest", "pytest", "pytest-cov"] + +[[package]] +name = "aiofiles" +version = "0.6.0" +description = "File support for asyncio." +category = "main" +optional = false +python-versions = "*" + [[package]] name = "aiohttp" version = "3.8.1" @@ -18,6 +40,21 @@ yarl = ">=1.0,<2.0" [package.extras] speedups = ["aiodns", "brotli", "cchardet"] +[[package]] +name = "aioredis" +version = "2.0.1" +description = "asyncio (PEP 3156) Redis support" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +async-timeout = "*" +typing-extensions = "*" + +[package.extras] +hiredis = ["hiredis (>=1.0)"] + [[package]] name = "aiosignal" version = "1.2.0" @@ -29,6 +66,33 @@ python-versions = ">=3.6" [package.dependencies] frozenlist = ">=1.1.0" +[[package]] +name = "aiosqlite" +version = "0.17.0" +description = "asyncio bridge to the standard sqlite3 module" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +typing_extensions = ">=3.7.2" + +[[package]] +name = "ansicon" +version = "1.89.0" +description = "Python wrapper for loading Jason Hood's ANSICON" +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "async-generator" +version = "1.10" +description = "Async generators and context managers for Python 3.5+" +category = "main" +optional = false +python-versions = ">=3.5" + [[package]] name = "async-timeout" version = "4.0.2" @@ -37,6 +101,61 @@ category = "main" optional = false python-versions = ">=3.6" +[[package]] +name = "asyncio-extras" +version = "1.3.2" +description = "Asynchronous generators, context managers and more for asyncio" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +async-generator = ">=1.3" + +[package.extras] +doc = ["sphinx-autodoc-typehints"] +test = ["pytest", "pytest-asyncio", "pytest-cov"] + +[[package]] +name = "asyncpraw" +version = "7.5.0" +description = "Async PRAW, an abbreviation for `Asynchronous Python Reddit API Wrapper`, is a python package that allows for simple access to reddit's API." +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +aiofiles = "<=0.6.0" +aiosqlite = "<=0.17.0" +asyncio-extras = "<=1.3.2" +asyncprawcore = ">=2.1,<3" +update-checker = ">=0.18" + +[package.extras] +ci = ["coveralls"] +dev = ["packaging", "pre-commit", "sphinx", "sphinx-rtd-theme", "sphinxcontrib-trio", "asynctest (>=0.13.0)", "mock (>=0.8)", "pytest (>=2.7.3)", "pytest-asyncio", "pytest-vcr", "testfixtures (>4.13.2,<7)", "vcrpy (==4.1.1)"] +lint = ["pre-commit", "sphinx", "sphinx-rtd-theme", "sphinxcontrib-trio"] +readthedocs = ["sphinx", "sphinx-rtd-theme", "sphinxcontrib-trio"] +test = ["asynctest (>=0.13.0)", "mock (>=0.8)", "pytest (>=2.7.3)", "pytest-asyncio", "pytest-vcr", "testfixtures (>4.13.2,<7)", "vcrpy (==4.1.1)"] + +[[package]] +name = "asyncprawcore" +version = "2.3.0" +description = "Low-level asynchronous communication layer for Async PRAW 7+." +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +aiohttp = "*" +yarl = "*" + +[package.extras] +ci = ["coveralls"] +dev = ["black", "flake8", "flynt", "pre-commit", "pydocstyle", "asynctest (>=0.13.0)", "mock (>=0.8)", "pytest", "pytest-vcr", "testfixtures (>4.13.2,<7)", "vcrpy (==4.0.2)"] +lint = ["black", "flake8", "flynt", "pre-commit", "pydocstyle"] +test = ["asynctest (>=0.13.0)", "mock (>=0.8)", "pytest", "pytest-vcr", "testfixtures (>4.13.2,<7)", "vcrpy (==4.0.2)"] + [[package]] name = "attrs" version = "21.4.0" @@ -51,6 +170,30 @@ docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "cloudpickle"] tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "cloudpickle"] +[[package]] +name = "blessed" +version = "1.19.1" +description = "Easy, practical library for making terminal apps, by providing an elegant, well-documented interface to Colors, Keyboard input, and screen Positioning capabilities." +category = "main" +optional = false +python-versions = ">=2.7" + +[package.dependencies] +jinxed = {version = ">=1.1.0", markers = "platform_system == \"Windows\""} +six = ">=1.9.0" +wcwidth = ">=0.1.4" + +[[package]] +name = "caio" +version = "0.9.5" +description = "Asynchronous file IO for Linux Posix and Windows." +category = "main" +optional = false +python-versions = ">=3.5.*, <4" + +[package.extras] +develop = ["aiomisc", "pytest", "pytest-cov"] + [[package]] name = "certifi" version = "2021.10.8" @@ -70,6 +213,25 @@ python-versions = ">=3.5.0" [package.extras] unicode_backport = ["unicodedata2"] +[[package]] +name = "codefind" +version = "0.1.3" +description = "Find code objects and their referents" +category = "main" +optional = false +python-versions = ">=3.8,<4.0" + +[[package]] +name = "commonmark" +version = "0.9.1" +description = "Python parser for the CommonMark Markdown spec" +category = "main" +optional = false +python-versions = "*" + +[package.extras] +test = ["flake8 (==3.7.8)", "hypothesis (==3.55.3)"] + [[package]] name = "dateparser" version = "1.1.1" @@ -90,33 +252,12 @@ fasttext = ["fasttext"] langdetect = ["langdetect"] [[package]] -name = "dis-snek" -version = "8.0.0" -description = "An API wrapper for Discord filled with snakes" +name = "distro" +version = "1.7.0" +description = "Distro - an OS platform information API" category = "main" optional = false -python-versions = ">=3.10" - -[package.dependencies] -aiohttp = "*" -attrs = "*" -discord-typings = "*" -tomli = "*" - -[package.extras] -all = ["PyNaCl (>=1.5.0,<1.6)", "yt-dlp"] -voice = ["PyNaCl (>=1.5.0,<1.6)", "yt-dlp"] - -[[package]] -name = "discord-typings" -version = "0.3.1" -description = "Maintained typings of payloads that Discord sends" -category = "main" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -typing_extensions = ">=4,<5" +python-versions = ">=3.6" [[package]] name = "frozenlist" @@ -126,6 +267,14 @@ category = "main" optional = false python-versions = ">=3.7" +[[package]] +name = "funcsigs" +version = "1.0.2" +description = "Python function signatures from PEP362 for Python 2.6, 2.7 and 3.2+" +category = "main" +optional = false +python-versions = "*" + [[package]] name = "gitdb" version = "4.0.9" @@ -158,26 +307,56 @@ python-versions = ">=3.5" [[package]] name = "jarvis-core" -version = "0.7.0" -description = "" +version = "0.9.2" +description = "JARVIS core" category = "main" optional = false python-versions = "^3.10" develop = false [package.dependencies] -dis-snek = "*" +aiohttp = "^3.8.1" motor = "^2.5.1" +nanoid = "^2.0.0" orjson = "^3.6.6" pytz = "^2022.1" PyYAML = "^6.0" +rich = "^12.3.0" umongo = "^3.1.0" [package.source] type = "git" url = "https://git.zevaryx.com/stark-industries/jarvis/jarvis-core.git" reference = "main" -resolved_reference = "15521e73fb84fb4282a484ff0c9bb88ed1d144ae" +resolved_reference = "83117c1b3c5540acadeac3005f4d8e69cbf743fc" + +[[package]] +name = "jinxed" +version = "1.1.0" +description = "Jinxed Terminal Library" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +ansicon = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "jurigged" +version = "0.5.0" +description = "Live update of Python functions" +category = "main" +optional = false +python-versions = ">=3.8,<4.0" + +[package.dependencies] +blessed = ">=1.17.12,<2.0.0" +codefind = ">=0.1.3,<0.2.0" +ovld = ">=0.3.1,<0.4.0" +watchdog = ">=1.0.2,<2.0.0" + +[package.extras] +develoop = ["giving (>=0.3.6,<0.4.0)", "rich (>=10.13.0,<11.0.0)", "hrepr (>=0.4.0,<0.5.0)"] [[package]] name = "marshmallow" @@ -196,17 +375,6 @@ docs = ["sphinx (==4.4.0)", "sphinx-issues (==3.0.1)", "alabaster (==0.7.12)", " lint = ["mypy (==0.940)", "flake8 (==4.0.1)", "flake8-bugbear (==22.1.11)", "pre-commit (>=2.4,<3.0)"] tests = ["pytest", "pytz", "simplejson"] -[[package]] -name = "molter" -version = "0.11.0" -description = "Shedding a new skin on Dis-Snek's commands." -category = "main" -optional = false -python-versions = ">=3.10" - -[package.dependencies] -dis-snek = ">=8.0.0" - [[package]] name = "mongoengine" version = "0.23.1" @@ -240,6 +408,14 @@ category = "main" optional = false python-versions = ">=3.7" +[[package]] +name = "nanoid" +version = "2.0.0" +description = "A tiny, secure, URL-friendly, unique string ID generator for Python" +category = "main" +optional = false +python-versions = "*" + [[package]] name = "numpy" version = "1.22.3" @@ -279,12 +455,20 @@ numpy = [ [[package]] name = "orjson" -version = "3.6.7" +version = "3.6.8" description = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy" category = "main" optional = false python-versions = ">=3.7" +[[package]] +name = "ovld" +version = "0.3.2" +description = "Overloading Python functions" +category = "main" +optional = false +python-versions = ">=3.6,<4.0" + [[package]] name = "packaging" version = "21.3" @@ -298,8 +482,8 @@ pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" [[package]] name = "pastypy" -version = "1.0.1" -description = "" +version = "1.0.2" +description = "Pasty API wrapper" category = "main" optional = false python-versions = ">=3.10" @@ -308,11 +492,11 @@ python-versions = ">=3.10" aiohttp = {version = "3.8.1", markers = "python_version >= \"3.6\""} aiosignal = {version = "1.2.0", markers = "python_version >= \"3.6\""} async-timeout = {version = "4.0.2", markers = "python_version >= \"3.6\""} -attrs = {version = "21.4.0", markers = "python_version >= \"3.6\""} +attrs = {version = "21.4.0", markers = "python_version >= \"3.6\" and python_full_version < \"3.0.0\" or python_full_version >= \"3.5.0\" and python_version >= \"3.6\""} certifi = {version = "2021.10.8", markers = "python_version >= \"2.7\" and python_full_version < \"3.0.0\" or python_full_version >= \"3.6.0\""} -charset-normalizer = {version = "2.0.12", markers = "python_full_version >= \"3.6.0\""} +charset-normalizer = {version = "2.0.12", markers = "python_full_version >= \"3.6.0\" and python_version >= \"3.6\""} frozenlist = {version = "1.3.0", markers = "python_version >= \"3.7\""} -idna = {version = "3.3", markers = "python_full_version >= \"3.6.0\""} +idna = {version = "3.3", markers = "python_version >= \"3.6\" and python_full_version < \"3.0.0\" or python_full_version >= \"3.6.0\" and python_version >= \"3.6\""} multidict = {version = "6.0.2", markers = "python_version >= \"3.7\""} pycryptodome = {version = "3.14.1", markers = "python_version >= \"2.7\" and python_full_version < \"3.0.0\" or python_full_version >= \"3.5.0\""} requests = {version = "2.27.1", markers = "python_version >= \"2.7\" and python_full_version < \"3.0.0\" or python_full_version >= \"3.6.0\""} @@ -321,12 +505,24 @@ yarl = {version = "1.7.2", markers = "python_version >= \"3.6\""} [[package]] name = "pillow" -version = "9.0.1" +version = "9.1.0" description = "Python Imaging Library (Fork)" category = "main" optional = false python-versions = ">=3.7" +[package.extras] +docs = ["olefile", "sphinx (>=2.4)", "sphinx-copybutton", "sphinx-issues (>=3.0.1)", "sphinx-removed-in", "sphinx-rtd-theme (>=1.0)", "sphinxext-opengraph"] +tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"] + +[[package]] +name = "protobuf" +version = "3.20.1" +description = "Protocol Buffers" +category = "main" +optional = false +python-versions = ">=3.7" + [[package]] name = "psutil" version = "5.9.0" @@ -346,6 +542,14 @@ category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +[[package]] +name = "pygments" +version = "2.12.0" +description = "Pygments is a syntax highlighting package written in Python." +category = "main" +optional = false +python-versions = ">=3.6" + [[package]] name = "pymongo" version = "3.12.3" @@ -366,14 +570,14 @@ zstd = ["zstandard"] [[package]] name = "pyparsing" -version = "3.0.7" -description = "Python parsing module" +version = "3.0.8" +description = "pyparsing module - Classes and methods to define and execute parsing grammars" category = "main" optional = false -python-versions = ">=3.6" +python-versions = ">=3.6.8" [package.extras] -diagrams = ["jinja2", "railroad-diagrams"] +diagrams = ["railroad-diagrams", "jinja2"] [[package]] name = "python-dateutil" @@ -388,7 +592,7 @@ six = ">=1.5" [[package]] name = "python-gitlab" -version = "3.2.0" +version = "3.4.0" description = "Interact with GitLab API" category = "main" optional = false @@ -481,6 +685,41 @@ python-versions = "*" [package.dependencies] requests = ">=2.0.1,<3.0.0" +[[package]] +name = "rich" +version = "12.3.0" +description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" +category = "main" +optional = false +python-versions = ">=3.6.3,<4.0.0" + +[package.dependencies] +commonmark = ">=0.9.0,<0.10.0" +pygments = ">=2.6.0,<3.0.0" + +[package.extras] +jupyter = ["ipywidgets (>=7.5.1,<8.0.0)"] + +[[package]] +name = "rook" +version = "0.1.171" +description = "Rook is a Python package for on the fly debugging and data extraction for application in production" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +certifi = "*" +distro = "*" +funcsigs = "*" +protobuf = {version = ">=3.7.1,<=4.0.0", markers = "python_version > \"3.0\""} +psutil = ">=5.8.0" +six = ">=1.13" +websocket-client = ">=0.56,<0.58 || >0.58,<0.59 || >0.59,<1.0 || >1.0,<1.1 || >1.1" + +[package.extras] +ssl_backport = ["backports.ssl", "backports.ssl-match-hostname", "pyopenssl"] + [[package]] name = "six" version = "1.16.0" @@ -497,17 +736,9 @@ category = "main" optional = false python-versions = ">=3.6" -[[package]] -name = "tomli" -version = "2.0.1" -description = "A lil' TOML parser" -category = "main" -optional = false -python-versions = ">=3.7" - [[package]] name = "tweepy" -version = "4.7.0" +version = "4.8.0" description = "Twitter library for Python" category = "main" optional = false @@ -526,11 +757,11 @@ test = ["vcrpy (>=1.10.3)"] [[package]] name = "typing-extensions" -version = "4.1.1" -description = "Backported and Experimental Type Hints for Python 3.6+" +version = "4.2.0" +description = "Backported and Experimental Type Hints for Python 3.7+" category = "main" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [[package]] name = "tzdata" @@ -542,7 +773,7 @@ python-versions = ">=2" [[package]] name = "tzlocal" -version = "4.1" +version = "4.2" description = "tzinfo object for the local timezone" category = "main" optional = false @@ -581,6 +812,22 @@ mongomock = ["mongomock"] motor = ["motor (>=2.0,<3.0)"] txmongo = ["txmongo (>=19.2.0)"] +[[package]] +name = "update-checker" +version = "0.18.0" +description = "A python module that will check for package updates." +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +requests = ">=2.3.0" + +[package.extras] +dev = ["black", "flake8", "pytest (>=2.7.3)"] +lint = ["black", "flake8"] +test = ["pytest (>=2.7.3)"] + [[package]] name = "urllib3" version = "1.26.8" @@ -594,6 +841,38 @@ brotli = ["brotlipy (>=0.6.0)"] secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] +[[package]] +name = "watchdog" +version = "1.0.2" +description = "Filesystem events monitoring" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.extras] +watchmedo = ["PyYAML (>=3.10)", "argh (>=0.24.1)"] + +[[package]] +name = "wcwidth" +version = "0.2.5" +description = "Measures the displayed width of unicode strings in a terminal" +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "websocket-client" +version = "1.3.2" +description = "WebSocket client for Python with low level API options" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.extras] +docs = ["Sphinx (>=3.4)", "sphinx-rtd-theme (>=0.5)"] +optional = ["python-socks", "wsaccel"] +test = ["websockets"] + [[package]] name = "yarl" version = "1.7.2" @@ -609,9 +888,17 @@ multidict = ">=4.0" [metadata] lock-version = "1.1" python-versions = "^3.10" -content-hash = "af521f2487cac903d3766516484dcfdf999d222abacb1a8233248489591f6a34" +content-hash = "8bb2b59de1ccb8f5e5588ae3ac600e7fb6d7f638224c9cc24228f79e666aec63" [metadata.files] +aiofile = [ + {file = "aiofile-3.7.4-py3-none-any.whl", hash = "sha256:0e2a524e4714efda47ce8964b13d4da94cf553411f9f6da813df615a4cd73d95"}, + {file = "aiofile-3.7.4.tar.gz", hash = "sha256:0aefa1d91d000d3a20a515d153db2ebf713076c7c94edf2fca85d3d83316abc5"}, +] +aiofiles = [ + {file = "aiofiles-0.6.0-py3-none-any.whl", hash = "sha256:bd3019af67f83b739f8e4053c6c0512a7f545b9a8d91aaeab55e6e0f9d123c27"}, + {file = "aiofiles-0.6.0.tar.gz", hash = "sha256:e0281b157d3d5d59d803e3f4557dcc9a3dff28a4dd4829a9ff478adae50ca092"}, +] aiohttp = [ {file = "aiohttp-3.8.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:1ed0b6477896559f17b9eaeb6d38e07f7f9ffe40b9f0f9627ae8b9926ae260a8"}, {file = "aiohttp-3.8.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7dadf3c307b31e0e61689cbf9e06be7a867c563d5a63ce9dca578f956609abf8"}, @@ -686,18 +973,67 @@ aiohttp = [ {file = "aiohttp-3.8.1-cp39-cp39-win_amd64.whl", hash = "sha256:1c182cb873bc91b411e184dab7a2b664d4fea2743df0e4d57402f7f3fa644bac"}, {file = "aiohttp-3.8.1.tar.gz", hash = "sha256:fc5471e1a54de15ef71c1bc6ebe80d4dc681ea600e68bfd1cbce40427f0b7578"}, ] +aioredis = [ + {file = "aioredis-2.0.1-py3-none-any.whl", hash = "sha256:9ac0d0b3b485d293b8ca1987e6de8658d7dafcca1cddfcd1d506cae8cdebfdd6"}, + {file = "aioredis-2.0.1.tar.gz", hash = "sha256:eaa51aaf993f2d71f54b70527c440437ba65340588afeb786cd87c55c89cd98e"}, +] aiosignal = [ {file = "aiosignal-1.2.0-py3-none-any.whl", hash = "sha256:26e62109036cd181df6e6ad646f91f0dcfd05fe16d0cb924138ff2ab75d64e3a"}, {file = "aiosignal-1.2.0.tar.gz", hash = "sha256:78ed67db6c7b7ced4f98e495e572106d5c432a93e1ddd1bf475e1dc05f5b7df2"}, ] +aiosqlite = [ + {file = "aiosqlite-0.17.0-py3-none-any.whl", hash = "sha256:6c49dc6d3405929b1d08eeccc72306d3677503cc5e5e43771efc1e00232e8231"}, + {file = "aiosqlite-0.17.0.tar.gz", hash = "sha256:f0e6acc24bc4864149267ac82fb46dfb3be4455f99fe21df82609cc6e6baee51"}, +] +ansicon = [ + {file = "ansicon-1.89.0-py2.py3-none-any.whl", hash = "sha256:f1def52d17f65c2c9682cf8370c03f541f410c1752d6a14029f97318e4b9dfec"}, + {file = "ansicon-1.89.0.tar.gz", hash = "sha256:e4d039def5768a47e4afec8e89e83ec3ae5a26bf00ad851f914d1240b444d2b1"}, +] +async-generator = [ + {file = "async_generator-1.10-py3-none-any.whl", hash = "sha256:01c7bf666359b4967d2cda0000cc2e4af16a0ae098cbffcb8472fb9e8ad6585b"}, + {file = "async_generator-1.10.tar.gz", hash = "sha256:6ebb3d106c12920aaae42ccb6f787ef5eefdcdd166ea3d628fa8476abe712144"}, +] async-timeout = [ {file = "async-timeout-4.0.2.tar.gz", hash = "sha256:2163e1640ddb52b7a8c80d0a67a08587e5d245cc9c553a74a847056bc2976b15"}, {file = "async_timeout-4.0.2-py3-none-any.whl", hash = "sha256:8ca1e4fcf50d07413d66d1a5e416e42cfdf5851c981d679a09851a6853383b3c"}, ] +asyncio-extras = [ + {file = "asyncio_extras-1.3.2-py3-none-any.whl", hash = "sha256:839568ba07c3470c9aa2c441aa2417c108f7d3755862bc2bd39d69b524303993"}, + {file = "asyncio_extras-1.3.2.tar.gz", hash = "sha256:084b62bebc19c6ba106d438a274bbb5566941c469128cd4af1a85f00a2c81f8d"}, +] +asyncpraw = [ + {file = "asyncpraw-7.5.0-py3-none-any.whl", hash = "sha256:b40f3db3464077a7a7e30a89181ba15ba4c5bc550dc2642e815b235f42ad8eb2"}, + {file = "asyncpraw-7.5.0.tar.gz", hash = "sha256:61aabf05052472d8b29e0f0500a6ec8b483129374d36dad286d94e4b6864572d"}, +] +asyncprawcore = [ + {file = "asyncprawcore-2.3.0-py3-none-any.whl", hash = "sha256:46c52e6cfe91801a8c9490a0ee29a85cbc6713ccc535d5c704d448aee9729e5b"}, + {file = "asyncprawcore-2.3.0.tar.gz", hash = "sha256:2a4a2d1ca7f78c8fa7d4903e6bd18cfe96742ad1f167b59473f64be0e7060d5d"}, +] attrs = [ {file = "attrs-21.4.0-py2.py3-none-any.whl", hash = "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4"}, {file = "attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"}, ] +blessed = [ + {file = "blessed-1.19.1-py2.py3-none-any.whl", hash = "sha256:63b8554ae2e0e7f43749b6715c734cc8f3883010a809bf16790102563e6cf25b"}, + {file = "blessed-1.19.1.tar.gz", hash = "sha256:9a0d099695bf621d4680dd6c73f6ad547f6a3442fbdbe80c4b1daa1edbc492fc"}, +] +caio = [ + {file = "caio-0.9.5-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:cd1c20aab04c18f0534b3f0b59103a94dede3c7d7b43c9cc525df3980b4c7c54"}, + {file = "caio-0.9.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:dd316270757d77f384c97e336588267e7942c1f1492a3a2e07b9a80dca027538"}, + {file = "caio-0.9.5-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:013aa374158c5074b3c65a0da6b9c6b20a987d85fb317dd077b045e84e2478e1"}, + {file = "caio-0.9.5-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:d767faf537a9ea774e8408ba15a0f1dc734f06857c2d28bdf4258a63b5885f42"}, + {file = "caio-0.9.5-cp36-cp36m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:97d9a10522a8a25798229fc1113cfaba3832b1cd0c1a3648b009b9740ef5e054"}, + {file = "caio-0.9.5-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:4fe9eff5cf7a2d6f3f418aeeccd11ce9a38329e07527b6f52da085edb44bc2fd"}, + {file = "caio-0.9.5-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:b8b6be369139edd678817dc0a313392d710f66fb521c275dce0a9067089b346b"}, + {file = "caio-0.9.5-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:3ffc6259239e03962f9e14829e02795ca9d196eedf32fe61688ba6ed33da46c8"}, + {file = "caio-0.9.5-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:7a19dfdec6736affb645da233a6007c2590678490d2a1e0f1fb82a696c0a1ddf"}, + {file = "caio-0.9.5-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:15f70d27e1009d279e4f9ff86290aad00b0511ce82a1879c40745244f0a9ec92"}, + {file = "caio-0.9.5-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:0427a58c1814a091bfbb84318d344fdb9a68f3d49adc74e5fdc7bc9478e1e4fe"}, + {file = "caio-0.9.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7f48fa58e5f699b428f1fd85e394ecec05be4048fcaf1fdf1981b748cd1e03a6"}, + {file = "caio-0.9.5-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:01061288391020f28e1ab8b0437420f7fe1e0ecc29b4107f7a8dcf7789f33b22"}, + {file = "caio-0.9.5-py3-none-any.whl", hash = "sha256:3c74d84dff2bec5f93685cf2f32eb22e4cc5663434a9be5f4a759247229b69b3"}, + {file = "caio-0.9.5.tar.gz", hash = "sha256:167d9342a807bae441b2e88f9ecb62da2f236b319939a8679f68f510a0194c40"}, +] certifi = [ {file = "certifi-2021.10.8-py2.py3-none-any.whl", hash = "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569"}, {file = "certifi-2021.10.8.tar.gz", hash = "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872"}, @@ -706,17 +1042,21 @@ charset-normalizer = [ {file = "charset-normalizer-2.0.12.tar.gz", hash = "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597"}, {file = "charset_normalizer-2.0.12-py3-none-any.whl", hash = "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df"}, ] +codefind = [ + {file = "codefind-0.1.3-py3-none-any.whl", hash = "sha256:3ffe85b74595b5c9f82391a11171ce7d68f1f555485720ab922f3b86f9bf30ec"}, + {file = "codefind-0.1.3.tar.gz", hash = "sha256:5667050361bf601a253031b2437d16b7d82cb0fa0e756d93e548c7b35ce6f910"}, +] +commonmark = [ + {file = "commonmark-0.9.1-py2.py3-none-any.whl", hash = "sha256:da2f38c92590f83de410ba1a3cbceafbc74fee9def35f9251ba9a971d6d66fd9"}, + {file = "commonmark-0.9.1.tar.gz", hash = "sha256:452f9dc859be7f06631ddcb328b6919c67984aca654e5fefb3914d54691aed60"}, +] dateparser = [ {file = "dateparser-1.1.1-py2.py3-none-any.whl", hash = "sha256:9600874312ff28a41f96ec7ccdc73be1d1c44435719da47fea3339d55ff5a628"}, {file = "dateparser-1.1.1.tar.gz", hash = "sha256:038196b1f12c7397e38aad3d61588833257f6f552baa63a1499e6987fa8d42d9"}, ] -dis-snek = [ - {file = "dis-snek-8.0.0.tar.gz", hash = "sha256:c035a4f664f9a638b80089f2a9a3330a4254fc227ef2c83c96582df06f392281"}, - {file = "dis_snek-8.0.0-py3-none-any.whl", hash = "sha256:3a89c8f78c27407fb67d42dfaa51be6a507306306779e45cd47687bd846b3b23"}, -] -discord-typings = [ - {file = "discord-typings-0.3.1.tar.gz", hash = "sha256:854cfb66d34edad49b36d8aaffc93179bb397a97c81caba2da02896e72821a74"}, - {file = "discord_typings-0.3.1-py3-none-any.whl", hash = "sha256:65890c467751daa025dcef15683c32160f07427baf83380cfdf11d84ceec980a"}, +distro = [ + {file = "distro-1.7.0-py3-none-any.whl", hash = "sha256:d596311d707e692c2160c37807f83e3820c5d539d5a83e87cfb6babd8ba3a06b"}, + {file = "distro-1.7.0.tar.gz", hash = "sha256:151aeccf60c216402932b52e40ee477a939f8d58898927378a02abbe852c1c39"}, ] frozenlist = [ {file = "frozenlist-1.3.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d2257aaba9660f78c7b1d8fea963b68f3feffb1a9d5d05a18401ca9eb3e8d0a3"}, @@ -779,6 +1119,10 @@ frozenlist = [ {file = "frozenlist-1.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:772965f773757a6026dea111a15e6e2678fbd6216180f82a48a40b27de1ee2ab"}, {file = "frozenlist-1.3.0.tar.gz", hash = "sha256:ce6f2ba0edb7b0c1d8976565298ad2deba6f8064d2bebb6ffce2ca896eb35b0b"}, ] +funcsigs = [ + {file = "funcsigs-1.0.2-py2.py3-none-any.whl", hash = "sha256:330cc27ccbf7f1e992e69fef78261dc7c6569012cf397db8d3de0234e6c937ca"}, + {file = "funcsigs-1.0.2.tar.gz", hash = "sha256:a7bb0f2cf3a3fd1ab2732cb49eba4252c2af4240442415b4abce3b87022a8f50"}, +] gitdb = [ {file = "gitdb-4.0.9-py3-none-any.whl", hash = "sha256:8033ad4e853066ba6ca92050b9df2f89301b8fc8bf7e9324d412a63f8bf1a8fd"}, {file = "gitdb-4.0.9.tar.gz", hash = "sha256:bac2fd45c0a1c9cf619e63a90d62bdc63892ef92387424b855792a6cabe789aa"}, @@ -792,14 +1136,18 @@ idna = [ {file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"}, ] jarvis-core = [] +jinxed = [ + {file = "jinxed-1.1.0-py2.py3-none-any.whl", hash = "sha256:6a61ccf963c16aa885304f27e6e5693783676897cea0c7f223270c8b8e78baf8"}, + {file = "jinxed-1.1.0.tar.gz", hash = "sha256:d8f1731f134e9e6b04d95095845ae6c10eb15cb223a5f0cabdea87d4a279c305"}, +] +jurigged = [ + {file = "jurigged-0.5.0-py3-none-any.whl", hash = "sha256:28d86ca6d97669bc183773f7537e59f50fdd36e7637092fc2451b91bcc935d62"}, + {file = "jurigged-0.5.0.tar.gz", hash = "sha256:f23c3536b1654d2618d6e6b34f0752acf377c1b35283889d3a28663a7b1f72cb"}, +] marshmallow = [ {file = "marshmallow-3.15.0-py3-none-any.whl", hash = "sha256:ff79885ed43b579782f48c251d262e062bce49c65c52412458769a4fb57ac30f"}, {file = "marshmallow-3.15.0.tar.gz", hash = "sha256:2aaaab4f01ef4f5a011a21319af9fce17ab13bf28a026d1252adab0e035648d5"}, ] -molter = [ - {file = "molter-0.11.0-py3-none-any.whl", hash = "sha256:4ae311e34fc93bfa37643f86c382b1f104753e451e9904995f0f34f5edda8daa"}, - {file = "molter-0.11.0.tar.gz", hash = "sha256:1e06e021a00986b9218e67bce062cb52eab5c86e8187b28e68f7dca8df853aaa"}, -] mongoengine = [ {file = "mongoengine-0.23.1-py3-none-any.whl", hash = "sha256:3d1c8b9f5d43144bd726a3f01e58d2831c6fb112960a4a60b3a26fa85e026ab3"}, {file = "mongoengine-0.23.1.tar.gz", hash = "sha256:de275e70cd58891dc46eef43369c522ce450dccb6d6f1979cbc9b93e6bdaf6cb"}, @@ -869,6 +1217,10 @@ multidict = [ {file = "multidict-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:4bae31803d708f6f15fd98be6a6ac0b6958fcf68fda3c77a048a4f9073704aae"}, {file = "multidict-6.0.2.tar.gz", hash = "sha256:5ff3bd75f38e4c43f1f470f2df7a4d430b821c4ce22be384e1459cb57d6bb013"}, ] +nanoid = [ + {file = "nanoid-2.0.0-py3-none-any.whl", hash = "sha256:90aefa650e328cffb0893bbd4c236cfd44c48bc1f2d0b525ecc53c3187b653bb"}, + {file = "nanoid-2.0.0.tar.gz", hash = "sha256:5a80cad5e9c6e9ae3a41fa2fb34ae189f7cb420b2a5d8f82bd9d23466e4efa68"}, +] numpy = [ {file = "numpy-1.22.3-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:92bfa69cfbdf7dfc3040978ad09a48091143cffb778ec3b03fa170c494118d75"}, {file = "numpy-1.22.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8251ed96f38b47b4295b1ae51631de7ffa8260b5b087808ef09a39a9d66c97ab"}, @@ -905,83 +1257,116 @@ opencv-python = [ {file = "opencv_python-4.5.5.64-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:7787bb017ae93d5f9bb1b817ac8e13e45dd193743cb648498fcab21d00cf20a3"}, ] orjson = [ - {file = "orjson-3.6.7-cp310-cp310-macosx_10_7_x86_64.whl", hash = "sha256:93188a9d6eb566419ad48befa202dfe7cd7a161756444b99c4ec77faea9352a4"}, - {file = "orjson-3.6.7-cp310-cp310-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:82515226ecb77689a029061552b5df1802b75d861780c401e96ca6bc8495f775"}, - {file = "orjson-3.6.7-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3af57ffab7848aaec6ba6b9e9b41331250b57bf696f9d502bacdc71a0ebab0ba"}, - {file = "orjson-3.6.7-cp310-cp310-manylinux_2_24_aarch64.whl", hash = "sha256:a7297504d1142e7efa236ffc53f056d73934a993a08646dbcee89fc4308a8fcf"}, - {file = "orjson-3.6.7-cp310-cp310-manylinux_2_24_x86_64.whl", hash = "sha256:5a50cde0dbbde255ce751fd1bca39d00ecd878ba0903c0480961b31984f2fab7"}, - {file = "orjson-3.6.7-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d21f9a2d1c30e58070f93988db4cad154b9009fafbde238b52c1c760e3607fbe"}, - {file = "orjson-3.6.7-cp310-none-win_amd64.whl", hash = "sha256:e152464c4606b49398afd911777decebcf9749cc8810c5b4199039e1afb0991e"}, - {file = "orjson-3.6.7-cp37-cp37m-macosx_10_7_x86_64.whl", hash = "sha256:0a65f3c403f38b0117c6dd8e76e85a7bd51fcd92f06c5598dfeddbc44697d3e5"}, - {file = "orjson-3.6.7-cp37-cp37m-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:6c47cfca18e41f7f37b08ff3e7abf5ada2d0f27b5ade934f05be5fc5bb956e9d"}, - {file = "orjson-3.6.7-cp37-cp37m-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:63185af814c243fad7a72441e5f98120c9ecddf2675befa486d669fb65539e9b"}, - {file = "orjson-3.6.7-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b2da6fde42182b80b40df2e6ab855c55090ebfa3fcc21c182b7ad1762b61d55c"}, - {file = "orjson-3.6.7-cp37-cp37m-manylinux_2_24_aarch64.whl", hash = "sha256:48c5831ec388b4e2682d4ff56d6bfa4a2ef76c963f5e75f4ff4785f9cf338a80"}, - {file = "orjson-3.6.7-cp37-cp37m-manylinux_2_24_x86_64.whl", hash = "sha256:913fac5d594ccabf5e8fbac15b9b3bb9c576d537d49eeec9f664e7a64dde4c4b"}, - {file = "orjson-3.6.7-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:58f244775f20476e5851e7546df109f75160a5178d44257d437ba6d7e562bfe8"}, - {file = "orjson-3.6.7-cp37-none-win_amd64.whl", hash = "sha256:2d5f45c6b85e5f14646df2d32ecd7ff20fcccc71c0ea1155f4d3df8c5299bbb7"}, - {file = "orjson-3.6.7-cp38-cp38-macosx_10_7_x86_64.whl", hash = "sha256:612d242493afeeb2068bc72ff2544aa3b1e627578fcf92edee9daebb5893ffea"}, - {file = "orjson-3.6.7-cp38-cp38-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:539cdc5067db38db27985e257772d073cd2eb9462d0a41bde96da4e4e60bd99b"}, - {file = "orjson-3.6.7-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6d103b721bbc4f5703f62b3882e638c0b65fcdd48622531c7ffd45047ef8e87c"}, - {file = "orjson-3.6.7-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb10a20f80e95102dd35dfbc3a22531661b44a09b55236b012a446955846b023"}, - {file = "orjson-3.6.7-cp38-cp38-manylinux_2_24_aarch64.whl", hash = "sha256:bb68d0da349cf8a68971a48ad179434f75256159fe8b0715275d9b49fa23b7a3"}, - {file = "orjson-3.6.7-cp38-cp38-manylinux_2_24_x86_64.whl", hash = "sha256:4a2c7d0a236aaeab7f69c17b7ab4c078874e817da1bfbb9827cb8c73058b3050"}, - {file = "orjson-3.6.7-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:3be045ca3b96119f592904cf34b962969ce97bd7843cbfca084009f6c8d2f268"}, - {file = "orjson-3.6.7-cp38-none-win_amd64.whl", hash = "sha256:bd765c06c359d8a814b90f948538f957fa8a1f55ad1aaffcdc5771996aaea061"}, - {file = "orjson-3.6.7-cp39-cp39-macosx_10_7_x86_64.whl", hash = "sha256:7dd9e1e46c0776eee9e0649e3ae9584ea368d96851bcaeba18e217fa5d755283"}, - {file = "orjson-3.6.7-cp39-cp39-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:c4b4f20a1e3df7e7c83717aff0ef4ab69e42ce2fb1f5234682f618153c458406"}, - {file = "orjson-3.6.7-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7107a5673fd0b05adbb58bf71c1578fc84d662d29c096eb6d998982c8635c221"}, - {file = "orjson-3.6.7-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a08b6940dd9a98ccf09785890112a0f81eadb4f35b51b9a80736d1725437e22c"}, - {file = "orjson-3.6.7-cp39-cp39-manylinux_2_24_aarch64.whl", hash = "sha256:f5d1648e5a9d1070f3628a69a7c6c17634dbb0caf22f2085eca6910f7427bf1f"}, - {file = "orjson-3.6.7-cp39-cp39-manylinux_2_24_x86_64.whl", hash = "sha256:e6201494e8dff2ce7fd21da4e3f6dfca1a3fed38f9dcefc972f552f6596a7621"}, - {file = "orjson-3.6.7-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:70d0386abe02879ebaead2f9632dd2acb71000b4721fd8c1a2fb8c031a38d4d5"}, - {file = "orjson-3.6.7-cp39-none-win_amd64.whl", hash = "sha256:d9a3288861bfd26f3511fb4081561ca768674612bac59513cb9081bb61fcc87f"}, - {file = "orjson-3.6.7.tar.gz", hash = "sha256:a4bb62b11289b7620eead2f25695212e9ac77fcfba76f050fa8a540fb5c32401"}, + {file = "orjson-3.6.8-cp310-cp310-macosx_10_7_x86_64.whl", hash = "sha256:3a287a650458de2211db03681b71c3e5cb2212b62f17a39df8ad99fc54855d0f"}, + {file = "orjson-3.6.8-cp310-cp310-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:5204e25c12cea58e524fc82f7c27ed0586f592f777b33075a92ab7b3eb3687c2"}, + {file = "orjson-3.6.8-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:77e8386393add64f959c044e0fb682364fd0e611a6f477aa13f0e6a733bd6a28"}, + {file = "orjson-3.6.8-cp310-cp310-manylinux_2_24_aarch64.whl", hash = "sha256:279f2d2af393fdf8601020744cb206b91b54ad60fb8401e0761819c7bda1f4e4"}, + {file = "orjson-3.6.8-cp310-cp310-manylinux_2_24_x86_64.whl", hash = "sha256:c31c9f389be7906f978ed4192eb58a4b74a37ad60556a0b88ddc47c576697770"}, + {file = "orjson-3.6.8-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0db5c5a0c5b89f092d52f6e5a3701660a9d6ffa9e2968b3ce17c2bc4f5eb0414"}, + {file = "orjson-3.6.8-cp310-none-win_amd64.whl", hash = "sha256:eb22485847b9a0c4bbedc668df860126ac931edbed1d456cf41a59f3cb961ed8"}, + {file = "orjson-3.6.8-cp37-cp37m-macosx_10_7_x86_64.whl", hash = "sha256:1a5fe569310bc819279bd4d5f2c349910b104ed3207936246dd5d5e0b085e74a"}, + {file = "orjson-3.6.8-cp37-cp37m-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:ccb356a47ab1067cd3549847e9db1d279a63fe0482d315b3ffd6e7abef35ef77"}, + {file = "orjson-3.6.8-cp37-cp37m-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ab29c069c222248ce302a25855b4e1664f9436e8ae5a131fb0859daf31676d2b"}, + {file = "orjson-3.6.8-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d2b5e4cba9e774ac011071d9d27760f97f4b8cd46003e971d122e712f971345"}, + {file = "orjson-3.6.8-cp37-cp37m-manylinux_2_24_aarch64.whl", hash = "sha256:c311ec504414d22834d5b972a209619925b48263856a11a14d90230f9682d49c"}, + {file = "orjson-3.6.8-cp37-cp37m-manylinux_2_24_x86_64.whl", hash = "sha256:a3dfec7950b90fb8d143743503ee53fa06b32e6068bdea792fc866284da3d71d"}, + {file = "orjson-3.6.8-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:b890dbbada2cbb26eb29bd43a848426f007f094bb0758df10dfe7a438e1cb4b4"}, + {file = "orjson-3.6.8-cp37-none-win_amd64.whl", hash = "sha256:9143ae2c52771525be9ad11a7a8cc8e7fd75391b107e7e644a9e0050496f6b4f"}, + {file = "orjson-3.6.8-cp38-cp38-macosx_10_7_x86_64.whl", hash = "sha256:33a82199fd42f6436f833e210ae5129c922a5c355629356ca7a8e82964da7285"}, + {file = "orjson-3.6.8-cp38-cp38-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:90159ea8b9a5a2a98fa33dc7b421cfac4d2ae91ba5e1058f5909e7f059f6b467"}, + {file = "orjson-3.6.8-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:656fbe15d9ef0733e740d9def78f4fdb4153102f4836ee774a05123499005931"}, + {file = "orjson-3.6.8-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7be3be6153843e0f01351b1313a5ad4723595427680dac2dfff22a37e652ce02"}, + {file = "orjson-3.6.8-cp38-cp38-manylinux_2_24_aarch64.whl", hash = "sha256:dd24f66b6697ee7424f7da575ec6cbffc8ede441114d53470949cda4d97c6e56"}, + {file = "orjson-3.6.8-cp38-cp38-manylinux_2_24_x86_64.whl", hash = "sha256:b07c780f7345ecf5901356dc21dee0669defc489c38ce7b9ab0f5e008cc0385c"}, + {file = "orjson-3.6.8-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:ea32015a5d8a4ce00d348a0de5dc7040e0ad58f970a8fcbb5713a1eac129e493"}, + {file = "orjson-3.6.8-cp38-none-win_amd64.whl", hash = "sha256:c5a3e382194c838988ec128a26b08aa92044e5e055491cc4056142af0c1c54d7"}, + {file = "orjson-3.6.8-cp39-cp39-macosx_10_7_x86_64.whl", hash = "sha256:83a8424e857ae1bf53530e88b4eb2f16ca2b489073b924e655f1575cacd7f52a"}, + {file = "orjson-3.6.8-cp39-cp39-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:81e1a6a2d67f15007dadacbf9ba5d3d79237e5e33786c028557fe5a2b72f1c9a"}, + {file = "orjson-3.6.8-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:137b539881c77866eba86ff6a11df910daf2eb9ab8f1acae62f879e83d7c38af"}, + {file = "orjson-3.6.8-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2cbd358f3b3ad539a27e36900e8e7d172d0e1b72ad9dd7d69544dcbc0f067ee7"}, + {file = "orjson-3.6.8-cp39-cp39-manylinux_2_24_aarch64.whl", hash = "sha256:6ab94701542d40b90903ecfc339333f458884979a01cb9268bc662cc67a5f6d8"}, + {file = "orjson-3.6.8-cp39-cp39-manylinux_2_24_x86_64.whl", hash = "sha256:32b6f26593a9eb606b40775826beb0dac152e3d224ea393688fced036045a821"}, + {file = "orjson-3.6.8-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:afd9e329ebd3418cac3cd747769b1d52daa25fa672bbf414ab59f0e0881b32b9"}, + {file = "orjson-3.6.8-cp39-none-win_amd64.whl", hash = "sha256:0c89b419914d3d1f65a1b0883f377abe42a6e44f6624ba1c63e8846cbfc2fa60"}, + {file = "orjson-3.6.8.tar.gz", hash = "sha256:e19d23741c5de13689bb316abfccea15a19c264e3ec8eb332a5319a583595ace"}, +] +ovld = [ + {file = "ovld-0.3.2-py3-none-any.whl", hash = "sha256:3a5f08f66573198b490fc69dcf93a2ad9b4d90fd1fef885cf7a8dbe565f17837"}, + {file = "ovld-0.3.2.tar.gz", hash = "sha256:f8918636c240a2935175406801944d4314823710b3afbd5a8db3e79cd9391c42"}, ] packaging = [ {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, ] pastypy = [ - {file = "pastypy-1.0.1-py3-none-any.whl", hash = "sha256:63cc664568f86f6ddeb7e5687422bbf4b338d067ea887ed240223c8cbcf6fd2d"}, - {file = "pastypy-1.0.1.tar.gz", hash = "sha256:0393d1635b5031170eae3efaf376b14c3a4af7737c778d7ba7d56f2bd25bf5b1"}, + {file = "pastypy-1.0.2-py3-none-any.whl", hash = "sha256:4476e47b5e52600a4d69c58cbbba2c5d42458f552ccfc2854d5fe97a119dcc20"}, + {file = "pastypy-1.0.2.tar.gz", hash = "sha256:81e0c4a65ec40c85d62685627b64d26397304ac91d68ddc80f833974504c13b8"}, ] pillow = [ - {file = "Pillow-9.0.1-1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a5d24e1d674dd9d72c66ad3ea9131322819ff86250b30dc5821cbafcfa0b96b4"}, - {file = "Pillow-9.0.1-1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2632d0f846b7c7600edf53c48f8f9f1e13e62f66a6dbc15191029d950bfed976"}, - {file = "Pillow-9.0.1-1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b9618823bd237c0d2575283f2939655f54d51b4527ec3972907a927acbcc5bfc"}, - {file = "Pillow-9.0.1-cp310-cp310-macosx_10_10_universal2.whl", hash = "sha256:9bfdb82cdfeccec50aad441afc332faf8606dfa5e8efd18a6692b5d6e79f00fd"}, - {file = "Pillow-9.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5100b45a4638e3c00e4d2320d3193bdabb2d75e79793af7c3eb139e4f569f16f"}, - {file = "Pillow-9.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:528a2a692c65dd5cafc130de286030af251d2ee0483a5bf50c9348aefe834e8a"}, - {file = "Pillow-9.0.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f29d831e2151e0b7b39981756d201f7108d3d215896212ffe2e992d06bfe049"}, - {file = "Pillow-9.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:855c583f268edde09474b081e3ddcd5cf3b20c12f26e0d434e1386cc5d318e7a"}, - {file = "Pillow-9.0.1-cp310-cp310-win32.whl", hash = "sha256:d9d7942b624b04b895cb95af03a23407f17646815495ce4547f0e60e0b06f58e"}, - {file = "Pillow-9.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:81c4b81611e3a3cb30e59b0cf05b888c675f97e3adb2c8672c3154047980726b"}, - {file = "Pillow-9.0.1-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:413ce0bbf9fc6278b2d63309dfeefe452835e1c78398efb431bab0672fe9274e"}, - {file = "Pillow-9.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:80fe64a6deb6fcfdf7b8386f2cf216d329be6f2781f7d90304351811fb591360"}, - {file = "Pillow-9.0.1-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cef9c85ccbe9bee00909758936ea841ef12035296c748aaceee535969e27d31b"}, - {file = "Pillow-9.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1d19397351f73a88904ad1aee421e800fe4bbcd1aeee6435fb62d0a05ccd1030"}, - {file = "Pillow-9.0.1-cp37-cp37m-win32.whl", hash = "sha256:d21237d0cd37acded35154e29aec853e945950321dd2ffd1a7d86fe686814669"}, - {file = "Pillow-9.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:ede5af4a2702444a832a800b8eb7f0a7a1c0eed55b644642e049c98d589e5092"}, - {file = "Pillow-9.0.1-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:b5b3f092fe345c03bca1e0b687dfbb39364b21ebb8ba90e3fa707374b7915204"}, - {file = "Pillow-9.0.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:335ace1a22325395c4ea88e00ba3dc89ca029bd66bd5a3c382d53e44f0ccd77e"}, - {file = "Pillow-9.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:db6d9fac65bd08cea7f3540b899977c6dee9edad959fa4eaf305940d9cbd861c"}, - {file = "Pillow-9.0.1-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f154d173286a5d1863637a7dcd8c3437bb557520b01bddb0be0258dcb72696b5"}, - {file = "Pillow-9.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14d4b1341ac07ae07eb2cc682f459bec932a380c3b122f5540432d8977e64eae"}, - {file = "Pillow-9.0.1-cp38-cp38-win32.whl", hash = "sha256:effb7749713d5317478bb3acb3f81d9d7c7f86726d41c1facca068a04cf5bb4c"}, - {file = "Pillow-9.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:7f7609a718b177bf171ac93cea9fd2ddc0e03e84d8fa4e887bdfc39671d46b00"}, - {file = "Pillow-9.0.1-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:80ca33961ced9c63358056bd08403ff866512038883e74f3a4bf88ad3eb66838"}, - {file = "Pillow-9.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1c3c33ac69cf059bbb9d1a71eeaba76781b450bc307e2291f8a4764d779a6b28"}, - {file = "Pillow-9.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:12875d118f21cf35604176872447cdb57b07126750a33748bac15e77f90f1f9c"}, - {file = "Pillow-9.0.1-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:514ceac913076feefbeaf89771fd6febde78b0c4c1b23aaeab082c41c694e81b"}, - {file = "Pillow-9.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d3c5c79ab7dfce6d88f1ba639b77e77a17ea33a01b07b99840d6ed08031cb2a7"}, - {file = "Pillow-9.0.1-cp39-cp39-win32.whl", hash = "sha256:718856856ba31f14f13ba885ff13874be7fefc53984d2832458f12c38205f7f7"}, - {file = "Pillow-9.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:f25ed6e28ddf50de7e7ea99d7a976d6a9c415f03adcaac9c41ff6ff41b6d86ac"}, - {file = "Pillow-9.0.1-pp37-pypy37_pp73-macosx_10_10_x86_64.whl", hash = "sha256:011233e0c42a4a7836498e98c1acf5e744c96a67dd5032a6f666cc1fb97eab97"}, - {file = "Pillow-9.0.1-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:253e8a302a96df6927310a9d44e6103055e8fb96a6822f8b7f514bb7ef77de56"}, - {file = "Pillow-9.0.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6295f6763749b89c994fcb6d8a7f7ce03c3992e695f89f00b741b4580b199b7e"}, - {file = "Pillow-9.0.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:a9f44cd7e162ac6191491d7249cceb02b8116b0f7e847ee33f739d7cb1ea1f70"}, - {file = "Pillow-9.0.1.tar.gz", hash = "sha256:6c8bc8238a7dfdaf7a75f5ec5a663f4173f8c367e5a39f87e720495e1eed75fa"}, + {file = "Pillow-9.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:af79d3fde1fc2e33561166d62e3b63f0cc3e47b5a3a2e5fea40d4917754734ea"}, + {file = "Pillow-9.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:55dd1cf09a1fd7c7b78425967aacae9b0d70125f7d3ab973fadc7b5abc3de652"}, + {file = "Pillow-9.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:66822d01e82506a19407d1afc104c3fcea3b81d5eb11485e593ad6b8492f995a"}, + {file = "Pillow-9.1.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a5eaf3b42df2bcda61c53a742ee2c6e63f777d0e085bbc6b2ab7ed57deb13db7"}, + {file = "Pillow-9.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:01ce45deec9df310cbbee11104bae1a2a43308dd9c317f99235b6d3080ddd66e"}, + {file = "Pillow-9.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:aea7ce61328e15943d7b9eaca87e81f7c62ff90f669116f857262e9da4057ba3"}, + {file = "Pillow-9.1.0-cp310-cp310-win32.whl", hash = "sha256:7a053bd4d65a3294b153bdd7724dce864a1d548416a5ef61f6d03bf149205160"}, + {file = "Pillow-9.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:97bda660702a856c2c9e12ec26fc6d187631ddfd896ff685814ab21ef0597033"}, + {file = "Pillow-9.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:21dee8466b42912335151d24c1665fcf44dc2ee47e021d233a40c3ca5adae59c"}, + {file = "Pillow-9.1.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b6d4050b208c8ff886fd3db6690bf04f9a48749d78b41b7a5bf24c236ab0165"}, + {file = "Pillow-9.1.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5cfca31ab4c13552a0f354c87fbd7f162a4fafd25e6b521bba93a57fe6a3700a"}, + {file = "Pillow-9.1.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed742214068efa95e9844c2d9129e209ed63f61baa4d54dbf4cf8b5e2d30ccf2"}, + {file = "Pillow-9.1.0-cp37-cp37m-win32.whl", hash = "sha256:c9efef876c21788366ea1f50ecb39d5d6f65febe25ad1d4c0b8dff98843ac244"}, + {file = "Pillow-9.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:de344bcf6e2463bb25179d74d6e7989e375f906bcec8cb86edb8b12acbc7dfef"}, + {file = "Pillow-9.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:17869489de2fce6c36690a0c721bd3db176194af5f39249c1ac56d0bb0fcc512"}, + {file = "Pillow-9.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:25023a6209a4d7c42154073144608c9a71d3512b648a2f5d4465182cb93d3477"}, + {file = "Pillow-9.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8782189c796eff29dbb37dd87afa4ad4d40fc90b2742704f94812851b725964b"}, + {file = "Pillow-9.1.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:463acf531f5d0925ca55904fa668bb3461c3ef6bc779e1d6d8a488092bdee378"}, + {file = "Pillow-9.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f42364485bfdab19c1373b5cd62f7c5ab7cc052e19644862ec8f15bb8af289e"}, + {file = "Pillow-9.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:3fddcdb619ba04491e8f771636583a7cc5a5051cd193ff1aa1ee8616d2a692c5"}, + {file = "Pillow-9.1.0-cp38-cp38-win32.whl", hash = "sha256:4fe29a070de394e449fd88ebe1624d1e2d7ddeed4c12e0b31624561b58948d9a"}, + {file = "Pillow-9.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:c24f718f9dd73bb2b31a6201e6db5ea4a61fdd1d1c200f43ee585fc6dcd21b34"}, + {file = "Pillow-9.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fb89397013cf302f282f0fc998bb7abf11d49dcff72c8ecb320f76ea6e2c5717"}, + {file = "Pillow-9.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c870193cce4b76713a2b29be5d8327c8ccbe0d4a49bc22968aa1e680930f5581"}, + {file = "Pillow-9.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69e5ddc609230d4408277af135c5b5c8fe7a54b2bdb8ad7c5100b86b3aab04c6"}, + {file = "Pillow-9.1.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:35be4a9f65441d9982240e6966c1eaa1c654c4e5e931eaf580130409e31804d4"}, + {file = "Pillow-9.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82283af99c1c3a5ba1da44c67296d5aad19f11c535b551a5ae55328a317ce331"}, + {file = "Pillow-9.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a325ac71914c5c043fa50441b36606e64a10cd262de12f7a179620f579752ff8"}, + {file = "Pillow-9.1.0-cp39-cp39-win32.whl", hash = "sha256:a598d8830f6ef5501002ae85c7dbfcd9c27cc4efc02a1989369303ba85573e58"}, + {file = "Pillow-9.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:0c51cb9edac8a5abd069fd0758ac0a8bfe52c261ee0e330f363548aca6893595"}, + {file = "Pillow-9.1.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a336a4f74baf67e26f3acc4d61c913e378e931817cd1e2ef4dfb79d3e051b481"}, + {file = "Pillow-9.1.0-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb1b89b11256b5b6cad5e7593f9061ac4624f7651f7a8eb4dfa37caa1dfaa4d0"}, + {file = "Pillow-9.1.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:255c9d69754a4c90b0ee484967fc8818c7ff8311c6dddcc43a4340e10cd1636a"}, + {file = "Pillow-9.1.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:5a3ecc026ea0e14d0ad7cd990ea7f48bfcb3eb4271034657dc9d06933c6629a7"}, + {file = "Pillow-9.1.0-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c5b0ff59785d93b3437c3703e3c64c178aabada51dea2a7f2c5eccf1bcf565a3"}, + {file = "Pillow-9.1.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c7110ec1701b0bf8df569a7592a196c9d07c764a0a74f65471ea56816f10e2c8"}, + {file = "Pillow-9.1.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:8d79c6f468215d1a8415aa53d9868a6b40c4682165b8cb62a221b1baa47db458"}, + {file = "Pillow-9.1.0.tar.gz", hash = "sha256:f401ed2bbb155e1ade150ccc63db1a4f6c1909d3d378f7d1235a44e90d75fb97"}, +] +protobuf = [ + {file = "protobuf-3.20.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3cc797c9d15d7689ed507b165cd05913acb992d78b379f6014e013f9ecb20996"}, + {file = "protobuf-3.20.1-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:ff8d8fa42675249bb456f5db06c00de6c2f4c27a065955917b28c4f15978b9c3"}, + {file = "protobuf-3.20.1-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:cd68be2559e2a3b84f517fb029ee611546f7812b1fdd0aa2ecc9bc6ec0e4fdde"}, + {file = "protobuf-3.20.1-cp310-cp310-win32.whl", hash = "sha256:9016d01c91e8e625141d24ec1b20fed584703e527d28512aa8c8707f105a683c"}, + {file = "protobuf-3.20.1-cp310-cp310-win_amd64.whl", hash = "sha256:32ca378605b41fd180dfe4e14d3226386d8d1b002ab31c969c366549e66a2bb7"}, + {file = "protobuf-3.20.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:9be73ad47579abc26c12024239d3540e6b765182a91dbc88e23658ab71767153"}, + {file = "protobuf-3.20.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:097c5d8a9808302fb0da7e20edf0b8d4703274d140fd25c5edabddcde43e081f"}, + {file = "protobuf-3.20.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e250a42f15bf9d5b09fe1b293bdba2801cd520a9f5ea2d7fb7536d4441811d20"}, + {file = "protobuf-3.20.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:cdee09140e1cd184ba9324ec1df410e7147242b94b5f8b0c64fc89e38a8ba531"}, + {file = "protobuf-3.20.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:af0ebadc74e281a517141daad9d0f2c5d93ab78e9d455113719a45a49da9db4e"}, + {file = "protobuf-3.20.1-cp37-cp37m-win32.whl", hash = "sha256:755f3aee41354ae395e104d62119cb223339a8f3276a0cd009ffabfcdd46bb0c"}, + {file = "protobuf-3.20.1-cp37-cp37m-win_amd64.whl", hash = "sha256:62f1b5c4cd6c5402b4e2d63804ba49a327e0c386c99b1675c8a0fefda23b2067"}, + {file = "protobuf-3.20.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:06059eb6953ff01e56a25cd02cca1a9649a75a7e65397b5b9b4e929ed71d10cf"}, + {file = "protobuf-3.20.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:cb29edb9eab15742d791e1025dd7b6a8f6fcb53802ad2f6e3adcb102051063ab"}, + {file = "protobuf-3.20.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:69ccfdf3657ba59569c64295b7d51325f91af586f8d5793b734260dfe2e94e2c"}, + {file = "protobuf-3.20.1-cp38-cp38-win32.whl", hash = "sha256:dd5789b2948ca702c17027c84c2accb552fc30f4622a98ab5c51fcfe8c50d3e7"}, + {file = "protobuf-3.20.1-cp38-cp38-win_amd64.whl", hash = "sha256:77053d28427a29987ca9caf7b72ccafee011257561259faba8dd308fda9a8739"}, + {file = "protobuf-3.20.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6f50601512a3d23625d8a85b1638d914a0970f17920ff39cec63aaef80a93fb7"}, + {file = "protobuf-3.20.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:284f86a6207c897542d7e956eb243a36bb8f9564c1742b253462386e96c6b78f"}, + {file = "protobuf-3.20.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:7403941f6d0992d40161aa8bb23e12575637008a5a02283a930addc0508982f9"}, + {file = "protobuf-3.20.1-cp39-cp39-win32.whl", hash = "sha256:db977c4ca738dd9ce508557d4fce0f5aebd105e158c725beec86feb1f6bc20d8"}, + {file = "protobuf-3.20.1-cp39-cp39-win_amd64.whl", hash = "sha256:7e371f10abe57cee5021797126c93479f59fccc9693dafd6bd5633ab67808a91"}, + {file = "protobuf-3.20.1-py2.py3-none-any.whl", hash = "sha256:adfc6cf69c7f8c50fd24c793964eef18f0ac321315439d94945820612849c388"}, + {file = "protobuf-3.20.1.tar.gz", hash = "sha256:adc31566d027f45efe3f44eeb5b1f329da43891634d61c75a5944e9be6dd42c9"}, ] psutil = [ {file = "psutil-5.9.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:55ce319452e3d139e25d6c3f85a1acf12d1607ddedea5e35fb47a552c051161b"}, @@ -1049,6 +1434,10 @@ pycryptodome = [ {file = "pycryptodome-3.14.1-pp36-pypy36_pp73-win32.whl", hash = "sha256:7fb90a5000cc9c9ff34b4d99f7f039e9c3477700e309ff234eafca7b7471afc0"}, {file = "pycryptodome-3.14.1.tar.gz", hash = "sha256:e04e40a7f8c1669195536a37979dd87da2c32dbdc73d6fe35f0077b0c17c803b"}, ] +pygments = [ + {file = "Pygments-2.12.0-py3-none-any.whl", hash = "sha256:dc9c10fb40944260f6ed4c688ece0cd2048414940f1cea51b8b226318411c519"}, + {file = "Pygments-2.12.0.tar.gz", hash = "sha256:5eb116118f9612ff1ee89ac96437bb6b49e8f04d8a13b514ba26f620208e26eb"}, +] pymongo = [ {file = "pymongo-3.12.3-cp27-cp27m-macosx_10_14_intel.whl", hash = "sha256:c164eda0be9048f83c24b9b2656900041e069ddf72de81c17d874d0c32f6079f"}, {file = "pymongo-3.12.3-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:a055d29f1302892a9389a382bed10a3f77708bcf3e49bfb76f7712fa5f391cc6"}, @@ -1159,16 +1548,16 @@ pymongo = [ {file = "pymongo-3.12.3.tar.gz", hash = "sha256:0a89cadc0062a5e53664dde043f6c097172b8c1c5f0094490095282ff9995a5f"}, ] pyparsing = [ - {file = "pyparsing-3.0.7-py3-none-any.whl", hash = "sha256:a6c06a88f252e6c322f65faf8f418b16213b51bdfaece0524c1c1bc30c63c484"}, - {file = "pyparsing-3.0.7.tar.gz", hash = "sha256:18ee9022775d270c55187733956460083db60b37d0d0fb357445f3094eed3eea"}, + {file = "pyparsing-3.0.8-py3-none-any.whl", hash = "sha256:ef7b523f6356f763771559412c0d7134753f037822dad1b16945b7b846f7ad06"}, + {file = "pyparsing-3.0.8.tar.gz", hash = "sha256:7bf433498c016c4314268d95df76c81b842a4cb2b276fa3312cfb1e1d85f6954"}, ] python-dateutil = [ {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, ] python-gitlab = [ - {file = "python-gitlab-3.2.0.tar.gz", hash = "sha256:8f6ee81109fec231fc2b74e2c4035bb7de0548eaf82dd119fe294df2c4a524be"}, - {file = "python_gitlab-3.2.0-py3-none-any.whl", hash = "sha256:48f72e033c06ab1c244266af85de2cb0a175f8a3614417567e2b14254ead9b2e"}, + {file = "python-gitlab-3.4.0.tar.gz", hash = "sha256:6180b81ee2f265ad8d8412956a1740b4d3ceca7b28ae2f707dfe62375fed0082"}, + {file = "python_gitlab-3.4.0-py3-none-any.whl", hash = "sha256:251b63f0589d51f854516948c84e9eb8df26e1e9dea595cf86b43f17c43007dd"}, ] pytz = [ {file = "pytz-2022.1-py2.py3-none-any.whl", hash = "sha256:e68985985296d9a66a881eb3193b0906246245294a881e7c8afe623866ac6a5c"}, @@ -1301,6 +1690,32 @@ requests-toolbelt = [ {file = "requests-toolbelt-0.9.1.tar.gz", hash = "sha256:968089d4584ad4ad7c171454f0a5c6dac23971e9472521ea3b6d49d610aa6fc0"}, {file = "requests_toolbelt-0.9.1-py2.py3-none-any.whl", hash = "sha256:380606e1d10dc85c3bd47bf5a6095f815ec007be7a8b69c878507068df059e6f"}, ] +rich = [ + {file = "rich-12.3.0-py3-none-any.whl", hash = "sha256:0eb63013630c6ee1237e0e395d51cb23513de6b5531235e33889e8842bdf3a6f"}, + {file = "rich-12.3.0.tar.gz", hash = "sha256:7e8700cda776337036a712ff0495b04052fb5f957c7dfb8df997f88350044b64"}, +] +rook = [ + {file = "rook-0.1.171-cp27-cp27m-macosx_10_11_x86_64.whl", hash = "sha256:290ee068d18992fa5c27ebdb5c8745853c682cb44f26bedf858d323832ec8b74"}, + {file = "rook-0.1.171-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:96bd8983ad478f50ca22c524ec20fe095945f85c027b4d316ba46e844976bc35"}, + {file = "rook-0.1.171-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:f36b614a85325cfd41e8e4a75ae29e4d7b91aac5981c0c1c4c36452e183e234e"}, + {file = "rook-0.1.171-cp310-cp310-macosx_10_11_x86_64.whl", hash = "sha256:b2d530a77fa1ebb59b15f7efbe810101fdbf7a10851a355c898ddedbfdafb513"}, + {file = "rook-0.1.171-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:9a8f4a998c5e8c03dc5a844b9831d916d8e7b79e704600f8d91b2886ef0fe62a"}, + {file = "rook-0.1.171-cp35-cp35m-macosx_10_11_x86_64.whl", hash = "sha256:01fa10624a6c773ba8b71d9cd397fd37425ca484f0e64f15a9ee6b3214351cdb"}, + {file = "rook-0.1.171-cp35-cp35m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:91db652819b6f6f99a5789bbd840cc46036a54908202492926ff62fbbaf9dcc5"}, + {file = "rook-0.1.171-cp36-cp36m-macosx_10_11_x86_64.whl", hash = "sha256:d3770f3cc4626e56718d7c998024ca6cc75e240b82269b924b488e3d3f73e20c"}, + {file = "rook-0.1.171-cp36-cp36m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3c7e20ecb27ceec33e4ca4efa4c77664d13ff47f64d176eabf81d97a481c8ed4"}, + {file = "rook-0.1.171-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:aacdb2aea2ca559ce3d93eddea9da70d12b10d8b710c049a52781d88dc2e3b6f"}, + {file = "rook-0.1.171-cp37-cp37m-macosx_10_11_x86_64.whl", hash = "sha256:6add21f04e3c28242638483cf34830fc931c61aac4bf47a88197f04273835e1f"}, + {file = "rook-0.1.171-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f8d478bda592fcc20fb73334fa24630cf2467c1faca81a1004908fc581d987b3"}, + {file = "rook-0.1.171-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:a30e32da91b29629fc279990cdb952d6a7cd4945fbafafd7ad88fd71cf9fa624"}, + {file = "rook-0.1.171-cp38-cp38-macosx_10_11_x86_64.whl", hash = "sha256:d87f98c6f059571c40bcb395480cb10a33872e96c1520e8ea98d4703003c148c"}, + {file = "rook-0.1.171-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4474484145b6b4676fc51634dd979376caea7b5722694ec38132ba0e545c85ae"}, + {file = "rook-0.1.171-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:332f572df6a96f6e33e07d0354dfabade1d46a0aeb1aa54e25841bc1fd4239d2"}, + {file = "rook-0.1.171-cp39-cp39-macosx_10_11_x86_64.whl", hash = "sha256:b56ee99de3598a4dc76b25a66b09967f45bbab9c9dec3b6c91b03d4eed1cad7a"}, + {file = "rook-0.1.171-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6bc2dc7a329c7bcac29cae88ed486aba8452437456ee515b3e0ffcb1a1c8dfc9"}, + {file = "rook-0.1.171-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:2e9b96ab66de7616f6e65ad3f30f2bd442ee966a639faef3afcc740c16ed6c00"}, + {file = "rook-0.1.171.tar.gz", hash = "sha256:3ede95c8461546fd0baac2618397458ab7ddbbcb3f56e925fe21a871c70376c9"}, +] six = [ {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, @@ -1309,25 +1724,21 @@ smmap = [ {file = "smmap-5.0.0-py3-none-any.whl", hash = "sha256:2aba19d6a040e78d8b09de5c57e96207b09ed71d8e55ce0959eeee6c8e190d94"}, {file = "smmap-5.0.0.tar.gz", hash = "sha256:c840e62059cd3be204b0c9c9f74be2c09d5648eddd4580d9314c3ecde0b30936"}, ] -tomli = [ - {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, - {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, -] tweepy = [ - {file = "tweepy-4.7.0-py2.py3-none-any.whl", hash = "sha256:d7e78c49232e849b660d82c7c2e504e8487d8014dcb73b39b490b61827965aba"}, - {file = "tweepy-4.7.0.tar.gz", hash = "sha256:82323505d549e3868e14a4570fc069ab3058ef95f9e578d1476d69bf2c831483"}, + {file = "tweepy-4.8.0-py2.py3-none-any.whl", hash = "sha256:f281bb53ab3ba999ff5e3d743d92d3ed543ee5551c7250948f9e56190ec7a43e"}, + {file = "tweepy-4.8.0.tar.gz", hash = "sha256:8ba5774ac1663b09e5fce1b030daf076f2c9b3ddbf2e7e7ea0bae762e3b1fe3e"}, ] typing-extensions = [ - {file = "typing_extensions-4.1.1-py3-none-any.whl", hash = "sha256:21c85e0fe4b9a155d0799430b0ad741cdce7e359660ccbd8b530613e8df88ce2"}, - {file = "typing_extensions-4.1.1.tar.gz", hash = "sha256:1a9462dcc3347a79b1f1c0271fbe79e844580bb598bafa1ed208b94da3cdcd42"}, + {file = "typing_extensions-4.2.0-py3-none-any.whl", hash = "sha256:6657594ee297170d19f67d55c05852a874e7eb634f4f753dbd667855e07c1708"}, + {file = "typing_extensions-4.2.0.tar.gz", hash = "sha256:f1c24655a0da0d1b67f07e17a5e6b2a105894e6824b92096378bb3668ef02376"}, ] tzdata = [ {file = "tzdata-2022.1-py2.py3-none-any.whl", hash = "sha256:238e70234214138ed7b4e8a0fab0e5e13872edab3be586ab8198c407620e2ab9"}, {file = "tzdata-2022.1.tar.gz", hash = "sha256:8b536a8ec63dc0751342b3984193a3118f8fca2afe25752bb9b7fffd398552d3"}, ] tzlocal = [ - {file = "tzlocal-4.1-py3-none-any.whl", hash = "sha256:28ba8d9fcb6c9a782d6e0078b4f6627af1ea26aeaa32b4eab5324abc7df4149f"}, - {file = "tzlocal-4.1.tar.gz", hash = "sha256:0f28015ac68a5c067210400a9197fc5d36ba9bc3f8eaf1da3cbd59acdfed9e09"}, + {file = "tzlocal-4.2-py3-none-any.whl", hash = "sha256:89885494684c929d9191c57aa27502afc87a579be5cdd3225c77c463ea043745"}, + {file = "tzlocal-4.2.tar.gz", hash = "sha256:ee5842fa3a795f023514ac2d801c4a81d1743bbe642e3940143326b3a00addd7"}, ] ulid-py = [ {file = "ulid-py-1.1.0.tar.gz", hash = "sha256:dc6884be91558df077c3011b9fb0c87d1097cb8fc6534b11f310161afd5738f0"}, @@ -1337,10 +1748,41 @@ umongo = [ {file = "umongo-3.1.0-py2.py3-none-any.whl", hash = "sha256:f6913027651ae673d71aaf54285f9ebf1e49a3f57662e526d029ba72e1a3fcd5"}, {file = "umongo-3.1.0.tar.gz", hash = "sha256:20c72f09edae931285c22c1928862af35b90ec639a4dac2dbf015aaaac00e931"}, ] +update-checker = [ + {file = "update_checker-0.18.0-py3-none-any.whl", hash = "sha256:cbba64760a36fe2640d80d85306e8fe82b6816659190993b7bdabadee4d4bbfd"}, + {file = "update_checker-0.18.0.tar.gz", hash = "sha256:6a2d45bb4ac585884a6b03f9eade9161cedd9e8111545141e9aa9058932acb13"}, +] urllib3 = [ {file = "urllib3-1.26.8-py2.py3-none-any.whl", hash = "sha256:000ca7f471a233c2251c6c7023ee85305721bfdf18621ebff4fd17a8653427ed"}, {file = "urllib3-1.26.8.tar.gz", hash = "sha256:0e7c33d9a63e7ddfcb86780aac87befc2fbddf46c58dbb487e0855f7ceec283c"}, ] +watchdog = [ + {file = "watchdog-1.0.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:e2a531e71be7b5cc3499ae2d1494d51b6a26684bcc7c3146f63c810c00e8a3cc"}, + {file = "watchdog-1.0.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e7c73edef48f4ceeebb987317a67e0080e5c9228601ff67b3c4062fa020403c7"}, + {file = "watchdog-1.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:85e6574395aa6c1e14e0f030d9d7f35c2340a6cf95d5671354ce876ac3ffdd4d"}, + {file = "watchdog-1.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:27d9b4666938d5d40afdcdf2c751781e9ce36320788b70208d0f87f7401caf93"}, + {file = "watchdog-1.0.2-pp36-pypy36_pp73-macosx_10_9_x86_64.whl", hash = "sha256:2f1ade0d0802503fda4340374d333408831cff23da66d7e711e279ba50fe6c4a"}, + {file = "watchdog-1.0.2-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:f1d0e878fd69129d0d68b87cee5d9543f20d8018e82998efb79f7e412d42154a"}, + {file = "watchdog-1.0.2-py3-none-manylinux2014_aarch64.whl", hash = "sha256:d948ad9ab9aba705f9836625b32e965b9ae607284811cd98334423f659ea537a"}, + {file = "watchdog-1.0.2-py3-none-manylinux2014_armv7l.whl", hash = "sha256:101532b8db506559e52a9b5d75a308729b3f68264d930670e6155c976d0e52a0"}, + {file = "watchdog-1.0.2-py3-none-manylinux2014_i686.whl", hash = "sha256:b1d723852ce90a14abf0ec0ca9e80689d9509ee4c9ee27163118d87b564a12ac"}, + {file = "watchdog-1.0.2-py3-none-manylinux2014_ppc64.whl", hash = "sha256:68744de2003a5ea2dfbb104f9a74192cf381334a9e2c0ed2bbe1581828d50b61"}, + {file = "watchdog-1.0.2-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:602dbd9498592eacc42e0632c19781c3df1728ef9cbab555fab6778effc29eeb"}, + {file = "watchdog-1.0.2-py3-none-manylinux2014_s390x.whl", hash = "sha256:016b01495b9c55b5d4126ed8ae75d93ea0d99377084107c33162df52887cee18"}, + {file = "watchdog-1.0.2-py3-none-manylinux2014_x86_64.whl", hash = "sha256:5f1f3b65142175366ba94c64d8d4c8f4015825e0beaacee1c301823266b47b9b"}, + {file = "watchdog-1.0.2-py3-none-win32.whl", hash = "sha256:57f05e55aa603c3b053eed7e679f0a83873c540255b88d58c6223c7493833bac"}, + {file = "watchdog-1.0.2-py3-none-win_amd64.whl", hash = "sha256:f84146f7864339c8addf2c2b9903271df21d18d2c721e9a77f779493234a82b5"}, + {file = "watchdog-1.0.2-py3-none-win_ia64.whl", hash = "sha256:ee21aeebe6b3e51e4ba64564c94cee8dbe7438b9cb60f0bb350c4fa70d1b52c2"}, + {file = "watchdog-1.0.2.tar.gz", hash = "sha256:376cbc2a35c0392b0fe7ff16fbc1b303fd99d4dd9911ab5581ee9d69adc88982"}, +] +wcwidth = [ + {file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"}, + {file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"}, +] +websocket-client = [ + {file = "websocket-client-1.3.2.tar.gz", hash = "sha256:50b21db0058f7a953d67cc0445be4b948d7fc196ecbeb8083d68d94628e4abf6"}, + {file = "websocket_client-1.3.2-py3-none-any.whl", hash = "sha256:722b171be00f2b90e1d4fb2f2b53146a536ca38db1da8ff49c972a4e1365d0ef"}, +] yarl = [ {file = "yarl-1.7.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f2a8508f7350512434e41065684076f640ecce176d262a7d54f0da41d99c5a95"}, {file = "yarl-1.7.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:da6df107b9ccfe52d3a48165e48d72db0eca3e3029b5b8cb4fe6ee3cb870ba8b"}, diff --git a/pyproject.toml b/pyproject.toml index 971dbb8..a379526 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,13 +1,12 @@ [tool.poetry] name = "jarvis" -version = "2.0.0b1" +version = "2.0.0b2" description = "J.A.R.V.I.S. admin bot" authors = ["Zevaryx "] [tool.poetry.dependencies] python = "^3.10" PyYAML = "^6.0" -dis-snek = "*" GitPython = "^3.1.26" mongoengine = "^0.23.1" opencv-python = "^4.5.5" @@ -22,7 +21,11 @@ aiohttp = "^3.8.1" pastypy = "^1.0.1" dateparser = "^1.1.1" aiofile = "^3.7.4" -molter = "^0.11.0" +asyncpraw = "^7.5.0" +rook = "^0.1.170" +rich = "^12.3.0" +jurigged = "^0.5.0" +aioredis = "^2.0.1" [build-system] requires = ["poetry-core>=1.0.0"]