diff --git a/.gitignore b/.gitignore index 184fdcc..4190e69 100644 --- a/.gitignore +++ b/.gitignore @@ -145,3 +145,6 @@ config.yaml # VSCode .vscode/ + +# Custom NAFF versions +naff/ diff --git a/jarvis/__init__.py b/jarvis/__init__.py index 71f3859..acb5cd0 100644 --- a/jarvis/__init__.py +++ b/jarvis/__init__.py @@ -19,6 +19,7 @@ __version__ = const.__version__ async def run() -> None: """Run JARVIS""" + # Configure logger jconfig = JarvisConfig.from_yaml() logger = get_logger("jarvis", show_locals=jconfig.log_level == "DEBUG") logger.setLevel(jconfig.log_level) @@ -28,6 +29,7 @@ async def run() -> None: ) logger.addHandler(file_handler) + # Configure client intents = ( Intents.DEFAULT | Intents.MESSAGES | Intents.GUILD_MEMBERS | Intents.GUILD_MESSAGE_CONTENT ) @@ -42,18 +44,23 @@ async def run() -> None: delete_unused_application_cmds=True, send_command_tracebacks=False, redis=redis, + logger=logger, ) + # External modules if jconfig.log_level == "DEBUG": jurigged.watch(pattern="jarvis/*.py") if jconfig.rook_token: rook.start(token=jconfig.rook_token, labels={"env": "dev"}) + + # Initialize bot logger.info("Starting JARVIS") logger.debug("Connecting to database") connect(**jconfig.mongo["connect"], testing=jconfig.mongo["database"] != "jarvis") logger.debug("Loading configuration from database") # jconfig.get_db_config() + # Load extensions logger.debug("Loading extensions") for extension in get_extensions(cogs_path): jarvis.load_extension(extension) diff --git a/jarvis/client.py b/jarvis/client.py index 33ac521..904edd0 100644 --- a/jarvis/client.py +++ b/jarvis/client.py @@ -430,10 +430,11 @@ class Jarvis(StatsClient): """Handle autopurge events.""" autopurge = await Autopurge.find_one(q(guild=message.guild.id, channel=message.channel.id)) if autopurge: - self.logger.debug( - f"Autopurging message {message.guild.id}/{message.channel.id}/{message.id}" - ) - await message.delete(delay=autopurge.delay) + if not message.author.has_permission(Permissions.ADMINISTRATOR): + self.logger.debug( + f"Autopurging message {message.guild.id}/{message.channel.id}/{message.id}" + ) + await message.delete(delay=autopurge.delay) async def autoreact(self, message: Message) -> None: """Handle autoreact events.""" @@ -450,7 +451,8 @@ class Jarvis(StatsClient): for reaction in autoreact.reactions: await message.add_reaction(reaction) if autoreact.thread: - name = message.content + name = message.content.replace("\n", " ") + name = re.sub(r"<:\w+:(\d+)>", "", name) if len(name) > 100: name = name[:97] + "..." await message.create_thread(name=message.content, reason="Autoreact") diff --git a/jarvis/cogs/botutil.py b/jarvis/cogs/botutil.py index 61a49f5..8cd9d26 100644 --- a/jarvis/cogs/botutil.py +++ b/jarvis/cogs/botutil.py @@ -1,4 +1,5 @@ """JARVIS bot utility commands.""" +import asyncio import logging import platform from io import BytesIO @@ -26,6 +27,12 @@ class BotutilCog(Extension): """Checks if author is bot owner.""" return ctx.author.id == self.bot.owner.id + @prefixed_command(name="stop") + async def _stop(self, ctx: PrefixedContext) -> None: + await ctx.send("Shutting down now") + loop = asyncio.get_running_loop() + loop.stop() + @prefixed_command(name="tail") async def _tail(self, ctx: PrefixedContext, count: int = 10) -> None: lines = [] diff --git a/jarvis/cogs/ctc2.py b/jarvis/cogs/ctc2.py index 2919fc4..9bf774c 100644 --- a/jarvis/cogs/ctc2.py +++ b/jarvis/cogs/ctc2.py @@ -5,6 +5,8 @@ import re import aiohttp from jarvis_core.db import q from jarvis_core.db.models import Guess + +from jarvis.utils import build_embed from naff import Client, Extension, InteractionContext from naff.ext.paginators import Paginator from naff.models.discord.components import ActionRow, Button, ButtonStyles @@ -109,14 +111,16 @@ class CTCCog(Extension): @cooldown(bucket=Buckets.USER, rate=1, interval=2) async def _guesses(self, ctx: InteractionContext) -> None: await ctx.defer() + cache = {} guesses = Guess.find().sort("correct", -1).sort("id", -1) fields = [] async for guess in guesses: - user = await self.bot.fetch_user(guess["user"]) + user = cache.get(guess["user"]) or await self.bot.fetch_user(guess["user"]) if not user: user = "[redacted]" if isinstance(user, (Member, User)): user = user.username + "#" + user.discriminator + cache[guess["user"]] = user name = "Correctly" if guess["correct"] else "Incorrectly" name += " guessed by: " + user fields.append( diff --git a/jarvis/cogs/dbrand.py b/jarvis/cogs/dbrand.py index d203a5c..c60fc43 100644 --- a/jarvis/cogs/dbrand.py +++ b/jarvis/cogs/dbrand.py @@ -1,6 +1,7 @@ """JARVIS dbrand cog.""" import logging import re +from datetime import datetime, timedelta import aiohttp from naff import Client, Extension, InteractionContext @@ -116,14 +117,15 @@ class DbrandCog(Extension): search = matches[0] dest = search.lower() data = self.cache.get(dest, None) - if not data: + if not data or data["cache_expiry"] < datetime.utcnow(): api_link = self.api_url + dest data = await self._session.get(api_link) if 200 <= data.status < 400: data = await data.json() + data["cache_expiry"] = datetime.utcnow() + timedelta(hours=24) + self.cache[dest] = data else: data = None - self.cache[dest] = data fields = None if data is not None and data["is_valid"] and data["shipping_available"]: fields = [] @@ -131,10 +133,16 @@ class DbrandCog(Extension): EmbedField(data["carrier"] + " " + data["tier-title"], data["time-title"]) ) for service in data["shipping_services_available"][1:]: - service_data = await self._session.get(self.api_url + dest + "/" + service["url"]) - if service_data.status > 400: - continue - service_data = await service_data.json() + service_data = self.cache.get(f"{dest}-{service}") + if not service_data or service_data["cache_expiry"] < datetime.utcnow(): + service_data = await self._session.get( + self.api_url + dest + "/" + service["url"] + ) + if service_data.status > 400: + continue + service_data = await service_data.json() + service_data["cache_expiry"] = datetime.utcnow() + timedelta(hours=24) + self.cache[f"{dest}-{service}"] = service_data fields.append( EmbedField( service_data["carrier"] + " " + service_data["tier-title"], diff --git a/jarvis/cogs/gl.py b/jarvis/cogs/gl.py index 1b97585..6e79c14 100644 --- a/jarvis/cogs/gl.py +++ b/jarvis/cogs/gl.py @@ -16,8 +16,6 @@ from naff.models.naff.application_commands import ( slash_command, slash_option, ) -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 @@ -424,7 +422,6 @@ class GitlabCog(Extension): opt_type=OptionTypes.USER, required=False, ) - @cooldown(bucket=Buckets.USER, rate=1, interval=600) async def _open_issue(self, ctx: InteractionContext, user: Member = None) -> None: user = user or ctx.author modal = Modal( diff --git a/jarvis/cogs/reddit.py b/jarvis/cogs/reddit.py index ff87bca..c8b8333 100644 --- a/jarvis/cogs/reddit.py +++ b/jarvis/cogs/reddit.py @@ -9,7 +9,18 @@ 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, UserSetting +from jarvis_core.db.models import ( + Redditor, + RedditorFollow, + Subreddit, + SubredditFollow, + UserSetting, +) + +from jarvis import const +from jarvis.config import JarvisConfig +from jarvis.utils import build_embed +from jarvis.utils.permissions import admin_or_permissions from naff import Client, Extension, InteractionContext, Permissions from naff.client.utils.misc_utils import get from naff.models.discord.channel import ChannelTypes, GuildText @@ -23,13 +34,10 @@ from naff.models.naff.application_commands import ( ) 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)" sub_name = re.compile(r"\A[A-Za-z0-9][A-Za-z0-9_]{2,20}\Z") +user_name = re.compile(r"[A-Za-z0-9_-]+") +image_link = re.compile(r"https?://(?:www)?\.?preview\.redd\.it\/(.*\..*)\?.*") class RedditCog(Extension): @@ -89,6 +97,7 @@ class RedditCog(Extension): if post.spoiler: content += "||" content += f"\n\n[View this post]({url})" + content = "\n".join(image_link.sub(r"https://i.redd.it/\1", x) for x in content.split("\n")) if not images and not content: self.logger.debug(f"Post {post.id} had neither content nor images?") @@ -122,8 +131,126 @@ class RedditCog(Extension): return embeds reddit = SlashCommand(name="reddit", description="Manage Reddit follows") + follow = reddit.group(name="follow", description="Add a follow") + unfollow = reddit.group(name="unfollow", description="Remove a follow") - @reddit.subcommand(sub_cmd_name="follow", sub_cmd_description="Follow a Subreddit") + @follow.subcommand(sub_cmd_name="redditor", sub_cmd_description="Follow a Redditor") + @slash_option( + name="name", + description="Redditor 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 _redditor_follow( + self, ctx: InteractionContext, name: str, channel: GuildText + ) -> None: + if not user_name.match(name): + await ctx.send("Invalid Redditor name", ephemeral=True) + return + + if not isinstance(channel, GuildText): + await ctx.send("Channel must be a text channel", ephemeral=True) + return + + try: + redditor = await self.api.redditor(name) + await redditor.load() + except (NotFound, Forbidden, Redirect) as e: + self.logger.debug(f"Redditor {name} raised {e.__class__.__name__} on add") + await ctx.send("Redditor may be deleted or nonexistent.", ephemeral=True) + return + + exists = await RedditorFollow.find_one(q(name=redditor.name, guild=ctx.guild.id)) + if exists: + await ctx.send("Redditor 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 Redditors", ephemeral=True) + return + + sr = await Redditor.find_one(q(name=redditor.name)) + if not sr: + sr = Redditor(name=redditor.name) + await sr.commit() + + srf = RedditorFollow( + name=redditor.name, + channel=channel.id, + guild=ctx.guild.id, + admin=ctx.author.id, + ) + await srf.commit() + + await ctx.send(f"Now following `u/{name}` in {channel.mention}") + + @unfollow.subcommand(sub_cmd_name="redditor", sub_cmd_description="Unfollow Redditor") + @check(admin_or_permissions(Permissions.MANAGE_GUILD)) + async def _redditor_unfollow(self, ctx: InteractionContext) -> None: + subs = RedditorFollow.find(q(guild=ctx.guild.id)) + redditors = [] + async for sub in subs: + redditors.append(sub) + if not redditors: + await ctx.send("You need to follow a redditor first", ephemeral=True) + return + + options = [] + names = [] + for idx, redditor in enumerate(redditors): + sub = await Redditor.find_one(q(name=redditor.name)) + names.append(sub.name) + option = SelectOption(label=sub.name, value=str(idx)) + options.append(option) + + select = Select( + options=options, custom_id="to_delete", min_values=1, max_values=len(redditors) + ) + + components = [ActionRow(select)] + block = "\n".join(x for x in names) + message = await ctx.send( + content=f"You are following the following redditors:\n```\n{block}\n```\n\n" + "Please choose redditors 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(redditors, guild=ctx.guild.id, 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) + + @follow.subcommand(sub_cmd_name="subreddit", sub_cmd_description="Follow a Subreddit") @slash_option( name="name", description="Subreddit display name", @@ -138,7 +265,9 @@ class RedditCog(Extension): required=True, ) @check(admin_or_permissions(Permissions.MANAGE_GUILD)) - async def _reddit_follow(self, ctx: InteractionContext, name: str, channel: GuildText) -> None: + async def _subreddit_follow( + self, ctx: InteractionContext, name: str, channel: GuildText + ) -> None: if not sub_name.match(name): await ctx.send("Invalid Subreddit name", ephemeral=True) return @@ -189,7 +318,7 @@ class RedditCog(Extension): await ctx.send(f"Now following `r/{name}` in {channel.mention}") - @reddit.subcommand(sub_cmd_name="unfollow", sub_cmd_description="Unfollow Subreddits") + @unfollow.subcommand(sub_cmd_name="subreddit", 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)) diff --git a/jarvis/cogs/tags.py b/jarvis/cogs/tags.py new file mode 100644 index 0000000..27aa648 --- /dev/null +++ b/jarvis/cogs/tags.py @@ -0,0 +1,330 @@ +"""JARVIS Tags Cog.""" +import asyncio +import re +from datetime import datetime, timezone +from typing import Dict, List + +from jarvis_core.db import q +from jarvis_core.db.models import Setting, Tag +from naff import AutocompleteContext, Client, Extension, InteractionContext +from naff.models.discord.embed import EmbedField +from naff.models.discord.enums import Permissions +from naff.models.discord.modal import InputText, Modal, TextStyles +from naff.models.naff.application_commands import ( + OptionTypes, + SlashCommand, + slash_option, +) +from thefuzz import process + +from jarvis.utils import build_embed + +invites = re.compile( + r"(?:https?://)?(?:www.)?(?:discord.(?:gg|io|me|li)|discord(?:app)?.com/invite)/([^\s/]+?)(?=\b)", # noqa: E501 + flags=re.IGNORECASE, +) +tag_name = re.compile(r"$[\w\ \-]{1,40}^") + + +class TagCog(Extension): + def __init__(self, bot: Client): + self.bot = bot + self.cache: Dict[int, List[int]] = {} + + tag = SlashCommand(name="tag", description="Create and manage custom tags") + + @tag.subcommand(sub_cmd_name="get", sub_cmd_description="Get a tag") + @slash_option( + name="name", + description="Tag to get", + autocomplete=True, + opt_type=OptionTypes.STRING, + required=True, + ) + async def _get(self, ctx: InteractionContext, name: str) -> None: + tag = await Tag.find_one(q(guild=ctx.guild.id, name=name)) + if not tag: + await ctx.send( + "Well this is awkward, looks like the tag was deleted just now", ephemeral=True + ) + return + + await ctx.send(tag.content) + + @tag.subcommand(sub_cmd_name="create", sub_cmd_description="Create a tag") + async def _create(self, ctx: InteractionContext) -> None: + modal = Modal( + title="Create a new tag!", + components=[ + InputText( + label="Tag name", + placeholder="name", + style=TextStyles.SHORT, + custom_id="name", + max_length=40, + ), + InputText( + label="Content", + placeholder="Content to send here", + style=TextStyles.PARAGRAPH, + custom_id="content", + max_length=512, + ), + ], + ) + + await ctx.send_modal(modal) + try: + response = await self.bot.wait_for_modal(modal, author=ctx.author.id, timeout=60 * 5) + name = response.responses.get("name").replace("`", "") + content = response.responses.get("content") + except asyncio.TimeoutError: + return + + noinvite = await Setting.find_one(q(guild=ctx.guild.id, setting="noinvite")) + + if ( + (invites.search(content) or invites.search(name)) + and noinvite.value + and not ( + ctx.author.has_permission(Permissions.ADMINISTRATOR) + or ctx.author.has_permission(Permissions.MANAGE_MESSAGES) + ) + ): + await response.send( + "Listen, don't use this to try and bypass the rules", ephemeral=True + ) + return + elif not content.strip() or not name.strip(): + await response.send("Content and name required", ephemeral=True) + return + elif not tag_name.match(name): + await response.send("Tag name must only contain: [A-Za-z0-9_- ]", ephemeral=True) + return + + tag = await Tag.find_one(q(guild=ctx.guild.id, name=name)) + if tag: + await response.send("That tag already exists", ephemeral=True) + return + + content = re.sub(r"\\?([@<])", r"\\\g<1>", content) + + tag = Tag( + creator=ctx.author.id, + name=name, + content=content, + guild=ctx.guild.id, + ) + await tag.commit() + + embed = build_embed( + title="Tag Created", + description=f"{ctx.author.mention} created a new tag", + fields=[EmbedField(name="Name", value=name), EmbedField(name="Content", value=content)], + ) + + embed.set_author( + name=ctx.author.username + "#" + ctx.author.discriminator, + icon_url=ctx.author.display_avatar.url, + ) + + await response.send(embeds=embed) + if ctx.guild.id not in self.cache: + self.cache[ctx.guild.id] = [] + self.cache[ctx.guild.id].append(tag.name) + + @tag.subcommand(sub_cmd_name="edit", sub_cmd_description="Edit a tag") + @slash_option( + name="name", + description="Tag name", + opt_type=OptionTypes.STRING, + autocomplete=True, + required=True, + ) + async def _edit(self, ctx: InteractionContext, name: str) -> None: + old_name = name + tag = await Tag.find_one(q(guild=ctx.guild.id, name=name)) + if not tag: + await ctx.send("Tag not found", ephemeral=True) + return + elif tag.creator != ctx.author.id and not ( + ctx.author.has_permission(Permissions.ADMINISTRATOR) + or ctx.author.has_permission(Permissions.MANAGE_MESSAGES) + ): + await ctx.send("You didn't create this tag, ask the creator to edit it", ephemeral=True) + return + + modal = Modal( + title="Edit a tag!", + components=[ + InputText( + label="Tag name", + value=tag.name, + style=TextStyles.SHORT, + custom_id="name", + max_length=40, + ), + InputText( + label="Content", + value=tag.content, + style=TextStyles.PARAGRAPH, + custom_id="content", + max_length=512, + ), + ], + ) + + await ctx.send_modal(modal) + try: + response = await self.bot.wait_for_modal(modal, author=ctx.author.id, timeout=60 * 5) + name = response.responses.get("name").replace("`", "") + content = response.responses.get("content") + except asyncio.TimeoutError: + return + + new_tag = await Tag.find_one(q(guild=ctx.guild.id, name=name)) + if new_tag and new_tag.id != tag.id: + await ctx.send( + "That tag name is used by another tag, choose another name", ephemeral=True + ) + return + + noinvite = await Setting.find_one(q(guild=ctx.guild.id, setting="noinvite")) + + if ( + (invites.search(content) or invites.search(name)) + and noinvite.value + and not ( + ctx.author.has_permission(Permissions.ADMINISTRATOR) + or ctx.author.has_permission(Permissions.MANAGE_MESSAGES) + ) + ): + await response.send( + "Listen, don't use this to try and bypass the rules", ephemeral=True + ) + return + elif not content.strip() or not name.strip(): + await response.send("Content and name required", ephemeral=True) + return + elif not tag_name.match(name): + await response.send("Tag name must only contain: [A-Za-z0-9_- ]", ephemeral=True) + return + + tag.content = re.sub(r"\\?([@<])", r"\\\g<1>", content) + tag.name = name + tag.edited_at = datetime.now(tz=timezone.utc) + tag.editor = ctx.author.id + + await tag.commit() + + embed = build_embed( + title="Tag Updated", + description=f"{ctx.author.mention} updated a tag", + fields=[ + EmbedField(name="Name", value=name), + EmbedField(name="Content", value=tag.content), + ], + ) + + embed.set_author( + name=ctx.author.username + "#" + ctx.author.discriminator, + icon_url=ctx.author.display_avatar.url, + ) + + await response.send(embeds=embed) + if tag.name not in self.cache[ctx.guild.id]: + self.cache[ctx.guild.id].remove(old_name) + self.cache[ctx.guild.id].append(tag.name) + + @tag.subcommand(sub_cmd_name="delete", sub_cmd_description="Delete a tag") + @slash_option( + name="name", + description="Tag name", + opt_type=OptionTypes.STRING, + required=True, + autocomplete=True, + ) + async def _delete(self, ctx: InteractionContext, name: str) -> None: + tag = await Tag.find_one(q(guild=ctx.guild.id, name=name)) + if not tag: + await ctx.send("Tag not found", ephemeral=True) + return + elif tag.creator != ctx.author.id and not ( + ctx.author.has_permission(Permissions.ADMINISTRATOR) + or ctx.author.has_permission(Permissions.MANAGE_MESSAGES) + ): + await ctx.send( + "You didn't create this tag, ask the creator to delete it", ephemeral=True + ) + return + + await tag.delete() + await ctx.send(f"Tag `{name}` deleted") + self.cache[ctx.guild.id].remove(tag.name) + + @tag.subcommand(sub_cmd_name="info", sub_cmd_description="Get info on a tag") + @slash_option( + name="name", + description="Tag name", + opt_type=OptionTypes.STRING, + required=True, + autocomplete=True, + ) + async def _info(self, ctx: InteractionContext, name: str) -> None: + tag = await Tag.find_one(q(guild=ctx.guild.id, name=name)) + if not tag: + await ctx.send("Tag not found", ephemeral=True) + return + + username, discrim, url, mention = None, None, None, "Unknown User" + author = await self.bot.fetch_user(tag.creator) + if author: + username = author.username + discrim = author.discriminator + url = author.display_avatar.url + mention = author.mention + + ts = int(tag.created_at.timestamp()) + + embed = build_embed( + title="Tag Info", + description=f"Here's the info on the tag `{name}`", + fields=[ + EmbedField(name="Name", value=name), + EmbedField(name="Content", value=tag.content), + EmbedField(name="Created At", value=f""), + EmbedField(name="Created By", value=mention), + ], + ) + if tag.edited_at: + ets = int(tag.edited_at.timestamp()) + editor = await self.bot.fetch_user(tag.editor) + emention = "Unknown User" + if editor: + emention = editor.mention + embed.add_field(name="Edited At", value=f"") + embed.add_field(name="Edited By", value=emention) + + embed.set_author( + name=f"{username}#{discrim}" if username else "Unknown User", + icon_url=url, + ) + + await ctx.send(embeds=embed) + + @_get.autocomplete("name") + @_edit.autocomplete("name") + @_delete.autocomplete("name") + @_info.autocomplete("name") + async def _autocomplete(self, ctx: AutocompleteContext, name: str) -> None: + if not self.cache.get(ctx.guild.id): + tags = await Tag.find(q(guild=ctx.guild.id)).to_list(None) + self.cache[ctx.guild.id] = [tag.name for tag in tags] + results = process.extract(name, self.cache.get(ctx.guild.id), limit=25) + choices = [{"name": r[0], "value": r[0]} for r in results] + await ctx.send(choices=choices) + + +def setup(bot: Client) -> None: + """Add TagCog to JARVIS""" + TagCog(bot) diff --git a/jarvis/cogs/util.py b/jarvis/cogs/util.py index edf79a1..16b7bd3 100644 --- a/jarvis/cogs/util.py +++ b/jarvis/cogs/util.py @@ -8,13 +8,20 @@ from io import BytesIO import numpy as np from dateparser import parse +from PIL import Image +from tzlocal import get_localzone + +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 from naff import Client, Extension, 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.discord.user import User from naff.models.naff.application_commands import ( CommandTypes, OptionTypes, @@ -25,13 +32,6 @@ from naff.models.naff.application_commands import ( ) from naff.models.naff.command import cooldown from naff.models.naff.cooldowns import Buckets -from PIL import Image -from tzlocal import get_localzone - -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 JARVIS_LOGO = Image.open("jarvis_small.png").convert("RGBA") @@ -125,9 +125,7 @@ class UtilCog(Extension): if not user: user = ctx.author - avatar = user.avatar.url - if isinstance(user, Member): - avatar = user.display_avatar.url + avatar = user.display_avatar.url embed = build_embed(title="Avatar", description="", fields=[], color="#00FFEE") embed.set_image(url=avatar) diff --git a/poetry.lock b/poetry.lock index 57d4285..4f99613 100644 --- a/poetry.lock +++ b/poetry.lock @@ -106,17 +106,6 @@ category = "main" optional = false python-versions = "*" -[[package]] -name = "asgiref" -version = "3.5.2" -description = "ASGI specs, helper code, and adapters" -category = "main" -optional = false -python-versions = ">=3.7" - -[package.extras] -tests = ["pytest", "pytest-asyncio", "mypy (>=0.800)"] - [[package]] name = "async-generator" version = "1.10" @@ -414,7 +403,7 @@ testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest- [[package]] name = "jarvis-core" -version = "0.10.2" +version = "0.12.0" description = "JARVIS core" category = "main" optional = false @@ -435,7 +424,7 @@ umongo = "^3.1.0" type = "git" url = "https://git.zevaryx.com/stark-industries/jarvis/jarvis-core.git" reference = "main" -resolved_reference = "7bb9b25f636fbcbea97e0924f2192a1e497258dd" +resolved_reference = "fe24fce330cfd23a7af3834ef11b675780e6325d" [[package]] name = "jinxed" @@ -525,7 +514,7 @@ python-versions = "*" [[package]] name = "naff" -version = "1.4.0" +version = "1.7.1" description = "Not another freaking fork" category = "main" optional = false @@ -540,6 +529,7 @@ tomli = "*" [package.extras] all = ["PyNaCl (>=1.5.0,<1.6)", "cchardet", "aiodns", "orjson", "brotli"] speedup = ["cchardet", "aiodns", "orjson", "brotli"] +tests = ["pytest", "pytest-recording", "pytest-asyncio", "pytest-cov", "typeguard"] voice = ["PyNaCl (>=1.5.0,<1.6)"] [[package]] @@ -552,15 +542,15 @@ python-versions = "^3.10" develop = false [package.dependencies] -naff = {version = "^1.2.0", extras = ["orjson"]} +naff = {version = "^1.7.1", extras = ["orjson"]} prometheus-client = "^0.14.1" -uvicorn = "^0.17.6" +uvicorn = "^0.18.2" [package.source] type = "git" url = "https://github.com/artem30801/nafftrack.git" reference = "master" -resolved_reference = "e3b6f102d6784731d90c52e84b401c86117583f2" +resolved_reference = "eae6ffd93a1a7854347eb0e147b894bf307c0003" [[package]] name = "nanoid" @@ -570,6 +560,14 @@ category = "main" optional = false python-versions = "*" +[[package]] +name = "nest-asyncio" +version = "1.5.5" +description = "Patch asyncio to allow nested event loops" +category = "main" +optional = false +python-versions = ">=3.5" + [[package]] name = "numpy" version = "1.22.4" @@ -816,6 +814,14 @@ requests-toolbelt = ">=0.9.1" autocompletion = ["argcomplete (>=1.10.0,<3)"] yaml = ["PyYaml (>=5.2)"] +[[package]] +name = "python-levenshtein" +version = "0.12.2" +description = "Python extension for computing string edit distances and similarities." +category = "main" +optional = false +python-versions = "*" + [[package]] name = "pytz" version = "2022.1" @@ -946,6 +952,20 @@ category = "main" optional = false python-versions = ">=3.6" +[[package]] +name = "thefuzz" +version = "0.19.0" +description = "Fuzzy string matching in python" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +python-levenshtein = {version = ">=0.12", optional = true, markers = "extra == \"speedup\""} + +[package.extras] +speedup = ["python-levenshtein (>=0.12)"] + [[package]] name = "tomli" version = "2.0.1" @@ -1078,19 +1098,18 @@ socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] [[package]] name = "uvicorn" -version = "0.17.6" +version = "0.18.2" description = "The lightning-fast ASGI server." category = "main" optional = false python-versions = ">=3.7" [package.dependencies] -asgiref = ">=3.4.0" click = ">=7.0" h11 = ">=0.8" [package.extras] -standard = ["websockets (>=10.0)", "httptools (>=0.4.0)", "watchgod (>=0.6)", "python-dotenv (>=0.13)", "PyYAML (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "colorama (>=0.4)"] +standard = ["websockets (>=10.0)", "httptools (>=0.4.0)", "watchfiles (>=0.13)", "python-dotenv (>=0.13)", "PyYAML (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "colorama (>=0.4)"] [[package]] name = "watchdog" @@ -1159,7 +1178,7 @@ testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest- [metadata] lock-version = "1.1" python-versions = "^3.10" -content-hash = "c1a2a46f16c8966603c1e92166f4ad1e243b4425752f1cf8e78d2a421aacd0b9" +content-hash = "d4a3ccd2f79fe0c323784bfba2c5950817257639bbdcdb57a6e71682a8846504" [metadata.files] aiofile = [ @@ -1268,10 +1287,6 @@ appdirs = [ {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, ] -asgiref = [ - {file = "asgiref-3.5.2-py3-none-any.whl", hash = "sha256:1d2880b792ae8757289136f1db2b7b99100ce959b2aa57fd69dab783d05afac4"}, - {file = "asgiref-3.5.2.tar.gz", hash = "sha256:4a29362a6acebe09bf1d6640db38c1dc3d9217c68e6f9f6204d72667fc19a424"}, -] async-generator = [ {file = "async_generator-1.10-py3-none-any.whl", hash = "sha256:01c7bf666359b4967d2cda0000cc2e4af16a0ae098cbffcb8472fb9e8ad6585b"}, {file = "async_generator-1.10.tar.gz", hash = "sha256:6ebb3d106c12920aaae42ccb6f787ef5eefdcdd166ea3d628fa8476abe712144"}, @@ -1549,15 +1564,16 @@ mypy-extensions = [ {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, ] -naff = [ - {file = "naff-1.4.0-py3-none-any.whl", hash = "sha256:81d1e42dbc761b5ec3820b3bbf64f45c23ffdd185aed6c5512c9a8b24e0277de"}, - {file = "naff-1.4.0.tar.gz", hash = "sha256:2f8bc2216c54a0b58db05aa8f787d33e2ad3db3d1e512751dc3efb16e5891653"}, -] +naff = [] nafftrack = [] nanoid = [ {file = "nanoid-2.0.0-py3-none-any.whl", hash = "sha256:90aefa650e328cffb0893bbd4c236cfd44c48bc1f2d0b525ecc53c3187b653bb"}, {file = "nanoid-2.0.0.tar.gz", hash = "sha256:5a80cad5e9c6e9ae3a41fa2fb34ae189f7cb420b2a5d8f82bd9d23466e4efa68"}, ] +nest-asyncio = [ + {file = "nest_asyncio-1.5.5-py3-none-any.whl", hash = "sha256:b98e3ec1b246135e4642eceffa5a6c23a3ab12c82ff816a92c612d68205813b2"}, + {file = "nest_asyncio-1.5.5.tar.gz", hash = "sha256:e442291cd942698be619823a17a86a5759eabe1f8613084790de189fe9e16d65"}, +] numpy = [ {file = "numpy-1.22.4-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:ba9ead61dfb5d971d77b6c131a9dbee62294a932bf6a356e48c75ae684e635b3"}, {file = "numpy-1.22.4-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:1ce7ab2053e36c0a71e7a13a7475bd3b1f54750b4b433adc96313e127b870887"}, @@ -1919,6 +1935,7 @@ python-gitlab = [ {file = "python-gitlab-3.5.0.tar.gz", hash = "sha256:29ae7fb9b8c9aeb2e6e19bd2fd04867e93ecd7af719978ce68fac0cf116ab30d"}, {file = "python_gitlab-3.5.0-py3-none-any.whl", hash = "sha256:73b5aa6502efa557ee1a51227cceb0243fac5529627da34f08c5f265bf50417c"}, ] +python-levenshtein = [] pytz = [ {file = "pytz-2022.1-py2.py3-none-any.whl", hash = "sha256:e68985985296d9a66a881eb3193b0906246245294a881e7c8afe623866ac6a5c"}, {file = "pytz-2022.1.tar.gz", hash = "sha256:1e760e2fe6a8163bc0b3d9a19c4f84342afa0a2affebfaa84b01b978a02ecaa7"}, @@ -2084,6 +2101,7 @@ smmap = [ {file = "smmap-5.0.0-py3-none-any.whl", hash = "sha256:2aba19d6a040e78d8b09de5c57e96207b09ed71d8e55ce0959eeee6c8e190d94"}, {file = "smmap-5.0.0.tar.gz", hash = "sha256:c840e62059cd3be204b0c9c9f74be2c09d5648eddd4580d9314c3ecde0b30936"}, ] +thefuzz = [] tomli = [ {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, @@ -2125,8 +2143,8 @@ urllib3 = [ {file = "urllib3-1.26.8.tar.gz", hash = "sha256:0e7c33d9a63e7ddfcb86780aac87befc2fbddf46c58dbb487e0855f7ceec283c"}, ] uvicorn = [ - {file = "uvicorn-0.17.6-py3-none-any.whl", hash = "sha256:19e2a0e96c9ac5581c01eb1a79a7d2f72bb479691acd2b8921fce48ed5b961a6"}, - {file = "uvicorn-0.17.6.tar.gz", hash = "sha256:5180f9d059611747d841a4a4c4ab675edf54c8489e97f96d0583ee90ac3bfc23"}, + {file = "uvicorn-0.18.2-py3-none-any.whl", hash = "sha256:c19a057deb1c5bb060946e2e5c262fc01590c6529c0af2c3d9ce941e89bc30e0"}, + {file = "uvicorn-0.18.2.tar.gz", hash = "sha256:cade07c403c397f9fe275492a48c1b869efd175d5d8a692df649e6e7e2ed8f4e"}, ] watchdog = [ {file = "watchdog-2.1.8-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:676263bee67b165f16b05abc52acc7a94feac5b5ab2449b491f1a97638a79277"}, diff --git a/pyproject.toml b/pyproject.toml index d0dca4d..5417210 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,6 +28,8 @@ aioredis = "^2.0.1" naff = { version = "^1.2.0", extras = ["orjson"] } nafftrack = {git = "https://github.com/artem30801/nafftrack.git", rev = "master"} ansitoimg = "^2022.1" +nest-asyncio = "^1.5.5" +thefuzz = {extras = ["speedup"], version = "^0.19.0"} [tool.poetry.dev-dependencies] black = {version = "^22.3.0", allow-prereleases = true} diff --git a/run.py b/run.py index 5899e3b..0dcb1ed 100755 --- a/run.py +++ b/run.py @@ -5,7 +5,11 @@ nest_asyncio.apply() import asyncio +import nest_asyncio + from jarvis import run +nest_asyncio.apply() + if __name__ == "__main__": asyncio.run(run())