From 6439a015c9550bc7a99641fbac7cb4fd83e8572a Mon Sep 17 00:00:00 2001 From: zevaryx Date: Tue, 21 Jun 2022 14:51:03 -0600 Subject: [PATCH 01/19] Add nest-asyncio --- poetry.lock | 14 +++++++++++++- pyproject.toml | 1 + run.py | 4 ++++ 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/poetry.lock b/poetry.lock index 57d4285..cf0685a 100644 --- a/poetry.lock +++ b/poetry.lock @@ -570,6 +570,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" @@ -1159,7 +1167,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 = "549486089ef65c69b0932e799efdbb0d22d6631d925de845ec4b6ba98d57c527" [metadata.files] aiofile = [ @@ -1558,6 +1566,10 @@ 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"}, diff --git a/pyproject.toml b/pyproject.toml index d0dca4d..9c455c1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,6 +28,7 @@ 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" [tool.poetry.dev-dependencies] black = {version = "^22.3.0", allow-prereleases = true} diff --git a/run.py b/run.py index d53ee79..0b964f9 100755 --- a/run.py +++ b/run.py @@ -1,7 +1,11 @@ """Main run file for J.A.R.V.I.S.""" import asyncio +import nest_asyncio + from jarvis import run +nest_asyncio.apply() + if __name__ == "__main__": asyncio.run(run()) From 10f331765fc8d52e07ff32449eefcf801ee1593b Mon Sep 17 00:00:00 2001 From: Zevaryx Date: Fri, 24 Jun 2022 16:58:04 -0600 Subject: [PATCH 02/19] Add regex for finding image links --- jarvis/cogs/reddit.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/jarvis/cogs/reddit.py b/jarvis/cogs/reddit.py index ff87bca..4f75c6c 100644 --- a/jarvis/cogs/reddit.py +++ b/jarvis/cogs/reddit.py @@ -30,6 +30,7 @@ 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") +image_link = re.compile(r"(.*preview\.redd\.it\/)(.*\..*)\?") class RedditCog(Extension): @@ -89,6 +90,7 @@ class RedditCog(Extension): if post.spoiler: content += "||" content += f"\n\n[View this post]({url})" + content = image_link.sub(content, "https://i.redd.it/\2") if not images and not content: self.logger.debug(f"Post {post.id} had neither content nor images?") From 6a4aba2aa7e6c56b513f0122dd54b276fe7864ba Mon Sep 17 00:00:00 2001 From: Zevaryx Date: Fri, 24 Jun 2022 16:59:33 -0600 Subject: [PATCH 03/19] Change cloc output to image only --- jarvis/cogs/dev.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jarvis/cogs/dev.py b/jarvis/cogs/dev.py index dec5340..3b4386c 100644 --- a/jarvis/cogs/dev.py +++ b/jarvis/cogs/dev.py @@ -292,7 +292,7 @@ class DevCog(Extension): file_bytes.write(raw) file_bytes.seek(0) tokei = File(file_bytes, file_name="tokei.png") - await ctx.send(content=f"```ansi\n{capture.get()}\n```", file=tokei) + await ctx.send(file=tokei) def setup(bot: Client) -> None: From 6055412bcedf74cfdd17e19f940a3a0484e3d2dd Mon Sep 17 00:00:00 2001 From: Zevaryx Date: Fri, 24 Jun 2022 17:15:16 -0600 Subject: [PATCH 04/19] Fix regex pattern matching --- jarvis/cogs/reddit.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/jarvis/cogs/reddit.py b/jarvis/cogs/reddit.py index 4f75c6c..235ac5c 100644 --- a/jarvis/cogs/reddit.py +++ b/jarvis/cogs/reddit.py @@ -30,7 +30,7 @@ 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") -image_link = re.compile(r"(.*preview\.redd\.it\/)(.*\..*)\?") +image_link = re.compile(r".*preview\.redd\.it\/(.*\..*)\?.*") class RedditCog(Extension): @@ -90,7 +90,7 @@ class RedditCog(Extension): if post.spoiler: content += "||" content += f"\n\n[View this post]({url})" - content = image_link.sub(content, "https://i.redd.it/\2") + content = "\n".join(image_link.sub(r"https://i.redd.it/\2", 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?") From f29f2b3cfa6e914e325e2918d7e4ab3696c385c4 Mon Sep 17 00:00:00 2001 From: Zevaryx Date: Fri, 24 Jun 2022 17:17:20 -0600 Subject: [PATCH 05/19] Fix regex pattern --- jarvis/cogs/reddit.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/jarvis/cogs/reddit.py b/jarvis/cogs/reddit.py index 235ac5c..ed93c54 100644 --- a/jarvis/cogs/reddit.py +++ b/jarvis/cogs/reddit.py @@ -30,7 +30,7 @@ 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") -image_link = re.compile(r".*preview\.redd\.it\/(.*\..*)\?.*") +image_link = re.compile(r"https?://(?:www)?\.?preview\.redd\.it\/(.*\..*)\?.*") class RedditCog(Extension): @@ -90,7 +90,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/\2", x) for x in content.split("\n")) + 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?") From 73d6cefa1953121b290968054368b186a61062ed Mon Sep 17 00:00:00 2001 From: Zevaryx Date: Mon, 27 Jun 2022 09:00:56 -0600 Subject: [PATCH 06/19] Add caching to db ship api calls --- jarvis/cogs/dbrand.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/jarvis/cogs/dbrand.py b/jarvis/cogs/dbrand.py index 45f6c68..a2685b2 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"], From 469c0a935169cde25f973aa9a6a474f4f789555e Mon Sep 17 00:00:00 2001 From: Zevaryx Date: Wed, 10 Aug 2022 14:28:49 -0600 Subject: [PATCH 07/19] Add user cache to ctc2 guesses --- jarvis/cogs/ctc2.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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( From 895dc87448d5c693bbed6e11e6ac72526400c95e Mon Sep 17 00:00:00 2001 From: Zevaryx Date: Wed, 10 Aug 2022 14:30:18 -0600 Subject: [PATCH 08/19] Update dependencies, add NAFF in gitignore for dev purposes --- .gitignore | 3 +++ poetry.lock | 12 +++++------- 2 files changed, 8 insertions(+), 7 deletions(-) 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/poetry.lock b/poetry.lock index cf0685a..f9748f6 100644 --- a/poetry.lock +++ b/poetry.lock @@ -414,7 +414,7 @@ testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest- [[package]] name = "jarvis-core" -version = "0.10.2" +version = "0.11.0" description = "JARVIS core" category = "main" optional = false @@ -435,7 +435,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 = "fce3b829a30583abd48b3221825c3ed303610de8" [[package]] name = "jinxed" @@ -525,7 +525,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 +540,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]] @@ -1557,10 +1558,7 @@ 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"}, From c1972bd5da081b0bb61bbb9e7fc250a121a5d1f4 Mon Sep 17 00:00:00 2001 From: Zevaryx Date: Wed, 10 Aug 2022 14:31:51 -0600 Subject: [PATCH 09/19] Add redditor following --- jarvis/cogs/reddit.py | 145 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 136 insertions(+), 9 deletions(-) diff --git a/jarvis/cogs/reddit.py b/jarvis/cogs/reddit.py index ed93c54..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,9 @@ 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\/(.*\..*)\?.*") @@ -124,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", @@ -140,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 @@ -191,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)) From dc26d1456655abf86bf9ac358923431eba5ba67a Mon Sep 17 00:00:00 2001 From: Zevaryx Date: Wed, 10 Aug 2022 14:35:47 -0600 Subject: [PATCH 10/19] Fix bug in avatar with non-guild users --- jarvis/cogs/util.py | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) 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) From 4269ad0a219318a802e4c175f850fa3c66132a5f Mon Sep 17 00:00:00 2001 From: Zevaryx Date: Wed, 10 Aug 2022 14:54:04 -0600 Subject: [PATCH 11/19] Clean up thread names, closes #163, closes #165 --- jarvis/client.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) 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") From 78a2ac014deeced17681b5c45624f1f55740a6a6 Mon Sep 17 00:00:00 2001 From: Zevaryx Date: Wed, 10 Aug 2022 14:55:19 -0600 Subject: [PATCH 12/19] Remove limits on issue command for now, closes #164 --- jarvis/cogs/gl.py | 3 --- 1 file changed, 3 deletions(-) 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( From f7553f5c8f0706cb1f3cf04da76aa0edc4f6bafd Mon Sep 17 00:00:00 2001 From: Zevaryx Date: Wed, 10 Aug 2022 22:21:50 -0600 Subject: [PATCH 13/19] Add tag support --- jarvis/cogs/tags.py | 276 ++++++++++++++++++++++++++++++++++++++++++++ poetry.lock | 30 ++++- pyproject.toml | 1 + 3 files changed, 304 insertions(+), 3 deletions(-) create mode 100644 jarvis/cogs/tags.py diff --git a/jarvis/cogs/tags.py b/jarvis/cogs/tags.py new file mode 100644 index 0000000..5034cdf --- /dev/null +++ b/jarvis/cogs/tags.py @@ -0,0 +1,276 @@ +"""JARVIS Tags Cog.""" +import asyncio +import re + +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, +) + + +class TagCog(Extension): + + 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=1000, + ), + ], + ) + + 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") + 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: + 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 + + 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) + + @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: + 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=1000, + ), + ], + ) + + 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") + 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: + 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 + + tag.content = re.sub(r"\\?([@<])", r"\\\g<1>", content) + tag.name = name + + 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) + + @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") + + @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 = None, None, None + author = await self.bot.fetch_user(tag.creator) + if author: + username = author.username + discrim = author.discriminator + url = author.display_avatar.url + + 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""), + ], + ) + + embed.set_author( + name=f"{username}#{discrim}" if username else "Unknown Author", + 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: + tags = await Tag.find(q(guild=ctx.guild.id)).to_list(None) + names = [tag.name for tag in tags] + results = process.extract(name, names, 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/poetry.lock b/poetry.lock index f9748f6..c56cab1 100644 --- a/poetry.lock +++ b/poetry.lock @@ -414,7 +414,7 @@ testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest- [[package]] name = "jarvis-core" -version = "0.11.0" +version = "0.12.0" description = "JARVIS core" category = "main" optional = false @@ -435,7 +435,7 @@ umongo = "^3.1.0" type = "git" url = "https://git.zevaryx.com/stark-industries/jarvis/jarvis-core.git" reference = "main" -resolved_reference = "fce3b829a30583abd48b3221825c3ed303610de8" +resolved_reference = "4cece14cd9cd1604bf12845339fdb5f66b6c0719" [[package]] name = "jinxed" @@ -825,6 +825,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" @@ -955,6 +963,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" @@ -1168,7 +1190,7 @@ testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest- [metadata] lock-version = "1.1" python-versions = "^3.10" -content-hash = "549486089ef65c69b0932e799efdbb0d22d6631d925de845ec4b6ba98d57c527" +content-hash = "d4a3ccd2f79fe0c323784bfba2c5950817257639bbdcdb57a6e71682a8846504" [metadata.files] aiofile = [ @@ -1929,6 +1951,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"}, @@ -2094,6 +2117,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"}, diff --git a/pyproject.toml b/pyproject.toml index 9c455c1..5417210 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,6 +29,7 @@ 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} From 85bce061a17fbb900383933038c61a25a0ea76d0 Mon Sep 17 00:00:00 2001 From: Zevaryx Date: Thu, 11 Aug 2022 14:42:36 -0600 Subject: [PATCH 14/19] Improve functionality of tags --- jarvis/cogs/tags.py | 69 +++++++++++++++++++++++++++++++++++++-------- poetry.lock | 2 +- 2 files changed, 59 insertions(+), 12 deletions(-) diff --git a/jarvis/cogs/tags.py b/jarvis/cogs/tags.py index 5034cdf..512e302 100644 --- a/jarvis/cogs/tags.py +++ b/jarvis/cogs/tags.py @@ -1,6 +1,8 @@ """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 @@ -24,6 +26,9 @@ invites = re.compile( 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") @@ -62,7 +67,7 @@ class TagCog(Extension): placeholder="Content to send here", style=TextStyles.PARAGRAPH, custom_id="content", - max_length=1000, + max_length=512, ), ], ) @@ -70,14 +75,21 @@ class TagCog(Extension): 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") + 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: + 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 ) @@ -113,6 +125,9 @@ class TagCog(Extension): ) 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( @@ -123,6 +138,7 @@ class TagCog(Extension): 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) @@ -149,7 +165,7 @@ class TagCog(Extension): value=tag.content, style=TextStyles.PARAGRAPH, custom_id="content", - max_length=1000, + max_length=512, ), ], ) @@ -157,14 +173,28 @@ class TagCog(Extension): 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") + 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: + 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 ) @@ -175,6 +205,8 @@ class TagCog(Extension): 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() @@ -193,6 +225,9 @@ class TagCog(Extension): ) 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( @@ -218,6 +253,7 @@ class TagCog(Extension): 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( @@ -233,12 +269,13 @@ class TagCog(Extension): await ctx.send("Tag not found", ephemeral=True) return - username, discrim, url = None, None, None + 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()) @@ -249,11 +286,20 @@ class TagCog(Extension): 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 Author", + name=f"{username}#{discrim}" if username else "Unknown User", icon_url=url, ) @@ -264,9 +310,10 @@ class TagCog(Extension): @_delete.autocomplete("name") @_info.autocomplete("name") async def _autocomplete(self, ctx: AutocompleteContext, name: str) -> None: - tags = await Tag.find(q(guild=ctx.guild.id)).to_list(None) - names = [tag.name for tag in tags] - results = process.extract(name, names, limit=25) + 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) diff --git a/poetry.lock b/poetry.lock index c56cab1..6b3616f 100644 --- a/poetry.lock +++ b/poetry.lock @@ -435,7 +435,7 @@ umongo = "^3.1.0" type = "git" url = "https://git.zevaryx.com/stark-industries/jarvis/jarvis-core.git" reference = "main" -resolved_reference = "4cece14cd9cd1604bf12845339fdb5f66b6c0719" +resolved_reference = "fe24fce330cfd23a7af3834ef11b675780e6325d" [[package]] name = "jinxed" From b84f5a771e705ebd30c3b047d697fb0007d04392 Mon Sep 17 00:00:00 2001 From: Zevaryx Date: Thu, 11 Aug 2022 14:43:57 -0600 Subject: [PATCH 15/19] Add stop command, fix logging --- jarvis/__init__.py | 7 +++++++ jarvis/cogs/botutil.py | 7 +++++++ 2 files changed, 14 insertions(+) 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/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 = [] From 1863ad6c25ad637396ac5077f3dc09d2f2f21137 Mon Sep 17 00:00:00 2001 From: Zevaryx Date: Thu, 11 Aug 2022 14:43:57 -0600 Subject: [PATCH 16/19] Add stop command, fix logging --- jarvis/__init__.py | 7 +++++++ jarvis/cogs/botutil.py | 7 +++++++ 2 files changed, 14 insertions(+) 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/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 = [] From 080fba0fb086ebe979d8d448d796afff2ace128f Mon Sep 17 00:00:00 2001 From: Zevaryx Date: Thu, 11 Aug 2022 22:24:18 -0600 Subject: [PATCH 17/19] Update dependencies --- poetry.lock | 30 +++++++----------------------- 1 file changed, 7 insertions(+), 23 deletions(-) diff --git a/poetry.lock b/poetry.lock index 6b3616f..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" @@ -553,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" @@ -1109,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" @@ -1299,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"}, @@ -2159,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"}, From 73e8cd87d175598617575108b4973e8a30084060 Mon Sep 17 00:00:00 2001 From: Zevaryx Date: Fri, 12 Aug 2022 11:07:40 -0600 Subject: [PATCH 18/19] Restrict tag name characters --- jarvis/cogs/tags.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/jarvis/cogs/tags.py b/jarvis/cogs/tags.py index 512e302..3d25a30 100644 --- a/jarvis/cogs/tags.py +++ b/jarvis/cogs/tags.py @@ -23,6 +23,7 @@ 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): @@ -97,6 +98,9 @@ class TagCog(Extension): 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_- ]") + return tag = await Tag.find_one(q(guild=ctx.guild.id, name=name)) if tag: @@ -202,6 +206,9 @@ class TagCog(Extension): 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_- ]") + return tag.content = re.sub(r"\\?([@<])", r"\\\g<1>", content) tag.name = name From edab8c5e9b0934b1b338f3974398d59af112a347 Mon Sep 17 00:00:00 2001 From: Zevaryx Date: Fri, 12 Aug 2022 11:09:21 -0600 Subject: [PATCH 19/19] Ephemeral message on invalid tag name --- jarvis/cogs/tags.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/jarvis/cogs/tags.py b/jarvis/cogs/tags.py index 3d25a30..27aa648 100644 --- a/jarvis/cogs/tags.py +++ b/jarvis/cogs/tags.py @@ -99,7 +99,7 @@ class TagCog(Extension): 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_- ]") + 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)) @@ -207,7 +207,7 @@ class TagCog(Extension): 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_- ]") + await response.send("Tag name must only contain: [A-Za-z0-9_- ]", ephemeral=True) return tag.content = re.sub(r"\\?([@<])", r"\\\g<1>", content)