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]
|
[flake8]
|
||||||
extend-ignore =
|
extend-ignore =
|
||||||
Q0, E501, C812, E203, W503, # These default to arguing with Black. We might configure some of them eventually
|
Q0, E501, C812, E203, W503, # These default to arguing with Black. We might configure some of them eventually
|
||||||
|
ANN002, ANN003, # Ignore *args, **kwargs
|
||||||
ANN1, # Ignore self and cls annotations
|
ANN1, # Ignore self and cls annotations
|
||||||
ANN204, ANN206, # return annotations for special methods and class methods
|
ANN204, ANN206, # return annotations for special methods and class methods
|
||||||
D105, D107, # Missing Docstrings in magic method and __init__
|
D105, D107, # Missing Docstrings in magic method and __init__
|
||||||
|
@ -10,5 +11,6 @@ extend-ignore =
|
||||||
D101, # Missing docstring in public class
|
D101, # Missing docstring in public class
|
||||||
|
|
||||||
# Plugins we don't currently include: flake8-return
|
# Plugins we don't currently include: flake8-return
|
||||||
|
R502, # do not implicitly return None in function able to return non-None value.
|
||||||
R503, # missing explicit return at the end of function ableto return non-None value.
|
R503, # missing explicit return at the end of function ableto return non-None value.
|
||||||
max-line-length=100
|
max-line-length=100
|
||||||
|
|
|
@ -19,7 +19,7 @@ repos:
|
||||||
- id: python-check-blanket-noqa
|
- id: python-check-blanket-noqa
|
||||||
|
|
||||||
- repo: https://github.com/psf/black
|
- repo: https://github.com/psf/black
|
||||||
rev: 22.1.0
|
rev: 22.3.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: black
|
- id: black
|
||||||
args: [--line-length=100, --target-version=py310]
|
args: [--line-length=100, --target-version=py310]
|
||||||
|
|
|
@ -1,24 +1,34 @@
|
||||||
---
|
---
|
||||||
token: api key here
|
token: bot token
|
||||||
client_id: 123456789012345678
|
twitter:
|
||||||
logo: alligator2
|
consumer_key: key
|
||||||
|
consumer_secret: secret
|
||||||
|
access_token: access token
|
||||||
|
access_secret: access secret
|
||||||
mongo:
|
mongo:
|
||||||
connect:
|
connect:
|
||||||
username: user
|
username: username
|
||||||
password: pass
|
password: password
|
||||||
host: localhost
|
host: hostname
|
||||||
port: 27017
|
port: 27017
|
||||||
database: database
|
database: database
|
||||||
urls:
|
urls:
|
||||||
url_name: url
|
extra: urls
|
||||||
url_name2: url2
|
max_messages: 10000
|
||||||
max_messages: 1000
|
gitlab_token: token
|
||||||
gitlab_token: null
|
|
||||||
cogs:
|
cogs:
|
||||||
- list
|
- admin
|
||||||
- of
|
- autoreact
|
||||||
- enabled
|
- dev
|
||||||
- cogs
|
- image
|
||||||
- all
|
- gl
|
||||||
- if
|
- remindme
|
||||||
- empty
|
- rolegiver
|
||||||
|
# - settings
|
||||||
|
- starboard
|
||||||
|
- twitter
|
||||||
|
- util
|
||||||
|
- verify
|
||||||
|
log_level: INFO
|
||||||
|
sync: false
|
||||||
|
#sync_commands: True
|
||||||
|
|
|
@ -1,41 +1,53 @@
|
||||||
"""Main J.A.R.V.I.S. package."""
|
"""Main JARVIS package."""
|
||||||
import logging
|
import logging
|
||||||
from importlib.metadata import version as _v
|
from importlib.metadata import version as _v
|
||||||
|
|
||||||
from dis_snek import Intents
|
from dis_snek import Intents
|
||||||
from jarvis_core.db import connect
|
from jarvis_core.db import connect
|
||||||
|
from jarvis_core.log import get_logger
|
||||||
|
|
||||||
from jarvis import utils
|
from jarvis import utils
|
||||||
from jarvis.client import Jarvis
|
from jarvis.client import Jarvis
|
||||||
from jarvis.config import get_config
|
from jarvis.config import JarvisConfig
|
||||||
|
|
||||||
try:
|
try:
|
||||||
__version__ = _v("jarvis")
|
__version__ = _v("jarvis")
|
||||||
except Exception:
|
except Exception:
|
||||||
__version__ = "0.0.0"
|
__version__ = "0.0.0"
|
||||||
|
|
||||||
jconfig = get_config()
|
jconfig = JarvisConfig.from_yaml()
|
||||||
|
logger = get_logger("jarvis")
|
||||||
logger = logging.getLogger("discord")
|
logger.setLevel(jconfig.log_level)
|
||||||
logger.setLevel(logging.getLevelName(jconfig.log_level))
|
|
||||||
file_handler = logging.FileHandler(filename="jarvis.log", encoding="UTF-8", mode="w")
|
file_handler = logging.FileHandler(filename="jarvis.log", encoding="UTF-8", mode="w")
|
||||||
file_handler.setFormatter(logging.Formatter("[%(asctime)s][%(levelname)s][%(name)s] %(message)s"))
|
file_handler.setFormatter(
|
||||||
|
logging.Formatter("[%(asctime)s] [%(name)s] [%(levelname)8s] %(message)s")
|
||||||
|
)
|
||||||
logger.addHandler(file_handler)
|
logger.addHandler(file_handler)
|
||||||
|
|
||||||
intents = Intents.DEFAULT | Intents.MESSAGES | Intents.GUILD_MEMBERS | Intents.GUILD_MESSAGES
|
intents = Intents.DEFAULT | Intents.MESSAGES | Intents.GUILD_MEMBERS | Intents.GUILD_MESSAGES
|
||||||
restart_ctx = None
|
restart_ctx = None
|
||||||
|
|
||||||
|
jarvis = Jarvis(
|
||||||
jarvis = Jarvis(intents=intents, default_prefix="!", sync_interactions=jconfig.sync)
|
intents=intents,
|
||||||
|
sync_interactions=jconfig.sync,
|
||||||
|
delete_unused_application_cmds=True,
|
||||||
|
send_command_tracebacks=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def run() -> None:
|
async def run() -> None:
|
||||||
"""Run J.A.R.V.I.S."""
|
"""Run JARVIS"""
|
||||||
|
logger.info("Starting JARVIS")
|
||||||
|
logger.debug("Connecting to database")
|
||||||
connect(**jconfig.mongo["connect"], testing=jconfig.mongo["database"] != "jarvis")
|
connect(**jconfig.mongo["connect"], testing=jconfig.mongo["database"] != "jarvis")
|
||||||
jconfig.get_db_config()
|
logger.debug("Loading configuration from database")
|
||||||
|
# jconfig.get_db_config()
|
||||||
|
|
||||||
|
logger.debug("Loading extensions")
|
||||||
for extension in utils.get_extensions():
|
for extension in utils.get_extensions():
|
||||||
jarvis.load_extension(extension)
|
jarvis.load_extension(extension)
|
||||||
|
logger.debug(f"Loaded {extension}")
|
||||||
|
|
||||||
jarvis.max_messages = jconfig.max_messages
|
jarvis.max_messages = jconfig.max_messages
|
||||||
|
logger.debug("Running JARVIS")
|
||||||
await jarvis.astart(jconfig.token)
|
await jarvis.astart(jconfig.token)
|
||||||
|
|
368
jarvis/client.py
368
jarvis/client.py
|
@ -1,18 +1,25 @@
|
||||||
"""Custom JARVIS client."""
|
"""Custom JARVIS client."""
|
||||||
|
import logging
|
||||||
import re
|
import re
|
||||||
import traceback
|
import traceback
|
||||||
from datetime import datetime
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
from aiohttp import ClientSession
|
from aiohttp import ClientSession
|
||||||
from dis_snek import Snake, listen
|
from dis_snek import Snake, listen
|
||||||
from dis_snek.api.events.discord import MessageCreate, MessageDelete, MessageUpdate
|
from dis_snek.api.events.discord import (
|
||||||
|
MemberAdd,
|
||||||
|
MemberRemove,
|
||||||
|
MessageCreate,
|
||||||
|
MessageDelete,
|
||||||
|
MessageUpdate,
|
||||||
|
)
|
||||||
|
from dis_snek.client.errors import CommandCheckFailure, CommandOnCooldown
|
||||||
from dis_snek.client.utils.misc_utils import find_all
|
from dis_snek.client.utils.misc_utils import find_all
|
||||||
from dis_snek.models.discord.channel import DMChannel
|
from dis_snek.models.discord.channel import DMChannel
|
||||||
from dis_snek.models.discord.embed import EmbedField
|
from dis_snek.models.discord.embed import EmbedField
|
||||||
from dis_snek.models.discord.enums import Permissions
|
from dis_snek.models.discord.enums import Permissions
|
||||||
from dis_snek.models.discord.message import Message
|
from dis_snek.models.discord.message import Message
|
||||||
from dis_snek.models.discord.user import Member
|
from dis_snek.models.snek.context import Context, InteractionContext, MessageContext
|
||||||
from dis_snek.models.snek.context import Context, InteractionContext
|
|
||||||
from dis_snek.models.snek.tasks.task import Task
|
from dis_snek.models.snek.tasks.task import Task
|
||||||
from dis_snek.models.snek.tasks.triggers import IntervalTrigger
|
from dis_snek.models.snek.tasks.triggers import IntervalTrigger
|
||||||
from jarvis_core.db import q
|
from jarvis_core.db import q
|
||||||
|
@ -30,6 +37,7 @@ DEFAULT_SITE = "https://paste.zevs.me"
|
||||||
|
|
||||||
ERROR_MSG = """
|
ERROR_MSG = """
|
||||||
Command Information:
|
Command Information:
|
||||||
|
Guild: {guild_name}
|
||||||
Name: {invoked_name}
|
Name: {invoked_name}
|
||||||
Args:
|
Args:
|
||||||
{arg_str}
|
{arg_str}
|
||||||
|
@ -49,15 +57,20 @@ CMD_FMT = fmt(Fore.GREEN, Format.BOLD)
|
||||||
class Jarvis(Snake):
|
class Jarvis(Snake):
|
||||||
def __init__(self, *args, **kwargs): # noqa: ANN002 ANN003
|
def __init__(self, *args, **kwargs): # noqa: ANN002 ANN003
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
self.logger = logging.getLogger(__name__)
|
||||||
self.phishing_domains = []
|
self.phishing_domains = []
|
||||||
|
self.pre_run_callback = self._prerun
|
||||||
|
|
||||||
@Task.create(IntervalTrigger(days=1))
|
@Task.create(IntervalTrigger(days=1))
|
||||||
async def _update_domains(self) -> None:
|
async def _update_domains(self) -> None:
|
||||||
|
self.logger.debug("Updating phishing domains")
|
||||||
async with ClientSession(headers={"X-Identity": "Discord: zevaryx#5779"}) as session:
|
async with ClientSession(headers={"X-Identity": "Discord: zevaryx#5779"}) as session:
|
||||||
response = await session.get("https://phish.sinking.yachts/v2/recent/86415")
|
response = await session.get("https://phish.sinking.yachts/v2/recent/86415")
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
data = await response.json()
|
data = await response.json()
|
||||||
|
|
||||||
|
self.logger.debug(f"Found {len(data)} changes to phishing domains")
|
||||||
|
|
||||||
for update in data:
|
for update in data:
|
||||||
if update["type"] == "add":
|
if update["type"] == "add":
|
||||||
if update["domain"] not in self.phishing_domains:
|
if update["domain"] not in self.phishing_domains:
|
||||||
|
@ -66,20 +79,29 @@ class Jarvis(Snake):
|
||||||
if update["domain"] in self.phishing_domains:
|
if update["domain"] in self.phishing_domains:
|
||||||
self.phishing_domains.remove(update["domain"])
|
self.phishing_domains.remove(update["domain"])
|
||||||
|
|
||||||
|
async def _prerun(self, ctx: Context, *args, **kwargs) -> None:
|
||||||
|
name = ctx.invoked_name
|
||||||
|
if isinstance(ctx, InteractionContext) and ctx.target_id:
|
||||||
|
kwargs["context target"] = ctx.target
|
||||||
|
args = " ".join(f"{k}:{v}" for k, v in kwargs.items())
|
||||||
|
self.logger.debug(f"Running command `{name}` with args: {args or 'None'}")
|
||||||
|
|
||||||
async def _sync_domains(self) -> None:
|
async def _sync_domains(self) -> None:
|
||||||
|
self.logger.debug("Loading phishing domains")
|
||||||
async with ClientSession(headers={"X-Identity": "Discord: zevaryx#5779"}) as session:
|
async with ClientSession(headers={"X-Identity": "Discord: zevaryx#5779"}) as session:
|
||||||
response = await session.get("https://phish.sinking.yachts/v2/all")
|
response = await session.get("https://phish.sinking.yachts/v2/all")
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
self.phishing_domains = await response.json()
|
self.phishing_domains = await response.json()
|
||||||
|
self.logger.info(f"Protected from {len(self.phishing_domains)} phishing domains")
|
||||||
|
|
||||||
@listen()
|
@listen()
|
||||||
async def on_ready(self) -> None:
|
async def on_ready(self) -> None:
|
||||||
"""Lepton on_ready override."""
|
"""Lepton on_ready override."""
|
||||||
await self._sync_domains()
|
await self._sync_domains()
|
||||||
self._update_domains.start()
|
self._update_domains.start()
|
||||||
print("Logged in as {}".format(self.user)) # noqa: T001
|
self.logger.info("Logged in as {}".format(self.user)) # noqa: T001
|
||||||
print("Connected to {} guild(s)".format(len(self.guilds))) # noqa: T001
|
self.logger.info("Connected to {} guild(s)".format(len(self.guilds))) # noqa: T001
|
||||||
print( # noqa: T001
|
self.logger.info( # noqa: T001
|
||||||
"https://discord.com/api/oauth2/authorize?client_id="
|
"https://discord.com/api/oauth2/authorize?client_id="
|
||||||
"{}&permissions=8&scope=bot%20applications.commands".format(self.user.id)
|
"{}&permissions=8&scope=bot%20applications.commands".format(self.user.id)
|
||||||
)
|
)
|
||||||
|
@ -88,37 +110,57 @@ class Jarvis(Snake):
|
||||||
self, ctx: Context, error: Exception, *args: list, **kwargs: dict
|
self, ctx: Context, error: Exception, *args: list, **kwargs: dict
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Lepton on_command_error override."""
|
"""Lepton on_command_error override."""
|
||||||
|
self.logger.debug(f"Handling error in {ctx.invoked_name}: {error}")
|
||||||
|
if isinstance(error, CommandOnCooldown):
|
||||||
|
await ctx.send(str(error), ephemeral=True)
|
||||||
|
return
|
||||||
|
elif isinstance(error, CommandCheckFailure):
|
||||||
|
await ctx.send("I'm afraid I can't let you do that", ephemeral=True)
|
||||||
|
return
|
||||||
guild = await self.fetch_guild(DEFAULT_GUILD)
|
guild = await self.fetch_guild(DEFAULT_GUILD)
|
||||||
channel = await guild.fetch_channel(DEFAULT_ERROR_CHANNEL)
|
channel = await guild.fetch_channel(DEFAULT_ERROR_CHANNEL)
|
||||||
error_time = datetime.utcnow().strftime("%d-%m-%Y %H:%M-%S.%f UTC")
|
error_time = datetime.now(tz=timezone.utc).strftime("%d-%m-%Y %H:%M-%S.%f UTC")
|
||||||
timestamp = int(datetime.now().timestamp())
|
timestamp = int(datetime.now(tz=timezone.utc).timestamp())
|
||||||
timestamp = f"<t:{timestamp}:T>"
|
timestamp = f"<t:{timestamp}:T>"
|
||||||
arg_str = (
|
arg_str = ""
|
||||||
"\n".join(f" {k}: {v}" for k, v in ctx.kwargs.items()) if ctx.kwargs else " None"
|
if isinstance(ctx, InteractionContext) and ctx.target_id:
|
||||||
)
|
ctx.kwargs["context target"] = ctx.target
|
||||||
|
if isinstance(ctx, InteractionContext):
|
||||||
|
for k, v in ctx.kwargs.items():
|
||||||
|
arg_str += f" {k}: "
|
||||||
|
if isinstance(v, str) and len(v) > 100:
|
||||||
|
v = v[97] + "..."
|
||||||
|
arg_str += f"{v}\n"
|
||||||
|
elif isinstance(ctx, MessageContext):
|
||||||
|
for v in ctx.args:
|
||||||
|
if isinstance(v, str) and len(v) > 100:
|
||||||
|
v = v[97] + "..."
|
||||||
|
arg_str += f" - {v}"
|
||||||
callback_args = "\n".join(f" - {i}" for i in args) if args else " None"
|
callback_args = "\n".join(f" - {i}" for i in args) if args else " None"
|
||||||
callback_kwargs = (
|
callback_kwargs = (
|
||||||
"\n".join(f" {k}: {v}" for k, v in kwargs.items()) if kwargs else " None"
|
"\n".join(f" {k}: {v}" for k, v in kwargs.items()) if kwargs else " None"
|
||||||
)
|
)
|
||||||
full_message = ERROR_MSG.format(
|
full_message = ERROR_MSG.format(
|
||||||
|
guild_name=ctx.guild.name,
|
||||||
error_time=error_time,
|
error_time=error_time,
|
||||||
invoked_name=ctx.invoked_name,
|
invoked_name=ctx.invoked_name,
|
||||||
arg_str=arg_str,
|
arg_str=arg_str,
|
||||||
callback_args=callback_args,
|
callback_args=callback_args,
|
||||||
callback_kwargs=callback_kwargs,
|
callback_kwargs=callback_kwargs,
|
||||||
)
|
)
|
||||||
if len(full_message) >= 1900:
|
error_message = "".join(traceback.format_exception(error))
|
||||||
error_message = " ".join(traceback.format_exception(error))
|
if len(full_message + error_message) >= 1800:
|
||||||
|
error_message = "\n ".join(error_message.split("\n"))
|
||||||
full_message += "Exception: |\n " + error_message
|
full_message += "Exception: |\n " + error_message
|
||||||
paste = Paste(content=full_message)
|
paste = Paste(content=full_message, site=DEFAULT_SITE)
|
||||||
await paste.save(DEFAULT_SITE)
|
key = await paste.save()
|
||||||
|
self.logger.debug(f"Large traceback, saved to Pasty {paste.id}, {key=}")
|
||||||
|
|
||||||
await channel.send(
|
await channel.send(
|
||||||
f"JARVIS encountered an error at {timestamp}. Log too big to send over Discord."
|
f"JARVIS encountered an error at {timestamp}. Log too big to send over Discord."
|
||||||
f"\nPlease see log at {paste.url}"
|
f"\nPlease see log at {paste.url}"
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
error_message = "".join(traceback.format_exception(error))
|
|
||||||
await channel.send(
|
await channel.send(
|
||||||
f"JARVIS encountered an error at {timestamp}:"
|
f"JARVIS encountered an error at {timestamp}:"
|
||||||
f"\n```yaml\n{full_message}\n```"
|
f"\n```yaml\n{full_message}\n```"
|
||||||
|
@ -128,13 +170,28 @@ class Jarvis(Snake):
|
||||||
return await super().on_command_error(ctx, error, *args, **kwargs)
|
return await super().on_command_error(ctx, error, *args, **kwargs)
|
||||||
|
|
||||||
# Modlog
|
# Modlog
|
||||||
async def on_command(self, ctx: InteractionContext) -> None:
|
async def on_command(self, ctx: Context) -> None:
|
||||||
"""Lepton on_command override."""
|
"""Lepton on_command override."""
|
||||||
if not isinstance(ctx.channel, DMChannel) and ctx.invoked_name not in ["pw"]:
|
if not isinstance(ctx.channel, DMChannel) and ctx.invoked_name not in ["pw"]:
|
||||||
modlog = await Setting.find_one(q(guild=ctx.guild.id, setting="modlog"))
|
modlog = await Setting.find_one(q(guild=ctx.guild.id, setting="activitylog"))
|
||||||
if modlog:
|
if modlog:
|
||||||
channel = await ctx.guild.fetch_channel(modlog.value)
|
channel = await ctx.guild.fetch_channel(modlog.value)
|
||||||
args = " ".join(f"{KEY_FMT}{k}:{VAL_FMT}{v}{RESET}" for k, v in ctx.kwargs.items())
|
args = []
|
||||||
|
if isinstance(ctx, InteractionContext) and ctx.target_id:
|
||||||
|
args.append(f"{KEY_FMT}context target:{VAL_FMT}{ctx.target}{RESET}")
|
||||||
|
if isinstance(ctx, InteractionContext):
|
||||||
|
for k, v in ctx.kwargs.items():
|
||||||
|
if isinstance(v, str):
|
||||||
|
v = v.replace("`", "\\`")
|
||||||
|
if len(v) > 100:
|
||||||
|
v = v[:97] + "..."
|
||||||
|
args.append(f"{KEY_FMT}{k}:{VAL_FMT}{v}{RESET}")
|
||||||
|
elif isinstance(ctx, MessageContext):
|
||||||
|
for v in ctx.args:
|
||||||
|
if isinstance(v, str) and len(v) > 100:
|
||||||
|
v = v[97] + "..."
|
||||||
|
args.append(f"{VAL_FMT}{v}{RESET}")
|
||||||
|
args = " ".join(args)
|
||||||
fields = [
|
fields = [
|
||||||
EmbedField(
|
EmbedField(
|
||||||
name="Command",
|
name="Command",
|
||||||
|
@ -144,7 +201,7 @@ class Jarvis(Snake):
|
||||||
]
|
]
|
||||||
embed = build_embed(
|
embed = build_embed(
|
||||||
title="Command Invoked",
|
title="Command Invoked",
|
||||||
description=f"{ctx.author.mention} invoked a command",
|
description=f"{ctx.author.mention} invoked a command in {ctx.channel.mention}",
|
||||||
fields=fields,
|
fields=fields,
|
||||||
color="#fc9e3f",
|
color="#fc9e3f",
|
||||||
)
|
)
|
||||||
|
@ -152,22 +209,54 @@ class Jarvis(Snake):
|
||||||
embed.set_footer(
|
embed.set_footer(
|
||||||
text=f"{ctx.author.user.username}#{ctx.author.discriminator} | {ctx.author.id}"
|
text=f"{ctx.author.user.username}#{ctx.author.discriminator} | {ctx.author.id}"
|
||||||
)
|
)
|
||||||
await channel.send(embed=embed)
|
if channel:
|
||||||
|
await channel.send(embed=embed)
|
||||||
|
else:
|
||||||
|
self.logger.warning(
|
||||||
|
f"Activitylog channel no longer exists in {ctx.guild.name}, removing"
|
||||||
|
)
|
||||||
|
await modlog.delete()
|
||||||
|
|
||||||
# Events
|
# Events
|
||||||
async def on_member_join(self, user: Member) -> None:
|
# Member
|
||||||
"""Handle on_member_join event."""
|
@listen()
|
||||||
guild = user.guild
|
async def on_member_add(self, event: MemberAdd) -> None:
|
||||||
|
"""Handle on_member_add event."""
|
||||||
|
user = event.member
|
||||||
|
guild = event.guild
|
||||||
unverified = await Setting.find_one(q(guild=guild.id, setting="unverified"))
|
unverified = await Setting.find_one(q(guild=guild.id, setting="unverified"))
|
||||||
if unverified:
|
if unverified:
|
||||||
role = guild.get_role(unverified.value)
|
self.logger.debug(f"Applying unverified role to {user.id} in {guild.id}")
|
||||||
|
role = await guild.fetch_role(unverified.value)
|
||||||
if role not in user.roles:
|
if role not in user.roles:
|
||||||
await user.add_role(role, reason="User just joined and is unverified")
|
await user.add_role(role, reason="User just joined and is unverified")
|
||||||
|
|
||||||
|
@listen()
|
||||||
|
async def on_member_remove(self, event: MemberRemove) -> None:
|
||||||
|
"""Handle on_member_remove event."""
|
||||||
|
user = event.member
|
||||||
|
guild = event.guild
|
||||||
|
log = await Setting.find_one(q(guild=guild.id, setting="activitylog"))
|
||||||
|
if log:
|
||||||
|
self.logger.debug(f"User {user.id} left {guild.id}")
|
||||||
|
channel = await guild.fetch_channel(log.channel)
|
||||||
|
embed = build_embed(
|
||||||
|
title="Member Left",
|
||||||
|
desciption=f"{user.username}#{user.discriminator} left {guild.name}",
|
||||||
|
fields=[],
|
||||||
|
)
|
||||||
|
embed.set_author(name=user.username, icon_url=user.avatar.url)
|
||||||
|
embed.set_footer(text=f"{user.username}#{user.discriminator} | {user.id}")
|
||||||
|
await channel.send(embed=embed)
|
||||||
|
|
||||||
|
# Message
|
||||||
async def autopurge(self, message: Message) -> None:
|
async def autopurge(self, message: Message) -> None:
|
||||||
"""Handle autopurge events."""
|
"""Handle autopurge events."""
|
||||||
autopurge = await Autopurge.find_one(q(guild=message.guild.id, channel=message.channel.id))
|
autopurge = await Autopurge.find_one(q(guild=message.guild.id, channel=message.channel.id))
|
||||||
if autopurge:
|
if autopurge:
|
||||||
|
self.logger.debug(
|
||||||
|
f"Autopurging message {message.guild.id}/{message.channel.id}/{message.id}"
|
||||||
|
)
|
||||||
await message.delete(delay=autopurge.delay)
|
await message.delete(delay=autopurge.delay)
|
||||||
|
|
||||||
async def autoreact(self, message: Message) -> None:
|
async def autoreact(self, message: Message) -> None:
|
||||||
|
@ -179,8 +268,16 @@ class Jarvis(Snake):
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
if autoreact:
|
if autoreact:
|
||||||
|
self.logger.debug(
|
||||||
|
f"Autoreacting to message {message.guild.id}/{message.channel.id}/{message.id}"
|
||||||
|
)
|
||||||
for reaction in autoreact.reactions:
|
for reaction in autoreact.reactions:
|
||||||
await message.add_reaction(reaction)
|
await message.add_reaction(reaction)
|
||||||
|
if autoreact.thread:
|
||||||
|
name = message.content
|
||||||
|
if len(name) > 100:
|
||||||
|
name = name[:97] + "..."
|
||||||
|
await message.create_thread(name=message.content, reason="Autoreact")
|
||||||
|
|
||||||
async def checks(self, message: Message) -> None:
|
async def checks(self, message: Message) -> None:
|
||||||
"""Other message checks."""
|
"""Other message checks."""
|
||||||
|
@ -197,24 +294,28 @@ class Jarvis(Snake):
|
||||||
setting = Setting(guild=message.guild.id, setting="noinvite", value=True)
|
setting = Setting(guild=message.guild.id, setting="noinvite", value=True)
|
||||||
await setting.commit()
|
await setting.commit()
|
||||||
if match:
|
if match:
|
||||||
guild_invites = await message.guild.invites()
|
guild_invites = await message.guild.fetch_invites()
|
||||||
guild_invites.append(message.guild.vanity_url_code)
|
if message.guild.vanity_url_code:
|
||||||
|
guild_invites.append(message.guild.vanity_url_code)
|
||||||
allowed = [x.code for x in guild_invites] + [
|
allowed = [x.code for x in guild_invites] + [
|
||||||
"dbrand",
|
"dbrand",
|
||||||
"VtgZntXcnZ",
|
"VtgZntXcnZ",
|
||||||
"gPfYGbvTCE",
|
"gPfYGbvTCE",
|
||||||
]
|
]
|
||||||
if match.group(1) not in allowed and setting.value:
|
if (m := match.group(1)) not in allowed and setting.value:
|
||||||
await message.delete()
|
self.logger.debug(f"Removing non-allowed invite `{m}` from {message.guild.id}")
|
||||||
w = Warning(
|
try:
|
||||||
|
await message.delete()
|
||||||
|
except Exception:
|
||||||
|
self.logger.debug("Message deleted before action taken")
|
||||||
|
await Warning(
|
||||||
active=True,
|
active=True,
|
||||||
admin=self.user.id,
|
admin=self.user.id,
|
||||||
duration=24,
|
duration=24,
|
||||||
guild=message.guild.id,
|
guild=message.guild.id,
|
||||||
reason="Sent an invite link",
|
reason="Sent an invite link",
|
||||||
user=message.author.id,
|
user=message.author.id,
|
||||||
)
|
).commit()
|
||||||
await w.commit()
|
|
||||||
embed = warning_embed(message.author, "Sent an invite link")
|
embed = warning_embed(message.author, "Sent an invite link")
|
||||||
await message.channel.send(embed=embed)
|
await message.channel.send(embed=embed)
|
||||||
|
|
||||||
|
@ -234,20 +335,24 @@ class Jarvis(Snake):
|
||||||
- (1 if message.author.id in message._mention_ids else 0) # noqa: W503
|
- (1 if message.author.id in message._mention_ids else 0) # noqa: W503
|
||||||
> massmention.value # noqa: W503
|
> massmention.value # noqa: W503
|
||||||
):
|
):
|
||||||
w = Warning(
|
self.logger.debug(
|
||||||
|
f"Massmention threshold on {message.guild.id}/{message.channel.id}/{message.id}"
|
||||||
|
)
|
||||||
|
await Warning(
|
||||||
active=True,
|
active=True,
|
||||||
admin=self.user.id,
|
admin=self.user.id,
|
||||||
duration=24,
|
duration=24,
|
||||||
guild=message.guild.id,
|
guild=message.guild.id,
|
||||||
reason="Mass Mention",
|
reason="Mass Mention",
|
||||||
user=message.author.id,
|
user=message.author.id,
|
||||||
)
|
).commit()
|
||||||
await w.commit()
|
|
||||||
embed = warning_embed(message.author, "Mass Mention")
|
embed = warning_embed(message.author, "Mass Mention")
|
||||||
await message.channel.send(embed=embed)
|
await message.channel.send(embed=embed)
|
||||||
|
|
||||||
async def roleping(self, message: Message) -> None:
|
async def roleping(self, message: Message) -> None:
|
||||||
"""Handle roleping events."""
|
"""Handle roleping events."""
|
||||||
|
if message.author.has_permission(Permissions.MANAGE_GUILD):
|
||||||
|
return
|
||||||
if await Roleping.collection.count_documents(q(guild=message.guild.id, active=True)) == 0:
|
if await Roleping.collection.count_documents(q(guild=message.guild.id, active=True)) == 0:
|
||||||
return
|
return
|
||||||
rolepings = await Roleping.find(q(guild=message.guild.id, active=True)).to_list(None)
|
rolepings = await Roleping.find(q(guild=message.guild.id, active=True)).to_list(None)
|
||||||
|
@ -286,31 +391,35 @@ class Jarvis(Snake):
|
||||||
break
|
break
|
||||||
|
|
||||||
if role_in_rolepings and user_missing_role and not user_is_admin and not user_has_bypass:
|
if role_in_rolepings and user_missing_role and not user_is_admin and not user_has_bypass:
|
||||||
w = Warning(
|
self.logger.debug(
|
||||||
|
f"Rolepinged role in {message.guild.id}/{message.channel.id}/{message.id}"
|
||||||
|
)
|
||||||
|
await Warning(
|
||||||
active=True,
|
active=True,
|
||||||
admin=self.user.id,
|
admin=self.user.id,
|
||||||
duration=24,
|
duration=24,
|
||||||
guild=message.guild.id,
|
guild=message.guild.id,
|
||||||
reason="Pinged a blocked role/user with a blocked role",
|
reason="Pinged a blocked role/user with a blocked role",
|
||||||
user=message.author.id,
|
user=message.author.id,
|
||||||
)
|
).commit()
|
||||||
await w.commit()
|
|
||||||
embed = warning_embed(message.author, "Pinged a blocked role/user with a blocked role")
|
embed = warning_embed(message.author, "Pinged a blocked role/user with a blocked role")
|
||||||
await message.channel.send(embed=embed)
|
await message.channel.send(embed=embed)
|
||||||
|
|
||||||
async def phishing(self, message: Message) -> None:
|
async def phishing(self, message: Message) -> None:
|
||||||
"""Check if the message contains any known phishing domains."""
|
"""Check if the message contains any known phishing domains."""
|
||||||
for match in url.finditer(message.content):
|
for match in url.finditer(message.content):
|
||||||
if match.group("domain") in self.phishing_domains:
|
if (m := match.group("domain")) in self.phishing_domains:
|
||||||
w = Warning(
|
self.logger.debug(
|
||||||
|
f"Phishing url `{m}` detected in {message.guild.id}/{message.channel.id}/{message.id}"
|
||||||
|
)
|
||||||
|
await Warning(
|
||||||
active=True,
|
active=True,
|
||||||
admin=self.user.id,
|
admin=self.user.id,
|
||||||
duration=24,
|
duration=24,
|
||||||
guild=message.guild.id,
|
guild=message.guild.id,
|
||||||
reason="Phishing URL",
|
reason="Phishing URL",
|
||||||
user=message.author.id,
|
user=message.author.id,
|
||||||
)
|
).commit()
|
||||||
await w.commit()
|
|
||||||
embed = warning_embed(message.author, "Phishing URL")
|
embed = warning_embed(message.author, "Phishing URL")
|
||||||
await message.channel.send(embed=embed)
|
await message.channel.send(embed=embed)
|
||||||
await message.delete()
|
await message.delete()
|
||||||
|
@ -329,15 +438,17 @@ class Jarvis(Snake):
|
||||||
data = await resp.json()
|
data = await resp.json()
|
||||||
for item in data["processed"]["urls"].values():
|
for item in data["processed"]["urls"].values():
|
||||||
if not item["safe"]:
|
if not item["safe"]:
|
||||||
w = Warning(
|
self.logger.debug(
|
||||||
|
f"Scam url `{match.string}` detected in {message.guild.id}/{message.channel.id}/{message.id}"
|
||||||
|
)
|
||||||
|
await Warning(
|
||||||
active=True,
|
active=True,
|
||||||
admin=self.user.id,
|
admin=self.user.id,
|
||||||
duration=24,
|
duration=24,
|
||||||
guild=message.guild.id,
|
guild=message.guild.id,
|
||||||
reason="Unsafe URL",
|
reason="Unsafe URL",
|
||||||
user=message.author.id,
|
user=message.author.id,
|
||||||
)
|
).commit()
|
||||||
await w.commit()
|
|
||||||
reasons = ", ".join(item["not_safe_reasons"])
|
reasons = ", ".join(item["not_safe_reasons"])
|
||||||
embed = warning_embed(message.author, reasons)
|
embed = warning_embed(message.author, reasons)
|
||||||
await message.channel.send(embed=embed)
|
await message.channel.send(embed=embed)
|
||||||
|
@ -364,40 +475,45 @@ class Jarvis(Snake):
|
||||||
before = event.before
|
before = event.before
|
||||||
after = event.after
|
after = event.after
|
||||||
if not after.author.bot:
|
if not after.author.bot:
|
||||||
modlog = await Setting.find_one(q(guild=after.guild.id, setting="modlog"))
|
modlog = await Setting.find_one(q(guild=after.guild.id, setting="activitylog"))
|
||||||
if modlog:
|
if modlog:
|
||||||
if not before or before.content == after.content or before.content is None:
|
if not before or before.content == after.content or before.content is None:
|
||||||
return
|
return
|
||||||
channel = before.guild.get_channel(modlog.value)
|
try:
|
||||||
fields = [
|
channel = before.guild.get_channel(modlog.value)
|
||||||
EmbedField(
|
fields = [
|
||||||
"Original Message",
|
EmbedField(
|
||||||
before.content if before.content else "N/A",
|
"Original Message",
|
||||||
False,
|
before.content if before.content else "N/A",
|
||||||
),
|
False,
|
||||||
EmbedField(
|
),
|
||||||
"New Message",
|
EmbedField(
|
||||||
after.content if after.content else "N/A",
|
"New Message",
|
||||||
False,
|
after.content if after.content else "N/A",
|
||||||
),
|
False,
|
||||||
]
|
),
|
||||||
embed = build_embed(
|
]
|
||||||
title="Message Edited",
|
embed = build_embed(
|
||||||
description=f"{after.author.mention} edited a message",
|
title="Message Edited",
|
||||||
fields=fields,
|
description=f"{after.author.mention} edited a message in {before.channel.mention}",
|
||||||
color="#fc9e3f",
|
fields=fields,
|
||||||
timestamp=after.edited_timestamp,
|
color="#fc9e3f",
|
||||||
url=after.jump_url,
|
timestamp=after.edited_timestamp,
|
||||||
)
|
url=after.jump_url,
|
||||||
embed.set_author(
|
)
|
||||||
name=after.author.username,
|
embed.set_author(
|
||||||
icon_url=after.author.display_avatar.url,
|
name=after.author.username,
|
||||||
url=after.jump_url,
|
icon_url=after.author.display_avatar.url,
|
||||||
)
|
url=after.jump_url,
|
||||||
embed.set_footer(
|
)
|
||||||
text=f"{after.author.user.username}#{after.author.discriminator} | {after.author.id}"
|
embed.set_footer(
|
||||||
)
|
text=f"{after.author.username}#{after.author.discriminator} | {after.author.id}"
|
||||||
await channel.send(embed=embed)
|
)
|
||||||
|
await channel.send(embed=embed)
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.warn(
|
||||||
|
f"Failed to process edit {before.guild.id}/{before.channel.id}/{before.id}: {e}"
|
||||||
|
)
|
||||||
if not isinstance(after.channel, DMChannel) and not after.author.bot:
|
if not isinstance(after.channel, DMChannel) and not after.author.bot:
|
||||||
await self.massmention(after)
|
await self.massmention(after)
|
||||||
await self.roleping(after)
|
await self.roleping(after)
|
||||||
|
@ -411,54 +527,66 @@ class Jarvis(Snake):
|
||||||
async def on_message_delete(self, event: MessageDelete) -> None:
|
async def on_message_delete(self, event: MessageDelete) -> None:
|
||||||
"""Process on_message_delete events."""
|
"""Process on_message_delete events."""
|
||||||
message = event.message
|
message = event.message
|
||||||
modlog = await Setting.find_one(q(guild=message.guild.id, setting="modlog"))
|
modlog = await Setting.find_one(q(guild=message.guild.id, setting="activitylog"))
|
||||||
if modlog:
|
if modlog:
|
||||||
fields = [EmbedField("Original Message", message.content or "N/A", False)]
|
try:
|
||||||
|
content = message.content or "N/A"
|
||||||
|
except AttributeError:
|
||||||
|
content = "N/A"
|
||||||
|
fields = [EmbedField("Original Message", content, False)]
|
||||||
|
|
||||||
if message.attachments:
|
try:
|
||||||
value = "\n".join([f"[{x.filename}]({x.url})" for x in message.attachments])
|
if message.attachments:
|
||||||
fields.append(
|
value = "\n".join([f"[{x.filename}]({x.url})" for x in message.attachments])
|
||||||
EmbedField(
|
fields.append(
|
||||||
name="Attachments",
|
EmbedField(
|
||||||
value=value,
|
name="Attachments",
|
||||||
inline=False,
|
value=value,
|
||||||
|
inline=False,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if message.sticker_items:
|
||||||
|
value = "\n".join([f"Sticker: {x.name}" for x in message.sticker_items])
|
||||||
|
fields.append(
|
||||||
|
EmbedField(
|
||||||
|
name="Stickers",
|
||||||
|
value=value,
|
||||||
|
inline=False,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if message.embeds:
|
||||||
|
value = str(len(message.embeds)) + " embeds"
|
||||||
|
fields.append(
|
||||||
|
EmbedField(
|
||||||
|
name="Embeds",
|
||||||
|
value=value,
|
||||||
|
inline=False,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
channel = message.guild.get_channel(modlog.value)
|
||||||
|
embed = build_embed(
|
||||||
|
title="Message Deleted",
|
||||||
|
description=f"{message.author.mention}'s message was deleted from {message.channel.mention}",
|
||||||
|
fields=fields,
|
||||||
|
color="#fc9e3f",
|
||||||
)
|
)
|
||||||
|
|
||||||
if message.sticker_items:
|
embed.set_author(
|
||||||
value = "\n".join([f"Sticker: {x.name}" for x in message.sticker_items])
|
name=message.author.username,
|
||||||
fields.append(
|
icon_url=message.author.display_avatar.url,
|
||||||
EmbedField(
|
url=message.jump_url,
|
||||||
name="Stickers",
|
)
|
||||||
value=value,
|
embed.set_footer(
|
||||||
inline=False,
|
text=(
|
||||||
|
f"{message.author.username}#{message.author.discriminator} | "
|
||||||
|
f"{message.author.id}"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
await channel.send(embed=embed)
|
||||||
if message.embeds:
|
except Exception as e:
|
||||||
value = str(len(message.embeds)) + " embeds"
|
self.logger.warn(
|
||||||
fields.append(
|
f"Failed to process edit {message.guild.id}/{message.channel.id}/{message.id}: {e}"
|
||||||
EmbedField(
|
|
||||||
name="Embeds",
|
|
||||||
value=value,
|
|
||||||
inline=False,
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
channel = message.guild.get_channel(modlog.value)
|
|
||||||
embed = build_embed(
|
|
||||||
title="Message Deleted",
|
|
||||||
description=f"{message.author.mention}'s message was deleted",
|
|
||||||
fields=fields,
|
|
||||||
color="#fc9e3f",
|
|
||||||
)
|
|
||||||
|
|
||||||
embed.set_author(
|
|
||||||
name=message.author.username,
|
|
||||||
icon_url=message.author.display_avatar.url,
|
|
||||||
url=message.jump_url,
|
|
||||||
)
|
|
||||||
embed.set_footer(
|
|
||||||
text=f"{message.author.user.username}#{message.author.discriminator} | {message.author.id}"
|
|
||||||
)
|
|
||||||
await channel.send(embed=embed)
|
|
||||||
|
|
|
@ -1,16 +1,28 @@
|
||||||
"""J.A.R.V.I.S. Admin Cogs."""
|
"""JARVIS Admin Cogs."""
|
||||||
|
import logging
|
||||||
|
|
||||||
from dis_snek import Snake
|
from dis_snek import Snake
|
||||||
|
|
||||||
from jarvis.cogs.admin import ban, kick, mute, purge, roleping, warning
|
from jarvis.cogs.admin import ban, kick, lock, lockdown, mute, purge, roleping, warning
|
||||||
|
|
||||||
|
|
||||||
def setup(bot: Snake) -> None:
|
def setup(bot: Snake) -> None:
|
||||||
"""Add admin cogs to J.A.R.V.I.S."""
|
"""Add admin cogs to JARVIS"""
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
msg = "Loaded jarvis.cogs.admin.{}"
|
||||||
ban.BanCog(bot)
|
ban.BanCog(bot)
|
||||||
|
logger.debug(msg.format("ban"))
|
||||||
kick.KickCog(bot)
|
kick.KickCog(bot)
|
||||||
# lock.LockCog(bot)
|
logger.debug(msg.format("kick"))
|
||||||
# lockdown.LockdownCog(bot)
|
lock.LockCog(bot)
|
||||||
|
logger.debug(msg.format("lock"))
|
||||||
|
lockdown.LockdownCog(bot)
|
||||||
|
logger.debug(msg.format("ban"))
|
||||||
mute.MuteCog(bot)
|
mute.MuteCog(bot)
|
||||||
|
logger.debug(msg.format("mute"))
|
||||||
purge.PurgeCog(bot)
|
purge.PurgeCog(bot)
|
||||||
|
logger.debug(msg.format("purge"))
|
||||||
roleping.RolepingCog(bot)
|
roleping.RolepingCog(bot)
|
||||||
|
logger.debug(msg.format("roleping"))
|
||||||
warning.WarningCog(bot)
|
warning.WarningCog(bot)
|
||||||
|
logger.debug(msg.format("warning"))
|
||||||
|
|
|
@ -1,13 +1,15 @@
|
||||||
"""J.A.R.V.I.S. BanCog."""
|
"""JARVIS BanCog."""
|
||||||
|
import logging
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from dis_snek import InteractionContext, Permissions, Scale
|
from dis_snek import InteractionContext, Permissions, Snake
|
||||||
from dis_snek.client.utils.misc_utils import find, find_all
|
from dis_snek.client.utils.misc_utils import find, find_all
|
||||||
from dis_snek.ext.paginators import Paginator
|
from dis_snek.ext.paginators import Paginator
|
||||||
from dis_snek.models.discord.embed import EmbedField
|
from dis_snek.models.discord.embed import EmbedField
|
||||||
from dis_snek.models.discord.user import User
|
from dis_snek.models.discord.user import User
|
||||||
from dis_snek.models.snek.application_commands import (
|
from dis_snek.models.snek.application_commands import (
|
||||||
OptionTypes,
|
OptionTypes,
|
||||||
|
SlashCommand,
|
||||||
SlashCommandChoice,
|
SlashCommandChoice,
|
||||||
slash_command,
|
slash_command,
|
||||||
slash_option,
|
slash_option,
|
||||||
|
@ -17,11 +19,16 @@ from jarvis_core.db import q
|
||||||
from jarvis_core.db.models import Ban, Unban
|
from jarvis_core.db.models import Ban, Unban
|
||||||
|
|
||||||
from jarvis.utils import build_embed
|
from jarvis.utils import build_embed
|
||||||
|
from jarvis.utils.cogs import ModcaseCog
|
||||||
from jarvis.utils.permissions import admin_or_permissions
|
from jarvis.utils.permissions import admin_or_permissions
|
||||||
|
|
||||||
|
|
||||||
class BanCog(Scale):
|
class BanCog(ModcaseCog):
|
||||||
"""J.A.R.V.I.S. BanCog."""
|
"""JARVIS BanCog."""
|
||||||
|
|
||||||
|
def __init__(self, bot: Snake):
|
||||||
|
super().__init__(bot)
|
||||||
|
self.logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
async def discord_apply_ban(
|
async def discord_apply_ban(
|
||||||
self,
|
self,
|
||||||
|
@ -56,9 +63,9 @@ class BanCog(Scale):
|
||||||
|
|
||||||
embed.set_author(
|
embed.set_author(
|
||||||
name=user.display_name,
|
name=user.display_name,
|
||||||
icon_url=user.avatar,
|
icon_url=user.avatar.url,
|
||||||
)
|
)
|
||||||
embed.set_thumbnail(url=user.avatar)
|
embed.set_thumbnail(url=user.avatar.url)
|
||||||
embed.set_footer(text=f"{user.username}#{user.discriminator} | {user.id}")
|
embed.set_footer(text=f"{user.username}#{user.discriminator} | {user.id}")
|
||||||
|
|
||||||
await ctx.send(embed=embed)
|
await ctx.send(embed=embed)
|
||||||
|
@ -83,9 +90,9 @@ class BanCog(Scale):
|
||||||
)
|
)
|
||||||
embed.set_author(
|
embed.set_author(
|
||||||
name=user.username,
|
name=user.username,
|
||||||
icon_url=user.avatar,
|
icon_url=user.avatar.url,
|
||||||
)
|
)
|
||||||
embed.set_thumbnail(url=user.avatar)
|
embed.set_thumbnail(url=user.avatar.url)
|
||||||
embed.set_footer(text=f"{user.username}#{user.discriminator} | {user.id}")
|
embed.set_footer(text=f"{user.username}#{user.discriminator} | {user.id}")
|
||||||
await ctx.send(embed=embed)
|
await ctx.send(embed=embed)
|
||||||
|
|
||||||
|
@ -105,19 +112,25 @@ class BanCog(Scale):
|
||||||
SlashCommandChoice(name="Soft", value="soft"),
|
SlashCommandChoice(name="Soft", value="soft"),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
@slash_option(
|
||||||
|
name="duration",
|
||||||
|
description="Temp ban duration in hours",
|
||||||
|
opt_type=OptionTypes.INTEGER,
|
||||||
|
required=False,
|
||||||
|
)
|
||||||
@check(admin_or_permissions(Permissions.BAN_MEMBERS))
|
@check(admin_or_permissions(Permissions.BAN_MEMBERS))
|
||||||
async def _ban(
|
async def _ban(
|
||||||
self,
|
self,
|
||||||
ctx: InteractionContext,
|
ctx: InteractionContext,
|
||||||
|
user: User,
|
||||||
reason: str,
|
reason: str,
|
||||||
user: User = None,
|
|
||||||
btype: str = "perm",
|
btype: str = "perm",
|
||||||
duration: int = 4,
|
duration: int = 4,
|
||||||
) -> None:
|
) -> None:
|
||||||
if not user or user == ctx.author:
|
if user.id == ctx.author.id:
|
||||||
await ctx.send("You cannot ban yourself.", ephemeral=True)
|
await ctx.send("You cannot ban yourself.", ephemeral=True)
|
||||||
return
|
return
|
||||||
if user == self.bot.user:
|
if user.id == self.bot.user.id:
|
||||||
await ctx.send("I'm afraid I can't let you do that", ephemeral=True)
|
await ctx.send("I'm afraid I can't let you do that", ephemeral=True)
|
||||||
return
|
return
|
||||||
if btype == "temp" and duration < 0:
|
if btype == "temp" and duration < 0:
|
||||||
|
@ -203,9 +216,10 @@ class BanCog(Scale):
|
||||||
discord_ban_info = None
|
discord_ban_info = None
|
||||||
database_ban_info = None
|
database_ban_info = None
|
||||||
|
|
||||||
bans = await ctx.guild.bans()
|
bans = await ctx.guild.fetch_bans()
|
||||||
|
|
||||||
# Try to get ban information out of Discord
|
# Try to get ban information out of Discord
|
||||||
|
self.logger.debug(f"{user}")
|
||||||
if re.match(r"^[0-9]{1,}$", user): # User ID
|
if re.match(r"^[0-9]{1,}$", user): # User ID
|
||||||
user = int(user)
|
user = int(user)
|
||||||
discord_ban_info = find(lambda x: x.user.id == user, bans)
|
discord_ban_info = find(lambda x: x.user.id == user, bans)
|
||||||
|
@ -240,9 +254,9 @@ class BanCog(Scale):
|
||||||
# try to find the relevant information in the database.
|
# try to find the relevant information in the database.
|
||||||
# We take advantage of the previous checks to save CPU cycles
|
# We take advantage of the previous checks to save CPU cycles
|
||||||
if not discord_ban_info:
|
if not discord_ban_info:
|
||||||
if isinstance(user, int):
|
if isinstance(user, User):
|
||||||
database_ban_info = await Ban.find_one(
|
database_ban_info = await Ban.find_one(
|
||||||
q(guild=ctx.guild.id, user=user, active=True)
|
q(guild=ctx.guild.id, user=user.id, active=True)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
search = {
|
search = {
|
||||||
|
@ -277,9 +291,9 @@ class BanCog(Scale):
|
||||||
).save()
|
).save()
|
||||||
await ctx.send("Unable to find user in Discord, but removed entry from database.")
|
await ctx.send("Unable to find user in Discord, but removed entry from database.")
|
||||||
|
|
||||||
@slash_command(
|
bans = SlashCommand(name="bans", description="User bans")
|
||||||
name="bans", description="User bans", sub_cmd_name="list", sub_cmd_description="List bans"
|
|
||||||
)
|
@bans.subcommand(sub_cmd_name="list", sub_cmd_description="List bans")
|
||||||
@slash_option(
|
@slash_option(
|
||||||
name="btype",
|
name="btype",
|
||||||
description="Ban type",
|
description="Ban type",
|
||||||
|
@ -295,23 +309,23 @@ class BanCog(Scale):
|
||||||
@slash_option(
|
@slash_option(
|
||||||
name="active",
|
name="active",
|
||||||
description="Active bans",
|
description="Active bans",
|
||||||
opt_type=OptionTypes.INTEGER,
|
opt_type=OptionTypes.BOOLEAN,
|
||||||
required=False,
|
required=False,
|
||||||
choices=[SlashCommandChoice(name="Yes", value=1), SlashCommandChoice(name="No", value=0)],
|
|
||||||
)
|
)
|
||||||
@check(admin_or_permissions(Permissions.BAN_MEMBERS))
|
@check(admin_or_permissions(Permissions.BAN_MEMBERS))
|
||||||
async def _bans_list(self, ctx: InteractionContext, btype: int = 0, active: int = 1) -> None:
|
async def _bans_list(
|
||||||
active = bool(active)
|
self, ctx: InteractionContext, btype: int = 0, active: bool = True
|
||||||
|
) -> None:
|
||||||
types = [0, "perm", "temp", "soft"]
|
types = [0, "perm", "temp", "soft"]
|
||||||
search = {"guild": ctx.guild.id}
|
search = {"guild": ctx.guild.id}
|
||||||
if active:
|
if active:
|
||||||
search["active"] = True
|
search["active"] = True
|
||||||
if btype > 0:
|
if btype > 0:
|
||||||
search["type"] = types[btype]
|
search["type"] = types[btype]
|
||||||
bans = Ban.find(search).sort([("created_at", -1)])
|
bans = await Ban.find(search).sort([("created_at", -1)]).to_list(None)
|
||||||
db_bans = []
|
db_bans = []
|
||||||
fields = []
|
fields = []
|
||||||
async for ban in bans:
|
for ban in bans:
|
||||||
if not ban.username:
|
if not ban.username:
|
||||||
user = await self.bot.fetch_user(ban.user)
|
user = await self.bot.fetch_user(ban.user)
|
||||||
ban.username = user.username if user else "[deleted user]"
|
ban.username = user.username if user else "[deleted user]"
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
"""J.A.R.V.I.S. KickCog."""
|
"""JARVIS KickCog."""
|
||||||
from dis_snek import InteractionContext, Permissions, Scale
|
import logging
|
||||||
|
|
||||||
|
from dis_snek import InteractionContext, Permissions, Snake
|
||||||
from dis_snek.models.discord.embed import EmbedField
|
from dis_snek.models.discord.embed import EmbedField
|
||||||
from dis_snek.models.discord.user import User
|
from dis_snek.models.discord.user import User
|
||||||
from dis_snek.models.snek.application_commands import (
|
from dis_snek.models.snek.application_commands import (
|
||||||
|
@ -11,11 +13,16 @@ from dis_snek.models.snek.command import check
|
||||||
from jarvis_core.db.models import Kick
|
from jarvis_core.db.models import Kick
|
||||||
|
|
||||||
from jarvis.utils import build_embed
|
from jarvis.utils import build_embed
|
||||||
|
from jarvis.utils.cogs import ModcaseCog
|
||||||
from jarvis.utils.permissions import admin_or_permissions
|
from jarvis.utils.permissions import admin_or_permissions
|
||||||
|
|
||||||
|
|
||||||
class KickCog(Scale):
|
class KickCog(ModcaseCog):
|
||||||
"""J.A.R.V.I.S. KickCog."""
|
"""JARVIS KickCog."""
|
||||||
|
|
||||||
|
def __init__(self, bot: Snake):
|
||||||
|
super().__init__(bot)
|
||||||
|
self.logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@slash_command(name="kick", description="Kick a user")
|
@slash_command(name="kick", description="Kick a user")
|
||||||
@slash_option(name="user", description="User to kick", opt_type=OptionTypes.USER, required=True)
|
@slash_option(name="user", description="User to kick", opt_type=OptionTypes.USER, required=True)
|
||||||
|
@ -52,7 +59,11 @@ class KickCog(Scale):
|
||||||
await user.send(embed=embed)
|
await user.send(embed=embed)
|
||||||
except Exception:
|
except Exception:
|
||||||
send_failed = True
|
send_failed = True
|
||||||
await ctx.guild.kick(user, reason=reason)
|
try:
|
||||||
|
await ctx.guild.kick(user, reason=reason)
|
||||||
|
except Exception as e:
|
||||||
|
await ctx.send(f"Failed to kick user:\n```\n{e}\n```", ephemeral=True)
|
||||||
|
return
|
||||||
|
|
||||||
fields = [EmbedField(name="DM Sent?", value=str(not send_failed))]
|
fields = [EmbedField(name="DM Sent?", value=str(not send_failed))]
|
||||||
embed = build_embed(
|
embed = build_embed(
|
||||||
|
|
|
@ -1,113 +1,118 @@
|
||||||
"""J.A.R.V.I.S. LockCog."""
|
"""JARVIS LockCog."""
|
||||||
# from dis_snek import Scale
|
import logging
|
||||||
#
|
from typing import Union
|
||||||
# # TODO: Uncomment 99% of code once implementation is figured out
|
|
||||||
# from contextlib import suppress
|
from dis_snek import InteractionContext, Scale, Snake
|
||||||
# from typing import Union
|
from dis_snek.client.utils.misc_utils import get
|
||||||
#
|
from dis_snek.models.discord.channel import GuildText, GuildVoice
|
||||||
# from dis_snek import InteractionContext, Scale, Snake
|
from dis_snek.models.discord.enums import Permissions
|
||||||
# from dis_snek.models.discord.enums import Permissions
|
from dis_snek.models.snek.application_commands import (
|
||||||
# from dis_snek.models.discord.role import Role
|
OptionTypes,
|
||||||
# from dis_snek.models.discord.user import User
|
slash_command,
|
||||||
# from dis_snek.models.discord.channel import GuildText, GuildVoice, PermissionOverwrite
|
slash_option,
|
||||||
# from dis_snek.models.snek.application_commands import (
|
)
|
||||||
# OptionTypes,
|
from dis_snek.models.snek.command import check
|
||||||
# PermissionTypes,
|
from jarvis_core.db import q
|
||||||
# slash_command,
|
from jarvis_core.db.models import Lock, Permission
|
||||||
# slash_option,
|
|
||||||
# )
|
from jarvis.utils.permissions import admin_or_permissions
|
||||||
# from dis_snek.models.snek.command import check
|
|
||||||
#
|
|
||||||
# from jarvis.db.models import Lock
|
class LockCog(Scale):
|
||||||
# from jarvis.utils.permissions import admin_or_permissions
|
"""JARVIS LockCog."""
|
||||||
#
|
|
||||||
#
|
def __init__(self, bot: Snake):
|
||||||
# class LockCog(Scale):
|
self.bot = bot
|
||||||
# """J.A.R.V.I.S. LockCog."""
|
self.logger = logging.getLogger(__name__)
|
||||||
#
|
|
||||||
# @slash_command(name="lock", description="Lock a channel")
|
@slash_command(name="lock", description="Lock a channel")
|
||||||
# @slash_option(name="reason",
|
@slash_option(
|
||||||
# description="Lock Reason",
|
name="reason",
|
||||||
# opt_type=3,
|
description="Lock Reason",
|
||||||
# required=True,)
|
opt_type=3,
|
||||||
# @slash_option(name="duration",
|
required=True,
|
||||||
# description="Lock duration in minutes (default 10)",
|
)
|
||||||
# opt_type=4,
|
@slash_option(
|
||||||
# required=False,)
|
name="duration",
|
||||||
# @slash_option(name="channel",
|
description="Lock duration in minutes (default 10)",
|
||||||
# description="Channel to lock",
|
opt_type=4,
|
||||||
# opt_type=7,
|
required=False,
|
||||||
# required=False,)
|
)
|
||||||
# @check(admin_or_permissions(Permissions.MANAGE_CHANNELS))
|
@slash_option(
|
||||||
# async def _lock(
|
name="channel",
|
||||||
# self,
|
description="Channel to lock",
|
||||||
# ctx: InteractionContext,
|
opt_type=7,
|
||||||
# reason: str,
|
required=False,
|
||||||
# duration: int = 10,
|
)
|
||||||
# channel: Union[GuildText, GuildVoice] = None,
|
@check(admin_or_permissions(Permissions.MANAGE_CHANNELS))
|
||||||
# ) -> None:
|
async def _lock(
|
||||||
# await ctx.defer(ephemeral=True)
|
self,
|
||||||
# if duration <= 0:
|
ctx: InteractionContext,
|
||||||
# await ctx.send("Duration must be > 0", ephemeral=True)
|
reason: str,
|
||||||
# return
|
duration: int = 10,
|
||||||
#
|
channel: Union[GuildText, GuildVoice] = None,
|
||||||
# elif duration > 60 * 12:
|
) -> None:
|
||||||
# await ctx.send("Duration must be <= 12 hours", ephemeral=True)
|
await ctx.defer(ephemeral=True)
|
||||||
# return
|
if duration <= 0:
|
||||||
#
|
await ctx.send("Duration must be > 0", ephemeral=True)
|
||||||
# if len(reason) > 100:
|
return
|
||||||
# await ctx.send("Reason must be <= 100 characters", ephemeral=True)
|
|
||||||
# return
|
elif duration > 60 * 12:
|
||||||
# if not channel:
|
await ctx.send("Duration must be <= 12 hours", ephemeral=True)
|
||||||
# channel = ctx.channel
|
return
|
||||||
#
|
|
||||||
# # role = ctx.guild.default_role # Uncomment once implemented
|
if len(reason) > 100:
|
||||||
# if isinstance(channel, GuildText):
|
await ctx.send("Reason must be <= 100 characters", ephemeral=True)
|
||||||
# to_deny = Permissions.SEND_MESSAGES
|
return
|
||||||
# elif isinstance(channel, GuildVoice):
|
if not channel:
|
||||||
# to_deny = Permissions.CONNECT | Permissions.SPEAK
|
channel = ctx.channel
|
||||||
#
|
|
||||||
# overwrite = PermissionOverwrite(type=PermissionTypes.ROLE, deny=to_deny)
|
to_deny = Permissions.CONNECT | Permissions.SPEAK | Permissions.SEND_MESSAGES
|
||||||
# # TODO: Get original permissions
|
|
||||||
# # TODO: Apply overwrite
|
current = get(channel.permission_overwrites, id=ctx.guild.id)
|
||||||
# overwrite = overwrite
|
if current:
|
||||||
# _ = Lock(
|
current = Permission(id=ctx.guild.id, allow=int(current.allow), deny=int(current.deny))
|
||||||
# channel=channel.id,
|
role = await ctx.guild.fetch_role(ctx.guild.id)
|
||||||
# guild=ctx.guild.id,
|
|
||||||
# admin=ctx.author.id,
|
await channel.add_permission(target=role, deny=to_deny, reason="Locked")
|
||||||
# reason=reason,
|
await Lock(
|
||||||
# duration=duration,
|
channel=channel.id,
|
||||||
# ) # .save() # Uncomment once implemented
|
guild=ctx.guild.id,
|
||||||
# # await ctx.send(f"{channel.mention} locked for {duration} minute(s)")
|
admin=ctx.author.id,
|
||||||
# await ctx.send("Unfortunately, this is not yet implemented", hidden=True)
|
reason=reason,
|
||||||
#
|
duration=duration,
|
||||||
# @cog_ext.cog_slash(
|
original_perms=current,
|
||||||
# name="unlock",
|
).commit()
|
||||||
# description="Unlocks a channel",
|
await ctx.send(f"{channel.mention} locked for {duration} minute(s)")
|
||||||
# choices=[
|
|
||||||
# create_option(
|
@slash_command(name="unlock", description="Unlock a channel")
|
||||||
# name="channel",
|
@slash_option(
|
||||||
# description="Channel to lock",
|
name="channel",
|
||||||
# opt_type=7,
|
description="Channel to unlock",
|
||||||
# required=False,
|
opt_type=OptionTypes.CHANNEL,
|
||||||
# ),
|
required=False,
|
||||||
# ],
|
)
|
||||||
# )
|
@check(admin_or_permissions(Permissions.MANAGE_CHANNELS))
|
||||||
# @check(admin_or_permissions(Permissions.MANAGE_CHANNELS))
|
async def _unlock(
|
||||||
# async def _unlock(
|
self,
|
||||||
# self,
|
ctx: InteractionContext,
|
||||||
# ctx: InteractionContext,
|
channel: Union[GuildText, GuildVoice] = None,
|
||||||
# channel: Union[GuildText, GuildVoice] = None,
|
) -> None:
|
||||||
# ) -> None:
|
if not channel:
|
||||||
# if not channel:
|
channel = ctx.channel
|
||||||
# channel = ctx.channel
|
lock = await Lock.find_one(q(guild=ctx.guild.id, channel=channel.id, active=True))
|
||||||
# lock = Lock.objects(guild=ctx.guild.id, channel=channel.id, active=True).first()
|
if not lock:
|
||||||
# if not lock:
|
await ctx.send(f"{channel.mention} not locked.", ephemeral=True)
|
||||||
# await ctx.send(f"{channel.mention} not locked.", ephemeral=True)
|
return
|
||||||
# return
|
|
||||||
# for role in ctx.guild.roles:
|
overwrite = get(channel.permission_overwrites, id=ctx.guild.id)
|
||||||
# with suppress(Exception):
|
if overwrite and lock.original_perms:
|
||||||
# await self._unlock_channel(channel, role, ctx.author)
|
overwrite.allow = lock.original_perms.allow
|
||||||
# lock.active = False
|
overwrite.deny = lock.original_perms.deny
|
||||||
# lock.save()
|
await channel.edit_permission(overwrite, reason="Unlock")
|
||||||
# await ctx.send(f"{channel.mention} unlocked")
|
elif overwrite and not lock.original_perms:
|
||||||
|
await channel.delete_permission(target=overwrite, reason="Unlock")
|
||||||
|
|
||||||
|
lock.active = False
|
||||||
|
await lock.commit()
|
||||||
|
await ctx.send(f"{channel.mention} unlocked")
|
||||||
|
|
|
@ -1,101 +1,168 @@
|
||||||
"""J.A.R.V.I.S. LockdownCog."""
|
"""JARVIS LockdownCog."""
|
||||||
from contextlib import suppress
|
import logging
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
from discord.ext import commands
|
from dis_snek import InteractionContext, Scale, Snake
|
||||||
from discord_slash import SlashContext, cog_ext
|
from dis_snek.client.utils.misc_utils import find_all, get
|
||||||
from discord_slash.utils.manage_commands import create_option
|
from dis_snek.models.discord.channel import GuildCategory, GuildChannel
|
||||||
|
from dis_snek.models.discord.enums import Permissions
|
||||||
|
from dis_snek.models.discord.guild import Guild
|
||||||
|
from dis_snek.models.discord.user import Member
|
||||||
|
from dis_snek.models.snek.application_commands import (
|
||||||
|
OptionTypes,
|
||||||
|
SlashCommand,
|
||||||
|
slash_option,
|
||||||
|
)
|
||||||
|
from dis_snek.models.snek.command import check
|
||||||
|
from jarvis_core.db import q
|
||||||
|
from jarvis_core.db.models import Lock, Lockdown, Permission
|
||||||
|
|
||||||
from jarvis.db.models import Lock
|
from jarvis.utils.permissions import admin_or_permissions
|
||||||
from jarvis.utils.cachecog import CacheCog
|
|
||||||
|
|
||||||
# from jarvis.utils.permissions import admin_or_permissions
|
|
||||||
|
|
||||||
|
|
||||||
class LockdownCog(CacheCog):
|
async def lock(bot: Snake, target: GuildChannel, admin: Member, reason: str, duration: int) -> None:
|
||||||
"""J.A.R.V.I.S. LockdownCog."""
|
"""
|
||||||
|
Lock an existing channel
|
||||||
|
|
||||||
def __init__(self, bot: commands.Bot):
|
Args:
|
||||||
super().__init__(bot)
|
bot: Bot instance
|
||||||
|
target: Target channel
|
||||||
|
admin: Admin who initiated lockdown
|
||||||
|
"""
|
||||||
|
to_deny = Permissions.SEND_MESSAGES | Permissions.CONNECT | Permissions.SPEAK
|
||||||
|
current = get(target.permission_overwrites, id=target.guild.id)
|
||||||
|
if current:
|
||||||
|
current = Permission(id=target.guild.id, allow=int(current.allow), deny=int(current.deny))
|
||||||
|
role = await target.guild.fetch_role(target.guild.id)
|
||||||
|
await target.add_permission(target=role, deny=to_deny, reason="Lockdown")
|
||||||
|
await Lock(
|
||||||
|
channel=target.id,
|
||||||
|
guild=target.guild.id,
|
||||||
|
admin=admin.id,
|
||||||
|
reason=reason,
|
||||||
|
duration=duration,
|
||||||
|
original_perms=current,
|
||||||
|
).commit()
|
||||||
|
|
||||||
@cog_ext.cog_subcommand(
|
|
||||||
base="lockdown",
|
async def lock_all(bot: Snake, guild: Guild, admin: Member, reason: str, duration: int) -> None:
|
||||||
name="start",
|
"""
|
||||||
description="Locks a server",
|
Lock all channels
|
||||||
choices=[
|
|
||||||
create_option(
|
Args:
|
||||||
name="reason",
|
bot: Bot instance
|
||||||
description="Lockdown Reason",
|
guild: Target guild
|
||||||
opt_type=3,
|
admin: Admin who initiated lockdown
|
||||||
required=True,
|
"""
|
||||||
),
|
role = await guild.fetch_role(guild.id)
|
||||||
create_option(
|
categories = find_all(lambda x: isinstance(x, GuildCategory), guild.channels)
|
||||||
name="duration",
|
for category in categories:
|
||||||
description="Lockdown duration in minutes (default 10)",
|
await lock(bot, category, admin, reason, duration)
|
||||||
opt_type=4,
|
perms = category.permissions_for(role)
|
||||||
required=False,
|
|
||||||
),
|
for channel in category.channels:
|
||||||
],
|
if perms != channel.permissions_for(role):
|
||||||
|
await lock(bot, channel, admin, reason, duration)
|
||||||
|
|
||||||
|
|
||||||
|
async def unlock_all(bot: Snake, guild: Guild, admin: Member) -> None:
|
||||||
|
"""
|
||||||
|
Unlock all locked channels
|
||||||
|
|
||||||
|
Args:
|
||||||
|
bot: Bot instance
|
||||||
|
target: Target channel
|
||||||
|
admin: Admin who ended lockdown
|
||||||
|
"""
|
||||||
|
locks = Lock.find(q(guild=guild.id, active=True))
|
||||||
|
async for lock in locks:
|
||||||
|
target = await guild.fetch_channel(lock.channel)
|
||||||
|
if target:
|
||||||
|
overwrite = get(target.permission_overwrites, id=guild.id)
|
||||||
|
if overwrite and lock.original_perms:
|
||||||
|
overwrite.allow = lock.original_perms.allow
|
||||||
|
overwrite.deny = lock.original_perms.deny
|
||||||
|
await target.edit_permission(overwrite, reason="Lockdown end")
|
||||||
|
elif overwrite and not lock.original_perms:
|
||||||
|
await target.delete_permission(target=overwrite, reason="Lockdown end")
|
||||||
|
lock.active = False
|
||||||
|
await lock.commit()
|
||||||
|
lockdown = await Lockdown.find_one(q(guild=guild.id, active=True))
|
||||||
|
if lockdown:
|
||||||
|
lockdown.active = False
|
||||||
|
await lockdown.commit()
|
||||||
|
|
||||||
|
|
||||||
|
class LockdownCog(Scale):
|
||||||
|
"""JARVIS LockdownCog."""
|
||||||
|
|
||||||
|
def __init__(self, bot: Snake):
|
||||||
|
self.bot = bot
|
||||||
|
self.logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
lockdown = SlashCommand(
|
||||||
|
name="lockdown",
|
||||||
|
description="Manage server-wide lockdown",
|
||||||
)
|
)
|
||||||
# @check(admin_or_permissions(manage_channels=True))
|
|
||||||
|
@lockdown.subcommand(
|
||||||
|
sub_cmd_name="start",
|
||||||
|
sub_cmd_description="Lockdown the server",
|
||||||
|
)
|
||||||
|
@slash_option(
|
||||||
|
name="reason", description="Lockdown reason", opt_type=OptionTypes.STRING, required=True
|
||||||
|
)
|
||||||
|
@slash_option(
|
||||||
|
name="duration",
|
||||||
|
description="Duration in minutes",
|
||||||
|
opt_type=OptionTypes.INTEGER,
|
||||||
|
required=False,
|
||||||
|
)
|
||||||
|
@check(admin_or_permissions(Permissions.MANAGE_CHANNELS))
|
||||||
async def _lockdown_start(
|
async def _lockdown_start(
|
||||||
self,
|
self,
|
||||||
ctx: SlashContext,
|
ctx: InteractionContext,
|
||||||
reason: str,
|
reason: str,
|
||||||
duration: int = 10,
|
duration: int = 10,
|
||||||
) -> None:
|
) -> None:
|
||||||
await ctx.defer(ephemeral=True)
|
await ctx.defer()
|
||||||
if duration <= 0:
|
if duration <= 0:
|
||||||
await ctx.send("Duration must be > 0", ephemeral=True)
|
await ctx.send("Duration must be > 0", ephemeral=True)
|
||||||
return
|
return
|
||||||
elif duration >= 300:
|
elif duration >= 300:
|
||||||
await ctx.send("Duration must be < 5 hours", ephemeral=True)
|
await ctx.send("Duration must be < 5 hours", ephemeral=True)
|
||||||
return
|
return
|
||||||
channels = ctx.guild.channels
|
|
||||||
roles = ctx.guild.roles
|
|
||||||
updates = []
|
|
||||||
for channel in channels:
|
|
||||||
for role in roles:
|
|
||||||
with suppress(Exception):
|
|
||||||
await self._lock_channel(channel, role, ctx.author, reason)
|
|
||||||
updates.append(
|
|
||||||
Lock(
|
|
||||||
channel=channel.id,
|
|
||||||
guild=ctx.guild.id,
|
|
||||||
admin=ctx.author.id,
|
|
||||||
reason=reason,
|
|
||||||
duration=duration,
|
|
||||||
active=True,
|
|
||||||
created_at=datetime.utcnow(),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
if updates:
|
|
||||||
Lock.objects().insert(updates)
|
|
||||||
await ctx.send(f"Server locked for {duration} minute(s)")
|
|
||||||
|
|
||||||
@cog_ext.cog_subcommand(
|
exists = await Lockdown.find_one(q(guild=ctx.guild.id, active=True))
|
||||||
base="lockdown",
|
if exists:
|
||||||
name="end",
|
await ctx.send("Server already in lockdown", ephemeral=True)
|
||||||
description="Unlocks a server",
|
return
|
||||||
)
|
|
||||||
@commands.has_permissions(administrator=True)
|
await lock_all(self.bot, ctx.guild, ctx.author, reason, duration)
|
||||||
|
role = await ctx.guild.fetch_role(ctx.guild.id)
|
||||||
|
original_perms = role.permissions
|
||||||
|
new_perms = role.permissions & ~Permissions.SEND_MESSAGES
|
||||||
|
await role.edit(permissions=new_perms)
|
||||||
|
await Lockdown(
|
||||||
|
admin=ctx.author.id,
|
||||||
|
duration=duration,
|
||||||
|
guild=ctx.guild.id,
|
||||||
|
reason=reason,
|
||||||
|
original_perms=int(original_perms),
|
||||||
|
).commit()
|
||||||
|
await ctx.send("Server now in lockdown.")
|
||||||
|
|
||||||
|
@lockdown.subcommand(sub_cmd_name="end", sub_cmd_description="End a lockdown")
|
||||||
|
@check(admin_or_permissions(Permissions.MANAGE_CHANNELS))
|
||||||
async def _lockdown_end(
|
async def _lockdown_end(
|
||||||
self,
|
self,
|
||||||
ctx: SlashContext,
|
ctx: InteractionContext,
|
||||||
) -> None:
|
) -> None:
|
||||||
channels = ctx.guild.channels
|
|
||||||
roles = ctx.guild.roles
|
|
||||||
update = False
|
|
||||||
locks = Lock.objects(guild=ctx.guild.id, active=True)
|
|
||||||
if not locks:
|
|
||||||
await ctx.send("No lockdown detected.", ephemeral=True)
|
|
||||||
return
|
|
||||||
await ctx.defer()
|
await ctx.defer()
|
||||||
for channel in channels:
|
|
||||||
for role in roles:
|
lockdown = await Lockdown.find_one(q(guild=ctx.guild.id, active=True))
|
||||||
with suppress(Exception):
|
if not lockdown:
|
||||||
await self._unlock_channel(channel, role, ctx.author)
|
await ctx.send("Server not in lockdown", ephemeral=True)
|
||||||
update = True
|
return
|
||||||
if update:
|
|
||||||
Lock.objects(guild=ctx.guild.id, active=True).update(set__active=False)
|
await unlock_all(self.bot, ctx.guild, ctx.author)
|
||||||
await ctx.send("Server unlocked")
|
await ctx.send("Server no longer in lockdown.")
|
||||||
|
|
|
@ -1,12 +1,19 @@
|
||||||
"""J.A.R.V.I.S. MuteCog."""
|
"""JARVIS MuteCog."""
|
||||||
from datetime import datetime
|
import asyncio
|
||||||
|
import logging
|
||||||
|
from datetime import datetime, timedelta, timezone
|
||||||
|
|
||||||
from dis_snek import InteractionContext, Permissions, Scale, Snake
|
from dateparser import parse
|
||||||
|
from dateparser_data.settings import default_parsers
|
||||||
|
from dis_snek import InteractionContext, Permissions, Snake
|
||||||
from dis_snek.models.discord.embed import EmbedField
|
from dis_snek.models.discord.embed import EmbedField
|
||||||
|
from dis_snek.models.discord.modal import InputText, Modal, TextStyles
|
||||||
from dis_snek.models.discord.user import Member
|
from dis_snek.models.discord.user import Member
|
||||||
from dis_snek.models.snek.application_commands import (
|
from dis_snek.models.snek.application_commands import (
|
||||||
|
CommandTypes,
|
||||||
OptionTypes,
|
OptionTypes,
|
||||||
SlashCommandChoice,
|
SlashCommandChoice,
|
||||||
|
context_menu,
|
||||||
slash_command,
|
slash_command,
|
||||||
slash_option,
|
slash_option,
|
||||||
)
|
)
|
||||||
|
@ -14,14 +21,112 @@ from dis_snek.models.snek.command import check
|
||||||
from jarvis_core.db.models import Mute
|
from jarvis_core.db.models import Mute
|
||||||
|
|
||||||
from jarvis.utils import build_embed
|
from jarvis.utils import build_embed
|
||||||
|
from jarvis.utils.cogs import ModcaseCog
|
||||||
from jarvis.utils.permissions import admin_or_permissions
|
from jarvis.utils.permissions import admin_or_permissions
|
||||||
|
|
||||||
|
|
||||||
class MuteCog(Scale):
|
class MuteCog(ModcaseCog):
|
||||||
"""J.A.R.V.I.S. MuteCog."""
|
"""JARVIS MuteCog."""
|
||||||
|
|
||||||
def __init__(self, bot: Snake):
|
def __init__(self, bot: Snake):
|
||||||
self.bot = bot
|
super().__init__(bot)
|
||||||
|
self.logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
async def _apply_timeout(
|
||||||
|
self, ctx: InteractionContext, user: Member, reason: str, until: datetime
|
||||||
|
) -> None:
|
||||||
|
await user.timeout(communication_disabled_until=until, reason=reason)
|
||||||
|
duration = int((until - datetime.now(tz=timezone.utc)).seconds / 60)
|
||||||
|
await Mute(
|
||||||
|
user=user.id,
|
||||||
|
reason=reason,
|
||||||
|
admin=ctx.author.id,
|
||||||
|
guild=ctx.guild.id,
|
||||||
|
duration=duration,
|
||||||
|
active=True,
|
||||||
|
).commit()
|
||||||
|
ts = int(until.timestamp())
|
||||||
|
|
||||||
|
embed = build_embed(
|
||||||
|
title="User Muted",
|
||||||
|
description=f"{user.mention} has been muted",
|
||||||
|
fields=[
|
||||||
|
EmbedField(name="Reason", value=reason),
|
||||||
|
EmbedField(name="Until", value=f"<t:{ts}:F> <t:{ts}:R>"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
embed.set_author(name=user.display_name, icon_url=user.display_avatar.url)
|
||||||
|
embed.set_thumbnail(url=user.display_avatar.url)
|
||||||
|
embed.set_footer(text=f"{user.username}#{user.discriminator} | {user.id}")
|
||||||
|
return embed
|
||||||
|
|
||||||
|
@context_menu(name="Mute User", context_type=CommandTypes.USER)
|
||||||
|
@check(
|
||||||
|
admin_or_permissions(
|
||||||
|
Permissions.MUTE_MEMBERS, Permissions.BAN_MEMBERS, Permissions.KICK_MEMBERS
|
||||||
|
)
|
||||||
|
)
|
||||||
|
async def _timeout_cm(self, ctx: InteractionContext) -> None:
|
||||||
|
modal = Modal(
|
||||||
|
title=f"Muting {ctx.target.mention}",
|
||||||
|
components=[
|
||||||
|
InputText(
|
||||||
|
label="Reason?",
|
||||||
|
placeholder="Spamming, harrassment, etc",
|
||||||
|
style=TextStyles.SHORT,
|
||||||
|
custom_id="reason",
|
||||||
|
max_length=100,
|
||||||
|
),
|
||||||
|
InputText(
|
||||||
|
label="Duration",
|
||||||
|
placeholder="1h 30m | in 5 minutes | in 4 weeks",
|
||||||
|
style=TextStyles.SHORT,
|
||||||
|
custom_id="until",
|
||||||
|
max_length=100,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
await ctx.send_modal(modal)
|
||||||
|
try:
|
||||||
|
response = await self.bot.wait_for_modal(modal, author=ctx.author.id, timeout=60 * 5)
|
||||||
|
reason = response.responses.get("reason")
|
||||||
|
until = response.responses.get("until")
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
return
|
||||||
|
base_settings = {
|
||||||
|
"PREFER_DATES_FROM": "future",
|
||||||
|
"TIMEZONE": "UTC",
|
||||||
|
"RETURN_AS_TIMEZONE_AWARE": True,
|
||||||
|
}
|
||||||
|
rt_settings = base_settings.copy()
|
||||||
|
rt_settings["PARSERS"] = [
|
||||||
|
x for x in default_parsers if x not in ["absolute-time", "timestamp"]
|
||||||
|
]
|
||||||
|
|
||||||
|
rt_until = parse(until, settings=rt_settings)
|
||||||
|
|
||||||
|
at_settings = base_settings.copy()
|
||||||
|
at_settings["PARSERS"] = [x for x in default_parsers if x != "relative-time"]
|
||||||
|
at_until = parse(until, settings=at_settings)
|
||||||
|
|
||||||
|
old_until = until
|
||||||
|
if rt_until:
|
||||||
|
until = rt_until
|
||||||
|
elif at_until:
|
||||||
|
until = at_until
|
||||||
|
else:
|
||||||
|
self.logger.debug(f"Failed to parse delay: {until}")
|
||||||
|
await response.send(
|
||||||
|
f"`{until}` is not a parsable date, please try again", ephemeral=True
|
||||||
|
)
|
||||||
|
return
|
||||||
|
if until < datetime.now(tz=timezone.utc):
|
||||||
|
await response.send(
|
||||||
|
f"`{old_until}` is in the past, which isn't allowed", ephemeral=True
|
||||||
|
)
|
||||||
|
return
|
||||||
|
embed = await self._apply_timeout(ctx, ctx.target, reason, until)
|
||||||
|
await response.send(embed=embed)
|
||||||
|
|
||||||
@slash_command(name="mute", description="Mute a user")
|
@slash_command(name="mute", description="Mute a user")
|
||||||
@slash_option(name="user", description="User to mute", opt_type=OptionTypes.USER, required=True)
|
@slash_option(name="user", description="User to mute", opt_type=OptionTypes.USER, required=True)
|
||||||
|
@ -73,25 +178,8 @@ class MuteCog(Scale):
|
||||||
await ctx.send("Mute must be less than 4 weeks (2419200 seconds)", ephemeral=True)
|
await ctx.send("Mute must be less than 4 weeks (2419200 seconds)", ephemeral=True)
|
||||||
return
|
return
|
||||||
|
|
||||||
await user.timeout(communication_disabled_until=duration, reason=reason)
|
until = datetime.now(tz=timezone.utc) + timedelta(minutes=duration)
|
||||||
m = Mute(
|
embed = await self._apply_timeout(ctx, user, reason, until)
|
||||||
user=user.id,
|
|
||||||
reason=reason,
|
|
||||||
admin=ctx.author.id,
|
|
||||||
guild=ctx.guild.id,
|
|
||||||
duration=duration,
|
|
||||||
active=True,
|
|
||||||
)
|
|
||||||
await m.commit()
|
|
||||||
|
|
||||||
embed = build_embed(
|
|
||||||
title="User Muted",
|
|
||||||
description=f"{user.mention} has been muted",
|
|
||||||
fields=[EmbedField(name="Reason", value=reason)],
|
|
||||||
)
|
|
||||||
embed.set_author(name=user.display_name, icon_url=user.display_avatar.url)
|
|
||||||
embed.set_thumbnail(url=user.display_avatar.url)
|
|
||||||
embed.set_footer(text=f"{user.username}#{user.discriminator} | {user.id}")
|
|
||||||
await ctx.send(embed=embed)
|
await ctx.send(embed=embed)
|
||||||
|
|
||||||
@slash_command(name="unmute", description="Unmute a user")
|
@slash_command(name="unmute", description="Unmute a user")
|
||||||
|
@ -106,11 +194,14 @@ class MuteCog(Scale):
|
||||||
async def _unmute(self, ctx: InteractionContext, user: Member) -> None:
|
async def _unmute(self, ctx: InteractionContext, user: Member) -> None:
|
||||||
if (
|
if (
|
||||||
not user.communication_disabled_until
|
not user.communication_disabled_until
|
||||||
or user.communication_disabled_until < datetime.now() # noqa: W503
|
or user.communication_disabled_until.timestamp()
|
||||||
|
< datetime.now(tz=timezone.utc).timestamp() # noqa: W503
|
||||||
):
|
):
|
||||||
await ctx.send("User is not muted", ephemeral=True)
|
await ctx.send("User is not muted", ephemeral=True)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
await user.timeout(communication_disabled_until=datetime.now(tz=timezone.utc))
|
||||||
|
|
||||||
embed = build_embed(
|
embed = build_embed(
|
||||||
title="User Unmuted",
|
title="User Unmuted",
|
||||||
description=f"{user.mention} has been unmuted",
|
description=f"{user.mention} has been unmuted",
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
"""J.A.R.V.I.S. PurgeCog."""
|
"""JARVIS PurgeCog."""
|
||||||
|
import logging
|
||||||
|
|
||||||
from dis_snek import InteractionContext, Permissions, Scale, Snake
|
from dis_snek import InteractionContext, Permissions, Scale, Snake
|
||||||
from dis_snek.models.discord.channel import GuildText
|
from dis_snek.models.discord.channel import GuildText
|
||||||
from dis_snek.models.snek.application_commands import (
|
from dis_snek.models.snek.application_commands import (
|
||||||
|
@ -14,10 +16,11 @@ from jarvis.utils.permissions import admin_or_permissions
|
||||||
|
|
||||||
|
|
||||||
class PurgeCog(Scale):
|
class PurgeCog(Scale):
|
||||||
"""J.A.R.V.I.S. PurgeCog."""
|
"""JARVIS PurgeCog."""
|
||||||
|
|
||||||
def __init__(self, bot: Snake):
|
def __init__(self, bot: Snake):
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
|
self.logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@slash_command(name="purge", description="Purge messages from channel")
|
@slash_command(name="purge", description="Purge messages from channel")
|
||||||
@slash_option(
|
@slash_option(
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
"""J.A.R.V.I.S. RolepingCog."""
|
"""JARVIS RolepingCog."""
|
||||||
from dis_snek import InteractionContext, Permissions, Scale
|
import logging
|
||||||
|
|
||||||
|
from dis_snek import InteractionContext, Permissions, Scale, Snake
|
||||||
from dis_snek.client.utils.misc_utils import find_all
|
from dis_snek.client.utils.misc_utils import find_all
|
||||||
from dis_snek.ext.paginators import Paginator
|
from dis_snek.ext.paginators import Paginator
|
||||||
from dis_snek.models.discord.embed import EmbedField
|
from dis_snek.models.discord.embed import EmbedField
|
||||||
|
@ -7,7 +9,7 @@ from dis_snek.models.discord.role import Role
|
||||||
from dis_snek.models.discord.user import Member
|
from dis_snek.models.discord.user import Member
|
||||||
from dis_snek.models.snek.application_commands import (
|
from dis_snek.models.snek.application_commands import (
|
||||||
OptionTypes,
|
OptionTypes,
|
||||||
slash_command,
|
SlashCommand,
|
||||||
slash_option,
|
slash_option,
|
||||||
)
|
)
|
||||||
from dis_snek.models.snek.command import check
|
from dis_snek.models.snek.command import check
|
||||||
|
@ -19,10 +21,19 @@ from jarvis.utils.permissions import admin_or_permissions
|
||||||
|
|
||||||
|
|
||||||
class RolepingCog(Scale):
|
class RolepingCog(Scale):
|
||||||
"""J.A.R.V.I.S. RolepingCog."""
|
"""JARVIS RolepingCog."""
|
||||||
|
|
||||||
@slash_command(
|
def __init__(self, bot: Snake):
|
||||||
name="roleping", sub_cmd_name="add", sub_cmd_description="Add a role to roleping"
|
self.bot = bot
|
||||||
|
self.logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
roleping = SlashCommand(
|
||||||
|
name="roleping", description="Set up warnings for pinging specific roles"
|
||||||
|
)
|
||||||
|
|
||||||
|
@roleping.subcommand(
|
||||||
|
sub_cmd_name="add",
|
||||||
|
sub_cmd_description="Add a role to roleping",
|
||||||
)
|
)
|
||||||
@slash_option(name="role", description="Role to add", opt_type=OptionTypes.ROLE, required=True)
|
@slash_option(name="role", description="Role to add", opt_type=OptionTypes.ROLE, required=True)
|
||||||
@check(admin_or_permissions(Permissions.MANAGE_GUILD))
|
@check(admin_or_permissions(Permissions.MANAGE_GUILD))
|
||||||
|
@ -32,6 +43,10 @@ class RolepingCog(Scale):
|
||||||
await ctx.send(f"Role `{role.name}` already in roleping.", ephemeral=True)
|
await ctx.send(f"Role `{role.name}` already in roleping.", ephemeral=True)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if role.id == ctx.guild.id:
|
||||||
|
await ctx.send("Cannot add `@everyone` to roleping", ephemeral=True)
|
||||||
|
return
|
||||||
|
|
||||||
_ = await Roleping(
|
_ = await Roleping(
|
||||||
role=role.id,
|
role=role.id,
|
||||||
guild=ctx.guild.id,
|
guild=ctx.guild.id,
|
||||||
|
@ -41,7 +56,7 @@ class RolepingCog(Scale):
|
||||||
).commit()
|
).commit()
|
||||||
await ctx.send(f"Role `{role.name}` added to roleping.")
|
await ctx.send(f"Role `{role.name}` added to roleping.")
|
||||||
|
|
||||||
@slash_command(name="roleping", sub_cmd_name="remove", sub_cmd_description="Remove a role")
|
@roleping.subcommand(sub_cmd_name="remove", sub_cmd_description="Remove a role")
|
||||||
@slash_option(
|
@slash_option(
|
||||||
name="role", description="Role to remove", opt_type=OptionTypes.ROLE, required=True
|
name="role", description="Role to remove", opt_type=OptionTypes.ROLE, required=True
|
||||||
)
|
)
|
||||||
|
@ -52,10 +67,13 @@ class RolepingCog(Scale):
|
||||||
await ctx.send("Roleping does not exist", ephemeral=True)
|
await ctx.send("Roleping does not exist", ephemeral=True)
|
||||||
return
|
return
|
||||||
|
|
||||||
await roleping.delete()
|
try:
|
||||||
|
await roleping.delete()
|
||||||
|
except Exception:
|
||||||
|
self.logger.debug("Ignoring deletion error")
|
||||||
await ctx.send(f"Role `{role.name}` removed from roleping.")
|
await ctx.send(f"Role `{role.name}` removed from roleping.")
|
||||||
|
|
||||||
@slash_command(name="roleping", sub_cmd_name="list", description="Lick all blocklisted roles")
|
@roleping.subcommand(sub_cmd_name="list", sub_cmd_description="Lick all blocklisted roles")
|
||||||
async def _roleping_list(self, ctx: InteractionContext) -> None:
|
async def _roleping_list(self, ctx: InteractionContext) -> None:
|
||||||
|
|
||||||
rolepings = await Roleping.find(q(guild=ctx.guild.id)).to_list(None)
|
rolepings = await Roleping.find(q(guild=ctx.guild.id)).to_list(None)
|
||||||
|
@ -66,6 +84,9 @@ class RolepingCog(Scale):
|
||||||
embeds = []
|
embeds = []
|
||||||
for roleping in rolepings:
|
for roleping in rolepings:
|
||||||
role = await ctx.guild.fetch_role(roleping.role)
|
role = await ctx.guild.fetch_role(roleping.role)
|
||||||
|
if not role:
|
||||||
|
await roleping.delete()
|
||||||
|
continue
|
||||||
broles = find_all(lambda x: x.id in roleping.bypass["roles"], ctx.guild.roles)
|
broles = find_all(lambda x: x.id in roleping.bypass["roles"], ctx.guild.roles)
|
||||||
bypass_roles = [r.mention or "||`[redacted]`||" for r in broles]
|
bypass_roles = [r.mention or "||`[redacted]`||" for r in broles]
|
||||||
bypass_users = [
|
bypass_users = [
|
||||||
|
@ -111,11 +132,11 @@ class RolepingCog(Scale):
|
||||||
|
|
||||||
await paginator.send(ctx)
|
await paginator.send(ctx)
|
||||||
|
|
||||||
@slash_command(
|
bypass = roleping.group(
|
||||||
name="roleping",
|
name="bypass", description="Allow specific users/roles to ping rolepings"
|
||||||
description="Block roles from being pinged",
|
)
|
||||||
group_name="bypass",
|
|
||||||
group_description="Allow specific users/roles to ping rolepings",
|
@bypass.subcommand(
|
||||||
sub_cmd_name="user",
|
sub_cmd_name="user",
|
||||||
sub_cmd_description="Add a user as a bypass to a roleping",
|
sub_cmd_description="Add a user as a bypass to a roleping",
|
||||||
)
|
)
|
||||||
|
@ -158,11 +179,9 @@ class RolepingCog(Scale):
|
||||||
await roleping.commit()
|
await roleping.commit()
|
||||||
await ctx.send(f"{bypass.display_name} user bypass added for `{role.name}`")
|
await ctx.send(f"{bypass.display_name} user bypass added for `{role.name}`")
|
||||||
|
|
||||||
@slash_command(
|
@bypass.subcommand(
|
||||||
name="roleping",
|
|
||||||
group_name="bypass",
|
|
||||||
sub_cmd_name="role",
|
sub_cmd_name="role",
|
||||||
description="Add a role as a bypass to roleping",
|
sub_cmd_description="Add a role as a bypass to roleping",
|
||||||
)
|
)
|
||||||
@slash_option(
|
@slash_option(
|
||||||
name="bypass", description="Role to add", opt_type=OptionTypes.ROLE, required=True
|
name="bypass", description="Role to add", opt_type=OptionTypes.ROLE, required=True
|
||||||
|
@ -174,6 +193,9 @@ class RolepingCog(Scale):
|
||||||
async def _roleping_bypass_role(
|
async def _roleping_bypass_role(
|
||||||
self, ctx: InteractionContext, bypass: Role, role: Role
|
self, ctx: InteractionContext, bypass: Role, role: Role
|
||||||
) -> None:
|
) -> None:
|
||||||
|
if bypass.id == ctx.guild.id:
|
||||||
|
await ctx.send("Cannot add `@everyone` as a bypass", ephemeral=True)
|
||||||
|
return
|
||||||
roleping = await Roleping.find_one(q(guild=ctx.guild.id, role=role.id))
|
roleping = await Roleping.find_one(q(guild=ctx.guild.id, role=role.id))
|
||||||
if not roleping:
|
if not roleping:
|
||||||
await ctx.send(f"Roleping not configured for {role.mention}", ephemeral=True)
|
await ctx.send(f"Roleping not configured for {role.mention}", ephemeral=True)
|
||||||
|
@ -195,11 +217,9 @@ class RolepingCog(Scale):
|
||||||
await roleping.commit()
|
await roleping.commit()
|
||||||
await ctx.send(f"{bypass.name} role bypass added for `{role.name}`")
|
await ctx.send(f"{bypass.name} role bypass added for `{role.name}`")
|
||||||
|
|
||||||
@slash_command(
|
restore = roleping.group(name="restore", description="Remove a roleping bypass")
|
||||||
name="roleping",
|
|
||||||
description="Block roles from being pinged",
|
@restore.subcommand(
|
||||||
group_name="restore",
|
|
||||||
group_description="Remove a roleping bypass",
|
|
||||||
sub_cmd_name="user",
|
sub_cmd_name="user",
|
||||||
sub_cmd_description="Remove a bypass from a roleping (restoring it)",
|
sub_cmd_description="Remove a bypass from a roleping (restoring it)",
|
||||||
)
|
)
|
||||||
|
@ -226,11 +246,9 @@ class RolepingCog(Scale):
|
||||||
await roleping.commit()
|
await roleping.commit()
|
||||||
await ctx.send(f"{bypass.display_name} user bypass removed for `{role.name}`")
|
await ctx.send(f"{bypass.display_name} user bypass removed for `{role.name}`")
|
||||||
|
|
||||||
@slash_command(
|
@restore.subcommand(
|
||||||
name="roleping",
|
|
||||||
group_name="restore",
|
|
||||||
sub_cmd_name="role",
|
sub_cmd_name="role",
|
||||||
description="Remove a bypass from a roleping (restoring it)",
|
sub_cmd_description="Remove a bypass from a roleping (restoring it)",
|
||||||
)
|
)
|
||||||
@slash_option(
|
@slash_option(
|
||||||
name="bypass", description="Role to remove", opt_type=OptionTypes.ROLE, required=True
|
name="bypass", description="Role to remove", opt_type=OptionTypes.ROLE, required=True
|
||||||
|
|
|
@ -1,25 +1,32 @@
|
||||||
"""J.A.R.V.I.S. WarningCog."""
|
"""JARVIS WarningCog."""
|
||||||
from dis_snek import InteractionContext, Permissions, Scale
|
import logging
|
||||||
|
|
||||||
|
from dis_snek import InteractionContext, Permissions, Snake
|
||||||
from dis_snek.client.utils.misc_utils import get_all
|
from dis_snek.client.utils.misc_utils import get_all
|
||||||
from dis_snek.ext.paginators import Paginator
|
from dis_snek.ext.paginators import Paginator
|
||||||
from dis_snek.models.discord.embed import EmbedField
|
from dis_snek.models.discord.embed import EmbedField
|
||||||
from dis_snek.models.discord.user import User
|
from dis_snek.models.discord.user import Member
|
||||||
from dis_snek.models.snek.application_commands import (
|
from dis_snek.models.snek.application_commands import (
|
||||||
OptionTypes,
|
OptionTypes,
|
||||||
SlashCommandChoice,
|
|
||||||
slash_command,
|
slash_command,
|
||||||
slash_option,
|
slash_option,
|
||||||
)
|
)
|
||||||
from dis_snek.models.snek.command import check
|
from dis_snek.models.snek.command import check
|
||||||
|
from jarvis_core.db import q
|
||||||
from jarvis_core.db.models import Warning
|
from jarvis_core.db.models import Warning
|
||||||
|
|
||||||
from jarvis.utils import build_embed
|
from jarvis.utils import build_embed
|
||||||
|
from jarvis.utils.cogs import ModcaseCog
|
||||||
from jarvis.utils.embeds import warning_embed
|
from jarvis.utils.embeds import warning_embed
|
||||||
from jarvis.utils.permissions import admin_or_permissions
|
from jarvis.utils.permissions import admin_or_permissions
|
||||||
|
|
||||||
|
|
||||||
class WarningCog(Scale):
|
class WarningCog(ModcaseCog):
|
||||||
"""J.A.R.V.I.S. WarningCog."""
|
"""JARVIS WarningCog."""
|
||||||
|
|
||||||
|
def __init__(self, bot: Snake):
|
||||||
|
super().__init__(bot)
|
||||||
|
self.logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@slash_command(name="warn", description="Warn a user")
|
@slash_command(name="warn", description="Warn a user")
|
||||||
@slash_option(name="user", description="User to warn", opt_type=OptionTypes.USER, required=True)
|
@slash_option(name="user", description="User to warn", opt_type=OptionTypes.USER, required=True)
|
||||||
|
@ -37,7 +44,7 @@ class WarningCog(Scale):
|
||||||
)
|
)
|
||||||
@check(admin_or_permissions(Permissions.MANAGE_GUILD))
|
@check(admin_or_permissions(Permissions.MANAGE_GUILD))
|
||||||
async def _warn(
|
async def _warn(
|
||||||
self, ctx: InteractionContext, user: User, reason: str, duration: int = 24
|
self, ctx: InteractionContext, user: Member, reason: str, duration: int = 24
|
||||||
) -> None:
|
) -> None:
|
||||||
if len(reason) > 100:
|
if len(reason) > 100:
|
||||||
await ctx.send("Reason must be < 100 characters", ephemeral=True)
|
await ctx.send("Reason must be < 100 characters", ephemeral=True)
|
||||||
|
@ -48,6 +55,9 @@ class WarningCog(Scale):
|
||||||
elif duration >= 120:
|
elif duration >= 120:
|
||||||
await ctx.send("Duration must be < 5 days", ephemeral=True)
|
await ctx.send("Duration must be < 5 days", ephemeral=True)
|
||||||
return
|
return
|
||||||
|
if not await ctx.guild.fetch_member(user.id):
|
||||||
|
await ctx.send("User not in guild", ephemeral=True)
|
||||||
|
return
|
||||||
await ctx.defer()
|
await ctx.defer()
|
||||||
await Warning(
|
await Warning(
|
||||||
user=user.id,
|
user=user.id,
|
||||||
|
@ -65,25 +75,24 @@ class WarningCog(Scale):
|
||||||
@slash_option(
|
@slash_option(
|
||||||
name="active",
|
name="active",
|
||||||
description="View active only",
|
description="View active only",
|
||||||
opt_type=OptionTypes.INTEGER,
|
opt_type=OptionTypes.BOOLEAN,
|
||||||
required=False,
|
required=False,
|
||||||
choices=[
|
|
||||||
SlashCommandChoice(name="Yes", value=1),
|
|
||||||
SlashCommandChoice(name="No", value=0),
|
|
||||||
],
|
|
||||||
)
|
)
|
||||||
@check(admin_or_permissions(Permissions.MANAGE_GUILD))
|
@check(admin_or_permissions(Permissions.MANAGE_GUILD))
|
||||||
async def _warnings(self, ctx: InteractionContext, user: User, active: bool = 1) -> None:
|
async def _warnings(self, ctx: InteractionContext, user: Member, active: bool = True) -> None:
|
||||||
active = bool(active)
|
|
||||||
|
|
||||||
warnings = (
|
warnings = (
|
||||||
await Warning.find(
|
await Warning.find(
|
||||||
user=user.id,
|
q(
|
||||||
guild=ctx.guild.id,
|
user=user.id,
|
||||||
|
guild=ctx.guild.id,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
.sort("created_at", -1)
|
.sort("created_at", -1)
|
||||||
.to_list(None)
|
.to_list(None)
|
||||||
)
|
)
|
||||||
|
if len(warnings) == 0:
|
||||||
|
await ctx.send("That user has no warnings.", ephemeral=True)
|
||||||
|
return
|
||||||
active_warns = get_all(warnings, active=True)
|
active_warns = get_all(warnings, active=True)
|
||||||
|
|
||||||
pages = []
|
pages = []
|
||||||
|
@ -91,7 +100,7 @@ class WarningCog(Scale):
|
||||||
if len(active_warns) == 0:
|
if len(active_warns) == 0:
|
||||||
embed = build_embed(
|
embed = build_embed(
|
||||||
title="Warnings",
|
title="Warnings",
|
||||||
description=f"{warnings.count()} total | 0 currently active",
|
description=f"{len(warnings)} total | 0 currently active",
|
||||||
fields=[],
|
fields=[],
|
||||||
)
|
)
|
||||||
embed.set_author(name=user.username, icon_url=user.display_avatar.url)
|
embed.set_author(name=user.username, icon_url=user.display_avatar.url)
|
||||||
|
@ -100,13 +109,14 @@ class WarningCog(Scale):
|
||||||
else:
|
else:
|
||||||
fields = []
|
fields = []
|
||||||
for warn in active_warns:
|
for warn in active_warns:
|
||||||
admin = await ctx.guild.get_member(warn.admin)
|
admin = await ctx.guild.fetch_member(warn.admin)
|
||||||
|
ts = int(warn.created_at.timestamp())
|
||||||
admin_name = "||`[redacted]`||"
|
admin_name = "||`[redacted]`||"
|
||||||
if admin:
|
if admin:
|
||||||
admin_name = f"{admin.username}#{admin.discriminator}"
|
admin_name = f"{admin.username}#{admin.discriminator}"
|
||||||
fields.append(
|
fields.append(
|
||||||
EmbedField(
|
EmbedField(
|
||||||
name=warn.created_at.strftime("%Y-%m-%d %H:%M:%S UTC"),
|
name=f"<t:{ts}:F>",
|
||||||
value=f"{warn.reason}\nAdmin: {admin_name}\n\u200b",
|
value=f"{warn.reason}\nAdmin: {admin_name}\n\u200b",
|
||||||
inline=False,
|
inline=False,
|
||||||
)
|
)
|
||||||
|
@ -129,8 +139,9 @@ class WarningCog(Scale):
|
||||||
else:
|
else:
|
||||||
fields = []
|
fields = []
|
||||||
for warn in warnings:
|
for warn in warnings:
|
||||||
|
ts = int(warn.created_at.timestamp())
|
||||||
title = "[A] " if warn.active else "[I] "
|
title = "[A] " if warn.active else "[I] "
|
||||||
title += warn.created_at.strftime("%Y-%m-%d %H:%M:%S UTC")
|
title += f"<t:{ts}:F>"
|
||||||
fields.append(
|
fields.append(
|
||||||
EmbedField(
|
EmbedField(
|
||||||
name=title,
|
name=title,
|
||||||
|
@ -150,6 +161,6 @@ class WarningCog(Scale):
|
||||||
embed.set_thumbnail(url=ctx.guild.icon.url)
|
embed.set_thumbnail(url=ctx.guild.icon.url)
|
||||||
pages.append(embed)
|
pages.append(embed)
|
||||||
|
|
||||||
paginator = Paginator(bot=self.bot, *pages, timeout=300)
|
paginator = Paginator.create_from_embeds(self.bot, *pages, timeout=300)
|
||||||
|
|
||||||
await paginator.send(ctx)
|
await paginator.send(ctx)
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
"""J.A.R.V.I.S. Autoreact Cog."""
|
"""JARVIS Autoreact Cog."""
|
||||||
|
import logging
|
||||||
import re
|
import re
|
||||||
from typing import Optional, Tuple
|
from typing import Optional, Tuple
|
||||||
|
|
||||||
|
@ -7,7 +8,7 @@ from dis_snek.client.utils.misc_utils import find
|
||||||
from dis_snek.models.discord.channel import GuildText
|
from dis_snek.models.discord.channel import GuildText
|
||||||
from dis_snek.models.snek.application_commands import (
|
from dis_snek.models.snek.application_commands import (
|
||||||
OptionTypes,
|
OptionTypes,
|
||||||
slash_command,
|
SlashCommand,
|
||||||
slash_option,
|
slash_option,
|
||||||
)
|
)
|
||||||
from dis_snek.models.snek.command import check
|
from dis_snek.models.snek.command import check
|
||||||
|
@ -19,14 +20,15 @@ from jarvis.utils.permissions import admin_or_permissions
|
||||||
|
|
||||||
|
|
||||||
class AutoReactCog(Scale):
|
class AutoReactCog(Scale):
|
||||||
"""J.A.R.V.I.S. Autoreact Cog."""
|
"""JARVIS Autoreact Cog."""
|
||||||
|
|
||||||
def __init__(self, bot: Snake):
|
def __init__(self, bot: Snake):
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
|
self.logger = logging.getLogger(__name__)
|
||||||
self.custom_emote = re.compile(r"^<:\w+:(\d+)>$")
|
self.custom_emote = re.compile(r"^<:\w+:(\d+)>$")
|
||||||
|
|
||||||
async def create_autoreact(
|
async def create_autoreact(
|
||||||
self, ctx: InteractionContext, channel: GuildText
|
self, ctx: InteractionContext, channel: GuildText, thread: bool
|
||||||
) -> Tuple[bool, Optional[str]]:
|
) -> Tuple[bool, Optional[str]]:
|
||||||
"""
|
"""
|
||||||
Create an autoreact monitor on a channel.
|
Create an autoreact monitor on a channel.
|
||||||
|
@ -34,6 +36,7 @@ class AutoReactCog(Scale):
|
||||||
Args:
|
Args:
|
||||||
ctx: Interaction context of command
|
ctx: Interaction context of command
|
||||||
channel: Channel to monitor
|
channel: Channel to monitor
|
||||||
|
thread: Create a thread
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Tuple of success? and error message
|
Tuple of success? and error message
|
||||||
|
@ -42,12 +45,13 @@ class AutoReactCog(Scale):
|
||||||
if exists:
|
if exists:
|
||||||
return False, f"Autoreact already exists for {channel.mention}."
|
return False, f"Autoreact already exists for {channel.mention}."
|
||||||
|
|
||||||
_ = Autoreact(
|
await Autoreact(
|
||||||
guild=ctx.guild.id,
|
guild=ctx.guild.id,
|
||||||
channel=channel.id,
|
channel=channel.id,
|
||||||
reactions=[],
|
reactions=[],
|
||||||
|
thread=thread,
|
||||||
admin=ctx.author.id,
|
admin=ctx.author.id,
|
||||||
).save()
|
).commit()
|
||||||
|
|
||||||
return True, None
|
return True, None
|
||||||
|
|
||||||
|
@ -62,10 +66,15 @@ class AutoReactCog(Scale):
|
||||||
Returns:
|
Returns:
|
||||||
Success?
|
Success?
|
||||||
"""
|
"""
|
||||||
return Autoreact.objects(guild=ctx.guild.id, channel=channel.id).delete() is not None
|
ar = await Autoreact.find_one(q(guild=ctx.guild.id, channel=channel.id))
|
||||||
|
if ar:
|
||||||
|
await ar.delete()
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
@slash_command(
|
autoreact = SlashCommand(name="autoreact", description="Channel message autoreacts")
|
||||||
name="autoreact",
|
|
||||||
|
@autoreact.subcommand(
|
||||||
sub_cmd_name="add",
|
sub_cmd_name="add",
|
||||||
sub_cmd_description="Add an autoreact emote to a channel",
|
sub_cmd_description="Add an autoreact emote to a channel",
|
||||||
)
|
)
|
||||||
|
@ -76,46 +85,57 @@ class AutoReactCog(Scale):
|
||||||
required=True,
|
required=True,
|
||||||
)
|
)
|
||||||
@slash_option(
|
@slash_option(
|
||||||
name="emote", description="Emote to add", opt_type=OptionTypes.STRING, required=True
|
name="thread", description="Create a thread?", opt_type=OptionTypes.BOOLEAN, required=False
|
||||||
|
)
|
||||||
|
@slash_option(
|
||||||
|
name="emote", description="Emote to add", opt_type=OptionTypes.STRING, required=False
|
||||||
)
|
)
|
||||||
@check(admin_or_permissions(Permissions.MANAGE_GUILD))
|
@check(admin_or_permissions(Permissions.MANAGE_GUILD))
|
||||||
async def _autoreact_add(self, ctx: InteractionContext, channel: GuildText, emote: str) -> None:
|
async def _autoreact_add(
|
||||||
|
self, ctx: InteractionContext, channel: GuildText, thread: bool = True, emote: str = None
|
||||||
|
) -> None:
|
||||||
await ctx.defer()
|
await ctx.defer()
|
||||||
custom_emoji = self.custom_emote.match(emote)
|
if emote:
|
||||||
standard_emoji = emote in emoji_list
|
custom_emoji = self.custom_emote.match(emote)
|
||||||
if not custom_emoji and not standard_emoji:
|
standard_emoji = emote in emoji_list
|
||||||
await ctx.send(
|
if not custom_emoji and not standard_emoji:
|
||||||
"Please use either an emote from this server or a unicode emoji.",
|
await ctx.send(
|
||||||
ephemeral=True,
|
"Please use either an emote from this server or a unicode emoji.",
|
||||||
)
|
ephemeral=True,
|
||||||
return
|
)
|
||||||
if custom_emoji:
|
|
||||||
emoji_id = int(custom_emoji.group(1))
|
|
||||||
if not find(lambda x: x.id == emoji_id, ctx.guild.emojis):
|
|
||||||
await ctx.send("Please use a custom emote from this server.", ephemeral=True)
|
|
||||||
return
|
return
|
||||||
|
if custom_emoji:
|
||||||
|
emoji_id = int(custom_emoji.group(1))
|
||||||
|
if not find(lambda x: x.id == emoji_id, await ctx.guild.fetch_all_custom_emojis()):
|
||||||
|
await ctx.send("Please use a custom emote from this server.", ephemeral=True)
|
||||||
|
return
|
||||||
autoreact = await Autoreact.find_one(q(guild=ctx.guild.id, channel=channel.id))
|
autoreact = await Autoreact.find_one(q(guild=ctx.guild.id, channel=channel.id))
|
||||||
if not autoreact:
|
if not autoreact:
|
||||||
self.create_autoreact(ctx, channel)
|
await self.create_autoreact(ctx, channel, thread)
|
||||||
autoreact = await Autoreact.find_one(q(guild=ctx.guild.id, channel=channel.id))
|
autoreact = await Autoreact.find_one(q(guild=ctx.guild.id, channel=channel.id))
|
||||||
if emote in autoreact.reactions:
|
if emote and emote in autoreact.reactions:
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
f"Emote already added to {channel.mention} autoreactions.",
|
f"Emote already added to {channel.mention} autoreactions.",
|
||||||
ephemeral=True,
|
ephemeral=True,
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
if len(autoreact.reactions) >= 5:
|
if emote and len(autoreact.reactions) >= 5:
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
"Max number of reactions hit. Remove a different one to add this one",
|
"Max number of reactions hit. Remove a different one to add this one",
|
||||||
ephemeral=True,
|
ephemeral=True,
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
autoreact.reactions.append(emote)
|
if emote:
|
||||||
autoreact.save()
|
autoreact.reactions.append(emote)
|
||||||
await ctx.send(f"Added {emote} to {channel.mention} autoreact.")
|
autoreact.thread = thread
|
||||||
|
await autoreact.commit()
|
||||||
|
message = ""
|
||||||
|
if emote:
|
||||||
|
message += f" Added {emote} to {channel.mention} autoreact."
|
||||||
|
message += f" Set autoreact thread creation to {thread} in {channel.mention}"
|
||||||
|
await ctx.send(message)
|
||||||
|
|
||||||
@slash_command(
|
@autoreact.subcommand(
|
||||||
name="autoreact",
|
|
||||||
sub_cmd_name="remove",
|
sub_cmd_name="remove",
|
||||||
sub_cmd_description="Remove an autoreact emote to a channel",
|
sub_cmd_description="Remove an autoreact emote to a channel",
|
||||||
)
|
)
|
||||||
|
@ -143,7 +163,7 @@ class AutoReactCog(Scale):
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
if emote.lower() == "all":
|
if emote.lower() == "all":
|
||||||
self.delete_autoreact(ctx, channel)
|
await self.delete_autoreact(ctx, channel)
|
||||||
await ctx.send(f"Autoreact removed from {channel.mention}")
|
await ctx.send(f"Autoreact removed from {channel.mention}")
|
||||||
elif emote not in autoreact.reactions:
|
elif emote not in autoreact.reactions:
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
|
@ -153,13 +173,12 @@ class AutoReactCog(Scale):
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
autoreact.reactions.remove(emote)
|
autoreact.reactions.remove(emote)
|
||||||
autoreact.save()
|
await autoreact.commit()
|
||||||
if len(autoreact.reactions) == 0:
|
if len(autoreact.reactions) == 0 and not autoreact.thread:
|
||||||
self.delete_autoreact(ctx, channel)
|
await self.delete_autoreact(ctx, channel)
|
||||||
await ctx.send(f"Removed {emote} from {channel.mention} autoreact.")
|
await ctx.send(f"Removed {emote} from {channel.mention} autoreact.")
|
||||||
|
|
||||||
@slash_command(
|
@autoreact.subcommand(
|
||||||
name="autoreact",
|
|
||||||
sub_cmd_name="list",
|
sub_cmd_name="list",
|
||||||
sub_cmd_description="List all autoreacts on a channel",
|
sub_cmd_description="List all autoreacts on a channel",
|
||||||
)
|
)
|
||||||
|
@ -188,5 +207,5 @@ class AutoReactCog(Scale):
|
||||||
|
|
||||||
|
|
||||||
def setup(bot: Snake) -> None:
|
def setup(bot: Snake) -> None:
|
||||||
"""Add AutoReactCog to J.A.R.V.I.S."""
|
"""Add AutoReactCog to JARVIS"""
|
||||||
AutoReactCog(bot)
|
AutoReactCog(bot)
|
||||||
|
|
54
jarvis/cogs/botutil.py
Normal file
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
|
import re
|
||||||
from datetime import datetime, timedelta
|
|
||||||
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
from dis_snek import InteractionContext, Snake
|
from dis_snek import InteractionContext, Scale, Snake
|
||||||
from dis_snek.ext.paginators import Paginator
|
from dis_snek.ext.paginators import Paginator
|
||||||
|
from dis_snek.models.discord.components import ActionRow, Button, ButtonStyles
|
||||||
from dis_snek.models.discord.embed import EmbedField
|
from dis_snek.models.discord.embed import EmbedField
|
||||||
from dis_snek.models.discord.user import Member, User
|
from dis_snek.models.discord.user import Member, User
|
||||||
from dis_snek.models.snek.application_commands import slash_command
|
from dis_snek.models.snek.application_commands import (
|
||||||
|
OptionTypes,
|
||||||
|
SlashCommand,
|
||||||
|
slash_option,
|
||||||
|
)
|
||||||
from dis_snek.models.snek.command import cooldown
|
from dis_snek.models.snek.command import cooldown
|
||||||
from dis_snek.models.snek.cooldowns import Buckets
|
from dis_snek.models.snek.cooldowns import Buckets
|
||||||
from jarvis_core.db import q
|
from jarvis_core.db import q
|
||||||
from jarvis_core.db.models import Guess
|
from jarvis_core.db.models import Guess
|
||||||
|
|
||||||
from jarvis.utils import build_embed
|
from jarvis.utils import build_embed
|
||||||
from jarvis.utils.cachecog import CacheCog
|
|
||||||
|
|
||||||
guild_ids = [578757004059738142, 520021794380447745, 862402786116763668]
|
guild_ids = [862402786116763668] # [578757004059738142, 520021794380447745, 862402786116763668]
|
||||||
|
|
||||||
valid = re.compile(r"[\w\s\-\\/.!@#$%^*()+=<>,\u0080-\U000E0FFF]*")
|
valid = re.compile(r"[\w\s\-\\/.!@#$%^*()+=<>,\u0080-\U000E0FFF]*")
|
||||||
invites = re.compile(
|
invites = re.compile(
|
||||||
|
@ -25,29 +29,38 @@ invites = re.compile(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class CTCCog(CacheCog):
|
class CTCCog(Scale):
|
||||||
"""J.A.R.V.I.S. Complete the Code 2 Cog."""
|
"""JARVIS Complete the Code 2 Cog."""
|
||||||
|
|
||||||
def __init__(self, bot: Snake):
|
def __init__(self, bot: Snake):
|
||||||
super().__init__(bot)
|
self.bot = bot
|
||||||
|
self.logger = logging.getLogger(__name__)
|
||||||
self._session = aiohttp.ClientSession()
|
self._session = aiohttp.ClientSession()
|
||||||
self.url = "https://completethecodetwo.cards/pw"
|
self.url = "https://completethecodetwo.cards/pw"
|
||||||
|
|
||||||
def __del__(self):
|
def __del__(self):
|
||||||
self._session.close()
|
self._session.close()
|
||||||
|
|
||||||
@slash_command(
|
ctc2 = SlashCommand(name="ctc2", description="CTC2 related commands", scopes=guild_ids)
|
||||||
name="ctc2", sub_cmd_name="about", description="CTC2 related commands", scopes=guild_ids
|
|
||||||
)
|
@ctc2.subcommand(sub_cmd_name="about")
|
||||||
@cooldown(bucket=Buckets.USER, rate=1, interval=30)
|
@cooldown(bucket=Buckets.USER, rate=1, interval=30)
|
||||||
async def _about(self, ctx: InteractionContext) -> None:
|
async def _about(self, ctx: InteractionContext) -> None:
|
||||||
await ctx.send("See https://completethecode.com for more information")
|
components = [
|
||||||
|
ActionRow(
|
||||||
|
Button(style=ButtonStyles.URL, url="https://completethecode.com", label="More Info")
|
||||||
|
)
|
||||||
|
]
|
||||||
|
await ctx.send(
|
||||||
|
"See https://completethecode.com for more information", components=components
|
||||||
|
)
|
||||||
|
|
||||||
@slash_command(
|
@ctc2.subcommand(
|
||||||
name="ctc2",
|
|
||||||
sub_cmd_name="pw",
|
sub_cmd_name="pw",
|
||||||
sub_cmd_description="Guess a password for https://completethecodetwo.cards",
|
sub_cmd_description="Guess a password for https://completethecodetwo.cards",
|
||||||
scopes=guild_ids,
|
)
|
||||||
|
@slash_option(
|
||||||
|
name="guess", description="Guess a password", opt_type=OptionTypes.STRING, required=True
|
||||||
)
|
)
|
||||||
@cooldown(bucket=Buckets.USER, rate=1, interval=2)
|
@cooldown(bucket=Buckets.USER, rate=1, interval=2)
|
||||||
async def _pw(self, ctx: InteractionContext, guess: str) -> None:
|
async def _pw(self, ctx: InteractionContext, guess: str) -> None:
|
||||||
|
@ -79,6 +92,7 @@ class CTCCog(CacheCog):
|
||||||
if guessed:
|
if guessed:
|
||||||
await ctx.send("Already guessed, dipshit.", ephemeral=True)
|
await ctx.send("Already guessed, dipshit.", ephemeral=True)
|
||||||
return
|
return
|
||||||
|
|
||||||
result = await self._session.post(self.url, data=guess)
|
result = await self._session.post(self.url, data=guess)
|
||||||
correct = False
|
correct = False
|
||||||
if 200 <= result.status < 400:
|
if 200 <= result.status < 400:
|
||||||
|
@ -86,28 +100,18 @@ class CTCCog(CacheCog):
|
||||||
correct = True
|
correct = True
|
||||||
else:
|
else:
|
||||||
await ctx.send("Nope.", ephemeral=True)
|
await ctx.send("Nope.", ephemeral=True)
|
||||||
_ = Guess(guess=guess, user=ctx.author.id, correct=correct).save()
|
await Guess(guess=guess, user=ctx.author.id, correct=correct).commit()
|
||||||
|
|
||||||
@slash_command(
|
@ctc2.subcommand(
|
||||||
name="ctc2",
|
|
||||||
sub_cmd_name="guesses",
|
sub_cmd_name="guesses",
|
||||||
sub_cmd_description="Show guesses made for https://completethecodetwo.cards",
|
sub_cmd_description="Show guesses made for https://completethecodetwo.cards",
|
||||||
scopes=guild_ids,
|
|
||||||
)
|
)
|
||||||
@cooldown(bucket=Buckets.USER, rate=1, interval=2)
|
@cooldown(bucket=Buckets.USER, rate=1, interval=2)
|
||||||
async def _guesses(self, ctx: InteractionContext) -> None:
|
async def _guesses(self, ctx: InteractionContext) -> None:
|
||||||
exists = self.check_cache(ctx)
|
await ctx.defer()
|
||||||
if exists:
|
guesses = Guess.find().sort("correct", -1).sort("id", -1)
|
||||||
await ctx.defer(ephemeral=True)
|
|
||||||
await ctx.send(
|
|
||||||
f"Please use existing interaction: {exists['paginator']._message.jump_url}",
|
|
||||||
ephemeral=True,
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
guesses = Guess.objects().order_by("-correct", "-id")
|
|
||||||
fields = []
|
fields = []
|
||||||
for guess in guesses:
|
async for guess in guesses:
|
||||||
user = await ctx.guild.get_member(guess["user"])
|
user = await ctx.guild.get_member(guess["user"])
|
||||||
if not user:
|
if not user:
|
||||||
user = await self.bot.fetch_user(guess["user"])
|
user = await self.bot.fetch_user(guess["user"])
|
||||||
|
@ -141,17 +145,9 @@ class CTCCog(CacheCog):
|
||||||
|
|
||||||
paginator = Paginator.create_from_embeds(self.bot, *pages, timeout=300)
|
paginator = Paginator.create_from_embeds(self.bot, *pages, timeout=300)
|
||||||
|
|
||||||
self.cache[hash(paginator)] = {
|
|
||||||
"guild": ctx.guild.id,
|
|
||||||
"user": ctx.author.id,
|
|
||||||
"timeout": datetime.utcnow() + timedelta(minutes=5),
|
|
||||||
"command": ctx.subcommand_name,
|
|
||||||
"paginator": paginator,
|
|
||||||
}
|
|
||||||
|
|
||||||
await paginator.send(ctx)
|
await paginator.send(ctx)
|
||||||
|
|
||||||
|
|
||||||
def setup(bot: Snake) -> None:
|
def setup(bot: Snake) -> None:
|
||||||
"""Add CTCCog to J.A.R.V.I.S."""
|
"""Add CTCCog to JARVIS"""
|
||||||
CTCCog(bot)
|
CTCCog(bot)
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
"""J.A.R.V.I.S. dbrand cog."""
|
"""JARVIS dbrand cog."""
|
||||||
|
import logging
|
||||||
import re
|
import re
|
||||||
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
|
@ -6,7 +7,7 @@ from dis_snek import InteractionContext, Scale, Snake
|
||||||
from dis_snek.models.discord.embed import EmbedField
|
from dis_snek.models.discord.embed import EmbedField
|
||||||
from dis_snek.models.snek.application_commands import (
|
from dis_snek.models.snek.application_commands import (
|
||||||
OptionTypes,
|
OptionTypes,
|
||||||
slash_command,
|
SlashCommand,
|
||||||
slash_option,
|
slash_option,
|
||||||
)
|
)
|
||||||
from dis_snek.models.snek.command import cooldown
|
from dis_snek.models.snek.command import cooldown
|
||||||
|
@ -16,18 +17,19 @@ from jarvis.config import get_config
|
||||||
from jarvis.data.dbrand import shipping_lookup
|
from jarvis.data.dbrand import shipping_lookup
|
||||||
from jarvis.utils import build_embed
|
from jarvis.utils import build_embed
|
||||||
|
|
||||||
guild_ids = [578757004059738142, 520021794380447745, 862402786116763668]
|
guild_ids = [862402786116763668] # [578757004059738142, 520021794380447745, 862402786116763668]
|
||||||
|
|
||||||
|
|
||||||
class DbrandCog(Scale):
|
class DbrandCog(Scale):
|
||||||
"""
|
"""
|
||||||
dbrand functions for J.A.R.V.I.S.
|
dbrand functions for JARVIS
|
||||||
|
|
||||||
Mostly support functions. Credit @cpixl for the shipping API
|
Mostly support functions. Credit @cpixl for the shipping API
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, bot: Snake):
|
def __init__(self, bot: Snake):
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
|
self.logger = logging.getLogger(__name__)
|
||||||
self.base_url = "https://dbrand.com/"
|
self.base_url = "https://dbrand.com/"
|
||||||
self._session = aiohttp.ClientSession()
|
self._session = aiohttp.ClientSession()
|
||||||
self._session.headers.update({"Content-Type": "application/json"})
|
self._session.headers.update({"Content-Type": "application/json"})
|
||||||
|
@ -37,121 +39,53 @@ class DbrandCog(Scale):
|
||||||
def __del__(self):
|
def __del__(self):
|
||||||
self._session.close()
|
self._session.close()
|
||||||
|
|
||||||
@slash_command(
|
db = SlashCommand(name="db", description="dbrand commands", scopes=guild_ids)
|
||||||
name="db",
|
|
||||||
sub_cmd_name="skin",
|
|
||||||
scopes=guild_ids,
|
|
||||||
sub_cmd_description="See what skins are available",
|
|
||||||
)
|
|
||||||
@cooldown(bucket=Buckets.USER, rate=1, interval=30)
|
|
||||||
async def _skin(self, ctx: InteractionContext) -> None:
|
|
||||||
await ctx.send(self.base_url + "/skins")
|
|
||||||
|
|
||||||
@slash_command(
|
@db.subcommand(sub_cmd_name="info", sub_cmd_description="Get useful links")
|
||||||
name="db",
|
|
||||||
sub_cmd_name="robotcamo",
|
|
||||||
scopes=guild_ids,
|
|
||||||
sub_cmd_description="Get some robot camo. Make Tony Stark proud",
|
|
||||||
)
|
|
||||||
@cooldown(bucket=Buckets.USER, rate=1, interval=30)
|
@cooldown(bucket=Buckets.USER, rate=1, interval=30)
|
||||||
async def _camo(self, ctx: InteractionContext) -> None:
|
async def _info(self, ctx: InteractionContext) -> None:
|
||||||
await ctx.send(self.base_url + "robot-camo")
|
urls = [
|
||||||
|
f"[Get Skins]({self.base_url + 'skins'})",
|
||||||
|
f"[Robot Camo]({self.base_url + 'robot-camo'})",
|
||||||
|
f"[Get a Grip]({self.base_url + 'grip'})",
|
||||||
|
f"[Shop All Products]({self.base_url + 'shop'})",
|
||||||
|
f"[Order Status]({self.base_url + 'order-status'})",
|
||||||
|
f"[dbrand Status]({self.base_url + 'status'})",
|
||||||
|
f"[Be (not) extorted]({self.base_url + 'not-extortion'})",
|
||||||
|
"[Robot Camo Wallpapers](https://db.io/wallpapers)",
|
||||||
|
]
|
||||||
|
embed = build_embed(
|
||||||
|
title="Useful Links", description="\n\n".join(urls), fields=[], color="#FFBB00"
|
||||||
|
)
|
||||||
|
embed.set_footer(
|
||||||
|
text="dbrand.com",
|
||||||
|
icon_url="https://dev.zevaryx.com/db_logo.png",
|
||||||
|
)
|
||||||
|
embed.set_thumbnail(url="https://dev.zevaryx.com/db_logo.png")
|
||||||
|
embed.set_author(
|
||||||
|
name="dbrand", url=self.base_url, icon_url="https://dev.zevaryx.com/db_logo.png"
|
||||||
|
)
|
||||||
|
await ctx.send(embed=embed)
|
||||||
|
|
||||||
@slash_command(
|
@db.subcommand(
|
||||||
name="db",
|
|
||||||
sub_cmd_name="grip",
|
|
||||||
scopes=guild_ids,
|
|
||||||
sub_cmd_description="See devices with Grip support",
|
|
||||||
)
|
|
||||||
@cooldown(bucket=Buckets.USER, rate=1, interval=30)
|
|
||||||
async def _grip(self, ctx: InteractionContext) -> None:
|
|
||||||
await ctx.send(self.base_url + "grip")
|
|
||||||
|
|
||||||
@slash_command(
|
|
||||||
name="db",
|
|
||||||
sub_cmd_name="contact",
|
sub_cmd_name="contact",
|
||||||
scopes=guild_ids,
|
|
||||||
sub_cmd_description="Contact support",
|
sub_cmd_description="Contact support",
|
||||||
)
|
)
|
||||||
@cooldown(bucket=Buckets.USER, rate=1, interval=30)
|
@cooldown(bucket=Buckets.USER, rate=1, interval=30)
|
||||||
async def _contact(self, ctx: InteractionContext) -> None:
|
async def _contact(self, ctx: InteractionContext) -> None:
|
||||||
await ctx.send("Contact dbrand support here: " + self.base_url + "contact")
|
await ctx.send("Contact dbrand support here: " + self.base_url + "contact")
|
||||||
|
|
||||||
@slash_command(
|
@db.subcommand(
|
||||||
name="db",
|
|
||||||
sub_cmd_name="support",
|
sub_cmd_name="support",
|
||||||
scopes=guild_ids,
|
|
||||||
sub_cmd_description="Contact support",
|
sub_cmd_description="Contact support",
|
||||||
)
|
)
|
||||||
@cooldown(bucket=Buckets.USER, rate=1, interval=30)
|
@cooldown(bucket=Buckets.USER, rate=1, interval=30)
|
||||||
async def _support(self, ctx: InteractionContext) -> None:
|
async def _support(self, ctx: InteractionContext) -> None:
|
||||||
await ctx.send("Contact dbrand support here: " + self.base_url + "contact")
|
await ctx.send("Contact dbrand support here: " + self.base_url + "contact")
|
||||||
|
|
||||||
@slash_command(
|
@db.subcommand(
|
||||||
name="db",
|
|
||||||
sub_cmd_name="orderstat",
|
|
||||||
scopes=guild_ids,
|
|
||||||
sub_cmd_description="Get your order status",
|
|
||||||
)
|
|
||||||
@cooldown(bucket=Buckets.USER, rate=1, interval=30)
|
|
||||||
async def _orderstat(self, ctx: InteractionContext) -> None:
|
|
||||||
await ctx.send(self.base_url + "order-status")
|
|
||||||
|
|
||||||
@slash_command(
|
|
||||||
name="db",
|
|
||||||
sub_cmd_name="orders",
|
|
||||||
scopes=guild_ids,
|
|
||||||
sub_cmd_description="Get your order status",
|
|
||||||
)
|
|
||||||
@cooldown(bucket=Buckets.USER, rate=1, interval=30)
|
|
||||||
async def _orders(self, ctx: InteractionContext) -> None:
|
|
||||||
await ctx.send(self.base_url + "order-status")
|
|
||||||
|
|
||||||
@slash_command(
|
|
||||||
name="db",
|
|
||||||
sub_cmd_name="status",
|
|
||||||
scopes=guild_ids,
|
|
||||||
sub_cmd_description="dbrand status",
|
|
||||||
)
|
|
||||||
@cooldown(bucket=Buckets.USER, rate=1, interval=30)
|
|
||||||
async def _status(self, ctx: InteractionContext) -> None:
|
|
||||||
await ctx.send(self.base_url + "status")
|
|
||||||
|
|
||||||
@slash_command(
|
|
||||||
name="db",
|
|
||||||
sub_cmd_name="buy",
|
|
||||||
scopes=guild_ids,
|
|
||||||
sub_cmd_description="Give us your money!",
|
|
||||||
)
|
|
||||||
@cooldown(bucket=Buckets.USER, rate=1, interval=30)
|
|
||||||
async def _buy(self, ctx: InteractionContext) -> None:
|
|
||||||
await ctx.send("Give us your money! " + self.base_url + "shop")
|
|
||||||
|
|
||||||
@slash_command(
|
|
||||||
name="db",
|
|
||||||
sub_cmd_name="extortion",
|
|
||||||
scopes=guild_ids,
|
|
||||||
sub_cmd_description="(not) extortion",
|
|
||||||
)
|
|
||||||
@cooldown(bucket=Buckets.USER, rate=1, interval=30)
|
|
||||||
async def _extort(self, ctx: InteractionContext) -> None:
|
|
||||||
await ctx.send("Be (not) extorted here: " + self.base_url + "not-extortion")
|
|
||||||
|
|
||||||
@slash_command(
|
|
||||||
name="db",
|
|
||||||
sub_cmd_name="wallpapers",
|
|
||||||
sub_cmd_description="Robot Camo Wallpapers",
|
|
||||||
scopes=guild_ids,
|
|
||||||
)
|
|
||||||
@cooldown(bucket=Buckets.USER, rate=1, interval=30)
|
|
||||||
async def _wallpapers(self, ctx: InteractionContext) -> None:
|
|
||||||
await ctx.send("Get robot camo wallpapers here: https://db.io/wallpapers")
|
|
||||||
|
|
||||||
@slash_command(
|
|
||||||
name="db",
|
|
||||||
sub_cmd_name="ship",
|
sub_cmd_name="ship",
|
||||||
sub_cmd_description="Get shipping information for your country",
|
sub_cmd_description="Get shipping information for your country",
|
||||||
scopes=guild_ids,
|
|
||||||
)
|
)
|
||||||
@slash_option(
|
@slash_option(
|
||||||
name="search",
|
name="search",
|
||||||
|
@ -215,7 +149,7 @@ class DbrandCog(Scale):
|
||||||
)
|
)
|
||||||
embed = build_embed(
|
embed = build_embed(
|
||||||
title="Shipping to {}".format(data["country"]),
|
title="Shipping to {}".format(data["country"]),
|
||||||
sub_cmd_description=description,
|
description=description,
|
||||||
color="#FFBB00",
|
color="#FFBB00",
|
||||||
fields=fields,
|
fields=fields,
|
||||||
url=self.base_url + "shipping/" + country,
|
url=self.base_url + "shipping/" + country,
|
||||||
|
@ -229,7 +163,7 @@ class DbrandCog(Scale):
|
||||||
elif not data["is_valid"]:
|
elif not data["is_valid"]:
|
||||||
embed = build_embed(
|
embed = build_embed(
|
||||||
title="Check Shipping Times",
|
title="Check Shipping Times",
|
||||||
sub_cmd_description=(
|
description=(
|
||||||
"Country not found.\nYou can [view all shipping "
|
"Country not found.\nYou can [view all shipping "
|
||||||
"destinations here](https://dbrand.com/shipping)"
|
"destinations here](https://dbrand.com/shipping)"
|
||||||
),
|
),
|
||||||
|
@ -246,7 +180,7 @@ class DbrandCog(Scale):
|
||||||
elif not data["shipping_available"]:
|
elif not data["shipping_available"]:
|
||||||
embed = build_embed(
|
embed = build_embed(
|
||||||
title="Shipping to {}".format(data["country"]),
|
title="Shipping to {}".format(data["country"]),
|
||||||
sub_cmd_description=(
|
description=(
|
||||||
"No shipping available.\nTime to move to a country"
|
"No shipping available.\nTime to move to a country"
|
||||||
" that has shipping available.\nYou can [find a new country "
|
" that has shipping available.\nYou can [find a new country "
|
||||||
"to live in here](https://dbrand.com/shipping)"
|
"to live in here](https://dbrand.com/shipping)"
|
||||||
|
@ -264,5 +198,5 @@ class DbrandCog(Scale):
|
||||||
|
|
||||||
|
|
||||||
def setup(bot: Snake) -> None:
|
def setup(bot: Snake) -> None:
|
||||||
"""Add dbrandcog to J.A.R.V.I.S."""
|
"""Add dbrandcog to JARVIS"""
|
||||||
DbrandCog(bot)
|
DbrandCog(bot)
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
"""J.A.R.V.I.S. Developer Cog."""
|
"""JARVIS Developer Cog."""
|
||||||
import base64
|
import base64
|
||||||
import hashlib
|
import hashlib
|
||||||
|
import logging
|
||||||
import re
|
import re
|
||||||
import subprocess # noqa: S404
|
import subprocess # noqa: S404
|
||||||
import uuid as uuidpy
|
import uuid as uuidpy
|
||||||
|
@ -45,7 +46,11 @@ MAX_FILESIZE = 5 * (1024**3) # 5GB
|
||||||
|
|
||||||
|
|
||||||
class DevCog(Scale):
|
class DevCog(Scale):
|
||||||
"""J.A.R.V.I.S. Developer Cog."""
|
"""JARVIS Developer Cog."""
|
||||||
|
|
||||||
|
def __init__(self, bot: Snake):
|
||||||
|
self.bot = bot
|
||||||
|
self.logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@slash_command(name="hash", description="Hash some data")
|
@slash_command(name="hash", description="Hash some data")
|
||||||
@slash_option(
|
@slash_option(
|
||||||
|
@ -83,8 +88,9 @@ class DevCog(Scale):
|
||||||
title = attach.filename
|
title = attach.filename
|
||||||
elif url.match(data):
|
elif url.match(data):
|
||||||
try:
|
try:
|
||||||
if await get_size(data) > MAX_FILESIZE:
|
if (size := await get_size(data)) > MAX_FILESIZE:
|
||||||
await ctx.send("Please hash files that are <= 5GB in size", ephemeral=True)
|
await ctx.send("Please hash files that are <= 5GB in size", ephemeral=True)
|
||||||
|
self.logger.debug(f"Refused to hash file of size {convert_bytesize(size)}")
|
||||||
return
|
return
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
await ctx.send(f"Failed to retrieve URL: ```\n{e}\n```", ephemeral=True)
|
await ctx.send(f"Failed to retrieve URL: ```\n{e}\n```", ephemeral=True)
|
||||||
|
@ -162,6 +168,9 @@ class DevCog(Scale):
|
||||||
name="uuid2ulid",
|
name="uuid2ulid",
|
||||||
description="Convert a UUID to a ULID",
|
description="Convert a UUID to a ULID",
|
||||||
)
|
)
|
||||||
|
@slash_option(
|
||||||
|
name="uuid", description="UUID to convert", opt_type=OptionTypes.STRING, required=True
|
||||||
|
)
|
||||||
@cooldown(bucket=Buckets.USER, rate=1, interval=2)
|
@cooldown(bucket=Buckets.USER, rate=1, interval=2)
|
||||||
async def _uuid2ulid(self, ctx: InteractionContext, uuid: str) -> None:
|
async def _uuid2ulid(self, ctx: InteractionContext, uuid: str) -> None:
|
||||||
if UUID_VERIFY.match(uuid):
|
if UUID_VERIFY.match(uuid):
|
||||||
|
@ -174,6 +183,9 @@ class DevCog(Scale):
|
||||||
name="ulid2uuid",
|
name="ulid2uuid",
|
||||||
description="Convert a ULID to a UUID",
|
description="Convert a ULID to a UUID",
|
||||||
)
|
)
|
||||||
|
@slash_option(
|
||||||
|
name="ulid", description="ULID to convert", opt_type=OptionTypes.STRING, required=True
|
||||||
|
)
|
||||||
@cooldown(bucket=Buckets.USER, rate=1, interval=2)
|
@cooldown(bucket=Buckets.USER, rate=1, interval=2)
|
||||||
async def _ulid2uuid(self, ctx: InteractionContext, ulid: str) -> None:
|
async def _ulid2uuid(self, ctx: InteractionContext, ulid: str) -> None:
|
||||||
if ULID_VERIFY.match(ulid):
|
if ULID_VERIFY.match(ulid):
|
||||||
|
@ -199,9 +211,19 @@ class DevCog(Scale):
|
||||||
required=True,
|
required=True,
|
||||||
)
|
)
|
||||||
async def _encode(self, ctx: InteractionContext, method: str, data: str) -> None:
|
async def _encode(self, ctx: InteractionContext, method: str, data: str) -> None:
|
||||||
|
if invites.search(data):
|
||||||
|
await ctx.send(
|
||||||
|
"Please don't use this to bypass invite restrictions",
|
||||||
|
ephemeral=True,
|
||||||
|
)
|
||||||
|
return
|
||||||
mstr = method
|
mstr = method
|
||||||
method = getattr(base64, method + "encode")
|
method = getattr(base64, method + "encode")
|
||||||
encoded = method(data.encode("UTF-8")).decode("UTF-8")
|
try:
|
||||||
|
encoded = method(data.encode("UTF-8")).decode("UTF-8")
|
||||||
|
except Exception as e:
|
||||||
|
await ctx.send(f"Failed to encode data: {e}")
|
||||||
|
return
|
||||||
fields = [
|
fields = [
|
||||||
EmbedField(name="Plaintext", value=f"`{data}`", inline=False),
|
EmbedField(name="Plaintext", value=f"`{data}`", inline=False),
|
||||||
EmbedField(name=mstr, value=f"`{encoded}`", inline=False),
|
EmbedField(name=mstr, value=f"`{encoded}`", inline=False),
|
||||||
|
@ -226,7 +248,11 @@ class DevCog(Scale):
|
||||||
async def _decode(self, ctx: InteractionContext, method: str, data: str) -> None:
|
async def _decode(self, ctx: InteractionContext, method: str, data: str) -> None:
|
||||||
mstr = method
|
mstr = method
|
||||||
method = getattr(base64, method + "decode")
|
method = getattr(base64, method + "decode")
|
||||||
decoded = method(data.encode("UTF-8")).decode("UTF-8")
|
try:
|
||||||
|
decoded = method(data.encode("UTF-8")).decode("UTF-8")
|
||||||
|
except Exception as e:
|
||||||
|
await ctx.send(f"Failed to decode data: {e}")
|
||||||
|
return
|
||||||
if invites.search(decoded):
|
if invites.search(decoded):
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
"Please don't use this to bypass invite restrictions",
|
"Please don't use this to bypass invite restrictions",
|
||||||
|
@ -240,7 +266,7 @@ class DevCog(Scale):
|
||||||
embed = build_embed(title="Decoded Data", description="", fields=fields)
|
embed = build_embed(title="Decoded Data", description="", fields=fields)
|
||||||
await ctx.send(embed=embed)
|
await ctx.send(embed=embed)
|
||||||
|
|
||||||
@slash_command(name="cloc", description="Get J.A.R.V.I.S. lines of code")
|
@slash_command(name="cloc", description="Get JARVIS lines of code")
|
||||||
@cooldown(bucket=Buckets.CHANNEL, rate=1, interval=30)
|
@cooldown(bucket=Buckets.CHANNEL, rate=1, interval=30)
|
||||||
async def _cloc(self, ctx: InteractionContext) -> None:
|
async def _cloc(self, ctx: InteractionContext) -> None:
|
||||||
output = subprocess.check_output( # noqa: S603, S607
|
output = subprocess.check_output( # noqa: S603, S607
|
||||||
|
@ -250,5 +276,5 @@ class DevCog(Scale):
|
||||||
|
|
||||||
|
|
||||||
def setup(bot: Snake) -> None:
|
def setup(bot: Snake) -> None:
|
||||||
"""Add DevCog to J.A.R.V.I.S."""
|
"""Add DevCog to JARVIS"""
|
||||||
DevCog(bot)
|
DevCog(bot)
|
||||||
|
|
|
@ -1,35 +1,46 @@
|
||||||
"""J.A.R.V.I.S. GitLab Cog."""
|
"""JARVIS GitLab Cog."""
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
import gitlab
|
import gitlab
|
||||||
from dis_snek import InteractionContext, Scale, Snake
|
from dis_snek import InteractionContext, Scale, Snake
|
||||||
from dis_snek.ext.paginators import Paginator
|
from dis_snek.ext.paginators import Paginator
|
||||||
from dis_snek.models.discord.embed import Embed, EmbedField
|
from dis_snek.models.discord.embed import Embed, EmbedField
|
||||||
|
from dis_snek.models.discord.modal import InputText, Modal, TextStyles
|
||||||
|
from dis_snek.models.discord.user import Member
|
||||||
from dis_snek.models.snek.application_commands import (
|
from dis_snek.models.snek.application_commands import (
|
||||||
OptionTypes,
|
OptionTypes,
|
||||||
|
SlashCommand,
|
||||||
SlashCommandChoice,
|
SlashCommandChoice,
|
||||||
slash_command,
|
slash_command,
|
||||||
slash_option,
|
slash_option,
|
||||||
)
|
)
|
||||||
|
from dis_snek.models.snek.command import cooldown
|
||||||
|
from dis_snek.models.snek.cooldowns import Buckets
|
||||||
|
|
||||||
from jarvis.config import get_config
|
from jarvis.config import JarvisConfig
|
||||||
from jarvis.utils import build_embed
|
from jarvis.utils import build_embed
|
||||||
|
|
||||||
guild_ids = [862402786116763668]
|
guild_ids = [862402786116763668]
|
||||||
|
|
||||||
|
|
||||||
class GitlabCog(Scale):
|
class GitlabCog(Scale):
|
||||||
"""J.A.R.V.I.S. GitLab Cog."""
|
"""JARVIS GitLab Cog."""
|
||||||
|
|
||||||
def __init__(self, bot: Snake):
|
def __init__(self, bot: Snake):
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
config = get_config()
|
self.logger = logging.getLogger(__name__)
|
||||||
|
config = JarvisConfig.from_yaml()
|
||||||
self._gitlab = gitlab.Gitlab("https://git.zevaryx.com", private_token=config.gitlab_token)
|
self._gitlab = gitlab.Gitlab("https://git.zevaryx.com", private_token=config.gitlab_token)
|
||||||
# J.A.R.V.I.S. GitLab ID is 29
|
# JARVIS GitLab ID is 29
|
||||||
self.project = self._gitlab.projects.get(29)
|
self.project = self._gitlab.projects.get(29)
|
||||||
|
|
||||||
@slash_command(
|
gl = SlashCommand(name="gl", description="Get GitLab info", scopes=guild_ids)
|
||||||
name="gl", sub_cmd_name="issue", description="Get an issue from GitLab", scopes=guild_ids
|
|
||||||
|
@gl.subcommand(
|
||||||
|
sub_cmd_name="issue",
|
||||||
|
sub_cmd_description="Get an issue from GitLab",
|
||||||
)
|
)
|
||||||
@slash_option(name="id", description="Issue ID", opt_type=OptionTypes.INTEGER, required=True)
|
@slash_option(name="id", description="Issue ID", opt_type=OptionTypes.INTEGER, required=True)
|
||||||
async def _issue(self, ctx: InteractionContext, id: int) -> None:
|
async def _issue(self, ctx: InteractionContext, id: int) -> None:
|
||||||
|
@ -85,7 +96,7 @@ class GitlabCog(Scale):
|
||||||
)
|
)
|
||||||
embed.set_author(
|
embed.set_author(
|
||||||
name=issue.author["name"],
|
name=issue.author["name"],
|
||||||
icon_url=issue.author["display_avatar"],
|
icon_url=issue.author["avatar_url"],
|
||||||
url=issue.author["web_url"],
|
url=issue.author["web_url"],
|
||||||
)
|
)
|
||||||
embed.set_thumbnail(
|
embed.set_thumbnail(
|
||||||
|
@ -93,11 +104,9 @@ class GitlabCog(Scale):
|
||||||
)
|
)
|
||||||
await ctx.send(embed=embed)
|
await ctx.send(embed=embed)
|
||||||
|
|
||||||
@slash_command(
|
@gl.subcommand(
|
||||||
name="gl",
|
|
||||||
sub_cmd_name="milestone",
|
sub_cmd_name="milestone",
|
||||||
description="Get a milestone from GitLab",
|
sub_cmd_description="Get a milestone from GitLab",
|
||||||
scopes=guild_ids,
|
|
||||||
)
|
)
|
||||||
@slash_option(
|
@slash_option(
|
||||||
name="id", description="Milestone ID", opt_type=OptionTypes.INTEGER, required=True
|
name="id", description="Milestone ID", opt_type=OptionTypes.INTEGER, required=True
|
||||||
|
@ -140,7 +149,7 @@ class GitlabCog(Scale):
|
||||||
url=milestone.web_url,
|
url=milestone.web_url,
|
||||||
)
|
)
|
||||||
embed.set_author(
|
embed.set_author(
|
||||||
name="J.A.R.V.I.S.",
|
name="JARVIS",
|
||||||
url="https://git.zevaryx.com/jarvis",
|
url="https://git.zevaryx.com/jarvis",
|
||||||
icon_url="https://git.zevaryx.com/uploads/-/system/user/avatar/11/avatar.png",
|
icon_url="https://git.zevaryx.com/uploads/-/system/user/avatar/11/avatar.png",
|
||||||
)
|
)
|
||||||
|
@ -149,11 +158,9 @@ class GitlabCog(Scale):
|
||||||
)
|
)
|
||||||
await ctx.send(embed=embed)
|
await ctx.send(embed=embed)
|
||||||
|
|
||||||
@slash_command(
|
@gl.subcommand(
|
||||||
name="gl",
|
|
||||||
sub_cmd_name="mr",
|
sub_cmd_name="mr",
|
||||||
description="Get a merge request from GitLab",
|
sub_cmd_description="Get a merge request from GitLab",
|
||||||
scopes=guild_ids,
|
|
||||||
)
|
)
|
||||||
@slash_option(
|
@slash_option(
|
||||||
name="id", description="Merge Request ID", opt_type=OptionTypes.INTEGER, required=True
|
name="id", description="Merge Request ID", opt_type=OptionTypes.INTEGER, required=True
|
||||||
|
@ -181,25 +188,26 @@ class GitlabCog(Scale):
|
||||||
labels = "None"
|
labels = "None"
|
||||||
|
|
||||||
fields = [
|
fields = [
|
||||||
EmbedField(name="State", value=mr.state[0].upper() + mr.state[1:]),
|
EmbedField(name="State", value=mr.state[0].upper() + mr.state[1:], inline=True),
|
||||||
EmbedField(name="Assignee", value=assignee),
|
EmbedField(name="Draft?", value=str(mr.draft), inline=True),
|
||||||
EmbedField(name="Labels", value=labels),
|
EmbedField(name="Assignee", value=assignee, inline=True),
|
||||||
|
EmbedField(name="Labels", value=labels, inline=True),
|
||||||
]
|
]
|
||||||
if mr.labels:
|
if mr.labels:
|
||||||
color = self.project.labels.get(mr.labels[0]).color
|
color = self.project.labels.get(mr.labels[0]).color
|
||||||
else:
|
else:
|
||||||
color = "#00FFEE"
|
color = "#00FFEE"
|
||||||
fields.append(EmbedField(name="Created At", value=created_at))
|
fields.append(EmbedField(name="Created At", value=created_at, inline=True))
|
||||||
if mr.state == "merged":
|
if mr.state == "merged":
|
||||||
merged_at = datetime.strptime(mr.merged_at, "%Y-%m-%dT%H:%M:%S.%fZ").strftime(
|
merged_at = datetime.strptime(mr.merged_at, "%Y-%m-%dT%H:%M:%S.%fZ").strftime(
|
||||||
"%Y-%m-%d %H:%M:%S UTC"
|
"%Y-%m-%d %H:%M:%S UTC"
|
||||||
)
|
)
|
||||||
fields.append(EmbedField(name="Merged At", value=merged_at))
|
fields.append(EmbedField(name="Merged At", value=merged_at, inline=True))
|
||||||
elif mr.state == "closed":
|
elif mr.state == "closed":
|
||||||
closed_at = datetime.strptime(mr.closed_at, "%Y-%m-%dT%H:%M:%S.%fZ").strftime(
|
closed_at = datetime.strptime(mr.closed_at, "%Y-%m-%dT%H:%M:%S.%fZ").strftime(
|
||||||
"%Y-%m-%d %H:%M:%S UTC"
|
"%Y-%m-%d %H:%M:%S UTC"
|
||||||
)
|
)
|
||||||
fields.append(EmbedField(name="Closed At", value=closed_at))
|
fields.append(EmbedField(name="Closed At", value=closed_at, inline=True))
|
||||||
if mr.milestone:
|
if mr.milestone:
|
||||||
fields.append(
|
fields.append(
|
||||||
EmbedField(
|
EmbedField(
|
||||||
|
@ -219,7 +227,7 @@ class GitlabCog(Scale):
|
||||||
)
|
)
|
||||||
embed.set_author(
|
embed.set_author(
|
||||||
name=mr.author["name"],
|
name=mr.author["name"],
|
||||||
icon_url=mr.author["display_avatar"],
|
icon_url=mr.author["avatar_url"],
|
||||||
url=mr.author["web_url"],
|
url=mr.author["web_url"],
|
||||||
)
|
)
|
||||||
embed.set_thumbnail(
|
embed.set_thumbnail(
|
||||||
|
@ -232,7 +240,7 @@ class GitlabCog(Scale):
|
||||||
title = ""
|
title = ""
|
||||||
if t_state:
|
if t_state:
|
||||||
title = f"{t_state} "
|
title = f"{t_state} "
|
||||||
title += f"J.A.R.V.I.S. {name}s"
|
title += f"JARVIS {name}s"
|
||||||
fields = []
|
fields = []
|
||||||
for item in api_list:
|
for item in api_list:
|
||||||
description = item.description or "No description"
|
description = item.description or "No description"
|
||||||
|
@ -248,10 +256,10 @@ class GitlabCog(Scale):
|
||||||
title=title,
|
title=title,
|
||||||
description="",
|
description="",
|
||||||
fields=fields,
|
fields=fields,
|
||||||
url=f"https://git.zevaryx.com/stark-industries/j.a.r.v.i.s./{name.replace(' ', '_')}s",
|
url=f"https://git.zevaryx.com/stark-industries/JARVIS/{name.replace(' ', '_')}s",
|
||||||
)
|
)
|
||||||
embed.set_author(
|
embed.set_author(
|
||||||
name="J.A.R.V.I.S.",
|
name="JARVIS",
|
||||||
url="https://git.zevaryx.com/jarvis",
|
url="https://git.zevaryx.com/jarvis",
|
||||||
icon_url="https://git.zevaryx.com/uploads/-/system/user/avatar/11/avatar.png",
|
icon_url="https://git.zevaryx.com/uploads/-/system/user/avatar/11/avatar.png",
|
||||||
)
|
)
|
||||||
|
@ -260,8 +268,9 @@ class GitlabCog(Scale):
|
||||||
)
|
)
|
||||||
return embed
|
return embed
|
||||||
|
|
||||||
@slash_command(
|
@gl.subcommand(
|
||||||
name="gl", sub_cmd_name="issues", description="Get issues from GitLab", scopes=guild_ids
|
sub_cmd_name="issues",
|
||||||
|
sub_cmd_description="Get issues from GitLab",
|
||||||
)
|
)
|
||||||
@slash_option(
|
@slash_option(
|
||||||
name="state",
|
name="state",
|
||||||
|
@ -313,11 +322,9 @@ class GitlabCog(Scale):
|
||||||
|
|
||||||
await paginator.send(ctx)
|
await paginator.send(ctx)
|
||||||
|
|
||||||
@slash_command(
|
@gl.subcommand(
|
||||||
name="gl",
|
|
||||||
sub_cmd_name="mrs",
|
sub_cmd_name="mrs",
|
||||||
description="Get merge requests from GitLab",
|
sub_cmd_description="Get merge requests from GitLab",
|
||||||
scopes=guild_ids,
|
|
||||||
)
|
)
|
||||||
@slash_option(
|
@slash_option(
|
||||||
name="state",
|
name="state",
|
||||||
|
@ -371,11 +378,9 @@ class GitlabCog(Scale):
|
||||||
|
|
||||||
await paginator.send(ctx)
|
await paginator.send(ctx)
|
||||||
|
|
||||||
@slash_command(
|
@gl.subcommand(
|
||||||
name="gl",
|
|
||||||
sub_cmd_name="milestones",
|
sub_cmd_name="milestones",
|
||||||
description="Get milestones from GitLab",
|
sub_cmd_description="Get milestones from GitLab",
|
||||||
scopes=guild_ids,
|
|
||||||
)
|
)
|
||||||
async def _milestones(self, ctx: InteractionContext) -> None:
|
async def _milestones(self, ctx: InteractionContext) -> None:
|
||||||
await ctx.defer()
|
await ctx.defer()
|
||||||
|
@ -410,8 +415,55 @@ class GitlabCog(Scale):
|
||||||
|
|
||||||
await paginator.send(ctx)
|
await paginator.send(ctx)
|
||||||
|
|
||||||
|
@slash_command(name="issue", description="Report an issue on GitLab", scopes=guild_ids)
|
||||||
|
@slash_option(
|
||||||
|
name="user",
|
||||||
|
description="Credit someone else for this issue",
|
||||||
|
opt_type=OptionTypes.USER,
|
||||||
|
required=False,
|
||||||
|
)
|
||||||
|
@cooldown(bucket=Buckets.USER, rate=1, interval=600)
|
||||||
|
async def _open_issue(self, ctx: InteractionContext, user: Member = None) -> None:
|
||||||
|
user = user or ctx.author
|
||||||
|
modal = Modal(
|
||||||
|
title="Open a new issue on GitLab",
|
||||||
|
components=[
|
||||||
|
InputText(
|
||||||
|
label="Issue Title",
|
||||||
|
placeholder="Descriptive Title",
|
||||||
|
style=TextStyles.SHORT,
|
||||||
|
custom_id="title",
|
||||||
|
max_length=200,
|
||||||
|
),
|
||||||
|
InputText(
|
||||||
|
label="Description (supports Markdown!)",
|
||||||
|
placeholder="Detailed Description",
|
||||||
|
style=TextStyles.PARAGRAPH,
|
||||||
|
custom_id="description",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
await ctx.send_modal(modal)
|
||||||
|
try:
|
||||||
|
resp = await self.bot.wait_for_modal(modal, author=ctx.author.id, timeout=60 * 5)
|
||||||
|
title = resp.responses.get("title")
|
||||||
|
desc = resp.responses.get("description")
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
return
|
||||||
|
if not title.startswith("[Discord]"):
|
||||||
|
title = "[Discord] " + title
|
||||||
|
desc = f"Opened by `@{user.username}` on Discord\n\n" + desc
|
||||||
|
issue = self.project.issues.create(data={"title": title, "description": desc})
|
||||||
|
embed = build_embed(
|
||||||
|
title=f"Issue #{issue.id} Created",
|
||||||
|
description=("Thank you for opening an issue!\n\n[View it online]({issue['web_url']})"),
|
||||||
|
fields=[],
|
||||||
|
color="#00FFEE",
|
||||||
|
)
|
||||||
|
await resp.send(embed=embed)
|
||||||
|
|
||||||
|
|
||||||
def setup(bot: Snake) -> None:
|
def setup(bot: Snake) -> None:
|
||||||
"""Add GitlabCog to J.A.R.V.I.S. if Gitlab token exists."""
|
"""Add GitlabCog to JARVIS if Gitlab token exists."""
|
||||||
if get_config().gitlab_token:
|
if JarvisConfig.from_yaml().gitlab_token:
|
||||||
GitlabCog(bot)
|
GitlabCog(bot)
|
|
@ -1,4 +1,5 @@
|
||||||
"""J.A.R.V.I.S. image processing cog."""
|
"""JARVIS image processing cog."""
|
||||||
|
import logging
|
||||||
import re
|
import re
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
|
|
||||||
|
@ -21,13 +22,14 @@ MIN_ACCURACY = 0.80
|
||||||
|
|
||||||
class ImageCog(Scale):
|
class ImageCog(Scale):
|
||||||
"""
|
"""
|
||||||
Image processing functions for J.A.R.V.I.S.
|
Image processing functions for JARVIS
|
||||||
|
|
||||||
May be categorized under util later
|
May be categorized under util later
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, bot: Snake):
|
def __init__(self, bot: Snake):
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
|
self.logger = logging.getLogger(__name__)
|
||||||
self._session = aiohttp.ClientSession()
|
self._session = aiohttp.ClientSession()
|
||||||
self.tgt_match = re.compile(r"([0-9]*\.?[0-9]*?) ?([KMGTP]?B?)", re.IGNORECASE)
|
self.tgt_match = re.compile(r"([0-9]*\.?[0-9]*?) ?([KMGTP]?B?)", re.IGNORECASE)
|
||||||
|
|
||||||
|
@ -56,6 +58,7 @@ class ImageCog(Scale):
|
||||||
async def _resize(
|
async def _resize(
|
||||||
self, ctx: InteractionContext, target: str, attachment: Attachment = None, url: str = None
|
self, ctx: InteractionContext, target: str, attachment: Attachment = None, url: str = None
|
||||||
) -> None:
|
) -> None:
|
||||||
|
await ctx.defer()
|
||||||
if not attachment and not url:
|
if not attachment and not url:
|
||||||
await ctx.send("A URL or attachment is required", ephemeral=True)
|
await ctx.send("A URL or attachment is required", ephemeral=True)
|
||||||
return
|
return
|
||||||
|
@ -71,10 +74,18 @@ class ImageCog(Scale):
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
tgt_size = unconvert_bytesize(float(tgt.groups()[0]), tgt.groups()[1])
|
try:
|
||||||
|
tgt_size = unconvert_bytesize(float(tgt.groups()[0]), tgt.groups()[1])
|
||||||
|
except ValueError:
|
||||||
|
await ctx.send("Failed to read your target size. Try a more sane one", ephemeral=True)
|
||||||
|
return
|
||||||
|
|
||||||
if tgt_size > unconvert_bytesize(8, "MB"):
|
if tgt_size > unconvert_bytesize(8, "MB"):
|
||||||
await ctx.send("Target too large to send. Please make target < 8MB", ephemeral=True)
|
await ctx.send("Target too large to send. Please make target < 8MB", ephemeral=True)
|
||||||
return
|
return
|
||||||
|
elif tgt_size < 1024:
|
||||||
|
await ctx.send("Sizes < 1KB are extremely unreliable and are disabled", ephemeral=True)
|
||||||
|
return
|
||||||
|
|
||||||
if attachment:
|
if attachment:
|
||||||
url = attachment.url
|
url = attachment.url
|
||||||
|
@ -83,13 +94,24 @@ class ImageCog(Scale):
|
||||||
filename = url.split("/")[-1]
|
filename = url.split("/")[-1]
|
||||||
|
|
||||||
data = None
|
data = None
|
||||||
async with self._session.get(url) as resp:
|
try:
|
||||||
if resp.status == 200:
|
async with self._session.get(url) as resp:
|
||||||
data = await resp.read()
|
resp.raise_for_status()
|
||||||
|
if resp.content_type in ["image/jpeg", "image/png"]:
|
||||||
|
data = await resp.read()
|
||||||
|
else:
|
||||||
|
await ctx.send(
|
||||||
|
"Unsupported content type. Please send a URL to a JPEG or PNG",
|
||||||
|
ephemeral=True,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
except Exception:
|
||||||
|
await ctx.send("Failed to retrieve image. Please verify url", ephemeral=True)
|
||||||
|
return
|
||||||
|
|
||||||
size = len(data)
|
size = len(data)
|
||||||
if size <= tgt_size:
|
if size <= tgt_size:
|
||||||
await ctx.send("Image already meets target.")
|
await ctx.send("Image already meets target.", ephemeral=True)
|
||||||
return
|
return
|
||||||
|
|
||||||
ratio = max(tgt_size / size - 0.02, 0.50)
|
ratio = max(tgt_size / size - 0.02, 0.50)
|
||||||
|
@ -130,5 +152,5 @@ class ImageCog(Scale):
|
||||||
|
|
||||||
|
|
||||||
def setup(bot: Snake) -> None:
|
def setup(bot: Snake) -> None:
|
||||||
"""Add ImageCog to J.A.R.V.I.S."""
|
"""Add ImageCog to JARVIS"""
|
||||||
ImageCog(bot)
|
ImageCog(bot)
|
||||||
|
|
|
@ -1,10 +1,13 @@
|
||||||
"""J.A.R.V.I.S. Remind Me Cog."""
|
"""JARVIS Remind Me Cog."""
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import logging
|
||||||
import re
|
import re
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timezone
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
from bson import ObjectId
|
from bson import ObjectId
|
||||||
|
from dateparser import parse
|
||||||
|
from dateparser_data.settings import default_parsers
|
||||||
from dis_snek import InteractionContext, Scale, Snake
|
from dis_snek import InteractionContext, Scale, Snake
|
||||||
from dis_snek.client.utils.misc_utils import get
|
from dis_snek.client.utils.misc_utils import get
|
||||||
from dis_snek.models.discord.channel import GuildChannel
|
from dis_snek.models.discord.channel import GuildChannel
|
||||||
|
@ -13,7 +16,7 @@ from dis_snek.models.discord.embed import Embed, EmbedField
|
||||||
from dis_snek.models.discord.modal import InputText, Modal, TextStyles
|
from dis_snek.models.discord.modal import InputText, Modal, TextStyles
|
||||||
from dis_snek.models.snek.application_commands import (
|
from dis_snek.models.snek.application_commands import (
|
||||||
OptionTypes,
|
OptionTypes,
|
||||||
SlashCommandChoice,
|
SlashCommand,
|
||||||
slash_command,
|
slash_command,
|
||||||
slash_option,
|
slash_option,
|
||||||
)
|
)
|
||||||
|
@ -31,25 +34,32 @@ invites = re.compile(
|
||||||
|
|
||||||
|
|
||||||
class RemindmeCog(Scale):
|
class RemindmeCog(Scale):
|
||||||
"""J.A.R.V.I.S. Remind Me Cog."""
|
"""JARVIS Remind Me Cog."""
|
||||||
|
|
||||||
|
def __init__(self, bot: Snake):
|
||||||
|
self.bot = bot
|
||||||
|
self.logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@slash_command(name="remindme", description="Set a reminder")
|
@slash_command(name="remindme", description="Set a reminder")
|
||||||
@slash_option(
|
@slash_option(
|
||||||
name="private",
|
name="private",
|
||||||
description="Send as DM?",
|
description="Send as DM?",
|
||||||
opt_type=OptionTypes.STRING,
|
opt_type=OptionTypes.BOOLEAN,
|
||||||
required=False,
|
required=False,
|
||||||
choices=[
|
|
||||||
SlashCommandChoice(name="Yes", value="y"),
|
|
||||||
SlashCommandChoice(name="No", value="n"),
|
|
||||||
],
|
|
||||||
)
|
)
|
||||||
async def _remindme(
|
async def _remindme(
|
||||||
self,
|
self,
|
||||||
ctx: InteractionContext,
|
ctx: InteractionContext,
|
||||||
private: str = "n",
|
private: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
private = private == "y"
|
reminders = len([x async for x in Reminder.find(q(user=ctx.author.id, active=True))])
|
||||||
|
if reminders >= 5:
|
||||||
|
await ctx.send(
|
||||||
|
"You already have 5 (or more) active reminders. "
|
||||||
|
"Please either remove an old one, or wait for one to pass",
|
||||||
|
ephemeral=True,
|
||||||
|
)
|
||||||
|
return
|
||||||
modal = Modal(
|
modal = Modal(
|
||||||
title="Set your reminder!",
|
title="Set your reminder!",
|
||||||
components=[
|
components=[
|
||||||
|
@ -62,12 +72,13 @@ class RemindmeCog(Scale):
|
||||||
),
|
),
|
||||||
InputText(
|
InputText(
|
||||||
label="When to remind you?",
|
label="When to remind you?",
|
||||||
placeholder="1h 30m",
|
placeholder="1h 30m | in 5 minutes | November 11, 4011",
|
||||||
style=TextStyles.SHORT,
|
style=TextStyles.SHORT,
|
||||||
custom_id="delay",
|
custom_id="delay",
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
await ctx.send_modal(modal)
|
await ctx.send_modal(modal)
|
||||||
try:
|
try:
|
||||||
response = await self.bot.wait_for_modal(modal, author=ctx.author.id, timeout=60 * 5)
|
response = await self.bot.wait_for_modal(modal, author=ctx.author.id, timeout=60 * 5)
|
||||||
|
@ -76,44 +87,53 @@ class RemindmeCog(Scale):
|
||||||
except asyncio.TimeoutError:
|
except asyncio.TimeoutError:
|
||||||
return
|
return
|
||||||
if len(message) > 500:
|
if len(message) > 500:
|
||||||
await ctx.send("Reminder cannot be > 500 characters.", ephemeral=True)
|
await response.send("Reminder cannot be > 500 characters.", ephemeral=True)
|
||||||
return
|
return
|
||||||
elif invites.search(message):
|
elif invites.search(message):
|
||||||
await ctx.send(
|
await response.send(
|
||||||
"Listen, don't use this to try and bypass the rules",
|
"Listen, don't use this to try and bypass the rules",
|
||||||
ephemeral=True,
|
ephemeral=True,
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
elif not valid.fullmatch(message):
|
elif not valid.fullmatch(message):
|
||||||
await ctx.send("Hey, you should probably make this readable", ephemeral=True)
|
await response.send("Hey, you should probably make this readable", ephemeral=True)
|
||||||
return
|
return
|
||||||
|
|
||||||
units = {"w": "weeks", "d": "days", "h": "hours", "m": "minutes", "s": "seconds"}
|
base_settings = {
|
||||||
delta = {"weeks": 0, "days": 0, "hours": 0, "minutes": 0, "seconds": 0}
|
"PREFER_DATES_FROM": "future",
|
||||||
|
"TIMEZONE": "UTC",
|
||||||
|
"RETURN_AS_TIMEZONE_AWARE": True,
|
||||||
|
}
|
||||||
|
rt_settings = base_settings.copy()
|
||||||
|
rt_settings["PARSERS"] = [
|
||||||
|
x for x in default_parsers if x not in ["absolute-time", "timestamp"]
|
||||||
|
]
|
||||||
|
|
||||||
if times := time_pattern.findall(delay):
|
rt_remind_at = parse(delay, settings=rt_settings)
|
||||||
for t in times:
|
|
||||||
delta[units[t[-1]]] += float(t[:-1])
|
at_settings = base_settings.copy()
|
||||||
|
at_settings["PARSERS"] = [x for x in default_parsers if x != "relative-time"]
|
||||||
|
at_remind_at = parse(delay, settings=at_settings)
|
||||||
|
|
||||||
|
if rt_remind_at:
|
||||||
|
remind_at = rt_remind_at
|
||||||
|
elif at_remind_at:
|
||||||
|
remind_at = at_remind_at
|
||||||
else:
|
else:
|
||||||
await ctx.send(
|
self.logger.debug(f"Failed to parse delay: {delay}")
|
||||||
"Invalid time string, please follow example: `1w 3d 7h 5m 20s`", ephemeral=True
|
await response.send(
|
||||||
|
f"`{delay}` is not a parsable date, please try again", ephemeral=True
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
if not any(value for value in delta.items()):
|
if remind_at < datetime.now(tz=timezone.utc):
|
||||||
await ctx.send("At least one time period is required", ephemeral=True)
|
await response.send(
|
||||||
return
|
f"`{delay}` is in the past. Past reminders aren't allowed", ephemeral=True
|
||||||
|
|
||||||
reminders = len([x async for x in Reminder.find(q(user=ctx.author.id, active=True))])
|
|
||||||
if reminders >= 5:
|
|
||||||
await ctx.send(
|
|
||||||
"You already have 5 (or more) active reminders. "
|
|
||||||
"Please either remove an old one, or wait for one to pass",
|
|
||||||
ephemeral=True,
|
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
remind_at = datetime.now() + timedelta(**delta)
|
elif remind_at < datetime.now(tz=timezone.utc):
|
||||||
|
pass
|
||||||
|
|
||||||
r = Reminder(
|
r = Reminder(
|
||||||
user=ctx.author.id,
|
user=ctx.author.id,
|
||||||
|
@ -134,7 +154,7 @@ class RemindmeCog(Scale):
|
||||||
EmbedField(name="Message", value=message),
|
EmbedField(name="Message", value=message),
|
||||||
EmbedField(
|
EmbedField(
|
||||||
name="When",
|
name="When",
|
||||||
value=remind_at.strftime("%Y-%m-%d %H:%M UTC"),
|
value=f"<t:{int(remind_at.timestamp())}:F>",
|
||||||
inline=False,
|
inline=False,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
@ -157,7 +177,7 @@ class RemindmeCog(Scale):
|
||||||
if reminder.private and isinstance(ctx.channel, GuildChannel):
|
if reminder.private and isinstance(ctx.channel, GuildChannel):
|
||||||
fields.embed(
|
fields.embed(
|
||||||
EmbedField(
|
EmbedField(
|
||||||
name=reminder.remind_at.strftime("%Y-%m-%d %H:%M UTC"),
|
name=f"<t:{int(reminder.remind_at.timestamp())}:F>",
|
||||||
value="Please DM me this command to view the content of this reminder",
|
value="Please DM me this command to view the content of this reminder",
|
||||||
inline=False,
|
inline=False,
|
||||||
)
|
)
|
||||||
|
@ -165,7 +185,7 @@ class RemindmeCog(Scale):
|
||||||
else:
|
else:
|
||||||
fields.append(
|
fields.append(
|
||||||
EmbedField(
|
EmbedField(
|
||||||
name=reminder.remind_at.strftime("%Y-%m-%d %H:%M UTC"),
|
name=f"<t:{int(reminder.remind_at.timestamp())}:F>",
|
||||||
value=f"{reminder.message}\n\u200b",
|
value=f"{reminder.message}\n\u200b",
|
||||||
inline=False,
|
inline=False,
|
||||||
)
|
)
|
||||||
|
@ -185,17 +205,11 @@ class RemindmeCog(Scale):
|
||||||
|
|
||||||
return embed
|
return embed
|
||||||
|
|
||||||
@slash_command(name="reminders", sub_cmd_name="list", sub_cmd_description="List reminders")
|
reminders = SlashCommand(name="reminders", description="Manage reminders")
|
||||||
|
|
||||||
|
@reminders.subcommand(sub_cmd_name="list", sub_cmd_description="List reminders")
|
||||||
async def _list(self, ctx: InteractionContext) -> None:
|
async def _list(self, ctx: InteractionContext) -> None:
|
||||||
exists = self.check_cache(ctx)
|
reminders = await Reminder.find(q(user=ctx.author.id, active=True)).to_list(None)
|
||||||
if exists:
|
|
||||||
await ctx.defer(ephemeral=True)
|
|
||||||
await ctx.send(
|
|
||||||
f"Please use existing interaction: {exists['paginator']._message.jump_url}",
|
|
||||||
ephemeral=True,
|
|
||||||
)
|
|
||||||
return
|
|
||||||
reminders = await Reminder.find(q(user=ctx.author.id, active=True))
|
|
||||||
if not reminders:
|
if not reminders:
|
||||||
await ctx.send("You have no reminders set.", ephemeral=True)
|
await ctx.send("You have no reminders set.", ephemeral=True)
|
||||||
return
|
return
|
||||||
|
@ -204,9 +218,9 @@ class RemindmeCog(Scale):
|
||||||
|
|
||||||
await ctx.send(embed=embed)
|
await ctx.send(embed=embed)
|
||||||
|
|
||||||
@slash_command(name="reminders", sub_cmd_name="delete", sub_cmd_description="Delete a reminder")
|
@reminders.subcommand(sub_cmd_name="delete", sub_cmd_description="Delete a reminder")
|
||||||
async def _delete(self, ctx: InteractionContext) -> None:
|
async def _delete(self, ctx: InteractionContext) -> None:
|
||||||
reminders = await Reminder.find(q(user=ctx.author.id, active=True))
|
reminders = await Reminder.find(q(user=ctx.author.id, active=True)).to_list(None)
|
||||||
if not reminders:
|
if not reminders:
|
||||||
await ctx.send("You have no reminders set", ephemeral=True)
|
await ctx.send("You have no reminders set", ephemeral=True)
|
||||||
return
|
return
|
||||||
|
@ -214,7 +228,7 @@ class RemindmeCog(Scale):
|
||||||
options = []
|
options = []
|
||||||
for reminder in reminders:
|
for reminder in reminders:
|
||||||
option = SelectOption(
|
option = SelectOption(
|
||||||
label=reminder.remind_at.strftime("%Y-%m-%d %H:%M UTC"),
|
label=f"{reminder.remind_at}",
|
||||||
value=str(reminder.id),
|
value=str(reminder.id),
|
||||||
emoji="⏰",
|
emoji="⏰",
|
||||||
)
|
)
|
||||||
|
@ -249,7 +263,7 @@ class RemindmeCog(Scale):
|
||||||
if reminder.private and isinstance(ctx.channel, GuildChannel):
|
if reminder.private and isinstance(ctx.channel, GuildChannel):
|
||||||
fields.append(
|
fields.append(
|
||||||
EmbedField(
|
EmbedField(
|
||||||
name=reminder.remind_at.strftime("%Y-%m-%d %H:%M UTC"),
|
name=f"<t:{int(reminder.remind_at.timestamp())}:F>",
|
||||||
value="Private reminder",
|
value="Private reminder",
|
||||||
inline=False,
|
inline=False,
|
||||||
)
|
)
|
||||||
|
@ -257,12 +271,15 @@ class RemindmeCog(Scale):
|
||||||
else:
|
else:
|
||||||
fields.append(
|
fields.append(
|
||||||
EmbedField(
|
EmbedField(
|
||||||
name=reminder.remind_at.strftime("%Y-%m-%d %H:%M UTC"),
|
name=f"<t:{int(reminder.remind_at.timestamp())}:F>",
|
||||||
value=reminder.message,
|
value=reminder.message,
|
||||||
inline=False,
|
inline=False,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
await reminder.delete()
|
try:
|
||||||
|
await reminder.delete()
|
||||||
|
except Exception:
|
||||||
|
self.logger.debug("Ignoring deletion error")
|
||||||
|
|
||||||
for row in components:
|
for row in components:
|
||||||
for component in row.components:
|
for component in row.components:
|
||||||
|
@ -291,7 +308,34 @@ class RemindmeCog(Scale):
|
||||||
component.disabled = True
|
component.disabled = True
|
||||||
await message.edit(components=components)
|
await message.edit(components=components)
|
||||||
|
|
||||||
|
@reminders.subcommand(
|
||||||
|
sub_cmd_name="fetch",
|
||||||
|
sub_cmd_description="Fetch a reminder that failed to send",
|
||||||
|
)
|
||||||
|
@slash_option(
|
||||||
|
name="id", description="ID of the reminder", opt_type=OptionTypes.STRING, required=True
|
||||||
|
)
|
||||||
|
async def _fetch(self, ctx: InteractionContext, id: str) -> None:
|
||||||
|
reminder = await Reminder.find_one(q(id=id))
|
||||||
|
if not reminder:
|
||||||
|
await ctx.send(f"Reminder `{id}` does not exist", ephemeral=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
embed = build_embed(title="You have a reminder!", description=reminder.message, fields=[])
|
||||||
|
embed.set_author(
|
||||||
|
name=ctx.author.display_name + "#" + ctx.author.discriminator,
|
||||||
|
icon_url=ctx.author.display_avatar,
|
||||||
|
)
|
||||||
|
|
||||||
|
embed.set_thumbnail(url=ctx.author.display_avatar)
|
||||||
|
await ctx.send(embed=embed, ephemeral=reminder.private)
|
||||||
|
if reminder.remind_at <= datetime.now(tz=timezone.utc):
|
||||||
|
try:
|
||||||
|
await reminder.delete()
|
||||||
|
except Exception:
|
||||||
|
self.logger.debug("Ignoring deletion error")
|
||||||
|
|
||||||
|
|
||||||
def setup(bot: Snake) -> None:
|
def setup(bot: Snake) -> None:
|
||||||
"""Add RemindmeCog to J.A.R.V.I.S."""
|
"""Add RemindmeCog to JARVIS"""
|
||||||
RemindmeCog(bot)
|
RemindmeCog(bot)
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
"""J.A.R.V.I.S. Role Giver Cog."""
|
"""JARVIS Role Giver Cog."""
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import logging
|
||||||
|
|
||||||
from dis_snek import InteractionContext, Permissions, Scale, Snake
|
from dis_snek import InteractionContext, Permissions, Scale, Snake
|
||||||
from dis_snek.client.utils.misc_utils import get
|
from dis_snek.client.utils.misc_utils import get
|
||||||
|
@ -8,7 +9,7 @@ from dis_snek.models.discord.embed import EmbedField
|
||||||
from dis_snek.models.discord.role import Role
|
from dis_snek.models.discord.role import Role
|
||||||
from dis_snek.models.snek.application_commands import (
|
from dis_snek.models.snek.application_commands import (
|
||||||
OptionTypes,
|
OptionTypes,
|
||||||
slash_command,
|
SlashCommand,
|
||||||
slash_option,
|
slash_option,
|
||||||
)
|
)
|
||||||
from dis_snek.models.snek.command import check, cooldown
|
from dis_snek.models.snek.command import check, cooldown
|
||||||
|
@ -21,17 +22,25 @@ from jarvis.utils.permissions import admin_or_permissions
|
||||||
|
|
||||||
|
|
||||||
class RolegiverCog(Scale):
|
class RolegiverCog(Scale):
|
||||||
"""J.A.R.V.I.S. Role Giver Cog."""
|
"""JARVIS Role Giver Cog."""
|
||||||
|
|
||||||
def __init__(self, bot: Snake):
|
def __init__(self, bot: Snake):
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
|
self.logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@slash_command(
|
rolegiver = SlashCommand(name="rolegiver", description="Allow users to choose their own roles")
|
||||||
name="rolegiver", sub_cmd_name="add", sub_cmd_description="Add a role to rolegiver"
|
|
||||||
|
@rolegiver.subcommand(
|
||||||
|
sub_cmd_name="add",
|
||||||
|
sub_cmd_description="Add a role to rolegiver",
|
||||||
)
|
)
|
||||||
@slash_option(name="role", description="Role to add", opt_type=OptionTypes.ROLE, required=True)
|
@slash_option(name="role", description="Role to add", opt_type=OptionTypes.ROLE, required=True)
|
||||||
@check(admin_or_permissions(Permissions.MANAGE_GUILD))
|
@check(admin_or_permissions(Permissions.MANAGE_GUILD))
|
||||||
async def _rolegiver_add(self, ctx: InteractionContext, role: Role) -> None:
|
async def _rolegiver_add(self, ctx: InteractionContext, role: Role) -> None:
|
||||||
|
if role.id == ctx.guild.id:
|
||||||
|
await ctx.send("Cannot add `@everyone` to rolegiver", ephemeral=True)
|
||||||
|
return
|
||||||
|
|
||||||
setting = await Rolegiver.find_one(q(guild=ctx.guild.id))
|
setting = await Rolegiver.find_one(q(guild=ctx.guild.id))
|
||||||
if setting and role.id in setting.roles:
|
if setting and role.id in setting.roles:
|
||||||
await ctx.send("Role already in rolegiver", ephemeral=True)
|
await ctx.send("Role already in rolegiver", ephemeral=True)
|
||||||
|
@ -45,13 +54,13 @@ class RolegiverCog(Scale):
|
||||||
return
|
return
|
||||||
|
|
||||||
setting.roles.append(role.id)
|
setting.roles.append(role.id)
|
||||||
setting.save()
|
await setting.commit()
|
||||||
|
|
||||||
roles = []
|
roles = []
|
||||||
for role_id in setting.roles:
|
for role_id in setting.roles:
|
||||||
if role_id == role.id:
|
if role_id == role.id:
|
||||||
continue
|
continue
|
||||||
e_role = await ctx.guild.get_role(role_id)
|
e_role = await ctx.guild.fetch_role(role_id)
|
||||||
if not e_role:
|
if not e_role:
|
||||||
continue
|
continue
|
||||||
roles.append(e_role)
|
roles.append(e_role)
|
||||||
|
@ -77,9 +86,7 @@ class RolegiverCog(Scale):
|
||||||
|
|
||||||
await ctx.send(embed=embed)
|
await ctx.send(embed=embed)
|
||||||
|
|
||||||
@slash_command(
|
@rolegiver.subcommand(sub_cmd_name="remove", sub_cmd_description="Remove a role from rolegiver")
|
||||||
name="rolegiver", sub_cmd_name="remove", sub_cmd_description="Remove a role from rolegiver"
|
|
||||||
)
|
|
||||||
@check(admin_or_permissions(Permissions.MANAGE_GUILD))
|
@check(admin_or_permissions(Permissions.MANAGE_GUILD))
|
||||||
async def _rolegiver_remove(self, ctx: InteractionContext) -> None:
|
async def _rolegiver_remove(self, ctx: InteractionContext) -> None:
|
||||||
setting = await Rolegiver.find_one(q(guild=ctx.guild.id))
|
setting = await Rolegiver.find_one(q(guild=ctx.guild.id))
|
||||||
|
@ -89,7 +96,7 @@ class RolegiverCog(Scale):
|
||||||
|
|
||||||
options = []
|
options = []
|
||||||
for role in setting.roles:
|
for role in setting.roles:
|
||||||
role: Role = await ctx.guild.get_role(role)
|
role: Role = await ctx.guild.fetch_role(role)
|
||||||
option = SelectOption(label=role.name, value=str(role.id))
|
option = SelectOption(label=role.name, value=str(role.id))
|
||||||
options.append(option)
|
options.append(option)
|
||||||
|
|
||||||
|
@ -111,11 +118,11 @@ class RolegiverCog(Scale):
|
||||||
)
|
)
|
||||||
removed_roles = []
|
removed_roles = []
|
||||||
for to_delete in context.context.values:
|
for to_delete in context.context.values:
|
||||||
role = await ctx.guild.get_role(to_delete)
|
role = await ctx.guild.fetch_role(to_delete)
|
||||||
if role:
|
if role:
|
||||||
removed_roles.append(role)
|
removed_roles.append(role)
|
||||||
setting.roles.remove(int(to_delete))
|
setting.roles.remove(int(to_delete))
|
||||||
setting.save()
|
await setting.commit()
|
||||||
|
|
||||||
for row in components:
|
for row in components:
|
||||||
for component in row.components:
|
for component in row.components:
|
||||||
|
@ -123,7 +130,7 @@ class RolegiverCog(Scale):
|
||||||
|
|
||||||
roles = []
|
roles = []
|
||||||
for role_id in setting.roles:
|
for role_id in setting.roles:
|
||||||
e_role = await ctx.guild.get_role(role_id)
|
e_role = await ctx.guild.fetch_role(role_id)
|
||||||
if not e_role:
|
if not e_role:
|
||||||
continue
|
continue
|
||||||
roles.append(e_role)
|
roles.append(e_role)
|
||||||
|
@ -162,7 +169,7 @@ class RolegiverCog(Scale):
|
||||||
component.disabled = True
|
component.disabled = True
|
||||||
await message.edit(components=components)
|
await message.edit(components=components)
|
||||||
|
|
||||||
@slash_command(name="rolegiver", sub_cmd_name="list", description="List rolegiver roles")
|
@rolegiver.subcommand(sub_cmd_name="list", sub_cmd_description="List rolegiver roles")
|
||||||
async def _rolegiver_list(self, ctx: InteractionContext) -> None:
|
async def _rolegiver_list(self, ctx: InteractionContext) -> None:
|
||||||
setting = await Rolegiver.find_one(q(guild=ctx.guild.id))
|
setting = await Rolegiver.find_one(q(guild=ctx.guild.id))
|
||||||
if not setting or (setting and not setting.roles):
|
if not setting or (setting and not setting.roles):
|
||||||
|
@ -171,7 +178,7 @@ class RolegiverCog(Scale):
|
||||||
|
|
||||||
roles = []
|
roles = []
|
||||||
for role_id in setting.roles:
|
for role_id in setting.roles:
|
||||||
e_role = await ctx.guild.get_role(role_id)
|
e_role = await ctx.guild.fetch_role(role_id)
|
||||||
if not e_role:
|
if not e_role:
|
||||||
continue
|
continue
|
||||||
roles.append(e_role)
|
roles.append(e_role)
|
||||||
|
@ -197,7 +204,9 @@ class RolegiverCog(Scale):
|
||||||
|
|
||||||
await ctx.send(embed=embed)
|
await ctx.send(embed=embed)
|
||||||
|
|
||||||
@slash_command(name="role", sub_cmd_name="get", sub_cmd_description="Get a role")
|
role = SlashCommand(name="role", description="Get/Remove Rolegiver roles")
|
||||||
|
|
||||||
|
@role.subcommand(sub_cmd_name="get", sub_cmd_description="Get a role")
|
||||||
@cooldown(bucket=Buckets.USER, rate=1, interval=10)
|
@cooldown(bucket=Buckets.USER, rate=1, interval=10)
|
||||||
async def _role_get(self, ctx: InteractionContext) -> None:
|
async def _role_get(self, ctx: InteractionContext) -> None:
|
||||||
setting = await Rolegiver.find_one(q(guild=ctx.guild.id))
|
setting = await Rolegiver.find_one(q(guild=ctx.guild.id))
|
||||||
|
@ -207,7 +216,7 @@ class RolegiverCog(Scale):
|
||||||
|
|
||||||
options = []
|
options = []
|
||||||
for role in setting.roles:
|
for role in setting.roles:
|
||||||
role: Role = await ctx.guild.get_role(role)
|
role: Role = await ctx.guild.fetch_role(role)
|
||||||
option = SelectOption(label=role.name, value=str(role.id))
|
option = SelectOption(label=role.name, value=str(role.id))
|
||||||
options.append(option)
|
options.append(option)
|
||||||
|
|
||||||
|
@ -230,7 +239,7 @@ class RolegiverCog(Scale):
|
||||||
|
|
||||||
added_roles = []
|
added_roles = []
|
||||||
for role in context.context.values:
|
for role in context.context.values:
|
||||||
role = await ctx.guild.get_role(int(role))
|
role = await ctx.guild.fetch_role(int(role))
|
||||||
added_roles.append(role)
|
added_roles.append(role)
|
||||||
await ctx.author.add_role(role, reason="Rolegiver")
|
await ctx.author.add_role(role, reason="Rolegiver")
|
||||||
|
|
||||||
|
@ -273,7 +282,7 @@ class RolegiverCog(Scale):
|
||||||
component.disabled = True
|
component.disabled = True
|
||||||
await message.edit(components=components)
|
await message.edit(components=components)
|
||||||
|
|
||||||
@slash_command(name="role", sub_cmd_name="remove", sub_cmd_description="Remove a role")
|
@role.subcommand(sub_cmd_name="remove", sub_cmd_description="Remove a role")
|
||||||
@cooldown(bucket=Buckets.USER, rate=1, interval=10)
|
@cooldown(bucket=Buckets.USER, rate=1, interval=10)
|
||||||
async def _role_remove(self, ctx: InteractionContext) -> None:
|
async def _role_remove(self, ctx: InteractionContext) -> None:
|
||||||
user_roles = ctx.author.roles
|
user_roles = ctx.author.roles
|
||||||
|
@ -352,8 +361,8 @@ class RolegiverCog(Scale):
|
||||||
component.disabled = True
|
component.disabled = True
|
||||||
await message.edit(components=components)
|
await message.edit(components=components)
|
||||||
|
|
||||||
@slash_command(
|
@rolegiver.subcommand(
|
||||||
name="rolegiver", sub_cmd_name="cleanup", description="Removed deleted roles from rolegiver"
|
sub_cmd_name="cleanup", sub_cmd_description="Removed deleted roles from rolegiver"
|
||||||
)
|
)
|
||||||
@check(admin_or_permissions(Permissions.MANAGE_GUILD))
|
@check(admin_or_permissions(Permissions.MANAGE_GUILD))
|
||||||
async def _rolegiver_cleanup(self, ctx: InteractionContext) -> None:
|
async def _rolegiver_cleanup(self, ctx: InteractionContext) -> None:
|
||||||
|
@ -364,11 +373,11 @@ class RolegiverCog(Scale):
|
||||||
for role_id in setting.roles:
|
for role_id in setting.roles:
|
||||||
if role_id not in guild_role_ids:
|
if role_id not in guild_role_ids:
|
||||||
setting.roles.remove(role_id)
|
setting.roles.remove(role_id)
|
||||||
setting.save()
|
await setting.commit()
|
||||||
|
|
||||||
await ctx.send("Rolegiver cleanup finished")
|
await ctx.send("Rolegiver cleanup finished")
|
||||||
|
|
||||||
|
|
||||||
def setup(bot: Snake) -> None:
|
def setup(bot: Snake) -> None:
|
||||||
"""Add RolegiverCog to J.A.R.V.I.S."""
|
"""Add RolegiverCog to JARVIS"""
|
||||||
RolegiverCog(bot)
|
RolegiverCog(bot)
|
||||||
|
|
|
@ -1,25 +1,33 @@
|
||||||
"""J.A.R.V.I.S. Settings Management Cog."""
|
"""JARVIS Settings Management Cog."""
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
from dis_snek import InteractionContext, Scale, Snake
|
||||||
|
from dis_snek.models.discord.channel import GuildText
|
||||||
|
from dis_snek.models.discord.components import ActionRow, Button, ButtonStyles
|
||||||
|
from dis_snek.models.discord.embed import EmbedField
|
||||||
|
from dis_snek.models.discord.enums import Permissions
|
||||||
|
from dis_snek.models.discord.role import Role
|
||||||
|
from dis_snek.models.snek.application_commands import (
|
||||||
|
OptionTypes,
|
||||||
|
SlashCommand,
|
||||||
|
slash_option,
|
||||||
|
)
|
||||||
from dis_snek.models.snek.command import check
|
from dis_snek.models.snek.command import check
|
||||||
from discord import Role, TextChannel
|
|
||||||
from discord.ext import commands
|
|
||||||
from discord.utils import find
|
|
||||||
from discord_slash import SlashContext, cog_ext
|
|
||||||
from discord_slash.utils.manage_commands import create_option
|
|
||||||
from jarvis_core.db import q
|
from jarvis_core.db import q
|
||||||
|
from jarvis_core.db.models import Setting
|
||||||
|
|
||||||
from jarvis.db.models import Setting
|
|
||||||
from jarvis.utils import build_embed
|
from jarvis.utils import build_embed
|
||||||
from jarvis.utils.field import Field
|
|
||||||
from jarvis.utils.permissions import admin_or_permissions
|
from jarvis.utils.permissions import admin_or_permissions
|
||||||
|
|
||||||
|
|
||||||
class SettingsCog(commands.Cog):
|
class SettingsCog(Scale):
|
||||||
"""J.A.R.V.I.S. Settings Management Cog."""
|
"""JARVIS Settings Management Cog."""
|
||||||
|
|
||||||
def __init__(self, bot: commands.Bot):
|
def __init__(self, bot: Snake):
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
|
self.logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
async def update_settings(self, setting: str, value: Any, guild: int) -> bool:
|
async def update_settings(self, setting: str, value: Any, guild: int) -> bool:
|
||||||
"""Update a guild setting."""
|
"""Update a guild setting."""
|
||||||
|
@ -27,7 +35,7 @@ class SettingsCog(commands.Cog):
|
||||||
if not existing:
|
if not existing:
|
||||||
existing = Setting(setting=setting, guild=guild, value=value)
|
existing = Setting(setting=setting, guild=guild, value=value)
|
||||||
existing.value = value
|
existing.value = value
|
||||||
updated = existing.save()
|
updated = await existing.commit()
|
||||||
|
|
||||||
return updated is not None
|
return updated is not None
|
||||||
|
|
||||||
|
@ -38,204 +46,177 @@ class SettingsCog(commands.Cog):
|
||||||
return await existing.delete()
|
return await existing.delete()
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@cog_ext.cog_subcommand(
|
settings = SlashCommand(name="settings", description="Control guild settings")
|
||||||
base="settings",
|
set_ = settings.group(name="set", description="Set a setting")
|
||||||
subcommand_group="set",
|
unset = settings.group(name="unset", description="Unset a setting")
|
||||||
name="modlog",
|
|
||||||
description="Set modlog channel",
|
@set_.subcommand(
|
||||||
choices=[
|
sub_cmd_name="modlog",
|
||||||
create_option(
|
sub_cmd_description="Set Moglod channel",
|
||||||
name="channel",
|
|
||||||
description="Modlog channel",
|
|
||||||
opt_type=7,
|
|
||||||
required=True,
|
|
||||||
)
|
|
||||||
],
|
|
||||||
)
|
)
|
||||||
@check(admin_or_permissions(manage_guild=True))
|
@slash_option(
|
||||||
async def _set_modlog(self, ctx: SlashContext, channel: TextChannel) -> None:
|
name="channel", description="ModLog Channel", opt_type=OptionTypes.CHANNEL, required=True
|
||||||
if not isinstance(channel, TextChannel):
|
)
|
||||||
await ctx.send("Channel must be a TextChannel", ephemeral=True)
|
@check(admin_or_permissions(Permissions.MANAGE_GUILD))
|
||||||
|
async def _set_modlog(self, ctx: InteractionContext, channel: GuildText) -> None:
|
||||||
|
if not isinstance(channel, GuildText):
|
||||||
|
await ctx.send("Channel must be a GuildText", ephemeral=True)
|
||||||
return
|
return
|
||||||
self.update_settings("modlog", channel.id, ctx.guild.id)
|
await self.update_settings("modlog", channel.id, ctx.guild.id)
|
||||||
await ctx.send(f"Settings applied. New modlog channel is {channel.mention}")
|
await ctx.send(f"Settings applied. New modlog channel is {channel.mention}")
|
||||||
|
|
||||||
@cog_ext.cog_subcommand(
|
@set_.subcommand(
|
||||||
base="settings",
|
sub_cmd_name="activitylog",
|
||||||
subcommand_group="set",
|
sub_cmd_description="Set Activitylog channel",
|
||||||
name="activitylog",
|
|
||||||
description="Set activitylog channel",
|
|
||||||
choices=[
|
|
||||||
create_option(
|
|
||||||
name="channel",
|
|
||||||
description="Activitylog channel",
|
|
||||||
opt_type=7,
|
|
||||||
required=True,
|
|
||||||
)
|
|
||||||
],
|
|
||||||
)
|
)
|
||||||
@check(admin_or_permissions(manage_guild=True))
|
@slash_option(
|
||||||
async def _set_activitylog(self, ctx: SlashContext, channel: TextChannel) -> None:
|
name="channel",
|
||||||
if not isinstance(channel, TextChannel):
|
description="Activitylog Channel",
|
||||||
await ctx.send("Channel must be a TextChannel", ephemeral=True)
|
opt_type=OptionTypes.CHANNEL,
|
||||||
|
required=True,
|
||||||
|
)
|
||||||
|
@check(admin_or_permissions(Permissions.MANAGE_GUILD))
|
||||||
|
async def _set_activitylog(self, ctx: InteractionContext, channel: GuildText) -> None:
|
||||||
|
if not isinstance(channel, GuildText):
|
||||||
|
await ctx.send("Channel must be a GuildText", ephemeral=True)
|
||||||
return
|
return
|
||||||
self.update_settings("activitylog", channel.id, ctx.guild.id)
|
await self.update_settings("activitylog", channel.id, ctx.guild.id)
|
||||||
await ctx.send(f"Settings applied. New activitylog channel is {channel.mention}")
|
await ctx.send(f"Settings applied. New activitylog channel is {channel.mention}")
|
||||||
|
|
||||||
@cog_ext.cog_subcommand(
|
@set_.subcommand(sub_cmd_name="massmention", sub_cmd_description="Set massmention output")
|
||||||
base="settings",
|
@slash_option(
|
||||||
subcommand_group="set",
|
name="amount",
|
||||||
name="massmention",
|
description="Amount of mentions (0 to disable)",
|
||||||
description="Set massmention amount",
|
opt_type=OptionTypes.INTEGER,
|
||||||
choices=[
|
required=True,
|
||||||
create_option(
|
|
||||||
name="amount",
|
|
||||||
description="Amount of mentions (0 to disable)",
|
|
||||||
opt_type=4,
|
|
||||||
required=True,
|
|
||||||
)
|
|
||||||
],
|
|
||||||
)
|
)
|
||||||
@check(admin_or_permissions(manage_guild=True))
|
@check(admin_or_permissions(Permissions.MANAGE_GUILD))
|
||||||
async def _set_massmention(self, ctx: SlashContext, amount: int) -> None:
|
async def _set_massmention(self, ctx: InteractionContext, amount: int) -> None:
|
||||||
await ctx.defer()
|
await ctx.defer()
|
||||||
self.update_settings("massmention", amount, ctx.guild.id)
|
await self.update_settings("massmention", amount, ctx.guild.id)
|
||||||
await ctx.send(f"Settings applied. New massmention limit is {amount}")
|
await ctx.send(f"Settings applied. New massmention limit is {amount}")
|
||||||
|
|
||||||
@cog_ext.cog_subcommand(
|
@set_.subcommand(sub_cmd_name="verified", sub_cmd_description="Set verified role")
|
||||||
base="settings",
|
@slash_option(
|
||||||
subcommand_group="set",
|
name="role", description="Verified role", opt_type=OptionTypes.ROLE, required=True
|
||||||
name="verified",
|
|
||||||
description="Set verified role",
|
|
||||||
choices=[
|
|
||||||
create_option(
|
|
||||||
name="role",
|
|
||||||
description="verified role",
|
|
||||||
opt_type=8,
|
|
||||||
required=True,
|
|
||||||
)
|
|
||||||
],
|
|
||||||
)
|
)
|
||||||
@check(admin_or_permissions(manage_guild=True))
|
@check(admin_or_permissions(Permissions.MANAGE_GUILD))
|
||||||
async def _set_verified(self, ctx: SlashContext, role: Role) -> None:
|
async def _set_verified(self, ctx: InteractionContext, role: Role) -> None:
|
||||||
|
if role.id == ctx.guild.id:
|
||||||
|
await ctx.send("Cannot set verified to `@everyone`", ephemeral=True)
|
||||||
|
return
|
||||||
await ctx.defer()
|
await ctx.defer()
|
||||||
self.update_settings("verified", role.id, ctx.guild.id)
|
await self.update_settings("verified", role.id, ctx.guild.id)
|
||||||
await ctx.send(f"Settings applied. New verified role is `{role.name}`")
|
await ctx.send(f"Settings applied. New verified role is `{role.name}`")
|
||||||
|
|
||||||
@cog_ext.cog_subcommand(
|
@set_.subcommand(sub_cmd_name="unverified", sub_cmd_description="Set unverified role")
|
||||||
base="settings",
|
@slash_option(
|
||||||
subcommand_group="set",
|
name="role", description="Unverified role", opt_type=OptionTypes.ROLE, required=True
|
||||||
name="unverified",
|
|
||||||
description="Set unverified role",
|
|
||||||
choices=[
|
|
||||||
create_option(
|
|
||||||
name="role",
|
|
||||||
description="Unverified role",
|
|
||||||
opt_type=8,
|
|
||||||
required=True,
|
|
||||||
)
|
|
||||||
],
|
|
||||||
)
|
)
|
||||||
@check(admin_or_permissions(manage_guild=True))
|
@check(admin_or_permissions(Permissions.MANAGE_GUILD))
|
||||||
async def _set_unverified(self, ctx: SlashContext, role: Role) -> None:
|
async def _set_unverified(self, ctx: InteractionContext, role: Role) -> None:
|
||||||
|
if role.id == ctx.guild.id:
|
||||||
|
await ctx.send("Cannot set unverified to `@everyone`", ephemeral=True)
|
||||||
|
return
|
||||||
await ctx.defer()
|
await ctx.defer()
|
||||||
self.update_settings("unverified", role.id, ctx.guild.id)
|
await self.update_settings("unverified", role.id, ctx.guild.id)
|
||||||
await ctx.send(f"Settings applied. New unverified role is `{role.name}`")
|
await ctx.send(f"Settings applied. New unverified role is `{role.name}`")
|
||||||
|
|
||||||
@cog_ext.cog_subcommand(
|
@set_.subcommand(
|
||||||
base="settings",
|
sub_cmd_name="noinvite", sub_cmd_description="Set if invite deletion should happen"
|
||||||
subcommand_group="set",
|
|
||||||
name="noinvite",
|
|
||||||
description="Set if invite deletion should happen",
|
|
||||||
choices=[
|
|
||||||
create_option(
|
|
||||||
name="active",
|
|
||||||
description="Active?",
|
|
||||||
opt_type=4,
|
|
||||||
required=True,
|
|
||||||
)
|
|
||||||
],
|
|
||||||
)
|
)
|
||||||
@check(admin_or_permissions(manage_guild=True))
|
@slash_option(name="active", description="Active?", opt_type=OptionTypes.BOOLEAN, required=True)
|
||||||
async def _set_invitedel(self, ctx: SlashContext, active: int) -> None:
|
@check(admin_or_permissions(Permissions.MANAGE_GUILD))
|
||||||
|
async def _set_invitedel(self, ctx: InteractionContext, active: bool) -> None:
|
||||||
await ctx.defer()
|
await ctx.defer()
|
||||||
self.update_settings("noinvite", bool(active), ctx.guild.id)
|
await self.update_settings("noinvite", active, ctx.guild.id)
|
||||||
await ctx.send(f"Settings applied. Automatic invite active: {bool(active)}")
|
await ctx.send(f"Settings applied. Automatic invite active: {active}")
|
||||||
|
|
||||||
@cog_ext.cog_subcommand(
|
@set_.subcommand(sub_cmd_name="notify", sub_cmd_description="Notify users of admin action?")
|
||||||
base="settings",
|
@slash_option(name="active", description="Notify?", opt_type=OptionTypes.BOOLEAN, required=True)
|
||||||
subcommand_group="unset",
|
@check(admin_or_permissions(Permissions.MANAGE_GUILD))
|
||||||
name="modlog",
|
async def _set_notify(self, ctx: InteractionContext, active: bool) -> None:
|
||||||
description="Unset modlog channel",
|
|
||||||
)
|
|
||||||
@check(admin_or_permissions(manage_guild=True))
|
|
||||||
async def _unset_modlog(self, ctx: SlashContext) -> None:
|
|
||||||
self.delete_settings("modlog", ctx.guild.id)
|
|
||||||
await ctx.send("Setting removed.")
|
|
||||||
|
|
||||||
@cog_ext.cog_subcommand(
|
|
||||||
base="settings",
|
|
||||||
subcommand_group="unset",
|
|
||||||
name="activitylog",
|
|
||||||
description="Unset activitylog channel",
|
|
||||||
)
|
|
||||||
@check(admin_or_permissions(manage_guild=True))
|
|
||||||
async def _unset_activitylog(self, ctx: SlashContext) -> None:
|
|
||||||
self.delete_settings("activitylog", ctx.guild.id)
|
|
||||||
await ctx.send("Setting removed.")
|
|
||||||
|
|
||||||
@cog_ext.cog_subcommand(
|
|
||||||
base="settings",
|
|
||||||
subcommand_group="unset",
|
|
||||||
name="massmention",
|
|
||||||
description="Unet massmention amount",
|
|
||||||
)
|
|
||||||
@check(admin_or_permissions(manage_guild=True))
|
|
||||||
async def _massmention(self, ctx: SlashContext) -> None:
|
|
||||||
await ctx.defer()
|
await ctx.defer()
|
||||||
self.delete_settings("massmention", ctx.guild.id)
|
await self.update_settings("notify", active, ctx.guild.id)
|
||||||
await ctx.send("Setting removed.")
|
await ctx.send(f"Settings applied. Notifications active: {active}")
|
||||||
|
|
||||||
@cog_ext.cog_subcommand(
|
# Unset
|
||||||
base="settings",
|
@unset.subcommand(
|
||||||
subcommand_group="unset",
|
sub_cmd_name="modlog",
|
||||||
name="verified",
|
sub_cmd_description="Unset Modlog channel",
|
||||||
description="Unset verified role",
|
|
||||||
)
|
)
|
||||||
@check(admin_or_permissions(manage_guild=True))
|
@check(admin_or_permissions(Permissions.MANAGE_GUILD))
|
||||||
async def _verified(self, ctx: SlashContext) -> None:
|
async def _unset_modlog(self, ctx: InteractionContext) -> None:
|
||||||
await ctx.defer()
|
await ctx.defer()
|
||||||
self.delete_settings("verified", ctx.guild.id)
|
await self.delete_settings("modlog", ctx.guild.id)
|
||||||
await ctx.send("Setting removed.")
|
await ctx.send("Setting `modlog` unset")
|
||||||
|
|
||||||
@cog_ext.cog_subcommand(
|
@unset.subcommand(
|
||||||
base="settings",
|
sub_cmd_name="activitylog",
|
||||||
subcommand_group="unset",
|
sub_cmd_description="Unset Activitylog channel",
|
||||||
name="unverified",
|
|
||||||
description="Unset unverified role",
|
|
||||||
)
|
)
|
||||||
@check(admin_or_permissions(manage_guild=True))
|
@check(admin_or_permissions(Permissions.MANAGE_GUILD))
|
||||||
async def _unverified(self, ctx: SlashContext) -> None:
|
async def _unset_activitylog(self, ctx: InteractionContext) -> None:
|
||||||
await ctx.defer()
|
await ctx.defer()
|
||||||
self.delete_settings("unverified", ctx.guild.id)
|
await self.delete_settings("activitylog", ctx.guild.id)
|
||||||
await ctx.send("Setting removed.")
|
await ctx.send("Setting `activitylog` unset")
|
||||||
|
|
||||||
@cog_ext.cog_subcommand(base="settings", name="view", description="View settings")
|
@unset.subcommand(sub_cmd_name="massmention", sub_cmd_description="Unset massmention output")
|
||||||
@check(admin_or_permissions(manage_guild=True))
|
@check(admin_or_permissions(Permissions.MANAGE_GUILD))
|
||||||
async def _view(self, ctx: SlashContext) -> None:
|
async def _unset_massmention(self, ctx: InteractionContext) -> None:
|
||||||
settings = Setting.objects(guild=ctx.guild.id)
|
await ctx.defer()
|
||||||
|
await self.delete_settings("massmention", ctx.guild.id)
|
||||||
|
await ctx.send("Setting `massmention` unset")
|
||||||
|
|
||||||
|
@unset.subcommand(sub_cmd_name="verified", sub_cmd_description="Unset verified role")
|
||||||
|
@slash_option(
|
||||||
|
name="role", description="Verified role", opt_type=OptionTypes.ROLE, required=True
|
||||||
|
)
|
||||||
|
@check(admin_or_permissions(Permissions.MANAGE_GUILD))
|
||||||
|
async def _unset_verified(self, ctx: InteractionContext) -> None:
|
||||||
|
await ctx.defer()
|
||||||
|
await self.delete_settings("verified", ctx.guild.id)
|
||||||
|
await ctx.send("Setting `massmention` unset")
|
||||||
|
|
||||||
|
@unset.subcommand(sub_cmd_name="unverified", sub_cmd_description="Unset unverified role")
|
||||||
|
@check(admin_or_permissions(Permissions.MANAGE_GUILD))
|
||||||
|
async def _unset_unverified(self, ctx: InteractionContext) -> None:
|
||||||
|
await ctx.defer()
|
||||||
|
await self.delete_settings("unverified", ctx.guild.id)
|
||||||
|
await ctx.send("Setting `unverified` unset")
|
||||||
|
|
||||||
|
@unset.subcommand(
|
||||||
|
sub_cmd_name="noinvite", sub_cmd_description="Unset if invite deletion should happen"
|
||||||
|
)
|
||||||
|
@check(admin_or_permissions(Permissions.MANAGE_GUILD))
|
||||||
|
async def _unset_invitedel(self, ctx: InteractionContext, active: bool) -> None:
|
||||||
|
await ctx.defer()
|
||||||
|
await self.delete_settings("noinvite", ctx.guild.id)
|
||||||
|
await ctx.send(f"Setting `{active}` unset")
|
||||||
|
|
||||||
|
@unset.subcommand(sub_cmd_name="notify", sub_cmd_description="Unset admin action notifications")
|
||||||
|
@check(admin_or_permissions(Permissions.MANAGE_GUILD))
|
||||||
|
async def _unset_notify(self, ctx: InteractionContext) -> None:
|
||||||
|
await ctx.defer()
|
||||||
|
await self.delete_settings("notify", ctx.guild.id)
|
||||||
|
await ctx.send("Setting `notify` unset")
|
||||||
|
|
||||||
|
@settings.subcommand(sub_cmd_name="view", sub_cmd_description="View settings")
|
||||||
|
@check(admin_or_permissions(Permissions.MANAGE_GUILD))
|
||||||
|
async def _view(self, ctx: InteractionContext) -> None:
|
||||||
|
settings = Setting.find(q(guild=ctx.guild.id))
|
||||||
|
|
||||||
fields = []
|
fields = []
|
||||||
for setting in settings:
|
async for setting in settings:
|
||||||
value = setting.value
|
value = setting.value
|
||||||
if setting.setting in ["unverified", "verified", "mute"]:
|
if setting.setting in ["unverified", "verified", "mute"]:
|
||||||
value = find(lambda x: x.id == value, ctx.guild.roles)
|
value = await ctx.guild.fetch_role(value)
|
||||||
if value:
|
if value:
|
||||||
value = value.mention
|
value = value.mention
|
||||||
else:
|
else:
|
||||||
value = "||`[redacted]`||"
|
value = "||`[redacted]`||"
|
||||||
elif setting.setting in ["activitylog", "modlog"]:
|
elif setting.setting in ["activitylog", "modlog"]:
|
||||||
value = find(lambda x: x.id == value, ctx.guild.text_channels)
|
value = await ctx.guild.fetch_channel(value)
|
||||||
if value:
|
if value:
|
||||||
value = value.mention
|
value = value.mention
|
||||||
else:
|
else:
|
||||||
|
@ -243,24 +224,51 @@ class SettingsCog(commands.Cog):
|
||||||
elif setting.setting == "rolegiver":
|
elif setting.setting == "rolegiver":
|
||||||
value = ""
|
value = ""
|
||||||
for _role in setting.value:
|
for _role in setting.value:
|
||||||
nvalue = find(lambda x: x.id == value, ctx.guild.roles)
|
nvalue = await ctx.guild.fetch_role(_role)
|
||||||
if value:
|
if nvalue:
|
||||||
value += "\n" + nvalue.mention
|
value += "\n" + nvalue.mention
|
||||||
else:
|
else:
|
||||||
value += "\n||`[redacted]`||"
|
value += "\n||`[redacted]`||"
|
||||||
fields.append(Field(name=setting.setting, value=value or "N/A"))
|
fields.append(EmbedField(name=setting.setting, value=str(value) or "N/A", inline=False))
|
||||||
|
|
||||||
embed = build_embed(title="Current Settings", description="", fields=fields)
|
embed = build_embed(title="Current Settings", description="", fields=fields)
|
||||||
|
|
||||||
await ctx.send(embed=embed)
|
await ctx.send(embed=embed)
|
||||||
|
|
||||||
@cog_ext.cog_subcommand(base="settings", name="clear", description="Clear all settings")
|
@settings.subcommand(sub_cmd_name="clear", sub_cmd_description="Clear all settings")
|
||||||
@check(admin_or_permissions(manage_guild=True))
|
@check(admin_or_permissions(Permissions.MANAGE_GUILD))
|
||||||
async def _clear(self, ctx: SlashContext) -> None:
|
async def _clear(self, ctx: InteractionContext) -> None:
|
||||||
deleted = Setting.objects(guild=ctx.guild.id).delete()
|
components = [
|
||||||
await ctx.send(f"Guild settings cleared: `{deleted is not None}`")
|
ActionRow(
|
||||||
|
Button(style=ButtonStyles.RED, emoji="✖️", custom_id="no"),
|
||||||
|
Button(style=ButtonStyles.GREEN, emoji="✔️", custom_id="yes"),
|
||||||
|
)
|
||||||
|
]
|
||||||
|
message = await ctx.send("***Are you sure?***", components=components)
|
||||||
|
try:
|
||||||
|
context = await self.bot.wait_for_component(
|
||||||
|
check=lambda x: ctx.author.id == x.context.author.id,
|
||||||
|
messages=message,
|
||||||
|
timeout=60 * 5,
|
||||||
|
)
|
||||||
|
content = "***Are you sure?***"
|
||||||
|
if context.context.custom_id == "yes":
|
||||||
|
async for setting in Setting.find(q(guild=ctx.guild.id)):
|
||||||
|
await setting.delete()
|
||||||
|
content = "Guild settings cleared"
|
||||||
|
else:
|
||||||
|
content = "Guild settings not cleared"
|
||||||
|
for row in components:
|
||||||
|
for component in row.components:
|
||||||
|
component.disabled = True
|
||||||
|
await context.context.edit_origin(content=content, components=components)
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
for row in components:
|
||||||
|
for component in row.components:
|
||||||
|
component.disabled = True
|
||||||
|
await message.edit(content="Guild settings not cleared", components=components)
|
||||||
|
|
||||||
|
|
||||||
def setup(bot: commands.Bot) -> None:
|
def setup(bot: Snake) -> None:
|
||||||
"""Add SettingsCog to J.A.R.V.I.S."""
|
"""Add SettingsCog to JARVIS"""
|
||||||
SettingsCog(bot)
|
SettingsCog(bot)
|
||||||
|
|
|
@ -1,14 +1,15 @@
|
||||||
"""J.A.R.V.I.S. Starboard Cog."""
|
"""JARVIS Starboard Cog."""
|
||||||
|
import logging
|
||||||
|
|
||||||
from dis_snek import InteractionContext, Permissions, Scale, Snake
|
from dis_snek import InteractionContext, Permissions, Scale, Snake
|
||||||
from dis_snek.client.utils.misc_utils import find
|
|
||||||
from dis_snek.models.discord.channel import GuildText
|
from dis_snek.models.discord.channel import GuildText
|
||||||
from dis_snek.models.discord.components import ActionRow, Select, SelectOption
|
from dis_snek.models.discord.components import ActionRow, Select, SelectOption
|
||||||
from dis_snek.models.discord.message import Message
|
from dis_snek.models.discord.message import Message
|
||||||
from dis_snek.models.snek.application_commands import (
|
from dis_snek.models.snek.application_commands import (
|
||||||
CommandTypes,
|
CommandTypes,
|
||||||
OptionTypes,
|
OptionTypes,
|
||||||
|
SlashCommand,
|
||||||
context_menu,
|
context_menu,
|
||||||
slash_command,
|
|
||||||
slash_option,
|
slash_option,
|
||||||
)
|
)
|
||||||
from dis_snek.models.snek.command import check
|
from dis_snek.models.snek.command import check
|
||||||
|
@ -28,12 +29,18 @@ supported_images = [
|
||||||
|
|
||||||
|
|
||||||
class StarboardCog(Scale):
|
class StarboardCog(Scale):
|
||||||
"""J.A.R.V.I.S. Starboard Cog."""
|
"""JARVIS Starboard Cog."""
|
||||||
|
|
||||||
def __init__(self, bot: Snake):
|
def __init__(self, bot: Snake):
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
|
self.logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@slash_command(name="starboard", sub_cmd_name="list", sub_cmd_description="List all starboards")
|
starboard = SlashCommand(name="starboard", description="Extra pins! Manage starboards")
|
||||||
|
|
||||||
|
@starboard.subcommand(
|
||||||
|
sub_cmd_name="list",
|
||||||
|
sub_cmd_description="List all starboards",
|
||||||
|
)
|
||||||
@check(admin_or_permissions(Permissions.MANAGE_GUILD))
|
@check(admin_or_permissions(Permissions.MANAGE_GUILD))
|
||||||
async def _list(self, ctx: InteractionContext) -> None:
|
async def _list(self, ctx: InteractionContext) -> None:
|
||||||
starboards = await Starboard.find(q(guild=ctx.guild.id)).to_list(None)
|
starboards = await Starboard.find(q(guild=ctx.guild.id)).to_list(None)
|
||||||
|
@ -45,9 +52,7 @@ class StarboardCog(Scale):
|
||||||
else:
|
else:
|
||||||
await ctx.send("No Starboards available.")
|
await ctx.send("No Starboards available.")
|
||||||
|
|
||||||
@slash_command(
|
@starboard.subcommand(sub_cmd_name="create", sub_cmd_description="Create a starboard")
|
||||||
name="starboard", sub_cmd_name="create", sub_cmd_description="Create a starboard"
|
|
||||||
)
|
|
||||||
@slash_option(
|
@slash_option(
|
||||||
name="channel",
|
name="channel",
|
||||||
description="Starboard channel",
|
description="Starboard channel",
|
||||||
|
@ -83,9 +88,7 @@ class StarboardCog(Scale):
|
||||||
).commit()
|
).commit()
|
||||||
await ctx.send(f"Starboard created. Check it out at {channel.mention}.")
|
await ctx.send(f"Starboard created. Check it out at {channel.mention}.")
|
||||||
|
|
||||||
@slash_command(
|
@starboard.subcommand(sub_cmd_name="delete", sub_cmd_description="Delete a starboard")
|
||||||
name="starboard", sub_cmd_name="delete", sub_cmd_description="Delete a starboard"
|
|
||||||
)
|
|
||||||
@slash_option(
|
@slash_option(
|
||||||
name="channel",
|
name="channel",
|
||||||
description="Starboard channel",
|
description="Starboard channel",
|
||||||
|
@ -126,8 +129,27 @@ class StarboardCog(Scale):
|
||||||
return
|
return
|
||||||
|
|
||||||
channel_list = []
|
channel_list = []
|
||||||
|
to_delete = []
|
||||||
for starboard in starboards:
|
for starboard in starboards:
|
||||||
channel_list.append(find(lambda x: x.id == starboard.channel, ctx.guild.channels))
|
c = await ctx.guild.fetch_channel(starboard.channel)
|
||||||
|
if c and isinstance(c, GuildText):
|
||||||
|
channel_list.append(c)
|
||||||
|
else:
|
||||||
|
self.logger.warn(
|
||||||
|
f"Starboard {starboard.channel} no longer valid in {ctx.guild.name}"
|
||||||
|
)
|
||||||
|
to_delete.append(starboard)
|
||||||
|
|
||||||
|
for starboard in to_delete:
|
||||||
|
try:
|
||||||
|
await starboard.delete()
|
||||||
|
except Exception:
|
||||||
|
self.logger.debug("Ignoring deletion error")
|
||||||
|
|
||||||
|
select_channels = []
|
||||||
|
for idx, x in enumerate(channel_list):
|
||||||
|
if x:
|
||||||
|
select_channels.append(SelectOption(label=x.name, value=str(idx)))
|
||||||
|
|
||||||
select_channels = [
|
select_channels = [
|
||||||
SelectOption(label=x.name, value=str(idx)) for idx, x in enumerate(channel_list)
|
SelectOption(label=x.name, value=str(idx)) for idx, x in enumerate(channel_list)
|
||||||
|
@ -221,7 +243,15 @@ class StarboardCog(Scale):
|
||||||
async def _star_message(self, ctx: InteractionContext) -> None:
|
async def _star_message(self, ctx: InteractionContext) -> None:
|
||||||
await self._star_add(ctx, message=str(ctx.target_id))
|
await self._star_add(ctx, message=str(ctx.target_id))
|
||||||
|
|
||||||
@slash_command(name="star", sub_cmd_name="add", description="Star a message")
|
star = SlashCommand(
|
||||||
|
name="star",
|
||||||
|
description="Manage stars",
|
||||||
|
)
|
||||||
|
|
||||||
|
@star.subcommand(
|
||||||
|
sub_cmd_name="add",
|
||||||
|
sub_cmd_description="Star a message",
|
||||||
|
)
|
||||||
@slash_option(
|
@slash_option(
|
||||||
name="message", description="Message to star", opt_type=OptionTypes.STRING, required=True
|
name="message", description="Message to star", opt_type=OptionTypes.STRING, required=True
|
||||||
)
|
)
|
||||||
|
@ -237,7 +267,7 @@ class StarboardCog(Scale):
|
||||||
) -> None:
|
) -> None:
|
||||||
await self._star_add(ctx, message, channel)
|
await self._star_add(ctx, message, channel)
|
||||||
|
|
||||||
@slash_command(name="star", sub_cmd_name="delete", description="Delete a starred message")
|
@star.subcommand(sub_cmd_name="delete", sub_cmd_description="Delete a starred message")
|
||||||
@slash_option(
|
@slash_option(
|
||||||
name="id", description="Star ID to delete", opt_type=OptionTypes.INTEGER, required=True
|
name="id", description="Star ID to delete", opt_type=OptionTypes.INTEGER, required=True
|
||||||
)
|
)
|
||||||
|
@ -289,5 +319,5 @@ class StarboardCog(Scale):
|
||||||
|
|
||||||
|
|
||||||
def setup(bot: Snake) -> None:
|
def setup(bot: Snake) -> None:
|
||||||
"""Add StarboardCog to J.A.R.V.I.S."""
|
"""Add StarboardCog to JARVIS"""
|
||||||
StarboardCog(bot)
|
StarboardCog(bot)
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
"""J.A.R.V.I.S. Twitter Cog."""
|
"""JARVIS Twitter Cog."""
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import logging
|
||||||
|
|
||||||
import tweepy
|
import tweepy
|
||||||
from dis_snek import InteractionContext, Permissions, Scale, Snake
|
from dis_snek import InteractionContext, Permissions, Scale, Snake
|
||||||
|
@ -8,24 +9,24 @@ from dis_snek.models.discord.channel import GuildText
|
||||||
from dis_snek.models.discord.components import ActionRow, Select, SelectOption
|
from dis_snek.models.discord.components import ActionRow, Select, SelectOption
|
||||||
from dis_snek.models.snek.application_commands import (
|
from dis_snek.models.snek.application_commands import (
|
||||||
OptionTypes,
|
OptionTypes,
|
||||||
SlashCommandChoice,
|
SlashCommand,
|
||||||
slash_command,
|
|
||||||
slash_option,
|
slash_option,
|
||||||
)
|
)
|
||||||
from dis_snek.models.snek.command import check
|
from dis_snek.models.snek.command import check
|
||||||
from jarvis_core.db import q
|
from jarvis_core.db import q
|
||||||
from jarvis_core.db.models import TwitterAccount, TwitterFollow
|
from jarvis_core.db.models import TwitterAccount, TwitterFollow
|
||||||
|
|
||||||
from jarvis import jconfig
|
from jarvis.config import JarvisConfig
|
||||||
from jarvis.utils.permissions import admin_or_permissions
|
from jarvis.utils.permissions import admin_or_permissions
|
||||||
|
|
||||||
|
|
||||||
class TwitterCog(Scale):
|
class TwitterCog(Scale):
|
||||||
"""J.A.R.V.I.S. Twitter Cog."""
|
"""JARVIS Twitter Cog."""
|
||||||
|
|
||||||
def __init__(self, bot: Snake):
|
def __init__(self, bot: Snake):
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
config = jconfig
|
self.logger = logging.getLogger(__name__)
|
||||||
|
config = JarvisConfig.from_yaml()
|
||||||
auth = tweepy.AppAuthHandler(
|
auth = tweepy.AppAuthHandler(
|
||||||
config.twitter["consumer_key"], config.twitter["consumer_secret"]
|
config.twitter["consumer_key"], config.twitter["consumer_secret"]
|
||||||
)
|
)
|
||||||
|
@ -33,7 +34,15 @@ class TwitterCog(Scale):
|
||||||
self._guild_cache = {}
|
self._guild_cache = {}
|
||||||
self._channel_cache = {}
|
self._channel_cache = {}
|
||||||
|
|
||||||
@slash_command(name="twitter", sub_cmd_name="follow", description="Follow a Twitter acount")
|
twitter = SlashCommand(
|
||||||
|
name="twitter",
|
||||||
|
description="Manage Twitter follows",
|
||||||
|
)
|
||||||
|
|
||||||
|
@twitter.subcommand(
|
||||||
|
sub_cmd_name="follow",
|
||||||
|
sub_cmd_description="Follow a Twitter acount",
|
||||||
|
)
|
||||||
@slash_option(
|
@slash_option(
|
||||||
name="handle", description="Twitter account", opt_type=OptionTypes.STRING, required=True
|
name="handle", description="Twitter account", opt_type=OptionTypes.STRING, required=True
|
||||||
)
|
)
|
||||||
|
@ -46,19 +55,14 @@ class TwitterCog(Scale):
|
||||||
@slash_option(
|
@slash_option(
|
||||||
name="retweets",
|
name="retweets",
|
||||||
description="Mirror re-tweets?",
|
description="Mirror re-tweets?",
|
||||||
opt_type=OptionTypes.STRING,
|
opt_type=OptionTypes.BOOLEAN,
|
||||||
required=False,
|
required=False,
|
||||||
choices=[
|
|
||||||
SlashCommandChoice(name="Yes", value="Yes"),
|
|
||||||
SlashCommandChoice(name="No", value="No"),
|
|
||||||
],
|
|
||||||
)
|
)
|
||||||
@check(admin_or_permissions(Permissions.MANAGE_GUILD))
|
@check(admin_or_permissions(Permissions.MANAGE_GUILD))
|
||||||
async def _twitter_follow(
|
async def _twitter_follow(
|
||||||
self, ctx: InteractionContext, handle: str, channel: GuildText, retweets: str = "Yes"
|
self, ctx: InteractionContext, handle: str, channel: GuildText, retweets: bool = True
|
||||||
) -> None:
|
) -> None:
|
||||||
handle = handle.lower()
|
handle = handle.lower()
|
||||||
retweets = retweets == "Yes"
|
|
||||||
if len(handle) > 15:
|
if len(handle) > 15:
|
||||||
await ctx.send("Invalid Twitter handle", ephemeral=True)
|
await ctx.send("Invalid Twitter handle", ephemeral=True)
|
||||||
return
|
return
|
||||||
|
@ -107,7 +111,7 @@ class TwitterCog(Scale):
|
||||||
|
|
||||||
await ctx.send(f"Now following `@{handle}` in {channel.mention}")
|
await ctx.send(f"Now following `@{handle}` in {channel.mention}")
|
||||||
|
|
||||||
@slash_command(name="twitter", sub_cmd_name="unfollow", description="Unfollow Twitter accounts")
|
@twitter.subcommand(sub_cmd_name="unfollow", sub_cmd_description="Unfollow Twitter accounts")
|
||||||
@check(admin_or_permissions(Permissions.MANAGE_GUILD))
|
@check(admin_or_permissions(Permissions.MANAGE_GUILD))
|
||||||
async def _twitter_unfollow(self, ctx: InteractionContext) -> None:
|
async def _twitter_unfollow(self, ctx: InteractionContext) -> None:
|
||||||
t = TwitterFollow.find(q(guild=ctx.guild.id))
|
t = TwitterFollow.find(q(guild=ctx.guild.id))
|
||||||
|
@ -146,7 +150,10 @@ class TwitterCog(Scale):
|
||||||
)
|
)
|
||||||
for to_delete in context.context.values:
|
for to_delete in context.context.values:
|
||||||
follow = get(twitters, guild=ctx.guild.id, twitter_id=int(to_delete))
|
follow = get(twitters, guild=ctx.guild.id, twitter_id=int(to_delete))
|
||||||
await follow.delete()
|
try:
|
||||||
|
await follow.delete()
|
||||||
|
except Exception:
|
||||||
|
self.logger.debug("Ignoring deletion error")
|
||||||
for row in components:
|
for row in components:
|
||||||
for component in row.components:
|
for component in row.components:
|
||||||
component.disabled = True
|
component.disabled = True
|
||||||
|
@ -161,22 +168,18 @@ class TwitterCog(Scale):
|
||||||
component.disabled = True
|
component.disabled = True
|
||||||
await message.edit(components=components)
|
await message.edit(components=components)
|
||||||
|
|
||||||
@slash_command(
|
@twitter.subcommand(
|
||||||
name="twitter", sub_cmd_name="retweets", description="Modify followed Twitter accounts"
|
sub_cmd_name="retweets",
|
||||||
|
sub_cmd_description="Modify followed Twitter accounts",
|
||||||
)
|
)
|
||||||
@slash_option(
|
@slash_option(
|
||||||
name="retweets",
|
name="retweets",
|
||||||
description="Mirror re-tweets?",
|
description="Mirror re-tweets?",
|
||||||
opt_type=OptionTypes.STRING,
|
opt_type=OptionTypes.BOOLEAN,
|
||||||
required=False,
|
required=False,
|
||||||
choices=[
|
|
||||||
SlashCommandChoice(name="Yes", value="Yes"),
|
|
||||||
SlashCommandChoice(name="No", value="No"),
|
|
||||||
],
|
|
||||||
)
|
)
|
||||||
@check(admin_or_permissions(Permissions.MANAGE_GUILD))
|
@check(admin_or_permissions(Permissions.MANAGE_GUILD))
|
||||||
async def _twitter_modify(self, ctx: InteractionContext, retweets: str) -> None:
|
async def _twitter_modify(self, ctx: InteractionContext, retweets: bool = True) -> None:
|
||||||
retweets = retweets == "Yes"
|
|
||||||
t = TwitterFollow.find(q(guild=ctx.guild.id))
|
t = TwitterFollow.find(q(guild=ctx.guild.id))
|
||||||
twitters = []
|
twitters = []
|
||||||
async for twitter in t:
|
async for twitter in t:
|
||||||
|
@ -241,5 +244,5 @@ class TwitterCog(Scale):
|
||||||
|
|
||||||
|
|
||||||
def setup(bot: Snake) -> None:
|
def setup(bot: Snake) -> None:
|
||||||
"""Add TwitterCog to J.A.R.V.I.S."""
|
"""Add TwitterCog to JARVIS"""
|
||||||
TwitterCog(bot)
|
TwitterCog(bot)
|
||||||
|
|
|
@ -1,30 +1,35 @@
|
||||||
"""J.A.R.V.I.S. Utility Cog."""
|
"""JARVIS Utility Cog."""
|
||||||
|
import logging
|
||||||
import platform
|
import platform
|
||||||
import re
|
import re
|
||||||
import secrets
|
import secrets
|
||||||
import string
|
import string
|
||||||
|
from datetime import timezone
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
from dateparser import parse
|
||||||
from dis_snek import InteractionContext, Scale, Snake, const
|
from dis_snek import InteractionContext, Scale, Snake, const
|
||||||
from dis_snek.models.discord.channel import GuildCategory, GuildText, GuildVoice
|
from dis_snek.models.discord.channel import GuildCategory, GuildText, GuildVoice
|
||||||
from dis_snek.models.discord.embed import EmbedField
|
from dis_snek.models.discord.embed import EmbedField
|
||||||
from dis_snek.models.discord.file import File
|
from dis_snek.models.discord.file import File
|
||||||
from dis_snek.models.discord.guild import Guild
|
from dis_snek.models.discord.guild import Guild
|
||||||
from dis_snek.models.discord.role import Role
|
from dis_snek.models.discord.role import Role
|
||||||
from dis_snek.models.discord.user import User
|
from dis_snek.models.discord.user import Member, User
|
||||||
from dis_snek.models.snek.application_commands import (
|
from dis_snek.models.snek.application_commands import (
|
||||||
|
CommandTypes,
|
||||||
OptionTypes,
|
OptionTypes,
|
||||||
SlashCommandChoice,
|
SlashCommandChoice,
|
||||||
|
context_menu,
|
||||||
slash_command,
|
slash_command,
|
||||||
slash_option,
|
slash_option,
|
||||||
)
|
)
|
||||||
from dis_snek.models.snek.command import cooldown
|
from dis_snek.models.snek.command import cooldown
|
||||||
from dis_snek.models.snek.cooldowns import Buckets
|
from dis_snek.models.snek.cooldowns import Buckets
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
from tzlocal import get_localzone
|
||||||
|
|
||||||
import jarvis
|
import jarvis
|
||||||
from jarvis.config import get_config
|
|
||||||
from jarvis.data import pigpen
|
from jarvis.data import pigpen
|
||||||
from jarvis.data.robotcamo import emotes, hk, names
|
from jarvis.data.robotcamo import emotes, hk, names
|
||||||
from jarvis.utils import build_embed, get_repo_hash
|
from jarvis.utils import build_embed, get_repo_hash
|
||||||
|
@ -34,26 +39,32 @@ JARVIS_LOGO = Image.open("jarvis_small.png").convert("RGBA")
|
||||||
|
|
||||||
class UtilCog(Scale):
|
class UtilCog(Scale):
|
||||||
"""
|
"""
|
||||||
Utility functions for J.A.R.V.I.S.
|
Utility functions for JARVIS
|
||||||
|
|
||||||
Mostly system utility functions, but may change over time
|
Mostly system utility functions, but may change over time
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, bot: Snake):
|
def __init__(self, bot: Snake):
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
self.config = get_config()
|
self.logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@slash_command(name="status", description="Retrieve J.A.R.V.I.S. status")
|
@slash_command(name="status", description="Retrieve JARVIS status")
|
||||||
@cooldown(bucket=Buckets.CHANNEL, rate=1, interval=30)
|
@cooldown(bucket=Buckets.CHANNEL, rate=1, interval=30)
|
||||||
async def _status(self, ctx: InteractionContext) -> None:
|
async def _status(self, ctx: InteractionContext) -> None:
|
||||||
title = "J.A.R.V.I.S. Status"
|
title = "JARVIS Status"
|
||||||
desc = "All systems online"
|
desc = f"All systems online\nConnected to **{len(self.bot.guilds)}** guilds"
|
||||||
color = "#3498db"
|
color = "#3498db"
|
||||||
fields = []
|
fields = []
|
||||||
|
|
||||||
fields.append(EmbedField(name="dis-snek", value=const.__version__))
|
fields.append(EmbedField(name="dis-snek", value=const.__version__))
|
||||||
fields.append(EmbedField(name="Version", value=jarvis.__version__, inline=False))
|
fields.append(EmbedField(name="Version", value=jarvis.__version__, inline=False))
|
||||||
fields.append(EmbedField(name="Git Hash", value=get_repo_hash()[:7], inline=False))
|
fields.append(EmbedField(name="Git Hash", value=get_repo_hash()[:7], inline=False))
|
||||||
|
num_domains = len(self.bot.phishing_domains)
|
||||||
|
fields.append(
|
||||||
|
EmbedField(
|
||||||
|
name="Phishing Protection", value=f"Detecting {num_domains} phishing domains"
|
||||||
|
)
|
||||||
|
)
|
||||||
embed = build_embed(title=title, description=desc, fields=fields, color=color)
|
embed = build_embed(title=title, description=desc, fields=fields, color=color)
|
||||||
await ctx.send(embed=embed)
|
await ctx.send(embed=embed)
|
||||||
|
|
||||||
|
@ -96,6 +107,8 @@ class UtilCog(Scale):
|
||||||
to_send += f":{names[id]}:"
|
to_send += f":{names[id]}:"
|
||||||
if len(to_send) > 2000:
|
if len(to_send) > 2000:
|
||||||
await ctx.send("Too long.", ephemeral=True)
|
await ctx.send("Too long.", ephemeral=True)
|
||||||
|
elif len(to_send) == 0:
|
||||||
|
await ctx.send("No valid text found", ephemeral=True)
|
||||||
else:
|
else:
|
||||||
await ctx.send(to_send)
|
await ctx.send(to_send)
|
||||||
|
|
||||||
|
@ -111,7 +124,10 @@ class UtilCog(Scale):
|
||||||
if not user:
|
if not user:
|
||||||
user = ctx.author
|
user = ctx.author
|
||||||
|
|
||||||
avatar = user.display_avatar.url
|
avatar = user.avatar.url
|
||||||
|
if isinstance(user, Member):
|
||||||
|
avatar = user.display_avatar.url
|
||||||
|
|
||||||
embed = build_embed(title="Avatar", description="", fields=[], color="#00FFEE")
|
embed = build_embed(title="Avatar", description="", fields=[], color="#00FFEE")
|
||||||
embed.set_image(url=avatar)
|
embed.set_image(url=avatar)
|
||||||
embed.set_author(name=f"{user.username}#{user.discriminator}", icon_url=avatar)
|
embed.set_author(name=f"{user.username}#{user.discriminator}", icon_url=avatar)
|
||||||
|
@ -130,7 +146,7 @@ class UtilCog(Scale):
|
||||||
async def _roleinfo(self, ctx: InteractionContext, role: Role) -> None:
|
async def _roleinfo(self, ctx: InteractionContext, role: Role) -> None:
|
||||||
fields = [
|
fields = [
|
||||||
EmbedField(name="ID", value=str(role.id), inline=True),
|
EmbedField(name="ID", value=str(role.id), inline=True),
|
||||||
EmbedField(name="Name", value=role.name, inline=True),
|
EmbedField(name="Name", value=role.mention, inline=True),
|
||||||
EmbedField(name="Color", value=str(role.color.hex), inline=True),
|
EmbedField(name="Color", value=str(role.color.hex), inline=True),
|
||||||
EmbedField(name="Mention", value=f"`{role.mention}`", inline=True),
|
EmbedField(name="Mention", value=f"`{role.mention}`", inline=True),
|
||||||
EmbedField(name="Hoisted", value="Yes" if role.hoist else "No", inline=True),
|
EmbedField(name="Hoisted", value="Yes" if role.hoist else "No", inline=True),
|
||||||
|
@ -164,19 +180,13 @@ class UtilCog(Scale):
|
||||||
|
|
||||||
await ctx.send(embed=embed, file=color_show)
|
await ctx.send(embed=embed, file=color_show)
|
||||||
|
|
||||||
@slash_command(
|
|
||||||
name="userinfo",
|
|
||||||
description="Get user info",
|
|
||||||
)
|
|
||||||
@slash_option(
|
|
||||||
name="user",
|
|
||||||
description="User to get info of",
|
|
||||||
opt_type=OptionTypes.USER,
|
|
||||||
required=False,
|
|
||||||
)
|
|
||||||
async def _userinfo(self, ctx: InteractionContext, user: User = None) -> None:
|
async def _userinfo(self, ctx: InteractionContext, user: User = None) -> None:
|
||||||
|
await ctx.defer()
|
||||||
if not user:
|
if not user:
|
||||||
user = ctx.author
|
user = ctx.author
|
||||||
|
if not await ctx.guild.fetch_member(user.id):
|
||||||
|
await ctx.send("That user isn't in this guild.", ephemeral=True)
|
||||||
|
return
|
||||||
user_roles = user.roles
|
user_roles = user.roles
|
||||||
if user_roles:
|
if user_roles:
|
||||||
user_roles = sorted(user.roles, key=lambda x: -x.position)
|
user_roles = sorted(user.roles, key=lambda x: -x.position)
|
||||||
|
@ -215,6 +225,23 @@ class UtilCog(Scale):
|
||||||
|
|
||||||
await ctx.send(embed=embed)
|
await ctx.send(embed=embed)
|
||||||
|
|
||||||
|
@slash_command(
|
||||||
|
name="userinfo",
|
||||||
|
description="Get user info",
|
||||||
|
)
|
||||||
|
@slash_option(
|
||||||
|
name="user",
|
||||||
|
description="User to get info of",
|
||||||
|
opt_type=OptionTypes.USER,
|
||||||
|
required=False,
|
||||||
|
)
|
||||||
|
async def _userinfo_slsh(self, ctx: InteractionContext, user: User = None) -> None:
|
||||||
|
await self._userinfo(ctx, user)
|
||||||
|
|
||||||
|
@context_menu(name="User Info", context_type=CommandTypes.USER)
|
||||||
|
async def _userinfo_menu(self, ctx: InteractionContext) -> None:
|
||||||
|
await self._userinfo(ctx, ctx.target)
|
||||||
|
|
||||||
@slash_command(name="serverinfo", description="Get server info")
|
@slash_command(name="serverinfo", description="Get server info")
|
||||||
async def _server_info(self, ctx: InteractionContext) -> None:
|
async def _server_info(self, ctx: InteractionContext) -> None:
|
||||||
guild: Guild = ctx.guild
|
guild: Guild = ctx.guild
|
||||||
|
@ -281,6 +308,7 @@ class UtilCog(Scale):
|
||||||
if length > 256:
|
if length > 256:
|
||||||
await ctx.send("Please limit password to 256 characters", ephemeral=True)
|
await ctx.send("Please limit password to 256 characters", ephemeral=True)
|
||||||
return
|
return
|
||||||
|
|
||||||
choices = [
|
choices = [
|
||||||
string.ascii_letters,
|
string.ascii_letters,
|
||||||
string.hexdigits,
|
string.hexdigits,
|
||||||
|
@ -314,7 +342,39 @@ class UtilCog(Scale):
|
||||||
outp += "`"
|
outp += "`"
|
||||||
await ctx.send(outp[:2000])
|
await ctx.send(outp[:2000])
|
||||||
|
|
||||||
|
@slash_command(
|
||||||
|
name="timestamp", description="Convert a datetime or timestamp into it's counterpart"
|
||||||
|
)
|
||||||
|
@slash_option(
|
||||||
|
name="string", description="String to convert", opt_type=OptionTypes.STRING, required=True
|
||||||
|
)
|
||||||
|
@slash_option(
|
||||||
|
name="private", description="Respond quietly?", opt_type=OptionTypes.BOOLEAN, required=False
|
||||||
|
)
|
||||||
|
async def _timestamp(self, ctx: InteractionContext, string: str, private: bool = False) -> None:
|
||||||
|
timestamp = parse(string)
|
||||||
|
if not timestamp:
|
||||||
|
await ctx.send("Valid time not found, try again", ephemeral=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
if not timestamp.tzinfo:
|
||||||
|
timestamp = timestamp.replace(tzinfo=get_localzone()).astimezone(tz=timezone.utc)
|
||||||
|
|
||||||
|
timestamp_utc = timestamp.astimezone(tz=timezone.utc)
|
||||||
|
|
||||||
|
ts = int(timestamp.timestamp())
|
||||||
|
ts_utc = int(timestamp_utc.timestamp())
|
||||||
|
fields = [
|
||||||
|
EmbedField(name="Unix Epoch", value=f"`{ts}`"),
|
||||||
|
EmbedField(name="Unix Epoch (UTC)", value=f"`{ts_utc}`"),
|
||||||
|
EmbedField(name="Absolute Time", value=f"<t:{ts_utc}:F>\n`<t:{ts_utc}:F>`"),
|
||||||
|
EmbedField(name="Relative Time", value=f"<t:{ts_utc}:R>\n`<t:{ts_utc}:R>`"),
|
||||||
|
EmbedField(name="ISO8601", value=timestamp.isoformat()),
|
||||||
|
]
|
||||||
|
embed = build_embed(title="Converted Time", description=f"`{string}`", fields=fields)
|
||||||
|
await ctx.send(embed=embed, ephemeral=private)
|
||||||
|
|
||||||
|
|
||||||
def setup(bot: Snake) -> None:
|
def setup(bot: Snake) -> None:
|
||||||
"""Add UtilCog to J.A.R.V.I.S."""
|
"""Add UtilCog to JARVIS"""
|
||||||
UtilCog(bot)
|
UtilCog(bot)
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
"""J.A.R.V.I.S. Verify Cog."""
|
"""JARVIS Verify Cog."""
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import logging
|
||||||
from random import randint
|
from random import randint
|
||||||
|
|
||||||
from dis_snek import InteractionContext, Scale, Snake
|
from dis_snek import InteractionContext, Scale, Snake
|
||||||
from dis_snek.models.application_commands import slash_command
|
|
||||||
from dis_snek.models.discord.components import Button, ButtonStyles, spread_to_rows
|
from dis_snek.models.discord.components import Button, ButtonStyles, spread_to_rows
|
||||||
|
from dis_snek.models.snek.application_commands import slash_command
|
||||||
from dis_snek.models.snek.command import cooldown
|
from dis_snek.models.snek.command import cooldown
|
||||||
from dis_snek.models.snek.cooldowns import Buckets
|
from dis_snek.models.snek.cooldowns import Buckets
|
||||||
from jarvis_core.db import q
|
from jarvis_core.db import q
|
||||||
|
@ -30,21 +31,22 @@ def create_layout() -> list:
|
||||||
|
|
||||||
|
|
||||||
class VerifyCog(Scale):
|
class VerifyCog(Scale):
|
||||||
"""J.A.R.V.I.S. Verify Cog."""
|
"""JARVIS Verify Cog."""
|
||||||
|
|
||||||
def __init__(self, bot: Snake):
|
def __init__(self, bot: Snake):
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
|
self.logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@slash_command(name="verify", description="Verify that you've read the rules")
|
@slash_command(name="verify", description="Verify that you've read the rules")
|
||||||
@cooldown(bucket=Buckets.USER, rate=1, interval=15)
|
@cooldown(bucket=Buckets.USER, rate=1, interval=30)
|
||||||
async def _verify(self, ctx: InteractionContext) -> None:
|
async def _verify(self, ctx: InteractionContext) -> None:
|
||||||
await ctx.defer()
|
await ctx.defer()
|
||||||
role = await Setting.find_one(q(guild=ctx.guild.id, setting="verified"))
|
role = await Setting.find_one(q(guild=ctx.guild.id, setting="verified"))
|
||||||
if not role:
|
if not role:
|
||||||
await ctx.send("This guild has not enabled verification", delete_after=5)
|
message = await ctx.send("This guild has not enabled verification", ephemeral=True)
|
||||||
return
|
return
|
||||||
if await ctx.guild.get_role(role.value) in ctx.author.roles:
|
if await ctx.guild.fetch_role(role.value) in ctx.author.roles:
|
||||||
await ctx.send("You are already verified.", delete_after=5)
|
await ctx.send("You are already verified.", ephemeral=True)
|
||||||
return
|
return
|
||||||
components = create_layout()
|
components = create_layout()
|
||||||
message = await ctx.send(
|
message = await ctx.send(
|
||||||
|
@ -53,39 +55,45 @@ class VerifyCog(Scale):
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
context = await self.bot.wait_for_component(
|
verified = False
|
||||||
messages=message, check=lambda x: ctx.author.id == x.author.id, timeout=30
|
while not verified:
|
||||||
)
|
response = await self.bot.wait_for_component(
|
||||||
|
messages=message,
|
||||||
correct = context.context.custom_id.split("||")[-1] == "yes"
|
check=lambda x: ctx.author.id == x.context.author.id,
|
||||||
if correct:
|
timeout=30,
|
||||||
for row in components:
|
|
||||||
for component in row.components:
|
|
||||||
component.disabled = True
|
|
||||||
setting = await Setting.find_one(guild=ctx.guild.id, setting="verified")
|
|
||||||
role = await ctx.guild.get_role(setting.value)
|
|
||||||
await ctx.author.add_roles(role, reason="Verification passed")
|
|
||||||
setting = await Setting.find_one(guild=ctx.guild.id, setting="unverified")
|
|
||||||
if setting:
|
|
||||||
role = await ctx.guild.get_role(setting.value)
|
|
||||||
await ctx.author.remove_roles(role, reason="Verification passed")
|
|
||||||
|
|
||||||
await context.context.edit_origin(
|
|
||||||
content=f"Welcome, {ctx.author.mention}. Please enjoy your stay.",
|
|
||||||
components=components,
|
|
||||||
)
|
)
|
||||||
await context.context.message.delete(delay=5)
|
|
||||||
else:
|
correct = response.context.custom_id.split("||")[-1] == "yes"
|
||||||
await context.context.edit_origin(
|
if correct:
|
||||||
content=(
|
for row in components:
|
||||||
f"{ctx.author.mention}, incorrect. "
|
for component in row.components:
|
||||||
"Please press the button that says `YES`"
|
component.disabled = True
|
||||||
|
setting = await Setting.find_one(q(guild=ctx.guild.id, setting="verified"))
|
||||||
|
role = await ctx.guild.fetch_role(setting.value)
|
||||||
|
await ctx.author.add_role(role, reason="Verification passed")
|
||||||
|
setting = await Setting.find_one(q(guild=ctx.guild.id, setting="unverified"))
|
||||||
|
if setting:
|
||||||
|
role = await ctx.guild.fetch_role(setting.value)
|
||||||
|
await ctx.author.remove_role(role, reason="Verification passed")
|
||||||
|
|
||||||
|
await response.context.edit_origin(
|
||||||
|
content=f"Welcome, {ctx.author.mention}. Please enjoy your stay.",
|
||||||
|
components=components,
|
||||||
|
)
|
||||||
|
await response.context.message.delete(delay=5)
|
||||||
|
self.logger.debug(f"User {ctx.author.id} verified successfully")
|
||||||
|
else:
|
||||||
|
await response.context.edit_origin(
|
||||||
|
content=(
|
||||||
|
f"{ctx.author.mention}, incorrect. "
|
||||||
|
"Please press the button that says `YES`"
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
|
||||||
except asyncio.TimeoutError:
|
except asyncio.TimeoutError:
|
||||||
await message.delete(delay=30)
|
await message.delete(delay=2)
|
||||||
|
self.logger.debug(f"User {ctx.author.id} failed to verify before timeout")
|
||||||
|
|
||||||
|
|
||||||
def setup(bot: Snake) -> None:
|
def setup(bot: Snake) -> None:
|
||||||
"""Add VerifyCog to J.A.R.V.I.S."""
|
"""Add VerifyCog to JARVIS"""
|
||||||
VerifyCog(bot)
|
VerifyCog(bot)
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
"""Load the config for J.A.R.V.I.S."""
|
"""Load the config for JARVIS"""
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from jarvis_core.config import Config as CConfig
|
from jarvis_core.config import Config as CConfig
|
||||||
|
@ -12,7 +12,7 @@ except ImportError:
|
||||||
|
|
||||||
|
|
||||||
class JarvisConfig(CConfig):
|
class JarvisConfig(CConfig):
|
||||||
REQUIRED = ["token", "client_id", "mongo", "urls"]
|
REQUIRED = ["token", "mongo", "urls"]
|
||||||
OPTIONAL = {
|
OPTIONAL = {
|
||||||
"sync": False,
|
"sync": False,
|
||||||
"log_level": "WARNING",
|
"log_level": "WARNING",
|
||||||
|
@ -25,7 +25,7 @@ class JarvisConfig(CConfig):
|
||||||
|
|
||||||
|
|
||||||
class Config(object):
|
class Config(object):
|
||||||
"""Config singleton object for J.A.R.V.I.S."""
|
"""Config singleton object for JARVIS"""
|
||||||
|
|
||||||
def __new__(cls, *args: list, **kwargs: dict):
|
def __new__(cls, *args: list, **kwargs: dict):
|
||||||
"""Get the singleton config, or creates a new one."""
|
"""Get the singleton config, or creates a new one."""
|
||||||
|
@ -39,8 +39,6 @@ class Config(object):
|
||||||
def init(
|
def init(
|
||||||
self,
|
self,
|
||||||
token: str,
|
token: str,
|
||||||
client_id: str,
|
|
||||||
logo: str,
|
|
||||||
mongo: dict,
|
mongo: dict,
|
||||||
urls: dict,
|
urls: dict,
|
||||||
sync: bool = False,
|
sync: bool = False,
|
||||||
|
@ -53,8 +51,6 @@ class Config(object):
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize the config object."""
|
"""Initialize the config object."""
|
||||||
self.token = token
|
self.token = token
|
||||||
self.client_id = client_id
|
|
||||||
self.logo = logo
|
|
||||||
self.mongo = mongo
|
self.mongo = mongo
|
||||||
self.urls = urls
|
self.urls = urls
|
||||||
self.log_level = log_level
|
self.log_level = log_level
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
"""J.A.R.V.I.S. Utility Functions."""
|
"""JARVIS Utility Functions."""
|
||||||
from datetime import datetime
|
from datetime import datetime, timezone
|
||||||
from pkgutil import iter_modules
|
from pkgutil import iter_modules
|
||||||
|
|
||||||
import git
|
import git
|
||||||
|
@ -23,7 +23,7 @@ def build_embed(
|
||||||
) -> Embed:
|
) -> Embed:
|
||||||
"""Embed builder utility function."""
|
"""Embed builder utility function."""
|
||||||
if not timestamp:
|
if not timestamp:
|
||||||
timestamp = datetime.now()
|
timestamp = datetime.now(tz=timezone.utc)
|
||||||
embed = Embed(
|
embed = Embed(
|
||||||
title=title,
|
title=title,
|
||||||
description=description,
|
description=description,
|
||||||
|
@ -65,14 +65,14 @@ def modlog_embed(
|
||||||
|
|
||||||
|
|
||||||
def get_extensions(path: str = jarvis.cogs.__path__) -> list:
|
def get_extensions(path: str = jarvis.cogs.__path__) -> list:
|
||||||
"""Get J.A.R.V.I.S. cogs."""
|
"""Get JARVIS cogs."""
|
||||||
config = get_config()
|
config = get_config()
|
||||||
vals = config.cogs or [x.name for x in iter_modules(path)]
|
vals = config.cogs or [x.name for x in iter_modules(path)]
|
||||||
return ["jarvis.cogs.{}".format(x) for x in vals]
|
return ["jarvis.cogs.{}".format(x) for x in vals]
|
||||||
|
|
||||||
|
|
||||||
def update() -> int:
|
def update() -> int:
|
||||||
"""J.A.R.V.I.S. update utility."""
|
"""JARVIS update utility."""
|
||||||
repo = git.Repo(".")
|
repo = git.Repo(".")
|
||||||
dirty = repo.is_dirty()
|
dirty = repo.is_dirty()
|
||||||
current_hash = repo.head.object.hexsha
|
current_hash = repo.head.object.hexsha
|
||||||
|
@ -87,6 +87,6 @@ def update() -> int:
|
||||||
|
|
||||||
|
|
||||||
def get_repo_hash() -> str:
|
def get_repo_hash() -> str:
|
||||||
"""J.A.R.V.I.S. current branch hash."""
|
"""JARVIS current branch hash."""
|
||||||
repo = git.Repo(".")
|
repo = git.Repo(".")
|
||||||
return repo.head.object.hexsha
|
return repo.head.object.hexsha
|
||||||
|
|
|
@ -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:
|
def user_is_bot_admin() -> bool:
|
||||||
"""Check if a user is a J.A.R.V.I.S. admin."""
|
"""Check if a user is a JARVIS admin."""
|
||||||
|
|
||||||
async def predicate(ctx: InteractionContext) -> bool:
|
async def predicate(ctx: InteractionContext) -> bool:
|
||||||
"""Command check predicate."""
|
"""Command check predicate."""
|
||||||
|
|
753
poetry.lock
generated
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"}
|
jarvis-core = {git = "https://git.zevaryx.com/stark-industries/jarvis/jarvis-core.git", rev = "main"}
|
||||||
aiohttp = "^3.8.1"
|
aiohttp = "^3.8.1"
|
||||||
pastypy = "^1.0.1"
|
pastypy = "^1.0.1"
|
||||||
|
dateparser = "^1.1.1"
|
||||||
[tool.poetry.dev-dependencies]
|
aiofile = "^3.7.4"
|
||||||
python-lsp-server = {extras = ["all"], version = "^1.3.3"}
|
molter = "^0.11.0"
|
||||||
black = "^22.1.0"
|
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = ["poetry-core>=1.0.0"]
|
requires = ["poetry-core>=1.0.0"]
|
||||||
|
|
Loading…
Add table
Reference in a new issue