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]
extend-ignore =
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
ANN204, ANN206, # return annotations for special methods and class methods
D105, D107, # Missing Docstrings in magic method and __init__
@ -10,5 +11,6 @@ extend-ignore =
D101, # Missing docstring in public class
# 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.
max-line-length=100

View file

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

View file

@ -1,24 +1,34 @@
---
token: api key here
client_id: 123456789012345678
logo: alligator2
token: bot token
twitter:
consumer_key: key
consumer_secret: secret
access_token: access token
access_secret: access secret
mongo:
connect:
username: user
password: pass
host: localhost
username: username
password: password
host: hostname
port: 27017
database: database
urls:
url_name: url
url_name2: url2
max_messages: 1000
gitlab_token: null
extra: urls
max_messages: 10000
gitlab_token: token
cogs:
- list
- of
- enabled
- cogs
- all
- if
- empty
- admin
- autoreact
- dev
- image
- gl
- remindme
- 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
from importlib.metadata import version as _v
from dis_snek import Intents
from jarvis_core.db import connect
from jarvis_core.log import get_logger
from jarvis import utils
from jarvis.client import Jarvis
from jarvis.config import get_config
from jarvis.config import JarvisConfig
try:
__version__ = _v("jarvis")
except Exception:
__version__ = "0.0.0"
jconfig = get_config()
logger = logging.getLogger("discord")
logger.setLevel(logging.getLevelName(jconfig.log_level))
jconfig = JarvisConfig.from_yaml()
logger = get_logger("jarvis")
logger.setLevel(jconfig.log_level)
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)
intents = Intents.DEFAULT | Intents.MESSAGES | Intents.GUILD_MEMBERS | Intents.GUILD_MESSAGES
restart_ctx = None
jarvis = Jarvis(intents=intents, default_prefix="!", sync_interactions=jconfig.sync)
jarvis = Jarvis(
intents=intents,
sync_interactions=jconfig.sync,
delete_unused_application_cmds=True,
send_command_tracebacks=False,
)
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")
jconfig.get_db_config()
logger.debug("Loading configuration from database")
# jconfig.get_db_config()
logger.debug("Loading extensions")
for extension in utils.get_extensions():
jarvis.load_extension(extension)
logger.debug(f"Loaded {extension}")
jarvis.max_messages = jconfig.max_messages
logger.debug("Running JARVIS")
await jarvis.astart(jconfig.token)

View file

@ -1,18 +1,25 @@
"""Custom JARVIS client."""
import logging
import re
import traceback
from datetime import datetime
from datetime import datetime, timezone
from aiohttp import ClientSession
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.models.discord.channel import DMChannel
from dis_snek.models.discord.embed import EmbedField
from dis_snek.models.discord.enums import Permissions
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
from dis_snek.models.snek.context import Context, InteractionContext, MessageContext
from dis_snek.models.snek.tasks.task import Task
from dis_snek.models.snek.tasks.triggers import IntervalTrigger
from jarvis_core.db import q
@ -30,6 +37,7 @@ DEFAULT_SITE = "https://paste.zevs.me"
ERROR_MSG = """
Command Information:
Guild: {guild_name}
Name: {invoked_name}
Args:
{arg_str}
@ -49,15 +57,20 @@ CMD_FMT = fmt(Fore.GREEN, Format.BOLD)
class Jarvis(Snake):
def __init__(self, *args, **kwargs): # noqa: ANN002 ANN003
super().__init__(*args, **kwargs)
self.logger = logging.getLogger(__name__)
self.phishing_domains = []
self.pre_run_callback = self._prerun
@Task.create(IntervalTrigger(days=1))
async def _update_domains(self) -> None:
self.logger.debug("Updating phishing domains")
async with ClientSession(headers={"X-Identity": "Discord: zevaryx#5779"}) as session:
response = await session.get("https://phish.sinking.yachts/v2/recent/86415")
response.raise_for_status()
data = await response.json()
self.logger.debug(f"Found {len(data)} changes to phishing domains")
for update in data:
if update["type"] == "add":
if update["domain"] not in self.phishing_domains:
@ -66,20 +79,29 @@ class Jarvis(Snake):
if update["domain"] in self.phishing_domains:
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:
self.logger.debug("Loading phishing domains")
async with ClientSession(headers={"X-Identity": "Discord: zevaryx#5779"}) as session:
response = await session.get("https://phish.sinking.yachts/v2/all")
response.raise_for_status()
self.phishing_domains = await response.json()
self.logger.info(f"Protected from {len(self.phishing_domains)} phishing domains")
@listen()
async def on_ready(self) -> None:
"""Lepton on_ready override."""
await self._sync_domains()
self._update_domains.start()
print("Logged in as {}".format(self.user)) # noqa: T001
print("Connected to {} guild(s)".format(len(self.guilds))) # noqa: T001
print( # noqa: T001
self.logger.info("Logged in as {}".format(self.user)) # noqa: T001
self.logger.info("Connected to {} guild(s)".format(len(self.guilds))) # noqa: T001
self.logger.info( # noqa: T001
"https://discord.com/api/oauth2/authorize?client_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
) -> None:
"""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)
channel = await guild.fetch_channel(DEFAULT_ERROR_CHANNEL)
error_time = datetime.utcnow().strftime("%d-%m-%Y %H:%M-%S.%f UTC")
timestamp = int(datetime.now().timestamp())
error_time = datetime.now(tz=timezone.utc).strftime("%d-%m-%Y %H:%M-%S.%f UTC")
timestamp = int(datetime.now(tz=timezone.utc).timestamp())
timestamp = f"<t:{timestamp}:T>"
arg_str = (
"\n".join(f" {k}: {v}" for k, v in ctx.kwargs.items()) if ctx.kwargs else " None"
)
arg_str = ""
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_kwargs = (
"\n".join(f" {k}: {v}" for k, v in kwargs.items()) if kwargs else " None"
)
full_message = ERROR_MSG.format(
guild_name=ctx.guild.name,
error_time=error_time,
invoked_name=ctx.invoked_name,
arg_str=arg_str,
callback_args=callback_args,
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
paste = Paste(content=full_message)
await paste.save(DEFAULT_SITE)
paste = Paste(content=full_message, site=DEFAULT_SITE)
key = await paste.save()
self.logger.debug(f"Large traceback, saved to Pasty {paste.id}, {key=}")
await channel.send(
f"JARVIS encountered an error at {timestamp}. Log too big to send over Discord."
f"\nPlease see log at {paste.url}"
)
else:
error_message = "".join(traceback.format_exception(error))
await channel.send(
f"JARVIS encountered an error at {timestamp}:"
f"\n```yaml\n{full_message}\n```"
@ -128,13 +170,28 @@ class Jarvis(Snake):
return await super().on_command_error(ctx, error, *args, **kwargs)
# Modlog
async def on_command(self, ctx: InteractionContext) -> None:
async def on_command(self, ctx: Context) -> None:
"""Lepton on_command override."""
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:
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 = [
EmbedField(
name="Command",
@ -144,7 +201,7 @@ class Jarvis(Snake):
]
embed = build_embed(
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,
color="#fc9e3f",
)
@ -152,22 +209,54 @@ class Jarvis(Snake):
embed.set_footer(
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
async def on_member_join(self, user: Member) -> None:
"""Handle on_member_join event."""
guild = user.guild
# Member
@listen()
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"))
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:
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:
"""Handle autopurge events."""
autopurge = await Autopurge.find_one(q(guild=message.guild.id, channel=message.channel.id))
if autopurge:
self.logger.debug(
f"Autopurging message {message.guild.id}/{message.channel.id}/{message.id}"
)
await message.delete(delay=autopurge.delay)
async def autoreact(self, message: Message) -> None:
@ -179,8 +268,16 @@ class Jarvis(Snake):
)
)
if autoreact:
self.logger.debug(
f"Autoreacting to message {message.guild.id}/{message.channel.id}/{message.id}"
)
for reaction in autoreact.reactions:
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:
"""Other message checks."""
@ -197,24 +294,28 @@ class Jarvis(Snake):
setting = Setting(guild=message.guild.id, setting="noinvite", value=True)
await setting.commit()
if match:
guild_invites = await message.guild.invites()
guild_invites.append(message.guild.vanity_url_code)
guild_invites = await message.guild.fetch_invites()
if message.guild.vanity_url_code:
guild_invites.append(message.guild.vanity_url_code)
allowed = [x.code for x in guild_invites] + [
"dbrand",
"VtgZntXcnZ",
"gPfYGbvTCE",
]
if match.group(1) not in allowed and setting.value:
await message.delete()
w = Warning(
if (m := match.group(1)) not in allowed and setting.value:
self.logger.debug(f"Removing non-allowed invite `{m}` from {message.guild.id}")
try:
await message.delete()
except Exception:
self.logger.debug("Message deleted before action taken")
await Warning(
active=True,
admin=self.user.id,
duration=24,
guild=message.guild.id,
reason="Sent an invite link",
user=message.author.id,
)
await w.commit()
).commit()
embed = warning_embed(message.author, "Sent an invite link")
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
> 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,
admin=self.user.id,
duration=24,
guild=message.guild.id,
reason="Mass Mention",
user=message.author.id,
)
await w.commit()
).commit()
embed = warning_embed(message.author, "Mass Mention")
await message.channel.send(embed=embed)
async def roleping(self, message: Message) -> None:
"""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:
return
rolepings = await Roleping.find(q(guild=message.guild.id, active=True)).to_list(None)
@ -286,31 +391,35 @@ class Jarvis(Snake):
break
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,
admin=self.user.id,
duration=24,
guild=message.guild.id,
reason="Pinged a blocked role/user with a blocked role",
user=message.author.id,
)
await w.commit()
).commit()
embed = warning_embed(message.author, "Pinged a blocked role/user with a blocked role")
await message.channel.send(embed=embed)
async def phishing(self, message: Message) -> None:
"""Check if the message contains any known phishing domains."""
for match in url.finditer(message.content):
if match.group("domain") in self.phishing_domains:
w = Warning(
if (m := match.group("domain")) in self.phishing_domains:
self.logger.debug(
f"Phishing url `{m}` detected in {message.guild.id}/{message.channel.id}/{message.id}"
)
await Warning(
active=True,
admin=self.user.id,
duration=24,
guild=message.guild.id,
reason="Phishing URL",
user=message.author.id,
)
await w.commit()
).commit()
embed = warning_embed(message.author, "Phishing URL")
await message.channel.send(embed=embed)
await message.delete()
@ -329,15 +438,17 @@ class Jarvis(Snake):
data = await resp.json()
for item in data["processed"]["urls"].values():
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,
admin=self.user.id,
duration=24,
guild=message.guild.id,
reason="Unsafe URL",
user=message.author.id,
)
await w.commit()
).commit()
reasons = ", ".join(item["not_safe_reasons"])
embed = warning_embed(message.author, reasons)
await message.channel.send(embed=embed)
@ -364,40 +475,45 @@ class Jarvis(Snake):
before = event.before
after = event.after
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 not before or before.content == after.content or before.content is None:
return
channel = before.guild.get_channel(modlog.value)
fields = [
EmbedField(
"Original Message",
before.content if before.content else "N/A",
False,
),
EmbedField(
"New Message",
after.content if after.content else "N/A",
False,
),
]
embed = build_embed(
title="Message Edited",
description=f"{after.author.mention} edited a message",
fields=fields,
color="#fc9e3f",
timestamp=after.edited_timestamp,
url=after.jump_url,
)
embed.set_author(
name=after.author.username,
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}"
)
await channel.send(embed=embed)
try:
channel = before.guild.get_channel(modlog.value)
fields = [
EmbedField(
"Original Message",
before.content if before.content else "N/A",
False,
),
EmbedField(
"New Message",
after.content if after.content else "N/A",
False,
),
]
embed = build_embed(
title="Message Edited",
description=f"{after.author.mention} edited a message in {before.channel.mention}",
fields=fields,
color="#fc9e3f",
timestamp=after.edited_timestamp,
url=after.jump_url,
)
embed.set_author(
name=after.author.username,
icon_url=after.author.display_avatar.url,
url=after.jump_url,
)
embed.set_footer(
text=f"{after.author.username}#{after.author.discriminator} | {after.author.id}"
)
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:
await self.massmention(after)
await self.roleping(after)
@ -411,54 +527,66 @@ class Jarvis(Snake):
async def on_message_delete(self, event: MessageDelete) -> None:
"""Process on_message_delete events."""
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:
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:
value = "\n".join([f"[{x.filename}]({x.url})" for x in message.attachments])
fields.append(
EmbedField(
name="Attachments",
value=value,
inline=False,
try:
if message.attachments:
value = "\n".join([f"[{x.filename}]({x.url})" for x in message.attachments])
fields.append(
EmbedField(
name="Attachments",
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:
value = "\n".join([f"Sticker: {x.name}" for x in message.sticker_items])
fields.append(
EmbedField(
name="Stickers",
value=value,
inline=False,
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.username}#{message.author.discriminator} | "
f"{message.author.id}"
)
)
if message.embeds:
value = str(len(message.embeds)) + " embeds"
fields.append(
EmbedField(
name="Embeds",
value=value,
inline=False,
)
await channel.send(embed=embed)
except Exception as e:
self.logger.warn(
f"Failed to process edit {message.guild.id}/{message.channel.id}/{message.id}: {e}"
)
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 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:
"""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)
logger.debug(msg.format("ban"))
kick.KickCog(bot)
# lock.LockCog(bot)
# lockdown.LockdownCog(bot)
logger.debug(msg.format("kick"))
lock.LockCog(bot)
logger.debug(msg.format("lock"))
lockdown.LockdownCog(bot)
logger.debug(msg.format("ban"))
mute.MuteCog(bot)
logger.debug(msg.format("mute"))
purge.PurgeCog(bot)
logger.debug(msg.format("purge"))
roleping.RolepingCog(bot)
logger.debug(msg.format("roleping"))
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
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.ext.paginators import Paginator
from dis_snek.models.discord.embed import EmbedField
from dis_snek.models.discord.user import User
from dis_snek.models.snek.application_commands import (
OptionTypes,
SlashCommand,
SlashCommandChoice,
slash_command,
slash_option,
@ -17,11 +19,16 @@ from jarvis_core.db import q
from jarvis_core.db.models import Ban, Unban
from jarvis.utils import build_embed
from jarvis.utils.cogs import ModcaseCog
from jarvis.utils.permissions import admin_or_permissions
class BanCog(Scale):
"""J.A.R.V.I.S. BanCog."""
class BanCog(ModcaseCog):
"""JARVIS BanCog."""
def __init__(self, bot: Snake):
super().__init__(bot)
self.logger = logging.getLogger(__name__)
async def discord_apply_ban(
self,
@ -56,9 +63,9 @@ class BanCog(Scale):
embed.set_author(
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}")
await ctx.send(embed=embed)
@ -83,9 +90,9 @@ class BanCog(Scale):
)
embed.set_author(
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}")
await ctx.send(embed=embed)
@ -105,19 +112,25 @@ class BanCog(Scale):
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))
async def _ban(
self,
ctx: InteractionContext,
user: User,
reason: str,
user: User = None,
btype: str = "perm",
duration: int = 4,
) -> None:
if not user or user == ctx.author:
if user.id == ctx.author.id:
await ctx.send("You cannot ban yourself.", ephemeral=True)
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)
return
if btype == "temp" and duration < 0:
@ -203,9 +216,10 @@ class BanCog(Scale):
discord_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
self.logger.debug(f"{user}")
if re.match(r"^[0-9]{1,}$", user): # User ID
user = int(user)
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.
# We take advantage of the previous checks to save CPU cycles
if not discord_ban_info:
if isinstance(user, int):
if isinstance(user, User):
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:
search = {
@ -277,9 +291,9 @@ class BanCog(Scale):
).save()
await ctx.send("Unable to find user in Discord, but removed entry from database.")
@slash_command(
name="bans", description="User bans", sub_cmd_name="list", sub_cmd_description="List bans"
)
bans = SlashCommand(name="bans", description="User bans")
@bans.subcommand(sub_cmd_name="list", sub_cmd_description="List bans")
@slash_option(
name="btype",
description="Ban type",
@ -295,23 +309,23 @@ class BanCog(Scale):
@slash_option(
name="active",
description="Active bans",
opt_type=OptionTypes.INTEGER,
opt_type=OptionTypes.BOOLEAN,
required=False,
choices=[SlashCommandChoice(name="Yes", value=1), SlashCommandChoice(name="No", value=0)],
)
@check(admin_or_permissions(Permissions.BAN_MEMBERS))
async def _bans_list(self, ctx: InteractionContext, btype: int = 0, active: int = 1) -> None:
active = bool(active)
async def _bans_list(
self, ctx: InteractionContext, btype: int = 0, active: bool = True
) -> None:
types = [0, "perm", "temp", "soft"]
search = {"guild": ctx.guild.id}
if active:
search["active"] = True
if btype > 0:
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 = []
fields = []
async for ban in bans:
for ban in bans:
if not ban.username:
user = await self.bot.fetch_user(ban.user)
ban.username = user.username if user else "[deleted user]"

View file

@ -1,5 +1,7 @@
"""J.A.R.V.I.S. KickCog."""
from dis_snek import InteractionContext, Permissions, Scale
"""JARVIS KickCog."""
import logging
from dis_snek import InteractionContext, Permissions, Snake
from dis_snek.models.discord.embed import EmbedField
from dis_snek.models.discord.user import User
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.utils import build_embed
from jarvis.utils.cogs import ModcaseCog
from jarvis.utils.permissions import admin_or_permissions
class KickCog(Scale):
"""J.A.R.V.I.S. KickCog."""
class KickCog(ModcaseCog):
"""JARVIS KickCog."""
def __init__(self, bot: Snake):
super().__init__(bot)
self.logger = logging.getLogger(__name__)
@slash_command(name="kick", description="Kick a user")
@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)
except Exception:
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))]
embed = build_embed(

View file

@ -1,113 +1,118 @@
"""J.A.R.V.I.S. LockCog."""
# from dis_snek import Scale
#
# # TODO: Uncomment 99% of code once implementation is figured out
# from contextlib import suppress
# from typing import Union
#
# from dis_snek import InteractionContext, Scale, Snake
# from dis_snek.models.discord.enums import Permissions
# from dis_snek.models.discord.role import Role
# from dis_snek.models.discord.user import User
# from dis_snek.models.discord.channel import GuildText, GuildVoice, PermissionOverwrite
# from dis_snek.models.snek.application_commands import (
# OptionTypes,
# PermissionTypes,
# slash_command,
# slash_option,
# )
# from dis_snek.models.snek.command import check
#
# from jarvis.db.models import Lock
# from jarvis.utils.permissions import admin_or_permissions
#
#
# class LockCog(Scale):
# """J.A.R.V.I.S. LockCog."""
#
# @slash_command(name="lock", description="Lock a channel")
# @slash_option(name="reason",
# description="Lock Reason",
# opt_type=3,
# required=True,)
# @slash_option(name="duration",
# description="Lock duration in minutes (default 10)",
# opt_type=4,
# required=False,)
# @slash_option(name="channel",
# description="Channel to lock",
# opt_type=7,
# required=False,)
# @check(admin_or_permissions(Permissions.MANAGE_CHANNELS))
# async def _lock(
# self,
# ctx: InteractionContext,
# reason: str,
# duration: int = 10,
# channel: Union[GuildText, GuildVoice] = None,
# ) -> None:
# await ctx.defer(ephemeral=True)
# if duration <= 0:
# await ctx.send("Duration must be > 0", ephemeral=True)
# return
#
# elif duration > 60 * 12:
# await ctx.send("Duration must be <= 12 hours", ephemeral=True)
# return
#
# if len(reason) > 100:
# await ctx.send("Reason must be <= 100 characters", ephemeral=True)
# return
# if not channel:
# channel = ctx.channel
#
# # role = ctx.guild.default_role # Uncomment once implemented
# if isinstance(channel, GuildText):
# to_deny = Permissions.SEND_MESSAGES
# elif isinstance(channel, GuildVoice):
# to_deny = Permissions.CONNECT | Permissions.SPEAK
#
# overwrite = PermissionOverwrite(type=PermissionTypes.ROLE, deny=to_deny)
# # TODO: Get original permissions
# # TODO: Apply overwrite
# overwrite = overwrite
# _ = Lock(
# channel=channel.id,
# guild=ctx.guild.id,
# admin=ctx.author.id,
# reason=reason,
# duration=duration,
# ) # .save() # Uncomment once implemented
# # await ctx.send(f"{channel.mention} locked for {duration} minute(s)")
# await ctx.send("Unfortunately, this is not yet implemented", hidden=True)
#
# @cog_ext.cog_slash(
# name="unlock",
# description="Unlocks a channel",
# choices=[
# create_option(
# name="channel",
# description="Channel to lock",
# opt_type=7,
# required=False,
# ),
# ],
# )
# @check(admin_or_permissions(Permissions.MANAGE_CHANNELS))
# async def _unlock(
# self,
# ctx: InteractionContext,
# channel: Union[GuildText, GuildVoice] = None,
# ) -> None:
# if not channel:
# channel = ctx.channel
# lock = Lock.objects(guild=ctx.guild.id, channel=channel.id, active=True).first()
# if not lock:
# await ctx.send(f"{channel.mention} not locked.", ephemeral=True)
# return
# for role in ctx.guild.roles:
# with suppress(Exception):
# await self._unlock_channel(channel, role, ctx.author)
# lock.active = False
# lock.save()
# await ctx.send(f"{channel.mention} unlocked")
"""JARVIS LockCog."""
import logging
from typing import Union
from dis_snek import InteractionContext, Scale, Snake
from dis_snek.client.utils.misc_utils import get
from dis_snek.models.discord.channel import GuildText, GuildVoice
from dis_snek.models.discord.enums import Permissions
from dis_snek.models.snek.application_commands import (
OptionTypes,
slash_command,
slash_option,
)
from dis_snek.models.snek.command import check
from jarvis_core.db import q
from jarvis_core.db.models import Lock, Permission
from jarvis.utils.permissions import admin_or_permissions
class LockCog(Scale):
"""JARVIS LockCog."""
def __init__(self, bot: Snake):
self.bot = bot
self.logger = logging.getLogger(__name__)
@slash_command(name="lock", description="Lock a channel")
@slash_option(
name="reason",
description="Lock Reason",
opt_type=3,
required=True,
)
@slash_option(
name="duration",
description="Lock duration in minutes (default 10)",
opt_type=4,
required=False,
)
@slash_option(
name="channel",
description="Channel to lock",
opt_type=7,
required=False,
)
@check(admin_or_permissions(Permissions.MANAGE_CHANNELS))
async def _lock(
self,
ctx: InteractionContext,
reason: str,
duration: int = 10,
channel: Union[GuildText, GuildVoice] = None,
) -> None:
await ctx.defer(ephemeral=True)
if duration <= 0:
await ctx.send("Duration must be > 0", ephemeral=True)
return
elif duration > 60 * 12:
await ctx.send("Duration must be <= 12 hours", ephemeral=True)
return
if len(reason) > 100:
await ctx.send("Reason must be <= 100 characters", ephemeral=True)
return
if not channel:
channel = ctx.channel
to_deny = Permissions.CONNECT | Permissions.SPEAK | Permissions.SEND_MESSAGES
current = get(channel.permission_overwrites, id=ctx.guild.id)
if current:
current = Permission(id=ctx.guild.id, allow=int(current.allow), deny=int(current.deny))
role = await ctx.guild.fetch_role(ctx.guild.id)
await channel.add_permission(target=role, deny=to_deny, reason="Locked")
await Lock(
channel=channel.id,
guild=ctx.guild.id,
admin=ctx.author.id,
reason=reason,
duration=duration,
original_perms=current,
).commit()
await ctx.send(f"{channel.mention} locked for {duration} minute(s)")
@slash_command(name="unlock", description="Unlock a channel")
@slash_option(
name="channel",
description="Channel to unlock",
opt_type=OptionTypes.CHANNEL,
required=False,
)
@check(admin_or_permissions(Permissions.MANAGE_CHANNELS))
async def _unlock(
self,
ctx: InteractionContext,
channel: Union[GuildText, GuildVoice] = None,
) -> None:
if not channel:
channel = ctx.channel
lock = await Lock.find_one(q(guild=ctx.guild.id, channel=channel.id, active=True))
if not lock:
await ctx.send(f"{channel.mention} not locked.", ephemeral=True)
return
overwrite = get(channel.permission_overwrites, id=ctx.guild.id)
if overwrite and lock.original_perms:
overwrite.allow = lock.original_perms.allow
overwrite.deny = lock.original_perms.deny
await channel.edit_permission(overwrite, reason="Unlock")
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."""
from contextlib import suppress
from datetime import datetime
"""JARVIS LockdownCog."""
import logging
from discord.ext import commands
from discord_slash import SlashContext, cog_ext
from discord_slash.utils.manage_commands import create_option
from dis_snek import InteractionContext, Scale, Snake
from dis_snek.client.utils.misc_utils import find_all, get
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.cachecog import CacheCog
# from jarvis.utils.permissions import admin_or_permissions
from jarvis.utils.permissions import admin_or_permissions
class LockdownCog(CacheCog):
"""J.A.R.V.I.S. LockdownCog."""
async def lock(bot: Snake, target: GuildChannel, admin: Member, reason: str, duration: int) -> None:
"""
Lock an existing channel
def __init__(self, bot: commands.Bot):
super().__init__(bot)
Args:
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",
name="start",
description="Locks a server",
choices=[
create_option(
name="reason",
description="Lockdown Reason",
opt_type=3,
required=True,
),
create_option(
name="duration",
description="Lockdown duration in minutes (default 10)",
opt_type=4,
required=False,
),
],
async def lock_all(bot: Snake, guild: Guild, admin: Member, reason: str, duration: int) -> None:
"""
Lock all channels
Args:
bot: Bot instance
guild: Target guild
admin: Admin who initiated lockdown
"""
role = await guild.fetch_role(guild.id)
categories = find_all(lambda x: isinstance(x, GuildCategory), guild.channels)
for category in categories:
await lock(bot, category, admin, reason, duration)
perms = category.permissions_for(role)
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(
self,
ctx: SlashContext,
ctx: InteractionContext,
reason: str,
duration: int = 10,
) -> None:
await ctx.defer(ephemeral=True)
await ctx.defer()
if duration <= 0:
await ctx.send("Duration must be > 0", ephemeral=True)
return
elif duration >= 300:
await ctx.send("Duration must be < 5 hours", ephemeral=True)
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(
base="lockdown",
name="end",
description="Unlocks a server",
)
@commands.has_permissions(administrator=True)
exists = await Lockdown.find_one(q(guild=ctx.guild.id, active=True))
if exists:
await ctx.send("Server already in lockdown", ephemeral=True)
return
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(
self,
ctx: SlashContext,
ctx: InteractionContext,
) -> 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()
for channel in channels:
for role in roles:
with suppress(Exception):
await self._unlock_channel(channel, role, ctx.author)
update = True
if update:
Lock.objects(guild=ctx.guild.id, active=True).update(set__active=False)
await ctx.send("Server unlocked")
lockdown = await Lockdown.find_one(q(guild=ctx.guild.id, active=True))
if not lockdown:
await ctx.send("Server not in lockdown", ephemeral=True)
return
await unlock_all(self.bot, ctx.guild, ctx.author)
await ctx.send("Server no longer in lockdown.")

View file

@ -1,12 +1,19 @@
"""J.A.R.V.I.S. MuteCog."""
from datetime import datetime
"""JARVIS MuteCog."""
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.modal import InputText, Modal, TextStyles
from dis_snek.models.discord.user import Member
from dis_snek.models.snek.application_commands import (
CommandTypes,
OptionTypes,
SlashCommandChoice,
context_menu,
slash_command,
slash_option,
)
@ -14,14 +21,112 @@ from dis_snek.models.snek.command import check
from jarvis_core.db.models import Mute
from jarvis.utils import build_embed
from jarvis.utils.cogs import ModcaseCog
from jarvis.utils.permissions import admin_or_permissions
class MuteCog(Scale):
"""J.A.R.V.I.S. MuteCog."""
class MuteCog(ModcaseCog):
"""JARVIS MuteCog."""
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_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)
return
await user.timeout(communication_disabled_until=duration, reason=reason)
m = Mute(
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}")
until = datetime.now(tz=timezone.utc) + timedelta(minutes=duration)
embed = await self._apply_timeout(ctx, user, reason, until)
await ctx.send(embed=embed)
@slash_command(name="unmute", description="Unmute a user")
@ -106,11 +194,14 @@ class MuteCog(Scale):
async def _unmute(self, ctx: InteractionContext, user: Member) -> None:
if (
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)
return
await user.timeout(communication_disabled_until=datetime.now(tz=timezone.utc))
embed = build_embed(
title="User 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.models.discord.channel import GuildText
from dis_snek.models.snek.application_commands import (
@ -14,10 +16,11 @@ from jarvis.utils.permissions import admin_or_permissions
class PurgeCog(Scale):
"""J.A.R.V.I.S. PurgeCog."""
"""JARVIS PurgeCog."""
def __init__(self, bot: Snake):
self.bot = bot
self.logger = logging.getLogger(__name__)
@slash_command(name="purge", description="Purge messages from channel")
@slash_option(

View file

@ -1,5 +1,7 @@
"""J.A.R.V.I.S. RolepingCog."""
from dis_snek import InteractionContext, Permissions, Scale
"""JARVIS RolepingCog."""
import logging
from dis_snek import InteractionContext, Permissions, Scale, Snake
from dis_snek.client.utils.misc_utils import find_all
from dis_snek.ext.paginators import Paginator
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.snek.application_commands import (
OptionTypes,
slash_command,
SlashCommand,
slash_option,
)
from dis_snek.models.snek.command import check
@ -19,10 +21,19 @@ from jarvis.utils.permissions import admin_or_permissions
class RolepingCog(Scale):
"""J.A.R.V.I.S. RolepingCog."""
"""JARVIS RolepingCog."""
@slash_command(
name="roleping", sub_cmd_name="add", sub_cmd_description="Add a role to roleping"
def __init__(self, bot: Snake):
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)
@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)
return
if role.id == ctx.guild.id:
await ctx.send("Cannot add `@everyone` to roleping", ephemeral=True)
return
_ = await Roleping(
role=role.id,
guild=ctx.guild.id,
@ -41,7 +56,7 @@ class RolepingCog(Scale):
).commit()
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(
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)
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.")
@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:
rolepings = await Roleping.find(q(guild=ctx.guild.id)).to_list(None)
@ -66,6 +84,9 @@ class RolepingCog(Scale):
embeds = []
for roleping in rolepings:
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)
bypass_roles = [r.mention or "||`[redacted]`||" for r in broles]
bypass_users = [
@ -111,11 +132,11 @@ class RolepingCog(Scale):
await paginator.send(ctx)
@slash_command(
name="roleping",
description="Block roles from being pinged",
group_name="bypass",
group_description="Allow specific users/roles to ping rolepings",
bypass = roleping.group(
name="bypass", description="Allow specific users/roles to ping rolepings"
)
@bypass.subcommand(
sub_cmd_name="user",
sub_cmd_description="Add a user as a bypass to a roleping",
)
@ -158,11 +179,9 @@ class RolepingCog(Scale):
await roleping.commit()
await ctx.send(f"{bypass.display_name} user bypass added for `{role.name}`")
@slash_command(
name="roleping",
group_name="bypass",
@bypass.subcommand(
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(
name="bypass", description="Role to add", opt_type=OptionTypes.ROLE, required=True
@ -174,6 +193,9 @@ class RolepingCog(Scale):
async def _roleping_bypass_role(
self, ctx: InteractionContext, bypass: Role, role: Role
) -> 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))
if not roleping:
await ctx.send(f"Roleping not configured for {role.mention}", ephemeral=True)
@ -195,11 +217,9 @@ class RolepingCog(Scale):
await roleping.commit()
await ctx.send(f"{bypass.name} role bypass added for `{role.name}`")
@slash_command(
name="roleping",
description="Block roles from being pinged",
group_name="restore",
group_description="Remove a roleping bypass",
restore = roleping.group(name="restore", description="Remove a roleping bypass")
@restore.subcommand(
sub_cmd_name="user",
sub_cmd_description="Remove a bypass from a roleping (restoring it)",
)
@ -226,11 +246,9 @@ class RolepingCog(Scale):
await roleping.commit()
await ctx.send(f"{bypass.display_name} user bypass removed for `{role.name}`")
@slash_command(
name="roleping",
group_name="restore",
@restore.subcommand(
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(
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."""
from dis_snek import InteractionContext, Permissions, Scale
"""JARVIS WarningCog."""
import logging
from dis_snek import InteractionContext, Permissions, Snake
from dis_snek.client.utils.misc_utils import get_all
from dis_snek.ext.paginators import Paginator
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 (
OptionTypes,
SlashCommandChoice,
slash_command,
slash_option,
)
from dis_snek.models.snek.command import check
from jarvis_core.db import q
from jarvis_core.db.models import Warning
from jarvis.utils import build_embed
from jarvis.utils.cogs import ModcaseCog
from jarvis.utils.embeds import warning_embed
from jarvis.utils.permissions import admin_or_permissions
class WarningCog(Scale):
"""J.A.R.V.I.S. WarningCog."""
class WarningCog(ModcaseCog):
"""JARVIS WarningCog."""
def __init__(self, bot: Snake):
super().__init__(bot)
self.logger = logging.getLogger(__name__)
@slash_command(name="warn", description="Warn a user")
@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))
async def _warn(
self, ctx: InteractionContext, user: User, reason: str, duration: int = 24
self, ctx: InteractionContext, user: Member, reason: str, duration: int = 24
) -> None:
if len(reason) > 100:
await ctx.send("Reason must be < 100 characters", ephemeral=True)
@ -48,6 +55,9 @@ class WarningCog(Scale):
elif duration >= 120:
await ctx.send("Duration must be < 5 days", ephemeral=True)
return
if not await ctx.guild.fetch_member(user.id):
await ctx.send("User not in guild", ephemeral=True)
return
await ctx.defer()
await Warning(
user=user.id,
@ -65,25 +75,24 @@ class WarningCog(Scale):
@slash_option(
name="active",
description="View active only",
opt_type=OptionTypes.INTEGER,
opt_type=OptionTypes.BOOLEAN,
required=False,
choices=[
SlashCommandChoice(name="Yes", value=1),
SlashCommandChoice(name="No", value=0),
],
)
@check(admin_or_permissions(Permissions.MANAGE_GUILD))
async def _warnings(self, ctx: InteractionContext, user: User, active: bool = 1) -> None:
active = bool(active)
async def _warnings(self, ctx: InteractionContext, user: Member, active: bool = True) -> None:
warnings = (
await Warning.find(
user=user.id,
guild=ctx.guild.id,
q(
user=user.id,
guild=ctx.guild.id,
)
)
.sort("created_at", -1)
.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)
pages = []
@ -91,7 +100,7 @@ class WarningCog(Scale):
if len(active_warns) == 0:
embed = build_embed(
title="Warnings",
description=f"{warnings.count()} total | 0 currently active",
description=f"{len(warnings)} total | 0 currently active",
fields=[],
)
embed.set_author(name=user.username, icon_url=user.display_avatar.url)
@ -100,13 +109,14 @@ class WarningCog(Scale):
else:
fields = []
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]`||"
if admin:
admin_name = f"{admin.username}#{admin.discriminator}"
fields.append(
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",
inline=False,
)
@ -129,8 +139,9 @@ class WarningCog(Scale):
else:
fields = []
for warn in warnings:
ts = int(warn.created_at.timestamp())
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(
EmbedField(
name=title,
@ -150,6 +161,6 @@ class WarningCog(Scale):
embed.set_thumbnail(url=ctx.guild.icon.url)
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)

View file

@ -1,4 +1,5 @@
"""J.A.R.V.I.S. Autoreact Cog."""
"""JARVIS Autoreact Cog."""
import logging
import re
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.snek.application_commands import (
OptionTypes,
slash_command,
SlashCommand,
slash_option,
)
from dis_snek.models.snek.command import check
@ -19,14 +20,15 @@ from jarvis.utils.permissions import admin_or_permissions
class AutoReactCog(Scale):
"""J.A.R.V.I.S. Autoreact Cog."""
"""JARVIS Autoreact Cog."""
def __init__(self, bot: Snake):
self.bot = bot
self.logger = logging.getLogger(__name__)
self.custom_emote = re.compile(r"^<:\w+:(\d+)>$")
async def create_autoreact(
self, ctx: InteractionContext, channel: GuildText
self, ctx: InteractionContext, channel: GuildText, thread: bool
) -> Tuple[bool, Optional[str]]:
"""
Create an autoreact monitor on a channel.
@ -34,6 +36,7 @@ class AutoReactCog(Scale):
Args:
ctx: Interaction context of command
channel: Channel to monitor
thread: Create a thread
Returns:
Tuple of success? and error message
@ -42,12 +45,13 @@ class AutoReactCog(Scale):
if exists:
return False, f"Autoreact already exists for {channel.mention}."
_ = Autoreact(
await Autoreact(
guild=ctx.guild.id,
channel=channel.id,
reactions=[],
thread=thread,
admin=ctx.author.id,
).save()
).commit()
return True, None
@ -62,10 +66,15 @@ class AutoReactCog(Scale):
Returns:
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(
name="autoreact",
autoreact = SlashCommand(name="autoreact", description="Channel message autoreacts")
@autoreact.subcommand(
sub_cmd_name="add",
sub_cmd_description="Add an autoreact emote to a channel",
)
@ -76,46 +85,57 @@ class AutoReactCog(Scale):
required=True,
)
@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))
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()
custom_emoji = self.custom_emote.match(emote)
standard_emoji = emote in emoji_list
if not custom_emoji and not standard_emoji:
await ctx.send(
"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)
if emote:
custom_emoji = self.custom_emote.match(emote)
standard_emoji = emote in emoji_list
if not custom_emoji and not standard_emoji:
await ctx.send(
"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, 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))
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))
if emote in autoreact.reactions:
if emote and emote in autoreact.reactions:
await ctx.send(
f"Emote already added to {channel.mention} autoreactions.",
ephemeral=True,
)
return
if len(autoreact.reactions) >= 5:
if emote and len(autoreact.reactions) >= 5:
await ctx.send(
"Max number of reactions hit. Remove a different one to add this one",
ephemeral=True,
)
return
autoreact.reactions.append(emote)
autoreact.save()
await ctx.send(f"Added {emote} to {channel.mention} autoreact.")
if emote:
autoreact.reactions.append(emote)
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(
name="autoreact",
@autoreact.subcommand(
sub_cmd_name="remove",
sub_cmd_description="Remove an autoreact emote to a channel",
)
@ -143,7 +163,7 @@ class AutoReactCog(Scale):
)
return
if emote.lower() == "all":
self.delete_autoreact(ctx, channel)
await self.delete_autoreact(ctx, channel)
await ctx.send(f"Autoreact removed from {channel.mention}")
elif emote not in autoreact.reactions:
await ctx.send(
@ -153,13 +173,12 @@ class AutoReactCog(Scale):
return
else:
autoreact.reactions.remove(emote)
autoreact.save()
if len(autoreact.reactions) == 0:
self.delete_autoreact(ctx, channel)
await autoreact.commit()
if len(autoreact.reactions) == 0 and not autoreact.thread:
await self.delete_autoreact(ctx, channel)
await ctx.send(f"Removed {emote} from {channel.mention} autoreact.")
@slash_command(
name="autoreact",
@autoreact.subcommand(
sub_cmd_name="list",
sub_cmd_description="List all autoreacts on a channel",
)
@ -188,5 +207,5 @@ class AutoReactCog(Scale):
def setup(bot: Snake) -> None:
"""Add AutoReactCog to J.A.R.V.I.S."""
"""Add AutoReactCog to JARVIS"""
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
from datetime import datetime, timedelta
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.models.discord.components import ActionRow, Button, ButtonStyles
from dis_snek.models.discord.embed import EmbedField
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.cooldowns import Buckets
from jarvis_core.db import q
from jarvis_core.db.models import Guess
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]*")
invites = re.compile(
@ -25,29 +29,38 @@ invites = re.compile(
)
class CTCCog(CacheCog):
"""J.A.R.V.I.S. Complete the Code 2 Cog."""
class CTCCog(Scale):
"""JARVIS Complete the Code 2 Cog."""
def __init__(self, bot: Snake):
super().__init__(bot)
self.bot = bot
self.logger = logging.getLogger(__name__)
self._session = aiohttp.ClientSession()
self.url = "https://completethecodetwo.cards/pw"
def __del__(self):
self._session.close()
@slash_command(
name="ctc2", sub_cmd_name="about", description="CTC2 related commands", scopes=guild_ids
)
ctc2 = SlashCommand(name="ctc2", description="CTC2 related commands", scopes=guild_ids)
@ctc2.subcommand(sub_cmd_name="about")
@cooldown(bucket=Buckets.USER, rate=1, interval=30)
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(
name="ctc2",
@ctc2.subcommand(
sub_cmd_name="pw",
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)
async def _pw(self, ctx: InteractionContext, guess: str) -> None:
@ -79,6 +92,7 @@ class CTCCog(CacheCog):
if guessed:
await ctx.send("Already guessed, dipshit.", ephemeral=True)
return
result = await self._session.post(self.url, data=guess)
correct = False
if 200 <= result.status < 400:
@ -86,28 +100,18 @@ class CTCCog(CacheCog):
correct = True
else:
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(
name="ctc2",
@ctc2.subcommand(
sub_cmd_name="guesses",
sub_cmd_description="Show guesses made for https://completethecodetwo.cards",
scopes=guild_ids,
)
@cooldown(bucket=Buckets.USER, rate=1, interval=2)
async def _guesses(self, ctx: InteractionContext) -> None:
exists = self.check_cache(ctx)
if exists:
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")
await ctx.defer()
guesses = Guess.find().sort("correct", -1).sort("id", -1)
fields = []
for guess in guesses:
async for guess in guesses:
user = await ctx.guild.get_member(guess["user"])
if not 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)
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)
def setup(bot: Snake) -> None:
"""Add CTCCog to J.A.R.V.I.S."""
"""Add CTCCog to JARVIS"""
CTCCog(bot)

View file

@ -1,4 +1,5 @@
"""J.A.R.V.I.S. dbrand cog."""
"""JARVIS dbrand cog."""
import logging
import re
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.snek.application_commands import (
OptionTypes,
slash_command,
SlashCommand,
slash_option,
)
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.utils import build_embed
guild_ids = [578757004059738142, 520021794380447745, 862402786116763668]
guild_ids = [862402786116763668] # [578757004059738142, 520021794380447745, 862402786116763668]
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
"""
def __init__(self, bot: Snake):
self.bot = bot
self.logger = logging.getLogger(__name__)
self.base_url = "https://dbrand.com/"
self._session = aiohttp.ClientSession()
self._session.headers.update({"Content-Type": "application/json"})
@ -37,121 +39,53 @@ class DbrandCog(Scale):
def __del__(self):
self._session.close()
@slash_command(
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")
db = SlashCommand(name="db", description="dbrand commands", scopes=guild_ids)
@slash_command(
name="db",
sub_cmd_name="robotcamo",
scopes=guild_ids,
sub_cmd_description="Get some robot camo. Make Tony Stark proud",
)
@db.subcommand(sub_cmd_name="info", sub_cmd_description="Get useful links")
@cooldown(bucket=Buckets.USER, rate=1, interval=30)
async def _camo(self, ctx: InteractionContext) -> None:
await ctx.send(self.base_url + "robot-camo")
async def _info(self, ctx: InteractionContext) -> None:
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(
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",
@db.subcommand(
sub_cmd_name="contact",
scopes=guild_ids,
sub_cmd_description="Contact support",
)
@cooldown(bucket=Buckets.USER, rate=1, interval=30)
async def _contact(self, ctx: InteractionContext) -> None:
await ctx.send("Contact dbrand support here: " + self.base_url + "contact")
@slash_command(
name="db",
@db.subcommand(
sub_cmd_name="support",
scopes=guild_ids,
sub_cmd_description="Contact support",
)
@cooldown(bucket=Buckets.USER, rate=1, interval=30)
async def _support(self, ctx: InteractionContext) -> None:
await ctx.send("Contact dbrand support here: " + self.base_url + "contact")
@slash_command(
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",
@db.subcommand(
sub_cmd_name="ship",
sub_cmd_description="Get shipping information for your country",
scopes=guild_ids,
)
@slash_option(
name="search",
@ -215,7 +149,7 @@ class DbrandCog(Scale):
)
embed = build_embed(
title="Shipping to {}".format(data["country"]),
sub_cmd_description=description,
description=description,
color="#FFBB00",
fields=fields,
url=self.base_url + "shipping/" + country,
@ -229,7 +163,7 @@ class DbrandCog(Scale):
elif not data["is_valid"]:
embed = build_embed(
title="Check Shipping Times",
sub_cmd_description=(
description=(
"Country not found.\nYou can [view all shipping "
"destinations here](https://dbrand.com/shipping)"
),
@ -246,7 +180,7 @@ class DbrandCog(Scale):
elif not data["shipping_available"]:
embed = build_embed(
title="Shipping to {}".format(data["country"]),
sub_cmd_description=(
description=(
"No shipping available.\nTime to move to a country"
" that has shipping available.\nYou can [find a new country "
"to live in here](https://dbrand.com/shipping)"
@ -264,5 +198,5 @@ class DbrandCog(Scale):
def setup(bot: Snake) -> None:
"""Add dbrandcog to J.A.R.V.I.S."""
"""Add dbrandcog to JARVIS"""
DbrandCog(bot)

View file

@ -1,6 +1,7 @@
"""J.A.R.V.I.S. Developer Cog."""
"""JARVIS Developer Cog."""
import base64
import hashlib
import logging
import re
import subprocess # noqa: S404
import uuid as uuidpy
@ -45,7 +46,11 @@ MAX_FILESIZE = 5 * (1024**3) # 5GB
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_option(
@ -83,8 +88,9 @@ class DevCog(Scale):
title = attach.filename
elif url.match(data):
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)
self.logger.debug(f"Refused to hash file of size {convert_bytesize(size)}")
return
except Exception as e:
await ctx.send(f"Failed to retrieve URL: ```\n{e}\n```", ephemeral=True)
@ -162,6 +168,9 @@ class DevCog(Scale):
name="uuid2ulid",
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)
async def _uuid2ulid(self, ctx: InteractionContext, uuid: str) -> None:
if UUID_VERIFY.match(uuid):
@ -174,6 +183,9 @@ class DevCog(Scale):
name="ulid2uuid",
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)
async def _ulid2uuid(self, ctx: InteractionContext, ulid: str) -> None:
if ULID_VERIFY.match(ulid):
@ -199,9 +211,19 @@ class DevCog(Scale):
required=True,
)
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
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 = [
EmbedField(name="Plaintext", value=f"`{data}`", 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:
mstr = method
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):
await ctx.send(
"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)
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)
async def _cloc(self, ctx: InteractionContext) -> None:
output = subprocess.check_output( # noqa: S603, S607
@ -250,5 +276,5 @@ class DevCog(Scale):
def setup(bot: Snake) -> None:
"""Add DevCog to J.A.R.V.I.S."""
"""Add DevCog to JARVIS"""
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
import gitlab
from dis_snek import InteractionContext, Scale, Snake
from dis_snek.ext.paginators import Paginator
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 (
OptionTypes,
SlashCommand,
SlashCommandChoice,
slash_command,
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
guild_ids = [862402786116763668]
class GitlabCog(Scale):
"""J.A.R.V.I.S. GitLab Cog."""
"""JARVIS GitLab Cog."""
def __init__(self, bot: Snake):
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)
# J.A.R.V.I.S. GitLab ID is 29
# JARVIS GitLab ID is 29
self.project = self._gitlab.projects.get(29)
@slash_command(
name="gl", sub_cmd_name="issue", description="Get an issue from GitLab", scopes=guild_ids
gl = SlashCommand(name="gl", description="Get GitLab info", 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)
async def _issue(self, ctx: InteractionContext, id: int) -> None:
@ -85,7 +96,7 @@ class GitlabCog(Scale):
)
embed.set_author(
name=issue.author["name"],
icon_url=issue.author["display_avatar"],
icon_url=issue.author["avatar_url"],
url=issue.author["web_url"],
)
embed.set_thumbnail(
@ -93,11 +104,9 @@ class GitlabCog(Scale):
)
await ctx.send(embed=embed)
@slash_command(
name="gl",
@gl.subcommand(
sub_cmd_name="milestone",
description="Get a milestone from GitLab",
scopes=guild_ids,
sub_cmd_description="Get a milestone from GitLab",
)
@slash_option(
name="id", description="Milestone ID", opt_type=OptionTypes.INTEGER, required=True
@ -140,7 +149,7 @@ class GitlabCog(Scale):
url=milestone.web_url,
)
embed.set_author(
name="J.A.R.V.I.S.",
name="JARVIS",
url="https://git.zevaryx.com/jarvis",
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)
@slash_command(
name="gl",
@gl.subcommand(
sub_cmd_name="mr",
description="Get a merge request from GitLab",
scopes=guild_ids,
sub_cmd_description="Get a merge request from GitLab",
)
@slash_option(
name="id", description="Merge Request ID", opt_type=OptionTypes.INTEGER, required=True
@ -181,25 +188,26 @@ class GitlabCog(Scale):
labels = "None"
fields = [
EmbedField(name="State", value=mr.state[0].upper() + mr.state[1:]),
EmbedField(name="Assignee", value=assignee),
EmbedField(name="Labels", value=labels),
EmbedField(name="State", value=mr.state[0].upper() + mr.state[1:], inline=True),
EmbedField(name="Draft?", value=str(mr.draft), inline=True),
EmbedField(name="Assignee", value=assignee, inline=True),
EmbedField(name="Labels", value=labels, inline=True),
]
if mr.labels:
color = self.project.labels.get(mr.labels[0]).color
else:
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":
merged_at = datetime.strptime(mr.merged_at, "%Y-%m-%dT%H:%M:%S.%fZ").strftime(
"%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":
closed_at = datetime.strptime(mr.closed_at, "%Y-%m-%dT%H:%M:%S.%fZ").strftime(
"%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:
fields.append(
EmbedField(
@ -219,7 +227,7 @@ class GitlabCog(Scale):
)
embed.set_author(
name=mr.author["name"],
icon_url=mr.author["display_avatar"],
icon_url=mr.author["avatar_url"],
url=mr.author["web_url"],
)
embed.set_thumbnail(
@ -232,7 +240,7 @@ class GitlabCog(Scale):
title = ""
if t_state:
title = f"{t_state} "
title += f"J.A.R.V.I.S. {name}s"
title += f"JARVIS {name}s"
fields = []
for item in api_list:
description = item.description or "No description"
@ -248,10 +256,10 @@ class GitlabCog(Scale):
title=title,
description="",
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(
name="J.A.R.V.I.S.",
name="JARVIS",
url="https://git.zevaryx.com/jarvis",
icon_url="https://git.zevaryx.com/uploads/-/system/user/avatar/11/avatar.png",
)
@ -260,8 +268,9 @@ class GitlabCog(Scale):
)
return embed
@slash_command(
name="gl", sub_cmd_name="issues", description="Get issues from GitLab", scopes=guild_ids
@gl.subcommand(
sub_cmd_name="issues",
sub_cmd_description="Get issues from GitLab",
)
@slash_option(
name="state",
@ -313,11 +322,9 @@ class GitlabCog(Scale):
await paginator.send(ctx)
@slash_command(
name="gl",
@gl.subcommand(
sub_cmd_name="mrs",
description="Get merge requests from GitLab",
scopes=guild_ids,
sub_cmd_description="Get merge requests from GitLab",
)
@slash_option(
name="state",
@ -371,11 +378,9 @@ class GitlabCog(Scale):
await paginator.send(ctx)
@slash_command(
name="gl",
@gl.subcommand(
sub_cmd_name="milestones",
description="Get milestones from GitLab",
scopes=guild_ids,
sub_cmd_description="Get milestones from GitLab",
)
async def _milestones(self, ctx: InteractionContext) -> None:
await ctx.defer()
@ -410,8 +415,55 @@ class GitlabCog(Scale):
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:
"""Add GitlabCog to J.A.R.V.I.S. if Gitlab token exists."""
if get_config().gitlab_token:
"""Add GitlabCog to JARVIS if Gitlab token exists."""
if JarvisConfig.from_yaml().gitlab_token:
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
from io import BytesIO
@ -21,13 +22,14 @@ MIN_ACCURACY = 0.80
class ImageCog(Scale):
"""
Image processing functions for J.A.R.V.I.S.
Image processing functions for JARVIS
May be categorized under util later
"""
def __init__(self, bot: Snake):
self.bot = bot
self.logger = logging.getLogger(__name__)
self._session = aiohttp.ClientSession()
self.tgt_match = re.compile(r"([0-9]*\.?[0-9]*?) ?([KMGTP]?B?)", re.IGNORECASE)
@ -56,6 +58,7 @@ class ImageCog(Scale):
async def _resize(
self, ctx: InteractionContext, target: str, attachment: Attachment = None, url: str = None
) -> None:
await ctx.defer()
if not attachment and not url:
await ctx.send("A URL or attachment is required", ephemeral=True)
return
@ -71,10 +74,18 @@ class ImageCog(Scale):
)
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"):
await ctx.send("Target too large to send. Please make target < 8MB", ephemeral=True)
return
elif tgt_size < 1024:
await ctx.send("Sizes < 1KB are extremely unreliable and are disabled", ephemeral=True)
return
if attachment:
url = attachment.url
@ -83,13 +94,24 @@ class ImageCog(Scale):
filename = url.split("/")[-1]
data = None
async with self._session.get(url) as resp:
if resp.status == 200:
data = await resp.read()
try:
async with self._session.get(url) as resp:
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)
if size <= tgt_size:
await ctx.send("Image already meets target.")
await ctx.send("Image already meets target.", ephemeral=True)
return
ratio = max(tgt_size / size - 0.02, 0.50)
@ -130,5 +152,5 @@ class ImageCog(Scale):
def setup(bot: Snake) -> None:
"""Add ImageCog to J.A.R.V.I.S."""
"""Add ImageCog to JARVIS"""
ImageCog(bot)

View file

@ -1,10 +1,13 @@
"""J.A.R.V.I.S. Remind Me Cog."""
"""JARVIS Remind Me Cog."""
import asyncio
import logging
import re
from datetime import datetime, timedelta
from datetime import datetime, timezone
from typing import List
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.client.utils.misc_utils import get
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.snek.application_commands import (
OptionTypes,
SlashCommandChoice,
SlashCommand,
slash_command,
slash_option,
)
@ -31,25 +34,32 @@ invites = re.compile(
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_option(
name="private",
description="Send as DM?",
opt_type=OptionTypes.STRING,
opt_type=OptionTypes.BOOLEAN,
required=False,
choices=[
SlashCommandChoice(name="Yes", value="y"),
SlashCommandChoice(name="No", value="n"),
],
)
async def _remindme(
self,
ctx: InteractionContext,
private: str = "n",
private: bool = False,
) -> 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(
title="Set your reminder!",
components=[
@ -62,12 +72,13 @@ class RemindmeCog(Scale):
),
InputText(
label="When to remind you?",
placeholder="1h 30m",
placeholder="1h 30m | in 5 minutes | November 11, 4011",
style=TextStyles.SHORT,
custom_id="delay",
),
],
)
await ctx.send_modal(modal)
try:
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:
return
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
elif invites.search(message):
await ctx.send(
await response.send(
"Listen, don't use this to try and bypass the rules",
ephemeral=True,
)
return
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
units = {"w": "weeks", "d": "days", "h": "hours", "m": "minutes", "s": "seconds"}
delta = {"weeks": 0, "days": 0, "hours": 0, "minutes": 0, "seconds": 0}
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"]
]
if times := time_pattern.findall(delay):
for t in times:
delta[units[t[-1]]] += float(t[:-1])
rt_remind_at = parse(delay, settings=rt_settings)
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:
await ctx.send(
"Invalid time string, please follow example: `1w 3d 7h 5m 20s`", ephemeral=True
self.logger.debug(f"Failed to parse delay: {delay}")
await response.send(
f"`{delay}` is not a parsable date, please try again", ephemeral=True
)
return
if not any(value for value in delta.items()):
await ctx.send("At least one time period is required", ephemeral=True)
return
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,
if remind_at < datetime.now(tz=timezone.utc):
await response.send(
f"`{delay}` is in the past. Past reminders aren't allowed", ephemeral=True
)
return
remind_at = datetime.now() + timedelta(**delta)
elif remind_at < datetime.now(tz=timezone.utc):
pass
r = Reminder(
user=ctx.author.id,
@ -134,7 +154,7 @@ class RemindmeCog(Scale):
EmbedField(name="Message", value=message),
EmbedField(
name="When",
value=remind_at.strftime("%Y-%m-%d %H:%M UTC"),
value=f"<t:{int(remind_at.timestamp())}:F>",
inline=False,
),
],
@ -157,7 +177,7 @@ class RemindmeCog(Scale):
if reminder.private and isinstance(ctx.channel, GuildChannel):
fields.embed(
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",
inline=False,
)
@ -165,7 +185,7 @@ class RemindmeCog(Scale):
else:
fields.append(
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",
inline=False,
)
@ -185,17 +205,11 @@ class RemindmeCog(Scale):
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:
exists = self.check_cache(ctx)
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))
reminders = await Reminder.find(q(user=ctx.author.id, active=True)).to_list(None)
if not reminders:
await ctx.send("You have no reminders set.", ephemeral=True)
return
@ -204,9 +218,9 @@ class RemindmeCog(Scale):
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:
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:
await ctx.send("You have no reminders set", ephemeral=True)
return
@ -214,7 +228,7 @@ class RemindmeCog(Scale):
options = []
for reminder in reminders:
option = SelectOption(
label=reminder.remind_at.strftime("%Y-%m-%d %H:%M UTC"),
label=f"{reminder.remind_at}",
value=str(reminder.id),
emoji="",
)
@ -249,7 +263,7 @@ class RemindmeCog(Scale):
if reminder.private and isinstance(ctx.channel, GuildChannel):
fields.append(
EmbedField(
name=reminder.remind_at.strftime("%Y-%m-%d %H:%M UTC"),
name=f"<t:{int(reminder.remind_at.timestamp())}:F>",
value="Private reminder",
inline=False,
)
@ -257,12 +271,15 @@ class RemindmeCog(Scale):
else:
fields.append(
EmbedField(
name=reminder.remind_at.strftime("%Y-%m-%d %H:%M UTC"),
name=f"<t:{int(reminder.remind_at.timestamp())}:F>",
value=reminder.message,
inline=False,
)
)
await reminder.delete()
try:
await reminder.delete()
except Exception:
self.logger.debug("Ignoring deletion error")
for row in components:
for component in row.components:
@ -291,7 +308,34 @@ class RemindmeCog(Scale):
component.disabled = True
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:
"""Add RemindmeCog to J.A.R.V.I.S."""
"""Add RemindmeCog to JARVIS"""
RemindmeCog(bot)

View file

@ -1,5 +1,6 @@
"""J.A.R.V.I.S. Role Giver Cog."""
"""JARVIS Role Giver Cog."""
import asyncio
import logging
from dis_snek import InteractionContext, Permissions, Scale, Snake
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.snek.application_commands import (
OptionTypes,
slash_command,
SlashCommand,
slash_option,
)
from dis_snek.models.snek.command import check, cooldown
@ -21,17 +22,25 @@ from jarvis.utils.permissions import admin_or_permissions
class RolegiverCog(Scale):
"""J.A.R.V.I.S. Role Giver Cog."""
"""JARVIS Role Giver Cog."""
def __init__(self, bot: Snake):
self.bot = bot
self.logger = logging.getLogger(__name__)
@slash_command(
name="rolegiver", sub_cmd_name="add", sub_cmd_description="Add a role to rolegiver"
rolegiver = SlashCommand(name="rolegiver", description="Allow users to choose their own roles")
@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)
@check(admin_or_permissions(Permissions.MANAGE_GUILD))
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))
if setting and role.id in setting.roles:
await ctx.send("Role already in rolegiver", ephemeral=True)
@ -45,13 +54,13 @@ class RolegiverCog(Scale):
return
setting.roles.append(role.id)
setting.save()
await setting.commit()
roles = []
for role_id in setting.roles:
if role_id == role.id:
continue
e_role = await ctx.guild.get_role(role_id)
e_role = await ctx.guild.fetch_role(role_id)
if not e_role:
continue
roles.append(e_role)
@ -77,9 +86,7 @@ class RolegiverCog(Scale):
await ctx.send(embed=embed)
@slash_command(
name="rolegiver", sub_cmd_name="remove", sub_cmd_description="Remove a role from rolegiver"
)
@rolegiver.subcommand(sub_cmd_name="remove", sub_cmd_description="Remove a role from rolegiver")
@check(admin_or_permissions(Permissions.MANAGE_GUILD))
async def _rolegiver_remove(self, ctx: InteractionContext) -> None:
setting = await Rolegiver.find_one(q(guild=ctx.guild.id))
@ -89,7 +96,7 @@ class RolegiverCog(Scale):
options = []
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))
options.append(option)
@ -111,11 +118,11 @@ class RolegiverCog(Scale):
)
removed_roles = []
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:
removed_roles.append(role)
setting.roles.remove(int(to_delete))
setting.save()
await setting.commit()
for row in components:
for component in row.components:
@ -123,7 +130,7 @@ class RolegiverCog(Scale):
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:
continue
roles.append(e_role)
@ -162,7 +169,7 @@ class RolegiverCog(Scale):
component.disabled = True
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:
setting = await Rolegiver.find_one(q(guild=ctx.guild.id))
if not setting or (setting and not setting.roles):
@ -171,7 +178,7 @@ class RolegiverCog(Scale):
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:
continue
roles.append(e_role)
@ -197,7 +204,9 @@ class RolegiverCog(Scale):
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)
async def _role_get(self, ctx: InteractionContext) -> None:
setting = await Rolegiver.find_one(q(guild=ctx.guild.id))
@ -207,7 +216,7 @@ class RolegiverCog(Scale):
options = []
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))
options.append(option)
@ -230,7 +239,7 @@ class RolegiverCog(Scale):
added_roles = []
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)
await ctx.author.add_role(role, reason="Rolegiver")
@ -273,7 +282,7 @@ class RolegiverCog(Scale):
component.disabled = True
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)
async def _role_remove(self, ctx: InteractionContext) -> None:
user_roles = ctx.author.roles
@ -352,8 +361,8 @@ class RolegiverCog(Scale):
component.disabled = True
await message.edit(components=components)
@slash_command(
name="rolegiver", sub_cmd_name="cleanup", description="Removed deleted roles from rolegiver"
@rolegiver.subcommand(
sub_cmd_name="cleanup", sub_cmd_description="Removed deleted roles from rolegiver"
)
@check(admin_or_permissions(Permissions.MANAGE_GUILD))
async def _rolegiver_cleanup(self, ctx: InteractionContext) -> None:
@ -364,11 +373,11 @@ class RolegiverCog(Scale):
for role_id in setting.roles:
if role_id not in guild_role_ids:
setting.roles.remove(role_id)
setting.save()
await setting.commit()
await ctx.send("Rolegiver cleanup finished")
def setup(bot: Snake) -> None:
"""Add RolegiverCog to J.A.R.V.I.S."""
"""Add RolegiverCog to JARVIS"""
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 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 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.models import Setting
from jarvis.db.models import Setting
from jarvis.utils import build_embed
from jarvis.utils.field import Field
from jarvis.utils.permissions import admin_or_permissions
class SettingsCog(commands.Cog):
"""J.A.R.V.I.S. Settings Management Cog."""
class SettingsCog(Scale):
"""JARVIS Settings Management Cog."""
def __init__(self, bot: commands.Bot):
def __init__(self, bot: Snake):
self.bot = bot
self.logger = logging.getLogger(__name__)
async def update_settings(self, setting: str, value: Any, guild: int) -> bool:
"""Update a guild setting."""
@ -27,7 +35,7 @@ class SettingsCog(commands.Cog):
if not existing:
existing = Setting(setting=setting, guild=guild, value=value)
existing.value = value
updated = existing.save()
updated = await existing.commit()
return updated is not None
@ -38,204 +46,177 @@ class SettingsCog(commands.Cog):
return await existing.delete()
return False
@cog_ext.cog_subcommand(
base="settings",
subcommand_group="set",
name="modlog",
description="Set modlog channel",
choices=[
create_option(
name="channel",
description="Modlog channel",
opt_type=7,
required=True,
)
],
settings = SlashCommand(name="settings", description="Control guild settings")
set_ = settings.group(name="set", description="Set a setting")
unset = settings.group(name="unset", description="Unset a setting")
@set_.subcommand(
sub_cmd_name="modlog",
sub_cmd_description="Set Moglod channel",
)
@check(admin_or_permissions(manage_guild=True))
async def _set_modlog(self, ctx: SlashContext, channel: TextChannel) -> None:
if not isinstance(channel, TextChannel):
await ctx.send("Channel must be a TextChannel", ephemeral=True)
@slash_option(
name="channel", description="ModLog Channel", opt_type=OptionTypes.CHANNEL, required=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
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}")
@cog_ext.cog_subcommand(
base="settings",
subcommand_group="set",
name="activitylog",
description="Set activitylog channel",
choices=[
create_option(
name="channel",
description="Activitylog channel",
opt_type=7,
required=True,
)
],
@set_.subcommand(
sub_cmd_name="activitylog",
sub_cmd_description="Set Activitylog channel",
)
@check(admin_or_permissions(manage_guild=True))
async def _set_activitylog(self, ctx: SlashContext, channel: TextChannel) -> None:
if not isinstance(channel, TextChannel):
await ctx.send("Channel must be a TextChannel", ephemeral=True)
@slash_option(
name="channel",
description="Activitylog Channel",
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
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}")
@cog_ext.cog_subcommand(
base="settings",
subcommand_group="set",
name="massmention",
description="Set massmention amount",
choices=[
create_option(
name="amount",
description="Amount of mentions (0 to disable)",
opt_type=4,
required=True,
)
],
@set_.subcommand(sub_cmd_name="massmention", sub_cmd_description="Set massmention output")
@slash_option(
name="amount",
description="Amount of mentions (0 to disable)",
opt_type=OptionTypes.INTEGER,
required=True,
)
@check(admin_or_permissions(manage_guild=True))
async def _set_massmention(self, ctx: SlashContext, amount: int) -> None:
@check(admin_or_permissions(Permissions.MANAGE_GUILD))
async def _set_massmention(self, ctx: InteractionContext, amount: int) -> None:
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}")
@cog_ext.cog_subcommand(
base="settings",
subcommand_group="set",
name="verified",
description="Set verified role",
choices=[
create_option(
name="role",
description="verified role",
opt_type=8,
required=True,
)
],
@set_.subcommand(sub_cmd_name="verified", sub_cmd_description="Set verified role")
@slash_option(
name="role", description="Verified role", opt_type=OptionTypes.ROLE, required=True
)
@check(admin_or_permissions(manage_guild=True))
async def _set_verified(self, ctx: SlashContext, role: Role) -> None:
@check(admin_or_permissions(Permissions.MANAGE_GUILD))
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()
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}`")
@cog_ext.cog_subcommand(
base="settings",
subcommand_group="set",
name="unverified",
description="Set unverified role",
choices=[
create_option(
name="role",
description="Unverified role",
opt_type=8,
required=True,
)
],
@set_.subcommand(sub_cmd_name="unverified", sub_cmd_description="Set unverified role")
@slash_option(
name="role", description="Unverified role", opt_type=OptionTypes.ROLE, required=True
)
@check(admin_or_permissions(manage_guild=True))
async def _set_unverified(self, ctx: SlashContext, role: Role) -> None:
@check(admin_or_permissions(Permissions.MANAGE_GUILD))
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()
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}`")
@cog_ext.cog_subcommand(
base="settings",
subcommand_group="set",
name="noinvite",
description="Set if invite deletion should happen",
choices=[
create_option(
name="active",
description="Active?",
opt_type=4,
required=True,
)
],
@set_.subcommand(
sub_cmd_name="noinvite", sub_cmd_description="Set if invite deletion should happen"
)
@check(admin_or_permissions(manage_guild=True))
async def _set_invitedel(self, ctx: SlashContext, active: int) -> None:
@slash_option(name="active", description="Active?", opt_type=OptionTypes.BOOLEAN, required=True)
@check(admin_or_permissions(Permissions.MANAGE_GUILD))
async def _set_invitedel(self, ctx: InteractionContext, active: bool) -> None:
await ctx.defer()
self.update_settings("noinvite", bool(active), ctx.guild.id)
await ctx.send(f"Settings applied. Automatic invite active: {bool(active)}")
await self.update_settings("noinvite", active, ctx.guild.id)
await ctx.send(f"Settings applied. Automatic invite active: {active}")
@cog_ext.cog_subcommand(
base="settings",
subcommand_group="unset",
name="modlog",
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:
@set_.subcommand(sub_cmd_name="notify", sub_cmd_description="Notify users of admin action?")
@slash_option(name="active", description="Notify?", opt_type=OptionTypes.BOOLEAN, required=True)
@check(admin_or_permissions(Permissions.MANAGE_GUILD))
async def _set_notify(self, ctx: InteractionContext, active: bool) -> None:
await ctx.defer()
self.delete_settings("massmention", ctx.guild.id)
await ctx.send("Setting removed.")
await self.update_settings("notify", active, ctx.guild.id)
await ctx.send(f"Settings applied. Notifications active: {active}")
@cog_ext.cog_subcommand(
base="settings",
subcommand_group="unset",
name="verified",
description="Unset verified role",
# Unset
@unset.subcommand(
sub_cmd_name="modlog",
sub_cmd_description="Unset Modlog channel",
)
@check(admin_or_permissions(manage_guild=True))
async def _verified(self, ctx: SlashContext) -> None:
@check(admin_or_permissions(Permissions.MANAGE_GUILD))
async def _unset_modlog(self, ctx: InteractionContext) -> None:
await ctx.defer()
self.delete_settings("verified", ctx.guild.id)
await ctx.send("Setting removed.")
await self.delete_settings("modlog", ctx.guild.id)
await ctx.send("Setting `modlog` unset")
@cog_ext.cog_subcommand(
base="settings",
subcommand_group="unset",
name="unverified",
description="Unset unverified role",
@unset.subcommand(
sub_cmd_name="activitylog",
sub_cmd_description="Unset Activitylog channel",
)
@check(admin_or_permissions(manage_guild=True))
async def _unverified(self, ctx: SlashContext) -> None:
@check(admin_or_permissions(Permissions.MANAGE_GUILD))
async def _unset_activitylog(self, ctx: InteractionContext) -> None:
await ctx.defer()
self.delete_settings("unverified", ctx.guild.id)
await ctx.send("Setting removed.")
await self.delete_settings("activitylog", ctx.guild.id)
await ctx.send("Setting `activitylog` unset")
@cog_ext.cog_subcommand(base="settings", name="view", description="View settings")
@check(admin_or_permissions(manage_guild=True))
async def _view(self, ctx: SlashContext) -> None:
settings = Setting.objects(guild=ctx.guild.id)
@unset.subcommand(sub_cmd_name="massmention", sub_cmd_description="Unset massmention output")
@check(admin_or_permissions(Permissions.MANAGE_GUILD))
async def _unset_massmention(self, ctx: InteractionContext) -> None:
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 = []
for setting in settings:
async for setting in settings:
value = setting.value
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:
value = value.mention
else:
value = "||`[redacted]`||"
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:
value = value.mention
else:
@ -243,24 +224,51 @@ class SettingsCog(commands.Cog):
elif setting.setting == "rolegiver":
value = ""
for _role in setting.value:
nvalue = find(lambda x: x.id == value, ctx.guild.roles)
if value:
nvalue = await ctx.guild.fetch_role(_role)
if nvalue:
value += "\n" + nvalue.mention
else:
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)
await ctx.send(embed=embed)
@cog_ext.cog_subcommand(base="settings", name="clear", description="Clear all settings")
@check(admin_or_permissions(manage_guild=True))
async def _clear(self, ctx: SlashContext) -> None:
deleted = Setting.objects(guild=ctx.guild.id).delete()
await ctx.send(f"Guild settings cleared: `{deleted is not None}`")
@settings.subcommand(sub_cmd_name="clear", sub_cmd_description="Clear all settings")
@check(admin_or_permissions(Permissions.MANAGE_GUILD))
async def _clear(self, ctx: InteractionContext) -> None:
components = [
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:
"""Add SettingsCog to J.A.R.V.I.S."""
def setup(bot: Snake) -> None:
"""Add SettingsCog to JARVIS"""
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.client.utils.misc_utils import find
from dis_snek.models.discord.channel import GuildText
from dis_snek.models.discord.components import ActionRow, Select, SelectOption
from dis_snek.models.discord.message import Message
from dis_snek.models.snek.application_commands import (
CommandTypes,
OptionTypes,
SlashCommand,
context_menu,
slash_command,
slash_option,
)
from dis_snek.models.snek.command import check
@ -28,12 +29,18 @@ supported_images = [
class StarboardCog(Scale):
"""J.A.R.V.I.S. Starboard Cog."""
"""JARVIS Starboard Cog."""
def __init__(self, bot: Snake):
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))
async def _list(self, ctx: InteractionContext) -> None:
starboards = await Starboard.find(q(guild=ctx.guild.id)).to_list(None)
@ -45,9 +52,7 @@ class StarboardCog(Scale):
else:
await ctx.send("No Starboards available.")
@slash_command(
name="starboard", sub_cmd_name="create", sub_cmd_description="Create a starboard"
)
@starboard.subcommand(sub_cmd_name="create", sub_cmd_description="Create a starboard")
@slash_option(
name="channel",
description="Starboard channel",
@ -83,9 +88,7 @@ class StarboardCog(Scale):
).commit()
await ctx.send(f"Starboard created. Check it out at {channel.mention}.")
@slash_command(
name="starboard", sub_cmd_name="delete", sub_cmd_description="Delete a starboard"
)
@starboard.subcommand(sub_cmd_name="delete", sub_cmd_description="Delete a starboard")
@slash_option(
name="channel",
description="Starboard channel",
@ -126,8 +129,27 @@ class StarboardCog(Scale):
return
channel_list = []
to_delete = []
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 = [
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:
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(
name="message", description="Message to star", opt_type=OptionTypes.STRING, required=True
)
@ -237,7 +267,7 @@ class StarboardCog(Scale):
) -> None:
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(
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:
"""Add StarboardCog to J.A.R.V.I.S."""
"""Add StarboardCog to JARVIS"""
StarboardCog(bot)

View file

@ -1,5 +1,6 @@
"""J.A.R.V.I.S. Twitter Cog."""
"""JARVIS Twitter Cog."""
import asyncio
import logging
import tweepy
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.snek.application_commands import (
OptionTypes,
SlashCommandChoice,
slash_command,
SlashCommand,
slash_option,
)
from dis_snek.models.snek.command import check
from jarvis_core.db import q
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
class TwitterCog(Scale):
"""J.A.R.V.I.S. Twitter Cog."""
"""JARVIS Twitter Cog."""
def __init__(self, bot: Snake):
self.bot = bot
config = jconfig
self.logger = logging.getLogger(__name__)
config = JarvisConfig.from_yaml()
auth = tweepy.AppAuthHandler(
config.twitter["consumer_key"], config.twitter["consumer_secret"]
)
@ -33,7 +34,15 @@ class TwitterCog(Scale):
self._guild_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(
name="handle", description="Twitter account", opt_type=OptionTypes.STRING, required=True
)
@ -46,19 +55,14 @@ class TwitterCog(Scale):
@slash_option(
name="retweets",
description="Mirror re-tweets?",
opt_type=OptionTypes.STRING,
opt_type=OptionTypes.BOOLEAN,
required=False,
choices=[
SlashCommandChoice(name="Yes", value="Yes"),
SlashCommandChoice(name="No", value="No"),
],
)
@check(admin_or_permissions(Permissions.MANAGE_GUILD))
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:
handle = handle.lower()
retweets = retweets == "Yes"
if len(handle) > 15:
await ctx.send("Invalid Twitter handle", ephemeral=True)
return
@ -107,7 +111,7 @@ class TwitterCog(Scale):
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))
async def _twitter_unfollow(self, ctx: InteractionContext) -> None:
t = TwitterFollow.find(q(guild=ctx.guild.id))
@ -146,7 +150,10 @@ class TwitterCog(Scale):
)
for to_delete in context.context.values:
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 component in row.components:
component.disabled = True
@ -161,22 +168,18 @@ class TwitterCog(Scale):
component.disabled = True
await message.edit(components=components)
@slash_command(
name="twitter", sub_cmd_name="retweets", description="Modify followed Twitter accounts"
@twitter.subcommand(
sub_cmd_name="retweets",
sub_cmd_description="Modify followed Twitter accounts",
)
@slash_option(
name="retweets",
description="Mirror re-tweets?",
opt_type=OptionTypes.STRING,
opt_type=OptionTypes.BOOLEAN,
required=False,
choices=[
SlashCommandChoice(name="Yes", value="Yes"),
SlashCommandChoice(name="No", value="No"),
],
)
@check(admin_or_permissions(Permissions.MANAGE_GUILD))
async def _twitter_modify(self, ctx: InteractionContext, retweets: str) -> None:
retweets = retweets == "Yes"
async def _twitter_modify(self, ctx: InteractionContext, retweets: bool = True) -> None:
t = TwitterFollow.find(q(guild=ctx.guild.id))
twitters = []
async for twitter in t:
@ -241,5 +244,5 @@ class TwitterCog(Scale):
def setup(bot: Snake) -> None:
"""Add TwitterCog to J.A.R.V.I.S."""
"""Add TwitterCog to JARVIS"""
TwitterCog(bot)

View file

@ -1,30 +1,35 @@
"""J.A.R.V.I.S. Utility Cog."""
"""JARVIS Utility Cog."""
import logging
import platform
import re
import secrets
import string
from datetime import timezone
from io import BytesIO
import numpy as np
from dateparser import parse
from dis_snek import InteractionContext, Scale, Snake, const
from dis_snek.models.discord.channel import GuildCategory, GuildText, GuildVoice
from dis_snek.models.discord.embed import EmbedField
from dis_snek.models.discord.file import File
from dis_snek.models.discord.guild import Guild
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 (
CommandTypes,
OptionTypes,
SlashCommandChoice,
context_menu,
slash_command,
slash_option,
)
from dis_snek.models.snek.command import cooldown
from dis_snek.models.snek.cooldowns import Buckets
from PIL import Image
from tzlocal import get_localzone
import jarvis
from jarvis.config import get_config
from jarvis.data import pigpen
from jarvis.data.robotcamo import emotes, hk, names
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):
"""
Utility functions for J.A.R.V.I.S.
Utility functions for JARVIS
Mostly system utility functions, but may change over time
"""
def __init__(self, bot: Snake):
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)
async def _status(self, ctx: InteractionContext) -> None:
title = "J.A.R.V.I.S. Status"
desc = "All systems online"
title = "JARVIS Status"
desc = f"All systems online\nConnected to **{len(self.bot.guilds)}** guilds"
color = "#3498db"
fields = []
fields.append(EmbedField(name="dis-snek", value=const.__version__))
fields.append(EmbedField(name="Version", value=jarvis.__version__, 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)
await ctx.send(embed=embed)
@ -96,6 +107,8 @@ class UtilCog(Scale):
to_send += f":{names[id]}:"
if len(to_send) > 2000:
await ctx.send("Too long.", ephemeral=True)
elif len(to_send) == 0:
await ctx.send("No valid text found", ephemeral=True)
else:
await ctx.send(to_send)
@ -111,7 +124,10 @@ class UtilCog(Scale):
if not user:
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.set_image(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:
fields = [
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="Mention", value=f"`{role.mention}`", 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)
@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:
await ctx.defer()
if not user:
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
if user_roles:
user_roles = sorted(user.roles, key=lambda x: -x.position)
@ -215,6 +225,23 @@ class UtilCog(Scale):
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")
async def _server_info(self, ctx: InteractionContext) -> None:
guild: Guild = ctx.guild
@ -281,6 +308,7 @@ class UtilCog(Scale):
if length > 256:
await ctx.send("Please limit password to 256 characters", ephemeral=True)
return
choices = [
string.ascii_letters,
string.hexdigits,
@ -314,7 +342,39 @@ class UtilCog(Scale):
outp += "`"
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:
"""Add UtilCog to J.A.R.V.I.S."""
"""Add UtilCog to JARVIS"""
UtilCog(bot)

View file

@ -1,10 +1,11 @@
"""J.A.R.V.I.S. Verify Cog."""
"""JARVIS Verify Cog."""
import asyncio
import logging
from random import randint
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.snek.application_commands import slash_command
from dis_snek.models.snek.command import cooldown
from dis_snek.models.snek.cooldowns import Buckets
from jarvis_core.db import q
@ -30,21 +31,22 @@ def create_layout() -> list:
class VerifyCog(Scale):
"""J.A.R.V.I.S. Verify Cog."""
"""JARVIS Verify Cog."""
def __init__(self, bot: Snake):
self.bot = bot
self.logger = logging.getLogger(__name__)
@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:
await ctx.defer()
role = await Setting.find_one(q(guild=ctx.guild.id, setting="verified"))
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
if await ctx.guild.get_role(role.value) in ctx.author.roles:
await ctx.send("You are already verified.", delete_after=5)
if await ctx.guild.fetch_role(role.value) in ctx.author.roles:
await ctx.send("You are already verified.", ephemeral=True)
return
components = create_layout()
message = await ctx.send(
@ -53,39 +55,45 @@ class VerifyCog(Scale):
)
try:
context = await self.bot.wait_for_component(
messages=message, check=lambda x: ctx.author.id == x.author.id, timeout=30
)
correct = context.context.custom_id.split("||")[-1] == "yes"
if correct:
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,
verified = False
while not verified:
response = await self.bot.wait_for_component(
messages=message,
check=lambda x: ctx.author.id == x.context.author.id,
timeout=30,
)
await context.context.message.delete(delay=5)
else:
await context.context.edit_origin(
content=(
f"{ctx.author.mention}, incorrect. "
"Please press the button that says `YES`"
correct = response.context.custom_id.split("||")[-1] == "yes"
if correct:
for row in components:
for component in row.components:
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:
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:
"""Add VerifyCog to J.A.R.V.I.S."""
"""Add VerifyCog to JARVIS"""
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
from jarvis_core.config import Config as CConfig
@ -12,7 +12,7 @@ except ImportError:
class JarvisConfig(CConfig):
REQUIRED = ["token", "client_id", "mongo", "urls"]
REQUIRED = ["token", "mongo", "urls"]
OPTIONAL = {
"sync": False,
"log_level": "WARNING",
@ -25,7 +25,7 @@ class JarvisConfig(CConfig):
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):
"""Get the singleton config, or creates a new one."""
@ -39,8 +39,6 @@ class Config(object):
def init(
self,
token: str,
client_id: str,
logo: str,
mongo: dict,
urls: dict,
sync: bool = False,
@ -53,8 +51,6 @@ class Config(object):
) -> None:
"""Initialize the config object."""
self.token = token
self.client_id = client_id
self.logo = logo
self.mongo = mongo
self.urls = urls
self.log_level = log_level

View file

@ -1,5 +1,5 @@
"""J.A.R.V.I.S. Utility Functions."""
from datetime import datetime
"""JARVIS Utility Functions."""
from datetime import datetime, timezone
from pkgutil import iter_modules
import git
@ -23,7 +23,7 @@ def build_embed(
) -> Embed:
"""Embed builder utility function."""
if not timestamp:
timestamp = datetime.now()
timestamp = datetime.now(tz=timezone.utc)
embed = Embed(
title=title,
description=description,
@ -65,14 +65,14 @@ def modlog_embed(
def get_extensions(path: str = jarvis.cogs.__path__) -> list:
"""Get J.A.R.V.I.S. cogs."""
"""Get JARVIS cogs."""
config = get_config()
vals = config.cogs or [x.name for x in iter_modules(path)]
return ["jarvis.cogs.{}".format(x) for x in vals]
def update() -> int:
"""J.A.R.V.I.S. update utility."""
"""JARVIS update utility."""
repo = git.Repo(".")
dirty = repo.is_dirty()
current_hash = repo.head.object.hexsha
@ -87,6 +87,6 @@ def update() -> int:
def get_repo_hash() -> str:
"""J.A.R.V.I.S. current branch hash."""
"""JARVIS current branch hash."""
repo = git.Repo(".")
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:
"""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:
"""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"}
aiohttp = "^3.8.1"
pastypy = "^1.0.1"
[tool.poetry.dev-dependencies]
python-lsp-server = {extras = ["all"], version = "^1.3.3"}
black = "^22.1.0"
dateparser = "^1.1.1"
aiofile = "^3.7.4"
molter = "^0.11.0"
[build-system]
requires = ["poetry-core>=1.0.0"]