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 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.util.ansi import RESET, Fore, Format, fmt
|
||||
from naff import Client, listen
|
||||
|
@ -17,6 +26,7 @@ from naff.api.events.discord import (
|
|||
MessageDelete,
|
||||
MessageUpdate,
|
||||
)
|
||||
from naff.api.events.internal import Button
|
||||
from naff.client.errors import CommandCheckFailure, CommandOnCooldown, HTTPException
|
||||
from naff.client.utils.misc_utils import find_all
|
||||
from naff.models.discord.channel import DMChannel
|
||||
|
@ -57,7 +67,9 @@ CMD_FMT = fmt(Fore.GREEN, Format.BOLD)
|
|||
|
||||
class Jarvis(Client):
|
||||
def __init__(self, *args, **kwargs): # noqa: ANN002 ANN003
|
||||
redis = kwargs.pop("redis")
|
||||
super().__init__(*args, **kwargs)
|
||||
self.redis = redis
|
||||
self.logger = logging.getLogger(__name__)
|
||||
self.phishing_domains = []
|
||||
self.pre_run_callback = self._prerun
|
||||
|
@ -634,3 +646,62 @@ class Jarvis(Client):
|
|||
self.logger.warning(
|
||||
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"""
|
||||
import os
|
||||
|
||||
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):
|
||||
REQUIRED = ("token", "mongo", "urls")
|
||||
REQUIRED = ("token", "mongo", "urls", "redis")
|
||||
OPTIONAL = {
|
||||
"sync": False,
|
||||
"log_level": "WARNING",
|
||||
|
@ -24,79 +15,3 @@ class JarvisConfig(CConfig):
|
|||
"reddit": 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.user import Member
|
||||
|
||||
from jarvis.config import get_config
|
||||
from jarvis.config import JarvisConfig
|
||||
|
||||
|
||||
def build_embed(
|
||||
|
@ -63,7 +63,7 @@ def modlog_embed(
|
|||
|
||||
def get_extensions(path: str) -> list:
|
||||
"""Get JARVIS cogs."""
|
||||
config = get_config()
|
||||
config = JarvisConfig.from_yaml()
|
||||
vals = config.cogs or [x.name for x in iter_modules(path)]
|
||||
return [f"jarvis.cogs.{x}" for x in vals]
|
||||
|
||||
|
|
|
@ -2,17 +2,9 @@
|
|||
import logging
|
||||
|
||||
from jarvis_core.db import q
|
||||
from jarvis_core.db.models import (
|
||||
Action,
|
||||
Ban,
|
||||
Kick,
|
||||
Modlog,
|
||||
Mute,
|
||||
Note,
|
||||
Setting,
|
||||
Warning,
|
||||
)
|
||||
from jarvis_core.db.models import Ban, Kick, Mute, Setting, Warning
|
||||
from naff import Client, Cog, InteractionContext
|
||||
from naff.models.discord.components import ActionRow, Button, ButtonStyles
|
||||
from naff.models.discord.embed import EmbedField
|
||||
|
||||
from jarvis.utils import build_embed
|
||||
|
@ -55,9 +47,6 @@ class ModcaseCog(Cog):
|
|||
self.logger.warning("Missing action %s, exiting", name)
|
||||
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))
|
||||
if notify and name not in ("Kick", "Ban"): # Ignore Kick and Ban, as these are unique
|
||||
fields = (
|
||||
|
@ -74,4 +63,45 @@ class ModcaseCog(Cog):
|
|||
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_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."""
|
||||
from naff import InteractionContext, Permissions
|
||||
|
||||
from jarvis.config import get_config
|
||||
from jarvis.config import JarvisConfig
|
||||
|
||||
|
||||
def user_is_bot_admin() -> bool:
|
||||
|
@ -9,8 +9,9 @@ def user_is_bot_admin() -> bool:
|
|||
|
||||
async def predicate(ctx: InteractionContext) -> bool:
|
||||
"""Command check predicate."""
|
||||
if getattr(get_config(), "admins", None):
|
||||
return ctx.author.id in get_config().admins
|
||||
cfg = JarvisConfig.from_yaml()
|
||||
if getattr(cfg, "admins", None):
|
||||
return ctx.author.id in cfg.admins
|
||||
else:
|
||||
return False
|
||||
|
||||
|
|
27
poetry.lock
generated
27
poetry.lock
generated
|
@ -40,6 +40,21 @@ yarl = ">=1.0,<2.0"
|
|||
[package.extras]
|
||||
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]]
|
||||
name = "aiosignal"
|
||||
version = "1.2.0"
|
||||
|
@ -497,11 +512,11 @@ python-versions = ">=3.10"
|
|||
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\""}
|
||||
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\""}
|
||||
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\""}
|
||||
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\""}
|
||||
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\""}
|
||||
|
@ -901,7 +916,7 @@ multidict = ">=4.0"
|
|||
[metadata]
|
||||
lock-version = "1.1"
|
||||
python-versions = "^3.10"
|
||||
content-hash = "3bfe48a36c3bc4bef6e6840eaeb51b01ffd6d038135451e45307eb843df981e0"
|
||||
content-hash = "3fe4606bc1a4c1e58ee535a4b4126f676c0780c2fd02d15e3df9657586967b1e"
|
||||
|
||||
[metadata.files]
|
||||
aiofile = [
|
||||
|
@ -986,6 +1001,10 @@ aiohttp = [
|
|||
{file = "aiohttp-3.8.1-cp39-cp39-win_amd64.whl", hash = "sha256:1c182cb873bc91b411e184dab7a2b664d4fea2743df0e4d57402f7f3fa644bac"},
|
||||
{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 = [
|
||||
{file = "aiosignal-1.2.0-py3-none-any.whl", hash = "sha256:26e62109036cd181df6e6ad646f91f0dcfd05fe16d0cb924138ff2ab75d64e3a"},
|
||||
{file = "aiosignal-1.2.0.tar.gz", hash = "sha256:78ed67db6c7b7ced4f98e495e572106d5c432a93e1ddd1bf475e1dc05f5b7df2"},
|
||||
|
|
|
@ -26,6 +26,7 @@ asyncpraw = "^7.5.0"
|
|||
rook = "^0.1.170"
|
||||
rich = "^12.3.0"
|
||||
jurigged = "^0.5.0"
|
||||
aioredis = "^2.0.1"
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core>=1.0.0"]
|
||||
|
|
Loading…
Add table
Reference in a new issue