Add smarter moderation cases
This commit is contained in:
parent
8e07dceed7
commit
e17d05873e
7 changed files with 147 additions and 110 deletions
|
@ -6,7 +6,16 @@ from datetime import datetime, timedelta, timezone
|
||||||
|
|
||||||
from aiohttp import ClientSession
|
from aiohttp import ClientSession
|
||||||
from jarvis_core.db import q
|
from jarvis_core.db import q
|
||||||
from jarvis_core.db.models import Autopurge, Autoreact, Roleping, Setting, Warning
|
from jarvis_core.db.models import (
|
||||||
|
Action,
|
||||||
|
Autopurge,
|
||||||
|
Autoreact,
|
||||||
|
Modlog,
|
||||||
|
Note,
|
||||||
|
Roleping,
|
||||||
|
Setting,
|
||||||
|
Warning,
|
||||||
|
)
|
||||||
from jarvis_core.filters import invites, url
|
from jarvis_core.filters import invites, url
|
||||||
from jarvis_core.util.ansi import RESET, Fore, Format, fmt
|
from jarvis_core.util.ansi import RESET, Fore, Format, fmt
|
||||||
from naff import Client, listen
|
from naff import Client, listen
|
||||||
|
@ -17,6 +26,7 @@ from naff.api.events.discord import (
|
||||||
MessageDelete,
|
MessageDelete,
|
||||||
MessageUpdate,
|
MessageUpdate,
|
||||||
)
|
)
|
||||||
|
from naff.api.events.internal import Button
|
||||||
from naff.client.errors import CommandCheckFailure, CommandOnCooldown, HTTPException
|
from naff.client.errors import CommandCheckFailure, CommandOnCooldown, HTTPException
|
||||||
from naff.client.utils.misc_utils import find_all
|
from naff.client.utils.misc_utils import find_all
|
||||||
from naff.models.discord.channel import DMChannel
|
from naff.models.discord.channel import DMChannel
|
||||||
|
@ -57,7 +67,9 @@ CMD_FMT = fmt(Fore.GREEN, Format.BOLD)
|
||||||
|
|
||||||
class Jarvis(Client):
|
class Jarvis(Client):
|
||||||
def __init__(self, *args, **kwargs): # noqa: ANN002 ANN003
|
def __init__(self, *args, **kwargs): # noqa: ANN002 ANN003
|
||||||
|
redis = kwargs.pop("redis")
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
self.redis = redis
|
||||||
self.logger = logging.getLogger(__name__)
|
self.logger = logging.getLogger(__name__)
|
||||||
self.phishing_domains = []
|
self.phishing_domains = []
|
||||||
self.pre_run_callback = self._prerun
|
self.pre_run_callback = self._prerun
|
||||||
|
@ -634,3 +646,62 @@ class Jarvis(Client):
|
||||||
self.logger.warning(
|
self.logger.warning(
|
||||||
f"Failed to process edit {message.guild.id}/{message.channel.id}/{message.id}: {e}"
|
f"Failed to process edit {message.guild.id}/{message.channel.id}/{message.id}: {e}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@listen()
|
||||||
|
async def on_button(self, event: Button) -> None:
|
||||||
|
"""Process button events."""
|
||||||
|
context = event.context
|
||||||
|
await context.defer(ephemeral=True)
|
||||||
|
if not context.custom_id.startswith("modcase|"):
|
||||||
|
return await super().on_button(event)
|
||||||
|
|
||||||
|
if not context.author.has_permissions(Permissions.MANAGE_USERS):
|
||||||
|
return
|
||||||
|
|
||||||
|
user_key = f"msg|{context.message.id}"
|
||||||
|
|
||||||
|
if context.custom_id == "modcase|yes":
|
||||||
|
if user_id := await self.redis.get(user_key):
|
||||||
|
action_key = f"{user_id}|{context.guild.id}"
|
||||||
|
if (user := await context.guild.fetch_member(user_id)) and (
|
||||||
|
action_data := await self.redis.get(action_key)
|
||||||
|
):
|
||||||
|
name, parent = action_data.split("|")[:2]
|
||||||
|
action = Action(action_type=name, parent=parent)
|
||||||
|
note = Note(
|
||||||
|
admin=context.author.id, content="Moderation case opened via message"
|
||||||
|
)
|
||||||
|
modlog = Modlog(
|
||||||
|
user=user.id, admin=context.author.id, actions=[action], notes=[note]
|
||||||
|
)
|
||||||
|
await modlog.commit()
|
||||||
|
|
||||||
|
fields = (
|
||||||
|
EmbedField(name="Admin", value=context.author.mention),
|
||||||
|
EmbedField(name="Opening Action", value=f"{name} {parent}"),
|
||||||
|
)
|
||||||
|
embed = build_embed(
|
||||||
|
title="Moderation Case Opened",
|
||||||
|
description="Moderation case opened against {user.mention}",
|
||||||
|
fields=fields,
|
||||||
|
)
|
||||||
|
embed.set_author(
|
||||||
|
name=user.username + "#" + user.discriminator,
|
||||||
|
icon_url=user.display_avatar.url,
|
||||||
|
)
|
||||||
|
|
||||||
|
await context.message.edit(embed=embed)
|
||||||
|
elif not user:
|
||||||
|
self.logger.debug("User no longer in guild")
|
||||||
|
await context.send("User no longer in guild", ephemeral=True)
|
||||||
|
else:
|
||||||
|
self.logger.warn("Unable to get action data ( %s )", action_key)
|
||||||
|
await context.send("Unable to get action data", ephemeral=True)
|
||||||
|
|
||||||
|
await self.bot.redis.delete(user_key)
|
||||||
|
await self.bot.redis.delete(action_key)
|
||||||
|
|
||||||
|
for row in context.message.components:
|
||||||
|
for component in row.components:
|
||||||
|
component.disabled = True
|
||||||
|
await context.message.edit(components=context.message.components)
|
||||||
|
|
|
@ -1,18 +1,9 @@
|
||||||
"""Load the config for JARVIS"""
|
"""Load the config for JARVIS"""
|
||||||
import os
|
|
||||||
|
|
||||||
from jarvis_core.config import Config as CConfig
|
from jarvis_core.config import Config as CConfig
|
||||||
from pymongo import MongoClient
|
|
||||||
from yaml import load
|
|
||||||
|
|
||||||
try:
|
|
||||||
from yaml import CLoader as Loader
|
|
||||||
except ImportError:
|
|
||||||
from yaml import Loader
|
|
||||||
|
|
||||||
|
|
||||||
class JarvisConfig(CConfig):
|
class JarvisConfig(CConfig):
|
||||||
REQUIRED = ("token", "mongo", "urls")
|
REQUIRED = ("token", "mongo", "urls", "redis")
|
||||||
OPTIONAL = {
|
OPTIONAL = {
|
||||||
"sync": False,
|
"sync": False,
|
||||||
"log_level": "WARNING",
|
"log_level": "WARNING",
|
||||||
|
@ -24,79 +15,3 @@ class JarvisConfig(CConfig):
|
||||||
"reddit": None,
|
"reddit": None,
|
||||||
"rook_token": None,
|
"rook_token": None,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class Config(object):
|
|
||||||
"""Config singleton object for JARVIS"""
|
|
||||||
|
|
||||||
def __new__(cls, *args: list, **kwargs: dict):
|
|
||||||
"""Get the singleton config, or creates a new one."""
|
|
||||||
it = cls.__dict__.get("it")
|
|
||||||
if it is not None:
|
|
||||||
return it
|
|
||||||
cls.__it__ = it = object.__new__(cls)
|
|
||||||
it.init(*args, **kwargs)
|
|
||||||
return it
|
|
||||||
|
|
||||||
def init(
|
|
||||||
self,
|
|
||||||
token: str,
|
|
||||||
mongo: dict,
|
|
||||||
urls: dict,
|
|
||||||
sync: bool = False,
|
|
||||||
log_level: str = "WARNING",
|
|
||||||
cogs: list = None,
|
|
||||||
events: bool = True,
|
|
||||||
gitlab_token: str = None,
|
|
||||||
max_messages: int = 1000,
|
|
||||||
twitter: dict = None,
|
|
||||||
reddit: dict = None,
|
|
||||||
rook_token: str = None,
|
|
||||||
) -> None:
|
|
||||||
"""Initialize the config object."""
|
|
||||||
self.token = token
|
|
||||||
self.mongo = mongo
|
|
||||||
self.urls = urls
|
|
||||||
self.log_level = log_level
|
|
||||||
self.cogs = cogs
|
|
||||||
self.events = events
|
|
||||||
self.max_messages = max_messages
|
|
||||||
self.gitlab_token = gitlab_token
|
|
||||||
self.twitter = twitter
|
|
||||||
self.reddit = reddit
|
|
||||||
self.sync = sync or os.environ.get("SYNC_COMMANDS", False)
|
|
||||||
self.rook_token = rook_token
|
|
||||||
self.__db_loaded = False
|
|
||||||
self.__mongo = MongoClient(**self.mongo["connect"])
|
|
||||||
|
|
||||||
def get_db_config(self) -> None:
|
|
||||||
"""Load the database config objects."""
|
|
||||||
if not self.__db_loaded:
|
|
||||||
db = self.__mongo[self.mongo["database"]]
|
|
||||||
items = db.config.find()
|
|
||||||
for item in items:
|
|
||||||
setattr(self, item["key"], item["value"])
|
|
||||||
self.__db_loaded = True
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_yaml(cls, y: dict) -> "Config":
|
|
||||||
"""Load the yaml config file."""
|
|
||||||
return cls(**y)
|
|
||||||
|
|
||||||
|
|
||||||
def get_config(path: str = "config.yaml") -> Config:
|
|
||||||
"""Get the config from the specified yaml file."""
|
|
||||||
if Config.__dict__.get("it"):
|
|
||||||
return Config()
|
|
||||||
with open(path) as f:
|
|
||||||
raw = f.read()
|
|
||||||
y = load(raw, Loader=Loader)
|
|
||||||
config = Config.from_yaml(y)
|
|
||||||
config.get_db_config()
|
|
||||||
return config
|
|
||||||
|
|
||||||
|
|
||||||
def reload_config() -> None:
|
|
||||||
"""Force reload of the config singleton on next call."""
|
|
||||||
if "it" in Config.__dict__:
|
|
||||||
Config.__dict__.pop("it")
|
|
||||||
|
|
|
@ -7,7 +7,7 @@ from naff.models.discord.embed import Embed, EmbedField
|
||||||
from naff.models.discord.guild import AuditLogEntry
|
from naff.models.discord.guild import AuditLogEntry
|
||||||
from naff.models.discord.user import Member
|
from naff.models.discord.user import Member
|
||||||
|
|
||||||
from jarvis.config import get_config
|
from jarvis.config import JarvisConfig
|
||||||
|
|
||||||
|
|
||||||
def build_embed(
|
def build_embed(
|
||||||
|
@ -63,7 +63,7 @@ def modlog_embed(
|
||||||
|
|
||||||
def get_extensions(path: str) -> list:
|
def get_extensions(path: str) -> list:
|
||||||
"""Get JARVIS cogs."""
|
"""Get JARVIS cogs."""
|
||||||
config = get_config()
|
config = JarvisConfig.from_yaml()
|
||||||
vals = config.cogs or [x.name for x in iter_modules(path)]
|
vals = config.cogs or [x.name for x in iter_modules(path)]
|
||||||
return [f"jarvis.cogs.{x}" for x in vals]
|
return [f"jarvis.cogs.{x}" for x in vals]
|
||||||
|
|
||||||
|
|
|
@ -2,17 +2,9 @@
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from jarvis_core.db import q
|
from jarvis_core.db import q
|
||||||
from jarvis_core.db.models import (
|
from jarvis_core.db.models import Ban, Kick, Mute, Setting, Warning
|
||||||
Action,
|
|
||||||
Ban,
|
|
||||||
Kick,
|
|
||||||
Modlog,
|
|
||||||
Mute,
|
|
||||||
Note,
|
|
||||||
Setting,
|
|
||||||
Warning,
|
|
||||||
)
|
|
||||||
from naff import Client, Cog, InteractionContext
|
from naff import Client, Cog, InteractionContext
|
||||||
|
from naff.models.discord.components import ActionRow, Button, ButtonStyles
|
||||||
from naff.models.discord.embed import EmbedField
|
from naff.models.discord.embed import EmbedField
|
||||||
|
|
||||||
from jarvis.utils import build_embed
|
from jarvis.utils import build_embed
|
||||||
|
@ -55,9 +47,6 @@ class ModcaseCog(Cog):
|
||||||
self.logger.warning("Missing action %s, exiting", name)
|
self.logger.warning("Missing action %s, exiting", name)
|
||||||
return
|
return
|
||||||
|
|
||||||
action = Action(action_type=name.lower(), parent=action.id)
|
|
||||||
note = Note(admin=self.bot.user.id, content="Moderation case opened automatically")
|
|
||||||
await Modlog(user=user.id, admin=ctx.author.id, actions=[action], notes=[note]).commit()
|
|
||||||
notify = await Setting.find_one(q(guild=ctx.guild.id, setting="notify", value=True))
|
notify = await Setting.find_one(q(guild=ctx.guild.id, setting="notify", value=True))
|
||||||
if notify and name not in ("Kick", "Ban"): # Ignore Kick and Ban, as these are unique
|
if notify and name not in ("Kick", "Ban"): # Ignore Kick and Ban, as these are unique
|
||||||
fields = (
|
fields = (
|
||||||
|
@ -74,4 +63,45 @@ class ModcaseCog(Cog):
|
||||||
guild_url = f"https://discord.com/channels/{ctx.guild.id}"
|
guild_url = f"https://discord.com/channels/{ctx.guild.id}"
|
||||||
embed.set_author(name=ctx.guild.name, icon_url=ctx.guild.icon.url, url=guild_url)
|
embed.set_author(name=ctx.guild.name, icon_url=ctx.guild.icon.url, url=guild_url)
|
||||||
embed.set_thumbnail(url=ctx.guild.icon.url)
|
embed.set_thumbnail(url=ctx.guild.icon.url)
|
||||||
await user.send(embed=embed)
|
try:
|
||||||
|
await user.send(embed=embed)
|
||||||
|
except Exception:
|
||||||
|
self.logger.debug("User not warned of action due to closed DMs")
|
||||||
|
|
||||||
|
lookup_key = f"{user.id}|{ctx.guild.id}"
|
||||||
|
|
||||||
|
async with self.bot.redis.lock(lookup_key):
|
||||||
|
if await self.bot.redis.get(lookup_key):
|
||||||
|
self.logger.debug(f"User {user.id} in {ctx.guild.id} already has pending case")
|
||||||
|
return
|
||||||
|
|
||||||
|
modlog = await Setting.find_one(q(guild=ctx.guild.id, setting="modlog"))
|
||||||
|
if not modlog:
|
||||||
|
return
|
||||||
|
|
||||||
|
channel = await ctx.guild.fetch_channel(modlog.value)
|
||||||
|
if not channel:
|
||||||
|
self.logger.warn(
|
||||||
|
f"Guild {ctx.guild.id} modlog channel no longer exists, deleting"
|
||||||
|
)
|
||||||
|
await modlog.delete()
|
||||||
|
return
|
||||||
|
|
||||||
|
embed = build_embed(
|
||||||
|
title="Recent Action Taken",
|
||||||
|
description="Would you like to open a moderation case for {user.mention}?",
|
||||||
|
fields=[],
|
||||||
|
)
|
||||||
|
embed.set_author(
|
||||||
|
name=user.username + "#" + user.discriminator, icon_url=user.display_avatar.url
|
||||||
|
)
|
||||||
|
components = [
|
||||||
|
ActionRow(
|
||||||
|
Button(style=ButtonStyles.RED, emoji="✖️", custom_id="modcase|no"),
|
||||||
|
Button(style=ButtonStyles.GREEN, emoji="✔️", custom_id="modcase|yes"),
|
||||||
|
)
|
||||||
|
]
|
||||||
|
message = await ctx.send(embed=embed, components=components)
|
||||||
|
await self.bot.redis.set(lookup_key, f"{name.lower()}|{action.id}")
|
||||||
|
|
||||||
|
await self.bot.redis.set(f"msg|{message.id}", user.id)
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
"""Permissions wrappers."""
|
"""Permissions wrappers."""
|
||||||
from naff import InteractionContext, Permissions
|
from naff import InteractionContext, Permissions
|
||||||
|
|
||||||
from jarvis.config import get_config
|
from jarvis.config import JarvisConfig
|
||||||
|
|
||||||
|
|
||||||
def user_is_bot_admin() -> bool:
|
def user_is_bot_admin() -> bool:
|
||||||
|
@ -9,8 +9,9 @@ def user_is_bot_admin() -> bool:
|
||||||
|
|
||||||
async def predicate(ctx: InteractionContext) -> bool:
|
async def predicate(ctx: InteractionContext) -> bool:
|
||||||
"""Command check predicate."""
|
"""Command check predicate."""
|
||||||
if getattr(get_config(), "admins", None):
|
cfg = JarvisConfig.from_yaml()
|
||||||
return ctx.author.id in get_config().admins
|
if getattr(cfg, "admins", None):
|
||||||
|
return ctx.author.id in cfg.admins
|
||||||
else:
|
else:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
27
poetry.lock
generated
27
poetry.lock
generated
|
@ -40,6 +40,21 @@ yarl = ">=1.0,<2.0"
|
||||||
[package.extras]
|
[package.extras]
|
||||||
speedups = ["aiodns", "brotli", "cchardet"]
|
speedups = ["aiodns", "brotli", "cchardet"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "aioredis"
|
||||||
|
version = "2.0.1"
|
||||||
|
description = "asyncio (PEP 3156) Redis support"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
async-timeout = "*"
|
||||||
|
typing-extensions = "*"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
hiredis = ["hiredis (>=1.0)"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aiosignal"
|
name = "aiosignal"
|
||||||
version = "1.2.0"
|
version = "1.2.0"
|
||||||
|
@ -497,11 +512,11 @@ python-versions = ">=3.10"
|
||||||
aiohttp = {version = "3.8.1", markers = "python_version >= \"3.6\""}
|
aiohttp = {version = "3.8.1", markers = "python_version >= \"3.6\""}
|
||||||
aiosignal = {version = "1.2.0", 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\""}
|
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\""}
|
attrs = {version = "21.4.0", markers = "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\""}
|
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\""}
|
charset-normalizer = {version = "2.0.12", markers = "python_full_version >= \"3.6.0\""}
|
||||||
frozenlist = {version = "1.3.0", markers = "python_version >= \"3.7\""}
|
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\""}
|
idna = {version = "3.3", markers = "python_full_version >= \"3.6.0\""}
|
||||||
multidict = {version = "6.0.2", markers = "python_version >= \"3.7\""}
|
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\""}
|
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\""}
|
requests = {version = "2.27.1", markers = "python_version >= \"2.7\" and python_full_version < \"3.0.0\" or python_full_version >= \"3.6.0\""}
|
||||||
|
@ -901,7 +916,7 @@ multidict = ">=4.0"
|
||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "1.1"
|
lock-version = "1.1"
|
||||||
python-versions = "^3.10"
|
python-versions = "^3.10"
|
||||||
content-hash = "3bfe48a36c3bc4bef6e6840eaeb51b01ffd6d038135451e45307eb843df981e0"
|
content-hash = "3fe4606bc1a4c1e58ee535a4b4126f676c0780c2fd02d15e3df9657586967b1e"
|
||||||
|
|
||||||
[metadata.files]
|
[metadata.files]
|
||||||
aiofile = [
|
aiofile = [
|
||||||
|
@ -986,6 +1001,10 @@ aiohttp = [
|
||||||
{file = "aiohttp-3.8.1-cp39-cp39-win_amd64.whl", hash = "sha256:1c182cb873bc91b411e184dab7a2b664d4fea2743df0e4d57402f7f3fa644bac"},
|
{file = "aiohttp-3.8.1-cp39-cp39-win_amd64.whl", hash = "sha256:1c182cb873bc91b411e184dab7a2b664d4fea2743df0e4d57402f7f3fa644bac"},
|
||||||
{file = "aiohttp-3.8.1.tar.gz", hash = "sha256:fc5471e1a54de15ef71c1bc6ebe80d4dc681ea600e68bfd1cbce40427f0b7578"},
|
{file = "aiohttp-3.8.1.tar.gz", hash = "sha256:fc5471e1a54de15ef71c1bc6ebe80d4dc681ea600e68bfd1cbce40427f0b7578"},
|
||||||
]
|
]
|
||||||
|
aioredis = [
|
||||||
|
{file = "aioredis-2.0.1-py3-none-any.whl", hash = "sha256:9ac0d0b3b485d293b8ca1987e6de8658d7dafcca1cddfcd1d506cae8cdebfdd6"},
|
||||||
|
{file = "aioredis-2.0.1.tar.gz", hash = "sha256:eaa51aaf993f2d71f54b70527c440437ba65340588afeb786cd87c55c89cd98e"},
|
||||||
|
]
|
||||||
aiosignal = [
|
aiosignal = [
|
||||||
{file = "aiosignal-1.2.0-py3-none-any.whl", hash = "sha256:26e62109036cd181df6e6ad646f91f0dcfd05fe16d0cb924138ff2ab75d64e3a"},
|
{file = "aiosignal-1.2.0-py3-none-any.whl", hash = "sha256:26e62109036cd181df6e6ad646f91f0dcfd05fe16d0cb924138ff2ab75d64e3a"},
|
||||||
{file = "aiosignal-1.2.0.tar.gz", hash = "sha256:78ed67db6c7b7ced4f98e495e572106d5c432a93e1ddd1bf475e1dc05f5b7df2"},
|
{file = "aiosignal-1.2.0.tar.gz", hash = "sha256:78ed67db6c7b7ced4f98e495e572106d5c432a93e1ddd1bf475e1dc05f5b7df2"},
|
||||||
|
|
|
@ -26,6 +26,7 @@ asyncpraw = "^7.5.0"
|
||||||
rook = "^0.1.170"
|
rook = "^0.1.170"
|
||||||
rich = "^12.3.0"
|
rich = "^12.3.0"
|
||||||
jurigged = "^0.5.0"
|
jurigged = "^0.5.0"
|
||||||
|
aioredis = "^2.0.1"
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = ["poetry-core>=1.0.0"]
|
requires = ["poetry-core>=1.0.0"]
|
||||||
|
|
Loading…
Add table
Reference in a new issue