Massive update to beanie, ipy5, re-work and update of many commands

This commit is contained in:
Zeva Rose 2023-08-27 14:20:14 -06:00
parent 910ac2c304
commit f594f02b1b
34 changed files with 2656 additions and 2686 deletions

View file

@ -8,6 +8,8 @@
!/jarvis_small.png !/jarvis_small.png
!/run.py !/run.py
!/config.yaml !/config.yaml
# Needed for jarvis-compose
!/.git
# Block other files # Block other files
**/__pycache__ **/__pycache__

View file

@ -2,8 +2,8 @@
<img width=15% alt="JARVIS" src="https://git.zevaryx.com/stark-industries/jarvis/jarvis-bot/-/raw/main/jarvis_small.png"> <img width=15% alt="JARVIS" src="https://git.zevaryx.com/stark-industries/jarvis/jarvis-bot/-/raw/main/jarvis_small.png">
# Just Another Rather Very Intelligent System # Just Another Rather Very Intelligent System
<br />
<br />
[![python 3.10+](https://img.shields.io/badge/python-3.10+-blue)]() [![python 3.10+](https://img.shields.io/badge/python-3.10+-blue)]()
[![tokei lines of code](http://135.148.148.80:8000/b1/gitlab/zevaryx/jarvis-bot?category=code)](https://git.zevaryx.com/stark-industries/jarvis/jarvis-bot) [![tokei lines of code](http://135.148.148.80:8000/b1/gitlab/zevaryx/jarvis-bot?category=code)](https://git.zevaryx.com/stark-industries/jarvis/jarvis-bot)
@ -12,28 +12,22 @@
[![ko-fi](https://www.ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/zevaryx) [![ko-fi](https://www.ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/zevaryx)
</div> </div>
Welcome to the JARVIS Initiative, an open-source multi-purpose bot
Welcome to the JARVIS Initiative! While the main goal is to create the best discord bot there can be, a great achievement would be to present him to the Robots and have him integrated into the dbrand server. Feel free to suggest anything you may think to be useful… or cool.
**Note:** Some commands have been custom made to be used in the dbrand server.
## Features ## Features
JARVIS currently offers: JARVIS currently offers:
- 👩‍💼 **Administration**: `verify`, `ban/unban`, `kick`, `purge`, `mute/unmute` and more! - 👩‍💼 **Administration**: `verify`, `ban/unban`, `kick`, `purge`, `mute/unmute` and more!
- 🚓 **Moderation**: `lock/unlock`, `lockdown`, `warn`, `autoreact`, and also more! - 🚓 **Moderation**: `lock/unlock`, `lockdown`, `warn`, `autoreact`, and also more!
- 🔗 **Social Media**: `reddit` and `twitter` syncing! - 🔧 **Utilities**: `remindme`, `rolegiver`, `temprole`, `image`, and so many more!
- 🔧 **Utilities**: `remindme`, `rolegiver`, `temprole`, `image`, and so many more! - 🏷️ **Tags**: Custom `tag`s! Useful for custom messages without the hassle!
- 🏷️ **Tags**: Custom `tag`s! Useful for custom messages without the hassle!
- 👑 **dbrand**: `ctc2` and other dbrand-specific commands. [Join their server](https://discord.gg/dbrand) to see them in action!
## Contributing ## Contributing
Before **creating an issue**, please ensure that it hasn't already been reported/suggested. Before **creating an issue**, please ensure that it hasn't already been reported/suggested.
If you have a question, please join the community Discord before opening an issue. If you have a question, please join the community Discord before opening an issue.
If you wish to contribute to the JARVIS codebase or documentation, join the Discord! The recognized developers there will help you get started. If you wish to contribute to the JARVIS codebase or documentation, join the Discord! The recognized developers there will help you get started.
## Community ## Community
@ -41,12 +35,12 @@ If you wish to contribute to the JARVIS codebase or documentation, join the Disc
Join the [Stark R&D Department Discord server](https://discord.gg/VtgZntXcnZ) to be kept up-to-date on code updates and issues. Join the [Stark R&D Department Discord server](https://discord.gg/VtgZntXcnZ) to be kept up-to-date on code updates and issues.
## Requirements ## Requirements
- MongoDB 5.0 or higher
- MongoDB 6.0 or higher
- Python 3.10 or higher - Python 3.10 or higher
- [tokei](https://github.com/XAMPPRocky/tokei) 12.1 or higher - [tokei](https://github.com/XAMPPRocky/tokei) 12.1 or higher
- Everything in `requirements.txt` - Everything in `requirements.txt`
## JARVIS Cogs ## JARVIS Cogs
Current cogs that are implemented: Current cogs that are implemented:
@ -57,10 +51,6 @@ Current cogs that are implemented:
- Handles autoreaction configuration - Handles autoreaction configuration
- `BotutilCog` - `BotutilCog`
- Handles internal bot utilities (private use only) - Handles internal bot utilities (private use only)
- `CTC2Cog`
- dbrand Complete the Code utilities
- `DbrandCog`
- dbrand-specific functions and utilities
- `DevCog` - `DevCog`
- Developer utilities, such as hashing, encoding, and UUID generation - Developer utilities, such as hashing, encoding, and UUID generation
- `GitlabCog` - `GitlabCog`
@ -88,7 +78,6 @@ Current cogs that are implemented:
- `VerifyCog` - `VerifyCog`
- Guild verification - Guild verification
## Directories ## Directories
### `jarvis` ### `jarvis`
@ -102,6 +91,7 @@ All of the cogs listed above are stored in this directory
##### `jarvis.cogs.admin` ##### `jarvis.cogs.admin`
Contains all AdminCogs, including: Contains all AdminCogs, including:
- `BanCog` - `BanCog`
- `KickCog` - `KickCog`
- `LockCog` - `LockCog`

View file

@ -61,18 +61,31 @@ async def run() -> None:
config = load_config() config = load_config()
logger = get_logger("jarvis", show_locals=False) # jconfig.log_level == "DEBUG") logger = get_logger("jarvis", show_locals=False) # jconfig.log_level == "DEBUG")
logger.setLevel(config.log_level) logger.setLevel(config.log_level)
file_handler = logging.FileHandler(filename="jarvis.log", encoding="UTF-8", mode="w") file_handler = logging.FileHandler(
file_handler.setFormatter(logging.Formatter("[%(asctime)s] [%(name)s] [%(levelname)8s] %(message)s")) filename="jarvis.log", encoding="UTF-8", mode="w"
)
file_handler.setFormatter(
logging.Formatter("[%(asctime)s] [%(name)s] [%(levelname)8s] %(message)s")
)
logger.addHandler(file_handler) logger.addHandler(file_handler)
# Configure client # Configure client
intents = Intents.DEFAULT | Intents.MESSAGES | Intents.GUILD_MEMBERS | Intents.GUILD_MESSAGES intents = (
Intents.DEFAULT
| Intents.MESSAGES
| Intents.GUILD_MEMBERS
| Intents.GUILD_MESSAGES
)
redis_config = config.redis.dict() redis_config = config.redis.dict()
redis_host = redis_config.pop("host") redis_host = redis_config.pop("host")
redis = await aioredis.from_url(redis_host, decode_responses=True, **redis_config) redis = await aioredis.from_url(redis_host, decode_responses=True, **redis_config)
await connect(**config.mongo.dict(), testing=config.environment.value == "develop", extra_models=[StaticStat, Stat]) await connect(
**config.mongo.dict(),
testing=config.environment.value == "develop",
extra_models=[StaticStat, Stat],
)
jarvis = Jarvis( jarvis = Jarvis(
intents=intents, intents=intents,
@ -81,6 +94,7 @@ async def run() -> None:
send_command_tracebacks=False, send_command_tracebacks=False,
redis=redis, redis=redis,
logger=logger, logger=logger,
erapi=config.erapi,
) )
# External modules # External modules

View file

@ -2,6 +2,7 @@
import logging import logging
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from erapi import ERAPI
from interactions.ext.prefixed_commands.context import PrefixedContext from interactions.ext.prefixed_commands.context import PrefixedContext
from interactions.models.internal.context import BaseContext, InteractionContext from interactions.models.internal.context import BaseContext, InteractionContext
from jarvis_core.util.ansi import Fore, Format, fmt from jarvis_core.util.ansi import Fore, Format, fmt
@ -20,13 +21,16 @@ CMD_FMT = fmt(Fore.GREEN, Format.BOLD)
class Jarvis(StatipyClient, ErrorMixin, EventMixin, TaskMixin): class Jarvis(StatipyClient, ErrorMixin, EventMixin, TaskMixin):
def __init__(self, redis: "aioredis.Redis", *args, **kwargs): # noqa: ANN002 ANN003 def __init__(
self, redis: "aioredis.Redis", erapi: str, *args, **kwargs
): # noqa: ANN002 ANN003
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.redis = redis self.redis = redis
self.logger = logging.getLogger(__name__) self.logger = logging.getLogger(__name__)
self.phishing_domains = [] self.phishing_domains = []
self.pre_run_callback = self._prerun self.pre_run_callback = self._prerun
self.synced = False self.synced = False
self.erapi = ERAPI(erapi)
async def _prerun(self, ctx: BaseContext, *args, **kwargs) -> None: async def _prerun(self, ctx: BaseContext, *args, **kwargs) -> None:
name = ctx.invoke_target name = ctx.invoke_target

View file

@ -2,6 +2,8 @@
import traceback import traceback
from datetime import datetime, timezone from datetime import datetime, timezone
from interactions import listen
from interactions.api.events import Error
from interactions.client.errors import ( from interactions.client.errors import (
CommandCheckFailure, CommandCheckFailure,
CommandOnCooldown, CommandOnCooldown,
@ -31,16 +33,24 @@ Callback:
class ErrorMixin: class ErrorMixin:
async def on_error(self, source: str, error: Exception, *args, **kwargs) -> None: @listen()
async def on_error(self, event: Error, *args, **kwargs) -> None:
"""NAFF on_error override.""" """NAFF on_error override."""
source = event.source
error = event.error
if isinstance(error, HTTPException): if isinstance(error, HTTPException):
errors = error.search_for_message(error.errors) errors = error.search_for_message(error.errors)
out = f"HTTPException: {error.status}|{error.response.reason}: " + "\n".join(errors) out = (
f"HTTPException: {error.status}|{error.response.reason}: "
+ "\n".join(errors)
)
self.logger.error(out, exc_info=error) self.logger.error(out, exc_info=error)
else: else:
self.logger.error(f"Ignoring exception in {source}", exc_info=error) self.logger.error(f"Ignoring exception in {source}", exc_info=error)
async def on_command_error(self, ctx: BaseContext, error: Exception, *args: list, **kwargs: dict) -> None: async def on_command_error(
self, ctx: BaseContext, error: Exception, *args: list, **kwargs: dict
) -> None:
"""NAFF on_command_error override.""" """NAFF on_command_error override."""
name = ctx.invoke_target name = ctx.invoke_target
self.logger.debug(f"Handling error in {name}: {error}") self.logger.debug(f"Handling error in {name}: {error}")
@ -70,7 +80,11 @@ class ErrorMixin:
v = v[97] + "..." v = v[97] + "..."
arg_str += f" - {v}" arg_str += f" - {v}"
callback_args = "\n".join(f" - {i}" for i in args) if args else " None" callback_args = "\n".join(f" - {i}" for i in args) if args else " None"
callback_kwargs = "\n".join(f" {k}: {v}" for k, v in kwargs.items()) if kwargs else " None" callback_kwargs = (
"\n".join(f" {k}: {v}" for k, v in kwargs.items())
if kwargs
else " None"
)
full_message = ERROR_MSG.format( full_message = ERROR_MSG.format(
guild_name=ctx.guild.name, guild_name=ctx.guild.name,
error_time=error_time, error_time=error_time,
@ -82,7 +96,11 @@ class ErrorMixin:
tb = traceback.format_exception(error) tb = traceback.format_exception(error)
if isinstance(error, HTTPException): if isinstance(error, HTTPException):
errors = error.search_for_message(error.errors) errors = error.search_for_message(error.errors)
tb[-1] = f"HTTPException: {error.status}|{error.response.reason}: " + "\n".join(errors) tb[
-1
] = f"HTTPException: {error.status}|{error.response.reason}: " + "\n".join(
errors
)
error_message = "".join(traceback.format_exception(error)) error_message = "".join(traceback.format_exception(error))
if len(full_message + error_message) >= 1800: if len(full_message + error_message) >= 1800:
error_message = "\n ".join(error_message.split("\n")) error_message = "\n ".join(error_message.split("\n"))
@ -101,7 +119,9 @@ class ErrorMixin:
f"\n```yaml\n{full_message}\n```" f"\n```yaml\n{full_message}\n```"
f"\nException:\n```py\n{error_message}\n```" f"\nException:\n```py\n{error_message}\n```"
) )
await ctx.send("Whoops! Encountered an error. The error has been logged.", ephemeral=True) await ctx.send(
"Whoops! Encountered an error. The error has been logged.", ephemeral=True
)
try: try:
await ctx.defer(ephemeral=True) await ctx.defer(ephemeral=True)
return await super().on_command_error(ctx, error, *args, **kwargs) return await super().on_command_error(ctx, error, *args, **kwargs)

View file

@ -35,16 +35,22 @@ class EventMixin(MemberEventMixin, MessageEventMixin, ComponentEventMixin):
async def _sync_domains(self) -> None: async def _sync_domains(self) -> None:
self.logger.debug("Loading phishing domains") self.logger.debug("Loading phishing domains")
async with ClientSession(headers={"X-Identity": "Discord: zevaryx#5779"}) as session: async with ClientSession(
headers={"X-Identity": "Discord: zevaryx#5779"}
) as session:
response = await session.get("https://phish.sinking.yachts/v2/all") response = await session.get("https://phish.sinking.yachts/v2/all")
response.raise_for_status() response.raise_for_status()
self.phishing_domains = await response.json() self.phishing_domains = await response.json()
self.logger.info(f"Protected from {len(self.phishing_domains)} phishing domains") self.logger.info(
f"Protected from {len(self.phishing_domains)} phishing domains"
)
@listen() @listen()
async def on_startup(self) -> None: async def on_startup(self) -> None:
"""NAFF on_startup override. Prometheus info generated here.""" """NAFF on_startup override. Prometheus info generated here."""
await StaticStat.find_one(StaticStat.name == "jarvis_version", StaticStat.client_id == self.user.id).upsert( await StaticStat.find_one(
StaticStat.name == "jarvis_version", StaticStat.client_id == self.user.id
).upsert(
Set( Set(
{ {
StaticStat.client_name: self.client_name, StaticStat.client_name: self.client_name,
@ -67,7 +73,9 @@ class EventMixin(MemberEventMixin, MessageEventMixin, ComponentEventMixin):
except Exception as e: except Exception as e:
self.logger.error("Failed to load anti-phishing", exc_info=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("Logged in as {}".format(self.user)) # noqa: T001
self.logger.info("Connected to {} guild(s)".format(len(self.guilds))) # 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("Current version: {}".format(const.__version__))
self.logger.info( # noqa: T001 self.logger.info( # noqa: T001
"https://discord.com/api/oauth2/authorize?client_id=" "https://discord.com/api/oauth2/authorize?client_id="
@ -87,7 +95,9 @@ class EventMixin(MemberEventMixin, MessageEventMixin, ComponentEventMixin):
if not isinstance(self.interaction_tree[cid][_], ContextMenu) if not isinstance(self.interaction_tree[cid][_], ContextMenu)
) )
global_context_menus = sum( global_context_menus = sum(
1 for _ in self.interaction_tree[cid] if isinstance(self.interaction_tree[cid][_], ContextMenu) 1
for _ in self.interaction_tree[cid]
if isinstance(self.interaction_tree[cid][_], ContextMenu)
) )
else: else:
guild_base_commands += sum( guild_base_commands += sum(
@ -96,25 +106,42 @@ class EventMixin(MemberEventMixin, MessageEventMixin, ComponentEventMixin):
if not isinstance(self.interaction_tree[cid][_], ContextMenu) if not isinstance(self.interaction_tree[cid][_], ContextMenu)
) )
guild_context_menus += sum( guild_context_menus += sum(
1 for _ in self.interaction_tree[cid] if isinstance(self.interaction_tree[cid][_], ContextMenu) 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(
self.logger.info("Loaded {:>3} global context menus".format(global_context_menus)) "Loaded {:>3} global base slash commands".format(global_base_commands)
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.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)
)
except Exception: except Exception:
self.logger.error("interaction_tree not found, try updating NAFF") self.logger.error("interaction_tree not found, try updating NAFF")
self.logger.debug(self.interaction_tree)
self.logger.debug("Hitting Reminders for faster loads") self.logger.debug("Hitting Reminders for faster loads")
_ = await Reminder.find().to_list(None) _ = await Reminder.find().to_list(None)
self.logger.debug("Updating ERAPI")
await self.erapi.update_async()
# Modlog # Modlog
async def on_command(self, ctx: BaseContext) -> None: async def on_command(self, ctx: BaseContext) -> None:
"""NAFF on_command override.""" """NAFF on_command override."""
name = ctx.invoke_target name = ctx.invoke_target
if not isinstance(ctx.channel, DMChannel) and name not in ["pw"]: if not isinstance(ctx.channel, DMChannel) and name not in ["pw"]:
modlog = await Setting.find_one(Setting.guild == ctx.guild.id, Setting.setting == "activitylog") modlog = await Setting.find_one(
ignore = await Setting.find_one(Setting.guild == ctx.guild.id, Setting.setting == "log_ignore") Setting.guild == ctx.guild.id, Setting.setting == "activitylog"
)
ignore = await Setting.find_one(
Setting.guild == ctx.guild.id, Setting.setting == "log_ignore"
)
if modlog and (ignore and ctx.channel.id not in ignore.value): if modlog and (ignore and ctx.channel.id not in ignore.value):
channel = await ctx.guild.fetch_channel(modlog.value) channel = await ctx.guild.fetch_channel(modlog.value)
args = [] args = []
@ -146,10 +173,14 @@ class EventMixin(MemberEventMixin, MessageEventMixin, ComponentEventMixin):
fields=fields, fields=fields,
color="#fc9e3f", color="#fc9e3f",
) )
embed.set_author(name=ctx.author.username, icon_url=ctx.author.display_avatar.url) embed.set_author(
embed.set_footer(text=f"{ctx.author.user.username}#{ctx.author.discriminator} | {ctx.author.id}") name=ctx.author.username, icon_url=ctx.author.display_avatar.url
)
embed.set_footer(text=f"{ctx.author.user.username} | {ctx.author.id}")
if channel: if channel:
await channel.send(embeds=embed) await channel.send(embeds=embed)
else: else:
self.logger.warning(f"Activitylog channel no longer exists in {ctx.guild.name}, removing") self.logger.warning(
f"Activitylog channel no longer exists in {ctx.guild.name}, removing"
)
await modlog.delete() await modlog.delete()

View file

@ -1,4 +1,5 @@
"""JARVIS component event mixin.""" """JARVIS component event mixin."""
from beanie import PydanticObjectId
from interactions import listen from interactions import listen
from interactions.api.events.internal import ButtonPressed from interactions.api.events.internal import ButtonPressed
from interactions.models.discord.embed import EmbedField from interactions.models.discord.embed import EmbedField
@ -32,13 +33,21 @@ class ComponentEventMixin:
): ):
name, parent = action_data.split("|")[:2] name, parent = action_data.split("|")[:2]
action = Action(action_type=name, parent=parent) action = Action(action_type=name, parent=parent)
note = Note(admin=context.author.id, content="Moderation case opened via message") note = Note(
admin=context.author.id,
content="Moderation case opened via message",
)
modlog = await Modlog.find_one( modlog = await Modlog.find_one(
Modlog.user == user.id, Modlog.guild == context.guild.id, Modlog.open == True Modlog.user == user.id,
Modlog.guild == context.guild.id,
Modlog.open == True,
) )
if modlog: if modlog:
self.logger.debug("User already has active case in guild") self.logger.debug("User already has active case in guild")
await context.send(f"User already has open case: {modlog.nanoid}", ephemeral=True) await context.send(
f"User already has open case: {modlog.nanoid}",
ephemeral=True,
)
else: else:
modlog = Modlog( modlog = Modlog(
user=user.id, user=user.id,
@ -59,7 +68,7 @@ class ComponentEventMixin:
fields=fields, fields=fields,
) )
embed.set_author( embed.set_author(
name=user.username + "#" + user.discriminator, name=user.username,
icon_url=user.display_avatar.url, icon_url=user.display_avatar.url,
) )
@ -75,7 +84,11 @@ class ComponentEventMixin:
for component in row.components: for component in row.components:
component.disabled = True component.disabled = True
await context.message.edit(components=context.message.components) await context.message.edit(components=context.message.components)
msg = "Cancelled" if context.custom_id == "modcase|no" else "Moderation case opened" msg = (
"Cancelled"
if context.custom_id == "modcase|no"
else "Moderation case opened"
)
await context.send(msg) await context.send(msg)
await self.redis.delete(user_key) await self.redis.delete(user_key)
await self.redis.delete(action_key) await self.redis.delete(action_key)
@ -101,7 +114,9 @@ class ComponentEventMixin:
await context.send("I'm afraid I can't let you do that", ephemeral=True) await context.send("I'm afraid I can't let you do that", ephemeral=True)
return True # User does not have perms to delete return True # User does not have perms to delete
if pin := await Pin.find_one(Pin.pin == context.message.id, Pin.guild == context.guild.id): if pin := await Pin.find_one(
Pin.pin == context.message.id, Pin.guild == context.guild.id
):
await pin.delete() await pin.delete()
await context.message.delete() await context.message.delete()
@ -119,20 +134,35 @@ class ComponentEventMixin:
what, rid = context.custom_id.split("|")[1:] what, rid = context.custom_id.split("|")[1:]
if what == "rme": if what == "rme":
reminder = await Reminder.find_one(Reminder.id == rid) reminder = await Reminder.find_one(Reminder.id == PydanticObjectId(rid))
if reminder: if reminder:
new_reminder = Reminder( if await Reminder.find_one(
user=context.author.id, Reminder.parent == str(reminder.id),
channel=context.channel.id, Reminder.user == context.author.id,
guild=context.guild.id, ) or await Reminder.find_one(
message=reminder.message, Reminder.id == reminder.id, Reminder.user == context.author.id
remind_at=reminder.remind_at, ):
private=reminder.private, await context.send(
active=reminder.active, "You've already copied this reminder!", ephemeral=True
) )
await new_reminder.save() else:
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,
parent=str(reminder.id),
)
await new_reminder.save()
await context.send("Reminder copied!", ephemeral=True) await context.send("Reminder copied!", ephemeral=True)
else:
await context.send(
"That reminder doesn't exist anymore", ephemeral=True
)
return True return True

View file

@ -38,11 +38,11 @@ class MemberEventMixin:
channel = await guild.fetch_channel(log.value) channel = await guild.fetch_channel(log.value)
embed = build_embed( embed = build_embed(
title="Member Left", title="Member Left",
description=f"{user.username}#{user.discriminator} left {guild.name}", description=f"{user.username} left {guild.name}",
fields=[], fields=[],
) )
embed.set_author(name=user.username, icon_url=user.avatar.url) embed.set_author(name=user.username, icon_url=user.avatar.url)
embed.set_footer(text=f"{user.username}#{user.discriminator} | {user.id}") embed.set_footer(text=f"{user.username} | {user.id}")
await channel.send(embeds=embed) await channel.send(embeds=embed)
async def process_verify(self, before: Member, after: Member) -> Embed: async def process_verify(self, before: Member, after: Member) -> Embed:
@ -56,7 +56,7 @@ class MemberEventMixin:
admin_text = "[N/A]" admin_text = "[N/A]"
if admin := await after.guild.fet_member(audit_event.user_id): if admin := await after.guild.fet_member(audit_event.user_id):
admin_mention = admin.mention admin_mention = admin.mention
admin_text = f"{admin.username}#{admin.discriminator}" admin_text = f"{admin.username}"
fields = ( fields = (
EmbedField(name="Moderator", value=f"{admin_mention} ({admin_text})"), EmbedField(name="Moderator", value=f"{admin_mention} ({admin_text})"),
EmbedField(name="Reason", value=audit_event.reason), EmbedField(name="Reason", value=audit_event.reason),
@ -67,7 +67,7 @@ class MemberEventMixin:
fields=fields, fields=fields,
) )
embed.set_author(name=after.display_name, icon_url=after.display_avatar.url) embed.set_author(name=after.display_name, icon_url=after.display_avatar.url)
embed.set_footer(text=f"{after.username}#{after.discriminator} | {after.id}") embed.set_footer(text=f"{after.username} | {after.id}")
return embed return embed
async def process_rolechange(self, before: Member, after: Member) -> Embed: async def process_rolechange(self, before: Member, after: Member) -> Embed:
@ -98,14 +98,13 @@ class MemberEventMixin:
fields=fields, fields=fields,
) )
embed.set_author(name=after.display_name, icon_url=after.display_avatar.url) embed.set_author(name=after.display_name, icon_url=after.display_avatar.url)
embed.set_footer(text=f"{after.username}#{after.discriminator} | {after.id}") embed.set_footer(text=f"{after.username} | {after.id}")
return embed return embed
async def process_rename(self, before: Member, after: Member) -> None: async def process_rename(self, before: Member, after: Member) -> None:
"""Process name change.""" """Process name change."""
if ( if (
before.nickname == after.nickname before.nickname == after.nickname
and before.discriminator == after.discriminator
and before.username == after.username and before.username == after.username
): ):
return return
@ -113,9 +112,9 @@ class MemberEventMixin:
fields = ( fields = (
EmbedField( EmbedField(
name="Before", name="Before",
value=f"{before.display_name} ({before.username}#{before.discriminator})", value=f"{before.display_name} ({before.username})",
), ),
EmbedField(name="After", value=f"{after.display_name} ({after.username}#{after.discriminator})"), EmbedField(name="After", value=f"{after.display_name} ({after.username})"),
) )
embed = build_embed( embed = build_embed(
title="User Renamed", title="User Renamed",
@ -124,7 +123,7 @@ class MemberEventMixin:
color="#fc9e3f", color="#fc9e3f",
) )
embed.set_author(name=after.display_name, icon_url=after.display_avatar.url) embed.set_author(name=after.display_name, icon_url=after.display_avatar.url)
embed.set_footer(text=f"{after.username}#{after.discriminator} | {after.id}") embed.set_footer(text=f"{after.username} | {after.id}")
return embed return embed
@listen() @listen()

View file

@ -40,7 +40,9 @@ class MessageEventMixin:
) )
if autopurge: if autopurge:
if not message.author.has_permission(Permissions.ADMINISTRATOR): if not message.author.has_permission(Permissions.ADMINISTRATOR):
self.logger.debug(f"Autopurging message {message.guild.id}/{message.channel.id}/{message.id}") self.logger.debug(
f"Autopurging message {message.guild.id}/{message.channel.id}/{message.id}"
)
await message.delete(delay=autopurge.delay) await message.delete(delay=autopurge.delay)
async def autoreact(self, message: Message) -> None: async def autoreact(self, message: Message) -> None:
@ -50,13 +52,15 @@ class MessageEventMixin:
Autoreact.channel == message.channel.id, Autoreact.channel == message.channel.id,
) )
if autoreact: if autoreact:
self.logger.debug(f"Autoreacting to message {message.guild.id}/{message.channel.id}/{message.id}") self.logger.debug(
f"Autoreacting to message {message.guild.id}/{message.channel.id}/{message.id}"
)
for reaction in autoreact.reactions: for reaction in autoreact.reactions:
await message.add_reaction(reaction) await message.add_reaction(reaction)
if autoreact.thread: if autoreact.thread:
name = message.content.replace("\n", " ") name = message.content.replace("\n", " ")
name = re.sub(r"<:\w+:(\d+)>", "", name) name = re.sub(r"<:\w+:(\d+)>", "", name)
if len(name) > 100: if len(name) >= 100:
name = name[:97] + "..." name = name[:97] + "..."
await message.create_thread(name=message.content, reason="Autoreact") await message.create_thread(name=message.content, reason="Autoreact")
@ -70,7 +74,9 @@ class MessageEventMixin:
# ) # )
content = re.sub(r"\s+", "", message.content) content = re.sub(r"\s+", "", message.content)
match = invites.search(content) match = invites.search(content)
setting = await Setting.find_one(Setting.guild == message.guild.id, Setting.setting == "noinvite") setting = await Setting.find_one(
Setting.guild == message.guild.id, Setting.setting == "noinvite"
)
if not setting: if not setting:
setting = Setting(guild=message.guild.id, setting="noinvite", value=True) setting = Setting(guild=message.guild.id, setting="noinvite", value=True)
await setting.save() await setting.save()
@ -78,12 +84,20 @@ class MessageEventMixin:
guild_invites = [x.code for x in await message.guild.fetch_invites()] guild_invites = [x.code for x in await message.guild.fetch_invites()]
if message.guild.vanity_url_code: if message.guild.vanity_url_code:
guild_invites.append(message.guild.vanity_url_code) guild_invites.append(message.guild.vanity_url_code)
allowed = guild_invites + ["dbrand", "VtgZntXcnZ", "gPfYGbvTCE", "interactions", "NTSHu97tHg"] allowed = guild_invites + [
is_mod = message.author.has_permission(Permissions.MANAGE_GUILD) or message.author.has_permission( "dbrand",
Permissions.ADMINISTRATOR "VtgZntXcnZ",
) "gPfYGbvTCE",
"interactions",
"NTSHu97tHg",
]
is_mod = message.author.has_permission(
Permissions.MANAGE_GUILD
) or message.author.has_permission(Permissions.ADMINISTRATOR)
if (m := match.group(1)) not in allowed and setting.value and not is_mod: if (m := match.group(1)) not in allowed and setting.value and not is_mod:
self.logger.debug(f"Removing non-allowed invite `{m}` from {message.guild.id}") self.logger.debug(
f"Removing non-allowed invite `{m}` from {message.guild.id}"
)
try: try:
await message.delete() await message.delete()
except Exception: except Exception:
@ -116,8 +130,7 @@ class MessageEventMixin:
async def filters(self, message: Message) -> None: async def filters(self, message: Message) -> None:
"""Handle filter evennts.""" """Handle filter evennts."""
filters = Filter.find(Filter.guild == message.guild.id) async for item in Filter.find(Filter.guild == message.guild.id):
async for item in filters:
for f in item.filters: for f in item.filters:
if re.search(f, message.content, re.IGNORECASE): if re.search(f, message.content, re.IGNORECASE):
expires_at = datetime.now(tz=timezone.utc) + timedelta(hours=24) expires_at = datetime.now(tz=timezone.utc) + timedelta(hours=24)
@ -139,7 +152,9 @@ class MessageEventMixin:
value=1, value=1,
) )
await Stat(meta=md, name="warning").insert() await Stat(meta=md, name="warning").insert()
embed = warning_embed(message.author, "Sent a message with a filtered word", self.user) embed = warning_embed(
message.author, "Sent a message with a filtered word", self.user
)
try: try:
await message.reply(embeds=embed) await message.reply(embeds=embed)
except Exception: except Exception:
@ -153,24 +168,26 @@ class MessageEventMixin:
async def massmention(self, message: Message) -> None: async def massmention(self, message: Message) -> None:
"""Handle massmention events.""" """Handle massmention events."""
massmention = await Setting.find_one( massmention: Setting = await Setting.find_one(
Setting.guild == message.guild.id, Setting.guild == message.guild.id,
Setting.setting == "massmention", Setting.setting == "massmention",
) )
is_mod = message.author.has_permission(Permissions.MANAGE_GUILD) or message.author.has_permission( is_mod = message.author.has_permission(
Permissions.ADMINISTRATOR Permissions.MANAGE_GUILD
) ) or message.author.has_permission(Permissions.ADMINISTRATOR)
if ( if (
massmention massmention
and massmention.value > 0 # noqa: W503 and int(massmention.value) > 0 # noqa: W503
and len(message._mention_ids + message._mention_roles) # 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 - (1 if message.author.id in message._mention_ids else 0) # noqa: W503
> massmention.value # noqa: W503 > massmention.value # noqa: W503
and not is_mod # noqa: W503 and not is_mod # noqa: W503
): ):
self.logger.debug(f"Massmention threshold on {message.guild.id}/{message.channel.id}/{message.id}") 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) expires_at = datetime.now(tz=timezone.utc) + timedelta(hours=24)
await Warning( await Warning(
active=True, active=True,
@ -202,11 +219,20 @@ class MessageEventMixin:
if message.author.has_permission(Permissions.MANAGE_GUILD): if message.author.has_permission(Permissions.MANAGE_GUILD):
return return
except Exception as e: except Exception as e:
self.logger.error("Failed to get permissions, pretending check failed", exc_info=e) self.logger.error(
"Failed to get permissions, pretending check failed", exc_info=e
)
if await Roleping.find(Roleping.guild == message.guild.id, Roleping.active == True).count() == 0: if (
await Roleping.find(
Roleping.guild == message.guild.id, Roleping.active == True
).count()
== 0
):
return return
rolepings = await Roleping.find(Roleping.guild == message.guild.id, Roleping.active == True).to_list() rolepings = await Roleping.find(
Roleping.guild == message.guild.id, Roleping.active == True
).to_list()
# Get all role IDs involved with message # Get all role IDs involved with message
roles = [x.id async for x in message.mention_roles] roles = [x.id async for x in message.mention_roles]
@ -230,7 +256,9 @@ class MessageEventMixin:
# Check if user in a bypass list # Check if user in a bypass list
def check_has_role(roleping: Roleping) -> bool: def check_has_role(roleping: Roleping) -> bool:
return any(role.id in roleping.bypass.roles for role in message.author.roles) return any(
role.id in roleping.bypass.roles for role in message.author.roles
)
user_has_bypass = False user_has_bypass = False
for roleping in rolepings: for roleping in rolepings:
@ -241,8 +269,15 @@ class MessageEventMixin:
user_has_bypass = True user_has_bypass = True
break break
if role_in_rolepings and user_missing_role and not user_is_admin and not user_has_bypass: if (
self.logger.debug(f"Rolepinged role in {message.guild.id}/{message.channel.id}/{message.id}") 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) expires_at = datetime.now(tz=timezone.utc) + timedelta(hours=24)
await Warning( await Warning(
active=True, active=True,
@ -262,7 +297,11 @@ class MessageEventMixin:
value=1, value=1,
) )
await Stat(meta=md, name="warning").insert() await Stat(meta=md, name="warning").insert()
embed = warning_embed(message.author, "Pinged a blocked role/user with a blocked role", self.user) embed = warning_embed(
message.author,
"Pinged a blocked role/user with a blocked role",
self.user,
)
try: try:
await message.channel.send(embeds=embed) await message.channel.send(embeds=embed)
except Exception: except Exception:
@ -318,8 +357,16 @@ class MessageEventMixin:
fields=[EmbedField(name="URL", value=m)], fields=[EmbedField(name="URL", value=m)],
) )
valid_button = Button(style=ButtonStyle.GREEN, emoji="✔️", custom_id=f"pl|valid|{pl.id}") valid_button = Button(
invalid_button = Button(style=ButtonStyle.RED, emoji="✖️", custom_id=f"pl|invalid|{pl.id}") style=ButtonStyle.GREEN,
emoji="✔️",
custom_id=f"pl|valid|{pl.id}",
)
invalid_button = Button(
style=ButtonStyle.RED,
emoji="✖️",
custom_id=f"pl|invalid|{pl.id}",
)
channel = await self.fetch_channel(1026918337554423868) channel = await self.fetch_channel(1026918337554423868)
@ -372,7 +419,9 @@ class MessageEventMixin:
value=1, value=1,
) )
await Stat(meta=md, name="warning").insert() await Stat(meta=md, name="warning").insert()
reasons = ", ".join(f"{m['source']}: {m['type']}" for m in data["matches"]) reasons = ", ".join(
f"{m['source']}: {m['type']}" for m in data["matches"]
)
embed = warning_embed(message.author, reasons, self.user) embed = warning_embed(message.author, reasons, self.user)
try: try:
await message.channel.send(embeds=embed) await message.channel.send(embeds=embed)
@ -394,8 +443,16 @@ class MessageEventMixin:
fields=[EmbedField(name="URL", value=m)], fields=[EmbedField(name="URL", value=m)],
) )
valid_button = Button(style=ButtonStyle.GREEN, emoji="✔️", custom_id=f"pl|valid|{pl.id}") valid_button = Button(
invalid_button = Button(style=ButtonStyle.RED, emoji="✖️", custom_id=f"pl|invalid|{pl.id}") style=ButtonStyle.GREEN,
emoji="✔️",
custom_id=f"pl|valid|{pl.id}",
)
invalid_button = Button(
style=ButtonStyle.RED,
emoji="✖️",
custom_id=f"pl|invalid|{pl.id}",
)
channel = await self.fetch_channel(1026918337554423868) channel = await self.fetch_channel(1026918337554423868)
@ -410,7 +467,9 @@ class MessageEventMixin:
"""Timeout a user.""" """Timeout a user."""
expires_at = datetime.now(tz=timezone.utc) + timedelta(minutes=30) expires_at = datetime.now(tz=timezone.utc) + timedelta(minutes=30)
try: try:
await user.timeout(communication_disabled_until=expires_at, reason="Phishing link") await user.timeout(
communication_disabled_until=expires_at, reason="Phishing link"
)
await Mute( await Mute(
user=user.id, user=user.id,
reason="Auto mute for harmful link", reason="Auto mute for harmful link",
@ -431,7 +490,7 @@ class MessageEventMixin:
) )
embed.set_author(name=user.display_name, icon_url=user.display_avatar.url) embed.set_author(name=user.display_name, icon_url=user.display_avatar.url)
embed.set_thumbnail(url=user.display_avatar.url) embed.set_thumbnail(url=user.display_avatar.url)
embed.set_footer(text=f"{user.username}#{user.discriminator} | {user.id}") embed.set_footer(text=f"{user.username} | {user.id}")
await channel.send(embeds=embed) await channel.send(embeds=embed)
except Exception: except Exception:
@ -459,10 +518,20 @@ class MessageEventMixin:
before = event.before before = event.before
after = event.after after = event.after
if not after.author.bot: if not after.author.bot:
modlog = await Setting.find_one(Setting.guild == after.guild.id, Setting.setting == "activitylog") modlog = await Setting.find_one(
ignore = await Setting.find_one(Setting.guild == after.guild.id, Setting.setting == "log_ignore") Setting.guild == after.guild.id, Setting.setting == "activitylog"
if modlog and (not ignore or (ignore and after.channel.id not in ignore.value)): )
if not before or before.content == after.content or before.content is None: ignore = await Setting.find_one(
Setting.guild == after.guild.id, Setting.setting == "log_ignore"
)
if modlog and (
not ignore or (ignore and after.channel.id not in ignore.value)
):
if (
not before
or before.content == after.content
or before.content is None
):
return return
try: try:
channel = before.guild.get_channel(modlog.value) channel = before.guild.get_channel(modlog.value)
@ -491,7 +560,9 @@ class MessageEventMixin:
icon_url=after.author.display_avatar.url, icon_url=after.author.display_avatar.url,
url=after.jump_url, url=after.jump_url,
) )
embed.set_footer(text=f"{after.author.username}#{after.author.discriminator} | {after.author.id}") embed.set_footer(
text=f"{after.author.username} | {after.author.id}"
)
await channel.send(embeds=embed) await channel.send(embeds=embed)
except Exception as e: except Exception as e:
self.logger.warning( self.logger.warning(
@ -512,9 +583,15 @@ class MessageEventMixin:
async def on_message_delete(self, event: MessageDelete) -> None: async def on_message_delete(self, event: MessageDelete) -> None:
"""Process on_message_delete events.""" """Process on_message_delete events."""
message = event.message message = event.message
modlog = await Setting.find_one(Setting.guild == message.guild.id, Setting.setting == "activitylog") modlog = await Setting.find_one(
ignore = await Setting.find_one(Setting.guild == message.guild.id, Setting.setting == "log_ignore") Setting.guild == message.guild.id, Setting.setting == "activitylog"
if modlog and (not ignore or (ignore and message.channel.id not in ignore.value)): )
ignore = await Setting.find_one(
Setting.guild == message.guild.id, Setting.setting == "log_ignore"
)
if modlog and (
not ignore or (ignore and message.channel.id not in ignore.value)
):
try: try:
content = message.content or "N/A" content = message.content or "N/A"
except AttributeError: except AttributeError:
@ -523,7 +600,9 @@ class MessageEventMixin:
try: try:
if message.attachments: if message.attachments:
value = "\n".join([f"[{x.filename}]({x.url})" for x in message.attachments]) value = "\n".join(
[f"[{x.filename}]({x.url})" for x in message.attachments]
)
fields.append( fields.append(
EmbedField( EmbedField(
name="Attachments", name="Attachments",
@ -533,7 +612,9 @@ class MessageEventMixin:
) )
if message.sticker_items: if message.sticker_items:
value = "\n".join([f"Sticker: {x.name}" for x in message.sticker_items]) value = "\n".join(
[f"Sticker: {x.name}" for x in message.sticker_items]
)
fields.append( fields.append(
EmbedField( EmbedField(
name="Stickers", name="Stickers",
@ -566,8 +647,10 @@ class MessageEventMixin:
url=message.jump_url, url=message.jump_url,
) )
embed.set_footer( embed.set_footer(
text=(f"{message.author.username}#{message.author.discriminator} | " f"{message.author.id}") text=(f"{message.author.username} | " f"{message.author.id}")
) )
await channel.send(embeds=embed) await channel.send(embeds=embed)
except Exception as e: except Exception as e:
self.logger.warning(f"Failed to process edit {message.guild.id}/{message.channel.id}/{message.id}: {e}") self.logger.warning(
f"Failed to process edit {message.guild.id}/{message.channel.id}/{message.id}: {e}"
)

View file

@ -7,7 +7,9 @@ from interactions.models.internal.tasks.triggers import IntervalTrigger
class TaskMixin: class TaskMixin:
@Task.create(IntervalTrigger(minutes=1)) @Task.create(IntervalTrigger(minutes=1))
async def _update_domains(self) -> None: async def _update_domains(self) -> None:
async with ClientSession(headers={"X-Identity": "Discord: zevaryx#5779"}) as session: async with ClientSession(
headers={"X-Identity": "Discord: zevaryx#5779"}
) as session:
response = await session.get("https://phish.sinking.yachts/v2/recent/60") response = await session.get("https://phish.sinking.yachts/v2/recent/60")
response.raise_for_status() response.raise_for_status()
data = await response.json() data = await response.json()
@ -31,3 +33,7 @@ class TaskMixin:
sub -= 1 sub -= 1
self.phishing_domains.remove(domain) self.phishing_domains.remove(domain)
self.logger.info(f"[antiphish] {add} additions, {sub} removals") self.logger.info(f"[antiphish] {add} additions, {sub} removals")
@Task.create(IntervalTrigger(minutes=30))
async def _update_currencies(self) -> None:
await self.erapi.update_async()

View file

@ -41,10 +41,13 @@ class BanCog(ModcaseCog):
) -> None: ) -> None:
"""Apply a Discord ban.""" """Apply a Discord ban."""
await ctx.guild.ban(user, reason=reason, delete_message_seconds=delete_history) await ctx.guild.ban(user, reason=reason, delete_message_seconds=delete_history)
discrim = user.discriminator
if discrim == 0:
discrim = None
b = Ban( b = Ban(
user=user.id, user=user.id,
username=user.username, username=user.username,
discrim=user.discriminator, discrim=discrim,
reason=reason, reason=reason,
admin=ctx.author.id, admin=ctx.author.id,
guild=ctx.guild.id, guild=ctx.guild.id,
@ -65,13 +68,18 @@ class BanCog(ModcaseCog):
await ctx.send(embeds=embed) await ctx.send(embeds=embed)
async def discord_apply_unban(self, ctx: InteractionContext, user: User, reason: str) -> None: async def discord_apply_unban(
self, ctx: InteractionContext, user: User, reason: str
) -> None:
"""Apply a Discord unban.""" """Apply a Discord unban."""
await ctx.guild.unban(user, reason=reason) await ctx.guild.unban(user, reason=reason)
discrim = user.discriminator
if discrim == 0:
discrim = None
u = Unban( u = Unban(
user=user.id, user=user.id,
username=user.username, username=user.username,
discrim=user.discriminator, discrim=discrim,
guild=ctx.guild.id, guild=ctx.guild.id,
admin=ctx.author.id, admin=ctx.author.id,
reason=reason, reason=reason,
@ -83,8 +91,15 @@ class BanCog(ModcaseCog):
await ctx.send(embeds=embed) await ctx.send(embeds=embed)
@slash_command(name="ban", description="Ban a user") @slash_command(name="ban", description="Ban a user")
@slash_option(name="user", description="User to ban", opt_type=OptionType.USER, required=True) @slash_option(
@slash_option(name="reason", description="Ban reason", opt_type=OptionType.STRING, required=True) name="user", description="User to ban", opt_type=OptionType.USER, required=True
)
@slash_option(
name="reason",
description="Ban reason",
opt_type=OptionType.STRING,
required=True,
)
@slash_option( @slash_option(
name="btype", name="btype",
description="Ban type", description="Ban type",
@ -131,14 +146,23 @@ class BanCog(ModcaseCog):
await ctx.send("You cannot set a temp ban to > 1 month", ephemeral=True) await ctx.send("You cannot set a temp ban to > 1 month", ephemeral=True)
return return
if delete_history and not time_pattern.match(delete_history): if delete_history and not time_pattern.match(delete_history):
await ctx.send("Invalid time string, please follow example: 1w 3d 7h 5m 20s", ephemeral=True) await ctx.send(
"Invalid time string, please follow example: 1w 3d 7h 5m 20s",
ephemeral=True,
)
return return
if len(reason) > 100: if len(reason) > 100:
await ctx.send("Reason must be < 100 characters", ephemeral=True) await ctx.send("Reason must be < 100 characters", ephemeral=True)
return return
if delete_history: if delete_history:
units = {"w": "weeks", "d": "days", "h": "hours", "m": "minutes", "s": "seconds"} units = {
"w": "weeks",
"d": "days",
"h": "hours",
"m": "minutes",
"s": "seconds",
}
delta = {"weeks": 0, "days": 0, "hours": 0, "minutes": 0, "seconds": 0} delta = {"weeks": 0, "days": 0, "hours": 0, "minutes": 0, "seconds": 0}
delete_history = delete_history.strip().lower() delete_history = delete_history.strip().lower()
if delete_history: if delete_history:
@ -148,7 +172,10 @@ class BanCog(ModcaseCog):
delete_history = int(timedelta(**delta).total_seconds()) delete_history = int(timedelta(**delta).total_seconds())
if delete_history > 604800: if delete_history > 604800:
await ctx.send("Delete history cannot be greater than 7 days (604800 seconds)", ephemeral=True) await ctx.send(
"Delete history cannot be greater than 7 days (604800 seconds)",
ephemeral=True,
)
return return
await ctx.defer() await ctx.defer()
@ -158,7 +185,9 @@ class BanCog(ModcaseCog):
mtype = "perma" mtype = "perma"
guild_name = ctx.guild.name guild_name = ctx.guild.name
user_message = f"You have been {mtype}banned from {guild_name}." + f" Reason:\n{reason}" user_message = (
f"You have been {mtype}banned from {guild_name}." + f" Reason:\n{reason}"
)
if mtype == "temp": if mtype == "temp":
user_message += f"\nDuration: {duration} hours" user_message += f"\nDuration: {duration} hours"
@ -187,7 +216,9 @@ class BanCog(ModcaseCog):
except Exception: except Exception:
self.logger.warn(f"Failed to send ban embed to {user.id}") self.logger.warn(f"Failed to send ban embed to {user.id}")
try: try:
await self.discord_apply_ban(ctx, reason, user, duration, active, mtype, delete_history or 0) await self.discord_apply_ban(
ctx, reason, user, duration, active, mtype, delete_history or 0
)
except Exception as e: except Exception as e:
await ctx.send(f"Failed to ban user:\n```\n{e}\n```", ephemeral=True) await ctx.send(f"Failed to ban user:\n```\n{e}\n```", ephemeral=True)
return return
@ -196,8 +227,18 @@ class BanCog(ModcaseCog):
await ctx.guild.unban(user, reason="Ban was softban") await ctx.guild.unban(user, reason="Ban was softban")
@slash_command(name="unban", description="Unban a user") @slash_command(name="unban", description="Unban a user")
@slash_option(name="user", description="User to unban", opt_type=OptionType.STRING, required=True) @slash_option(
@slash_option(name="reason", description="Unban reason", opt_type=OptionType.STRING, required=True) name="user",
description="User to unban",
opt_type=OptionType.STRING,
required=True,
)
@slash_option(
name="reason",
description="Unban reason",
opt_type=OptionType.STRING,
required=True,
)
@check(admin_or_permissions(Permissions.BAN_MEMBERS)) @check(admin_or_permissions(Permissions.BAN_MEMBERS))
async def _unban( async def _unban(
self, self,
@ -226,7 +267,8 @@ class BanCog(ModcaseCog):
user, discrim = user.split("#") user, discrim = user.split("#")
if discrim: if discrim:
discord_ban_info = find( discord_ban_info = find(
lambda x: x.user.username == user and x.user.discriminator == discrim, lambda x: x.user.username == user
and x.user.discriminator == discrim,
bans, bans,
) )
else: else:
@ -235,9 +277,16 @@ class BanCog(ModcaseCog):
if len(results) > 1: if len(results) > 1:
active_bans = [] active_bans = []
for ban in bans: for ban in bans:
active_bans.append("{0} ({1}): {2}".format(ban.user.username, ban.user.id, ban.reason)) active_bans.append(
"{0} ({1}): {2}".format(
ban.user.username, ban.user.id, ban.reason
)
)
ab_message = "\n".join(active_bans) ab_message = "\n".join(active_bans)
message = "More than one result. " f"Please use one of the following IDs:\n```{ab_message}\n```" message = (
"More than one result. "
f"Please use one of the following IDs:\n```{ab_message}\n```"
)
await ctx.send(message) await ctx.send(message)
return return
discord_ban_info = results[0] discord_ban_info = results[0]
@ -259,7 +308,7 @@ class BanCog(ModcaseCog):
if discrim: if discrim:
search["discrim"] = discrim search["discrim"] = discrim
database_ban_info = await Ban.find_one(*[getattr(Ban, k) == v for k, v in search.items]) database_ban_info = await Ban.find_one(search)
if not discord_ban_info and not database_ban_info: if not discord_ban_info and not database_ban_info:
await ctx.send(f"Unable to find user {orig_user}", ephemeral=True) await ctx.send(f"Unable to find user {orig_user}", ephemeral=True)
@ -282,7 +331,9 @@ class BanCog(ModcaseCog):
admin=ctx.author.id, admin=ctx.author.id,
reason=reason, reason=reason,
).save() ).save()
await ctx.send("Unable to find user in Discord, but removed entry from database.") await ctx.send(
"Unable to find user in Discord, but removed entry from database."
)
bans = SlashCommand(name="bans", description="User bans") bans = SlashCommand(name="bans", description="User bans")
@ -306,14 +357,16 @@ class BanCog(ModcaseCog):
required=False, required=False,
) )
@check(admin_or_permissions(Permissions.BAN_MEMBERS)) @check(admin_or_permissions(Permissions.BAN_MEMBERS))
async def _bans_list(self, ctx: InteractionContext, btype: int = 0, active: bool = True) -> None: async def _bans_list(
self, ctx: InteractionContext, btype: int = 0, active: bool = True
) -> None:
types = [0, "perm", "temp", "soft"] types = [0, "perm", "temp", "soft"]
search = {"guild": ctx.guild.id} search = {"guild": ctx.guild.id}
if active: if active:
search["active"] = True search["active"] = True
if btype > 0: if btype > 0:
search["type"] = types[btype] search["type"] = types[btype]
bans = await Ban.find(*[getattr(Ban, k) == v for k, v in search.items]).sort(-Ban.created_at).to_list() bans = await Ban.find(search).sort(-Ban.created_at).to_list()
db_bans = [] db_bans = []
fields = [] fields = []
for ban in bans: for ban in bans:
@ -322,7 +375,7 @@ class BanCog(ModcaseCog):
ban.username = user.username if user else "[deleted user]" ban.username = user.username if user else "[deleted user]"
fields.append( fields.append(
EmbedField( EmbedField(
name=f"Username: {ban.username}#{ban.discrim}", name=f"Username: {ban.username}",
value=( value=(
f"Date: {ban.created_at.strftime('%d-%m-%Y')}\n" f"Date: {ban.created_at.strftime('%d-%m-%Y')}\n"
f"User ID: {ban.user}\n" f"User ID: {ban.user}\n"
@ -339,7 +392,7 @@ class BanCog(ModcaseCog):
if ban.user.id not in db_bans: if ban.user.id not in db_bans:
fields.append( fields.append(
EmbedField( EmbedField(
name=f"Username: {ban.user.username}#" + f"{ban.user.discriminator}", name=f"Username: {ban.user.username}",
value=( value=(
f"Date: [unknown]\n" f"Date: [unknown]\n"
f"User ID: {ban.user.id}\n" f"User ID: {ban.user.id}\n"
@ -368,7 +421,9 @@ class BanCog(ModcaseCog):
pages.append(embed) pages.append(embed)
else: else:
for i in range(0, len(bans), 5): for i in range(0, len(bans), 5):
embed = build_embed(title=title, description="", fields=fields[i : i + 5]) embed = build_embed(
title=title, description="", fields=fields[i : i + 5]
)
embed.set_thumbnail(url=ctx.guild.icon.url) embed.set_thumbnail(url=ctx.guild.icon.url)
pages.append(embed) pages.append(embed)

View file

@ -31,63 +31,81 @@ class FilterCog(Extension):
self.bot = bot self.bot = bot
self.cache: Dict[int, List[str]] = {} self.cache: Dict[int, List[str]] = {}
async def _edit_filter(self, ctx: InteractionContext, name: str, search: bool = False) -> None: async def _edit_filter(
content = "" self, ctx: InteractionContext, name: str, search: bool = False
f: Filter = None ) -> None:
if search: try:
if f := await Filter.find_one(Filter.name == name, Filter.guild == ctx.guild.id): content = ""
content = "\n".join(f.filters) f: Filter = None
if search:
if f := await Filter.find_one(
Filter.name == name, Filter.guild == ctx.guild.id
):
content = "\n".join(f.filters)
kw = "Updating" if search else "Creating" kw = "Updating" if search else "Creating"
modal = Modal( modal = Modal(
title=f'{kw} filter "{name}"', *[
components=[ InputText(
InputText( label="Filter (one statement per line)",
label="Filter (one statement per line)", placeholder="" if content else "i.e. $bad_word^",
placeholder="" if content else "i.e. $bad_word^", custom_id="filters",
custom_id="filters", max_length=3000,
max_length=3000, value=content,
value=content, style=TextStyles.PARAGRAPH,
style=TextStyles.PARAGRAPH, )
],
title=f'{kw} filter "{name}"',
)
await ctx.send_modal(modal)
try:
data = await self.bot.wait_for_modal(
modal, author=ctx.author.id, timeout=60 * 5
) )
], filters = data.responses.get("filters").split("\n")
) except asyncio.TimeoutError:
await ctx.send_modal(modal) return
try: # Thanks, Glitter
data = await self.bot.wait_for_modal(modal, author=ctx.author.id, timeout=60 * 5) new_name = re.sub(r"[^\w-]", "", name)
filters = data.responses.get("filters").split("\n") try:
except asyncio.TimeoutError: if not f:
return f = Filter(name=new_name, guild=ctx.guild.id, filters=filters)
# Thanks, Glitter else:
new_name = re.sub(r"[^\w-]", "", name) f.name = new_name
try: f.filters = filters
if not f: await f.save()
f = Filter(name=new_name, guild=ctx.guild.id, filters=filters) except Exception as e:
else: await data.send(f"{e}", ephemeral=True)
f.name = new_name return
f.filters = filters
await f.save() content = content.splitlines()
diff = "\n".join(difflib.ndiff(content, filters)).replace("`", "\u200b`")
await data.send(
f"Filter `{new_name}` has been updated:\n\n```diff\n{diff}\n```"
)
if ctx.guild.id not in self.cache:
self.cache[ctx.guild.id] = []
if new_name not in self.cache[ctx.guild.id]:
self.cache[ctx.guild.id].append(new_name)
if name != new_name:
self.cache[ctx.guild.id].remove(name)
except Exception as e: except Exception as e:
await data.send(f"{e}", ephemeral=True) self.logger.error(e, exc_info=True)
return
content = content.splitlines()
diff = "\n".join(difflib.ndiff(content, filters)).replace("`", "\u200b`")
await data.send(f"Filter `{new_name}` has been updated:\n\n```diff\n{diff}\n```")
if ctx.guild.id not in self.cache:
self.cache[ctx.guild.id] = []
if new_name not in self.cache[ctx.guild.id]:
self.cache[ctx.guild.id].append(new_name)
if name != new_name:
self.cache[ctx.guild.id].remove(name)
filter_ = SlashCommand(name="filter", description="Manage keyword filters") filter_ = SlashCommand(name="filter", description="Manage keyword filters")
@filter_.subcommand(sub_cmd_name="create", sub_cmd_description="Create a new filter") @filter_.subcommand(
@slash_option(name="name", description="Name of new filter", required=True, opt_type=OptionType.STRING) sub_cmd_name="create", sub_cmd_description="Create a new filter"
)
@slash_option(
name="name",
description="Name of new filter",
required=True,
opt_type=OptionType.STRING,
)
@check(admin_or_permissions(Permissions.MANAGE_MESSAGES)) @check(admin_or_permissions(Permissions.MANAGE_MESSAGES))
async def _filter_create(self, ctx: InteractionContext, name: str) -> None: async def _filter_create(self, ctx: InteractionContext, name: str) -> None:
return await self._edit_filter(ctx, name) return await self._edit_filter(ctx, name)
@ -148,11 +166,13 @@ class FilterCog(Extension):
@_filter_edit.autocomplete("name") @_filter_edit.autocomplete("name")
@_filter_view.autocomplete("name") @_filter_view.autocomplete("name")
@_filter_delete.autocomplete("name") @_filter_delete.autocomplete("name")
async def _autocomplete(self, ctx: AutocompleteContext, name: str) -> None: async def _autocomplete(self, ctx: AutocompleteContext) -> None:
if not self.cache.get(ctx.guild.id): if not self.cache.get(ctx.guild.id):
filters = await Filter.find(Filter.guild == ctx.guild.id).to_list() filters = await Filter.find(Filter.guild == ctx.guild.id).to_list()
self.cache[ctx.guild.id] = [f.name for f in filters] self.cache[ctx.guild.id] = [f.name for f in filters]
results = process.extract(name, self.cache.get(ctx.guild.id), limit=25) results = process.extract(
ctx.input_text, self.cache.get(ctx.guild.id), limit=25
)
choices = [{"name": r[0], "value": r[0]} for r in results] choices = [{"name": r[0], "value": r[0]} for r in results]
await ctx.send(choices=choices) await ctx.send(choices=choices)

View file

@ -42,12 +42,18 @@ class CaseCog(Extension):
guild: Originating guild guild: Originating guild
""" """
action_table = Table() action_table = Table()
action_table.add_column(header="Type", justify="left", style="orange4", no_wrap=True) action_table.add_column(
action_table.add_column(header="Admin", justify="left", style="cyan", no_wrap=True) header="Type", justify="left", style="orange4", no_wrap=True
)
action_table.add_column(
header="Admin", justify="left", style="cyan", no_wrap=True
)
action_table.add_column(header="Reason", justify="left", style="white") action_table.add_column(header="Reason", justify="left", style="white")
note_table = Table() note_table = Table()
note_table.add_column(header="Admin", justify="left", style="cyan", no_wrap=True) note_table.add_column(
header="Admin", justify="left", style="cyan", no_wrap=True
)
note_table.add_column(header="Content", justify="left", style="white") note_table.add_column(header="Content", justify="left", style="white")
console = Console() console = Console()
@ -64,14 +70,18 @@ class CaseCog(Extension):
admin = await self.bot.fetch_user(parent_action.admin) admin = await self.bot.fetch_user(parent_action.admin)
admin_text = "[N/A]" admin_text = "[N/A]"
if admin: if admin:
admin_text = f"{admin.username}#{admin.discriminator}" admin_text = f"{admin.username}"
action_table.add_row(action.action_type.title(), admin_text, parent_action.reason) action_table.add_row(
action.action_type.title(), admin_text, parent_action.reason
)
with console.capture() as cap: with console.capture() as cap:
console.print(action_table) console.print(action_table)
tmp_output = cap.get() tmp_output = cap.get()
if len(tmp_output) >= 800: if len(tmp_output) >= 800:
action_output_extra = f"... and {len(mod_case.actions[idx:])} more actions" action_output_extra = (
f"... and {len(mod_case.actions[idx:])} more actions"
)
break break
action_output = tmp_output action_output = tmp_output
@ -83,7 +93,7 @@ class CaseCog(Extension):
admin = await self.bot.fetch_user(note.admin) admin = await self.bot.fetch_user(note.admin)
admin_text = "[N/A]" admin_text = "[N/A]"
if admin: if admin:
admin_text = f"{admin.username}#{admin.discriminator}" admin_text = f"{admin.username}"
note_table.add_row(admin_text, note.content) note_table.add_row(admin_text, note.content)
with console.capture() as cap: with console.capture() as cap:
@ -102,7 +112,7 @@ class CaseCog(Extension):
username = "[N/A]" username = "[N/A]"
user_text = "[N/A]" user_text = "[N/A]"
if user: if user:
username = f"{user.username}#{user.discriminator}" username = f"{user.username}"
user_text = user.mention user_text = user.mention
admin = await self.bot.fetch_user(mod_case.admin) admin = await self.bot.fetch_user(mod_case.admin)
@ -114,8 +124,13 @@ class CaseCog(Extension):
note_output = f"```ansi\n{note_output}\n{note_output_extra}\n```" note_output = f"```ansi\n{note_output}\n{note_output_extra}\n```"
fields = ( fields = (
EmbedField(name="Actions", value=action_output if mod_case.actions else "No Actions Found"), EmbedField(
EmbedField(name="Notes", value=note_output if mod_case.notes else "No Notes Found"), name="Actions",
value=action_output if mod_case.actions else "No Actions Found",
),
EmbedField(
name="Notes", value=note_output if mod_case.notes else "No Notes Found"
),
) )
embed = build_embed( embed = build_embed(
@ -148,7 +163,7 @@ class CaseCog(Extension):
user_mention = "[N/A]" user_mention = "[N/A]"
avatar_url = None avatar_url = None
if user: if user:
username = f"{user.username}#{user.discriminator}" username = f"{user.username}"
avatar_url = user.avatar.url avatar_url = user.avatar.url
user_mention = user.mention user_mention = user.mention
@ -166,7 +181,9 @@ class CaseCog(Extension):
if admin: if admin:
admin_text = admin.mention admin_text = admin.mention
fields = (EmbedField(name=action.action_type.title(), value=parent_action.reason),) fields = (
EmbedField(name=action.action_type.title(), value=parent_action.reason),
)
embed = build_embed( embed = build_embed(
title="Moderation Case Action", title="Moderation Case Action",
description=f"{admin_text} initiated an action against {user_mention}", description=f"{admin_text} initiated an action against {user_mention}",
@ -195,7 +212,12 @@ class CaseCog(Extension):
required=False, required=False,
) )
@check(admin_or_permissions(Permissions.BAN_MEMBERS)) @check(admin_or_permissions(Permissions.BAN_MEMBERS))
async def _cases_list(self, ctx: InteractionContext, user: Optional[Member] = None, closed: bool = False) -> None: async def _cases_list(
self,
ctx: InteractionContext,
user: Optional[Member] = None,
closed: bool = False,
) -> None:
query = [Modlog.guild == ctx.guild.id] query = [Modlog.guild == ctx.guild.id]
if not closed: if not closed:
query.append(Modlog.open == True) query.append(Modlog.open == True)
@ -214,8 +236,12 @@ class CaseCog(Extension):
case = SlashCommand(name="case", description="Manage a moderation case") case = SlashCommand(name="case", description="Manage a moderation case")
show = case.group(name="show", description="Show information about a specific case") show = case.group(name="show", description="Show information about a specific case")
@show.subcommand(sub_cmd_name="summary", sub_cmd_description="Summarize a specific case") @show.subcommand(
@slash_option(name="cid", description="Case ID", opt_type=OptionType.STRING, required=True) sub_cmd_name="summary", sub_cmd_description="Summarize a specific case"
)
@slash_option(
name="cid", description="Case ID", opt_type=OptionType.STRING, required=True
)
@check(admin_or_permissions(Permissions.BAN_MEMBERS)) @check(admin_or_permissions(Permissions.BAN_MEMBERS))
async def _case_show_summary(self, ctx: InteractionContext, cid: str) -> None: async def _case_show_summary(self, ctx: InteractionContext, cid: str) -> None:
case = await Modlog.find_one(Modlog.guild == ctx.guild.id, Modlog.nanoid == cid) case = await Modlog.find_one(Modlog.guild == ctx.guild.id, Modlog.nanoid == cid)
@ -227,7 +253,9 @@ class CaseCog(Extension):
await ctx.send(embeds=embed) await ctx.send(embeds=embed)
@show.subcommand(sub_cmd_name="actions", sub_cmd_description="Get case actions") @show.subcommand(sub_cmd_name="actions", sub_cmd_description="Get case actions")
@slash_option(name="cid", description="Case ID", opt_type=OptionType.STRING, required=True) @slash_option(
name="cid", description="Case ID", opt_type=OptionType.STRING, required=True
)
@check(admin_or_permissions(Permissions.BAN_MEMBERS)) @check(admin_or_permissions(Permissions.BAN_MEMBERS))
async def _case_show_actions(self, ctx: InteractionContext, cid: str) -> None: async def _case_show_actions(self, ctx: InteractionContext, cid: str) -> None:
case = await Modlog.find_one(Modlog.guild == ctx.guild.id, Modlog.nanoid == cid) case = await Modlog.find_one(Modlog.guild == ctx.guild.id, Modlog.nanoid == cid)
@ -240,7 +268,9 @@ class CaseCog(Extension):
await paginator.send(ctx) await paginator.send(ctx)
@case.subcommand(sub_cmd_name="close", sub_cmd_description="Show a specific case") @case.subcommand(sub_cmd_name="close", sub_cmd_description="Show a specific case")
@slash_option(name="cid", description="Case ID", opt_type=OptionType.STRING, required=True) @slash_option(
name="cid", description="Case ID", opt_type=OptionType.STRING, required=True
)
@check(admin_or_permissions(Permissions.BAN_MEMBERS)) @check(admin_or_permissions(Permissions.BAN_MEMBERS))
async def _case_close(self, ctx: InteractionContext, cid: str) -> None: async def _case_close(self, ctx: InteractionContext, cid: str) -> None:
case = await Modlog.find_one(Modlog.guild == ctx.guild.id, Modlog.nanoid == cid) case = await Modlog.find_one(Modlog.guild == ctx.guild.id, Modlog.nanoid == cid)
@ -254,8 +284,12 @@ class CaseCog(Extension):
embed = await self.get_summary_embed(case, ctx.guild) embed = await self.get_summary_embed(case, ctx.guild)
await ctx.send(embeds=embed) await ctx.send(embeds=embed)
@case.subcommand(sub_cmd_name="repoen", sub_cmd_description="Reopen a specific case") @case.subcommand(
@slash_option(name="cid", description="Case ID", opt_type=OptionType.STRING, required=True) sub_cmd_name="repoen", sub_cmd_description="Reopen a specific case"
)
@slash_option(
name="cid", description="Case ID", opt_type=OptionType.STRING, required=True
)
@check(admin_or_permissions(Permissions.BAN_MEMBERS)) @check(admin_or_permissions(Permissions.BAN_MEMBERS))
async def _case_reopen(self, ctx: InteractionContext, cid: str) -> None: async def _case_reopen(self, ctx: InteractionContext, cid: str) -> None:
case = await Modlog.find_one(Modlog.guild == ctx.guild.id, Modlog.nanoid == cid) case = await Modlog.find_one(Modlog.guild == ctx.guild.id, Modlog.nanoid == cid)
@ -269,9 +303,18 @@ class CaseCog(Extension):
embed = await self.get_summary_embed(case, ctx.guild) embed = await self.get_summary_embed(case, ctx.guild)
await ctx.send(embeds=embed) await ctx.send(embeds=embed)
@case.subcommand(sub_cmd_name="note", sub_cmd_description="Add a note to a specific case") @case.subcommand(
@slash_option(name="cid", description="Case ID", opt_type=OptionType.STRING, required=True) sub_cmd_name="note", sub_cmd_description="Add a note to a specific case"
@slash_option(name="note", description="Note to add", opt_type=OptionType.STRING, required=True) )
@slash_option(
name="cid", description="Case ID", opt_type=OptionType.STRING, required=True
)
@slash_option(
name="note",
description="Note to add",
opt_type=OptionType.STRING,
required=True,
)
@check(admin_or_permissions(Permissions.BAN_MEMBERS)) @check(admin_or_permissions(Permissions.BAN_MEMBERS))
async def _case_note(self, ctx: InteractionContext, cid: str, note: str) -> None: async def _case_note(self, ctx: InteractionContext, cid: str, note: str) -> None:
case = await Modlog.find_one(Modlog.guild == ctx.guild.id, Modlog.nanoid == cid) case = await Modlog.find_one(Modlog.guild == ctx.guild.id, Modlog.nanoid == cid)
@ -280,7 +323,9 @@ class CaseCog(Extension):
return return
if not case.open: if not case.open:
await ctx.send("Case is closed, please re-open to add a new comment", ephemeral=True) await ctx.send(
"Case is closed, please re-open to add a new comment", ephemeral=True
)
return return
if len(note) > 50: if len(note) > 50:
@ -296,11 +341,20 @@ class CaseCog(Extension):
await ctx.send(embeds=embed) await ctx.send(embeds=embed)
@case.subcommand(sub_cmd_name="new", sub_cmd_description="Open a new case") @case.subcommand(sub_cmd_name="new", sub_cmd_description="Open a new case")
@slash_option(name="user", description="Target user", opt_type=OptionType.USER, required=True) @slash_option(
@slash_option(name="note", description="Note to add", opt_type=OptionType.STRING, required=True) name="user", description="Target user", opt_type=OptionType.USER, required=True
)
@slash_option(
name="note",
description="Note to add",
opt_type=OptionType.STRING,
required=True,
)
@check(admin_or_permissions(Permissions.BAN_MEMBERS)) @check(admin_or_permissions(Permissions.BAN_MEMBERS))
async def _case_new(self, ctx: InteractionContext, user: Member, note: str) -> None: async def _case_new(self, ctx: InteractionContext, user: Member, note: str) -> None:
case = await Modlog.find_one(Modlog.guild == ctx.guild.id, Modlog.user == user.id, Modlog.open == True) case = await Modlog.find_one(
Modlog.guild == ctx.guild.id, Modlog.user == user.id, Modlog.open == True
)
if case: if case:
await ctx.send(f"Case already open with ID `{case.nanoid}`", ephemeral=True) await ctx.send(f"Case already open with ID `{case.nanoid}`", ephemeral=True)
return return
@ -315,7 +369,13 @@ class CaseCog(Extension):
note = Note(admin=ctx.author.id, content=note) note = Note(admin=ctx.author.id, content=note)
case = Modlog(user=user.id, guild=ctx.guild.id, admin=ctx.author.id, notes=[note], actions=[]) case = Modlog(
user=user.id,
guild=ctx.guild.id,
admin=ctx.author.id,
notes=[note],
actions=[],
)
await case.save() await case.save()
embed = await self.get_summary_embed(case, ctx.guild) embed = await self.get_summary_embed(case, ctx.guild)

View file

@ -27,7 +27,9 @@ from jarvis.utils.permissions import admin_or_permissions
class MuteCog(ModcaseCog): class MuteCog(ModcaseCog):
"""JARVIS MuteCog.""" """JARVIS MuteCog."""
async def _apply_timeout(self, ctx: InteractionContext, user: Member, reason: str, until: datetime) -> None: async def _apply_timeout(
self, ctx: InteractionContext, user: Member, reason: str, until: datetime
) -> None:
await user.timeout(communication_disabled_until=until, reason=reason) await user.timeout(communication_disabled_until=until, reason=reason)
duration = int((until - datetime.now(tz=timezone.utc)).seconds / 60) duration = int((until - datetime.now(tz=timezone.utc)).seconds / 60)
await Mute( await Mute(
@ -42,11 +44,14 @@ class MuteCog(ModcaseCog):
return mute_embed(user=user, admin=ctx.author, reason=reason, guild=ctx.guild) return mute_embed(user=user, admin=ctx.author, reason=reason, guild=ctx.guild)
@context_menu(name="Mute User", context_type=CommandType.USER) @context_menu(name="Mute User", context_type=CommandType.USER)
@check(admin_or_permissions(Permissions.MUTE_MEMBERS, Permissions.BAN_MEMBERS, Permissions.KICK_MEMBERS)) @check(
admin_or_permissions(
Permissions.MUTE_MEMBERS, Permissions.BAN_MEMBERS, Permissions.KICK_MEMBERS
)
)
async def _timeout_cm(self, ctx: InteractionContext) -> None: async def _timeout_cm(self, ctx: InteractionContext) -> None:
modal = Modal( modal = Modal(
title=f"Muting {ctx.target.mention}", *[
components=[
InputText( InputText(
label="Reason?", label="Reason?",
placeholder="Spamming, harrassment, etc", placeholder="Spamming, harrassment, etc",
@ -62,10 +67,13 @@ class MuteCog(ModcaseCog):
max_length=100, max_length=100,
), ),
], ],
title=f"Muting {ctx.target.mention}",
) )
await ctx.send_modal(modal) await ctx.send_modal(modal)
try: try:
response = await self.bot.wait_for_modal(modal, author=ctx.author.id, timeout=60 * 5) response = await self.bot.wait_for_modal(
modal, author=ctx.author.id, timeout=60 * 5
)
reason = response.responses.get("reason") reason = response.responses.get("reason")
until = response.responses.get("until") until = response.responses.get("until")
except asyncio.TimeoutError: except asyncio.TimeoutError:
@ -76,7 +84,9 @@ class MuteCog(ModcaseCog):
"RETURN_AS_TIMEZONE_AWARE": True, "RETURN_AS_TIMEZONE_AWARE": True,
} }
rt_settings = base_settings.copy() rt_settings = base_settings.copy()
rt_settings["PARSERS"] = [x for x in default_parsers if x not in ["absolute-time", "timestamp"]] rt_settings["PARSERS"] = [
x for x in default_parsers if x not in ["absolute-time", "timestamp"]
]
rt_until = parse(until, settings=rt_settings) rt_until = parse(until, settings=rt_settings)
@ -91,10 +101,14 @@ class MuteCog(ModcaseCog):
until = at_until until = at_until
else: else:
self.logger.debug(f"Failed to parse delay: {until}") self.logger.debug(f"Failed to parse delay: {until}")
await response.send(f"`{until}` is not a parsable date, please try again", ephemeral=True) await response.send(
f"`{until}` is not a parsable date, please try again", ephemeral=True
)
return return
if until < datetime.now(tz=timezone.utc): if until < datetime.now(tz=timezone.utc):
await response.send(f"`{old_until}` is in the past, which isn't allowed", ephemeral=True) await response.send(
f"`{old_until}` is in the past, which isn't allowed", ephemeral=True
)
return return
try: try:
embed = await self._apply_timeout(ctx, ctx.target, reason, until) embed = await self._apply_timeout(ctx, ctx.target, reason, until)
@ -103,7 +117,9 @@ class MuteCog(ModcaseCog):
await response.send("Unable to mute this user", ephemeral=True) await response.send("Unable to mute this user", ephemeral=True)
@slash_command(name="mute", description="Mute a user") @slash_command(name="mute", description="Mute a user")
@slash_option(name="user", description="User to mute", opt_type=OptionType.USER, required=True) @slash_option(
name="user", description="User to mute", opt_type=OptionType.USER, required=True
)
@slash_option( @slash_option(
name="reason", name="reason",
description="Reason for mute", description="Reason for mute",
@ -128,9 +144,18 @@ class MuteCog(ModcaseCog):
SlashCommandChoice(name="Week(s)", value=10080), SlashCommandChoice(name="Week(s)", value=10080),
], ],
) )
@check(admin_or_permissions(Permissions.MUTE_MEMBERS, Permissions.BAN_MEMBERS, Permissions.KICK_MEMBERS)) @check(
admin_or_permissions(
Permissions.MUTE_MEMBERS, Permissions.BAN_MEMBERS, Permissions.KICK_MEMBERS
)
)
async def _timeout( async def _timeout(
self, ctx: InteractionContext, user: Member, reason: str, time: int = 1, scale: int = 60 self,
ctx: InteractionContext,
user: Member,
reason: str,
time: int = 1,
scale: int = 60,
) -> None: ) -> None:
if user == ctx.author: if user == ctx.author:
await ctx.send("You cannot mute yourself.", ephemeral=True) await ctx.send("You cannot mute yourself.", ephemeral=True)
@ -148,7 +173,9 @@ class MuteCog(ModcaseCog):
# Max 4 weeks (2419200 seconds) per API # Max 4 weeks (2419200 seconds) per API
duration = time * scale duration = time * scale
if duration > 40320: if duration > 40320:
await ctx.send("Mute must be less than 4 weeks (40,320 minutes)", ephemeral=True) await ctx.send(
"Mute must be less than 4 weeks (40,320 minutes)", ephemeral=True
)
return return
until = datetime.now(tz=timezone.utc) + timedelta(minutes=duration) until = datetime.now(tz=timezone.utc) + timedelta(minutes=duration)
@ -159,13 +186,28 @@ class MuteCog(ModcaseCog):
await ctx.send("Unable to mute this user", ephemeral=True) await ctx.send("Unable to mute this user", ephemeral=True)
@slash_command(name="unmute", description="Unmute a user") @slash_command(name="unmute", description="Unmute a user")
@slash_option(name="user", description="User to unmute", opt_type=OptionType.USER, required=True) @slash_option(
@slash_option(name="reason", description="Reason for unmute", opt_type=OptionType.STRING, required=True) name="user",
@check(admin_or_permissions(Permissions.MUTE_MEMBERS, Permissions.BAN_MEMBERS, Permissions.KICK_MEMBERS)) description="User to unmute",
opt_type=OptionType.USER,
required=True,
)
@slash_option(
name="reason",
description="Reason for unmute",
opt_type=OptionType.STRING,
required=True,
)
@check(
admin_or_permissions(
Permissions.MUTE_MEMBERS, Permissions.BAN_MEMBERS, Permissions.KICK_MEMBERS
)
)
async def _unmute(self, ctx: InteractionContext, user: Member, reason: str) -> None: async def _unmute(self, ctx: InteractionContext, user: Member, reason: str) -> None:
if ( if (
not user.communication_disabled_until not user.communication_disabled_until
or user.communication_disabled_until.timestamp() < datetime.now(tz=timezone.utc).timestamp() # noqa: W503 or user.communication_disabled_until.timestamp()
< datetime.now(tz=timezone.utc).timestamp() # noqa: W503
): ):
await ctx.send("User is not muted", ephemeral=True) await ctx.send("User is not muted", ephemeral=True)
return return
@ -176,6 +218,8 @@ class MuteCog(ModcaseCog):
await user.timeout(communication_disabled_until=datetime.now(tz=timezone.utc)) await user.timeout(communication_disabled_until=datetime.now(tz=timezone.utc))
embed = unmute_embed(user=user, admin=ctx.author, reason=reason, guild=ctx.guild) embed = unmute_embed(
user=user, admin=ctx.author, reason=reason, guild=ctx.guild
)
await ctx.send(embeds=embed) await ctx.send(embeds=embed)

View file

@ -24,7 +24,9 @@ class WarningCog(ModcaseCog):
"""JARVIS WarningCog.""" """JARVIS WarningCog."""
@slash_command(name="warn", description="Warn a user") @slash_command(name="warn", description="Warn a user")
@slash_option(name="user", description="User to warn", opt_type=OptionType.USER, required=True) @slash_option(
name="user", description="User to warn", opt_type=OptionType.USER, required=True
)
@slash_option( @slash_option(
name="reason", name="reason",
description="Reason for warning", description="Reason for warning",
@ -38,7 +40,9 @@ class WarningCog(ModcaseCog):
required=False, required=False,
) )
@check(admin_or_permissions(Permissions.MANAGE_GUILD)) @check(admin_or_permissions(Permissions.MANAGE_GUILD))
async def _warn(self, ctx: InteractionContext, user: Member, reason: str, duration: int = 24) -> None: async def _warn(
self, ctx: InteractionContext, user: Member, reason: str, duration: int = 24
) -> None:
if len(reason) > 100: if len(reason) > 100:
await ctx.send("Reason must be < 100 characters", ephemeral=True) await ctx.send("Reason must be < 100 characters", ephemeral=True)
return return
@ -66,15 +70,19 @@ class WarningCog(ModcaseCog):
await ctx.send(embeds=embed) await ctx.send(embeds=embed)
@slash_command(name="warnings", description="Get count of user warnings") @slash_command(name="warnings", description="Get count of user warnings")
@slash_option(name="user", description="User to view", opt_type=OptionType.USER, required=True) @slash_option(
name="user", description="User to view", opt_type=OptionType.USER, required=True
)
@slash_option( @slash_option(
name="active", name="active",
description="View active only", description="View active only",
opt_type=OptionType.BOOLEAN, opt_type=OptionType.BOOLEAN,
required=False, required=False,
) )
@check(admin_or_permissions(Permissions.MANAGE_GUILD)) # @check(admin_or_permissions(Permissions.MANAGE_GUILD))
async def _warnings(self, ctx: InteractionContext, user: Member, active: bool = True) -> None: async def _warnings(
self, ctx: InteractionContext, user: Member, active: bool = True
) -> None:
warnings = ( warnings = (
await Warning.find( await Warning.find(
Warning.user == user.id, Warning.user == user.id,
@ -84,6 +92,7 @@ class WarningCog(ModcaseCog):
.to_list() .to_list()
) )
if len(warnings) == 0: if len(warnings) == 0:
await ctx.defer(ephemeral=True)
await ctx.send("That user has no warnings.", ephemeral=True) await ctx.send("That user has no warnings.", ephemeral=True)
return return
active_warns = get_all(warnings, active=True) active_warns = get_all(warnings, active=True)
@ -117,7 +126,9 @@ class WarningCog(ModcaseCog):
for i in range(0, len(fields), 5): for i in range(0, len(fields), 5):
embed = build_embed( embed = build_embed(
title="Warnings", title="Warnings",
description=(f"{len(warnings)} total | {len(active_warns)} currently active"), description=(
f"{len(warnings)} total | {len(active_warns)} currently active"
),
fields=fields[i : i + 5], fields=fields[i : i + 5],
) )
embed.set_author( embed.set_author(
@ -125,7 +136,9 @@ class WarningCog(ModcaseCog):
icon_url=user.display_avatar.url, icon_url=user.display_avatar.url,
) )
embed.set_thumbnail(url=ctx.guild.icon.url) embed.set_thumbnail(url=ctx.guild.icon.url)
embed.set_footer(text=f"{user.username}#{user.discriminator} | {user.id}") embed.set_footer(
text=f"{user.username}#{user.discriminator} | {user.id}"
)
pages.append(embed) pages.append(embed)
else: else:
fields = [] fields = []
@ -143,10 +156,15 @@ class WarningCog(ModcaseCog):
for i in range(0, len(fields), 5): for i in range(0, len(fields), 5):
embed = build_embed( embed = build_embed(
title="Warnings", title="Warnings",
description=(f"{len(warnings)} total | {len(active_warns)} currently active"), description=(
f"{len(warnings)} total | {len(active_warns)} currently active"
),
fields=fields[i : i + 5], fields=fields[i : i + 5],
) )
embed.set_author(name=user.username + "#" + user.discriminator, icon_url=user.display_avatar.url) embed.set_author(
name=user.username + "#" + user.discriminator,
icon_url=user.display_avatar.url,
)
embed.set_thumbnail(url=ctx.guild.icon.url) embed.set_thumbnail(url=ctx.guild.icon.url)
pages.append(embed) pages.append(embed)

View file

@ -5,6 +5,8 @@ import re
from datetime import datetime, timezone from datetime import datetime, timezone
from typing import List from typing import List
import pytz
from croniter import croniter
from dateparser import parse from dateparser import parse
from dateparser_data.settings import default_parsers from dateparser_data.settings import default_parsers
from interactions import AutocompleteContext, Client, Extension, InteractionContext from interactions import AutocompleteContext, Client, Extension, InteractionContext
@ -23,7 +25,7 @@ from thefuzz import process
from jarvis.utils import build_embed from jarvis.utils import build_embed
valid = re.compile(r"[\w\s\-\\/.!@#$%^*()+=<>:'\",\u0080-\U000E0FFF]*") valid = re.compile(r"[\w\s\-\\/.!@?#$%^*()+=<>:'\",\u0080-\U000E0FFF]*")
time_pattern = re.compile(r"(\d+\.?\d?[s|m|h|d|w]{1})\s?", flags=re.IGNORECASE) time_pattern = re.compile(r"(\d+\.?\d?[s|m|h|d|w]{1})\s?", flags=re.IGNORECASE)
invites = re.compile( invites = re.compile(
r"(?:https?://)?(?:www.)?(?:discord.(?:gg|io|me|li)|discord(?:app)?.com/invite)/([^\s/]+?)(?=\b)", # noqa: E501 r"(?:https?://)?(?:www.)?(?:discord.(?:gg|io|me|li)|discord(?:app)?.com/invite)/([^\s/]+?)(?=\b)", # noqa: E501
@ -41,6 +43,13 @@ class RemindmeCog(Extension):
reminders = SlashCommand(name="reminders", description="Manage reminders") reminders = SlashCommand(name="reminders", description="Manage reminders")
@reminders.subcommand(sub_cmd_name="set", sub_cmd_description="Set a reminder") @reminders.subcommand(sub_cmd_name="set", sub_cmd_description="Set a reminder")
@slash_option(
name="timezone",
description="Timezone to use",
opt_type=OptionType.STRING,
required=False,
autocomplete=True,
)
@slash_option( @slash_option(
name="private", name="private",
description="Send as DM?", description="Send as DM?",
@ -50,15 +59,16 @@ class RemindmeCog(Extension):
async def _remindme( async def _remindme(
self, self,
ctx: InteractionContext, ctx: InteractionContext,
timezone: str = "UTC",
private: bool = None, private: bool = None,
) -> None: ) -> None:
if private is None and ctx.guild: if private is None and ctx.guild:
private = ctx.guild.member_count >= 5000 private = ctx.guild.member_count >= 5000
elif private is None and not ctx.guild: elif private is None and not ctx.guild:
private = False private = False
timezone = pytz.timezone(timezone)
modal = Modal( modal = Modal(
title="Set your reminder!", *[
components=[
InputText( InputText(
label="What to remind you?", label="What to remind you?",
placeholder="Reminder", placeholder="Reminder",
@ -72,14 +82,26 @@ class RemindmeCog(Extension):
style=TextStyles.SHORT, style=TextStyles.SHORT,
custom_id="delay", custom_id="delay",
), ),
InputText(
label="Cron pattern for repeating",
placeholder="0 12 * * *",
style=TextStyles.SHORT,
max_length=40,
custom_id="cron",
required=False,
),
], ],
title="Set your reminder!",
) )
await ctx.send_modal(modal) await ctx.send_modal(modal)
try: try:
response = await self.bot.wait_for_modal(modal, author=ctx.author.id, timeout=60 * 5) response = await self.bot.wait_for_modal(
modal, author=ctx.author.id, timeout=60 * 5
)
message = response.responses.get("message").strip() message = response.responses.get("message").strip()
delay = response.responses.get("delay").strip() delay = response.responses.get("delay").strip()
cron = response.responses.get("cron").strip()
except asyncio.TimeoutError: except asyncio.TimeoutError:
return return
if len(message) > 500: if len(message) > 500:
@ -91,20 +113,32 @@ class RemindmeCog(Extension):
ephemeral=True, ephemeral=True,
) )
return return
elif not valid.fullmatch(message): # elif not valid.fullmatch(message):
await response.send("Hey, you should probably make this readable", ephemeral=True) # await response.send(
return # "Hey, you should probably make this readable", ephemeral=True
# )
# return
elif len(message) == 0: elif len(message) == 0:
await response.send("Hey, you should probably add content to your reminder", ephemeral=True) await response.send(
"Hey, you should probably add content to your reminder", ephemeral=True
)
return
elif cron and not croniter.is_valid(cron):
await response.send(
f"Invalid cron: {cron}\n\nUse https://crontab.guru to help",
ephemeral=True,
)
return return
base_settings = { base_settings = {
"PREFER_DATES_FROM": "future", "PREFER_DATES_FROM": "future",
"TIMEZONE": "UTC", "TIMEZONE": str(timezone),
"RETURN_AS_TIMEZONE_AWARE": True, "RETURN_AS_TIMEZONE_AWARE": True,
} }
rt_settings = base_settings.copy() rt_settings = base_settings.copy()
rt_settings["PARSERS"] = [x for x in default_parsers if x not in ["absolute-time", "timestamp"]] rt_settings["PARSERS"] = [
x for x in default_parsers if x not in ["absolute-time", "timestamp"]
]
rt_remind_at = parse(delay, settings=rt_settings) rt_remind_at = parse(delay, settings=rt_settings)
@ -118,14 +152,20 @@ class RemindmeCog(Extension):
remind_at = at_remind_at remind_at = at_remind_at
else: else:
self.logger.debug(f"Failed to parse delay: {delay}") self.logger.debug(f"Failed to parse delay: {delay}")
await response.send(f"`{delay}` is not a parsable date, please try again", ephemeral=True) await response.send(
f"`{delay}` is not a parsable date, please try again",
ephemeral=True,
)
return return
if remind_at < datetime.now(tz=timezone.utc): if remind_at < datetime.now(tz=timezone):
await response.send(f"`{delay}` is in the past. Past reminders aren't allowed", ephemeral=True) await response.send(
f"`{delay}` is in the past. Past reminders aren't allowed",
ephemeral=True,
)
return return
elif remind_at < datetime.now(tz=timezone.utc): elif remind_at < datetime.now(tz=timezone):
pass pass
r = Reminder( r = Reminder(
@ -135,39 +175,59 @@ class RemindmeCog(Extension):
message=message, message=message,
remind_at=remind_at, remind_at=remind_at,
private=private, private=private,
repeat=cron,
timezone=str(timezone),
active=True, active=True,
) )
await r.save() await r.save()
fields = [
EmbedField(name="Message", value=message),
EmbedField(
name="When",
value=f"<t:{int(remind_at.timestamp())}:F> (<t:{int(remind_at.timestamp())}:R>)",
inline=False,
),
]
if r.repeat:
c = croniter(cron, remind_at)
fields.append(EmbedField(name="Repeat Schedule", value=f"`{cron}`"))
next_5 = [c.get_next() for _ in range(5)]
next_5_str = "\n".join(f"<t:{int(x)}:F> (<t:{int(x)}:R>)" for x in next_5)
fields.append(EmbedField(name="Next 5 runs", value=next_5_str))
embed = build_embed( embed = build_embed(
title="Reminder Set", title="Reminder Set",
description=f"{ctx.author.mention} set a reminder", description=f"{ctx.author.mention} set a reminder",
fields=[ fields=fields,
EmbedField(name="Message", value=message),
EmbedField(
name="When",
value=f"<t:{int(remind_at.timestamp())}:F> (<t:{int(remind_at.timestamp())}:R>)",
inline=False,
),
],
) )
embed.set_author( embed.set_author(
name=ctx.author.username + "#" + ctx.author.discriminator, name=ctx.author.username,
icon_url=ctx.author.display_avatar.url, icon_url=ctx.author.display_avatar.url,
) )
embed.set_thumbnail(url=ctx.author.display_avatar.url) embed.set_thumbnail(url=ctx.author.display_avatar.url)
delete_button = Button(style=ButtonStyle.DANGER, emoji="🗑️", custom_id=f"delete|{ctx.author.id}") delete_button = Button(
style=ButtonStyle.DANGER,
emoji="🗑️",
custom_id=f"delete|{ctx.author.id}",
)
components = [delete_button] components = [delete_button]
if not r.guild == ctx.author.id: if not r.guild == ctx.author.id:
copy_button = Button(style=ButtonStyle.GREEN, emoji="📋", custom_id=f"copy|rme|{r.id}") copy_button = Button(
style=ButtonStyle.GREEN, emoji="📋", custom_id=f"copy|rme|{r.id}"
)
components.append(copy_button) components.append(copy_button)
private = private if private is not None else False private = private if private is not None else False
components = [ActionRow(*components)] components = [ActionRow(*components)]
await response.send(embeds=embed, components=components, ephemeral=private) await response.send(embeds=embed, components=components, ephemeral=private)
async def get_reminders_embed(self, ctx: InteractionContext, reminders: List[Reminder]) -> Embed: async def get_reminders_embed(
self, ctx: InteractionContext, reminders: List[Reminder]
) -> Embed:
"""Build embed for paginator.""" """Build embed for paginator."""
fields = [] fields = []
for reminder in reminders: for reminder in reminders:
@ -195,7 +255,7 @@ class RemindmeCog(Extension):
) )
embed.set_author( embed.set_author(
name=ctx.author.username + "#" + ctx.author.discriminator, name=ctx.author.username,
icon_url=ctx.author.display_avatar.url, icon_url=ctx.author.display_avatar.url,
) )
embed.set_thumbnail(url=ctx.author.display_avatar.url) embed.set_thumbnail(url=ctx.author.display_avatar.url)
@ -204,16 +264,22 @@ class RemindmeCog(Extension):
@reminders.subcommand(sub_cmd_name="list", sub_cmd_description="List reminders") @reminders.subcommand(sub_cmd_name="list", sub_cmd_description="List reminders")
async def _list(self, ctx: InteractionContext) -> None: async def _list(self, ctx: InteractionContext) -> None:
reminders = await Reminder.find(Reminder.user == ctx.author.id, Reminder.active == True).to_list() reminders = await Reminder.find(
Reminder.user == ctx.author.id, Reminder.active == True
).to_list()
if not reminders: if not reminders:
await ctx.send("You have no reminders set.", ephemeral=True) await ctx.send("You have no reminders set.", ephemeral=True)
return return
embed = await self.get_reminders_embed(ctx, reminders) embed = await self.get_reminders_embed(ctx, reminders)
components = Button(style=ButtonStyle.DANGER, emoji="🗑️", custom_id=f"delete|{ctx.author.id}") components = Button(
style=ButtonStyle.DANGER, emoji="🗑️", custom_id=f"delete|{ctx.author.id}"
)
await ctx.send(embeds=embed, components=components) await ctx.send(embeds=embed, components=components)
@reminders.subcommand(sub_cmd_name="delete", sub_cmd_description="Delete a reminder") @reminders.subcommand(
sub_cmd_name="delete", sub_cmd_description="Delete a reminder"
)
@slash_option( @slash_option(
name="content", name="content",
description="Content of the reminder", description="Content of the reminder",
@ -222,7 +288,7 @@ class RemindmeCog(Extension):
autocomplete=True, autocomplete=True,
) )
async def _delete(self, ctx: InteractionContext, content: str) -> None: async def _delete(self, ctx: InteractionContext, content: str) -> None:
reminder = await Reminder.find_one(Reminder.id == content) reminder = await Reminder.get(content)
if not reminder: if not reminder:
await ctx.send(f"Reminder `{content}` does not exist", ephemeral=True) await ctx.send(f"Reminder `{content}` does not exist", ephemeral=True)
return return
@ -238,12 +304,14 @@ class RemindmeCog(Extension):
) )
embed.set_author( embed.set_author(
name=ctx.author.display_name + "#" + ctx.author.discriminator, name=ctx.author.display_name,
icon_url=ctx.author.display_avatar.url, icon_url=ctx.author.display_avatar.url,
) )
embed.set_thumbnail(url=ctx.author.display_avatar.url) embed.set_thumbnail(url=ctx.author.display_avatar.url)
components = Button(style=ButtonStyle.DANGER, emoji="🗑️", custom_id=f"delete|{ctx.author.id}") components = Button(
style=ButtonStyle.DANGER, emoji="🗑️", custom_id=f"delete|{ctx.author.id}"
)
try: try:
await reminder.delete() await reminder.delete()
except Exception: except Exception:
@ -275,14 +343,18 @@ class RemindmeCog(Extension):
EmbedField(name="Created At", value=f"<t:{cts}:F> (<t:{cts}:R>)"), EmbedField(name="Created At", value=f"<t:{cts}:F> (<t:{cts}:R>)"),
] ]
embed = build_embed(title="You have a reminder!", description=reminder.message, fields=fields) embed = build_embed(
title="You have a reminder!", description=reminder.message, fields=fields
)
embed.set_author( embed.set_author(
name=ctx.author.display_name + "#" + ctx.author.discriminator, name=ctx.author.display_name,
icon_url=ctx.author.display_avatar.url, icon_url=ctx.author.display_avatar.url,
) )
embed.set_thumbnail(url=ctx.author.display_avatar.url) embed.set_thumbnail(url=ctx.author.display_avatar.url)
components = Button(style=ButtonStyle.DANGER, emoji="🗑️", custom_id=f"delete|{ctx.author.id}") components = Button(
style=ButtonStyle.DANGER, emoji="🗑️", custom_id=f"delete|{ctx.author.id}"
)
await ctx.send(embeds=embed, ephemeral=reminder.private, components=components) await ctx.send(embeds=embed, ephemeral=reminder.private, components=components)
if reminder.remind_at <= datetime.now(tz=timezone.utc) and not reminder.active: if reminder.remind_at <= datetime.now(tz=timezone.utc) and not reminder.active:
try: try:
@ -292,13 +364,22 @@ class RemindmeCog(Extension):
@_fetch.autocomplete("content") @_fetch.autocomplete("content")
@_delete.autocomplete("content") @_delete.autocomplete("content")
async def _search_reminders(self, ctx: AutocompleteContext, content: str) -> None: async def _search_reminders(self, ctx: AutocompleteContext) -> None:
reminders = await Reminder.find(Reminder.user == ctx.author.id).to_list() reminders = await Reminder.find(Reminder.user == ctx.author.id).to_list()
lookup = {r.message: str(r.id) for r in reminders} lookup = {
results = process.extract(content, list(lookup.keys()), limit=5) f"[{r.created_at.strftime('%d/%m/%Y %H:%M.%S')}] {r.message}": str(r.id)
for r in reminders
}
results = process.extract(ctx.input_text, list(lookup.keys()), limit=5)
choices = [{"name": r[0], "value": lookup[r[0]]} for r in results] choices = [{"name": r[0], "value": lookup[r[0]]} for r in results]
await ctx.send(choices=choices) await ctx.send(choices=choices)
@_remindme.autocomplete("timezone")
async def _timezone_autocomplete(self, ctx: AutocompleteContext):
results = process.extract(ctx.input_text, pytz.all_timezones_set, limit=5)
choices = [{"name": r[0], "value": r[0]} for r in results if r[1] > 80.0]
await ctx.send(choices)
def setup(bot: Client) -> None: def setup(bot: Client) -> None:
"""Add RemindmeCog to JARVIS""" """Add RemindmeCog to JARVIS"""

View file

@ -2,10 +2,8 @@
from interactions import Client from interactions import Client
from jarvis.cogs.core.socials import reddit, twitter
def setup(bot: Client) -> None: def setup(bot: Client) -> None:
"""Add social cogs to JARVIS""" """Add social cogs to JARVIS"""
reddit.RedditCog(bot) # Unfortunately there's no social cogs anymore
twitter.TwitterCog(bot) # Mastodon will come in the future

View file

@ -1,568 +0,0 @@
"""JARVIS Reddit cog."""
import asyncio
import logging
import re
from typing import List, Optional
from asyncpraw import Reddit
from asyncpraw.models.reddit.submission import Submission
from asyncpraw.models.reddit.submission import Subreddit as Sub
from asyncprawcore.exceptions import Forbidden, NotFound, Redirect
from interactions import Client, Extension, InteractionContext, Permissions
from interactions.client.utils.misc_utils import get
from interactions.models.discord.channel import ChannelType, GuildText
from interactions.models.discord.components import (
ActionRow,
StringSelectMenu,
StringSelectOption,
)
from interactions.models.discord.embed import Embed, EmbedField
from interactions.models.internal.application_commands import (
OptionType,
SlashCommand,
SlashCommandChoice,
slash_option,
)
from interactions.models.internal.command import check
from jarvis_core.db.models import Subreddit, SubredditFollow, UserSetting
from jarvis import const
from jarvis.config import load_config
from jarvis.utils import build_embed
from jarvis.utils.permissions import admin_or_permissions
DEFAULT_USER_AGENT = f"python:JARVIS:{const.__version__} (by u/zevaryx)"
sub_name = re.compile(r"\A[A-Za-z0-9][A-Za-z0-9_]{2,20}\Z")
user_name = re.compile(r"[A-Za-z0-9_-]+")
image_link = re.compile(r"https?://(?:www)?\.?preview\.redd\.it\/(.*\..*)\?.*")
class RedditCog(Extension):
"""JARVIS Reddit Cog."""
def __init__(self, bot: Client):
self.bot = bot
self.logger = logging.getLogger(__name__)
config = load_config()
config.reddit.user_agent = config.reddit.user_agent or DEFAULT_USER_AGENT
self.api = Reddit(**config.reddit.dict())
async def post_embeds(self, sub: Sub, post: Submission) -> Optional[List[Embed]]:
"""
Build a post embeds.
Args:
post: Post to build embeds
"""
url = f"https://redd.it/{post.id}"
await post.author.load()
author_url = f"https://reddit.com/u/{post.author.name}"
author_icon = post.author.icon_img
images = []
title = post.title
if len(title) > 256:
title = title[:253] + "..."
fields = []
content = ""
og_post = None
if "crosspost_parent_list" in vars(post):
og_post = post # noqa: F841
post = await self.api.submission(post.crosspost_parent_list[0]["id"])
await post.load()
fields.append(EmbedField(name="Crossposted From", value=post.subreddit_name_prefixed))
content = f"> **{post.title}**"
if "url" in vars(post):
if any(post.url.endswith(x) for x in ["jpeg", "jpg", "png", "gif"]):
images = [post.url]
if "media_metadata" in vars(post):
for k, v in post.media_metadata.items():
if v["status"] != "valid" or v["m"] not in ["image/jpg", "image/png", "image/gif"]:
continue
ext = v["m"].split("/")[-1]
i_url = f"https://i.redd.it/{k}.{ext}"
images.append(i_url)
if len(images) == 4:
break
if "selftext" in vars(post) and post.selftext:
text = post.selftext
if post.spoiler:
text = "||" + text + "||"
content += "\n\n" + post.selftext
if len(content) > 900:
content = content[:900] + "..."
if post.spoiler:
content += "||"
content += f"\n\n[View this post]({url})"
content = "\n".join(image_link.sub(r"https://i.redd.it/\1", x) for x in content.split("\n"))
if not images and not content:
self.logger.debug(f"Post {post.id} had neither content nor images?")
return None
color = "#FF4500"
if "primary_color" in vars(sub):
color = sub.primary_color
base_embed = build_embed(
title=title,
description=content,
fields=fields,
timestamp=post.created_utc,
url=url,
color=color,
)
base_embed.set_author(name="u/" + post.author.name, url=author_url, icon_url=author_icon)
base_embed.set_footer(
text=f"r/{sub.display_name}",
icon_url="https://www.redditinc.com/assets/images/site/reddit-logo.png",
)
embeds = [base_embed]
if len(images) > 0:
embeds[0].set_image(url=images[0])
for image in images[1:4]:
embed = Embed(url=url)
embed.set_image(url=image)
embeds.append(embed)
return embeds
reddit = SlashCommand(name="reddit", description="Manage Reddit follows")
follow = reddit.group(name="follow", description="Add a follow")
unfollow = reddit.group(name="unfollow", description="Remove a follow")
# Due to bugs and missing models, this section is commented out for the time being
# TODO:
# 1. Fix bugs
# 2. Migrate to beanie
#
# @follow.subcommand(sub_cmd_name="redditor", sub_cmd_description="Follow a Redditor")
# @slash_option(
# name="name",
# description="Redditor name",
# opt_type=OptionType.STRING,
# required=True,
# )
# @slash_option(
# name="channel",
# description="Channel to post to",
# opt_type=OptionType.CHANNEL,
# channel_types=[ChannelType.GUILD_TEXT],
# required=True,
# )
# @check(admin_or_permissions(Permissions.MANAGE_GUILD))
# async def _redditor_follow(self, ctx: InteractionContext, name: str, channel: GuildText) -> None:
# if not user_name.match(name):
# await ctx.send("Invalid Redditor name", ephemeral=True)
# return
# if not isinstance(channel, GuildText):
# await ctx.send("Channel must be a text channel", ephemeral=True)
# return
# try:
# redditor = await self.api.redditor(name)
# await redditor.load()
# except (NotFound, Forbidden, Redirect) as e:
# self.logger.debug(f"Redditor {name} raised {e.__class__.__name__} on add")
# await ctx.send("Redditor may be deleted or nonexistent.", ephemeral=True)
# return
# exists = await RedditorFollow.find_one(q(name=redditor.name, guild=ctx.guild.id))
# if exists:
# await ctx.send("Redditor already being followed in this guild", ephemeral=True)
# return
# count = len([i async for i in SubredditFollow.find(q(guild=ctx.guild.id))])
# if count >= 12:
# await ctx.send("Cannot follow more than 12 Redditors", ephemeral=True)
# return
# sr = await Redditor.find_one(q(name=redditor.name))
# if not sr:
# sr = Redditor(name=redditor.name)
# await sr.commit()
# srf = RedditorFollow(
# name=redditor.name,
# channel=channel.id,
# guild=ctx.guild.id,
# admin=ctx.author.id,
# )
# await srf.commit()
# await ctx.send(f"Now following `u/{name}` in {channel.mention}")
# @unfollow.subcommand(sub_cmd_name="redditor", sub_cmd_description="Unfollow Redditor")
# @check(admin_or_permissions(Permissions.MANAGE_GUILD))
# async def _redditor_unfollow(self, ctx: InteractionContext) -> None:
# subs = RedditorFollow.find(q(guild=ctx.guild.id))
# redditors = []
# async for sub in subs:
# redditors.append(sub)
# if not redditors:
# await ctx.send("You need to follow a redditor first", ephemeral=True)
# return
# options = []
# names = []
# for idx, redditor in enumerate(redditors):
# sub = await Redditor.find_one(q(name=redditor.name))
# names.append(sub.name)
# option = StringSelectOption(label=sub.name, value=str(idx))
# options.append(option)
# select = StringSelectMenu(options=options, custom_id="to_delete", min_values=1, max_values=len(redditors))
# components = [ActionRow(select)]
# block = "\n".join(x for x in names)
# message = await ctx.send(
# content=f"You are following the following redditors:\n```\n{block}\n```\n\n"
# "Please choose redditors to unfollow",
# components=components,
# )
# try:
# context = await self.bot.wait_for_component(
# check=lambda x: ctx.author.id == x.ctx.author.id,
# messages=message,
# timeout=60 * 5,
# )
# for to_delete in context.ctx.values:
# follow = get(redditors, guild=ctx.guild.id, name=names[int(to_delete)])
# try:
# await follow.delete()
# except Exception:
# self.logger.debug("Ignoring deletion error")
# for row in components:
# for component in row.components:
# component.disabled = True
# block = "\n".join(names[int(x)] for x in context.ctx.values)
# await context.ctx.edit_origin(
# content=f"Unfollowed the following:\n```\n{block}\n```", components=components
# )
# except asyncio.TimeoutError:
# for row in components:
# for component in row.components:
# component.disabled = True
# await message.edit(components=components)
@follow.subcommand(sub_cmd_name="subreddit", sub_cmd_description="Follow a Subreddit")
@slash_option(
name="name",
description="Subreddit display name",
opt_type=OptionType.STRING,
required=True,
)
@slash_option(
name="channel",
description="Channel to post to",
opt_type=OptionType.CHANNEL,
channel_types=[ChannelType.GUILD_TEXT],
required=True,
)
@check(admin_or_permissions(Permissions.MANAGE_GUILD))
async def _subreddit_follow(self, ctx: InteractionContext, name: str, channel: GuildText) -> None:
if not sub_name.match(name):
await ctx.send("Invalid Subreddit name", ephemeral=True)
return
if not isinstance(channel, GuildText):
await ctx.send("Channel must be a text channel", ephemeral=True)
return
try:
subreddit = await self.api.subreddit(name)
await subreddit.load()
except (NotFound, Forbidden, Redirect) as e:
self.logger.debug(f"Subreddit {name} raised {e.__class__.__name__} on add")
await ctx.send("Subreddit may be private, quarantined, or nonexistent.", ephemeral=True)
return
exists = await SubredditFollow.find_one(
SubredditFollow.display_name == subreddit.display_name, SubredditFollow.guild == ctx.guild.id
)
if exists:
await ctx.send("Subreddit already being followed in this guild", ephemeral=True)
return
count = await SubredditFollow.find(SubredditFollow.guild == ctx.guild.id).count()
if count >= 12:
await ctx.send("Cannot follow more than 12 Subreddits", ephemeral=True)
return
if subreddit.over18 and not channel.nsfw:
await ctx.send(
"Subreddit is nsfw, but channel is not. Mark the channel NSFW first.",
ephemeral=True,
)
return
sr = await Subreddit.find_one(Subreddit.display_name == subreddit.display_name)
if not sr:
sr = Subreddit(display_name=subreddit.display_name, over18=subreddit.over18)
await sr.save()
srf = SubredditFollow(
display_name=subreddit.display_name,
channel=channel.id,
guild=ctx.guild.id,
admin=ctx.author.id,
)
await srf.save()
await ctx.send(f"Now following `r/{name}` in {channel.mention}")
@unfollow.subcommand(sub_cmd_name="subreddit", sub_cmd_description="Unfollow Subreddits")
@check(admin_or_permissions(Permissions.MANAGE_GUILD))
async def _subreddit_unfollow(self, ctx: InteractionContext) -> None:
subreddits = await SubredditFollow.find(SubredditFollow.guild == ctx.guild.id).to_list()
if not subreddits:
await ctx.send("You need to follow a Subreddit first", ephemeral=True)
return
options = []
names = []
for idx, subreddit in enumerate(subreddits):
sub = await Subreddit.find_one(Subreddit.display_name == subreddit.display_name)
names.append(sub.display_name)
option = StringSelectOption(label=sub.display_name, value=str(idx))
options.append(option)
select = StringSelectMenu(options=options, custom_id="to_delete", min_values=1, max_values=len(subreddits))
components = [ActionRow(select)]
block = "\n".join(x for x in names)
message = await ctx.send(
content=f"You are following the following subreddits:\n```\n{block}\n```\n\n"
"Please choose subreddits to unfollow",
components=components,
)
try:
context = await self.bot.wait_for_component(
check=lambda x: ctx.author.id == x.ctx.author.id,
messages=message,
timeout=60 * 5,
)
for to_delete in context.ctx.values:
follow = get(subreddits, guild=ctx.guild.id, display_name=names[int(to_delete)])
try:
await follow.delete()
except Exception:
self.logger.debug("Ignoring deletion error")
for row in components:
for component in row.components:
component.disabled = True
block = "\n".join(names[int(x)] for x in context.ctx.values)
await context.ctx.edit_origin(
content=f"Unfollowed the following:\n```\n{block}\n```", components=components
)
except asyncio.TimeoutError:
for row in components:
for component in row.components:
component.disabled = True
await message.edit(components=components)
@reddit.subcommand(sub_cmd_name="hot", sub_cmd_description="Get the hot post of a subreddit")
@slash_option(name="name", description="Subreddit name", opt_type=OptionType.STRING, required=True)
async def _subreddit_hot(self, ctx: InteractionContext, name: str) -> None:
if not sub_name.match(name):
await ctx.send("Invalid Subreddit name", ephemeral=True)
return
try:
await ctx.defer()
subreddit = await self.api.subreddit(name)
await subreddit.load()
except (NotFound, Forbidden, Redirect) as e:
self.logger.debug(f"Subreddit {name} raised {e.__class__.__name__} in hot")
await ctx.send("Subreddit may be private, quarantined, or nonexistent.", ephemeral=True)
return
try:
post = [x async for x in subreddit.hot(limit=1)][0]
except Exception as e:
self.logger.error(f"Failed to get post from {name}", exc_info=e)
await ctx.send("Well, this is awkward. Something went wrong", ephemeral=True)
return
embeds = await self.post_embeds(subreddit, post)
if post.over_18 and not ctx.channel.nsfw:
setting = await UserSetting.find_one(
UserSetting.user == ctx.author.id, UserSetting.type == "reddit", UserSetting.setting == "dm_nsfw"
)
if setting and setting.value:
try:
await ctx.author.send(embeds=embeds)
except Exception:
pass
await ctx.send("Hey! Due to content, I cannot share the result")
else:
await ctx.send(embeds=embeds)
@reddit.subcommand(sub_cmd_name="top", sub_cmd_description="Get the top post of a subreddit")
@slash_option(name="name", description="Subreddit name", opt_type=OptionType.STRING, required=True)
@slash_option(
name="time",
description="Top time",
opt_type=OptionType.STRING,
required=False,
choices=[
SlashCommandChoice(name="All", value="all"),
SlashCommandChoice(name="Day", value="day"),
SlashCommandChoice(name="Hour", value="hour"),
SlashCommandChoice(name="Month", value="month"),
SlashCommandChoice(name="Week", value="week"),
SlashCommandChoice(name="Year", value="year"),
],
)
async def _subreddit_top(self, ctx: InteractionContext, name: str, time: str = "all") -> None:
if not sub_name.match(name):
await ctx.send("Invalid Subreddit name", ephemeral=True)
return
try:
await ctx.defer()
subreddit = await self.api.subreddit(name)
await subreddit.load()
except (NotFound, Forbidden, Redirect) as e:
self.logger.debug(f"Subreddit {name} raised {e.__class__.__name__} in top")
await ctx.send("Subreddit may be private, quarantined, or nonexistent.", ephemeral=True)
return
try:
post = [x async for x in subreddit.top(time_filter=time, limit=1)][0]
except Exception as e:
self.logger.error(f"Failed to get post from {name}", exc_info=e)
await ctx.send("Well, this is awkward. Something went wrong", ephemeral=True)
return
embeds = await self.post_embeds(subreddit, post)
if post.over_18 and not ctx.channel.nsfw:
setting = await UserSetting.find_one(
UserSetting.user == ctx.author.id, UserSetting.type == "reddit", UserSetting.setting == "dm_nsfw"
)
if setting and setting.value:
try:
await ctx.author.send(embeds=embeds)
except Exception:
pass
await ctx.send("Hey! Due to content, I cannot share the result")
else:
await ctx.send(embeds=embeds)
@reddit.subcommand(sub_cmd_name="random", sub_cmd_description="Get a random post of a subreddit")
@slash_option(name="name", description="Subreddit name", opt_type=OptionType.STRING, required=True)
async def _subreddit_random(self, ctx: InteractionContext, name: str) -> None:
if not sub_name.match(name):
await ctx.send("Invalid Subreddit name", ephemeral=True)
return
try:
await ctx.defer()
subreddit = await self.api.subreddit(name)
await subreddit.load()
except (NotFound, Forbidden, Redirect) as e:
self.logger.debug(f"Subreddit {name} raised {e.__class__.__name__} in random")
await ctx.send("Subreddit may be private, quarantined, or nonexistent.", ephemeral=True)
return
try:
post = await subreddit.random()
except Exception as e:
self.logger.error(f"Failed to get post from {name}", exc_info=e)
await ctx.send("Well, this is awkward. Something went wrong", ephemeral=True)
return
embeds = await self.post_embeds(subreddit, post)
if post.over_18 and not ctx.channel.nsfw:
setting = await UserSetting.find_one(
UserSetting.user == ctx.author.id, UserSetting.type == "reddit", UserSetting.setting == "dm_nsfw"
)
if setting and setting.value:
try:
await ctx.author.send(embeds=embeds)
except Exception:
pass
await ctx.send("Hey! Due to content, I cannot share the result")
else:
await ctx.send(embeds=embeds)
@reddit.subcommand(sub_cmd_name="rising", sub_cmd_description="Get a rising post of a subreddit")
@slash_option(name="name", description="Subreddit name", opt_type=OptionType.STRING, required=True)
async def _subreddit_rising(self, ctx: InteractionContext, name: str) -> None:
if not sub_name.match(name):
await ctx.send("Invalid Subreddit name", ephemeral=True)
return
try:
await ctx.defer()
subreddit = await self.api.subreddit(name)
await subreddit.load()
except (NotFound, Forbidden, Redirect) as e:
self.logger.debug(f"Subreddit {name} raised {e.__class__.__name__} in rising")
await ctx.send("Subreddit may be private, quarantined, or nonexistent.", ephemeral=True)
return
try:
post = [x async for x in subreddit.rising(limit=1)][0]
except Exception as e:
self.logger.error(f"Failed to get post from {name}", exc_info=e)
await ctx.send("Well, this is awkward. Something went wrong", ephemeral=True)
return
embeds = await self.post_embeds(subreddit, post)
if post.over_18 and not ctx.channel.nsfw:
setting = await UserSetting.find_one(
UserSetting.user == ctx.author.id, UserSetting.type == "reddit", UserSetting.setting == "dm_nsfw"
)
if setting and setting.value:
try:
await ctx.author.send(embeds=embeds)
except Exception:
pass
await ctx.send("Hey! Due to content, I cannot share the result")
else:
await ctx.send(embeds=embeds)
@reddit.subcommand(sub_cmd_name="post", sub_cmd_description="Get a specific submission")
@slash_option(name="sid", description="Submission ID", opt_type=OptionType.STRING, required=True)
async def _reddit_post(self, ctx: InteractionContext, sid: str) -> None:
await ctx.defer()
try:
post = await self.api.submission(sid)
await post.load()
except (NotFound, Forbidden, Redirect) as e:
self.logger.debug(f"Submission {sid} raised {e.__class__.__name__} in post")
await ctx.send("Post could not be found.", ephemeral=True)
return
embeds = await self.post_embeds(post.subreddit, post)
if post.over_18 and not ctx.channel.nsfw:
setting = await UserSetting.find_one(
UserSetting.user == ctx.author.id, UserSetting.type == "reddit", UserSetting.setting == "dm_nsfw"
)
if setting and setting.value:
try:
await ctx.author.send(embeds=embeds)
except Exception:
pass
await ctx.send("Hey! Due to content, I cannot share the result")
else:
await ctx.send(embeds=embeds)
@reddit.subcommand(sub_cmd_name="dm_nsfw", sub_cmd_description="DM NSFW posts if channel isn't NSFW")
@slash_option(name="dm", description="Send DM?", opt_type=OptionType.BOOLEAN, required=True)
async def _reddit_dm(self, ctx: InteractionContext, dm: bool) -> None:
setting = await UserSetting.find_one(
UserSetting.user == ctx.author.id, UserSetting.type == "reddit", UserSetting.setting == "dm_nsfw"
)
if not setting:
setting = UserSetting(user=ctx.author.id, type="reddit", setting="dm_nsfw", value=dm)
setting.value = dm
await setting.save()
await ctx.send(f"Reddit DM NSFW setting is now set to {dm}", ephemeral=True)
def setup(bot: Client) -> None:
"""Add RedditCog to JARVIS"""
if load_config().reddit:
RedditCog(bot)
else:
bot.logger.info("Missing Reddit configuration, not loading")

View file

@ -1,246 +0,0 @@
"""JARVIS Twitter Cog."""
import asyncio
import logging
import tweepy
from interactions import Client, Extension, InteractionContext, Permissions
from interactions.client.utils.misc_utils import get
from interactions.models.discord.channel import GuildText
from interactions.models.discord.components import (
ActionRow,
StringSelectMenu,
StringSelectOption,
)
from interactions.models.internal.application_commands import (
OptionType,
SlashCommand,
slash_option,
)
from interactions.models.internal.command import check
from jarvis_core.db.models import TwitterAccount, TwitterFollow
from jarvis.config import load_config
from jarvis.utils.permissions import admin_or_permissions
class TwitterCog(Extension):
"""JARVIS Twitter Cog."""
def __init__(self, bot: Client):
self.bot = bot
self.logger = logging.getLogger(__name__)
config = load_config()
auth = tweepy.AppAuthHandler(config.twitter.consumer_key, config.twitter.consumer_secret)
self.api = tweepy.API(auth)
self._guild_cache = {}
self._channel_cache = {}
twitter = SlashCommand(
name="twitter",
description="Manage Twitter follows",
)
@twitter.subcommand(
sub_cmd_name="follow",
sub_cmd_description="Follow a Twitter acount",
)
@slash_option(name="handle", description="Twitter account", opt_type=OptionType.STRING, required=True)
@slash_option(
name="channel",
description="Channel to post tweets to",
opt_type=OptionType.CHANNEL,
required=True,
)
@slash_option(
name="retweets",
description="Mirror re-tweets?",
opt_type=OptionType.BOOLEAN,
required=False,
)
@check(admin_or_permissions(Permissions.MANAGE_GUILD))
async def _twitter_follow(
self, ctx: InteractionContext, handle: str, channel: GuildText, retweets: bool = True
) -> None:
handle = handle.lower()
if len(handle) > 15 or len(handle) < 4:
await ctx.send("Invalid Twitter handle", ephemeral=True)
return
if not isinstance(channel, GuildText):
await ctx.send("Channel must be a text channel", ephemeral=True)
return
try:
account = await asyncio.to_thread(self.api.get_user, screen_name=handle)
latest_tweet = (await asyncio.to_thread(self.api.user_timeline, screen_name=handle))[0]
except Exception:
await ctx.send("Unable to get user timeline. Are you sure the handle is correct?", ephemeral=True)
return
exists = await TwitterFollow.find_one(
TwitterFollow.twitter_id == account.id, TwitterFollow.guild == ctx.guild.id
)
if exists:
await ctx.send("Twitter account already being followed in this guild", ephemeral=True)
return
count = await TwitterFollow.find(TwitterFollow.guild == ctx.guild.id).count()
if count >= 12:
await ctx.send("Cannot follow more than 12 Twitter accounts", ephemeral=True)
return
ta = await TwitterAccount.find_one(TwitterAccount.twitter_id == account.id)
if not ta:
ta = TwitterAccount(
handle=account.screen_name,
twitter_id=account.id,
last_tweet=latest_tweet.id,
)
await ta.save()
tf = TwitterFollow(
twitter_id=account.id,
guild=ctx.guild.id,
channel=channel.id,
admin=ctx.author.id,
retweets=retweets,
)
await tf.save()
await ctx.send(f"Now following `@{handle}` in {channel.mention}")
@twitter.subcommand(sub_cmd_name="unfollow", sub_cmd_description="Unfollow Twitter accounts")
@check(admin_or_permissions(Permissions.MANAGE_GUILD))
async def _twitter_unfollow(self, ctx: InteractionContext) -> None:
t = TwitterFollow.find(TwitterFollow.guild == ctx.guild.id)
twitters = []
async for twitter in t:
twitters.append(twitter)
if not twitters:
await ctx.send("You need to follow a Twitter account first", ephemeral=True)
return
options = []
handlemap = {}
for twitter in twitters:
account = await TwitterAccount.find_one(TwitterAccount.twitter_id == twitter.twitter_id)
handlemap[str(twitter.twitter_id)] = account.handle
option = StringSelectOption(label=account.handle, value=str(twitter.twitter_id))
options.append(option)
select = StringSelectMenu(options=options, custom_id="to_delete", min_values=1, max_values=len(twitters))
components = [ActionRow(select)]
block = "\n".join(x for x in handlemap.values())
message = await ctx.send(
content=f"You are following the following accounts:\n```\n{block}\n```\n\n"
"Please choose accounts to unfollow",
components=components,
)
try:
context = await self.bot.wait_for_component(
check=lambda x: ctx.author.id == x.ctx.author.id,
messages=message,
timeout=60 * 5,
)
for to_delete in context.ctx.values:
follow = get(twitters, guild=ctx.guild.id, twitter_id=int(to_delete))
try:
await follow.delete()
except Exception:
self.logger.debug("Ignoring deletion error")
for row in components:
for component in row.components:
component.disabled = True
block = "\n".join(handlemap[x] for x in context.ctx.values)
await context.ctx.edit_origin(
content=f"Unfollowed the following:\n```\n{block}\n```", components=components
)
except asyncio.TimeoutError:
for row in components:
for component in row.components:
component.disabled = True
await message.edit(components=components)
@twitter.subcommand(
sub_cmd_name="retweets",
sub_cmd_description="Modify followed Twitter accounts",
)
@slash_option(
name="retweets",
description="Mirror re-tweets?",
opt_type=OptionType.BOOLEAN,
required=False,
)
@check(admin_or_permissions(Permissions.MANAGE_GUILD))
async def _twitter_modify(self, ctx: InteractionContext, retweets: bool = True) -> None:
t = TwitterFollow.find(TwitterFollow.guild == ctx.guild.id)
twitters = []
async for twitter in t:
twitters.append(twitter)
if not twitters:
await ctx.send("You need to follow a Twitter account first", ephemeral=True)
return
options = []
handlemap = {}
for twitter in twitters:
account = await TwitterAccount.find_one(TwitterAccount.twitter_id == twitter.id)
handlemap[str(twitter.twitter_id)] = account.handle
option = StringSelectOption(label=account.handle, value=str(twitter.twitter_id))
options.append(option)
select = StringSelectMenu(options=options, custom_id="to_update", min_values=1, max_values=len(twitters))
components = [ActionRow(select)]
block = "\n".join(x for x in handlemap.values())
message = await ctx.send(
content=f"You are following the following accounts:\n```\n{block}\n```\n\n"
f"Please choose which accounts to {'un' if not retweets else ''}follow retweets from",
components=components,
)
try:
context = await self.bot.wait_for_component(
check=lambda x: ctx.author.id == x.ctx.author.id,
messages=message,
timeout=60 * 5,
)
handlemap = {}
for to_update in context.ctx.values:
account = await TwitterAccount.find_one(TwitterAccount.twitter_id == int(to_update))
handlemap[str(twitter.twitter_id)] = account.handle
t = get(twitters, guild=ctx.guild.id, twitter_id=int(to_update))
t.retweets = True
await t.save()
for row in components:
for component in row.components:
component.disabled = True
block = "\n".join(handlemap[x] for x in context.ctx.values)
await context.ctx.edit_origin(
content=(
f"{'Unfollowed' if not retweets else 'Followed'} "
"retweets from the following:"
f"\n```\n{block}\n```"
),
components=components,
)
except asyncio.TimeoutError:
for row in components:
for component in row.components:
component.disabled = True
await message.edit(components=components)
def setup(bot: Client) -> None:
"""Add TwitterCog to JARVIS"""
if load_config().twitter:
TwitterCog(bot)
else:
bot.logger.info("No Twitter configuration, not loading")

View file

@ -92,16 +92,35 @@ class UtilCog(Extension):
) )
) )
fields.append( fields.append(
EmbedField(name="interactions", value=f"[{ipyv}](https://interactionspy.readthedocs.io)", inline=True) EmbedField(
name="interactions",
value=f"[{ipyv}](https://interactionspy.readthedocs.io)",
inline=True,
)
) )
self.bot.logger.debug("Getting repo information") self.bot.logger.debug("Getting repo information")
repo_url = f"https://git.zevaryx.com/stark-industries/jarvis/jarvis-bot/-/tree/{get_repo_hash()}" 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(
fields.append(EmbedField(name="Online Since", value=f"<t:{uptime}:F>", inline=False)) 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) num_domains = len(self.bot.phishing_domains)
fields.append(EmbedField(name="Phishing Protection", value=f"Detecting {num_domains} phishing domains")) fields.append(
EmbedField(
name="Phishing Protection",
value=f"Detecting {num_domains} phishing domains",
)
)
embed = build_embed(title=title, description=desc, fields=fields, color=color) embed = build_embed(title=title, description=desc, fields=fields, color=color)
components = Button(style=ButtonStyle.DANGER, emoji="🗑️", custom_id=f"delete|{ctx.author.id}") components = Button(
style=ButtonStyle.DANGER, emoji="🗑️", custom_id=f"delete|{ctx.author.id}"
)
await ctx.send(embeds=embed, components=components) await ctx.send(embeds=embed, components=components)
@bot.subcommand( @bot.subcommand(
@ -114,7 +133,11 @@ class UtilCog(Extension):
JARVIS_LOGO.save(image_bytes, "PNG") JARVIS_LOGO.save(image_bytes, "PNG")
image_bytes.seek(0) image_bytes.seek(0)
logo = File(image_bytes, file_name="logo.png") logo = File(image_bytes, file_name="logo.png")
components = Button(style=ButtonStyle.DANGER, emoji="🗑️", custom_id=f"delete|{ctx.author.id}") components = Button(
style=ButtonStyle.DANGER,
emoji="🗑️",
custom_id=f"delete|{ctx.author.id}",
)
await ctx.send(file=logo, components=components) await ctx.send(file=logo, components=components)
rc = SlashCommand(name="rc", description="Robot Camo emoji commands") rc = SlashCommand(name="rc", description="Robot Camo emoji commands")
@ -159,8 +182,10 @@ class UtilCog(Extension):
embed = build_embed(title="Avatar", description="", fields=[], color="#00FFEE") embed = build_embed(title="Avatar", description="", fields=[], color="#00FFEE")
embed.set_image(url=avatar) embed.set_image(url=avatar)
embed.set_author(name=f"{user.username}#{user.discriminator}", icon_url=avatar) embed.set_author(name=f"{user.username}", icon_url=avatar)
components = Button(style=ButtonStyle.DANGER, emoji="🗑️", custom_id=f"delete|{ctx.author.id}") components = Button(
style=ButtonStyle.DANGER, emoji="🗑️", custom_id=f"delete|{ctx.author.id}"
)
await ctx.send(embeds=embed, components=components) await ctx.send(embeds=embed, components=components)
@slash_command( @slash_command(
@ -179,11 +204,19 @@ class UtilCog(Extension):
EmbedField(name="Name", value=role.mention, inline=True), EmbedField(name="Name", value=role.mention, inline=True),
EmbedField(name="Color", value=str(role.color.hex), inline=True), EmbedField(name="Color", value=str(role.color.hex), inline=True),
EmbedField(name="Mention", value=f"`{role.mention}`", inline=True), EmbedField(name="Mention", value=f"`{role.mention}`", inline=True),
EmbedField(name="Hoisted", value="Yes" if role.hoist else "No", inline=True), EmbedField(
name="Hoisted", value="Yes" if role.hoist else "No", inline=True
),
EmbedField(name="Position", value=str(role.position), inline=True), EmbedField(name="Position", value=str(role.position), inline=True),
EmbedField(name="Mentionable", value="Yes" if role.mentionable else "No", inline=True), EmbedField(
name="Mentionable",
value="Yes" if role.mentionable else "No",
inline=True,
),
EmbedField(name="Member Count", value=str(len(role.members)), inline=True), EmbedField(name="Member Count", value=str(len(role.members)), inline=True),
EmbedField(name="Created At", value=f"<t:{int(role.created_at.timestamp())}:F>"), EmbedField(
name="Created At", value=f"<t:{int(role.created_at.timestamp())}:F>"
),
] ]
embed = build_embed( embed = build_embed(
title="", title="",
@ -208,7 +241,11 @@ class UtilCog(Extension):
im.save(image_bytes, "PNG") im.save(image_bytes, "PNG")
image_bytes.seek(0) image_bytes.seek(0)
color_show = File(image_bytes, file_name="color_show.png") color_show = File(image_bytes, file_name="color_show.png")
components = Button(style=ButtonStyle.DANGER, emoji="🗑️", custom_id=f"delete|{ctx.author.id}") components = Button(
style=ButtonStyle.DANGER,
emoji="🗑️",
custom_id=f"delete|{ctx.author.id}",
)
await ctx.send(embeds=embed, file=color_show, components=components) await ctx.send(embeds=embed, file=color_show, components=components)
@slash_command(name="avatar", description="Get a user avatar") @slash_command(name="avatar", description="Get a user avatar")
@ -252,14 +289,18 @@ class UtilCog(Extension):
), ),
EmbedField( EmbedField(
name=f"Roles [{len(user_roles)}]", name=f"Roles [{len(user_roles)}]",
value=" ".join([x.mention for x in user_roles]) if user_roles else "None", value=" ".join([x.mention for x in user_roles])
if user_roles
else "None",
inline=False, inline=False,
), ),
] ]
if muted: if muted:
ts = int(user.communication_disabled_until.timestamp()) ts = int(user.communication_disabled_until.timestamp())
fields.append(EmbedField(name="Muted Until", value=f"<t:{ts}:F> (<t:{ts}:R>)")) fields.append(
EmbedField(name="Muted Until", value=f"<t:{ts}:F> (<t:{ts}:R>)")
)
embed = build_embed( embed = build_embed(
title="", title="",
@ -269,16 +310,23 @@ class UtilCog(Extension):
) )
embed.set_author( embed.set_author(
name=f"{'🔇 ' if muted else ''}{user.display_name}#{user.discriminator}", name=f"{'🔇 ' if muted else ''}{user.display_name}",
icon_url=user.display_avatar.url, icon_url=user.display_avatar.url,
) )
embed.set_thumbnail(url=user.display_avatar.url) embed.set_thumbnail(url=user.display_avatar.url)
embed.set_footer(text=f"ID: {user.id}") embed.set_footer(text=f"ID: {user.id}")
components = Button(style=ButtonStyle.DANGER, emoji="🗑️", custom_id=f"delete|{ctx.author.id}") components = Button(
style=ButtonStyle.DANGER, emoji="🗑️", custom_id=f"delete|{ctx.author.id}"
)
await ctx.send(embeds=embed, components=components) await ctx.send(embeds=embed, components=components)
@slash_command(name="lmgtfy", description="Let me Google that for you") @slash_command(name="lmgtfy", description="Let me Google that for you")
@slash_option(name="search", description="What to search", opt_type=OptionType.STRING, required=True) @slash_option(
name="search",
description="What to search",
opt_type=OptionType.STRING,
required=True,
)
async def _lmgtfy(self, ctx: SlashContext, search: str) -> None: async def _lmgtfy(self, ctx: SlashContext, search: str) -> None:
url = "https://letmegooglethat.com/?q=" + urllib.parse.quote_plus(search) url = "https://letmegooglethat.com/?q=" + urllib.parse.quote_plus(search)
await ctx.send(url) await ctx.send(url)
@ -306,7 +354,7 @@ class UtilCog(Extension):
owner = await guild.fetch_owner() owner = await guild.fetch_owner()
owner = f"{owner.username}#{owner.discriminator}" if owner else "||`[redacted]`||" owner = f"{owner.username}" if owner else "||`[redacted]`||"
categories = len([x for x in guild.channels if isinstance(x, GuildCategory)]) categories = len([x for x in guild.channels if isinstance(x, GuildCategory)])
text_channels = len([x for x in guild.channels if isinstance(x, GuildText)]) text_channels = len([x for x in guild.channels if isinstance(x, GuildText)])
@ -325,24 +373,29 @@ class UtilCog(Extension):
EmbedField(name="Threads", value=str(threads), inline=True), EmbedField(name="Threads", value=str(threads), inline=True),
EmbedField(name="Members", value=str(members), inline=True), EmbedField(name="Members", value=str(members), inline=True),
EmbedField(name="Roles", value=str(roles), inline=True), EmbedField(name="Roles", value=str(roles), inline=True),
EmbedField(name="Created At", value=f"<t:{int(guild.created_at.timestamp())}:F>"), EmbedField(
name="Created At", value=f"<t:{int(guild.created_at.timestamp())}:F>"
),
] ]
if len(role_list) < 1024: if len(role_list) < 1024:
fields.append(EmbedField(name="Role List", value=role_list, inline=False)) fields.append(EmbedField(name="Role List", value=role_list, inline=False))
embed = build_embed(title="", description="", fields=fields, timestamp=guild.created_at) embed = build_embed(
title="", description="", fields=fields, timestamp=guild.created_at
)
embed.set_author(name=guild.name, icon_url=guild.icon.url) embed.set_author(name=guild.name, icon_url=guild.icon.url)
embed.set_thumbnail(url=guild.icon.url) embed.set_thumbnail(url=guild.icon.url)
embed.set_footer(text=f"ID: {guild.id} | Server Created") embed.set_footer(text=f"ID: {guild.id} | Server Created")
components = Button(style=ButtonStyle.DANGER, emoji="🗑️", custom_id=f"delete|{ctx.author.id}") components = Button(
style=ButtonStyle.DANGER, emoji="🗑️", custom_id=f"delete|{ctx.author.id}"
)
await ctx.send(embeds=embed, components=components) await ctx.send(embeds=embed, components=components)
@slash_command( @slash_command(
name="pw", name="pw",
sub_cmd_name="gen", sub_cmd_name="gen",
description="Generate a secure password", description="Generate a secure password",
scopes=[862402786116763668],
) )
@slash_option( @slash_option(
name="length", name="length",
@ -363,7 +416,9 @@ class UtilCog(Extension):
], ],
) )
@cooldown(bucket=Buckets.USER, rate=1, interval=15) @cooldown(bucket=Buckets.USER, rate=1, interval=15)
async def _pw_gen(self, ctx: SlashContext, length: int = 32, chars: int = 3) -> None: async def _pw_gen(
self, ctx: SlashContext, length: int = 32, chars: int = 3
) -> None:
if length > 256: if length > 256:
await ctx.send("Please limit password to 256 characters", ephemeral=True) await ctx.send("Please limit password to 256 characters", ephemeral=True)
return return
@ -384,7 +439,12 @@ class UtilCog(Extension):
) )
@slash_command(name="pigpen", description="Encode a string into pigpen") @slash_command(name="pigpen", description="Encode a string into pigpen")
@slash_option(name="text", description="Text to encode", opt_type=OptionType.STRING, required=True) @slash_option(
name="text",
description="Text to encode",
opt_type=OptionType.STRING,
required=True,
)
async def _pigpen(self, ctx: SlashContext, text: str) -> None: async def _pigpen(self, ctx: SlashContext, text: str) -> None:
outp = "`" outp = "`"
for c in text: for c in text:
@ -399,17 +459,34 @@ class UtilCog(Extension):
outp += "`" outp += "`"
await ctx.send(outp[:2000]) await ctx.send(outp[:2000])
@slash_command(name="timestamp", description="Convert a datetime or timestamp into it's counterpart") @slash_command(
@slash_option(name="string", description="String to convert", opt_type=OptionType.STRING, required=True) name="timestamp",
@slash_option(name="private", description="Respond quietly?", opt_type=OptionType.BOOLEAN, required=False) description="Convert a datetime or timestamp into it's counterpart",
async def _timestamp(self, ctx: SlashContext, string: str, private: bool = False) -> None: )
@slash_option(
name="string",
description="String to convert",
opt_type=OptionType.STRING,
required=True,
)
@slash_option(
name="private",
description="Respond quietly?",
opt_type=OptionType.BOOLEAN,
required=False,
)
async def _timestamp(
self, ctx: SlashContext, string: str, private: bool = False
) -> None:
timestamp = parse(string) timestamp = parse(string)
if not timestamp: if not timestamp:
await ctx.send("Valid time not found, try again", ephemeral=True) await ctx.send("Valid time not found, try again", ephemeral=True)
return return
if not timestamp.tzinfo: if not timestamp.tzinfo:
timestamp = timestamp.replace(tzinfo=get_localzone()).astimezone(tz=timezone.utc) timestamp = timestamp.replace(tzinfo=get_localzone()).astimezone(
tz=timezone.utc
)
timestamp_utc = timestamp.astimezone(tz=timezone.utc) timestamp_utc = timestamp.astimezone(tz=timezone.utc)
@ -422,8 +499,12 @@ class UtilCog(Extension):
EmbedField(name="Relative Time", value=f"<t:{ts_utc}:R>\n`<t:{ts_utc}:R>`"), EmbedField(name="Relative Time", value=f"<t:{ts_utc}:R>\n`<t:{ts_utc}:R>`"),
EmbedField(name="ISO8601", value=timestamp.isoformat()), EmbedField(name="ISO8601", value=timestamp.isoformat()),
] ]
embed = build_embed(title="Converted Time", description=f"`{string}`", fields=fields) embed = build_embed(
components = Button(style=ButtonStyle.DANGER, emoji="🗑️", custom_id=f"delete|{ctx.author.id}") title="Converted Time", description=f"`{string}`", fields=fields
)
components = Button(
style=ButtonStyle.DANGER, emoji="🗑️", custom_id=f"delete|{ctx.author.id}"
)
await ctx.send(embeds=embed, ephemeral=private, components=components) await ctx.send(embeds=embed, ephemeral=private, components=components)
@bot.subcommand(sub_cmd_name="support", sub_cmd_description="Got issues?") @bot.subcommand(sub_cmd_name="support", sub_cmd_description="Got issues?")
@ -438,7 +519,10 @@ We'll help as best we can with whatever issues you encounter.
""" """
) )
@bot.subcommand(sub_cmd_name="privacy_terms", sub_cmd_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: SlashContext) -> None: async def _privacy_terms(self, ctx: SlashContext) -> None:
await ctx.send( await ctx.send(
""" """

View file

@ -1,8 +1,6 @@
"""JARVIS Calculator Cog.""" """JARVIS Calculator Cog."""
import json
from aiohttp import ClientSession
from calculator import calculate from calculator import calculate
from erapi import const
from interactions import AutocompleteContext, Client, Extension, InteractionContext from interactions import AutocompleteContext, Client, Extension, InteractionContext
from interactions.models.discord.components import Button from interactions.models.discord.components import Button
from interactions.models.discord.embed import Embed, EmbedField from interactions.models.discord.embed import Embed, EmbedField
@ -24,39 +22,8 @@ TEMP_CHOICES = (
SlashCommandChoice(name="Kelvin", value=2), SlashCommandChoice(name="Kelvin", value=2),
) )
TEMP_LOOKUP = {0: "F", 1: "C", 2: "K"} TEMP_LOOKUP = {0: "F", 1: "C", 2: "K"}
CURRENCIES = ( CURRENCY_BY_NAME = {x["name"]: x["code"] for x in const.VALID_CODES}
"AUD", CURRENCY_BY_CODE = {x["code"]: x["name"] for x in const.VALID_CODES}
"BGN",
"BRL",
"CAD",
"CHF",
"CNY",
"CZK",
"DKK",
"EUR",
"GBP",
"HKD",
"HRK",
"HUF",
"IDR",
"INR",
"ISK",
"JPY",
"KRW",
"MXN",
"MYR",
"NOK",
"NZD",
"PHP",
"PLN",
"RON",
"SEK",
"SGD",
"THB",
"TRY",
"USD",
"ZAR",
)
class CalcCog(Extension): class CalcCog(Extension):
@ -67,22 +34,32 @@ class CalcCog(Extension):
async def _get_currency_conversion(self, from_: str, to: str) -> int: async def _get_currency_conversion(self, from_: str, to: str) -> int:
"""Get the conversion rate.""" """Get the conversion rate."""
async with ClientSession() as session: return self.bot.erapi.get_conversion_rate(from_, to)
async with session.get("https://theforexapi.com/api/latest", params={"base": from_, "symbols": to}) as resp:
raw = await resp.content.read()
data = json.loads(raw.decode("UTF8"))
return data["rates"][to]
calc = SlashCommand(name="calc", description="Calculate some things") calc = SlashCommand(name="calc", description="Calculate some things")
@calc.subcommand(sub_cmd_name="math", sub_cmd_description="Do a basic math calculation") @calc.subcommand(
@slash_option(name="expression", description="Expression to calculate", required=True, opt_type=OptionType.STRING) sub_cmd_name="math", sub_cmd_description="Do a basic math calculation"
)
@slash_option(
name="expression",
description="Expression to calculate",
required=True,
opt_type=OptionType.STRING,
)
async def _calc_math(self, ctx: InteractionContext, expression: str) -> None: async def _calc_math(self, ctx: InteractionContext, expression: str) -> None:
if expression == "The answer to life, the universe, and everything": if expression == "The answer to life, the universe, and everything":
fields = (EmbedField(name="Expression", value=f"`{expression}`"), EmbedField(name="Result", value=str(42))) fields = (
EmbedField(name="Expression", value=f"`{expression}`"),
EmbedField(name="Result", value=str(42)),
)
embed = build_embed(title="Calculator", description=None, fields=fields) embed = build_embed(title="Calculator", description=None, fields=fields)
components = Button(style=ButtonStyle.DANGER, emoji="🗑️", custom_id=f"delete|{ctx.author.id}") components = Button(
style=ButtonStyle.DANGER,
emoji="🗑️",
custom_id=f"delete|{ctx.author.id}",
)
await ctx.send(embeds=embed, components=components) await ctx.send(embeds=embed, components=components)
return return
try: try:
@ -94,21 +71,41 @@ class CalcCog(Extension):
await ctx.send("No value? Try a valid expression", ephemeral=True) await ctx.send("No value? Try a valid expression", ephemeral=True)
return return
fields = (EmbedField(name="Expression", value=f"`{expression}`"), EmbedField(name="Result", value=str(value))) fields = (
EmbedField(name="Expression", value=f"`{expression}`"),
EmbedField(name="Result", value=str(value)),
)
embed = build_embed(title="Calculator", description=None, fields=fields) embed = build_embed(title="Calculator", description=None, fields=fields)
components = Button(style=ButtonStyle.DANGER, emoji="🗑️", custom_id=f"delete|{ctx.author.id}") components = Button(
style=ButtonStyle.DANGER, emoji="🗑️", custom_id=f"delete|{ctx.author.id}"
)
await ctx.send(embeds=embed, components=components) await ctx.send(embeds=embed, components=components)
convert = calc.group(name="convert", description="Conversion helpers") convert = calc.group(name="convert", description="Conversion helpers")
@convert.subcommand(sub_cmd_name="temperature", sub_cmd_description="Convert between temperatures") @convert.subcommand(
@slash_option(name="value", description="Value to convert", required=True, opt_type=OptionType.NUMBER) sub_cmd_name="temperature", sub_cmd_description="Convert between temperatures"
@slash_option(
name="from_unit", description="From unit", required=True, opt_type=OptionType.INTEGER, choices=TEMP_CHOICES
) )
@slash_option( @slash_option(
name="to_unit", description="To unit", required=True, opt_type=OptionType.INTEGER, choices=TEMP_CHOICES name="value",
description="Value to convert",
required=True,
opt_type=OptionType.NUMBER,
)
@slash_option(
name="from_unit",
description="From unit",
required=True,
opt_type=OptionType.INTEGER,
choices=TEMP_CHOICES,
)
@slash_option(
name="to_unit",
description="To unit",
required=True,
opt_type=OptionType.INTEGER,
choices=TEMP_CHOICES,
) )
async def _calc_convert_temperature( async def _calc_convert_temperature(
self, ctx: InteractionContext, value: int, from_unit: int, to_unit: int self, ctx: InteractionContext, value: int, from_unit: int, to_unit: int
@ -139,11 +136,21 @@ class CalcCog(Extension):
description=f"°{TEMP_LOOKUP.get(from_unit)} -> °{TEMP_LOOKUP.get(to_unit)}", description=f"°{TEMP_LOOKUP.get(from_unit)} -> °{TEMP_LOOKUP.get(to_unit)}",
fields=fields, fields=fields,
) )
components = Button(style=ButtonStyle.DANGER, emoji="🗑️", custom_id=f"delete|{ctx.author.id}") components = Button(
style=ButtonStyle.DANGER, emoji="🗑️", custom_id=f"delete|{ctx.author.id}"
)
await ctx.send(embeds=embed, components=components) await ctx.send(embeds=embed, components=components)
@convert.subcommand(sub_cmd_name="currency", sub_cmd_description="Convert currency based on current rates") @convert.subcommand(
@slash_option(name="value", description="Value of starting currency", required=True, opt_type=OptionType.NUMBER) sub_cmd_name="currency",
sub_cmd_description="Convert currency based on current rates",
)
@slash_option(
name="value",
description="Value of starting currency",
required=True,
opt_type=OptionType.NUMBER,
)
@slash_option( @slash_option(
name="from_currency", name="from_currency",
description="Currency to convert from", description="Currency to convert from",
@ -168,27 +175,58 @@ class CalcCog(Extension):
conv = value * rate conv = value * rate
fields = ( fields = (
EmbedField(name="Conversion Rate", value=f"1 {from_currency} ~= {rate:0.4f} {to_currency}"), EmbedField(
EmbedField(name=from_currency, value=f"{value:0.2f}"), name="Conversion Rate",
EmbedField(name=to_currency, value=f"{conv:0.2f}"), value=f"1 {from_currency} ~= {rate:0.4f} {to_currency}",
),
EmbedField(
name=f"{CURRENCY_BY_CODE[from_currency]} ({from_currency})",
value=f"{value:0.2f}",
),
EmbedField(
name=f"{CURRENCY_BY_CODE[to_currency]} ({to_currency})",
value=f"{conv:0.2f}",
),
) )
embed = build_embed(title="Conversion", description=f"{from_currency} -> {to_currency}", fields=fields) embed = build_embed(
components = Button(style=ButtonStyle.DANGER, emoji="🗑️", custom_id=f"delete|{ctx.author.id}") title="Conversion",
description=f"{from_currency} -> {to_currency}",
fields=fields,
)
components = Button(
style=ButtonStyle.DANGER,
emoji="🗑️",
custom_id=f"delete|{ctx.author.id}",
)
await ctx.send(embeds=embed, components=components) await ctx.send(embeds=embed, components=components)
async def _convert(self, ctx: InteractionContext, from_: str, to: str, value: int) -> Embed: async def _convert(
self, ctx: InteractionContext, from_: str, to: str, value: int
) -> Embed:
*_, which = ctx.invoke_target.split(" ") *_, which = ctx.invoke_target.split(" ")
which = getattr(units, which.capitalize(), None) which = getattr(units, which.capitalize(), None)
ratio = which.get_rate(from_, to) ratio = which.get_rate(from_, to)
converted = value / ratio converted = value / ratio
fields = (EmbedField(name=from_, value=f"{value:0.2f}"), EmbedField(name=to, value=f"{converted:0.2f}")) fields = (
embed = build_embed(title="Conversion", description=f"{from_} -> {to}", fields=fields) EmbedField(name=from_, value=f"{value:0.2f}"),
components = Button(style=ButtonStyle.DANGER, emoji="🗑️", custom_id=f"delete|{ctx.author.id}") EmbedField(name=to, value=f"{converted:0.2f}"),
)
embed = build_embed(
title="Conversion", description=f"{from_} -> {to}", fields=fields
)
components = Button(
style=ButtonStyle.DANGER, emoji="🗑️", custom_id=f"delete|{ctx.author.id}"
)
await ctx.send(embeds=embed, components=components) await ctx.send(embeds=embed, components=components)
@convert.subcommand(sub_cmd_name="angle", sub_cmd_description="Convert angles") @convert.subcommand(sub_cmd_name="angle", sub_cmd_description="Convert angles")
@slash_option(name="value", description="Angle to convert", opt_type=OptionType.NUMBER, required=True) @slash_option(
name="value",
description="Angle to convert",
opt_type=OptionType.NUMBER,
required=True,
)
@slash_option( @slash_option(
name="from_unit", name="from_unit",
description="Units to convert from", description="Units to convert from",
@ -197,13 +235,24 @@ class CalcCog(Extension):
autocomplete=True, autocomplete=True,
) )
@slash_option( @slash_option(
name="to_unit", description="Units to convert to", opt_type=OptionType.STRING, required=True, autocomplete=True name="to_unit",
description="Units to convert to",
opt_type=OptionType.STRING,
required=True,
autocomplete=True,
) )
async def _calc_convert_angle(self, ctx: InteractionContext, value: int, from_unit: str, to_unit: str) -> None: async def _calc_convert_angle(
self, ctx: InteractionContext, value: int, from_unit: str, to_unit: str
) -> None:
await self._convert(ctx, from_unit, to_unit, value) await self._convert(ctx, from_unit, to_unit, value)
@convert.subcommand(sub_cmd_name="area", sub_cmd_description="Convert areas") @convert.subcommand(sub_cmd_name="area", sub_cmd_description="Convert areas")
@slash_option(name="value", description="Area to convert", opt_type=OptionType.NUMBER, required=True) @slash_option(
name="value",
description="Area to convert",
opt_type=OptionType.NUMBER,
required=True,
)
@slash_option( @slash_option(
name="from_unit", name="from_unit",
description="Units to convert from", description="Units to convert from",
@ -212,13 +261,24 @@ class CalcCog(Extension):
autocomplete=True, autocomplete=True,
) )
@slash_option( @slash_option(
name="to_unit", description="Units to convert to", opt_type=OptionType.STRING, required=True, autocomplete=True name="to_unit",
description="Units to convert to",
opt_type=OptionType.STRING,
required=True,
autocomplete=True,
) )
async def _calc_convert_area(self, ctx: InteractionContext, value: int, from_unit: str, to_unit: str) -> None: async def _calc_convert_area(
self, ctx: InteractionContext, value: int, from_unit: str, to_unit: str
) -> None:
await self._convert(ctx, from_unit, to_unit, value) await self._convert(ctx, from_unit, to_unit, value)
@convert.subcommand(sub_cmd_name="data", sub_cmd_description="Convert data sizes") @convert.subcommand(sub_cmd_name="data", sub_cmd_description="Convert data sizes")
@slash_option(name="value", description="Data size to convert", opt_type=OptionType.NUMBER, required=True) @slash_option(
name="value",
description="Data size to convert",
opt_type=OptionType.NUMBER,
required=True,
)
@slash_option( @slash_option(
name="from_unit", name="from_unit",
description="Units to convert from", description="Units to convert from",
@ -227,13 +287,24 @@ class CalcCog(Extension):
autocomplete=True, autocomplete=True,
) )
@slash_option( @slash_option(
name="to_unit", description="Units to convert to", opt_type=OptionType.STRING, required=True, autocomplete=True name="to_unit",
description="Units to convert to",
opt_type=OptionType.STRING,
required=True,
autocomplete=True,
) )
async def _calc_convert_data(self, ctx: InteractionContext, value: int, from_unit: str, to_unit: str) -> None: async def _calc_convert_data(
self, ctx: InteractionContext, value: int, from_unit: str, to_unit: str
) -> None:
await self._convert(ctx, from_unit, to_unit, value) await self._convert(ctx, from_unit, to_unit, value)
@convert.subcommand(sub_cmd_name="energy", sub_cmd_description="Convert energy") @convert.subcommand(sub_cmd_name="energy", sub_cmd_description="Convert energy")
@slash_option(name="value", description="Energy to convert", opt_type=OptionType.NUMBER, required=True) @slash_option(
name="value",
description="Energy to convert",
opt_type=OptionType.NUMBER,
required=True,
)
@slash_option( @slash_option(
name="from_unit", name="from_unit",
description="Units to convert from", description="Units to convert from",
@ -242,13 +313,24 @@ class CalcCog(Extension):
autocomplete=True, autocomplete=True,
) )
@slash_option( @slash_option(
name="to_unit", description="Units to convert to", opt_type=OptionType.STRING, required=True, autocomplete=True name="to_unit",
description="Units to convert to",
opt_type=OptionType.STRING,
required=True,
autocomplete=True,
) )
async def _calc_convert_energy(self, ctx: InteractionContext, value: int, from_unit: str, to_unit: str) -> None: async def _calc_convert_energy(
self, ctx: InteractionContext, value: int, from_unit: str, to_unit: str
) -> None:
await self._convert(ctx, from_unit, to_unit, value) await self._convert(ctx, from_unit, to_unit, value)
@convert.subcommand(sub_cmd_name="length", sub_cmd_description="Convert lengths") @convert.subcommand(sub_cmd_name="length", sub_cmd_description="Convert lengths")
@slash_option(name="value", description="Length to convert", opt_type=OptionType.NUMBER, required=True) @slash_option(
name="value",
description="Length to convert",
opt_type=OptionType.NUMBER,
required=True,
)
@slash_option( @slash_option(
name="from_unit", name="from_unit",
description="Units to convert from", description="Units to convert from",
@ -257,13 +339,24 @@ class CalcCog(Extension):
autocomplete=True, autocomplete=True,
) )
@slash_option( @slash_option(
name="to_unit", description="Units to convert to", opt_type=OptionType.STRING, required=True, autocomplete=True name="to_unit",
description="Units to convert to",
opt_type=OptionType.STRING,
required=True,
autocomplete=True,
) )
async def _calc_convert_length(self, ctx: InteractionContext, value: int, from_unit: str, to_unit: str) -> None: async def _calc_convert_length(
self, ctx: InteractionContext, value: int, from_unit: str, to_unit: str
) -> None:
await self._convert(ctx, from_unit, to_unit, value) await self._convert(ctx, from_unit, to_unit, value)
@convert.subcommand(sub_cmd_name="power", sub_cmd_description="Convert powers") @convert.subcommand(sub_cmd_name="power", sub_cmd_description="Convert powers")
@slash_option(name="value", description="Power to convert", opt_type=OptionType.NUMBER, required=True) @slash_option(
name="value",
description="Power to convert",
opt_type=OptionType.NUMBER,
required=True,
)
@slash_option( @slash_option(
name="from_unit", name="from_unit",
description="Units to convert from", description="Units to convert from",
@ -272,13 +365,26 @@ class CalcCog(Extension):
autocomplete=True, autocomplete=True,
) )
@slash_option( @slash_option(
name="to_unit", description="Units to convert to", opt_type=OptionType.STRING, required=True, autocomplete=True name="to_unit",
description="Units to convert to",
opt_type=OptionType.STRING,
required=True,
autocomplete=True,
) )
async def _calc_convert_power(self, ctx: InteractionContext, value: int, from_unit: str, to_unit: str) -> None: async def _calc_convert_power(
self, ctx: InteractionContext, value: int, from_unit: str, to_unit: str
) -> None:
await self._convert(ctx, from_unit, to_unit, value) await self._convert(ctx, from_unit, to_unit, value)
@convert.subcommand(sub_cmd_name="pressure", sub_cmd_description="Convert pressures") @convert.subcommand(
@slash_option(name="value", description="Pressure to convert", opt_type=OptionType.NUMBER, required=True) sub_cmd_name="pressure", sub_cmd_description="Convert pressures"
)
@slash_option(
name="value",
description="Pressure to convert",
opt_type=OptionType.NUMBER,
required=True,
)
@slash_option( @slash_option(
name="from_unit", name="from_unit",
description="Units to convert from", description="Units to convert from",
@ -287,13 +393,24 @@ class CalcCog(Extension):
autocomplete=True, autocomplete=True,
) )
@slash_option( @slash_option(
name="to_unit", description="Units to convert to", opt_type=OptionType.STRING, required=True, autocomplete=True name="to_unit",
description="Units to convert to",
opt_type=OptionType.STRING,
required=True,
autocomplete=True,
) )
async def _calc_convert_pressure(self, ctx: InteractionContext, value: int, from_unit: str, to_unit: str) -> None: async def _calc_convert_pressure(
self, ctx: InteractionContext, value: int, from_unit: str, to_unit: str
) -> None:
await self._convert(ctx, from_unit, to_unit, value) await self._convert(ctx, from_unit, to_unit, value)
@convert.subcommand(sub_cmd_name="speed", sub_cmd_description="Convert speeds") @convert.subcommand(sub_cmd_name="speed", sub_cmd_description="Convert speeds")
@slash_option(name="value", description="Speed to convert", opt_type=OptionType.NUMBER, required=True) @slash_option(
name="value",
description="Speed to convert",
opt_type=OptionType.NUMBER,
required=True,
)
@slash_option( @slash_option(
name="from_unit", name="from_unit",
description="Units to convert from", description="Units to convert from",
@ -302,13 +419,24 @@ class CalcCog(Extension):
autocomplete=True, autocomplete=True,
) )
@slash_option( @slash_option(
name="to_unit", description="Units to convert to", opt_type=OptionType.STRING, required=True, autocomplete=True name="to_unit",
description="Units to convert to",
opt_type=OptionType.STRING,
required=True,
autocomplete=True,
) )
async def _calc_convert_speed(self, ctx: InteractionContext, value: int, from_unit: str, to_unit: str) -> None: async def _calc_convert_speed(
self, ctx: InteractionContext, value: int, from_unit: str, to_unit: str
) -> None:
await self._convert(ctx, from_unit, to_unit, value) await self._convert(ctx, from_unit, to_unit, value)
@convert.subcommand(sub_cmd_name="time", sub_cmd_description="Convert times") @convert.subcommand(sub_cmd_name="time", sub_cmd_description="Convert times")
@slash_option(name="value", description="Time to convert", opt_type=OptionType.NUMBER, required=True) @slash_option(
name="value",
description="Time to convert",
opt_type=OptionType.NUMBER,
required=True,
)
@slash_option( @slash_option(
name="from_unit", name="from_unit",
description="Units to convert from", description="Units to convert from",
@ -317,13 +445,24 @@ class CalcCog(Extension):
autocomplete=True, autocomplete=True,
) )
@slash_option( @slash_option(
name="to_unit", description="Units to convert to", opt_type=OptionType.STRING, required=True, autocomplete=True name="to_unit",
description="Units to convert to",
opt_type=OptionType.STRING,
required=True,
autocomplete=True,
) )
async def _calc_convert_time(self, ctx: InteractionContext, value: int, from_unit: str, to_unit: str) -> None: async def _calc_convert_time(
self, ctx: InteractionContext, value: int, from_unit: str, to_unit: str
) -> None:
await self._convert(ctx, from_unit, to_unit, value) await self._convert(ctx, from_unit, to_unit, value)
@convert.subcommand(sub_cmd_name="volume", sub_cmd_description="Convert volumes") @convert.subcommand(sub_cmd_name="volume", sub_cmd_description="Convert volumes")
@slash_option(name="value", description="Volume to convert", opt_type=OptionType.NUMBER, required=True) @slash_option(
name="value",
description="Volume to convert",
opt_type=OptionType.NUMBER,
required=True,
)
@slash_option( @slash_option(
name="from_unit", name="from_unit",
description="Units to convert from", description="Units to convert from",
@ -332,13 +471,24 @@ class CalcCog(Extension):
autocomplete=True, autocomplete=True,
) )
@slash_option( @slash_option(
name="to_unit", description="Units to convert to", opt_type=OptionType.STRING, required=True, autocomplete=True name="to_unit",
description="Units to convert to",
opt_type=OptionType.STRING,
required=True,
autocomplete=True,
) )
async def _calc_convert_volume(self, ctx: InteractionContext, value: int, from_unit: str, to_unit: str) -> None: async def _calc_convert_volume(
self, ctx: InteractionContext, value: int, from_unit: str, to_unit: str
) -> None:
await self._convert(ctx, from_unit, to_unit, value) await self._convert(ctx, from_unit, to_unit, value)
@convert.subcommand(sub_cmd_name="weight", sub_cmd_description="Convert weights") @convert.subcommand(sub_cmd_name="weight", sub_cmd_description="Convert weights")
@slash_option(name="value", description="Weight to convert", opt_type=OptionType.NUMBER, required=True) @slash_option(
name="value",
description="Weight to convert",
opt_type=OptionType.NUMBER,
required=True,
)
@slash_option( @slash_option(
name="from_unit", name="from_unit",
description="Units to convert from", description="Units to convert from",
@ -347,12 +497,20 @@ class CalcCog(Extension):
autocomplete=True, autocomplete=True,
) )
@slash_option( @slash_option(
name="to_unit", description="Units to convert to", opt_type=OptionType.STRING, required=True, autocomplete=True name="to_unit",
description="Units to convert to",
opt_type=OptionType.STRING,
required=True,
autocomplete=True,
) )
async def _calc_convert_weight(self, ctx: InteractionContext, value: int, from_unit: str, to_unit: str) -> None: async def _calc_convert_weight(
self, ctx: InteractionContext, value: int, from_unit: str, to_unit: str
) -> None:
await self._convert(ctx, from_unit, to_unit, value) await self._convert(ctx, from_unit, to_unit, value)
def _unit_autocomplete(self, which: units.Converter, unit: str) -> list[dict[str, str]]: def _unit_autocomplete(
self, which: units.Converter, unit: str
) -> list[dict[str, str]]:
options = list(which.CONVERSIONS.keys()) options = list(which.CONVERSIONS.keys())
results = process.extract(unit, options, limit=25) results = process.extract(unit, options, limit=25)
if any([r[1] > 0 for r in results]): if any([r[1] > 0 for r in results]):
@ -392,10 +550,24 @@ class CalcCog(Extension):
await ctx.send(choices=self._unit_autocomplete(which, ctx.input_text)) await ctx.send(choices=self._unit_autocomplete(which, ctx.input_text))
def _currency_autocomplete(self, currency: str) -> list[dict[str, str]]: def _currency_autocomplete(self, currency: str) -> list[dict[str, str]]:
results = process.extract(currency, CURRENCIES, limit=25) lookup_name = {f"{k} ({v})": v for k, v in CURRENCY_BY_NAME.items()}
lookup_value = {v: k for k, v in lookup_name.items()}
results_name = process.extract(currency, list(lookup_name.keys()), limit=25)
results_value = process.extract(currency, list(lookup_value.keys()), limit=25)
results = {}
for r in results_value + results_name:
name = r[0]
if len(name) == 3:
name = lookup_value[name]
if name not in results:
results[name] = r[1]
if r[1] > results[name]:
results[name] = r[1]
results = sorted(list(results.items()), key=lambda x: -x[1])[:10]
if any([r[1] > 0 for r in results]): if any([r[1] > 0 for r in results]):
return [{"name": r[0], "value": r[0]} for r in results if r[1] > 50] return [{"name": r[0], "value": lookup_name[r[0]]} for r in results if r[1]]
return [{"name": r[0], "value": r[0]} for r in results] return [{"name": r[0], "value": lookup_name[r[0]]} for r in results]
@_calc_convert_currency.autocomplete("from_currency") @_calc_convert_currency.autocomplete("from_currency")
async def _autocomplete_from_currency(self, ctx: AutocompleteContext) -> None: async def _autocomplete_from_currency(self, ctx: AutocompleteContext) -> None:

View file

@ -5,14 +5,17 @@ import logging
import re import re
import subprocess # noqa: S404 import subprocess # noqa: S404
import uuid as uuidpy import uuid as uuidpy
from datetime import datetime
from io import BytesIO from io import BytesIO
import nanoid import nanoid
import pytz
import ulid as ulidpy import ulid as ulidpy
from aiofile import AIOFile from aiofile import AIOFile
from ansitoimg.render import ansiToRender from ansitoimg.render import ansiToRender
from bson import ObjectId from bson import ObjectId
from interactions import Client, Extension, InteractionContext from croniter import croniter
from interactions import Client, Extension, InteractionContext, AutocompleteContext
from interactions.models.discord.components import Button from interactions.models.discord.components import Button
from interactions.models.discord.embed import EmbedField from interactions.models.discord.embed import EmbedField
from interactions.models.discord.enums import ButtonStyle from interactions.models.discord.enums import ButtonStyle
@ -30,13 +33,16 @@ from jarvis_core.filters import invites, url
from jarvis_core.util import convert_bytesize, hash from jarvis_core.util import convert_bytesize, hash
from jarvis_core.util.http import get_size from jarvis_core.util.http import get_size
from rich.console import Console from rich.console import Console
from thefuzz import process
from jarvis.utils import build_embed from jarvis.utils import build_embed
supported_hashes = {x for x in hashlib.algorithms_guaranteed if "shake" not in x} supported_hashes = {x for x in hashlib.algorithms_guaranteed if "shake" not in x}
OID_VERIFY = re.compile(r"^([1-9][0-9]{0,3}|0)(\.([1-9][0-9]{0,3}|0)){5,13}$") OID_VERIFY = re.compile(r"^([1-9][0-9]{0,3}|0)(\.([1-9][0-9]{0,3}|0)){5,13}$")
URL_VERIFY = re.compile(r"http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+") URL_VERIFY = re.compile(
r"http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+"
)
DN_VERIFY = re.compile( DN_VERIFY = re.compile(
r"^(?:(?P<cn>CN=(?P<name>[^,]*)),)?(?:(?P<path>(?:(?:CN|OU)=[^,]+,?)+),)?(?P<domain>(?:DC=[^,]+,?)+)$" # noqa: E501 r"^(?:(?P<cn>CN=(?P<name>[^,]*)),)?(?:(?P<path>(?:(?:CN|OU)=[^,]+,?)+),)?(?P<domain>(?:DC=[^,]+,?)+)$" # noqa: E501
) )
@ -60,6 +66,42 @@ class DevCog(Extension):
dev = SlashCommand(name="dev", description="Developer utilities") dev = SlashCommand(name="dev", description="Developer utilities")
@dev.subcommand(sub_cmd_name="cron", sub_cmd_description="Test cron strings")
@slash_option(
name="cron",
description="Cron pattern",
opt_type=OptionType.STRING,
required=True,
)
@slash_option(
name="timezone",
description="Timezone",
opt_type=OptionType.STRING,
required=False,
autocomplete=True,
)
async def _dev_cron(self, ctx: InteractionContext, cron: str, timezone="utc"):
try:
if not croniter.is_valid(cron):
await ctx.defer(ephemeral=True)
await ctx.send(f"Invalid cron pattern: `{cron}`", ephemeral=True)
return
base = datetime.now(tz=pytz.timezone(timezone))
parsed = croniter(cron, base)
next_5 = [parsed.get_next() for _ in range(5)]
next_5_str = "\n".join(f"<t:{int(x)}:F> (<t:{int(x)}:R>)" for x in next_5)
embed = build_embed(
title="Cron",
description=f"Pattern: `{cron}`\n\nNext 5 runs:\n{next_5_str}\n\nTimezone: `{timezone}`",
fields=[],
)
await ctx.send(embeds=[embed])
except Exception:
self.logger.error("Encountered error", exc_info=True)
@dev.subcommand(sub_cmd_name="hash", sub_cmd_description="Hash some data") @dev.subcommand(sub_cmd_name="hash", sub_cmd_description="Hash some data")
@slash_option( @slash_option(
name="method", name="method",
@ -74,9 +116,20 @@ class DevCog(Extension):
opt_type=OptionType.STRING, opt_type=OptionType.STRING,
required=False, required=False,
) )
@slash_option(name="attach", description="File to hash", opt_type=OptionType.ATTACHMENT, required=False) @slash_option(
name="attach",
description="File to hash",
opt_type=OptionType.ATTACHMENT,
required=False,
)
@cooldown(bucket=Buckets.USER, rate=1, interval=2) @cooldown(bucket=Buckets.USER, rate=1, interval=2)
async def _hash(self, ctx: InteractionContext, method: str, data: str = None, attach: Attachment = None) -> None: async def _hash(
self,
ctx: InteractionContext,
method: str,
data: str = None,
attach: Attachment = None,
) -> None:
if not data and not attach: if not data and not attach:
await ctx.send( await ctx.send(
"No data to hash", "No data to hash",
@ -93,8 +146,12 @@ class DevCog(Extension):
elif url.match(data): elif url.match(data):
try: try:
if (size := await get_size(data)) > MAX_FILESIZE: if (size := await get_size(data)) > MAX_FILESIZE:
await ctx.send("Please hash files that are <= 5GB in size", ephemeral=True) await ctx.send(
self.logger.debug(f"Refused to hash file of size {convert_bytesize(size)}") "Please hash files that are <= 5GB in size", ephemeral=True
)
self.logger.debug(
f"Refused to hash file of size {convert_bytesize(size)}"
)
return return
except Exception as e: except Exception as e:
await ctx.send(f"Failed to retrieve URL: ```\n{e}\n```", ephemeral=True) await ctx.send(f"Failed to retrieve URL: ```\n{e}\n```", ephemeral=True)
@ -117,7 +174,9 @@ class DevCog(Extension):
] ]
embed = build_embed(title=title, description=description, fields=fields) embed = build_embed(title=title, description=description, fields=fields)
components = Button(style=ButtonStyle.DANGER, emoji="🗑️", custom_id=f"delete|{ctx.author.id}") components = Button(
style=ButtonStyle.DANGER, emoji="🗑️", custom_id=f"delete|{ctx.author.id}"
)
await ctx.send(embeds=embed, components=components) await ctx.send(embeds=embed, components=components)
@dev.subcommand(sub_cmd_name="uuid", sub_cmd_description="Generate a UUID") @dev.subcommand(sub_cmd_name="uuid", sub_cmd_description="Generate a UUID")
@ -134,7 +193,9 @@ class DevCog(Extension):
opt_type=OptionType.STRING, opt_type=OptionType.STRING,
required=False, required=False,
) )
async def _uuid(self, ctx: InteractionContext, version: str, data: str = None) -> None: async def _uuid(
self, ctx: InteractionContext, version: str, data: str = None
) -> None:
version = int(version) version = int(version)
if version in [3, 5] and not data: if version in [3, 5] and not data:
await ctx.send(f"UUID{version} requires data.", ephemeral=True) await ctx.send(f"UUID{version} requires data.", ephemeral=True)
@ -173,7 +234,12 @@ class DevCog(Extension):
sub_cmd_name="uuid2ulid", sub_cmd_name="uuid2ulid",
sub_cmd_description="Convert a UUID to a ULID", sub_cmd_description="Convert a UUID to a ULID",
) )
@slash_option(name="uuid", description="UUID to convert", opt_type=OptionType.STRING, required=True) @slash_option(
name="uuid",
description="UUID to convert",
opt_type=OptionType.STRING,
required=True,
)
@cooldown(bucket=Buckets.USER, rate=1, interval=2) @cooldown(bucket=Buckets.USER, rate=1, interval=2)
async def _uuid2ulid(self, ctx: InteractionContext, uuid: str) -> None: async def _uuid2ulid(self, ctx: InteractionContext, uuid: str) -> None:
if UUID_VERIFY.match(uuid): if UUID_VERIFY.match(uuid):
@ -186,7 +252,12 @@ class DevCog(Extension):
sub_cmd_name="ulid2uuid", sub_cmd_name="ulid2uuid",
sub_cmd_description="Convert a ULID to a UUID", sub_cmd_description="Convert a ULID to a UUID",
) )
@slash_option(name="ulid", description="ULID to convert", opt_type=OptionType.STRING, required=True) @slash_option(
name="ulid",
description="ULID to convert",
opt_type=OptionType.STRING,
required=True,
)
@cooldown(bucket=Buckets.USER, rate=1, interval=2) @cooldown(bucket=Buckets.USER, rate=1, interval=2)
async def _ulid2uuid(self, ctx: InteractionContext, ulid: str) -> None: async def _ulid2uuid(self, ctx: InteractionContext, ulid: str) -> None:
if ULID_VERIFY.match(ulid): if ULID_VERIFY.match(ulid):
@ -230,7 +301,9 @@ class DevCog(Extension):
EmbedField(name=mstr, value=f"`{encoded}`", inline=False), EmbedField(name=mstr, value=f"`{encoded}`", inline=False),
] ]
embed = build_embed(title="Encoded Data", description="", fields=fields) embed = build_embed(title="Encoded Data", description="", fields=fields)
components = Button(style=ButtonStyle.DANGER, emoji="🗑️", custom_id=f"delete|{ctx.author.id}") components = Button(
style=ButtonStyle.DANGER, emoji="🗑️", custom_id=f"delete|{ctx.author.id}"
)
await ctx.send(embeds=embed, components=components) await ctx.send(embeds=embed, components=components)
@dev.subcommand(sub_cmd_name="decode", sub_cmd_description="Decode some data") @dev.subcommand(sub_cmd_name="decode", sub_cmd_description="Decode some data")
@ -266,14 +339,18 @@ class DevCog(Extension):
EmbedField(name=mstr, value=f"`{decoded}`", inline=False), EmbedField(name=mstr, value=f"`{decoded}`", inline=False),
] ]
embed = build_embed(title="Decoded Data", description="", fields=fields) embed = build_embed(title="Decoded Data", description="", fields=fields)
components = Button(style=ButtonStyle.DANGER, emoji="🗑️", custom_id=f"delete|{ctx.author.id}") components = Button(
style=ButtonStyle.DANGER, emoji="🗑️", custom_id=f"delete|{ctx.author.id}"
)
await ctx.send(embeds=embed, components=components) await ctx.send(embeds=embed, components=components)
@dev.subcommand(sub_cmd_name="cloc", sub_cmd_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) @cooldown(bucket=Buckets.CHANNEL, rate=1, interval=30)
async def _cloc(self, ctx: InteractionContext) -> None: async def _cloc(self, ctx: InteractionContext) -> None:
await ctx.defer() await ctx.defer()
output = subprocess.check_output(["tokei", "-C", "--sort", "code"]).decode("UTF-8") # noqa: S603, S607 output = subprocess.check_output(["tokei", "-C", "--sort", "code"]).decode(
"UTF-8"
) # noqa: S603, S607
console = Console() console = Console()
with console.capture() as capture: with console.capture() as capture:
console.print(output) console.print(output)
@ -290,6 +367,12 @@ class DevCog(Extension):
tokei = File(file_bytes, file_name="tokei.png") tokei = File(file_bytes, file_name="tokei.png")
await ctx.send(file=tokei) await ctx.send(file=tokei)
@_dev_cron.autocomplete("timezone")
async def _timezone_autocomplete(self, ctx: AutocompleteContext):
results = process.extract(ctx.input_text, pytz.all_timezones_set, limit=5)
choices = [{"name": r[0], "value": r[0]} for r in results if r[1] > 80.0]
await ctx.send(choices)
def setup(bot: Client) -> None: def setup(bot: Client) -> None:
"""Add DevCog to JARVIS""" """Add DevCog to JARVIS"""

View file

@ -44,7 +44,9 @@ class PinboardCog(Extension):
async def _purge_starboard(self, ctx: InteractionContext, board: Pinboard) -> None: async def _purge_starboard(self, ctx: InteractionContext, board: Pinboard) -> None:
channel = await ctx.guild.fetch_channel(board.channel) channel = await ctx.guild.fetch_channel(board.channel)
async for pin in Pin.find(Pin.pinboard == channel.id, Pin.guild == ctx.guild.id): async for pin in Pin.find(
Pin.pinboard == channel.id, Pin.guild == ctx.guild.id
):
if message := await channel.fetch_message(pin.message): if message := await channel.fetch_message(pin.message):
try: try:
await message.delete() await message.delete()
@ -89,9 +91,13 @@ class PinboardCog(Extension):
await ctx.send("Channel must be a GuildText", ephemeral=True) await ctx.send("Channel must be a GuildText", ephemeral=True)
return return
exists = await Pinboard.find_one(Pinboard.channel == channel.id, Pinboard.guild == ctx.guild.id) exists = await Pinboard.find_one(
Pinboard.channel == channel.id, Pinboard.guild == ctx.guild.id
)
if exists: if exists:
await ctx.send(f"Pinboard already exists at {channel.mention}.", ephemeral=True) await ctx.send(
f"Pinboard already exists at {channel.mention}.", ephemeral=True
)
return return
count = await Pinboard.find(Pinboard.guild == ctx.guild.id).count() count = await Pinboard.find(Pinboard.guild == ctx.guild.id).count()
@ -115,7 +121,9 @@ class PinboardCog(Extension):
) )
@check(admin_or_permissions(Permissions.MANAGE_GUILD, Permissions.MANAGE_MESSAGES)) @check(admin_or_permissions(Permissions.MANAGE_GUILD, Permissions.MANAGE_MESSAGES))
async def _delete(self, ctx: InteractionContext, channel: GuildText) -> None: async def _delete(self, ctx: InteractionContext, channel: GuildText) -> None:
found = await Pinboard.find_one(Pinboard.channel == channel.id, Pinboard.guild == ctx.guild.id) found = await Pinboard.find_one(
Pinboard.channel == channel.id, Pinboard.guild == ctx.guild.id
)
if found: if found:
await found.delete() await found.delete()
asyncio.create_task(self._purge_starboard(ctx, found)) asyncio.create_task(self._purge_starboard(ctx, found))
@ -129,128 +137,147 @@ class PinboardCog(Extension):
message: str, message: str,
channel: GuildText = None, channel: GuildText = None,
) -> None: ) -> None:
if not channel: try:
channel = ctx.channel if not channel:
pinboards = await Pinboard.find(Pinboard.guild == ctx.guild.id).to_list() channel = ctx.channel
if not pinboards: pinboards = await Pinboard.find(Pinboard.guild == ctx.guild.id).to_list()
await ctx.send("No pinboards exist.", ephemeral=True) if not pinboards:
return await ctx.send("No pinboards exist.", ephemeral=True)
await ctx.defer()
if not isinstance(message, Message):
if message.startswith("https://"):
message = message.split("/")[-1]
message = await channel.fetch_message(int(message))
if not message:
await ctx.send("Message not found", ephemeral=True)
return return
channel_list = [] await ctx.defer()
to_delete: list[Pinboard] = []
for pinboard in pinboards: if not isinstance(message, Message):
c = await ctx.guild.fetch_channel(pinboard.channel) if message.startswith("https://"):
if c and isinstance(c, GuildText): message = message.split("/")[-1]
channel_list.append(c) message = await channel.fetch_message(int(message))
else:
self.logger.warning(f"Pinboard {pinboard.channel} no longer valid in {ctx.guild.name}")
to_delete.append(pinboard)
for pinboard in to_delete: if not message:
try: await ctx.send("Message not found", ephemeral=True)
await pinboard.delete() return
except Exception:
self.logger.debug("Ignoring deletion error")
select_channels = [] channel_list = []
for idx, x in enumerate(channel_list): to_delete: list[Pinboard] = []
if x:
select_channels.append(StringSelectOption(label=x.name, value=str(idx)))
select_channels = [StringSelectOption(label=x.name, value=str(idx)) for idx, x in enumerate(channel_list)] channel_to_pinboard = {}
select = StringSelectMenu( for pinboard in pinboards:
options=select_channels, c = await ctx.guild.fetch_channel(pinboard.channel)
min_values=1, if c and isinstance(c, GuildText):
max_values=1, channel_list.append(c)
) channel_to_pinboard[c.id] = pinboard
else:
self.logger.warning(
f"Pinboard {pinboard.channel} no longer valid in {ctx.guild.name}"
)
to_delete.append(pinboard)
components = [ActionRow(select)] for pinboard in to_delete:
try:
await pinboard.delete()
except Exception:
self.logger.debug("Ignoring deletion error")
msg = await ctx.send(content="Choose a pinboard", components=components) select_channels = []
for idx, x in enumerate(channel_list):
if x:
select_channels.append(
StringSelectOption(label=x.name, value=str(idx))
)
com_ctx = await self.bot.wait_for_component( select_channels = [
messages=msg, StringSelectOption(label=x.name, value=str(idx))
components=components, for idx, x in enumerate(channel_list)
check=lambda x: ctx.author.id == x.context.author.id, ]
)
pinboard = channel_list[int(com_ctx.context.values[0])] select = StringSelectMenu(
*select_channels,
exists = await Pin.find_one( min_values=1,
Pin.message == message.id, max_values=1,
Pin.channel == channel.id,
Pin.guild == ctx.guild.id,
Pin.pinboard == pinboard.id,
)
if exists:
await ctx.send(
f"Message already sent to Pinboard {pinboard.mention}",
ephemeral=True,
) )
return
count = await Pin.find(Pin.guild == ctx.guild.id, Pin.pinboard == pinboard.id).count() components = [ActionRow(select)]
content = message.content
attachments = message.attachments msg = await ctx.send(content="Choose a pinboard", components=components)
image_url = None
if attachments:
for attachment in attachments:
if attachment.content_type in supported_images:
image_url = attachment.url
break
if not content and image_url:
content = "\u200b"
embed = build_embed( com_ctx = await self.bot.wait_for_component(
title=f"[#{count}] Click Here to view context", messages=msg,
description=content, components=components,
fields=[], check=lambda x: ctx.author.id == x.ctx.author.id,
url=message.jump_url, )
timestamp=message.created_at,
)
embed.set_author(
name=message.author.display_name,
url=message.jump_url,
icon_url=message.author.avatar.url,
)
embed.set_footer(text=ctx.guild.name + " | " + channel.name)
if image_url:
embed.set_image(url=image_url)
star_components = Button(style=ButtonStyle.DANGER, emoji="🗑️", custom_id=f"delete|{ctx.author.id}")
pin = await pinboard.send(embeds=embed, components=star_components)
await Pin( pinboard = channel_list[int(com_ctx.ctx.values[0])]
index=count,
message=message.id,
channel=channel.id,
guild=ctx.guild.id,
pinboard=pinboard.id,
admin=ctx.author.id,
pin=pin.id,
active=True,
).save()
components[0].components[0].disabled = True exists = await Pin.find_one(
Pin.message == int(message.id),
Pin.channel == int(channel.id),
Pin.guild == int(ctx.guild.id),
Pin.pinboard == int(pinboard.id),
)
await com_ctx.context.edit_origin( if exists:
content=f"Message saved to Pinboard.\nSee it in {pinboard.mention}", await ctx.send(
components=components, f"Message already sent to Pinboard {pinboard.mention}",
) ephemeral=True,
)
return
count = await Pin.find(
Pin.guild == ctx.guild.id, Pin.pinboard == pinboard.id
).count()
content = message.content
attachments = message.attachments
image_url = None
if attachments:
for attachment in attachments:
if attachment.content_type in supported_images:
image_url = attachment.url
break
if not content and image_url:
content = "\u200b"
embed = build_embed(
title=f"[#{count}] Click Here to view context",
description=content,
fields=[],
url=message.jump_url,
timestamp=message.created_at,
)
embed.set_author(
name=message.author.display_name,
url=message.jump_url,
icon_url=message.author.avatar.url,
)
embed.set_footer(text=ctx.guild.name + " | " + channel.name)
if image_url:
embed.set_image(url=image_url)
star_components = Button(
style=ButtonStyle.DANGER,
emoji="🗑️",
custom_id=f"delete|{ctx.author.id}",
)
pin = await pinboard.send(embeds=embed, components=star_components)
await Pin(
index=count,
message=int(message.id),
channel=int(channel.id),
guild=int(ctx.guild.id),
pinboard=channel_to_pinboard[pinboard.id],
admin=int(ctx.author.id),
pin=int(pin.id),
active=True,
).save()
components[0].components[0].disabled = True
await com_ctx.ctx.edit_origin(
content=f"Message saved to Pinboard.\nSee it in {pinboard.mention}",
components=components,
)
except:
self.bot.logger.error("E", exc_info=True)
@context_menu(name="Pin Message", context_type=CommandType.MESSAGE) @context_menu(name="Pin Message", context_type=CommandType.MESSAGE)
@check(admin_or_permissions(Permissions.MANAGE_GUILD, Permissions.MANAGE_MESSAGES)) @check(admin_or_permissions(Permissions.MANAGE_GUILD, Permissions.MANAGE_MESSAGES))

View file

@ -64,13 +64,17 @@ class RolegiverCog(Extension):
rolegiver.roles = roles rolegiver.roles = roles
await rolegiver.save() await rolegiver.save()
rolegiver = SlashCommand(name="rolegiver", description="Allow users to choose their own roles") rolegiver = SlashCommand(
name="rolegiver", description="Allow users to choose their own roles"
)
@rolegiver.subcommand( @rolegiver.subcommand(
sub_cmd_name="add", sub_cmd_name="add",
sub_cmd_description="Add a role to rolegiver", sub_cmd_description="Add a role to rolegiver",
) )
@slash_option(name="role", description="Role to add", opt_type=OptionType.ROLE, required=True) @slash_option(
name="role", description="Role to add", opt_type=OptionType.ROLE, required=True
)
@check(admin_or_permissions(Permissions.MANAGE_GUILD)) @check(admin_or_permissions(Permissions.MANAGE_GUILD))
async def _rolegiver_add(self, ctx: InteractionContext, role: Role) -> None: async def _rolegiver_add(self, ctx: InteractionContext, role: Role) -> None:
if role.id == ctx.guild.id: if role.id == ctx.guild.id:
@ -122,15 +126,19 @@ class RolegiverCog(Extension):
embed.set_thumbnail(url=ctx.guild.icon.url) embed.set_thumbnail(url=ctx.guild.icon.url)
embed.set_footer(text=f"{ctx.author.username}#{ctx.author.discriminator} | {ctx.author.id}") embed.set_footer(text=f"{ctx.author.username} | {ctx.author.id}")
components = Button(style=ButtonStyle.DANGER, emoji="🗑️", custom_id=f"delete|{ctx.author.id}") components = Button(
style=ButtonStyle.DANGER, emoji="🗑️", custom_id=f"delete|{ctx.author.id}"
)
await ctx.send(embeds=embed, components=components) await ctx.send(embeds=embed, components=components)
if ctx.guild.id not in self.cache: if ctx.guild.id not in self.cache:
self.cache[ctx.guild.id] = {} self.cache[ctx.guild.id] = {}
self.cache[ctx.guild.id][role.name] = role.id self.cache[ctx.guild.id][role.name] = role.id
@rolegiver.subcommand(sub_cmd_name="remove", sub_cmd_description="Remove a role from rolegiver") @rolegiver.subcommand(
sub_cmd_name="remove", sub_cmd_description="Remove a role from rolegiver"
)
@slash_option( @slash_option(
name="role", name="role",
description="Name of role to add", description="Name of role to add",
@ -149,7 +157,9 @@ class RolegiverCog(Extension):
if cache: if cache:
role_id = cache.get(role) role_id = cache.get(role)
else: else:
await ctx.send("Something went wrong, please try a different role", ephemeral=True) await ctx.send(
"Something went wrong, please try a different role", ephemeral=True
)
return return
setting.value.remove(role_id) setting.value.remove(role_id)
@ -171,14 +181,21 @@ class RolegiverCog(Extension):
fields = [ fields = [
EmbedField(name="Removed Role", value=role.mention), EmbedField(name="Removed Role", value=role.mention),
EmbedField(name="Remaining Role(s)", value="\n".join([x.mention for x in remaining])), EmbedField(
name="Remaining Role(s)",
value="\n".join([x.mention for x in remaining]),
),
] ]
embed = build_embed(title="Rolegiver Updated", description="Role removed from rolegiver", fields=fields) embed = build_embed(
title="Rolegiver Updated",
description="Role removed from rolegiver",
fields=fields,
)
embed.set_thumbnail(url=ctx.guild.icon.url) embed.set_thumbnail(url=ctx.guild.icon.url)
embed.set_footer(text=f"{ctx.author.username}#{ctx.author.discriminator} | {ctx.author.id}") embed.set_footer(text=f"{ctx.author.username} | {ctx.author.id}")
await ctx.send( await ctx.send(
embeds=embed, embeds=embed,
@ -187,7 +204,9 @@ class RolegiverCog(Extension):
if ctx.guild.id in self.cache: if ctx.guild.id in self.cache:
self.cache[ctx.guild.id].pop(role.name) self.cache[ctx.guild.id].pop(role.name)
@rolegiver.subcommand(sub_cmd_name="list", sub_cmd_description="List rolegiver roles") @rolegiver.subcommand(
sub_cmd_name="list", sub_cmd_description="List rolegiver roles"
)
async def _rolegiver_list(self, ctx: InteractionContext) -> None: async def _rolegiver_list(self, ctx: InteractionContext) -> None:
setting = await Rolegiver.find_one(Rolegiver.guild == ctx.guild.id) setting = await Rolegiver.find_one(Rolegiver.guild == ctx.guild.id)
if not setting or (setting and not setting.roles): if not setting or (setting and not setting.roles):
@ -214,34 +233,40 @@ class RolegiverCog(Extension):
embed.set_thumbnail(url=ctx.guild.icon.url) embed.set_thumbnail(url=ctx.guild.icon.url)
embed.set_footer(text=f"{ctx.author.username}#{ctx.author.discriminator} | {ctx.author.id}") embed.set_footer(text=f"{ctx.author.username} | {ctx.author.id}")
components = Button(style=ButtonStyle.DANGER, emoji="🗑️", custom_id=f"delete|{ctx.author.id}") components = Button(
style=ButtonStyle.DANGER, emoji="🗑️", custom_id=f"delete|{ctx.author.id}"
)
await ctx.send(embeds=embed, components=components) await ctx.send(embeds=embed, components=components)
@rolegiver.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) @cooldown(bucket=Buckets.USER, rate=1, interval=10)
async def _role_get(self, ctx: InteractionContext) -> None: async def _role_get(self, ctx: InteractionContext) -> None:
setting = await Rolegiver.find_one(Rolegiver.quild == ctx.guild.id) try:
if not setting or (setting and not setting.roles): setting = await Rolegiver.find_one(Rolegiver.guild == ctx.guild.id)
await ctx.send("Rolegiver has no roles", ephemeral=True) if not setting or (setting and not setting.roles):
await ctx.send("Rolegiver has no roles", ephemeral=True)
return
options = []
for role in setting.roles:
role: Role = await ctx.guild.fetch_role(role)
option = StringSelectOption(label=role.name, value=str(role.id))
options.append(option)
select = StringSelectMenu(
*options,
placeholder="Select roles to add",
min_values=1,
max_values=len(options),
)
components = [ActionRow(select)]
message = await ctx.send(content="\u200b", components=components)
except Exception as e:
self.logger.error("Encountered error", exc_info=True)
return return
options = []
for role in setting.roles:
role: Role = await ctx.guild.fetch_role(role)
option = StringSelectOption(label=role.name, value=str(role.id))
options.append(option)
select = StringSelectMenu(
options=options,
placeholder="Select roles to add",
min_values=1,
max_values=len(options),
)
components = [ActionRow(select)]
message = await ctx.send(content="\u200b", components=components)
try: try:
context = await self.bot.wait_for_component( context = await self.bot.wait_for_component(
check=lambda x: ctx.author.id == x.ctx.author.id, check=lambda x: ctx.author.id == x.ctx.author.id,
@ -255,16 +280,11 @@ class RolegiverCog(Extension):
added_roles.append(role) added_roles.append(role)
await ctx.author.add_role(role, reason="Rolegiver") await ctx.author.add_role(role, reason="Rolegiver")
roles = ctx.author.roles avalue = (
if roles: "\n".join([r.mention for r in added_roles]) if added_roles else "None"
roles.sort(key=lambda x: -x.position) )
_ = roles.pop(-1)
avalue = "\n".join([r.mention for r in added_roles]) if added_roles else "None"
value = "\n".join([r.mention for r in roles]) if roles else "None"
fields = [ fields = [
EmbedField(name="Added Role(s)", value=avalue), EmbedField(name="Added Role(s)", value=avalue),
EmbedField(name="Prior Role(s)", value=value),
] ]
embed = build_embed( embed = build_embed(
@ -279,13 +299,15 @@ class RolegiverCog(Extension):
icon_url=ctx.author.display_avatar.url, icon_url=ctx.author.display_avatar.url,
) )
embed.set_footer(text=f"{ctx.author.username}#{ctx.author.discriminator} | {ctx.author.id}") embed.set_footer(text=f"{ctx.author.username} | {ctx.author.id}")
for row in components: for row in components:
for component in row.components: for component in row.components:
component.disabled = True component.disabled = True
await context.ctx.edit_origin(embeds=embed, content="\u200b", components=components) await context.ctx.edit_origin(
embeds=embed, content="\u200b", components=components
)
except asyncio.TimeoutError: except asyncio.TimeoutError:
for row in components: for row in components:
for component in row.components: for component in row.components:
@ -312,7 +334,7 @@ class RolegiverCog(Extension):
options.append(option) options.append(option)
select = StringSelectMenu( select = StringSelectMenu(
options=options, *options,
custom_id="to_remove", custom_id="to_remove",
placeholder="Select roles to remove", placeholder="Select roles to remove",
min_values=1, min_values=1,
@ -336,14 +358,13 @@ class RolegiverCog(Extension):
user_roles.remove(role) user_roles.remove(role)
removed_roles.append(role) removed_roles.append(role)
user_roles.sort(key=lambda x: -x.position) rvalue = (
_ = user_roles.pop(-1) "\n".join([r.mention for r in removed_roles])
if removed_roles
value = "\n".join([r.mention for r in user_roles]) if user_roles else "None" else "None"
rvalue = "\n".join([r.mention for r in removed_roles]) if removed_roles else "None" )
fields = [ fields = [
EmbedField(name="Removed Role(s)", value=rvalue), EmbedField(name="Removed Role(s)", value=rvalue),
EmbedField(name="Remaining Role(s)", value=value),
] ]
embed = build_embed( embed = build_embed(
@ -353,15 +374,19 @@ class RolegiverCog(Extension):
) )
embed.set_thumbnail(url=ctx.guild.icon.url) embed.set_thumbnail(url=ctx.guild.icon.url)
embed.set_author(name=ctx.author.display_name, icon_url=ctx.author.display_avatar.url) embed.set_author(
name=ctx.author.display_name, icon_url=ctx.author.display_avatar.url
)
embed.set_footer(text=f"{ctx.author.username}#{ctx.author.discriminator} | {ctx.author.id}") embed.set_footer(text=f"{ctx.author.username} | {ctx.author.id}")
for row in components: for row in components:
for component in row.components: for component in row.components:
component.disabled = True component.disabled = True
await context.ctx.edit_origin(embeds=embed, components=components, content="\u200b") await context.ctx.edit_origin(
embeds=embed, components=components, content="\u200b"
)
except asyncio.TimeoutError: except asyncio.TimeoutError:
for row in components: for row in components:
@ -369,7 +394,10 @@ class RolegiverCog(Extension):
component.disabled = True component.disabled = True
await message.edit(components=components) await message.edit(components=components)
@rolegiver.subcommand(sub_cmd_name="cleanup", sub_cmd_description="Removed deleted roles from rolegiver") @rolegiver.subcommand(
sub_cmd_name="cleanup",
sub_cmd_description="Removed deleted roles from rolegiver",
)
@check(admin_or_permissions(Permissions.MANAGE_GUILD)) @check(admin_or_permissions(Permissions.MANAGE_GUILD))
async def _rolegiver_cleanup(self, ctx: InteractionContext) -> None: async def _rolegiver_cleanup(self, ctx: InteractionContext) -> None:
setting = await Rolegiver.find_one(Rolegiver.guild == ctx.guild.id) setting = await Rolegiver.find_one(Rolegiver.guild == ctx.guild.id)

View file

@ -50,7 +50,10 @@ class TagCog(Extension):
async def _get(self, ctx: InteractionContext, name: str) -> None: async def _get(self, ctx: InteractionContext, name: str) -> None:
tag = await Tag.find_one(Tag.guild == ctx.guild.id, Tag.name == name) tag = await Tag.find_one(Tag.guild == ctx.guild.id, Tag.name == name)
if not tag: if not tag:
await ctx.send("Well this is awkward, looks like the tag was deleted just now", ephemeral=True) await ctx.send(
"Well this is awkward, looks like the tag was deleted just now",
ephemeral=True,
)
return return
await ctx.send(tag.content) await ctx.send(tag.content)
@ -58,8 +61,7 @@ class TagCog(Extension):
@tag.subcommand(sub_cmd_name="create", sub_cmd_description="Create a tag") @tag.subcommand(sub_cmd_name="create", sub_cmd_description="Create a tag")
async def _create(self, ctx: SlashContext) -> None: async def _create(self, ctx: SlashContext) -> None:
modal = Modal( modal = Modal(
title="Create a new tag!", *[
components=[
InputText( InputText(
label="Tag name", label="Tag name",
placeholder="name", placeholder="name",
@ -75,17 +77,22 @@ class TagCog(Extension):
max_length=512, max_length=512,
), ),
], ],
title="Create a new tag!",
) )
await ctx.send_modal(modal) await ctx.send_modal(modal)
try: try:
response = await self.bot.wait_for_modal(modal, author=ctx.author.id, timeout=60 * 5) response = await self.bot.wait_for_modal(
modal, author=ctx.author.id, timeout=60 * 5
)
name = response.responses.get("name").replace("`", "") name = response.responses.get("name").replace("`", "")
content = response.responses.get("content") content = response.responses.get("content")
except asyncio.TimeoutError: except asyncio.TimeoutError:
return return
noinvite = await Setting.find_one(Setting.guild == ctx.guild.id, Setting.setting == "noinvite") noinvite = await Setting.find_one(
Setting.guild == ctx.guild.id, Setting.setting == "noinvite"
)
if ( if (
(invites.search(content) or invites.search(name)) (invites.search(content) or invites.search(name))
@ -95,13 +102,17 @@ class TagCog(Extension):
or ctx.author.has_permission(Permissions.MANAGE_MESSAGES) or ctx.author.has_permission(Permissions.MANAGE_MESSAGES)
) )
): ):
await response.send("Listen, don't use this to try and bypass the rules", ephemeral=True) await response.send(
"Listen, don't use this to try and bypass the rules", ephemeral=True
)
return return
elif not content.strip() or not name.strip(): elif not content.strip() or not name.strip():
await response.send("Content and name required", ephemeral=True) await response.send("Content and name required", ephemeral=True)
return return
elif not tag_name.match(name): elif not tag_name.match(name):
await response.send("Tag name must only contain: [A-Za-z0-9_- ]", ephemeral=True) await response.send(
"Tag name must only contain: [A-Za-z0-9_- ]", ephemeral=True
)
return return
tag = await Tag.find_one(Tag.guild == ctx.guild.id, Tag.name == name) tag = await Tag.find_one(Tag.guild == ctx.guild.id, Tag.name == name)
@ -122,7 +133,10 @@ class TagCog(Extension):
embed = build_embed( embed = build_embed(
title="Tag Created", title="Tag Created",
description=f"{ctx.author.mention} created a new tag", description=f"{ctx.author.mention} created a new tag",
fields=[EmbedField(name="Name", value=name), EmbedField(name="Content", value=content)], fields=[
EmbedField(name="Name", value=name),
EmbedField(name="Content", value=content),
],
) )
embed.set_author( embed.set_author(
@ -130,7 +144,9 @@ class TagCog(Extension):
icon_url=ctx.author.display_avatar.url, icon_url=ctx.author.display_avatar.url,
) )
components = Button(style=ButtonStyle.DANGER, emoji="🗑️", custom_id=f"delete|{ctx.author.id}") components = Button(
style=ButtonStyle.DANGER, emoji="🗑️", custom_id=f"delete|{ctx.author.id}"
)
await response.send(embeds=embed, components=components) await response.send(embeds=embed, components=components)
if ctx.guild.id not in self.cache: if ctx.guild.id not in self.cache:
@ -155,12 +171,13 @@ class TagCog(Extension):
ctx.author.has_permission(Permissions.ADMINISTRATOR) ctx.author.has_permission(Permissions.ADMINISTRATOR)
or ctx.author.has_permission(Permissions.MANAGE_MESSAGES) or ctx.author.has_permission(Permissions.MANAGE_MESSAGES)
): ):
await ctx.send("You didn't create this tag, ask the creator to edit it", ephemeral=True) await ctx.send(
"You didn't create this tag, ask the creator to edit it", ephemeral=True
)
return return
modal = Modal( modal = Modal(
title="Edit a tag!", *[
components=[
InputText( InputText(
label="Tag name", label="Tag name",
value=tag.name, value=tag.name,
@ -176,11 +193,14 @@ class TagCog(Extension):
max_length=512, max_length=512,
), ),
], ],
title="Edit a tag!",
) )
await ctx.send_modal(modal) await ctx.send_modal(modal)
try: try:
response = await self.bot.wait_for_modal(modal, author=ctx.author.id, timeout=60 * 5) response = await self.bot.wait_for_modal(
modal, author=ctx.author.id, timeout=60 * 5
)
name = response.responses.get("name").replace("`", "") name = response.responses.get("name").replace("`", "")
content = response.responses.get("content") content = response.responses.get("content")
except asyncio.TimeoutError: except asyncio.TimeoutError:
@ -188,10 +208,15 @@ class TagCog(Extension):
new_tag = await Tag.find_one(Tag.guild == ctx.guild.id, Tag.name == name) new_tag = await Tag.find_one(Tag.guild == ctx.guild.id, Tag.name == name)
if new_tag and new_tag.id != tag.id: if new_tag and new_tag.id != tag.id:
await ctx.send("That tag name is used by another tag, choose another name", ephemeral=True) await ctx.send(
"That tag name is used by another tag, choose another name",
ephemeral=True,
)
return return
noinvite = await Setting.find_one(Setting.guild == ctx.guild.id, Setting.setting == "noinvite") noinvite = await Setting.find_one(
Setting.guild == ctx.guild.id, Setting.setting == "noinvite"
)
if ( if (
(invites.search(content) or invites.search(name)) (invites.search(content) or invites.search(name))
@ -201,13 +226,17 @@ class TagCog(Extension):
or ctx.author.has_permission(Permissions.MANAGE_MESSAGES) or ctx.author.has_permission(Permissions.MANAGE_MESSAGES)
) )
): ):
await response.send("Listen, don't use this to try and bypass the rules", ephemeral=True) await response.send(
"Listen, don't use this to try and bypass the rules", ephemeral=True
)
return return
elif not content.strip() or not name.strip(): elif not content.strip() or not name.strip():
await response.send("Content and name required", ephemeral=True) await response.send("Content and name required", ephemeral=True)
return return
elif not tag_name.match(name): elif not tag_name.match(name):
await response.send("Tag name must only contain: [A-Za-z0-9_- ]", ephemeral=True) await response.send(
"Tag name must only contain: [A-Za-z0-9_- ]", ephemeral=True
)
return return
tag.content = re.sub(r"\\?([@<])", r"\\\g<1>", content) tag.content = re.sub(r"\\?([@<])", r"\\\g<1>", content)
@ -230,7 +259,9 @@ class TagCog(Extension):
name=ctx.author.username + "#" + ctx.author.discriminator, name=ctx.author.username + "#" + ctx.author.discriminator,
icon_url=ctx.author.display_avatar.url, icon_url=ctx.author.display_avatar.url,
) )
components = Button(style=ButtonStyle.DANGER, emoji="🗑️", custom_id=f"delete|{ctx.author.id}") components = Button(
style=ButtonStyle.DANGER, emoji="🗑️", custom_id=f"delete|{ctx.author.id}"
)
await response.send(embeds=embed, components=components) await response.send(embeds=embed, components=components)
if tag.name not in self.cache[ctx.guild.id]: if tag.name not in self.cache[ctx.guild.id]:
self.cache[ctx.guild.id].remove(old_name) self.cache[ctx.guild.id].remove(old_name)
@ -253,7 +284,10 @@ class TagCog(Extension):
ctx.author.has_permission(Permissions.ADMINISTRATOR) ctx.author.has_permission(Permissions.ADMINISTRATOR)
or ctx.author.has_permission(Permissions.MANAGE_MESSAGES) or ctx.author.has_permission(Permissions.MANAGE_MESSAGES)
): ):
await ctx.send("You didn't create this tag, ask the creator to delete it", ephemeral=True) await ctx.send(
"You didn't create this tag, ask the creator to delete it",
ephemeral=True,
)
return return
await tag.delete() await tag.delete()
@ -307,7 +341,9 @@ class TagCog(Extension):
name=f"{username}#{discrim}" if username else "Unknown User", name=f"{username}#{discrim}" if username else "Unknown User",
icon_url=url, icon_url=url,
) )
components = Button(style=ButtonStyle.DANGER, emoji="🗑️", custom_id=f"delete|{ctx.author.id}") components = Button(
style=ButtonStyle.DANGER, emoji="🗑️", custom_id=f"delete|{ctx.author.id}"
)
await ctx.send(embeds=embed, components=components) await ctx.send(embeds=embed, components=components)
@tag.subcommand(sub_cmd_name="list", sub_cmd_description="List tag names") @tag.subcommand(sub_cmd_name="list", sub_cmd_description="List tag names")
@ -315,7 +351,9 @@ class TagCog(Extension):
tags = await Tag.find(Tag.guild == ctx.guild.id).to_list() tags = await Tag.find(Tag.guild == ctx.guild.id).to_list()
names = "\n".join(f"`{t.name}`" for t in tags) names = "\n".join(f"`{t.name}`" for t in tags)
embed = build_embed(title="All Tags", description=names, fields=[]) embed = build_embed(title="All Tags", description=names, fields=[])
components = Button(style=ButtonStyle.DANGER, emoji="🗑️", custom_id=f"delete|{ctx.author.id}") components = Button(
style=ButtonStyle.DANGER, emoji="🗑️", custom_id=f"delete|{ctx.author.id}"
)
await ctx.send(embeds=embed, components=components) await ctx.send(embeds=embed, components=components)
@_get.autocomplete("name") @_get.autocomplete("name")
@ -326,7 +364,9 @@ class TagCog(Extension):
if not self.cache.get(ctx.guild.id): if not self.cache.get(ctx.guild.id):
tags = await Tag.find(Tag.guild == ctx.guild.id).to_list() tags = await Tag.find(Tag.guild == ctx.guild.id).to_list()
self.cache[ctx.guild.id] = [tag.name for tag in tags] self.cache[ctx.guild.id] = [tag.name for tag in tags]
results = process.extract(ctx.input_text, self.cache.get(ctx.guild.id), limit=25) results = process.extract(
ctx.input_text, self.cache.get(ctx.guild.id), limit=25
)
choices = [{"name": r[0], "value": r[0]} for r in results] choices = [{"name": r[0], "value": r[0]} for r in results]
await ctx.send(choices=choices) await ctx.send(choices=choices)

View file

@ -1,4 +1,10 @@
"""JARVIS Complete the Code 2 Cog.""" """
JARVIS Complete the Code 2 Cog.
This cog is now maintenance-only due to conflict with the dbrand moderators.
Please do not file feature requests related to this cog; they will be closed.
"""
import logging import logging
import re import re
@ -19,7 +25,7 @@ from jarvis_core.db.models import Guess
from jarvis.utils import build_embed from jarvis.utils import build_embed
guild_ids = [578757004059738142, 520021794380447745, 862402786116763668] guild_ids = [] # [578757004059738142, 520021794380447745, 862402786116763668]
valid = re.compile(r"[\w\s\-\\/.!@#$%^*()+=<>,\u0080-\U000E0FFF]*") valid = re.compile(r"[\w\s\-\\/.!@#$%^*()+=<>,\u0080-\U000E0FFF]*")
invites = re.compile( invites = re.compile(
@ -40,30 +46,54 @@ class CTCCog(Extension):
def __del__(self): def __del__(self):
self._session.close() self._session.close()
ctc2 = SlashCommand(name="ctc2", description="CTC2 related commands", scopes=guild_ids) ctc2 = SlashCommand(
name="ctc2", description="CTC2 related commands", scopes=guild_ids
)
@ctc2.subcommand(sub_cmd_name="about") @ctc2.subcommand(sub_cmd_name="about")
@cooldown(bucket=Buckets.USER, rate=1, interval=30) @cooldown(bucket=Buckets.USER, rate=1, interval=30)
async def _about(self, ctx: InteractionContext) -> None: async def _about(self, ctx: InteractionContext) -> None:
components = [ActionRow(Button(style=ButtonStyle.URL, url="https://completethecode.com", label="More Info"))] components = [
await ctx.send("See https://completethecode.com for more information", components=components) ActionRow(
Button(
style=ButtonStyle.URL,
url="https://completethecode.com",
label="More Info",
)
)
]
await ctx.send(
"See https://completethecode.com for more information",
components=components,
)
@ctc2.subcommand( @ctc2.subcommand(
sub_cmd_name="pw", sub_cmd_name="pw",
sub_cmd_description="Guess a password for https://completethecodetwo.cards", sub_cmd_description="Guess a password for https://completethecodetwo.cards",
) )
@slash_option(name="guess", description="Guess a password", opt_type=OptionType.STRING, required=True) @slash_option(
name="guess",
description="Guess a password",
opt_type=OptionType.STRING,
required=True,
)
@cooldown(bucket=Buckets.USER, rate=1, interval=2) @cooldown(bucket=Buckets.USER, rate=1, interval=2)
async def _pw(self, ctx: InteractionContext, guess: str) -> None: async def _pw(self, ctx: InteractionContext, guess: str) -> None:
if len(guess) > 800: if len(guess) > 800:
await ctx.send( await ctx.send(
("Listen here, dipshit. Don't be like <@256110768724901889>. " "Make your guesses < 800 characters."), (
"Listen here, dipshit. Don't be like <@256110768724901889>. "
"Make your guesses < 800 characters."
),
ephemeral=True, ephemeral=True,
) )
return return
elif not valid.fullmatch(guess): elif not valid.fullmatch(guess):
await ctx.send( await ctx.send(
("Listen here, dipshit. Don't be like <@256110768724901889>. " "Make your guesses *readable*."), (
"Listen here, dipshit. Don't be like <@256110768724901889>. "
"Make your guesses *readable*."
),
ephemeral=True, ephemeral=True,
) )
return return
@ -102,7 +132,7 @@ class CTCCog(Extension):
if not user: if not user:
user = "[redacted]" user = "[redacted]"
if isinstance(user, (Member, User)): if isinstance(user, (Member, User)):
user = user.username + "#" + user.discriminator user = user.username
cache[guess.user] = user cache[guess.user] = user
name = "Correctly" if guess["correct"] else "Incorrectly" name = "Correctly" if guess["correct"] else "Incorrectly"
name += " guessed by: " + user name += " guessed by: " + user

View file

@ -1,4 +1,10 @@
"""JARVIS dbrand cog.""" """
JARVIS dbrand cog.
This cog is now maintenance-only due to conflict with the dbrand moderators.
Please do not file feature requests related to this cog; they will be closed.
"""
import logging import logging
import re import re
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
@ -63,7 +69,9 @@ async def parse_db_status() -> dict:
else: else:
cell = cell.get_text().strip() cell = cell.get_text().strip()
row_data.append(cell) row_data.append(cell)
data[data_key].append({headers[idx]: value for idx, value in enumerate(row_data)}) data[data_key].append(
{headers[idx]: value for idx, value in enumerate(row_data)}
)
return data return data
@ -88,7 +96,9 @@ class DbrandCog(Extension):
db = SlashCommand(name="db", description="dbrand commands", scopes=guild_ids) db = SlashCommand(name="db", description="dbrand commands", scopes=guild_ids)
@db.subcommand(sub_cmd_name="status", sub_cmd_description="Get dbrand operational status") @db.subcommand(
sub_cmd_name="status", sub_cmd_description="Get dbrand operational status"
)
async def _status(self, ctx: InteractionContext) -> None: async def _status(self, ctx: InteractionContext) -> None:
status = self.cache.get("status") status = self.cache.get("status")
if not status or status["cache_expiry"] <= datetime.now(tz=timezone.utc): if not status or status["cache_expiry"] <= datetime.now(tz=timezone.utc):
@ -97,7 +107,10 @@ class DbrandCog(Extension):
self.cache["status"] = status self.cache["status"] = status
status = status.get("operations") status = status.get("operations")
emojies = [x["Status"] for x in status] emojies = [x["Status"] for x in status]
fields = [EmbedField(name=f'{x["Status"]} {x["Service"]}', value=x["Detail"]) for x in status] fields = [
EmbedField(name=f'{x["Status"]} {x["Service"]}', value=x["Detail"])
for x in status
]
color = "#FBBD1E" color = "#FBBD1E"
if all("green" in x for x in emojies): if all("green" in x for x in emojies):
color = "#38F657" color = "#38F657"
@ -169,7 +182,9 @@ class DbrandCog(Extension):
async def _support(self, ctx: InteractionContext) -> None: async def _support(self, ctx: InteractionContext) -> None:
return await self._db_support_cmd(ctx) return await self._db_support_cmd(ctx)
@db.subcommand(sub_cmd_name="gripcheck", sub_cmd_description="Watch a dbrand grip get thrown") @db.subcommand(
sub_cmd_name="gripcheck", sub_cmd_description="Watch a dbrand grip get thrown"
)
async def _gripcheck(self, ctx: InteractionContext) -> None: async def _gripcheck(self, ctx: InteractionContext) -> None:
video_url = "https://cdn.discordapp.com/attachments/599068193339736096/890679742263623751/video0.mov" video_url = "https://cdn.discordapp.com/attachments/599068193339736096/890679742263623751/video0.mov"
image_url = "https://cdn.discordapp.com/attachments/599068193339736096/890680198306095104/image0.jpg" image_url = "https://cdn.discordapp.com/attachments/599068193339736096/890680198306095104/image0.jpg"
@ -188,13 +203,22 @@ class DbrandCog(Extension):
f"[Be (not) extorted]({self.base_url + 'not-extortion'})", f"[Be (not) extorted]({self.base_url + 'not-extortion'})",
"[Robot Camo Wallpapers](https://db.io/wallpapers)", "[Robot Camo Wallpapers](https://db.io/wallpapers)",
] ]
embed = build_embed(title="Useful Links", description="\n\n".join(urls), fields=[], color="#FFBB00") embed = build_embed(
title="Useful Links",
description="\n\n".join(urls),
fields=[],
color="#FFBB00",
)
embed.set_footer( embed.set_footer(
text="dbrand.com", text="dbrand.com",
icon_url="https://dev.zevaryx.com/db_logo.png", icon_url="https://dev.zevaryx.com/db_logo.png",
) )
embed.set_thumbnail(url="https://dev.zevaryx.com/db_logo.png") embed.set_thumbnail(url="https://dev.zevaryx.com/db_logo.png")
embed.set_author(name="dbrand", url=self.base_url, icon_url="https://dev.zevaryx.com/db_logo.png") embed.set_author(
name="dbrand",
url=self.base_url,
icon_url="https://dev.zevaryx.com/db_logo.png",
)
await ctx.send(embeds=embed) await ctx.send(embeds=embed)
@db.subcommand( @db.subcommand(
@ -217,11 +241,15 @@ class DbrandCog(Extension):
): ):
# Magic number, subtract from flag char to get ascii char # Magic number, subtract from flag char to get ascii char
uni2ascii = 127365 uni2ascii = 127365
search = chr(ord(search[0]) - uni2ascii) + chr(ord(search[1]) - uni2ascii) search = chr(ord(search[0]) - uni2ascii) + chr(
ord(search[1]) - uni2ascii
)
elif search == "🏳️": elif search == "🏳️":
search = "fr" search = "fr"
else: else:
await ctx.send("Please use text to search for shipping.", ephemeral=True) await ctx.send(
"Please use text to search for shipping.", ephemeral=True
)
return return
if len(search) > 3: if len(search) > 3:
countries = {x["country"]: x["alpha-2"] for x in shipping_lookup} countries = {x["country"]: x["alpha-2"] for x in shipping_lookup}
@ -246,7 +274,9 @@ class DbrandCog(Extension):
data = await self._session.get(api_link) data = await self._session.get(api_link)
if 200 <= data.status < 400: if 200 <= data.status < 400:
data = await data.json() data = await data.json()
data["cache_expiry"] = datetime.now(tz=timezone.utc) + timedelta(hours=24) data["cache_expiry"] = datetime.now(tz=timezone.utc) + timedelta(
hours=24
)
self.cache[dest] = data self.cache[dest] = data
else: else:
data = None data = None
@ -255,12 +285,18 @@ class DbrandCog(Extension):
fields = [] fields = []
for service in data["shipping_services_available"]: for service in data["shipping_services_available"]:
service_data = self.cache.get(f"{dest}-{service}") service_data = self.cache.get(f"{dest}-{service}")
if not service_data or service_data["cache_expiry"] < datetime.now(tz=timezone.utc): if not service_data or service_data["cache_expiry"] < datetime.now(
service_data = await self._session.get(self.api_url + dest + "/" + service["url"]) tz=timezone.utc
):
service_data = await self._session.get(
self.api_url + dest + "/" + service["url"]
)
if service_data.status > 400: if service_data.status > 400:
continue continue
service_data = await service_data.json() service_data = await service_data.json()
service_data["cache_expiry"] = datetime.now(tz=timezone.utc) + timedelta(hours=24) service_data["cache_expiry"] = datetime.now(
tz=timezone.utc
) + timedelta(hours=24)
self.cache[f"{dest}-{service}"] = service_data self.cache[f"{dest}-{service}"] = service_data
title = f'{service_data["carrier"]} {service_data["tier-title"]} | {service_data["costs-min"]}' title = f'{service_data["carrier"]} {service_data["tier-title"]} | {service_data["costs-min"]}'
message = service_data["time-title"] message = service_data["time-title"]
@ -271,7 +307,9 @@ class DbrandCog(Extension):
status = self.cache.get("status") status = self.cache.get("status")
if not status or status["cache_expiry"] <= datetime.now(tz=timezone.utc): if not status or status["cache_expiry"] <= datetime.now(tz=timezone.utc):
status = await parse_db_status() status = await parse_db_status()
status["cache_expiry"] = datetime.now(tz=timezone.utc) + timedelta(hours=2) status["cache_expiry"] = datetime.now(tz=timezone.utc) + timedelta(
hours=2
)
self.cache["status"] = status self.cache["status"] = status
status = status["countries"] status = status["countries"]
@ -284,10 +322,10 @@ class DbrandCog(Extension):
description = "" description = ""
color = "#FFBB00" color = "#FFBB00"
if shipping_info: if shipping_info:
description = ( description = f'{shipping_info["Status"]}\u200b \u200b {shipping_info["Est. Delivery Time"].split(":")[0]}'
f'{shipping_info["Status"]}\u200b \u200b {shipping_info["Est. Delivery Time"].split(":")[0]}' created = self.cache.get("status").get("cache_expiry") - timedelta(
hours=2
) )
created = self.cache.get("status").get("cache_expiry") - timedelta(hours=2)
ts = int(created.timestamp()) ts = int(created.timestamp())
description += f" \u200b | \u200b Last updated: <t:{ts}:R>\n\u200b" description += f" \u200b | \u200b Last updated: <t:{ts}:R>\n\u200b"
if "green" in shipping_info["Status"]: if "green" in shipping_info["Status"]:
@ -316,7 +354,8 @@ class DbrandCog(Extension):
embed = build_embed( embed = build_embed(
title="Check Shipping Times", title="Check Shipping Times",
description=( description=(
"Country not found.\nYou can [view all shipping " "destinations here](https://dbrand.com/shipping)" "Country not found.\nYou can [view all shipping "
"destinations here](https://dbrand.com/shipping)"
), ),
fields=[], fields=[],
url="https://dbrand.com/shipping", url="https://dbrand.com/shipping",

View file

@ -40,24 +40,6 @@ class Mastodon(BaseModel):
url: str url: str
class Reddit(BaseModel):
"""Reddit config."""
user_agent: Optional[str] = None
client_secret: str
client_id: str
class Twitter(BaseModel):
"""Twitter config."""
consumer_key: str
consumer_secret: str
access_token: str
access_secret: str
bearer_token: str
class Environment(Enum): class Environment(Enum):
"""JARVIS running environment.""" """JARVIS running environment."""
@ -69,12 +51,13 @@ class Config(BaseModel):
"""JARVIS config model.""" """JARVIS config model."""
token: str token: str
"""Bot token"""
erapi: str
"""exchangerate-api.org API token"""
environment: Environment = Environment.develop environment: Environment = Environment.develop
mongo: Mongo mongo: Mongo
redis: Redis redis: Redis
mastodon: Optional[Mastodon] = None mastodon: Optional[Mastodon] = None
reddit: Optional[Reddit] = None
twitter: Optional[Twitter] = None
urls: Optional[dict[str, str]] = None urls: Optional[dict[str, str]] = None
sync: bool = False sync: bool = False
log_level: str = "INFO" log_level: str = "INFO"
@ -112,22 +95,16 @@ def _load_env() -> Config | None:
mongo = {} mongo = {}
redis = {} redis = {}
mastodon = {} mastodon = {}
twitter = {}
reddit = {}
urls = {} urls = {}
mongo_keys = find_all(lambda x: x.upper().startswith("MONGO"), environ.keys()) mongo_keys = find_all(lambda x: x.upper().startswith("MONGO"), environ.keys())
redis_keys = find_all(lambda x: x.upper().startswith("REDIS"), environ.keys()) redis_keys = find_all(lambda x: x.upper().startswith("REDIS"), environ.keys())
mastodon_keys = find_all(lambda x: x.upper().startswith("MASTODON"), environ.keys()) mastodon_keys = find_all(lambda x: x.upper().startswith("MASTODON"), environ.keys())
reddit_keys = find_all(lambda x: x.upper().startswith("REDDIT"), environ.keys())
twitter_keys = find_all(lambda x: x.upper().startswith("TWITTER"), environ.keys())
url_keys = find_all(lambda x: x.upper().startswith("URLS"), environ.keys()) url_keys = find_all(lambda x: x.upper().startswith("URLS"), environ.keys())
config_keys = ( config_keys = (
mongo_keys mongo_keys
+ redis_keys + redis_keys
+ mastodon_keys + mastodon_keys
+ reddit_keys
+ twitter_keys
+ url_keys + url_keys
+ ["TOKEN", "SYNC", "LOG_LEVEL", "JURIGGED"] + ["TOKEN", "SYNC", "LOG_LEVEL", "JURIGGED"]
) )
@ -145,12 +122,6 @@ def _load_env() -> Config | None:
elif item in mastodon_keys: elif item in mastodon_keys:
key = "_".join(item.split("_")[1:]).lower() key = "_".join(item.split("_")[1:]).lower()
mastodon[key] = value mastodon[key] = value
elif item in twitter_keys:
key = "_".join(item.split("_")[1:]).lower()
twitter[key] = value
elif item in reddit_keys:
key = "_".join(item.split("_")[1:]).lower()
reddit[key] = value
elif item in url_keys: elif item in url_keys:
key = "_".join(item.split("_")[1:]).lower() key = "_".join(item.split("_")[1:]).lower()
urls[key] = value urls[key] = value
@ -161,10 +132,6 @@ def _load_env() -> Config | None:
data["mongo"] = mongo data["mongo"] = mongo
data["redis"] = redis data["redis"] = redis
if all(x is not None for x in reddit.values()):
data["reddit"] = reddit
if all(x is not None for x in twitter.values()):
data["twitter"] = twitter
if all(x is not None for x in mastodon.values()): if all(x is not None for x in mastodon.values()):
data["mastodon"] = mastodon data["mastodon"] = mastodon
data["urls"] = {k: v for k, v in urls if v} data["urls"] = {k: v for k, v in urls if v}

View file

@ -1,7 +1,7 @@
"""dbrand-specific data.""" """dbrand-specific data."""
shipping_lookup = [ shipping_lookup = [
{"country": "Afghanistan", "alpha-2": "AF", "alpha-3": "AFG", "numeric": "0004"}, {"country": "Afghanistan", "alpha-2": "AF", "alpha-3": "AFG", "numeric": "0004"},
{"country": "Ã…land Islands", "alpha-2": "AX", "alpha-3": "ALA", "numeric": "0248"}, {"country": "Aland Islands", "alpha-2": "AX", "alpha-3": "ALA", "numeric": "0248"},
{"country": "Albania", "alpha-2": "AL", "alpha-3": "ALB", "numeric": "0008"}, {"country": "Albania", "alpha-2": "AL", "alpha-3": "ALB", "numeric": "0008"},
{"country": "Algeria", "alpha-2": "DZ", "alpha-3": "DZA", "numeric": "0012"}, {"country": "Algeria", "alpha-2": "DZ", "alpha-3": "DZA", "numeric": "0012"},
{"country": "American Samoa", "alpha-2": "AS", "alpha-3": "ASM", "numeric": "0016"}, {"country": "American Samoa", "alpha-2": "AS", "alpha-3": "ASM", "numeric": "0016"},
@ -9,7 +9,12 @@ shipping_lookup = [
{"country": "Angola", "alpha-2": "AO", "alpha-3": "AGO", "numeric": "0024"}, {"country": "Angola", "alpha-2": "AO", "alpha-3": "AGO", "numeric": "0024"},
{"country": "Anguilla", "alpha-2": "AI", "alpha-3": "AIA", "numeric": "0660"}, {"country": "Anguilla", "alpha-2": "AI", "alpha-3": "AIA", "numeric": "0660"},
{"country": "Antarctica", "alpha-2": "AQ", "alpha-3": "ATA", "numeric": "0010"}, {"country": "Antarctica", "alpha-2": "AQ", "alpha-3": "ATA", "numeric": "0010"},
{"country": "Antigua and Barbuda", "alpha-2": "AG", "alpha-3": "ATG", "numeric": "0028"}, {
"country": "Antigua and Barbuda",
"alpha-2": "AG",
"alpha-3": "ATG",
"numeric": "0028",
},
{"country": "Argentina", "alpha-2": "AR", "alpha-3": "ARG", "numeric": "0032"}, {"country": "Argentina", "alpha-2": "AR", "alpha-3": "ARG", "numeric": "0032"},
{"country": "Armenia", "alpha-2": "AM", "alpha-3": "ARM", "numeric": "0051"}, {"country": "Armenia", "alpha-2": "AM", "alpha-3": "ARM", "numeric": "0051"},
{"country": "Aruba", "alpha-2": "AW", "alpha-3": "ABW", "numeric": "0533"}, {"country": "Aruba", "alpha-2": "AW", "alpha-3": "ABW", "numeric": "0533"},
@ -38,7 +43,12 @@ shipping_lookup = [
"alpha-3": "BES", "alpha-3": "BES",
"numeric": "0535", "numeric": "0535",
}, },
{"country": "Bosnia and Herzegovina", "alpha-2": "BA", "alpha-3": "BIH", "numeric": "0070"}, {
"country": "Bosnia and Herzegovina",
"alpha-2": "BA",
"alpha-3": "BIH",
"numeric": "0070",
},
{"country": "Botswana", "alpha-2": "BW", "alpha-3": "BWA", "numeric": "0072"}, {"country": "Botswana", "alpha-2": "BW", "alpha-3": "BWA", "numeric": "0072"},
{"country": "Bouvet Island", "alpha-2": "BV", "alpha-3": "BVT", "numeric": "0074"}, {"country": "Bouvet Island", "alpha-2": "BV", "alpha-3": "BVT", "numeric": "0074"},
{"country": "Brazil", "alpha-2": "BR", "alpha-3": "BRA", "numeric": "0076"}, {"country": "Brazil", "alpha-2": "BR", "alpha-3": "BRA", "numeric": "0076"},
@ -48,7 +58,12 @@ shipping_lookup = [
"alpha-3": "IOT", "alpha-3": "IOT",
"numeric": "0086", "numeric": "0086",
}, },
{"country": "Brunei Darussalam", "alpha-2": "BN", "alpha-3": "BRN", "numeric": "0096"}, {
"country": "Brunei Darussalam",
"alpha-2": "BN",
"alpha-3": "BRN",
"numeric": "0096",
},
{"country": "Bulgaria", "alpha-2": "BG", "alpha-3": "BGR", "numeric": "0100"}, {"country": "Bulgaria", "alpha-2": "BG", "alpha-3": "BGR", "numeric": "0100"},
{"country": "Burkina Faso", "alpha-2": "BF", "alpha-3": "BFA", "numeric": "0854"}, {"country": "Burkina Faso", "alpha-2": "BF", "alpha-3": "BFA", "numeric": "0854"},
{"country": "Burundi", "alpha-2": "BI", "alpha-3": "BDI", "numeric": "0108"}, {"country": "Burundi", "alpha-2": "BI", "alpha-3": "BDI", "numeric": "0108"},
@ -56,7 +71,12 @@ shipping_lookup = [
{"country": "Cambodia", "alpha-2": "KH", "alpha-3": "KHM", "numeric": "0116"}, {"country": "Cambodia", "alpha-2": "KH", "alpha-3": "KHM", "numeric": "0116"},
{"country": "Cameroon", "alpha-2": "CM", "alpha-3": "CMR", "numeric": "0120"}, {"country": "Cameroon", "alpha-2": "CM", "alpha-3": "CMR", "numeric": "0120"},
{"country": "Canada", "alpha-2": "CA", "alpha-3": "CAN", "numeric": "0124"}, {"country": "Canada", "alpha-2": "CA", "alpha-3": "CAN", "numeric": "0124"},
{"country": "Cayman Islands (the)", "alpha-2": "KY", "alpha-3": "CYM", "numeric": "0136"}, {
"country": "Cayman Islands (the)",
"alpha-2": "KY",
"alpha-3": "CYM",
"numeric": "0136",
},
{ {
"country": "Central African Republic (the)", "country": "Central African Republic (the)",
"alpha-2": "CF", "alpha-2": "CF",
@ -66,7 +86,12 @@ shipping_lookup = [
{"country": "Chad", "alpha-2": "TD", "alpha-3": "TCD", "numeric": "0148"}, {"country": "Chad", "alpha-2": "TD", "alpha-3": "TCD", "numeric": "0148"},
{"country": "Chile", "alpha-2": "CL", "alpha-3": "CHL", "numeric": "0152"}, {"country": "Chile", "alpha-2": "CL", "alpha-3": "CHL", "numeric": "0152"},
{"country": "China", "alpha-2": "CN", "alpha-3": "CHN", "numeric": "0156"}, {"country": "China", "alpha-2": "CN", "alpha-3": "CHN", "numeric": "0156"},
{"country": "Christmas Island", "alpha-2": "CX", "alpha-3": "CXR", "numeric": "0162"}, {
"country": "Christmas Island",
"alpha-2": "CX",
"alpha-3": "CXR",
"numeric": "0162",
},
{ {
"country": "Cocos (Keeling) Islands (the)", "country": "Cocos (Keeling) Islands (the)",
"alpha-2": "CC", "alpha-2": "CC",
@ -82,22 +107,37 @@ shipping_lookup = [
"numeric": "0180", "numeric": "0180",
}, },
{"country": "Congo (the)", "alpha-2": "CG", "alpha-3": "COG", "numeric": "0178"}, {"country": "Congo (the)", "alpha-2": "CG", "alpha-3": "COG", "numeric": "0178"},
{"country": "Cook Islands (the)", "alpha-2": "CK", "alpha-3": "COK", "numeric": "0184"}, {
"country": "Cook Islands (the)",
"alpha-2": "CK",
"alpha-3": "COK",
"numeric": "0184",
},
{"country": "Costa Rica", "alpha-2": "CR", "alpha-3": "CRI", "numeric": "0188"}, {"country": "Costa Rica", "alpha-2": "CR", "alpha-3": "CRI", "numeric": "0188"},
{"country": "´te d'Ivoire", "alpha-2": "CI", "alpha-3": "CIV", "numeric": "0384"}, {"country": "Ivory Coast", "alpha-2": "CI", "alpha-3": "CIV", "numeric": "0384"},
{"country": "Croatia", "alpha-2": "HR", "alpha-3": "HRV", "numeric": "0191"}, {"country": "Croatia", "alpha-2": "HR", "alpha-3": "HRV", "numeric": "0191"},
{"country": "Cuba", "alpha-2": "CU", "alpha-3": "CUB", "numeric": "0192"}, {"country": "Cuba", "alpha-2": "CU", "alpha-3": "CUB", "numeric": "0192"},
{"country": "Curaçao", "alpha-2": "CW", "alpha-3": "CUW", "numeric": "0531"}, {"country": "Curacao", "alpha-2": "CW", "alpha-3": "CUW", "numeric": "0531"},
{"country": "Cyprus", "alpha-2": "CY", "alpha-3": "CYP", "numeric": "0196"}, {"country": "Cyprus", "alpha-2": "CY", "alpha-3": "CYP", "numeric": "0196"},
{"country": "Czechia", "alpha-2": "CZ", "alpha-3": "CZE", "numeric": "0203"}, {"country": "Czechia", "alpha-2": "CZ", "alpha-3": "CZE", "numeric": "0203"},
{"country": "Denmark", "alpha-2": "DK", "alpha-3": "DNK", "numeric": "0208"}, {"country": "Denmark", "alpha-2": "DK", "alpha-3": "DNK", "numeric": "0208"},
{"country": "Djibouti", "alpha-2": "DJ", "alpha-3": "DJI", "numeric": "0262"}, {"country": "Djibouti", "alpha-2": "DJ", "alpha-3": "DJI", "numeric": "0262"},
{"country": "Dominica", "alpha-2": "DM", "alpha-3": "DMA", "numeric": "0212"}, {"country": "Dominica", "alpha-2": "DM", "alpha-3": "DMA", "numeric": "0212"},
{"country": "Dominican Republic (the)", "alpha-2": "DO", "alpha-3": "DOM", "numeric": "0214"}, {
"country": "Dominican Republic (the)",
"alpha-2": "DO",
"alpha-3": "DOM",
"numeric": "0214",
},
{"country": "Ecuador", "alpha-2": "EC", "alpha-3": "ECU", "numeric": "0218"}, {"country": "Ecuador", "alpha-2": "EC", "alpha-3": "ECU", "numeric": "0218"},
{"country": "Egypt", "alpha-2": "EG", "alpha-3": "EGY", "numeric": "0818"}, {"country": "Egypt", "alpha-2": "EG", "alpha-3": "EGY", "numeric": "0818"},
{"country": "El Salvador", "alpha-2": "SV", "alpha-3": "SLV", "numeric": "0222"}, {"country": "El Salvador", "alpha-2": "SV", "alpha-3": "SLV", "numeric": "0222"},
{"country": "Equatorial Guinea", "alpha-2": "GQ", "alpha-3": "GNQ", "numeric": "0226"}, {
"country": "Equatorial Guinea",
"alpha-2": "GQ",
"alpha-3": "GNQ",
"numeric": "0226",
},
{"country": "Eritrea", "alpha-2": "ER", "alpha-3": "ERI", "numeric": "0232"}, {"country": "Eritrea", "alpha-2": "ER", "alpha-3": "ERI", "numeric": "0232"},
{"country": "Estonia", "alpha-2": "EE", "alpha-3": "EST", "numeric": "0233"}, {"country": "Estonia", "alpha-2": "EE", "alpha-3": "EST", "numeric": "0233"},
{"country": "Eswatini", "alpha-2": "SZ", "alpha-3": "SWZ", "numeric": "0748"}, {"country": "Eswatini", "alpha-2": "SZ", "alpha-3": "SWZ", "numeric": "0748"},
@ -108,12 +148,22 @@ shipping_lookup = [
"alpha-3": "FLK", "alpha-3": "FLK",
"numeric": "0238", "numeric": "0238",
}, },
{"country": "Faroe Islands (the)", "alpha-2": "FO", "alpha-3": "FRO", "numeric": "0234"}, {
"country": "Faroe Islands (the)",
"alpha-2": "FO",
"alpha-3": "FRO",
"numeric": "0234",
},
{"country": "Fiji", "alpha-2": "FJ", "alpha-3": "FJI", "numeric": "0242"}, {"country": "Fiji", "alpha-2": "FJ", "alpha-3": "FJI", "numeric": "0242"},
{"country": "Finland", "alpha-2": "FI", "alpha-3": "FIN", "numeric": "0246"}, {"country": "Finland", "alpha-2": "FI", "alpha-3": "FIN", "numeric": "0246"},
{"country": "France", "alpha-2": "FR", "alpha-3": "FRA", "numeric": "0250"}, {"country": "France", "alpha-2": "FR", "alpha-3": "FRA", "numeric": "0250"},
{"country": "French Guiana", "alpha-2": "GF", "alpha-3": "GUF", "numeric": "0254"}, {"country": "French Guiana", "alpha-2": "GF", "alpha-3": "GUF", "numeric": "0254"},
{"country": "French Polynesia", "alpha-2": "PF", "alpha-3": "PYF", "numeric": "0258"}, {
"country": "French Polynesia",
"alpha-2": "PF",
"alpha-3": "PYF",
"numeric": "0258",
},
{ {
"country": "French Southern Territories (the)", "country": "French Southern Territories (the)",
"alpha-2": "TF", "alpha-2": "TF",
@ -150,7 +200,12 @@ shipping_lookup = [
{"country": "Iceland", "alpha-2": "IS", "alpha-3": "ISL", "numeric": "0352"}, {"country": "Iceland", "alpha-2": "IS", "alpha-3": "ISL", "numeric": "0352"},
{"country": "India", "alpha-2": "IN", "alpha-3": "IND", "numeric": "0356"}, {"country": "India", "alpha-2": "IN", "alpha-3": "IND", "numeric": "0356"},
{"country": "Indonesia", "alpha-2": "ID", "alpha-3": "IDN", "numeric": "0360"}, {"country": "Indonesia", "alpha-2": "ID", "alpha-3": "IDN", "numeric": "0360"},
{"country": "Iran (Islamic Republic of)", "alpha-2": "IR", "alpha-3": "IRN", "numeric": "0364"}, {
"country": "Iran (Islamic Republic of)",
"alpha-2": "IR",
"alpha-3": "IRN",
"numeric": "0364",
},
{"country": "Iraq", "alpha-2": "IQ", "alpha-3": "IRQ", "numeric": "0368"}, {"country": "Iraq", "alpha-2": "IQ", "alpha-3": "IRQ", "numeric": "0368"},
{"country": "Ireland", "alpha-2": "IE", "alpha-3": "IRL", "numeric": "0372"}, {"country": "Ireland", "alpha-2": "IE", "alpha-3": "IRL", "numeric": "0372"},
{"country": "Isle of Man", "alpha-2": "IM", "alpha-3": "IMN", "numeric": "0833"}, {"country": "Isle of Man", "alpha-2": "IM", "alpha-3": "IMN", "numeric": "0833"},
@ -169,7 +224,12 @@ shipping_lookup = [
"alpha-3": "PRK", "alpha-3": "PRK",
"numeric": "0408", "numeric": "0408",
}, },
{"country": "Korea (the Republic of)", "alpha-2": "KR", "alpha-3": "KOR", "numeric": "0410"}, {
"country": "Korea (the Republic of)",
"alpha-2": "KR",
"alpha-3": "KOR",
"numeric": "0410",
},
{"country": "Kuwait", "alpha-2": "KW", "alpha-3": "KWT", "numeric": "0414"}, {"country": "Kuwait", "alpha-2": "KW", "alpha-3": "KWT", "numeric": "0414"},
{"country": "Kyrgyzstan", "alpha-2": "KG", "alpha-3": "KGZ", "numeric": "0417"}, {"country": "Kyrgyzstan", "alpha-2": "KG", "alpha-3": "KGZ", "numeric": "0417"},
{ {
@ -199,7 +259,12 @@ shipping_lookup = [
{"country": "Maldives", "alpha-2": "MV", "alpha-3": "MDV", "numeric": "0462"}, {"country": "Maldives", "alpha-2": "MV", "alpha-3": "MDV", "numeric": "0462"},
{"country": "Mali", "alpha-2": "ML", "alpha-3": "MLI", "numeric": "0466"}, {"country": "Mali", "alpha-2": "ML", "alpha-3": "MLI", "numeric": "0466"},
{"country": "Malta", "alpha-2": "MT", "alpha-3": "MLT", "numeric": "0470"}, {"country": "Malta", "alpha-2": "MT", "alpha-3": "MLT", "numeric": "0470"},
{"country": "Marshall Islands (the)", "alpha-2": "MH", "alpha-3": "MHL", "numeric": "0584"}, {
"country": "Marshall Islands (the)",
"alpha-2": "MH",
"alpha-3": "MHL",
"numeric": "0584",
},
{"country": "Martinique", "alpha-2": "MQ", "alpha-3": "MTQ", "numeric": "0474"}, {"country": "Martinique", "alpha-2": "MQ", "alpha-3": "MTQ", "numeric": "0474"},
{"country": "Mauritania", "alpha-2": "MR", "alpha-3": "MRT", "numeric": "0478"}, {"country": "Mauritania", "alpha-2": "MR", "alpha-3": "MRT", "numeric": "0478"},
{"country": "Mauritius", "alpha-2": "MU", "alpha-3": "MUS", "numeric": "0480"}, {"country": "Mauritius", "alpha-2": "MU", "alpha-3": "MUS", "numeric": "0480"},
@ -211,7 +276,12 @@ shipping_lookup = [
"alpha-3": "FSM", "alpha-3": "FSM",
"numeric": "0583", "numeric": "0583",
}, },
{"country": "Moldova (the Republic of)", "alpha-2": "MD", "alpha-3": "MDA", "numeric": "0498"}, {
"country": "Moldova (the Republic of)",
"alpha-2": "MD",
"alpha-3": "MDA",
"numeric": "0498",
},
{"country": "Monaco", "alpha-2": "MC", "alpha-3": "MCO", "numeric": "0492"}, {"country": "Monaco", "alpha-2": "MC", "alpha-3": "MCO", "numeric": "0492"},
{"country": "Mongolia", "alpha-2": "MN", "alpha-3": "MNG", "numeric": "0496"}, {"country": "Mongolia", "alpha-2": "MN", "alpha-3": "MNG", "numeric": "0496"},
{"country": "Montenegro", "alpha-2": "ME", "alpha-3": "MNE", "numeric": "0499"}, {"country": "Montenegro", "alpha-2": "ME", "alpha-3": "MNE", "numeric": "0499"},
@ -222,7 +292,12 @@ shipping_lookup = [
{"country": "Namibia", "alpha-2": "NA", "alpha-3": "NAM", "numeric": "0516"}, {"country": "Namibia", "alpha-2": "NA", "alpha-3": "NAM", "numeric": "0516"},
{"country": "Nauru", "alpha-2": "NR", "alpha-3": "NRU", "numeric": "0520"}, {"country": "Nauru", "alpha-2": "NR", "alpha-3": "NRU", "numeric": "0520"},
{"country": "Nepal", "alpha-2": "NP", "alpha-3": "NPL", "numeric": "0524"}, {"country": "Nepal", "alpha-2": "NP", "alpha-3": "NPL", "numeric": "0524"},
{"country": "Netherlands (the)", "alpha-2": "NL", "alpha-3": "NLD", "numeric": "0528"}, {
"country": "Netherlands (the)",
"alpha-2": "NL",
"alpha-3": "NLD",
"numeric": "0528",
},
{"country": "New Caledonia", "alpha-2": "NC", "alpha-3": "NCL", "numeric": "0540"}, {"country": "New Caledonia", "alpha-2": "NC", "alpha-3": "NCL", "numeric": "0540"},
{"country": "New Zealand", "alpha-2": "NZ", "alpha-3": "NZL", "numeric": "0554"}, {"country": "New Zealand", "alpha-2": "NZ", "alpha-3": "NZL", "numeric": "0554"},
{"country": "Nicaragua", "alpha-2": "NI", "alpha-3": "NIC", "numeric": "0558"}, {"country": "Nicaragua", "alpha-2": "NI", "alpha-3": "NIC", "numeric": "0558"},
@ -240,32 +315,72 @@ shipping_lookup = [
{"country": "Oman", "alpha-2": "OM", "alpha-3": "OMN", "numeric": "0512"}, {"country": "Oman", "alpha-2": "OM", "alpha-3": "OMN", "numeric": "0512"},
{"country": "Pakistan", "alpha-2": "PK", "alpha-3": "PAK", "numeric": "0586"}, {"country": "Pakistan", "alpha-2": "PK", "alpha-3": "PAK", "numeric": "0586"},
{"country": "Palau", "alpha-2": "PW", "alpha-3": "PLW", "numeric": "0585"}, {"country": "Palau", "alpha-2": "PW", "alpha-3": "PLW", "numeric": "0585"},
{"country": "Palestine, State of", "alpha-2": "PS", "alpha-3": "PSE", "numeric": "0275"}, {
"country": "Palestine, State of",
"alpha-2": "PS",
"alpha-3": "PSE",
"numeric": "0275",
},
{"country": "Panama", "alpha-2": "PA", "alpha-3": "PAN", "numeric": "0591"}, {"country": "Panama", "alpha-2": "PA", "alpha-3": "PAN", "numeric": "0591"},
{"country": "Papua New Guinea", "alpha-2": "PG", "alpha-3": "PNG", "numeric": "0598"}, {
"country": "Papua New Guinea",
"alpha-2": "PG",
"alpha-3": "PNG",
"numeric": "0598",
},
{"country": "Paraguay", "alpha-2": "PY", "alpha-3": "PRY", "numeric": "0600"}, {"country": "Paraguay", "alpha-2": "PY", "alpha-3": "PRY", "numeric": "0600"},
{"country": "Peru", "alpha-2": "PE", "alpha-3": "PER", "numeric": "0604"}, {"country": "Peru", "alpha-2": "PE", "alpha-3": "PER", "numeric": "0604"},
{"country": "Philippines (the)", "alpha-2": "PH", "alpha-3": "PHL", "numeric": "0608"}, {
"country": "Philippines (the)",
"alpha-2": "PH",
"alpha-3": "PHL",
"numeric": "0608",
},
{"country": "Pitcairn", "alpha-2": "PN", "alpha-3": "PCN", "numeric": "0612"}, {"country": "Pitcairn", "alpha-2": "PN", "alpha-3": "PCN", "numeric": "0612"},
{"country": "Poland", "alpha-2": "PL", "alpha-3": "POL", "numeric": "0616"}, {"country": "Poland", "alpha-2": "PL", "alpha-3": "POL", "numeric": "0616"},
{"country": "Portugal", "alpha-2": "PT", "alpha-3": "PRT", "numeric": "0620"}, {"country": "Portugal", "alpha-2": "PT", "alpha-3": "PRT", "numeric": "0620"},
{"country": "Puerto Rico", "alpha-2": "PR", "alpha-3": "PRI", "numeric": "0630"}, {"country": "Puerto Rico", "alpha-2": "PR", "alpha-3": "PRI", "numeric": "0630"},
{"country": "Qatar", "alpha-2": "QA", "alpha-3": "QAT", "numeric": "0634"}, {"country": "Qatar", "alpha-2": "QA", "alpha-3": "QAT", "numeric": "0634"},
{"country": "Réunion", "alpha-2": "RE", "alpha-3": "REU", "numeric": "0638"}, {"country": "Reunion", "alpha-2": "RE", "alpha-3": "REU", "numeric": "0638"},
{"country": "Romania", "alpha-2": "RO", "alpha-3": "ROU", "numeric": "0642"}, {"country": "Romania", "alpha-2": "RO", "alpha-3": "ROU", "numeric": "0642"},
{"country": "Russian Federation (the)", "alpha-2": "RU", "alpha-3": "RUS", "numeric": "0643"}, {
"country": "Russian Federation (the)",
"alpha-2": "RU",
"alpha-3": "RUS",
"numeric": "0643",
},
{"country": "Rwanda", "alpha-2": "RW", "alpha-3": "RWA", "numeric": "0646"}, {"country": "Rwanda", "alpha-2": "RW", "alpha-3": "RWA", "numeric": "0646"},
{"country": "Saint Barthélemy", "alpha-2": "BL", "alpha-3": "BLM", "numeric": "0652"}, {
"country": "Saint Barthelemy",
"alpha-2": "BL",
"alpha-3": "BLM",
"numeric": "0652",
},
{ {
"country": "Saint Helena, Ascension and Tristan da Cunha", "country": "Saint Helena, Ascension and Tristan da Cunha",
"alpha-2": "SH", "alpha-2": "SH",
"alpha-3": "SHN", "alpha-3": "SHN",
"numeric": "0654", "numeric": "0654",
}, },
{"country": "Saint Kitts and Nevis", "alpha-2": "KN", "alpha-3": "KNA", "numeric": "0659"}, {
"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 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 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", "country": "Saint Vincent and the Grenadines",
"alpha-2": "VC", "alpha-2": "VC",
@ -274,17 +389,32 @@ shipping_lookup = [
}, },
{"country": "Samoa", "alpha-2": "WS", "alpha-3": "WSM", "numeric": "0882"}, {"country": "Samoa", "alpha-2": "WS", "alpha-3": "WSM", "numeric": "0882"},
{"country": "San Marino", "alpha-2": "SM", "alpha-3": "SMR", "numeric": "0674"}, {"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": "Sao Tome and Principe",
"alpha-2": "ST",
"alpha-3": "STP",
"numeric": "0678",
},
{"country": "Saudi Arabia", "alpha-2": "SA", "alpha-3": "SAU", "numeric": "0682"}, {"country": "Saudi Arabia", "alpha-2": "SA", "alpha-3": "SAU", "numeric": "0682"},
{"country": "Senegal", "alpha-2": "SN", "alpha-3": "SEN", "numeric": "0686"}, {"country": "Senegal", "alpha-2": "SN", "alpha-3": "SEN", "numeric": "0686"},
{"country": "Serbia", "alpha-2": "RS", "alpha-3": "SRB", "numeric": "0688"}, {"country": "Serbia", "alpha-2": "RS", "alpha-3": "SRB", "numeric": "0688"},
{"country": "Seychelles", "alpha-2": "SC", "alpha-3": "SYC", "numeric": "0690"}, {"country": "Seychelles", "alpha-2": "SC", "alpha-3": "SYC", "numeric": "0690"},
{"country": "Sierra Leone", "alpha-2": "SL", "alpha-3": "SLE", "numeric": "0694"}, {"country": "Sierra Leone", "alpha-2": "SL", "alpha-3": "SLE", "numeric": "0694"},
{"country": "Singapore", "alpha-2": "SG", "alpha-3": "SGP", "numeric": "0702"}, {"country": "Singapore", "alpha-2": "SG", "alpha-3": "SGP", "numeric": "0702"},
{"country": "Sint Maarten (Dutch part)", "alpha-2": "SX", "alpha-3": "SXM", "numeric": "0534"}, {
"country": "Sint Maarten (Dutch part)",
"alpha-2": "SX",
"alpha-3": "SXM",
"numeric": "0534",
},
{"country": "Slovakia", "alpha-2": "SK", "alpha-3": "SVK", "numeric": "0703"}, {"country": "Slovakia", "alpha-2": "SK", "alpha-3": "SVK", "numeric": "0703"},
{"country": "Slovenia", "alpha-2": "SI", "alpha-3": "SVN", "numeric": "0705"}, {"country": "Slovenia", "alpha-2": "SI", "alpha-3": "SVN", "numeric": "0705"},
{"country": "Solomon Islands", "alpha-2": "SB", "alpha-3": "SLB", "numeric": "0090"}, {
"country": "Solomon Islands",
"alpha-2": "SB",
"alpha-3": "SLB",
"numeric": "0090",
},
{"country": "Somalia", "alpha-2": "SO", "alpha-3": "SOM", "numeric": "0706"}, {"country": "Somalia", "alpha-2": "SO", "alpha-3": "SOM", "numeric": "0706"},
{"country": "South Africa", "alpha-2": "ZA", "alpha-3": "ZAF", "numeric": "0710"}, {"country": "South Africa", "alpha-2": "ZA", "alpha-3": "ZAF", "numeric": "0710"},
{ {
@ -298,11 +428,26 @@ shipping_lookup = [
{"country": "Sri Lanka", "alpha-2": "LK", "alpha-3": "LKA", "numeric": "0144"}, {"country": "Sri Lanka", "alpha-2": "LK", "alpha-3": "LKA", "numeric": "0144"},
{"country": "Sudan (the)", "alpha-2": "SD", "alpha-3": "SDN", "numeric": "0729"}, {"country": "Sudan (the)", "alpha-2": "SD", "alpha-3": "SDN", "numeric": "0729"},
{"country": "Suriname", "alpha-2": "SR", "alpha-3": "SUR", "numeric": "0740"}, {"country": "Suriname", "alpha-2": "SR", "alpha-3": "SUR", "numeric": "0740"},
{"country": "Svalbard and Jan Mayen", "alpha-2": "SJ", "alpha-3": "SJM", "numeric": "0744"}, {
"country": "Svalbard and Jan Mayen",
"alpha-2": "SJ",
"alpha-3": "SJM",
"numeric": "0744",
},
{"country": "Sweden", "alpha-2": "SE", "alpha-3": "SWE", "numeric": "0752"}, {"country": "Sweden", "alpha-2": "SE", "alpha-3": "SWE", "numeric": "0752"},
{"country": "Switzerland", "alpha-2": "CH", "alpha-3": "CHE", "numeric": "0756"}, {"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": "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": "Tajikistan", "alpha-2": "TJ", "alpha-3": "TJK", "numeric": "0762"},
{ {
"country": "Tanzania, United Republic of", "country": "Tanzania, United Republic of",
@ -315,7 +460,12 @@ shipping_lookup = [
{"country": "Togo", "alpha-2": "TG", "alpha-3": "TGO", "numeric": "0768"}, {"country": "Togo", "alpha-2": "TG", "alpha-3": "TGO", "numeric": "0768"},
{"country": "Tokelau", "alpha-2": "TK", "alpha-3": "TKL", "numeric": "0772"}, {"country": "Tokelau", "alpha-2": "TK", "alpha-3": "TKL", "numeric": "0772"},
{"country": "Tonga", "alpha-2": "TO", "alpha-3": "TON", "numeric": "0776"}, {"country": "Tonga", "alpha-2": "TO", "alpha-3": "TON", "numeric": "0776"},
{"country": "Trinidad and Tobago", "alpha-2": "TT", "alpha-3": "TTO", "numeric": "0780"}, {
"country": "Trinidad and Tobago",
"alpha-2": "TT",
"alpha-3": "TTO",
"numeric": "0780",
},
{"country": "Tunisia", "alpha-2": "TN", "alpha-3": "TUN", "numeric": "0788"}, {"country": "Tunisia", "alpha-2": "TN", "alpha-3": "TUN", "numeric": "0788"},
{"country": "Turkey", "alpha-2": "TR", "alpha-3": "TUR", "numeric": "0792"}, {"country": "Turkey", "alpha-2": "TR", "alpha-3": "TUR", "numeric": "0792"},
{"country": "Turkmenistan", "alpha-2": "TM", "alpha-3": "TKM", "numeric": "0795"}, {"country": "Turkmenistan", "alpha-2": "TM", "alpha-3": "TKM", "numeric": "0795"},
@ -328,7 +478,12 @@ shipping_lookup = [
{"country": "Tuvalu", "alpha-2": "TV", "alpha-3": "TUV", "numeric": "0798"}, {"country": "Tuvalu", "alpha-2": "TV", "alpha-3": "TUV", "numeric": "0798"},
{"country": "Uganda", "alpha-2": "UG", "alpha-3": "UGA", "numeric": "0800"}, {"country": "Uganda", "alpha-2": "UG", "alpha-3": "UGA", "numeric": "0800"},
{"country": "Ukraine", "alpha-2": "UA", "alpha-3": "UKR", "numeric": "0804"}, {"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 Arab Emirates (the)",
"alpha-2": "AE",
"alpha-3": "ARE",
"numeric": "0784",
},
{ {
"country": "United Kingdom of Great Britain and Northern Ireland (the)", "country": "United Kingdom of Great Britain and Northern Ireland (the)",
"alpha-2": "GB", "alpha-2": "GB",
@ -357,258 +512,26 @@ shipping_lookup = [
"numeric": "0862", "numeric": "0862",
}, },
{"country": "Viet Nam", "alpha-2": "VN", "alpha-3": "VNM", "numeric": "0704"}, {"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": "Virgin Islands (British)",
{"country": "Wallis and Futuna", "alpha-2": "WF", "alpha-3": "WLF", "numeric": "0876"}, "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": "Western Sahara", "alpha-2": "EH", "alpha-3": "ESH", "numeric": "0732"},
{"country": "Yemen", "alpha-2": "YE", "alpha-3": "YEM", "numeric": "0887"}, {"country": "Yemen", "alpha-2": "YE", "alpha-3": "YEM", "numeric": "0887"},
{"country": "Zambia", "alpha-2": "ZM", "alpha-3": "ZMB", "numeric": "0894"}, {"country": "Zambia", "alpha-2": "ZM", "alpha-3": "ZMB", "numeric": "0894"},
{"country": "Zimbabwe", "alpha-2": "ZW", "alpha-3": "ZWE", "numeric": "0716"}, {"country": "Zimbabwe", "alpha-2": "ZW", "alpha-3": "ZWE", "numeric": "0716"},
] ]
# TODO: Implement lookup for this. Currently not doable
how_to_array = [
"https://dbrand.com/how-to-apply/airpower",
"https://dbrand.com/how-to-apply/alienware-13-r3",
"https://dbrand.com/how-to-apply/alienware-15-r3",
"https://dbrand.com/how-to-apply/alienware-17-r4-eye-tracking",
"https://dbrand.com/how-to-apply/alienware-17-r4-no-eye-tracking",
"https://dbrand.com/how-to-apply/alienware-17-r5-eye-tracking",
"https://dbrand.com/how-to-apply/alienware-17-r5-no-eye-tracking",
"https://dbrand.com/how-to-apply/anker-powercore-13000-usb-c",
"https://dbrand.com/how-to-apply/anker-powercore-plus-20100-usb-c",
"https://dbrand.com/how-to-apply/anker-powercore-plus-26800-pd",
"https://dbrand.com/how-to-apply/anker-powercore-slim-10000-pd",
"https://dbrand.com/how-to-apply/apple-18w-usb-c-power-adapter",
"https://dbrand.com/how-to-apply/apple-5w-usb-power-adapter",
"https://dbrand.com/how-to-apply/apple-airpods-gen-1",
"https://dbrand.com/how-to-apply/apple-airpods-gen-2-no-wireless-charging",
"https://dbrand.com/how-to-apply/apple-airpods-gen-2-wireless-charging",
"https://dbrand.com/how-to-apply/apple-airpods-pro",
"https://dbrand.com/how-to-apply/apple-card",
"https://dbrand.com/how-to-apply/apple-pencil",
"https://dbrand.com/how-to-apply/apple-pencil-2",
"https://dbrand.com/how-to-apply/axon-7",
"https://dbrand.com/how-to-apply/blade-14-2014-2016-gtx-970m",
"https://dbrand.com/how-to-apply/blade-14-2016-2017-gtx-1060",
"https://dbrand.com/how-to-apply/blade-stealth-125-early-2016-skylake",
"https://dbrand.com/how-to-apply/blade-stealth-125-late-2016-2017-kaby-lake",
"https://dbrand.com/how-to-apply/blade-stealth-13.3-2017-2018",
"https://dbrand.com/how-to-apply/blade-stealth-13.3-early-2019",
"https://dbrand.com/how-to-apply/blade-stealth-13.3-late-2019",
"https://dbrand.com/how-to-apply/dell-xps-13-2-in-1-7390",
"https://dbrand.com/how-to-apply/dell-xps-13-2-in-1-9365",
"https://dbrand.com/how-to-apply/dell-xps-13-7390",
"https://dbrand.com/how-to-apply/dell-xps-13-9350-9360",
"https://dbrand.com/how-to-apply/dell-xps-15-9550",
"https://dbrand.com/how-to-apply/dell-xps-15-9560",
"https://dbrand.com/how-to-apply/eluktronics-mag-15",
"https://dbrand.com/how-to-apply/essential-phone",
"https://dbrand.com/how-to-apply/eve-v",
"https://dbrand.com/how-to-apply/galaxy-a50",
"https://dbrand.com/how-to-apply/galaxy-a70",
"https://dbrand.com/how-to-apply/galaxy-buds",
"https://dbrand.com/how-to-apply/galaxy-fold",
"https://dbrand.com/how-to-apply/galaxy-note-10",
"https://dbrand.com/how-to-apply/galaxy-note-10-plus",
"https://dbrand.com/how-to-apply/galaxy-note-10-plus-5g",
"https://dbrand.com/how-to-apply/galaxy-note-4",
"https://dbrand.com/how-to-apply/galaxy-note-5",
"https://dbrand.com/how-to-apply/galaxy-note-7",
"https://dbrand.com/how-to-apply/galaxy-note-8",
"https://dbrand.com/how-to-apply/galaxy-note-9",
"https://dbrand.com/how-to-apply/galaxy-note-fe",
"https://dbrand.com/how-to-apply/galaxy-s10",
"https://dbrand.com/how-to-apply/galaxy-s10-5g",
"https://dbrand.com/how-to-apply/galaxy-s10e",
"https://dbrand.com/how-to-apply/galaxy-s10-plus",
"https://dbrand.com/how-to-apply/galaxy-s6",
"https://dbrand.com/how-to-apply/galaxy-s6-active",
"https://dbrand.com/how-to-apply/galaxy-s6-edge",
"https://dbrand.com/how-to-apply/galaxy-s6-edge-plus",
"https://dbrand.com/how-to-apply/galaxy-s7",
"https://dbrand.com/how-to-apply/galaxy-s7-active",
"https://dbrand.com/how-to-apply/galaxy-s7-edge",
"https://dbrand.com/how-to-apply/galaxy-s8",
"https://dbrand.com/how-to-apply/galaxy-s8-active",
"https://dbrand.com/how-to-apply/galaxy-s8-plus",
"https://dbrand.com/how-to-apply/galaxy-s9",
"https://dbrand.com/how-to-apply/galaxy-s9-plus",
"https://dbrand.com/how-to-apply/google-home",
"https://dbrand.com/how-to-apply/honor-8",
"https://dbrand.com/how-to-apply/htc-10",
"https://dbrand.com/how-to-apply/htc-one-m7",
"https://dbrand.com/how-to-apply/htc-one-m8",
"https://dbrand.com/how-to-apply/htc-one-m9",
"https://dbrand.com/how-to-apply/htc-u-ultra",
"https://dbrand.com/how-to-apply/huawei-mate-10",
"https://dbrand.com/how-to-apply/huawei-mate-10-pro",
"https://dbrand.com/how-to-apply/huawei-mate-20",
"https://dbrand.com/how-to-apply/huawei-mate-20-pro",
"https://dbrand.com/how-to-apply/huawei-mate-30-pro",
"https://dbrand.com/how-to-apply/huawei-matebook-x-pro-2018",
"https://dbrand.com/how-to-apply/huawei-matebook-x-pro-2019",
"https://dbrand.com/how-to-apply/huawei-p10",
"https://dbrand.com/how-to-apply/huawei-p10-plus",
"https://dbrand.com/how-to-apply/huawei-p20",
"https://dbrand.com/how-to-apply/huawei-p20-pro",
"https://dbrand.com/how-to-apply/huawei-p30",
"https://dbrand.com/how-to-apply/huawei-p30-pro",
"https://dbrand.com/how-to-apply/huawei-p9",
"https://dbrand.com/how-to-apply/intel-nuc-mainstream-mini-pc",
"https://dbrand.com/how-to-apply/ipad-10.2-2019-gen-7",
"https://dbrand.com/how-to-apply/ipad-9.7-skins-2017-2018",
"https://dbrand.com/how-to-apply/ipad-air-2",
"https://dbrand.com/how-to-apply/ipad-air-3",
"https://dbrand.com/how-to-apply/ipad-mini-4",
"https://dbrand.com/how-to-apply/ipad-mini-5",
"https://dbrand.com/how-to-apply/ipad-pro-105",
"https://dbrand.com/how-to-apply/ipad-pro-11",
"https://dbrand.com/how-to-apply/ipad-pro-12.9-2018-gen-3",
"https://dbrand.com/how-to-apply/ipad-pro-129-2016-gen-1",
"https://dbrand.com/how-to-apply/ipad-pro-129-2017-gen-2",
"https://dbrand.com/how-to-apply/ipad-pro-97-2016",
"https://dbrand.com/how-to-apply/iphone-11",
"https://dbrand.com/how-to-apply/iphone-11-pro",
"https://dbrand.com/how-to-apply/iphone-11-pro-max",
"https://dbrand.com/how-to-apply/iphone-4-4s",
"https://dbrand.com/how-to-apply/iphone-5",
"https://dbrand.com/how-to-apply/iphone-5s",
"https://dbrand.com/how-to-apply/iphone-6",
"https://dbrand.com/how-to-apply/iphone-6-plus",
"https://dbrand.com/how-to-apply/iphone-6s",
"https://dbrand.com/how-to-apply/iphone-6s-plus",
"https://dbrand.com/how-to-apply/iphone-7",
"https://dbrand.com/how-to-apply/iphone-7-plus",
"https://dbrand.com/how-to-apply/iphone-8",
"https://dbrand.com/how-to-apply/iphone-8-plus",
"https://dbrand.com/how-to-apply/iphone-se",
"https://dbrand.com/how-to-apply/iphone-x",
"https://dbrand.com/how-to-apply/iphone-xr",
"https://dbrand.com/how-to-apply/iphone-xs",
"https://dbrand.com/how-to-apply/iphone-xs-max",
"https://dbrand.com/how-to-apply/juul",
"https://dbrand.com/how-to-apply/juul-c1",
"https://dbrand.com/how-to-apply/lenovo-thinkpad-x1-carbon-6th-gen",
"https://dbrand.com/how-to-apply/lenovo-thinkpad-x1-carbon-7th-gen",
"https://dbrand.com/how-to-apply/lg-g3",
"https://dbrand.com/how-to-apply/lg-g4",
"https://dbrand.com/how-to-apply/lg-g5",
"https://dbrand.com/how-to-apply/lg-g6",
"https://dbrand.com/how-to-apply/lg-g7",
"https://dbrand.com/how-to-apply/lg-v20",
"https://dbrand.com/how-to-apply/lg-v30",
"https://dbrand.com/how-to-apply/m40x",
"https://dbrand.com/how-to-apply/m50",
"https://dbrand.com/how-to-apply/m50x",
"https://dbrand.com/how-to-apply/macbook-12-2015-2018-retina",
"https://dbrand.com/how-to-apply/macbook-air-11",
"https://dbrand.com/how-to-apply/macbook-air-13",
"https://dbrand.com/how-to-apply/macbook-air-13-2018-2019",
"https://dbrand.com/how-to-apply/macbook-pro-13-2013-2015-retina",
"https://dbrand.com/how-to-apply/macbook-pro-13-skins-2016-2018-four-thunderbolt",
"https://dbrand.com/how-to-apply/macbook-pro-13-skins-2016-2018-two-thunderbolt",
"https://dbrand.com/how-to-apply/macbook-pro-13-skins-2019-four-thunderbolt",
"https://dbrand.com/how-to-apply/macbook-pro-13-skins-2019-two-thunderbolt",
"https://dbrand.com/how-to-apply/macbook-pro-15-2013-2015-retina",
"https://dbrand.com/how-to-apply/macbook-pro-15-touch-bar",
"https://dbrand.com/how-to-apply/macbook-pro-16-2019",
"https://dbrand.com/how-to-apply/mac-pro-and-pro-display-xdr",
"https://dbrand.com/how-to-apply/maingear-element",
"https://dbrand.com/how-to-apply/moto-g-2013",
"https://dbrand.com/how-to-apply/moto-g-2014",
"https://dbrand.com/how-to-apply/moto-x-2013",
"https://dbrand.com/how-to-apply/moto-x-2014",
"https://dbrand.com/how-to-apply/moto-x4",
"https://dbrand.com/how-to-apply/moto-x-play",
"https://dbrand.com/how-to-apply/moto-x-style-pure",
"https://dbrand.com/how-to-apply/moto-z",
"https://dbrand.com/how-to-apply/moto-z-force",
"https://dbrand.com/how-to-apply/nextbit-robin",
"https://dbrand.com/how-to-apply/nexus-4",
"https://dbrand.com/how-to-apply/nexus-5",
"https://dbrand.com/how-to-apply/nexus-5x",
"https://dbrand.com/how-to-apply/nexus-6",
"https://dbrand.com/how-to-apply/nexus-6p",
"https://dbrand.com/how-to-apply/nexus-7-2012",
"https://dbrand.com/how-to-apply/nexus-7-2013",
"https://dbrand.com/how-to-apply/nexus-9",
"https://dbrand.com/how-to-apply/nintendo-switch",
"https://dbrand.com/how-to-apply/nintendo-switch-lite",
"https://dbrand.com/how-to-apply/nintendo-switch-pro-controller",
"https://dbrand.com/how-to-apply/oneplus-2",
"https://dbrand.com/how-to-apply/oneplus-3",
"https://dbrand.com/how-to-apply/oneplus-3t",
"https://dbrand.com/how-to-apply/oneplus-5",
"https://dbrand.com/how-to-apply/oneplus-5t",
"https://dbrand.com/how-to-apply/oneplus-6",
"https://dbrand.com/how-to-apply/oneplus-6t",
"https://dbrand.com/how-to-apply/oneplus-7",
"https://dbrand.com/how-to-apply/oneplus-7-pro",
"https://dbrand.com/how-to-apply/oneplus-7t",
"https://dbrand.com/how-to-apply/oneplus-7t-pro",
"https://dbrand.com/how-to-apply/oneplus-one",
"https://dbrand.com/how-to-apply/oneplus-x",
"https://dbrand.com/how-to-apply/pebble-time",
"https://dbrand.com/how-to-apply/pebble-watch",
"https://dbrand.com/how-to-apply/pixel",
"https://dbrand.com/how-to-apply/pixel-2",
"https://dbrand.com/how-to-apply/pixel-2-xl",
"https://dbrand.com/how-to-apply/pixel-3",
"https://dbrand.com/how-to-apply/pixel-3a",
"https://dbrand.com/how-to-apply/pixel-3a-xl",
"https://dbrand.com/how-to-apply/pixel-3-xl",
"https://dbrand.com/how-to-apply/pixel-4",
"https://dbrand.com/how-to-apply/pixel-4-xl",
"https://dbrand.com/how-to-apply/pixelbook",
"https://dbrand.com/how-to-apply/pixelbook-go",
"https://dbrand.com/how-to-apply/pixel-xl",
"https://dbrand.com/how-to-apply/playstation-3",
"https://dbrand.com/how-to-apply/playstation-4",
"https://dbrand.com/how-to-apply/playstation-4-pro",
"https://dbrand.com/how-to-apply/playstation-4-slim",
"https://dbrand.com/how-to-apply/playstation-vita",
"https://dbrand.com/how-to-apply/pocophone-f1",
"https://dbrand.com/how-to-apply/razer-blade-15.6-skins-2018-advanced-no-ethernet-gtx",
"https://dbrand.com/how-to-apply/razer-blade-15.6-skins-2018-base-with-ethernet-gtx",
"https://dbrand.com/how-to-apply/razer-blade-15.6-skins-2019-advanced-no-ethernet-rtx",
"https://dbrand.com/how-to-apply/razer-blade-pro-17-2019",
"https://dbrand.com/how-to-apply/razer-phone",
"https://dbrand.com/how-to-apply/razer-phone-2",
"https://dbrand.com/how-to-apply/redmi-k20",
"https://dbrand.com/how-to-apply/redmi-k20-pro",
"https://dbrand.com/how-to-apply/surface-book",
"https://dbrand.com/how-to-apply/surface-book-2-13",
"https://dbrand.com/how-to-apply/surface-book-2-15",
"https://dbrand.com/how-to-apply/surface-go",
"https://dbrand.com/how-to-apply/surface-laptop",
"https://dbrand.com/how-to-apply/surface-laptop-2",
"https://dbrand.com/how-to-apply/surface-laptop-3-13",
"https://dbrand.com/how-to-apply/surface-laptop-3-15",
"https://dbrand.com/how-to-apply/surface-pro-2017",
"https://dbrand.com/how-to-apply/surface-pro-4",
"https://dbrand.com/how-to-apply/surface-pro-6",
"https://dbrand.com/how-to-apply/surface-pro-7",
"https://dbrand.com/how-to-apply/surface-pro-x",
"https://dbrand.com/how-to-apply/tesla-cybertruck",
"https://dbrand.com/how-to-apply/xbox-360",
"https://dbrand.com/how-to-apply/xbox-one",
"https://dbrand.com/how-to-apply/xbox-one-s",
"https://dbrand.com/how-to-apply/xbox-one-x",
"https://dbrand.com/how-to-apply/xiaomi-mi-9t",
"https://dbrand.com/how-to-apply/xiaomi-mi-9t-pro",
"https://dbrand.com/how-to-apply/xperia-z1",
"https://dbrand.com/how-to-apply/xperia-z2",
"https://dbrand.com/how-to-apply/xperia-z3",
"https://dbrand.com/how-to-apply/xperia-z3-compact",
"https://dbrand.com/how-to-apply/xperia-z5",
"https://dbrand.com/how-to-apply/xperia-z5-compact",
"https://dbrand.com/how-to-apply/xperia-z5-premium",
"https://dbrand.com/how-to-apply/xperia-z-ultra",
"https://dbrand.com/how-to-apply/xps-13-9370",
"https://dbrand.com/how-to-apply/xps-13-9380",
"https://dbrand.com/how-to-apply/xps-15-2-in-1-9575",
"https://dbrand.com/how-to-apply/xps-15-7590",
"https://dbrand.com/how-to-apply/xps-15-9570",
"https://dbrand.com/how-to-apply/zenfone-2",
]

View file

@ -45,7 +45,7 @@ def modlog_embed(
fields = [ fields = [
EmbedField( EmbedField(
name="Moderator", name="Moderator",
value=f"{admin.mention} ({admin.username}#{admin.discriminator})", value=f"{admin.mention} ({admin.username})",
), ),
] ]
if log and log.reason: if log and log.reason:
@ -59,7 +59,7 @@ def modlog_embed(
timestamp=log.created_at, timestamp=log.created_at,
) )
embed.set_author(name=f"{member.username}", icon_url=member.display_avatar.url) embed.set_author(name=f"{member.username}", icon_url=member.display_avatar.url)
embed.set_footer(text=f"{member.username}#{member.discriminator} | {member.id}") embed.set_footer(text=f"{member.username} | {member.id}")
return embed return embed

View file

@ -68,19 +68,31 @@ class ModcaseCog(Extension):
return return
action = await coll.find_one( action = await coll.find_one(
coll.user == user.id, coll.guild == ctx.guild.id, coll.active == True, sort=[("_id", -1)] coll.user == user.id,
coll.guild == ctx.guild.id,
coll.active == True,
sort=[("_id", -1)],
) )
if not action: if not action:
self.logger.warning("Missing action %s, exiting", name) self.logger.warning("Missing action %s, exiting", name)
return return
notify = await Setting.find_one( notify = await Setting.find_one(
Setting.guild == ctx.guild.id, Setting.setting == "notify", Setting.value == True Setting.guild == ctx.guild.id,
Setting.setting == "notify",
Setting.value == True,
) )
if notify and name not in ("Kick", "Ban"): # Ignore Kick and Ban, as these are unique if notify and name not in (
"Kick",
"Ban",
): # Ignore Kick and Ban, as these are unique
fields = ( fields = (
EmbedField(name="Action Type", value=name, inline=False), EmbedField(name="Action Type", value=name, inline=False),
EmbedField(name="Reason", value=kwargs.get("reason", None) or "N/A", inline=False), EmbedField(
name="Reason",
value=kwargs.get("reason", None) or "N/A",
inline=False,
),
) )
embed = build_embed( embed = build_embed(
title="Admin action taken", title="Admin action taken",
@ -89,16 +101,24 @@ class ModcaseCog(Extension):
) )
if name == "Mute": if name == "Mute":
mts = int(user.communication_disabled_until.timestamp()) mts = int(user.communication_disabled_until.timestamp())
embed.add_field(name="Muted Until", value=f"<t:{mts}:F> (<t:{mts}:R>)") embed.add_field(
name="Muted Until", value=f"<t:{mts}:F> (<t:{mts}:R>)"
)
guild_url = f"https://discord.com/channels/{ctx.guild.id}" guild_url = f"https://discord.com/channels/{ctx.guild.id}"
embed.set_author(name=ctx.guild.name, icon_url=ctx.guild.icon.url, url=guild_url) embed.set_author(
name=ctx.guild.name, icon_url=ctx.guild.icon.url, url=guild_url
)
embed.set_thumbnail(url=ctx.guild.icon.url) embed.set_thumbnail(url=ctx.guild.icon.url)
try: try:
await user.send(embeds=embed) await user.send(embeds=embed)
except Exception: except Exception:
self.logger.debug("User not warned of action due to closed DMs") self.logger.debug("User not warned of action due to closed DMs")
modlog = await Modlog.find_one(Modlog.user == user.id, Modlog.guild == ctx.guild.id, Modlog.open == True) modlog = await Modlog.find_one(
Modlog.user == user.id,
Modlog.guild == ctx.guild.id,
Modlog.open == True,
)
if modlog: if modlog:
m_action = Action(action_type=name.lower(), parent=action.id) m_action = Action(action_type=name.lower(), parent=action.id)
@ -106,7 +126,9 @@ class ModcaseCog(Extension):
await modlog.save() await modlog.save()
return return
modlog = await Setting.find_one(Setting.guild == ctx.guild.id, Setting.setting == "modlog") modlog = await Setting.find_one(
Setting.guild == ctx.guild.id, Setting.setting == "modlog"
)
if not modlog: if not modlog:
return return
@ -114,7 +136,11 @@ class ModcaseCog(Extension):
if channel: if channel:
fields = ( fields = (
EmbedField(name="Action Type", value=name, inline=False), EmbedField(name="Action Type", value=name, inline=False),
EmbedField(name="Reason", value=kwargs.get("reason", None) or "N/A", inline=False), EmbedField(
name="Reason",
value=kwargs.get("reason", None) or "N/A",
inline=False,
),
EmbedField(name="Admin", value=ctx.author.mention, inline=False), EmbedField(name="Admin", value=ctx.author.mention, inline=False),
) )
embed = build_embed( embed = build_embed(
@ -122,23 +148,31 @@ class ModcaseCog(Extension):
description=f"Admin action has been taken against {user.mention}", description=f"Admin action has been taken against {user.mention}",
fields=fields, fields=fields,
) )
embed.set_author(name=f"{user.username}#{user.discriminator}", icon_url=user.display_avatar.url) embed.set_author(
name=f"{user.username}", icon_url=user.display_avatar.url
)
embed.set_footer(text=f"User ID: {user.id}") embed.set_footer(text=f"User ID: {user.id}")
if name == "Mute": if name == "Mute":
mts = int(user.communication_disabled_until.timestamp()) mts = int(user.communication_disabled_until.timestamp())
embed.add_field(name="Muted Until", value=f"<t:{mts}:F> (<t:{mts}:R>)") embed.add_field(
name="Muted Until", value=f"<t:{mts}:F> (<t:{mts}:R>)"
)
await channel.send(embeds=embed) await channel.send(embeds=embed)
lookup_key = f"{user.id}|{ctx.guild.id}" lookup_key = f"{user.id}|{ctx.guild.id}"
async with self.bot.redis.lock("lock|" + lookup_key): async with self.bot.redis.lock("lock|" + lookup_key):
if await self.bot.redis.get(lookup_key): if await self.bot.redis.get(lookup_key):
self.logger.debug(f"User {user.id} in {ctx.guild.id} already has pending case") self.logger.debug(
f"User {user.id} in {ctx.guild.id} already has pending case"
)
return return
channel = await ctx.guild.fetch_channel(modlog.value) channel = await ctx.guild.fetch_channel(modlog.value)
if not channel: if not channel:
self.logger.warn(f"Guild {ctx.guild.id} modlog channel no longer exists, deleting") self.logger.warn(
f"Guild {ctx.guild.id} modlog channel no longer exists, deleting"
)
await modlog.delete() await modlog.delete()
return return
@ -150,14 +184,22 @@ class ModcaseCog(Extension):
avatar_url = user.avatar.url avatar_url = user.avatar.url
if isinstance(user, Member): if isinstance(user, Member):
avatar_url = user.display_avatar.url avatar_url = user.display_avatar.url
embed.set_author(name=user.username + "#" + user.discriminator, icon_url=avatar_url) embed.set_author(name=user.username, icon_url=avatar_url)
components = [ components = [
ActionRow( ActionRow(
Button(style=ButtonStyle.RED, emoji="✖️", custom_id="modcase|no"), Button(
Button(style=ButtonStyle.GREEN, emoji="✔️", custom_id="modcase|yes"), style=ButtonStyle.RED, emoji="✖️", custom_id="modcase|no"
),
Button(
style=ButtonStyle.GREEN, emoji="✔️", custom_id="modcase|yes"
),
) )
] ]
message = await channel.send(embeds=embed, components=components) message = await channel.send(embeds=embed, components=components)
await self.bot.redis.set(lookup_key, f"{name.lower()}|{action.id}", ex=timedelta(days=7)) await self.bot.redis.set(
await self.bot.redis.set(f"msg|{message.id}", user.id, ex=timedelta(days=7)) lookup_key, f"{name.lower()}|{action.id}", ex=timedelta(days=7)
)
await self.bot.redis.set(
f"msg|{message.id}", user.id, ex=timedelta(days=7)
)

1612
poetry.lock generated

File diff suppressed because it is too large Load diff

View file

@ -5,7 +5,7 @@ description = "JARVIS admin bot"
authors = ["Zevaryx <zevaryx@gmail.com>"] authors = ["Zevaryx <zevaryx@gmail.com>"]
[tool.poetry.dependencies] [tool.poetry.dependencies]
python = ">=3.10,<4" python = ">=3.11,<4"
PyYAML = "^6.0" PyYAML = "^6.0"
GitPython = "^3.1.26" GitPython = "^3.1.26"
opencv-python = "^4.5.5" opencv-python = "^4.5.5"
@ -14,7 +14,7 @@ psutil = "^5.9.0"
python-gitlab = "^3.1.1" python-gitlab = "^3.1.1"
ulid-py = "^1.1.0" ulid-py = "^1.1.0"
tweepy = "^4.5.0" tweepy = "^4.5.0"
jarvis-core = {git = "https://git.zevaryx.com/stark-industries/jarvis/jarvis-core.git", rev = "main"} # Mine jarvis-core = { git = "https://git.zevaryx.com/stark-industries/jarvis/jarvis-core.git", rev = "beanie" } # Mine
aiohttp = "^3.8.3" aiohttp = "^3.8.3"
pastypy = "^1.0.3.post1" # Mine pastypy = "^1.0.3.post1" # Mine
dateparser = "^1.1.1" dateparser = "^1.1.1"
@ -24,15 +24,19 @@ rich = "^12.3.0"
jurigged = "^0.5.3" # Contributed jurigged = "^0.5.3" # Contributed
ansitoimg = "^2022.1" ansitoimg = "^2022.1"
nest-asyncio = "^1.5.5" nest-asyncio = "^1.5.5"
thefuzz = {extras = ["speedup"], git = "https://github.com/zevaryx/thefuzz.git", rev = "master"} # Forked thefuzz = { extras = [
"speedup",
], git = "https://github.com/zevaryx/thefuzz.git", rev = "master" } # Forked
beautifulsoup4 = "^4.11.1" beautifulsoup4 = "^4.11.1"
calculator = {git = "https://git.zevaryx.com/zevaryx/calculator.git"} # Mine calculator = { git = "https://git.zevaryx.com/zevaryx/calculator.git" } # Mine
redis = "^4.4.0" redis = "^4.4.0"
interactions = {git = "https://github.com/interactions-py/interactions.py", rev = "5.x"} interactions-py = ">=5.3,<6"
statipy = {git = "https://github.com/zevaryx/statipy", rev = "main"} statipy = { git = "https://github.com/zevaryx/statipy", rev = "main" }
beanie = "^1.17.0" beanie = "^1.17.0"
pydantic = "^1.10.7" pydantic = ">=2.3.0,<3"
orjson = "^3.8.8" orjson = "^3.8.8"
croniter = "^1.4.1"
erapi = { git = "https://git.zevaryx.com/zevaryx-technologies/erapi.git" }
[tool.poetry.group.dev.dependencies] [tool.poetry.group.dev.dependencies]
pre-commit = "^2.21.0" pre-commit = "^2.21.0"

38
sample.env Normal file
View file

@ -0,0 +1,38 @@
# Base Config, required
TOKEN=
# Base Config, optional
ENVIRONMENT=develop
SYNC=false
LOG_LEVEL=INFO
JURIGGED=false
# MongoDB, required
MONGO_HOST=localhost
MONGO_USERNAME=
MONGO_PASSWORD=
MONGO_PORT=27017
# Redis, required
REDIS_HOST=localhost
REDIS_USERNAME=
REDIS_PASSWORD=
# Mastodon, optional
MASTODON_TOKEN=
MASTODON_URL=
# Reddit, optional
REDDIT_USER_AGENT=
REDDIT_CLIENT_SECRET=
REDDIT_CLIENT_ID=
# Twitter, optional
TWITTER_CONSUMER_KEY=
TWITTER_CONSUMER_SECRET=
TWITTER_ACCESS_TOKEN=
TWITTER_ACCESS_SECRET=
TWITTER_BEARER_TOKEN=
# URLs, optional
URL_DBRAND=