v2.2.0
This commit is contained in:
commit
c806f14935
13 changed files with 575 additions and 66 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -145,3 +145,6 @@ config.yaml
|
|||
|
||||
# VSCode
|
||||
.vscode/
|
||||
|
||||
# Custom NAFF versions
|
||||
naff/
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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 = []
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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"],
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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))
|
||||
|
|
330
jarvis/cogs/tags.py
Normal file
330
jarvis/cogs/tags.py
Normal file
|
@ -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"<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)
|
|
@ -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)
|
||||
|
|
80
poetry.lock
generated
80
poetry.lock
generated
|
@ -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"},
|
||||
|
|
|
@ -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}
|
||||
|
|
4
run.py
4
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())
|
||||
|
|
Loading…
Add table
Reference in a new issue