Merge branch 'dev' into embeds
This commit is contained in:
commit
61a3cdbcd1
23 changed files with 1826 additions and 1402 deletions
|
@ -1,5 +1,7 @@
|
|||
"""Main JARVIS package."""
|
||||
import logging
|
||||
from functools import partial
|
||||
from typing import Any
|
||||
|
||||
import aioredis
|
||||
import jurigged
|
||||
|
@ -17,11 +19,47 @@ from jarvis.utils import get_extensions
|
|||
__version__ = const.__version__
|
||||
|
||||
|
||||
def jlogger(logger: logging.Logger, event: Any) -> None:
|
||||
"""
|
||||
Logging for jurigged
|
||||
|
||||
Args:
|
||||
logger: Logger to use
|
||||
event: Event to parse
|
||||
"""
|
||||
jlog = partial(logger.log, 11)
|
||||
if isinstance(event, jurigged.live.WatchOperation):
|
||||
jlog(f"[bold]Watch[/] {event.filename}", extra={"markup": True})
|
||||
elif isinstance(event, jurigged.codetools.AddOperation):
|
||||
event_str = f"{event.defn.parent.dotpath()}:{event.defn.stashed.lineno}"
|
||||
if isinstance(event.defn, jurigged.codetools.LineDefinition):
|
||||
event_str += f" | {event.defn.text}"
|
||||
jlog(
|
||||
f"[bold green]Run[/] {event_str}",
|
||||
extra={"markup": True},
|
||||
)
|
||||
else:
|
||||
jlog(f"[bold green]Add[/] {event_str}", extra={"markup": True})
|
||||
elif isinstance(event, jurigged.codetools.UpdateOperation):
|
||||
if isinstance(event.defn, jurigged.codetools.FunctionDefinition):
|
||||
event_str = f"{event.defn.parent.dotpath()}:{event.defn.stashed.lineno}"
|
||||
jlog(f"[bold yellow]Update[/] {event_str}", extra={"markup": True})
|
||||
elif isinstance(event, jurigged.codetools.DeleteOperation):
|
||||
event_str = f"{event.defn.parent.dotpath()}:{event.defn.stashed.lineno}"
|
||||
if isinstance(event.defn, jurigged.codetools.LineDefinition):
|
||||
event_str += f" | {event.defn.text}"
|
||||
jlog(f"[bold red]Delete[/] {event_str}", extra={"markup": True})
|
||||
elif isinstance(event, (Exception, SyntaxError)):
|
||||
logger.exception("Jurigged encountered error", exc_info=True)
|
||||
else:
|
||||
jlog(event)
|
||||
|
||||
|
||||
async def run() -> None:
|
||||
"""Run JARVIS"""
|
||||
# Configure logger
|
||||
jconfig = JarvisConfig.from_yaml()
|
||||
logger = get_logger("jarvis", show_locals=jconfig.log_level == "DEBUG")
|
||||
logger = get_logger("jarvis", show_locals=False) # jconfig.log_level == "DEBUG")
|
||||
logger.setLevel(jconfig.log_level)
|
||||
file_handler = logging.FileHandler(filename="jarvis.log", encoding="UTF-8", mode="w")
|
||||
file_handler.setFormatter(
|
||||
|
@ -49,7 +87,8 @@ async def run() -> None:
|
|||
|
||||
# External modules
|
||||
if jconfig.log_level == "DEBUG":
|
||||
jurigged.watch(pattern="jarvis/*.py")
|
||||
logging.addLevelName(11, "\033[35mJURIG\033[0m ")
|
||||
jurigged.watch(pattern="jarvis/*.py", logger=partial(jlogger, logger))
|
||||
if jconfig.rook_token:
|
||||
rook.start(token=jconfig.rook_token, labels={"env": "dev"})
|
||||
|
||||
|
|
|
@ -11,8 +11,13 @@ COMMAND_TYPES = {
|
|||
"SOFT": ["warning"],
|
||||
"GOOD": ["unban", "unmute"],
|
||||
}
|
||||
|
||||
CUSTOM_COMMANDS = {}
|
||||
CUSTOM_EMOJIS = {
|
||||
"ico_clock_green": "<:ico_clock_green:1019710693206933605>",
|
||||
"ico_clock_yellow": "<:ico_clock_yellow:1019710734340472834>",
|
||||
"ico_clock_red": "<:ico_clock_red:1019710735896551534>",
|
||||
"ico_check_green": "<:ico_check_green:1019725504120639549>",
|
||||
}
|
||||
|
||||
|
||||
def get_command_color(command: str) -> str:
|
||||
|
|
982
jarvis/client.py
982
jarvis/client.py
|
@ -1,982 +0,0 @@
|
|||
"""Custom JARVIS client."""
|
||||
import asyncio
|
||||
import logging
|
||||
import re
|
||||
import traceback
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from aiohttp import ClientSession
|
||||
from jarvis_core.db import q
|
||||
from jarvis_core.db.models import (
|
||||
Action,
|
||||
Autopurge,
|
||||
Autoreact,
|
||||
Modlog,
|
||||
Note,
|
||||
Reminder,
|
||||
Roleping,
|
||||
Setting,
|
||||
Star,
|
||||
Warning,
|
||||
)
|
||||
from jarvis_core.filters import invites, url
|
||||
from jarvis_core.util.ansi import RESET, Fore, Format, fmt
|
||||
from naff import listen
|
||||
from naff.api.events.discord import (
|
||||
MemberAdd,
|
||||
MemberRemove,
|
||||
MemberUpdate,
|
||||
MessageCreate,
|
||||
MessageDelete,
|
||||
MessageUpdate,
|
||||
)
|
||||
from naff.api.events.internal import Button
|
||||
from naff.client.errors import CommandCheckFailure, CommandOnCooldown, HTTPException
|
||||
from naff.client.utils.misc_utils import find_all, get
|
||||
from naff.models.discord.channel import DMChannel
|
||||
from naff.models.discord.embed import Embed, EmbedField
|
||||
from naff.models.discord.enums import AuditLogEventType, Permissions
|
||||
from naff.models.discord.message import Message
|
||||
from naff.models.discord.user import Member
|
||||
from naff.models.naff.context import Context, InteractionContext, PrefixedContext
|
||||
from naff.models.naff.tasks.task import Task
|
||||
from naff.models.naff.tasks.triggers import IntervalTrigger
|
||||
from nafftrack.client import StatsClient
|
||||
from pastypy import AsyncPaste as Paste
|
||||
|
||||
from jarvis import const
|
||||
from jarvis.embeds.admin import warning_embed
|
||||
from jarvis.tracking import jarvis_info, malicious_tracker, warnings_tracker
|
||||
from jarvis.utils import build_embed
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from aioredis import Redis
|
||||
|
||||
DEFAULT_GUILD = 862402786116763668
|
||||
DEFAULT_ERROR_CHANNEL = 943395824560394250
|
||||
DEFAULT_SITE = "https://paste.zevs.me"
|
||||
|
||||
ERROR_MSG = """
|
||||
Command Information:
|
||||
Guild: {guild_name}
|
||||
Name: {invoked_name}
|
||||
Args:
|
||||
{arg_str}
|
||||
|
||||
Callback:
|
||||
Args:
|
||||
{callback_args}
|
||||
Kwargs:
|
||||
{callback_kwargs}
|
||||
"""
|
||||
|
||||
KEY_FMT = fmt(Fore.GRAY)
|
||||
VAL_FMT = fmt(Fore.WHITE)
|
||||
CMD_FMT = fmt(Fore.GREEN, Format.BOLD)
|
||||
|
||||
|
||||
class Jarvis(StatsClient):
|
||||
def __init__(self, redis: "Redis", *args, **kwargs): # noqa: ANN002 ANN003
|
||||
super().__init__(*args, **kwargs)
|
||||
self.redis = redis
|
||||
self.logger = logging.getLogger(__name__)
|
||||
self.phishing_domains = []
|
||||
self.pre_run_callback = self._prerun
|
||||
|
||||
async def _chunk_all(self) -> None:
|
||||
"""Chunk all guilds."""
|
||||
for guild in self.guilds:
|
||||
self.logger.debug(f"Chunking guild {guild.name} <{guild.id}>")
|
||||
await guild.chunk_guild()
|
||||
|
||||
@Task.create(IntervalTrigger(hours=1))
|
||||
async def _update_domains(self) -> None:
|
||||
self.logger.debug("Updating phishing domains")
|
||||
async with ClientSession(headers={"X-Identity": "Discord: zevaryx#5779"}) as session:
|
||||
response = await session.get("https://phish.sinking.yachts/v2/recent/3700")
|
||||
response.raise_for_status()
|
||||
data = await response.json()
|
||||
|
||||
self.logger.debug(f"Found {len(data)} changes to phishing domains")
|
||||
|
||||
add = 0
|
||||
sub = 0
|
||||
|
||||
for update in data:
|
||||
if update["type"] == "add":
|
||||
for domain in update["domains"]:
|
||||
if domain not in self.phishing_domains:
|
||||
add += 1
|
||||
self.phishing_domains.append(domain)
|
||||
elif update["type"] == "delete":
|
||||
for domain in update["domains"]:
|
||||
if domain in self.phishing_domains:
|
||||
sub -= 1
|
||||
self.phishing_domains.remove(domain)
|
||||
self.logger.debug(f"{add} additions, {sub} removals")
|
||||
|
||||
async def _prerun(self, ctx: Context, *args, **kwargs) -> None:
|
||||
name = ctx.invoke_target
|
||||
cargs = ""
|
||||
if isinstance(ctx, InteractionContext) and ctx.target_id:
|
||||
kwargs["context target"] = ctx.target
|
||||
cargs = " ".join(f"{k}:{v}" for k, v in kwargs.items())
|
||||
elif isinstance(ctx, PrefixedContext):
|
||||
cargs = " ".join(args)
|
||||
self.logger.debug(f"Running command `{name}` with args: {cargs or 'None'}")
|
||||
|
||||
async def _sync_domains(self) -> None:
|
||||
self.logger.debug("Loading phishing domains")
|
||||
async with ClientSession(headers={"X-Identity": "Discord: zevaryx#5779"}) as session:
|
||||
response = await session.get("https://phish.sinking.yachts/v2/all")
|
||||
response.raise_for_status()
|
||||
self.phishing_domains = await response.json()
|
||||
self.logger.info(f"Protected from {len(self.phishing_domains)} phishing domains")
|
||||
|
||||
@listen()
|
||||
async def on_startup(self) -> None:
|
||||
"""NAFF on_startup override. Prometheus info generated here."""
|
||||
jarvis_info.info({"version": const.__version__})
|
||||
|
||||
@listen()
|
||||
async def on_ready(self) -> None:
|
||||
"""NAFF on_ready override."""
|
||||
try:
|
||||
await self._sync_domains()
|
||||
self._update_domains.start()
|
||||
asyncio.create_task(self._chunk_all())
|
||||
except Exception as e:
|
||||
self.logger.error("Failed to load anti-phishing", exc_info=e)
|
||||
self.logger.info("Logged in as {}".format(self.user)) # noqa: T001
|
||||
self.logger.info("Connected to {} guild(s)".format(len(self.guilds))) # noqa: T001
|
||||
self.logger.info("Current version: {}".format(const.__version__))
|
||||
self.logger.info( # noqa: T001
|
||||
"https://discord.com/api/oauth2/authorize?client_id="
|
||||
"{}&permissions=8&scope=bot%20applications.commands".format(self.user.id)
|
||||
)
|
||||
|
||||
self.logger.debug("Hitting Reminders for faster loads")
|
||||
_ = await Reminder.find().to_list(None)
|
||||
|
||||
async def on_error(self, source: str, error: Exception, *args, **kwargs) -> None:
|
||||
"""NAFF on_error override."""
|
||||
if isinstance(error, HTTPException):
|
||||
errors = error.search_for_message(error.errors)
|
||||
out = f"HTTPException: {error.status}|{error.response.reason}: " + "\n".join(errors)
|
||||
self.logger.error(out, exc_info=error)
|
||||
else:
|
||||
self.logger.error(f"Ignoring exception in {source}", exc_info=error)
|
||||
|
||||
async def on_command_error(
|
||||
self, ctx: Context, error: Exception, *args: list, **kwargs: dict
|
||||
) -> None:
|
||||
"""NAFF on_command_error override."""
|
||||
name = ctx.invoke_target
|
||||
self.logger.debug(f"Handling error in {name}: {error}")
|
||||
if isinstance(error, CommandOnCooldown):
|
||||
await ctx.send(str(error), ephemeral=True)
|
||||
return
|
||||
elif isinstance(error, CommandCheckFailure):
|
||||
await ctx.send("I'm afraid I can't let you do that", ephemeral=True)
|
||||
return
|
||||
guild = await self.fetch_guild(DEFAULT_GUILD)
|
||||
channel = await guild.fetch_channel(DEFAULT_ERROR_CHANNEL)
|
||||
error_time = datetime.now(tz=timezone.utc).strftime("%d-%m-%Y %H:%M-%S.%f UTC")
|
||||
timestamp = int(datetime.now(tz=timezone.utc).timestamp())
|
||||
timestamp = f"<t:{timestamp}:T>"
|
||||
arg_str = ""
|
||||
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, PrefixedContext):
|
||||
for v in ctx.args:
|
||||
if isinstance(v, str) and len(v) > 100:
|
||||
v = v[97] + "..."
|
||||
arg_str += f" - {v}"
|
||||
callback_args = "\n".join(f" - {i}" for i in args) if args else " None"
|
||||
callback_kwargs = (
|
||||
"\n".join(f" {k}: {v}" for k, v in kwargs.items()) if kwargs else " None"
|
||||
)
|
||||
full_message = ERROR_MSG.format(
|
||||
guild_name=ctx.guild.name,
|
||||
error_time=error_time,
|
||||
invoked_name=name,
|
||||
arg_str=arg_str,
|
||||
callback_args=callback_args,
|
||||
callback_kwargs=callback_kwargs,
|
||||
)
|
||||
tb = traceback.format_exception(error)
|
||||
if isinstance(error, HTTPException):
|
||||
errors = error.search_for_message(error.errors)
|
||||
tb[-1] = f"HTTPException: {error.status}|{error.response.reason}: " + "\n".join(errors)
|
||||
error_message = "".join(traceback.format_exception(error))
|
||||
if len(full_message + error_message) >= 1800:
|
||||
error_message = "\n ".join(error_message.split("\n"))
|
||||
full_message += "Exception: |\n " + error_message
|
||||
paste = Paste(content=full_message, site=DEFAULT_SITE)
|
||||
key = await paste.save()
|
||||
self.logger.debug(f"Large traceback, saved to Pasty {paste.id}, {key=}")
|
||||
|
||||
await channel.send(
|
||||
f"JARVIS encountered an error at {timestamp}. Log too big to send over Discord."
|
||||
f"\nPlease see log at {paste.url}"
|
||||
)
|
||||
else:
|
||||
await channel.send(
|
||||
f"JARVIS encountered an error at {timestamp}:"
|
||||
f"\n```yaml\n{full_message}\n```"
|
||||
f"\nException:\n```py\n{error_message}\n```"
|
||||
)
|
||||
await ctx.send("Whoops! Encountered an error. The error has been logged.", ephemeral=True)
|
||||
try:
|
||||
return await super().on_command_error(ctx, error, *args, **kwargs)
|
||||
except Exception as e:
|
||||
self.logger.error("Uncaught exception", exc_info=e)
|
||||
|
||||
# Modlog
|
||||
async def on_command(self, ctx: Context) -> None:
|
||||
"""NAFF on_command override."""
|
||||
name = ctx.invoke_target
|
||||
if not isinstance(ctx.channel, DMChannel) and name not in ["pw"]:
|
||||
modlog = await Setting.find_one(q(guild=ctx.guild.id, setting="activitylog"))
|
||||
ignore = await Setting.find_one(q(guild=ctx.guild.id, setting="log_ignore"))
|
||||
if modlog and (ignore and ctx.channel.id not in ignore.value):
|
||||
channel = await ctx.guild.fetch_channel(modlog.value)
|
||||
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, PrefixedContext):
|
||||
for v in ctx.args:
|
||||
if isinstance(v, str) and len(v) > 100:
|
||||
v = v[97] + "..."
|
||||
args.append(f"{VAL_FMT}{v}{RESET}")
|
||||
args = " ".join(args)
|
||||
fields = [
|
||||
EmbedField(
|
||||
name="Command",
|
||||
value=f"```ansi\n{CMD_FMT}{ctx.invoke_target}{RESET} {args}\n```",
|
||||
inline=False,
|
||||
),
|
||||
]
|
||||
embed = build_embed(
|
||||
title="Command Invoked",
|
||||
description=f"{ctx.author.mention} invoked a command in {ctx.channel.mention}",
|
||||
fields=fields,
|
||||
color="#fc9e3f",
|
||||
)
|
||||
embed.set_author(name=ctx.author.username, icon_url=ctx.author.display_avatar.url)
|
||||
embed.set_footer(
|
||||
text=f"{ctx.author.user.username}#{ctx.author.discriminator} | {ctx.author.id}"
|
||||
)
|
||||
if channel:
|
||||
await channel.send(embeds=embed)
|
||||
else:
|
||||
self.logger.warning(
|
||||
f"Activitylog channel no longer exists in {ctx.guild.name}, removing"
|
||||
)
|
||||
await modlog.delete()
|
||||
|
||||
# Events
|
||||
# Member
|
||||
@listen()
|
||||
async def on_member_add(self, event: MemberAdd) -> None:
|
||||
"""Handle on_member_add event."""
|
||||
user = event.member
|
||||
guild = event.guild
|
||||
unverified = await Setting.find_one(q(guild=guild.id, setting="unverified"))
|
||||
if unverified:
|
||||
self.logger.debug(f"Applying unverified role to {user.id} in {guild.id}")
|
||||
role = await guild.fetch_role(unverified.value)
|
||||
if role not in user.roles:
|
||||
await user.add_role(role, reason="User just joined and is unverified")
|
||||
|
||||
@listen()
|
||||
async def on_member_remove(self, event: MemberRemove) -> None:
|
||||
"""Handle on_member_remove event."""
|
||||
user = event.member
|
||||
guild = event.guild
|
||||
log = await Setting.find_one(q(guild=guild.id, setting="activitylog"))
|
||||
if log:
|
||||
self.logger.debug(f"User {user.id} left {guild.id}")
|
||||
channel = await guild.fetch_channel(log.channel)
|
||||
embed = build_embed(
|
||||
title="Member Left",
|
||||
description=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(embeds=embed)
|
||||
|
||||
async def process_verify(self, before: Member, after: Member) -> Embed:
|
||||
"""Process user verification."""
|
||||
auditlog = await after.guild.fetch_audit_log(
|
||||
user_id=before.id, action_type=AuditLogEventType.MEMBER_ROLE_UPDATE
|
||||
)
|
||||
audit_event = get(auditlog.events, reason="Verification passed")
|
||||
if audit_event:
|
||||
admin_mention = "[N/A]"
|
||||
admin_text = "[N/A]"
|
||||
if admin := await after.guild.fet_member(audit_event.user_id):
|
||||
admin_mention = admin.mention
|
||||
admin_text = f"{admin.username}#{admin.discriminator}"
|
||||
fields = (
|
||||
EmbedField(name="Moderator", value=f"{admin_mention} ({admin_text})"),
|
||||
EmbedField(name="Reason", value=audit_event.reason),
|
||||
)
|
||||
embed = build_embed(
|
||||
title="User Verified",
|
||||
description=f"{after.mention} was verified",
|
||||
fields=fields,
|
||||
)
|
||||
embed.set_author(name=after.display_name, icon_url=after.display_avatar.url)
|
||||
embed.set_footer(text=f"{after.username}#{after.discriminator} | {after.id}")
|
||||
return embed
|
||||
|
||||
async def process_rolechange(self, before: Member, after: Member) -> Embed:
|
||||
"""Process role changes."""
|
||||
if before.roles == after.roles:
|
||||
return
|
||||
|
||||
new_roles = []
|
||||
removed_roles = []
|
||||
|
||||
for role in before.roles:
|
||||
if role not in after.roles:
|
||||
removed_roles.append(role)
|
||||
for role in after.roles:
|
||||
if role not in before.roles:
|
||||
new_roles.append(role)
|
||||
|
||||
new_text = "\n".join(role.mention for role in new_roles) or "None"
|
||||
removed_text = "\n".join(role.mention for role in removed_roles) or "None"
|
||||
|
||||
fields = (
|
||||
EmbedField(name="Added Roles", value=new_text),
|
||||
EmbedField(name="Removed Roles", value=removed_text),
|
||||
)
|
||||
embed = build_embed(
|
||||
title="User Roles Changed",
|
||||
description=f"{after.mention} had roles changed",
|
||||
fields=fields,
|
||||
)
|
||||
embed.set_author(name=after.display_name, icon_url=after.display_avatar.url)
|
||||
embed.set_footer(text=f"{after.username}#{after.discriminator} | {after.id}")
|
||||
return embed
|
||||
|
||||
async def process_rename(self, before: Member, after: Member) -> None:
|
||||
"""Process name change."""
|
||||
if (
|
||||
before.nickname == after.nickname
|
||||
and before.discriminator == after.discriminator
|
||||
and before.username == after.username
|
||||
):
|
||||
return
|
||||
|
||||
fields = (
|
||||
EmbedField(
|
||||
name="Before",
|
||||
value=f"{before.display_name} ({before.username}#{before.discriminator})",
|
||||
),
|
||||
EmbedField(
|
||||
name="After", value=f"{after.display_name} ({after.username}#{after.discriminator})"
|
||||
),
|
||||
)
|
||||
embed = build_embed(
|
||||
title="User Renamed",
|
||||
description=f"{after.mention} changed their name",
|
||||
fields=fields,
|
||||
color="#fc9e3f",
|
||||
)
|
||||
embed.set_author(name=after.display_name, icon_url=after.display_avatar.url)
|
||||
embed.set_footer(text=f"{after.username}#{after.discriminator} | {after.id}")
|
||||
return embed
|
||||
|
||||
@listen()
|
||||
async def on_member_update(self, event: MemberUpdate) -> None:
|
||||
"""Handle on_member_update event."""
|
||||
before = event.before
|
||||
after = event.after
|
||||
|
||||
if (before.display_name == after.display_name and before.roles == after.roles) or (
|
||||
not after or not before
|
||||
):
|
||||
return
|
||||
|
||||
log = await Setting.find_one(q(guild=before.guild.id, setting="activitylog"))
|
||||
if log:
|
||||
channel = await before.guild.fetch_channel(log.value)
|
||||
await asyncio.sleep(0.5) # Wait for audit log
|
||||
embed = None
|
||||
if before._role_ids != after._role_ids:
|
||||
verified = await Setting.find_one(q(guild=before.guild.id, setting="verified"))
|
||||
v_role = None
|
||||
if verified:
|
||||
v_role = await before.guild.fetch_role(verified.value)
|
||||
if not v_role:
|
||||
self.logger.debug(f"Guild {before.guild.id} verified role no longer exists")
|
||||
await verified.delete()
|
||||
else:
|
||||
if not before.has_role(v_role) and after.has_role(v_role):
|
||||
embed = await self.process_verify(before, after)
|
||||
embed = embed or await self.process_rolechange(before, after)
|
||||
embed = embed or await self.process_rename(before, after)
|
||||
if embed:
|
||||
await channel.send(embeds=embed)
|
||||
|
||||
# Message
|
||||
async def autopurge(self, message: Message) -> None:
|
||||
"""Handle autopurge events."""
|
||||
autopurge = await Autopurge.find_one(q(guild=message.guild.id, channel=message.channel.id))
|
||||
if autopurge:
|
||||
if not message.author.has_permission(Permissions.ADMINISTRATOR):
|
||||
self.logger.debug(
|
||||
f"Autopurging message {message.guild.id}/{message.channel.id}/{message.id}"
|
||||
)
|
||||
await message.delete(delay=autopurge.delay)
|
||||
|
||||
async def autoreact(self, message: Message) -> None:
|
||||
"""Handle autoreact events."""
|
||||
autoreact = await Autoreact.find_one(
|
||||
q(
|
||||
guild=message.guild.id,
|
||||
channel=message.channel.id,
|
||||
)
|
||||
)
|
||||
if autoreact:
|
||||
self.logger.debug(
|
||||
f"Autoreacting to message {message.guild.id}/{message.channel.id}/{message.id}"
|
||||
)
|
||||
for reaction in autoreact.reactions:
|
||||
await message.add_reaction(reaction)
|
||||
if autoreact.thread:
|
||||
name = message.content.replace("\n", " ")
|
||||
name = re.sub(r"<:\w+:(\d+)>", "", name)
|
||||
if len(name) > 100:
|
||||
name = name[:97] + "..."
|
||||
await message.create_thread(name=message.content, reason="Autoreact")
|
||||
|
||||
async def checks(self, message: Message) -> None:
|
||||
"""Other message checks."""
|
||||
# #tech
|
||||
# channel = find(lambda x: x.id == 599068193339736096, message._mention_ids)
|
||||
# if channel and message.author.id == 293795462752894976:
|
||||
# await channel.send(
|
||||
# content="https://cdn.discordapp.com/attachments/664621130044407838/805218508866453554/tech.gif" # noqa: E501
|
||||
# )
|
||||
content = re.sub(r"\s+", "", message.content)
|
||||
match = invites.search(content)
|
||||
setting = await Setting.find_one(q(guild=message.guild.id, setting="noinvite"))
|
||||
if not setting:
|
||||
setting = Setting(guild=message.guild.id, setting="noinvite", value=True)
|
||||
await setting.commit()
|
||||
if match:
|
||||
guild_invites = await message.guild.fetch_invites()
|
||||
if message.guild.vanity_url_code:
|
||||
guild_invites.append(message.guild.vanity_url_code)
|
||||
allowed = [x.code for x in guild_invites] + [
|
||||
"dbrand",
|
||||
"VtgZntXcnZ",
|
||||
"gPfYGbvTCE",
|
||||
]
|
||||
if (m := match.group(1)) not in allowed and setting.value:
|
||||
self.logger.debug(f"Removing non-allowed invite `{m}` from {message.guild.id}")
|
||||
try:
|
||||
await message.delete()
|
||||
except Exception:
|
||||
self.logger.debug("Message deleted before action taken")
|
||||
|
||||
expires_at = datetime.now(tz=timezone.utc) + timedelta(hours=24)
|
||||
await Warning(
|
||||
active=True,
|
||||
admin=self.user.id,
|
||||
duration=24,
|
||||
expires_at=expires_at,
|
||||
guild=message.guild.id,
|
||||
reason="Sent an invite link",
|
||||
user=message.author.id,
|
||||
).commit()
|
||||
tracker = warnings_tracker.labels(
|
||||
guild_id=message.guild.id, guild_name=message.guild.name
|
||||
)
|
||||
tracker.inc()
|
||||
embed = warning_embed(message.author, "Sent an invite link", admin=self.user)
|
||||
try:
|
||||
await message.channel.send(embeds=embed)
|
||||
except Exception:
|
||||
self.logger.warn("Failed to send warning embed")
|
||||
|
||||
async def massmention(self, message: Message) -> None:
|
||||
"""Handle massmention events."""
|
||||
massmention = await Setting.find_one(
|
||||
q(
|
||||
guild=message.guild.id,
|
||||
setting="massmention",
|
||||
)
|
||||
)
|
||||
|
||||
if (
|
||||
massmention
|
||||
and massmention.value > 0 # noqa: W503
|
||||
and len(message._mention_ids + message._mention_roles) # noqa: W503
|
||||
- (1 if message.author.id in message._mention_ids else 0) # noqa: W503
|
||||
> massmention.value # noqa: W503
|
||||
):
|
||||
self.logger.debug(
|
||||
f"Massmention threshold on {message.guild.id}/{message.channel.id}/{message.id}"
|
||||
)
|
||||
expires_at = datetime.now(tz=timezone.utc) + timedelta(hours=24)
|
||||
await Warning(
|
||||
active=True,
|
||||
admin=self.user.id,
|
||||
duration=24,
|
||||
expires_at=expires_at,
|
||||
guild=message.guild.id,
|
||||
reason="Mass Mention",
|
||||
user=message.author.id,
|
||||
).commit()
|
||||
tracker = warnings_tracker.labels(
|
||||
guild_id=message.guild.id, guild_name=message.guild.name
|
||||
)
|
||||
tracker.inc()
|
||||
embed = warning_embed(message.author, "Mass Mention", admin=self.user)
|
||||
try:
|
||||
await message.channel.send(embeds=embed)
|
||||
except Exception:
|
||||
self.logger.warn("Failed to send warning embed")
|
||||
|
||||
async def roleping(self, message: Message) -> None:
|
||||
"""Handle roleping events."""
|
||||
try:
|
||||
if message.author.has_permission(Permissions.MANAGE_GUILD):
|
||||
return
|
||||
except Exception as e:
|
||||
self.logger.error("Failed to get permissions, pretending check failed", exc_info=e)
|
||||
if await Roleping.collection.count_documents(q(guild=message.guild.id, active=True)) == 0:
|
||||
return
|
||||
rolepings = await Roleping.find(q(guild=message.guild.id, active=True)).to_list(None)
|
||||
|
||||
# Get all role IDs involved with message
|
||||
roles = [x.id async for x in message.mention_roles]
|
||||
async for mention in message.mention_users:
|
||||
roles += [x.id for x in mention.roles]
|
||||
|
||||
if not roles:
|
||||
return
|
||||
|
||||
# Get all roles that are rolepinged
|
||||
roleping_ids = [r.role for r in rolepings]
|
||||
|
||||
# Get roles in rolepings
|
||||
role_in_rolepings = find_all(lambda x: x in roleping_ids, roles)
|
||||
|
||||
# Check if the user has the role, so they are allowed to ping it
|
||||
user_missing_role = any(x.id not in roleping_ids for x in message.author.roles)
|
||||
|
||||
# Admins can ping whoever
|
||||
user_is_admin = message.author.has_permission(Permissions.ADMINISTRATOR)
|
||||
|
||||
# Check if user in a bypass list
|
||||
def check_has_role(roleping: Roleping) -> bool:
|
||||
return any(role.id in roleping.bypass["roles"] for role in message.author.roles)
|
||||
|
||||
user_has_bypass = False
|
||||
for roleping in rolepings:
|
||||
if message.author.id in roleping.bypass["users"]:
|
||||
user_has_bypass = True
|
||||
break
|
||||
if check_has_role(roleping):
|
||||
user_has_bypass = True
|
||||
break
|
||||
|
||||
if role_in_rolepings and user_missing_role and not user_is_admin and not user_has_bypass:
|
||||
self.logger.debug(
|
||||
f"Rolepinged role in {message.guild.id}/{message.channel.id}/{message.id}"
|
||||
)
|
||||
expires_at = datetime.now(tz=timezone.utc) + timedelta(hours=24)
|
||||
await Warning(
|
||||
active=True,
|
||||
admin=self.user.id,
|
||||
duration=24,
|
||||
expires_at=expires_at,
|
||||
guild=message.guild.id,
|
||||
reason="Pinged a blocked role/user with a blocked role",
|
||||
user=message.author.id,
|
||||
).commit()
|
||||
tracker = warnings_tracker.labels(
|
||||
guild_id=message.guild.id, guild_name=message.guild.name
|
||||
)
|
||||
tracker.inc()
|
||||
embed = warning_embed(
|
||||
message.author, "Pinged a blocked role/user with a blocked role", admin=self.user
|
||||
)
|
||||
try:
|
||||
await message.channel.send(embeds=embed)
|
||||
except Exception:
|
||||
self.logger.warn("Failed to send warning embed")
|
||||
|
||||
async def phishing(self, message: Message) -> None:
|
||||
"""Check if the message contains any known phishing domains."""
|
||||
for match in url.finditer(message.content):
|
||||
if (m := match.group("domain")) in self.phishing_domains:
|
||||
self.logger.debug(
|
||||
f"Phishing url `{m}` detected in {message.guild.id}/{message.channel.id}/{message.id}"
|
||||
)
|
||||
expires_at = datetime.now(tz=timezone.utc) + timedelta(hours=24)
|
||||
await Warning(
|
||||
active=True,
|
||||
admin=self.user.id,
|
||||
duration=24,
|
||||
expires_at=expires_at,
|
||||
guild=message.guild.id,
|
||||
reason="Phishing URL",
|
||||
user=message.author.id,
|
||||
).commit()
|
||||
tracker = warnings_tracker.labels(
|
||||
guild_id=message.guild.id, guild_name=message.guild.name
|
||||
)
|
||||
tracker.inc()
|
||||
embed = warning_embed(message.author, "Phishing URL", admin=self.user)
|
||||
try:
|
||||
await message.channel.send(embeds=embed)
|
||||
except Exception:
|
||||
self.logger.warn("Failed to send warning embed")
|
||||
try:
|
||||
await message.delete()
|
||||
except Exception:
|
||||
self.logger.warn("Failed to delete malicious message")
|
||||
tracker = malicious_tracker.labels(
|
||||
guild_id=message.guild.id, guild_name=message.guild.name
|
||||
)
|
||||
tracker.inc()
|
||||
return True
|
||||
return False
|
||||
|
||||
async def malicious_url(self, message: Message) -> None:
|
||||
"""Check if the message contains any known phishing domains."""
|
||||
for match in url.finditer(message.content):
|
||||
async with ClientSession() as session:
|
||||
resp = await session.get(
|
||||
"https://spoopy.oceanlord.me/api/check_website", json={"website": match.string}
|
||||
)
|
||||
if resp.status != 200:
|
||||
break
|
||||
data = await resp.json()
|
||||
for item in data["processed"]["urls"].values():
|
||||
if not item["safe"]:
|
||||
self.logger.debug(
|
||||
f"Scam url `{match.string}` detected in {message.guild.id}/{message.channel.id}/{message.id}"
|
||||
)
|
||||
expires_at = datetime.now(tz=timezone.utc) + timedelta(hours=24)
|
||||
await Warning(
|
||||
active=True,
|
||||
admin=self.user.id,
|
||||
duration=24,
|
||||
expires_at=expires_at,
|
||||
guild=message.guild.id,
|
||||
reason="Unsafe URL",
|
||||
user=message.author.id,
|
||||
).commit()
|
||||
tracker = warnings_tracker.labels(
|
||||
guild_id=message.guild.id, guild_name=message.guild.name
|
||||
)
|
||||
tracker.inc()
|
||||
reasons = ", ".join(item["not_safe_reasons"])
|
||||
embed = warning_embed(message.author, reasons, admin=self.user)
|
||||
try:
|
||||
await message.channel.send(embeds=embed)
|
||||
except Exception:
|
||||
self.logger.warn("Failed to send warning embed")
|
||||
try:
|
||||
await message.delete()
|
||||
except Exception:
|
||||
self.logger.warn("Failed to delete malicious message")
|
||||
tracker = malicious_tracker.labels(
|
||||
guild_id=message.guild.id, guild_name=message.guild.name
|
||||
)
|
||||
tracker.inc()
|
||||
return True
|
||||
return False
|
||||
|
||||
@listen()
|
||||
async def on_message(self, event: MessageCreate) -> None:
|
||||
"""Handle on_message event. Calls other event handlers."""
|
||||
message = event.message
|
||||
if not isinstance(message.channel, DMChannel) and not message.author.bot:
|
||||
await self.autoreact(message)
|
||||
await self.massmention(message)
|
||||
await self.roleping(message)
|
||||
await self.autopurge(message)
|
||||
await self.checks(message)
|
||||
if not await self.phishing(message):
|
||||
await self.malicious_url(message)
|
||||
|
||||
@listen()
|
||||
async def on_message_edit(self, event: MessageUpdate) -> None:
|
||||
"""Process on_message_edit events."""
|
||||
before = event.before
|
||||
after = event.after
|
||||
if not after.author.bot:
|
||||
modlog = await Setting.find_one(q(guild=after.guild.id, setting="activitylog"))
|
||||
ignore = await Setting.find_one(q(guild=after.guild.id, setting="log_ignore"))
|
||||
if modlog and (ignore and after.channel.id not in ignore.value):
|
||||
if not before or before.content == after.content or before.content is None:
|
||||
return
|
||||
try:
|
||||
channel = before.guild.get_channel(modlog.value)
|
||||
fields = [
|
||||
EmbedField(
|
||||
"Original Message",
|
||||
before.content if before.content else "N/A",
|
||||
False,
|
||||
),
|
||||
EmbedField(
|
||||
"New Message",
|
||||
after.content if after.content else "N/A",
|
||||
False,
|
||||
),
|
||||
]
|
||||
embed = build_embed(
|
||||
title="Message Edited",
|
||||
description=f"{after.author.mention} edited a message in {before.channel.mention}",
|
||||
fields=fields,
|
||||
color="#fc9e3f",
|
||||
timestamp=after.edited_timestamp,
|
||||
url=after.jump_url,
|
||||
)
|
||||
embed.set_author(
|
||||
name=after.author.username,
|
||||
icon_url=after.author.display_avatar.url,
|
||||
url=after.jump_url,
|
||||
)
|
||||
embed.set_footer(
|
||||
text=f"{after.author.username}#{after.author.discriminator} | {after.author.id}"
|
||||
)
|
||||
await channel.send(embeds=embed)
|
||||
except Exception as e:
|
||||
self.logger.warning(
|
||||
f"Failed to process edit {before.guild.id}/{before.channel.id}/{before.id}: {e}"
|
||||
)
|
||||
if not isinstance(after.channel, DMChannel) and not after.author.bot:
|
||||
await self.massmention(after)
|
||||
await self.roleping(after)
|
||||
await self.checks(after)
|
||||
await self.roleping(after)
|
||||
await self.checks(after)
|
||||
if not await self.phishing(after):
|
||||
await self.malicious_url(after)
|
||||
|
||||
@listen()
|
||||
async def on_message_delete(self, event: MessageDelete) -> None:
|
||||
"""Process on_message_delete events."""
|
||||
message = event.message
|
||||
modlog = await Setting.find_one(q(guild=message.guild.id, setting="activitylog"))
|
||||
ignore = await Setting.find_one(q(guild=message.guild.id, setting="log_ignore"))
|
||||
if modlog and (ignore and message.channel.id not in ignore.value):
|
||||
try:
|
||||
content = message.content or "N/A"
|
||||
except AttributeError:
|
||||
content = "N/A"
|
||||
fields = [EmbedField("Original Message", content, False)]
|
||||
|
||||
try:
|
||||
if message.attachments:
|
||||
value = "\n".join([f"[{x.filename}]({x.url})" for x in message.attachments])
|
||||
fields.append(
|
||||
EmbedField(
|
||||
name="Attachments",
|
||||
value=value,
|
||||
inline=False,
|
||||
)
|
||||
)
|
||||
|
||||
if message.sticker_items:
|
||||
value = "\n".join([f"Sticker: {x.name}" for x in message.sticker_items])
|
||||
fields.append(
|
||||
EmbedField(
|
||||
name="Stickers",
|
||||
value=value,
|
||||
inline=False,
|
||||
)
|
||||
)
|
||||
|
||||
if message.embeds:
|
||||
value = str(len(message.embeds)) + " embeds"
|
||||
fields.append(
|
||||
EmbedField(
|
||||
name="Embeds",
|
||||
value=value,
|
||||
inline=False,
|
||||
)
|
||||
)
|
||||
|
||||
channel = message.guild.get_channel(modlog.value)
|
||||
embed = build_embed(
|
||||
title="Message Deleted",
|
||||
description=f"{message.author.mention}'s message was deleted from {message.channel.mention}",
|
||||
fields=fields,
|
||||
color="#fc9e3f",
|
||||
)
|
||||
|
||||
embed.set_author(
|
||||
name=message.author.username,
|
||||
icon_url=message.author.display_avatar.url,
|
||||
url=message.jump_url,
|
||||
)
|
||||
embed.set_footer(
|
||||
text=(
|
||||
f"{message.author.username}#{message.author.discriminator} | "
|
||||
f"{message.author.id}"
|
||||
)
|
||||
)
|
||||
await channel.send(embeds=embed)
|
||||
except Exception as e:
|
||||
self.logger.warning(
|
||||
f"Failed to process edit {message.guild.id}/{message.channel.id}/{message.id}: {e}"
|
||||
)
|
||||
|
||||
async def _handle_modcase_button(self, event: Button) -> None:
|
||||
context = event.context
|
||||
|
||||
if not context.custom_id.startswith("modcase|"):
|
||||
return # Failsafe
|
||||
|
||||
if not context.deferred and not context.responded:
|
||||
await context.defer(ephemeral=True)
|
||||
|
||||
if not context.author.has_permission(Permissions.MODERATE_MEMBERS):
|
||||
return
|
||||
|
||||
user_key = f"msg|{context.message.id}"
|
||||
action_key = ""
|
||||
|
||||
if context.custom_id == "modcase|yes":
|
||||
if user_id := await self.redis.get(user_key):
|
||||
action_key = f"{user_id}|{context.guild.id}"
|
||||
if (user := await context.guild.fetch_member(user_id)) and (
|
||||
action_data := await self.redis.get(action_key)
|
||||
):
|
||||
name, parent = action_data.split("|")[:2]
|
||||
action = Action(action_type=name, parent=parent)
|
||||
note = Note(
|
||||
admin=context.author.id, content="Moderation case opened via message"
|
||||
)
|
||||
modlog = await Modlog.find_one(
|
||||
q(user=user.id, guild=context.guild.id, open=True)
|
||||
)
|
||||
if modlog:
|
||||
self.logger.debug("User already has active case in guild")
|
||||
await context.send(
|
||||
f"User already has open case: {modlog.nanoid}", ephemeral=True
|
||||
)
|
||||
else:
|
||||
modlog = Modlog(
|
||||
user=user.id,
|
||||
admin=context.author.id,
|
||||
guild=context.guild.id,
|
||||
actions=[action],
|
||||
notes=[note],
|
||||
)
|
||||
await modlog.commit()
|
||||
|
||||
fields = (
|
||||
EmbedField(name="Admin", value=context.author.mention),
|
||||
EmbedField(name="Opening Action", value=f"{name} {parent}"),
|
||||
)
|
||||
embed = build_embed(
|
||||
title="Moderation Case Opened",
|
||||
description=f"Moderation case opened against {user.mention}",
|
||||
fields=fields,
|
||||
)
|
||||
embed.set_author(
|
||||
name=user.username + "#" + user.discriminator,
|
||||
icon_url=user.display_avatar.url,
|
||||
)
|
||||
|
||||
await context.message.edit(embeds=embed)
|
||||
elif not user:
|
||||
self.logger.debug("User no longer in guild")
|
||||
await context.send("User no longer in guild", ephemeral=True)
|
||||
else:
|
||||
self.logger.warn("Unable to get action data ( %s )", action_key)
|
||||
await context.send("Unable to get action data", ephemeral=True)
|
||||
|
||||
for row in context.message.components:
|
||||
for component in row.components:
|
||||
component.disabled = True
|
||||
await context.message.edit(components=context.message.components)
|
||||
msg = "Cancelled" if context.custom_id == "modcase|no" else "Moderation case opened"
|
||||
await context.send(msg)
|
||||
await self.redis.delete(user_key)
|
||||
await self.redis.delete(action_key)
|
||||
|
||||
async def _handle_delete_button(self, event: Button) -> None:
|
||||
context = event.context
|
||||
|
||||
if not context.custom_id.startswith("delete|"):
|
||||
return # Failsafe
|
||||
|
||||
if not context.deferred and not context.responded:
|
||||
await context.defer(ephemeral=True)
|
||||
|
||||
uid = context.custom_id.split("|")[1]
|
||||
|
||||
if (
|
||||
not context.author.has_permission(Permissions.MANAGE_MESSAGES)
|
||||
and not context.author.has_permission(Permissions.ADMINISTRATOR)
|
||||
and not str(context.author) == uid
|
||||
):
|
||||
await context.send("I'm afraid I can't let you do that", ephemeral=True)
|
||||
return # User does not have perms to delete
|
||||
|
||||
if star := await Star.find_one(q(star=context.message.id, guild=context.guild.id)):
|
||||
await star.delete()
|
||||
|
||||
await context.message.delete()
|
||||
await context.send("Message deleted", ephemeral=True)
|
||||
|
||||
async def _handle_copy_button(self, event: Button) -> None:
|
||||
context = event.context
|
||||
|
||||
if not context.custom_id.startswith("copy|"):
|
||||
return
|
||||
|
||||
if not context.deferred and not context.responded:
|
||||
await context.defer(ephemeral=True)
|
||||
|
||||
what, rid = context.custom_id.split("|")[1:]
|
||||
if what == "rme":
|
||||
reminder = await Reminder.find_one(q(_id=rid))
|
||||
if reminder:
|
||||
new_reminder = Reminder(
|
||||
user=context.author.id,
|
||||
channel=context.channel.id,
|
||||
guild=context.guild.id,
|
||||
message=reminder.message,
|
||||
remind_at=reminder.remind_at,
|
||||
private=reminder.private,
|
||||
active=reminder.active,
|
||||
)
|
||||
await new_reminder.commit()
|
||||
|
||||
await context.send("Reminder copied!", ephemeral=True)
|
||||
|
||||
@listen()
|
||||
async def on_button(self, event: Button) -> None:
|
||||
"""Process button events."""
|
||||
await self._handle_modcase_button(event)
|
||||
await self._handle_delete_button(event)
|
||||
await self._handle_copy_button(event)
|
38
jarvis/client/__init__.py
Normal file
38
jarvis/client/__init__.py
Normal file
|
@ -0,0 +1,38 @@
|
|||
"""Custom JARVIS client."""
|
||||
import logging
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from jarvis_core.util.ansi import Fore, Format, fmt
|
||||
from naff.models.naff.context import Context, InteractionContext, PrefixedContext
|
||||
from nafftrack.client import StatsClient
|
||||
|
||||
from jarvis.client.errors import ErrorMixin
|
||||
from jarvis.client.events import EventMixin
|
||||
from jarvis.client.tasks import TaskMixin
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from aioredis import Redis
|
||||
|
||||
KEY_FMT = fmt(Fore.GRAY)
|
||||
VAL_FMT = fmt(Fore.WHITE)
|
||||
CMD_FMT = fmt(Fore.GREEN, Format.BOLD)
|
||||
|
||||
|
||||
class Jarvis(StatsClient, ErrorMixin, EventMixin, TaskMixin):
|
||||
def __init__(self, redis: "Redis", *args, **kwargs): # noqa: ANN002 ANN003
|
||||
super().__init__(*args, **kwargs)
|
||||
self.redis = redis
|
||||
self.logger = logging.getLogger(__name__)
|
||||
self.phishing_domains = []
|
||||
self.pre_run_callback = self._prerun
|
||||
self.synced = False
|
||||
|
||||
async def _prerun(self, ctx: Context, *args, **kwargs) -> None:
|
||||
name = ctx.invoke_target
|
||||
cargs = ""
|
||||
if isinstance(ctx, InteractionContext) and ctx.target_id:
|
||||
kwargs["context target"] = ctx.target
|
||||
cargs = " ".join(f"{k}:{v}" for k, v in kwargs.items())
|
||||
elif isinstance(ctx, PrefixedContext):
|
||||
cargs = " ".join(args)
|
||||
self.logger.debug(f"Running command `{name}` with args: {cargs or 'None'}")
|
107
jarvis/client/errors.py
Normal file
107
jarvis/client/errors.py
Normal file
|
@ -0,0 +1,107 @@
|
|||
"""JARVIS error handling mixin."""
|
||||
import traceback
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from naff.client.errors import CommandCheckFailure, CommandOnCooldown, HTTPException
|
||||
from naff.models.naff.context import Context, InteractionContext, PrefixedContext
|
||||
from pastypy import AsyncPaste as Paste
|
||||
|
||||
DEFAULT_GUILD = 862402786116763668
|
||||
DEFAULT_ERROR_CHANNEL = 943395824560394250
|
||||
DEFAULT_SITE = "https://paste.zevs.me"
|
||||
|
||||
ERROR_MSG = """
|
||||
Command Information:
|
||||
Guild: {guild_name}
|
||||
Name: {invoked_name}
|
||||
Args:
|
||||
{arg_str}
|
||||
|
||||
Callback:
|
||||
Args:
|
||||
{callback_args}
|
||||
Kwargs:
|
||||
{callback_kwargs}
|
||||
"""
|
||||
|
||||
|
||||
class ErrorMixin:
|
||||
async def on_error(self, source: str, error: Exception, *args, **kwargs) -> None:
|
||||
"""NAFF on_error override."""
|
||||
if isinstance(error, HTTPException):
|
||||
errors = error.search_for_message(error.errors)
|
||||
out = f"HTTPException: {error.status}|{error.response.reason}: " + "\n".join(errors)
|
||||
self.logger.error(out, exc_info=error)
|
||||
else:
|
||||
self.logger.error(f"Ignoring exception in {source}", exc_info=error)
|
||||
|
||||
async def on_command_error(
|
||||
self, ctx: Context, error: Exception, *args: list, **kwargs: dict
|
||||
) -> None:
|
||||
"""NAFF on_command_error override."""
|
||||
name = ctx.invoke_target
|
||||
self.logger.debug(f"Handling error in {name}: {error}")
|
||||
if isinstance(error, CommandOnCooldown):
|
||||
await ctx.send(str(error), ephemeral=True)
|
||||
return
|
||||
elif isinstance(error, CommandCheckFailure):
|
||||
await ctx.send("I'm afraid I can't let you do that", ephemeral=True)
|
||||
return
|
||||
guild = await self.fetch_guild(DEFAULT_GUILD)
|
||||
channel = await guild.fetch_channel(DEFAULT_ERROR_CHANNEL)
|
||||
error_time = datetime.now(tz=timezone.utc).strftime("%d-%m-%Y %H:%M-%S.%f UTC")
|
||||
timestamp = int(datetime.now(tz=timezone.utc).timestamp())
|
||||
timestamp = f"<t:{timestamp}:T>"
|
||||
arg_str = ""
|
||||
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, PrefixedContext):
|
||||
for v in ctx.args:
|
||||
if isinstance(v, str) and len(v) > 100:
|
||||
v = v[97] + "..."
|
||||
arg_str += f" - {v}"
|
||||
callback_args = "\n".join(f" - {i}" for i in args) if args else " None"
|
||||
callback_kwargs = (
|
||||
"\n".join(f" {k}: {v}" for k, v in kwargs.items()) if kwargs else " None"
|
||||
)
|
||||
full_message = ERROR_MSG.format(
|
||||
guild_name=ctx.guild.name,
|
||||
error_time=error_time,
|
||||
invoked_name=name,
|
||||
arg_str=arg_str,
|
||||
callback_args=callback_args,
|
||||
callback_kwargs=callback_kwargs,
|
||||
)
|
||||
tb = traceback.format_exception(error)
|
||||
if isinstance(error, HTTPException):
|
||||
errors = error.search_for_message(error.errors)
|
||||
tb[-1] = f"HTTPException: {error.status}|{error.response.reason}: " + "\n".join(errors)
|
||||
error_message = "".join(traceback.format_exception(error))
|
||||
if len(full_message + error_message) >= 1800:
|
||||
error_message = "\n ".join(error_message.split("\n"))
|
||||
full_message += "Exception: |\n " + error_message
|
||||
paste = Paste(content=full_message, site=DEFAULT_SITE)
|
||||
key = await paste.save()
|
||||
self.logger.debug(f"Large traceback, saved to Pasty {paste.id}, {key=}")
|
||||
|
||||
await channel.send(
|
||||
f"JARVIS encountered an error at {timestamp}. Log too big to send over Discord."
|
||||
f"\nPlease see log at {paste.url}"
|
||||
)
|
||||
else:
|
||||
await channel.send(
|
||||
f"JARVIS encountered an error at {timestamp}:"
|
||||
f"\n```yaml\n{full_message}\n```"
|
||||
f"\nException:\n```py\n{error_message}\n```"
|
||||
)
|
||||
await ctx.send("Whoops! Encountered an error. The error has been logged.", ephemeral=True)
|
||||
try:
|
||||
return await super().on_command_error(ctx, error, *args, **kwargs)
|
||||
except Exception as e:
|
||||
self.logger.error("Uncaught exception", exc_info=e)
|
144
jarvis/client/events/__init__.py
Normal file
144
jarvis/client/events/__init__.py
Normal file
|
@ -0,0 +1,144 @@
|
|||
"""JARVIS event mixin."""
|
||||
import asyncio
|
||||
|
||||
from aiohttp import ClientSession
|
||||
from jarvis_core.db import q
|
||||
from jarvis_core.db.models import Reminder, Setting
|
||||
from jarvis_core.util.ansi import RESET, Fore, Format, fmt
|
||||
from naff import listen
|
||||
from naff.models.discord.channel import DMChannel
|
||||
from naff.models.discord.embed import EmbedField
|
||||
from naff.models.naff.application_commands import ContextMenu
|
||||
from naff.models.naff.context import Context, InteractionContext, PrefixedContext
|
||||
|
||||
from jarvis import const
|
||||
from jarvis.client.events.components import ComponentEventMixin
|
||||
from jarvis.client.events.member import MemberEventMixin
|
||||
from jarvis.client.events.message import MessageEventMixin
|
||||
from jarvis.tracking import jarvis_info
|
||||
from jarvis.utils import build_embed
|
||||
|
||||
KEY_FMT = fmt(Fore.GRAY)
|
||||
VAL_FMT = fmt(Fore.WHITE)
|
||||
CMD_FMT = fmt(Fore.GREEN, Format.BOLD)
|
||||
|
||||
|
||||
class EventMixin(MemberEventMixin, MessageEventMixin, ComponentEventMixin):
|
||||
async def _chunk_all(self) -> None:
|
||||
"""Chunk all guilds."""
|
||||
for guild in self.guilds:
|
||||
self.logger.debug(f"Chunking guild {guild.name} <{guild.id}>")
|
||||
await guild.chunk_guild()
|
||||
|
||||
async def _sync_domains(self) -> None:
|
||||
self.logger.debug("Loading phishing domains")
|
||||
async with ClientSession(headers={"X-Identity": "Discord: zevaryx#5779"}) as session:
|
||||
response = await session.get("https://phish.sinking.yachts/v2/all")
|
||||
response.raise_for_status()
|
||||
self.phishing_domains = await response.json()
|
||||
self.logger.info(f"Protected from {len(self.phishing_domains)} phishing domains")
|
||||
|
||||
@listen()
|
||||
async def on_startup(self) -> None:
|
||||
"""NAFF on_startup override. Prometheus info generated here."""
|
||||
jarvis_info.info({"version": const.__version__})
|
||||
try:
|
||||
if not self.synced:
|
||||
await self._sync_domains()
|
||||
self._update_domains.start()
|
||||
asyncio.create_task(self._chunk_all())
|
||||
self.synced = True
|
||||
except Exception as e:
|
||||
self.logger.error("Failed to load anti-phishing", exc_info=e)
|
||||
self.logger.info("Logged in as {}".format(self.user)) # noqa: T001
|
||||
self.logger.info("Connected to {} guild(s)".format(len(self.guilds))) # noqa: T001
|
||||
self.logger.info("Current version: {}".format(const.__version__))
|
||||
self.logger.info( # noqa: T001
|
||||
"https://discord.com/api/oauth2/authorize?client_id="
|
||||
"{}&permissions=8&scope=bot%20applications.commands".format(self.user.id)
|
||||
)
|
||||
|
||||
global_base_commands = 0
|
||||
guild_base_commands = 0
|
||||
global_context_menus = 0
|
||||
guild_context_menus = 0
|
||||
for cid in self.interaction_tree:
|
||||
if cid == 0:
|
||||
global_base_commands = sum(
|
||||
1
|
||||
for _ in self.interaction_tree[cid]
|
||||
if not isinstance(self.interaction_tree[cid][_], ContextMenu)
|
||||
)
|
||||
global_context_menus = sum(
|
||||
1
|
||||
for _ in self.interaction_tree[cid]
|
||||
if isinstance(self.interaction_tree[cid][_], ContextMenu)
|
||||
)
|
||||
else:
|
||||
guild_base_commands += sum(
|
||||
1
|
||||
for _ in self.interaction_tree[cid]
|
||||
if not isinstance(self.interaction_tree[cid][_], ContextMenu)
|
||||
)
|
||||
guild_context_menus += sum(
|
||||
1
|
||||
for _ in self.interaction_tree[cid]
|
||||
if isinstance(self.interaction_tree[cid][_], ContextMenu)
|
||||
)
|
||||
|
||||
self.logger.info("Loaded {:>3} global base slash commands".format(global_base_commands))
|
||||
self.logger.info("Loaded {:>3} global context menus".format(global_context_menus))
|
||||
self.logger.info("Loaded {:>3} guild base slash commands".format(guild_base_commands))
|
||||
self.logger.info("Loaded {:>3} guild context menus".format(guild_context_menus))
|
||||
self.logger.debug("Hitting Reminders for faster loads")
|
||||
_ = await Reminder.find().to_list(None)
|
||||
|
||||
# Modlog
|
||||
async def on_command(self, ctx: Context) -> None:
|
||||
"""NAFF on_command override."""
|
||||
name = ctx.invoke_target
|
||||
if not isinstance(ctx.channel, DMChannel) and name not in ["pw"]:
|
||||
modlog = await Setting.find_one(q(guild=ctx.guild.id, setting="activitylog"))
|
||||
ignore = await Setting.find_one(q(guild=ctx.guild.id, setting="log_ignore"))
|
||||
if modlog and (ignore and ctx.channel.id not in ignore.value):
|
||||
channel = await ctx.guild.fetch_channel(modlog.value)
|
||||
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, PrefixedContext):
|
||||
for v in ctx.args:
|
||||
if isinstance(v, str) and len(v) > 100:
|
||||
v = v[97] + "..."
|
||||
args.append(f"{VAL_FMT}{v}{RESET}")
|
||||
args = " ".join(args)
|
||||
fields = [
|
||||
EmbedField(
|
||||
name="Command",
|
||||
value=f"```ansi\n{CMD_FMT}{ctx.invoke_target}{RESET} {args}\n```",
|
||||
inline=False,
|
||||
),
|
||||
]
|
||||
embed = build_embed(
|
||||
title="Command Invoked",
|
||||
description=f"{ctx.author.mention} invoked a command in {ctx.channel.mention}",
|
||||
fields=fields,
|
||||
color="#fc9e3f",
|
||||
)
|
||||
embed.set_author(name=ctx.author.username, icon_url=ctx.author.display_avatar.url)
|
||||
embed.set_footer(
|
||||
text=f"{ctx.author.user.username}#{ctx.author.discriminator} | {ctx.author.id}"
|
||||
)
|
||||
if channel:
|
||||
await channel.send(embeds=embed)
|
||||
else:
|
||||
self.logger.warning(
|
||||
f"Activitylog channel no longer exists in {ctx.guild.name}, removing"
|
||||
)
|
||||
await modlog.delete()
|
144
jarvis/client/events/components.py
Normal file
144
jarvis/client/events/components.py
Normal file
|
@ -0,0 +1,144 @@
|
|||
"""JARVIS component event mixin."""
|
||||
from jarvis_core.db import q
|
||||
from jarvis_core.db.models import Action, Modlog, Note, Reminder, Star
|
||||
from naff import listen
|
||||
from naff.api.events.internal import Button
|
||||
from naff.models.discord.embed import EmbedField
|
||||
from naff.models.discord.enums import Permissions
|
||||
|
||||
from jarvis.utils import build_embed
|
||||
|
||||
|
||||
class ComponentEventMixin:
|
||||
async def _handle_modcase_button(self, event: Button) -> None:
|
||||
context = event.context
|
||||
|
||||
if not context.custom_id.startswith("modcase|"):
|
||||
return # Failsafe
|
||||
|
||||
if not context.deferred and not context.responded:
|
||||
await context.defer(ephemeral=True)
|
||||
|
||||
if not context.author.has_permission(Permissions.MODERATE_MEMBERS):
|
||||
return
|
||||
|
||||
user_key = f"msg|{context.message.id}"
|
||||
action_key = ""
|
||||
|
||||
if context.custom_id == "modcase|yes":
|
||||
if user_id := await self.redis.get(user_key):
|
||||
action_key = f"{user_id}|{context.guild.id}"
|
||||
if (user := await context.guild.fetch_member(user_id)) and (
|
||||
action_data := await self.redis.get(action_key)
|
||||
):
|
||||
name, parent = action_data.split("|")[:2]
|
||||
action = Action(action_type=name, parent=parent)
|
||||
note = Note(
|
||||
admin=context.author.id, content="Moderation case opened via message"
|
||||
)
|
||||
modlog = await Modlog.find_one(
|
||||
q(user=user.id, guild=context.guild.id, open=True)
|
||||
)
|
||||
if modlog:
|
||||
self.logger.debug("User already has active case in guild")
|
||||
await context.send(
|
||||
f"User already has open case: {modlog.nanoid}", ephemeral=True
|
||||
)
|
||||
else:
|
||||
modlog = Modlog(
|
||||
user=user.id,
|
||||
admin=context.author.id,
|
||||
guild=context.guild.id,
|
||||
actions=[action],
|
||||
notes=[note],
|
||||
)
|
||||
await modlog.commit()
|
||||
|
||||
fields = (
|
||||
EmbedField(name="Admin", value=context.author.mention),
|
||||
EmbedField(name="Opening Action", value=f"{name} {parent}"),
|
||||
)
|
||||
embed = build_embed(
|
||||
title="Moderation Case Opened",
|
||||
description=f"Moderation case opened against {user.mention}",
|
||||
fields=fields,
|
||||
)
|
||||
embed.set_author(
|
||||
name=user.username + "#" + user.discriminator,
|
||||
icon_url=user.display_avatar.url,
|
||||
)
|
||||
|
||||
await context.message.edit(embeds=embed)
|
||||
elif not user:
|
||||
self.logger.debug("User no longer in guild")
|
||||
await context.send("User no longer in guild", ephemeral=True)
|
||||
else:
|
||||
self.logger.warn("Unable to get action data ( %s )", action_key)
|
||||
await context.send("Unable to get action data", ephemeral=True)
|
||||
|
||||
for row in context.message.components:
|
||||
for component in row.components:
|
||||
component.disabled = True
|
||||
await context.message.edit(components=context.message.components)
|
||||
msg = "Cancelled" if context.custom_id == "modcase|no" else "Moderation case opened"
|
||||
await context.send(msg)
|
||||
await self.redis.delete(user_key)
|
||||
await self.redis.delete(action_key)
|
||||
|
||||
async def _handle_delete_button(self, event: Button) -> None:
|
||||
context = event.context
|
||||
|
||||
if not context.custom_id.startswith("delete|"):
|
||||
return # Failsafe
|
||||
|
||||
if not context.deferred and not context.responded:
|
||||
await context.defer(ephemeral=True)
|
||||
|
||||
uid = context.custom_id.split("|")[1]
|
||||
|
||||
if (
|
||||
not context.author.has_permission(Permissions.MANAGE_MESSAGES)
|
||||
and not context.author.has_permission(Permissions.ADMINISTRATOR)
|
||||
and not str(context.author.id) == uid
|
||||
):
|
||||
await context.send("I'm afraid I can't let you do that", ephemeral=True)
|
||||
return # User does not have perms to delete
|
||||
|
||||
if star := await Star.find_one(q(star=context.message.id, guild=context.guild.id)):
|
||||
await star.delete()
|
||||
|
||||
await context.message.delete()
|
||||
await context.send("Message deleted", ephemeral=True)
|
||||
|
||||
async def _handle_copy_button(self, event: Button) -> None:
|
||||
context = event.context
|
||||
|
||||
if not context.custom_id.startswith("copy|"):
|
||||
return
|
||||
|
||||
if not context.deferred and not context.responded:
|
||||
await context.defer(ephemeral=True)
|
||||
|
||||
what, rid = context.custom_id.split("|")[1:]
|
||||
if what == "rme":
|
||||
reminder = await Reminder.find_one(q(_id=rid))
|
||||
if reminder:
|
||||
new_reminder = Reminder(
|
||||
user=context.author.id,
|
||||
channel=context.channel.id,
|
||||
guild=context.guild.id,
|
||||
message=reminder.message,
|
||||
remind_at=reminder.remind_at,
|
||||
private=reminder.private,
|
||||
active=reminder.active,
|
||||
)
|
||||
await new_reminder.commit()
|
||||
|
||||
await context.send("Reminder copied!", ephemeral=True)
|
||||
|
||||
@listen()
|
||||
async def on_button(self, event: Button) -> None:
|
||||
"""Process button events."""
|
||||
await self._handle_modcase_button(event)
|
||||
await self._handle_delete_button(event)
|
||||
await self._handle_copy_button(event)
|
163
jarvis/client/events/member.py
Normal file
163
jarvis/client/events/member.py
Normal file
|
@ -0,0 +1,163 @@
|
|||
"""JARVIS member event mixin."""
|
||||
import asyncio
|
||||
|
||||
from jarvis_core.db import q
|
||||
from jarvis_core.db.models import Setting
|
||||
from naff import listen
|
||||
from naff.api.events.discord import MemberAdd, MemberRemove, MemberUpdate
|
||||
from naff.client.utils.misc_utils import get
|
||||
from naff.models.discord.embed import Embed, EmbedField
|
||||
from naff.models.discord.enums import AuditLogEventType
|
||||
from naff.models.discord.user import Member
|
||||
|
||||
from jarvis.utils import build_embed
|
||||
|
||||
|
||||
class MemberEventMixin:
|
||||
# Events
|
||||
# Member
|
||||
@listen()
|
||||
async def on_member_add(self, event: MemberAdd) -> None:
|
||||
"""Handle on_member_add event."""
|
||||
user = event.member
|
||||
guild = event.guild
|
||||
unverified = await Setting.find_one(q(guild=guild.id, setting="unverified"))
|
||||
if unverified:
|
||||
self.logger.debug(f"Applying unverified role to {user.id} in {guild.id}")
|
||||
role = await guild.fetch_role(unverified.value)
|
||||
if role not in user.roles:
|
||||
await user.add_role(role, reason="User just joined and is unverified")
|
||||
|
||||
@listen()
|
||||
async def on_member_remove(self, event: MemberRemove) -> None:
|
||||
"""Handle on_member_remove event."""
|
||||
user = event.member
|
||||
guild = event.guild
|
||||
log = await Setting.find_one(q(guild=guild.id, setting="activitylog"))
|
||||
if log:
|
||||
self.logger.debug(f"User {user.id} left {guild.id}")
|
||||
channel = await guild.fetch_channel(log.value)
|
||||
embed = build_embed(
|
||||
title="Member Left",
|
||||
description=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(embeds=embed)
|
||||
|
||||
async def process_verify(self, before: Member, after: Member) -> Embed:
|
||||
"""Process user verification."""
|
||||
auditlog = await after.guild.fetch_audit_log(
|
||||
user_id=before.id, action_type=AuditLogEventType.MEMBER_ROLE_UPDATE
|
||||
)
|
||||
audit_event = get(auditlog.events, reason="Verification passed")
|
||||
if audit_event:
|
||||
admin_mention = "[N/A]"
|
||||
admin_text = "[N/A]"
|
||||
if admin := await after.guild.fet_member(audit_event.user_id):
|
||||
admin_mention = admin.mention
|
||||
admin_text = f"{admin.username}#{admin.discriminator}"
|
||||
fields = (
|
||||
EmbedField(name="Moderator", value=f"{admin_mention} ({admin_text})"),
|
||||
EmbedField(name="Reason", value=audit_event.reason),
|
||||
)
|
||||
embed = build_embed(
|
||||
title="User Verified",
|
||||
description=f"{after.mention} was verified",
|
||||
fields=fields,
|
||||
)
|
||||
embed.set_author(name=after.display_name, icon_url=after.display_avatar.url)
|
||||
embed.set_footer(text=f"{after.username}#{after.discriminator} | {after.id}")
|
||||
return embed
|
||||
|
||||
async def process_rolechange(self, before: Member, after: Member) -> Embed:
|
||||
"""Process role changes."""
|
||||
if before.roles == after.roles:
|
||||
return
|
||||
|
||||
new_roles = []
|
||||
removed_roles = []
|
||||
|
||||
for role in before.roles:
|
||||
if role not in after.roles:
|
||||
removed_roles.append(role)
|
||||
for role in after.roles:
|
||||
if role not in before.roles:
|
||||
new_roles.append(role)
|
||||
|
||||
new_text = "\n".join(role.mention for role in new_roles) or "None"
|
||||
removed_text = "\n".join(role.mention for role in removed_roles) or "None"
|
||||
|
||||
fields = (
|
||||
EmbedField(name="Added Roles", value=new_text),
|
||||
EmbedField(name="Removed Roles", value=removed_text),
|
||||
)
|
||||
embed = build_embed(
|
||||
title="User Roles Changed",
|
||||
description=f"{after.mention} had roles changed",
|
||||
fields=fields,
|
||||
)
|
||||
embed.set_author(name=after.display_name, icon_url=after.display_avatar.url)
|
||||
embed.set_footer(text=f"{after.username}#{after.discriminator} | {after.id}")
|
||||
return embed
|
||||
|
||||
async def process_rename(self, before: Member, after: Member) -> None:
|
||||
"""Process name change."""
|
||||
if (
|
||||
before.nickname == after.nickname
|
||||
and before.discriminator == after.discriminator
|
||||
and before.username == after.username
|
||||
):
|
||||
return
|
||||
|
||||
fields = (
|
||||
EmbedField(
|
||||
name="Before",
|
||||
value=f"{before.display_name} ({before.username}#{before.discriminator})",
|
||||
),
|
||||
EmbedField(
|
||||
name="After", value=f"{after.display_name} ({after.username}#{after.discriminator})"
|
||||
),
|
||||
)
|
||||
embed = build_embed(
|
||||
title="User Renamed",
|
||||
description=f"{after.mention} changed their name",
|
||||
fields=fields,
|
||||
color="#fc9e3f",
|
||||
)
|
||||
embed.set_author(name=after.display_name, icon_url=after.display_avatar.url)
|
||||
embed.set_footer(text=f"{after.username}#{after.discriminator} | {after.id}")
|
||||
return embed
|
||||
|
||||
@listen()
|
||||
async def on_member_update(self, event: MemberUpdate) -> None:
|
||||
"""Handle on_member_update event."""
|
||||
before = event.before
|
||||
after = event.after
|
||||
|
||||
if (before.display_name == after.display_name and before.roles == after.roles) or (
|
||||
not after or not before
|
||||
):
|
||||
return
|
||||
|
||||
log = await Setting.find_one(q(guild=before.guild.id, setting="activitylog"))
|
||||
if log:
|
||||
channel = await before.guild.fetch_channel(log.value)
|
||||
await asyncio.sleep(0.5) # Wait for audit log
|
||||
embed = None
|
||||
if before._role_ids != after._role_ids:
|
||||
verified = await Setting.find_one(q(guild=before.guild.id, setting="verified"))
|
||||
v_role = None
|
||||
if verified:
|
||||
v_role = await before.guild.fetch_role(verified.value)
|
||||
if not v_role:
|
||||
self.logger.debug(f"Guild {before.guild.id} verified role no longer exists")
|
||||
await verified.delete()
|
||||
else:
|
||||
if not before.has_role(v_role) and after.has_role(v_role):
|
||||
embed = await self.process_verify(before, after)
|
||||
embed = embed or await self.process_rolechange(before, after)
|
||||
embed = embed or await self.process_rename(before, after)
|
||||
if embed:
|
||||
await channel.send(embeds=embed)
|
471
jarvis/client/events/message.py
Normal file
471
jarvis/client/events/message.py
Normal file
|
@ -0,0 +1,471 @@
|
|||
"""JARVIS message event mixin"""
|
||||
import re
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
from aiohttp import ClientSession
|
||||
from jarvis_core.db import q
|
||||
from jarvis_core.db.models import Autopurge, Autoreact, Mute, Roleping, Setting, Warning
|
||||
from jarvis_core.filters import invites, url
|
||||
from naff import listen
|
||||
from naff.api.events.discord import MessageCreate, MessageDelete, MessageUpdate
|
||||
from naff.client.utils.misc_utils import find_all
|
||||
from naff.models.discord.channel import DMChannel, GuildText
|
||||
from naff.models.discord.embed import EmbedField
|
||||
from naff.models.discord.enums import Permissions
|
||||
from naff.models.discord.message import Message
|
||||
from naff.models.discord.user import Member
|
||||
|
||||
from jarvis.branding import get_command_color
|
||||
from jarvis.tracking import malicious_tracker, warnings_tracker
|
||||
from jarvis.utils import build_embed
|
||||
from jarvis.utils.embeds import warning_embed
|
||||
|
||||
|
||||
class MessageEventMixin:
|
||||
# Message
|
||||
async def autopurge(self, message: Message) -> None:
|
||||
"""Handle autopurge events."""
|
||||
autopurge = await Autopurge.find_one(q(guild=message.guild.id, channel=message.channel.id))
|
||||
if autopurge:
|
||||
if not message.author.has_permission(Permissions.ADMINISTRATOR):
|
||||
self.logger.debug(
|
||||
f"Autopurging message {message.guild.id}/{message.channel.id}/{message.id}"
|
||||
)
|
||||
await message.delete(delay=autopurge.delay)
|
||||
|
||||
async def autoreact(self, message: Message) -> None:
|
||||
"""Handle autoreact events."""
|
||||
autoreact = await Autoreact.find_one(
|
||||
q(
|
||||
guild=message.guild.id,
|
||||
channel=message.channel.id,
|
||||
)
|
||||
)
|
||||
if autoreact:
|
||||
self.logger.debug(
|
||||
f"Autoreacting to message {message.guild.id}/{message.channel.id}/{message.id}"
|
||||
)
|
||||
for reaction in autoreact.reactions:
|
||||
await message.add_reaction(reaction)
|
||||
if autoreact.thread:
|
||||
name = message.content.replace("\n", " ")
|
||||
name = re.sub(r"<:\w+:(\d+)>", "", name)
|
||||
if len(name) > 100:
|
||||
name = name[:97] + "..."
|
||||
await message.create_thread(name=message.content, reason="Autoreact")
|
||||
|
||||
async def checks(self, message: Message) -> None:
|
||||
"""Other message checks."""
|
||||
# #tech
|
||||
# channel = find(lambda x: x.id == 599068193339736096, message._mention_ids)
|
||||
# if channel and message.author.id == 293795462752894976:
|
||||
# await channel.send(
|
||||
# content="https://cdn.discordapp.com/attachments/664621130044407838/805218508866453554/tech.gif" # noqa: E501
|
||||
# )
|
||||
content = re.sub(r"\s+", "", message.content)
|
||||
match = invites.search(content)
|
||||
setting = await Setting.find_one(q(guild=message.guild.id, setting="noinvite"))
|
||||
if not setting:
|
||||
setting = Setting(guild=message.guild.id, setting="noinvite", value=True)
|
||||
await setting.commit()
|
||||
if match:
|
||||
guild_invites = [x.code for x in await message.guild.fetch_invites()]
|
||||
if message.guild.vanity_url_code:
|
||||
guild_invites.append(message.guild.vanity_url_code)
|
||||
allowed = guild_invites + [
|
||||
"dbrand",
|
||||
"VtgZntXcnZ",
|
||||
"gPfYGbvTCE",
|
||||
]
|
||||
if (m := match.group(1)) not in allowed and setting.value:
|
||||
self.logger.debug(f"Removing non-allowed invite `{m}` from {message.guild.id}")
|
||||
try:
|
||||
await message.delete()
|
||||
except Exception:
|
||||
self.logger.debug("Message deleted before action taken")
|
||||
|
||||
expires_at = datetime.now(tz=timezone.utc) + timedelta(hours=24)
|
||||
await Warning(
|
||||
active=True,
|
||||
admin=self.user.id,
|
||||
duration=24,
|
||||
expires_at=expires_at,
|
||||
guild=message.guild.id,
|
||||
reason="Sent an invite link",
|
||||
user=message.author.id,
|
||||
).commit()
|
||||
tracker = warnings_tracker.labels(
|
||||
guild_id=message.guild.id, guild_name=message.guild.name
|
||||
)
|
||||
tracker.inc()
|
||||
embed = warning_embed(message.author, "Sent an invite link")
|
||||
try:
|
||||
await message.channel.send(embeds=embed)
|
||||
except Exception:
|
||||
self.logger.warn("Failed to send warning embed")
|
||||
|
||||
async def massmention(self, message: Message) -> None:
|
||||
"""Handle massmention events."""
|
||||
massmention = await Setting.find_one(
|
||||
q(
|
||||
guild=message.guild.id,
|
||||
setting="massmention",
|
||||
)
|
||||
)
|
||||
|
||||
if (
|
||||
massmention
|
||||
and massmention.value > 0 # noqa: W503
|
||||
and len(message._mention_ids + message._mention_roles) # noqa: W503
|
||||
- (1 if message.author.id in message._mention_ids else 0) # noqa: W503
|
||||
> massmention.value # noqa: W503
|
||||
):
|
||||
self.logger.debug(
|
||||
f"Massmention threshold on {message.guild.id}/{message.channel.id}/{message.id}"
|
||||
)
|
||||
expires_at = datetime.now(tz=timezone.utc) + timedelta(hours=24)
|
||||
await Warning(
|
||||
active=True,
|
||||
admin=self.user.id,
|
||||
duration=24,
|
||||
expires_at=expires_at,
|
||||
guild=message.guild.id,
|
||||
reason="Mass Mention",
|
||||
user=message.author.id,
|
||||
).commit()
|
||||
tracker = warnings_tracker.labels(
|
||||
guild_id=message.guild.id, guild_name=message.guild.name
|
||||
)
|
||||
tracker.inc()
|
||||
embed = warning_embed(message.author, "Mass Mention")
|
||||
try:
|
||||
await message.channel.send(embeds=embed)
|
||||
except Exception:
|
||||
self.logger.warn("Failed to send warning embed")
|
||||
|
||||
async def roleping(self, message: Message) -> None:
|
||||
"""Handle roleping events."""
|
||||
try:
|
||||
if message.author.has_permission(Permissions.MANAGE_GUILD):
|
||||
return
|
||||
except Exception as e:
|
||||
self.logger.error("Failed to get permissions, pretending check failed", exc_info=e)
|
||||
if await Roleping.collection.count_documents(q(guild=message.guild.id, active=True)) == 0:
|
||||
return
|
||||
rolepings = await Roleping.find(q(guild=message.guild.id, active=True)).to_list(None)
|
||||
|
||||
# Get all role IDs involved with message
|
||||
roles = [x.id async for x in message.mention_roles]
|
||||
async for mention in message.mention_users:
|
||||
roles += [x.id for x in mention.roles]
|
||||
|
||||
if not roles:
|
||||
return
|
||||
|
||||
# Get all roles that are rolepinged
|
||||
roleping_ids = [r.role for r in rolepings]
|
||||
|
||||
# Get roles in rolepings
|
||||
role_in_rolepings = find_all(lambda x: x in roleping_ids, roles)
|
||||
|
||||
# Check if the user has the role, so they are allowed to ping it
|
||||
user_missing_role = any(x.id not in roleping_ids for x in message.author.roles)
|
||||
|
||||
# Admins can ping whoever
|
||||
user_is_admin = message.author.has_permission(Permissions.ADMINISTRATOR)
|
||||
|
||||
# Check if user in a bypass list
|
||||
def check_has_role(roleping: Roleping) -> bool:
|
||||
return any(role.id in roleping.bypass["roles"] for role in message.author.roles)
|
||||
|
||||
user_has_bypass = False
|
||||
for roleping in rolepings:
|
||||
if message.author.id in roleping.bypass["users"]:
|
||||
user_has_bypass = True
|
||||
break
|
||||
if check_has_role(roleping):
|
||||
user_has_bypass = True
|
||||
break
|
||||
|
||||
if role_in_rolepings and user_missing_role and not user_is_admin and not user_has_bypass:
|
||||
self.logger.debug(
|
||||
f"Rolepinged role in {message.guild.id}/{message.channel.id}/{message.id}"
|
||||
)
|
||||
expires_at = datetime.now(tz=timezone.utc) + timedelta(hours=24)
|
||||
await Warning(
|
||||
active=True,
|
||||
admin=self.user.id,
|
||||
duration=24,
|
||||
expires_at=expires_at,
|
||||
guild=message.guild.id,
|
||||
reason="Pinged a blocked role/user with a blocked role",
|
||||
user=message.author.id,
|
||||
).commit()
|
||||
tracker = warnings_tracker.labels(
|
||||
guild_id=message.guild.id, guild_name=message.guild.name
|
||||
)
|
||||
tracker.inc()
|
||||
embed = warning_embed(message.author, "Pinged a blocked role/user with a blocked role")
|
||||
try:
|
||||
await message.channel.send(embeds=embed)
|
||||
except Exception:
|
||||
self.logger.warn("Failed to send warning embed")
|
||||
|
||||
async def phishing(self, message: Message) -> None:
|
||||
"""Check if the message contains any known phishing domains."""
|
||||
for match in url.finditer(message.content):
|
||||
if (m := match.group("domain")) in self.phishing_domains:
|
||||
self.logger.debug(
|
||||
f"Phishing url `{m}` detected in {message.guild.id}/{message.channel.id}/{message.id}"
|
||||
)
|
||||
expires_at = datetime.now(tz=timezone.utc) + timedelta(hours=24)
|
||||
await Warning(
|
||||
active=True,
|
||||
admin=self.user.id,
|
||||
duration=24,
|
||||
expires_at=expires_at,
|
||||
guild=message.guild.id,
|
||||
reason="Phishing URL",
|
||||
user=message.author.id,
|
||||
).commit()
|
||||
tracker = warnings_tracker.labels(
|
||||
guild_id=message.guild.id, guild_name=message.guild.name
|
||||
)
|
||||
tracker.inc()
|
||||
embed = warning_embed(message.author, "Phishing URL")
|
||||
try:
|
||||
await message.channel.send(embeds=embed)
|
||||
except Exception:
|
||||
self.logger.warn("Failed to send warning embed")
|
||||
try:
|
||||
await message.delete()
|
||||
except Exception:
|
||||
self.logger.warn("Failed to delete malicious message")
|
||||
tracker = malicious_tracker.labels(
|
||||
guild_id=message.guild.id, guild_name=message.guild.name
|
||||
)
|
||||
tracker.inc()
|
||||
return True
|
||||
return False
|
||||
|
||||
async def malicious_url(self, message: Message) -> None:
|
||||
"""Check if the message contains any known phishing domains."""
|
||||
for match in url.finditer(message.content):
|
||||
async with ClientSession() as session:
|
||||
resp = await session.post(
|
||||
"https://anti-fish.bitflow.dev/check",
|
||||
json={"message": match.string},
|
||||
headers={
|
||||
"Application-Name": "JARVIS",
|
||||
"Application-Link": "https://git.zevaryx.com/stark-industries/jarvis",
|
||||
},
|
||||
)
|
||||
if resp.status != 200:
|
||||
break
|
||||
data = await resp.json()
|
||||
if data["match"]:
|
||||
self.logger.debug(
|
||||
f"Scam url `{match.string}` detected in {message.guild.id}/{message.channel.id}/{message.id}"
|
||||
)
|
||||
expires_at = datetime.now(tz=timezone.utc) + timedelta(hours=24)
|
||||
await Warning(
|
||||
active=True,
|
||||
admin=self.user.id,
|
||||
duration=24,
|
||||
expires_at=expires_at,
|
||||
guild=message.guild.id,
|
||||
reason="Unsafe URL",
|
||||
user=message.author.id,
|
||||
).commit()
|
||||
tracker = warnings_tracker.labels(
|
||||
guild_id=message.guild.id, guild_name=message.guild.name
|
||||
)
|
||||
tracker.inc()
|
||||
reasons = ", ".join(f"{m['source']}: {m['type']}" for m in data["matches"])
|
||||
embed = warning_embed(message.author, reasons)
|
||||
try:
|
||||
await message.channel.send(embeds=embed)
|
||||
except Exception:
|
||||
self.logger.warn("Failed to send warning embed")
|
||||
try:
|
||||
await message.delete()
|
||||
except Exception:
|
||||
self.logger.warn("Failed to delete malicious message")
|
||||
tracker = malicious_tracker.labels(
|
||||
guild_id=message.guild.id, guild_name=message.guild.name
|
||||
)
|
||||
tracker.inc()
|
||||
return True
|
||||
return False
|
||||
|
||||
async def timeout_user(self, user: Member, channel: GuildText) -> None:
|
||||
"""Timeout a user."""
|
||||
expires_at = datetime.now(tz=timezone.utc) + timedelta(minutes=30)
|
||||
try:
|
||||
await user.timeout(communication_disabled_until=expires_at, reason="Phishing link")
|
||||
await Mute(
|
||||
user=user.id,
|
||||
reason="Auto mute for harmful link",
|
||||
admin=self.user.id,
|
||||
guild=user.guild.id,
|
||||
duration=30,
|
||||
active=True,
|
||||
).commit()
|
||||
ts = int(expires_at.timestamp())
|
||||
embed = build_embed(
|
||||
title="User Muted",
|
||||
description=f"{user.mention} has been muted",
|
||||
fields=[
|
||||
EmbedField(name="Reason", value="Auto mute for harmful link"),
|
||||
EmbedField(name="Until", value=f"<t:{ts}:F> <t:{ts}:R>"),
|
||||
],
|
||||
color=get_command_color("mute"),
|
||||
)
|
||||
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 channel.send(embeds=embed)
|
||||
|
||||
except Exception:
|
||||
self.logger.warn("Failed to timeout user for phishing")
|
||||
|
||||
@listen()
|
||||
async def on_message(self, event: MessageCreate) -> None:
|
||||
"""Handle on_message event. Calls other event handlers."""
|
||||
message = event.message
|
||||
if not isinstance(message.channel, DMChannel) and not message.author.bot:
|
||||
await self.autoreact(message)
|
||||
await self.massmention(message)
|
||||
await self.roleping(message)
|
||||
await self.autopurge(message)
|
||||
await self.checks(message)
|
||||
if not (phish := await self.phishing(message)):
|
||||
malicious = await self.malicious_url(message)
|
||||
if phish or malicious:
|
||||
await self.timeout_user(message.author, message.channel)
|
||||
|
||||
@listen()
|
||||
async def on_message_edit(self, event: MessageUpdate) -> None:
|
||||
"""Process on_message_edit events."""
|
||||
before = event.before
|
||||
after = event.after
|
||||
if not after.author.bot:
|
||||
modlog = await Setting.find_one(q(guild=after.guild.id, setting="activitylog"))
|
||||
ignore = await Setting.find_one(q(guild=after.guild.id, setting="log_ignore"))
|
||||
if modlog and (ignore and after.channel.id not in ignore.value):
|
||||
if not before or before.content == after.content or before.content is None:
|
||||
return
|
||||
try:
|
||||
channel = before.guild.get_channel(modlog.value)
|
||||
fields = [
|
||||
EmbedField(
|
||||
"Original Message",
|
||||
before.content if before.content else "N/A",
|
||||
False,
|
||||
),
|
||||
EmbedField(
|
||||
"New Message",
|
||||
after.content if after.content else "N/A",
|
||||
False,
|
||||
),
|
||||
]
|
||||
embed = build_embed(
|
||||
title="Message Edited",
|
||||
description=f"{after.author.mention} edited a message in {before.channel.mention}",
|
||||
fields=fields,
|
||||
color="#fc9e3f",
|
||||
timestamp=after.edited_timestamp,
|
||||
url=after.jump_url,
|
||||
)
|
||||
embed.set_author(
|
||||
name=after.author.username,
|
||||
icon_url=after.author.display_avatar.url,
|
||||
url=after.jump_url,
|
||||
)
|
||||
embed.set_footer(
|
||||
text=f"{after.author.username}#{after.author.discriminator} | {after.author.id}"
|
||||
)
|
||||
await channel.send(embeds=embed)
|
||||
except Exception as e:
|
||||
self.logger.warning(
|
||||
f"Failed to process edit {before.guild.id}/{before.channel.id}/{before.id}: {e}"
|
||||
)
|
||||
if not isinstance(after.channel, DMChannel) and not after.author.bot:
|
||||
await self.massmention(after)
|
||||
await self.roleping(after)
|
||||
await self.checks(after)
|
||||
await self.roleping(after)
|
||||
await self.checks(after)
|
||||
if not (phish := await self.phishing(after)):
|
||||
malicious = await self.malicious_url(after)
|
||||
if phish or malicious:
|
||||
await self.timeout_user(after.author, after.channel)
|
||||
|
||||
@listen()
|
||||
async def on_message_delete(self, event: MessageDelete) -> None:
|
||||
"""Process on_message_delete events."""
|
||||
message = event.message
|
||||
modlog = await Setting.find_one(q(guild=message.guild.id, setting="activitylog"))
|
||||
ignore = await Setting.find_one(q(guild=message.guild.id, setting="log_ignore"))
|
||||
if modlog and (ignore and message.channel.id not in ignore.value):
|
||||
try:
|
||||
content = message.content or "N/A"
|
||||
except AttributeError:
|
||||
content = "N/A"
|
||||
fields = [EmbedField("Original Message", content, False)]
|
||||
|
||||
try:
|
||||
if message.attachments:
|
||||
value = "\n".join([f"[{x.filename}]({x.url})" for x in message.attachments])
|
||||
fields.append(
|
||||
EmbedField(
|
||||
name="Attachments",
|
||||
value=value,
|
||||
inline=False,
|
||||
)
|
||||
)
|
||||
|
||||
if message.sticker_items:
|
||||
value = "\n".join([f"Sticker: {x.name}" for x in message.sticker_items])
|
||||
fields.append(
|
||||
EmbedField(
|
||||
name="Stickers",
|
||||
value=value,
|
||||
inline=False,
|
||||
)
|
||||
)
|
||||
|
||||
if message.embeds:
|
||||
value = str(len(message.embeds)) + " embeds"
|
||||
fields.append(
|
||||
EmbedField(
|
||||
name="Embeds",
|
||||
value=value,
|
||||
inline=False,
|
||||
)
|
||||
)
|
||||
|
||||
channel = message.guild.get_channel(modlog.value)
|
||||
embed = build_embed(
|
||||
title="Message Deleted",
|
||||
description=f"{message.author.mention}'s message was deleted from {message.channel.mention}",
|
||||
fields=fields,
|
||||
color="#fc9e3f",
|
||||
)
|
||||
|
||||
embed.set_author(
|
||||
name=message.author.username,
|
||||
icon_url=message.author.display_avatar.url,
|
||||
url=message.jump_url,
|
||||
)
|
||||
embed.set_footer(
|
||||
text=(
|
||||
f"{message.author.username}#{message.author.discriminator} | "
|
||||
f"{message.author.id}"
|
||||
)
|
||||
)
|
||||
await channel.send(embeds=embed)
|
||||
except Exception as e:
|
||||
self.logger.warning(
|
||||
f"Failed to process edit {message.guild.id}/{message.channel.id}/{message.id}: {e}"
|
||||
)
|
33
jarvis/client/tasks.py
Normal file
33
jarvis/client/tasks.py
Normal file
|
@ -0,0 +1,33 @@
|
|||
"""JARVIS task mixin."""
|
||||
from aiohttp import ClientSession
|
||||
from naff.models.naff.tasks.task import Task
|
||||
from naff.models.naff.tasks.triggers import IntervalTrigger
|
||||
|
||||
|
||||
class TaskMixin:
|
||||
@Task.create(IntervalTrigger(minutes=1))
|
||||
async def _update_domains(self) -> None:
|
||||
async with ClientSession(headers={"X-Identity": "Discord: zevaryx#5779"}) as session:
|
||||
response = await session.get("https://phish.sinking.yachts/v2/recent/60")
|
||||
response.raise_for_status()
|
||||
data = await response.json()
|
||||
|
||||
if len(data) == 0:
|
||||
return
|
||||
self.logger.debug(f"Found {len(data)} changes to phishing domains")
|
||||
|
||||
add = 0
|
||||
sub = 0
|
||||
|
||||
for update in data:
|
||||
if update["type"] == "add":
|
||||
for domain in update["domains"]:
|
||||
if domain not in self.phishing_domains:
|
||||
add += 1
|
||||
self.phishing_domains.append(domain)
|
||||
elif update["type"] == "delete":
|
||||
for domain in update["domains"]:
|
||||
if domain in self.phishing_domains:
|
||||
sub -= 1
|
||||
self.phishing_domains.remove(domain)
|
||||
self.logger.debug(f"{add} additions, {sub} removals")
|
|
@ -57,8 +57,8 @@ class LockCog(Extension):
|
|||
await ctx.send("Duration must be > 0", ephemeral=True)
|
||||
return
|
||||
|
||||
elif duration > 60 * 12:
|
||||
await ctx.send("Duration must be <= 12 hours", ephemeral=True)
|
||||
elif duration > 60 * 24 * 7:
|
||||
await ctx.send("Duration must be <= 7 days", ephemeral=True)
|
||||
return
|
||||
|
||||
if len(reason) > 100:
|
||||
|
|
|
@ -130,8 +130,8 @@ class LockdownCog(Extension):
|
|||
if duration <= 0:
|
||||
await ctx.send("Duration must be > 0", ephemeral=True)
|
||||
return
|
||||
elif duration >= 300:
|
||||
await ctx.send("Duration must be < 5 hours", ephemeral=True)
|
||||
elif duration > 60 * 24 * 7:
|
||||
await ctx.send("Duration must be <= 7 days", ephemeral=True)
|
||||
return
|
||||
|
||||
exists = await Lockdown.find_one(q(guild=ctx.guild.id, active=True))
|
||||
|
|
|
@ -195,6 +195,10 @@ class MuteCog(ModcaseCog):
|
|||
await ctx.send("User is not muted", ephemeral=True)
|
||||
return
|
||||
|
||||
if not await ctx.guild.fetch_member(user.id):
|
||||
await ctx.send("User must be in guild", ephemeral=True)
|
||||
return
|
||||
|
||||
await user.timeout(communication_disabled_until=datetime.now(tz=timezone.utc))
|
||||
|
||||
embed = unmute_embed(user=user, admin=ctx.author, reason=reason, guild=ctx.guild)
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
"""JARVIS dbrand cog."""
|
||||
import logging
|
||||
import re
|
||||
from datetime import datetime, timedelta
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
import aiohttp
|
||||
from bs4 import BeautifulSoup
|
||||
from naff import Client, Extension, InteractionContext
|
||||
from naff.client.utils import find
|
||||
from naff.models.discord.embed import EmbedField
|
||||
from naff.models.naff.application_commands import (
|
||||
OptionTypes,
|
||||
|
@ -13,7 +15,9 @@ from naff.models.naff.application_commands import (
|
|||
)
|
||||
from naff.models.naff.command import cooldown
|
||||
from naff.models.naff.cooldowns import Buckets
|
||||
from thefuzz import process
|
||||
|
||||
from jarvis.branding import CUSTOM_EMOJIS
|
||||
from jarvis.config import JarvisConfig
|
||||
from jarvis.data.dbrand import shipping_lookup
|
||||
from jarvis.utils import build_embed
|
||||
|
@ -21,6 +25,50 @@ from jarvis.utils import build_embed
|
|||
guild_ids = [578757004059738142, 520021794380447745, 862402786116763668]
|
||||
|
||||
|
||||
async def parse_db_status() -> dict:
|
||||
"""Parse the dbrand status page for a local API"""
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get("https://dbrand.com/status") as response:
|
||||
response.raise_for_status()
|
||||
soup = BeautifulSoup(await response.content.read(), features="html.parser")
|
||||
tables = soup.find_all("table")
|
||||
data = {}
|
||||
for table in tables:
|
||||
data_key = "countries"
|
||||
rows = table.find_all("tr")
|
||||
headers = rows.pop(0)
|
||||
headers = [h.get_text() for h in headers.find_all("th")]
|
||||
if headers[0] == "Service":
|
||||
data_key = "operations"
|
||||
data[data_key] = []
|
||||
for row in rows:
|
||||
row_data = []
|
||||
cells = row.find_all("td")
|
||||
for cell in cells:
|
||||
if "column--comment" in cell["class"]:
|
||||
text = cell.find("span").get_text()
|
||||
if cell != "Unavailable":
|
||||
text += ": " + cell.find("div").get_text()
|
||||
cell = text.strip()
|
||||
elif "column--status" in cell["class"]:
|
||||
info = cell.find("span")["class"]
|
||||
if any("green" in x for x in info):
|
||||
cell = CUSTOM_EMOJIS.get("ico_clock_green", "🟢")
|
||||
elif any("yellow" in x for x in info):
|
||||
cell = CUSTOM_EMOJIS.get("ico_clock_yellow", "🟡")
|
||||
elif any("red" in x for x in info):
|
||||
cell = CUSTOM_EMOJIS.get("ico_clock_red", "🔴")
|
||||
elif any("black" in x for x in info):
|
||||
cell = "⚫"
|
||||
else:
|
||||
cell = cell.get_text().strip()
|
||||
row_data.append(cell)
|
||||
data[data_key].append(
|
||||
{headers[idx]: value for idx, value in enumerate(row_data)}
|
||||
)
|
||||
return data
|
||||
|
||||
|
||||
class DbrandCog(Extension):
|
||||
"""
|
||||
dbrand functions for JARVIS
|
||||
|
@ -42,6 +90,45 @@ class DbrandCog(Extension):
|
|||
|
||||
db = SlashCommand(name="db", description="dbrand commands", scopes=guild_ids)
|
||||
|
||||
@db.subcommand(sub_cmd_name="status", sub_cmd_description="Get dbrand operational status")
|
||||
async def _status(self, ctx: InteractionContext) -> None:
|
||||
status = self.cache.get("status")
|
||||
if not status or status["cache_expiry"] <= datetime.now(tz=timezone.utc):
|
||||
status = await parse_db_status()
|
||||
status["cache_expiry"] = datetime.now(tz=timezone.utc) + timedelta(hours=2)
|
||||
self.cache["status"] = status
|
||||
status = status.get("operations")
|
||||
emojies = [x["Status"] for x in status]
|
||||
fields = [
|
||||
EmbedField(name=f'{x["Status"]} {x["Service"]}', value=x["Detail"]) for x in status
|
||||
]
|
||||
color = "#FBBD1E"
|
||||
if all("green" in x for x in emojies):
|
||||
color = "#38F657"
|
||||
elif all("red" in x for x in emojies):
|
||||
color = "#F12D20"
|
||||
embed = build_embed(
|
||||
title="Operational Status",
|
||||
description="Current dbrand operational status.\n[View online](https://dbrand.com/status)",
|
||||
fields=fields,
|
||||
url="https://dbrand.com/status",
|
||||
color=color,
|
||||
)
|
||||
|
||||
embed.set_thumbnail(url="https://dev.zevaryx.com/db_logo.png")
|
||||
embed.set_footer(
|
||||
text="dbrand.com",
|
||||
icon_url="https://dev.zevaryx.com/db_logo.png",
|
||||
)
|
||||
|
||||
await ctx.send(embeds=embed)
|
||||
|
||||
@db.subcommand(sub_cmd_name="gripcheck", sub_cmd_description="Watch a dbrand grip get thrown")
|
||||
async def _gripcheck(self, ctx: InteractionContext) -> None:
|
||||
video_url = "https://cdn.discordapp.com/attachments/599068193339736096/890679742263623751/video0.mov"
|
||||
image_url = "https://cdn.discordapp.com/attachments/599068193339736096/890680198306095104/image0.jpg"
|
||||
await ctx.send(f"Video: {video_url}\nResults: {image_url}")
|
||||
|
||||
@db.subcommand(sub_cmd_name="info", sub_cmd_description="Get useful links")
|
||||
@cooldown(bucket=Buckets.USER, rate=1, interval=30)
|
||||
async def _info(self, ctx: InteractionContext) -> None:
|
||||
|
@ -96,7 +183,6 @@ class DbrandCog(Extension):
|
|||
)
|
||||
@cooldown(bucket=Buckets.USER, rate=1, interval=2)
|
||||
async def _shipping(self, ctx: InteractionContext, search: str) -> None:
|
||||
await ctx.defer()
|
||||
if not re.match(r"^[A-Z- ]+$", search, re.IGNORECASE):
|
||||
if re.match(
|
||||
r"^[\U0001f1e6-\U0001f1ff]{2}$",
|
||||
|
@ -109,58 +195,90 @@ class DbrandCog(Extension):
|
|||
elif search == "🏳️":
|
||||
search = "fr"
|
||||
else:
|
||||
await ctx.send("Please use text to search for shipping.")
|
||||
await ctx.send("Please use text to search for shipping.", ephemeral=True)
|
||||
return
|
||||
if len(search) > 2:
|
||||
matches = [x["code"] for x in shipping_lookup if search.lower() in x["country"]]
|
||||
if len(matches) > 0:
|
||||
search = matches[0]
|
||||
if len(search) > 3:
|
||||
countries = {x["country"]: x["alpha-2"] for x in shipping_lookup}
|
||||
match = process.extractOne(search, countries.keys())
|
||||
if match:
|
||||
search = countries[match[0]]
|
||||
else:
|
||||
await ctx.send(f"Unable to find country {search}", ephemeral=True)
|
||||
return
|
||||
elif len(search) == 3:
|
||||
alpha3 = {x["alpha-3"]: x["alpha-2"] for x in shipping_lookup}
|
||||
if search in alpha3:
|
||||
search = alpha3[search]
|
||||
else:
|
||||
match = process.extractOne(search, alpha3.keys())
|
||||
search = alpha3[match[0]]
|
||||
await ctx.defer()
|
||||
dest = search.lower()
|
||||
data = self.cache.get(dest, None)
|
||||
if not data or data["cache_expiry"] < datetime.utcnow():
|
||||
if not data or data["cache_expiry"] < datetime.now(tz=timezone.utc):
|
||||
api_link = self.api_url + dest
|
||||
data = await self._session.get(api_link)
|
||||
if 200 <= data.status < 400:
|
||||
data = await data.json()
|
||||
data["cache_expiry"] = datetime.utcnow() + timedelta(hours=24)
|
||||
data["cache_expiry"] = datetime.now(tz=timezone.utc) + timedelta(hours=24)
|
||||
self.cache[dest] = data
|
||||
else:
|
||||
data = None
|
||||
fields = None
|
||||
if data is not None and data["is_valid"] and data["shipping_available"]:
|
||||
fields = []
|
||||
fields.append(
|
||||
EmbedField(data["carrier"] + " " + data["tier-title"], data["time-title"])
|
||||
)
|
||||
for service in data["shipping_services_available"][1:]:
|
||||
for service in data["shipping_services_available"]:
|
||||
service_data = self.cache.get(f"{dest}-{service}")
|
||||
if not service_data or service_data["cache_expiry"] < datetime.utcnow():
|
||||
if not service_data or service_data["cache_expiry"] < datetime.now(tz=timezone.utc):
|
||||
service_data = await self._session.get(
|
||||
self.api_url + dest + "/" + service["url"]
|
||||
)
|
||||
if service_data.status > 400:
|
||||
continue
|
||||
service_data = await service_data.json()
|
||||
service_data["cache_expiry"] = datetime.utcnow() + timedelta(hours=24)
|
||||
self.cache[f"{dest}-{service}"] = service_data
|
||||
fields.append(
|
||||
EmbedField(
|
||||
service_data["carrier"] + " " + service_data["tier-title"],
|
||||
service_data["time-title"],
|
||||
service_data["cache_expiry"] = datetime.now(tz=timezone.utc) + timedelta(
|
||||
hours=24
|
||||
)
|
||||
)
|
||||
self.cache[f"{dest}-{service}"] = service_data
|
||||
title = f'{service_data["carrier"]} {service_data["tier-title"]} | {service_data["costs-min"]}'
|
||||
message = service_data["time-title"]
|
||||
if service_data["free_threshold_available"]:
|
||||
title += " | Free over " + service_data["free-threshold"]
|
||||
fields.append(EmbedField(title, message))
|
||||
|
||||
status = self.cache.get("status")
|
||||
if not status or status["cache_expiry"] <= datetime.now(tz=timezone.utc):
|
||||
status = await parse_db_status()
|
||||
status["cache_expiry"] = datetime.now(tz=timezone.utc) + timedelta(hours=2)
|
||||
self.cache["status"] = status
|
||||
status = status["countries"]
|
||||
|
||||
country = data["country"]
|
||||
if country.startswith("the"):
|
||||
country = country.replace("the", "").strip()
|
||||
shipping_info = find(lambda x: x["Country"] == country, status)
|
||||
|
||||
country = "-".join(x for x in data["country"].split(" ") if x != "the")
|
||||
country_urlsafe = country.replace("-", "%20")
|
||||
description = (
|
||||
f"Click the link above to see shipping time to {data['country']}."
|
||||
"\n[View all shipping destinations](https://dbrand.com/shipping)"
|
||||
" | [Check shipping status]"
|
||||
f"(https://dbrand.com/status#main-content:~:text={country_urlsafe})"
|
||||
)
|
||||
description = ""
|
||||
color = "#FFBB00"
|
||||
if shipping_info:
|
||||
description = f'{shipping_info["Status"]}\u200b \u200b {shipping_info["Est. Delivery Time"].split(":")[0]}'
|
||||
created = self.cache.get("status").get("cache_expiry") - timedelta(hours=2)
|
||||
ts = int(created.timestamp())
|
||||
description += f" \u200b | \u200b Last updated: <t:{ts}:R>\n\u200b"
|
||||
if "green" in shipping_info["Status"]:
|
||||
color = "#38F657"
|
||||
elif "yellow" in shipping_info["Status"]:
|
||||
color = "#FBBD1E"
|
||||
elif "red" in shipping_info["Status"]:
|
||||
color = "#F12D20"
|
||||
else:
|
||||
color = "#FFFFFF"
|
||||
|
||||
embed = build_embed(
|
||||
title="Shipping to {}".format(data["country"]),
|
||||
description=description,
|
||||
color="#FFBB00",
|
||||
color=color,
|
||||
fields=fields,
|
||||
url=self.base_url + "shipping/" + country,
|
||||
)
|
||||
|
|
|
@ -23,8 +23,8 @@ from naff.models.discord.file import File
|
|||
from naff.models.discord.message import Attachment
|
||||
from naff.models.naff.application_commands import (
|
||||
OptionTypes,
|
||||
SlashCommand,
|
||||
SlashCommandChoice,
|
||||
slash_command,
|
||||
slash_option,
|
||||
)
|
||||
from naff.models.naff.command import cooldown
|
||||
|
@ -60,7 +60,9 @@ class DevCog(Extension):
|
|||
self.bot = bot
|
||||
self.logger = logging.getLogger(__name__)
|
||||
|
||||
@slash_command(name="hash", description="Hash some data")
|
||||
dev = SlashCommand(name="dev", description="Developer utilities")
|
||||
|
||||
@dev.subcommand(sub_cmd_name="hash", sub_cmd_description="Hash some data")
|
||||
@slash_option(
|
||||
name="method",
|
||||
description="Hash method",
|
||||
|
@ -126,7 +128,7 @@ class DevCog(Extension):
|
|||
)
|
||||
await ctx.send(embeds=embed, components=components)
|
||||
|
||||
@slash_command(name="uuid", description="Generate a UUID")
|
||||
@dev.subcommand(sub_cmd_name="uuid", sub_cmd_description="Generate a UUID")
|
||||
@slash_option(
|
||||
name="version",
|
||||
description="UUID version",
|
||||
|
@ -159,25 +161,25 @@ class DevCog(Extension):
|
|||
to_send = UUID_GET[version](uuidpy.NAMESPACE_DNS, data)
|
||||
await ctx.send(f"UUID{version}: `{to_send}`")
|
||||
|
||||
@slash_command(
|
||||
name="objectid",
|
||||
description="Generate an ObjectID",
|
||||
@dev.subcommand(
|
||||
sub_cmd_name="objectid",
|
||||
sub_cmd_description="Generate an ObjectID",
|
||||
)
|
||||
@cooldown(bucket=Buckets.USER, rate=1, interval=2)
|
||||
async def _objectid(self, ctx: InteractionContext) -> None:
|
||||
await ctx.send(f"ObjectId: `{str(ObjectId())}`")
|
||||
|
||||
@slash_command(
|
||||
name="ulid",
|
||||
description="Generate a ULID",
|
||||
@dev.subcommand(
|
||||
sub_cmd_name="ulid",
|
||||
sub_cmd_description="Generate a ULID",
|
||||
)
|
||||
@cooldown(bucket=Buckets.USER, rate=1, interval=2)
|
||||
async def _ulid(self, ctx: InteractionContext) -> None:
|
||||
await ctx.send(f"ULID: `{ulidpy.new().str}`")
|
||||
|
||||
@slash_command(
|
||||
name="uuid2ulid",
|
||||
description="Convert a UUID to a ULID",
|
||||
@dev.subcommand(
|
||||
sub_cmd_name="uuid2ulid",
|
||||
sub_cmd_description="Convert a UUID to a ULID",
|
||||
)
|
||||
@slash_option(
|
||||
name="uuid", description="UUID to convert", opt_type=OptionTypes.STRING, required=True
|
||||
|
@ -190,9 +192,9 @@ class DevCog(Extension):
|
|||
else:
|
||||
await ctx.send("Invalid UUID")
|
||||
|
||||
@slash_command(
|
||||
name="ulid2uuid",
|
||||
description="Convert a ULID to a UUID",
|
||||
@dev.subcommand(
|
||||
sub_cmd_name="ulid2uuid",
|
||||
sub_cmd_description="Convert a ULID to a UUID",
|
||||
)
|
||||
@slash_option(
|
||||
name="ulid", description="ULID to convert", opt_type=OptionTypes.STRING, required=True
|
||||
|
@ -207,7 +209,7 @@ class DevCog(Extension):
|
|||
|
||||
base64_methods = ["b64", "b16", "b32", "a85", "b85"]
|
||||
|
||||
@slash_command(name="encode", description="Encode some data")
|
||||
@dev.subcommand(sub_cmd_name="encode", sub_cmd_description="Encode some data")
|
||||
@slash_option(
|
||||
name="method",
|
||||
description="Encode method",
|
||||
|
@ -239,13 +241,13 @@ class DevCog(Extension):
|
|||
EmbedField(name="Plaintext", value=f"`{data}`", inline=False),
|
||||
EmbedField(name=mstr, value=f"`{encoded}`", inline=False),
|
||||
]
|
||||
embed = build_embed(title="Decoded Data", description="", fields=fields)
|
||||
embed = build_embed(title="Encoded Data", description="", fields=fields)
|
||||
components = Button(
|
||||
style=ButtonStyles.DANGER, emoji="🗑️", custom_id=f"delete|{ctx.author.id}"
|
||||
)
|
||||
await ctx.send(embeds=embed, components=components)
|
||||
|
||||
@slash_command(name="decode", description="Decode some data")
|
||||
@dev.subcommand(sub_cmd_name="decode", sub_cmd_description="Decode some data")
|
||||
@slash_option(
|
||||
name="method",
|
||||
description="Decode method",
|
||||
|
@ -255,7 +257,7 @@ class DevCog(Extension):
|
|||
)
|
||||
@slash_option(
|
||||
name="data",
|
||||
description="Data to encode",
|
||||
description="Data to decode",
|
||||
opt_type=OptionTypes.STRING,
|
||||
required=True,
|
||||
)
|
||||
|
@ -274,7 +276,7 @@ class DevCog(Extension):
|
|||
)
|
||||
return
|
||||
fields = [
|
||||
EmbedField(name="Plaintext", value=f"`{data}`", inline=False),
|
||||
EmbedField(name="Encoded Text", value=f"`{data}`", inline=False),
|
||||
EmbedField(name=mstr, value=f"`{decoded}`", inline=False),
|
||||
]
|
||||
embed = build_embed(title="Decoded Data", description="", fields=fields)
|
||||
|
@ -283,13 +285,13 @@ class DevCog(Extension):
|
|||
)
|
||||
await ctx.send(embeds=embed, components=components)
|
||||
|
||||
@slash_command(name="cloc", description="Get JARVIS lines of code")
|
||||
@dev.subcommand(sub_cmd_name="cloc", sub_cmd_description="Get JARVIS lines of code")
|
||||
@cooldown(bucket=Buckets.CHANNEL, rate=1, interval=30)
|
||||
async def _cloc(self, ctx: InteractionContext) -> None:
|
||||
await ctx.defer()
|
||||
output = subprocess.check_output( # noqa: S603, S607
|
||||
["tokei", "-C", "--sort", "code"]
|
||||
).decode("UTF-8")
|
||||
output = subprocess.check_output(["tokei", "-C", "--sort", "code"]).decode(
|
||||
"UTF-8"
|
||||
) # noqa: S603, S607
|
||||
console = Console()
|
||||
with console.capture() as capture:
|
||||
console.print(output)
|
||||
|
|
|
@ -15,7 +15,7 @@ from naff.models.discord.file import File
|
|||
from naff.models.discord.message import Attachment
|
||||
from naff.models.naff.application_commands import (
|
||||
OptionTypes,
|
||||
slash_command,
|
||||
SlashCommand,
|
||||
slash_option,
|
||||
)
|
||||
|
||||
|
@ -40,7 +40,9 @@ class ImageCog(Extension):
|
|||
def __del__(self):
|
||||
self._session.close()
|
||||
|
||||
@slash_command(name="resize", description="Resize an image")
|
||||
image = SlashCommand(name="image", description="Manipulate images")
|
||||
|
||||
@image.subcommand(sub_cmd_name="shrink", sub_cmd_description="Shrink an image")
|
||||
@slash_option(
|
||||
name="target",
|
||||
description="Target size, i.e. 200KB",
|
||||
|
|
|
@ -18,7 +18,6 @@ from naff.models.discord.modal import InputText, Modal, TextStyles
|
|||
from naff.models.naff.application_commands import (
|
||||
OptionTypes,
|
||||
SlashCommand,
|
||||
slash_command,
|
||||
slash_option,
|
||||
)
|
||||
from thefuzz import process
|
||||
|
@ -40,7 +39,9 @@ class RemindmeCog(Extension):
|
|||
self.bot = bot
|
||||
self.logger = logging.getLogger(__name__)
|
||||
|
||||
@slash_command(name="remindme", description="Set a reminder")
|
||||
reminders = SlashCommand(name="reminders", description="Manage reminders")
|
||||
|
||||
@reminders.subcommand(sub_cmd_name="set", sub_cmd_description="Set a reminder")
|
||||
@slash_option(
|
||||
name="private",
|
||||
description="Send as DM?",
|
||||
|
@ -210,8 +211,6 @@ class RemindmeCog(Extension):
|
|||
|
||||
return embed
|
||||
|
||||
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:
|
||||
reminders = await Reminder.find(q(user=ctx.author.id, active=True)).to_list(None)
|
||||
|
|
|
@ -212,9 +212,7 @@ class RolegiverCog(Extension):
|
|||
)
|
||||
await ctx.send(embeds=embed, components=components)
|
||||
|
||||
role = SlashCommand(name="role", description="Get/Remove Rolegiver roles")
|
||||
|
||||
@role.subcommand(sub_cmd_name="get", sub_cmd_description="Get a role")
|
||||
@rolegiver.subcommand(sub_cmd_name="get", sub_cmd_description="Get a role")
|
||||
@cooldown(bucket=Buckets.USER, rate=1, interval=10)
|
||||
async def _role_get(self, ctx: InteractionContext) -> None:
|
||||
setting = await Rolegiver.find_one(q(guild=ctx.guild.id))
|
||||
|
@ -290,7 +288,7 @@ class RolegiverCog(Extension):
|
|||
component.disabled = True
|
||||
await message.edit(components=components)
|
||||
|
||||
@role.subcommand(sub_cmd_name="remove", sub_cmd_description="Remove a role")
|
||||
@rolegiver.subcommand(sub_cmd_name="forfeit", sub_cmd_description="Forfeit a role")
|
||||
@cooldown(bucket=Buckets.USER, rate=1, interval=10)
|
||||
async def _role_remove(self, ctx: InteractionContext) -> None:
|
||||
user_roles = ctx.author.roles
|
||||
|
|
|
@ -260,61 +260,6 @@ class StarboardCog(Extension):
|
|||
async def _star_message(self, ctx: InteractionContext) -> None:
|
||||
await self._star_add(ctx, message=str(ctx.target_id))
|
||||
|
||||
star = SlashCommand(
|
||||
name="star",
|
||||
description="Manage stars",
|
||||
)
|
||||
|
||||
@star.subcommand(sub_cmd_name="delete", sub_cmd_description="Delete a starred message")
|
||||
@slash_option(
|
||||
name="id", description="Star ID to delete", opt_type=OptionTypes.INTEGER, required=True
|
||||
)
|
||||
@slash_option(
|
||||
name="starboard",
|
||||
description="Starboard to delete star from",
|
||||
opt_type=OptionTypes.CHANNEL,
|
||||
required=True,
|
||||
)
|
||||
@check(admin_or_permissions(Permissions.MANAGE_GUILD))
|
||||
async def _star_delete(
|
||||
self,
|
||||
ctx: InteractionContext,
|
||||
id: int,
|
||||
starboard: GuildText,
|
||||
) -> None:
|
||||
if not isinstance(starboard, GuildText):
|
||||
await ctx.send("Channel must be a GuildText channel", ephemeral=True)
|
||||
return
|
||||
|
||||
exists = await Starboard.find_one(q(channel=starboard.id, guild=ctx.guild.id))
|
||||
if not exists:
|
||||
# TODO: automagically create starboard
|
||||
await ctx.send(
|
||||
f"Starboard does not exist in {starboard.mention}. Please create it first",
|
||||
ephemeral=True,
|
||||
)
|
||||
return
|
||||
|
||||
star = await Star.find_one(
|
||||
q(
|
||||
starboard=starboard.id,
|
||||
index=id,
|
||||
guild=ctx.guild.id,
|
||||
active=True,
|
||||
)
|
||||
)
|
||||
if not star:
|
||||
await ctx.send(f"No star exists with id {id}", ephemeral=True)
|
||||
return
|
||||
|
||||
message = await starboard.fetch_message(star.star)
|
||||
if message:
|
||||
await message.delete()
|
||||
|
||||
await star.delete()
|
||||
|
||||
await ctx.send(f"Star {id} deleted from {starboard.mention}")
|
||||
|
||||
|
||||
def setup(bot: Client) -> None:
|
||||
"""Add StarboardCog to JARVIS"""
|
||||
|
|
|
@ -20,6 +20,7 @@ from naff.models.discord.user import User
|
|||
from naff.models.naff.application_commands import (
|
||||
CommandTypes,
|
||||
OptionTypes,
|
||||
SlashCommand,
|
||||
SlashCommandChoice,
|
||||
context_menu,
|
||||
slash_command,
|
||||
|
@ -49,18 +50,37 @@ class UtilCog(Extension):
|
|||
self.bot = bot
|
||||
self.logger = logging.getLogger(__name__)
|
||||
|
||||
@slash_command(name="status", description="Retrieve JARVIS status")
|
||||
bot = SlashCommand(name="bot", description="Bot commands")
|
||||
|
||||
@bot.subcommand(sub_cmd_name="status", sub_cmd_description="Retrieve JARVIS status")
|
||||
@cooldown(bucket=Buckets.CHANNEL, rate=1, interval=30)
|
||||
async def _status(self, ctx: InteractionContext) -> None:
|
||||
title = "JARVIS Status"
|
||||
desc = f"All systems online\nConnected to **{len(self.bot.guilds)}** guilds"
|
||||
desc = (
|
||||
f"All systems online"
|
||||
f"\nConnected to **{len(self.bot.guilds)}** guilds"
|
||||
f"\nListening for **{len(self.bot.application_commands)}** commands"
|
||||
)
|
||||
color = "#3498db"
|
||||
fields = []
|
||||
uptime = int(self.bot.start_time.timestamp())
|
||||
|
||||
fields.append(EmbedField(name="Version", value=jconst.__version__, inline=True))
|
||||
fields.append(EmbedField(name="naff", value=const.__version__, inline=True))
|
||||
fields.append(EmbedField(name="Git Hash", value=get_repo_hash()[:7], inline=True))
|
||||
fields.append(
|
||||
EmbedField(
|
||||
name="Version",
|
||||
value=f"[{jconst.__version__}](https://git.zevaryx.com/stark-industries/jarvis/jarvis-bot)",
|
||||
inline=True,
|
||||
)
|
||||
)
|
||||
fields.append(
|
||||
EmbedField(name="NAFF", value=f"[{const.__version__}](https://naff.info)", inline=True)
|
||||
)
|
||||
repo_url = (
|
||||
f"https://git.zevaryx.com/stark-industries/jarvis/jarvis-bot/-/tree/{get_repo_hash()}"
|
||||
)
|
||||
fields.append(
|
||||
EmbedField(name="Git Hash", value=f"[{get_repo_hash()[:7]}]({repo_url})", inline=True)
|
||||
)
|
||||
fields.append(EmbedField(name="Online Since", value=f"<t:{uptime}:F>", inline=False))
|
||||
num_domains = len(self.bot.phishing_domains)
|
||||
fields.append(
|
||||
|
@ -74,9 +94,9 @@ class UtilCog(Extension):
|
|||
)
|
||||
await ctx.send(embeds=embed, components=components)
|
||||
|
||||
@slash_command(
|
||||
name="logo",
|
||||
description="Get the current logo",
|
||||
@bot.subcommand(
|
||||
sub_cmd_name="logo",
|
||||
sub_cmd_description="Get the current logo",
|
||||
)
|
||||
@cooldown(bucket=Buckets.CHANNEL, rate=1, interval=30)
|
||||
async def _logo(self, ctx: InteractionContext) -> None:
|
||||
|
@ -89,13 +109,15 @@ class UtilCog(Extension):
|
|||
)
|
||||
await ctx.send(file=logo, components=components)
|
||||
|
||||
@slash_command(name="rchk", description="Robot Camo HK416")
|
||||
rc = SlashCommand(name="rc", description="Robot Camo emoji commands")
|
||||
|
||||
@rc.subcommand(sub_cmd_name="hk", sub_cmd_description="Robot Camo HK416")
|
||||
async def _rchk(self, ctx: InteractionContext) -> None:
|
||||
await ctx.send(content=hk, ephemeral=True)
|
||||
|
||||
@slash_command(
|
||||
name="rcauto",
|
||||
description="Automates robot camo letters",
|
||||
@rc.subcommand(
|
||||
sub_cmd_name="auto",
|
||||
sub_cmd_description="Automates robot camo letters",
|
||||
)
|
||||
@slash_option(
|
||||
name="text",
|
||||
|
@ -121,14 +143,6 @@ class UtilCog(Extension):
|
|||
else:
|
||||
await ctx.send(to_send, ephemeral=True)
|
||||
|
||||
@slash_command(name="avatar", description="Get a user avatar")
|
||||
@slash_option(
|
||||
name="user",
|
||||
description="User to view avatar of",
|
||||
opt_type=OptionTypes.USER,
|
||||
required=False,
|
||||
)
|
||||
@cooldown(bucket=Buckets.USER, rate=1, interval=5)
|
||||
async def _avatar(self, ctx: InteractionContext, user: User = None) -> None:
|
||||
if not user:
|
||||
user = ctx.author
|
||||
|
@ -176,7 +190,7 @@ class UtilCog(Extension):
|
|||
embed.set_thumbnail(url="attachment://color_show.png")
|
||||
|
||||
data = np.array(JARVIS_LOGO)
|
||||
r, g, b, a = data.T
|
||||
*_, a = data.T
|
||||
|
||||
fill = a > 0
|
||||
|
||||
|
@ -193,6 +207,21 @@ class UtilCog(Extension):
|
|||
)
|
||||
await ctx.send(embeds=embed, file=color_show, components=components)
|
||||
|
||||
@slash_command(name="avatar", description="Get a user avatar")
|
||||
@slash_option(
|
||||
name="user",
|
||||
description="User to view avatar of",
|
||||
opt_type=OptionTypes.USER,
|
||||
required=False,
|
||||
)
|
||||
@cooldown(bucket=Buckets.USER, rate=1, interval=5)
|
||||
async def _avatar_slash(self, ctx: InteractionContext, user: User = None) -> None:
|
||||
await self._userinfo(ctx, user)
|
||||
|
||||
@context_menu(name="Avatar", context_type=CommandTypes.USER)
|
||||
async def _avatar_menu(self, ctx: InteractionContext) -> None:
|
||||
await self._avatar(ctx, ctx.target)
|
||||
|
||||
async def _userinfo(self, ctx: InteractionContext, user: User = None) -> None:
|
||||
await ctx.defer()
|
||||
if not user:
|
||||
|
@ -397,7 +426,7 @@ class UtilCog(Extension):
|
|||
)
|
||||
await ctx.send(embeds=embed, ephemeral=private, components=components)
|
||||
|
||||
@slash_command(name="support", description="Got issues?")
|
||||
@bot.subcommand(sub_cmd_name="support", sub_cmd_description="Got issues?")
|
||||
async def _support(self, ctx: InteractionContext) -> None:
|
||||
await ctx.send(
|
||||
f"""
|
||||
|
@ -409,7 +438,9 @@ We'll help as best we can with whatever issues you encounter.
|
|||
"""
|
||||
)
|
||||
|
||||
@slash_command(name="privacy_terms", description="View Privacy and Terms of Use")
|
||||
@bot.subcommand(
|
||||
sub_cmd_name="privacy_terms", sub_cmd_description="View Privacy and Terms of Use"
|
||||
)
|
||||
async def _privacy_terms(self, ctx: InteractionContext) -> None:
|
||||
await ctx.send(
|
||||
"""
|
||||
|
|
|
@ -1,257 +1,369 @@
|
|||
"""dbrand-specific data."""
|
||||
shipping_lookup = [
|
||||
{"country": "afghanistan", "code": "AF"},
|
||||
{"country": "albania", "code": "AL"},
|
||||
{"country": "algeria", "code": "DZ"},
|
||||
{"country": "american samoa", "code": "AS"},
|
||||
{"country": "andorra", "code": "AD"},
|
||||
{"country": "angola", "code": "AO"},
|
||||
{"country": "anguilla", "code": "AI"},
|
||||
{"country": "antarctica", "code": "AQ"},
|
||||
{"country": "antigua and barbuda", "code": "AG"},
|
||||
{"country": "argentina", "code": "AR"},
|
||||
{"country": "armenia", "code": "AM"},
|
||||
{"country": "aruba", "code": "AW"},
|
||||
{"country": "australia", "code": "AU"},
|
||||
{"country": "austria", "code": "AT"},
|
||||
{"country": "azerbaijan", "code": "AZ"},
|
||||
{"country": "bahamas (the)", "code": "BS"},
|
||||
{"country": "bahrain", "code": "BH"},
|
||||
{"country": "bangladesh", "code": "BD"},
|
||||
{"country": "barbados", "code": "BB"},
|
||||
{"country": "belarus", "code": "BY"},
|
||||
{"country": "belgium", "code": "BE"},
|
||||
{"country": "belize", "code": "BZ"},
|
||||
{"country": "benin", "code": "BJ"},
|
||||
{"country": "bermuda", "code": "BM"},
|
||||
{"country": "bhutan", "code": "BT"},
|
||||
{"country": "bolivia (plurinational state of)", "code": "BO"},
|
||||
{"country": "bonaire, sint eustatius and saba", "code": "BQ"},
|
||||
{"country": "bosnia and herzegovina", "code": "BA"},
|
||||
{"country": "botswana", "code": "BW"},
|
||||
{"country": "bouvet island", "code": "BV"},
|
||||
{"country": "brazil", "code": "BR"},
|
||||
{"country": "british indian ocean territory (the)", "code": "IO"},
|
||||
{"country": "brunei darussalam", "code": "BN"},
|
||||
{"country": "bulgaria", "code": "BG"},
|
||||
{"country": "burkina faso", "code": "BF"},
|
||||
{"country": "burundi", "code": "BI"},
|
||||
{"country": "cabo verde", "code": "CV"},
|
||||
{"country": "cambodia", "code": "KH"},
|
||||
{"country": "cameroon", "code": "CM"},
|
||||
{"country": "canada", "code": "CA"},
|
||||
{"country": "cayman islands (the)", "code": "KY"},
|
||||
{"country": "central african republic (the)", "code": "CF"},
|
||||
{"country": "chad", "code": "TD"},
|
||||
{"country": "chile", "code": "CL"},
|
||||
{"country": "china", "code": "CN"},
|
||||
{"country": "christmas island", "code": "CX"},
|
||||
{"country": "cocos (keeling) islands (the)", "code": "CC"},
|
||||
{"country": "colombia", "code": "CO"},
|
||||
{"country": "comoros (the)", "code": "KM"},
|
||||
{"country": "congo (the democratic republic of the)", "code": "CD"},
|
||||
{"country": "congo (the)", "code": "CG"},
|
||||
{"country": "cook islands (the)", "code": "CK"},
|
||||
{"country": "costa rica", "code": "CR"},
|
||||
{"country": "croatia", "code": "HR"},
|
||||
{"country": "cuba", "code": "CU"},
|
||||
{"country": "curaçao", "code": "CW"},
|
||||
{"country": "cyprus", "code": "CY"},
|
||||
{"country": "czechia", "code": "CZ"},
|
||||
{"country": "côte d'ivoire", "code": "CI"},
|
||||
{"country": "denmark", "code": "DK"},
|
||||
{"country": "djibouti", "code": "DJ"},
|
||||
{"country": "dominica", "code": "DM"},
|
||||
{"country": "dominican republic (the)", "code": "DO"},
|
||||
{"country": "ecuador", "code": "EC"},
|
||||
{"country": "egypt", "code": "EG"},
|
||||
{"country": "el salvador", "code": "SV"},
|
||||
{"country": "equatorial guinea", "code": "GQ"},
|
||||
{"country": "eritrea", "code": "ER"},
|
||||
{"country": "estonia", "code": "EE"},
|
||||
{"country": "eswatini", "code": "SZ"},
|
||||
{"country": "ethiopia", "code": "ET"},
|
||||
{"country": "falkland islands (the) [malvinas]", "code": "FK"},
|
||||
{"country": "faroe islands (the)", "code": "FO"},
|
||||
{"country": "fiji", "code": "FJ"},
|
||||
{"country": "finland", "code": "FI"},
|
||||
{"country": "france", "code": "FR"},
|
||||
{"country": "french guiana", "code": "GF"},
|
||||
{"country": "french polynesia", "code": "PF"},
|
||||
{"country": "french southern territories (the)", "code": "TF"},
|
||||
{"country": "gabon", "code": "GA"},
|
||||
{"country": "gambia (the)", "code": "GM"},
|
||||
{"country": "georgia", "code": "GE"},
|
||||
{"country": "germany", "code": "DE"},
|
||||
{"country": "ghana", "code": "GH"},
|
||||
{"country": "gibraltar", "code": "GI"},
|
||||
{"country": "greece", "code": "GR"},
|
||||
{"country": "greenland", "code": "GL"},
|
||||
{"country": "grenada", "code": "GD"},
|
||||
{"country": "guadeloupe", "code": "GP"},
|
||||
{"country": "guam", "code": "GU"},
|
||||
{"country": "guatemala", "code": "GT"},
|
||||
{"country": "guernsey", "code": "GG"},
|
||||
{"country": "guinea", "code": "GN"},
|
||||
{"country": "guinea-bissau", "code": "GW"},
|
||||
{"country": "guyana", "code": "GY"},
|
||||
{"country": "haiti", "code": "HT"},
|
||||
{"country": "heard island and mcdonald islands", "code": "HM"},
|
||||
{"country": "holy see (the)", "code": "VA"},
|
||||
{"country": "honduras", "code": "HN"},
|
||||
{"country": "hong kong", "code": "HK"},
|
||||
{"country": "hungary", "code": "HU"},
|
||||
{"country": "iceland", "code": "IS"},
|
||||
{"country": "india", "code": "IN"},
|
||||
{"country": "indonesia", "code": "ID"},
|
||||
{"country": "iran (islamic republic of)", "code": "IR"},
|
||||
{"country": "iraq", "code": "IQ"},
|
||||
{"country": "ireland", "code": "IE"},
|
||||
{"country": "isle of man", "code": "IM"},
|
||||
{"country": "israel", "code": "IL"},
|
||||
{"country": "italy", "code": "IT"},
|
||||
{"country": "jamaica", "code": "JM"},
|
||||
{"country": "japan", "code": "JP"},
|
||||
{"country": "jersey", "code": "JE"},
|
||||
{"country": "jordan", "code": "JO"},
|
||||
{"country": "kazakhstan", "code": "KZ"},
|
||||
{"country": "kenya", "code": "KE"},
|
||||
{"country": "kiribati", "code": "KI"},
|
||||
{"country": "korea (the democratic people's republic of)", "code": "KP"},
|
||||
{"country": "korea (the republic of)", "code": "KR"},
|
||||
{"country": "kuwait", "code": "KW"},
|
||||
{"country": "kyrgyzstan", "code": "KG"},
|
||||
{"country": "lao people's democratic republic (the)", "code": "LA"},
|
||||
{"country": "latvia", "code": "LV"},
|
||||
{"country": "lebanon", "code": "LB"},
|
||||
{"country": "lesotho", "code": "LS"},
|
||||
{"country": "liberia", "code": "LR"},
|
||||
{"country": "libya", "code": "LY"},
|
||||
{"country": "liechtenstein", "code": "LI"},
|
||||
{"country": "lithuania", "code": "LT"},
|
||||
{"country": "luxembourg", "code": "LU"},
|
||||
{"country": "macao", "code": "MO"},
|
||||
{"country": "madagascar", "code": "MG"},
|
||||
{"country": "malawi", "code": "MW"},
|
||||
{"country": "malaysia", "code": "MY"},
|
||||
{"country": "maldives", "code": "MV"},
|
||||
{"country": "mali", "code": "ML"},
|
||||
{"country": "malta", "code": "MT"},
|
||||
{"country": "marshall islands (the)", "code": "MH"},
|
||||
{"country": "martinique", "code": "MQ"},
|
||||
{"country": "mauritania", "code": "MR"},
|
||||
{"country": "mauritius", "code": "MU"},
|
||||
{"country": "mayotte", "code": "YT"},
|
||||
{"country": "mexico", "code": "MX"},
|
||||
{"country": "micronesia (federated states of)", "code": "FM"},
|
||||
{"country": "moldova (the republic of)", "code": "MD"},
|
||||
{"country": "monaco", "code": "MC"},
|
||||
{"country": "mongolia", "code": "MN"},
|
||||
{"country": "montenegro", "code": "ME"},
|
||||
{"country": "montserrat", "code": "MS"},
|
||||
{"country": "morocco", "code": "MA"},
|
||||
{"country": "mozambique", "code": "MZ"},
|
||||
{"country": "myanmar", "code": "MM"},
|
||||
{"country": "namibia", "code": "NA"},
|
||||
{"country": "nauru", "code": "NR"},
|
||||
{"country": "nepal", "code": "NP"},
|
||||
{"country": "netherlands (the)", "code": "NL"},
|
||||
{"country": "new caledonia", "code": "NC"},
|
||||
{"country": "new zealand", "code": "NZ"},
|
||||
{"country": "nicaragua", "code": "NI"},
|
||||
{"country": "niger (the)", "code": "NE"},
|
||||
{"country": "nigeria", "code": "NG"},
|
||||
{"country": "niue", "code": "NU"},
|
||||
{"country": "norfolk island", "code": "NF"},
|
||||
{"country": "northern mariana islands (the)", "code": "MP"},
|
||||
{"country": "norway", "code": "NO"},
|
||||
{"country": "oman", "code": "OM"},
|
||||
{"country": "pakistan", "code": "PK"},
|
||||
{"country": "palau", "code": "PW"},
|
||||
{"country": "palestine, state of", "code": "PS"},
|
||||
{"country": "panama", "code": "PA"},
|
||||
{"country": "papua new guinea", "code": "PG"},
|
||||
{"country": "paraguay", "code": "PY"},
|
||||
{"country": "peru", "code": "PE"},
|
||||
{"country": "philippines (the)", "code": "PH"},
|
||||
{"country": "pitcairn", "code": "PN"},
|
||||
{"country": "poland", "code": "PL"},
|
||||
{"country": "portugal", "code": "PT"},
|
||||
{"country": "puerto rico", "code": "PR"},
|
||||
{"country": "qatar", "code": "QA"},
|
||||
{"country": "republic of north macedonia", "code": "MK"},
|
||||
{"country": "romania", "code": "RO"},
|
||||
{"country": "russian federation (the)", "code": "RU"},
|
||||
{"country": "rwanda", "code": "RW"},
|
||||
{"country": "réunion", "code": "RE"},
|
||||
{"country": "saint barthélemy", "code": "BL"},
|
||||
{"country": "saint helena, ascension and tristan da cunha", "code": "SH"},
|
||||
{"country": "saint kitts and nevis", "code": "KN"},
|
||||
{"country": "saint lucia", "code": "LC"},
|
||||
{"country": "saint martin (french part)", "code": "MF"},
|
||||
{"country": "saint pierre and miquelon", "code": "PM"},
|
||||
{"country": "saint vincent and the grenadines", "code": "VC"},
|
||||
{"country": "samoa", "code": "WS"},
|
||||
{"country": "san marino", "code": "SM"},
|
||||
{"country": "sao tome and principe", "code": "ST"},
|
||||
{"country": "saudi arabia", "code": "SA"},
|
||||
{"country": "senegal", "code": "SN"},
|
||||
{"country": "serbia", "code": "RS"},
|
||||
{"country": "seychelles", "code": "SC"},
|
||||
{"country": "sierra leone", "code": "SL"},
|
||||
{"country": "singapore", "code": "SG"},
|
||||
{"country": "sint maarten (dutch part)", "code": "SX"},
|
||||
{"country": "slovakia", "code": "SK"},
|
||||
{"country": "slovenia", "code": "SI"},
|
||||
{"country": "solomon islands", "code": "SB"},
|
||||
{"country": "somalia", "code": "SO"},
|
||||
{"country": "south africa", "code": "ZA"},
|
||||
{"country": "south georgia and the south sandwich islands", "code": "GS"},
|
||||
{"country": "south sudan", "code": "SS"},
|
||||
{"country": "spain", "code": "ES"},
|
||||
{"country": "sri lanka", "code": "LK"},
|
||||
{"country": "sudan (the)", "code": "SD"},
|
||||
{"country": "suriname", "code": "SR"},
|
||||
{"country": "svalbard and jan mayen", "code": "SJ"},
|
||||
{"country": "sweden", "code": "SE"},
|
||||
{"country": "switzerland", "code": "CH"},
|
||||
{"country": "syrian arab republic", "code": "SY"},
|
||||
{"country": "taiwan", "code": "TW"},
|
||||
{"country": "tajikistan", "code": "TJ"},
|
||||
{"country": "tanzania, united republic of", "code": "TZ"},
|
||||
{"country": "thailand", "code": "TH"},
|
||||
{"country": "timor-leste", "code": "TL"},
|
||||
{"country": "togo", "code": "TG"},
|
||||
{"country": "tokelau", "code": "TK"},
|
||||
{"country": "tonga", "code": "TO"},
|
||||
{"country": "trinidad and tobago", "code": "TT"},
|
||||
{"country": "tunisia", "code": "TN"},
|
||||
{"country": "turkey", "code": "TR"},
|
||||
{"country": "turkmenistan", "code": "TM"},
|
||||
{"country": "turks and caicos islands (the)", "code": "TC"},
|
||||
{"country": "tuvalu", "code": "TV"},
|
||||
{"country": "uganda", "code": "UG"},
|
||||
{"country": "ukraine", "code": "UA"},
|
||||
{"country": "united arab emirates (the)", "code": "AE"},
|
||||
{"country": "Afghanistan", "alpha-2": "AF", "alpha-3": "AFG", "numeric": "0004"},
|
||||
{"country": "Ã…land Islands", "alpha-2": "AX", "alpha-3": "ALA", "numeric": "0248"},
|
||||
{"country": "Albania", "alpha-2": "AL", "alpha-3": "ALB", "numeric": "0008"},
|
||||
{"country": "Algeria", "alpha-2": "DZ", "alpha-3": "DZA", "numeric": "0012"},
|
||||
{"country": "American Samoa", "alpha-2": "AS", "alpha-3": "ASM", "numeric": "0016"},
|
||||
{"country": "Andorra", "alpha-2": "AD", "alpha-3": "AND", "numeric": "0020"},
|
||||
{"country": "Angola", "alpha-2": "AO", "alpha-3": "AGO", "numeric": "0024"},
|
||||
{"country": "Anguilla", "alpha-2": "AI", "alpha-3": "AIA", "numeric": "0660"},
|
||||
{"country": "Antarctica", "alpha-2": "AQ", "alpha-3": "ATA", "numeric": "0010"},
|
||||
{"country": "Antigua and Barbuda", "alpha-2": "AG", "alpha-3": "ATG", "numeric": "0028"},
|
||||
{"country": "Argentina", "alpha-2": "AR", "alpha-3": "ARG", "numeric": "0032"},
|
||||
{"country": "Armenia", "alpha-2": "AM", "alpha-3": "ARM", "numeric": "0051"},
|
||||
{"country": "Aruba", "alpha-2": "AW", "alpha-3": "ABW", "numeric": "0533"},
|
||||
{"country": "Australia", "alpha-2": "AU", "alpha-3": "AUS", "numeric": "0036"},
|
||||
{"country": "Austria", "alpha-2": "AT", "alpha-3": "AUT", "numeric": "0040"},
|
||||
{"country": "Azerbaijan", "alpha-2": "AZ", "alpha-3": "AZE", "numeric": "0031"},
|
||||
{"country": "Bahamas (the)", "alpha-2": "BS", "alpha-3": "BHS", "numeric": "0044"},
|
||||
{"country": "Bahrain", "alpha-2": "BH", "alpha-3": "BHR", "numeric": "0048"},
|
||||
{"country": "Bangladesh", "alpha-2": "BD", "alpha-3": "BGD", "numeric": "0050"},
|
||||
{"country": "Barbados", "alpha-2": "BB", "alpha-3": "BRB", "numeric": "0052"},
|
||||
{"country": "Belarus", "alpha-2": "BY", "alpha-3": "BLR", "numeric": "0112"},
|
||||
{"country": "Belgium", "alpha-2": "BE", "alpha-3": "BEL", "numeric": "0056"},
|
||||
{"country": "Belize", "alpha-2": "BZ", "alpha-3": "BLZ", "numeric": "0084"},
|
||||
{"country": "Benin", "alpha-2": "BJ", "alpha-3": "BEN", "numeric": "0204"},
|
||||
{"country": "Bermuda", "alpha-2": "BM", "alpha-3": "BMU", "numeric": "0060"},
|
||||
{"country": "Bhutan", "alpha-2": "BT", "alpha-3": "BTN", "numeric": "0064"},
|
||||
{
|
||||
"country": "united kingdom of great britain and northern ireland (the)",
|
||||
"code": "GB",
|
||||
"country": "Bolivia (Plurinational State of)",
|
||||
"alpha-2": "BO",
|
||||
"alpha-3": "BOL",
|
||||
"numeric": "0068",
|
||||
},
|
||||
{"country": "the united states of america", "code": "US"},
|
||||
{"country": "united states minor outlying islands (the)", "code": "UM"},
|
||||
{"country": "uruguay", "code": "UY"},
|
||||
{"country": "uzbekistan", "code": "UZ"},
|
||||
{"country": "vanuatu", "code": "VU"},
|
||||
{"country": "venezuela (bolivarian republic of)", "code": "VE"},
|
||||
{"country": "viet nam", "code": "VN"},
|
||||
{"country": "virgin islands (british)", "code": "VG"},
|
||||
{"country": "virgin islands (u.s.)", "code": "VI"},
|
||||
{"country": "wallis and futuna", "code": "WF"},
|
||||
{"country": "western sahara", "code": "EH"},
|
||||
{"country": "yemen", "code": "YE"},
|
||||
{"country": "zambia", "code": "ZM"},
|
||||
{"country": "zimbabwe", "code": "ZW"},
|
||||
{"country": "åland islands", "code": "AX"},
|
||||
{
|
||||
"country": "Bonaire, Sint Eustatius and Saba",
|
||||
"alpha-2": "BQ",
|
||||
"alpha-3": "BES",
|
||||
"numeric": "0535",
|
||||
},
|
||||
{"country": "Bosnia and Herzegovina", "alpha-2": "BA", "alpha-3": "BIH", "numeric": "0070"},
|
||||
{"country": "Botswana", "alpha-2": "BW", "alpha-3": "BWA", "numeric": "0072"},
|
||||
{"country": "Bouvet Island", "alpha-2": "BV", "alpha-3": "BVT", "numeric": "0074"},
|
||||
{"country": "Brazil", "alpha-2": "BR", "alpha-3": "BRA", "numeric": "0076"},
|
||||
{
|
||||
"country": "British Indian Ocean Territory (the)",
|
||||
"alpha-2": "IO",
|
||||
"alpha-3": "IOT",
|
||||
"numeric": "0086",
|
||||
},
|
||||
{"country": "Brunei Darussalam", "alpha-2": "BN", "alpha-3": "BRN", "numeric": "0096"},
|
||||
{"country": "Bulgaria", "alpha-2": "BG", "alpha-3": "BGR", "numeric": "0100"},
|
||||
{"country": "Burkina Faso", "alpha-2": "BF", "alpha-3": "BFA", "numeric": "0854"},
|
||||
{"country": "Burundi", "alpha-2": "BI", "alpha-3": "BDI", "numeric": "0108"},
|
||||
{"country": "Cabo Verde", "alpha-2": "CV", "alpha-3": "CPV", "numeric": "0132"},
|
||||
{"country": "Cambodia", "alpha-2": "KH", "alpha-3": "KHM", "numeric": "0116"},
|
||||
{"country": "Cameroon", "alpha-2": "CM", "alpha-3": "CMR", "numeric": "0120"},
|
||||
{"country": "Canada", "alpha-2": "CA", "alpha-3": "CAN", "numeric": "0124"},
|
||||
{"country": "Cayman Islands (the)", "alpha-2": "KY", "alpha-3": "CYM", "numeric": "0136"},
|
||||
{
|
||||
"country": "Central African Republic (the)",
|
||||
"alpha-2": "CF",
|
||||
"alpha-3": "CAF",
|
||||
"numeric": "0140",
|
||||
},
|
||||
{"country": "Chad", "alpha-2": "TD", "alpha-3": "TCD", "numeric": "0148"},
|
||||
{"country": "Chile", "alpha-2": "CL", "alpha-3": "CHL", "numeric": "0152"},
|
||||
{"country": "China", "alpha-2": "CN", "alpha-3": "CHN", "numeric": "0156"},
|
||||
{"country": "Christmas Island", "alpha-2": "CX", "alpha-3": "CXR", "numeric": "0162"},
|
||||
{
|
||||
"country": "Cocos (Keeling) Islands (the)",
|
||||
"alpha-2": "CC",
|
||||
"alpha-3": "CCK",
|
||||
"numeric": "0166",
|
||||
},
|
||||
{"country": "Colombia", "alpha-2": "CO", "alpha-3": "COL", "numeric": "0170"},
|
||||
{"country": "Comoros (the)", "alpha-2": "KM", "alpha-3": "COM", "numeric": "0174"},
|
||||
{
|
||||
"country": "Congo (the Democratic Republic of the)",
|
||||
"alpha-2": "CD",
|
||||
"alpha-3": "COD",
|
||||
"numeric": "0180",
|
||||
},
|
||||
{"country": "Congo (the)", "alpha-2": "CG", "alpha-3": "COG", "numeric": "0178"},
|
||||
{"country": "Cook Islands (the)", "alpha-2": "CK", "alpha-3": "COK", "numeric": "0184"},
|
||||
{"country": "Costa Rica", "alpha-2": "CR", "alpha-3": "CRI", "numeric": "0188"},
|
||||
{"country": "Côte d'Ivoire", "alpha-2": "CI", "alpha-3": "CIV", "numeric": "0384"},
|
||||
{"country": "Croatia", "alpha-2": "HR", "alpha-3": "HRV", "numeric": "0191"},
|
||||
{"country": "Cuba", "alpha-2": "CU", "alpha-3": "CUB", "numeric": "0192"},
|
||||
{"country": "Curaçao", "alpha-2": "CW", "alpha-3": "CUW", "numeric": "0531"},
|
||||
{"country": "Cyprus", "alpha-2": "CY", "alpha-3": "CYP", "numeric": "0196"},
|
||||
{"country": "Czechia", "alpha-2": "CZ", "alpha-3": "CZE", "numeric": "0203"},
|
||||
{"country": "Denmark", "alpha-2": "DK", "alpha-3": "DNK", "numeric": "0208"},
|
||||
{"country": "Djibouti", "alpha-2": "DJ", "alpha-3": "DJI", "numeric": "0262"},
|
||||
{"country": "Dominica", "alpha-2": "DM", "alpha-3": "DMA", "numeric": "0212"},
|
||||
{"country": "Dominican Republic (the)", "alpha-2": "DO", "alpha-3": "DOM", "numeric": "0214"},
|
||||
{"country": "Ecuador", "alpha-2": "EC", "alpha-3": "ECU", "numeric": "0218"},
|
||||
{"country": "Egypt", "alpha-2": "EG", "alpha-3": "EGY", "numeric": "0818"},
|
||||
{"country": "El Salvador", "alpha-2": "SV", "alpha-3": "SLV", "numeric": "0222"},
|
||||
{"country": "Equatorial Guinea", "alpha-2": "GQ", "alpha-3": "GNQ", "numeric": "0226"},
|
||||
{"country": "Eritrea", "alpha-2": "ER", "alpha-3": "ERI", "numeric": "0232"},
|
||||
{"country": "Estonia", "alpha-2": "EE", "alpha-3": "EST", "numeric": "0233"},
|
||||
{"country": "Eswatini", "alpha-2": "SZ", "alpha-3": "SWZ", "numeric": "0748"},
|
||||
{"country": "Ethiopia", "alpha-2": "ET", "alpha-3": "ETH", "numeric": "0231"},
|
||||
{
|
||||
"country": "Falkland Islands (the) [Malvinas]",
|
||||
"alpha-2": "FK",
|
||||
"alpha-3": "FLK",
|
||||
"numeric": "0238",
|
||||
},
|
||||
{"country": "Faroe Islands (the)", "alpha-2": "FO", "alpha-3": "FRO", "numeric": "0234"},
|
||||
{"country": "Fiji", "alpha-2": "FJ", "alpha-3": "FJI", "numeric": "0242"},
|
||||
{"country": "Finland", "alpha-2": "FI", "alpha-3": "FIN", "numeric": "0246"},
|
||||
{"country": "France", "alpha-2": "FR", "alpha-3": "FRA", "numeric": "0250"},
|
||||
{"country": "French Guiana", "alpha-2": "GF", "alpha-3": "GUF", "numeric": "0254"},
|
||||
{"country": "French Polynesia", "alpha-2": "PF", "alpha-3": "PYF", "numeric": "0258"},
|
||||
{
|
||||
"country": "French Southern Territories (the)",
|
||||
"alpha-2": "TF",
|
||||
"alpha-3": "ATF",
|
||||
"numeric": "0260",
|
||||
},
|
||||
{"country": "Gabon", "alpha-2": "GA", "alpha-3": "GAB", "numeric": "0266"},
|
||||
{"country": "Gambia (the)", "alpha-2": "GM", "alpha-3": "GMB", "numeric": "0270"},
|
||||
{"country": "Georgia", "alpha-2": "GE", "alpha-3": "GEO", "numeric": "0268"},
|
||||
{"country": "Germany", "alpha-2": "DE", "alpha-3": "DEU", "numeric": "0276"},
|
||||
{"country": "Ghana", "alpha-2": "GH", "alpha-3": "GHA", "numeric": "0288"},
|
||||
{"country": "Gibraltar", "alpha-2": "GI", "alpha-3": "GIB", "numeric": "0292"},
|
||||
{"country": "Greece", "alpha-2": "GR", "alpha-3": "GRC", "numeric": "0300"},
|
||||
{"country": "Greenland", "alpha-2": "GL", "alpha-3": "GRL", "numeric": "0304"},
|
||||
{"country": "Grenada", "alpha-2": "GD", "alpha-3": "GRD", "numeric": "0308"},
|
||||
{"country": "Guadeloupe", "alpha-2": "GP", "alpha-3": "GLP", "numeric": "0312"},
|
||||
{"country": "Guam", "alpha-2": "GU", "alpha-3": "GUM", "numeric": "0316"},
|
||||
{"country": "Guatemala", "alpha-2": "GT", "alpha-3": "GTM", "numeric": "0320"},
|
||||
{"country": "Guernsey", "alpha-2": "GG", "alpha-3": "GGY", "numeric": "0831"},
|
||||
{"country": "Guinea", "alpha-2": "GN", "alpha-3": "GIN", "numeric": "0324"},
|
||||
{"country": "Guinea-Bissau", "alpha-2": "GW", "alpha-3": "GNB", "numeric": "0624"},
|
||||
{"country": "Guyana", "alpha-2": "GY", "alpha-3": "GUY", "numeric": "0328"},
|
||||
{"country": "Haiti", "alpha-2": "HT", "alpha-3": "HTI", "numeric": "0332"},
|
||||
{
|
||||
"country": "Heard Island and McDonald Islands",
|
||||
"alpha-2": "HM",
|
||||
"alpha-3": "HMD",
|
||||
"numeric": "0334",
|
||||
},
|
||||
{"country": "Holy See (the)", "alpha-2": "VA", "alpha-3": "VAT", "numeric": "0336"},
|
||||
{"country": "Honduras", "alpha-2": "HN", "alpha-3": "HND", "numeric": "0340"},
|
||||
{"country": "Hong Kong", "alpha-2": "HK", "alpha-3": "HKG", "numeric": "0344"},
|
||||
{"country": "Hungary", "alpha-2": "HU", "alpha-3": "HUN", "numeric": "0348"},
|
||||
{"country": "Iceland", "alpha-2": "IS", "alpha-3": "ISL", "numeric": "0352"},
|
||||
{"country": "India", "alpha-2": "IN", "alpha-3": "IND", "numeric": "0356"},
|
||||
{"country": "Indonesia", "alpha-2": "ID", "alpha-3": "IDN", "numeric": "0360"},
|
||||
{"country": "Iran (Islamic Republic of)", "alpha-2": "IR", "alpha-3": "IRN", "numeric": "0364"},
|
||||
{"country": "Iraq", "alpha-2": "IQ", "alpha-3": "IRQ", "numeric": "0368"},
|
||||
{"country": "Ireland", "alpha-2": "IE", "alpha-3": "IRL", "numeric": "0372"},
|
||||
{"country": "Isle of Man", "alpha-2": "IM", "alpha-3": "IMN", "numeric": "0833"},
|
||||
{"country": "Israel", "alpha-2": "IL", "alpha-3": "ISR", "numeric": "0376"},
|
||||
{"country": "Italy", "alpha-2": "IT", "alpha-3": "ITA", "numeric": "0380"},
|
||||
{"country": "Jamaica", "alpha-2": "JM", "alpha-3": "JAM", "numeric": "0388"},
|
||||
{"country": "Japan", "alpha-2": "JP", "alpha-3": "JPN", "numeric": "0392"},
|
||||
{"country": "Jersey", "alpha-2": "JE", "alpha-3": "JEY", "numeric": "0832"},
|
||||
{"country": "Jordan", "alpha-2": "JO", "alpha-3": "JOR", "numeric": "0400"},
|
||||
{"country": "Kazakhstan", "alpha-2": "KZ", "alpha-3": "KAZ", "numeric": "0398"},
|
||||
{"country": "Kenya", "alpha-2": "KE", "alpha-3": "KEN", "numeric": "0404"},
|
||||
{"country": "Kiribati", "alpha-2": "KI", "alpha-3": "KIR", "numeric": "0296"},
|
||||
{
|
||||
"country": "Korea (the Democratic People's Republic of)",
|
||||
"alpha-2": "KP",
|
||||
"alpha-3": "PRK",
|
||||
"numeric": "0408",
|
||||
},
|
||||
{"country": "Korea (the Republic of)", "alpha-2": "KR", "alpha-3": "KOR", "numeric": "0410"},
|
||||
{"country": "Kuwait", "alpha-2": "KW", "alpha-3": "KWT", "numeric": "0414"},
|
||||
{"country": "Kyrgyzstan", "alpha-2": "KG", "alpha-3": "KGZ", "numeric": "0417"},
|
||||
{
|
||||
"country": "Lao People's Democratic Republic (the)",
|
||||
"alpha-2": "LA",
|
||||
"alpha-3": "LAO",
|
||||
"numeric": "0418",
|
||||
},
|
||||
{"country": "Latvia", "alpha-2": "LV", "alpha-3": "LVA", "numeric": "0428"},
|
||||
{"country": "Lebanon", "alpha-2": "LB", "alpha-3": "LBN", "numeric": "0422"},
|
||||
{"country": "Lesotho", "alpha-2": "LS", "alpha-3": "LSO", "numeric": "0426"},
|
||||
{"country": "Liberia", "alpha-2": "LR", "alpha-3": "LBR", "numeric": "0430"},
|
||||
{"country": "Libya", "alpha-2": "LY", "alpha-3": "LBY", "numeric": "0434"},
|
||||
{"country": "Liechtenstein", "alpha-2": "LI", "alpha-3": "LIE", "numeric": "0438"},
|
||||
{"country": "Lithuania", "alpha-2": "LT", "alpha-3": "LTU", "numeric": "0440"},
|
||||
{"country": "Luxembourg", "alpha-2": "LU", "alpha-3": "LUX", "numeric": "0442"},
|
||||
{"country": "Macao", "alpha-2": "MO", "alpha-3": "MAC", "numeric": "0446"},
|
||||
{
|
||||
"country": "Republic of North Macedonia",
|
||||
"alpha-2": "MK",
|
||||
"alpha-3": "MKD",
|
||||
"numeric": "0807",
|
||||
},
|
||||
{"country": "Madagascar", "alpha-2": "MG", "alpha-3": "MDG", "numeric": "0450"},
|
||||
{"country": "Malawi", "alpha-2": "MW", "alpha-3": "MWI", "numeric": "0454"},
|
||||
{"country": "Malaysia", "alpha-2": "MY", "alpha-3": "MYS", "numeric": "0458"},
|
||||
{"country": "Maldives", "alpha-2": "MV", "alpha-3": "MDV", "numeric": "0462"},
|
||||
{"country": "Mali", "alpha-2": "ML", "alpha-3": "MLI", "numeric": "0466"},
|
||||
{"country": "Malta", "alpha-2": "MT", "alpha-3": "MLT", "numeric": "0470"},
|
||||
{"country": "Marshall Islands (the)", "alpha-2": "MH", "alpha-3": "MHL", "numeric": "0584"},
|
||||
{"country": "Martinique", "alpha-2": "MQ", "alpha-3": "MTQ", "numeric": "0474"},
|
||||
{"country": "Mauritania", "alpha-2": "MR", "alpha-3": "MRT", "numeric": "0478"},
|
||||
{"country": "Mauritius", "alpha-2": "MU", "alpha-3": "MUS", "numeric": "0480"},
|
||||
{"country": "Mayotte", "alpha-2": "YT", "alpha-3": "MYT", "numeric": "0175"},
|
||||
{"country": "Mexico", "alpha-2": "MX", "alpha-3": "MEX", "numeric": "0484"},
|
||||
{
|
||||
"country": "Micronesia (Federated States of)",
|
||||
"alpha-2": "FM",
|
||||
"alpha-3": "FSM",
|
||||
"numeric": "0583",
|
||||
},
|
||||
{"country": "Moldova (the Republic of)", "alpha-2": "MD", "alpha-3": "MDA", "numeric": "0498"},
|
||||
{"country": "Monaco", "alpha-2": "MC", "alpha-3": "MCO", "numeric": "0492"},
|
||||
{"country": "Mongolia", "alpha-2": "MN", "alpha-3": "MNG", "numeric": "0496"},
|
||||
{"country": "Montenegro", "alpha-2": "ME", "alpha-3": "MNE", "numeric": "0499"},
|
||||
{"country": "Montserrat", "alpha-2": "MS", "alpha-3": "MSR", "numeric": "0500"},
|
||||
{"country": "Morocco", "alpha-2": "MA", "alpha-3": "MAR", "numeric": "0504"},
|
||||
{"country": "Mozambique", "alpha-2": "MZ", "alpha-3": "MOZ", "numeric": "0508"},
|
||||
{"country": "Myanmar", "alpha-2": "MM", "alpha-3": "MMR", "numeric": "0104"},
|
||||
{"country": "Namibia", "alpha-2": "NA", "alpha-3": "NAM", "numeric": "0516"},
|
||||
{"country": "Nauru", "alpha-2": "NR", "alpha-3": "NRU", "numeric": "0520"},
|
||||
{"country": "Nepal", "alpha-2": "NP", "alpha-3": "NPL", "numeric": "0524"},
|
||||
{"country": "Netherlands (the)", "alpha-2": "NL", "alpha-3": "NLD", "numeric": "0528"},
|
||||
{"country": "New Caledonia", "alpha-2": "NC", "alpha-3": "NCL", "numeric": "0540"},
|
||||
{"country": "New Zealand", "alpha-2": "NZ", "alpha-3": "NZL", "numeric": "0554"},
|
||||
{"country": "Nicaragua", "alpha-2": "NI", "alpha-3": "NIC", "numeric": "0558"},
|
||||
{"country": "Niger (the)", "alpha-2": "NE", "alpha-3": "NER", "numeric": "0562"},
|
||||
{"country": "Nigeria", "alpha-2": "NG", "alpha-3": "NGA", "numeric": "0566"},
|
||||
{"country": "Niue", "alpha-2": "NU", "alpha-3": "NIU", "numeric": "0570"},
|
||||
{"country": "Norfolk Island", "alpha-2": "NF", "alpha-3": "NFK", "numeric": "0574"},
|
||||
{
|
||||
"country": "Northern Mariana Islands (the)",
|
||||
"alpha-2": "MP",
|
||||
"alpha-3": "MNP",
|
||||
"numeric": "0580",
|
||||
},
|
||||
{"country": "Norway", "alpha-2": "NO", "alpha-3": "NOR", "numeric": "0578"},
|
||||
{"country": "Oman", "alpha-2": "OM", "alpha-3": "OMN", "numeric": "0512"},
|
||||
{"country": "Pakistan", "alpha-2": "PK", "alpha-3": "PAK", "numeric": "0586"},
|
||||
{"country": "Palau", "alpha-2": "PW", "alpha-3": "PLW", "numeric": "0585"},
|
||||
{"country": "Palestine, State of", "alpha-2": "PS", "alpha-3": "PSE", "numeric": "0275"},
|
||||
{"country": "Panama", "alpha-2": "PA", "alpha-3": "PAN", "numeric": "0591"},
|
||||
{"country": "Papua New Guinea", "alpha-2": "PG", "alpha-3": "PNG", "numeric": "0598"},
|
||||
{"country": "Paraguay", "alpha-2": "PY", "alpha-3": "PRY", "numeric": "0600"},
|
||||
{"country": "Peru", "alpha-2": "PE", "alpha-3": "PER", "numeric": "0604"},
|
||||
{"country": "Philippines (the)", "alpha-2": "PH", "alpha-3": "PHL", "numeric": "0608"},
|
||||
{"country": "Pitcairn", "alpha-2": "PN", "alpha-3": "PCN", "numeric": "0612"},
|
||||
{"country": "Poland", "alpha-2": "PL", "alpha-3": "POL", "numeric": "0616"},
|
||||
{"country": "Portugal", "alpha-2": "PT", "alpha-3": "PRT", "numeric": "0620"},
|
||||
{"country": "Puerto Rico", "alpha-2": "PR", "alpha-3": "PRI", "numeric": "0630"},
|
||||
{"country": "Qatar", "alpha-2": "QA", "alpha-3": "QAT", "numeric": "0634"},
|
||||
{"country": "Réunion", "alpha-2": "RE", "alpha-3": "REU", "numeric": "0638"},
|
||||
{"country": "Romania", "alpha-2": "RO", "alpha-3": "ROU", "numeric": "0642"},
|
||||
{"country": "Russian Federation (the)", "alpha-2": "RU", "alpha-3": "RUS", "numeric": "0643"},
|
||||
{"country": "Rwanda", "alpha-2": "RW", "alpha-3": "RWA", "numeric": "0646"},
|
||||
{"country": "Saint Barthélemy", "alpha-2": "BL", "alpha-3": "BLM", "numeric": "0652"},
|
||||
{
|
||||
"country": "Saint Helena, Ascension and Tristan da Cunha",
|
||||
"alpha-2": "SH",
|
||||
"alpha-3": "SHN",
|
||||
"numeric": "0654",
|
||||
},
|
||||
{"country": "Saint Kitts and Nevis", "alpha-2": "KN", "alpha-3": "KNA", "numeric": "0659"},
|
||||
{"country": "Saint Lucia", "alpha-2": "LC", "alpha-3": "LCA", "numeric": "0662"},
|
||||
{"country": "Saint Martin (French part)", "alpha-2": "MF", "alpha-3": "MAF", "numeric": "0663"},
|
||||
{"country": "Saint Pierre and Miquelon", "alpha-2": "PM", "alpha-3": "SPM", "numeric": "0666"},
|
||||
{
|
||||
"country": "Saint Vincent and the Grenadines",
|
||||
"alpha-2": "VC",
|
||||
"alpha-3": "VCT",
|
||||
"numeric": "0670",
|
||||
},
|
||||
{"country": "Samoa", "alpha-2": "WS", "alpha-3": "WSM", "numeric": "0882"},
|
||||
{"country": "San Marino", "alpha-2": "SM", "alpha-3": "SMR", "numeric": "0674"},
|
||||
{"country": "Sao Tome and Principe", "alpha-2": "ST", "alpha-3": "STP", "numeric": "0678"},
|
||||
{"country": "Saudi Arabia", "alpha-2": "SA", "alpha-3": "SAU", "numeric": "0682"},
|
||||
{"country": "Senegal", "alpha-2": "SN", "alpha-3": "SEN", "numeric": "0686"},
|
||||
{"country": "Serbia", "alpha-2": "RS", "alpha-3": "SRB", "numeric": "0688"},
|
||||
{"country": "Seychelles", "alpha-2": "SC", "alpha-3": "SYC", "numeric": "0690"},
|
||||
{"country": "Sierra Leone", "alpha-2": "SL", "alpha-3": "SLE", "numeric": "0694"},
|
||||
{"country": "Singapore", "alpha-2": "SG", "alpha-3": "SGP", "numeric": "0702"},
|
||||
{"country": "Sint Maarten (Dutch part)", "alpha-2": "SX", "alpha-3": "SXM", "numeric": "0534"},
|
||||
{"country": "Slovakia", "alpha-2": "SK", "alpha-3": "SVK", "numeric": "0703"},
|
||||
{"country": "Slovenia", "alpha-2": "SI", "alpha-3": "SVN", "numeric": "0705"},
|
||||
{"country": "Solomon Islands", "alpha-2": "SB", "alpha-3": "SLB", "numeric": "0090"},
|
||||
{"country": "Somalia", "alpha-2": "SO", "alpha-3": "SOM", "numeric": "0706"},
|
||||
{"country": "South Africa", "alpha-2": "ZA", "alpha-3": "ZAF", "numeric": "0710"},
|
||||
{
|
||||
"country": "South Georgia and the South Sandwich Islands",
|
||||
"alpha-2": "GS",
|
||||
"alpha-3": "SGS",
|
||||
"numeric": "0239",
|
||||
},
|
||||
{"country": "South Sudan", "alpha-2": "SS", "alpha-3": "SSD", "numeric": "0728"},
|
||||
{"country": "Spain", "alpha-2": "ES", "alpha-3": "ESP", "numeric": "0724"},
|
||||
{"country": "Sri Lanka", "alpha-2": "LK", "alpha-3": "LKA", "numeric": "0144"},
|
||||
{"country": "Sudan (the)", "alpha-2": "SD", "alpha-3": "SDN", "numeric": "0729"},
|
||||
{"country": "Suriname", "alpha-2": "SR", "alpha-3": "SUR", "numeric": "0740"},
|
||||
{"country": "Svalbard and Jan Mayen", "alpha-2": "SJ", "alpha-3": "SJM", "numeric": "0744"},
|
||||
{"country": "Sweden", "alpha-2": "SE", "alpha-3": "SWE", "numeric": "0752"},
|
||||
{"country": "Switzerland", "alpha-2": "CH", "alpha-3": "CHE", "numeric": "0756"},
|
||||
{"country": "Syrian Arab Republic", "alpha-2": "SY", "alpha-3": "SYR", "numeric": "0760"},
|
||||
{"country": "Taiwan (Province of China)", "alpha-2": "TW", "alpha-3": "TWN", "numeric": "0158"},
|
||||
{"country": "Tajikistan", "alpha-2": "TJ", "alpha-3": "TJK", "numeric": "0762"},
|
||||
{
|
||||
"country": "Tanzania, United Republic of",
|
||||
"alpha-2": "TZ",
|
||||
"alpha-3": "TZA",
|
||||
"numeric": "0834",
|
||||
},
|
||||
{"country": "Thailand", "alpha-2": "TH", "alpha-3": "THA", "numeric": "0764"},
|
||||
{"country": "Timor-Leste", "alpha-2": "TL", "alpha-3": "TLS", "numeric": "0626"},
|
||||
{"country": "Togo", "alpha-2": "TG", "alpha-3": "TGO", "numeric": "0768"},
|
||||
{"country": "Tokelau", "alpha-2": "TK", "alpha-3": "TKL", "numeric": "0772"},
|
||||
{"country": "Tonga", "alpha-2": "TO", "alpha-3": "TON", "numeric": "0776"},
|
||||
{"country": "Trinidad and Tobago", "alpha-2": "TT", "alpha-3": "TTO", "numeric": "0780"},
|
||||
{"country": "Tunisia", "alpha-2": "TN", "alpha-3": "TUN", "numeric": "0788"},
|
||||
{"country": "Turkey", "alpha-2": "TR", "alpha-3": "TUR", "numeric": "0792"},
|
||||
{"country": "Turkmenistan", "alpha-2": "TM", "alpha-3": "TKM", "numeric": "0795"},
|
||||
{
|
||||
"country": "Turks and Caicos Islands (the)",
|
||||
"alpha-2": "TC",
|
||||
"alpha-3": "TCA",
|
||||
"numeric": "0796",
|
||||
},
|
||||
{"country": "Tuvalu", "alpha-2": "TV", "alpha-3": "TUV", "numeric": "0798"},
|
||||
{"country": "Uganda", "alpha-2": "UG", "alpha-3": "UGA", "numeric": "0800"},
|
||||
{"country": "Ukraine", "alpha-2": "UA", "alpha-3": "UKR", "numeric": "0804"},
|
||||
{"country": "United Arab Emirates (the)", "alpha-2": "AE", "alpha-3": "ARE", "numeric": "0784"},
|
||||
{
|
||||
"country": "United Kingdom of Great Britain and Northern Ireland (the)",
|
||||
"alpha-2": "GB",
|
||||
"alpha-3": "GBR",
|
||||
"numeric": "0826",
|
||||
},
|
||||
{
|
||||
"country": "United States Minor Outlying Islands (the)",
|
||||
"alpha-2": "UM",
|
||||
"alpha-3": "UMI",
|
||||
"numeric": "0581",
|
||||
},
|
||||
{
|
||||
"country": "United States of America (the)",
|
||||
"alpha-2": "US",
|
||||
"alpha-3": "USA",
|
||||
"numeric": "0840",
|
||||
},
|
||||
{"country": "Uruguay", "alpha-2": "UY", "alpha-3": "URY", "numeric": "0858"},
|
||||
{"country": "Uzbekistan", "alpha-2": "UZ", "alpha-3": "UZB", "numeric": "0860"},
|
||||
{"country": "Vanuatu", "alpha-2": "VU", "alpha-3": "VUT", "numeric": "0548"},
|
||||
{
|
||||
"country": "Venezuela (Bolivarian Republic of)",
|
||||
"alpha-2": "VE",
|
||||
"alpha-3": "VEN",
|
||||
"numeric": "0862",
|
||||
},
|
||||
{"country": "Viet Nam", "alpha-2": "VN", "alpha-3": "VNM", "numeric": "0704"},
|
||||
{"country": "Virgin Islands (British)", "alpha-2": "VG", "alpha-3": "VGB", "numeric": "0092"},
|
||||
{"country": "Virgin Islands (U.S.)", "alpha-2": "VI", "alpha-3": "VIR", "numeric": "0850"},
|
||||
{"country": "Wallis and Futuna", "alpha-2": "WF", "alpha-3": "WLF", "numeric": "0876"},
|
||||
{"country": "Western Sahara", "alpha-2": "EH", "alpha-3": "ESH", "numeric": "0732"},
|
||||
{"country": "Yemen", "alpha-2": "YE", "alpha-3": "YEM", "numeric": "0887"},
|
||||
{"country": "Zambia", "alpha-2": "ZM", "alpha-3": "ZMB", "numeric": "0894"},
|
||||
{"country": "Zimbabwe", "alpha-2": "ZW", "alpha-3": "ZWE", "numeric": "0716"},
|
||||
]
|
||||
|
||||
# TODO: Implement lookup for this. Currently not doable
|
||||
|
|
92
poetry.lock
generated
92
poetry.lock
generated
|
@ -172,10 +172,10 @@ aiohttp = "*"
|
|||
yarl = "*"
|
||||
|
||||
[package.extras]
|
||||
test = ["vcrpy (==4.0.2)", "testfixtures (>4.13.2,<7)", "pytest-vcr", "pytest", "mock (>=0.8)", "asynctest (>=0.13.0)"]
|
||||
lint = ["pydocstyle", "pre-commit", "flynt", "flake8", "black"]
|
||||
dev = ["vcrpy (==4.0.2)", "testfixtures (>4.13.2,<7)", "pytest-vcr", "pytest", "mock (>=0.8)", "asynctest (>=0.13.0)", "pydocstyle", "pre-commit", "flynt", "flake8", "black"]
|
||||
ci = ["coveralls"]
|
||||
dev = ["black", "flake8", "flynt", "pre-commit", "pydocstyle", "asynctest (>=0.13.0)", "mock (>=0.8)", "pytest", "pytest-vcr", "testfixtures (>4.13.2,<7)", "vcrpy (==4.0.2)"]
|
||||
lint = ["black", "flake8", "flynt", "pre-commit", "pydocstyle"]
|
||||
test = ["asynctest (>=0.13.0)", "mock (>=0.8)", "pytest", "pytest-vcr", "testfixtures (>4.13.2,<7)", "vcrpy (==4.0.2)"]
|
||||
|
||||
[[package]]
|
||||
name = "attrs"
|
||||
|
@ -191,6 +191,21 @@ docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"]
|
|||
tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "cloudpickle"]
|
||||
tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "cloudpickle"]
|
||||
|
||||
[[package]]
|
||||
name = "beautifulsoup4"
|
||||
version = "4.11.1"
|
||||
description = "Screen-scraping library"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.6.0"
|
||||
|
||||
[package.dependencies]
|
||||
soupsieve = ">1.2"
|
||||
|
||||
[package.extras]
|
||||
html5lib = ["html5lib"]
|
||||
lxml = ["lxml"]
|
||||
|
||||
[[package]]
|
||||
name = "black"
|
||||
version = "22.3.0"
|
||||
|
@ -291,7 +306,7 @@ optional = false
|
|||
python-versions = "*"
|
||||
|
||||
[package.extras]
|
||||
test = ["flake8 (==3.7.8)", "hypothesis (==3.55.3)"]
|
||||
test = ["hypothesis (==3.55.3)", "flake8 (==3.7.8)"]
|
||||
|
||||
[[package]]
|
||||
name = "dateparser"
|
||||
|
@ -308,9 +323,9 @@ regex = "<2019.02.19 || >2019.02.19,<2021.8.27 || >2021.8.27,<2022.3.15"
|
|||
tzlocal = "*"
|
||||
|
||||
[package.extras]
|
||||
calendars = ["convertdate", "hijri-converter", "convertdate"]
|
||||
fasttext = ["fasttext"]
|
||||
langdetect = ["langdetect"]
|
||||
fasttext = ["fasttext"]
|
||||
calendars = ["convertdate", "hijri-converter", "convertdate"]
|
||||
|
||||
[[package]]
|
||||
name = "discord-typings"
|
||||
|
@ -439,7 +454,7 @@ ansicon = {version = "*", markers = "platform_system == \"Windows\""}
|
|||
|
||||
[[package]]
|
||||
name = "jurigged"
|
||||
version = "0.5.2"
|
||||
version = "0.5.3"
|
||||
description = "Live update of Python functions"
|
||||
category = "main"
|
||||
optional = false
|
||||
|
@ -452,7 +467,7 @@ ovld = ">=0.3.1,<0.4.0"
|
|||
watchdog = ">=1.0.2"
|
||||
|
||||
[package.extras]
|
||||
develoop = ["giving (>=0.3.6,<0.4.0)", "rich (>=10.13.0,<11.0.0)", "hrepr (>=0.4.0,<0.5.0)"]
|
||||
develoop = ["hrepr (>=0.4.0,<0.5.0)", "rich (>=10.13.0,<11.0.0)", "giving (>=0.3.6,<0.4.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "marshmallow"
|
||||
|
@ -514,7 +529,7 @@ python-versions = "*"
|
|||
|
||||
[[package]]
|
||||
name = "naff"
|
||||
version = "1.9.0"
|
||||
version = "1.10.0"
|
||||
description = "Not another freaking fork"
|
||||
category = "main"
|
||||
optional = false
|
||||
|
@ -769,7 +784,7 @@ optional = false
|
|||
python-versions = ">=3.6.8"
|
||||
|
||||
[package.extras]
|
||||
diagrams = ["railroad-diagrams", "jinja2"]
|
||||
diagrams = ["jinja2", "railroad-diagrams"]
|
||||
|
||||
[[package]]
|
||||
name = "pyppeteer"
|
||||
|
@ -953,6 +968,14 @@ category = "main"
|
|||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
|
||||
[[package]]
|
||||
name = "soupsieve"
|
||||
version = "2.3.2.post1"
|
||||
description = "A modern CSS selector implementation for Beautiful Soup."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
|
||||
[[package]]
|
||||
name = "thefuzz"
|
||||
version = "0.19.0"
|
||||
|
@ -1140,9 +1163,9 @@ optional = false
|
|||
python-versions = ">=3.7"
|
||||
|
||||
[package.extras]
|
||||
docs = ["Sphinx (>=3.4)", "sphinx-rtd-theme (>=0.5)"]
|
||||
optional = ["python-socks", "wsaccel"]
|
||||
test = ["websockets"]
|
||||
optional = ["wsaccel", "python-socks"]
|
||||
docs = ["sphinx-rtd-theme (>=0.5)", "Sphinx (>=3.4)"]
|
||||
|
||||
[[package]]
|
||||
name = "websockets"
|
||||
|
@ -1179,7 +1202,7 @@ testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-
|
|||
[metadata]
|
||||
lock-version = "1.1"
|
||||
python-versions = "^3.10"
|
||||
content-hash = "d4a3ccd2f79fe0c323784bfba2c5950817257639bbdcdb57a6e71682a8846504"
|
||||
content-hash = "e95b3b3bed46990e5d3bd4f8662fd888dbf35a9272f09a18192c78d6c60b6f83"
|
||||
|
||||
[metadata.files]
|
||||
aiofile = [
|
||||
|
@ -1312,6 +1335,10 @@ attrs = [
|
|||
{file = "attrs-21.4.0-py2.py3-none-any.whl", hash = "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4"},
|
||||
{file = "attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"},
|
||||
]
|
||||
beautifulsoup4 = [
|
||||
{file = "beautifulsoup4-4.11.1-py3-none-any.whl", hash = "sha256:58d5c3d29f5a36ffeb94f02f0d786cd53014cf9b3b3951d42e0080d8a9498d30"},
|
||||
{file = "beautifulsoup4-4.11.1.tar.gz", hash = "sha256:ad9aa55b65ef2808eb405f46cf74df7fcb7044d5cbc26487f96eb2ef2e436693"},
|
||||
]
|
||||
black = [
|
||||
{file = "black-22.3.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2497f9c2386572e28921fa8bec7be3e51de6801f7459dffd6e62492531c47e09"},
|
||||
{file = "black-22.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5795a0375eb87bfe902e80e0c8cfaedf8af4d49694d69161e5bd3206c18618bb"},
|
||||
|
@ -1386,7 +1413,10 @@ dateparser = [
|
|||
{file = "dateparser-1.1.1-py2.py3-none-any.whl", hash = "sha256:9600874312ff28a41f96ec7ccdc73be1d1c44435719da47fea3339d55ff5a628"},
|
||||
{file = "dateparser-1.1.1.tar.gz", hash = "sha256:038196b1f12c7397e38aad3d61588833257f6f552baa63a1499e6987fa8d42d9"},
|
||||
]
|
||||
discord-typings = []
|
||||
discord-typings = [
|
||||
{file = "discord-typings-0.5.1.tar.gz", hash = "sha256:1a4fb1e00201416ae94ca64ca5935d447c005e0475b1ec274c1a6e09072db70e"},
|
||||
{file = "discord_typings-0.5.1-py3-none-any.whl", hash = "sha256:55ebdb6d6f0f47df774a0c31193ba6a45de14625fab9c6fbd43bfe87bb8c0128"},
|
||||
]
|
||||
distro = [
|
||||
{file = "distro-1.7.0-py3-none-any.whl", hash = "sha256:d596311d707e692c2160c37807f83e3820c5d539d5a83e87cfb6babd8ba3a06b"},
|
||||
{file = "distro-1.7.0.tar.gz", hash = "sha256:151aeccf60c216402932b52e40ee477a939f8d58898927378a02abbe852c1c39"},
|
||||
|
@ -1482,8 +1512,8 @@ jinxed = [
|
|||
{file = "jinxed-1.2.0.tar.gz", hash = "sha256:032acda92d5c57cd216033cbbd53de731e6ed50deb63eb4781336ca55f72cda5"},
|
||||
]
|
||||
jurigged = [
|
||||
{file = "jurigged-0.5.2-py3-none-any.whl", hash = "sha256:410ff6199c659108dace9179507342883fe2fffec1966fd19709f9d59fd69e24"},
|
||||
{file = "jurigged-0.5.2.tar.gz", hash = "sha256:de1d4daeb99c0299eaa86f691d35cb1eab3bfa836cfe9a3551a56f3829479e3b"},
|
||||
{file = "jurigged-0.5.3-py3-none-any.whl", hash = "sha256:355a9bddf42cae541e862796fb125827fc35573a982c6f35d3dc5621e59c91e3"},
|
||||
{file = "jurigged-0.5.3.tar.gz", hash = "sha256:47cf4e9f10455a39602caa447888c06adda962699c65f19d8c37509817341b5e"},
|
||||
]
|
||||
marshmallow = [
|
||||
{file = "marshmallow-3.16.0-py3-none-any.whl", hash = "sha256:53a1e0ee69f79e1f3e80d17393b25cfc917eda52f859e8183b4af72c3390c1f1"},
|
||||
|
@ -1563,8 +1593,8 @@ mypy-extensions = [
|
|||
{file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"},
|
||||
]
|
||||
naff = [
|
||||
{file = "naff-1.9.0-py3-none-any.whl", hash = "sha256:20144495aed9452d9d2e713eb6ade9636601457ca3de255684b2186068505bcd"},
|
||||
{file = "naff-1.9.0.tar.gz", hash = "sha256:f4870ea304747368d6d750f3d52fcbc96017bd7afaa7ec06a3e9a68ff301997d"},
|
||||
{file = "naff-1.10.0-py3-none-any.whl", hash = "sha256:bb28ef19efb3f8e04f3569a3aac6b3e2738cf5747dea0bed483c458588933682"},
|
||||
{file = "naff-1.10.0.tar.gz", hash = "sha256:d0ab71c39ea5bf352228f0bc3d3dfe3610122cb01733bca4565497078de95650"},
|
||||
]
|
||||
nafftrack = []
|
||||
nanoid = [
|
||||
|
@ -1823,6 +1853,7 @@ pymongo = [
|
|||
{file = "pymongo-3.12.3-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:71c0db2c313ea8a80825fb61b7826b8015874aec29ee6364ade5cb774fe4511b"},
|
||||
{file = "pymongo-3.12.3-cp27-cp27mu-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5b779e87300635b8075e8d5cfd4fdf7f46078cd7610c381d956bca5556bb8f97"},
|
||||
{file = "pymongo-3.12.3-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:351a2efe1c9566c348ad0076f4bf541f4905a0ebe2d271f112f60852575f3c16"},
|
||||
{file = "pymongo-3.12.3-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:858af7c2ab98f21ed06b642578b769ecfcabe4754648b033168a91536f7beef9"},
|
||||
{file = "pymongo-3.12.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0a02313e71b7c370c43056f6b16c45effbb2d29a44d24403a3d5ba6ed322fa3f"},
|
||||
{file = "pymongo-3.12.3-cp310-cp310-manylinux1_i686.whl", hash = "sha256:d3082e5c4d7b388792124f5e805b469109e58f1ab1eb1fbd8b998e8ab766ffb7"},
|
||||
{file = "pymongo-3.12.3-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:514e78d20d8382d5b97f32b20c83d1d0452c302c9a135f0a9022236eb9940fda"},
|
||||
|
@ -1936,7 +1967,9 @@ python-gitlab = [
|
|||
{file = "python-gitlab-3.5.0.tar.gz", hash = "sha256:29ae7fb9b8c9aeb2e6e19bd2fd04867e93ecd7af719978ce68fac0cf116ab30d"},
|
||||
{file = "python_gitlab-3.5.0-py3-none-any.whl", hash = "sha256:73b5aa6502efa557ee1a51227cceb0243fac5529627da34f08c5f265bf50417c"},
|
||||
]
|
||||
python-levenshtein = []
|
||||
python-levenshtein = [
|
||||
{file = "python-Levenshtein-0.12.2.tar.gz", hash = "sha256:dc2395fbd148a1ab31090dd113c366695934b9e85fe5a4b2a032745efd0346f6"},
|
||||
]
|
||||
pytz = [
|
||||
{file = "pytz-2022.1-py2.py3-none-any.whl", hash = "sha256:e68985985296d9a66a881eb3193b0906246245294a881e7c8afe623866ac6a5c"},
|
||||
{file = "pytz-2022.1.tar.gz", hash = "sha256:1e760e2fe6a8163bc0b3d9a19c4f84342afa0a2affebfaa84b01b978a02ecaa7"},
|
||||
|
@ -1953,6 +1986,13 @@ pyyaml = [
|
|||
{file = "PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"},
|
||||
{file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"},
|
||||
{file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"},
|
||||
{file = "PyYAML-6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d4b0ba9512519522b118090257be113b9468d804b19d63c71dbcf4a48fa32358"},
|
||||
{file = "PyYAML-6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:81957921f441d50af23654aa6c5e5eaf9b06aba7f0a19c18a538dc7ef291c5a1"},
|
||||
{file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afa17f5bc4d1b10afd4466fd3a44dc0e245382deca5b3c353d8b757f9e3ecb8d"},
|
||||
{file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dbad0e9d368bb989f4515da330b88a057617d16b6a8245084f1b05400f24609f"},
|
||||
{file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:432557aa2c09802be39460360ddffd48156e30721f5e8d917f01d31694216782"},
|
||||
{file = "PyYAML-6.0-cp311-cp311-win32.whl", hash = "sha256:bfaef573a63ba8923503d27530362590ff4f576c626d86a9fed95822a8255fd7"},
|
||||
{file = "PyYAML-6.0-cp311-cp311-win_amd64.whl", hash = "sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf"},
|
||||
{file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"},
|
||||
{file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"},
|
||||
{file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"},
|
||||
|
@ -2102,7 +2142,14 @@ smmap = [
|
|||
{file = "smmap-5.0.0-py3-none-any.whl", hash = "sha256:2aba19d6a040e78d8b09de5c57e96207b09ed71d8e55ce0959eeee6c8e190d94"},
|
||||
{file = "smmap-5.0.0.tar.gz", hash = "sha256:c840e62059cd3be204b0c9c9f74be2c09d5648eddd4580d9314c3ecde0b30936"},
|
||||
]
|
||||
thefuzz = []
|
||||
soupsieve = [
|
||||
{file = "soupsieve-2.3.2.post1-py3-none-any.whl", hash = "sha256:3b2503d3c7084a42b1ebd08116e5f81aadfaea95863628c80a3b774a11b7c759"},
|
||||
{file = "soupsieve-2.3.2.post1.tar.gz", hash = "sha256:fc53893b3da2c33de295667a0e19f078c14bf86544af307354de5fcf12a3f30d"},
|
||||
]
|
||||
thefuzz = [
|
||||
{file = "thefuzz-0.19.0-py2.py3-none-any.whl", hash = "sha256:4fcdde8e40f5ca5e8106bc7665181f9598a9c8b18b0a4d38c41a095ba6788972"},
|
||||
{file = "thefuzz-0.19.0.tar.gz", hash = "sha256:6f7126db2f2c8a54212b05e3a740e45f4291c497d75d20751728f635bb74aa3d"},
|
||||
]
|
||||
tomli = [
|
||||
{file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
|
||||
{file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
|
||||
|
@ -2115,7 +2162,10 @@ tweepy = [
|
|||
{file = "tweepy-4.10.0-py3-none-any.whl", hash = "sha256:f0abbd234a588e572f880f99a094ac321217ff3eade6c0eca118ed6db8e2cf0a"},
|
||||
{file = "tweepy-4.10.0.tar.gz", hash = "sha256:7f92574920c2f233663fff154745fc2bb0d10aedc23617379a912d8e4fefa399"},
|
||||
]
|
||||
typing-extensions = []
|
||||
typing-extensions = [
|
||||
{file = "typing_extensions-4.3.0-py3-none-any.whl", hash = "sha256:25642c956049920a5aa49edcdd6ab1e06d7e5d467fc00e0506c44ac86fbfca02"},
|
||||
{file = "typing_extensions-4.3.0.tar.gz", hash = "sha256:e6d2677a32f47fc7eb2795db1dd15c1f34eff616bcaf2cfb5e997f854fa1c4a6"},
|
||||
]
|
||||
tzdata = [
|
||||
{file = "tzdata-2022.1-py2.py3-none-any.whl", hash = "sha256:238e70234214138ed7b4e8a0fab0e5e13872edab3be586ab8198c407620e2ab9"},
|
||||
{file = "tzdata-2022.1.tar.gz", hash = "sha256:8b536a8ec63dc0751342b3984193a3118f8fca2afe25752bb9b7fffd398552d3"},
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[tool.poetry]
|
||||
name = "jarvis"
|
||||
version = "2.2.1"
|
||||
version = "2.2.3"
|
||||
description = "JARVIS admin bot"
|
||||
authors = ["Zevaryx <zevaryx@gmail.com>"]
|
||||
|
||||
|
@ -30,6 +30,7 @@ nafftrack = {git = "https://github.com/artem30801/nafftrack.git", rev = "master"
|
|||
ansitoimg = "^2022.1"
|
||||
nest-asyncio = "^1.5.5"
|
||||
thefuzz = {extras = ["speedup"], version = "^0.19.0"}
|
||||
beautifulsoup4 = "^4.11.1"
|
||||
|
||||
[tool.poetry.dev-dependencies]
|
||||
black = {version = "^22.3.0", allow-prereleases = true}
|
||||
|
|
Loading…
Add table
Reference in a new issue