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
!/run.py
!/config.yaml
# Needed for jarvis-compose
!/.git
# Block other files
**/__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">
# Just Another Rather Very Intelligent System
<br />
<br />
[![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)
@ -12,28 +12,22 @@
[![ko-fi](https://www.ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/zevaryx)
</div>
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.
Welcome to the JARVIS Initiative, an open-source multi-purpose bot
## Features
JARVIS currently offers:
- 👩‍💼 **Administration**: `verify`, `ban/unban`, `kick`, `purge`, `mute/unmute` and 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!
- 🏷️ **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!
- 👩‍💼 **Administration**: `verify`, `ban/unban`, `kick`, `purge`, `mute/unmute` and more!
- 🚓 **Moderation**: `lock/unlock`, `lockdown`, `warn`, `autoreact`, and also more!
- 🔧 **Utilities**: `remindme`, `rolegiver`, `temprole`, `image`, and so many more!
- 🏷️ **Tags**: Custom `tag`s! Useful for custom messages without the hassle!
## Contributing
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 wish to contribute to the JARVIS codebase or documentation, join the Discord! The recognized developers there will help you get started.
## 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.
## Requirements
- MongoDB 5.0 or higher
- MongoDB 6.0 or higher
- Python 3.10 or higher
- [tokei](https://github.com/XAMPPRocky/tokei) 12.1 or higher
- Everything in `requirements.txt`
## JARVIS Cogs
Current cogs that are implemented:
@ -57,10 +51,6 @@ Current cogs that are implemented:
- Handles autoreaction configuration
- `BotutilCog`
- Handles internal bot utilities (private use only)
- `CTC2Cog`
- dbrand Complete the Code utilities
- `DbrandCog`
- dbrand-specific functions and utilities
- `DevCog`
- Developer utilities, such as hashing, encoding, and UUID generation
- `GitlabCog`
@ -88,7 +78,6 @@ Current cogs that are implemented:
- `VerifyCog`
- Guild verification
## Directories
### `jarvis`
@ -102,6 +91,7 @@ All of the cogs listed above are stored in this directory
##### `jarvis.cogs.admin`
Contains all AdminCogs, including:
- `BanCog`
- `KickCog`
- `LockCog`

View file

@ -61,18 +61,31 @@ async def run() -> None:
config = load_config()
logger = get_logger("jarvis", show_locals=False) # jconfig.log_level == "DEBUG")
logger.setLevel(config.log_level)
file_handler = logging.FileHandler(filename="jarvis.log", encoding="UTF-8", mode="w")
file_handler.setFormatter(logging.Formatter("[%(asctime)s] [%(name)s] [%(levelname)8s] %(message)s"))
file_handler = logging.FileHandler(
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)
# 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_host = redis_config.pop("host")
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(
intents=intents,
@ -81,6 +94,7 @@ async def run() -> None:
send_command_tracebacks=False,
redis=redis,
logger=logger,
erapi=config.erapi,
)
# External modules

View file

@ -2,6 +2,7 @@
import logging
from typing import TYPE_CHECKING
from erapi import ERAPI
from interactions.ext.prefixed_commands.context import PrefixedContext
from interactions.models.internal.context import BaseContext, InteractionContext
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):
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)
self.redis = redis
self.logger = logging.getLogger(__name__)
self.phishing_domains = []
self.pre_run_callback = self._prerun
self.synced = False
self.erapi = ERAPI(erapi)
async def _prerun(self, ctx: BaseContext, *args, **kwargs) -> None:
name = ctx.invoke_target

View file

@ -2,6 +2,8 @@
import traceback
from datetime import datetime, timezone
from interactions import listen
from interactions.api.events import Error
from interactions.client.errors import (
CommandCheckFailure,
CommandOnCooldown,
@ -31,16 +33,24 @@ Callback:
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."""
source = event.source
error = event.error
if isinstance(error, HTTPException):
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)
else:
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."""
name = ctx.invoke_target
self.logger.debug(f"Handling error in {name}: {error}")
@ -70,7 +80,11 @@ class ErrorMixin:
v = v[97] + "..."
arg_str += f" - {v}"
callback_args = "\n".join(f" - {i}" for i in args) if args else " None"
callback_kwargs = "\n".join(f" {k}: {v}" for k, v in kwargs.items()) if kwargs else " None"
callback_kwargs = (
"\n".join(f" {k}: {v}" for k, v in kwargs.items())
if kwargs
else " None"
)
full_message = ERROR_MSG.format(
guild_name=ctx.guild.name,
error_time=error_time,
@ -82,7 +96,11 @@ class ErrorMixin:
tb = traceback.format_exception(error)
if isinstance(error, HTTPException):
errors = error.search_for_message(error.errors)
tb[-1] = f"HTTPException: {error.status}|{error.response.reason}: " + "\n".join(errors)
tb[
-1
] = f"HTTPException: {error.status}|{error.response.reason}: " + "\n".join(
errors
)
error_message = "".join(traceback.format_exception(error))
if len(full_message + error_message) >= 1800:
error_message = "\n ".join(error_message.split("\n"))
@ -101,7 +119,9 @@ class ErrorMixin:
f"\n```yaml\n{full_message}\n```"
f"\nException:\n```py\n{error_message}\n```"
)
await ctx.send("Whoops! Encountered an error. The error has been logged.", ephemeral=True)
await ctx.send(
"Whoops! Encountered an error. The error has been logged.", ephemeral=True
)
try:
await ctx.defer(ephemeral=True)
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:
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.raise_for_status()
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()
async def on_startup(self) -> None:
"""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(
{
StaticStat.client_name: self.client_name,
@ -67,7 +73,9 @@ class EventMixin(MemberEventMixin, MessageEventMixin, ComponentEventMixin):
except Exception as e:
self.logger.error("Failed to load anti-phishing", exc_info=e)
self.logger.info("Logged in as {}".format(self.user)) # noqa: T001
self.logger.info("Connected to {} guild(s)".format(len(self.guilds))) # noqa: T001
self.logger.info(
"Connected to {} guild(s)".format(len(self.guilds))
) # noqa: T001
self.logger.info("Current version: {}".format(const.__version__))
self.logger.info( # noqa: T001
"https://discord.com/api/oauth2/authorize?client_id="
@ -87,7 +95,9 @@ class EventMixin(MemberEventMixin, MessageEventMixin, ComponentEventMixin):
if not isinstance(self.interaction_tree[cid][_], ContextMenu)
)
global_context_menus = sum(
1 for _ in self.interaction_tree[cid] if isinstance(self.interaction_tree[cid][_], ContextMenu)
1
for _ in self.interaction_tree[cid]
if isinstance(self.interaction_tree[cid][_], ContextMenu)
)
else:
guild_base_commands += sum(
@ -96,25 +106,42 @@ class EventMixin(MemberEventMixin, MessageEventMixin, ComponentEventMixin):
if not isinstance(self.interaction_tree[cid][_], ContextMenu)
)
guild_context_menus += sum(
1 for _ in self.interaction_tree[cid] if isinstance(self.interaction_tree[cid][_], ContextMenu)
1
for _ in self.interaction_tree[cid]
if isinstance(self.interaction_tree[cid][_], ContextMenu)
)
self.logger.info("Loaded {:>3} global base slash commands".format(global_base_commands))
self.logger.info("Loaded {:>3} global context menus".format(global_context_menus))
self.logger.info("Loaded {:>3} guild base slash commands".format(guild_base_commands))
self.logger.info("Loaded {:>3} guild context menus".format(guild_context_menus))
self.logger.info(
"Loaded {:>3} global base slash commands".format(global_base_commands)
)
self.logger.info(
"Loaded {:>3} global context menus".format(global_context_menus)
)
self.logger.info(
"Loaded {:>3} guild base slash commands".format(guild_base_commands)
)
self.logger.info(
"Loaded {:>3} guild context menus".format(guild_context_menus)
)
except Exception:
self.logger.error("interaction_tree not found, try updating NAFF")
self.logger.debug(self.interaction_tree)
self.logger.debug("Hitting Reminders for faster loads")
_ = await Reminder.find().to_list(None)
self.logger.debug("Updating ERAPI")
await self.erapi.update_async()
# Modlog
async def on_command(self, ctx: BaseContext) -> None:
"""NAFF on_command override."""
name = ctx.invoke_target
if not isinstance(ctx.channel, DMChannel) and name not in ["pw"]:
modlog = await Setting.find_one(Setting.guild == ctx.guild.id, Setting.setting == "activitylog")
ignore = await Setting.find_one(Setting.guild == ctx.guild.id, Setting.setting == "log_ignore")
modlog = await Setting.find_one(
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):
channel = await ctx.guild.fetch_channel(modlog.value)
args = []
@ -146,10 +173,14 @@ class EventMixin(MemberEventMixin, MessageEventMixin, ComponentEventMixin):
fields=fields,
color="#fc9e3f",
)
embed.set_author(name=ctx.author.username, icon_url=ctx.author.display_avatar.url)
embed.set_footer(text=f"{ctx.author.user.username}#{ctx.author.discriminator} | {ctx.author.id}")
embed.set_author(
name=ctx.author.username, icon_url=ctx.author.display_avatar.url
)
embed.set_footer(text=f"{ctx.author.user.username} | {ctx.author.id}")
if channel:
await channel.send(embeds=embed)
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()

View file

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

View file

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

View file

@ -40,7 +40,9 @@ class MessageEventMixin:
)
if autopurge:
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)
async def autoreact(self, message: Message) -> None:
@ -50,13 +52,15 @@ class MessageEventMixin:
Autoreact.channel == message.channel.id,
)
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:
await message.add_reaction(reaction)
if autoreact.thread:
name = message.content.replace("\n", " ")
name = re.sub(r"<:\w+:(\d+)>", "", name)
if len(name) > 100:
if len(name) >= 100:
name = name[:97] + "..."
await message.create_thread(name=message.content, reason="Autoreact")
@ -70,7 +74,9 @@ class MessageEventMixin:
# )
content = re.sub(r"\s+", "", message.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:
setting = Setting(guild=message.guild.id, setting="noinvite", value=True)
await setting.save()
@ -78,12 +84,20 @@ class MessageEventMixin:
guild_invites = [x.code for x in await message.guild.fetch_invites()]
if message.guild.vanity_url_code:
guild_invites.append(message.guild.vanity_url_code)
allowed = guild_invites + ["dbrand", "VtgZntXcnZ", "gPfYGbvTCE", "interactions", "NTSHu97tHg"]
is_mod = message.author.has_permission(Permissions.MANAGE_GUILD) or message.author.has_permission(
Permissions.ADMINISTRATOR
)
allowed = guild_invites + [
"dbrand",
"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:
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:
await message.delete()
except Exception:
@ -116,8 +130,7 @@ class MessageEventMixin:
async def filters(self, message: Message) -> None:
"""Handle filter evennts."""
filters = Filter.find(Filter.guild == message.guild.id)
async for item in filters:
async for item in Filter.find(Filter.guild == message.guild.id):
for f in item.filters:
if re.search(f, message.content, re.IGNORECASE):
expires_at = datetime.now(tz=timezone.utc) + timedelta(hours=24)
@ -139,7 +152,9 @@ class MessageEventMixin:
value=1,
)
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:
await message.reply(embeds=embed)
except Exception:
@ -153,24 +168,26 @@ class MessageEventMixin:
async def massmention(self, message: Message) -> None:
"""Handle massmention events."""
massmention = await Setting.find_one(
massmention: Setting = await Setting.find_one(
Setting.guild == message.guild.id,
Setting.setting == "massmention",
)
is_mod = message.author.has_permission(Permissions.MANAGE_GUILD) or message.author.has_permission(
Permissions.ADMINISTRATOR
)
is_mod = message.author.has_permission(
Permissions.MANAGE_GUILD
) or message.author.has_permission(Permissions.ADMINISTRATOR)
if (
massmention
and massmention.value > 0 # noqa: W503
and int(massmention.value) > 0 # noqa: W503
and len(message._mention_ids + message._mention_roles) # noqa: W503
- (1 if message.author.id in message._mention_ids else 0) # noqa: W503
> massmention.value # noqa: W503
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)
await Warning(
active=True,
@ -202,11 +219,20 @@ class MessageEventMixin:
if message.author.has_permission(Permissions.MANAGE_GUILD):
return
except Exception as e:
self.logger.error("Failed to get permissions, pretending check failed", exc_info=e)
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
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
roles = [x.id async for x in message.mention_roles]
@ -230,7 +256,9 @@ class MessageEventMixin:
# Check if user in a bypass list
def check_has_role(roleping: Roleping) -> bool:
return any(role.id in roleping.bypass.roles for role in message.author.roles)
return any(
role.id in roleping.bypass.roles for role in message.author.roles
)
user_has_bypass = False
for roleping in rolepings:
@ -241,8 +269,15 @@ class MessageEventMixin:
user_has_bypass = True
break
if role_in_rolepings and user_missing_role and not user_is_admin and not user_has_bypass:
self.logger.debug(f"Rolepinged role in {message.guild.id}/{message.channel.id}/{message.id}")
if (
role_in_rolepings
and user_missing_role
and not user_is_admin
and not user_has_bypass
):
self.logger.debug(
f"Rolepinged role in {message.guild.id}/{message.channel.id}/{message.id}"
)
expires_at = datetime.now(tz=timezone.utc) + timedelta(hours=24)
await Warning(
active=True,
@ -262,7 +297,11 @@ class MessageEventMixin:
value=1,
)
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:
await message.channel.send(embeds=embed)
except Exception:
@ -318,8 +357,16 @@ class MessageEventMixin:
fields=[EmbedField(name="URL", value=m)],
)
valid_button = Button(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}")
valid_button = Button(
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)
@ -372,7 +419,9 @@ class MessageEventMixin:
value=1,
)
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)
try:
await message.channel.send(embeds=embed)
@ -394,8 +443,16 @@ class MessageEventMixin:
fields=[EmbedField(name="URL", value=m)],
)
valid_button = Button(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}")
valid_button = Button(
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)
@ -410,7 +467,9 @@ class MessageEventMixin:
"""Timeout a user."""
expires_at = datetime.now(tz=timezone.utc) + timedelta(minutes=30)
try:
await user.timeout(communication_disabled_until=expires_at, reason="Phishing link")
await user.timeout(
communication_disabled_until=expires_at, reason="Phishing link"
)
await Mute(
user=user.id,
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_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)
except Exception:
@ -459,10 +518,20 @@ class MessageEventMixin:
before = event.before
after = event.after
if not after.author.bot:
modlog = await Setting.find_one(Setting.guild == after.guild.id, Setting.setting == "activitylog")
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:
modlog = await Setting.find_one(
Setting.guild == after.guild.id, Setting.setting == "activitylog"
)
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
try:
channel = before.guild.get_channel(modlog.value)
@ -491,7 +560,9 @@ class MessageEventMixin:
icon_url=after.author.display_avatar.url,
url=after.jump_url,
)
embed.set_footer(text=f"{after.author.username}#{after.author.discriminator} | {after.author.id}")
embed.set_footer(
text=f"{after.author.username} | {after.author.id}"
)
await channel.send(embeds=embed)
except Exception as e:
self.logger.warning(
@ -512,9 +583,15 @@ class MessageEventMixin:
async def on_message_delete(self, event: MessageDelete) -> None:
"""Process on_message_delete events."""
message = event.message
modlog = await Setting.find_one(Setting.guild == message.guild.id, Setting.setting == "activitylog")
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)):
modlog = await Setting.find_one(
Setting.guild == message.guild.id, Setting.setting == "activitylog"
)
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:
content = message.content or "N/A"
except AttributeError:
@ -523,7 +600,9 @@ class MessageEventMixin:
try:
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(
EmbedField(
name="Attachments",
@ -533,7 +612,9 @@ class MessageEventMixin:
)
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(
EmbedField(
name="Stickers",
@ -566,8 +647,10 @@ class MessageEventMixin:
url=message.jump_url,
)
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)
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:
@Task.create(IntervalTrigger(minutes=1))
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.raise_for_status()
data = await response.json()
@ -31,3 +33,7 @@ class TaskMixin:
sub -= 1
self.phishing_domains.remove(domain)
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:
"""Apply a Discord ban."""
await ctx.guild.ban(user, reason=reason, delete_message_seconds=delete_history)
discrim = user.discriminator
if discrim == 0:
discrim = None
b = Ban(
user=user.id,
username=user.username,
discrim=user.discriminator,
discrim=discrim,
reason=reason,
admin=ctx.author.id,
guild=ctx.guild.id,
@ -65,13 +68,18 @@ class BanCog(ModcaseCog):
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."""
await ctx.guild.unban(user, reason=reason)
discrim = user.discriminator
if discrim == 0:
discrim = None
u = Unban(
user=user.id,
username=user.username,
discrim=user.discriminator,
discrim=discrim,
guild=ctx.guild.id,
admin=ctx.author.id,
reason=reason,
@ -83,8 +91,15 @@ class BanCog(ModcaseCog):
await ctx.send(embeds=embed)
@slash_command(name="ban", description="Ban a user")
@slash_option(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(
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(
name="btype",
description="Ban type",
@ -131,14 +146,23 @@ class BanCog(ModcaseCog):
await ctx.send("You cannot set a temp ban to > 1 month", ephemeral=True)
return
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
if len(reason) > 100:
await ctx.send("Reason must be < 100 characters", ephemeral=True)
return
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}
delete_history = delete_history.strip().lower()
if delete_history:
@ -148,7 +172,10 @@ class BanCog(ModcaseCog):
delete_history = int(timedelta(**delta).total_seconds())
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
await ctx.defer()
@ -158,7 +185,9 @@ class BanCog(ModcaseCog):
mtype = "perma"
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":
user_message += f"\nDuration: {duration} hours"
@ -187,7 +216,9 @@ class BanCog(ModcaseCog):
except Exception:
self.logger.warn(f"Failed to send ban embed to {user.id}")
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:
await ctx.send(f"Failed to ban user:\n```\n{e}\n```", ephemeral=True)
return
@ -196,8 +227,18 @@ class BanCog(ModcaseCog):
await ctx.guild.unban(user, reason="Ban was softban")
@slash_command(name="unban", description="Unban a user")
@slash_option(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)
@slash_option(
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))
async def _unban(
self,
@ -226,7 +267,8 @@ class BanCog(ModcaseCog):
user, discrim = user.split("#")
if discrim:
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,
)
else:
@ -235,9 +277,16 @@ class BanCog(ModcaseCog):
if len(results) > 1:
active_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)
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)
return
discord_ban_info = results[0]
@ -259,7 +308,7 @@ class BanCog(ModcaseCog):
if 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:
await ctx.send(f"Unable to find user {orig_user}", ephemeral=True)
@ -282,7 +331,9 @@ class BanCog(ModcaseCog):
admin=ctx.author.id,
reason=reason,
).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")
@ -306,14 +357,16 @@ class BanCog(ModcaseCog):
required=False,
)
@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"]
search = {"guild": ctx.guild.id}
if active:
search["active"] = True
if btype > 0:
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 = []
fields = []
for ban in bans:
@ -322,7 +375,7 @@ class BanCog(ModcaseCog):
ban.username = user.username if user else "[deleted user]"
fields.append(
EmbedField(
name=f"Username: {ban.username}#{ban.discrim}",
name=f"Username: {ban.username}",
value=(
f"Date: {ban.created_at.strftime('%d-%m-%Y')}\n"
f"User ID: {ban.user}\n"
@ -339,7 +392,7 @@ class BanCog(ModcaseCog):
if ban.user.id not in db_bans:
fields.append(
EmbedField(
name=f"Username: {ban.user.username}#" + f"{ban.user.discriminator}",
name=f"Username: {ban.user.username}",
value=(
f"Date: [unknown]\n"
f"User ID: {ban.user.id}\n"
@ -368,7 +421,9 @@ class BanCog(ModcaseCog):
pages.append(embed)
else:
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)
pages.append(embed)

View file

@ -31,63 +31,81 @@ class FilterCog(Extension):
self.bot = bot
self.cache: Dict[int, List[str]] = {}
async def _edit_filter(self, ctx: InteractionContext, name: str, search: bool = False) -> None:
content = ""
f: Filter = None
if search:
if f := await Filter.find_one(Filter.name == name, Filter.guild == ctx.guild.id):
content = "\n".join(f.filters)
async def _edit_filter(
self, ctx: InteractionContext, name: str, search: bool = False
) -> None:
try:
content = ""
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(
title=f'{kw} filter "{name}"',
components=[
InputText(
label="Filter (one statement per line)",
placeholder="" if content else "i.e. $bad_word^",
custom_id="filters",
max_length=3000,
value=content,
style=TextStyles.PARAGRAPH,
modal = Modal(
*[
InputText(
label="Filter (one statement per line)",
placeholder="" if content else "i.e. $bad_word^",
custom_id="filters",
max_length=3000,
value=content,
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
)
],
)
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:
return
# Thanks, Glitter
new_name = re.sub(r"[^\w-]", "", name)
try:
if not f:
f = Filter(name=new_name, guild=ctx.guild.id, filters=filters)
else:
f.name = new_name
f.filters = filters
await f.save()
filters = data.responses.get("filters").split("\n")
except asyncio.TimeoutError:
return
# Thanks, Glitter
new_name = re.sub(r"[^\w-]", "", name)
try:
if not f:
f = Filter(name=new_name, guild=ctx.guild.id, filters=filters)
else:
f.name = new_name
f.filters = filters
await f.save()
except Exception as e:
await data.send(f"{e}", ephemeral=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)
except Exception as e:
await data.send(f"{e}", ephemeral=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)
self.logger.error(e, exc_info=True)
filter_ = SlashCommand(name="filter", description="Manage keyword filters")
@filter_.subcommand(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)
@filter_.subcommand(
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))
async def _filter_create(self, ctx: InteractionContext, name: str) -> None:
return await self._edit_filter(ctx, name)
@ -148,11 +166,13 @@ class FilterCog(Extension):
@_filter_edit.autocomplete("name")
@_filter_view.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):
filters = await Filter.find(Filter.guild == ctx.guild.id).to_list()
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]
await ctx.send(choices=choices)

View file

@ -42,12 +42,18 @@ class CaseCog(Extension):
guild: Originating guild
"""
action_table = Table()
action_table.add_column(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="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")
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")
console = Console()
@ -64,14 +70,18 @@ class CaseCog(Extension):
admin = await self.bot.fetch_user(parent_action.admin)
admin_text = "[N/A]"
if admin:
admin_text = f"{admin.username}#{admin.discriminator}"
action_table.add_row(action.action_type.title(), admin_text, parent_action.reason)
admin_text = f"{admin.username}"
action_table.add_row(
action.action_type.title(), admin_text, parent_action.reason
)
with console.capture() as cap:
console.print(action_table)
tmp_output = cap.get()
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
action_output = tmp_output
@ -83,7 +93,7 @@ class CaseCog(Extension):
admin = await self.bot.fetch_user(note.admin)
admin_text = "[N/A]"
if admin:
admin_text = f"{admin.username}#{admin.discriminator}"
admin_text = f"{admin.username}"
note_table.add_row(admin_text, note.content)
with console.capture() as cap:
@ -102,7 +112,7 @@ class CaseCog(Extension):
username = "[N/A]"
user_text = "[N/A]"
if user:
username = f"{user.username}#{user.discriminator}"
username = f"{user.username}"
user_text = user.mention
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```"
fields = (
EmbedField(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"),
EmbedField(
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(
@ -148,7 +163,7 @@ class CaseCog(Extension):
user_mention = "[N/A]"
avatar_url = None
if user:
username = f"{user.username}#{user.discriminator}"
username = f"{user.username}"
avatar_url = user.avatar.url
user_mention = user.mention
@ -166,7 +181,9 @@ class CaseCog(Extension):
if admin:
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(
title="Moderation Case Action",
description=f"{admin_text} initiated an action against {user_mention}",
@ -195,7 +212,12 @@ class CaseCog(Extension):
required=False,
)
@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]
if not closed:
query.append(Modlog.open == True)
@ -214,8 +236,12 @@ class CaseCog(Extension):
case = SlashCommand(name="case", description="Manage a moderation 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")
@slash_option(name="cid", description="Case ID", opt_type=OptionType.STRING, required=True)
@show.subcommand(
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))
async def _case_show_summary(self, ctx: InteractionContext, cid: str) -> None:
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)
@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))
async def _case_show_actions(self, ctx: InteractionContext, cid: str) -> None:
case = await Modlog.find_one(Modlog.guild == ctx.guild.id, Modlog.nanoid == cid)
@ -240,7 +268,9 @@ class CaseCog(Extension):
await paginator.send(ctx)
@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))
async def _case_close(self, ctx: InteractionContext, cid: str) -> None:
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)
await ctx.send(embeds=embed)
@case.subcommand(sub_cmd_name="repoen", sub_cmd_description="Reopen a specific case")
@slash_option(name="cid", description="Case ID", opt_type=OptionType.STRING, required=True)
@case.subcommand(
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))
async def _case_reopen(self, ctx: InteractionContext, cid: str) -> None:
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)
await ctx.send(embeds=embed)
@case.subcommand(sub_cmd_name="note", sub_cmd_description="Add a note to a specific case")
@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)
@case.subcommand(
sub_cmd_name="note", sub_cmd_description="Add a note to a specific case"
)
@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))
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)
@ -280,7 +323,9 @@ class CaseCog(Extension):
return
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
if len(note) > 50:
@ -296,11 +341,20 @@ class CaseCog(Extension):
await ctx.send(embeds=embed)
@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(name="note", description="Note to add", opt_type=OptionType.STRING, required=True)
@slash_option(
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))
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:
await ctx.send(f"Case already open with ID `{case.nanoid}`", ephemeral=True)
return
@ -315,7 +369,13 @@ class CaseCog(Extension):
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()
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):
"""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)
duration = int((until - datetime.now(tz=timezone.utc)).seconds / 60)
await Mute(
@ -42,11 +44,14 @@ class MuteCog(ModcaseCog):
return mute_embed(user=user, admin=ctx.author, reason=reason, guild=ctx.guild)
@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:
modal = Modal(
title=f"Muting {ctx.target.mention}",
components=[
*[
InputText(
label="Reason?",
placeholder="Spamming, harrassment, etc",
@ -62,10 +67,13 @@ class MuteCog(ModcaseCog):
max_length=100,
),
],
title=f"Muting {ctx.target.mention}",
)
await ctx.send_modal(modal)
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")
until = response.responses.get("until")
except asyncio.TimeoutError:
@ -76,7 +84,9 @@ class MuteCog(ModcaseCog):
"RETURN_AS_TIMEZONE_AWARE": True,
}
rt_settings = base_settings.copy()
rt_settings["PARSERS"] = [x for x in default_parsers if x not in ["absolute-time", "timestamp"]]
rt_settings["PARSERS"] = [
x for x in default_parsers if x not in ["absolute-time", "timestamp"]
]
rt_until = parse(until, settings=rt_settings)
@ -91,10 +101,14 @@ class MuteCog(ModcaseCog):
until = at_until
else:
self.logger.debug(f"Failed to parse delay: {until}")
await response.send(f"`{until}` is not a parsable date, please try again", ephemeral=True)
await response.send(
f"`{until}` is not a parsable date, please try again", ephemeral=True
)
return
if until < datetime.now(tz=timezone.utc):
await response.send(f"`{old_until}` is in the past, which isn't allowed", ephemeral=True)
await response.send(
f"`{old_until}` is in the past, which isn't allowed", ephemeral=True
)
return
try:
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)
@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(
name="reason",
description="Reason for mute",
@ -128,9 +144,18 @@ class MuteCog(ModcaseCog):
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(
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:
if user == ctx.author:
await ctx.send("You cannot mute yourself.", ephemeral=True)
@ -148,7 +173,9 @@ class MuteCog(ModcaseCog):
# Max 4 weeks (2419200 seconds) per API
duration = time * scale
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
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)
@slash_command(name="unmute", description="Unmute a user")
@slash_option(name="user", 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))
@slash_option(
name="user",
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:
if (
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)
return
@ -176,6 +218,8 @@ class MuteCog(ModcaseCog):
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)

View file

@ -24,7 +24,9 @@ class WarningCog(ModcaseCog):
"""JARVIS WarningCog."""
@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(
name="reason",
description="Reason for warning",
@ -38,7 +40,9 @@ class WarningCog(ModcaseCog):
required=False,
)
@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:
await ctx.send("Reason must be < 100 characters", ephemeral=True)
return
@ -66,15 +70,19 @@ class WarningCog(ModcaseCog):
await ctx.send(embeds=embed)
@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(
name="active",
description="View active only",
opt_type=OptionType.BOOLEAN,
required=False,
)
@check(admin_or_permissions(Permissions.MANAGE_GUILD))
async def _warnings(self, ctx: InteractionContext, user: Member, active: bool = True) -> None:
# @check(admin_or_permissions(Permissions.MANAGE_GUILD))
async def _warnings(
self, ctx: InteractionContext, user: Member, active: bool = True
) -> None:
warnings = (
await Warning.find(
Warning.user == user.id,
@ -84,6 +92,7 @@ class WarningCog(ModcaseCog):
.to_list()
)
if len(warnings) == 0:
await ctx.defer(ephemeral=True)
await ctx.send("That user has no warnings.", ephemeral=True)
return
active_warns = get_all(warnings, active=True)
@ -117,7 +126,9 @@ class WarningCog(ModcaseCog):
for i in range(0, len(fields), 5):
embed = build_embed(
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],
)
embed.set_author(
@ -125,7 +136,9 @@ class WarningCog(ModcaseCog):
icon_url=user.display_avatar.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)
else:
fields = []
@ -143,10 +156,15 @@ class WarningCog(ModcaseCog):
for i in range(0, len(fields), 5):
embed = build_embed(
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],
)
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)
pages.append(embed)

View file

@ -5,6 +5,8 @@ import re
from datetime import datetime, timezone
from typing import List
import pytz
from croniter import croniter
from dateparser import parse
from dateparser_data.settings import default_parsers
from interactions import AutocompleteContext, Client, Extension, InteractionContext
@ -23,7 +25,7 @@ from thefuzz import process
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)
invites = re.compile(
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.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(
name="private",
description="Send as DM?",
@ -50,15 +59,16 @@ class RemindmeCog(Extension):
async def _remindme(
self,
ctx: InteractionContext,
timezone: str = "UTC",
private: bool = None,
) -> None:
if private is None and ctx.guild:
private = ctx.guild.member_count >= 5000
elif private is None and not ctx.guild:
private = False
timezone = pytz.timezone(timezone)
modal = Modal(
title="Set your reminder!",
components=[
*[
InputText(
label="What to remind you?",
placeholder="Reminder",
@ -72,14 +82,26 @@ class RemindmeCog(Extension):
style=TextStyles.SHORT,
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)
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()
delay = response.responses.get("delay").strip()
cron = response.responses.get("cron").strip()
except asyncio.TimeoutError:
return
if len(message) > 500:
@ -91,20 +113,32 @@ class RemindmeCog(Extension):
ephemeral=True,
)
return
elif not valid.fullmatch(message):
await response.send("Hey, you should probably make this readable", ephemeral=True)
return
# elif not valid.fullmatch(message):
# await response.send(
# "Hey, you should probably make this readable", ephemeral=True
# )
# return
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
base_settings = {
"PREFER_DATES_FROM": "future",
"TIMEZONE": "UTC",
"TIMEZONE": str(timezone),
"RETURN_AS_TIMEZONE_AWARE": True,
}
rt_settings = base_settings.copy()
rt_settings["PARSERS"] = [x for x in default_parsers if x not in ["absolute-time", "timestamp"]]
rt_settings["PARSERS"] = [
x for x in default_parsers if x not in ["absolute-time", "timestamp"]
]
rt_remind_at = parse(delay, settings=rt_settings)
@ -118,14 +152,20 @@ class RemindmeCog(Extension):
remind_at = at_remind_at
else:
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
if remind_at < datetime.now(tz=timezone.utc):
await response.send(f"`{delay}` is in the past. Past reminders aren't allowed", ephemeral=True)
if remind_at < datetime.now(tz=timezone):
await response.send(
f"`{delay}` is in the past. Past reminders aren't allowed",
ephemeral=True,
)
return
elif remind_at < datetime.now(tz=timezone.utc):
elif remind_at < datetime.now(tz=timezone):
pass
r = Reminder(
@ -135,39 +175,59 @@ class RemindmeCog(Extension):
message=message,
remind_at=remind_at,
private=private,
repeat=cron,
timezone=str(timezone),
active=True,
)
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(
title="Reminder Set",
description=f"{ctx.author.mention} set a reminder",
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,
),
],
fields=fields,
)
embed.set_author(
name=ctx.author.username + "#" + ctx.author.discriminator,
name=ctx.author.username,
icon_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]
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)
private = private if private is not None else False
components = [ActionRow(*components)]
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."""
fields = []
for reminder in reminders:
@ -195,7 +255,7 @@ class RemindmeCog(Extension):
)
embed.set_author(
name=ctx.author.username + "#" + ctx.author.discriminator,
name=ctx.author.username,
icon_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")
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:
await ctx.send("You have no reminders set.", ephemeral=True)
return
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)
@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(
name="content",
description="Content of the reminder",
@ -222,7 +288,7 @@ class RemindmeCog(Extension):
autocomplete=True,
)
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:
await ctx.send(f"Reminder `{content}` does not exist", ephemeral=True)
return
@ -238,12 +304,14 @@ class RemindmeCog(Extension):
)
embed.set_author(
name=ctx.author.display_name + "#" + ctx.author.discriminator,
name=ctx.author.display_name,
icon_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:
await reminder.delete()
except Exception:
@ -275,14 +343,18 @@ class RemindmeCog(Extension):
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(
name=ctx.author.display_name + "#" + ctx.author.discriminator,
name=ctx.author.display_name,
icon_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)
if reminder.remind_at <= datetime.now(tz=timezone.utc) and not reminder.active:
try:
@ -292,13 +364,22 @@ class RemindmeCog(Extension):
@_fetch.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()
lookup = {r.message: str(r.id) for r in reminders}
results = process.extract(content, list(lookup.keys()), limit=5)
lookup = {
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]
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:
"""Add RemindmeCog to JARVIS"""

View file

@ -2,10 +2,8 @@
from interactions import Client
from jarvis.cogs.core.socials import reddit, twitter
def setup(bot: Client) -> None:
"""Add social cogs to JARVIS"""
reddit.RedditCog(bot)
twitter.TwitterCog(bot)
# Unfortunately there's no social cogs anymore
# 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(
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")
repo_url = f"https://git.zevaryx.com/stark-industries/jarvis/jarvis-bot/-/tree/{get_repo_hash()}"
fields.append(EmbedField(name="Git Hash", value=f"[{get_repo_hash()[:7]}]({repo_url})", inline=True))
fields.append(EmbedField(name="Online Since", value=f"<t:{uptime}:F>", inline=False))
fields.append(
EmbedField(
name="Git Hash",
value=f"[{get_repo_hash()[:7]}]({repo_url})",
inline=True,
)
)
fields.append(
EmbedField(name="Online Since", value=f"<t:{uptime}:F>", inline=False)
)
num_domains = len(self.bot.phishing_domains)
fields.append(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)
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)
@bot.subcommand(
@ -114,7 +133,11 @@ class UtilCog(Extension):
JARVIS_LOGO.save(image_bytes, "PNG")
image_bytes.seek(0)
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)
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.set_image(url=avatar)
embed.set_author(name=f"{user.username}#{user.discriminator}", icon_url=avatar)
components = Button(style=ButtonStyle.DANGER, emoji="🗑️", custom_id=f"delete|{ctx.author.id}")
embed.set_author(name=f"{user.username}", icon_url=avatar)
components = Button(
style=ButtonStyle.DANGER, emoji="🗑️", custom_id=f"delete|{ctx.author.id}"
)
await ctx.send(embeds=embed, components=components)
@slash_command(
@ -179,11 +204,19 @@ class UtilCog(Extension):
EmbedField(name="Name", value=role.mention, inline=True),
EmbedField(name="Color", value=str(role.color.hex), inline=True),
EmbedField(name="Mention", value=f"`{role.mention}`", inline=True),
EmbedField(name="Hoisted", value="Yes" if role.hoist else "No", inline=True),
EmbedField(
name="Hoisted", value="Yes" if role.hoist else "No", 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="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(
title="",
@ -208,7 +241,11 @@ class UtilCog(Extension):
im.save(image_bytes, "PNG")
image_bytes.seek(0)
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)
@slash_command(name="avatar", description="Get a user avatar")
@ -252,14 +289,18 @@ class UtilCog(Extension):
),
EmbedField(
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,
),
]
if muted:
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(
title="",
@ -269,16 +310,23 @@ class UtilCog(Extension):
)
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,
)
embed.set_thumbnail(url=user.display_avatar.url)
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)
@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:
url = "https://letmegooglethat.com/?q=" + urllib.parse.quote_plus(search)
await ctx.send(url)
@ -306,7 +354,7 @@ class UtilCog(Extension):
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)])
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="Members", value=str(members), 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:
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_thumbnail(url=guild.icon.url)
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)
@slash_command(
name="pw",
sub_cmd_name="gen",
description="Generate a secure password",
scopes=[862402786116763668],
)
@slash_option(
name="length",
@ -363,7 +416,9 @@ class UtilCog(Extension):
],
)
@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:
await ctx.send("Please limit password to 256 characters", ephemeral=True)
return
@ -384,7 +439,12 @@ class UtilCog(Extension):
)
@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:
outp = "`"
for c in text:
@ -399,17 +459,34 @@ class UtilCog(Extension):
outp += "`"
await ctx.send(outp[:2000])
@slash_command(name="timestamp", description="Convert a datetime or timestamp into it's counterpart")
@slash_option(name="string", description="String to convert", opt_type=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:
@slash_command(
name="timestamp",
description="Convert a datetime or timestamp into it's counterpart",
)
@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)
if not timestamp:
await ctx.send("Valid time not found, try again", ephemeral=True)
return
if not timestamp.tzinfo:
timestamp = timestamp.replace(tzinfo=get_localzone()).astimezone(tz=timezone.utc)
timestamp = timestamp.replace(tzinfo=get_localzone()).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="ISO8601", value=timestamp.isoformat()),
]
embed = build_embed(title="Converted Time", description=f"`{string}`", fields=fields)
components = Button(style=ButtonStyle.DANGER, emoji="🗑️", custom_id=f"delete|{ctx.author.id}")
embed = build_embed(
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)
@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:
await ctx.send(
"""

View file

@ -1,8 +1,6 @@
"""JARVIS Calculator Cog."""
import json
from aiohttp import ClientSession
from calculator import calculate
from erapi import const
from interactions import AutocompleteContext, Client, Extension, InteractionContext
from interactions.models.discord.components import Button
from interactions.models.discord.embed import Embed, EmbedField
@ -24,39 +22,8 @@ TEMP_CHOICES = (
SlashCommandChoice(name="Kelvin", value=2),
)
TEMP_LOOKUP = {0: "F", 1: "C", 2: "K"}
CURRENCIES = (
"AUD",
"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",
)
CURRENCY_BY_NAME = {x["name"]: x["code"] for x in const.VALID_CODES}
CURRENCY_BY_CODE = {x["code"]: x["name"] for x in const.VALID_CODES}
class CalcCog(Extension):
@ -67,22 +34,32 @@ class CalcCog(Extension):
async def _get_currency_conversion(self, from_: str, to: str) -> int:
"""Get the conversion rate."""
async with ClientSession() as session:
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]
return self.bot.erapi.get_conversion_rate(from_, to)
calc = SlashCommand(name="calc", description="Calculate some things")
@calc.subcommand(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)
@calc.subcommand(
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:
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)
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)
return
try:
@ -94,21 +71,41 @@ class CalcCog(Extension):
await ctx.send("No value? Try a valid expression", ephemeral=True)
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)
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)
convert = calc.group(name="convert", description="Conversion helpers")
@convert.subcommand(sub_cmd_name="temperature", sub_cmd_description="Convert between temperatures")
@slash_option(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
@convert.subcommand(
sub_cmd_name="temperature", sub_cmd_description="Convert between temperatures"
)
@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(
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)}",
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)
@convert.subcommand(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)
@convert.subcommand(
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(
name="from_currency",
description="Currency to convert from",
@ -168,27 +175,58 @@ class CalcCog(Extension):
conv = value * rate
fields = (
EmbedField(name="Conversion Rate", value=f"1 {from_currency} ~= {rate:0.4f} {to_currency}"),
EmbedField(name=from_currency, value=f"{value:0.2f}"),
EmbedField(name=to_currency, value=f"{conv:0.2f}"),
EmbedField(
name="Conversion Rate",
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)
components = Button(style=ButtonStyle.DANGER, emoji="🗑️", custom_id=f"delete|{ctx.author.id}")
embed = build_embed(
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)
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 = getattr(units, which.capitalize(), None)
ratio = which.get_rate(from_, to)
converted = value / ratio
fields = (EmbedField(name=from_, value=f"{value:0.2f}"), 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}")
fields = (
EmbedField(name=from_, value=f"{value:0.2f}"),
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)
@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(
name="from_unit",
description="Units to convert from",
@ -197,13 +235,24 @@ class CalcCog(Extension):
autocomplete=True,
)
@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)
@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(
name="from_unit",
description="Units to convert from",
@ -212,13 +261,24 @@ class CalcCog(Extension):
autocomplete=True,
)
@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)
@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(
name="from_unit",
description="Units to convert from",
@ -227,13 +287,24 @@ class CalcCog(Extension):
autocomplete=True,
)
@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)
@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(
name="from_unit",
description="Units to convert from",
@ -242,13 +313,24 @@ class CalcCog(Extension):
autocomplete=True,
)
@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)
@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(
name="from_unit",
description="Units to convert from",
@ -257,13 +339,24 @@ class CalcCog(Extension):
autocomplete=True,
)
@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)
@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(
name="from_unit",
description="Units to convert from",
@ -272,13 +365,26 @@ class CalcCog(Extension):
autocomplete=True,
)
@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)
@convert.subcommand(sub_cmd_name="pressure", sub_cmd_description="Convert pressures")
@slash_option(name="value", description="Pressure to convert", opt_type=OptionType.NUMBER, required=True)
@convert.subcommand(
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(
name="from_unit",
description="Units to convert from",
@ -287,13 +393,24 @@ class CalcCog(Extension):
autocomplete=True,
)
@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)
@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(
name="from_unit",
description="Units to convert from",
@ -302,13 +419,24 @@ class CalcCog(Extension):
autocomplete=True,
)
@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)
@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(
name="from_unit",
description="Units to convert from",
@ -317,13 +445,24 @@ class CalcCog(Extension):
autocomplete=True,
)
@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)
@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(
name="from_unit",
description="Units to convert from",
@ -332,13 +471,24 @@ class CalcCog(Extension):
autocomplete=True,
)
@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)
@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(
name="from_unit",
description="Units to convert from",
@ -347,12 +497,20 @@ class CalcCog(Extension):
autocomplete=True,
)
@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)
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())
results = process.extract(unit, options, limit=25)
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))
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]):
return [{"name": r[0], "value": r[0]} for r in results if r[1] > 50]
return [{"name": r[0], "value": r[0]} for r in results]
return [{"name": r[0], "value": lookup_name[r[0]]} for r in results if r[1]]
return [{"name": r[0], "value": lookup_name[r[0]]} for r in results]
@_calc_convert_currency.autocomplete("from_currency")
async def _autocomplete_from_currency(self, ctx: AutocompleteContext) -> None:

View file

@ -5,14 +5,17 @@ import logging
import re
import subprocess # noqa: S404
import uuid as uuidpy
from datetime import datetime
from io import BytesIO
import nanoid
import pytz
import ulid as ulidpy
from aiofile import AIOFile
from ansitoimg.render import ansiToRender
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.embed import EmbedField
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.http import get_size
from rich.console import Console
from thefuzz import process
from jarvis.utils import build_embed
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}$")
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(
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.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")
@slash_option(
name="method",
@ -74,9 +116,20 @@ class DevCog(Extension):
opt_type=OptionType.STRING,
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)
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:
await ctx.send(
"No data to hash",
@ -93,8 +146,12 @@ class DevCog(Extension):
elif url.match(data):
try:
if (size := await get_size(data)) > MAX_FILESIZE:
await ctx.send("Please hash files that are <= 5GB in size", ephemeral=True)
self.logger.debug(f"Refused to hash file of size {convert_bytesize(size)}")
await ctx.send(
"Please hash files that are <= 5GB in size", ephemeral=True
)
self.logger.debug(
f"Refused to hash file of size {convert_bytesize(size)}"
)
return
except Exception as e:
await ctx.send(f"Failed to retrieve URL: ```\n{e}\n```", ephemeral=True)
@ -117,7 +174,9 @@ class DevCog(Extension):
]
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)
@dev.subcommand(sub_cmd_name="uuid", sub_cmd_description="Generate a UUID")
@ -134,7 +193,9 @@ class DevCog(Extension):
opt_type=OptionType.STRING,
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)
if version in [3, 5] and not data:
await ctx.send(f"UUID{version} requires data.", ephemeral=True)
@ -173,7 +234,12 @@ class DevCog(Extension):
sub_cmd_name="uuid2ulid",
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)
async def _uuid2ulid(self, ctx: InteractionContext, uuid: str) -> None:
if UUID_VERIFY.match(uuid):
@ -186,7 +252,12 @@ class DevCog(Extension):
sub_cmd_name="ulid2uuid",
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)
async def _ulid2uuid(self, ctx: InteractionContext, ulid: str) -> None:
if ULID_VERIFY.match(ulid):
@ -230,7 +301,9 @@ class DevCog(Extension):
EmbedField(name=mstr, value=f"`{encoded}`", inline=False),
]
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)
@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),
]
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)
@dev.subcommand(sub_cmd_name="cloc", sub_cmd_description="Get JARVIS lines of code")
@cooldown(bucket=Buckets.CHANNEL, rate=1, interval=30)
async def _cloc(self, ctx: InteractionContext) -> None:
await ctx.defer()
output = subprocess.check_output(["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()
with console.capture() as capture:
console.print(output)
@ -290,6 +367,12 @@ class DevCog(Extension):
tokei = File(file_bytes, file_name="tokei.png")
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:
"""Add DevCog to JARVIS"""

View file

@ -44,7 +44,9 @@ class PinboardCog(Extension):
async def _purge_starboard(self, ctx: InteractionContext, board: Pinboard) -> None:
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):
try:
await message.delete()
@ -89,9 +91,13 @@ class PinboardCog(Extension):
await ctx.send("Channel must be a GuildText", ephemeral=True)
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:
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
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))
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:
await found.delete()
asyncio.create_task(self._purge_starboard(ctx, found))
@ -129,128 +137,147 @@ class PinboardCog(Extension):
message: str,
channel: GuildText = None,
) -> None:
if not channel:
channel = ctx.channel
pinboards = await Pinboard.find(Pinboard.guild == ctx.guild.id).to_list()
if not pinboards:
await ctx.send("No pinboards exist.", ephemeral=True)
return
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)
try:
if not channel:
channel = ctx.channel
pinboards = await Pinboard.find(Pinboard.guild == ctx.guild.id).to_list()
if not pinboards:
await ctx.send("No pinboards exist.", ephemeral=True)
return
channel_list = []
to_delete: list[Pinboard] = []
await ctx.defer()
for pinboard in pinboards:
c = await ctx.guild.fetch_channel(pinboard.channel)
if c and isinstance(c, GuildText):
channel_list.append(c)
else:
self.logger.warning(f"Pinboard {pinboard.channel} no longer valid in {ctx.guild.name}")
to_delete.append(pinboard)
if not isinstance(message, Message):
if message.startswith("https://"):
message = message.split("/")[-1]
message = await channel.fetch_message(int(message))
for pinboard in to_delete:
try:
await pinboard.delete()
except Exception:
self.logger.debug("Ignoring deletion error")
if not message:
await ctx.send("Message not found", ephemeral=True)
return
select_channels = []
for idx, x in enumerate(channel_list):
if x:
select_channels.append(StringSelectOption(label=x.name, value=str(idx)))
channel_list = []
to_delete: list[Pinboard] = []
select_channels = [StringSelectOption(label=x.name, value=str(idx)) for idx, x in enumerate(channel_list)]
channel_to_pinboard = {}
select = StringSelectMenu(
options=select_channels,
min_values=1,
max_values=1,
)
for pinboard in pinboards:
c = await ctx.guild.fetch_channel(pinboard.channel)
if c and isinstance(c, GuildText):
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(
messages=msg,
components=components,
check=lambda x: ctx.author.id == x.context.author.id,
)
select_channels = [
StringSelectOption(label=x.name, value=str(idx))
for idx, x in enumerate(channel_list)
]
pinboard = channel_list[int(com_ctx.context.values[0])]
exists = await Pin.find_one(
Pin.message == message.id,
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,
select = StringSelectMenu(
*select_channels,
min_values=1,
max_values=1,
)
return
count = await Pin.find(Pin.guild == ctx.guild.id, Pin.pinboard == pinboard.id).count()
content = message.content
components = [ActionRow(select)]
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"
msg = await ctx.send(content="Choose a pinboard", components=components)
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)
com_ctx = await self.bot.wait_for_component(
messages=msg,
components=components,
check=lambda x: ctx.author.id == x.ctx.author.id,
)
await Pin(
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()
pinboard = channel_list[int(com_ctx.ctx.values[0])]
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(
content=f"Message saved to Pinboard.\nSee it in {pinboard.mention}",
components=components,
)
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()
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)
@check(admin_or_permissions(Permissions.MANAGE_GUILD, Permissions.MANAGE_MESSAGES))

View file

@ -64,13 +64,17 @@ class RolegiverCog(Extension):
rolegiver.roles = roles
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(
sub_cmd_name="add",
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))
async def _rolegiver_add(self, ctx: InteractionContext, role: Role) -> None:
if role.id == ctx.guild.id:
@ -122,15 +126,19 @@ class RolegiverCog(Extension):
embed.set_thumbnail(url=ctx.guild.icon.url)
embed.set_footer(text=f"{ctx.author.username}#{ctx.author.discriminator} | {ctx.author.id}")
components = Button(style=ButtonStyle.DANGER, emoji="🗑️", custom_id=f"delete|{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}"
)
await ctx.send(embeds=embed, components=components)
if ctx.guild.id not in self.cache:
self.cache[ctx.guild.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(
name="role",
description="Name of role to add",
@ -149,7 +157,9 @@ class RolegiverCog(Extension):
if cache:
role_id = cache.get(role)
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
setting.value.remove(role_id)
@ -171,14 +181,21 @@ class RolegiverCog(Extension):
fields = [
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_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(
embeds=embed,
@ -187,7 +204,9 @@ class RolegiverCog(Extension):
if ctx.guild.id in self.cache:
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:
setting = await Rolegiver.find_one(Rolegiver.guild == ctx.guild.id)
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_footer(text=f"{ctx.author.username}#{ctx.author.discriminator} | {ctx.author.id}")
components = Button(style=ButtonStyle.DANGER, emoji="🗑️", custom_id=f"delete|{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}"
)
await ctx.send(embeds=embed, components=components)
@rolegiver.subcommand(sub_cmd_name="get", sub_cmd_description="Get a role")
@cooldown(bucket=Buckets.USER, rate=1, interval=10)
async def _role_get(self, ctx: InteractionContext) -> None:
setting = await Rolegiver.find_one(Rolegiver.quild == ctx.guild.id)
if not setting or (setting and not setting.roles):
await ctx.send("Rolegiver has no roles", ephemeral=True)
try:
setting = await Rolegiver.find_one(Rolegiver.guild == ctx.guild.id)
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
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:
context = await self.bot.wait_for_component(
check=lambda x: ctx.author.id == x.ctx.author.id,
@ -255,16 +280,11 @@ class RolegiverCog(Extension):
added_roles.append(role)
await ctx.author.add_role(role, reason="Rolegiver")
roles = ctx.author.roles
if roles:
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"
avalue = (
"\n".join([r.mention for r in added_roles]) if added_roles else "None"
)
fields = [
EmbedField(name="Added Role(s)", value=avalue),
EmbedField(name="Prior Role(s)", value=value),
]
embed = build_embed(
@ -279,13 +299,15 @@ class RolegiverCog(Extension):
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 component in row.components:
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:
for row in components:
for component in row.components:
@ -312,7 +334,7 @@ class RolegiverCog(Extension):
options.append(option)
select = StringSelectMenu(
options=options,
*options,
custom_id="to_remove",
placeholder="Select roles to remove",
min_values=1,
@ -336,14 +358,13 @@ class RolegiverCog(Extension):
user_roles.remove(role)
removed_roles.append(role)
user_roles.sort(key=lambda x: -x.position)
_ = user_roles.pop(-1)
value = "\n".join([r.mention for r in user_roles]) if user_roles else "None"
rvalue = "\n".join([r.mention for r in removed_roles]) if removed_roles else "None"
rvalue = (
"\n".join([r.mention for r in removed_roles])
if removed_roles
else "None"
)
fields = [
EmbedField(name="Removed Role(s)", value=rvalue),
EmbedField(name="Remaining Role(s)", value=value),
]
embed = build_embed(
@ -353,15 +374,19 @@ class RolegiverCog(Extension):
)
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 component in row.components:
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:
for row in components:
@ -369,7 +394,10 @@ class RolegiverCog(Extension):
component.disabled = True
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))
async def _rolegiver_cleanup(self, ctx: InteractionContext) -> None:
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:
tag = await Tag.find_one(Tag.guild == ctx.guild.id, Tag.name == name)
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
await ctx.send(tag.content)
@ -58,8 +61,7 @@ class TagCog(Extension):
@tag.subcommand(sub_cmd_name="create", sub_cmd_description="Create a tag")
async def _create(self, ctx: SlashContext) -> None:
modal = Modal(
title="Create a new tag!",
components=[
*[
InputText(
label="Tag name",
placeholder="name",
@ -75,17 +77,22 @@ class TagCog(Extension):
max_length=512,
),
],
title="Create a new tag!",
)
await ctx.send_modal(modal)
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("`", "")
content = response.responses.get("content")
except asyncio.TimeoutError:
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 (
(invites.search(content) or invites.search(name))
@ -95,13 +102,17 @@ class TagCog(Extension):
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
elif not content.strip() or not name.strip():
await response.send("Content and name required", ephemeral=True)
return
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
tag = await Tag.find_one(Tag.guild == ctx.guild.id, Tag.name == name)
@ -122,7 +133,10 @@ class TagCog(Extension):
embed = build_embed(
title="Tag Created",
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(
@ -130,7 +144,9 @@ class TagCog(Extension):
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)
if ctx.guild.id not in self.cache:
@ -155,12 +171,13 @@ class TagCog(Extension):
ctx.author.has_permission(Permissions.ADMINISTRATOR)
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
modal = Modal(
title="Edit a tag!",
components=[
*[
InputText(
label="Tag name",
value=tag.name,
@ -176,11 +193,14 @@ class TagCog(Extension):
max_length=512,
),
],
title="Edit a tag!",
)
await ctx.send_modal(modal)
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("`", "")
content = response.responses.get("content")
except asyncio.TimeoutError:
@ -188,10 +208,15 @@ class TagCog(Extension):
new_tag = await Tag.find_one(Tag.guild == ctx.guild.id, Tag.name == name)
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
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 (
(invites.search(content) or invites.search(name))
@ -201,13 +226,17 @@ class TagCog(Extension):
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
elif not content.strip() or not name.strip():
await response.send("Content and name required", ephemeral=True)
return
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
tag.content = re.sub(r"\\?([@<])", r"\\\g<1>", content)
@ -230,7 +259,9 @@ class TagCog(Extension):
name=ctx.author.username + "#" + ctx.author.discriminator,
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)
if tag.name not in self.cache[ctx.guild.id]:
self.cache[ctx.guild.id].remove(old_name)
@ -253,7 +284,10 @@ class TagCog(Extension):
ctx.author.has_permission(Permissions.ADMINISTRATOR)
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
await tag.delete()
@ -307,7 +341,9 @@ class TagCog(Extension):
name=f"{username}#{discrim}" if username else "Unknown User",
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)
@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()
names = "\n".join(f"`{t.name}`" for t in tags)
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)
@_get.autocomplete("name")
@ -326,7 +364,9 @@ class TagCog(Extension):
if not self.cache.get(ctx.guild.id):
tags = await Tag.find(Tag.guild == ctx.guild.id).to_list()
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]
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 re
@ -19,7 +25,7 @@ from jarvis_core.db.models import Guess
from jarvis.utils import build_embed
guild_ids = [578757004059738142, 520021794380447745, 862402786116763668]
guild_ids = [] # [578757004059738142, 520021794380447745, 862402786116763668]
valid = re.compile(r"[\w\s\-\\/.!@#$%^*()+=<>,\u0080-\U000E0FFF]*")
invites = re.compile(
@ -40,30 +46,54 @@ class CTCCog(Extension):
def __del__(self):
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")
@cooldown(bucket=Buckets.USER, rate=1, interval=30)
async def _about(self, ctx: InteractionContext) -> None:
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)
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(
sub_cmd_name="pw",
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)
async def _pw(self, ctx: InteractionContext, guess: str) -> None:
if len(guess) > 800:
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,
)
return
elif not valid.fullmatch(guess):
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,
)
return
@ -102,7 +132,7 @@ class CTCCog(Extension):
if not user:
user = "[redacted]"
if isinstance(user, (Member, User)):
user = user.username + "#" + user.discriminator
user = user.username
cache[guess.user] = user
name = "Correctly" if guess["correct"] else "Incorrectly"
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 re
from datetime import datetime, timedelta, timezone
@ -63,7 +69,9 @@ async def parse_db_status() -> dict:
else:
cell = cell.get_text().strip()
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
@ -88,7 +96,9 @@ class DbrandCog(Extension):
db = SlashCommand(name="db", description="dbrand commands", scopes=guild_ids)
@db.subcommand(sub_cmd_name="status", sub_cmd_description="Get dbrand operational status")
@db.subcommand(
sub_cmd_name="status", sub_cmd_description="Get dbrand operational status"
)
async def _status(self, ctx: InteractionContext) -> None:
status = self.cache.get("status")
if not status or status["cache_expiry"] <= datetime.now(tz=timezone.utc):
@ -97,7 +107,10 @@ class DbrandCog(Extension):
self.cache["status"] = status
status = status.get("operations")
emojies = [x["Status"] for x in status]
fields = [EmbedField(name=f'{x["Status"]} {x["Service"]}', value=x["Detail"]) for x in status]
fields = [
EmbedField(name=f'{x["Status"]} {x["Service"]}', value=x["Detail"])
for x in status
]
color = "#FBBD1E"
if all("green" in x for x in emojies):
color = "#38F657"
@ -169,7 +182,9 @@ class DbrandCog(Extension):
async def _support(self, ctx: InteractionContext) -> None:
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:
video_url = "https://cdn.discordapp.com/attachments/599068193339736096/890679742263623751/video0.mov"
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'})",
"[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(
text="dbrand.com",
icon_url="https://dev.zevaryx.com/db_logo.png",
)
embed.set_thumbnail(url="https://dev.zevaryx.com/db_logo.png")
embed.set_author(name="dbrand", url=self.base_url, icon_url="https://dev.zevaryx.com/db_logo.png")
embed.set_author(
name="dbrand",
url=self.base_url,
icon_url="https://dev.zevaryx.com/db_logo.png",
)
await ctx.send(embeds=embed)
@db.subcommand(
@ -217,11 +241,15 @@ class DbrandCog(Extension):
):
# Magic number, subtract from flag char to get ascii char
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 == "🏳️":
search = "fr"
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
if len(search) > 3:
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)
if 200 <= data.status < 400:
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
else:
data = None
@ -255,12 +285,18 @@ class DbrandCog(Extension):
fields = []
for service in data["shipping_services_available"]:
service_data = self.cache.get(f"{dest}-{service}")
if not service_data or service_data["cache_expiry"] < datetime.now(tz=timezone.utc):
service_data = await self._session.get(self.api_url + dest + "/" + service["url"])
if not service_data or service_data["cache_expiry"] < datetime.now(
tz=timezone.utc
):
service_data = await self._session.get(
self.api_url + dest + "/" + service["url"]
)
if service_data.status > 400:
continue
service_data = await service_data.json()
service_data["cache_expiry"] = datetime.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
title = f'{service_data["carrier"]} {service_data["tier-title"]} | {service_data["costs-min"]}'
message = service_data["time-title"]
@ -271,7 +307,9 @@ class DbrandCog(Extension):
status = self.cache.get("status")
if not status or status["cache_expiry"] <= datetime.now(tz=timezone.utc):
status = await parse_db_status()
status["cache_expiry"] = datetime.now(tz=timezone.utc) + timedelta(hours=2)
status["cache_expiry"] = datetime.now(tz=timezone.utc) + timedelta(
hours=2
)
self.cache["status"] = status
status = status["countries"]
@ -284,10 +322,10 @@ class DbrandCog(Extension):
description = ""
color = "#FFBB00"
if shipping_info:
description = (
f'{shipping_info["Status"]}\u200b \u200b {shipping_info["Est. Delivery Time"].split(":")[0]}'
description = f'{shipping_info["Status"]}\u200b \u200b {shipping_info["Est. Delivery Time"].split(":")[0]}'
created = self.cache.get("status").get("cache_expiry") - timedelta(
hours=2
)
created = self.cache.get("status").get("cache_expiry") - timedelta(hours=2)
ts = int(created.timestamp())
description += f" \u200b | \u200b Last updated: <t:{ts}:R>\n\u200b"
if "green" in shipping_info["Status"]:
@ -316,7 +354,8 @@ class DbrandCog(Extension):
embed = build_embed(
title="Check Shipping Times",
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=[],
url="https://dbrand.com/shipping",

View file

@ -40,24 +40,6 @@ class Mastodon(BaseModel):
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):
"""JARVIS running environment."""
@ -69,12 +51,13 @@ class Config(BaseModel):
"""JARVIS config model."""
token: str
"""Bot token"""
erapi: str
"""exchangerate-api.org API token"""
environment: Environment = Environment.develop
mongo: Mongo
redis: Redis
mastodon: Optional[Mastodon] = None
reddit: Optional[Reddit] = None
twitter: Optional[Twitter] = None
urls: Optional[dict[str, str]] = None
sync: bool = False
log_level: str = "INFO"
@ -112,22 +95,16 @@ def _load_env() -> Config | None:
mongo = {}
redis = {}
mastodon = {}
twitter = {}
reddit = {}
urls = {}
mongo_keys = find_all(lambda x: x.upper().startswith("MONGO"), 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())
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())
config_keys = (
mongo_keys
+ redis_keys
+ mastodon_keys
+ reddit_keys
+ twitter_keys
+ url_keys
+ ["TOKEN", "SYNC", "LOG_LEVEL", "JURIGGED"]
)
@ -145,12 +122,6 @@ def _load_env() -> Config | None:
elif item in mastodon_keys:
key = "_".join(item.split("_")[1:]).lower()
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:
key = "_".join(item.split("_")[1:]).lower()
urls[key] = value
@ -161,10 +132,6 @@ def _load_env() -> Config | None:
data["mongo"] = mongo
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()):
data["mastodon"] = mastodon
data["urls"] = {k: v for k, v in urls if v}

View file

@ -1,7 +1,7 @@
"""dbrand-specific data."""
shipping_lookup = [
{"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": "Algeria", "alpha-2": "DZ", "alpha-3": "DZA", "numeric": "0012"},
{"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": "Anguilla", "alpha-2": "AI", "alpha-3": "AIA", "numeric": "0660"},
{"country": "Antarctica", "alpha-2": "AQ", "alpha-3": "ATA", "numeric": "0010"},
{"country": "Antigua and Barbuda", "alpha-2": "AG", "alpha-3": "ATG", "numeric": "0028"},
{
"country": "Antigua and Barbuda",
"alpha-2": "AG",
"alpha-3": "ATG",
"numeric": "0028",
},
{"country": "Argentina", "alpha-2": "AR", "alpha-3": "ARG", "numeric": "0032"},
{"country": "Armenia", "alpha-2": "AM", "alpha-3": "ARM", "numeric": "0051"},
{"country": "Aruba", "alpha-2": "AW", "alpha-3": "ABW", "numeric": "0533"},
@ -38,7 +43,12 @@ shipping_lookup = [
"alpha-3": "BES",
"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": "Bouvet Island", "alpha-2": "BV", "alpha-3": "BVT", "numeric": "0074"},
{"country": "Brazil", "alpha-2": "BR", "alpha-3": "BRA", "numeric": "0076"},
@ -48,7 +58,12 @@ shipping_lookup = [
"alpha-3": "IOT",
"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": "Burkina Faso", "alpha-2": "BF", "alpha-3": "BFA", "numeric": "0854"},
{"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": "Cameroon", "alpha-2": "CM", "alpha-3": "CMR", "numeric": "0120"},
{"country": "Canada", "alpha-2": "CA", "alpha-3": "CAN", "numeric": "0124"},
{"country": "Cayman Islands (the)", "alpha-2": "KY", "alpha-3": "CYM", "numeric": "0136"},
{
"country": "Cayman Islands (the)",
"alpha-2": "KY",
"alpha-3": "CYM",
"numeric": "0136",
},
{
"country": "Central African Republic (the)",
"alpha-2": "CF",
@ -66,7 +86,12 @@ shipping_lookup = [
{"country": "Chad", "alpha-2": "TD", "alpha-3": "TCD", "numeric": "0148"},
{"country": "Chile", "alpha-2": "CL", "alpha-3": "CHL", "numeric": "0152"},
{"country": "China", "alpha-2": "CN", "alpha-3": "CHN", "numeric": "0156"},
{"country": "Christmas Island", "alpha-2": "CX", "alpha-3": "CXR", "numeric": "0162"},
{
"country": "Christmas Island",
"alpha-2": "CX",
"alpha-3": "CXR",
"numeric": "0162",
},
{
"country": "Cocos (Keeling) Islands (the)",
"alpha-2": "CC",
@ -82,22 +107,37 @@ shipping_lookup = [
"numeric": "0180",
},
{"country": "Congo (the)", "alpha-2": "CG", "alpha-3": "COG", "numeric": "0178"},
{"country": "Cook Islands (the)", "alpha-2": "CK", "alpha-3": "COK", "numeric": "0184"},
{
"country": "Cook Islands (the)",
"alpha-2": "CK",
"alpha-3": "COK",
"numeric": "0184",
},
{"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": "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": "Czechia", "alpha-2": "CZ", "alpha-3": "CZE", "numeric": "0203"},
{"country": "Denmark", "alpha-2": "DK", "alpha-3": "DNK", "numeric": "0208"},
{"country": "Djibouti", "alpha-2": "DJ", "alpha-3": "DJI", "numeric": "0262"},
{"country": "Dominica", "alpha-2": "DM", "alpha-3": "DMA", "numeric": "0212"},
{"country": "Dominican Republic (the)", "alpha-2": "DO", "alpha-3": "DOM", "numeric": "0214"},
{
"country": "Dominican Republic (the)",
"alpha-2": "DO",
"alpha-3": "DOM",
"numeric": "0214",
},
{"country": "Ecuador", "alpha-2": "EC", "alpha-3": "ECU", "numeric": "0218"},
{"country": "Egypt", "alpha-2": "EG", "alpha-3": "EGY", "numeric": "0818"},
{"country": "El Salvador", "alpha-2": "SV", "alpha-3": "SLV", "numeric": "0222"},
{"country": "Equatorial Guinea", "alpha-2": "GQ", "alpha-3": "GNQ", "numeric": "0226"},
{
"country": "Equatorial Guinea",
"alpha-2": "GQ",
"alpha-3": "GNQ",
"numeric": "0226",
},
{"country": "Eritrea", "alpha-2": "ER", "alpha-3": "ERI", "numeric": "0232"},
{"country": "Estonia", "alpha-2": "EE", "alpha-3": "EST", "numeric": "0233"},
{"country": "Eswatini", "alpha-2": "SZ", "alpha-3": "SWZ", "numeric": "0748"},
@ -108,12 +148,22 @@ shipping_lookup = [
"alpha-3": "FLK",
"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": "Finland", "alpha-2": "FI", "alpha-3": "FIN", "numeric": "0246"},
{"country": "France", "alpha-2": "FR", "alpha-3": "FRA", "numeric": "0250"},
{"country": "French Guiana", "alpha-2": "GF", "alpha-3": "GUF", "numeric": "0254"},
{"country": "French Polynesia", "alpha-2": "PF", "alpha-3": "PYF", "numeric": "0258"},
{
"country": "French Polynesia",
"alpha-2": "PF",
"alpha-3": "PYF",
"numeric": "0258",
},
{
"country": "French Southern Territories (the)",
"alpha-2": "TF",
@ -150,7 +200,12 @@ shipping_lookup = [
{"country": "Iceland", "alpha-2": "IS", "alpha-3": "ISL", "numeric": "0352"},
{"country": "India", "alpha-2": "IN", "alpha-3": "IND", "numeric": "0356"},
{"country": "Indonesia", "alpha-2": "ID", "alpha-3": "IDN", "numeric": "0360"},
{"country": "Iran (Islamic Republic of)", "alpha-2": "IR", "alpha-3": "IRN", "numeric": "0364"},
{
"country": "Iran (Islamic Republic of)",
"alpha-2": "IR",
"alpha-3": "IRN",
"numeric": "0364",
},
{"country": "Iraq", "alpha-2": "IQ", "alpha-3": "IRQ", "numeric": "0368"},
{"country": "Ireland", "alpha-2": "IE", "alpha-3": "IRL", "numeric": "0372"},
{"country": "Isle of Man", "alpha-2": "IM", "alpha-3": "IMN", "numeric": "0833"},
@ -169,7 +224,12 @@ shipping_lookup = [
"alpha-3": "PRK",
"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": "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": "Mali", "alpha-2": "ML", "alpha-3": "MLI", "numeric": "0466"},
{"country": "Malta", "alpha-2": "MT", "alpha-3": "MLT", "numeric": "0470"},
{"country": "Marshall Islands (the)", "alpha-2": "MH", "alpha-3": "MHL", "numeric": "0584"},
{
"country": "Marshall Islands (the)",
"alpha-2": "MH",
"alpha-3": "MHL",
"numeric": "0584",
},
{"country": "Martinique", "alpha-2": "MQ", "alpha-3": "MTQ", "numeric": "0474"},
{"country": "Mauritania", "alpha-2": "MR", "alpha-3": "MRT", "numeric": "0478"},
{"country": "Mauritius", "alpha-2": "MU", "alpha-3": "MUS", "numeric": "0480"},
@ -211,7 +276,12 @@ shipping_lookup = [
"alpha-3": "FSM",
"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": "Mongolia", "alpha-2": "MN", "alpha-3": "MNG", "numeric": "0496"},
{"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": "Nauru", "alpha-2": "NR", "alpha-3": "NRU", "numeric": "0520"},
{"country": "Nepal", "alpha-2": "NP", "alpha-3": "NPL", "numeric": "0524"},
{"country": "Netherlands (the)", "alpha-2": "NL", "alpha-3": "NLD", "numeric": "0528"},
{
"country": "Netherlands (the)",
"alpha-2": "NL",
"alpha-3": "NLD",
"numeric": "0528",
},
{"country": "New Caledonia", "alpha-2": "NC", "alpha-3": "NCL", "numeric": "0540"},
{"country": "New Zealand", "alpha-2": "NZ", "alpha-3": "NZL", "numeric": "0554"},
{"country": "Nicaragua", "alpha-2": "NI", "alpha-3": "NIC", "numeric": "0558"},
@ -240,32 +315,72 @@ shipping_lookup = [
{"country": "Oman", "alpha-2": "OM", "alpha-3": "OMN", "numeric": "0512"},
{"country": "Pakistan", "alpha-2": "PK", "alpha-3": "PAK", "numeric": "0586"},
{"country": "Palau", "alpha-2": "PW", "alpha-3": "PLW", "numeric": "0585"},
{"country": "Palestine, State of", "alpha-2": "PS", "alpha-3": "PSE", "numeric": "0275"},
{
"country": "Palestine, State of",
"alpha-2": "PS",
"alpha-3": "PSE",
"numeric": "0275",
},
{"country": "Panama", "alpha-2": "PA", "alpha-3": "PAN", "numeric": "0591"},
{"country": "Papua New Guinea", "alpha-2": "PG", "alpha-3": "PNG", "numeric": "0598"},
{
"country": "Papua New Guinea",
"alpha-2": "PG",
"alpha-3": "PNG",
"numeric": "0598",
},
{"country": "Paraguay", "alpha-2": "PY", "alpha-3": "PRY", "numeric": "0600"},
{"country": "Peru", "alpha-2": "PE", "alpha-3": "PER", "numeric": "0604"},
{"country": "Philippines (the)", "alpha-2": "PH", "alpha-3": "PHL", "numeric": "0608"},
{
"country": "Philippines (the)",
"alpha-2": "PH",
"alpha-3": "PHL",
"numeric": "0608",
},
{"country": "Pitcairn", "alpha-2": "PN", "alpha-3": "PCN", "numeric": "0612"},
{"country": "Poland", "alpha-2": "PL", "alpha-3": "POL", "numeric": "0616"},
{"country": "Portugal", "alpha-2": "PT", "alpha-3": "PRT", "numeric": "0620"},
{"country": "Puerto Rico", "alpha-2": "PR", "alpha-3": "PRI", "numeric": "0630"},
{"country": "Qatar", "alpha-2": "QA", "alpha-3": "QAT", "numeric": "0634"},
{"country": "Réunion", "alpha-2": "RE", "alpha-3": "REU", "numeric": "0638"},
{"country": "Reunion", "alpha-2": "RE", "alpha-3": "REU", "numeric": "0638"},
{"country": "Romania", "alpha-2": "RO", "alpha-3": "ROU", "numeric": "0642"},
{"country": "Russian Federation (the)", "alpha-2": "RU", "alpha-3": "RUS", "numeric": "0643"},
{
"country": "Russian Federation (the)",
"alpha-2": "RU",
"alpha-3": "RUS",
"numeric": "0643",
},
{"country": "Rwanda", "alpha-2": "RW", "alpha-3": "RWA", "numeric": "0646"},
{"country": "Saint Barthélemy", "alpha-2": "BL", "alpha-3": "BLM", "numeric": "0652"},
{
"country": "Saint Barthelemy",
"alpha-2": "BL",
"alpha-3": "BLM",
"numeric": "0652",
},
{
"country": "Saint Helena, Ascension and Tristan da Cunha",
"alpha-2": "SH",
"alpha-3": "SHN",
"numeric": "0654",
},
{"country": "Saint Kitts and Nevis", "alpha-2": "KN", "alpha-3": "KNA", "numeric": "0659"},
{
"country": "Saint Kitts and Nevis",
"alpha-2": "KN",
"alpha-3": "KNA",
"numeric": "0659",
},
{"country": "Saint Lucia", "alpha-2": "LC", "alpha-3": "LCA", "numeric": "0662"},
{"country": "Saint Martin (French part)", "alpha-2": "MF", "alpha-3": "MAF", "numeric": "0663"},
{"country": "Saint Pierre and Miquelon", "alpha-2": "PM", "alpha-3": "SPM", "numeric": "0666"},
{
"country": "Saint Martin (French part)",
"alpha-2": "MF",
"alpha-3": "MAF",
"numeric": "0663",
},
{
"country": "Saint Pierre and Miquelon",
"alpha-2": "PM",
"alpha-3": "SPM",
"numeric": "0666",
},
{
"country": "Saint Vincent and the Grenadines",
"alpha-2": "VC",
@ -274,17 +389,32 @@ shipping_lookup = [
},
{"country": "Samoa", "alpha-2": "WS", "alpha-3": "WSM", "numeric": "0882"},
{"country": "San Marino", "alpha-2": "SM", "alpha-3": "SMR", "numeric": "0674"},
{"country": "Sao Tome and Principe", "alpha-2": "ST", "alpha-3": "STP", "numeric": "0678"},
{
"country": "Sao Tome and Principe",
"alpha-2": "ST",
"alpha-3": "STP",
"numeric": "0678",
},
{"country": "Saudi Arabia", "alpha-2": "SA", "alpha-3": "SAU", "numeric": "0682"},
{"country": "Senegal", "alpha-2": "SN", "alpha-3": "SEN", "numeric": "0686"},
{"country": "Serbia", "alpha-2": "RS", "alpha-3": "SRB", "numeric": "0688"},
{"country": "Seychelles", "alpha-2": "SC", "alpha-3": "SYC", "numeric": "0690"},
{"country": "Sierra Leone", "alpha-2": "SL", "alpha-3": "SLE", "numeric": "0694"},
{"country": "Singapore", "alpha-2": "SG", "alpha-3": "SGP", "numeric": "0702"},
{"country": "Sint Maarten (Dutch part)", "alpha-2": "SX", "alpha-3": "SXM", "numeric": "0534"},
{
"country": "Sint Maarten (Dutch part)",
"alpha-2": "SX",
"alpha-3": "SXM",
"numeric": "0534",
},
{"country": "Slovakia", "alpha-2": "SK", "alpha-3": "SVK", "numeric": "0703"},
{"country": "Slovenia", "alpha-2": "SI", "alpha-3": "SVN", "numeric": "0705"},
{"country": "Solomon Islands", "alpha-2": "SB", "alpha-3": "SLB", "numeric": "0090"},
{
"country": "Solomon Islands",
"alpha-2": "SB",
"alpha-3": "SLB",
"numeric": "0090",
},
{"country": "Somalia", "alpha-2": "SO", "alpha-3": "SOM", "numeric": "0706"},
{"country": "South Africa", "alpha-2": "ZA", "alpha-3": "ZAF", "numeric": "0710"},
{
@ -298,11 +428,26 @@ shipping_lookup = [
{"country": "Sri Lanka", "alpha-2": "LK", "alpha-3": "LKA", "numeric": "0144"},
{"country": "Sudan (the)", "alpha-2": "SD", "alpha-3": "SDN", "numeric": "0729"},
{"country": "Suriname", "alpha-2": "SR", "alpha-3": "SUR", "numeric": "0740"},
{"country": "Svalbard and Jan Mayen", "alpha-2": "SJ", "alpha-3": "SJM", "numeric": "0744"},
{
"country": "Svalbard and Jan Mayen",
"alpha-2": "SJ",
"alpha-3": "SJM",
"numeric": "0744",
},
{"country": "Sweden", "alpha-2": "SE", "alpha-3": "SWE", "numeric": "0752"},
{"country": "Switzerland", "alpha-2": "CH", "alpha-3": "CHE", "numeric": "0756"},
{"country": "Syrian Arab Republic", "alpha-2": "SY", "alpha-3": "SYR", "numeric": "0760"},
{"country": "Taiwan (Province of China)", "alpha-2": "TW", "alpha-3": "TWN", "numeric": "0158"},
{
"country": "Syrian Arab Republic",
"alpha-2": "SY",
"alpha-3": "SYR",
"numeric": "0760",
},
{
"country": "Taiwan (Province of China)",
"alpha-2": "TW",
"alpha-3": "TWN",
"numeric": "0158",
},
{"country": "Tajikistan", "alpha-2": "TJ", "alpha-3": "TJK", "numeric": "0762"},
{
"country": "Tanzania, United Republic of",
@ -315,7 +460,12 @@ shipping_lookup = [
{"country": "Togo", "alpha-2": "TG", "alpha-3": "TGO", "numeric": "0768"},
{"country": "Tokelau", "alpha-2": "TK", "alpha-3": "TKL", "numeric": "0772"},
{"country": "Tonga", "alpha-2": "TO", "alpha-3": "TON", "numeric": "0776"},
{"country": "Trinidad and Tobago", "alpha-2": "TT", "alpha-3": "TTO", "numeric": "0780"},
{
"country": "Trinidad and Tobago",
"alpha-2": "TT",
"alpha-3": "TTO",
"numeric": "0780",
},
{"country": "Tunisia", "alpha-2": "TN", "alpha-3": "TUN", "numeric": "0788"},
{"country": "Turkey", "alpha-2": "TR", "alpha-3": "TUR", "numeric": "0792"},
{"country": "Turkmenistan", "alpha-2": "TM", "alpha-3": "TKM", "numeric": "0795"},
@ -328,7 +478,12 @@ shipping_lookup = [
{"country": "Tuvalu", "alpha-2": "TV", "alpha-3": "TUV", "numeric": "0798"},
{"country": "Uganda", "alpha-2": "UG", "alpha-3": "UGA", "numeric": "0800"},
{"country": "Ukraine", "alpha-2": "UA", "alpha-3": "UKR", "numeric": "0804"},
{"country": "United Arab Emirates (the)", "alpha-2": "AE", "alpha-3": "ARE", "numeric": "0784"},
{
"country": "United Arab Emirates (the)",
"alpha-2": "AE",
"alpha-3": "ARE",
"numeric": "0784",
},
{
"country": "United Kingdom of Great Britain and Northern Ireland (the)",
"alpha-2": "GB",
@ -357,258 +512,26 @@ shipping_lookup = [
"numeric": "0862",
},
{"country": "Viet Nam", "alpha-2": "VN", "alpha-3": "VNM", "numeric": "0704"},
{"country": "Virgin Islands (British)", "alpha-2": "VG", "alpha-3": "VGB", "numeric": "0092"},
{"country": "Virgin Islands (U.S.)", "alpha-2": "VI", "alpha-3": "VIR", "numeric": "0850"},
{"country": "Wallis and Futuna", "alpha-2": "WF", "alpha-3": "WLF", "numeric": "0876"},
{
"country": "Virgin Islands (British)",
"alpha-2": "VG",
"alpha-3": "VGB",
"numeric": "0092",
},
{
"country": "Virgin Islands (U.S.)",
"alpha-2": "VI",
"alpha-3": "VIR",
"numeric": "0850",
},
{
"country": "Wallis and Futuna",
"alpha-2": "WF",
"alpha-3": "WLF",
"numeric": "0876",
},
{"country": "Western Sahara", "alpha-2": "EH", "alpha-3": "ESH", "numeric": "0732"},
{"country": "Yemen", "alpha-2": "YE", "alpha-3": "YEM", "numeric": "0887"},
{"country": "Zambia", "alpha-2": "ZM", "alpha-3": "ZMB", "numeric": "0894"},
{"country": "Zimbabwe", "alpha-2": "ZW", "alpha-3": "ZWE", "numeric": "0716"},
]
# TODO: Implement lookup for this. Currently not doable
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 = [
EmbedField(
name="Moderator",
value=f"{admin.mention} ({admin.username}#{admin.discriminator})",
value=f"{admin.mention} ({admin.username})",
),
]
if log and log.reason:
@ -59,7 +59,7 @@ def modlog_embed(
timestamp=log.created_at,
)
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

View file

@ -68,19 +68,31 @@ class ModcaseCog(Extension):
return
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:
self.logger.warning("Missing action %s, exiting", name)
return
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 = (
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(
title="Admin action taken",
@ -89,16 +101,24 @@ class ModcaseCog(Extension):
)
if name == "Mute":
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}"
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)
try:
await user.send(embeds=embed)
except Exception:
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:
m_action = Action(action_type=name.lower(), parent=action.id)
@ -106,7 +126,9 @@ class ModcaseCog(Extension):
await modlog.save()
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:
return
@ -114,7 +136,11 @@ class ModcaseCog(Extension):
if channel:
fields = (
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),
)
embed = build_embed(
@ -122,23 +148,31 @@ class ModcaseCog(Extension):
description=f"Admin action has been taken against {user.mention}",
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}")
if name == "Mute":
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)
lookup_key = f"{user.id}|{ctx.guild.id}"
async with self.bot.redis.lock("lock|" + 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
channel = await ctx.guild.fetch_channel(modlog.value)
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()
return
@ -150,14 +184,22 @@ class ModcaseCog(Extension):
avatar_url = user.avatar.url
if isinstance(user, Member):
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 = [
ActionRow(
Button(style=ButtonStyle.RED, emoji="✖️", custom_id="modcase|no"),
Button(style=ButtonStyle.GREEN, emoji="✔️", custom_id="modcase|yes"),
Button(
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)
await self.bot.redis.set(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))
await self.bot.redis.set(
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>"]
[tool.poetry.dependencies]
python = ">=3.10,<4"
python = ">=3.11,<4"
PyYAML = "^6.0"
GitPython = "^3.1.26"
opencv-python = "^4.5.5"
@ -14,7 +14,7 @@ psutil = "^5.9.0"
python-gitlab = "^3.1.1"
ulid-py = "^1.1.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"
pastypy = "^1.0.3.post1" # Mine
dateparser = "^1.1.1"
@ -24,15 +24,19 @@ rich = "^12.3.0"
jurigged = "^0.5.3" # Contributed
ansitoimg = "^2022.1"
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"
calculator = {git = "https://git.zevaryx.com/zevaryx/calculator.git"} # Mine
calculator = { git = "https://git.zevaryx.com/zevaryx/calculator.git" } # Mine
redis = "^4.4.0"
interactions = {git = "https://github.com/interactions-py/interactions.py", rev = "5.x"}
statipy = {git = "https://github.com/zevaryx/statipy", rev = "main"}
interactions-py = ">=5.3,<6"
statipy = { git = "https://github.com/zevaryx/statipy", rev = "main" }
beanie = "^1.17.0"
pydantic = "^1.10.7"
pydantic = ">=2.3.0,<3"
orjson = "^3.8.8"
croniter = "^1.4.1"
erapi = { git = "https://git.zevaryx.com/zevaryx-technologies/erapi.git" }
[tool.poetry.group.dev.dependencies]
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=