Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,12 @@ Changes since 2026.06.04
### Moderator
- Adds autocomplete for /unwarn command
- Bans from max warnings will now show a ban in the text output
- Removes command use logging, in favor of modlog

### Modlog
- Completely rewrites modlog to log all moderation actions
- Reworks the output of the all of the modlog commands
- Adds new command to lookup action by ID

### Modmail
- Full migration to application commands
Expand Down
35 changes: 25 additions & 10 deletions core/databases.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,28 +63,43 @@ class ApplicationBans(bot.db.Model):
guild_id: str = bot.db.Column(bot.db.String)
applicant_id: str = bot.db.Column(bot.db.String)

class BanLog(bot.db.Model):
class ModLog(bot.db.Model):
"""The postgres table for banlogs
Currently used in modlog.py

Attributes:
__tablename__ (str): The name of the table in postgres
pk (int): The automatic primary key
guild_id (str): The string of the guild ID the user was banned in
guild_case_id (int): The case number of the action taken
action (str): The string representation of the action taken
reason (str): The reason of the ban
banning_moderator (str): The ID of the moderator who banned
banned_member (str): The ID of the user who was banned
ban_time (datetime): The date and time of the ban
data (str): Any extra data specific to the event logged
moderator_id (str): The ID of the moderator who banned
member_id (str): The ID of the user who was banned
action_time (datetime): The date and time of the ban
until_time (datetime): The time this action will expire
"""

__tablename__ = "banlog"
__tablename__ = "modlog"
__table_args__ = (bot.db.UniqueConstraint("guild_id", "guild_case_id"),)

pk = bot.db.Column(bot.db.Integer, primary_key=True, autoincrement=True)
guild_id = bot.db.Column(bot.db.String)
reason = bot.db.Column(bot.db.String)
banning_moderator = bot.db.Column(bot.db.String)
banned_member = bot.db.Column(bot.db.String)
ban_time = bot.db.Column(bot.db.DateTime, default=datetime.datetime.utcnow)
guild_case_id = bot.db.Column(bot.db.Integer)
action = bot.db.Column(bot.db.String)
reason = bot.db.Column(bot.db.String, default="")
data = bot.db.Column(bot.db.String, default="")
moderator_id = bot.db.Column(bot.db.String, default="")
member_id = bot.db.Column(bot.db.String, default="")
action_time = bot.db.Column(
bot.db.DateTime(timezone=True),
default=lambda: datetime.datetime.now(datetime.UTC),
)
until_time = bot.db.Column(
bot.db.DateTime(timezone=True),
nullable=True,
)

class DuckUser(bot.db.Model):
"""The postgres table for ducks
Expand Down Expand Up @@ -370,7 +385,7 @@ class XP(bot.db.Model):

bot.models.Applications = Applications
bot.models.AppBans = ApplicationBans
bot.models.BanLog = BanLog
bot.models.ModLog = ModLog
bot.models.DuckUser = DuckUser
bot.models.Factoid = Factoid
bot.models.FactoidJob = FactoidJob
Expand Down
63 changes: 0 additions & 63 deletions core/moderation.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@
import discord
import munch

import configuration


async def ban_user(
guild: discord.Guild, user: discord.User, delete_seconds: int, reason: str
Expand Down Expand Up @@ -203,67 +201,6 @@ async def get_all_notes(
return user_notes


async def send_command_usage_alert(
bot_object: object,
interaction: discord.Interaction,
command: str,
guild: discord.Guild,
target: discord.Member = None,
) -> None:
"""Sends a usage alert to the protect events channel, if configured

Args:
bot_object (object): The bot object to use
interaction (discord.Interaction): The interaction that trigger the command
command (str): The string representation of the command that was run
guild (discord.Guild): The guild the command was run in
target (discord.Member): The target of the command
"""

ALERT_ICON_URL: str = (
"https://www.iconarchive.com/download/i76061/martz90/circle-addon2/warning.512.png"
)

try:
alert_channel = guild.get_channel(
int(configuration.get_config_entry(guild.id, "moderation_alert_channel"))
)
except TypeError:
alert_channel = None

if not alert_channel:
return

embed = discord.Embed(title="Command Usage Alert")

embed.description = f"**Command**\n`{command}`"
embed.add_field(
name="Channel",
value=f"{interaction.channel.name} ({interaction.channel.mention}) [Jump to context]"
f"(https://discord.com/channels/{interaction.guild.id}/{interaction.channel.id}/"
f"{discord.utils.time_snowflake(datetime.datetime.utcnow())})",
)

embed.add_field(
name="Invoking User",
value=(
f"{interaction.user.display_name} ({interaction.user.mention}, {interaction.user.id})"
),
)

if target:
embed.add_field(
name="Target",
value=f"{target.display_name} ({target.mention}, {target.id})",
)

embed.set_thumbnail(url=ALERT_ICON_URL)
embed.color = discord.Color.red()
embed.timestamp = datetime.datetime.utcnow()

await alert_channel.send(embed=embed)


async def check_if_user_banned(user: discord.User, guild: discord.Guild) -> bool:
"""Queries the given guild to find if the given discord.User is banned or not

Expand Down
48 changes: 42 additions & 6 deletions modules/moderation/automod.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,12 +156,28 @@ async def response(

total_punishment = process_automod_violations(all_punishments=all_punishments)

logged = False

if total_punishment.mute > 0:
expires_at = datetime.datetime.now(datetime.UTC) + datetime.timedelta(
seconds=total_punishment.mute_duration
)

await moderation.mute_user(
user=ctx.author,
reason=total_punishment.violation_string,
duration=datetime.timedelta(seconds=total_punishment.mute_duration),
)
await modlog.log_action(
bot=self.bot,
action_type="timeout",
guild=ctx.guild,
member=ctx.author,
reason=total_punishment.violation_string,
expires_at=expires_at,
data=f"**Violating message:** {ctx.message.clean_content[:200]}",
)
logged = True

if total_punishment.delete_message:
await ctx.message.delete()
Expand All @@ -180,6 +196,15 @@ async def response(
ctx.channel.guild.me,
total_punishment.violation_string,
)
await modlog.log_action(
bot=self.bot,
action_type="warn",
guild=ctx.guild,
member=ctx.author,
reason=total_punishment.violation_string,
data=f"**Violating message:** {ctx.message.clean_content[:200]}",
)
logged = True
max_warnings = configuration.get_config_entry(
ctx.guild.id, "moderation_max_warnings"
)
Expand Down Expand Up @@ -215,14 +240,25 @@ async def response(
),
reason=total_punishment.violation_string,
)
await modlog.log_ban(
self.bot,
ctx.author,
ctx.guild.me,
ctx.guild,
total_punishment.violation_string,
await modlog.log_action(
bot=self.bot,
action_type="ban",
guild=ctx.guild,
member=ctx.author,
reason=total_punishment.violation_string,
data=f"**Violating message:** {ctx.message.clean_content[:100]}",
)

if not logged:
await modlog.log_action(
bot=self.bot,
action_type="automod notice",
guild=ctx.guild,
member=ctx.author,
reason=total_punishment.violation_string,
data=f"**Violating message:** {ctx.message.clean_content[:200]}",
)

if total_punishment.be_silent:
return

Expand Down
59 changes: 0 additions & 59 deletions modules/moderation/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -1074,65 +1074,6 @@ async def on_raw_member_remove(
embed_message=embed,
)

@commands.Cog.listener()
async def on_voice_state_update(
self: Self,
member: discord.Member,
before: discord.VoiceState,
after: discord.VoiceState,
) -> None:
"""This logs events related to server (un)mute/deafing of members
https://discordpy.readthedocs.io/en/latest/api.html#discord.on_voice_state_update

Args:
member (discord.Member): The member who had their account changed
before (discord.VoiceState): The previous state of the members voice account
after (discord.VoiceState): The new state of the members voice account
"""
# We need to handle server deafen and server mute
if before.mute != after.mute:
embed = EventEmbed(
title=f"Member Server {'un' if before.mute else ''}muted",
description="",
)

embed.setEventAuthor(member)
embed.addMemberField("Member", member)

if after.channel:
embed.addChannelField("Current Channel", after.channel)

console_message = f"{embed.title}: {member.name} ({member.id})"

await self.send_event_log(
guild=member.guild,
log_location="member",
string_message=console_message,
embed_message=embed,
channel_location=after.channel,
)

if before.deaf != after.deaf:
embed = EventEmbed(
title=f"Member Server {'un' if before.deaf else ''}deafened",
description="",
)

embed.setEventAuthor(member)
embed.addMemberField("Member", member)

if after.channel:
embed.addChannelField("Current Channel", after.channel)

console_message = f"{embed.title}: {member.name} ({member.id})"

await self.send_event_log(
guild=member.guild,
log_location="member",
string_message=console_message,
embed_message=embed,
)

@commands.Cog.listener()
async def on_user_update(
self: Self, before: discord.User, after: discord.User
Expand Down
38 changes: 10 additions & 28 deletions modules/moderation/honeypot.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,13 @@

from __future__ import annotations

import datetime
from typing import TYPE_CHECKING, Self

import discord
from discord.ext import commands

import configuration
from core import cogs
from modules.moderation import modlog

if TYPE_CHECKING:
import bot
Expand Down Expand Up @@ -61,35 +60,18 @@ async def response(
# This should be replaced with a guild wide purge when discord.py can be updated.
await ctx.author.ban(delete_message_days=1, reason="triggered honeypot")
await ctx.guild.unban(ctx.author, reason="triggered honeypot")
# Send an alert in the alert channel, if its configured
try:
alert_channel = ctx.guild.get_channel(
configuration.get_config_entry(ctx.guild.id, "moderation_alert_channel")
)
except TypeError:
alert_channel = None

if not alert_channel:
return

embed = discord.Embed(title="Honeypot triggered")
embed.add_field(
name="Offending member",
value=f"{ctx.author.global_name} ({ctx.author.name})",
)
embed.add_field(name="Message text", value=ctx.message.clean_content[:500])
embed.add_field(
name="Number of attachments", value=len(ctx.message.attachments)
)
embed.color = discord.Color.red()
embed.timestamp = datetime.datetime.utcnow()

embed.set_footer(
text=f"Author ID: {ctx.author.id} • Message ID: {ctx.message.id}"
await modlog.log_action(
bot=self.bot,
action_type="Honeypot",
guild=ctx.guild,
member=ctx.author,
data=(
f"**Content:** {ctx.message.clean_content[:500]}\n"
f"**Attachments:** {len(ctx.message.attachments)}"
),
)

await alert_channel.send(embed=embed)

# Get only message in the channel and edit the description
# Just in case, we make sure we pick the first message in the channel, as a foolproof method
history = ctx.channel.history(oldest_first=True, limit=1)
Expand Down
Loading
Loading