Merge Alpha 1 into dev

Closes #108, #111, #120, #121, #122

See merge request stark-industries/j.a.r.v.i.s.!49
This commit is contained in:
Zeva Rose 2022-03-19 02:18:10 +00:00
commit 3c26e3406a
50 changed files with 1597 additions and 2567 deletions

View file

@ -26,7 +26,7 @@ repos:
language_version: python3.10
- repo: https://github.com/pre-commit/mirrors-isort
rev: V5.10.1
rev: v5.10.1
hooks:
- id: isort
args: ["--profile", "black"]
@ -37,7 +37,7 @@ repos:
- id: flake8
additional_dependencies:
- flake8-annotations~=2.0
- flake8-bandit~=2.1
#- flake8-bandit~=2.1
- flake8-docstrings~=1.5
- flake8-bugbear
- flake8-comprehensions

13
LICENSE Normal file
View file

@ -0,0 +1,13 @@
“Commons Clause” License Condition v1.0
The Software is provided to you by the Licensor under the License, as defined below, subject to the following condition.
Without limiting other conditions in the License, the grant of rights under the License will not include, and the License does not grant to you, the right to Sell the Software.
For purposes of the foregoing, “Sell” means practicing any or all of the rights granted to you under the License to provide to third parties, for a fee or other consideration (including without limitation fees for hosting or consulting/ support services related to the Software), a product or service whose value derives, entirely or substantially, from the functionality of the Software. Any license notice or attribution required by the License must also include this Commons Clause License Condition notice.
Software: JARVIS
License: Expat License
Licensor: zevaryx

55
PRIVACY.md Normal file
View file

@ -0,0 +1,55 @@
# Privacy Policy
Your privacy is important to us. It is JARVIS' policy to respect your privacy and comply with any applicable law and regulation regarding any personal information we may collect about you through our JARVIS bot.
This policy is effective as of 20 March 2022 and was last updated on 10 March 2022.
## Information We Collect
Information we collect includes both information you knowingly and actively provide us when using or participating in any of our services and promotions, and any information automatically sent by your devices in the course of accessing our products and services.
## Log Data
When you use our JARVIS services, if opted in to usage data collection services, we may collect data in certain cicumstances:
- Administrative activity (i.e. ban, warn, mute)
- Your User ID (either as admin or as the recipient of the activity)
- Guild ID of activity origin
- Your discriminator at time of activity (bans only)
- Your username at time of activity (bans only)
- Admin commands
- User ID of admin who executes admin command
- Reminders
- Your User ID
- The guild in which the command originated
- The channel in which the command originated
- Private text entered via the command
- Starboard
- Message ID of starred message
- Channel ID of starred message
- Guild ID of origin guild
- Automated activity logging
- We store no information about users who edit nicknames, join/leave servers, or other related activities. However, this information, if configured by server admins, is relayed into a Discord channel and is not automatically deleted, nor do we have control over this information.
- This information is also stored by Discord via their Audit Log, which we also have no control over. Please contact Discord for their own privacy policy and asking about your rights on their platform.
## Use of Information
We use the information we collect to provide, maintain, and improve our services. Common uses where this data may be used includes sending reminders, helping administrate Discord servers, and providing extra utilities into Discord based on user content.
## Security of Your Personal Information
Although we will do our best to protect the personal information you provide to us, we advise that no method of electronic transmission or storage is 100% secure, and no one can guarantee absolute data security. We will comply with laws applicable to us in respect of any data breach.
## How Long We Keep Your Personal Information
We keep your personal information only for as long as we need to. This time period may depend on what we are using your information for, in accordance with this privacy policy. If your personal information is no longer required, we will delete it or make it anonymous by removing all details that identify you.
## Your Rights and Controlling Your Personal Information
You may request access to your personal information, and change what you are okay with us collecting from you. You may also request that we delete your personal identifying information. Please message **zevaryx#5779** on Discord, or join the Discord server at https://discord.gg/4TuFvW5n and ask in there.
## Limits of Our Policy
Our website may link to external sites that are not operated by us (ie. discord.com). Please be aware that we have no control over the content and policies of those sites, and cannot accept responsibility or liability for their respective privacy practices.
## Changes to This Policy
At our discretion, we may change our privacy policy to reflect updates to our business processes, current acceptable practices, or legislative or regulatory changes. If we decide to change this privacy policy, we will post the changes here at the same link by which you are accessing this privacy policy.
## Contact Us
For any questions or concerns regarding your privacy, you may contact us using the following details:
### Discord
#### zevaryx#5779
#### https://discord.gg/4TuFvW5n

15
TERMS.md Normal file
View file

@ -0,0 +1,15 @@
# Terms Of Use
Please just be reasonable do not try to mess with the bot in a malicious way. This includes but is not limited to:
- Spamming
- Flooding
- Hacking
- DOS Attacks
## Contact Us
For any questions or concerns regarding the terms, feel free to contact us:
### Discord
#### zevaryx#5779
#### https://discord.gg/4TuFvW5n

View file

@ -1,13 +1,18 @@
"""Main J.A.R.V.I.S. package."""
import logging
from importlib.metadata import version as _v
from dis_snek import Intents, Snake, listen
from mongoengine import connect
from dis_snek import Intents
from jarvis_core.db import connect
# from jarvis import logo # noqa: F401
from jarvis import tasks, utils
from jarvis import utils
from jarvis.client import Jarvis
from jarvis.config import get_config
from jarvis.events import member, message
try:
__version__ = _v("jarvis")
except Exception:
__version__ = "0.0.0"
jconfig = get_config()
@ -17,60 +22,20 @@ file_handler = logging.FileHandler(filename="jarvis.log", encoding="UTF-8", mode
file_handler.setFormatter(logging.Formatter("[%(asctime)s][%(levelname)s][%(name)s] %(message)s"))
logger.addHandler(file_handler)
intents = Intents.DEFAULT
intents.members = True
intents = Intents.DEFAULT | Intents.MESSAGES | Intents.GUILD_MEMBERS | Intents.GUILD_MESSAGES
restart_ctx = None
jarvis = Snake(intents=intents, default_prefix="!", sync_interactions=jconfig.sync)
__version__ = "2.0.0a0"
jarvis = Jarvis(intents=intents, default_prefix="!", sync_interactions=jconfig.sync)
@listen()
async def on_ready() -> None:
"""Lepton on_ready override."""
global restart_ctx
print(" Logged in as {0.user}".format(jarvis)) # noqa: T001
print(" Connected to {} guild(s)".format(len(jarvis.guilds))) # noqa: T001
@listen()
async def on_startup() -> None:
"""Lepton on_startup override."""
tasks.init()
def run() -> None:
async def run() -> None:
"""Run J.A.R.V.I.S."""
connect(
db="ctc2",
alias="ctc2",
authentication_source="admin",
**jconfig.mongo["connect"],
)
connect(
db=jconfig.mongo["database"],
alias="main",
authentication_source="admin",
**jconfig.mongo["connect"],
)
connect(**jconfig.mongo["connect"], testing=jconfig.mongo["database"] != "jarvis")
jconfig.get_db_config()
for extension in utils.get_extensions():
jarvis.load_extension(extension)
print( # noqa: T001
" https://discord.com/api/oauth2/authorize?client_id="
"{}&permissions=8&scope=bot%20applications.commands".format(jconfig.client_id)
)
jarvis.max_messages = jconfig.max_messages
# Add event listeners
if jconfig.events:
_ = [
member.MemberEventHandler(jarvis),
message.MessageEventHandler(jarvis),
]
jarvis.start(jconfig.token)
await jarvis.astart(jconfig.token)

464
jarvis/client.py Normal file
View file

@ -0,0 +1,464 @@
"""Custom JARVIS client."""
import re
import traceback
from datetime import datetime
from aiohttp import ClientSession
from dis_snek import Snake, listen
from dis_snek.api.events.discord import MessageCreate, MessageDelete, MessageUpdate
from dis_snek.client.utils.misc_utils import find_all
from dis_snek.models.discord.channel import DMChannel
from dis_snek.models.discord.embed import EmbedField
from dis_snek.models.discord.enums import Permissions
from dis_snek.models.discord.message import Message
from dis_snek.models.discord.user import Member
from dis_snek.models.snek.context import Context, InteractionContext
from dis_snek.models.snek.tasks.task import Task
from dis_snek.models.snek.tasks.triggers import IntervalTrigger
from jarvis_core.db import q
from jarvis_core.db.models import Autopurge, Autoreact, Roleping, Setting, Warning
from jarvis_core.filters import invites, url
from jarvis_core.util import build_embed
from jarvis_core.util.ansi import RESET, Fore, Format, fmt
from pastypy import AsyncPaste as Paste
from jarvis.utils.embeds import warning_embed
DEFAULT_GUILD = 862402786116763668
DEFAULT_ERROR_CHANNEL = 943395824560394250
DEFAULT_SITE = "https://paste.zevs.me"
ERROR_MSG = """
Command Information:
Name: {invoked_name}
Args:
{arg_str}
Callback:
Args:
{callback_args}
Kwargs:
{callback_kwargs}
"""
KEY_FMT = fmt(Fore.GRAY)
VAL_FMT = fmt(Fore.WHITE)
CMD_FMT = fmt(Fore.GREEN, Format.BOLD)
class Jarvis(Snake):
def __init__(self, *args, **kwargs): # noqa: ANN002 ANN003
super().__init__(*args, **kwargs)
self.phishing_domains = []
@Task.create(IntervalTrigger(days=1))
async def _update_domains(self) -> None:
async with ClientSession(headers={"X-Identity": "Discord: zevaryx#5779"}) as session:
response = await session.get("https://phish.sinking.yachts/v2/recent/86415")
response.raise_for_status()
data = await response.json()
for update in data:
if update["type"] == "add":
if update["domain"] not in self.phishing_domains:
self.phishing_domains.append(update["domain"])
elif update["type"] == "delete":
if update["domain"] in self.phishing_domains:
self.phishing_domains.remove(update["domain"])
async def _sync_domains(self) -> None:
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()
@listen()
async def on_ready(self) -> None:
"""Lepton on_ready override."""
await self._sync_domains()
self._update_domains.start()
print("Logged in as {}".format(self.user)) # noqa: T001
print("Connected to {} guild(s)".format(len(self.guilds))) # noqa: T001
print( # noqa: T001
"https://discord.com/api/oauth2/authorize?client_id="
"{}&permissions=8&scope=bot%20applications.commands".format(self.user.id)
)
async def on_command_error(
self, ctx: Context, error: Exception, *args: list, **kwargs: dict
) -> None:
"""Lepton on_command_error override."""
guild = await self.fetch_guild(DEFAULT_GUILD)
channel = await guild.fetch_channel(DEFAULT_ERROR_CHANNEL)
error_time = datetime.utcnow().strftime("%d-%m-%Y %H:%M-%S.%f UTC")
timestamp = int(datetime.now().timestamp())
timestamp = f"<t:{timestamp}:T>"
arg_str = (
"\n".join(f" {k}: {v}" for k, v in ctx.kwargs.items()) if ctx.kwargs else " None"
)
callback_args = "\n".join(f" - {i}" for i in args) if args else " None"
callback_kwargs = (
"\n".join(f" {k}: {v}" for k, v in kwargs.items()) if kwargs else " None"
)
full_message = ERROR_MSG.format(
error_time=error_time,
invoked_name=ctx.invoked_name,
arg_str=arg_str,
callback_args=callback_args,
callback_kwargs=callback_kwargs,
)
if len(full_message) >= 1900:
error_message = " ".join(traceback.format_exception(error))
full_message += "Exception: |\n " + error_message
paste = Paste(content=full_message)
await paste.save(DEFAULT_SITE)
await channel.send(
f"JARVIS encountered an error at {timestamp}. Log too big to send over Discord."
f"\nPlease see log at {paste.url}"
)
else:
error_message = "".join(traceback.format_exception(error))
await channel.send(
f"JARVIS encountered an error at {timestamp}:"
f"\n```yaml\n{full_message}\n```"
f"\nException:\n```py\n{error_message}\n```"
)
await ctx.send("Whoops! Encountered an error. The error has been logged.", ephemeral=True)
return await super().on_command_error(ctx, error, *args, **kwargs)
# Modlog
async def on_command(self, ctx: InteractionContext) -> None:
"""Lepton on_command override."""
if not isinstance(ctx.channel, DMChannel) and ctx.invoked_name not in ["pw"]:
modlog = await Setting.find_one(q(guild=ctx.guild.id, setting="modlog"))
if modlog:
channel = await ctx.guild.fetch_channel(modlog.value)
args = " ".join(f"{KEY_FMT}{k}:{VAL_FMT}{v}{RESET}" for k, v in ctx.kwargs.items())
fields = [
EmbedField(
name="Command",
value=f"```ansi\n{CMD_FMT}{ctx.invoked_name}{RESET} {args}\n```",
inline=False,
),
]
embed = build_embed(
title="Command Invoked",
description=f"{ctx.author.mention} invoked a command",
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}"
)
await channel.send(embed=embed)
# Events
async def on_member_join(self, user: Member) -> None:
"""Handle on_member_join event."""
guild = user.guild
unverified = await Setting.find_one(q(guild=guild.id, setting="unverified"))
if unverified:
role = guild.get_role(unverified.value)
if role not in user.roles:
await user.add_role(role, reason="User just joined and is unverified")
async def autopurge(self, message: Message) -> None:
"""Handle autopurge events."""
autopurge = await Autopurge.find_one(q(guild=message.guild.id, channel=message.channel.id))
if autopurge:
await message.delete(delay=autopurge.delay)
async def autoreact(self, message: Message) -> None:
"""Handle autoreact events."""
autoreact = await Autoreact.find_one(
q(
guild=message.guild.id,
channel=message.channel.id,
)
)
if autoreact:
for reaction in autoreact.reactions:
await message.add_reaction(reaction)
async def checks(self, message: Message) -> None:
"""Other message checks."""
# #tech
# channel = find(lambda x: x.id == 599068193339736096, message._mention_ids)
# if channel and message.author.id == 293795462752894976:
# await channel.send(
# content="https://cdn.discordapp.com/attachments/664621130044407838/805218508866453554/tech.gif" # noqa: E501
# )
content = re.sub(r"\s+", "", message.content)
match = invites.search(content)
setting = await Setting.find_one(q(guild=message.guild.id, setting="noinvite"))
if not setting:
setting = Setting(guild=message.guild.id, setting="noinvite", value=True)
await setting.commit()
if match:
guild_invites = await message.guild.invites()
guild_invites.append(message.guild.vanity_url_code)
allowed = [x.code for x in guild_invites] + [
"dbrand",
"VtgZntXcnZ",
"gPfYGbvTCE",
]
if match.group(1) not in allowed and setting.value:
await message.delete()
w = Warning(
active=True,
admin=self.user.id,
duration=24,
guild=message.guild.id,
reason="Sent an invite link",
user=message.author.id,
)
await w.commit()
embed = warning_embed(message.author, "Sent an invite link")
await message.channel.send(embed=embed)
async def massmention(self, message: Message) -> None:
"""Handle massmention events."""
massmention = await Setting.find_one(
q(
guild=message.guild.id,
setting="massmention",
)
)
if (
massmention
and massmention.value > 0 # noqa: W503
and len(message._mention_ids + message._mention_roles) # noqa: W503
- (1 if message.author.id in message._mention_ids else 0) # noqa: W503
> massmention.value # noqa: W503
):
w = Warning(
active=True,
admin=self.user.id,
duration=24,
guild=message.guild.id,
reason="Mass Mention",
user=message.author.id,
)
await w.commit()
embed = warning_embed(message.author, "Mass Mention")
await message.channel.send(embed=embed)
async def roleping(self, message: Message) -> None:
"""Handle roleping events."""
if await Roleping.collection.count_documents(q(guild=message.guild.id, active=True)) == 0:
return
rolepings = await Roleping.find(q(guild=message.guild.id, active=True)).to_list(None)
# Get all role IDs involved with message
roles = []
async for mention in message.mention_roles:
roles.append(mention.id)
async for mention in message.mention_users:
for role in mention.roles:
roles.append(role.id)
if not roles:
return
# Get all roles that are rolepinged
roleping_ids = [r.role for r in rolepings]
# Get roles in rolepings
role_in_rolepings = find_all(lambda x: x in roleping_ids, roles)
# Check if the user has the role, so they are allowed to ping it
user_missing_role = any(x.id not in roleping_ids for x in message.author.roles)
# Admins can ping whoever
user_is_admin = message.author.has_permission(Permissions.ADMINISTRATOR)
# Check if user in a bypass list
user_has_bypass = False
for roleping in rolepings:
if message.author.id in roleping.bypass["users"]:
user_has_bypass = True
break
if any(role.id in roleping.bypass["roles"] for role in message.author.roles):
user_has_bypass = True
break
if role_in_rolepings and user_missing_role and not user_is_admin and not user_has_bypass:
w = Warning(
active=True,
admin=self.user.id,
duration=24,
guild=message.guild.id,
reason="Pinged a blocked role/user with a blocked role",
user=message.author.id,
)
await w.commit()
embed = warning_embed(message.author, "Pinged a blocked role/user with a blocked role")
await message.channel.send(embed=embed)
async def phishing(self, message: Message) -> None:
"""Check if the message contains any known phishing domains."""
for match in url.finditer(message.content):
if match.group("domain") in self.phishing_domains:
w = Warning(
active=True,
admin=self.user.id,
duration=24,
guild=message.guild.id,
reason="Phishing URL",
user=message.author.id,
)
await w.commit()
embed = warning_embed(message.author, "Phishing URL")
await message.channel.send(embed=embed)
await message.delete()
return True
return False
async def malicious_url(self, message: Message) -> None:
"""Check if the message contains any known phishing domains."""
for match in url.finditer(message.content):
async with ClientSession() as session:
resp = await session.get(
"https://spoopy.oceanlord.me/api/check_website", json={"website": match.string}
)
if resp.status != 200:
break
data = await resp.json()
for item in data["processed"]["urls"].values():
if not item["safe"]:
w = Warning(
active=True,
admin=self.user.id,
duration=24,
guild=message.guild.id,
reason="Unsafe URL",
user=message.author.id,
)
await w.commit()
reasons = ", ".join(item["not_safe_reasons"])
embed = warning_embed(message.author, reasons)
await message.channel.send(embed=embed)
await message.delete()
return True
return False
@listen()
async def on_message(self, event: MessageCreate) -> None:
"""Handle on_message event. Calls other event handlers."""
message = event.message
if not isinstance(message.channel, DMChannel) and not message.author.bot:
await self.autoreact(message)
await self.massmention(message)
await self.roleping(message)
await self.autopurge(message)
await self.checks(message)
if not await self.phishing(message):
await self.malicious_url(message)
@listen()
async def on_message_edit(self, event: MessageUpdate) -> None:
"""Process on_message_edit events."""
before = event.before
after = event.after
if not after.author.bot:
modlog = await Setting.find_one(q(guild=after.guild.id, setting="modlog"))
if modlog:
if not before or before.content == after.content or before.content is None:
return
channel = before.guild.get_channel(modlog.value)
fields = [
EmbedField(
"Original Message",
before.content if before.content else "N/A",
False,
),
EmbedField(
"New Message",
after.content if after.content else "N/A",
False,
),
]
embed = build_embed(
title="Message Edited",
description=f"{after.author.mention} edited a message",
fields=fields,
color="#fc9e3f",
timestamp=after.edited_timestamp,
url=after.jump_url,
)
embed.set_author(
name=after.author.username,
icon_url=after.author.display_avatar.url,
url=after.jump_url,
)
embed.set_footer(
text=f"{after.author.user.username}#{after.author.discriminator} | {after.author.id}"
)
await channel.send(embed=embed)
if not isinstance(after.channel, DMChannel) and not after.author.bot:
await self.massmention(after)
await self.roleping(after)
await self.checks(after)
await self.roleping(after)
await self.checks(after)
if not await self.phishing(after):
await self.malicious_url(after)
@listen()
async def on_message_delete(self, event: MessageDelete) -> None:
"""Process on_message_delete events."""
message = event.message
modlog = await Setting.find_one(q(guild=message.guild.id, setting="modlog"))
if modlog:
fields = [EmbedField("Original Message", message.content or "N/A", False)]
if message.attachments:
value = "\n".join([f"[{x.filename}]({x.url})" for x in message.attachments])
fields.append(
EmbedField(
name="Attachments",
value=value,
inline=False,
)
)
if message.sticker_items:
value = "\n".join([f"Sticker: {x.name}" for x in message.sticker_items])
fields.append(
EmbedField(
name="Stickers",
value=value,
inline=False,
)
)
if message.embeds:
value = str(len(message.embeds)) + " embeds"
fields.append(
EmbedField(
name="Embeds",
value=value,
inline=False,
)
)
channel = message.guild.get_channel(modlog.value)
embed = build_embed(
title="Message Deleted",
description=f"{message.author.mention}'s message was deleted",
fields=fields,
color="#fc9e3f",
)
embed.set_author(
name=message.author.username,
icon_url=message.author.display_avatar.url,
url=message.jump_url,
)
embed.set_footer(
text=f"{message.author.user.username}#{message.author.discriminator} | {message.author.id}"
)
await channel.send(embed=embed)

View file

@ -1,8 +1,8 @@
"""J.A.R.V.I.S. BanCog."""
import re
from datetime import datetime, timedelta
from dis_snek import InteractionContext, Permissions, Snake
from dis_snek import InteractionContext, Permissions, Scale
from dis_snek.client.utils.misc_utils import find, find_all
from dis_snek.ext.paginators import Paginator
from dis_snek.models.discord.embed import EmbedField
from dis_snek.models.discord.user import User
@ -13,19 +13,16 @@ from dis_snek.models.snek.application_commands import (
slash_option,
)
from dis_snek.models.snek.command import check
from jarvis_core.db import q
from jarvis_core.db.models import Ban, Unban
from jarvis.db.models import Ban, Unban
from jarvis.utils import build_embed, find, find_all
from jarvis.utils.cachecog import CacheCog
from jarvis.utils import build_embed
from jarvis.utils.permissions import admin_or_permissions
class BanCog(CacheCog):
class BanCog(Scale):
"""J.A.R.V.I.S. BanCog."""
def __init__(self, bot: Snake):
super().__init__(bot)
async def discord_apply_ban(
self,
ctx: InteractionContext,
@ -38,7 +35,7 @@ class BanCog(CacheCog):
) -> None:
"""Apply a Discord ban."""
await ctx.guild.ban(user, reason=reason)
_ = Ban(
b = Ban(
user=user.id,
username=user.username,
discrim=user.discriminator,
@ -48,7 +45,8 @@ class BanCog(CacheCog):
type=mtype,
duration=duration,
active=active,
).save()
)
await b.commit()
embed = build_embed(
title="User Banned",
@ -68,14 +66,15 @@ class BanCog(CacheCog):
async def discord_apply_unban(self, ctx: InteractionContext, user: User, reason: str) -> None:
"""Apply a Discord unban."""
await ctx.guild.unban(user, reason=reason)
_ = Unban(
u = Unban(
user=user.id,
username=user.username,
discrim=user.discriminator,
guild=ctx.guild.id,
admin=ctx.author.id,
reason=reason,
).save()
)
await u.commit()
embed = build_embed(
title="User Unbanned",
@ -242,7 +241,9 @@ class BanCog(CacheCog):
# We take advantage of the previous checks to save CPU cycles
if not discord_ban_info:
if isinstance(user, int):
database_ban_info = Ban.objects(guild=ctx.guild.id, user=user, active=True).first()
database_ban_info = await Ban.find_one(
q(guild=ctx.guild.id, user=user, active=True)
)
else:
search = {
"guild": ctx.guild.id,
@ -251,15 +252,16 @@ class BanCog(CacheCog):
}
if discrim:
search["discrim"] = discrim
database_ban_info = Ban.objects(**search).first()
database_ban_info = await Ban.find_one(q(**search))
if not discord_ban_info and not database_ban_info:
await ctx.send(f"Unable to find user {orig_user}", ephemeral=True)
elif discord_ban_info:
elif discord_ban_info and not database_ban_info:
await self.discord_apply_unban(ctx, discord_ban_info.user, reason)
else:
discord_ban_info = find(lambda x: x.user.id == database_ban_info["id"], bans)
discord_ban_info = find(lambda x: x.user.id == database_ban_info.id, bans)
if discord_ban_info:
await self.discord_apply_unban(ctx, discord_ban_info.user, reason)
else:
@ -273,9 +275,7 @@ class BanCog(CacheCog):
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.")
@slash_command(
name="bans", description="User bans", sub_cmd_name="list", sub_cmd_description="List bans"
@ -300,26 +300,18 @@ class BanCog(CacheCog):
choices=[SlashCommandChoice(name="Yes", value=1), SlashCommandChoice(name="No", value=0)],
)
@check(admin_or_permissions(Permissions.BAN_MEMBERS))
async def _bans_list(self, ctx: InteractionContext, type: int = 0, active: int = 1) -> None:
async def _bans_list(self, ctx: InteractionContext, btype: int = 0, active: int = 1) -> None:
active = bool(active)
exists = self.check_cache(ctx, type=type, active=active)
if exists:
await ctx.defer(ephemeral=True)
await ctx.send(
f"Please use existing interaction: {exists['paginator']._message.jump_url}",
ephemeral=True,
)
return
types = [0, "perm", "temp", "soft"]
search = {"guild": ctx.guild.id}
if active:
search["active"] = True
if type > 0:
search["type"] = types[type]
bans = Ban.objects(**search).order_by("-created_at")
if btype > 0:
search["type"] = types[btype]
bans = Ban.find(search).sort([("created_at", -1)])
db_bans = []
fields = []
for ban in bans:
async for ban in bans:
if not ban.username:
user = await self.bot.fetch_user(ban.user)
ban.username = user.username if user else "[deleted user]"
@ -355,9 +347,9 @@ class BanCog(CacheCog):
pages = []
title = "Active " if active else "Inactive "
if type > 0:
title += types[type]
if type == 1:
if btype > 0:
title += types[btype]
if btype == 1:
title += "a"
title += "bans"
if len(fields) == 0:
@ -376,14 +368,4 @@ class BanCog(CacheCog):
paginator = Paginator.create_from_embeds(self.bot, *pages, timeout=300)
self.cache[hash(paginator)] = {
"guild": ctx.guild.id,
"user": ctx.author.id,
"timeout": datetime.utcnow() + timedelta(minutes=5),
"command": ctx.subcommand_name,
"type": type,
"active": active,
"paginator": paginator,
}
await paginator.send(ctx)

View file

@ -8,8 +8,8 @@ from dis_snek.models.snek.application_commands import (
slash_option,
)
from dis_snek.models.snek.command import check
from jarvis_core.db.models import Kick
from jarvis.db.models import Kick
from jarvis.utils import build_embed
from jarvis.utils.permissions import admin_or_permissions
@ -33,6 +33,7 @@ class KickCog(Scale):
if len(reason) > 100:
await ctx.send("Reason must be < 100 characters", ephemeral=True)
return
guild_name = ctx.guild.name
embed = build_embed(
title=f"You have been kicked from {guild_name}",
@ -64,10 +65,11 @@ class KickCog(Scale):
embed.set_thumbnail(url=user.display_avatar.url)
embed.set_footer(text=f"{user.username}#{user.discriminator} | {user.id}")
await ctx.send(embed=embed)
_ = Kick(
k = Kick(
user=user.id,
reason=reason,
admin=ctx.author.id,
guild=ctx.guild.id,
).save()
)
await k.commit()
await ctx.send(embed=embed)

View file

@ -1,7 +1,7 @@
"""J.A.R.V.I.S. LockCog."""
from dis_snek import Scale
# TODO: Uncomment 99% of code once implementation is figured out
# from dis_snek import Scale
#
# # TODO: Uncomment 99% of code once implementation is figured out
# from contextlib import suppress
# from typing import Union
#
@ -20,94 +20,94 @@ from dis_snek import Scale
#
# from jarvis.db.models import Lock
# from jarvis.utils.permissions import admin_or_permissions
class LockCog(Scale):
"""J.A.R.V.I.S. LockCog."""
# @slash_command(name="lock", description="Lock a channel")
# @slash_option(name="reason",
# description="Lock Reason",
# opt_type=3,
# required=True,)
# @slash_option(name="duration",
# description="Lock duration in minutes (default 10)",
# opt_type=4,
# required=False,)
# @slash_option(name="channel",
# description="Channel to lock",
# opt_type=7,
# required=False,)
# @check(admin_or_permissions(Permissions.MANAGE_CHANNELS))
# async def _lock(
# self,
# ctx: InteractionContext,
# reason: str,
# duration: int = 10,
# channel: Union[GuildText, GuildVoice] = None,
# ) -> None:
# await ctx.defer(ephemeral=True)
# if duration <= 0:
# await ctx.send("Duration must be > 0", ephemeral=True)
# return
#
# elif duration > 60 * 12:
# await ctx.send("Duration must be <= 12 hours", ephemeral=True)
# return
#
# if len(reason) > 100:
# await ctx.send("Reason must be <= 100 characters", ephemeral=True)
# return
# if not channel:
# channel = ctx.channel
#
# # role = ctx.guild.default_role # Uncomment once implemented
# if isinstance(channel, GuildText):
# to_deny = Permissions.SEND_MESSAGES
# elif isinstance(channel, GuildVoice):
# to_deny = Permissions.CONNECT | Permissions.SPEAK
#
# overwrite = PermissionOverwrite(type=PermissionTypes.ROLE, deny=to_deny)
# # TODO: Get original permissions
# # TODO: Apply overwrite
# overwrite = overwrite
# _ = Lock(
# channel=channel.id,
# guild=ctx.guild.id,
# admin=ctx.author.id,
# reason=reason,
# duration=duration,
# ) # .save() # Uncomment once implemented
# # await ctx.send(f"{channel.mention} locked for {duration} minute(s)")
# await ctx.send("Unfortunately, this is not yet implemented", hidden=True)
#
# @cog_ext.cog_slash(
# name="unlock",
# description="Unlocks a channel",
# choices=[
# create_option(
# name="channel",
# description="Channel to lock",
# opt_type=7,
# required=False,
# ),
# ],
# )
# @check(admin_or_permissions(Permissions.MANAGE_CHANNELS))
# async def _unlock(
# self,
# ctx: InteractionContext,
# channel: Union[GuildText, GuildVoice] = None,
# ) -> None:
# if not channel:
# channel = ctx.channel
# lock = Lock.objects(guild=ctx.guild.id, channel=channel.id, active=True).first()
# if not lock:
# await ctx.send(f"{channel.mention} not locked.", ephemeral=True)
# return
# for role in ctx.guild.roles:
# with suppress(Exception):
# await self._unlock_channel(channel, role, ctx.author)
# lock.active = False
# lock.save()
# await ctx.send(f"{channel.mention} unlocked")
#
#
# class LockCog(Scale):
# """J.A.R.V.I.S. LockCog."""
#
# @slash_command(name="lock", description="Lock a channel")
# @slash_option(name="reason",
# description="Lock Reason",
# opt_type=3,
# required=True,)
# @slash_option(name="duration",
# description="Lock duration in minutes (default 10)",
# opt_type=4,
# required=False,)
# @slash_option(name="channel",
# description="Channel to lock",
# opt_type=7,
# required=False,)
# @check(admin_or_permissions(Permissions.MANAGE_CHANNELS))
# async def _lock(
# self,
# ctx: InteractionContext,
# reason: str,
# duration: int = 10,
# channel: Union[GuildText, GuildVoice] = None,
# ) -> None:
# await ctx.defer(ephemeral=True)
# if duration <= 0:
# await ctx.send("Duration must be > 0", ephemeral=True)
# return
#
# elif duration > 60 * 12:
# await ctx.send("Duration must be <= 12 hours", ephemeral=True)
# return
#
# if len(reason) > 100:
# await ctx.send("Reason must be <= 100 characters", ephemeral=True)
# return
# if not channel:
# channel = ctx.channel
#
# # role = ctx.guild.default_role # Uncomment once implemented
# if isinstance(channel, GuildText):
# to_deny = Permissions.SEND_MESSAGES
# elif isinstance(channel, GuildVoice):
# to_deny = Permissions.CONNECT | Permissions.SPEAK
#
# overwrite = PermissionOverwrite(type=PermissionTypes.ROLE, deny=to_deny)
# # TODO: Get original permissions
# # TODO: Apply overwrite
# overwrite = overwrite
# _ = Lock(
# channel=channel.id,
# guild=ctx.guild.id,
# admin=ctx.author.id,
# reason=reason,
# duration=duration,
# ) # .save() # Uncomment once implemented
# # await ctx.send(f"{channel.mention} locked for {duration} minute(s)")
# await ctx.send("Unfortunately, this is not yet implemented", hidden=True)
#
# @cog_ext.cog_slash(
# name="unlock",
# description="Unlocks a channel",
# choices=[
# create_option(
# name="channel",
# description="Channel to lock",
# opt_type=7,
# required=False,
# ),
# ],
# )
# @check(admin_or_permissions(Permissions.MANAGE_CHANNELS))
# async def _unlock(
# self,
# ctx: InteractionContext,
# channel: Union[GuildText, GuildVoice] = None,
# ) -> None:
# if not channel:
# channel = ctx.channel
# lock = Lock.objects(guild=ctx.guild.id, channel=channel.id, active=True).first()
# if not lock:
# await ctx.send(f"{channel.mention} not locked.", ephemeral=True)
# return
# for role in ctx.guild.roles:
# with suppress(Exception):
# await self._unlock_channel(channel, role, ctx.author)
# lock.active = False
# lock.save()
# await ctx.send(f"{channel.mention} unlocked")

View file

@ -11,8 +11,8 @@ from dis_snek.models.snek.application_commands import (
slash_option,
)
from dis_snek.models.snek.command import check
from jarvis_core.db.models import Mute
from jarvis.db.models import Mute
from jarvis.utils import build_embed
from jarvis.utils.permissions import admin_or_permissions
@ -74,14 +74,15 @@ class MuteCog(Scale):
return
await user.timeout(communication_disabled_until=duration, reason=reason)
_ = Mute(
m = Mute(
user=user.id,
reason=reason,
admin=ctx.author.id,
guild=ctx.guild.id,
duration=duration,
active=True,
).save()
)
await m.commit()
embed = build_embed(
title="User Muted",
@ -119,7 +120,3 @@ class MuteCog(Scale):
embed.set_thumbnail(url=user.display_avatar.url)
embed.set_footer(text=f"{user.username}#{user.discriminator} | {user.id}")
await ctx.send(embed=embed)
embed.set_author(name=user.display_name, icon_url=user.display_avatar.url)
embed.set_thumbnail(url=user.display_avatar.url)
embed.set_footer(text=f"{user.username}#{user.discriminator} | {user.id}")
await ctx.send(embed=embed)

View file

@ -7,8 +7,9 @@ from dis_snek.models.snek.application_commands import (
slash_option,
)
from dis_snek.models.snek.command import check
from jarvis_core.db import q
from jarvis_core.db.models import Autopurge, Purge
from jarvis.db.models import Autopurge, Purge
from jarvis.utils.permissions import admin_or_permissions
@ -31,17 +32,17 @@ class PurgeCog(Scale):
await ctx.send("Amount must be >= 1", ephemeral=True)
return
await ctx.defer()
channel = ctx.channel
messages = []
async for message in channel.history(limit=amount + 1):
async for message in ctx.channel.history(limit=amount + 1):
messages.append(message)
await channel.delete_messages(messages)
_ = Purge(
await ctx.channel.delete_messages(messages, reason=f"Purge by {ctx.author.username}")
await Purge(
channel=ctx.channel.id,
guild=ctx.guild.id,
admin=ctx.author.id,
count=amount,
).save()
).commit()
@slash_command(
name="autopurge", sub_cmd_name="add", sub_cmd_description="Automatically purge messages"
@ -71,16 +72,19 @@ class PurgeCog(Scale):
elif delay > 300:
await ctx.send("Delay must be < 5 minutes", ephemeral=True)
return
autopurge = Autopurge.objects(guild=ctx.guild.id, channel=channel.id).first()
autopurge = await Autopurge.find_one(q(guild=ctx.guild.id, channel=channel.id))
if autopurge:
await ctx.send("Autopurge already exists.", ephemeral=True)
return
_ = Autopurge(
await Autopurge(
guild=ctx.guild.id,
channel=channel.id,
admin=ctx.author.id,
delay=delay,
).save()
).commit()
await ctx.send(f"Autopurge set up on {channel.mention}, delay is {delay} seconds")
@slash_command(
@ -94,11 +98,11 @@ class PurgeCog(Scale):
)
@check(admin_or_permissions(Permissions.MANAGE_MESSAGES))
async def _autopurge_remove(self, ctx: InteractionContext, channel: GuildText) -> None:
autopurge = Autopurge.objects(guild=ctx.guild.id, channel=channel.id)
autopurge = await Autopurge.find_one(q(guild=ctx.guild.id, channel=channel.id))
if not autopurge:
await ctx.send("Autopurge does not exist.", ephemeral=True)
return
autopurge.delete()
await autopurge.delete()
await ctx.send(f"Autopurge removed from {channel.mention}.")
@slash_command(
@ -122,10 +126,12 @@ class PurgeCog(Scale):
async def _autopurge_update(
self, ctx: InteractionContext, channel: GuildText, delay: int
) -> None:
autopurge = Autopurge.objects(guild=ctx.guild.id, channel=channel.id)
autopurge = await Autopurge.find_one(q(guild=ctx.guild.id, channel=channel.id))
if not autopurge:
await ctx.send("Autopurge does not exist.", ephemeral=True)
return
autopurge.delay = delay
autopurge.save()
await autopurge.commit()
await ctx.send(f"Autopurge delay updated to {delay} seconds on {channel.mention}.")

View file

@ -1,7 +1,6 @@
"""J.A.R.V.I.S. RolepingCog."""
from datetime import datetime, timedelta
from dis_snek import InteractionContext, Permissions, Snake
from dis_snek import InteractionContext, Permissions, Scale
from dis_snek.client.utils.misc_utils import find_all
from dis_snek.ext.paginators import Paginator
from dis_snek.models.discord.embed import EmbedField
from dis_snek.models.discord.role import Role
@ -12,36 +11,34 @@ from dis_snek.models.snek.application_commands import (
slash_option,
)
from dis_snek.models.snek.command import check
from jarvis_core.db import q
from jarvis_core.db.models import Roleping
from jarvis.db.models import Roleping
from jarvis.utils import build_embed
from jarvis.utils.cachecog import CacheCog
from jarvis.utils.permissions import admin_or_permissions
class RolepingCog(CacheCog):
class RolepingCog(Scale):
"""J.A.R.V.I.S. RolepingCog."""
def __init__(self, bot: Snake):
super().__init__(bot)
@slash_command(
name="roleping", sub_cmd_name="add", sub_cmd_description="Add a role to roleping"
)
@slash_option(name="role", description="Role to add", opt_type=OptionTypes.ROLE, required=True)
@check(admin_or_permissions(Permissions.MANAGE_GUILD))
async def _roleping_add(self, ctx: InteractionContext, role: Role) -> None:
roleping = Roleping.objects(guild=ctx.guild.id, role=role.id).first()
roleping = await Roleping.find_one(q(guild=ctx.guild.id, role=role.id))
if roleping:
await ctx.send(f"Role `{role.name}` already in roleping.", ephemeral=True)
return
_ = Roleping(
_ = await Roleping(
role=role.id,
guild=ctx.guild.id,
admin=ctx.author.id,
active=True,
bypass={"roles": [], "users": []},
).save()
).commit()
await ctx.send(f"Role `{role.name}` added to roleping.")
@slash_command(name="roleping", sub_cmd_name="remove", sub_cmd_description="Remove a role")
@ -50,37 +47,29 @@ class RolepingCog(CacheCog):
)
@check(admin_or_permissions(Permissions.MANAGE_GUILD))
async def _roleping_remove(self, ctx: InteractionContext, role: Role) -> None:
roleping = Roleping.objects(guild=ctx.guild.id, role=role.id)
roleping = await Roleping.find_one(q(guild=ctx.guild.id, role=role.id))
if not roleping:
await ctx.send("Roleping does not exist", ephemeral=True)
return
roleping.delete()
await roleping.delete()
await ctx.send(f"Role `{role.name}` removed from roleping.")
@slash_command(name="roleping", sub_cmd_name="list", description="Lick all blocklisted roles")
async def _roleping_list(self, ctx: InteractionContext) -> None:
exists = self.check_cache(ctx)
if exists:
await ctx.defer(ephemeral=True)
await ctx.send(
f"Please use existing interaction: {exists['paginator']._message.jump_url}",
ephemeral=True,
)
return
rolepings = Roleping.objects(guild=ctx.guild.id)
rolepings = await Roleping.find(q(guild=ctx.guild.id)).to_list(None)
if not rolepings:
await ctx.send("No rolepings configured", ephemeral=True)
return
embeds = []
for roleping in rolepings:
role = await ctx.guild.get_role(roleping.role)
bypass_roles = list(filter(lambda x: x.id in roleping.bypass["roles"], ctx.guild.roles))
bypass_roles = [r.mention or "||`[redacted]`||" for r in bypass_roles]
role = await ctx.guild.fetch_role(roleping.role)
broles = find_all(lambda x: x.id in roleping.bypass["roles"], ctx.guild.roles)
bypass_roles = [r.mention or "||`[redacted]`||" for r in broles]
bypass_users = [
await ctx.guild.get_member(u).mention or "||`[redacted]`||"
(await ctx.guild.fetch_member(u)).mention or "||`[redacted]`||"
for u in roleping.bypass["users"]
]
bypass_roles = bypass_roles or ["None"]
@ -95,37 +84,31 @@ class RolepingCog(CacheCog):
value=roleping.created_at.strftime("%a, %b %d, %Y %I:%M %p"),
inline=False,
),
EmbedField(name="Active", value=str(roleping.active)),
# EmbedField(name="Active", value=str(roleping.active), inline=True),
EmbedField(
name="Bypass Users",
value="\n".join(bypass_users),
inline=True,
),
EmbedField(
name="Bypass Roles",
value="\n".join(bypass_roles),
inline=True,
),
],
)
admin = await ctx.guild.get_member(roleping.admin)
admin = await ctx.guild.fetch_member(roleping.admin)
if not admin:
admin = self.bot.user
embed.set_author(name=admin.display_name, icon_url=admin.display_avatar.url)
embed.set_footer(text=f"{admin.name}#{admin.discriminator} | {admin.id}")
embed.set_footer(text=f"{admin.username}#{admin.discriminator} | {admin.id}")
embeds.append(embed)
paginator = Paginator.create_from_embeds(self.bot, *embeds, timeout=300)
self.cache[hash(paginator)] = {
"user": ctx.author.id,
"guild": ctx.guild.id,
"timeout": datetime.utcnow() + timedelta(minutes=5),
"command": ctx.subcommand_name,
"paginator": paginator,
}
await paginator.send(ctx)
@slash_command(
@ -136,21 +119,23 @@ class RolepingCog(CacheCog):
sub_cmd_name="user",
sub_cmd_description="Add a user as a bypass to a roleping",
)
@slash_option(name="user", description="User to add", opt_type=OptionTypes.USER, required=True)
@slash_option(
name="rping", description="Rolepinged role", opt_type=OptionTypes.ROLE, required=True
name="bypass", description="User to add", opt_type=OptionTypes.USER, required=True
)
@slash_option(
name="role", description="Rolepinged role", opt_type=OptionTypes.ROLE, required=True
)
@check(admin_or_permissions(Permissions.MANAGE_GUILD))
async def _roleping_bypass_user(
self, ctx: InteractionContext, user: Member, rping: Role
self, ctx: InteractionContext, bypass: Member, role: Role
) -> None:
roleping = Roleping.objects(guild=ctx.guild.id, role=rping.id).first()
roleping = await Roleping.find_one(q(guild=ctx.guild.id, role=role.id))
if not roleping:
await ctx.send(f"Roleping not configured for {rping.mention}", ephemeral=True)
await ctx.send(f"Roleping not configured for {role.mention}", ephemeral=True)
return
if user.id in roleping.bypass["users"]:
await ctx.send(f"{user.mention} already in bypass", ephemeral=True)
if bypass.id in roleping.bypass["users"]:
await ctx.send(f"{bypass.mention} already in bypass", ephemeral=True)
return
if len(roleping.bypass["users"]) == 10:
@ -160,18 +145,18 @@ class RolepingCog(CacheCog):
)
return
matching_role = list(filter(lambda x: x.id in roleping.bypass["roles"], user.roles))
matching_role = list(filter(lambda x: x.id in roleping.bypass["roles"], bypass.roles))
if matching_role:
await ctx.send(
f"{user.mention} already has bypass via {matching_role[0].mention}",
f"{bypass.mention} already has bypass via {matching_role[0].mention}",
ephemeral=True,
)
return
roleping.bypass["users"].append(user.id)
roleping.save()
await ctx.send(f"{user.display_name} user bypass added for `{rping.name}`")
roleping.bypass["users"].append(bypass.id)
await roleping.commit()
await ctx.send(f"{bypass.display_name} user bypass added for `{role.name}`")
@slash_command(
name="roleping",
@ -179,19 +164,23 @@ class RolepingCog(CacheCog):
sub_cmd_name="role",
description="Add a role as a bypass to roleping",
)
@slash_option(name="role", description="Role to add", opt_type=OptionTypes.ROLE, required=True)
@slash_option(
name="rping", description="Rolepinged role", opt_type=OptionTypes.ROLE, required=True
name="bypass", description="Role to add", opt_type=OptionTypes.ROLE, required=True
)
@slash_option(
name="role", description="Rolepinged role", opt_type=OptionTypes.ROLE, required=True
)
@check(admin_or_permissions(Permissions.MANAGE_GUILD))
async def _roleping_bypass_role(self, ctx: InteractionContext, role: Role, rping: Role) -> None:
roleping = Roleping.objects(guild=ctx.guild.id, role=rping.id).first()
async def _roleping_bypass_role(
self, ctx: InteractionContext, bypass: Role, role: Role
) -> None:
roleping = await Roleping.find_one(q(guild=ctx.guild.id, role=role.id))
if not roleping:
await ctx.send(f"Roleping not configured for {rping.mention}", ephemeral=True)
await ctx.send(f"Roleping not configured for {role.mention}", ephemeral=True)
return
if role.id in roleping.bypass["roles"]:
await ctx.send(f"{role.mention} already in bypass", ephemeral=True)
if bypass.id in roleping.bypass["roles"]:
await ctx.send(f"{bypass.mention} already in bypass", ephemeral=True)
return
if len(roleping.bypass["roles"]) == 10:
@ -202,9 +191,9 @@ class RolepingCog(CacheCog):
)
return
roleping.bypass["roles"].append(role.id)
roleping.save()
await ctx.send(f"{role.name} role bypass added for `{rping.name}`")
roleping.bypass["roles"].append(bypass.id)
await roleping.commit()
await ctx.send(f"{bypass.name} role bypass added for `{role.name}`")
@slash_command(
name="roleping",
@ -215,27 +204,27 @@ class RolepingCog(CacheCog):
sub_cmd_description="Remove a bypass from a roleping (restoring it)",
)
@slash_option(
name="user", description="User to remove", opt_type=OptionTypes.USER, required=True
name="bypass", description="User to remove", opt_type=OptionTypes.USER, required=True
)
@slash_option(
name="rping", description="Rolepinged role", opt_type=OptionTypes.ROLE, required=True
name="role", description="Rolepinged role", opt_type=OptionTypes.ROLE, required=True
)
@check(admin_or_permissions(Permissions.MANAGE_GUILD))
async def _roleping_restore_user(
self, ctx: InteractionContext, user: Member, rping: Role
self, ctx: InteractionContext, bypass: Member, role: Role
) -> None:
roleping = Roleping.objects(guild=ctx.guild.id, role=rping.id).first()
roleping: Roleping = await Roleping.find_one(q(guild=ctx.guild.id, role=role.id))
if not roleping:
await ctx.send(f"Roleping not configured for {rping.mention}", ephemeral=True)
await ctx.send(f"Roleping not configured for {role.mention}", ephemeral=True)
return
if user.id not in roleping.bypass["users"]:
await ctx.send(f"{user.mention} not in bypass", ephemeral=True)
if bypass.id not in roleping.bypass.users:
await ctx.send(f"{bypass.mention} not in bypass", ephemeral=True)
return
roleping.bypass["users"].delete(user.id)
roleping.save()
await ctx.send(f"{user.display_name} user bypass removed for `{rping.name}`")
roleping.bypass.users.remove(bypass.id)
await roleping.commit()
await ctx.send(f"{bypass.display_name} user bypass removed for `{role.name}`")
@slash_command(
name="roleping",
@ -244,32 +233,24 @@ class RolepingCog(CacheCog):
description="Remove a bypass from a roleping (restoring it)",
)
@slash_option(
name="role", description="Role to remove", opt_type=OptionTypes.ROLE, required=True
name="bypass", description="Role to remove", opt_type=OptionTypes.ROLE, required=True
)
@slash_option(
name="rping", description="Rolepinged role", opt_type=OptionTypes.ROLE, required=True
name="role", description="Rolepinged role", opt_type=OptionTypes.ROLE, required=True
)
@check(admin_or_permissions(manage_guild=True))
@check(admin_or_permissions(Permissions.MANAGE_GUILD))
async def _roleping_restore_role(
self, ctx: InteractionContext, role: Role, rping: Role
self, ctx: InteractionContext, bypass: Role, role: Role
) -> None:
roleping = Roleping.objects(guild=ctx.guild.id, role=rping.id).first()
roleping: Roleping = await Roleping.find_one(q(guild=ctx.guild.id, role=role.id))
if not roleping:
await ctx.send(f"Roleping not configured for {rping.mention}", ephemeral=True)
await ctx.send(f"Roleping not configured for {role.mention}", ephemeral=True)
return
if role.id in roleping.bypass["roles"]:
await ctx.send(f"{role.mention} already in bypass", ephemeral=True)
if bypass.id not in roleping.bypass.roles:
await ctx.send(f"{bypass.mention} not in bypass", ephemeral=True)
return
if len(roleping.bypass["roles"]) == 10:
await ctx.send(
"Already have 10 roles in bypass. "
"Please consider consolidating roles for roleping bypass",
ephemeral=True,
)
return
roleping.bypass["roles"].append(role.id)
roleping.save()
await ctx.send(f"{role.name} role bypass added for `{rping.name}`")
roleping.bypass.roles.remove(bypass.id)
await roleping.commit()
await ctx.send(f"{bypass.display_name} user bypass removed for `{role.name}`")

View file

@ -1,8 +1,8 @@
"""J.A.R.V.I.S. WarningCog."""
from datetime import datetime, timedelta
from dis_snek import InteractionContext, Permissions, Snake
from dis_snek import InteractionContext, Permissions, Scale
from dis_snek.client.utils.misc_utils import get_all
from dis_snek.ext.paginators import Paginator
from dis_snek.models.discord.embed import EmbedField
from dis_snek.models.discord.user import User
from dis_snek.models.snek.application_commands import (
OptionTypes,
@ -11,20 +11,16 @@ from dis_snek.models.snek.application_commands import (
slash_option,
)
from dis_snek.models.snek.command import check
from jarvis_core.db.models import Warning
from jarvis.db.models import Warning
from jarvis.utils import build_embed
from jarvis.utils.cachecog import CacheCog
from jarvis.utils.field import Field
from jarvis.utils.embeds import warning_embed
from jarvis.utils.permissions import admin_or_permissions
class WarningCog(CacheCog):
class WarningCog(Scale):
"""J.A.R.V.I.S. WarningCog."""
def __init__(self, bot: Snake):
super().__init__(bot)
@slash_command(name="warn", description="Warn a user")
@slash_option(name="user", description="User to warn", opt_type=OptionTypes.USER, required=True)
@slash_option(
@ -53,23 +49,15 @@ class WarningCog(CacheCog):
await ctx.send("Duration must be < 5 days", ephemeral=True)
return
await ctx.defer()
_ = Warning(
await Warning(
user=user.id,
reason=reason,
admin=ctx.author.id,
guild=ctx.guild.id,
duration=duration,
active=True,
).save()
fields = [Field("Reason", reason, False)]
embed = build_embed(
title="Warning",
description=f"{user.mention} has been warned",
fields=fields,
)
embed.set_author(name=user.display_name, icon_url=user.display_avatar.url)
embed.set_footer(text=f"{user.name}#{user.discriminator} | {user.id}")
).commit()
embed = warning_embed(user, reason)
await ctx.send(embed=embed)
@slash_command(name="warnings", description="Get count of user warnings")
@ -87,25 +75,20 @@ class WarningCog(CacheCog):
@check(admin_or_permissions(Permissions.MANAGE_GUILD))
async def _warnings(self, ctx: InteractionContext, user: User, active: bool = 1) -> None:
active = bool(active)
exists = self.check_cache(ctx, user_id=user.id, active=active)
if exists:
await ctx.defer(ephemeral=True)
await ctx.send(
f"Please use existing interaction: {exists['paginator']._message.jump_url}",
ephemeral=True,
warnings = (
await Warning.find(
user=user.id,
guild=ctx.guild.id,
)
return
warnings = Warning.objects(
user=user.id,
guild=ctx.guild.id,
).order_by("-created_at")
active_warns = Warning.objects(user=user.id, guild=ctx.guild.id, active=True).order_by(
"-created_at"
.sort("created_at", -1)
.to_list(None)
)
active_warns = get_all(warnings, active=True)
pages = []
if active:
if active_warns.count() == 0:
if len(active_warns) == 0:
embed = build_embed(
title="Warnings",
description=f"{warnings.count()} total | 0 currently active",
@ -122,7 +105,7 @@ class WarningCog(CacheCog):
if admin:
admin_name = f"{admin.username}#{admin.discriminator}"
fields.append(
Field(
EmbedField(
name=warn.created_at.strftime("%Y-%m-%d %H:%M:%S UTC"),
value=f"{warn.reason}\nAdmin: {admin_name}\n\u200b",
inline=False,
@ -132,7 +115,7 @@ class WarningCog(CacheCog):
embed = build_embed(
title="Warnings",
description=(
f"{warnings.count()} total | {active_warns.count()} currently active"
f"{len(warnings)} total | {len(active_warns)} currently active"
),
fields=fields[i : i + 5],
)
@ -149,7 +132,7 @@ class WarningCog(CacheCog):
title = "[A] " if warn.active else "[I] "
title += warn.created_at.strftime("%Y-%m-%d %H:%M:%S UTC")
fields.append(
Field(
EmbedField(
name=title,
value=warn.reason + "\n\u200b",
inline=False,
@ -158,9 +141,7 @@ class WarningCog(CacheCog):
for i in range(0, len(fields), 5):
embed = build_embed(
title="Warnings",
description=(
f"{warnings.count()} total | {active_warns.count()} currently active"
),
description=(f"{len(warnings)} total | {len(active_warns)} currently active"),
fields=fields[i : i + 5],
)
embed.set_author(
@ -171,14 +152,4 @@ class WarningCog(CacheCog):
paginator = Paginator(bot=self.bot, *pages, timeout=300)
self.cache[hash(paginator)] = {
"guild": ctx.guild.id,
"user": ctx.author.id,
"timeout": datetime.utcnow() + timedelta(minutes=5),
"command": ctx.subcommand_name,
"user_id": user.id,
"active": active,
"paginator": paginator,
}
await paginator.send(ctx)

View file

@ -3,6 +3,7 @@ import re
from typing import Optional, Tuple
from dis_snek import InteractionContext, Permissions, Scale, Snake
from dis_snek.client.utils.misc_utils import find
from dis_snek.models.discord.channel import GuildText
from dis_snek.models.snek.application_commands import (
OptionTypes,
@ -10,10 +11,10 @@ from dis_snek.models.snek.application_commands import (
slash_option,
)
from dis_snek.models.snek.command import check
from jarvis_core.db import q
from jarvis_core.db.models import Autoreact
from jarvis.data.unicode import emoji_list
from jarvis.db.models import Autoreact
from jarvis.utils import find
from jarvis.utils.permissions import admin_or_permissions
@ -37,7 +38,7 @@ class AutoReactCog(Scale):
Returns:
Tuple of success? and error message
"""
exists = Autoreact.objects(guild=ctx.guild.id, channel=channel.id).first()
exists = await Autoreact.find_one(q(guild=ctx.guild.id, channel=channel.id))
if exists:
return False, f"Autoreact already exists for {channel.mention}."
@ -93,10 +94,10 @@ class AutoReactCog(Scale):
if not find(lambda x: x.id == emoji_id, ctx.guild.emojis):
await ctx.send("Please use a custom emote from this server.", ephemeral=True)
return
autoreact = Autoreact.objects(guild=ctx.guild.id, channel=channel.id).first()
autoreact = await Autoreact.find_one(q(guild=ctx.guild.id, channel=channel.id))
if not autoreact:
self.create_autoreact(ctx, channel)
autoreact = Autoreact.objects(guild=ctx.guild.id, channel=channel.id).first()
autoreact = await Autoreact.find_one(q(guild=ctx.guild.id, channel=channel.id))
if emote in autoreact.reactions:
await ctx.send(
f"Emote already added to {channel.mention} autoreactions.",
@ -134,7 +135,7 @@ class AutoReactCog(Scale):
async def _autoreact_remove(
self, ctx: InteractionContext, channel: GuildText, emote: str
) -> None:
autoreact = Autoreact.objects(guild=ctx.guild.id, channel=channel.id).first()
autoreact = await Autoreact.find_one(q(guild=ctx.guild.id, channel=channel.id))
if not autoreact:
await ctx.send(
f"Please create autoreact first with /autoreact add {channel.mention} {emote}",
@ -169,7 +170,7 @@ class AutoReactCog(Scale):
required=True,
)
async def _autoreact_list(self, ctx: InteractionContext, channel: GuildText) -> None:
exists = Autoreact.objects(guild=ctx.guild.id, channel=channel.id).first()
exists = await Autoreact.find_one(q(guild=ctx.guild.id, channel=channel.id))
if not exists:
await ctx.send(
f"Please create autoreact first with /autoreact add {channel.mention} <emote>",

View file

@ -10,8 +10,9 @@ from dis_snek.models.discord.user import Member, User
from dis_snek.models.snek.application_commands import slash_command
from dis_snek.models.snek.command import cooldown
from dis_snek.models.snek.cooldowns import Buckets
from jarvis_core.db import q
from jarvis_core.db.models import Guess
from jarvis.db.models import Guess
from jarvis.utils import build_embed
from jarvis.utils.cachecog import CacheCog
@ -74,7 +75,7 @@ class CTCCog(CacheCog):
ephemeral=True,
)
return
guessed = Guess.objects(guess=guess).first()
guessed = await Guess.find_one(q(guess=guess))
if guessed:
await ctx.send("Already guessed, dipshit.", ephemeral=True)
return

View file

@ -4,12 +4,12 @@ import hashlib
import re
import subprocess # noqa: S404
import uuid as uuidpy
from typing import Any, Union
import ulid as ulidpy
from bson import ObjectId
from dis_snek import InteractionContext, Scale, Snake
from dis_snek.models.discord.embed import EmbedField
from dis_snek.models.discord.message import Attachment
from dis_snek.models.snek.application_commands import (
OptionTypes,
SlashCommandChoice,
@ -18,8 +18,11 @@ from dis_snek.models.snek.application_commands import (
)
from dis_snek.models.snek.command import cooldown
from dis_snek.models.snek.cooldowns import Buckets
from jarvis_core.filters import invites, url
from jarvis_core.util import convert_bytesize, hash
from jarvis_core.util.http import get_size
from jarvis.utils import build_embed, convert_bytesize
from jarvis.utils import build_embed
supported_hashes = {x for x in hashlib.algorithms_guaranteed if "shake" not in x}
@ -36,29 +39,9 @@ UUID_VERIFY = re.compile(
re.IGNORECASE,
)
invites = re.compile(
r"(?:https?://)?(?:www.)?(?:discord.(?:gg|io|me|li)|discord(?:app)?.com/invite)/([^\s/]+?)(?=\b)", # noqa: E501
flags=re.IGNORECASE,
)
UUID_GET = {3: uuidpy.uuid3, 5: uuidpy.uuid5}
def hash_obj(hash: Any, data: Union[str, bytes], text: bool = True) -> str:
"""Hash data with hash object.
Data can be text or binary
"""
if text:
hash.update(data.encode("UTF-8"))
return hash.hexdigest()
BSIZE = 65536
block_idx = 0
while block_idx * BSIZE < len(data):
block = data[BSIZE * block_idx : BSIZE * (block_idx + 1)]
hash.update(block)
block_idx += 1
return hash.hexdigest()
MAX_FILESIZE = 5 * (1024**3) # 5GB
class DevCog(Scale):
@ -76,26 +59,51 @@ class DevCog(Scale):
name="data",
description="Data to hash",
opt_type=OptionTypes.STRING,
required=True,
required=False,
)
@slash_option(
name="attach", description="File to hash", opt_type=OptionTypes.ATTACHMENT, required=False
)
@cooldown(bucket=Buckets.USER, rate=1, interval=2)
async def _hash(self, ctx: InteractionContext, method: str, data: str) -> None:
if not data:
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",
ephemeral=True,
)
return
text = True
# Default to sha256, just in case
hash = getattr(hashlib, method, hashlib.sha256)()
hex = hash_obj(hash, data, text)
data_size = convert_bytesize(len(data))
title = data if text else ctx.message.attachments[0].filename
if data and invites.match(data):
await ctx.send("No hashing invites", ephemeral=True)
return
title = data
if attach:
data = attach.url
title = attach.filename
elif url.match(data):
try:
if await get_size(data) > MAX_FILESIZE:
await ctx.send("Please hash files that are <= 5GB in size", ephemeral=True)
return
except Exception as e:
await ctx.send(f"Failed to retrieve URL: ```\n{e}\n```", ephemeral=True)
return
title = data.split("/")[-1]
await ctx.defer()
try:
hexstr, size, c_type = await hash(data, method)
except Exception as e:
await ctx.send(f"Failed to hash data: ```\n{e}\n```", ephemeral=True)
return
data_size = convert_bytesize(size)
description = "Hashed using " + method
fields = [
EmbedField("Content Type", c_type, False),
EmbedField("Data Size", data_size, False),
EmbedField("Hash", f"`{hex}`", False),
EmbedField("Hash", f"`{hexstr}`", False),
]
embed = build_embed(title=title, description=description, fields=fields)

View file

@ -1,56 +0,0 @@
"""J.A.R.V.I.S. error handling cog."""
from discord.ext import commands
from discord_slash import SlashContext
from jarvis import slash
class ErrorHandlerCog(commands.Cog):
"""J.A.R.V.I.S. error handling cog."""
def __init__(self, bot: commands.Bot):
self.bot = bot
@commands.Cog.listener()
async def on_command_error(self, ctx: commands.Context, error: Exception) -> None:
"""d.py on_command_error override."""
if isinstance(error, commands.errors.MissingPermissions):
await ctx.send("I'm afraid I can't let you do that.")
elif isinstance(error, commands.errors.CommandNotFound):
return
elif isinstance(error, commands.errors.CommandOnCooldown):
await ctx.send(
"Command on cooldown. "
f"Please wait {error.retry_after:0.2f}s before trying again",
)
else:
await ctx.send(f"Error processing command:\n```{error}```")
ctx.command.reset_cooldown(ctx)
@commands.Cog.listener()
async def on_slash_command_error(self, ctx: SlashContext, error: Exception) -> None:
"""discord_slash on_slash_command_error override."""
if isinstance(error, commands.errors.MissingPermissions) or isinstance(
error, commands.errors.CheckFailure
):
await ctx.send("I'm afraid I can't let you do that.", ephemeral=True)
elif isinstance(error, commands.errors.CommandNotFound):
return
elif isinstance(error, commands.errors.CommandOnCooldown):
await ctx.send(
"Command on cooldown. "
f"Please wait {error.retry_after:0.2f}s before trying again",
ephemeral=True,
)
else:
await ctx.send(
f"Error processing command:\n```{error}```",
ephemeral=True,
)
raise error
slash.commands[ctx.command].reset_cooldown(ctx)
def setup(bot: commands.Bot) -> None:
"""Add ErrorHandlerCog to J.A.R.V.I.S."""
ErrorHandlerCog(bot)

View file

@ -1,8 +1,8 @@
"""J.A.R.V.I.S. GitLab Cog."""
from datetime import datetime, timedelta
from datetime import datetime
import gitlab
from dis_snek import InteractionContext, Snake
from dis_snek import InteractionContext, Scale, Snake
from dis_snek.ext.paginators import Paginator
from dis_snek.models.discord.embed import Embed, EmbedField
from dis_snek.models.snek.application_commands import (
@ -14,16 +14,15 @@ from dis_snek.models.snek.application_commands import (
from jarvis.config import get_config
from jarvis.utils import build_embed
from jarvis.utils.cachecog import CacheCog
guild_ids = [862402786116763668]
class GitlabCog(CacheCog):
class GitlabCog(Scale):
"""J.A.R.V.I.S. GitLab Cog."""
def __init__(self, bot: Snake):
super().__init__(bot)
self.bot = bot
config = get_config()
self._gitlab = gitlab.Gitlab("https://git.zevaryx.com", private_token=config.gitlab_token)
# J.A.R.V.I.S. GitLab ID is 29
@ -236,10 +235,11 @@ class GitlabCog(CacheCog):
title += f"J.A.R.V.I.S. {name}s"
fields = []
for item in api_list:
description = item.description or "No description"
fields.append(
EmbedField(
name=f"[#{item.iid}] {item.title}",
value=item.description + f"\n\n[View this {name}]({item.web_url})",
value=(description[:200] + f"...\n\n[View this {name}]({item.web_url})"),
inline=False,
)
)
@ -275,14 +275,6 @@ class GitlabCog(CacheCog):
],
)
async def _issues(self, ctx: InteractionContext, state: str = "opened") -> None:
exists = self.check_cache(ctx, state=state)
if exists:
await ctx.defer(ephemeral=True)
await ctx.send(
"Please use existing interaction: " + f"{exists['paginator']._message.jump_url}",
ephemeral=True,
)
return
await ctx.defer()
m_state = state
if m_state == "all":
@ -319,15 +311,6 @@ class GitlabCog(CacheCog):
paginator = Paginator.create_from_embeds(self.bot, *pages, timeout=300)
self.cache[hash(paginator)] = {
"user": ctx.author.id,
"guild": ctx.guild.id,
"timeout": datetime.utcnow() + timedelta(minutes=5),
"command": ctx.subcommand_name,
"state": state,
"paginator": paginator,
}
await paginator.send(ctx)
@slash_command(
@ -348,14 +331,6 @@ class GitlabCog(CacheCog):
],
)
async def _mergerequests(self, ctx: InteractionContext, state: str = "opened") -> None:
exists = self.check_cache(ctx, state=state)
if exists:
await ctx.defer(ephemeral=True)
await ctx.send(
"Please use existing interaction: " + f"{exists['paginator']._message.jump_url}",
ephemeral=True,
)
return
await ctx.defer()
m_state = state
if m_state == "all":
@ -394,15 +369,6 @@ class GitlabCog(CacheCog):
paginator = Paginator.create_from_embeds(self.bot, *pages, timeout=300)
self.cache[hash(paginator)] = {
"user": ctx.author.id,
"guild": ctx.guild.id,
"timeout": datetime.utcnow() + timedelta(minutes=5),
"command": ctx.subcommand_name,
"state": state,
"paginator": paginator,
}
await paginator.send(ctx)
@slash_command(
@ -412,14 +378,6 @@ class GitlabCog(CacheCog):
scopes=guild_ids,
)
async def _milestones(self, ctx: InteractionContext) -> None:
exists = self.check_cache(ctx)
if exists:
await ctx.defer(ephemeral=True)
await ctx.send(
f"Please use existing interaction: {exists['paginator']._message.jump_url}",
ephemeral=True,
)
return
await ctx.defer()
milestones = []
page = 1
@ -450,14 +408,6 @@ class GitlabCog(CacheCog):
paginator = Paginator.create_from_embeds(self.bot, *pages, timeout=300)
self.cache[hash(paginator)] = {
"user": ctx.author.id,
"guild": ctx.guild.id,
"timeout": datetime.utcnow() + timedelta(minutes=5),
"command": ctx.subcommand_name,
"paginator": paginator,
}
await paginator.send(ctx)

View file

@ -5,13 +5,18 @@ from io import BytesIO
import aiohttp
import cv2
import numpy as np
from dis_snek import MessageContext, Scale, Snake, message_command
from dis_snek import InteractionContext, Scale, Snake
from dis_snek.models.discord.embed import EmbedField
from dis_snek.models.discord.file import File
from dis_snek.models.snek.command import cooldown
from dis_snek.models.snek.cooldowns import Buckets
from dis_snek.models.discord.message import Attachment
from dis_snek.models.snek.application_commands import (
OptionTypes,
slash_command,
slash_option,
)
from jarvis_core.util import build_embed, convert_bytesize, unconvert_bytesize
from jarvis.utils import build_embed, convert_bytesize, unconvert_bytesize
MIN_ACCURACY = 0.80
class ImageCog(Scale):
@ -24,39 +29,65 @@ class ImageCog(Scale):
def __init__(self, bot: Snake):
self.bot = bot
self._session = aiohttp.ClientSession()
self.tgt_match = re.compile(r"([0-9]*\.?[0-9]*?) ?([KMGTP]?B)", re.IGNORECASE)
self.tgt_match = re.compile(r"([0-9]*\.?[0-9]*?) ?([KMGTP]?B?)", re.IGNORECASE)
def __del__(self):
self._session.close()
async def _resize(self, ctx: MessageContext, target: str, url: str = None) -> None:
if not target:
await ctx.send("Missing target size, i.e. 200KB.")
@slash_command(name="resize", description="Resize an image")
@slash_option(
name="target",
description="Target size, i.e. 200KB",
opt_type=OptionTypes.STRING,
required=True,
)
@slash_option(
name="attachment",
description="Image to resize",
opt_type=OptionTypes.ATTACHMENT,
required=False,
)
@slash_option(
name="url",
description="URL to download and resize",
opt_type=OptionTypes.STRING,
required=False,
)
async def _resize(
self, ctx: InteractionContext, target: str, attachment: Attachment = None, url: str = None
) -> None:
if not attachment and not url:
await ctx.send("A URL or attachment is required", ephemeral=True)
return
if attachment and not attachment.content_type.startswith("image"):
await ctx.send("Attachment must be an image", ephemeral=True)
return
tgt = self.tgt_match.match(target)
if not tgt:
await ctx.send(f"Invalid target format ({target}). Expected format like 200KB")
await ctx.send(
f"Invalid target format ({target}). Expected format like 200KB", ephemeral=True
)
return
tgt_size = unconvert_bytesize(float(tgt.groups()[0]), tgt.groups()[1])
if tgt_size > unconvert_bytesize(8, "MB"):
await ctx.send("Target too large to send. Please make target < 8MB")
await ctx.send("Target too large to send. Please make target < 8MB", ephemeral=True)
return
file = None
filename = None
if ctx.message.attachments is not None and len(ctx.message.attachments) > 0:
file = await ctx.message.attachments[0].read()
filename = ctx.message.attachments[0].filename
elif url is not None:
async with self._session.get(url) as resp:
if resp.status == 200:
file = await resp.read()
filename = url.split("/")[-1]
else:
ctx.send("Missing file as either attachment or URL.")
size = len(file)
if attachment:
url = attachment.url
filename = attachment.filename
else:
filename = url.split("/")[-1]
data = None
async with self._session.get(url) as resp:
if resp.status == 200:
data = await resp.read()
size = len(data)
if size <= tgt_size:
await ctx.send("Image already meets target.")
return
@ -64,44 +95,39 @@ class ImageCog(Scale):
ratio = max(tgt_size / size - 0.02, 0.50)
accuracy = 0.0
# TODO: Optimize to not run multiple times
while len(file) > tgt_size or (len(file) <= tgt_size and accuracy < 0.65):
old_file = file
buffer = np.frombuffer(file, dtype=np.uint8)
while len(data) > tgt_size or (len(data) <= tgt_size and accuracy < MIN_ACCURACY):
old_file = data
buffer = np.frombuffer(data, dtype=np.uint8)
img = cv2.imdecode(buffer, flags=-1)
width = int(img.shape[1] * ratio)
height = int(img.shape[0] * ratio)
new_img = cv2.resize(img, (width, height))
file = cv2.imencode(".png", new_img)[1].tobytes()
accuracy = (len(file) / tgt_size) * 100
data = cv2.imencode(".png", new_img)[1].tobytes()
accuracy = (len(data) / tgt_size) * 100
if accuracy <= 0.50:
file = old_file
data = old_file
ratio += 0.1
else:
ratio = max(tgt_size / len(file) - 0.02, 0.65)
ratio = max(tgt_size / len(data) - 0.02, 0.65)
bufio = BytesIO(file)
accuracy = (len(file) / tgt_size) * 100
bufio = BytesIO(data)
accuracy = (len(data) / tgt_size) * 100
fields = [
EmbedField("Original Size", convert_bytesize(size), False),
EmbedField("New Size", convert_bytesize(len(file)), False),
EmbedField("New Size", convert_bytesize(len(data)), False),
EmbedField("Accuracy", f"{accuracy:.02f}%", False),
]
embed = build_embed(title=filename, description="", fields=fields)
embed.set_image(url="attachment://resized.png")
await ctx.send(
embed=embed,
file=File(file=bufio, filename="resized.png"),
file=File(file=bufio, file_name="resized.png"),
)
@message_command(name="resize")
@cooldown(bucket=Buckets.USER, rate=1, interval=60)
async def _resize_pref(self, ctx: MessageContext, target: str, url: str = None) -> None:
await self._resize(ctx, target, url)
def setup(bot: Snake) -> None:
"""Add ImageCog to J.A.R.V.I.S."""

View file

@ -1,121 +0,0 @@
"""J.A.R.V.I.S. Jokes module."""
import html
import re
import traceback
from datetime import datetime
from random import randint
from dis_snek import InteractionContext, Scale, Snake
from dis_snek.models.discord.embed import EmbedField
from dis_snek.models.snek.application_commands import (
OptionTypes,
slash_command,
slash_option,
)
from dis_snek.models.snek.command import cooldown
from dis_snek.models.snek.cooldowns import Buckets
from jarvis.db.models import Joke
from jarvis.utils import build_embed
class JokeCog(Scale):
"""
Joke library for J.A.R.V.I.S.
May adapt over time to create jokes using machine learning
"""
def __init__(self, bot: Snake):
self.bot = bot
# TODO: Make this a command group with subcommands
@slash_command(
name="joke",
description="Hear a joke",
)
@slash_option(name="id", description="Joke ID", required=False, opt_type=OptionTypes.INTEGER)
@cooldown(bucket=Buckets.CHANNEL, rate=1, interval=10)
async def _joke(self, ctx: InteractionContext, id: str = None) -> None:
"""Get a joke from the database."""
try:
if randint(1, 100_000) == 5779 and id is None: # noqa: S311
await ctx.send(f"<@{ctx.message.author.id}>")
return
# TODO: Add this as a parameter that can be passed in
threshold = 500 # Minimum score
result = None
if id:
result = Joke.objects(rid=id).first()
else:
pipeline = [
{"$match": {"score": {"$gt": threshold}}},
{"$sample": {"size": 1}},
]
result = Joke.objects().aggregate(pipeline).next()
while result["body"] in ["[removed]", "[deleted]"]:
result = Joke.objects().aggregate(pipeline).next()
if result is None:
await ctx.send("Humor module failed. Please try again later.", ephemeral=True)
return
emotes = re.findall(r"(&#x[a-fA-F0-9]*;)", result["body"])
for match in emotes:
result["body"] = result["body"].replace(match, html.unescape(match))
emotes = re.findall(r"(&#x[a-fA-F0-9]*;)", result["title"])
for match in emotes:
result["title"] = result["title"].replace(match, html.unescape(match))
body_chunks = []
body = ""
for word in result["body"].split(" "):
if len(body) + 1 + len(word) > 1024:
body_chunks.append(EmbedField("", body, False))
body = ""
if word == "\n" and body == "":
continue
elif word == "\n":
body += word
else:
body += " " + word
desc = ""
title = result["title"]
if len(title) > 256:
new_title = ""
limit = False
for word in title.split(" "):
if len(new_title) + len(word) + 1 > 253 and not limit:
new_title += "..."
desc = "..."
limit = True
if not limit:
new_title += word + " "
else:
desc += word + " "
body_chunks.append(EmbedField("", body, False))
fields = body_chunks
fields.append(EmbedField("Score", result["score"]))
# Field(
# "Created At",
# str(datetime.fromtimestamp(result["created_utc"])),
# ),
fields.append(EmbedField("ID", result["rid"]))
embed = build_embed(
title=title,
description=desc,
fields=fields,
url=f"https://reddit.com/r/jokes/comments/{result['rid']}",
timestamp=datetime.fromtimestamp(result["created_utc"]),
)
await ctx.send(embed=embed)
except Exception:
await ctx.send("Encountered error:\n```\n" + traceback.format_exc() + "\n```")
# await ctx.send(f"**{result['title']}**\n\n{result['body']}")
def setup(bot: Snake) -> None:
"""Add JokeCog to J.A.R.V.I.S."""
JokeCog(bot)

View file

@ -1,11 +0,0 @@
"""J.A.R.V.I.S. Modlog Cogs."""
from discord.ext.commands import Bot
from jarvis.cogs.modlog import command, member, message
def setup(bot: Bot) -> None:
"""Add modlog cogs to J.A.R.V.I.S."""
command.ModlogCommandCog(bot)
member.ModlogMemberCog(bot)
message.ModlogMessageCog(bot)

View file

@ -1,48 +0,0 @@
"""J.A.R.V.I.S. ModlogCommandCog."""
from discord import DMChannel
from discord.ext import commands
from discord_slash import SlashContext
from jarvis.db.models import Setting
from jarvis.utils import build_embed
from jarvis.utils.field import Field
class ModlogCommandCog(commands.Cog):
"""J.A.R.V.I.S. ModlogCommandCog."""
def __init__(self, bot: commands.Bot):
self.bot = bot
@commands.Cog.listener()
async def on_slash_command(self, ctx: SlashContext) -> None:
"""Process on_slash_command events."""
if not isinstance(ctx.channel, DMChannel) and ctx.name not in ["pw"]:
modlog = Setting.objects(guild=ctx.guild.id, setting="modlog").first()
if modlog:
channel = ctx.guild.get_channel(modlog.value)
fields = [
Field("Command", ctx.name),
]
if ctx.kwargs:
kwargs_string = " ".join(f"{k}: {str(ctx.kwargs[k])}" for k in ctx.kwargs)
fields.append(
Field(
"Keyword Args",
kwargs_string,
False,
)
)
if ctx.subcommand_name:
fields.insert(1, Field("Subcommand", ctx.subcommand_name))
embed = build_embed(
title="Command Invoked",
description=f"{ctx.author.mention} invoked a command",
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.username}#{ctx.author.discriminator} | {ctx.author.id}"
)
await channel.send(embed=embed)

View file

@ -1,332 +0,0 @@
"""J.A.R.V.I.S. ModlogMemberCog."""
import asyncio
from datetime import datetime, timedelta
import discord
from discord.ext import commands
from discord.utils import find
from jarvis.cogs.modlog.utils import get_latest_log, modlog_embed
from jarvis.config import get_config
from jarvis.db.models import Ban, Kick, Mute, Setting, Unban
from jarvis.utils import build_embed
from jarvis.utils.field import Field
class ModlogMemberCog(commands.Cog):
"""J.A.R.V.I.S. ModlogMemberCog."""
def __init__(self, bot: commands.Bot):
self.bot = bot
self.cache = []
@commands.Cog.listener()
async def on_member_ban(self, guild: discord.Guild, user: discord.User) -> None:
"""Process on_member_ban events."""
modlog = Setting.objects(guild=guild.id, setting="modlog").first()
if modlog:
channel = guild.get_channel(modlog.value)
await asyncio.sleep(0.5) # Need to wait for audit log
auditlog = await guild.audit_logs(
limit=50,
action=discord.AuditLogAction.ban,
after=datetime.utcnow() - timedelta(seconds=15),
oldest_first=False,
).flatten()
log: discord.AuditLogEntry = get_latest_log(auditlog, user)
admin: discord.User = log.user
if admin.id == get_config().client_id:
await asyncio.sleep(3)
ban = (
Ban.objects(
guild=guild.id,
user=user.id,
active=True,
)
.order_by("-created_at")
.first()
)
if ban:
admin = guild.get_member(ban.admin)
embed = modlog_embed(
user,
admin,
log,
"User banned",
f"{user.mention} was banned from {guild.name}",
)
await channel.send(embed=embed)
@commands.Cog.listener()
async def on_member_unban(self, guild: discord.Guild, user: discord.User) -> None:
"""Process on_member_unban events."""
modlog = Setting.objects(guild=guild.id, setting="modlog").first()
if modlog:
channel = guild.get_channel(modlog.value)
await asyncio.sleep(0.5) # Need to wait for audit log
auditlog = await guild.audit_logs(
limit=50,
action=discord.AuditLogAction.unban,
after=datetime.utcnow() - timedelta(seconds=15),
oldest_first=False,
).flatten()
log: discord.AuditLogEntry = get_latest_log(auditlog, user)
admin: discord.User = log.user
if admin.id == get_config().client_id:
await asyncio.sleep(3)
unban = (
Unban.objects(
guild=guild.id,
user=user.id,
)
.order_by("-created_at")
.first()
)
admin = guild.get_member(unban.admin)
embed = modlog_embed(
user,
admin,
log,
"User unbanned",
f"{user.mention} was unbanned from {guild.name}",
)
await channel.send(embed=embed)
@commands.Cog.listener()
async def on_member_remove(self, user: discord.Member) -> None:
"""Process on_member_remove events."""
modlog = Setting.objects(guild=user.guild.id, setting="modlog").first()
if modlog:
channel = user.guild.get_channel(modlog.value)
await asyncio.sleep(0.5) # Need to wait for audit log
auditlog = await user.guild.audit_logs(
limit=50,
action=discord.AuditLogAction.kick,
after=datetime.utcnow() - timedelta(seconds=15),
oldest_first=False,
).flatten()
count = 0
log: discord.AuditLogEntry = get_latest_log(auditlog, user)
while not log:
if count == 30:
break
await asyncio.sleep(0.5)
log: discord.AuditLogEntry = get_latest_log(auditlog, user)
count += 1
if not log:
return
admin: discord.User = log.user
if admin.id == get_config().client_id:
await asyncio.sleep(3)
kick = (
Kick.objects(
guild=user.guild.id,
user=user.id,
)
.order_by("-created_at")
.first()
)
if kick:
admin = user.guild.get_member(kick.admin)
embed = modlog_embed(
user,
admin,
log,
"User Kicked",
f"{user.mention} was kicked from {user.guild.name}",
)
await channel.send(embed=embed)
async def process_mute(self, before: discord.Member, after: discord.Member) -> discord.Embed:
"""Process mute event."""
await asyncio.sleep(0.5) # Need to wait for audit log
auditlog = await before.guild.audit_logs(
limit=50,
action=discord.AuditLogAction.member_role_update,
after=datetime.utcnow() - timedelta(seconds=15),
oldest_first=False,
).flatten()
log: discord.AuditLogEntry = get_latest_log(auditlog, before)
admin: discord.User = log.user
if admin.id == get_config().client_id:
await asyncio.sleep(3)
mute = (
Mute.objects(
guild=before.guild.id,
user=before.id,
active=True,
)
.order_by("-created_at")
.first()
)
if mute:
admin = before.guild.get_member(mute.admin)
return modlog_embed(
member=before,
admin=admin,
log=log,
title="User Muted",
desc=f"{before.mention} was muted",
)
async def process_unmute(self, before: discord.Member, after: discord.Member) -> discord.Embed:
"""Process unmute event."""
await asyncio.sleep(0.5) # Need to wait for audit log
auditlog = await before.guild.audit_logs(
limit=50,
action=discord.AuditLogAction.member_role_update,
after=datetime.utcnow() - timedelta(seconds=15),
oldest_first=False,
).flatten()
log: discord.AuditLogEntry = get_latest_log(auditlog, before)
admin: discord.User = log.user
if admin.id == get_config().client_id:
await asyncio.sleep(3)
mute = (
Mute.objects(
guild=before.guild.id,
user=before.id,
active=True,
)
.order_by("-created_at")
.first()
)
if mute:
admin = before.guild.get_member(mute.admin)
return modlog_embed(
member=before,
admin=admin,
log=log,
title="User Muted",
desc=f"{before.mention} was muted",
)
async def process_verify(self, before: discord.Member, after: discord.Member) -> discord.Embed:
"""Process verification event."""
await asyncio.sleep(0.5) # Need to wait for audit log
auditlog = await before.guild.audit_logs(
limit=50,
action=discord.AuditLogAction.member_role_update,
after=datetime.utcnow() - timedelta(seconds=15),
oldest_first=False,
).flatten()
log: discord.AuditLogEntry = get_latest_log(auditlog, before)
admin: discord.User = log.user
return modlog_embed(
member=before,
admin=admin,
log=log,
title="User Verified",
desc=f"{before.mention} was verified",
)
async def process_rolechange(
self, before: discord.Member, after: discord.Member
) -> discord.Embed:
"""Process rolechange event."""
await asyncio.sleep(0.5) # Need to wait for audit log
auditlog = await before.guild.audit_logs(
limit=50,
action=discord.AuditLogAction.member_role_update,
after=datetime.utcnow() - timedelta(seconds=15),
oldest_first=False,
).flatten()
log: discord.AuditLogEntry = get_latest_log(auditlog, before)
admin: discord.User = log.user
role = None
title = "User Given Role"
verb = "was given"
if len(before.roles) > len(after.roles):
title = "User Forfeited Role"
verb = "forfeited"
role = find(lambda x: x not in after.roles, before.roles)
elif len(before.roles) < len(after.roles):
role = find(lambda x: x not in before.roles, after.roles)
role_text = role.mention if role else "||`[redacted]`||"
return modlog_embed(
member=before,
admin=admin,
log=log,
title=title,
desc=f"{before.mention} {verb} role {role_text}",
)
@commands.Cog.listener()
async def on_member_update(self, before: discord.Member, after: discord.Member) -> None:
"""Process on_member_update events.
Caches events due to double-send bug
"""
h = hash(hash(before) * hash(after))
if h not in self.cache:
self.cache.append(h)
else:
return
modlog = Setting.objects(guild=before.guild.id, setting="modlog").first()
if modlog:
channel = after.guild.get_channel(modlog.value)
await asyncio.sleep(0.5) # Need to wait for audit log
embed = None
mute = Setting.objects(guild=before.guild.id, setting="mute").first()
verified = Setting.objects(guild=before.guild.id, setting="verified").first()
mute_role = None
verified_role = None
if mute:
mute_role = before.guild.get_role(mute.value)
if verified:
verified_role = before.guild.get_role(verified.value)
if mute and mute_role in after.roles and mute_role not in before.roles:
embed = await self.process_mute(before, after)
elif mute and mute_role in before.roles and mute_role not in after.roles:
embed = await self.process_unmute(before, after)
elif verified and verified_role not in before.roles and verified_role in after.roles:
embed = await self.process_verify(before, after)
elif before.nick != after.nick:
auditlog = await before.guild.audit_logs(
limit=50,
action=discord.AuditLogAction.member_update,
after=datetime.utcnow() - timedelta(seconds=15),
oldest_first=False,
).flatten()
log: discord.AuditLogEntry = get_latest_log(auditlog, before)
bname = before.nick if before.nick else before.name
aname = after.nick if after.nick else after.name
fields = [
Field(
name="Before",
value=f"{bname} ({before.name}#{before.discriminator})",
),
Field(
name="After",
value=f"{aname} ({after.name}#{after.discriminator})",
),
]
if log.user.id != before.id:
fields.append(
Field(
name="Moderator",
value=f"{log.user.mention} ({log.user.name}#{log.user.discriminator})",
)
)
if log.reason:
fields.append(
Field(name="Reason", value=log.reason, inline=False),
)
embed = build_embed(
title="User Nick Changed",
description=f"{after.mention} changed their nickname",
color="#fc9e3f",
fields=fields,
timestamp=log.created_at,
)
embed.set_author(name=f"{after.name}", icon_url=after.display_avatar.url)
embed.set_footer(text=f"{after.name}#{after.discriminator} | {after.id}")
elif len(before.roles) != len(after.roles):
# TODO: User got a new role
embed = await self.process_rolechange(before, after)
if embed:
await channel.send(embed=embed)
self.cache.remove(h)

View file

@ -1,108 +0,0 @@
"""J.A.R.V.I.S. ModlogMessageCog."""
import discord
from discord.ext import commands
from jarvis.db.models import Setting
from jarvis.utils import build_embed
from jarvis.utils.field import Field
class ModlogMessageCog(commands.Cog):
"""J.A.R.V.I.S. ModlogMessageCog."""
def __init__(self, bot: commands.Bot):
self.bot = bot
@commands.Cog.listener()
async def on_message_edit(self, before: discord.Message, after: discord.Message) -> None:
"""Process on_message_edit events."""
if not before.author.bot:
modlog = Setting.objects(guild=after.guild.id, setting="modlog").first()
if modlog:
if before.content == after.content or before.content is None:
return
channel = before.guild.get_channel(modlog.value)
fields = [
Field(
"Original Message",
before.content if before.content else "N/A",
False,
),
Field(
"New Message",
after.content if after.content else "N/A",
False,
),
]
embed = build_embed(
title="Message Edited",
description=f"{before.author.mention} edited a message",
fields=fields,
color="#fc9e3f",
timestamp=after.edited_at,
url=after.jump_url,
)
embed.set_author(
name=before.author.name,
icon_url=before.author.display_avatar.url,
url=after.jump_url,
)
embed.set_footer(
text=f"{before.author.name}#{before.author.discriminator} | {before.author.id}"
)
await channel.send(embed=embed)
@commands.Cog.listener()
async def on_message_delete(self, message: discord.Message) -> None:
"""Process on_message_delete events."""
modlog = Setting.objects(guild=message.guild.id, setting="modlog").first()
if modlog:
fields = [Field("Original Message", message.content or "N/A", False)]
if message.attachments:
value = "\n".join([f"[{x.filename}]({x.url})" for x in message.attachments])
fields.append(
Field(
name="Attachments",
value=value,
inline=False,
)
)
if message.stickers:
value = "\n".join([f"[{x.name}]({x.image_url})" for x in message.stickers])
fields.append(
Field(
name="Stickers",
value=value,
inline=False,
)
)
if message.embeds:
value = str(len(message.embeds)) + " embeds"
fields.append(
Field(
name="Embeds",
value=value,
inline=False,
)
)
channel = message.guild.get_channel(modlog.value)
embed = build_embed(
title="Message Deleted",
description=f"{message.author.mention}'s message was deleted",
fields=fields,
color="#fc9e3f",
)
embed.set_author(
name=message.author.name,
icon_url=message.author.display_avatar.url,
url=message.jump_url,
)
embed.set_footer(
text=f"{message.author.name}#{message.author.discriminator} | {message.author.id}"
)
await channel.send(embed=embed)

View file

@ -1,47 +0,0 @@
"""J.A.R.V.I.S. Modlog Cog Utilities."""
from datetime import datetime, timedelta
from typing import List
import discord
from discord import AuditLogEntry, Member
from discord.utils import find
from jarvis.utils import build_embed
from jarvis.utils.field import Field
def modlog_embed(
member: discord.Member,
admin: discord.Member,
log: discord.AuditLogEntry,
title: str,
desc: str,
) -> discord.Embed:
"""Get modlog embed."""
fields = [
Field(
name="Moderator",
value=f"{admin.mention} ({admin.name}#{admin.discriminator})",
),
]
if log.reason:
fields.append(Field(name="Reason", value=log.reason, inline=False))
embed = build_embed(
title=title,
description=desc,
color="#fc9e3f",
fields=fields,
timestamp=log.created_at,
)
embed.set_author(name=f"{member.name}", icon_url=member.display_avatar.url)
embed.set_footer(text=f"{member.name}#{member.discriminator} | {member.id}")
return embed
def get_latest_log(auditlog: List[AuditLogEntry], target: Member) -> AuditLogEntry:
"""Filter AuditLog to get latest entry."""
before = datetime.utcnow() - timedelta(seconds=10)
return find(
lambda x: x.target.id == target.id and x.created_at > before,
auditlog,
)

View file

@ -1,47 +0,0 @@
"""J.A.R.V.I.S. Owner Cog."""
from dis_snek import MessageContext, Scale, Snake, message_command
from dis_snek.models.discord.user import User
from dis_snek.models.snek.checks import is_owner
from dis_snek.models.snek.command import check
from jarvis.config import reload_config
from jarvis.db.models import Config
class OwnerCog(Scale):
"""
J.A.R.V.I.S. management cog.
Used by admins to control core J.A.R.V.I.S. systems
"""
def __init__(self, bot: Snake):
self.bot = bot
self.admins = Config.objects(key="admins").first()
@message_command(name="addadmin")
@check(is_owner())
async def _add(self, ctx: MessageContext, user: User) -> None:
if user.id in self.admins.value:
await ctx.send(f"{user.mention} is already an admin.")
return
self.admins.value.append(user.id)
self.admins.save()
reload_config()
await ctx.send(f"{user.mention} is now an admin. Use this power carefully.")
@message_command(name="deladmin")
@is_owner()
async def _remove(self, ctx: MessageContext, user: User) -> None:
if user.id not in self.admins.value:
await ctx.send(f"{user.mention} is not an admin.")
return
self.admins.value.remove(user.id)
self.admins.save()
reload_config()
await ctx.send(f"{user.mention} is no longer an admin.")
def setup(bot: Snake) -> None:
"""Add OwnerCog to J.A.R.V.I.S."""
OwnerCog(bot)

View file

@ -2,74 +2,81 @@
import asyncio
import re
from datetime import datetime, timedelta
from typing import List, Optional
from typing import List
from bson import ObjectId
from dis_snek import InteractionContext, Snake
from dis_snek import InteractionContext, Scale, Snake
from dis_snek.client.utils.misc_utils import get
from dis_snek.models.discord.channel import GuildChannel
from dis_snek.models.discord.components import ActionRow, Select, SelectOption
from dis_snek.models.discord.embed import Embed, EmbedField
from dis_snek.models.discord.modal import InputText, Modal, TextStyles
from dis_snek.models.snek.application_commands import (
OptionTypes,
SlashCommandChoice,
slash_command,
slash_option,
)
from jarvis_core.db import q
from jarvis_core.db.models import Reminder
from jarvis.db.models import Reminder
from jarvis.utils import build_embed
from jarvis.utils.cachecog import CacheCog
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
flags=re.IGNORECASE,
)
class RemindmeCog(CacheCog):
class RemindmeCog(Scale):
"""J.A.R.V.I.S. Remind Me Cog."""
def __init__(self, bot: Snake):
super().__init__(bot)
@slash_command(name="remindme", description="Set a reminder")
@slash_option(
name="message",
description="What to remind you of?",
name="private",
description="Send as DM?",
opt_type=OptionTypes.STRING,
required=True,
)
@slash_option(
name="weeks",
description="Number of weeks?",
opt_type=OptionTypes.INTEGER,
required=False,
)
@slash_option(
name="days", description="Number of days?", opt_type=OptionTypes.INTEGER, required=False
)
@slash_option(
name="hours",
description="Number of hours?",
opt_type=OptionTypes.INTEGER,
required=False,
)
@slash_option(
name="minutes",
description="Number of minutes?",
opt_type=OptionTypes.INTEGER,
required=False,
choices=[
SlashCommandChoice(name="Yes", value="y"),
SlashCommandChoice(name="No", value="n"),
],
)
async def _remindme(
self,
ctx: InteractionContext,
message: Optional[str] = None,
weeks: Optional[int] = 0,
days: Optional[int] = 0,
hours: Optional[int] = 0,
minutes: Optional[int] = 0,
private: str = "n",
) -> None:
if len(message) > 100:
await ctx.send("Reminder cannot be > 100 characters.", ephemeral=True)
private = private == "y"
modal = Modal(
title="Set your reminder!",
components=[
InputText(
label="What to remind you?",
placeholder="Reminder",
style=TextStyles.PARAGRAPH,
custom_id="message",
max_length=500,
),
InputText(
label="When to remind you?",
placeholder="1h 30m",
style=TextStyles.SHORT,
custom_id="delay",
),
],
)
await ctx.send_modal(modal)
try:
response = await self.bot.wait_for_modal(modal, author=ctx.author.id, timeout=60 * 5)
message = response.responses.get("message")
delay = response.responses.get("delay")
except asyncio.TimeoutError:
return
if len(message) > 500:
await ctx.send("Reminder cannot be > 500 characters.", ephemeral=True)
return
elif invites.search(message):
await ctx.send(
@ -81,32 +88,23 @@ class RemindmeCog(CacheCog):
await ctx.send("Hey, you should probably make this readable", ephemeral=True)
return
if not any([weeks, days, hours, minutes]):
units = {"w": "weeks", "d": "days", "h": "hours", "m": "minutes", "s": "seconds"}
delta = {"weeks": 0, "days": 0, "hours": 0, "minutes": 0, "seconds": 0}
if times := time_pattern.findall(delay):
for t in times:
delta[units[t[-1]]] += float(t[:-1])
else:
await ctx.send(
"Invalid time string, please follow example: `1w 3d 7h 5m 20s`", ephemeral=True
)
return
if not any(value for value in delta.items()):
await ctx.send("At least one time period is required", ephemeral=True)
return
weeks = abs(weeks)
days = abs(days)
hours = abs(hours)
minutes = abs(minutes)
if weeks and weeks > 4:
await ctx.send("Cannot be farther than 4 weeks out!", ephemeral=True)
return
elif days and days > 6:
await ctx.send("Use weeks instead of 7+ days, please.", ephemeral=True)
return
elif hours and hours > 23:
await ctx.send("Use days instead of 24+ hours, please.", ephemeral=True)
return
elif minutes and minutes > 59:
await ctx.send("Use hours instead of 59+ minutes, please.", ephemeral=True)
return
reminders = Reminder.objects(user=ctx.author.id, active=True).count()
reminders = len([x async for x in Reminder.find(q(user=ctx.author.id, active=True))])
if reminders >= 5:
await ctx.send(
"You already have 5 (or more) active reminders. "
@ -115,21 +113,19 @@ class RemindmeCog(CacheCog):
)
return
remind_at = datetime.utcnow() + timedelta(
weeks=weeks,
days=days,
hours=hours,
minutes=minutes,
)
remind_at = datetime.now() + timedelta(**delta)
_ = Reminder(
user=ctx.author_id,
r = Reminder(
user=ctx.author.id,
channel=ctx.channel.id,
guild=ctx.guild.id,
message=message,
remind_at=remind_at,
private=private,
active=True,
).save()
)
await r.commit()
embed = build_embed(
title="Reminder Set",
@ -150,7 +146,7 @@ class RemindmeCog(CacheCog):
)
embed.set_thumbnail(url=ctx.author.display_avatar.url)
await ctx.send(embed=embed)
await response.send(embed=embed, ephemeral=private)
async def get_reminders_embed(
self, ctx: InteractionContext, reminders: List[Reminder]
@ -158,13 +154,22 @@ class RemindmeCog(CacheCog):
"""Build embed for paginator."""
fields = []
for reminder in reminders:
fields.append(
EmbedField(
name=reminder.remind_at.strftime("%Y-%m-%d %H:%M UTC"),
value=f"{reminder.message}\n\u200b",
inline=False,
if reminder.private and isinstance(ctx.channel, GuildChannel):
fields.embed(
EmbedField(
name=reminder.remind_at.strftime("%Y-%m-%d %H:%M UTC"),
value="Please DM me this command to view the content of this reminder",
inline=False,
)
)
else:
fields.append(
EmbedField(
name=reminder.remind_at.strftime("%Y-%m-%d %H:%M UTC"),
value=f"{reminder.message}\n\u200b",
inline=False,
)
)
)
embed = build_embed(
title=f"{len(reminders)} Active Reminder(s)",
@ -190,7 +195,7 @@ class RemindmeCog(CacheCog):
ephemeral=True,
)
return
reminders = Reminder.objects(user=ctx.author.id, active=True)
reminders = await Reminder.find(q(user=ctx.author.id, active=True))
if not reminders:
await ctx.send("You have no reminders set.", ephemeral=True)
return
@ -201,7 +206,7 @@ class RemindmeCog(CacheCog):
@slash_command(name="reminders", sub_cmd_name="delete", sub_cmd_description="Delete a reminder")
async def _delete(self, ctx: InteractionContext) -> None:
reminders = Reminder.objects(user=ctx.author.id, active=True)
reminders = await Reminder.find(q(user=ctx.author.id, active=True))
if not reminders:
await ctx.send("You have no reminders set", ephemeral=True)
return
@ -237,22 +242,32 @@ class RemindmeCog(CacheCog):
messages=message,
timeout=60 * 5,
)
fields = []
for to_delete in context.context.values:
_ = Reminder.objects(user=ctx.author.id, id=ObjectId(to_delete)).delete()
reminder = get(reminders, user=ctx.author.id, id=ObjectId(to_delete))
if reminder.private and isinstance(ctx.channel, GuildChannel):
fields.append(
EmbedField(
name=reminder.remind_at.strftime("%Y-%m-%d %H:%M UTC"),
value="Private reminder",
inline=False,
)
)
else:
fields.append(
EmbedField(
name=reminder.remind_at.strftime("%Y-%m-%d %H:%M UTC"),
value=reminder.message,
inline=False,
)
)
await reminder.delete()
for row in components:
for component in row.components:
component.disabled = True
fields = []
for reminder in filter(lambda x: str(x.id) in context.context.values, reminders):
fields.append(
EmbedField(
name=reminder.remind_at.strftime("%Y-%m-%d %H:%M UTC"),
value=reminder.message,
inline=False,
)
)
embed = build_embed(
title="Deleted Reminder(s)",
description="",
@ -260,7 +275,7 @@ class RemindmeCog(CacheCog):
)
embed.set_author(
name=ctx.author.username + "#" + ctx.author.discriminator,
name=ctx.author.display_name + "#" + ctx.author.discriminator,
icon_url=ctx.author.display_avatar.url,
)
embed.set_thumbnail(url=ctx.author.display_avatar.url)

View file

@ -2,6 +2,7 @@
import asyncio
from dis_snek import InteractionContext, Permissions, Scale, Snake
from dis_snek.client.utils.misc_utils import get
from dis_snek.models.discord.components import ActionRow, Select, SelectOption
from dis_snek.models.discord.embed import EmbedField
from dis_snek.models.discord.role import Role
@ -12,9 +13,10 @@ from dis_snek.models.snek.application_commands import (
)
from dis_snek.models.snek.command import check, cooldown
from dis_snek.models.snek.cooldowns import Buckets
from jarvis_core.db import q
from jarvis_core.db.models import Rolegiver
from jarvis.db.models import Rolegiver
from jarvis.utils import build_embed, get
from jarvis.utils import build_embed
from jarvis.utils.permissions import admin_or_permissions
@ -30,7 +32,7 @@ class RolegiverCog(Scale):
@slash_option(name="role", description="Role to add", opt_type=OptionTypes.ROLE, required=True)
@check(admin_or_permissions(Permissions.MANAGE_GUILD))
async def _rolegiver_add(self, ctx: InteractionContext, role: Role) -> None:
setting = Rolegiver.objects(guild=ctx.guild.id).first()
setting = await Rolegiver.find_one(q(guild=ctx.guild.id))
if setting and role.id in setting.roles:
await ctx.send("Role already in rolegiver", ephemeral=True)
return
@ -80,7 +82,7 @@ class RolegiverCog(Scale):
)
@check(admin_or_permissions(Permissions.MANAGE_GUILD))
async def _rolegiver_remove(self, ctx: InteractionContext) -> None:
setting = Rolegiver.objects(guild=ctx.guild.id).first()
setting = await Rolegiver.find_one(q(guild=ctx.guild.id))
if not setting or (setting and not setting.roles):
await ctx.send("Rolegiver has no roles", ephemeral=True)
return
@ -162,7 +164,7 @@ class RolegiverCog(Scale):
@slash_command(name="rolegiver", sub_cmd_name="list", description="List rolegiver roles")
async def _rolegiver_list(self, ctx: InteractionContext) -> None:
setting = Rolegiver.objects(guild=ctx.guild.id).first()
setting = await Rolegiver.find_one(q(guild=ctx.guild.id))
if not setting or (setting and not setting.roles):
await ctx.send("Rolegiver has no roles", ephemeral=True)
return
@ -198,7 +200,7 @@ class RolegiverCog(Scale):
@slash_command(name="role", 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 = Rolegiver.objects(guild=ctx.guild.id).first()
setting = await Rolegiver.find_one(q(guild=ctx.guild.id))
if not setting or (setting and not setting.roles):
await ctx.send("Rolegiver has no roles", ephemeral=True)
return
@ -276,7 +278,7 @@ class RolegiverCog(Scale):
async def _role_remove(self, ctx: InteractionContext) -> None:
user_roles = ctx.author.roles
setting = Rolegiver.objects(guild=ctx.guild.id).first()
setting = await Rolegiver.find_one(q(guild=ctx.guild.id))
if not setting or (setting and not setting.roles):
await ctx.send("Rolegiver has no roles", ephemeral=True)
return
@ -355,7 +357,7 @@ class RolegiverCog(Scale):
)
@check(admin_or_permissions(Permissions.MANAGE_GUILD))
async def _rolegiver_cleanup(self, ctx: InteractionContext) -> None:
setting = Rolegiver.objects(guild=ctx.guild.id).first()
setting = await Rolegiver.find_one(q(guild=ctx.guild.id))
if not setting or not setting.roles:
await ctx.send("Rolegiver has no roles", ephemeral=True)
guild_role_ids = [r.id for r in ctx.guild.roles]

View file

@ -7,6 +7,7 @@ from discord.ext import commands
from discord.utils import find
from discord_slash import SlashContext, cog_ext
from discord_slash.utils.manage_commands import create_option
from jarvis_core.db import q
from jarvis.db.models import Setting
from jarvis.utils import build_embed
@ -20,9 +21,9 @@ class SettingsCog(commands.Cog):
def __init__(self, bot: commands.Bot):
self.bot = bot
def update_settings(self, setting: str, value: Any, guild: int) -> bool:
async def update_settings(self, setting: str, value: Any, guild: int) -> bool:
"""Update a guild setting."""
existing = Setting.objects(setting=setting, guild=guild).first()
existing = await Setting.find_one(q(setting=setting, guild=guild))
if not existing:
existing = Setting(setting=setting, guild=guild, value=value)
existing.value = value
@ -30,9 +31,12 @@ class SettingsCog(commands.Cog):
return updated is not None
def delete_settings(self, setting: str, guild: int) -> bool:
async def delete_settings(self, setting: str, guild: int) -> bool:
"""Delete a guild setting."""
return Setting.objects(setting=setting, guild=guild).delete()
existing = await Setting.find_one(q(setting=setting, guild=guild))
if existing:
return await existing.delete()
return False
@cog_ext.cog_subcommand(
base="settings",
@ -59,24 +63,24 @@ class SettingsCog(commands.Cog):
@cog_ext.cog_subcommand(
base="settings",
subcommand_group="set",
name="userlog",
description="Set userlog channel",
name="activitylog",
description="Set activitylog channel",
choices=[
create_option(
name="channel",
description="Userlog channel",
description="Activitylog channel",
opt_type=7,
required=True,
)
],
)
@check(admin_or_permissions(manage_guild=True))
async def _set_userlog(self, ctx: SlashContext, channel: TextChannel) -> None:
async def _set_activitylog(self, ctx: SlashContext, channel: TextChannel) -> None:
if not isinstance(channel, TextChannel):
await ctx.send("Channel must be a TextChannel", ephemeral=True)
return
self.update_settings("userlog", channel.id, ctx.guild.id)
await ctx.send(f"Settings applied. New userlog channel is {channel.mention}")
self.update_settings("activitylog", channel.id, ctx.guild.id)
await ctx.send(f"Settings applied. New activitylog channel is {channel.mention}")
@cog_ext.cog_subcommand(
base="settings",
@ -172,12 +176,12 @@ class SettingsCog(commands.Cog):
@cog_ext.cog_subcommand(
base="settings",
subcommand_group="unset",
name="userlog",
description="Unset userlog channel",
name="activitylog",
description="Unset activitylog channel",
)
@check(admin_or_permissions(manage_guild=True))
async def _unset_userlog(self, ctx: SlashContext) -> None:
self.delete_settings("userlog", ctx.guild.id)
async def _unset_activitylog(self, ctx: SlashContext) -> None:
self.delete_settings("activitylog", ctx.guild.id)
await ctx.send("Setting removed.")
@cog_ext.cog_subcommand(
@ -230,7 +234,7 @@ class SettingsCog(commands.Cog):
value = value.mention
else:
value = "||`[redacted]`||"
elif setting.setting in ["userlog", "modlog"]:
elif setting.setting in ["activitylog", "modlog"]:
value = find(lambda x: x.id == value, ctx.guild.text_channels)
if value:
value = value.mention

View file

@ -1,5 +1,6 @@
"""J.A.R.V.I.S. Starboard Cog."""
from dis_snek import InteractionContext, Permissions, Scale, Snake
from dis_snek.client.utils.misc_utils import find
from dis_snek.models.discord.channel import GuildText
from dis_snek.models.discord.components import ActionRow, Select, SelectOption
from dis_snek.models.discord.message import Message
@ -11,9 +12,10 @@ from dis_snek.models.snek.application_commands import (
slash_option,
)
from dis_snek.models.snek.command import check
from jarvis_core.db import q
from jarvis_core.db.models import Star, Starboard
from jarvis.db.models import Star, Starboard
from jarvis.utils import build_embed, find
from jarvis.utils import build_embed
from jarvis.utils.permissions import admin_or_permissions
supported_images = [
@ -34,7 +36,7 @@ class StarboardCog(Scale):
@slash_command(name="starboard", sub_cmd_name="list", sub_cmd_description="List all starboards")
@check(admin_or_permissions(Permissions.MANAGE_GUILD))
async def _list(self, ctx: InteractionContext) -> None:
starboards = Starboard.objects(guild=ctx.guild.id)
starboards = await Starboard.find(q(guild=ctx.guild.id)).to_list(None)
if starboards != []:
message = "Available Starboards:\n"
for s in starboards:
@ -64,21 +66,21 @@ class StarboardCog(Scale):
await ctx.send("Channel must be a GuildText", ephemeral=True)
return
exists = Starboard.objects(channel=channel.id, guild=ctx.guild.id).first()
exists = await Starboard.find_one(q(channel=channel.id, guild=ctx.guild.id))
if exists:
await ctx.send(f"Starboard already exists at {channel.mention}.", ephemeral=True)
return
count = Starboard.objects(guild=ctx.guild.id).count()
count = await Starboard.count_documents(q(guild=ctx.guild.id))
if count >= 25:
await ctx.send("25 starboard limit reached", ephemeral=True)
return
_ = Starboard(
await Starboard(
guild=ctx.guild.id,
channel=channel.id,
admin=ctx.author.id,
).save()
).commit()
await ctx.send(f"Starboard created. Check it out at {channel.mention}.")
@slash_command(
@ -92,29 +94,13 @@ class StarboardCog(Scale):
)
@check(admin_or_permissions(Permissions.MANAGE_GUILD))
async def _delete(self, ctx: InteractionContext, channel: GuildText) -> None:
deleted = Starboard.objects(channel=channel.id, guild=ctx.guild.id).delete()
if deleted:
_ = Star.objects(starboard=channel.id).delete()
found = await Starboard.find_one(q(channel=channel.id, guild=ctx.guild.id))
if found:
await found.delete()
await ctx.send(f"Starboard deleted from {channel.mention}.")
else:
await ctx.send(f"Starboard not found in {channel.mention}.", ephemeral=True)
@context_menu(name="Star Message", context_type=CommandTypes.MESSAGE)
async def _star_message(self, ctx: InteractionContext) -> None:
await self._star_add._can_run(ctx)
await self._star_add.callback(ctx, message=str(ctx.target_id))
@slash_command(name="star", sub_cmd_name="add", description="Star a message")
@slash_option(
name="message", description="Message to star", opt_type=OptionTypes.STRING, required=True
)
@slash_option(
name="channel",
description="Channel that has the message, not required if used in same channel",
opt_type=OptionTypes.CHANNEL,
required=False,
)
@check(admin_or_permissions(Permissions.MANAGE_GUILD))
async def _star_add(
self,
ctx: InteractionContext,
@ -123,7 +109,7 @@ class StarboardCog(Scale):
) -> None:
if not channel:
channel = ctx.channel
starboards = Starboard.objects(guild=ctx.guild.id)
starboards = await Starboard.find(q(guild=ctx.guild.id)).to_list(None)
if not starboards:
await ctx.send("No starboards exist.", ephemeral=True)
return
@ -133,7 +119,7 @@ class StarboardCog(Scale):
if not isinstance(message, Message):
if message.startswith("https://"):
message = message.split("/")[-1]
message = await channel.get_message(int(message))
message = await channel.fetch_message(int(message))
if not message:
await ctx.send("Message not found", ephemeral=True)
@ -165,12 +151,14 @@ class StarboardCog(Scale):
starboard = channel_list[int(com_ctx.context.values[0])]
exists = Star.objects(
message=message.id,
channel=message.channel.id,
guild=message.guild.id,
starboard=starboard.id,
).first()
exists = await Star.find_one(
q(
message=message.id,
channel=channel.id,
guild=ctx.guild.id,
starboard=starboard.id,
)
)
if exists:
await ctx.send(
@ -179,7 +167,7 @@ class StarboardCog(Scale):
)
return
count = Star.objects(guild=message.guild.id, starboard=starboard.id).count()
count = await Star.count_documents(q(guild=ctx.guild.id, starboard=starboard.id))
content = message.content
attachments = message.attachments
@ -202,24 +190,24 @@ class StarboardCog(Scale):
embed.set_author(
name=message.author.display_name,
url=message.jump_url,
icon_url=message.author.display_avatar.url,
icon_url=message.author.avatar.url,
)
embed.set_footer(text=message.guild.name + " | " + message.channel.name)
embed.set_footer(text=ctx.guild.name + " | " + channel.name)
if image_url:
embed.set_image(url=image_url)
star = await starboard.send(embed=embed)
_ = Star(
await Star(
index=count,
message=message.id,
channel=message.channel.id,
guild=message.guild.id,
channel=channel.id,
guild=ctx.guild.id,
starboard=starboard.id,
admin=ctx.author.id,
star=star.id,
active=True,
).save()
).commit()
components[0].components[0].disabled = True
@ -228,6 +216,27 @@ class StarboardCog(Scale):
components=components,
)
@context_menu(name="Star Message", context_type=CommandTypes.MESSAGE)
@check(admin_or_permissions(Permissions.MANAGE_GUILD))
async def _star_message(self, ctx: InteractionContext) -> None:
await self._star_add(ctx, message=str(ctx.target_id))
@slash_command(name="star", sub_cmd_name="add", description="Star a message")
@slash_option(
name="message", description="Message to star", opt_type=OptionTypes.STRING, required=True
)
@slash_option(
name="channel",
description="Channel that has the message, not required if used in same channel",
opt_type=OptionTypes.CHANNEL,
required=False,
)
@check(admin_or_permissions(Permissions.MANAGE_GUILD))
async def _star_message_slash(
self, ctx: InteractionContext, message: str, channel: GuildText
) -> None:
await self._star_add(ctx, message, channel)
@slash_command(name="star", sub_cmd_name="delete", description="Delete a starred message")
@slash_option(
name="id", description="Star ID to delete", opt_type=OptionTypes.INTEGER, required=True
@ -248,30 +257,33 @@ class StarboardCog(Scale):
if not isinstance(starboard, GuildText):
await ctx.send("Channel must be a GuildText channel", ephemeral=True)
return
exists = Starboard.objects(channel=starboard.id, guild=ctx.guild.id).first()
exists = await Starboard.find_one(q(channel=starboard.id, guild=ctx.guild.id))
if not exists:
# TODO: automagically create starboard
await ctx.send(
f"Starboard does not exist in {starboard.mention}. Please create it first",
ephemeral=True,
)
return
star = Star.objects(
starboard=starboard.id,
index=id,
guild=ctx.guild.id,
active=True,
).first()
star = await Star.find_one(
q(
starboard=starboard.id,
index=id,
guild=ctx.guild.id,
active=True,
)
)
if not star:
await ctx.send(f"No star exists with id {id}", ephemeral=True)
return
message = await starboard.get_message(star.star)
message = await starboard.fetch_message(star.star)
if message:
await message.delete()
star.active = False
star.save()
await star.delete()
await ctx.send(f"Star {id} deleted from {starboard.mention}")

View file

@ -2,8 +2,8 @@
import asyncio
import tweepy
from bson import ObjectId
from dis_snek import InteractionContext, Permissions, Scale, Snake
from dis_snek.client.utils.misc_utils import get
from dis_snek.models.discord.channel import GuildText
from dis_snek.models.discord.components import ActionRow, Select, SelectOption
from dis_snek.models.snek.application_commands import (
@ -13,9 +13,10 @@ from dis_snek.models.snek.application_commands import (
slash_option,
)
from dis_snek.models.snek.command import check
from jarvis_core.db import q
from jarvis_core.db.models import TwitterAccount, TwitterFollow
from jarvis.config import get_config
from jarvis.db.models import Twitter
from jarvis import jconfig
from jarvis.utils.permissions import admin_or_permissions
@ -24,7 +25,7 @@ class TwitterCog(Scale):
def __init__(self, bot: Snake):
self.bot = bot
config = get_config()
config = jconfig
auth = tweepy.AppAuthHandler(
config.twitter["consumer_key"], config.twitter["consumer_secret"]
)
@ -67,7 +68,7 @@ class TwitterCog(Scale):
return
try:
account = (await asyncio.to_thread(self.api.get_user(screen_name=handle)))[0]
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(
@ -75,42 +76,54 @@ class TwitterCog(Scale):
)
return
count = Twitter.objects(guild=ctx.guild.id).count()
count = len([i async for i in TwitterFollow.find(q(guild=ctx.guild.id))])
if count >= 12:
await ctx.send("Cannot follow more than 12 Twitter accounts", ephemeral=True)
return
exists = Twitter.objects(twitter_id=account.id, guild=ctx.guild.id)
exists = await TwitterFollow.find_one(q(twitter_id=account.id, guild=ctx.guild.id))
if exists:
await ctx.send("Twitter account already being followed in this guild", ephemeral=True)
return
t = Twitter(
handle=account.screen_name,
ta = await TwitterAccount.find_one(q(twitter_id=account.id))
if not ta:
ta = TwitterAccount(
handle=account.screen_name,
twitter_id=account.id,
last_tweet=latest_tweet.id,
)
await ta.commit()
tf = TwitterFollow(
twitter_id=account.id,
guild=ctx.guild.id,
channel=channel.id,
admin=ctx.author.id,
last_tweet=latest_tweet.id,
retweets=retweets,
)
t.save()
await tf.commit()
await ctx.send(f"Now following `@{handle}` in {channel.mention}")
@slash_command(name="twitter", sub_cmd_name="unfollow", description="Unfollow Twitter accounts")
@check(admin_or_permissions(Permissions.MANAGE_GUILD))
async def _twitter_unfollow(self, ctx: InteractionContext) -> None:
twitters = Twitter.objects(guild=ctx.guild.id)
t = TwitterFollow.find(q(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 = {str(x.id): x.handle for x in twitters}
handlemap = {}
for twitter in twitters:
option = SelectOption(label=twitter.handle, value=str(twitter.id))
account = await TwitterAccount.find_one(q(twitter_id=twitter.twitter_id))
handlemap[str(twitter.twitter_id)] = account.handle
option = SelectOption(label=account.handle, value=str(twitter.twitter_id))
options.append(option)
select = Select(
@ -118,7 +131,7 @@ class TwitterCog(Scale):
)
components = [ActionRow(select)]
block = "\n".join(x.handle for x in twitters)
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",
@ -132,7 +145,8 @@ class TwitterCog(Scale):
timeout=60 * 5,
)
for to_delete in context.context.values:
_ = Twitter.objects(guild=ctx.guild.id, id=ObjectId(to_delete)).delete()
follow = get(twitters, guild=ctx.guild.id, twitter_id=int(to_delete))
await follow.delete()
for row in components:
for component in row.components:
component.disabled = True
@ -163,14 +177,20 @@ class TwitterCog(Scale):
@check(admin_or_permissions(Permissions.MANAGE_GUILD))
async def _twitter_modify(self, ctx: InteractionContext, retweets: str) -> None:
retweets = retweets == "Yes"
twitters = Twitter.objects(guild=ctx.guild.id)
t = TwitterFollow.find(q(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:
option = SelectOption(label=twitter.handle, value=str(twitter.id))
account = await TwitterAccount.find_one(q(twitter_id=twitter.id))
handlemap[str(twitter.twitter_id)] = account.handle
option = SelectOption(label=account.handle, value=str(twitter.twitter_id))
options.append(option)
select = Select(
@ -178,7 +198,7 @@ class TwitterCog(Scale):
)
components = [ActionRow(select)]
block = "\n".join(x.handle for x in twitters)
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",
@ -192,11 +212,13 @@ class TwitterCog(Scale):
timeout=60 * 5,
)
handlemap = {str(x.id): x.handle for x in twitters}
handlemap = {}
for to_update in context.context.values:
t = Twitter.objects(guild=ctx.guild.id, id=ObjectId(to_update)).first()
t.retweets = retweets
t.save()
account = await TwitterAccount.find_one(q(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.update(q(retweets=True))
await t.commit()
for row in components:
for component in row.components:

View file

@ -219,7 +219,7 @@ class UtilCog(Scale):
async def _server_info(self, ctx: InteractionContext) -> None:
guild: Guild = ctx.guild
owner = await guild.get_owner()
owner = await guild.fetch_owner()
owner = f"{owner.username}#{owner.discriminator}" if owner else "||`[redacted]`||"

View file

@ -7,8 +7,8 @@ from dis_snek.models.application_commands import slash_command
from dis_snek.models.discord.components import Button, ButtonStyles, spread_to_rows
from dis_snek.models.snek.command import cooldown
from dis_snek.models.snek.cooldowns import Buckets
from jarvis.db.models import Setting
from jarvis_core.db import q
from jarvis_core.db.models import Setting
def create_layout() -> list:
@ -39,7 +39,7 @@ class VerifyCog(Scale):
@cooldown(bucket=Buckets.USER, rate=1, interval=15)
async def _verify(self, ctx: InteractionContext) -> None:
await ctx.defer()
role = Setting.objects(guild=ctx.guild.id, setting="verified").first()
role = await Setting.find_one(q(guild=ctx.guild.id, setting="verified"))
if not role:
await ctx.send("This guild has not enabled verification", delete_after=5)
return
@ -62,10 +62,10 @@ class VerifyCog(Scale):
for row in components:
for component in row.components:
component.disabled = True
setting = Setting.objects(guild=ctx.guild.id, setting="verified").first()
setting = await Setting.find_one(guild=ctx.guild.id, setting="verified")
role = await ctx.guild.get_role(setting.value)
await ctx.author.add_roles(role, reason="Verification passed")
setting = Setting.objects(guild=ctx.guild.id, setting="unverified").first()
setting = await Setting.find_one(guild=ctx.guild.id, setting="unverified")
if setting:
role = await ctx.guild.get_role(setting.value)
await ctx.author.remove_roles(role, reason="Verification passed")

View file

@ -1,6 +1,7 @@
"""Load the config for J.A.R.V.I.S."""
import os
from jarvis_core.config import Config as CConfig
from pymongo import MongoClient
from yaml import load
@ -10,6 +11,19 @@ except ImportError:
from yaml import Loader
class JarvisConfig(CConfig):
REQUIRED = ["token", "client_id", "mongo", "urls"]
OPTIONAL = {
"sync": False,
"log_level": "WARNING",
"scales": None,
"events": True,
"gitlab_token": None,
"max_messages": 1000,
"twitter": None,
}
class Config(object):
"""Config singleton object for J.A.R.V.I.S."""

View file

@ -1,263 +0,0 @@
"""J.A.R.V.I.S. database object for mongoengine."""
from datetime import datetime
from mongoengine import Document
from mongoengine.fields import (
BooleanField,
DateTimeField,
DictField,
DynamicField,
IntField,
ListField,
LongField,
StringField,
)
class SnowflakeField(LongField):
"""Snowflake LongField Override."""
pass
class Autopurge(Document):
"""Autopurge database object."""
guild = SnowflakeField(required=True)
channel = SnowflakeField(required=True)
delay = IntField(min_value=1, max_value=300, default=30)
admin = SnowflakeField(required=True)
created_at = DateTimeField(default=datetime.utcnow)
meta = {"db_alias": "main"}
class Autoreact(Document):
"""Autoreact database object."""
guild = SnowflakeField(required=True)
channel = SnowflakeField(required=True)
reactions = ListField(field=StringField())
admin = SnowflakeField(required=True)
created_at = DateTimeField(default=datetime.utcnow)
meta = {"db_alias": "main"}
class Ban(Document):
"""Ban database object."""
active = BooleanField(default=True)
admin = SnowflakeField(required=True)
user = SnowflakeField(required=True)
username = StringField(required=True)
discrim = IntField(min_value=1, max_value=9999, required=True)
duration = IntField(min_value=1, max_value=744, required=False)
guild = SnowflakeField(required=True)
type = StringField(default="perm", max_length=4, required=True)
reason = StringField(max_length=100, required=True)
created_at = DateTimeField(default=datetime.utcnow)
meta = {"db_alias": "main"}
class Config(Document):
"""Config database object."""
key = StringField(required=True)
value = DynamicField(required=True)
meta = {"db_alias": "main"}
class Guess(Document):
"""Guess database object."""
correct = BooleanField(default=False)
guess = StringField(max_length=800, required=True)
user = SnowflakeField(required=True)
meta = {"db_alias": "ctc2"}
class Joke(Document):
"""Joke database object."""
rid = StringField()
body = StringField()
title = StringField()
created_utc = DateTimeField()
over_18 = BooleanField()
score = IntField()
meta = {"db_alias": "main"}
class Kick(Document):
"""Kick database object."""
admin = SnowflakeField(required=True)
guild = SnowflakeField(required=True)
reason = StringField(max_length=100, required=True)
user = SnowflakeField(required=True)
created_at = DateTimeField(default=datetime.utcnow)
meta = {"db_alias": "main"}
class Lock(Document):
"""Lock database object."""
active = BooleanField(default=True)
admin = SnowflakeField(required=True)
channel = SnowflakeField(required=True)
duration = IntField(min_value=1, max_value=300, default=10)
guild = SnowflakeField(required=True)
reason = StringField(max_length=100, required=True)
created_at = DateTimeField(default=datetime.utcnow)
meta = {"db_alias": "main"}
class Mute(Document):
"""Mute database object."""
active = BooleanField(default=True)
user = SnowflakeField(required=True)
admin = SnowflakeField(required=True)
duration = IntField(min_value=-1, max_value=300, default=10)
guild = SnowflakeField(required=True)
reason = StringField(max_length=100, required=True)
created_at = DateTimeField(default=datetime.utcnow)
meta = {"db_alias": "main"}
class Purge(Document):
"""Purge database object."""
admin = SnowflakeField(required=True)
channel = SnowflakeField(required=True)
guild = SnowflakeField(required=True)
count = IntField(min_value=1, default=10)
created_at = DateTimeField(default=datetime.utcnow)
meta = {"db_alias": "main"}
class Reminder(Document):
"""Reminder database object."""
active = BooleanField(default=True)
user = SnowflakeField(required=True)
guild = SnowflakeField(required=True)
channel = SnowflakeField(required=True)
message = StringField(max_length=100, required=True)
remind_at = DateTimeField(required=True)
created_at = DateTimeField(default=datetime.utcnow)
meta = {"db_alias": "main"}
class Rolegiver(Document):
"""Rolegiver database object."""
guild = SnowflakeField(required=True)
roles = ListField(field=SnowflakeField())
meta = {"db_alias": "main"}
class Roleping(Document):
"""Roleping database object."""
active = BooleanField(default=True)
role = SnowflakeField(required=True)
guild = SnowflakeField(required=True)
admin = SnowflakeField(required=True)
bypass = DictField()
created_at = DateTimeField(default=datetime.utcnow)
meta = {"db_alias": "main"}
class Setting(Document):
"""Setting database object."""
guild = SnowflakeField(required=True)
setting = StringField(required=True)
value = DynamicField()
meta = {"db_alias": "main"}
class Star(Document):
"""Star database object."""
active = BooleanField(default=True)
index = IntField(required=True)
message = SnowflakeField(required=True)
channel = SnowflakeField(required=True)
starboard = SnowflakeField(required=True)
guild = SnowflakeField(required=True)
admin = SnowflakeField(required=True)
star = SnowflakeField(required=True)
created_at = DateTimeField(default=datetime.utcnow)
meta = {"db_alias": "main"}
class Starboard(Document):
"""Starboard database object."""
channel = SnowflakeField(required=True)
guild = SnowflakeField(required=True)
admin = SnowflakeField(required=True)
created_at = DateTimeField(default=datetime.utcnow)
meta = {"db_alias": "main"}
class Twitter(Document):
"""Twitter Follow object."""
active = BooleanField(default=True)
twitter_id = IntField(required=True)
handle = StringField(required=True)
channel = SnowflakeField(required=True)
guild = SnowflakeField(required=True)
last_tweet = SnowflakeField(required=True)
retweets = BooleanField(default=True)
admin = SnowflakeField(required=True)
created_at = DateTimeField(default=datetime.utcnow)
last_sync = DateTimeField()
meta = {"db_alias": "main"}
class Unban(Document):
"""Unban database object."""
user = SnowflakeField(required=True)
username = StringField(required=True)
discrim = IntField(min_value=1, max_value=9999, required=True)
guild = SnowflakeField(required=True)
admin = SnowflakeField(required=True)
reason = StringField(max_length=100, required=True)
created_at = DateTimeField(default=datetime.utcnow)
meta = {"db_alias": "main"}
class Warning(Document):
"""Warning database object."""
active = BooleanField(default=True)
admin = SnowflakeField(required=True)
user = SnowflakeField(required=True)
guild = SnowflakeField(required=True)
duration = IntField(min_value=1, max_value=120, default=24)
reason = StringField(max_length=100, required=True)
created_at = DateTimeField(default=datetime.utcnow)
meta = {"db_alias": "main"}

View file

@ -1,28 +0,0 @@
"""J.A.R.V.I.S. Member event handler."""
from dis_snek import Snake, listen
from dis_snek.models.discord.user import Member
from jarvis.db.models import Mute, Setting
class MemberEventHandler(object):
"""J.A.R.V.I.S. Member event handler."""
def __init__(self, bot: Snake):
self.bot = bot
self.bot.add_listener(self.on_member_join)
@listen()
async def on_member_join(self, user: Member) -> None:
"""Handle on_member_join event."""
guild = user.guild
mute = Mute.objects(guild=guild.id, user=user.id, active=True).first()
if mute:
mute_role = Setting.objects(guild=guild.id, setting="mute").first()
role = guild.get_role(mute_role.value)
await user.add_roles(role, reason="User is still muted from prior mute")
unverified = Setting.objects(guild=guild.id, setting="unverified").first()
if unverified:
role = guild.get_role(unverified.value)
if role not in user.roles:
await user.add_roles(role, reason="User just joined and is unverified")

View file

@ -1,226 +0,0 @@
"""J.A.R.V.I.S. Message event handler."""
import re
from dis_snek import Snake, listen
from dis_snek.models.discord.channel import DMChannel
from dis_snek.models.discord.embed import EmbedField
from dis_snek.models.discord.message import Message
from jarvis.config import get_config
from jarvis.db.models import Autopurge, Autoreact, Roleping, Setting, Warning
from jarvis.utils import build_embed, find
invites = re.compile(
r"(?:https?://)?(?:www.)?(?:discord.(?:gg|io|me|li)|discord(?:app)?.com/invite)/([^\s/]+?)(?=\b)", # noqa: E501
flags=re.IGNORECASE,
)
class MessageEventHandler(object):
"""J.A.R.V.I.S. Message event handler."""
def __init__(self, bot: Snake):
self.bot = bot
self.bot.add_listener(self.on_message)
self.bot.add_listener(self.on_message_edit)
async def autopurge(self, message: Message) -> None:
"""Handle autopurge events."""
autopurge = Autopurge.objects(guild=message.guild.id, channel=message.channel.id).first()
if autopurge:
await message.delete(delay=autopurge.delay)
async def autoreact(self, message: Message) -> None:
"""Handle autoreact events."""
autoreact = Autoreact.objects(
guild=message.guild.id,
channel=message.channel.id,
).first()
if autoreact:
for reaction in autoreact.reactions:
await message.add_reaction(reaction)
async def checks(self, message: Message) -> None:
"""Other message checks."""
# #tech
channel = find(lambda x: x.id == 599068193339736096, message.channel_mentions)
if channel and message.author.id == 293795462752894976:
await channel.send(
content="https://cdn.discordapp.com/attachments/664621130044407838/805218508866453554/tech.gif" # noqa: E501
)
content = re.sub(r"\s+", "", message.content)
match = invites.search(content)
setting = Setting.objects(guild=message.guild.id, setting="noinvite").first()
if not setting:
setting = Setting(guild=message.guild.id, setting="noinvite", value=True)
setting.save()
if match:
guild_invites = await message.guild.invites()
allowed = [x.code for x in guild_invites] + [
"dbrand",
"VtgZntXcnZ",
"gPfYGbvTCE",
]
if match.group(1) not in allowed and setting.value:
await message.delete()
_ = Warning(
active=True,
admin=get_config().client_id,
duration=24,
guild=message.guild.id,
reason="Sent an invite link",
user=message.author.id,
).save()
fields = [
EmbedField(
name="Reason",
value="Sent an invite link",
inline=False,
)
]
embed = build_embed(
title="Warning",
description=f"{message.author.mention} has been warned",
fields=fields,
)
embed.set_author(
name=message.author.nick if message.author.nick else message.author.name,
icon_url=message.author.display_avatar.url,
)
embed.set_footer(
text=f"{message.author.name}#{message.author.discriminator} | {message.author.id}" # noqa: E501
)
await message.channel.send(embed=embed)
async def massmention(self, message: Message) -> None:
"""Handle massmention events."""
massmention = Setting.objects(
guild=message.guild.id,
setting="massmention",
).first()
if (
massmention
and massmention.value > 0 # noqa: W503
and len(message.mentions) # noqa: W503
- (1 if message.author in message.mentions else 0) # noqa: W503
> massmention.value # noqa: W503
):
_ = Warning(
active=True,
admin=get_config().client_id,
duration=24,
guild=message.guild.id,
reason="Mass Mention",
user=message.author.id,
).save()
fields = [EmbedField(name="Reason", value="Mass Mention", inline=False)]
embed = build_embed(
title="Warning",
description=f"{message.author.mention} has been warned",
fields=fields,
)
embed.set_author(
name=message.author.nick if message.author.nick else message.author.name,
icon_url=message.author.display_avatar.url,
)
embed.set_footer(
text=f"{message.author.name}#{message.author.discriminator} | {message.author.id}"
)
await message.channel.send(embed=embed)
async def roleping(self, message: Message) -> None:
"""Handle roleping events."""
rolepings = Roleping.objects(guild=message.guild.id, active=True)
if not rolepings:
return
# Get all role IDs involved with message
roles = []
for mention in message.role_mentions:
roles.append(mention.id)
for mention in message.mentions:
for role in mention.roles:
roles.append(role.id)
if not roles:
return
# Get all roles that are rolepinged
roleping_ids = [r.role for r in rolepings]
# Get roles in rolepings
role_in_rolepings = list(filter(lambda x: x in roleping_ids, roles))
# Check if the user has the role, so they are allowed to ping it
user_missing_role = any(x.id not in roleping_ids for x in message.author.roles)
# Admins can ping whoever
user_is_admin = message.author.guild_permissions.administrator
# Check if user in a bypass list
user_has_bypass = False
for roleping in rolepings:
if message.author.id in roleping.bypass["users"]:
user_has_bypass = True
break
if any(role.id in roleping.bypass["roles"] for role in message.author.roles):
user_has_bypass = True
break
if role_in_rolepings and user_missing_role and not user_is_admin and not user_has_bypass:
_ = Warning(
active=True,
admin=get_config().client_id,
duration=24,
guild=message.guild.id,
reason="Pinged a blocked role/user with a blocked role",
user=message.author.id,
).save()
fields = [
EmbedField(
name="Reason",
value="Pinged a blocked role/user with a blocked role",
inline=False,
)
]
embed = build_embed(
title="Warning",
description=f"{message.author.mention} has been warned",
fields=fields,
)
embed.set_author(
name=message.author.nick if message.author.nick else message.author.name,
icon_url=message.author.display_avatar.url,
)
embed.set_footer(
text=f"{message.author.name}#{message.author.discriminator} | {message.author.id}"
)
await message.channel.send(embed=embed)
@listen()
async def on_message(self, message: Message) -> None:
"""Handle on_message event. Calls other event handlers."""
if not isinstance(message.channel, DMChannel) and not message.author.bot:
await self.autoreact(message)
await self.massmention(message)
await self.roleping(message)
await self.autopurge(message)
await self.checks(message)
@listen()
async def on_message_edit(self, before: Message, after: Message) -> None:
"""Handle on_message_edit event. Calls other event handlers."""
if not isinstance(after.channel, DMChannel) and not after.author.bot:
await self.massmention(after)
await self.roleping(after)
await self.checks(after)
await self.roleping(after)
await self.checks(after)
"""Handle on_message_edit event. Calls other event handlers."""
if not isinstance(after.channel, DMChannel) and not after.author.bot:
await self.massmention(after)
await self.roleping(after)
await self.checks(after)
await self.roleping(after)
await self.checks(after)

View file

@ -1,106 +0,0 @@
"""Logos for J.A.R.V.I.S."""
logo_doom = r"""
___ ___ ______ _ _ _____ _____
|_ | / _ \ | ___ \ | | | | |_ _| / ___|
| | / /_\ \ | |_/ / | | | | | | \ `--.
| | | _ | | / | | | | | | `--. \
/\__/ / _ | | | | _ | |\ \ _ \ \_/ / _ _| |_ _ /\__/ / _
\____/ (_)\_| |_/(_)\_| \_|(_) \___/ (_) \___/ (_)\____/ (_)
"""
logo_epic = r"""
_________ _______ _______ _________ _______
\__ _/ ( ___ ) ( ____ ) |\ /| \__ __/ ( ____ \
) ( | ( ) | | ( )| | ) ( | ) ( | ( \/
| | | (___) | | (____)| | | | | | | | (_____
| | | ___ | | __) ( ( ) ) | | (_____ )
| | | ( ) | | (\ ( \ \_/ / | | ) |
|\_) ) _ | ) ( | _ | ) \ \__ _ \ / _ ___) (___ _ /\____) | _
(____/ (_)|/ \|(_)|/ \__/(_) \_/ (_)\_______/(_)\_______)(_)
"""
logo_ivrit = r"""
_ _ ____ __ __ ___ ____
| | / \ | _ \ \ \ / / |_ _| / ___|
_ | | / _ \ | |_) | \ \ / / | | \___ \
| |_| | _ / ___ \ _ | _ < _ \ V / _ | | _ ___) | _
\___/ (_) /_/ \_\ (_) |_| \_\ (_) \_/ (_) |___| (_) |____/ (_)
"""
logo_kban = r"""
'||' . | . '||''|. . '||' '|' . '||' . .|'''.| .
|| ||| || || '|. .' || ||.. '
|| | || ||''|' || | || ''|||.
|| .''''|. || |. ||| || . '||
|| .|' .|. .||. .||. '|' | .||. |'....|'
'''
"""
logo_larry3d = r"""
_____ ______ ____ __ __ ______ ____
/\___ \ /\ _ \ /\ _`\ /\ \/\ \ /\__ _\ /\ _`\
\/__/\ \ \ \ \L\ \ \ \ \L\ \ \ \ \ \ \ \/_/\ \/ \ \,\L\_\
_\ \ \ \ \ __ \ \ \ , / \ \ \ \ \ \ \ \ \/_\__ \
/\ \_\ \ __ \ \ \/\ \ __ \ \ \\ \ __ \ \ \_/ \ __ \_\ \__ __ /\ \L\ \ __
\ \____//\_\ \ \_\ \_\/\_\ \ \_\ \_\/\_\ \ `\___//\_\ /\_____\/\_\ \ `\____\/\_\
\/___/ \/_/ \/_/\/_/\/_/ \/_/\/ /\/_/ `\/__/ \/_/ \/_____/\/_/ \/_____/\/_/
"""
logo_slane = r"""
__ ___ ____ _ __ ____ _____
/ / / | / __ \ | | / / / _/ / ___/
__ / / / /| | / /_/ / | | / / / / \__ \
/ /_/ / _ / ___ | _ / _, _/ _ | |/ / _ _/ / _ ___/ / _
\____/ (_)/_/ |_|(_)/_/ |_| (_)|___/ (_)/___/ (_)/____/ (_)
"""
logo_standard = r"""
_ _ ____ __ __ ___ ____
| | / \ | _ \ \ \ / / |_ _| / ___|
_ | | / _ \ | |_) | \ \ / / | | \___ \
| |_| | _ / ___ \ _ | _ < _ \ V / _ | | _ ___) | _
\___/ (_) /_/ \_\ (_) |_| \_\ (_) \_/ (_) |___| (_) |____/ (_)
"""
logo_alligator = r"""
::::::::::: ::: ::::::::: ::: ::: ::::::::::: ::::::::
:+: :+: :+: :+: :+: :+: :+: :+: :+: :+:
+:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+
+#+ +#++:++#++: +#++:++#: +#+ +:+ +#+ +#++:++#++
+#+ +#+ +#+ +#+ +#+ +#+ +#+ +#+ +#+
#+# #+# #+# #+# #+# #+# #+# #+# #+# #+#+#+# #+# #+# #+# #+# #+# #+#
##### ### ### ### ### ### ### ### ### ### ########### ### ######## ###
""" # noqa: E501
logo_alligator2 = r"""
::::::::::: ::: ::::::::: ::: ::: ::::::::::: ::::::::
:+: :+: :+: :+: :+: :+: :+: :+: :+: :+:
+:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+
+#+ +#++:++#++: +#++:++#: +#+ +:+ +#+ +#++:++#++
+#+ +#+ +#+ +#+ +#+ +#+ +#+ +#+ +#+
#+# #+# #+# #+# #+# #+# #+# #+# #+# #+#+#+# #+# #+# #+# #+# #+# #+#
##### ### ### ### ### ### ### ### ### ### ########### ### ######## ###
"""
def get_logo(lo: str) -> str:
"""Get a logo."""
if "logo_" not in lo:
lo = "logo_" + lo
return globals()[lo] if lo in globals() else logo_alligator2

View file

@ -1,10 +0,0 @@
"""J.A.R.V.I.S. background task handlers."""
from jarvis.tasks import twitter, unban, unlock, unwarn
def init() -> None:
"""Start the background task handlers."""
unban.unban.start()
unlock.unlock.start()
unwarn.unwarn.start()
twitter.tweets.start()

View file

@ -1,45 +0,0 @@
"""J.A.R.V.I.S. reminder background task handler."""
from asyncio import to_thread
from datetime import datetime, timedelta
from dis_snek.ext.tasks.task import Task
from dis_snek.ext.tasks.triggers import IntervalTrigger
import jarvis
from jarvis.db.models import Reminder
from jarvis.utils import build_embed
async def _remind() -> None:
"""J.A.R.V.I.S. reminder blocking task."""
reminders = Reminder.objects(remind_at__lte=datetime.utcnow() + timedelta(seconds=30))
for reminder in reminders:
if reminder.remind_at <= datetime.utcnow():
user = await jarvis.jarvis.fetch_user(reminder.user)
if not user:
reminder.delete()
continue
embed = build_embed(
title="You have a reminder",
description=reminder.message,
fields=[],
)
embed.set_author(
name=user.name + "#" + user.discriminator, icon_url=user.display_avatar.url
)
embed.set_thumbnail(url=user.display_avatar.url)
try:
await user.send(embed=embed)
except Exception:
guild = jarvis.jarvis.fetch_guild(reminder.guild)
channel = guild.get_channel(reminder.channel) if guild else None
if channel:
await channel.send(f"{user.mention}", embed=embed)
finally:
reminder.delete()
@Task.create(trigger=IntervalTrigger(seconds=15))
async def remind() -> None:
"""J.A.R.V.I.S. reminder background task."""
await to_thread(_remind)

View file

@ -1,70 +0,0 @@
"""J.A.R.V.I.S. twitter background task handler."""
import logging
from asyncio import to_thread
from datetime import datetime, timedelta
import tweepy
from dis_snek.ext.tasks.task import Task
from dis_snek.ext.tasks.triggers import IntervalTrigger
import jarvis
from jarvis.config import get_config
from jarvis.db.models import Twitter
logger = logging.getLogger("jarvis")
__auth = tweepy.AppAuthHandler(
get_config().twitter["consumer_key"], get_config().twitter["consumer_secret"]
)
__api = tweepy.API(__auth)
async def _tweets() -> None:
"""J.A.R.V.I.S. twitter blocking task."""
guild_cache = {}
channel_cache = {}
twitters = Twitter.objects(active=True)
for twitter in twitters:
try:
if not twitter.twitter_id or not twitter.last_sync:
user = __api.get_user(screen_name=twitter.handle)
twitter.twitter_id = user.id
twitter.handle = user.screen_name
twitter.last_sync = datetime.now()
if twitter.last_sync + timedelta(hours=1) <= datetime.now():
user = __api.get_user(id=twitter.twitter_id)
twitter.handle = user.screen_name
twitter.last_sync = datetime.now()
if tweets := __api.user_timeline(id=twitter.twitter_id):
guild_id = twitter.guild
channel_id = twitter.channel
tweets = sorted(tweets, key=lambda x: x.id)
if guild_id not in guild_cache:
guild_cache[guild_id] = await jarvis.jarvis.get_guild(guild_id)
guild = guild_cache[twitter.guild]
if channel_id not in channel_cache:
channel_cache[channel_id] = await guild.fetch_channel(channel_id)
channel = channel_cache[channel_id]
for tweet in tweets:
retweet = "retweeted_status" in tweet.__dict__
if retweet and not twitter.retweets:
continue
timestamp = int(tweet.created_at.timestamp())
url = f"https://twitter.com/{twitter.handle}/status/{tweet.id}"
verb = "re" if retweet else ""
await channel.send(
f"`@{twitter.handle}` {verb}tweeted this at <t:{timestamp}:f>: {url}"
)
newest = max(tweets, key=lambda x: x.id)
twitter.last_tweet = newest.id
twitter.save()
except Exception as e:
logger.error(f"Error with tweets: {e}")
@Task.create(trigger=IntervalTrigger(minutes=1))
async def tweets() -> None:
"""J.A.R.V.I.S. twitter background task."""
await to_thread(_tweets)

View file

@ -1,46 +0,0 @@
"""J.A.R.V.I.S. unban background task handler."""
from asyncio import to_thread
from datetime import datetime, timedelta
from dis_snek.ext.tasks.task import Task
from dis_snek.ext.tasks.triggers import IntervalTrigger
import jarvis
from jarvis.config import get_config
from jarvis.db.models import Ban, Unban
jarvis_id = get_config().client_id
async def _unban() -> None:
"""J.A.R.V.I.S. unban blocking task."""
bans = Ban.objects(type="temp", active=True)
unbans = []
for ban in bans:
if ban.created_at + timedelta(hours=ban.duration) < datetime.utcnow() + timedelta(
minutes=10
):
guild = await jarvis.jarvis.get_guild(ban.guild)
user = await jarvis.jarvis.get_user(ban.user)
if user:
await guild.unban(user=user, reason="Ban expired")
ban.active = False
ban.save()
unbans.append(
Unban(
user=user.id,
guild=guild.id,
username=user.name,
discrim=user.discriminator,
admin=jarvis_id,
reason="Ban expired",
)
)
if unbans:
Unban.objects().insert(unbans)
@Task.create(IntervalTrigger(minutes=10))
async def unban() -> None:
"""J.A.R.V.I.S. unban background task."""
await to_thread(_unban)

View file

@ -1,37 +0,0 @@
"""J.A.R.V.I.S. unlock background task handler."""
from asyncio import to_thread
from datetime import datetime, timedelta
from dis_snek.ext.tasks.task import Task
from dis_snek.ext.tasks.triggers import IntervalTrigger
import jarvis
from jarvis.db.models import Lock
async def _unlock() -> None:
"""J.A.R.V.I.S. unlock blocking task."""
locks = Lock.objects(active=True)
# Block execution for now
# TODO: Reevaluate with admin/lock[down]
if False:
for lock in locks:
if lock.created_at + timedelta(minutes=lock.duration) < datetime.utcnow():
guild = await jarvis.jarvis.get_guild(lock.guild)
channel = await jarvis.jarvis.get_guild(lock.channel)
if channel:
roles = await guild.fetch_roles()
for role in roles:
overrides = channel.overwrites_for(role)
overrides.send_messages = None
await channel.set_permissions(
role, overwrite=overrides, reason="Lock expired"
)
lock.active = False
lock.save()
@Task.create(IntervalTrigger(minutes=1))
async def unlock() -> None:
"""J.A.R.V.I.S. unlock background task."""
await to_thread(_unlock)

View file

@ -1,23 +0,0 @@
"""J.A.R.V.I.S. unwarn background task handler."""
from asyncio import to_thread
from datetime import datetime, timedelta
from dis_snek.ext.tasks.task import Task
from dis_snek.ext.tasks.triggers import IntervalTrigger
from jarvis.db.models import Warning
async def _unwarn() -> None:
"""J.A.R.V.I.S. unwarn blocking task."""
warns = Warning.objects(active=True)
for warn in warns:
if warn.created_at + timedelta(hours=warn.duration) < datetime.utcnow():
warn.active = False
warn.save()
@Task.create(IntervalTrigger(hours=1))
async def unwarn() -> None:
"""J.A.R.V.I.S. unwarn background task."""
await to_thread(_unwarn)

View file

@ -1,18 +1,16 @@
"""J.A.R.V.I.S. Utility Functions."""
from datetime import datetime
from pkgutil import iter_modules
from typing import Any, Callable, Iterable, List, Optional, TypeVar
import git
from dis_snek.models.discord.embed import Embed
from dis_snek.models.discord.embed import Embed, EmbedField
from dis_snek.models.discord.guild import AuditLogEntry
from dis_snek.models.discord.user import Member
import jarvis.cogs
import jarvis.db
from jarvis.config import get_config
__all__ = ["field", "db", "cachecog", "permissions"]
T = TypeVar("T")
__all__ = ["cachecog", "permissions"]
def build_embed(
@ -38,25 +36,32 @@ def build_embed(
return embed
def convert_bytesize(b: int) -> str:
"""Convert bytes amount to human readable."""
b = float(b)
sizes = ["B", "KB", "MB", "GB", "TB", "PB"]
size = 0
while b >= 1024 and size < len(sizes) - 1:
b = b / 1024
size += 1
return "{:0.3f} {}".format(b, sizes[size])
def unconvert_bytesize(size: int, ending: str) -> int:
"""Convert human readable to bytes."""
ending = ending.upper()
sizes = ["B", "KB", "MB", "GB", "TB", "PB"]
if ending == "B":
return size
# Rounding is only because bytes cannot be partial
return round(size * (1024 ** sizes.index(ending)))
def modlog_embed(
member: Member,
admin: Member,
log: AuditLogEntry,
title: str,
desc: str,
) -> Embed:
"""Get modlog embed."""
fields = [
EmbedField(
name="Moderator",
value=f"{admin.mention} ({admin.username}#{admin.discriminator})",
),
]
if log.reason:
fields.append(EmbedField(name="Reason", value=log.reason, inline=False))
embed = build_embed(
title=title,
description=desc,
color="#fc9e3f",
fields=fields,
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}")
return embed
def get_extensions(path: str = jarvis.cogs.__path__) -> list:
@ -85,103 +90,3 @@ def get_repo_hash() -> str:
"""J.A.R.V.I.S. current branch hash."""
repo = git.Repo(".")
return repo.head.object.hexsha
def find(predicate: Callable, sequence: Iterable) -> Optional[Any]:
"""
Find the first element in a sequence that matches the predicate.
??? Hint "Example Usage:"
```python
member = find(lambda m: m.name == "UserName", guild.members)
```
Args:
predicate: A callable that returns a boolean value
sequence: A sequence to be searched
Returns:
A match if found, otherwise None
"""
for el in sequence:
if predicate(el):
return el
return None
def find_all(predicate: Callable, sequence: Iterable) -> List[Any]:
"""
Find all elements in a sequence that match the predicate.
??? Hint "Example Usage:"
```python
members = find_all(lambda m: m.name == "UserName", guild.members)
```
Args:
predicate: A callable that returns a boolean value
sequence: A sequence to be searched
Returns:
A list of matches
"""
matches = []
for el in sequence:
if predicate(el):
matches.append(el)
return matches
def get(sequence: Iterable, **kwargs: Any) -> Optional[Any]:
"""
Find the first element in a sequence that matches all attrs.
??? Hint "Example Usage:"
```python
channel = get(guild.channels, nsfw=False, category="General")
```
Args:
sequence: A sequence to be searched
kwargs: Keyword arguments to search the sequence for
Returns:
A match if found, otherwise None
"""
if not kwargs:
return sequence[0]
for el in sequence:
if any(not hasattr(el, attr) for attr in kwargs.keys()):
continue
if all(getattr(el, attr) == value for attr, value in kwargs.items()):
return el
return None
def get_all(sequence: Iterable, **kwargs: Any) -> List[Any]:
"""
Find all elements in a sequence that match all attrs.
??? Hint "Example Usage:"
```python
channels = get_all(guild.channels, nsfw=False, category="General")
```
Args:
sequence: A sequence to be searched
kwargs: Keyword arguments to search the sequence for
Returns:
A list of matches
"""
if not kwargs:
return sequence
matches = []
for el in sequence:
if any(not hasattr(el, attr) for attr in kwargs.keys()):
continue
if all(getattr(el, attr) == value for attr, value in kwargs.items()):
matches.append(el)
return matches

View file

@ -2,11 +2,10 @@
from datetime import datetime, timedelta
from dis_snek import InteractionContext, Scale, Snake
from dis_snek.client.utils.misc_utils import find
from dis_snek.ext.tasks.task import Task
from dis_snek.ext.tasks.triggers import IntervalTrigger
from jarvis.utils import find
class CacheCog(Scale):
"""Cog wrapper for command caching."""

21
jarvis/utils/embeds.py Normal file
View file

@ -0,0 +1,21 @@
"""JARVIS bot-specific embeds."""
from dis_snek.models.discord.embed import Embed, EmbedField
from dis_snek.models.discord.user import Member
from jarvis_core.util import build_embed
def warning_embed(user: Member, reason: str) -> Embed:
"""
Generate a warning embed.
Args:
user: User to warn
reason: Warning reason
"""
fields = [EmbedField(name="Reason", value=reason, inline=False)]
embed = build_embed(
title="Warning", description=f"{user.mention} has been warned", fields=fields
)
embed.set_author(name=user.display_name, icon_url=user.display_avatar.url)
embed.set_footer(text=f"{user.username}#{user.discriminator} | {user.id}")
return embed

468
poetry.lock generated
View file

@ -106,7 +106,7 @@ python-versions = "*"
[[package]]
name = "charset-normalizer"
version = "2.0.11"
version = "2.0.12"
description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
category = "main"
optional = false
@ -117,7 +117,7 @@ unicode_backport = ["unicodedata2"]
[[package]]
name = "click"
version = "8.0.3"
version = "8.0.4"
description = "Composable command line interface toolkit"
category = "dev"
optional = false
@ -136,7 +136,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
[[package]]
name = "dis-snek"
version = "5.0.0"
version = "7.0.0"
description = "An API wrapper for Discord filled with snakes"
category = "main"
optional = false
@ -145,8 +145,20 @@ python-versions = ">=3.10"
[package.dependencies]
aiohttp = "*"
attrs = "*"
discord-typings = "*"
tomli = "*"
[[package]]
name = "discord-typings"
version = "0.3.1"
description = "Maintained typings of payloads that Discord sends"
category = "main"
optional = false
python-versions = ">=3.7"
[package.dependencies]
typing_extensions = ">=4,<5"
[[package]]
name = "flake8"
version = "4.0.1"
@ -181,7 +193,7 @@ smmap = ">=3.0.1,<6"
[[package]]
name = "gitpython"
version = "3.1.26"
version = "3.1.27"
description = "GitPython is a python library used to interact with Git repositories"
category = "main"
optional = false
@ -212,6 +224,28 @@ requirements_deprecated_finder = ["pipreqs", "pip-api"]
colors = ["colorama (>=0.4.3,<0.5.0)"]
plugins = ["setuptools"]
[[package]]
name = "jarvis-core"
version = "0.6.1"
description = ""
category = "main"
optional = false
python-versions = "^3.10"
develop = false
[package.dependencies]
dis-snek = "*"
motor = "^2.5.1"
orjson = "^3.6.6"
PyYAML = "^6.0"
umongo = "^3.1.0"
[package.source]
type = "git"
url = "https://git.zevaryx.com/stark-industries/jarvis/jarvis-core.git"
reference = "main"
resolved_reference = "52a3d568030a79db8ad5ddf65c26216913598bf5"
[[package]]
name = "jedi"
version = "0.18.1"
@ -235,6 +269,23 @@ category = "dev"
optional = false
python-versions = ">=3.6"
[[package]]
name = "marshmallow"
version = "3.15.0"
description = "A lightweight library for converting complex datatypes to and from native Python datatypes."
category = "main"
optional = false
python-versions = ">=3.7"
[package.dependencies]
packaging = "*"
[package.extras]
dev = ["pytest", "pytz", "simplejson", "mypy (==0.940)", "flake8 (==4.0.1)", "flake8-bugbear (==22.1.11)", "pre-commit (>=2.4,<3.0)", "tox"]
docs = ["sphinx (==4.4.0)", "sphinx-issues (==3.0.1)", "alabaster (==0.7.12)", "sphinx-version-warning (==1.1.2)", "autodocsumm (==0.2.7)"]
lint = ["mypy (==0.940)", "flake8 (==4.0.1)", "flake8-bugbear (==22.1.11)", "pre-commit (>=2.4,<3.0)"]
tests = ["pytest", "pytz", "simplejson"]
[[package]]
name = "mccabe"
version = "0.6.1"
@ -254,6 +305,20 @@ python-versions = ">=3.6"
[package.dependencies]
pymongo = ">=3.4,<4.0"
[[package]]
name = "motor"
version = "2.5.1"
description = "Non-blocking MongoDB driver for Tornado or asyncio"
category = "main"
optional = false
python-versions = ">=3.5.2"
[package.dependencies]
pymongo = ">=3.12,<4"
[package.extras]
encryption = ["pymongo[encryption] (>=3.12,<4)"]
[[package]]
name = "multidict"
version = "6.0.2"
@ -272,7 +337,7 @@ python-versions = "*"
[[package]]
name = "numpy"
version = "1.22.1"
version = "1.22.3"
description = "NumPy is the fundamental package for array computing with Python."
category = "main"
optional = false
@ -293,7 +358,7 @@ signedtoken = ["cryptography (>=3.0.0)", "pyjwt (>=2.0.0,<3)"]
[[package]]
name = "opencv-python"
version = "4.5.5.62"
version = "4.5.5.64"
description = "Wrapper package for OpenCV python bindings."
category = "main"
optional = false
@ -309,12 +374,23 @@ numpy = [
[[package]]
name = "orjson"
version = "3.6.6"
version = "3.6.7"
description = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy"
category = "main"
optional = false
python-versions = ">=3.7"
[[package]]
name = "packaging"
version = "21.3"
description = "Core utilities for Python packages"
category = "main"
optional = false
python-versions = ">=3.6"
[package.dependencies]
pyparsing = ">=2.0.2,<3.0.5 || >3.0.5"
[[package]]
name = "parso"
version = "0.8.3"
@ -327,6 +403,29 @@ python-versions = ">=3.6"
qa = ["flake8 (==3.8.3)", "mypy (==0.782)"]
testing = ["docopt", "pytest (<6.0.0)"]
[[package]]
name = "pastypy"
version = "1.0.1"
description = ""
category = "main"
optional = false
python-versions = ">=3.10"
[package.dependencies]
aiohttp = {version = "3.8.1", markers = "python_version >= \"3.6\""}
aiosignal = {version = "1.2.0", markers = "python_version >= \"3.6\""}
async-timeout = {version = "4.0.2", markers = "python_version >= \"3.6\""}
attrs = {version = "21.4.0", markers = "python_version >= \"3.6\" and python_full_version < \"3.0.0\" or python_full_version >= \"3.5.0\" and python_version >= \"3.6\""}
certifi = {version = "2021.10.8", markers = "python_version >= \"2.7\" and python_full_version < \"3.0.0\" or python_full_version >= \"3.6.0\""}
charset-normalizer = {version = "2.0.12", markers = "python_full_version >= \"3.6.0\" and python_version >= \"3.6\""}
frozenlist = {version = "1.3.0", markers = "python_version >= \"3.7\""}
idna = {version = "3.3", markers = "python_version >= \"3.6\" and python_full_version < \"3.0.0\" or python_full_version >= \"3.6.0\" and python_version >= \"3.6\""}
multidict = {version = "6.0.2", markers = "python_version >= \"3.7\""}
pycryptodome = {version = "3.14.1", markers = "python_version >= \"2.7\" and python_full_version < \"3.0.0\" or python_full_version >= \"3.5.0\""}
requests = {version = "2.27.1", markers = "python_version >= \"2.7\" and python_full_version < \"3.0.0\" or python_full_version >= \"3.6.0\""}
urllib3 = {version = "1.26.8", markers = "python_version >= \"2.7\" and python_full_version < \"3.0.0\" or python_full_version >= \"3.6.0\" and python_version < \"4\""}
yarl = {version = "1.7.2", markers = "python_version >= \"3.6\""}
[[package]]
name = "pathspec"
version = "0.9.0"
@ -337,7 +436,7 @@ python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
[[package]]
name = "pillow"
version = "9.0.0"
version = "9.0.1"
description = "Python Imaging Library (Fork)"
category = "main"
optional = false
@ -345,7 +444,7 @@ python-versions = ">=3.7"
[[package]]
name = "platformdirs"
version = "2.4.1"
version = "2.5.1"
description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
category = "dev"
optional = false
@ -386,6 +485,14 @@ category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
[[package]]
name = "pycryptodome"
version = "3.14.1"
description = "Cryptographic library for Python"
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
[[package]]
name = "pydocstyle"
version = "6.1.1"
@ -442,9 +549,20 @@ srv = ["dnspython (>=1.16.0,<1.17.0)"]
tls = ["ipaddress"]
zstd = ["zstandard"]
[[package]]
name = "pyparsing"
version = "3.0.7"
description = "Python parsing module"
category = "main"
optional = false
python-versions = ">=3.6"
[package.extras]
diagrams = ["jinja2", "railroad-diagrams"]
[[package]]
name = "python-gitlab"
version = "3.1.1"
version = "3.2.0"
description = "Interact with GitLab API"
category = "main"
optional = false
@ -474,18 +592,18 @@ test = ["pylint", "pycodestyle", "pyflakes", "pytest", "pytest-cov", "coverage"]
[[package]]
name = "python-lsp-server"
version = "1.3.3"
version = "1.4.0"
description = "Python Language Server for the Language Server Protocol"
category = "dev"
optional = false
python-versions = ">=3.6"
python-versions = ">=3.7"
[package.dependencies]
autopep8 = {version = ">=1.6.0,<1.7.0", optional = true, markers = "extra == \"all\""}
flake8 = {version = ">=4.0.0,<4.1.0", optional = true, markers = "extra == \"all\""}
jedi = ">=0.17.2,<0.19.0"
mccabe = {version = ">=0.6.0,<0.7.0", optional = true, markers = "extra == \"all\""}
pluggy = "*"
pluggy = ">=1.0.0"
pycodestyle = {version = ">=2.8.0,<2.9.0", optional = true, markers = "extra == \"all\""}
pydocstyle = {version = ">=2.0.0", optional = true, markers = "extra == \"all\""}
pyflakes = {version = ">=2.4.0,<2.5.0", optional = true, markers = "extra == \"all\""}
@ -562,7 +680,7 @@ requests = ">=2.0.1,<3.0.0"
[[package]]
name = "rope"
version = "0.22.0"
version = "0.23.0"
description = "a python refactoring library..."
category = "dev"
optional = false
@ -597,7 +715,7 @@ python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
[[package]]
name = "tomli"
version = "2.0.0"
version = "2.0.1"
description = "A lil' TOML parser"
category = "main"
optional = false
@ -605,22 +723,31 @@ python-versions = ">=3.7"
[[package]]
name = "tweepy"
version = "4.5.0"
version = "4.7.0"
description = "Twitter library for Python"
category = "main"
optional = false
python-versions = ">=3.6"
python-versions = ">=3.7"
[package.dependencies]
oauthlib = ">=3.2.0,<4"
requests = ">=2.27.0,<3"
requests-oauthlib = ">=1.0.0,<2"
requests-oauthlib = ">=1.2.0,<2"
[package.extras]
async = ["aiohttp (>=3.7.3,<4)", "oauthlib (>=3.1.0,<4)"]
async = ["aiohttp (>=3.7.3,<4)"]
dev = ["coveralls (>=2.1.0)", "tox (>=3.14.0)"]
socks = ["requests[socks] (>=2.27.0,<3)"]
test = ["vcrpy (>=1.10.3)"]
[[package]]
name = "typing-extensions"
version = "4.1.1"
description = "Backported and Experimental Type Hints for Python 3.6+"
category = "main"
optional = false
python-versions = ">=3.6"
[[package]]
name = "ujson"
version = "5.1.0"
@ -637,6 +764,23 @@ category = "main"
optional = false
python-versions = "*"
[[package]]
name = "umongo"
version = "3.1.0"
description = "sync/async MongoDB ODM, yes."
category = "main"
optional = false
python-versions = ">=3.7"
[package.dependencies]
marshmallow = ">=3.10.0"
pymongo = ">=3.7.0"
[package.extras]
mongomock = ["mongomock"]
motor = ["motor (>=2.0,<3.0)"]
txmongo = ["txmongo (>=19.2.0)"]
[[package]]
name = "urllib3"
version = "1.26.8"
@ -681,7 +825,7 @@ multidict = ">=4.0"
[metadata]
lock-version = "1.1"
python-versions = "^3.10"
content-hash = "8a1e6e29ff70363abddad36082a494c4ce1f9cc672fe7aff30b6d5b596d50dac"
content-hash = "2adcfd60566d51e43a6f5a3ee0f96140a38e91401800916042cc7cd7e6adb37d"
[metadata.files]
aiohttp = [
@ -808,20 +952,24 @@ certifi = [
{file = "certifi-2021.10.8.tar.gz", hash = "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872"},
]
charset-normalizer = [
{file = "charset-normalizer-2.0.11.tar.gz", hash = "sha256:98398a9d69ee80548c762ba991a4728bfc3836768ed226b3945908d1a688371c"},
{file = "charset_normalizer-2.0.11-py3-none-any.whl", hash = "sha256:2842d8f5e82a1f6aa437380934d5e1cd4fcf2003b06fed6940769c164a480a45"},
{file = "charset-normalizer-2.0.12.tar.gz", hash = "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597"},
{file = "charset_normalizer-2.0.12-py3-none-any.whl", hash = "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df"},
]
click = [
{file = "click-8.0.3-py3-none-any.whl", hash = "sha256:353f466495adaeb40b6b5f592f9f91cb22372351c84caeb068132442a4518ef3"},
{file = "click-8.0.3.tar.gz", hash = "sha256:410e932b050f5eed773c4cda94de75971c89cdb3155a72a0831139a79e5ecb5b"},
{file = "click-8.0.4-py3-none-any.whl", hash = "sha256:6a7a62563bbfabfda3a38f3023a1db4a35978c0abd76f6c9605ecd6554d6d9b1"},
{file = "click-8.0.4.tar.gz", hash = "sha256:8458d7b1287c5fb128c90e23381cf99dcde74beaf6c7ff6384ce84d6fe090adb"},
]
colorama = [
{file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"},
{file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"},
]
dis-snek = [
{file = "dis-snek-5.0.0.tar.gz", hash = "sha256:cc733b510d6b20523a8067f19b6d9b99804c13e37d78cd6e0fc401098adbca27"},
{file = "dis_snek-5.0.0-py3-none-any.whl", hash = "sha256:d1d50ba468ad6b0788e9281eb9d83f6eb2f8d964c1212ccd0e3fb33295462263"},
{file = "dis-snek-7.0.0.tar.gz", hash = "sha256:c39d0ff5e1f0cde3a0feefcd05f4a7d6de1d6b1aafbda745bbaa7a63d541af0f"},
{file = "dis_snek-7.0.0-py3-none-any.whl", hash = "sha256:5a1fa72d3d5de96a7550a480d33a4f4a6ac8509391fa20890c2eb495fb45d221"},
]
discord-typings = [
{file = "discord-typings-0.3.1.tar.gz", hash = "sha256:854cfb66d34edad49b36d8aaffc93179bb397a97c81caba2da02896e72821a74"},
{file = "discord_typings-0.3.1-py3-none-any.whl", hash = "sha256:65890c467751daa025dcef15683c32160f07427baf83380cfdf11d84ceec980a"},
]
flake8 = [
{file = "flake8-4.0.1-py2.py3-none-any.whl", hash = "sha256:479b1304f72536a55948cb40a32dce8bb0ffe3501e26eaf292c7e60eb5e0428d"},
@ -893,8 +1041,8 @@ gitdb = [
{file = "gitdb-4.0.9.tar.gz", hash = "sha256:bac2fd45c0a1c9cf619e63a90d62bdc63892ef92387424b855792a6cabe789aa"},
]
gitpython = [
{file = "GitPython-3.1.26-py3-none-any.whl", hash = "sha256:26ac35c212d1f7b16036361ca5cff3ec66e11753a0d677fb6c48fa4e1a9dd8d6"},
{file = "GitPython-3.1.26.tar.gz", hash = "sha256:fc8868f63a2e6d268fb25f481995ba185a85a66fcad126f039323ff6635669ee"},
{file = "GitPython-3.1.27-py3-none-any.whl", hash = "sha256:5b68b000463593e05ff2b261acff0ff0972df8ab1b70d3cdbd41b546c8b8fc3d"},
{file = "GitPython-3.1.27.tar.gz", hash = "sha256:1c885ce809e8ba2d88a29befeb385fcea06338d3640712b59ca623c220bb5704"},
]
idna = [
{file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"},
@ -904,6 +1052,7 @@ isort = [
{file = "isort-5.10.1-py3-none-any.whl", hash = "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7"},
{file = "isort-5.10.1.tar.gz", hash = "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951"},
]
jarvis-core = []
jedi = [
{file = "jedi-0.18.1-py2.py3-none-any.whl", hash = "sha256:637c9635fcf47945ceb91cd7f320234a7be540ded6f3e99a50cb6febdfd1ba8d"},
{file = "jedi-0.18.1.tar.gz", hash = "sha256:74137626a64a99c8eb6ae5832d99b3bdd7d29a3850fe2aa80a4126b2a7d949ab"},
@ -947,6 +1096,10 @@ lazy-object-proxy = [
{file = "lazy_object_proxy-1.7.1-cp39-cp39-win_amd64.whl", hash = "sha256:677ea950bef409b47e51e733283544ac3d660b709cfce7b187f5ace137960d61"},
{file = "lazy_object_proxy-1.7.1-pp37.pp38-none-any.whl", hash = "sha256:d66906d5785da8e0be7360912e99c9188b70f52c422f9fc18223347235691a84"},
]
marshmallow = [
{file = "marshmallow-3.15.0-py3-none-any.whl", hash = "sha256:ff79885ed43b579782f48c251d262e062bce49c65c52412458769a4fb57ac30f"},
{file = "marshmallow-3.15.0.tar.gz", hash = "sha256:2aaaab4f01ef4f5a011a21319af9fce17ab13bf28a026d1252adab0e035648d5"},
]
mccabe = [
{file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"},
{file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"},
@ -955,6 +1108,10 @@ mongoengine = [
{file = "mongoengine-0.23.1-py3-none-any.whl", hash = "sha256:3d1c8b9f5d43144bd726a3f01e58d2831c6fb112960a4a60b3a26fa85e026ab3"},
{file = "mongoengine-0.23.1.tar.gz", hash = "sha256:de275e70cd58891dc46eef43369c522ce450dccb6d6f1979cbc9b93e6bdaf6cb"},
]
motor = [
{file = "motor-2.5.1-py3-none-any.whl", hash = "sha256:961fdceacaae2c7236c939166f66415be81be8bbb762da528386738de3a0f509"},
{file = "motor-2.5.1.tar.gz", hash = "sha256:663473f4498f955d35db7b6f25651cb165514c247136f368b84419cb7635f6b8"},
]
multidict = [
{file = "multidict-6.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0b9e95a740109c6047602f4db4da9949e6c5945cefbad34a1299775ddc9a62e2"},
{file = "multidict-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ac0e27844758d7177989ce406acc6a83c16ed4524ebc363c1f748cba184d89d3"},
@ -1021,113 +1178,130 @@ mypy-extensions = [
{file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"},
]
numpy = [
{file = "numpy-1.22.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3d62d6b0870b53799204515145935608cdeb4cebb95a26800b6750e48884cc5b"},
{file = "numpy-1.22.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:831f2df87bd3afdfc77829bc94bd997a7c212663889d56518359c827d7113b1f"},
{file = "numpy-1.22.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8d1563060e77096367952fb44fca595f2b2f477156de389ce7c0ade3aef29e21"},
{file = "numpy-1.22.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69958735d5e01f7b38226a6c6e7187d72b7e4d42b6b496aca5860b611ca0c193"},
{file = "numpy-1.22.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45a7dfbf9ed8d68fd39763940591db7637cf8817c5bce1a44f7b56c97cbe211e"},
{file = "numpy-1.22.1-cp310-cp310-win_amd64.whl", hash = "sha256:7e957ca8112c689b728037cea9c9567c27cf912741fabda9efc2c7d33d29dfa1"},
{file = "numpy-1.22.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:800dfeaffb2219d49377da1371d710d7952c9533b57f3d51b15e61c4269a1b5b"},
{file = "numpy-1.22.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:65f5e257987601fdfc63f1d02fca4d1c44a2b85b802f03bd6abc2b0b14648dd2"},
{file = "numpy-1.22.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:632e062569b0fe05654b15ef0e91a53c0a95d08ffe698b66f6ba0f927ad267c2"},
{file = "numpy-1.22.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d245a2bf79188d3f361137608c3cd12ed79076badd743dc660750a9f3074f7c"},
{file = "numpy-1.22.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26b4018a19d2ad9606ce9089f3d52206a41b23de5dfe8dc947d2ec49ce45d015"},
{file = "numpy-1.22.1-cp38-cp38-win32.whl", hash = "sha256:f8ad59e6e341f38266f1549c7c2ec70ea0e3d1effb62a44e5c3dba41c55f0187"},
{file = "numpy-1.22.1-cp38-cp38-win_amd64.whl", hash = "sha256:60f19c61b589d44fbbab8ff126640ae712e163299c2dd422bfe4edc7ec51aa9b"},
{file = "numpy-1.22.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2db01d9838a497ba2aa9a87515aeaf458f42351d72d4e7f3b8ddbd1eba9479f2"},
{file = "numpy-1.22.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bcd19dab43b852b03868796f533b5f5561e6c0e3048415e675bec8d2e9d286c1"},
{file = "numpy-1.22.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:78bfbdf809fc236490e7e65715bbd98377b122f329457fffde206299e163e7f3"},
{file = "numpy-1.22.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c51124df17f012c3b757380782ae46eee85213a3215e51477e559739f57d9bf6"},
{file = "numpy-1.22.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88d54b7b516f0ca38a69590557814de2dd638d7d4ed04864826acaac5ebb8f01"},
{file = "numpy-1.22.1-cp39-cp39-win32.whl", hash = "sha256:b5ec9a5eaf391761c61fd873363ef3560a3614e9b4ead17347e4deda4358bca4"},
{file = "numpy-1.22.1-cp39-cp39-win_amd64.whl", hash = "sha256:4ac4d7c9f8ea2a79d721ebfcce81705fc3cd61a10b731354f1049eb8c99521e8"},
{file = "numpy-1.22.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e60ef82c358ded965fdd3132b5738eade055f48067ac8a5a8ac75acc00cad31f"},
{file = "numpy-1.22.1.zip", hash = "sha256:e348ccf5bc5235fc405ab19d53bec215bb373300e5523c7b476cc0da8a5e9973"},
{file = "numpy-1.22.3-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:92bfa69cfbdf7dfc3040978ad09a48091143cffb778ec3b03fa170c494118d75"},
{file = "numpy-1.22.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8251ed96f38b47b4295b1ae51631de7ffa8260b5b087808ef09a39a9d66c97ab"},
{file = "numpy-1.22.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48a3aecd3b997bf452a2dedb11f4e79bc5bfd21a1d4cc760e703c31d57c84b3e"},
{file = "numpy-1.22.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a3bae1a2ed00e90b3ba5f7bd0a7c7999b55d609e0c54ceb2b076a25e345fa9f4"},
{file = "numpy-1.22.3-cp310-cp310-win32.whl", hash = "sha256:f950f8845b480cffe522913d35567e29dd381b0dc7e4ce6a4a9f9156417d2430"},
{file = "numpy-1.22.3-cp310-cp310-win_amd64.whl", hash = "sha256:08d9b008d0156c70dc392bb3ab3abb6e7a711383c3247b410b39962263576cd4"},
{file = "numpy-1.22.3-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:201b4d0552831f7250a08d3b38de0d989d6f6e4658b709a02a73c524ccc6ffce"},
{file = "numpy-1.22.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f8c1f39caad2c896bc0018f699882b345b2a63708008be29b1f355ebf6f933fe"},
{file = "numpy-1.22.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:568dfd16224abddafb1cbcce2ff14f522abe037268514dd7e42c6776a1c3f8e5"},
{file = "numpy-1.22.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ca688e1b9b95d80250bca34b11a05e389b1420d00e87a0d12dc45f131f704a1"},
{file = "numpy-1.22.3-cp38-cp38-win32.whl", hash = "sha256:e7927a589df200c5e23c57970bafbd0cd322459aa7b1ff73b7c2e84d6e3eae62"},
{file = "numpy-1.22.3-cp38-cp38-win_amd64.whl", hash = "sha256:07a8c89a04997625236c5ecb7afe35a02af3896c8aa01890a849913a2309c676"},
{file = "numpy-1.22.3-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:2c10a93606e0b4b95c9b04b77dc349b398fdfbda382d2a39ba5a822f669a0123"},
{file = "numpy-1.22.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fade0d4f4d292b6f39951b6836d7a3c7ef5b2347f3c420cd9820a1d90d794802"},
{file = "numpy-1.22.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5bfb1bb598e8229c2d5d48db1860bcf4311337864ea3efdbe1171fb0c5da515d"},
{file = "numpy-1.22.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:97098b95aa4e418529099c26558eeb8486e66bd1e53a6b606d684d0c3616b168"},
{file = "numpy-1.22.3-cp39-cp39-win32.whl", hash = "sha256:fdf3c08bce27132395d3c3ba1503cac12e17282358cb4bddc25cc46b0aca07aa"},
{file = "numpy-1.22.3-cp39-cp39-win_amd64.whl", hash = "sha256:639b54cdf6aa4f82fe37ebf70401bbb74b8508fddcf4797f9fe59615b8c5813a"},
{file = "numpy-1.22.3-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c34ea7e9d13a70bf2ab64a2532fe149a9aced424cd05a2c4ba662fd989e3e45f"},
{file = "numpy-1.22.3.zip", hash = "sha256:dbc7601a3b7472d559dc7b933b18b4b66f9aa7452c120e87dfb33d02008c8a18"},
]
oauthlib = [
{file = "oauthlib-3.2.0-py3-none-any.whl", hash = "sha256:6db33440354787f9b7f3a6dbd4febf5d0f93758354060e802f6c06cb493022fe"},
{file = "oauthlib-3.2.0.tar.gz", hash = "sha256:23a8208d75b902797ea29fd31fa80a15ed9dc2c6c16fe73f5d346f83f6fa27a2"},
]
opencv-python = [
{file = "opencv-python-4.5.5.62.tar.gz", hash = "sha256:3efe232b32d5e1327e7c82bc6d61230737821c5190ce5c783e64a1bc8d514e18"},
{file = "opencv_python-4.5.5.62-cp36-abi3-macosx_10_15_x86_64.whl", hash = "sha256:2601388def0d6b957cc30dd88f8ff74a5651ae6940dd9e488241608cfa2b15c7"},
{file = "opencv_python-4.5.5.62-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71fdc49df412b102d97f14927321309043c79c4a3582cce1dc803370ff9c39c0"},
{file = "opencv_python-4.5.5.62-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:130cc75d56b29aa3c5de8b6ac438242dd2574ba6eaa8bccdffdcfd6b78632f7f"},
{file = "opencv_python-4.5.5.62-cp36-abi3-win32.whl", hash = "sha256:3a75c7ad45b032eea0c72e389aac6dd435f5c87e87f60237095c083400bc23aa"},
{file = "opencv_python-4.5.5.62-cp36-abi3-win_amd64.whl", hash = "sha256:c463d2276d8662b972d20ca9644702188507de200ca5405b89e1fe71c5c99989"},
{file = "opencv_python-4.5.5.62-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:ac92e743e22681f30001942d78512c1e39bce53dbffc504e5645fdc45c0f2c47"},
{file = "opencv-python-4.5.5.64.tar.gz", hash = "sha256:f65de0446a330c3b773cd04ba10345d8ce1b15dcac3f49770204e37602d0b3f7"},
{file = "opencv_python-4.5.5.64-cp36-abi3-macosx_10_15_x86_64.whl", hash = "sha256:a512a0c59b6fec0fac3844b2f47d6ecb1a9d18d235e6c5491ce8dbbe0663eae8"},
{file = "opencv_python-4.5.5.64-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca6138b6903910e384067d001763d40f97656875487381aed32993b076f44375"},
{file = "opencv_python-4.5.5.64-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b293ced62f4360d9f11cf72ae7e9df95320ff7bf5b834d87546f844e838c0c35"},
{file = "opencv_python-4.5.5.64-cp36-abi3-win32.whl", hash = "sha256:6247e584813c00c3b9ed69a795da40d2c153dc923d0182e957e1c2f00a554ac2"},
{file = "opencv_python-4.5.5.64-cp36-abi3-win_amd64.whl", hash = "sha256:408d5332550287aa797fd06bef47b2dfed163c6787668cc82ef9123a9484b56a"},
{file = "opencv_python-4.5.5.64-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:7787bb017ae93d5f9bb1b817ac8e13e45dd193743cb648498fcab21d00cf20a3"},
]
orjson = [
{file = "orjson-3.6.6-cp310-cp310-macosx_10_7_x86_64.whl", hash = "sha256:e4a7cad6c63306318453980d302c7c0b74c0cc290dd1f433bbd7d31a5af90cf1"},
{file = "orjson-3.6.6-cp310-cp310-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:e533941dca4a0530a876de32e54bf2fd3269cdec3751aebde7bfb5b5eba98e74"},
{file = "orjson-3.6.6-cp310-cp310-manylinux_2_24_aarch64.whl", hash = "sha256:9adf63be386eaa34278967512b83ff8fc4bed036a246391ae236f68d23c47452"},
{file = "orjson-3.6.6-cp310-cp310-manylinux_2_24_x86_64.whl", hash = "sha256:3b636753ae34d4619b11ea7d664a2f1e87e55e9738e5123e12bcce22acae9d13"},
{file = "orjson-3.6.6-cp310-none-win_amd64.whl", hash = "sha256:78a10295ed048fd916c6584d6d27c232eae805a43e7c14be56e3745f784f0eb6"},
{file = "orjson-3.6.6-cp37-cp37m-macosx_10_7_x86_64.whl", hash = "sha256:82b4f9fb2af7799b52932a62eac484083f930d5519560d6f64b24d66a368d03f"},
{file = "orjson-3.6.6-cp37-cp37m-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:a0033d07309cc7d8b8c4bc5d42f0dd4422b53ceb91dee9f4086bb2afa70b7772"},
{file = "orjson-3.6.6-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2b321f99473116ab7c7c028377372f7b4adba4029aaca19cd567e83898f55579"},
{file = "orjson-3.6.6-cp37-cp37m-manylinux_2_24_aarch64.whl", hash = "sha256:b9c98ed94f1688cc11b5c61b8eea39d854a1a2f09f71d8a5af005461b14994ed"},
{file = "orjson-3.6.6-cp37-cp37m-manylinux_2_24_x86_64.whl", hash = "sha256:00b333a41392bd07a8603c42670547dbedf9b291485d773f90c6470eff435608"},
{file = "orjson-3.6.6-cp37-none-win_amd64.whl", hash = "sha256:8d4fd3bdee65a81f2b79c50937d4b3c054e1e6bfa3fc72ed018a97c0c7c3d521"},
{file = "orjson-3.6.6-cp38-cp38-macosx_10_7_x86_64.whl", hash = "sha256:954c9f8547247cd7a8c91094ff39c9fe314b5eaeaec90b7bfb7384a4108f416f"},
{file = "orjson-3.6.6-cp38-cp38-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:74e5aed657ed0b91ef05d44d6a26d3e3e12ce4d2d71f75df41a477b05878c4a9"},
{file = "orjson-3.6.6-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4008a5130e6e9c33abaa95e939e0e755175da10745740aa6968461b2f16830e2"},
{file = "orjson-3.6.6-cp38-cp38-manylinux_2_24_aarch64.whl", hash = "sha256:012761d5f3d186deb4f6238f15e9ea7c1aac6deebc8f5b741ba3b4fafe017460"},
{file = "orjson-3.6.6-cp38-cp38-manylinux_2_24_x86_64.whl", hash = "sha256:b464546718a940b48d095a98df4c04808bfa6c8706fe751fc3f9390bc2f82643"},
{file = "orjson-3.6.6-cp38-none-win_amd64.whl", hash = "sha256:f10a800f4e5a4aab52076d4628e9e4dab9370bdd9d8ea254ebfde846b653ab25"},
{file = "orjson-3.6.6-cp39-cp39-macosx_10_7_x86_64.whl", hash = "sha256:8010d2610cfab721725ef14d578c7071e946bbdae63322d8f7b49061cf3fde8d"},
{file = "orjson-3.6.6-cp39-cp39-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:8dca67a4855e1e0f9a2ea0386e8db892708522e1171dc0ddf456932288fbae63"},
{file = "orjson-3.6.6-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:af065d60523139b99bd35b839c7a2d8c5da55df8a8c4402d2eb6cdc07fa7a624"},
{file = "orjson-3.6.6-cp39-cp39-manylinux_2_24_aarch64.whl", hash = "sha256:fa1f389cc9f766ae0cf7ba3533d5089836b01a5ccb3f8d904297f1fcf3d9dc34"},
{file = "orjson-3.6.6-cp39-cp39-manylinux_2_24_x86_64.whl", hash = "sha256:ec1221ad78f94d27b162a1d35672b62ef86f27f0e4c2b65051edb480cc86b286"},
{file = "orjson-3.6.6-cp39-none-win_amd64.whl", hash = "sha256:afed2af55eeda1de6b3f1cbc93431981b19d380fcc04f6ed86e74c1913070304"},
{file = "orjson-3.6.6.tar.gz", hash = "sha256:55dd988400fa7fbe0e31407c683f5aaab013b5bd967167b8fe058186773c4d6c"},
{file = "orjson-3.6.7-cp310-cp310-macosx_10_7_x86_64.whl", hash = "sha256:93188a9d6eb566419ad48befa202dfe7cd7a161756444b99c4ec77faea9352a4"},
{file = "orjson-3.6.7-cp310-cp310-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:82515226ecb77689a029061552b5df1802b75d861780c401e96ca6bc8495f775"},
{file = "orjson-3.6.7-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3af57ffab7848aaec6ba6b9e9b41331250b57bf696f9d502bacdc71a0ebab0ba"},
{file = "orjson-3.6.7-cp310-cp310-manylinux_2_24_aarch64.whl", hash = "sha256:a7297504d1142e7efa236ffc53f056d73934a993a08646dbcee89fc4308a8fcf"},
{file = "orjson-3.6.7-cp310-cp310-manylinux_2_24_x86_64.whl", hash = "sha256:5a50cde0dbbde255ce751fd1bca39d00ecd878ba0903c0480961b31984f2fab7"},
{file = "orjson-3.6.7-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d21f9a2d1c30e58070f93988db4cad154b9009fafbde238b52c1c760e3607fbe"},
{file = "orjson-3.6.7-cp310-none-win_amd64.whl", hash = "sha256:e152464c4606b49398afd911777decebcf9749cc8810c5b4199039e1afb0991e"},
{file = "orjson-3.6.7-cp37-cp37m-macosx_10_7_x86_64.whl", hash = "sha256:0a65f3c403f38b0117c6dd8e76e85a7bd51fcd92f06c5598dfeddbc44697d3e5"},
{file = "orjson-3.6.7-cp37-cp37m-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:6c47cfca18e41f7f37b08ff3e7abf5ada2d0f27b5ade934f05be5fc5bb956e9d"},
{file = "orjson-3.6.7-cp37-cp37m-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:63185af814c243fad7a72441e5f98120c9ecddf2675befa486d669fb65539e9b"},
{file = "orjson-3.6.7-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b2da6fde42182b80b40df2e6ab855c55090ebfa3fcc21c182b7ad1762b61d55c"},
{file = "orjson-3.6.7-cp37-cp37m-manylinux_2_24_aarch64.whl", hash = "sha256:48c5831ec388b4e2682d4ff56d6bfa4a2ef76c963f5e75f4ff4785f9cf338a80"},
{file = "orjson-3.6.7-cp37-cp37m-manylinux_2_24_x86_64.whl", hash = "sha256:913fac5d594ccabf5e8fbac15b9b3bb9c576d537d49eeec9f664e7a64dde4c4b"},
{file = "orjson-3.6.7-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:58f244775f20476e5851e7546df109f75160a5178d44257d437ba6d7e562bfe8"},
{file = "orjson-3.6.7-cp37-none-win_amd64.whl", hash = "sha256:2d5f45c6b85e5f14646df2d32ecd7ff20fcccc71c0ea1155f4d3df8c5299bbb7"},
{file = "orjson-3.6.7-cp38-cp38-macosx_10_7_x86_64.whl", hash = "sha256:612d242493afeeb2068bc72ff2544aa3b1e627578fcf92edee9daebb5893ffea"},
{file = "orjson-3.6.7-cp38-cp38-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:539cdc5067db38db27985e257772d073cd2eb9462d0a41bde96da4e4e60bd99b"},
{file = "orjson-3.6.7-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6d103b721bbc4f5703f62b3882e638c0b65fcdd48622531c7ffd45047ef8e87c"},
{file = "orjson-3.6.7-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb10a20f80e95102dd35dfbc3a22531661b44a09b55236b012a446955846b023"},
{file = "orjson-3.6.7-cp38-cp38-manylinux_2_24_aarch64.whl", hash = "sha256:bb68d0da349cf8a68971a48ad179434f75256159fe8b0715275d9b49fa23b7a3"},
{file = "orjson-3.6.7-cp38-cp38-manylinux_2_24_x86_64.whl", hash = "sha256:4a2c7d0a236aaeab7f69c17b7ab4c078874e817da1bfbb9827cb8c73058b3050"},
{file = "orjson-3.6.7-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:3be045ca3b96119f592904cf34b962969ce97bd7843cbfca084009f6c8d2f268"},
{file = "orjson-3.6.7-cp38-none-win_amd64.whl", hash = "sha256:bd765c06c359d8a814b90f948538f957fa8a1f55ad1aaffcdc5771996aaea061"},
{file = "orjson-3.6.7-cp39-cp39-macosx_10_7_x86_64.whl", hash = "sha256:7dd9e1e46c0776eee9e0649e3ae9584ea368d96851bcaeba18e217fa5d755283"},
{file = "orjson-3.6.7-cp39-cp39-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:c4b4f20a1e3df7e7c83717aff0ef4ab69e42ce2fb1f5234682f618153c458406"},
{file = "orjson-3.6.7-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7107a5673fd0b05adbb58bf71c1578fc84d662d29c096eb6d998982c8635c221"},
{file = "orjson-3.6.7-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a08b6940dd9a98ccf09785890112a0f81eadb4f35b51b9a80736d1725437e22c"},
{file = "orjson-3.6.7-cp39-cp39-manylinux_2_24_aarch64.whl", hash = "sha256:f5d1648e5a9d1070f3628a69a7c6c17634dbb0caf22f2085eca6910f7427bf1f"},
{file = "orjson-3.6.7-cp39-cp39-manylinux_2_24_x86_64.whl", hash = "sha256:e6201494e8dff2ce7fd21da4e3f6dfca1a3fed38f9dcefc972f552f6596a7621"},
{file = "orjson-3.6.7-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:70d0386abe02879ebaead2f9632dd2acb71000b4721fd8c1a2fb8c031a38d4d5"},
{file = "orjson-3.6.7-cp39-none-win_amd64.whl", hash = "sha256:d9a3288861bfd26f3511fb4081561ca768674612bac59513cb9081bb61fcc87f"},
{file = "orjson-3.6.7.tar.gz", hash = "sha256:a4bb62b11289b7620eead2f25695212e9ac77fcfba76f050fa8a540fb5c32401"},
]
packaging = [
{file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"},
{file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"},
]
parso = [
{file = "parso-0.8.3-py2.py3-none-any.whl", hash = "sha256:c001d4636cd3aecdaf33cbb40aebb59b094be2a74c556778ef5576c175e19e75"},
{file = "parso-0.8.3.tar.gz", hash = "sha256:8c07be290bb59f03588915921e29e8a50002acaf2cdc5fa0e0114f91709fafa0"},
]
pastypy = [
{file = "pastypy-1.0.1-py3-none-any.whl", hash = "sha256:63cc664568f86f6ddeb7e5687422bbf4b338d067ea887ed240223c8cbcf6fd2d"},
{file = "pastypy-1.0.1.tar.gz", hash = "sha256:0393d1635b5031170eae3efaf376b14c3a4af7737c778d7ba7d56f2bd25bf5b1"},
]
pathspec = [
{file = "pathspec-0.9.0-py2.py3-none-any.whl", hash = "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a"},
{file = "pathspec-0.9.0.tar.gz", hash = "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"},
]
pillow = [
{file = "Pillow-9.0.0-cp310-cp310-macosx_10_10_universal2.whl", hash = "sha256:113723312215b25c22df1fdf0e2da7a3b9c357a7d24a93ebbe80bfda4f37a8d4"},
{file = "Pillow-9.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:bb47a548cea95b86494a26c89d153fd31122ed65255db5dcbc421a2d28eb3379"},
{file = "Pillow-9.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:31b265496e603985fad54d52d11970383e317d11e18e856971bdbb86af7242a4"},
{file = "Pillow-9.0.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d154ed971a4cc04b93a6d5b47f37948d1f621f25de3e8fa0c26b2d44f24e3e8f"},
{file = "Pillow-9.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80fe92813d208ce8aa7d76da878bdc84b90809f79ccbad2a288e9bcbeac1d9bd"},
{file = "Pillow-9.0.0-cp310-cp310-win32.whl", hash = "sha256:d5dcea1387331c905405b09cdbfb34611050cc52c865d71f2362f354faee1e9f"},
{file = "Pillow-9.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:52abae4c96b5da630a8b4247de5428f593465291e5b239f3f843a911a3cf0105"},
{file = "Pillow-9.0.0-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:72c3110228944019e5f27232296c5923398496b28be42535e3b2dc7297b6e8b6"},
{file = "Pillow-9.0.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:97b6d21771da41497b81652d44191489296555b761684f82b7b544c49989110f"},
{file = "Pillow-9.0.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:72f649d93d4cc4d8cf79c91ebc25137c358718ad75f99e99e043325ea7d56100"},
{file = "Pillow-9.0.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7aaf07085c756f6cb1c692ee0d5a86c531703b6e8c9cae581b31b562c16b98ce"},
{file = "Pillow-9.0.0-cp37-cp37m-win32.whl", hash = "sha256:03b27b197deb4ee400ed57d8d4e572d2d8d80f825b6634daf6e2c18c3c6ccfa6"},
{file = "Pillow-9.0.0-cp37-cp37m-win_amd64.whl", hash = "sha256:a09a9d4ec2b7887f7a088bbaacfd5c07160e746e3d47ec5e8050ae3b2a229e9f"},
{file = "Pillow-9.0.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:490e52e99224858f154975db61c060686df8a6b3f0212a678e5d2e2ce24675c9"},
{file = "Pillow-9.0.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:500d397ddf4bbf2ca42e198399ac13e7841956c72645513e8ddf243b31ad2128"},
{file = "Pillow-9.0.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ebd8b9137630a7bbbff8c4b31e774ff05bbb90f7911d93ea2c9371e41039b52"},
{file = "Pillow-9.0.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd0e5062f11cb3e730450a7d9f323f4051b532781026395c4323b8ad055523c4"},
{file = "Pillow-9.0.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f3b4522148586d35e78313db4db0df4b759ddd7649ef70002b6c3767d0fdeb7"},
{file = "Pillow-9.0.0-cp38-cp38-win32.whl", hash = "sha256:0b281fcadbb688607ea6ece7649c5d59d4bbd574e90db6cd030e9e85bde9fecc"},
{file = "Pillow-9.0.0-cp38-cp38-win_amd64.whl", hash = "sha256:b5050d681bcf5c9f2570b93bee5d3ec8ae4cf23158812f91ed57f7126df91762"},
{file = "Pillow-9.0.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:c2067b3bb0781f14059b112c9da5a91c80a600a97915b4f48b37f197895dd925"},
{file = "Pillow-9.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2d16b6196fb7a54aff6b5e3ecd00f7c0bab1b56eee39214b2b223a9d938c50af"},
{file = "Pillow-9.0.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:98cb63ca63cb61f594511c06218ab4394bf80388b3d66cd61d0b1f63ee0ea69f"},
{file = "Pillow-9.0.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bc462d24500ba707e9cbdef436c16e5c8cbf29908278af053008d9f689f56dee"},
{file = "Pillow-9.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3586e12d874ce2f1bc875a3ffba98732ebb12e18fb6d97be482bd62b56803281"},
{file = "Pillow-9.0.0-cp39-cp39-win32.whl", hash = "sha256:68e06f8b2248f6dc8b899c3e7ecf02c9f413aab622f4d6190df53a78b93d97a5"},
{file = "Pillow-9.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:6579f9ba84a3d4f1807c4aab4be06f373017fc65fff43498885ac50a9b47a553"},
{file = "Pillow-9.0.0-pp37-pypy37_pp73-macosx_10_10_x86_64.whl", hash = "sha256:47f5cf60bcb9fbc46011f75c9b45a8b5ad077ca352a78185bd3e7f1d294b98bb"},
{file = "Pillow-9.0.0-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2fd8053e1f8ff1844419842fd474fc359676b2e2a2b66b11cc59f4fa0a301315"},
{file = "Pillow-9.0.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c5439bfb35a89cac50e81c751317faea647b9a3ec11c039900cd6915831064d"},
{file = "Pillow-9.0.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:95545137fc56ce8c10de646074d242001a112a92de169986abd8c88c27566a05"},
{file = "Pillow-9.0.0.tar.gz", hash = "sha256:ee6e2963e92762923956fe5d3479b1fdc3b76c83f290aad131a2f98c3df0593e"},
{file = "Pillow-9.0.1-1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a5d24e1d674dd9d72c66ad3ea9131322819ff86250b30dc5821cbafcfa0b96b4"},
{file = "Pillow-9.0.1-1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2632d0f846b7c7600edf53c48f8f9f1e13e62f66a6dbc15191029d950bfed976"},
{file = "Pillow-9.0.1-1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b9618823bd237c0d2575283f2939655f54d51b4527ec3972907a927acbcc5bfc"},
{file = "Pillow-9.0.1-cp310-cp310-macosx_10_10_universal2.whl", hash = "sha256:9bfdb82cdfeccec50aad441afc332faf8606dfa5e8efd18a6692b5d6e79f00fd"},
{file = "Pillow-9.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5100b45a4638e3c00e4d2320d3193bdabb2d75e79793af7c3eb139e4f569f16f"},
{file = "Pillow-9.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:528a2a692c65dd5cafc130de286030af251d2ee0483a5bf50c9348aefe834e8a"},
{file = "Pillow-9.0.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f29d831e2151e0b7b39981756d201f7108d3d215896212ffe2e992d06bfe049"},
{file = "Pillow-9.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:855c583f268edde09474b081e3ddcd5cf3b20c12f26e0d434e1386cc5d318e7a"},
{file = "Pillow-9.0.1-cp310-cp310-win32.whl", hash = "sha256:d9d7942b624b04b895cb95af03a23407f17646815495ce4547f0e60e0b06f58e"},
{file = "Pillow-9.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:81c4b81611e3a3cb30e59b0cf05b888c675f97e3adb2c8672c3154047980726b"},
{file = "Pillow-9.0.1-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:413ce0bbf9fc6278b2d63309dfeefe452835e1c78398efb431bab0672fe9274e"},
{file = "Pillow-9.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:80fe64a6deb6fcfdf7b8386f2cf216d329be6f2781f7d90304351811fb591360"},
{file = "Pillow-9.0.1-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cef9c85ccbe9bee00909758936ea841ef12035296c748aaceee535969e27d31b"},
{file = "Pillow-9.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1d19397351f73a88904ad1aee421e800fe4bbcd1aeee6435fb62d0a05ccd1030"},
{file = "Pillow-9.0.1-cp37-cp37m-win32.whl", hash = "sha256:d21237d0cd37acded35154e29aec853e945950321dd2ffd1a7d86fe686814669"},
{file = "Pillow-9.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:ede5af4a2702444a832a800b8eb7f0a7a1c0eed55b644642e049c98d589e5092"},
{file = "Pillow-9.0.1-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:b5b3f092fe345c03bca1e0b687dfbb39364b21ebb8ba90e3fa707374b7915204"},
{file = "Pillow-9.0.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:335ace1a22325395c4ea88e00ba3dc89ca029bd66bd5a3c382d53e44f0ccd77e"},
{file = "Pillow-9.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:db6d9fac65bd08cea7f3540b899977c6dee9edad959fa4eaf305940d9cbd861c"},
{file = "Pillow-9.0.1-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f154d173286a5d1863637a7dcd8c3437bb557520b01bddb0be0258dcb72696b5"},
{file = "Pillow-9.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14d4b1341ac07ae07eb2cc682f459bec932a380c3b122f5540432d8977e64eae"},
{file = "Pillow-9.0.1-cp38-cp38-win32.whl", hash = "sha256:effb7749713d5317478bb3acb3f81d9d7c7f86726d41c1facca068a04cf5bb4c"},
{file = "Pillow-9.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:7f7609a718b177bf171ac93cea9fd2ddc0e03e84d8fa4e887bdfc39671d46b00"},
{file = "Pillow-9.0.1-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:80ca33961ced9c63358056bd08403ff866512038883e74f3a4bf88ad3eb66838"},
{file = "Pillow-9.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1c3c33ac69cf059bbb9d1a71eeaba76781b450bc307e2291f8a4764d779a6b28"},
{file = "Pillow-9.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:12875d118f21cf35604176872447cdb57b07126750a33748bac15e77f90f1f9c"},
{file = "Pillow-9.0.1-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:514ceac913076feefbeaf89771fd6febde78b0c4c1b23aaeab082c41c694e81b"},
{file = "Pillow-9.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d3c5c79ab7dfce6d88f1ba639b77e77a17ea33a01b07b99840d6ed08031cb2a7"},
{file = "Pillow-9.0.1-cp39-cp39-win32.whl", hash = "sha256:718856856ba31f14f13ba885ff13874be7fefc53984d2832458f12c38205f7f7"},
{file = "Pillow-9.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:f25ed6e28ddf50de7e7ea99d7a976d6a9c415f03adcaac9c41ff6ff41b6d86ac"},
{file = "Pillow-9.0.1-pp37-pypy37_pp73-macosx_10_10_x86_64.whl", hash = "sha256:011233e0c42a4a7836498e98c1acf5e744c96a67dd5032a6f666cc1fb97eab97"},
{file = "Pillow-9.0.1-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:253e8a302a96df6927310a9d44e6103055e8fb96a6822f8b7f514bb7ef77de56"},
{file = "Pillow-9.0.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6295f6763749b89c994fcb6d8a7f7ce03c3992e695f89f00b741b4580b199b7e"},
{file = "Pillow-9.0.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:a9f44cd7e162ac6191491d7249cceb02b8116b0f7e847ee33f739d7cb1ea1f70"},
{file = "Pillow-9.0.1.tar.gz", hash = "sha256:6c8bc8238a7dfdaf7a75f5ec5a663f4173f8c367e5a39f87e720495e1eed75fa"},
]
platformdirs = [
{file = "platformdirs-2.4.1-py3-none-any.whl", hash = "sha256:1d7385c7db91728b83efd0ca99a5afb296cab9d0ed8313a45ed8ba17967ecfca"},
{file = "platformdirs-2.4.1.tar.gz", hash = "sha256:440633ddfebcc36264232365d7840a970e75e1018d15b4327d11f91909045fda"},
{file = "platformdirs-2.5.1-py3-none-any.whl", hash = "sha256:bcae7cab893c2d310a711b70b24efb93334febe65f8de776ee320b517471e227"},
{file = "platformdirs-2.5.1.tar.gz", hash = "sha256:7535e70dfa32e84d4b34996ea99c5e432fa29a708d0f4e394bbcb2a8faa4f16d"},
]
pluggy = [
{file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"},
@ -1171,6 +1345,38 @@ pycodestyle = [
{file = "pycodestyle-2.8.0-py2.py3-none-any.whl", hash = "sha256:720f8b39dde8b293825e7ff02c475f3077124006db4f440dcbc9a20b76548a20"},
{file = "pycodestyle-2.8.0.tar.gz", hash = "sha256:eddd5847ef438ea1c7870ca7eb78a9d47ce0cdb4851a5523949f2601d0cbbe7f"},
]
pycryptodome = [
{file = "pycryptodome-3.14.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:75a3a364fee153e77ed889c957f6f94ec6d234b82e7195b117180dcc9fc16f96"},
{file = "pycryptodome-3.14.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:aae395f79fa549fb1f6e3dc85cf277f0351e15a22e6547250056c7f0c990d6a5"},
{file = "pycryptodome-3.14.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:f403a3e297a59d94121cb3ee4b1cf41f844332940a62d71f9e4a009cc3533493"},
{file = "pycryptodome-3.14.1-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:ce7a875694cd6ccd8682017a7c06c6483600f151d8916f2b25cf7a439e600263"},
{file = "pycryptodome-3.14.1-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:a36ab51674b014ba03da7f98b675fcb8eabd709a2d8e18219f784aba2db73b72"},
{file = "pycryptodome-3.14.1-cp27-cp27m-manylinux2014_aarch64.whl", hash = "sha256:50a5346af703330944bea503106cd50c9c2212174cfcb9939db4deb5305a8367"},
{file = "pycryptodome-3.14.1-cp27-cp27m-win32.whl", hash = "sha256:36e3242c4792e54ed906c53f5d840712793dc68b726ec6baefd8d978c5282d30"},
{file = "pycryptodome-3.14.1-cp27-cp27m-win_amd64.whl", hash = "sha256:c880a98376939165b7dc504559f60abe234b99e294523a273847f9e7756f4132"},
{file = "pycryptodome-3.14.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:dcd65355acba9a1d0fc9b923875da35ed50506e339b35436277703d7ace3e222"},
{file = "pycryptodome-3.14.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:766a8e9832128c70012e0c2b263049506cbf334fb21ff7224e2704102b6ef59e"},
{file = "pycryptodome-3.14.1-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:2562de213960693b6d657098505fd4493c45f3429304da67efcbeb61f0edfe89"},
{file = "pycryptodome-3.14.1-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:d1b7739b68a032ad14c5e51f7e4e1a5f92f3628bba024a2bda1f30c481fc85d8"},
{file = "pycryptodome-3.14.1-cp27-cp27mu-manylinux2014_aarch64.whl", hash = "sha256:27e92c1293afcb8d2639baf7eb43f4baada86e4de0f1fb22312bfc989b95dae2"},
{file = "pycryptodome-3.14.1-cp35-abi3-macosx_10_9_x86_64.whl", hash = "sha256:f2772af1c3ef8025c85335f8b828d0193fa1e43256621f613280e2c81bfad423"},
{file = "pycryptodome-3.14.1-cp35-abi3-manylinux1_i686.whl", hash = "sha256:9ec761a35dbac4a99dcbc5cd557e6e57432ddf3e17af8c3c86b44af9da0189c0"},
{file = "pycryptodome-3.14.1-cp35-abi3-manylinux1_x86_64.whl", hash = "sha256:e64738207a02a83590df35f59d708bf1e7ea0d6adce712a777be2967e5f7043c"},
{file = "pycryptodome-3.14.1-cp35-abi3-manylinux2010_i686.whl", hash = "sha256:e24d4ec4b029611359566c52f31af45c5aecde7ef90bf8f31620fd44c438efe7"},
{file = "pycryptodome-3.14.1-cp35-abi3-manylinux2010_x86_64.whl", hash = "sha256:8b5c28058102e2974b9868d72ae5144128485d466ba8739abd674b77971454cc"},
{file = "pycryptodome-3.14.1-cp35-abi3-manylinux2014_aarch64.whl", hash = "sha256:924b6aad5386fb54f2645f22658cb0398b1f25bc1e714a6d1522c75d527deaa5"},
{file = "pycryptodome-3.14.1-cp35-abi3-win32.whl", hash = "sha256:53dedbd2a6a0b02924718b520a723e88bcf22e37076191eb9b91b79934fb2192"},
{file = "pycryptodome-3.14.1-cp35-abi3-win_amd64.whl", hash = "sha256:ea56a35fd0d13121417d39a83f291017551fa2c62d6daa6b04af6ece7ed30d84"},
{file = "pycryptodome-3.14.1-pp27-pypy_73-macosx_10_9_x86_64.whl", hash = "sha256:028dcbf62d128b4335b61c9fbb7dd8c376594db607ef36d5721ee659719935d5"},
{file = "pycryptodome-3.14.1-pp27-pypy_73-manylinux1_x86_64.whl", hash = "sha256:69f05aaa90c99ac2f2af72d8d7f185f729721ad7c4be89e9e3d0ab101b0ee875"},
{file = "pycryptodome-3.14.1-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:12ef157eb1e01a157ca43eda275fa68f8db0dd2792bc4fe00479ab8f0e6ae075"},
{file = "pycryptodome-3.14.1-pp27-pypy_73-win32.whl", hash = "sha256:f572a3ff7b6029dd9b904d6be4e0ce9e309dcb847b03e3ac8698d9d23bb36525"},
{file = "pycryptodome-3.14.1-pp36-pypy36_pp73-macosx_10_9_x86_64.whl", hash = "sha256:9924248d6920b59c260adcae3ee231cd5af404ac706ad30aa4cd87051bf09c50"},
{file = "pycryptodome-3.14.1-pp36-pypy36_pp73-manylinux1_x86_64.whl", hash = "sha256:e0c04c41e9ade19fbc0eff6aacea40b831bfcb2c91c266137bcdfd0d7b2f33ba"},
{file = "pycryptodome-3.14.1-pp36-pypy36_pp73-manylinux2010_x86_64.whl", hash = "sha256:893f32210de74b9f8ac869ed66c97d04e7d351182d6d39ebd3b36d3db8bda65d"},
{file = "pycryptodome-3.14.1-pp36-pypy36_pp73-win32.whl", hash = "sha256:7fb90a5000cc9c9ff34b4d99f7f039e9c3477700e309ff234eafca7b7471afc0"},
{file = "pycryptodome-3.14.1.tar.gz", hash = "sha256:e04e40a7f8c1669195536a37979dd87da2c32dbdc73d6fe35f0077b0c17c803b"},
]
pydocstyle = [
{file = "pydocstyle-6.1.1-py3-none-any.whl", hash = "sha256:6987826d6775056839940041beef5c08cc7e3d71d63149b48e36727f70144dc4"},
{file = "pydocstyle-6.1.1.tar.gz", hash = "sha256:1d41b7c459ba0ee6c345f2eb9ae827cab14a7533a88c5c6f7e94923f72df92dc"},
@ -1292,17 +1498,21 @@ pymongo = [
{file = "pymongo-3.12.3-py2.7-macosx-10.14-intel.egg", hash = "sha256:d81299f63dc33cc172c26faf59cc54dd795fc6dd5821a7676cca112a5ee8bbd6"},
{file = "pymongo-3.12.3.tar.gz", hash = "sha256:0a89cadc0062a5e53664dde043f6c097172b8c1c5f0094490095282ff9995a5f"},
]
pyparsing = [
{file = "pyparsing-3.0.7-py3-none-any.whl", hash = "sha256:a6c06a88f252e6c322f65faf8f418b16213b51bdfaece0524c1c1bc30c63c484"},
{file = "pyparsing-3.0.7.tar.gz", hash = "sha256:18ee9022775d270c55187733956460083db60b37d0d0fb357445f3094eed3eea"},
]
python-gitlab = [
{file = "python-gitlab-3.1.1.tar.gz", hash = "sha256:cad1338c1ff1a791a7bae7995dc621e26c77dfbf225bade456acec7512782825"},
{file = "python_gitlab-3.1.1-py3-none-any.whl", hash = "sha256:2a7de39c8976db6d0db20031e71b3e43d262e99e64b471ef09bf00482cd3d9fa"},
{file = "python-gitlab-3.2.0.tar.gz", hash = "sha256:8f6ee81109fec231fc2b74e2c4035bb7de0548eaf82dd119fe294df2c4a524be"},
{file = "python_gitlab-3.2.0-py3-none-any.whl", hash = "sha256:48f72e033c06ab1c244266af85de2cb0a175f8a3614417567e2b14254ead9b2e"},
]
python-lsp-jsonrpc = [
{file = "python-lsp-jsonrpc-1.0.0.tar.gz", hash = "sha256:7bec170733db628d3506ea3a5288ff76aa33c70215ed223abdb0d95e957660bd"},
{file = "python_lsp_jsonrpc-1.0.0-py3-none-any.whl", hash = "sha256:079b143be64b0a378bdb21dff5e28a8c1393fe7e8a654ef068322d754e545fc7"},
]
python-lsp-server = [
{file = "python-lsp-server-1.3.3.tar.gz", hash = "sha256:1b48ccd8b70103522e8a8b9cb9ae1be2b27a5db0dfd661e7e44e6253ebefdc40"},
{file = "python_lsp_server-1.3.3-py3-none-any.whl", hash = "sha256:ea7b1e623591ccbfbbf8e5dfe0df8119f27863125a58bdcacbacd1937d8e8cb3"},
{file = "python-lsp-server-1.4.0.tar.gz", hash = "sha256:769142c07573f6b66e930cbd7c588b826082550bef6267bb0aec63e7b6260009"},
{file = "python_lsp_server-1.4.0-py3-none-any.whl", hash = "sha256:3160c97c6d1edd8456f262fc0e4aad6b322c0cfd1b58d322a41679e57787594d"},
]
pyyaml = [
{file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"},
@ -1352,8 +1562,8 @@ requests-toolbelt = [
{file = "requests_toolbelt-0.9.1-py2.py3-none-any.whl", hash = "sha256:380606e1d10dc85c3bd47bf5a6095f815ec007be7a8b69c878507068df059e6f"},
]
rope = [
{file = "rope-0.22.0-py3-none-any.whl", hash = "sha256:2847220bf72ead09b5abe72b1edc9cacff90ab93663ece06913fc97324167870"},
{file = "rope-0.22.0.tar.gz", hash = "sha256:b00fbc064a26fc62d7220578a27fd639b2fad57213663cc396c137e92d73f10f"},
{file = "rope-0.23.0-py3-none-any.whl", hash = "sha256:edf2ed3c9b35a8814752ffd3ea55b293c791e5087e252461de898e953cf9c146"},
{file = "rope-0.23.0.tar.gz", hash = "sha256:f87662c565086d660fc855cc07f37820267876634c3e9e51bddb32ff51547268"},
]
smmap = [
{file = "smmap-5.0.0-py3-none-any.whl", hash = "sha256:2aba19d6a040e78d8b09de5c57e96207b09ed71d8e55ce0959eeee6c8e190d94"},
@ -1368,12 +1578,16 @@ toml = [
{file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},
]
tomli = [
{file = "tomli-2.0.0-py3-none-any.whl", hash = "sha256:b5bde28da1fed24b9bd1d4d2b8cba62300bfb4ec9a6187a957e8ddb9434c5224"},
{file = "tomli-2.0.0.tar.gz", hash = "sha256:c292c34f58502a1eb2bbb9f5bbc9a5ebc37bee10ffb8c2d6bbdfa8eb13cc14e1"},
{file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
{file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
]
tweepy = [
{file = "tweepy-4.5.0-py2.py3-none-any.whl", hash = "sha256:1efe228d5994e0d996577bd052b73c59dada96ff8045e176bf46c175afe61859"},
{file = "tweepy-4.5.0.tar.gz", hash = "sha256:12cc4b0a3d7b745928b08c3eb55a992236895e00028584d11fa41258f07df1b9"},
{file = "tweepy-4.7.0-py2.py3-none-any.whl", hash = "sha256:d7e78c49232e849b660d82c7c2e504e8487d8014dcb73b39b490b61827965aba"},
{file = "tweepy-4.7.0.tar.gz", hash = "sha256:82323505d549e3868e14a4570fc069ab3058ef95f9e578d1476d69bf2c831483"},
]
typing-extensions = [
{file = "typing_extensions-4.1.1-py3-none-any.whl", hash = "sha256:21c85e0fe4b9a155d0799430b0ad741cdce7e359660ccbd8b530613e8df88ce2"},
{file = "typing_extensions-4.1.1.tar.gz", hash = "sha256:1a9462dcc3347a79b1f1c0271fbe79e844580bb598bafa1ed208b94da3cdcd42"},
]
ujson = [
{file = "ujson-5.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:644552d1e89983c08d0c24358fbcb5829ae5b5deee9d876e16d20085cfa7dc81"},
@ -1431,6 +1645,10 @@ ulid-py = [
{file = "ulid-py-1.1.0.tar.gz", hash = "sha256:dc6884be91558df077c3011b9fb0c87d1097cb8fc6534b11f310161afd5738f0"},
{file = "ulid_py-1.1.0-py2.py3-none-any.whl", hash = "sha256:b56a0f809ef90d6020b21b89a87a48edc7c03aea80e5ed5174172e82d76e3987"},
]
umongo = [
{file = "umongo-3.1.0-py2.py3-none-any.whl", hash = "sha256:f6913027651ae673d71aaf54285f9ebf1e49a3f57662e526d029ba72e1a3fcd5"},
{file = "umongo-3.1.0.tar.gz", hash = "sha256:20c72f09edae931285c22c1928862af35b90ec639a4dac2dbf015aaaac00e931"},
]
urllib3 = [
{file = "urllib3-1.26.8-py2.py3-none-any.whl", hash = "sha256:000ca7f471a233c2251c6c7023ee85305721bfdf18621ebff4fd17a8653427ed"},
{file = "urllib3-1.26.8.tar.gz", hash = "sha256:0e7c33d9a63e7ddfcb86780aac87befc2fbddf46c58dbb487e0855f7ceec283c"},

View file

@ -1,13 +1,13 @@
[tool.poetry]
name = "jarvis"
version = "2.0.0a0"
version = "2.0.0b1"
description = "J.A.R.V.I.S. admin bot"
authors = ["Zevaryx <zevaryx@gmail.com>"]
[tool.poetry.dependencies]
python = "^3.10"
PyYAML = "^6.0"
dis-snek = "^5.0.0"
dis-snek = "*"
GitPython = "^3.1.26"
mongoengine = "^0.23.1"
opencv-python = "^4.5.5"
@ -17,6 +17,9 @@ python-gitlab = "^3.1.1"
ulid-py = "^1.1.0"
tweepy = "^4.5.0"
orjson = "^3.6.6"
jarvis-core = {git = "https://git.zevaryx.com/stark-industries/jarvis/jarvis-core.git", rev = "main"}
aiohttp = "^3.8.1"
pastypy = "^1.0.1"
[tool.poetry.dev-dependencies]
python-lsp-server = {extras = ["all"], version = "^1.3.3"}

4
run.py
View file

@ -1,5 +1,7 @@
"""Main run file for J.A.R.V.I.S."""
import asyncio
from jarvis import run
if __name__ == "__main__":
run()
asyncio.run(run())