v2.0 Beta 1
Closes #82, #134, #132, and #131 See merge request stark-industries/jarvis/jarvis-bot!51
This commit is contained in:
commit
2aeb064f1d
35 changed files with 2001 additions and 1655 deletions
2
.flake8
2
.flake8
|
@ -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
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
228
jarvis/client.py
228
jarvis/client.py
|
@ -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}"
|
||||
)
|
||||
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 = 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:
|
||||
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()
|
||||
w = Warning(
|
||||
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,10 +475,11 @@ 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
|
||||
try:
|
||||
channel = before.guild.get_channel(modlog.value)
|
||||
fields = [
|
||||
EmbedField(
|
||||
|
@ -383,7 +495,7 @@ class Jarvis(Snake):
|
|||
]
|
||||
embed = build_embed(
|
||||
title="Message Edited",
|
||||
description=f"{after.author.mention} edited a message",
|
||||
description=f"{after.author.mention} edited a message in {before.channel.mention}",
|
||||
fields=fields,
|
||||
color="#fc9e3f",
|
||||
timestamp=after.edited_timestamp,
|
||||
|
@ -395,9 +507,13 @@ class Jarvis(Snake):
|
|||
url=after.jump_url,
|
||||
)
|
||||
embed.set_footer(
|
||||
text=f"{after.author.user.username}#{after.author.discriminator} | {after.author.id}"
|
||||
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,10 +527,15 @@ 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)]
|
||||
|
||||
try:
|
||||
if message.attachments:
|
||||
value = "\n".join([f"[{x.filename}]({x.url})" for x in message.attachments])
|
||||
fields.append(
|
||||
|
@ -448,7 +569,7 @@ class Jarvis(Snake):
|
|||
channel = message.guild.get_channel(modlog.value)
|
||||
embed = build_embed(
|
||||
title="Message Deleted",
|
||||
description=f"{message.author.mention}'s message was deleted",
|
||||
description=f"{message.author.mention}'s message was deleted from {message.channel.mention}",
|
||||
fields=fields,
|
||||
color="#fc9e3f",
|
||||
)
|
||||
|
@ -459,6 +580,13 @@ class Jarvis(Snake):
|
|||
url=message.jump_url,
|
||||
)
|
||||
embed.set_footer(
|
||||
text=f"{message.author.user.username}#{message.author.discriminator} | {message.author.id}"
|
||||
text=(
|
||||
f"{message.author.username}#{message.author.discriminator} | "
|
||||
f"{message.author.id}"
|
||||
)
|
||||
)
|
||||
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}"
|
||||
)
|
||||
|
|
|
@ -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"))
|
||||
|
|
|
@ -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]"
|
||||
|
|
|
@ -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
|
||||
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(
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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.")
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
|
||||
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
|
||||
|
|
|
@ -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(
|
||||
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)
|
||||
|
|
|
@ -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,11 +85,17 @@ 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()
|
||||
if emote:
|
||||
custom_emoji = self.custom_emote.match(emote)
|
||||
standard_emoji = emote in emoji_list
|
||||
if not custom_emoji and not standard_emoji:
|
||||
|
@ -91,31 +106,36 @@ class AutoReactCog(Scale):
|
|||
return
|
||||
if custom_emoji:
|
||||
emoji_id = int(custom_emoji.group(1))
|
||||
if not find(lambda x: x.id == emoji_id, ctx.guild.emojis):
|
||||
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
|
||||
if emote:
|
||||
autoreact.reactions.append(emote)
|
||||
autoreact.save()
|
||||
await ctx.send(f"Added {emote} to {channel.mention} autoreact.")
|
||||
autoreact.thread = thread
|
||||
await autoreact.commit()
|
||||
message = ""
|
||||
if emote:
|
||||
message += f" Added {emote} to {channel.mention} autoreact."
|
||||
message += f" Set autoreact thread creation to {thread} in {channel.mention}"
|
||||
await ctx.send(message)
|
||||
|
||||
@slash_command(
|
||||
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
54
jarvis/cogs/botutil.py
Normal 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)
|
|
@ -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)
|
||||
|
|
|
@ -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")
|
||||
|
||||
@slash_command(
|
||||
name="db",
|
||||
sub_cmd_name="grip",
|
||||
scopes=guild_ids,
|
||||
sub_cmd_description="See devices with Grip support",
|
||||
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"
|
||||
)
|
||||
@cooldown(bucket=Buckets.USER, rate=1, interval=30)
|
||||
async def _grip(self, ctx: InteractionContext) -> None:
|
||||
await ctx.send(self.base_url + "grip")
|
||||
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",
|
||||
@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)
|
||||
|
|
|
@ -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")
|
||||
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")
|
||||
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)
|
||||
|
|
|
@ -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)
|
|
@ -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
|
||||
|
||||
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
|
||||
try:
|
||||
async with self._session.get(url) as resp:
|
||||
if resp.status == 200:
|
||||
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)
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
)
|
||||
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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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",
|
||||
)
|
||||
],
|
||||
@slash_option(
|
||||
name="channel", description="ModLog Channel", opt_type=OptionTypes.CHANNEL, required=True
|
||||
)
|
||||
@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)
|
||||
@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(
|
||||
@set_.subcommand(
|
||||
sub_cmd_name="activitylog",
|
||||
sub_cmd_description="Set Activitylog channel",
|
||||
)
|
||||
@slash_option(
|
||||
name="channel",
|
||||
description="Activitylog channel",
|
||||
opt_type=7,
|
||||
description="Activitylog Channel",
|
||||
opt_type=OptionTypes.CHANNEL,
|
||||
required=True,
|
||||
)
|
||||
],
|
||||
)
|
||||
@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)
|
||||
@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(
|
||||
@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=4,
|
||||
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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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))
|
||||
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)
|
||||
|
|
|
@ -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.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)
|
||||
|
|
|
@ -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
|
||||
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,
|
||||
)
|
||||
|
||||
correct = context.context.custom_id.split("||")[-1] == "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(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")
|
||||
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.get_role(setting.value)
|
||||
await ctx.author.remove_roles(role, reason="Verification passed")
|
||||
role = await ctx.guild.fetch_role(setting.value)
|
||||
await ctx.author.remove_role(role, reason="Verification passed")
|
||||
|
||||
await context.context.edit_origin(
|
||||
await response.context.edit_origin(
|
||||
content=f"Welcome, {ctx.author.mention}. Please enjoy your stay.",
|
||||
components=components,
|
||||
)
|
||||
await context.context.message.delete(delay=5)
|
||||
await response.context.message.delete(delay=5)
|
||||
self.logger.debug(f"User {ctx.author.id} verified successfully")
|
||||
else:
|
||||
await context.context.edit_origin(
|
||||
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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
106
jarvis/utils/cogs.py
Normal 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)
|
|
@ -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
753
poetry.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -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"]
|
||||
|
|
Loading…
Add table
Reference in a new issue