v2.0 Beta 1

Closes #82, #134, #132, and #131

See merge request stark-industries/jarvis/jarvis-bot!51
This commit is contained in:
Zeva Rose 2022-04-19 23:56:17 +00:00
commit 2aeb064f1d
35 changed files with 2001 additions and 1655 deletions

View file

@ -1,6 +1,7 @@
[flake8] [flake8]
extend-ignore = extend-ignore =
Q0, E501, C812, E203, W503, # These default to arguing with Black. We might configure some of them eventually Q0, E501, C812, E203, W503, # These default to arguing with Black. We might configure some of them eventually
ANN002, ANN003, # Ignore *args, **kwargs
ANN1, # Ignore self and cls annotations ANN1, # Ignore self and cls annotations
ANN204, ANN206, # return annotations for special methods and class methods ANN204, ANN206, # return annotations for special methods and class methods
D105, D107, # Missing Docstrings in magic method and __init__ D105, D107, # Missing Docstrings in magic method and __init__
@ -10,5 +11,6 @@ extend-ignore =
D101, # Missing docstring in public class D101, # Missing docstring in public class
# Plugins we don't currently include: flake8-return # Plugins we don't currently include: flake8-return
R502, # do not implicitly return None in function able to return non-None value.
R503, # missing explicit return at the end of function ableto return non-None value. R503, # missing explicit return at the end of function ableto return non-None value.
max-line-length=100 max-line-length=100

View file

@ -19,7 +19,7 @@ repos:
- id: python-check-blanket-noqa - id: python-check-blanket-noqa
- repo: https://github.com/psf/black - repo: https://github.com/psf/black
rev: 22.1.0 rev: 22.3.0
hooks: hooks:
- id: black - id: black
args: [--line-length=100, --target-version=py310] args: [--line-length=100, --target-version=py310]

View file

@ -1,24 +1,34 @@
--- ---
token: api key here token: bot token
client_id: 123456789012345678 twitter:
logo: alligator2 consumer_key: key
consumer_secret: secret
access_token: access token
access_secret: access secret
mongo: mongo:
connect: connect:
username: user username: username
password: pass password: password
host: localhost host: hostname
port: 27017 port: 27017
database: database database: database
urls: urls:
url_name: url extra: urls
url_name2: url2 max_messages: 10000
max_messages: 1000 gitlab_token: token
gitlab_token: null
cogs: cogs:
- list - admin
- of - autoreact
- enabled - dev
- cogs - image
- all - gl
- if - remindme
- empty - rolegiver
# - settings
- starboard
- twitter
- util
- verify
log_level: INFO
sync: false
#sync_commands: True

View file

@ -1,41 +1,53 @@
"""Main J.A.R.V.I.S. package.""" """Main JARVIS package."""
import logging import logging
from importlib.metadata import version as _v from importlib.metadata import version as _v
from dis_snek import Intents from dis_snek import Intents
from jarvis_core.db import connect from jarvis_core.db import connect
from jarvis_core.log import get_logger
from jarvis import utils from jarvis import utils
from jarvis.client import Jarvis from jarvis.client import Jarvis
from jarvis.config import get_config from jarvis.config import JarvisConfig
try: try:
__version__ = _v("jarvis") __version__ = _v("jarvis")
except Exception: except Exception:
__version__ = "0.0.0" __version__ = "0.0.0"
jconfig = get_config() jconfig = JarvisConfig.from_yaml()
logger = get_logger("jarvis")
logger = logging.getLogger("discord") logger.setLevel(jconfig.log_level)
logger.setLevel(logging.getLevelName(jconfig.log_level))
file_handler = logging.FileHandler(filename="jarvis.log", encoding="UTF-8", mode="w") file_handler = logging.FileHandler(filename="jarvis.log", encoding="UTF-8", mode="w")
file_handler.setFormatter(logging.Formatter("[%(asctime)s][%(levelname)s][%(name)s] %(message)s")) file_handler.setFormatter(
logging.Formatter("[%(asctime)s] [%(name)s] [%(levelname)8s] %(message)s")
)
logger.addHandler(file_handler) logger.addHandler(file_handler)
intents = Intents.DEFAULT | Intents.MESSAGES | Intents.GUILD_MEMBERS | Intents.GUILD_MESSAGES intents = Intents.DEFAULT | Intents.MESSAGES | Intents.GUILD_MEMBERS | Intents.GUILD_MESSAGES
restart_ctx = None restart_ctx = None
jarvis = Jarvis(
jarvis = Jarvis(intents=intents, default_prefix="!", sync_interactions=jconfig.sync) intents=intents,
sync_interactions=jconfig.sync,
delete_unused_application_cmds=True,
send_command_tracebacks=False,
)
async def run() -> None: async def run() -> None:
"""Run J.A.R.V.I.S.""" """Run JARVIS"""
logger.info("Starting JARVIS")
logger.debug("Connecting to database")
connect(**jconfig.mongo["connect"], testing=jconfig.mongo["database"] != "jarvis") connect(**jconfig.mongo["connect"], testing=jconfig.mongo["database"] != "jarvis")
jconfig.get_db_config() logger.debug("Loading configuration from database")
# jconfig.get_db_config()
logger.debug("Loading extensions")
for extension in utils.get_extensions(): for extension in utils.get_extensions():
jarvis.load_extension(extension) jarvis.load_extension(extension)
logger.debug(f"Loaded {extension}")
jarvis.max_messages = jconfig.max_messages jarvis.max_messages = jconfig.max_messages
logger.debug("Running JARVIS")
await jarvis.astart(jconfig.token) await jarvis.astart(jconfig.token)

View file

@ -1,18 +1,25 @@
"""Custom JARVIS client.""" """Custom JARVIS client."""
import logging
import re import re
import traceback import traceback
from datetime import datetime from datetime import datetime, timezone
from aiohttp import ClientSession from aiohttp import ClientSession
from dis_snek import Snake, listen from dis_snek import Snake, listen
from dis_snek.api.events.discord import MessageCreate, MessageDelete, MessageUpdate from dis_snek.api.events.discord import (
MemberAdd,
MemberRemove,
MessageCreate,
MessageDelete,
MessageUpdate,
)
from dis_snek.client.errors import CommandCheckFailure, CommandOnCooldown
from dis_snek.client.utils.misc_utils import find_all from dis_snek.client.utils.misc_utils import find_all
from dis_snek.models.discord.channel import DMChannel from dis_snek.models.discord.channel import DMChannel
from dis_snek.models.discord.embed import EmbedField from dis_snek.models.discord.embed import EmbedField
from dis_snek.models.discord.enums import Permissions from dis_snek.models.discord.enums import Permissions
from dis_snek.models.discord.message import Message from dis_snek.models.discord.message import Message
from dis_snek.models.discord.user import Member from dis_snek.models.snek.context import Context, InteractionContext, MessageContext
from dis_snek.models.snek.context import Context, InteractionContext
from dis_snek.models.snek.tasks.task import Task from dis_snek.models.snek.tasks.task import Task
from dis_snek.models.snek.tasks.triggers import IntervalTrigger from dis_snek.models.snek.tasks.triggers import IntervalTrigger
from jarvis_core.db import q from jarvis_core.db import q
@ -30,6 +37,7 @@ DEFAULT_SITE = "https://paste.zevs.me"
ERROR_MSG = """ ERROR_MSG = """
Command Information: Command Information:
Guild: {guild_name}
Name: {invoked_name} Name: {invoked_name}
Args: Args:
{arg_str} {arg_str}
@ -49,15 +57,20 @@ CMD_FMT = fmt(Fore.GREEN, Format.BOLD)
class Jarvis(Snake): class Jarvis(Snake):
def __init__(self, *args, **kwargs): # noqa: ANN002 ANN003 def __init__(self, *args, **kwargs): # noqa: ANN002 ANN003
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.logger = logging.getLogger(__name__)
self.phishing_domains = [] self.phishing_domains = []
self.pre_run_callback = self._prerun
@Task.create(IntervalTrigger(days=1)) @Task.create(IntervalTrigger(days=1))
async def _update_domains(self) -> None: async def _update_domains(self) -> None:
self.logger.debug("Updating phishing domains")
async with ClientSession(headers={"X-Identity": "Discord: zevaryx#5779"}) as session: async with ClientSession(headers={"X-Identity": "Discord: zevaryx#5779"}) as session:
response = await session.get("https://phish.sinking.yachts/v2/recent/86415") response = await session.get("https://phish.sinking.yachts/v2/recent/86415")
response.raise_for_status() response.raise_for_status()
data = await response.json() data = await response.json()
self.logger.debug(f"Found {len(data)} changes to phishing domains")
for update in data: for update in data:
if update["type"] == "add": if update["type"] == "add":
if update["domain"] not in self.phishing_domains: if update["domain"] not in self.phishing_domains:
@ -66,20 +79,29 @@ class Jarvis(Snake):
if update["domain"] in self.phishing_domains: if update["domain"] in self.phishing_domains:
self.phishing_domains.remove(update["domain"]) self.phishing_domains.remove(update["domain"])
async def _prerun(self, ctx: Context, *args, **kwargs) -> None:
name = ctx.invoked_name
if isinstance(ctx, InteractionContext) and ctx.target_id:
kwargs["context target"] = ctx.target
args = " ".join(f"{k}:{v}" for k, v in kwargs.items())
self.logger.debug(f"Running command `{name}` with args: {args or 'None'}")
async def _sync_domains(self) -> None: async def _sync_domains(self) -> None:
self.logger.debug("Loading phishing domains")
async with ClientSession(headers={"X-Identity": "Discord: zevaryx#5779"}) as session: async with ClientSession(headers={"X-Identity": "Discord: zevaryx#5779"}) as session:
response = await session.get("https://phish.sinking.yachts/v2/all") response = await session.get("https://phish.sinking.yachts/v2/all")
response.raise_for_status() response.raise_for_status()
self.phishing_domains = await response.json() self.phishing_domains = await response.json()
self.logger.info(f"Protected from {len(self.phishing_domains)} phishing domains")
@listen() @listen()
async def on_ready(self) -> None: async def on_ready(self) -> None:
"""Lepton on_ready override.""" """Lepton on_ready override."""
await self._sync_domains() await self._sync_domains()
self._update_domains.start() self._update_domains.start()
print("Logged in as {}".format(self.user)) # noqa: T001 self.logger.info("Logged in as {}".format(self.user)) # noqa: T001
print("Connected to {} guild(s)".format(len(self.guilds))) # noqa: T001 self.logger.info("Connected to {} guild(s)".format(len(self.guilds))) # noqa: T001
print( # noqa: T001 self.logger.info( # noqa: T001
"https://discord.com/api/oauth2/authorize?client_id=" "https://discord.com/api/oauth2/authorize?client_id="
"{}&permissions=8&scope=bot%20applications.commands".format(self.user.id) "{}&permissions=8&scope=bot%20applications.commands".format(self.user.id)
) )
@ -88,37 +110,57 @@ class Jarvis(Snake):
self, ctx: Context, error: Exception, *args: list, **kwargs: dict self, ctx: Context, error: Exception, *args: list, **kwargs: dict
) -> None: ) -> None:
"""Lepton on_command_error override.""" """Lepton on_command_error override."""
self.logger.debug(f"Handling error in {ctx.invoked_name}: {error}")
if isinstance(error, CommandOnCooldown):
await ctx.send(str(error), ephemeral=True)
return
elif isinstance(error, CommandCheckFailure):
await ctx.send("I'm afraid I can't let you do that", ephemeral=True)
return
guild = await self.fetch_guild(DEFAULT_GUILD) guild = await self.fetch_guild(DEFAULT_GUILD)
channel = await guild.fetch_channel(DEFAULT_ERROR_CHANNEL) channel = await guild.fetch_channel(DEFAULT_ERROR_CHANNEL)
error_time = datetime.utcnow().strftime("%d-%m-%Y %H:%M-%S.%f UTC") error_time = datetime.now(tz=timezone.utc).strftime("%d-%m-%Y %H:%M-%S.%f UTC")
timestamp = int(datetime.now().timestamp()) timestamp = int(datetime.now(tz=timezone.utc).timestamp())
timestamp = f"<t:{timestamp}:T>" timestamp = f"<t:{timestamp}:T>"
arg_str = ( arg_str = ""
"\n".join(f" {k}: {v}" for k, v in ctx.kwargs.items()) if ctx.kwargs else " None" if isinstance(ctx, InteractionContext) and ctx.target_id:
) ctx.kwargs["context target"] = ctx.target
if isinstance(ctx, InteractionContext):
for k, v in ctx.kwargs.items():
arg_str += f" {k}: "
if isinstance(v, str) and len(v) > 100:
v = v[97] + "..."
arg_str += f"{v}\n"
elif isinstance(ctx, MessageContext):
for v in ctx.args:
if isinstance(v, str) and len(v) > 100:
v = v[97] + "..."
arg_str += f" - {v}"
callback_args = "\n".join(f" - {i}" for i in args) if args else " None" callback_args = "\n".join(f" - {i}" for i in args) if args else " None"
callback_kwargs = ( callback_kwargs = (
"\n".join(f" {k}: {v}" for k, v in kwargs.items()) if kwargs else " None" "\n".join(f" {k}: {v}" for k, v in kwargs.items()) if kwargs else " None"
) )
full_message = ERROR_MSG.format( full_message = ERROR_MSG.format(
guild_name=ctx.guild.name,
error_time=error_time, error_time=error_time,
invoked_name=ctx.invoked_name, invoked_name=ctx.invoked_name,
arg_str=arg_str, arg_str=arg_str,
callback_args=callback_args, callback_args=callback_args,
callback_kwargs=callback_kwargs, callback_kwargs=callback_kwargs,
) )
if len(full_message) >= 1900: error_message = "".join(traceback.format_exception(error))
error_message = " ".join(traceback.format_exception(error)) if len(full_message + error_message) >= 1800:
error_message = "\n ".join(error_message.split("\n"))
full_message += "Exception: |\n " + error_message full_message += "Exception: |\n " + error_message
paste = Paste(content=full_message) paste = Paste(content=full_message, site=DEFAULT_SITE)
await paste.save(DEFAULT_SITE) key = await paste.save()
self.logger.debug(f"Large traceback, saved to Pasty {paste.id}, {key=}")
await channel.send( await channel.send(
f"JARVIS encountered an error at {timestamp}. Log too big to send over Discord." f"JARVIS encountered an error at {timestamp}. Log too big to send over Discord."
f"\nPlease see log at {paste.url}" f"\nPlease see log at {paste.url}"
) )
else: else:
error_message = "".join(traceback.format_exception(error))
await channel.send( await channel.send(
f"JARVIS encountered an error at {timestamp}:" f"JARVIS encountered an error at {timestamp}:"
f"\n```yaml\n{full_message}\n```" f"\n```yaml\n{full_message}\n```"
@ -128,13 +170,28 @@ class Jarvis(Snake):
return await super().on_command_error(ctx, error, *args, **kwargs) return await super().on_command_error(ctx, error, *args, **kwargs)
# Modlog # Modlog
async def on_command(self, ctx: InteractionContext) -> None: async def on_command(self, ctx: Context) -> None:
"""Lepton on_command override.""" """Lepton on_command override."""
if not isinstance(ctx.channel, DMChannel) and ctx.invoked_name not in ["pw"]: if not isinstance(ctx.channel, DMChannel) and ctx.invoked_name not in ["pw"]:
modlog = await Setting.find_one(q(guild=ctx.guild.id, setting="modlog")) modlog = await Setting.find_one(q(guild=ctx.guild.id, setting="activitylog"))
if modlog: if modlog:
channel = await ctx.guild.fetch_channel(modlog.value) channel = await ctx.guild.fetch_channel(modlog.value)
args = " ".join(f"{KEY_FMT}{k}:{VAL_FMT}{v}{RESET}" for k, v in ctx.kwargs.items()) args = []
if isinstance(ctx, InteractionContext) and ctx.target_id:
args.append(f"{KEY_FMT}context target:{VAL_FMT}{ctx.target}{RESET}")
if isinstance(ctx, InteractionContext):
for k, v in ctx.kwargs.items():
if isinstance(v, str):
v = v.replace("`", "\\`")
if len(v) > 100:
v = v[:97] + "..."
args.append(f"{KEY_FMT}{k}:{VAL_FMT}{v}{RESET}")
elif isinstance(ctx, MessageContext):
for v in ctx.args:
if isinstance(v, str) and len(v) > 100:
v = v[97] + "..."
args.append(f"{VAL_FMT}{v}{RESET}")
args = " ".join(args)
fields = [ fields = [
EmbedField( EmbedField(
name="Command", name="Command",
@ -144,7 +201,7 @@ class Jarvis(Snake):
] ]
embed = build_embed( embed = build_embed(
title="Command Invoked", title="Command Invoked",
description=f"{ctx.author.mention} invoked a command", description=f"{ctx.author.mention} invoked a command in {ctx.channel.mention}",
fields=fields, fields=fields,
color="#fc9e3f", color="#fc9e3f",
) )
@ -152,22 +209,54 @@ class Jarvis(Snake):
embed.set_footer( embed.set_footer(
text=f"{ctx.author.user.username}#{ctx.author.discriminator} | {ctx.author.id}" text=f"{ctx.author.user.username}#{ctx.author.discriminator} | {ctx.author.id}"
) )
await channel.send(embed=embed) if channel:
await channel.send(embed=embed)
else:
self.logger.warning(
f"Activitylog channel no longer exists in {ctx.guild.name}, removing"
)
await modlog.delete()
# Events # Events
async def on_member_join(self, user: Member) -> None: # Member
"""Handle on_member_join event.""" @listen()
guild = user.guild async def on_member_add(self, event: MemberAdd) -> None:
"""Handle on_member_add event."""
user = event.member
guild = event.guild
unverified = await Setting.find_one(q(guild=guild.id, setting="unverified")) unverified = await Setting.find_one(q(guild=guild.id, setting="unverified"))
if unverified: if unverified:
role = guild.get_role(unverified.value) self.logger.debug(f"Applying unverified role to {user.id} in {guild.id}")
role = await guild.fetch_role(unverified.value)
if role not in user.roles: if role not in user.roles:
await user.add_role(role, reason="User just joined and is unverified") await user.add_role(role, reason="User just joined and is unverified")
@listen()
async def on_member_remove(self, event: MemberRemove) -> None:
"""Handle on_member_remove event."""
user = event.member
guild = event.guild
log = await Setting.find_one(q(guild=guild.id, setting="activitylog"))
if log:
self.logger.debug(f"User {user.id} left {guild.id}")
channel = await guild.fetch_channel(log.channel)
embed = build_embed(
title="Member Left",
desciption=f"{user.username}#{user.discriminator} left {guild.name}",
fields=[],
)
embed.set_author(name=user.username, icon_url=user.avatar.url)
embed.set_footer(text=f"{user.username}#{user.discriminator} | {user.id}")
await channel.send(embed=embed)
# Message
async def autopurge(self, message: Message) -> None: async def autopurge(self, message: Message) -> None:
"""Handle autopurge events.""" """Handle autopurge events."""
autopurge = await Autopurge.find_one(q(guild=message.guild.id, channel=message.channel.id)) autopurge = await Autopurge.find_one(q(guild=message.guild.id, channel=message.channel.id))
if autopurge: if autopurge:
self.logger.debug(
f"Autopurging message {message.guild.id}/{message.channel.id}/{message.id}"
)
await message.delete(delay=autopurge.delay) await message.delete(delay=autopurge.delay)
async def autoreact(self, message: Message) -> None: async def autoreact(self, message: Message) -> None:
@ -179,8 +268,16 @@ class Jarvis(Snake):
) )
) )
if autoreact: if autoreact:
self.logger.debug(
f"Autoreacting to message {message.guild.id}/{message.channel.id}/{message.id}"
)
for reaction in autoreact.reactions: for reaction in autoreact.reactions:
await message.add_reaction(reaction) await message.add_reaction(reaction)
if autoreact.thread:
name = message.content
if len(name) > 100:
name = name[:97] + "..."
await message.create_thread(name=message.content, reason="Autoreact")
async def checks(self, message: Message) -> None: async def checks(self, message: Message) -> None:
"""Other message checks.""" """Other message checks."""
@ -197,24 +294,28 @@ class Jarvis(Snake):
setting = Setting(guild=message.guild.id, setting="noinvite", value=True) setting = Setting(guild=message.guild.id, setting="noinvite", value=True)
await setting.commit() await setting.commit()
if match: if match:
guild_invites = await message.guild.invites() guild_invites = await message.guild.fetch_invites()
guild_invites.append(message.guild.vanity_url_code) if message.guild.vanity_url_code:
guild_invites.append(message.guild.vanity_url_code)
allowed = [x.code for x in guild_invites] + [ allowed = [x.code for x in guild_invites] + [
"dbrand", "dbrand",
"VtgZntXcnZ", "VtgZntXcnZ",
"gPfYGbvTCE", "gPfYGbvTCE",
] ]
if match.group(1) not in allowed and setting.value: if (m := match.group(1)) not in allowed and setting.value:
await message.delete() self.logger.debug(f"Removing non-allowed invite `{m}` from {message.guild.id}")
w = Warning( try:
await message.delete()
except Exception:
self.logger.debug("Message deleted before action taken")
await Warning(
active=True, active=True,
admin=self.user.id, admin=self.user.id,
duration=24, duration=24,
guild=message.guild.id, guild=message.guild.id,
reason="Sent an invite link", reason="Sent an invite link",
user=message.author.id, user=message.author.id,
) ).commit()
await w.commit()
embed = warning_embed(message.author, "Sent an invite link") embed = warning_embed(message.author, "Sent an invite link")
await message.channel.send(embed=embed) await message.channel.send(embed=embed)
@ -234,20 +335,24 @@ class Jarvis(Snake):
- (1 if message.author.id in message._mention_ids else 0) # noqa: W503 - (1 if message.author.id in message._mention_ids else 0) # noqa: W503
> massmention.value # noqa: W503 > massmention.value # noqa: W503
): ):
w = Warning( self.logger.debug(
f"Massmention threshold on {message.guild.id}/{message.channel.id}/{message.id}"
)
await Warning(
active=True, active=True,
admin=self.user.id, admin=self.user.id,
duration=24, duration=24,
guild=message.guild.id, guild=message.guild.id,
reason="Mass Mention", reason="Mass Mention",
user=message.author.id, user=message.author.id,
) ).commit()
await w.commit()
embed = warning_embed(message.author, "Mass Mention") embed = warning_embed(message.author, "Mass Mention")
await message.channel.send(embed=embed) await message.channel.send(embed=embed)
async def roleping(self, message: Message) -> None: async def roleping(self, message: Message) -> None:
"""Handle roleping events.""" """Handle roleping events."""
if message.author.has_permission(Permissions.MANAGE_GUILD):
return
if await Roleping.collection.count_documents(q(guild=message.guild.id, active=True)) == 0: if await Roleping.collection.count_documents(q(guild=message.guild.id, active=True)) == 0:
return return
rolepings = await Roleping.find(q(guild=message.guild.id, active=True)).to_list(None) rolepings = await Roleping.find(q(guild=message.guild.id, active=True)).to_list(None)
@ -286,31 +391,35 @@ class Jarvis(Snake):
break break
if role_in_rolepings and user_missing_role and not user_is_admin and not user_has_bypass: if role_in_rolepings and user_missing_role and not user_is_admin and not user_has_bypass:
w = Warning( self.logger.debug(
f"Rolepinged role in {message.guild.id}/{message.channel.id}/{message.id}"
)
await Warning(
active=True, active=True,
admin=self.user.id, admin=self.user.id,
duration=24, duration=24,
guild=message.guild.id, guild=message.guild.id,
reason="Pinged a blocked role/user with a blocked role", reason="Pinged a blocked role/user with a blocked role",
user=message.author.id, user=message.author.id,
) ).commit()
await w.commit()
embed = warning_embed(message.author, "Pinged a blocked role/user with a blocked role") embed = warning_embed(message.author, "Pinged a blocked role/user with a blocked role")
await message.channel.send(embed=embed) await message.channel.send(embed=embed)
async def phishing(self, message: Message) -> None: async def phishing(self, message: Message) -> None:
"""Check if the message contains any known phishing domains.""" """Check if the message contains any known phishing domains."""
for match in url.finditer(message.content): for match in url.finditer(message.content):
if match.group("domain") in self.phishing_domains: if (m := match.group("domain")) in self.phishing_domains:
w = Warning( self.logger.debug(
f"Phishing url `{m}` detected in {message.guild.id}/{message.channel.id}/{message.id}"
)
await Warning(
active=True, active=True,
admin=self.user.id, admin=self.user.id,
duration=24, duration=24,
guild=message.guild.id, guild=message.guild.id,
reason="Phishing URL", reason="Phishing URL",
user=message.author.id, user=message.author.id,
) ).commit()
await w.commit()
embed = warning_embed(message.author, "Phishing URL") embed = warning_embed(message.author, "Phishing URL")
await message.channel.send(embed=embed) await message.channel.send(embed=embed)
await message.delete() await message.delete()
@ -329,15 +438,17 @@ class Jarvis(Snake):
data = await resp.json() data = await resp.json()
for item in data["processed"]["urls"].values(): for item in data["processed"]["urls"].values():
if not item["safe"]: if not item["safe"]:
w = Warning( self.logger.debug(
f"Scam url `{match.string}` detected in {message.guild.id}/{message.channel.id}/{message.id}"
)
await Warning(
active=True, active=True,
admin=self.user.id, admin=self.user.id,
duration=24, duration=24,
guild=message.guild.id, guild=message.guild.id,
reason="Unsafe URL", reason="Unsafe URL",
user=message.author.id, user=message.author.id,
) ).commit()
await w.commit()
reasons = ", ".join(item["not_safe_reasons"]) reasons = ", ".join(item["not_safe_reasons"])
embed = warning_embed(message.author, reasons) embed = warning_embed(message.author, reasons)
await message.channel.send(embed=embed) await message.channel.send(embed=embed)
@ -364,40 +475,45 @@ class Jarvis(Snake):
before = event.before before = event.before
after = event.after after = event.after
if not after.author.bot: if not after.author.bot:
modlog = await Setting.find_one(q(guild=after.guild.id, setting="modlog")) modlog = await Setting.find_one(q(guild=after.guild.id, setting="activitylog"))
if modlog: if modlog:
if not before or before.content == after.content or before.content is None: if not before or before.content == after.content or before.content is None:
return return
channel = before.guild.get_channel(modlog.value) try:
fields = [ channel = before.guild.get_channel(modlog.value)
EmbedField( fields = [
"Original Message", EmbedField(
before.content if before.content else "N/A", "Original Message",
False, before.content if before.content else "N/A",
), False,
EmbedField( ),
"New Message", EmbedField(
after.content if after.content else "N/A", "New Message",
False, after.content if after.content else "N/A",
), False,
] ),
embed = build_embed( ]
title="Message Edited", embed = build_embed(
description=f"{after.author.mention} edited a message", title="Message Edited",
fields=fields, description=f"{after.author.mention} edited a message in {before.channel.mention}",
color="#fc9e3f", fields=fields,
timestamp=after.edited_timestamp, color="#fc9e3f",
url=after.jump_url, timestamp=after.edited_timestamp,
) url=after.jump_url,
embed.set_author( )
name=after.author.username, embed.set_author(
icon_url=after.author.display_avatar.url, name=after.author.username,
url=after.jump_url, icon_url=after.author.display_avatar.url,
) url=after.jump_url,
embed.set_footer( )
text=f"{after.author.user.username}#{after.author.discriminator} | {after.author.id}" embed.set_footer(
) text=f"{after.author.username}#{after.author.discriminator} | {after.author.id}"
await channel.send(embed=embed) )
await channel.send(embed=embed)
except Exception as e:
self.logger.warn(
f"Failed to process edit {before.guild.id}/{before.channel.id}/{before.id}: {e}"
)
if not isinstance(after.channel, DMChannel) and not after.author.bot: if not isinstance(after.channel, DMChannel) and not after.author.bot:
await self.massmention(after) await self.massmention(after)
await self.roleping(after) await self.roleping(after)
@ -411,54 +527,66 @@ class Jarvis(Snake):
async def on_message_delete(self, event: MessageDelete) -> None: async def on_message_delete(self, event: MessageDelete) -> None:
"""Process on_message_delete events.""" """Process on_message_delete events."""
message = event.message message = event.message
modlog = await Setting.find_one(q(guild=message.guild.id, setting="modlog")) modlog = await Setting.find_one(q(guild=message.guild.id, setting="activitylog"))
if modlog: if modlog:
fields = [EmbedField("Original Message", message.content or "N/A", False)] try:
content = message.content or "N/A"
except AttributeError:
content = "N/A"
fields = [EmbedField("Original Message", content, False)]
if message.attachments: try:
value = "\n".join([f"[{x.filename}]({x.url})" for x in message.attachments]) if message.attachments:
fields.append( value = "\n".join([f"[{x.filename}]({x.url})" for x in message.attachments])
EmbedField( fields.append(
name="Attachments", EmbedField(
value=value, name="Attachments",
inline=False, value=value,
inline=False,
)
) )
if message.sticker_items:
value = "\n".join([f"Sticker: {x.name}" for x in message.sticker_items])
fields.append(
EmbedField(
name="Stickers",
value=value,
inline=False,
)
)
if message.embeds:
value = str(len(message.embeds)) + " embeds"
fields.append(
EmbedField(
name="Embeds",
value=value,
inline=False,
)
)
channel = message.guild.get_channel(modlog.value)
embed = build_embed(
title="Message Deleted",
description=f"{message.author.mention}'s message was deleted from {message.channel.mention}",
fields=fields,
color="#fc9e3f",
) )
if message.sticker_items: embed.set_author(
value = "\n".join([f"Sticker: {x.name}" for x in message.sticker_items]) name=message.author.username,
fields.append( icon_url=message.author.display_avatar.url,
EmbedField( url=message.jump_url,
name="Stickers", )
value=value, embed.set_footer(
inline=False, text=(
f"{message.author.username}#{message.author.discriminator} | "
f"{message.author.id}"
) )
) )
await channel.send(embed=embed)
if message.embeds: except Exception as e:
value = str(len(message.embeds)) + " embeds" self.logger.warn(
fields.append( f"Failed to process edit {message.guild.id}/{message.channel.id}/{message.id}: {e}"
EmbedField(
name="Embeds",
value=value,
inline=False,
)
) )
channel = message.guild.get_channel(modlog.value)
embed = build_embed(
title="Message Deleted",
description=f"{message.author.mention}'s message was deleted",
fields=fields,
color="#fc9e3f",
)
embed.set_author(
name=message.author.username,
icon_url=message.author.display_avatar.url,
url=message.jump_url,
)
embed.set_footer(
text=f"{message.author.user.username}#{message.author.discriminator} | {message.author.id}"
)
await channel.send(embed=embed)

View file

@ -1,16 +1,28 @@
"""J.A.R.V.I.S. Admin Cogs.""" """JARVIS Admin Cogs."""
import logging
from dis_snek import Snake from dis_snek import Snake
from jarvis.cogs.admin import ban, kick, mute, purge, roleping, warning from jarvis.cogs.admin import ban, kick, lock, lockdown, mute, purge, roleping, warning
def setup(bot: Snake) -> None: def setup(bot: Snake) -> None:
"""Add admin cogs to J.A.R.V.I.S.""" """Add admin cogs to JARVIS"""
logger = logging.getLogger(__name__)
msg = "Loaded jarvis.cogs.admin.{}"
ban.BanCog(bot) ban.BanCog(bot)
logger.debug(msg.format("ban"))
kick.KickCog(bot) kick.KickCog(bot)
# lock.LockCog(bot) logger.debug(msg.format("kick"))
# lockdown.LockdownCog(bot) lock.LockCog(bot)
logger.debug(msg.format("lock"))
lockdown.LockdownCog(bot)
logger.debug(msg.format("ban"))
mute.MuteCog(bot) mute.MuteCog(bot)
logger.debug(msg.format("mute"))
purge.PurgeCog(bot) purge.PurgeCog(bot)
logger.debug(msg.format("purge"))
roleping.RolepingCog(bot) roleping.RolepingCog(bot)
logger.debug(msg.format("roleping"))
warning.WarningCog(bot) warning.WarningCog(bot)
logger.debug(msg.format("warning"))

View file

@ -1,13 +1,15 @@
"""J.A.R.V.I.S. BanCog.""" """JARVIS BanCog."""
import logging
import re import re
from dis_snek import InteractionContext, Permissions, Scale from dis_snek import InteractionContext, Permissions, Snake
from dis_snek.client.utils.misc_utils import find, find_all from dis_snek.client.utils.misc_utils import find, find_all
from dis_snek.ext.paginators import Paginator from dis_snek.ext.paginators import Paginator
from dis_snek.models.discord.embed import EmbedField from dis_snek.models.discord.embed import EmbedField
from dis_snek.models.discord.user import User from dis_snek.models.discord.user import User
from dis_snek.models.snek.application_commands import ( from dis_snek.models.snek.application_commands import (
OptionTypes, OptionTypes,
SlashCommand,
SlashCommandChoice, SlashCommandChoice,
slash_command, slash_command,
slash_option, slash_option,
@ -17,11 +19,16 @@ from jarvis_core.db import q
from jarvis_core.db.models import Ban, Unban from jarvis_core.db.models import Ban, Unban
from jarvis.utils import build_embed from jarvis.utils import build_embed
from jarvis.utils.cogs import ModcaseCog
from jarvis.utils.permissions import admin_or_permissions from jarvis.utils.permissions import admin_or_permissions
class BanCog(Scale): class BanCog(ModcaseCog):
"""J.A.R.V.I.S. BanCog.""" """JARVIS BanCog."""
def __init__(self, bot: Snake):
super().__init__(bot)
self.logger = logging.getLogger(__name__)
async def discord_apply_ban( async def discord_apply_ban(
self, self,
@ -56,9 +63,9 @@ class BanCog(Scale):
embed.set_author( embed.set_author(
name=user.display_name, name=user.display_name,
icon_url=user.avatar, icon_url=user.avatar.url,
) )
embed.set_thumbnail(url=user.avatar) embed.set_thumbnail(url=user.avatar.url)
embed.set_footer(text=f"{user.username}#{user.discriminator} | {user.id}") embed.set_footer(text=f"{user.username}#{user.discriminator} | {user.id}")
await ctx.send(embed=embed) await ctx.send(embed=embed)
@ -83,9 +90,9 @@ class BanCog(Scale):
) )
embed.set_author( embed.set_author(
name=user.username, name=user.username,
icon_url=user.avatar, icon_url=user.avatar.url,
) )
embed.set_thumbnail(url=user.avatar) embed.set_thumbnail(url=user.avatar.url)
embed.set_footer(text=f"{user.username}#{user.discriminator} | {user.id}") embed.set_footer(text=f"{user.username}#{user.discriminator} | {user.id}")
await ctx.send(embed=embed) await ctx.send(embed=embed)
@ -105,19 +112,25 @@ class BanCog(Scale):
SlashCommandChoice(name="Soft", value="soft"), SlashCommandChoice(name="Soft", value="soft"),
], ],
) )
@slash_option(
name="duration",
description="Temp ban duration in hours",
opt_type=OptionTypes.INTEGER,
required=False,
)
@check(admin_or_permissions(Permissions.BAN_MEMBERS)) @check(admin_or_permissions(Permissions.BAN_MEMBERS))
async def _ban( async def _ban(
self, self,
ctx: InteractionContext, ctx: InteractionContext,
user: User,
reason: str, reason: str,
user: User = None,
btype: str = "perm", btype: str = "perm",
duration: int = 4, duration: int = 4,
) -> None: ) -> None:
if not user or user == ctx.author: if user.id == ctx.author.id:
await ctx.send("You cannot ban yourself.", ephemeral=True) await ctx.send("You cannot ban yourself.", ephemeral=True)
return return
if user == self.bot.user: if user.id == self.bot.user.id:
await ctx.send("I'm afraid I can't let you do that", ephemeral=True) await ctx.send("I'm afraid I can't let you do that", ephemeral=True)
return return
if btype == "temp" and duration < 0: if btype == "temp" and duration < 0:
@ -203,9 +216,10 @@ class BanCog(Scale):
discord_ban_info = None discord_ban_info = None
database_ban_info = None database_ban_info = None
bans = await ctx.guild.bans() bans = await ctx.guild.fetch_bans()
# Try to get ban information out of Discord # Try to get ban information out of Discord
self.logger.debug(f"{user}")
if re.match(r"^[0-9]{1,}$", user): # User ID if re.match(r"^[0-9]{1,}$", user): # User ID
user = int(user) user = int(user)
discord_ban_info = find(lambda x: x.user.id == user, bans) discord_ban_info = find(lambda x: x.user.id == user, bans)
@ -240,9 +254,9 @@ class BanCog(Scale):
# try to find the relevant information in the database. # try to find the relevant information in the database.
# We take advantage of the previous checks to save CPU cycles # We take advantage of the previous checks to save CPU cycles
if not discord_ban_info: if not discord_ban_info:
if isinstance(user, int): if isinstance(user, User):
database_ban_info = await Ban.find_one( database_ban_info = await Ban.find_one(
q(guild=ctx.guild.id, user=user, active=True) q(guild=ctx.guild.id, user=user.id, active=True)
) )
else: else:
search = { search = {
@ -277,9 +291,9 @@ class BanCog(Scale):
).save() ).save()
await ctx.send("Unable to find user in Discord, but removed entry from database.") await ctx.send("Unable to find user in Discord, but removed entry from database.")
@slash_command( bans = SlashCommand(name="bans", description="User bans")
name="bans", description="User bans", sub_cmd_name="list", sub_cmd_description="List bans"
) @bans.subcommand(sub_cmd_name="list", sub_cmd_description="List bans")
@slash_option( @slash_option(
name="btype", name="btype",
description="Ban type", description="Ban type",
@ -295,23 +309,23 @@ class BanCog(Scale):
@slash_option( @slash_option(
name="active", name="active",
description="Active bans", description="Active bans",
opt_type=OptionTypes.INTEGER, opt_type=OptionTypes.BOOLEAN,
required=False, required=False,
choices=[SlashCommandChoice(name="Yes", value=1), SlashCommandChoice(name="No", value=0)],
) )
@check(admin_or_permissions(Permissions.BAN_MEMBERS)) @check(admin_or_permissions(Permissions.BAN_MEMBERS))
async def _bans_list(self, ctx: InteractionContext, btype: int = 0, active: int = 1) -> None: async def _bans_list(
active = bool(active) self, ctx: InteractionContext, btype: int = 0, active: bool = True
) -> None:
types = [0, "perm", "temp", "soft"] types = [0, "perm", "temp", "soft"]
search = {"guild": ctx.guild.id} search = {"guild": ctx.guild.id}
if active: if active:
search["active"] = True search["active"] = True
if btype > 0: if btype > 0:
search["type"] = types[btype] search["type"] = types[btype]
bans = Ban.find(search).sort([("created_at", -1)]) bans = await Ban.find(search).sort([("created_at", -1)]).to_list(None)
db_bans = [] db_bans = []
fields = [] fields = []
async for ban in bans: for ban in bans:
if not ban.username: if not ban.username:
user = await self.bot.fetch_user(ban.user) user = await self.bot.fetch_user(ban.user)
ban.username = user.username if user else "[deleted user]" ban.username = user.username if user else "[deleted user]"

View file

@ -1,5 +1,7 @@
"""J.A.R.V.I.S. KickCog.""" """JARVIS KickCog."""
from dis_snek import InteractionContext, Permissions, Scale import logging
from dis_snek import InteractionContext, Permissions, Snake
from dis_snek.models.discord.embed import EmbedField from dis_snek.models.discord.embed import EmbedField
from dis_snek.models.discord.user import User from dis_snek.models.discord.user import User
from dis_snek.models.snek.application_commands import ( from dis_snek.models.snek.application_commands import (
@ -11,11 +13,16 @@ from dis_snek.models.snek.command import check
from jarvis_core.db.models import Kick from jarvis_core.db.models import Kick
from jarvis.utils import build_embed from jarvis.utils import build_embed
from jarvis.utils.cogs import ModcaseCog
from jarvis.utils.permissions import admin_or_permissions from jarvis.utils.permissions import admin_or_permissions
class KickCog(Scale): class KickCog(ModcaseCog):
"""J.A.R.V.I.S. KickCog.""" """JARVIS KickCog."""
def __init__(self, bot: Snake):
super().__init__(bot)
self.logger = logging.getLogger(__name__)
@slash_command(name="kick", description="Kick a user") @slash_command(name="kick", description="Kick a user")
@slash_option(name="user", description="User to kick", opt_type=OptionTypes.USER, required=True) @slash_option(name="user", description="User to kick", opt_type=OptionTypes.USER, required=True)
@ -52,7 +59,11 @@ class KickCog(Scale):
await user.send(embed=embed) await user.send(embed=embed)
except Exception: except Exception:
send_failed = True send_failed = True
await ctx.guild.kick(user, reason=reason) try:
await ctx.guild.kick(user, reason=reason)
except Exception as e:
await ctx.send(f"Failed to kick user:\n```\n{e}\n```", ephemeral=True)
return
fields = [EmbedField(name="DM Sent?", value=str(not send_failed))] fields = [EmbedField(name="DM Sent?", value=str(not send_failed))]
embed = build_embed( embed = build_embed(

View file

@ -1,113 +1,118 @@
"""J.A.R.V.I.S. LockCog.""" """JARVIS LockCog."""
# from dis_snek import Scale import logging
# from typing import Union
# # TODO: Uncomment 99% of code once implementation is figured out
# from contextlib import suppress from dis_snek import InteractionContext, Scale, Snake
# from typing import Union from dis_snek.client.utils.misc_utils import get
# from dis_snek.models.discord.channel import GuildText, GuildVoice
# from dis_snek import InteractionContext, Scale, Snake from dis_snek.models.discord.enums import Permissions
# from dis_snek.models.discord.enums import Permissions from dis_snek.models.snek.application_commands import (
# from dis_snek.models.discord.role import Role OptionTypes,
# from dis_snek.models.discord.user import User slash_command,
# from dis_snek.models.discord.channel import GuildText, GuildVoice, PermissionOverwrite slash_option,
# from dis_snek.models.snek.application_commands import ( )
# OptionTypes, from dis_snek.models.snek.command import check
# PermissionTypes, from jarvis_core.db import q
# slash_command, from jarvis_core.db.models import Lock, Permission
# slash_option,
# ) from jarvis.utils.permissions import admin_or_permissions
# from dis_snek.models.snek.command import check
#
# from jarvis.db.models import Lock class LockCog(Scale):
# from jarvis.utils.permissions import admin_or_permissions """JARVIS LockCog."""
#
# def __init__(self, bot: Snake):
# class LockCog(Scale): self.bot = bot
# """J.A.R.V.I.S. LockCog.""" self.logger = logging.getLogger(__name__)
#
# @slash_command(name="lock", description="Lock a channel") @slash_command(name="lock", description="Lock a channel")
# @slash_option(name="reason", @slash_option(
# description="Lock Reason", name="reason",
# opt_type=3, description="Lock Reason",
# required=True,) opt_type=3,
# @slash_option(name="duration", required=True,
# description="Lock duration in minutes (default 10)", )
# opt_type=4, @slash_option(
# required=False,) name="duration",
# @slash_option(name="channel", description="Lock duration in minutes (default 10)",
# description="Channel to lock", opt_type=4,
# opt_type=7, required=False,
# required=False,) )
# @check(admin_or_permissions(Permissions.MANAGE_CHANNELS)) @slash_option(
# async def _lock( name="channel",
# self, description="Channel to lock",
# ctx: InteractionContext, opt_type=7,
# reason: str, required=False,
# duration: int = 10, )
# channel: Union[GuildText, GuildVoice] = None, @check(admin_or_permissions(Permissions.MANAGE_CHANNELS))
# ) -> None: async def _lock(
# await ctx.defer(ephemeral=True) self,
# if duration <= 0: ctx: InteractionContext,
# await ctx.send("Duration must be > 0", ephemeral=True) reason: str,
# return duration: int = 10,
# channel: Union[GuildText, GuildVoice] = None,
# elif duration > 60 * 12: ) -> None:
# await ctx.send("Duration must be <= 12 hours", ephemeral=True) await ctx.defer(ephemeral=True)
# return if duration <= 0:
# await ctx.send("Duration must be > 0", ephemeral=True)
# if len(reason) > 100: return
# await ctx.send("Reason must be <= 100 characters", ephemeral=True)
# return elif duration > 60 * 12:
# if not channel: await ctx.send("Duration must be <= 12 hours", ephemeral=True)
# channel = ctx.channel return
#
# # role = ctx.guild.default_role # Uncomment once implemented if len(reason) > 100:
# if isinstance(channel, GuildText): await ctx.send("Reason must be <= 100 characters", ephemeral=True)
# to_deny = Permissions.SEND_MESSAGES return
# elif isinstance(channel, GuildVoice): if not channel:
# to_deny = Permissions.CONNECT | Permissions.SPEAK channel = ctx.channel
#
# overwrite = PermissionOverwrite(type=PermissionTypes.ROLE, deny=to_deny) to_deny = Permissions.CONNECT | Permissions.SPEAK | Permissions.SEND_MESSAGES
# # TODO: Get original permissions
# # TODO: Apply overwrite current = get(channel.permission_overwrites, id=ctx.guild.id)
# overwrite = overwrite if current:
# _ = Lock( current = Permission(id=ctx.guild.id, allow=int(current.allow), deny=int(current.deny))
# channel=channel.id, role = await ctx.guild.fetch_role(ctx.guild.id)
# guild=ctx.guild.id,
# admin=ctx.author.id, await channel.add_permission(target=role, deny=to_deny, reason="Locked")
# reason=reason, await Lock(
# duration=duration, channel=channel.id,
# ) # .save() # Uncomment once implemented guild=ctx.guild.id,
# # await ctx.send(f"{channel.mention} locked for {duration} minute(s)") admin=ctx.author.id,
# await ctx.send("Unfortunately, this is not yet implemented", hidden=True) reason=reason,
# duration=duration,
# @cog_ext.cog_slash( original_perms=current,
# name="unlock", ).commit()
# description="Unlocks a channel", await ctx.send(f"{channel.mention} locked for {duration} minute(s)")
# choices=[
# create_option( @slash_command(name="unlock", description="Unlock a channel")
# name="channel", @slash_option(
# description="Channel to lock", name="channel",
# opt_type=7, description="Channel to unlock",
# required=False, opt_type=OptionTypes.CHANNEL,
# ), required=False,
# ], )
# ) @check(admin_or_permissions(Permissions.MANAGE_CHANNELS))
# @check(admin_or_permissions(Permissions.MANAGE_CHANNELS)) async def _unlock(
# async def _unlock( self,
# self, ctx: InteractionContext,
# ctx: InteractionContext, channel: Union[GuildText, GuildVoice] = None,
# channel: Union[GuildText, GuildVoice] = None, ) -> None:
# ) -> None: if not channel:
# if not channel: channel = ctx.channel
# channel = ctx.channel lock = await Lock.find_one(q(guild=ctx.guild.id, channel=channel.id, active=True))
# lock = Lock.objects(guild=ctx.guild.id, channel=channel.id, active=True).first() if not lock:
# if not lock: await ctx.send(f"{channel.mention} not locked.", ephemeral=True)
# await ctx.send(f"{channel.mention} not locked.", ephemeral=True) return
# return
# for role in ctx.guild.roles: overwrite = get(channel.permission_overwrites, id=ctx.guild.id)
# with suppress(Exception): if overwrite and lock.original_perms:
# await self._unlock_channel(channel, role, ctx.author) overwrite.allow = lock.original_perms.allow
# lock.active = False overwrite.deny = lock.original_perms.deny
# lock.save() await channel.edit_permission(overwrite, reason="Unlock")
# await ctx.send(f"{channel.mention} unlocked") elif overwrite and not lock.original_perms:
await channel.delete_permission(target=overwrite, reason="Unlock")
lock.active = False
await lock.commit()
await ctx.send(f"{channel.mention} unlocked")

View file

@ -1,101 +1,168 @@
"""J.A.R.V.I.S. LockdownCog.""" """JARVIS LockdownCog."""
from contextlib import suppress import logging
from datetime import datetime
from discord.ext import commands from dis_snek import InteractionContext, Scale, Snake
from discord_slash import SlashContext, cog_ext from dis_snek.client.utils.misc_utils import find_all, get
from discord_slash.utils.manage_commands import create_option from dis_snek.models.discord.channel import GuildCategory, GuildChannel
from dis_snek.models.discord.enums import Permissions
from dis_snek.models.discord.guild import Guild
from dis_snek.models.discord.user import Member
from dis_snek.models.snek.application_commands import (
OptionTypes,
SlashCommand,
slash_option,
)
from dis_snek.models.snek.command import check
from jarvis_core.db import q
from jarvis_core.db.models import Lock, Lockdown, Permission
from jarvis.db.models import Lock from jarvis.utils.permissions import admin_or_permissions
from jarvis.utils.cachecog import CacheCog
# from jarvis.utils.permissions import admin_or_permissions
class LockdownCog(CacheCog): async def lock(bot: Snake, target: GuildChannel, admin: Member, reason: str, duration: int) -> None:
"""J.A.R.V.I.S. LockdownCog.""" """
Lock an existing channel
def __init__(self, bot: commands.Bot): Args:
super().__init__(bot) bot: Bot instance
target: Target channel
admin: Admin who initiated lockdown
"""
to_deny = Permissions.SEND_MESSAGES | Permissions.CONNECT | Permissions.SPEAK
current = get(target.permission_overwrites, id=target.guild.id)
if current:
current = Permission(id=target.guild.id, allow=int(current.allow), deny=int(current.deny))
role = await target.guild.fetch_role(target.guild.id)
await target.add_permission(target=role, deny=to_deny, reason="Lockdown")
await Lock(
channel=target.id,
guild=target.guild.id,
admin=admin.id,
reason=reason,
duration=duration,
original_perms=current,
).commit()
@cog_ext.cog_subcommand(
base="lockdown", async def lock_all(bot: Snake, guild: Guild, admin: Member, reason: str, duration: int) -> None:
name="start", """
description="Locks a server", Lock all channels
choices=[
create_option( Args:
name="reason", bot: Bot instance
description="Lockdown Reason", guild: Target guild
opt_type=3, admin: Admin who initiated lockdown
required=True, """
), role = await guild.fetch_role(guild.id)
create_option( categories = find_all(lambda x: isinstance(x, GuildCategory), guild.channels)
name="duration", for category in categories:
description="Lockdown duration in minutes (default 10)", await lock(bot, category, admin, reason, duration)
opt_type=4, perms = category.permissions_for(role)
required=False,
), for channel in category.channels:
], if perms != channel.permissions_for(role):
await lock(bot, channel, admin, reason, duration)
async def unlock_all(bot: Snake, guild: Guild, admin: Member) -> None:
"""
Unlock all locked channels
Args:
bot: Bot instance
target: Target channel
admin: Admin who ended lockdown
"""
locks = Lock.find(q(guild=guild.id, active=True))
async for lock in locks:
target = await guild.fetch_channel(lock.channel)
if target:
overwrite = get(target.permission_overwrites, id=guild.id)
if overwrite and lock.original_perms:
overwrite.allow = lock.original_perms.allow
overwrite.deny = lock.original_perms.deny
await target.edit_permission(overwrite, reason="Lockdown end")
elif overwrite and not lock.original_perms:
await target.delete_permission(target=overwrite, reason="Lockdown end")
lock.active = False
await lock.commit()
lockdown = await Lockdown.find_one(q(guild=guild.id, active=True))
if lockdown:
lockdown.active = False
await lockdown.commit()
class LockdownCog(Scale):
"""JARVIS LockdownCog."""
def __init__(self, bot: Snake):
self.bot = bot
self.logger = logging.getLogger(__name__)
lockdown = SlashCommand(
name="lockdown",
description="Manage server-wide lockdown",
) )
# @check(admin_or_permissions(manage_channels=True))
@lockdown.subcommand(
sub_cmd_name="start",
sub_cmd_description="Lockdown the server",
)
@slash_option(
name="reason", description="Lockdown reason", opt_type=OptionTypes.STRING, required=True
)
@slash_option(
name="duration",
description="Duration in minutes",
opt_type=OptionTypes.INTEGER,
required=False,
)
@check(admin_or_permissions(Permissions.MANAGE_CHANNELS))
async def _lockdown_start( async def _lockdown_start(
self, self,
ctx: SlashContext, ctx: InteractionContext,
reason: str, reason: str,
duration: int = 10, duration: int = 10,
) -> None: ) -> None:
await ctx.defer(ephemeral=True) await ctx.defer()
if duration <= 0: if duration <= 0:
await ctx.send("Duration must be > 0", ephemeral=True) await ctx.send("Duration must be > 0", ephemeral=True)
return return
elif duration >= 300: elif duration >= 300:
await ctx.send("Duration must be < 5 hours", ephemeral=True) await ctx.send("Duration must be < 5 hours", ephemeral=True)
return return
channels = ctx.guild.channels
roles = ctx.guild.roles
updates = []
for channel in channels:
for role in roles:
with suppress(Exception):
await self._lock_channel(channel, role, ctx.author, reason)
updates.append(
Lock(
channel=channel.id,
guild=ctx.guild.id,
admin=ctx.author.id,
reason=reason,
duration=duration,
active=True,
created_at=datetime.utcnow(),
)
)
if updates:
Lock.objects().insert(updates)
await ctx.send(f"Server locked for {duration} minute(s)")
@cog_ext.cog_subcommand( exists = await Lockdown.find_one(q(guild=ctx.guild.id, active=True))
base="lockdown", if exists:
name="end", await ctx.send("Server already in lockdown", ephemeral=True)
description="Unlocks a server", return
)
@commands.has_permissions(administrator=True) await lock_all(self.bot, ctx.guild, ctx.author, reason, duration)
role = await ctx.guild.fetch_role(ctx.guild.id)
original_perms = role.permissions
new_perms = role.permissions & ~Permissions.SEND_MESSAGES
await role.edit(permissions=new_perms)
await Lockdown(
admin=ctx.author.id,
duration=duration,
guild=ctx.guild.id,
reason=reason,
original_perms=int(original_perms),
).commit()
await ctx.send("Server now in lockdown.")
@lockdown.subcommand(sub_cmd_name="end", sub_cmd_description="End a lockdown")
@check(admin_or_permissions(Permissions.MANAGE_CHANNELS))
async def _lockdown_end( async def _lockdown_end(
self, self,
ctx: SlashContext, ctx: InteractionContext,
) -> None: ) -> None:
channels = ctx.guild.channels
roles = ctx.guild.roles
update = False
locks = Lock.objects(guild=ctx.guild.id, active=True)
if not locks:
await ctx.send("No lockdown detected.", ephemeral=True)
return
await ctx.defer() await ctx.defer()
for channel in channels:
for role in roles: lockdown = await Lockdown.find_one(q(guild=ctx.guild.id, active=True))
with suppress(Exception): if not lockdown:
await self._unlock_channel(channel, role, ctx.author) await ctx.send("Server not in lockdown", ephemeral=True)
update = True return
if update:
Lock.objects(guild=ctx.guild.id, active=True).update(set__active=False) await unlock_all(self.bot, ctx.guild, ctx.author)
await ctx.send("Server unlocked") await ctx.send("Server no longer in lockdown.")

View file

@ -1,12 +1,19 @@
"""J.A.R.V.I.S. MuteCog.""" """JARVIS MuteCog."""
from datetime import datetime import asyncio
import logging
from datetime import datetime, timedelta, timezone
from dis_snek import InteractionContext, Permissions, Scale, Snake from dateparser import parse
from dateparser_data.settings import default_parsers
from dis_snek import InteractionContext, Permissions, Snake
from dis_snek.models.discord.embed import EmbedField from dis_snek.models.discord.embed import EmbedField
from dis_snek.models.discord.modal import InputText, Modal, TextStyles
from dis_snek.models.discord.user import Member from dis_snek.models.discord.user import Member
from dis_snek.models.snek.application_commands import ( from dis_snek.models.snek.application_commands import (
CommandTypes,
OptionTypes, OptionTypes,
SlashCommandChoice, SlashCommandChoice,
context_menu,
slash_command, slash_command,
slash_option, slash_option,
) )
@ -14,14 +21,112 @@ from dis_snek.models.snek.command import check
from jarvis_core.db.models import Mute from jarvis_core.db.models import Mute
from jarvis.utils import build_embed from jarvis.utils import build_embed
from jarvis.utils.cogs import ModcaseCog
from jarvis.utils.permissions import admin_or_permissions from jarvis.utils.permissions import admin_or_permissions
class MuteCog(Scale): class MuteCog(ModcaseCog):
"""J.A.R.V.I.S. MuteCog.""" """JARVIS MuteCog."""
def __init__(self, bot: Snake): def __init__(self, bot: Snake):
self.bot = bot super().__init__(bot)
self.logger = logging.getLogger(__name__)
async def _apply_timeout(
self, ctx: InteractionContext, user: Member, reason: str, until: datetime
) -> None:
await user.timeout(communication_disabled_until=until, reason=reason)
duration = int((until - datetime.now(tz=timezone.utc)).seconds / 60)
await Mute(
user=user.id,
reason=reason,
admin=ctx.author.id,
guild=ctx.guild.id,
duration=duration,
active=True,
).commit()
ts = int(until.timestamp())
embed = build_embed(
title="User Muted",
description=f"{user.mention} has been muted",
fields=[
EmbedField(name="Reason", value=reason),
EmbedField(name="Until", value=f"<t:{ts}:F> <t:{ts}:R>"),
],
)
embed.set_author(name=user.display_name, icon_url=user.display_avatar.url)
embed.set_thumbnail(url=user.display_avatar.url)
embed.set_footer(text=f"{user.username}#{user.discriminator} | {user.id}")
return embed
@context_menu(name="Mute User", context_type=CommandTypes.USER)
@check(
admin_or_permissions(
Permissions.MUTE_MEMBERS, Permissions.BAN_MEMBERS, Permissions.KICK_MEMBERS
)
)
async def _timeout_cm(self, ctx: InteractionContext) -> None:
modal = Modal(
title=f"Muting {ctx.target.mention}",
components=[
InputText(
label="Reason?",
placeholder="Spamming, harrassment, etc",
style=TextStyles.SHORT,
custom_id="reason",
max_length=100,
),
InputText(
label="Duration",
placeholder="1h 30m | in 5 minutes | in 4 weeks",
style=TextStyles.SHORT,
custom_id="until",
max_length=100,
),
],
)
await ctx.send_modal(modal)
try:
response = await self.bot.wait_for_modal(modal, author=ctx.author.id, timeout=60 * 5)
reason = response.responses.get("reason")
until = response.responses.get("until")
except asyncio.TimeoutError:
return
base_settings = {
"PREFER_DATES_FROM": "future",
"TIMEZONE": "UTC",
"RETURN_AS_TIMEZONE_AWARE": True,
}
rt_settings = base_settings.copy()
rt_settings["PARSERS"] = [
x for x in default_parsers if x not in ["absolute-time", "timestamp"]
]
rt_until = parse(until, settings=rt_settings)
at_settings = base_settings.copy()
at_settings["PARSERS"] = [x for x in default_parsers if x != "relative-time"]
at_until = parse(until, settings=at_settings)
old_until = until
if rt_until:
until = rt_until
elif at_until:
until = at_until
else:
self.logger.debug(f"Failed to parse delay: {until}")
await response.send(
f"`{until}` is not a parsable date, please try again", ephemeral=True
)
return
if until < datetime.now(tz=timezone.utc):
await response.send(
f"`{old_until}` is in the past, which isn't allowed", ephemeral=True
)
return
embed = await self._apply_timeout(ctx, ctx.target, reason, until)
await response.send(embed=embed)
@slash_command(name="mute", description="Mute a user") @slash_command(name="mute", description="Mute a user")
@slash_option(name="user", description="User to mute", opt_type=OptionTypes.USER, required=True) @slash_option(name="user", description="User to mute", opt_type=OptionTypes.USER, required=True)
@ -73,25 +178,8 @@ class MuteCog(Scale):
await ctx.send("Mute must be less than 4 weeks (2419200 seconds)", ephemeral=True) await ctx.send("Mute must be less than 4 weeks (2419200 seconds)", ephemeral=True)
return return
await user.timeout(communication_disabled_until=duration, reason=reason) until = datetime.now(tz=timezone.utc) + timedelta(minutes=duration)
m = Mute( embed = await self._apply_timeout(ctx, user, reason, until)
user=user.id,
reason=reason,
admin=ctx.author.id,
guild=ctx.guild.id,
duration=duration,
active=True,
)
await m.commit()
embed = build_embed(
title="User Muted",
description=f"{user.mention} has been muted",
fields=[EmbedField(name="Reason", value=reason)],
)
embed.set_author(name=user.display_name, icon_url=user.display_avatar.url)
embed.set_thumbnail(url=user.display_avatar.url)
embed.set_footer(text=f"{user.username}#{user.discriminator} | {user.id}")
await ctx.send(embed=embed) await ctx.send(embed=embed)
@slash_command(name="unmute", description="Unmute a user") @slash_command(name="unmute", description="Unmute a user")
@ -106,11 +194,14 @@ class MuteCog(Scale):
async def _unmute(self, ctx: InteractionContext, user: Member) -> None: async def _unmute(self, ctx: InteractionContext, user: Member) -> None:
if ( if (
not user.communication_disabled_until not user.communication_disabled_until
or user.communication_disabled_until < datetime.now() # noqa: W503 or user.communication_disabled_until.timestamp()
< datetime.now(tz=timezone.utc).timestamp() # noqa: W503
): ):
await ctx.send("User is not muted", ephemeral=True) await ctx.send("User is not muted", ephemeral=True)
return return
await user.timeout(communication_disabled_until=datetime.now(tz=timezone.utc))
embed = build_embed( embed = build_embed(
title="User Unmuted", title="User Unmuted",
description=f"{user.mention} has been unmuted", description=f"{user.mention} has been unmuted",

View file

@ -1,4 +1,6 @@
"""J.A.R.V.I.S. PurgeCog.""" """JARVIS PurgeCog."""
import logging
from dis_snek import InteractionContext, Permissions, Scale, Snake from dis_snek import InteractionContext, Permissions, Scale, Snake
from dis_snek.models.discord.channel import GuildText from dis_snek.models.discord.channel import GuildText
from dis_snek.models.snek.application_commands import ( from dis_snek.models.snek.application_commands import (
@ -14,10 +16,11 @@ from jarvis.utils.permissions import admin_or_permissions
class PurgeCog(Scale): class PurgeCog(Scale):
"""J.A.R.V.I.S. PurgeCog.""" """JARVIS PurgeCog."""
def __init__(self, bot: Snake): def __init__(self, bot: Snake):
self.bot = bot self.bot = bot
self.logger = logging.getLogger(__name__)
@slash_command(name="purge", description="Purge messages from channel") @slash_command(name="purge", description="Purge messages from channel")
@slash_option( @slash_option(

View file

@ -1,5 +1,7 @@
"""J.A.R.V.I.S. RolepingCog.""" """JARVIS RolepingCog."""
from dis_snek import InteractionContext, Permissions, Scale import logging
from dis_snek import InteractionContext, Permissions, Scale, Snake
from dis_snek.client.utils.misc_utils import find_all from dis_snek.client.utils.misc_utils import find_all
from dis_snek.ext.paginators import Paginator from dis_snek.ext.paginators import Paginator
from dis_snek.models.discord.embed import EmbedField from dis_snek.models.discord.embed import EmbedField
@ -7,7 +9,7 @@ from dis_snek.models.discord.role import Role
from dis_snek.models.discord.user import Member from dis_snek.models.discord.user import Member
from dis_snek.models.snek.application_commands import ( from dis_snek.models.snek.application_commands import (
OptionTypes, OptionTypes,
slash_command, SlashCommand,
slash_option, slash_option,
) )
from dis_snek.models.snek.command import check from dis_snek.models.snek.command import check
@ -19,10 +21,19 @@ from jarvis.utils.permissions import admin_or_permissions
class RolepingCog(Scale): class RolepingCog(Scale):
"""J.A.R.V.I.S. RolepingCog.""" """JARVIS RolepingCog."""
@slash_command( def __init__(self, bot: Snake):
name="roleping", sub_cmd_name="add", sub_cmd_description="Add a role to roleping" self.bot = bot
self.logger = logging.getLogger(__name__)
roleping = SlashCommand(
name="roleping", description="Set up warnings for pinging specific roles"
)
@roleping.subcommand(
sub_cmd_name="add",
sub_cmd_description="Add a role to roleping",
) )
@slash_option(name="role", description="Role to add", opt_type=OptionTypes.ROLE, required=True) @slash_option(name="role", description="Role to add", opt_type=OptionTypes.ROLE, required=True)
@check(admin_or_permissions(Permissions.MANAGE_GUILD)) @check(admin_or_permissions(Permissions.MANAGE_GUILD))
@ -32,6 +43,10 @@ class RolepingCog(Scale):
await ctx.send(f"Role `{role.name}` already in roleping.", ephemeral=True) await ctx.send(f"Role `{role.name}` already in roleping.", ephemeral=True)
return return
if role.id == ctx.guild.id:
await ctx.send("Cannot add `@everyone` to roleping", ephemeral=True)
return
_ = await Roleping( _ = await Roleping(
role=role.id, role=role.id,
guild=ctx.guild.id, guild=ctx.guild.id,
@ -41,7 +56,7 @@ class RolepingCog(Scale):
).commit() ).commit()
await ctx.send(f"Role `{role.name}` added to roleping.") await ctx.send(f"Role `{role.name}` added to roleping.")
@slash_command(name="roleping", sub_cmd_name="remove", sub_cmd_description="Remove a role") @roleping.subcommand(sub_cmd_name="remove", sub_cmd_description="Remove a role")
@slash_option( @slash_option(
name="role", description="Role to remove", opt_type=OptionTypes.ROLE, required=True name="role", description="Role to remove", opt_type=OptionTypes.ROLE, required=True
) )
@ -52,10 +67,13 @@ class RolepingCog(Scale):
await ctx.send("Roleping does not exist", ephemeral=True) await ctx.send("Roleping does not exist", ephemeral=True)
return return
await roleping.delete() try:
await roleping.delete()
except Exception:
self.logger.debug("Ignoring deletion error")
await ctx.send(f"Role `{role.name}` removed from roleping.") await ctx.send(f"Role `{role.name}` removed from roleping.")
@slash_command(name="roleping", sub_cmd_name="list", description="Lick all blocklisted roles") @roleping.subcommand(sub_cmd_name="list", sub_cmd_description="Lick all blocklisted roles")
async def _roleping_list(self, ctx: InteractionContext) -> None: async def _roleping_list(self, ctx: InteractionContext) -> None:
rolepings = await Roleping.find(q(guild=ctx.guild.id)).to_list(None) rolepings = await Roleping.find(q(guild=ctx.guild.id)).to_list(None)
@ -66,6 +84,9 @@ class RolepingCog(Scale):
embeds = [] embeds = []
for roleping in rolepings: for roleping in rolepings:
role = await ctx.guild.fetch_role(roleping.role) role = await ctx.guild.fetch_role(roleping.role)
if not role:
await roleping.delete()
continue
broles = find_all(lambda x: x.id in roleping.bypass["roles"], ctx.guild.roles) broles = find_all(lambda x: x.id in roleping.bypass["roles"], ctx.guild.roles)
bypass_roles = [r.mention or "||`[redacted]`||" for r in broles] bypass_roles = [r.mention or "||`[redacted]`||" for r in broles]
bypass_users = [ bypass_users = [
@ -111,11 +132,11 @@ class RolepingCog(Scale):
await paginator.send(ctx) await paginator.send(ctx)
@slash_command( bypass = roleping.group(
name="roleping", name="bypass", description="Allow specific users/roles to ping rolepings"
description="Block roles from being pinged", )
group_name="bypass",
group_description="Allow specific users/roles to ping rolepings", @bypass.subcommand(
sub_cmd_name="user", sub_cmd_name="user",
sub_cmd_description="Add a user as a bypass to a roleping", sub_cmd_description="Add a user as a bypass to a roleping",
) )
@ -158,11 +179,9 @@ class RolepingCog(Scale):
await roleping.commit() await roleping.commit()
await ctx.send(f"{bypass.display_name} user bypass added for `{role.name}`") await ctx.send(f"{bypass.display_name} user bypass added for `{role.name}`")
@slash_command( @bypass.subcommand(
name="roleping",
group_name="bypass",
sub_cmd_name="role", sub_cmd_name="role",
description="Add a role as a bypass to roleping", sub_cmd_description="Add a role as a bypass to roleping",
) )
@slash_option( @slash_option(
name="bypass", description="Role to add", opt_type=OptionTypes.ROLE, required=True name="bypass", description="Role to add", opt_type=OptionTypes.ROLE, required=True
@ -174,6 +193,9 @@ class RolepingCog(Scale):
async def _roleping_bypass_role( async def _roleping_bypass_role(
self, ctx: InteractionContext, bypass: Role, role: Role self, ctx: InteractionContext, bypass: Role, role: Role
) -> None: ) -> None:
if bypass.id == ctx.guild.id:
await ctx.send("Cannot add `@everyone` as a bypass", ephemeral=True)
return
roleping = await Roleping.find_one(q(guild=ctx.guild.id, role=role.id)) roleping = await Roleping.find_one(q(guild=ctx.guild.id, role=role.id))
if not roleping: if not roleping:
await ctx.send(f"Roleping not configured for {role.mention}", ephemeral=True) await ctx.send(f"Roleping not configured for {role.mention}", ephemeral=True)
@ -195,11 +217,9 @@ class RolepingCog(Scale):
await roleping.commit() await roleping.commit()
await ctx.send(f"{bypass.name} role bypass added for `{role.name}`") await ctx.send(f"{bypass.name} role bypass added for `{role.name}`")
@slash_command( restore = roleping.group(name="restore", description="Remove a roleping bypass")
name="roleping",
description="Block roles from being pinged", @restore.subcommand(
group_name="restore",
group_description="Remove a roleping bypass",
sub_cmd_name="user", sub_cmd_name="user",
sub_cmd_description="Remove a bypass from a roleping (restoring it)", sub_cmd_description="Remove a bypass from a roleping (restoring it)",
) )
@ -226,11 +246,9 @@ class RolepingCog(Scale):
await roleping.commit() await roleping.commit()
await ctx.send(f"{bypass.display_name} user bypass removed for `{role.name}`") await ctx.send(f"{bypass.display_name} user bypass removed for `{role.name}`")
@slash_command( @restore.subcommand(
name="roleping",
group_name="restore",
sub_cmd_name="role", sub_cmd_name="role",
description="Remove a bypass from a roleping (restoring it)", sub_cmd_description="Remove a bypass from a roleping (restoring it)",
) )
@slash_option( @slash_option(
name="bypass", description="Role to remove", opt_type=OptionTypes.ROLE, required=True name="bypass", description="Role to remove", opt_type=OptionTypes.ROLE, required=True

View file

@ -1,25 +1,32 @@
"""J.A.R.V.I.S. WarningCog.""" """JARVIS WarningCog."""
from dis_snek import InteractionContext, Permissions, Scale import logging
from dis_snek import InteractionContext, Permissions, Snake
from dis_snek.client.utils.misc_utils import get_all from dis_snek.client.utils.misc_utils import get_all
from dis_snek.ext.paginators import Paginator from dis_snek.ext.paginators import Paginator
from dis_snek.models.discord.embed import EmbedField from dis_snek.models.discord.embed import EmbedField
from dis_snek.models.discord.user import User from dis_snek.models.discord.user import Member
from dis_snek.models.snek.application_commands import ( from dis_snek.models.snek.application_commands import (
OptionTypes, OptionTypes,
SlashCommandChoice,
slash_command, slash_command,
slash_option, slash_option,
) )
from dis_snek.models.snek.command import check from dis_snek.models.snek.command import check
from jarvis_core.db import q
from jarvis_core.db.models import Warning from jarvis_core.db.models import Warning
from jarvis.utils import build_embed from jarvis.utils import build_embed
from jarvis.utils.cogs import ModcaseCog
from jarvis.utils.embeds import warning_embed from jarvis.utils.embeds import warning_embed
from jarvis.utils.permissions import admin_or_permissions from jarvis.utils.permissions import admin_or_permissions
class WarningCog(Scale): class WarningCog(ModcaseCog):
"""J.A.R.V.I.S. WarningCog.""" """JARVIS WarningCog."""
def __init__(self, bot: Snake):
super().__init__(bot)
self.logger = logging.getLogger(__name__)
@slash_command(name="warn", description="Warn a user") @slash_command(name="warn", description="Warn a user")
@slash_option(name="user", description="User to warn", opt_type=OptionTypes.USER, required=True) @slash_option(name="user", description="User to warn", opt_type=OptionTypes.USER, required=True)
@ -37,7 +44,7 @@ class WarningCog(Scale):
) )
@check(admin_or_permissions(Permissions.MANAGE_GUILD)) @check(admin_or_permissions(Permissions.MANAGE_GUILD))
async def _warn( async def _warn(
self, ctx: InteractionContext, user: User, reason: str, duration: int = 24 self, ctx: InteractionContext, user: Member, reason: str, duration: int = 24
) -> None: ) -> None:
if len(reason) > 100: if len(reason) > 100:
await ctx.send("Reason must be < 100 characters", ephemeral=True) await ctx.send("Reason must be < 100 characters", ephemeral=True)
@ -48,6 +55,9 @@ class WarningCog(Scale):
elif duration >= 120: elif duration >= 120:
await ctx.send("Duration must be < 5 days", ephemeral=True) await ctx.send("Duration must be < 5 days", ephemeral=True)
return return
if not await ctx.guild.fetch_member(user.id):
await ctx.send("User not in guild", ephemeral=True)
return
await ctx.defer() await ctx.defer()
await Warning( await Warning(
user=user.id, user=user.id,
@ -65,25 +75,24 @@ class WarningCog(Scale):
@slash_option( @slash_option(
name="active", name="active",
description="View active only", description="View active only",
opt_type=OptionTypes.INTEGER, opt_type=OptionTypes.BOOLEAN,
required=False, required=False,
choices=[
SlashCommandChoice(name="Yes", value=1),
SlashCommandChoice(name="No", value=0),
],
) )
@check(admin_or_permissions(Permissions.MANAGE_GUILD)) @check(admin_or_permissions(Permissions.MANAGE_GUILD))
async def _warnings(self, ctx: InteractionContext, user: User, active: bool = 1) -> None: async def _warnings(self, ctx: InteractionContext, user: Member, active: bool = True) -> None:
active = bool(active)
warnings = ( warnings = (
await Warning.find( await Warning.find(
user=user.id, q(
guild=ctx.guild.id, user=user.id,
guild=ctx.guild.id,
)
) )
.sort("created_at", -1) .sort("created_at", -1)
.to_list(None) .to_list(None)
) )
if len(warnings) == 0:
await ctx.send("That user has no warnings.", ephemeral=True)
return
active_warns = get_all(warnings, active=True) active_warns = get_all(warnings, active=True)
pages = [] pages = []
@ -91,7 +100,7 @@ class WarningCog(Scale):
if len(active_warns) == 0: if len(active_warns) == 0:
embed = build_embed( embed = build_embed(
title="Warnings", title="Warnings",
description=f"{warnings.count()} total | 0 currently active", description=f"{len(warnings)} total | 0 currently active",
fields=[], fields=[],
) )
embed.set_author(name=user.username, icon_url=user.display_avatar.url) embed.set_author(name=user.username, icon_url=user.display_avatar.url)
@ -100,13 +109,14 @@ class WarningCog(Scale):
else: else:
fields = [] fields = []
for warn in active_warns: for warn in active_warns:
admin = await ctx.guild.get_member(warn.admin) admin = await ctx.guild.fetch_member(warn.admin)
ts = int(warn.created_at.timestamp())
admin_name = "||`[redacted]`||" admin_name = "||`[redacted]`||"
if admin: if admin:
admin_name = f"{admin.username}#{admin.discriminator}" admin_name = f"{admin.username}#{admin.discriminator}"
fields.append( fields.append(
EmbedField( EmbedField(
name=warn.created_at.strftime("%Y-%m-%d %H:%M:%S UTC"), name=f"<t:{ts}:F>",
value=f"{warn.reason}\nAdmin: {admin_name}\n\u200b", value=f"{warn.reason}\nAdmin: {admin_name}\n\u200b",
inline=False, inline=False,
) )
@ -129,8 +139,9 @@ class WarningCog(Scale):
else: else:
fields = [] fields = []
for warn in warnings: for warn in warnings:
ts = int(warn.created_at.timestamp())
title = "[A] " if warn.active else "[I] " title = "[A] " if warn.active else "[I] "
title += warn.created_at.strftime("%Y-%m-%d %H:%M:%S UTC") title += f"<t:{ts}:F>"
fields.append( fields.append(
EmbedField( EmbedField(
name=title, name=title,
@ -150,6 +161,6 @@ class WarningCog(Scale):
embed.set_thumbnail(url=ctx.guild.icon.url) embed.set_thumbnail(url=ctx.guild.icon.url)
pages.append(embed) pages.append(embed)
paginator = Paginator(bot=self.bot, *pages, timeout=300) paginator = Paginator.create_from_embeds(self.bot, *pages, timeout=300)
await paginator.send(ctx) await paginator.send(ctx)

View file

@ -1,4 +1,5 @@
"""J.A.R.V.I.S. Autoreact Cog.""" """JARVIS Autoreact Cog."""
import logging
import re import re
from typing import Optional, Tuple from typing import Optional, Tuple
@ -7,7 +8,7 @@ from dis_snek.client.utils.misc_utils import find
from dis_snek.models.discord.channel import GuildText from dis_snek.models.discord.channel import GuildText
from dis_snek.models.snek.application_commands import ( from dis_snek.models.snek.application_commands import (
OptionTypes, OptionTypes,
slash_command, SlashCommand,
slash_option, slash_option,
) )
from dis_snek.models.snek.command import check from dis_snek.models.snek.command import check
@ -19,14 +20,15 @@ from jarvis.utils.permissions import admin_or_permissions
class AutoReactCog(Scale): class AutoReactCog(Scale):
"""J.A.R.V.I.S. Autoreact Cog.""" """JARVIS Autoreact Cog."""
def __init__(self, bot: Snake): def __init__(self, bot: Snake):
self.bot = bot self.bot = bot
self.logger = logging.getLogger(__name__)
self.custom_emote = re.compile(r"^<:\w+:(\d+)>$") self.custom_emote = re.compile(r"^<:\w+:(\d+)>$")
async def create_autoreact( async def create_autoreact(
self, ctx: InteractionContext, channel: GuildText self, ctx: InteractionContext, channel: GuildText, thread: bool
) -> Tuple[bool, Optional[str]]: ) -> Tuple[bool, Optional[str]]:
""" """
Create an autoreact monitor on a channel. Create an autoreact monitor on a channel.
@ -34,6 +36,7 @@ class AutoReactCog(Scale):
Args: Args:
ctx: Interaction context of command ctx: Interaction context of command
channel: Channel to monitor channel: Channel to monitor
thread: Create a thread
Returns: Returns:
Tuple of success? and error message Tuple of success? and error message
@ -42,12 +45,13 @@ class AutoReactCog(Scale):
if exists: if exists:
return False, f"Autoreact already exists for {channel.mention}." return False, f"Autoreact already exists for {channel.mention}."
_ = Autoreact( await Autoreact(
guild=ctx.guild.id, guild=ctx.guild.id,
channel=channel.id, channel=channel.id,
reactions=[], reactions=[],
thread=thread,
admin=ctx.author.id, admin=ctx.author.id,
).save() ).commit()
return True, None return True, None
@ -62,10 +66,15 @@ class AutoReactCog(Scale):
Returns: Returns:
Success? Success?
""" """
return Autoreact.objects(guild=ctx.guild.id, channel=channel.id).delete() is not None ar = await Autoreact.find_one(q(guild=ctx.guild.id, channel=channel.id))
if ar:
await ar.delete()
return True
return False
@slash_command( autoreact = SlashCommand(name="autoreact", description="Channel message autoreacts")
name="autoreact",
@autoreact.subcommand(
sub_cmd_name="add", sub_cmd_name="add",
sub_cmd_description="Add an autoreact emote to a channel", sub_cmd_description="Add an autoreact emote to a channel",
) )
@ -76,46 +85,57 @@ class AutoReactCog(Scale):
required=True, required=True,
) )
@slash_option( @slash_option(
name="emote", description="Emote to add", opt_type=OptionTypes.STRING, required=True name="thread", description="Create a thread?", opt_type=OptionTypes.BOOLEAN, required=False
)
@slash_option(
name="emote", description="Emote to add", opt_type=OptionTypes.STRING, required=False
) )
@check(admin_or_permissions(Permissions.MANAGE_GUILD)) @check(admin_or_permissions(Permissions.MANAGE_GUILD))
async def _autoreact_add(self, ctx: InteractionContext, channel: GuildText, emote: str) -> None: async def _autoreact_add(
self, ctx: InteractionContext, channel: GuildText, thread: bool = True, emote: str = None
) -> None:
await ctx.defer() await ctx.defer()
custom_emoji = self.custom_emote.match(emote) if emote:
standard_emoji = emote in emoji_list custom_emoji = self.custom_emote.match(emote)
if not custom_emoji and not standard_emoji: standard_emoji = emote in emoji_list
await ctx.send( if not custom_emoji and not standard_emoji:
"Please use either an emote from this server or a unicode emoji.", await ctx.send(
ephemeral=True, "Please use either an emote from this server or a unicode emoji.",
) ephemeral=True,
return )
if custom_emoji:
emoji_id = int(custom_emoji.group(1))
if not find(lambda x: x.id == emoji_id, ctx.guild.emojis):
await ctx.send("Please use a custom emote from this server.", ephemeral=True)
return return
if custom_emoji:
emoji_id = int(custom_emoji.group(1))
if not find(lambda x: x.id == emoji_id, await ctx.guild.fetch_all_custom_emojis()):
await ctx.send("Please use a custom emote from this server.", ephemeral=True)
return
autoreact = await Autoreact.find_one(q(guild=ctx.guild.id, channel=channel.id)) autoreact = await Autoreact.find_one(q(guild=ctx.guild.id, channel=channel.id))
if not autoreact: if not autoreact:
self.create_autoreact(ctx, channel) await self.create_autoreact(ctx, channel, thread)
autoreact = await Autoreact.find_one(q(guild=ctx.guild.id, channel=channel.id)) autoreact = await Autoreact.find_one(q(guild=ctx.guild.id, channel=channel.id))
if emote in autoreact.reactions: if emote and emote in autoreact.reactions:
await ctx.send( await ctx.send(
f"Emote already added to {channel.mention} autoreactions.", f"Emote already added to {channel.mention} autoreactions.",
ephemeral=True, ephemeral=True,
) )
return return
if len(autoreact.reactions) >= 5: if emote and len(autoreact.reactions) >= 5:
await ctx.send( await ctx.send(
"Max number of reactions hit. Remove a different one to add this one", "Max number of reactions hit. Remove a different one to add this one",
ephemeral=True, ephemeral=True,
) )
return return
autoreact.reactions.append(emote) if emote:
autoreact.save() autoreact.reactions.append(emote)
await ctx.send(f"Added {emote} to {channel.mention} autoreact.") autoreact.thread = thread
await autoreact.commit()
message = ""
if emote:
message += f" Added {emote} to {channel.mention} autoreact."
message += f" Set autoreact thread creation to {thread} in {channel.mention}"
await ctx.send(message)
@slash_command( @autoreact.subcommand(
name="autoreact",
sub_cmd_name="remove", sub_cmd_name="remove",
sub_cmd_description="Remove an autoreact emote to a channel", sub_cmd_description="Remove an autoreact emote to a channel",
) )
@ -143,7 +163,7 @@ class AutoReactCog(Scale):
) )
return return
if emote.lower() == "all": if emote.lower() == "all":
self.delete_autoreact(ctx, channel) await self.delete_autoreact(ctx, channel)
await ctx.send(f"Autoreact removed from {channel.mention}") await ctx.send(f"Autoreact removed from {channel.mention}")
elif emote not in autoreact.reactions: elif emote not in autoreact.reactions:
await ctx.send( await ctx.send(
@ -153,13 +173,12 @@ class AutoReactCog(Scale):
return return
else: else:
autoreact.reactions.remove(emote) autoreact.reactions.remove(emote)
autoreact.save() await autoreact.commit()
if len(autoreact.reactions) == 0: if len(autoreact.reactions) == 0 and not autoreact.thread:
self.delete_autoreact(ctx, channel) await self.delete_autoreact(ctx, channel)
await ctx.send(f"Removed {emote} from {channel.mention} autoreact.") await ctx.send(f"Removed {emote} from {channel.mention} autoreact.")
@slash_command( @autoreact.subcommand(
name="autoreact",
sub_cmd_name="list", sub_cmd_name="list",
sub_cmd_description="List all autoreacts on a channel", sub_cmd_description="List all autoreacts on a channel",
) )
@ -188,5 +207,5 @@ class AutoReactCog(Scale):
def setup(bot: Snake) -> None: def setup(bot: Snake) -> None:
"""Add AutoReactCog to J.A.R.V.I.S.""" """Add AutoReactCog to JARVIS"""
AutoReactCog(bot) AutoReactCog(bot)

54
jarvis/cogs/botutil.py Normal file
View file

@ -0,0 +1,54 @@
"""JARVIS bot utility commands."""
import logging
from io import BytesIO
from aiofile import AIOFile, LineReader
from dis_snek import MessageContext, Scale, Snake
from dis_snek.models.discord.file import File
from molter import msg_command
class BotutilCog(Scale):
"""JARVIS Bot Utility Cog."""
def __init__(self, bot: Snake):
self.bot = bot
self.logger = logging.getLogger(__name__)
@msg_command(name="tail")
async def _tail(self, ctx: MessageContext, count: int = 10) -> None:
if ctx.author.id != self.bot.owner.id:
return
lines = []
async with AIOFile("jarvis.log", "r") as af:
async for line in LineReader(af):
lines.append(line)
if len(lines) == count + 1:
lines.pop(0)
log = "".join(lines)
if len(log) > 1500:
with BytesIO() as file_bytes:
file_bytes.write(log.encode("UTF8"))
file_bytes.seek(0)
log = File(file_bytes, file_name=f"tail_{count}.log")
await ctx.reply(content=f"Here's the last {count} lines of the log", file=log)
else:
await ctx.reply(content=f"```\n{log}\n```")
@msg_command(name="log")
async def _log(self, ctx: MessageContext) -> None:
if ctx.author.id != self.bot.owner.id:
return
async with AIOFile("jarvis.log", "r") as af:
with BytesIO() as file_bytes:
raw = await af.read_bytes()
file_bytes.write(raw)
file_bytes.seek(0)
log = File(file_bytes, file_name="jarvis.log")
await ctx.reply(content="Here's the latest log", file=log)
def setup(bot: Snake) -> None:
"""Add BotutilCog to JARVIS"""
BotutilCog(bot)

View file

@ -1,22 +1,26 @@
"""J.A.R.V.I.S. Complete the Code 2 Cog.""" """JARVIS Complete the Code 2 Cog."""
import logging
import re import re
from datetime import datetime, timedelta
import aiohttp import aiohttp
from dis_snek import InteractionContext, Snake from dis_snek import InteractionContext, Scale, Snake
from dis_snek.ext.paginators import Paginator from dis_snek.ext.paginators import Paginator
from dis_snek.models.discord.components import ActionRow, Button, ButtonStyles
from dis_snek.models.discord.embed import EmbedField from dis_snek.models.discord.embed import EmbedField
from dis_snek.models.discord.user import Member, User from dis_snek.models.discord.user import Member, User
from dis_snek.models.snek.application_commands import slash_command from dis_snek.models.snek.application_commands import (
OptionTypes,
SlashCommand,
slash_option,
)
from dis_snek.models.snek.command import cooldown from dis_snek.models.snek.command import cooldown
from dis_snek.models.snek.cooldowns import Buckets from dis_snek.models.snek.cooldowns import Buckets
from jarvis_core.db import q from jarvis_core.db import q
from jarvis_core.db.models import Guess from jarvis_core.db.models import Guess
from jarvis.utils import build_embed from jarvis.utils import build_embed
from jarvis.utils.cachecog import CacheCog
guild_ids = [578757004059738142, 520021794380447745, 862402786116763668] guild_ids = [862402786116763668] # [578757004059738142, 520021794380447745, 862402786116763668]
valid = re.compile(r"[\w\s\-\\/.!@#$%^*()+=<>,\u0080-\U000E0FFF]*") valid = re.compile(r"[\w\s\-\\/.!@#$%^*()+=<>,\u0080-\U000E0FFF]*")
invites = re.compile( invites = re.compile(
@ -25,29 +29,38 @@ invites = re.compile(
) )
class CTCCog(CacheCog): class CTCCog(Scale):
"""J.A.R.V.I.S. Complete the Code 2 Cog.""" """JARVIS Complete the Code 2 Cog."""
def __init__(self, bot: Snake): def __init__(self, bot: Snake):
super().__init__(bot) self.bot = bot
self.logger = logging.getLogger(__name__)
self._session = aiohttp.ClientSession() self._session = aiohttp.ClientSession()
self.url = "https://completethecodetwo.cards/pw" self.url = "https://completethecodetwo.cards/pw"
def __del__(self): def __del__(self):
self._session.close() self._session.close()
@slash_command( ctc2 = SlashCommand(name="ctc2", description="CTC2 related commands", scopes=guild_ids)
name="ctc2", sub_cmd_name="about", description="CTC2 related commands", scopes=guild_ids
) @ctc2.subcommand(sub_cmd_name="about")
@cooldown(bucket=Buckets.USER, rate=1, interval=30) @cooldown(bucket=Buckets.USER, rate=1, interval=30)
async def _about(self, ctx: InteractionContext) -> None: async def _about(self, ctx: InteractionContext) -> None:
await ctx.send("See https://completethecode.com for more information") components = [
ActionRow(
Button(style=ButtonStyles.URL, url="https://completethecode.com", label="More Info")
)
]
await ctx.send(
"See https://completethecode.com for more information", components=components
)
@slash_command( @ctc2.subcommand(
name="ctc2",
sub_cmd_name="pw", sub_cmd_name="pw",
sub_cmd_description="Guess a password for https://completethecodetwo.cards", sub_cmd_description="Guess a password for https://completethecodetwo.cards",
scopes=guild_ids, )
@slash_option(
name="guess", description="Guess a password", opt_type=OptionTypes.STRING, required=True
) )
@cooldown(bucket=Buckets.USER, rate=1, interval=2) @cooldown(bucket=Buckets.USER, rate=1, interval=2)
async def _pw(self, ctx: InteractionContext, guess: str) -> None: async def _pw(self, ctx: InteractionContext, guess: str) -> None:
@ -79,6 +92,7 @@ class CTCCog(CacheCog):
if guessed: if guessed:
await ctx.send("Already guessed, dipshit.", ephemeral=True) await ctx.send("Already guessed, dipshit.", ephemeral=True)
return return
result = await self._session.post(self.url, data=guess) result = await self._session.post(self.url, data=guess)
correct = False correct = False
if 200 <= result.status < 400: if 200 <= result.status < 400:
@ -86,28 +100,18 @@ class CTCCog(CacheCog):
correct = True correct = True
else: else:
await ctx.send("Nope.", ephemeral=True) await ctx.send("Nope.", ephemeral=True)
_ = Guess(guess=guess, user=ctx.author.id, correct=correct).save() await Guess(guess=guess, user=ctx.author.id, correct=correct).commit()
@slash_command( @ctc2.subcommand(
name="ctc2",
sub_cmd_name="guesses", sub_cmd_name="guesses",
sub_cmd_description="Show guesses made for https://completethecodetwo.cards", sub_cmd_description="Show guesses made for https://completethecodetwo.cards",
scopes=guild_ids,
) )
@cooldown(bucket=Buckets.USER, rate=1, interval=2) @cooldown(bucket=Buckets.USER, rate=1, interval=2)
async def _guesses(self, ctx: InteractionContext) -> None: async def _guesses(self, ctx: InteractionContext) -> None:
exists = self.check_cache(ctx) await ctx.defer()
if exists: guesses = Guess.find().sort("correct", -1).sort("id", -1)
await ctx.defer(ephemeral=True)
await ctx.send(
f"Please use existing interaction: {exists['paginator']._message.jump_url}",
ephemeral=True,
)
return
guesses = Guess.objects().order_by("-correct", "-id")
fields = [] fields = []
for guess in guesses: async for guess in guesses:
user = await ctx.guild.get_member(guess["user"]) user = await ctx.guild.get_member(guess["user"])
if not user: if not user:
user = await self.bot.fetch_user(guess["user"]) user = await self.bot.fetch_user(guess["user"])
@ -141,17 +145,9 @@ class CTCCog(CacheCog):
paginator = Paginator.create_from_embeds(self.bot, *pages, timeout=300) paginator = Paginator.create_from_embeds(self.bot, *pages, timeout=300)
self.cache[hash(paginator)] = {
"guild": ctx.guild.id,
"user": ctx.author.id,
"timeout": datetime.utcnow() + timedelta(minutes=5),
"command": ctx.subcommand_name,
"paginator": paginator,
}
await paginator.send(ctx) await paginator.send(ctx)
def setup(bot: Snake) -> None: def setup(bot: Snake) -> None:
"""Add CTCCog to J.A.R.V.I.S.""" """Add CTCCog to JARVIS"""
CTCCog(bot) CTCCog(bot)

View file

@ -1,4 +1,5 @@
"""J.A.R.V.I.S. dbrand cog.""" """JARVIS dbrand cog."""
import logging
import re import re
import aiohttp import aiohttp
@ -6,7 +7,7 @@ from dis_snek import InteractionContext, Scale, Snake
from dis_snek.models.discord.embed import EmbedField from dis_snek.models.discord.embed import EmbedField
from dis_snek.models.snek.application_commands import ( from dis_snek.models.snek.application_commands import (
OptionTypes, OptionTypes,
slash_command, SlashCommand,
slash_option, slash_option,
) )
from dis_snek.models.snek.command import cooldown from dis_snek.models.snek.command import cooldown
@ -16,18 +17,19 @@ from jarvis.config import get_config
from jarvis.data.dbrand import shipping_lookup from jarvis.data.dbrand import shipping_lookup
from jarvis.utils import build_embed from jarvis.utils import build_embed
guild_ids = [578757004059738142, 520021794380447745, 862402786116763668] guild_ids = [862402786116763668] # [578757004059738142, 520021794380447745, 862402786116763668]
class DbrandCog(Scale): class DbrandCog(Scale):
""" """
dbrand functions for J.A.R.V.I.S. dbrand functions for JARVIS
Mostly support functions. Credit @cpixl for the shipping API Mostly support functions. Credit @cpixl for the shipping API
""" """
def __init__(self, bot: Snake): def __init__(self, bot: Snake):
self.bot = bot self.bot = bot
self.logger = logging.getLogger(__name__)
self.base_url = "https://dbrand.com/" self.base_url = "https://dbrand.com/"
self._session = aiohttp.ClientSession() self._session = aiohttp.ClientSession()
self._session.headers.update({"Content-Type": "application/json"}) self._session.headers.update({"Content-Type": "application/json"})
@ -37,121 +39,53 @@ class DbrandCog(Scale):
def __del__(self): def __del__(self):
self._session.close() self._session.close()
@slash_command( db = SlashCommand(name="db", description="dbrand commands", scopes=guild_ids)
name="db",
sub_cmd_name="skin",
scopes=guild_ids,
sub_cmd_description="See what skins are available",
)
@cooldown(bucket=Buckets.USER, rate=1, interval=30)
async def _skin(self, ctx: InteractionContext) -> None:
await ctx.send(self.base_url + "/skins")
@slash_command( @db.subcommand(sub_cmd_name="info", sub_cmd_description="Get useful links")
name="db",
sub_cmd_name="robotcamo",
scopes=guild_ids,
sub_cmd_description="Get some robot camo. Make Tony Stark proud",
)
@cooldown(bucket=Buckets.USER, rate=1, interval=30) @cooldown(bucket=Buckets.USER, rate=1, interval=30)
async def _camo(self, ctx: InteractionContext) -> None: async def _info(self, ctx: InteractionContext) -> None:
await ctx.send(self.base_url + "robot-camo") urls = [
f"[Get Skins]({self.base_url + 'skins'})",
f"[Robot Camo]({self.base_url + 'robot-camo'})",
f"[Get a Grip]({self.base_url + 'grip'})",
f"[Shop All Products]({self.base_url + 'shop'})",
f"[Order Status]({self.base_url + 'order-status'})",
f"[dbrand Status]({self.base_url + 'status'})",
f"[Be (not) extorted]({self.base_url + 'not-extortion'})",
"[Robot Camo Wallpapers](https://db.io/wallpapers)",
]
embed = build_embed(
title="Useful Links", description="\n\n".join(urls), fields=[], color="#FFBB00"
)
embed.set_footer(
text="dbrand.com",
icon_url="https://dev.zevaryx.com/db_logo.png",
)
embed.set_thumbnail(url="https://dev.zevaryx.com/db_logo.png")
embed.set_author(
name="dbrand", url=self.base_url, icon_url="https://dev.zevaryx.com/db_logo.png"
)
await ctx.send(embed=embed)
@slash_command( @db.subcommand(
name="db",
sub_cmd_name="grip",
scopes=guild_ids,
sub_cmd_description="See devices with Grip support",
)
@cooldown(bucket=Buckets.USER, rate=1, interval=30)
async def _grip(self, ctx: InteractionContext) -> None:
await ctx.send(self.base_url + "grip")
@slash_command(
name="db",
sub_cmd_name="contact", sub_cmd_name="contact",
scopes=guild_ids,
sub_cmd_description="Contact support", sub_cmd_description="Contact support",
) )
@cooldown(bucket=Buckets.USER, rate=1, interval=30) @cooldown(bucket=Buckets.USER, rate=1, interval=30)
async def _contact(self, ctx: InteractionContext) -> None: async def _contact(self, ctx: InteractionContext) -> None:
await ctx.send("Contact dbrand support here: " + self.base_url + "contact") await ctx.send("Contact dbrand support here: " + self.base_url + "contact")
@slash_command( @db.subcommand(
name="db",
sub_cmd_name="support", sub_cmd_name="support",
scopes=guild_ids,
sub_cmd_description="Contact support", sub_cmd_description="Contact support",
) )
@cooldown(bucket=Buckets.USER, rate=1, interval=30) @cooldown(bucket=Buckets.USER, rate=1, interval=30)
async def _support(self, ctx: InteractionContext) -> None: async def _support(self, ctx: InteractionContext) -> None:
await ctx.send("Contact dbrand support here: " + self.base_url + "contact") await ctx.send("Contact dbrand support here: " + self.base_url + "contact")
@slash_command( @db.subcommand(
name="db",
sub_cmd_name="orderstat",
scopes=guild_ids,
sub_cmd_description="Get your order status",
)
@cooldown(bucket=Buckets.USER, rate=1, interval=30)
async def _orderstat(self, ctx: InteractionContext) -> None:
await ctx.send(self.base_url + "order-status")
@slash_command(
name="db",
sub_cmd_name="orders",
scopes=guild_ids,
sub_cmd_description="Get your order status",
)
@cooldown(bucket=Buckets.USER, rate=1, interval=30)
async def _orders(self, ctx: InteractionContext) -> None:
await ctx.send(self.base_url + "order-status")
@slash_command(
name="db",
sub_cmd_name="status",
scopes=guild_ids,
sub_cmd_description="dbrand status",
)
@cooldown(bucket=Buckets.USER, rate=1, interval=30)
async def _status(self, ctx: InteractionContext) -> None:
await ctx.send(self.base_url + "status")
@slash_command(
name="db",
sub_cmd_name="buy",
scopes=guild_ids,
sub_cmd_description="Give us your money!",
)
@cooldown(bucket=Buckets.USER, rate=1, interval=30)
async def _buy(self, ctx: InteractionContext) -> None:
await ctx.send("Give us your money! " + self.base_url + "shop")
@slash_command(
name="db",
sub_cmd_name="extortion",
scopes=guild_ids,
sub_cmd_description="(not) extortion",
)
@cooldown(bucket=Buckets.USER, rate=1, interval=30)
async def _extort(self, ctx: InteractionContext) -> None:
await ctx.send("Be (not) extorted here: " + self.base_url + "not-extortion")
@slash_command(
name="db",
sub_cmd_name="wallpapers",
sub_cmd_description="Robot Camo Wallpapers",
scopes=guild_ids,
)
@cooldown(bucket=Buckets.USER, rate=1, interval=30)
async def _wallpapers(self, ctx: InteractionContext) -> None:
await ctx.send("Get robot camo wallpapers here: https://db.io/wallpapers")
@slash_command(
name="db",
sub_cmd_name="ship", sub_cmd_name="ship",
sub_cmd_description="Get shipping information for your country", sub_cmd_description="Get shipping information for your country",
scopes=guild_ids,
) )
@slash_option( @slash_option(
name="search", name="search",
@ -215,7 +149,7 @@ class DbrandCog(Scale):
) )
embed = build_embed( embed = build_embed(
title="Shipping to {}".format(data["country"]), title="Shipping to {}".format(data["country"]),
sub_cmd_description=description, description=description,
color="#FFBB00", color="#FFBB00",
fields=fields, fields=fields,
url=self.base_url + "shipping/" + country, url=self.base_url + "shipping/" + country,
@ -229,7 +163,7 @@ class DbrandCog(Scale):
elif not data["is_valid"]: elif not data["is_valid"]:
embed = build_embed( embed = build_embed(
title="Check Shipping Times", title="Check Shipping Times",
sub_cmd_description=( description=(
"Country not found.\nYou can [view all shipping " "Country not found.\nYou can [view all shipping "
"destinations here](https://dbrand.com/shipping)" "destinations here](https://dbrand.com/shipping)"
), ),
@ -246,7 +180,7 @@ class DbrandCog(Scale):
elif not data["shipping_available"]: elif not data["shipping_available"]:
embed = build_embed( embed = build_embed(
title="Shipping to {}".format(data["country"]), title="Shipping to {}".format(data["country"]),
sub_cmd_description=( description=(
"No shipping available.\nTime to move to a country" "No shipping available.\nTime to move to a country"
" that has shipping available.\nYou can [find a new country " " that has shipping available.\nYou can [find a new country "
"to live in here](https://dbrand.com/shipping)" "to live in here](https://dbrand.com/shipping)"
@ -264,5 +198,5 @@ class DbrandCog(Scale):
def setup(bot: Snake) -> None: def setup(bot: Snake) -> None:
"""Add dbrandcog to J.A.R.V.I.S.""" """Add dbrandcog to JARVIS"""
DbrandCog(bot) DbrandCog(bot)

View file

@ -1,6 +1,7 @@
"""J.A.R.V.I.S. Developer Cog.""" """JARVIS Developer Cog."""
import base64 import base64
import hashlib import hashlib
import logging
import re import re
import subprocess # noqa: S404 import subprocess # noqa: S404
import uuid as uuidpy import uuid as uuidpy
@ -45,7 +46,11 @@ MAX_FILESIZE = 5 * (1024**3) # 5GB
class DevCog(Scale): class DevCog(Scale):
"""J.A.R.V.I.S. Developer Cog.""" """JARVIS Developer Cog."""
def __init__(self, bot: Snake):
self.bot = bot
self.logger = logging.getLogger(__name__)
@slash_command(name="hash", description="Hash some data") @slash_command(name="hash", description="Hash some data")
@slash_option( @slash_option(
@ -83,8 +88,9 @@ class DevCog(Scale):
title = attach.filename title = attach.filename
elif url.match(data): elif url.match(data):
try: try:
if await get_size(data) > MAX_FILESIZE: if (size := await get_size(data)) > MAX_FILESIZE:
await ctx.send("Please hash files that are <= 5GB in size", ephemeral=True) await ctx.send("Please hash files that are <= 5GB in size", ephemeral=True)
self.logger.debug(f"Refused to hash file of size {convert_bytesize(size)}")
return return
except Exception as e: except Exception as e:
await ctx.send(f"Failed to retrieve URL: ```\n{e}\n```", ephemeral=True) await ctx.send(f"Failed to retrieve URL: ```\n{e}\n```", ephemeral=True)
@ -162,6 +168,9 @@ class DevCog(Scale):
name="uuid2ulid", name="uuid2ulid",
description="Convert a UUID to a ULID", description="Convert a UUID to a ULID",
) )
@slash_option(
name="uuid", description="UUID to convert", opt_type=OptionTypes.STRING, required=True
)
@cooldown(bucket=Buckets.USER, rate=1, interval=2) @cooldown(bucket=Buckets.USER, rate=1, interval=2)
async def _uuid2ulid(self, ctx: InteractionContext, uuid: str) -> None: async def _uuid2ulid(self, ctx: InteractionContext, uuid: str) -> None:
if UUID_VERIFY.match(uuid): if UUID_VERIFY.match(uuid):
@ -174,6 +183,9 @@ class DevCog(Scale):
name="ulid2uuid", name="ulid2uuid",
description="Convert a ULID to a UUID", description="Convert a ULID to a UUID",
) )
@slash_option(
name="ulid", description="ULID to convert", opt_type=OptionTypes.STRING, required=True
)
@cooldown(bucket=Buckets.USER, rate=1, interval=2) @cooldown(bucket=Buckets.USER, rate=1, interval=2)
async def _ulid2uuid(self, ctx: InteractionContext, ulid: str) -> None: async def _ulid2uuid(self, ctx: InteractionContext, ulid: str) -> None:
if ULID_VERIFY.match(ulid): if ULID_VERIFY.match(ulid):
@ -199,9 +211,19 @@ class DevCog(Scale):
required=True, required=True,
) )
async def _encode(self, ctx: InteractionContext, method: str, data: str) -> None: async def _encode(self, ctx: InteractionContext, method: str, data: str) -> None:
if invites.search(data):
await ctx.send(
"Please don't use this to bypass invite restrictions",
ephemeral=True,
)
return
mstr = method mstr = method
method = getattr(base64, method + "encode") method = getattr(base64, method + "encode")
encoded = method(data.encode("UTF-8")).decode("UTF-8") try:
encoded = method(data.encode("UTF-8")).decode("UTF-8")
except Exception as e:
await ctx.send(f"Failed to encode data: {e}")
return
fields = [ fields = [
EmbedField(name="Plaintext", value=f"`{data}`", inline=False), EmbedField(name="Plaintext", value=f"`{data}`", inline=False),
EmbedField(name=mstr, value=f"`{encoded}`", inline=False), EmbedField(name=mstr, value=f"`{encoded}`", inline=False),
@ -226,7 +248,11 @@ class DevCog(Scale):
async def _decode(self, ctx: InteractionContext, method: str, data: str) -> None: async def _decode(self, ctx: InteractionContext, method: str, data: str) -> None:
mstr = method mstr = method
method = getattr(base64, method + "decode") method = getattr(base64, method + "decode")
decoded = method(data.encode("UTF-8")).decode("UTF-8") try:
decoded = method(data.encode("UTF-8")).decode("UTF-8")
except Exception as e:
await ctx.send(f"Failed to decode data: {e}")
return
if invites.search(decoded): if invites.search(decoded):
await ctx.send( await ctx.send(
"Please don't use this to bypass invite restrictions", "Please don't use this to bypass invite restrictions",
@ -240,7 +266,7 @@ class DevCog(Scale):
embed = build_embed(title="Decoded Data", description="", fields=fields) embed = build_embed(title="Decoded Data", description="", fields=fields)
await ctx.send(embed=embed) await ctx.send(embed=embed)
@slash_command(name="cloc", description="Get J.A.R.V.I.S. lines of code") @slash_command(name="cloc", description="Get JARVIS lines of code")
@cooldown(bucket=Buckets.CHANNEL, rate=1, interval=30) @cooldown(bucket=Buckets.CHANNEL, rate=1, interval=30)
async def _cloc(self, ctx: InteractionContext) -> None: async def _cloc(self, ctx: InteractionContext) -> None:
output = subprocess.check_output( # noqa: S603, S607 output = subprocess.check_output( # noqa: S603, S607
@ -250,5 +276,5 @@ class DevCog(Scale):
def setup(bot: Snake) -> None: def setup(bot: Snake) -> None:
"""Add DevCog to J.A.R.V.I.S.""" """Add DevCog to JARVIS"""
DevCog(bot) DevCog(bot)

View file

@ -1,35 +1,46 @@
"""J.A.R.V.I.S. GitLab Cog.""" """JARVIS GitLab Cog."""
import asyncio
import logging
from datetime import datetime from datetime import datetime
import gitlab import gitlab
from dis_snek import InteractionContext, Scale, Snake from dis_snek import InteractionContext, Scale, Snake
from dis_snek.ext.paginators import Paginator from dis_snek.ext.paginators import Paginator
from dis_snek.models.discord.embed import Embed, EmbedField from dis_snek.models.discord.embed import Embed, EmbedField
from dis_snek.models.discord.modal import InputText, Modal, TextStyles
from dis_snek.models.discord.user import Member
from dis_snek.models.snek.application_commands import ( from dis_snek.models.snek.application_commands import (
OptionTypes, OptionTypes,
SlashCommand,
SlashCommandChoice, SlashCommandChoice,
slash_command, slash_command,
slash_option, slash_option,
) )
from dis_snek.models.snek.command import cooldown
from dis_snek.models.snek.cooldowns import Buckets
from jarvis.config import get_config from jarvis.config import JarvisConfig
from jarvis.utils import build_embed from jarvis.utils import build_embed
guild_ids = [862402786116763668] guild_ids = [862402786116763668]
class GitlabCog(Scale): class GitlabCog(Scale):
"""J.A.R.V.I.S. GitLab Cog.""" """JARVIS GitLab Cog."""
def __init__(self, bot: Snake): def __init__(self, bot: Snake):
self.bot = bot self.bot = bot
config = get_config() self.logger = logging.getLogger(__name__)
config = JarvisConfig.from_yaml()
self._gitlab = gitlab.Gitlab("https://git.zevaryx.com", private_token=config.gitlab_token) self._gitlab = gitlab.Gitlab("https://git.zevaryx.com", private_token=config.gitlab_token)
# J.A.R.V.I.S. GitLab ID is 29 # JARVIS GitLab ID is 29
self.project = self._gitlab.projects.get(29) self.project = self._gitlab.projects.get(29)
@slash_command( gl = SlashCommand(name="gl", description="Get GitLab info", scopes=guild_ids)
name="gl", sub_cmd_name="issue", description="Get an issue from GitLab", scopes=guild_ids
@gl.subcommand(
sub_cmd_name="issue",
sub_cmd_description="Get an issue from GitLab",
) )
@slash_option(name="id", description="Issue ID", opt_type=OptionTypes.INTEGER, required=True) @slash_option(name="id", description="Issue ID", opt_type=OptionTypes.INTEGER, required=True)
async def _issue(self, ctx: InteractionContext, id: int) -> None: async def _issue(self, ctx: InteractionContext, id: int) -> None:
@ -85,7 +96,7 @@ class GitlabCog(Scale):
) )
embed.set_author( embed.set_author(
name=issue.author["name"], name=issue.author["name"],
icon_url=issue.author["display_avatar"], icon_url=issue.author["avatar_url"],
url=issue.author["web_url"], url=issue.author["web_url"],
) )
embed.set_thumbnail( embed.set_thumbnail(
@ -93,11 +104,9 @@ class GitlabCog(Scale):
) )
await ctx.send(embed=embed) await ctx.send(embed=embed)
@slash_command( @gl.subcommand(
name="gl",
sub_cmd_name="milestone", sub_cmd_name="milestone",
description="Get a milestone from GitLab", sub_cmd_description="Get a milestone from GitLab",
scopes=guild_ids,
) )
@slash_option( @slash_option(
name="id", description="Milestone ID", opt_type=OptionTypes.INTEGER, required=True name="id", description="Milestone ID", opt_type=OptionTypes.INTEGER, required=True
@ -140,7 +149,7 @@ class GitlabCog(Scale):
url=milestone.web_url, url=milestone.web_url,
) )
embed.set_author( embed.set_author(
name="J.A.R.V.I.S.", name="JARVIS",
url="https://git.zevaryx.com/jarvis", url="https://git.zevaryx.com/jarvis",
icon_url="https://git.zevaryx.com/uploads/-/system/user/avatar/11/avatar.png", icon_url="https://git.zevaryx.com/uploads/-/system/user/avatar/11/avatar.png",
) )
@ -149,11 +158,9 @@ class GitlabCog(Scale):
) )
await ctx.send(embed=embed) await ctx.send(embed=embed)
@slash_command( @gl.subcommand(
name="gl",
sub_cmd_name="mr", sub_cmd_name="mr",
description="Get a merge request from GitLab", sub_cmd_description="Get a merge request from GitLab",
scopes=guild_ids,
) )
@slash_option( @slash_option(
name="id", description="Merge Request ID", opt_type=OptionTypes.INTEGER, required=True name="id", description="Merge Request ID", opt_type=OptionTypes.INTEGER, required=True
@ -181,25 +188,26 @@ class GitlabCog(Scale):
labels = "None" labels = "None"
fields = [ fields = [
EmbedField(name="State", value=mr.state[0].upper() + mr.state[1:]), EmbedField(name="State", value=mr.state[0].upper() + mr.state[1:], inline=True),
EmbedField(name="Assignee", value=assignee), EmbedField(name="Draft?", value=str(mr.draft), inline=True),
EmbedField(name="Labels", value=labels), EmbedField(name="Assignee", value=assignee, inline=True),
EmbedField(name="Labels", value=labels, inline=True),
] ]
if mr.labels: if mr.labels:
color = self.project.labels.get(mr.labels[0]).color color = self.project.labels.get(mr.labels[0]).color
else: else:
color = "#00FFEE" color = "#00FFEE"
fields.append(EmbedField(name="Created At", value=created_at)) fields.append(EmbedField(name="Created At", value=created_at, inline=True))
if mr.state == "merged": if mr.state == "merged":
merged_at = datetime.strptime(mr.merged_at, "%Y-%m-%dT%H:%M:%S.%fZ").strftime( merged_at = datetime.strptime(mr.merged_at, "%Y-%m-%dT%H:%M:%S.%fZ").strftime(
"%Y-%m-%d %H:%M:%S UTC" "%Y-%m-%d %H:%M:%S UTC"
) )
fields.append(EmbedField(name="Merged At", value=merged_at)) fields.append(EmbedField(name="Merged At", value=merged_at, inline=True))
elif mr.state == "closed": elif mr.state == "closed":
closed_at = datetime.strptime(mr.closed_at, "%Y-%m-%dT%H:%M:%S.%fZ").strftime( closed_at = datetime.strptime(mr.closed_at, "%Y-%m-%dT%H:%M:%S.%fZ").strftime(
"%Y-%m-%d %H:%M:%S UTC" "%Y-%m-%d %H:%M:%S UTC"
) )
fields.append(EmbedField(name="Closed At", value=closed_at)) fields.append(EmbedField(name="Closed At", value=closed_at, inline=True))
if mr.milestone: if mr.milestone:
fields.append( fields.append(
EmbedField( EmbedField(
@ -219,7 +227,7 @@ class GitlabCog(Scale):
) )
embed.set_author( embed.set_author(
name=mr.author["name"], name=mr.author["name"],
icon_url=mr.author["display_avatar"], icon_url=mr.author["avatar_url"],
url=mr.author["web_url"], url=mr.author["web_url"],
) )
embed.set_thumbnail( embed.set_thumbnail(
@ -232,7 +240,7 @@ class GitlabCog(Scale):
title = "" title = ""
if t_state: if t_state:
title = f"{t_state} " title = f"{t_state} "
title += f"J.A.R.V.I.S. {name}s" title += f"JARVIS {name}s"
fields = [] fields = []
for item in api_list: for item in api_list:
description = item.description or "No description" description = item.description or "No description"
@ -248,10 +256,10 @@ class GitlabCog(Scale):
title=title, title=title,
description="", description="",
fields=fields, fields=fields,
url=f"https://git.zevaryx.com/stark-industries/j.a.r.v.i.s./{name.replace(' ', '_')}s", url=f"https://git.zevaryx.com/stark-industries/JARVIS/{name.replace(' ', '_')}s",
) )
embed.set_author( embed.set_author(
name="J.A.R.V.I.S.", name="JARVIS",
url="https://git.zevaryx.com/jarvis", url="https://git.zevaryx.com/jarvis",
icon_url="https://git.zevaryx.com/uploads/-/system/user/avatar/11/avatar.png", icon_url="https://git.zevaryx.com/uploads/-/system/user/avatar/11/avatar.png",
) )
@ -260,8 +268,9 @@ class GitlabCog(Scale):
) )
return embed return embed
@slash_command( @gl.subcommand(
name="gl", sub_cmd_name="issues", description="Get issues from GitLab", scopes=guild_ids sub_cmd_name="issues",
sub_cmd_description="Get issues from GitLab",
) )
@slash_option( @slash_option(
name="state", name="state",
@ -313,11 +322,9 @@ class GitlabCog(Scale):
await paginator.send(ctx) await paginator.send(ctx)
@slash_command( @gl.subcommand(
name="gl",
sub_cmd_name="mrs", sub_cmd_name="mrs",
description="Get merge requests from GitLab", sub_cmd_description="Get merge requests from GitLab",
scopes=guild_ids,
) )
@slash_option( @slash_option(
name="state", name="state",
@ -371,11 +378,9 @@ class GitlabCog(Scale):
await paginator.send(ctx) await paginator.send(ctx)
@slash_command( @gl.subcommand(
name="gl",
sub_cmd_name="milestones", sub_cmd_name="milestones",
description="Get milestones from GitLab", sub_cmd_description="Get milestones from GitLab",
scopes=guild_ids,
) )
async def _milestones(self, ctx: InteractionContext) -> None: async def _milestones(self, ctx: InteractionContext) -> None:
await ctx.defer() await ctx.defer()
@ -410,8 +415,55 @@ class GitlabCog(Scale):
await paginator.send(ctx) await paginator.send(ctx)
@slash_command(name="issue", description="Report an issue on GitLab", scopes=guild_ids)
@slash_option(
name="user",
description="Credit someone else for this issue",
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(
title="Open a new issue on GitLab",
components=[
InputText(
label="Issue Title",
placeholder="Descriptive Title",
style=TextStyles.SHORT,
custom_id="title",
max_length=200,
),
InputText(
label="Description (supports Markdown!)",
placeholder="Detailed Description",
style=TextStyles.PARAGRAPH,
custom_id="description",
),
],
)
await ctx.send_modal(modal)
try:
resp = await self.bot.wait_for_modal(modal, author=ctx.author.id, timeout=60 * 5)
title = resp.responses.get("title")
desc = resp.responses.get("description")
except asyncio.TimeoutError:
return
if not title.startswith("[Discord]"):
title = "[Discord] " + title
desc = f"Opened by `@{user.username}` on Discord\n\n" + desc
issue = self.project.issues.create(data={"title": title, "description": desc})
embed = build_embed(
title=f"Issue #{issue.id} Created",
description=("Thank you for opening an issue!\n\n[View it online]({issue['web_url']})"),
fields=[],
color="#00FFEE",
)
await resp.send(embed=embed)
def setup(bot: Snake) -> None: def setup(bot: Snake) -> None:
"""Add GitlabCog to J.A.R.V.I.S. if Gitlab token exists.""" """Add GitlabCog to JARVIS if Gitlab token exists."""
if get_config().gitlab_token: if JarvisConfig.from_yaml().gitlab_token:
GitlabCog(bot) GitlabCog(bot)

View file

@ -1,4 +1,5 @@
"""J.A.R.V.I.S. image processing cog.""" """JARVIS image processing cog."""
import logging
import re import re
from io import BytesIO from io import BytesIO
@ -21,13 +22,14 @@ MIN_ACCURACY = 0.80
class ImageCog(Scale): class ImageCog(Scale):
""" """
Image processing functions for J.A.R.V.I.S. Image processing functions for JARVIS
May be categorized under util later May be categorized under util later
""" """
def __init__(self, bot: Snake): def __init__(self, bot: Snake):
self.bot = bot self.bot = bot
self.logger = logging.getLogger(__name__)
self._session = aiohttp.ClientSession() self._session = aiohttp.ClientSession()
self.tgt_match = re.compile(r"([0-9]*\.?[0-9]*?) ?([KMGTP]?B?)", re.IGNORECASE) self.tgt_match = re.compile(r"([0-9]*\.?[0-9]*?) ?([KMGTP]?B?)", re.IGNORECASE)
@ -56,6 +58,7 @@ class ImageCog(Scale):
async def _resize( async def _resize(
self, ctx: InteractionContext, target: str, attachment: Attachment = None, url: str = None self, ctx: InteractionContext, target: str, attachment: Attachment = None, url: str = None
) -> None: ) -> None:
await ctx.defer()
if not attachment and not url: if not attachment and not url:
await ctx.send("A URL or attachment is required", ephemeral=True) await ctx.send("A URL or attachment is required", ephemeral=True)
return return
@ -71,10 +74,18 @@ class ImageCog(Scale):
) )
return return
tgt_size = unconvert_bytesize(float(tgt.groups()[0]), tgt.groups()[1]) try:
tgt_size = unconvert_bytesize(float(tgt.groups()[0]), tgt.groups()[1])
except ValueError:
await ctx.send("Failed to read your target size. Try a more sane one", ephemeral=True)
return
if tgt_size > unconvert_bytesize(8, "MB"): if tgt_size > unconvert_bytesize(8, "MB"):
await ctx.send("Target too large to send. Please make target < 8MB", ephemeral=True) await ctx.send("Target too large to send. Please make target < 8MB", ephemeral=True)
return return
elif tgt_size < 1024:
await ctx.send("Sizes < 1KB are extremely unreliable and are disabled", ephemeral=True)
return
if attachment: if attachment:
url = attachment.url url = attachment.url
@ -83,13 +94,24 @@ class ImageCog(Scale):
filename = url.split("/")[-1] filename = url.split("/")[-1]
data = None data = None
async with self._session.get(url) as resp: try:
if resp.status == 200: async with self._session.get(url) as resp:
data = await resp.read() resp.raise_for_status()
if resp.content_type in ["image/jpeg", "image/png"]:
data = await resp.read()
else:
await ctx.send(
"Unsupported content type. Please send a URL to a JPEG or PNG",
ephemeral=True,
)
return
except Exception:
await ctx.send("Failed to retrieve image. Please verify url", ephemeral=True)
return
size = len(data) size = len(data)
if size <= tgt_size: if size <= tgt_size:
await ctx.send("Image already meets target.") await ctx.send("Image already meets target.", ephemeral=True)
return return
ratio = max(tgt_size / size - 0.02, 0.50) ratio = max(tgt_size / size - 0.02, 0.50)
@ -130,5 +152,5 @@ class ImageCog(Scale):
def setup(bot: Snake) -> None: def setup(bot: Snake) -> None:
"""Add ImageCog to J.A.R.V.I.S.""" """Add ImageCog to JARVIS"""
ImageCog(bot) ImageCog(bot)

View file

@ -1,10 +1,13 @@
"""J.A.R.V.I.S. Remind Me Cog.""" """JARVIS Remind Me Cog."""
import asyncio import asyncio
import logging
import re import re
from datetime import datetime, timedelta from datetime import datetime, timezone
from typing import List from typing import List
from bson import ObjectId from bson import ObjectId
from dateparser import parse
from dateparser_data.settings import default_parsers
from dis_snek import InteractionContext, Scale, Snake from dis_snek import InteractionContext, Scale, Snake
from dis_snek.client.utils.misc_utils import get from dis_snek.client.utils.misc_utils import get
from dis_snek.models.discord.channel import GuildChannel from dis_snek.models.discord.channel import GuildChannel
@ -13,7 +16,7 @@ from dis_snek.models.discord.embed import Embed, EmbedField
from dis_snek.models.discord.modal import InputText, Modal, TextStyles from dis_snek.models.discord.modal import InputText, Modal, TextStyles
from dis_snek.models.snek.application_commands import ( from dis_snek.models.snek.application_commands import (
OptionTypes, OptionTypes,
SlashCommandChoice, SlashCommand,
slash_command, slash_command,
slash_option, slash_option,
) )
@ -31,25 +34,32 @@ invites = re.compile(
class RemindmeCog(Scale): class RemindmeCog(Scale):
"""J.A.R.V.I.S. Remind Me Cog.""" """JARVIS Remind Me Cog."""
def __init__(self, bot: Snake):
self.bot = bot
self.logger = logging.getLogger(__name__)
@slash_command(name="remindme", description="Set a reminder") @slash_command(name="remindme", description="Set a reminder")
@slash_option( @slash_option(
name="private", name="private",
description="Send as DM?", description="Send as DM?",
opt_type=OptionTypes.STRING, opt_type=OptionTypes.BOOLEAN,
required=False, required=False,
choices=[
SlashCommandChoice(name="Yes", value="y"),
SlashCommandChoice(name="No", value="n"),
],
) )
async def _remindme( async def _remindme(
self, self,
ctx: InteractionContext, ctx: InteractionContext,
private: str = "n", private: bool = False,
) -> None: ) -> None:
private = private == "y" reminders = len([x async for x in Reminder.find(q(user=ctx.author.id, active=True))])
if reminders >= 5:
await ctx.send(
"You already have 5 (or more) active reminders. "
"Please either remove an old one, or wait for one to pass",
ephemeral=True,
)
return
modal = Modal( modal = Modal(
title="Set your reminder!", title="Set your reminder!",
components=[ components=[
@ -62,12 +72,13 @@ class RemindmeCog(Scale):
), ),
InputText( InputText(
label="When to remind you?", label="When to remind you?",
placeholder="1h 30m", placeholder="1h 30m | in 5 minutes | November 11, 4011",
style=TextStyles.SHORT, style=TextStyles.SHORT,
custom_id="delay", custom_id="delay",
), ),
], ],
) )
await ctx.send_modal(modal) await ctx.send_modal(modal)
try: try:
response = await self.bot.wait_for_modal(modal, author=ctx.author.id, timeout=60 * 5) response = await self.bot.wait_for_modal(modal, author=ctx.author.id, timeout=60 * 5)
@ -76,44 +87,53 @@ class RemindmeCog(Scale):
except asyncio.TimeoutError: except asyncio.TimeoutError:
return return
if len(message) > 500: if len(message) > 500:
await ctx.send("Reminder cannot be > 500 characters.", ephemeral=True) await response.send("Reminder cannot be > 500 characters.", ephemeral=True)
return return
elif invites.search(message): elif invites.search(message):
await ctx.send( await response.send(
"Listen, don't use this to try and bypass the rules", "Listen, don't use this to try and bypass the rules",
ephemeral=True, ephemeral=True,
) )
return return
elif not valid.fullmatch(message): elif not valid.fullmatch(message):
await ctx.send("Hey, you should probably make this readable", ephemeral=True) await response.send("Hey, you should probably make this readable", ephemeral=True)
return return
units = {"w": "weeks", "d": "days", "h": "hours", "m": "minutes", "s": "seconds"} base_settings = {
delta = {"weeks": 0, "days": 0, "hours": 0, "minutes": 0, "seconds": 0} "PREFER_DATES_FROM": "future",
"TIMEZONE": "UTC",
"RETURN_AS_TIMEZONE_AWARE": True,
}
rt_settings = base_settings.copy()
rt_settings["PARSERS"] = [
x for x in default_parsers if x not in ["absolute-time", "timestamp"]
]
if times := time_pattern.findall(delay): rt_remind_at = parse(delay, settings=rt_settings)
for t in times:
delta[units[t[-1]]] += float(t[:-1]) at_settings = base_settings.copy()
at_settings["PARSERS"] = [x for x in default_parsers if x != "relative-time"]
at_remind_at = parse(delay, settings=at_settings)
if rt_remind_at:
remind_at = rt_remind_at
elif at_remind_at:
remind_at = at_remind_at
else: else:
await ctx.send( self.logger.debug(f"Failed to parse delay: {delay}")
"Invalid time string, please follow example: `1w 3d 7h 5m 20s`", ephemeral=True await response.send(
f"`{delay}` is not a parsable date, please try again", ephemeral=True
) )
return return
if not any(value for value in delta.items()): if remind_at < datetime.now(tz=timezone.utc):
await ctx.send("At least one time period is required", ephemeral=True) await response.send(
return f"`{delay}` is in the past. Past reminders aren't allowed", ephemeral=True
reminders = len([x async for x in Reminder.find(q(user=ctx.author.id, active=True))])
if reminders >= 5:
await ctx.send(
"You already have 5 (or more) active reminders. "
"Please either remove an old one, or wait for one to pass",
ephemeral=True,
) )
return return
remind_at = datetime.now() + timedelta(**delta) elif remind_at < datetime.now(tz=timezone.utc):
pass
r = Reminder( r = Reminder(
user=ctx.author.id, user=ctx.author.id,
@ -134,7 +154,7 @@ class RemindmeCog(Scale):
EmbedField(name="Message", value=message), EmbedField(name="Message", value=message),
EmbedField( EmbedField(
name="When", name="When",
value=remind_at.strftime("%Y-%m-%d %H:%M UTC"), value=f"<t:{int(remind_at.timestamp())}:F>",
inline=False, inline=False,
), ),
], ],
@ -157,7 +177,7 @@ class RemindmeCog(Scale):
if reminder.private and isinstance(ctx.channel, GuildChannel): if reminder.private and isinstance(ctx.channel, GuildChannel):
fields.embed( fields.embed(
EmbedField( EmbedField(
name=reminder.remind_at.strftime("%Y-%m-%d %H:%M UTC"), name=f"<t:{int(reminder.remind_at.timestamp())}:F>",
value="Please DM me this command to view the content of this reminder", value="Please DM me this command to view the content of this reminder",
inline=False, inline=False,
) )
@ -165,7 +185,7 @@ class RemindmeCog(Scale):
else: else:
fields.append( fields.append(
EmbedField( EmbedField(
name=reminder.remind_at.strftime("%Y-%m-%d %H:%M UTC"), name=f"<t:{int(reminder.remind_at.timestamp())}:F>",
value=f"{reminder.message}\n\u200b", value=f"{reminder.message}\n\u200b",
inline=False, inline=False,
) )
@ -185,17 +205,11 @@ class RemindmeCog(Scale):
return embed return embed
@slash_command(name="reminders", sub_cmd_name="list", sub_cmd_description="List reminders") reminders = SlashCommand(name="reminders", description="Manage reminders")
@reminders.subcommand(sub_cmd_name="list", sub_cmd_description="List reminders")
async def _list(self, ctx: InteractionContext) -> None: async def _list(self, ctx: InteractionContext) -> None:
exists = self.check_cache(ctx) reminders = await Reminder.find(q(user=ctx.author.id, active=True)).to_list(None)
if exists:
await ctx.defer(ephemeral=True)
await ctx.send(
f"Please use existing interaction: {exists['paginator']._message.jump_url}",
ephemeral=True,
)
return
reminders = await Reminder.find(q(user=ctx.author.id, active=True))
if not reminders: if not reminders:
await ctx.send("You have no reminders set.", ephemeral=True) await ctx.send("You have no reminders set.", ephemeral=True)
return return
@ -204,9 +218,9 @@ class RemindmeCog(Scale):
await ctx.send(embed=embed) await ctx.send(embed=embed)
@slash_command(name="reminders", sub_cmd_name="delete", sub_cmd_description="Delete a reminder") @reminders.subcommand(sub_cmd_name="delete", sub_cmd_description="Delete a reminder")
async def _delete(self, ctx: InteractionContext) -> None: async def _delete(self, ctx: InteractionContext) -> None:
reminders = await Reminder.find(q(user=ctx.author.id, active=True)) reminders = await Reminder.find(q(user=ctx.author.id, active=True)).to_list(None)
if not reminders: if not reminders:
await ctx.send("You have no reminders set", ephemeral=True) await ctx.send("You have no reminders set", ephemeral=True)
return return
@ -214,7 +228,7 @@ class RemindmeCog(Scale):
options = [] options = []
for reminder in reminders: for reminder in reminders:
option = SelectOption( option = SelectOption(
label=reminder.remind_at.strftime("%Y-%m-%d %H:%M UTC"), label=f"{reminder.remind_at}",
value=str(reminder.id), value=str(reminder.id),
emoji="", emoji="",
) )
@ -249,7 +263,7 @@ class RemindmeCog(Scale):
if reminder.private and isinstance(ctx.channel, GuildChannel): if reminder.private and isinstance(ctx.channel, GuildChannel):
fields.append( fields.append(
EmbedField( EmbedField(
name=reminder.remind_at.strftime("%Y-%m-%d %H:%M UTC"), name=f"<t:{int(reminder.remind_at.timestamp())}:F>",
value="Private reminder", value="Private reminder",
inline=False, inline=False,
) )
@ -257,12 +271,15 @@ class RemindmeCog(Scale):
else: else:
fields.append( fields.append(
EmbedField( EmbedField(
name=reminder.remind_at.strftime("%Y-%m-%d %H:%M UTC"), name=f"<t:{int(reminder.remind_at.timestamp())}:F>",
value=reminder.message, value=reminder.message,
inline=False, inline=False,
) )
) )
await reminder.delete() try:
await reminder.delete()
except Exception:
self.logger.debug("Ignoring deletion error")
for row in components: for row in components:
for component in row.components: for component in row.components:
@ -291,7 +308,34 @@ class RemindmeCog(Scale):
component.disabled = True component.disabled = True
await message.edit(components=components) await message.edit(components=components)
@reminders.subcommand(
sub_cmd_name="fetch",
sub_cmd_description="Fetch a reminder that failed to send",
)
@slash_option(
name="id", description="ID of the reminder", opt_type=OptionTypes.STRING, required=True
)
async def _fetch(self, ctx: InteractionContext, id: str) -> None:
reminder = await Reminder.find_one(q(id=id))
if not reminder:
await ctx.send(f"Reminder `{id}` does not exist", ephemeral=True)
return
embed = build_embed(title="You have a reminder!", description=reminder.message, fields=[])
embed.set_author(
name=ctx.author.display_name + "#" + ctx.author.discriminator,
icon_url=ctx.author.display_avatar,
)
embed.set_thumbnail(url=ctx.author.display_avatar)
await ctx.send(embed=embed, ephemeral=reminder.private)
if reminder.remind_at <= datetime.now(tz=timezone.utc):
try:
await reminder.delete()
except Exception:
self.logger.debug("Ignoring deletion error")
def setup(bot: Snake) -> None: def setup(bot: Snake) -> None:
"""Add RemindmeCog to J.A.R.V.I.S.""" """Add RemindmeCog to JARVIS"""
RemindmeCog(bot) RemindmeCog(bot)

View file

@ -1,5 +1,6 @@
"""J.A.R.V.I.S. Role Giver Cog.""" """JARVIS Role Giver Cog."""
import asyncio import asyncio
import logging
from dis_snek import InteractionContext, Permissions, Scale, Snake from dis_snek import InteractionContext, Permissions, Scale, Snake
from dis_snek.client.utils.misc_utils import get from dis_snek.client.utils.misc_utils import get
@ -8,7 +9,7 @@ from dis_snek.models.discord.embed import EmbedField
from dis_snek.models.discord.role import Role from dis_snek.models.discord.role import Role
from dis_snek.models.snek.application_commands import ( from dis_snek.models.snek.application_commands import (
OptionTypes, OptionTypes,
slash_command, SlashCommand,
slash_option, slash_option,
) )
from dis_snek.models.snek.command import check, cooldown from dis_snek.models.snek.command import check, cooldown
@ -21,17 +22,25 @@ from jarvis.utils.permissions import admin_or_permissions
class RolegiverCog(Scale): class RolegiverCog(Scale):
"""J.A.R.V.I.S. Role Giver Cog.""" """JARVIS Role Giver Cog."""
def __init__(self, bot: Snake): def __init__(self, bot: Snake):
self.bot = bot self.bot = bot
self.logger = logging.getLogger(__name__)
@slash_command( rolegiver = SlashCommand(name="rolegiver", description="Allow users to choose their own roles")
name="rolegiver", sub_cmd_name="add", sub_cmd_description="Add a role to rolegiver"
@rolegiver.subcommand(
sub_cmd_name="add",
sub_cmd_description="Add a role to rolegiver",
) )
@slash_option(name="role", description="Role to add", opt_type=OptionTypes.ROLE, required=True) @slash_option(name="role", description="Role to add", opt_type=OptionTypes.ROLE, required=True)
@check(admin_or_permissions(Permissions.MANAGE_GUILD)) @check(admin_or_permissions(Permissions.MANAGE_GUILD))
async def _rolegiver_add(self, ctx: InteractionContext, role: Role) -> None: async def _rolegiver_add(self, ctx: InteractionContext, role: Role) -> None:
if role.id == ctx.guild.id:
await ctx.send("Cannot add `@everyone` to rolegiver", ephemeral=True)
return
setting = await Rolegiver.find_one(q(guild=ctx.guild.id)) setting = await Rolegiver.find_one(q(guild=ctx.guild.id))
if setting and role.id in setting.roles: if setting and role.id in setting.roles:
await ctx.send("Role already in rolegiver", ephemeral=True) await ctx.send("Role already in rolegiver", ephemeral=True)
@ -45,13 +54,13 @@ class RolegiverCog(Scale):
return return
setting.roles.append(role.id) setting.roles.append(role.id)
setting.save() await setting.commit()
roles = [] roles = []
for role_id in setting.roles: for role_id in setting.roles:
if role_id == role.id: if role_id == role.id:
continue continue
e_role = await ctx.guild.get_role(role_id) e_role = await ctx.guild.fetch_role(role_id)
if not e_role: if not e_role:
continue continue
roles.append(e_role) roles.append(e_role)
@ -77,9 +86,7 @@ class RolegiverCog(Scale):
await ctx.send(embed=embed) await ctx.send(embed=embed)
@slash_command( @rolegiver.subcommand(sub_cmd_name="remove", sub_cmd_description="Remove a role from rolegiver")
name="rolegiver", sub_cmd_name="remove", sub_cmd_description="Remove a role from rolegiver"
)
@check(admin_or_permissions(Permissions.MANAGE_GUILD)) @check(admin_or_permissions(Permissions.MANAGE_GUILD))
async def _rolegiver_remove(self, ctx: InteractionContext) -> None: async def _rolegiver_remove(self, ctx: InteractionContext) -> None:
setting = await Rolegiver.find_one(q(guild=ctx.guild.id)) setting = await Rolegiver.find_one(q(guild=ctx.guild.id))
@ -89,7 +96,7 @@ class RolegiverCog(Scale):
options = [] options = []
for role in setting.roles: for role in setting.roles:
role: Role = await ctx.guild.get_role(role) role: Role = await ctx.guild.fetch_role(role)
option = SelectOption(label=role.name, value=str(role.id)) option = SelectOption(label=role.name, value=str(role.id))
options.append(option) options.append(option)
@ -111,11 +118,11 @@ class RolegiverCog(Scale):
) )
removed_roles = [] removed_roles = []
for to_delete in context.context.values: for to_delete in context.context.values:
role = await ctx.guild.get_role(to_delete) role = await ctx.guild.fetch_role(to_delete)
if role: if role:
removed_roles.append(role) removed_roles.append(role)
setting.roles.remove(int(to_delete)) setting.roles.remove(int(to_delete))
setting.save() await setting.commit()
for row in components: for row in components:
for component in row.components: for component in row.components:
@ -123,7 +130,7 @@ class RolegiverCog(Scale):
roles = [] roles = []
for role_id in setting.roles: for role_id in setting.roles:
e_role = await ctx.guild.get_role(role_id) e_role = await ctx.guild.fetch_role(role_id)
if not e_role: if not e_role:
continue continue
roles.append(e_role) roles.append(e_role)
@ -162,7 +169,7 @@ class RolegiverCog(Scale):
component.disabled = True component.disabled = True
await message.edit(components=components) await message.edit(components=components)
@slash_command(name="rolegiver", sub_cmd_name="list", description="List rolegiver roles") @rolegiver.subcommand(sub_cmd_name="list", sub_cmd_description="List rolegiver roles")
async def _rolegiver_list(self, ctx: InteractionContext) -> None: async def _rolegiver_list(self, ctx: InteractionContext) -> None:
setting = await Rolegiver.find_one(q(guild=ctx.guild.id)) setting = await Rolegiver.find_one(q(guild=ctx.guild.id))
if not setting or (setting and not setting.roles): if not setting or (setting and not setting.roles):
@ -171,7 +178,7 @@ class RolegiverCog(Scale):
roles = [] roles = []
for role_id in setting.roles: for role_id in setting.roles:
e_role = await ctx.guild.get_role(role_id) e_role = await ctx.guild.fetch_role(role_id)
if not e_role: if not e_role:
continue continue
roles.append(e_role) roles.append(e_role)
@ -197,7 +204,9 @@ class RolegiverCog(Scale):
await ctx.send(embed=embed) await ctx.send(embed=embed)
@slash_command(name="role", sub_cmd_name="get", sub_cmd_description="Get a role") role = SlashCommand(name="role", description="Get/Remove Rolegiver roles")
@role.subcommand(sub_cmd_name="get", sub_cmd_description="Get a role")
@cooldown(bucket=Buckets.USER, rate=1, interval=10) @cooldown(bucket=Buckets.USER, rate=1, interval=10)
async def _role_get(self, ctx: InteractionContext) -> None: async def _role_get(self, ctx: InteractionContext) -> None:
setting = await Rolegiver.find_one(q(guild=ctx.guild.id)) setting = await Rolegiver.find_one(q(guild=ctx.guild.id))
@ -207,7 +216,7 @@ class RolegiverCog(Scale):
options = [] options = []
for role in setting.roles: for role in setting.roles:
role: Role = await ctx.guild.get_role(role) role: Role = await ctx.guild.fetch_role(role)
option = SelectOption(label=role.name, value=str(role.id)) option = SelectOption(label=role.name, value=str(role.id))
options.append(option) options.append(option)
@ -230,7 +239,7 @@ class RolegiverCog(Scale):
added_roles = [] added_roles = []
for role in context.context.values: for role in context.context.values:
role = await ctx.guild.get_role(int(role)) role = await ctx.guild.fetch_role(int(role))
added_roles.append(role) added_roles.append(role)
await ctx.author.add_role(role, reason="Rolegiver") await ctx.author.add_role(role, reason="Rolegiver")
@ -273,7 +282,7 @@ class RolegiverCog(Scale):
component.disabled = True component.disabled = True
await message.edit(components=components) await message.edit(components=components)
@slash_command(name="role", sub_cmd_name="remove", sub_cmd_description="Remove a role") @role.subcommand(sub_cmd_name="remove", sub_cmd_description="Remove a role")
@cooldown(bucket=Buckets.USER, rate=1, interval=10) @cooldown(bucket=Buckets.USER, rate=1, interval=10)
async def _role_remove(self, ctx: InteractionContext) -> None: async def _role_remove(self, ctx: InteractionContext) -> None:
user_roles = ctx.author.roles user_roles = ctx.author.roles
@ -352,8 +361,8 @@ class RolegiverCog(Scale):
component.disabled = True component.disabled = True
await message.edit(components=components) await message.edit(components=components)
@slash_command( @rolegiver.subcommand(
name="rolegiver", sub_cmd_name="cleanup", description="Removed deleted roles from rolegiver" sub_cmd_name="cleanup", sub_cmd_description="Removed deleted roles from rolegiver"
) )
@check(admin_or_permissions(Permissions.MANAGE_GUILD)) @check(admin_or_permissions(Permissions.MANAGE_GUILD))
async def _rolegiver_cleanup(self, ctx: InteractionContext) -> None: async def _rolegiver_cleanup(self, ctx: InteractionContext) -> None:
@ -364,11 +373,11 @@ class RolegiverCog(Scale):
for role_id in setting.roles: for role_id in setting.roles:
if role_id not in guild_role_ids: if role_id not in guild_role_ids:
setting.roles.remove(role_id) setting.roles.remove(role_id)
setting.save() await setting.commit()
await ctx.send("Rolegiver cleanup finished") await ctx.send("Rolegiver cleanup finished")
def setup(bot: Snake) -> None: def setup(bot: Snake) -> None:
"""Add RolegiverCog to J.A.R.V.I.S.""" """Add RolegiverCog to JARVIS"""
RolegiverCog(bot) RolegiverCog(bot)

View file

@ -1,25 +1,33 @@
"""J.A.R.V.I.S. Settings Management Cog.""" """JARVIS Settings Management Cog."""
import asyncio
import logging
from typing import Any from typing import Any
from dis_snek import InteractionContext, Scale, Snake
from dis_snek.models.discord.channel import GuildText
from dis_snek.models.discord.components import ActionRow, Button, ButtonStyles
from dis_snek.models.discord.embed import EmbedField
from dis_snek.models.discord.enums import Permissions
from dis_snek.models.discord.role import Role
from dis_snek.models.snek.application_commands import (
OptionTypes,
SlashCommand,
slash_option,
)
from dis_snek.models.snek.command import check from dis_snek.models.snek.command import check
from discord import Role, TextChannel
from discord.ext import commands
from discord.utils import find
from discord_slash import SlashContext, cog_ext
from discord_slash.utils.manage_commands import create_option
from jarvis_core.db import q from jarvis_core.db import q
from jarvis_core.db.models import Setting
from jarvis.db.models import Setting
from jarvis.utils import build_embed from jarvis.utils import build_embed
from jarvis.utils.field import Field
from jarvis.utils.permissions import admin_or_permissions from jarvis.utils.permissions import admin_or_permissions
class SettingsCog(commands.Cog): class SettingsCog(Scale):
"""J.A.R.V.I.S. Settings Management Cog.""" """JARVIS Settings Management Cog."""
def __init__(self, bot: commands.Bot): def __init__(self, bot: Snake):
self.bot = bot self.bot = bot
self.logger = logging.getLogger(__name__)
async def update_settings(self, setting: str, value: Any, guild: int) -> bool: async def update_settings(self, setting: str, value: Any, guild: int) -> bool:
"""Update a guild setting.""" """Update a guild setting."""
@ -27,7 +35,7 @@ class SettingsCog(commands.Cog):
if not existing: if not existing:
existing = Setting(setting=setting, guild=guild, value=value) existing = Setting(setting=setting, guild=guild, value=value)
existing.value = value existing.value = value
updated = existing.save() updated = await existing.commit()
return updated is not None return updated is not None
@ -38,204 +46,177 @@ class SettingsCog(commands.Cog):
return await existing.delete() return await existing.delete()
return False return False
@cog_ext.cog_subcommand( settings = SlashCommand(name="settings", description="Control guild settings")
base="settings", set_ = settings.group(name="set", description="Set a setting")
subcommand_group="set", unset = settings.group(name="unset", description="Unset a setting")
name="modlog",
description="Set modlog channel", @set_.subcommand(
choices=[ sub_cmd_name="modlog",
create_option( sub_cmd_description="Set Moglod channel",
name="channel",
description="Modlog channel",
opt_type=7,
required=True,
)
],
) )
@check(admin_or_permissions(manage_guild=True)) @slash_option(
async def _set_modlog(self, ctx: SlashContext, channel: TextChannel) -> None: name="channel", description="ModLog Channel", opt_type=OptionTypes.CHANNEL, required=True
if not isinstance(channel, TextChannel): )
await ctx.send("Channel must be a TextChannel", ephemeral=True) @check(admin_or_permissions(Permissions.MANAGE_GUILD))
async def _set_modlog(self, ctx: InteractionContext, channel: GuildText) -> None:
if not isinstance(channel, GuildText):
await ctx.send("Channel must be a GuildText", ephemeral=True)
return return
self.update_settings("modlog", channel.id, ctx.guild.id) await self.update_settings("modlog", channel.id, ctx.guild.id)
await ctx.send(f"Settings applied. New modlog channel is {channel.mention}") await ctx.send(f"Settings applied. New modlog channel is {channel.mention}")
@cog_ext.cog_subcommand( @set_.subcommand(
base="settings", sub_cmd_name="activitylog",
subcommand_group="set", sub_cmd_description="Set Activitylog channel",
name="activitylog",
description="Set activitylog channel",
choices=[
create_option(
name="channel",
description="Activitylog channel",
opt_type=7,
required=True,
)
],
) )
@check(admin_or_permissions(manage_guild=True)) @slash_option(
async def _set_activitylog(self, ctx: SlashContext, channel: TextChannel) -> None: name="channel",
if not isinstance(channel, TextChannel): description="Activitylog Channel",
await ctx.send("Channel must be a TextChannel", ephemeral=True) opt_type=OptionTypes.CHANNEL,
required=True,
)
@check(admin_or_permissions(Permissions.MANAGE_GUILD))
async def _set_activitylog(self, ctx: InteractionContext, channel: GuildText) -> None:
if not isinstance(channel, GuildText):
await ctx.send("Channel must be a GuildText", ephemeral=True)
return return
self.update_settings("activitylog", channel.id, ctx.guild.id) await self.update_settings("activitylog", channel.id, ctx.guild.id)
await ctx.send(f"Settings applied. New activitylog channel is {channel.mention}") await ctx.send(f"Settings applied. New activitylog channel is {channel.mention}")
@cog_ext.cog_subcommand( @set_.subcommand(sub_cmd_name="massmention", sub_cmd_description="Set massmention output")
base="settings", @slash_option(
subcommand_group="set", name="amount",
name="massmention", description="Amount of mentions (0 to disable)",
description="Set massmention amount", opt_type=OptionTypes.INTEGER,
choices=[ required=True,
create_option(
name="amount",
description="Amount of mentions (0 to disable)",
opt_type=4,
required=True,
)
],
) )
@check(admin_or_permissions(manage_guild=True)) @check(admin_or_permissions(Permissions.MANAGE_GUILD))
async def _set_massmention(self, ctx: SlashContext, amount: int) -> None: async def _set_massmention(self, ctx: InteractionContext, amount: int) -> None:
await ctx.defer() await ctx.defer()
self.update_settings("massmention", amount, ctx.guild.id) await self.update_settings("massmention", amount, ctx.guild.id)
await ctx.send(f"Settings applied. New massmention limit is {amount}") await ctx.send(f"Settings applied. New massmention limit is {amount}")
@cog_ext.cog_subcommand( @set_.subcommand(sub_cmd_name="verified", sub_cmd_description="Set verified role")
base="settings", @slash_option(
subcommand_group="set", name="role", description="Verified role", opt_type=OptionTypes.ROLE, required=True
name="verified",
description="Set verified role",
choices=[
create_option(
name="role",
description="verified role",
opt_type=8,
required=True,
)
],
) )
@check(admin_or_permissions(manage_guild=True)) @check(admin_or_permissions(Permissions.MANAGE_GUILD))
async def _set_verified(self, ctx: SlashContext, role: Role) -> None: async def _set_verified(self, ctx: InteractionContext, role: Role) -> None:
if role.id == ctx.guild.id:
await ctx.send("Cannot set verified to `@everyone`", ephemeral=True)
return
await ctx.defer() await ctx.defer()
self.update_settings("verified", role.id, ctx.guild.id) await self.update_settings("verified", role.id, ctx.guild.id)
await ctx.send(f"Settings applied. New verified role is `{role.name}`") await ctx.send(f"Settings applied. New verified role is `{role.name}`")
@cog_ext.cog_subcommand( @set_.subcommand(sub_cmd_name="unverified", sub_cmd_description="Set unverified role")
base="settings", @slash_option(
subcommand_group="set", name="role", description="Unverified role", opt_type=OptionTypes.ROLE, required=True
name="unverified",
description="Set unverified role",
choices=[
create_option(
name="role",
description="Unverified role",
opt_type=8,
required=True,
)
],
) )
@check(admin_or_permissions(manage_guild=True)) @check(admin_or_permissions(Permissions.MANAGE_GUILD))
async def _set_unverified(self, ctx: SlashContext, role: Role) -> None: async def _set_unverified(self, ctx: InteractionContext, role: Role) -> None:
if role.id == ctx.guild.id:
await ctx.send("Cannot set unverified to `@everyone`", ephemeral=True)
return
await ctx.defer() await ctx.defer()
self.update_settings("unverified", role.id, ctx.guild.id) await self.update_settings("unverified", role.id, ctx.guild.id)
await ctx.send(f"Settings applied. New unverified role is `{role.name}`") await ctx.send(f"Settings applied. New unverified role is `{role.name}`")
@cog_ext.cog_subcommand( @set_.subcommand(
base="settings", sub_cmd_name="noinvite", sub_cmd_description="Set if invite deletion should happen"
subcommand_group="set",
name="noinvite",
description="Set if invite deletion should happen",
choices=[
create_option(
name="active",
description="Active?",
opt_type=4,
required=True,
)
],
) )
@check(admin_or_permissions(manage_guild=True)) @slash_option(name="active", description="Active?", opt_type=OptionTypes.BOOLEAN, required=True)
async def _set_invitedel(self, ctx: SlashContext, active: int) -> None: @check(admin_or_permissions(Permissions.MANAGE_GUILD))
async def _set_invitedel(self, ctx: InteractionContext, active: bool) -> None:
await ctx.defer() await ctx.defer()
self.update_settings("noinvite", bool(active), ctx.guild.id) await self.update_settings("noinvite", active, ctx.guild.id)
await ctx.send(f"Settings applied. Automatic invite active: {bool(active)}") await ctx.send(f"Settings applied. Automatic invite active: {active}")
@cog_ext.cog_subcommand( @set_.subcommand(sub_cmd_name="notify", sub_cmd_description="Notify users of admin action?")
base="settings", @slash_option(name="active", description="Notify?", opt_type=OptionTypes.BOOLEAN, required=True)
subcommand_group="unset", @check(admin_or_permissions(Permissions.MANAGE_GUILD))
name="modlog", async def _set_notify(self, ctx: InteractionContext, active: bool) -> None:
description="Unset modlog channel",
)
@check(admin_or_permissions(manage_guild=True))
async def _unset_modlog(self, ctx: SlashContext) -> None:
self.delete_settings("modlog", ctx.guild.id)
await ctx.send("Setting removed.")
@cog_ext.cog_subcommand(
base="settings",
subcommand_group="unset",
name="activitylog",
description="Unset activitylog channel",
)
@check(admin_or_permissions(manage_guild=True))
async def _unset_activitylog(self, ctx: SlashContext) -> None:
self.delete_settings("activitylog", ctx.guild.id)
await ctx.send("Setting removed.")
@cog_ext.cog_subcommand(
base="settings",
subcommand_group="unset",
name="massmention",
description="Unet massmention amount",
)
@check(admin_or_permissions(manage_guild=True))
async def _massmention(self, ctx: SlashContext) -> None:
await ctx.defer() await ctx.defer()
self.delete_settings("massmention", ctx.guild.id) await self.update_settings("notify", active, ctx.guild.id)
await ctx.send("Setting removed.") await ctx.send(f"Settings applied. Notifications active: {active}")
@cog_ext.cog_subcommand( # Unset
base="settings", @unset.subcommand(
subcommand_group="unset", sub_cmd_name="modlog",
name="verified", sub_cmd_description="Unset Modlog channel",
description="Unset verified role",
) )
@check(admin_or_permissions(manage_guild=True)) @check(admin_or_permissions(Permissions.MANAGE_GUILD))
async def _verified(self, ctx: SlashContext) -> None: async def _unset_modlog(self, ctx: InteractionContext) -> None:
await ctx.defer() await ctx.defer()
self.delete_settings("verified", ctx.guild.id) await self.delete_settings("modlog", ctx.guild.id)
await ctx.send("Setting removed.") await ctx.send("Setting `modlog` unset")
@cog_ext.cog_subcommand( @unset.subcommand(
base="settings", sub_cmd_name="activitylog",
subcommand_group="unset", sub_cmd_description="Unset Activitylog channel",
name="unverified",
description="Unset unverified role",
) )
@check(admin_or_permissions(manage_guild=True)) @check(admin_or_permissions(Permissions.MANAGE_GUILD))
async def _unverified(self, ctx: SlashContext) -> None: async def _unset_activitylog(self, ctx: InteractionContext) -> None:
await ctx.defer() await ctx.defer()
self.delete_settings("unverified", ctx.guild.id) await self.delete_settings("activitylog", ctx.guild.id)
await ctx.send("Setting removed.") await ctx.send("Setting `activitylog` unset")
@cog_ext.cog_subcommand(base="settings", name="view", description="View settings") @unset.subcommand(sub_cmd_name="massmention", sub_cmd_description="Unset massmention output")
@check(admin_or_permissions(manage_guild=True)) @check(admin_or_permissions(Permissions.MANAGE_GUILD))
async def _view(self, ctx: SlashContext) -> None: async def _unset_massmention(self, ctx: InteractionContext) -> None:
settings = Setting.objects(guild=ctx.guild.id) await ctx.defer()
await self.delete_settings("massmention", ctx.guild.id)
await ctx.send("Setting `massmention` unset")
@unset.subcommand(sub_cmd_name="verified", sub_cmd_description="Unset verified role")
@slash_option(
name="role", description="Verified role", opt_type=OptionTypes.ROLE, required=True
)
@check(admin_or_permissions(Permissions.MANAGE_GUILD))
async def _unset_verified(self, ctx: InteractionContext) -> None:
await ctx.defer()
await self.delete_settings("verified", ctx.guild.id)
await ctx.send("Setting `massmention` unset")
@unset.subcommand(sub_cmd_name="unverified", sub_cmd_description="Unset unverified role")
@check(admin_or_permissions(Permissions.MANAGE_GUILD))
async def _unset_unverified(self, ctx: InteractionContext) -> None:
await ctx.defer()
await self.delete_settings("unverified", ctx.guild.id)
await ctx.send("Setting `unverified` unset")
@unset.subcommand(
sub_cmd_name="noinvite", sub_cmd_description="Unset if invite deletion should happen"
)
@check(admin_or_permissions(Permissions.MANAGE_GUILD))
async def _unset_invitedel(self, ctx: InteractionContext, active: bool) -> None:
await ctx.defer()
await self.delete_settings("noinvite", ctx.guild.id)
await ctx.send(f"Setting `{active}` unset")
@unset.subcommand(sub_cmd_name="notify", sub_cmd_description="Unset admin action notifications")
@check(admin_or_permissions(Permissions.MANAGE_GUILD))
async def _unset_notify(self, ctx: InteractionContext) -> None:
await ctx.defer()
await self.delete_settings("notify", ctx.guild.id)
await ctx.send("Setting `notify` unset")
@settings.subcommand(sub_cmd_name="view", sub_cmd_description="View settings")
@check(admin_or_permissions(Permissions.MANAGE_GUILD))
async def _view(self, ctx: InteractionContext) -> None:
settings = Setting.find(q(guild=ctx.guild.id))
fields = [] fields = []
for setting in settings: async for setting in settings:
value = setting.value value = setting.value
if setting.setting in ["unverified", "verified", "mute"]: if setting.setting in ["unverified", "verified", "mute"]:
value = find(lambda x: x.id == value, ctx.guild.roles) value = await ctx.guild.fetch_role(value)
if value: if value:
value = value.mention value = value.mention
else: else:
value = "||`[redacted]`||" value = "||`[redacted]`||"
elif setting.setting in ["activitylog", "modlog"]: elif setting.setting in ["activitylog", "modlog"]:
value = find(lambda x: x.id == value, ctx.guild.text_channels) value = await ctx.guild.fetch_channel(value)
if value: if value:
value = value.mention value = value.mention
else: else:
@ -243,24 +224,51 @@ class SettingsCog(commands.Cog):
elif setting.setting == "rolegiver": elif setting.setting == "rolegiver":
value = "" value = ""
for _role in setting.value: for _role in setting.value:
nvalue = find(lambda x: x.id == value, ctx.guild.roles) nvalue = await ctx.guild.fetch_role(_role)
if value: if nvalue:
value += "\n" + nvalue.mention value += "\n" + nvalue.mention
else: else:
value += "\n||`[redacted]`||" value += "\n||`[redacted]`||"
fields.append(Field(name=setting.setting, value=value or "N/A")) fields.append(EmbedField(name=setting.setting, value=str(value) or "N/A", inline=False))
embed = build_embed(title="Current Settings", description="", fields=fields) embed = build_embed(title="Current Settings", description="", fields=fields)
await ctx.send(embed=embed) await ctx.send(embed=embed)
@cog_ext.cog_subcommand(base="settings", name="clear", description="Clear all settings") @settings.subcommand(sub_cmd_name="clear", sub_cmd_description="Clear all settings")
@check(admin_or_permissions(manage_guild=True)) @check(admin_or_permissions(Permissions.MANAGE_GUILD))
async def _clear(self, ctx: SlashContext) -> None: async def _clear(self, ctx: InteractionContext) -> None:
deleted = Setting.objects(guild=ctx.guild.id).delete() components = [
await ctx.send(f"Guild settings cleared: `{deleted is not None}`") ActionRow(
Button(style=ButtonStyles.RED, emoji="✖️", custom_id="no"),
Button(style=ButtonStyles.GREEN, emoji="✔️", custom_id="yes"),
)
]
message = await ctx.send("***Are you sure?***", 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,
)
content = "***Are you sure?***"
if context.context.custom_id == "yes":
async for setting in Setting.find(q(guild=ctx.guild.id)):
await setting.delete()
content = "Guild settings cleared"
else:
content = "Guild settings not cleared"
for row in components:
for component in row.components:
component.disabled = True
await context.context.edit_origin(content=content, components=components)
except asyncio.TimeoutError:
for row in components:
for component in row.components:
component.disabled = True
await message.edit(content="Guild settings not cleared", components=components)
def setup(bot: commands.Bot) -> None: def setup(bot: Snake) -> None:
"""Add SettingsCog to J.A.R.V.I.S.""" """Add SettingsCog to JARVIS"""
SettingsCog(bot) SettingsCog(bot)

View file

@ -1,14 +1,15 @@
"""J.A.R.V.I.S. Starboard Cog.""" """JARVIS Starboard Cog."""
import logging
from dis_snek import InteractionContext, Permissions, Scale, Snake from dis_snek import InteractionContext, Permissions, Scale, Snake
from dis_snek.client.utils.misc_utils import find
from dis_snek.models.discord.channel import GuildText from dis_snek.models.discord.channel import GuildText
from dis_snek.models.discord.components import ActionRow, Select, SelectOption from dis_snek.models.discord.components import ActionRow, Select, SelectOption
from dis_snek.models.discord.message import Message from dis_snek.models.discord.message import Message
from dis_snek.models.snek.application_commands import ( from dis_snek.models.snek.application_commands import (
CommandTypes, CommandTypes,
OptionTypes, OptionTypes,
SlashCommand,
context_menu, context_menu,
slash_command,
slash_option, slash_option,
) )
from dis_snek.models.snek.command import check from dis_snek.models.snek.command import check
@ -28,12 +29,18 @@ supported_images = [
class StarboardCog(Scale): class StarboardCog(Scale):
"""J.A.R.V.I.S. Starboard Cog.""" """JARVIS Starboard Cog."""
def __init__(self, bot: Snake): def __init__(self, bot: Snake):
self.bot = bot self.bot = bot
self.logger = logging.getLogger(__name__)
@slash_command(name="starboard", sub_cmd_name="list", sub_cmd_description="List all starboards") starboard = SlashCommand(name="starboard", description="Extra pins! Manage starboards")
@starboard.subcommand(
sub_cmd_name="list",
sub_cmd_description="List all starboards",
)
@check(admin_or_permissions(Permissions.MANAGE_GUILD)) @check(admin_or_permissions(Permissions.MANAGE_GUILD))
async def _list(self, ctx: InteractionContext) -> None: async def _list(self, ctx: InteractionContext) -> None:
starboards = await Starboard.find(q(guild=ctx.guild.id)).to_list(None) starboards = await Starboard.find(q(guild=ctx.guild.id)).to_list(None)
@ -45,9 +52,7 @@ class StarboardCog(Scale):
else: else:
await ctx.send("No Starboards available.") await ctx.send("No Starboards available.")
@slash_command( @starboard.subcommand(sub_cmd_name="create", sub_cmd_description="Create a starboard")
name="starboard", sub_cmd_name="create", sub_cmd_description="Create a starboard"
)
@slash_option( @slash_option(
name="channel", name="channel",
description="Starboard channel", description="Starboard channel",
@ -83,9 +88,7 @@ class StarboardCog(Scale):
).commit() ).commit()
await ctx.send(f"Starboard created. Check it out at {channel.mention}.") await ctx.send(f"Starboard created. Check it out at {channel.mention}.")
@slash_command( @starboard.subcommand(sub_cmd_name="delete", sub_cmd_description="Delete a starboard")
name="starboard", sub_cmd_name="delete", sub_cmd_description="Delete a starboard"
)
@slash_option( @slash_option(
name="channel", name="channel",
description="Starboard channel", description="Starboard channel",
@ -126,8 +129,27 @@ class StarboardCog(Scale):
return return
channel_list = [] channel_list = []
to_delete = []
for starboard in starboards: for starboard in starboards:
channel_list.append(find(lambda x: x.id == starboard.channel, ctx.guild.channels)) c = await ctx.guild.fetch_channel(starboard.channel)
if c and isinstance(c, GuildText):
channel_list.append(c)
else:
self.logger.warn(
f"Starboard {starboard.channel} no longer valid in {ctx.guild.name}"
)
to_delete.append(starboard)
for starboard in to_delete:
try:
await starboard.delete()
except Exception:
self.logger.debug("Ignoring deletion error")
select_channels = []
for idx, x in enumerate(channel_list):
if x:
select_channels.append(SelectOption(label=x.name, value=str(idx)))
select_channels = [ select_channels = [
SelectOption(label=x.name, value=str(idx)) for idx, x in enumerate(channel_list) SelectOption(label=x.name, value=str(idx)) for idx, x in enumerate(channel_list)
@ -221,7 +243,15 @@ class StarboardCog(Scale):
async def _star_message(self, ctx: InteractionContext) -> None: async def _star_message(self, ctx: InteractionContext) -> None:
await self._star_add(ctx, message=str(ctx.target_id)) await self._star_add(ctx, message=str(ctx.target_id))
@slash_command(name="star", sub_cmd_name="add", description="Star a message") star = SlashCommand(
name="star",
description="Manage stars",
)
@star.subcommand(
sub_cmd_name="add",
sub_cmd_description="Star a message",
)
@slash_option( @slash_option(
name="message", description="Message to star", opt_type=OptionTypes.STRING, required=True name="message", description="Message to star", opt_type=OptionTypes.STRING, required=True
) )
@ -237,7 +267,7 @@ class StarboardCog(Scale):
) -> None: ) -> None:
await self._star_add(ctx, message, channel) await self._star_add(ctx, message, channel)
@slash_command(name="star", sub_cmd_name="delete", description="Delete a starred message") @star.subcommand(sub_cmd_name="delete", sub_cmd_description="Delete a starred message")
@slash_option( @slash_option(
name="id", description="Star ID to delete", opt_type=OptionTypes.INTEGER, required=True name="id", description="Star ID to delete", opt_type=OptionTypes.INTEGER, required=True
) )
@ -289,5 +319,5 @@ class StarboardCog(Scale):
def setup(bot: Snake) -> None: def setup(bot: Snake) -> None:
"""Add StarboardCog to J.A.R.V.I.S.""" """Add StarboardCog to JARVIS"""
StarboardCog(bot) StarboardCog(bot)

View file

@ -1,5 +1,6 @@
"""J.A.R.V.I.S. Twitter Cog.""" """JARVIS Twitter Cog."""
import asyncio import asyncio
import logging
import tweepy import tweepy
from dis_snek import InteractionContext, Permissions, Scale, Snake from dis_snek import InteractionContext, Permissions, Scale, Snake
@ -8,24 +9,24 @@ from dis_snek.models.discord.channel import GuildText
from dis_snek.models.discord.components import ActionRow, Select, SelectOption from dis_snek.models.discord.components import ActionRow, Select, SelectOption
from dis_snek.models.snek.application_commands import ( from dis_snek.models.snek.application_commands import (
OptionTypes, OptionTypes,
SlashCommandChoice, SlashCommand,
slash_command,
slash_option, slash_option,
) )
from dis_snek.models.snek.command import check from dis_snek.models.snek.command import check
from jarvis_core.db import q from jarvis_core.db import q
from jarvis_core.db.models import TwitterAccount, TwitterFollow from jarvis_core.db.models import TwitterAccount, TwitterFollow
from jarvis import jconfig from jarvis.config import JarvisConfig
from jarvis.utils.permissions import admin_or_permissions from jarvis.utils.permissions import admin_or_permissions
class TwitterCog(Scale): class TwitterCog(Scale):
"""J.A.R.V.I.S. Twitter Cog.""" """JARVIS Twitter Cog."""
def __init__(self, bot: Snake): def __init__(self, bot: Snake):
self.bot = bot self.bot = bot
config = jconfig self.logger = logging.getLogger(__name__)
config = JarvisConfig.from_yaml()
auth = tweepy.AppAuthHandler( auth = tweepy.AppAuthHandler(
config.twitter["consumer_key"], config.twitter["consumer_secret"] config.twitter["consumer_key"], config.twitter["consumer_secret"]
) )
@ -33,7 +34,15 @@ class TwitterCog(Scale):
self._guild_cache = {} self._guild_cache = {}
self._channel_cache = {} self._channel_cache = {}
@slash_command(name="twitter", sub_cmd_name="follow", description="Follow a Twitter acount") twitter = SlashCommand(
name="twitter",
description="Manage Twitter follows",
)
@twitter.subcommand(
sub_cmd_name="follow",
sub_cmd_description="Follow a Twitter acount",
)
@slash_option( @slash_option(
name="handle", description="Twitter account", opt_type=OptionTypes.STRING, required=True name="handle", description="Twitter account", opt_type=OptionTypes.STRING, required=True
) )
@ -46,19 +55,14 @@ class TwitterCog(Scale):
@slash_option( @slash_option(
name="retweets", name="retweets",
description="Mirror re-tweets?", description="Mirror re-tweets?",
opt_type=OptionTypes.STRING, opt_type=OptionTypes.BOOLEAN,
required=False, required=False,
choices=[
SlashCommandChoice(name="Yes", value="Yes"),
SlashCommandChoice(name="No", value="No"),
],
) )
@check(admin_or_permissions(Permissions.MANAGE_GUILD)) @check(admin_or_permissions(Permissions.MANAGE_GUILD))
async def _twitter_follow( async def _twitter_follow(
self, ctx: InteractionContext, handle: str, channel: GuildText, retweets: str = "Yes" self, ctx: InteractionContext, handle: str, channel: GuildText, retweets: bool = True
) -> None: ) -> None:
handle = handle.lower() handle = handle.lower()
retweets = retweets == "Yes"
if len(handle) > 15: if len(handle) > 15:
await ctx.send("Invalid Twitter handle", ephemeral=True) await ctx.send("Invalid Twitter handle", ephemeral=True)
return return
@ -107,7 +111,7 @@ class TwitterCog(Scale):
await ctx.send(f"Now following `@{handle}` in {channel.mention}") await ctx.send(f"Now following `@{handle}` in {channel.mention}")
@slash_command(name="twitter", sub_cmd_name="unfollow", description="Unfollow Twitter accounts") @twitter.subcommand(sub_cmd_name="unfollow", sub_cmd_description="Unfollow Twitter accounts")
@check(admin_or_permissions(Permissions.MANAGE_GUILD)) @check(admin_or_permissions(Permissions.MANAGE_GUILD))
async def _twitter_unfollow(self, ctx: InteractionContext) -> None: async def _twitter_unfollow(self, ctx: InteractionContext) -> None:
t = TwitterFollow.find(q(guild=ctx.guild.id)) t = TwitterFollow.find(q(guild=ctx.guild.id))
@ -146,7 +150,10 @@ class TwitterCog(Scale):
) )
for to_delete in context.context.values: for to_delete in context.context.values:
follow = get(twitters, guild=ctx.guild.id, twitter_id=int(to_delete)) follow = get(twitters, guild=ctx.guild.id, twitter_id=int(to_delete))
await follow.delete() try:
await follow.delete()
except Exception:
self.logger.debug("Ignoring deletion error")
for row in components: for row in components:
for component in row.components: for component in row.components:
component.disabled = True component.disabled = True
@ -161,22 +168,18 @@ class TwitterCog(Scale):
component.disabled = True component.disabled = True
await message.edit(components=components) await message.edit(components=components)
@slash_command( @twitter.subcommand(
name="twitter", sub_cmd_name="retweets", description="Modify followed Twitter accounts" sub_cmd_name="retweets",
sub_cmd_description="Modify followed Twitter accounts",
) )
@slash_option( @slash_option(
name="retweets", name="retweets",
description="Mirror re-tweets?", description="Mirror re-tweets?",
opt_type=OptionTypes.STRING, opt_type=OptionTypes.BOOLEAN,
required=False, required=False,
choices=[
SlashCommandChoice(name="Yes", value="Yes"),
SlashCommandChoice(name="No", value="No"),
],
) )
@check(admin_or_permissions(Permissions.MANAGE_GUILD)) @check(admin_or_permissions(Permissions.MANAGE_GUILD))
async def _twitter_modify(self, ctx: InteractionContext, retweets: str) -> None: async def _twitter_modify(self, ctx: InteractionContext, retweets: bool = True) -> None:
retweets = retweets == "Yes"
t = TwitterFollow.find(q(guild=ctx.guild.id)) t = TwitterFollow.find(q(guild=ctx.guild.id))
twitters = [] twitters = []
async for twitter in t: async for twitter in t:
@ -241,5 +244,5 @@ class TwitterCog(Scale):
def setup(bot: Snake) -> None: def setup(bot: Snake) -> None:
"""Add TwitterCog to J.A.R.V.I.S.""" """Add TwitterCog to JARVIS"""
TwitterCog(bot) TwitterCog(bot)

View file

@ -1,30 +1,35 @@
"""J.A.R.V.I.S. Utility Cog.""" """JARVIS Utility Cog."""
import logging
import platform import platform
import re import re
import secrets import secrets
import string import string
from datetime import timezone
from io import BytesIO from io import BytesIO
import numpy as np import numpy as np
from dateparser import parse
from dis_snek import InteractionContext, Scale, Snake, const from dis_snek import InteractionContext, Scale, Snake, const
from dis_snek.models.discord.channel import GuildCategory, GuildText, GuildVoice from dis_snek.models.discord.channel import GuildCategory, GuildText, GuildVoice
from dis_snek.models.discord.embed import EmbedField from dis_snek.models.discord.embed import EmbedField
from dis_snek.models.discord.file import File from dis_snek.models.discord.file import File
from dis_snek.models.discord.guild import Guild from dis_snek.models.discord.guild import Guild
from dis_snek.models.discord.role import Role from dis_snek.models.discord.role import Role
from dis_snek.models.discord.user import User from dis_snek.models.discord.user import Member, User
from dis_snek.models.snek.application_commands import ( from dis_snek.models.snek.application_commands import (
CommandTypes,
OptionTypes, OptionTypes,
SlashCommandChoice, SlashCommandChoice,
context_menu,
slash_command, slash_command,
slash_option, slash_option,
) )
from dis_snek.models.snek.command import cooldown from dis_snek.models.snek.command import cooldown
from dis_snek.models.snek.cooldowns import Buckets from dis_snek.models.snek.cooldowns import Buckets
from PIL import Image from PIL import Image
from tzlocal import get_localzone
import jarvis import jarvis
from jarvis.config import get_config
from jarvis.data import pigpen from jarvis.data import pigpen
from jarvis.data.robotcamo import emotes, hk, names from jarvis.data.robotcamo import emotes, hk, names
from jarvis.utils import build_embed, get_repo_hash from jarvis.utils import build_embed, get_repo_hash
@ -34,26 +39,32 @@ JARVIS_LOGO = Image.open("jarvis_small.png").convert("RGBA")
class UtilCog(Scale): class UtilCog(Scale):
""" """
Utility functions for J.A.R.V.I.S. Utility functions for JARVIS
Mostly system utility functions, but may change over time Mostly system utility functions, but may change over time
""" """
def __init__(self, bot: Snake): def __init__(self, bot: Snake):
self.bot = bot self.bot = bot
self.config = get_config() self.logger = logging.getLogger(__name__)
@slash_command(name="status", description="Retrieve J.A.R.V.I.S. status") @slash_command(name="status", description="Retrieve JARVIS status")
@cooldown(bucket=Buckets.CHANNEL, rate=1, interval=30) @cooldown(bucket=Buckets.CHANNEL, rate=1, interval=30)
async def _status(self, ctx: InteractionContext) -> None: async def _status(self, ctx: InteractionContext) -> None:
title = "J.A.R.V.I.S. Status" title = "JARVIS Status"
desc = "All systems online" desc = f"All systems online\nConnected to **{len(self.bot.guilds)}** guilds"
color = "#3498db" color = "#3498db"
fields = [] fields = []
fields.append(EmbedField(name="dis-snek", value=const.__version__)) fields.append(EmbedField(name="dis-snek", value=const.__version__))
fields.append(EmbedField(name="Version", value=jarvis.__version__, inline=False)) fields.append(EmbedField(name="Version", value=jarvis.__version__, inline=False))
fields.append(EmbedField(name="Git Hash", value=get_repo_hash()[:7], inline=False)) fields.append(EmbedField(name="Git Hash", value=get_repo_hash()[:7], inline=False))
num_domains = len(self.bot.phishing_domains)
fields.append(
EmbedField(
name="Phishing Protection", value=f"Detecting {num_domains} phishing domains"
)
)
embed = build_embed(title=title, description=desc, fields=fields, color=color) embed = build_embed(title=title, description=desc, fields=fields, color=color)
await ctx.send(embed=embed) await ctx.send(embed=embed)
@ -96,6 +107,8 @@ class UtilCog(Scale):
to_send += f":{names[id]}:" to_send += f":{names[id]}:"
if len(to_send) > 2000: if len(to_send) > 2000:
await ctx.send("Too long.", ephemeral=True) await ctx.send("Too long.", ephemeral=True)
elif len(to_send) == 0:
await ctx.send("No valid text found", ephemeral=True)
else: else:
await ctx.send(to_send) await ctx.send(to_send)
@ -111,7 +124,10 @@ class UtilCog(Scale):
if not user: if not user:
user = ctx.author user = ctx.author
avatar = user.display_avatar.url avatar = user.avatar.url
if isinstance(user, Member):
avatar = user.display_avatar.url
embed = build_embed(title="Avatar", description="", fields=[], color="#00FFEE") embed = build_embed(title="Avatar", description="", fields=[], color="#00FFEE")
embed.set_image(url=avatar) embed.set_image(url=avatar)
embed.set_author(name=f"{user.username}#{user.discriminator}", icon_url=avatar) embed.set_author(name=f"{user.username}#{user.discriminator}", icon_url=avatar)
@ -130,7 +146,7 @@ class UtilCog(Scale):
async def _roleinfo(self, ctx: InteractionContext, role: Role) -> None: async def _roleinfo(self, ctx: InteractionContext, role: Role) -> None:
fields = [ fields = [
EmbedField(name="ID", value=str(role.id), inline=True), EmbedField(name="ID", value=str(role.id), inline=True),
EmbedField(name="Name", value=role.name, inline=True), EmbedField(name="Name", value=role.mention, inline=True),
EmbedField(name="Color", value=str(role.color.hex), inline=True), EmbedField(name="Color", value=str(role.color.hex), inline=True),
EmbedField(name="Mention", value=f"`{role.mention}`", inline=True), EmbedField(name="Mention", value=f"`{role.mention}`", inline=True),
EmbedField(name="Hoisted", value="Yes" if role.hoist else "No", inline=True), EmbedField(name="Hoisted", value="Yes" if role.hoist else "No", inline=True),
@ -164,19 +180,13 @@ class UtilCog(Scale):
await ctx.send(embed=embed, file=color_show) await ctx.send(embed=embed, file=color_show)
@slash_command(
name="userinfo",
description="Get user info",
)
@slash_option(
name="user",
description="User to get info of",
opt_type=OptionTypes.USER,
required=False,
)
async def _userinfo(self, ctx: InteractionContext, user: User = None) -> None: async def _userinfo(self, ctx: InteractionContext, user: User = None) -> None:
await ctx.defer()
if not user: if not user:
user = ctx.author user = ctx.author
if not await ctx.guild.fetch_member(user.id):
await ctx.send("That user isn't in this guild.", ephemeral=True)
return
user_roles = user.roles user_roles = user.roles
if user_roles: if user_roles:
user_roles = sorted(user.roles, key=lambda x: -x.position) user_roles = sorted(user.roles, key=lambda x: -x.position)
@ -215,6 +225,23 @@ class UtilCog(Scale):
await ctx.send(embed=embed) await ctx.send(embed=embed)
@slash_command(
name="userinfo",
description="Get user info",
)
@slash_option(
name="user",
description="User to get info of",
opt_type=OptionTypes.USER,
required=False,
)
async def _userinfo_slsh(self, ctx: InteractionContext, user: User = None) -> None:
await self._userinfo(ctx, user)
@context_menu(name="User Info", context_type=CommandTypes.USER)
async def _userinfo_menu(self, ctx: InteractionContext) -> None:
await self._userinfo(ctx, ctx.target)
@slash_command(name="serverinfo", description="Get server info") @slash_command(name="serverinfo", description="Get server info")
async def _server_info(self, ctx: InteractionContext) -> None: async def _server_info(self, ctx: InteractionContext) -> None:
guild: Guild = ctx.guild guild: Guild = ctx.guild
@ -281,6 +308,7 @@ class UtilCog(Scale):
if length > 256: if length > 256:
await ctx.send("Please limit password to 256 characters", ephemeral=True) await ctx.send("Please limit password to 256 characters", ephemeral=True)
return return
choices = [ choices = [
string.ascii_letters, string.ascii_letters,
string.hexdigits, string.hexdigits,
@ -314,7 +342,39 @@ class UtilCog(Scale):
outp += "`" outp += "`"
await ctx.send(outp[:2000]) await ctx.send(outp[:2000])
@slash_command(
name="timestamp", description="Convert a datetime or timestamp into it's counterpart"
)
@slash_option(
name="string", description="String to convert", opt_type=OptionTypes.STRING, required=True
)
@slash_option(
name="private", description="Respond quietly?", opt_type=OptionTypes.BOOLEAN, required=False
)
async def _timestamp(self, ctx: InteractionContext, string: str, private: bool = False) -> None:
timestamp = parse(string)
if not timestamp:
await ctx.send("Valid time not found, try again", ephemeral=True)
return
if not timestamp.tzinfo:
timestamp = timestamp.replace(tzinfo=get_localzone()).astimezone(tz=timezone.utc)
timestamp_utc = timestamp.astimezone(tz=timezone.utc)
ts = int(timestamp.timestamp())
ts_utc = int(timestamp_utc.timestamp())
fields = [
EmbedField(name="Unix Epoch", value=f"`{ts}`"),
EmbedField(name="Unix Epoch (UTC)", value=f"`{ts_utc}`"),
EmbedField(name="Absolute Time", value=f"<t:{ts_utc}:F>\n`<t:{ts_utc}:F>`"),
EmbedField(name="Relative Time", value=f"<t:{ts_utc}:R>\n`<t:{ts_utc}:R>`"),
EmbedField(name="ISO8601", value=timestamp.isoformat()),
]
embed = build_embed(title="Converted Time", description=f"`{string}`", fields=fields)
await ctx.send(embed=embed, ephemeral=private)
def setup(bot: Snake) -> None: def setup(bot: Snake) -> None:
"""Add UtilCog to J.A.R.V.I.S.""" """Add UtilCog to JARVIS"""
UtilCog(bot) UtilCog(bot)

View file

@ -1,10 +1,11 @@
"""J.A.R.V.I.S. Verify Cog.""" """JARVIS Verify Cog."""
import asyncio import asyncio
import logging
from random import randint from random import randint
from dis_snek import InteractionContext, Scale, Snake from dis_snek import InteractionContext, Scale, Snake
from dis_snek.models.application_commands import slash_command
from dis_snek.models.discord.components import Button, ButtonStyles, spread_to_rows from dis_snek.models.discord.components import Button, ButtonStyles, spread_to_rows
from dis_snek.models.snek.application_commands import slash_command
from dis_snek.models.snek.command import cooldown from dis_snek.models.snek.command import cooldown
from dis_snek.models.snek.cooldowns import Buckets from dis_snek.models.snek.cooldowns import Buckets
from jarvis_core.db import q from jarvis_core.db import q
@ -30,21 +31,22 @@ def create_layout() -> list:
class VerifyCog(Scale): class VerifyCog(Scale):
"""J.A.R.V.I.S. Verify Cog.""" """JARVIS Verify Cog."""
def __init__(self, bot: Snake): def __init__(self, bot: Snake):
self.bot = bot self.bot = bot
self.logger = logging.getLogger(__name__)
@slash_command(name="verify", description="Verify that you've read the rules") @slash_command(name="verify", description="Verify that you've read the rules")
@cooldown(bucket=Buckets.USER, rate=1, interval=15) @cooldown(bucket=Buckets.USER, rate=1, interval=30)
async def _verify(self, ctx: InteractionContext) -> None: async def _verify(self, ctx: InteractionContext) -> None:
await ctx.defer() await ctx.defer()
role = await Setting.find_one(q(guild=ctx.guild.id, setting="verified")) role = await Setting.find_one(q(guild=ctx.guild.id, setting="verified"))
if not role: if not role:
await ctx.send("This guild has not enabled verification", delete_after=5) message = await ctx.send("This guild has not enabled verification", ephemeral=True)
return return
if await ctx.guild.get_role(role.value) in ctx.author.roles: if await ctx.guild.fetch_role(role.value) in ctx.author.roles:
await ctx.send("You are already verified.", delete_after=5) await ctx.send("You are already verified.", ephemeral=True)
return return
components = create_layout() components = create_layout()
message = await ctx.send( message = await ctx.send(
@ -53,39 +55,45 @@ class VerifyCog(Scale):
) )
try: try:
context = await self.bot.wait_for_component( verified = False
messages=message, check=lambda x: ctx.author.id == x.author.id, timeout=30 while not verified:
) response = await self.bot.wait_for_component(
messages=message,
correct = context.context.custom_id.split("||")[-1] == "yes" check=lambda x: ctx.author.id == x.context.author.id,
if correct: timeout=30,
for row in components:
for component in row.components:
component.disabled = True
setting = await Setting.find_one(guild=ctx.guild.id, setting="verified")
role = await ctx.guild.get_role(setting.value)
await ctx.author.add_roles(role, reason="Verification passed")
setting = await Setting.find_one(guild=ctx.guild.id, setting="unverified")
if setting:
role = await ctx.guild.get_role(setting.value)
await ctx.author.remove_roles(role, reason="Verification passed")
await context.context.edit_origin(
content=f"Welcome, {ctx.author.mention}. Please enjoy your stay.",
components=components,
) )
await context.context.message.delete(delay=5)
else: correct = response.context.custom_id.split("||")[-1] == "yes"
await context.context.edit_origin( if correct:
content=( for row in components:
f"{ctx.author.mention}, incorrect. " for component in row.components:
"Please press the button that says `YES`" component.disabled = True
setting = await Setting.find_one(q(guild=ctx.guild.id, setting="verified"))
role = await ctx.guild.fetch_role(setting.value)
await ctx.author.add_role(role, reason="Verification passed")
setting = await Setting.find_one(q(guild=ctx.guild.id, setting="unverified"))
if setting:
role = await ctx.guild.fetch_role(setting.value)
await ctx.author.remove_role(role, reason="Verification passed")
await response.context.edit_origin(
content=f"Welcome, {ctx.author.mention}. Please enjoy your stay.",
components=components,
)
await response.context.message.delete(delay=5)
self.logger.debug(f"User {ctx.author.id} verified successfully")
else:
await response.context.edit_origin(
content=(
f"{ctx.author.mention}, incorrect. "
"Please press the button that says `YES`"
)
) )
)
except asyncio.TimeoutError: except asyncio.TimeoutError:
await message.delete(delay=30) await message.delete(delay=2)
self.logger.debug(f"User {ctx.author.id} failed to verify before timeout")
def setup(bot: Snake) -> None: def setup(bot: Snake) -> None:
"""Add VerifyCog to J.A.R.V.I.S.""" """Add VerifyCog to JARVIS"""
VerifyCog(bot) VerifyCog(bot)

View file

@ -1,4 +1,4 @@
"""Load the config for J.A.R.V.I.S.""" """Load the config for JARVIS"""
import os import os
from jarvis_core.config import Config as CConfig from jarvis_core.config import Config as CConfig
@ -12,7 +12,7 @@ except ImportError:
class JarvisConfig(CConfig): class JarvisConfig(CConfig):
REQUIRED = ["token", "client_id", "mongo", "urls"] REQUIRED = ["token", "mongo", "urls"]
OPTIONAL = { OPTIONAL = {
"sync": False, "sync": False,
"log_level": "WARNING", "log_level": "WARNING",
@ -25,7 +25,7 @@ class JarvisConfig(CConfig):
class Config(object): class Config(object):
"""Config singleton object for J.A.R.V.I.S.""" """Config singleton object for JARVIS"""
def __new__(cls, *args: list, **kwargs: dict): def __new__(cls, *args: list, **kwargs: dict):
"""Get the singleton config, or creates a new one.""" """Get the singleton config, or creates a new one."""
@ -39,8 +39,6 @@ class Config(object):
def init( def init(
self, self,
token: str, token: str,
client_id: str,
logo: str,
mongo: dict, mongo: dict,
urls: dict, urls: dict,
sync: bool = False, sync: bool = False,
@ -53,8 +51,6 @@ class Config(object):
) -> None: ) -> None:
"""Initialize the config object.""" """Initialize the config object."""
self.token = token self.token = token
self.client_id = client_id
self.logo = logo
self.mongo = mongo self.mongo = mongo
self.urls = urls self.urls = urls
self.log_level = log_level self.log_level = log_level

View file

@ -1,5 +1,5 @@
"""J.A.R.V.I.S. Utility Functions.""" """JARVIS Utility Functions."""
from datetime import datetime from datetime import datetime, timezone
from pkgutil import iter_modules from pkgutil import iter_modules
import git import git
@ -23,7 +23,7 @@ def build_embed(
) -> Embed: ) -> Embed:
"""Embed builder utility function.""" """Embed builder utility function."""
if not timestamp: if not timestamp:
timestamp = datetime.now() timestamp = datetime.now(tz=timezone.utc)
embed = Embed( embed = Embed(
title=title, title=title,
description=description, description=description,
@ -65,14 +65,14 @@ def modlog_embed(
def get_extensions(path: str = jarvis.cogs.__path__) -> list: def get_extensions(path: str = jarvis.cogs.__path__) -> list:
"""Get J.A.R.V.I.S. cogs.""" """Get JARVIS cogs."""
config = get_config() config = get_config()
vals = config.cogs or [x.name for x in iter_modules(path)] vals = config.cogs or [x.name for x in iter_modules(path)]
return ["jarvis.cogs.{}".format(x) for x in vals] return ["jarvis.cogs.{}".format(x) for x in vals]
def update() -> int: def update() -> int:
"""J.A.R.V.I.S. update utility.""" """JARVIS update utility."""
repo = git.Repo(".") repo = git.Repo(".")
dirty = repo.is_dirty() dirty = repo.is_dirty()
current_hash = repo.head.object.hexsha current_hash = repo.head.object.hexsha
@ -87,6 +87,6 @@ def update() -> int:
def get_repo_hash() -> str: def get_repo_hash() -> str:
"""J.A.R.V.I.S. current branch hash.""" """JARVIS current branch hash."""
repo = git.Repo(".") repo = git.Repo(".")
return repo.head.object.hexsha return repo.head.object.hexsha

View file

@ -1,35 +0,0 @@
"""Cog wrapper for command caching."""
from datetime import datetime, timedelta
from dis_snek import InteractionContext, Scale, Snake
from dis_snek.client.utils.misc_utils import find
from dis_snek.ext.tasks.task import Task
from dis_snek.ext.tasks.triggers import IntervalTrigger
class CacheCog(Scale):
"""Cog wrapper for command caching."""
def __init__(self, bot: Snake):
self.bot = bot
self.cache = {}
self._expire_interaction.start()
def check_cache(self, ctx: InteractionContext, **kwargs: dict) -> dict:
"""Check the cache."""
if not kwargs:
kwargs = {}
return find(
lambda x: x["command"] == ctx.subcommand_name # noqa: W503
and x["user"] == ctx.author.id # noqa: W503
and x["guild"] == ctx.guild.id # noqa: W503
and all(x[k] == v for k, v in kwargs.items()), # noqa: W503
self.cache.values(),
)
@Task.create(IntervalTrigger(minutes=1))
async def _expire_interaction(self) -> None:
keys = list(self.cache.keys())
for key in keys:
if self.cache[key]["timeout"] <= datetime.utcnow() + timedelta(minutes=1):
del self.cache[key]

106
jarvis/utils/cogs.py Normal file
View file

@ -0,0 +1,106 @@
"""Cog wrapper for command caching."""
from datetime import datetime, timedelta, timezone
from dis_snek import InteractionContext, Scale, Snake
from dis_snek.client.utils.misc_utils import find
from dis_snek.models.discord.embed import EmbedField
from dis_snek.models.snek.tasks.task import Task
from dis_snek.models.snek.tasks.triggers import IntervalTrigger
from jarvis_core.db import q
from jarvis_core.db.models import (
Action,
Ban,
Kick,
Modlog,
Mute,
Note,
Setting,
Warning,
)
from jarvis.utils import build_embed
MODLOG_LOOKUP = {"Ban": Ban, "Kick": Kick, "Mute": Mute, "Warning": Warning}
class CacheCog(Scale):
"""Cog wrapper for command caching."""
def __init__(self, bot: Snake):
self.bot = bot
self.cache = {}
self._expire_interaction.start()
def check_cache(self, ctx: InteractionContext, **kwargs: dict) -> dict:
"""Check the cache."""
if not kwargs:
kwargs = {}
return find(
lambda x: x["command"] == ctx.subcommand_name # noqa: W503
and x["user"] == ctx.author.id # noqa: W503
and x["guild"] == ctx.guild.id # noqa: W503
and all(x[k] == v for k, v in kwargs.items()), # noqa: W503
self.cache.values(),
)
@Task.create(IntervalTrigger(minutes=1))
async def _expire_interaction(self) -> None:
keys = list(self.cache.keys())
for key in keys:
if self.cache[key]["timeout"] <= datetime.now(tz=timezone.utc) + timedelta(minutes=1):
del self.cache[key]
class ModcaseCog(Scale):
"""Cog wrapper for moderation case logging."""
def __init__(self, bot: Snake):
self.bot = bot
self.add_scale_postrun(self.log)
async def log(self, ctx: InteractionContext, *args: list, **kwargs: dict) -> None:
"""
Log a moderation activity in a moderation case.
Args:
ctx: Command context
"""
name = self.__name__.replace("Cog", "")
if name not in ["Lock", "Lockdown", "Purge", "Roleping"]:
user = kwargs.pop("user", None)
if not user and not ctx.target_id:
self.logger.warn(f"Admin action {name} missing user, exiting")
return
elif ctx.target_id:
user = ctx.target
coll = MODLOG_LOOKUP.get(name, None)
if not coll:
self.logger.warn(f"Unsupported action {name}, exiting")
return
action = await coll.find_one(q(user=user.id, guild=ctx.guild_id, active=True))
if not action:
self.logger.warn(f"Missing action {name}, exiting")
return
action = Action(action_type=name.lower(), parent=action.id)
note = Note(admin=self.bot.user.id, content="Moderation case opened automatically")
await Modlog(user=user.id, admin=ctx.author.id, actions=[action], notes=[note]).commit()
notify = await Setting.find_one(q(guild=ctx.guild.id, setting="notify", value=True))
if notify and name not in ["Kick", "Ban"]: # Ignore Kick and Ban, as these are unique
fields = [
EmbedField(name="Action Type", value=name, inline=False),
EmbedField(
name="Reason", value=kwargs.get("reason", None) or "N/A", inline=False
),
]
embed = build_embed(
title="Admin action taken",
description=f"Admin action has been taken against you in {ctx.guild.name}",
fields=fields,
)
guild_url = f"https://discord.com/channels/{ctx.guild.id}"
embed.set_author(name=ctx.guild.name, icon_url=ctx.guild.icon.url, url=guild_url)
embed.set_thumbnail(url=ctx.guild.icon.url)
await user.send(embed=embed)

View file

@ -5,7 +5,7 @@ from jarvis.config import get_config
def user_is_bot_admin() -> bool: def user_is_bot_admin() -> bool:
"""Check if a user is a J.A.R.V.I.S. admin.""" """Check if a user is a JARVIS admin."""
async def predicate(ctx: InteractionContext) -> bool: async def predicate(ctx: InteractionContext) -> bool:
"""Command check predicate.""" """Command check predicate."""

753
poetry.lock generated

File diff suppressed because it is too large Load diff

View file

@ -20,10 +20,9 @@ orjson = "^3.6.6"
jarvis-core = {git = "https://git.zevaryx.com/stark-industries/jarvis/jarvis-core.git", rev = "main"} jarvis-core = {git = "https://git.zevaryx.com/stark-industries/jarvis/jarvis-core.git", rev = "main"}
aiohttp = "^3.8.1" aiohttp = "^3.8.1"
pastypy = "^1.0.1" pastypy = "^1.0.1"
dateparser = "^1.1.1"
[tool.poetry.dev-dependencies] aiofile = "^3.7.4"
python-lsp-server = {extras = ["all"], version = "^1.3.3"} molter = "^0.11.0"
black = "^22.1.0"
[build-system] [build-system]
requires = ["poetry-core>=1.0.0"] requires = ["poetry-core>=1.0.0"]