Merge branch 'tags' into 'dev'

Tags

See merge request stark-industries/jarvis/jarvis-bot!71
This commit is contained in:
Zeva Rose 2022-08-12 04:25:01 +00:00
commit 89f0bf59d3
3 changed files with 358 additions and 26 deletions

323
jarvis/cogs/tags.py Normal file
View file

@ -0,0 +1,323 @@
"""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,
)
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
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
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"<t:{ts}: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"<t:{ets}: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)

60
poetry.lock generated
View file

@ -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.11.0"
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 = "fce3b829a30583abd48b3221825c3ed303610de8"
resolved_reference = "fe24fce330cfd23a7af3834ef11b675780e6325d"
[[package]]
name = "jinxed"
@ -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"
@ -825,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"
@ -955,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"
@ -1087,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"
@ -1168,7 +1178,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 = [
@ -1277,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"},
@ -1929,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"},
@ -2094,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"},
@ -2135,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"},

View file

@ -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}