Add smarter moderation cases

This commit is contained in:
Zeva Rose 2022-05-03 01:05:25 -06:00
parent 8e07dceed7
commit e17d05873e
7 changed files with 147 additions and 110 deletions

View file

@ -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)

View file

@ -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")

View file

@ -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]

View file

@ -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)

View file

@ -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
View file

@ -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"},

View file

@ -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"]