Massive update to beanie, ipy5, re-work and update of many commands
This commit is contained in:
parent
910ac2c304
commit
f594f02b1b
34 changed files with 2656 additions and 2686 deletions
|
@ -8,6 +8,8 @@
|
|||
!/jarvis_small.png
|
||||
!/run.py
|
||||
!/config.yaml
|
||||
# Needed for jarvis-compose
|
||||
!/.git
|
||||
|
||||
# Block other files
|
||||
**/__pycache__
|
||||
|
|
30
README.md
30
README.md
|
@ -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 />
|
||||
|
||||
[]()
|
||||
[](https://git.zevaryx.com/stark-industries/jarvis/jarvis-bot)
|
||||
|
@ -12,28 +12,22 @@
|
|||
[](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`
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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}"
|
||||
)
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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"""
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")
|
|
@ -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")
|
|
@ -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(
|
||||
"""
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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"""
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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": "Cô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",
|
||||
]
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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
1612
poetry.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -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
38
sample.env
Normal 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=
|
Loading…
Add table
Reference in a new issue