aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--bot/cogs/events.py11
-rw-r--r--bot/cogs/filtering.py42
-rw-r--r--bot/cogs/moderation.py535
-rw-r--r--bot/cogs/modlog.py23
-rw-r--r--bot/constants.py4
-rw-r--r--config-default.yml4
6 files changed, 380 insertions, 239 deletions
diff --git a/bot/cogs/events.py b/bot/cogs/events.py
index f0baecd4b..8dac83d9b 100644
--- a/bot/cogs/events.py
+++ b/bot/cogs/events.py
@@ -1,4 +1,5 @@
import logging
+from functools import partial
from discord import Colour, Embed, Member, Object
from discord.ext.commands import (
@@ -7,7 +8,6 @@ from discord.ext.commands import (
Context, NoPrivateMessage, UserInputError
)
-from bot.cogs.modlog import ModLog
from bot.constants import (
Channels, Colours, DEBUG_MODE,
Guild, Icons, Keys,
@@ -28,8 +28,9 @@ class Events:
self.headers = {"X-API-KEY": Keys.site_api}
@property
- def mod_log(self) -> ModLog:
- return self.bot.get_cog("ModLog")
+ def send_log(self) -> partial:
+ cog = self.bot.get_cog("ModLog")
+ return partial(cog.send_log_message, channel_id=Channels.userlog)
async def send_updated_users(self, *users, replace_all=False):
users = list(filter(lambda user: str(Roles.verified) in user["roles"], users))
@@ -249,7 +250,7 @@ class Events:
except Exception as e:
log.exception("Failed to persist roles")
- await self.mod_log.send_log_message(
+ await self.send_log(
Icons.crown_red, Colour(Colours.soft_red), "Failed to persist roles",
f"```py\n{e}\n```",
member.avatar_url_as(static_format="png")
@@ -290,7 +291,7 @@ class Events:
reason="Roles restored"
)
- await self.mod_log.send_log_message(
+ await self.send_log(
Icons.crown_blurple, Colour.blurple(), "Roles restored",
f"Restored {len(new_roles)} roles",
member.avatar_url_as(static_format="png")
diff --git a/bot/cogs/filtering.py b/bot/cogs/filtering.py
index 570d6549f..6b4469ceb 100644
--- a/bot/cogs/filtering.py
+++ b/bot/cogs/filtering.py
@@ -1,7 +1,9 @@
import logging
import re
+from typing import Optional
import discord.errors
+from dateutil.relativedelta import relativedelta
from discord import Colour, DMChannel, Member, Message, TextChannel
from discord.ext.commands import Bot
@@ -73,18 +75,11 @@ class Filtering:
f"Your URL has been removed because it matched a blacklisted domain. {_staff_mistake_str}"
)
},
- "filter_rich_embeds": {
- "enabled": Filter.filter_rich_embeds,
+ "watch_rich_embeds": {
+ "enabled": Filter.watch_rich_embeds,
"function": self._has_rich_embed,
- "type": "filter",
+ "type": "watchlist",
"content_only": False,
- "user_notification": Filter.notify_user_rich_embeds,
- "notification_msg": (
- "Your post has been removed because it contained a rich embed. "
- "This indicates that you're either using an unofficial discord client or are using a self-bot, "
- f"both of which violate Discord's Terms of Service. {_staff_mistake_str}\n\n"
- "Please don't use a self-bot or an unofficial Discord client on our server."
- )
},
"watch_words": {
"enabled": Filter.watch_words,
@@ -107,10 +102,14 @@ class Filtering:
async def on_message(self, msg: Message):
await self._filter_message(msg)
- async def on_message_edit(self, _: Message, after: Message):
- await self._filter_message(after)
+ async def on_message_edit(self, before: Message, after: Message):
+ if not before.edited_at:
+ delta = relativedelta(after.edited_at, before.created_at).microseconds
+ else:
+ delta = None
+ await self._filter_message(after, delta)
- async def _filter_message(self, msg: Message):
+ async def _filter_message(self, msg: Message, delta: Optional[int] = None):
"""
Whenever a message is sent or edited,
run it through our filters to see if it
@@ -141,6 +140,13 @@ class Filtering:
for filter_name, _filter in self.filters.items():
# Is this specific filter enabled in the config?
if _filter["enabled"]:
+ # Double trigger check for the embeds filter
+ if filter_name == "watch_rich_embeds":
+ # If the edit delta is less than 0.001 seconds, then we're probably dealing
+ # with a double filter trigger.
+ if delta is not None and delta < 100:
+ return
+
# Does the filter only need the message content or the full message?
if _filter["content_only"]:
triggered = await _filter["function"](msg.content)
@@ -183,7 +189,7 @@ class Filtering:
log.debug(message)
- additional_embeds = msg.embeds if filter_name == "filter_rich_embeds" else None
+ additional_embeds = msg.embeds if filter_name == "watch_rich_embeds" else None
# Send pretty mod log embed to mod-alerts
await self.mod_log.send_log_message(
@@ -311,11 +317,13 @@ class Filtering:
@staticmethod
async def _has_rich_embed(msg: Message):
"""
- Returns True if any of the embeds in the message
- are of type 'rich', returns False otherwise
+ Returns True if any of the embeds in the message are of type 'rich', but are not twitter
+ embeds. Returns False otherwise.
"""
if msg.embeds:
- return any(embed.type == "rich" for embed in msg.embeds)
+ for embed in msg.embeds:
+ if embed.type == "rich" and (not embed.url or "twitter.com" not in embed.url):
+ return True
return False
async def notify_member(self, filtered_member: Member, reason: str, channel: TextChannel):
diff --git a/bot/cogs/moderation.py b/bot/cogs/moderation.py
index 64c76ae8a..6b90d43ab 100644
--- a/bot/cogs/moderation.py
+++ b/bot/cogs/moderation.py
@@ -45,7 +45,7 @@ def proxy_user(user_id: str) -> Object:
class Moderation(Scheduler):
"""
- Rowboat replacement moderation tools.
+ Server moderation tools.
"""
def __init__(self, bot: Bot):
@@ -66,32 +66,32 @@ class Moderation(Scheduler):
headers=self.headers
)
infraction_list = await response.json()
- loop = asyncio.get_event_loop()
for infraction_object in infraction_list:
if infraction_object["expires_at"] is not None:
- self.schedule_task(loop, infraction_object["id"], infraction_object)
+ self.schedule_task(self.bot.loop, infraction_object["id"], infraction_object)
# region: Permanent infractions
@with_role(*MODERATION_ROLES)
- @command(name="warn")
+ @command()
async def warn(self, ctx: Context, user: Union[User, proxy_user], *, reason: str = None):
"""
Create a warning infraction in the database for a user.
- :param user: accepts user mention, ID, etc.
- :param reason: The reason for the warning.
+
+ **`user`:** Accepts user mention, ID, etc.
+ **`reason`:** The reason for the warning.
"""
+ response_object = await post_infraction(ctx, user, type="warning", reason=reason)
+ if response_object is None:
+ return
+
notified = await self.notify_infraction(
user=user,
infr_type="Warning",
reason=reason
)
- response_object = await post_infraction(ctx, user, type="warning", reason=reason)
- if response_object is None:
- return
-
dm_result = ":incoming_envelope: " if notified else ""
action = f"{dm_result}:ok_hand: warned {user.mention}"
@@ -100,10 +100,13 @@ class Moderation(Scheduler):
else:
await ctx.send(f"{action} ({reason}).")
- if not notified:
- await self.log_notify_failure(user, ctx.author, "warning")
+ if notified:
+ dm_status = "Sent"
+ log_content = None
+ else:
+ dm_status = "**Failed**"
+ log_content = ctx.author.mention
- # Send a message to the mod log
await self.mod_log.send_log_message(
icon_url=Icons.user_warn,
colour=Colour(Colours.soft_red),
@@ -111,18 +114,22 @@ class Moderation(Scheduler):
thumbnail=user.avatar_url_as(static_format="png"),
text=textwrap.dedent(f"""
Member: {user.mention} (`{user.id}`)
- Actor: {ctx.message.author}
+ Actor: {ctx.author}
+ DM: {dm_status}
Reason: {reason}
- """)
+ """),
+ content=log_content,
+ footer=f"ID {response_object['infraction']['id']}"
)
@with_role(*MODERATION_ROLES)
- @command(name="kick")
+ @command()
async def kick(self, ctx: Context, user: Member, *, reason: str = None):
"""
Kicks a user.
- :param user: accepts user mention, ID, etc.
- :param reason: The reason for the kick.
+
+ **`user`:** Accepts user mention, ID, etc.
+ **`reason`:** The reason for the kick.
"""
if not await self.respect_role_hierarchy(ctx, user, 'kick'):
@@ -130,18 +137,23 @@ class Moderation(Scheduler):
# Warning is sent to ctx by the helper method
return
+ response_object = await post_infraction(ctx, user, type="kick", reason=reason)
+ if response_object is None:
+ return
+
notified = await self.notify_infraction(
user=user,
infr_type="Kick",
reason=reason
)
- response_object = await post_infraction(ctx, user, type="kick", reason=reason)
- if response_object is None:
- return
-
self.mod_log.ignore(Event.member_remove, user.id)
- await user.kick(reason=reason)
+
+ try:
+ await user.kick(reason=reason)
+ action_result = True
+ except Forbidden:
+ action_result = False
dm_result = ":incoming_envelope: " if notified else ""
action = f"{dm_result}:ok_hand: kicked {user.mention}"
@@ -151,29 +163,33 @@ class Moderation(Scheduler):
else:
await ctx.send(f"{action} ({reason}).")
- if not notified:
- await self.log_notify_failure(user, ctx.author, "kick")
+ dm_status = "Sent" if notified else "**Failed**"
+ title = "Member kicked" if action_result else "Member kicked (Failed)"
+ log_content = None if all((notified, action_result)) else ctx.author.mention
- # Send a log message to the mod log
await self.mod_log.send_log_message(
icon_url=Icons.sign_out,
colour=Colour(Colours.soft_red),
- title="Member kicked",
+ title=title,
thumbnail=user.avatar_url_as(static_format="png"),
text=textwrap.dedent(f"""
Member: {user.mention} (`{user.id}`)
Actor: {ctx.message.author}
+ DM: {dm_status}
Reason: {reason}
- """)
+ """),
+ content=log_content,
+ footer=f"ID {response_object['infraction']['id']}"
)
@with_role(*MODERATION_ROLES)
- @command(name="ban")
+ @command()
async def ban(self, ctx: Context, user: Union[User, proxy_user], *, reason: str = None):
"""
Create a permanent ban infraction in the database for a user.
- :param user: Accepts user mention, ID, etc.
- :param reason: The reason for the ban.
+
+ **`user`:** Accepts user mention, ID, etc.
+ **`reason`:** The reason for the ban.
"""
member = ctx.guild.get_member(user.id)
@@ -182,6 +198,10 @@ class Moderation(Scheduler):
# Warning is sent to ctx by the helper method
return
+ response_object = await post_infraction(ctx, user, type="ban", reason=reason)
+ if response_object is None:
+ return
+
notified = await self.notify_infraction(
user=user,
infr_type="Ban",
@@ -189,13 +209,14 @@ class Moderation(Scheduler):
reason=reason
)
- response_object = await post_infraction(ctx, user, type="ban", reason=reason)
- if response_object is None:
- return
-
self.mod_log.ignore(Event.member_ban, user.id)
self.mod_log.ignore(Event.member_remove, user.id)
- await ctx.guild.ban(user, reason=reason, delete_message_days=0)
+
+ try:
+ await ctx.guild.ban(user, reason=reason, delete_message_days=0)
+ action_result = True
+ except Forbidden:
+ action_result = False
dm_result = ":incoming_envelope: " if notified else ""
action = f"{dm_result}:ok_hand: permanently banned {user.mention}"
@@ -205,46 +226,51 @@ class Moderation(Scheduler):
else:
await ctx.send(f"{action} ({reason}).")
- if not notified:
- await self.log_notify_failure(user, ctx.author, "ban")
+ dm_status = "Sent" if notified else "**Failed**"
+ log_content = None if all((notified, action_result)) else ctx.author.mention
+ title = "Member permanently banned"
+ if not action_result:
+ title += " (Failed)"
- # Send a log message to the mod log
await self.mod_log.send_log_message(
icon_url=Icons.user_ban,
colour=Colour(Colours.soft_red),
- title="Member permanently banned",
+ title=title,
thumbnail=user.avatar_url_as(static_format="png"),
text=textwrap.dedent(f"""
Member: {user.mention} (`{user.id}`)
Actor: {ctx.message.author}
+ DM: {dm_status}
Reason: {reason}
- """)
+ """),
+ content=log_content,
+ footer=f"ID {response_object['infraction']['id']}"
)
@with_role(*MODERATION_ROLES)
- @command(name="mute")
+ @command()
async def mute(self, ctx: Context, user: Member, *, reason: str = None):
"""
Create a permanent mute infraction in the database for a user.
- :param user: Accepts user mention, ID, etc.
- :param reason: The reason for the mute.
- """
- notified = await self.notify_infraction(
- user=user,
- infr_type="Mute",
- duration="Permanent",
- reason=reason
- )
+ **`user`:** Accepts user mention, ID, etc.
+ **`reason`:** The reason for the mute.
+ """
response_object = await post_infraction(ctx, user, type="mute", reason=reason)
if response_object is None:
return
- # add the mute role
self.mod_log.ignore(Event.member_update, user.id)
await user.add_roles(self._muted_role, reason=reason)
+ notified = await self.notify_infraction(
+ user=user,
+ infr_type="Mute",
+ duration="Permanent",
+ reason=reason
+ )
+
dm_result = ":incoming_envelope: " if notified else ""
action = f"{dm_result}:ok_hand: permanently muted {user.mention}"
@@ -253,10 +279,13 @@ class Moderation(Scheduler):
else:
await ctx.send(f"{action} ({reason}).")
- if not notified:
- await self.log_notify_failure(user, ctx.author, "mute")
+ if notified:
+ dm_status = "Sent"
+ log_content = None
+ else:
+ dm_status = "**Failed**"
+ log_content = ctx.author.mention
- # Send a log message to the mod log
await self.mod_log.send_log_message(
icon_url=Icons.user_mute,
colour=Colour(Colours.soft_red),
@@ -265,42 +294,47 @@ class Moderation(Scheduler):
text=textwrap.dedent(f"""
Member: {user.mention} (`{user.id}`)
Actor: {ctx.message.author}
+ DM: {dm_status}
Reason: {reason}
- """)
+ """),
+ content=log_content,
+ footer=f"ID {response_object['infraction']['id']}"
)
# endregion
# region: Temporary infractions
@with_role(*MODERATION_ROLES)
- @command(name="tempmute")
+ @command()
async def tempmute(self, ctx: Context, user: Member, duration: str, *, reason: str = None):
"""
Create a temporary mute infraction in the database for a user.
- :param user: Accepts user mention, ID, etc.
- :param duration: The duration for the temporary mute infraction
- :param reason: The reason for the temporary mute.
+
+ **`user`:** Accepts user mention, ID, etc.
+ **`duration`:** The duration for the temporary mute infraction
+ **`reason`:** The reason for the temporary mute.
"""
- notified = await self.notify_infraction(
- user=user,
- infr_type="Mute",
- duration=duration,
- reason=reason
+ response_object = await post_infraction(
+ ctx, user, type="mute", reason=reason, duration=duration
)
-
- response_object = await post_infraction(ctx, user, type="mute", reason=reason, duration=duration)
if response_object is None:
return
self.mod_log.ignore(Event.member_update, user.id)
await user.add_roles(self._muted_role, reason=reason)
+ notified = await self.notify_infraction(
+ user=user,
+ infr_type="Mute",
+ duration=duration,
+ reason=reason
+ )
+
infraction_object = response_object["infraction"]
infraction_expiration = infraction_object["expires_at"]
- loop = asyncio.get_event_loop()
- self.schedule_task(loop, infraction_object["id"], infraction_object)
+ self.schedule_task(ctx.bot.loop, infraction_object["id"], infraction_object)
dm_result = ":incoming_envelope: " if notified else ""
action = f"{dm_result}:ok_hand: muted {user.mention} until {infraction_expiration}"
@@ -310,10 +344,13 @@ class Moderation(Scheduler):
else:
await ctx.send(f"{action} ({reason}).")
- if not notified:
- await self.log_notify_failure(user, ctx.author, "mute")
+ if notified:
+ dm_status = "Sent"
+ log_content = None
+ else:
+ dm_status = "**Failed**"
+ log_content = ctx.author.mention
- # Send a log message to the mod log
await self.mod_log.send_log_message(
icon_url=Icons.user_mute,
colour=Colour(Colours.soft_red),
@@ -322,20 +359,26 @@ class Moderation(Scheduler):
text=textwrap.dedent(f"""
Member: {user.mention} (`{user.id}`)
Actor: {ctx.message.author}
+ DM: {dm_status}
Reason: {reason}
Duration: {duration}
Expires: {infraction_expiration}
- """)
+ """),
+ content=log_content,
+ footer=f"ID {response_object['infraction']['id']}"
)
@with_role(*MODERATION_ROLES)
- @command(name="tempban")
- async def tempban(self, ctx: Context, user: Union[User, proxy_user], duration: str, *, reason: str = None):
+ @command()
+ async def tempban(
+ self, ctx: Context, user: Union[User, proxy_user], duration: str, *, reason: str = None
+ ):
"""
Create a temporary ban infraction in the database for a user.
- :param user: Accepts user mention, ID, etc.
- :param duration: The duration for the temporary ban infraction
- :param reason: The reason for the temporary ban.
+
+ **`user`:** Accepts user mention, ID, etc.
+ **`duration`:** The duration for the temporary ban infraction
+ **`reason`:** The reason for the temporary ban.
"""
member = ctx.guild.get_member(user.id)
@@ -344,6 +387,12 @@ class Moderation(Scheduler):
# Warning is sent to ctx by the helper method
return
+ response_object = await post_infraction(
+ ctx, user, type="ban", reason=reason, duration=duration
+ )
+ if response_object is None:
+ return
+
notified = await self.notify_infraction(
user=user,
infr_type="Ban",
@@ -351,20 +400,19 @@ class Moderation(Scheduler):
reason=reason
)
- response_object = await post_infraction(ctx, user, type="ban", reason=reason, duration=duration)
- if response_object is None:
- return
-
self.mod_log.ignore(Event.member_ban, user.id)
self.mod_log.ignore(Event.member_remove, user.id)
- guild: Guild = ctx.guild
- await guild.ban(user, reason=reason, delete_message_days=0)
+
+ try:
+ await ctx.guild.ban(user, reason=reason, delete_message_days=0)
+ action_result = True
+ except Forbidden:
+ action_result = False
infraction_object = response_object["infraction"]
infraction_expiration = infraction_object["expires_at"]
- loop = asyncio.get_event_loop()
- self.schedule_task(loop, infraction_object["id"], infraction_object)
+ self.schedule_task(ctx.bot.loop, infraction_object["id"], infraction_object)
dm_result = ":incoming_envelope: " if notified else ""
action = f"{dm_result}:ok_hand: banned {user.mention} until {infraction_expiration}"
@@ -374,67 +422,74 @@ class Moderation(Scheduler):
else:
await ctx.send(f"{action} ({reason}).")
- if not notified:
- await self.log_notify_failure(user, ctx.author, "ban")
+ dm_status = "Sent" if notified else "**Failed**"
+ log_content = None if all((notified, action_result)) else ctx.author.mention
+ title = "Member temporarily banned"
+ if not action_result:
+ title += " (Failed)"
- # Send a log message to the mod log
await self.mod_log.send_log_message(
icon_url=Icons.user_ban,
colour=Colour(Colours.soft_red),
thumbnail=user.avatar_url_as(static_format="png"),
- title="Member temporarily banned",
+ title=title,
text=textwrap.dedent(f"""
Member: {user.mention} (`{user.id}`)
Actor: {ctx.message.author}
+ DM: {dm_status}
Reason: {reason}
Duration: {duration}
Expires: {infraction_expiration}
- """)
+ """),
+ content=log_content,
+ footer=f"ID {response_object['infraction']['id']}"
)
# endregion
# region: Permanent shadow infractions
@with_role(*MODERATION_ROLES)
- @command(name="shadow_warn", hidden=True, aliases=['shadowwarn', 'swarn', 'note'])
- async def shadow_warn(self, ctx: Context, user: Union[User, proxy_user], *, reason: str = None):
+ @command(hidden=True, aliases=['shadowwarn', 'swarn', 'shadow_warn'])
+ async def note(self, ctx: Context, user: Union[User, proxy_user], *, reason: str = None):
"""
- Create a warning infraction in the database for a user.
- :param user: accepts user mention, ID, etc.
- :param reason: The reason for the warning.
+ Create a private infraction note in the database for a user.
+
+ **`user`:** accepts user mention, ID, etc.
+ **`reason`:** The reason for the warning.
"""
- response_object = await post_infraction(ctx, user, type="warning", reason=reason, hidden=True)
+ response_object = await post_infraction(
+ ctx, user, type="warning", reason=reason, hidden=True
+ )
if response_object is None:
return
if reason is None:
- result_message = f":ok_hand: note added for {user.mention}."
+ await ctx.send(f":ok_hand: note added for {user.mention}.")
else:
- result_message = f":ok_hand: note added for {user.mention} ({reason})."
+ await ctx.send(f":ok_hand: note added for {user.mention} ({reason}).")
- await ctx.send(result_message)
-
- # Send a message to the mod log
await self.mod_log.send_log_message(
icon_url=Icons.user_warn,
colour=Colour(Colours.soft_red),
- title="Member shadow warned",
+ title="Member note added",
thumbnail=user.avatar_url_as(static_format="png"),
text=textwrap.dedent(f"""
Member: {user.mention} (`{user.id}`)
Actor: {ctx.message.author}
Reason: {reason}
- """)
+ """),
+ footer=f"ID {response_object['infraction']['id']}"
)
@with_role(*MODERATION_ROLES)
- @command(name="shadow_kick", hidden=True, aliases=['shadowkick', 'skick'])
+ @command(hidden=True, aliases=['shadowkick', 'skick'])
async def shadow_kick(self, ctx: Context, user: Member, *, reason: str = None):
"""
Kicks a user.
- :param user: accepts user mention, ID, etc.
- :param reason: The reason for the kick.
+
+ **`user`:** accepts user mention, ID, etc.
+ **`reason`:** The reason for the kick.
"""
if not await self.respect_role_hierarchy(ctx, user, 'shadowkick'):
@@ -447,35 +502,47 @@ class Moderation(Scheduler):
return
self.mod_log.ignore(Event.member_remove, user.id)
- await user.kick(reason=reason)
+
+ try:
+ await user.kick(reason=reason)
+ action_result = True
+ except Forbidden:
+ action_result = False
if reason is None:
- result_message = f":ok_hand: kicked {user.mention}."
+ await ctx.send(f":ok_hand: kicked {user.mention}.")
else:
- result_message = f":ok_hand: kicked {user.mention} ({reason})."
+ await ctx.send(f":ok_hand: kicked {user.mention} ({reason}).")
- await ctx.send(result_message)
+ title = "Member shadow kicked"
+ if action_result:
+ log_content = None
+ else:
+ log_content = ctx.author.mention
+ title += " (Failed)"
- # Send a log message to the mod log
await self.mod_log.send_log_message(
icon_url=Icons.sign_out,
colour=Colour(Colours.soft_red),
- title="Member shadow kicked",
+ title=title,
thumbnail=user.avatar_url_as(static_format="png"),
text=textwrap.dedent(f"""
Member: {user.mention} (`{user.id}`)
Actor: {ctx.message.author}
Reason: {reason}
- """)
+ """),
+ content=log_content,
+ footer=f"ID {response_object['infraction']['id']}"
)
@with_role(*MODERATION_ROLES)
- @command(name="shadow_ban", hidden=True, aliases=['shadowban', 'sban'])
+ @command(hidden=True, aliases=['shadowban', 'sban'])
async def shadow_ban(self, ctx: Context, user: Union[User, proxy_user], *, reason: str = None):
"""
Create a permanent ban infraction in the database for a user.
- :param user: Accepts user mention, ID, etc.
- :param reason: The reason for the ban.
+
+ **`user`:** Accepts user mention, ID, etc.
+ **`reason`:** The reason for the ban.
"""
member = ctx.guild.get_member(user.id)
@@ -490,53 +557,61 @@ class Moderation(Scheduler):
self.mod_log.ignore(Event.member_ban, user.id)
self.mod_log.ignore(Event.member_remove, user.id)
- await ctx.guild.ban(user, reason=reason, delete_message_days=0)
+
+ try:
+ await ctx.guild.ban(user, reason=reason, delete_message_days=0)
+ action_result = True
+ except Forbidden:
+ action_result = False
if reason is None:
- result_message = f":ok_hand: permanently banned {user.mention}."
+ await ctx.send(f":ok_hand: permanently banned {user.mention}.")
else:
- result_message = f":ok_hand: permanently banned {user.mention} ({reason})."
+ await ctx.send(f":ok_hand: permanently banned {user.mention} ({reason}).")
- await ctx.send(result_message)
+ title = "Member permanently banned"
+ if action_result:
+ log_content = None
+ else:
+ log_content = ctx.author.mention
+ title += " (Failed)"
- # Send a log message to the mod log
await self.mod_log.send_log_message(
icon_url=Icons.user_ban,
colour=Colour(Colours.soft_red),
- title="Member permanently banned",
+ title=title,
thumbnail=user.avatar_url_as(static_format="png"),
text=textwrap.dedent(f"""
Member: {user.mention} (`{user.id}`)
Actor: {ctx.message.author}
Reason: {reason}
- """)
+ """),
+ content=log_content,
+ footer=f"ID {response_object['infraction']['id']}"
)
@with_role(*MODERATION_ROLES)
- @command(name="shadow_mute", hidden=True, aliases=['shadowmute', 'smute'])
+ @command(hidden=True, aliases=['shadowmute', 'smute'])
async def shadow_mute(self, ctx: Context, user: Member, *, reason: str = None):
"""
Create a permanent mute infraction in the database for a user.
- :param user: Accepts user mention, ID, etc.
- :param reason: The reason for the mute.
+
+ **`user`:** Accepts user mention, ID, etc.
+ **`reason`:** The reason for the mute.
"""
response_object = await post_infraction(ctx, user, type="mute", reason=reason, hidden=True)
if response_object is None:
return
- # add the mute role
self.mod_log.ignore(Event.member_update, user.id)
await user.add_roles(self._muted_role, reason=reason)
if reason is None:
- result_message = f":ok_hand: permanently muted {user.mention}."
+ await ctx.send(f":ok_hand: permanently muted {user.mention}.")
else:
- result_message = f":ok_hand: permanently muted {user.mention} ({reason})."
+ await ctx.send(f":ok_hand: permanently muted {user.mention} ({reason}).")
- await ctx.send(result_message)
-
- # Send a log message to the mod log
await self.mod_log.send_log_message(
icon_url=Icons.user_mute,
colour=Colour(Colours.soft_red),
@@ -546,23 +621,29 @@ class Moderation(Scheduler):
Member: {user.mention} (`{user.id}`)
Actor: {ctx.message.author}
Reason: {reason}
- """)
+ """),
+ footer=f"ID {response_object['infraction']['id']}"
)
# endregion
# region: Temporary shadow infractions
@with_role(*MODERATION_ROLES)
- @command(name="shadow_tempmute", hidden=True, aliases=["shadowtempmute, stempmute"])
- async def shadow_tempmute(self, ctx: Context, user: Member, duration: str, *, reason: str = None):
+ @command(hidden=True, aliases=["shadowtempmute, stempmute"])
+ async def shadow_tempmute(
+ self, ctx: Context, user: Member, duration: str, *, reason: str = None
+ ):
"""
Create a temporary mute infraction in the database for a user.
- :param user: Accepts user mention, ID, etc.
- :param duration: The duration for the temporary mute infraction
- :param reason: The reason for the temporary mute.
+
+ **`user`:** Accepts user mention, ID, etc.
+ **`duration`:** The duration for the temporary mute infraction
+ **`reason`:** The reason for the temporary mute.
"""
- response_object = await post_infraction(ctx, user, type="mute", reason=reason, duration=duration, hidden=True)
+ response_object = await post_infraction(
+ ctx, user, type="mute", reason=reason, duration=duration, hidden=True
+ )
if response_object is None:
return
@@ -572,17 +653,15 @@ class Moderation(Scheduler):
infraction_object = response_object["infraction"]
infraction_expiration = infraction_object["expires_at"]
- loop = asyncio.get_event_loop()
- self.schedule_expiration(loop, infraction_object)
+ self.schedule_expiration(ctx.bot.loop, infraction_object)
if reason is None:
- result_message = f":ok_hand: muted {user.mention} until {infraction_expiration}."
+ await ctx.send(f":ok_hand: muted {user.mention} until {infraction_expiration}.")
else:
- result_message = f":ok_hand: muted {user.mention} until {infraction_expiration} ({reason})."
-
- await ctx.send(result_message)
+ await ctx.send(
+ f":ok_hand: muted {user.mention} until {infraction_expiration} ({reason})."
+ )
- # Send a log message to the mod log
await self.mod_log.send_log_message(
icon_url=Icons.user_mute,
colour=Colour(Colours.soft_red),
@@ -594,19 +673,21 @@ class Moderation(Scheduler):
Reason: {reason}
Duration: {duration}
Expires: {infraction_expiration}
- """)
+ """),
+ footer=f"ID {response_object['infraction']['id']}"
)
@with_role(*MODERATION_ROLES)
- @command(name="shadow_tempban", hidden=True, aliases=["shadowtempban, stempban"])
+ @command(hidden=True, aliases=["shadowtempban, stempban"])
async def shadow_tempban(
self, ctx: Context, user: Union[User, proxy_user], duration: str, *, reason: str = None
):
"""
Create a temporary ban infraction in the database for a user.
- :param user: Accepts user mention, ID, etc.
- :param duration: The duration for the temporary ban infraction
- :param reason: The reason for the temporary ban.
+
+ **`user`:** Accepts user mention, ID, etc.
+ **`duration`:** The duration for the temporary ban infraction
+ **`reason`:** The reason for the temporary ban.
"""
member = ctx.guild.get_member(user.id)
@@ -615,52 +696,67 @@ class Moderation(Scheduler):
# Warning is sent to ctx by the helper method
return
- response_object = await post_infraction(ctx, user, type="ban", reason=reason, duration=duration, hidden=True)
+ response_object = await post_infraction(
+ ctx, user, type="ban", reason=reason, duration=duration, hidden=True
+ )
if response_object is None:
return
self.mod_log.ignore(Event.member_ban, user.id)
self.mod_log.ignore(Event.member_remove, user.id)
- guild: Guild = ctx.guild
- await guild.ban(user, reason=reason, delete_message_days=0)
+
+ try:
+ await ctx.guild.ban(user, reason=reason, delete_message_days=0)
+ action_result = True
+ except Forbidden:
+ action_result = False
infraction_object = response_object["infraction"]
infraction_expiration = infraction_object["expires_at"]
- loop = asyncio.get_event_loop()
- self.schedule_expiration(loop, infraction_object)
+ self.schedule_expiration(ctx.bot.loop, infraction_object)
if reason is None:
- result_message = f":ok_hand: banned {user.mention} until {infraction_expiration}."
+ await ctx.send(f":ok_hand: banned {user.mention} until {infraction_expiration}.")
else:
- result_message = f":ok_hand: banned {user.mention} until {infraction_expiration} ({reason})."
+ await ctx.send(
+ f":ok_hand: banned {user.mention} until {infraction_expiration} ({reason})."
+ )
- await ctx.send(result_message)
+ title = "Member temporarily banned"
+ if action_result:
+ log_content = None
+ else:
+ log_content = ctx.author.mention
+ title += " (Failed)"
# Send a log message to the mod log
await self.mod_log.send_log_message(
icon_url=Icons.user_ban,
colour=Colour(Colours.soft_red),
thumbnail=user.avatar_url_as(static_format="png"),
- title="Member temporarily banned",
+ title=title,
text=textwrap.dedent(f"""
Member: {user.mention} (`{user.id}`)
Actor: {ctx.message.author}
Reason: {reason}
Duration: {duration}
Expires: {infraction_expiration}
- """)
+ """),
+ content=log_content,
+ footer=f"ID {response_object['infraction']['id']}"
)
# endregion
# region: Remove infractions (un- commands)
@with_role(*MODERATION_ROLES)
- @command(name="unmute")
+ @command()
async def unmute(self, ctx: Context, user: Member):
"""
Deactivates the active mute infraction for a user.
- :param user: Accepts user mention, ID, etc.
+
+ **`user`:** Accepts user mention, ID, etc.
"""
try:
@@ -672,16 +768,20 @@ class Moderation(Scheduler):
),
headers=self.headers
)
+
response_object = await response.json()
if "error_code" in response_object:
- await ctx.send(f":x: There was an error removing the infraction: {response_object['error_message']}")
- return
+ return await ctx.send(
+ ":x: There was an error removing the infraction: "
+ f"{response_object['error_message']}"
+ )
infraction_object = response_object["infraction"]
if infraction_object is None:
# no active infraction
- await ctx.send(f":x: There is no active mute infraction for user {user.mention}.")
- return
+ return await ctx.send(
+ f":x: There is no active mute infraction for user {user.mention}."
+ )
await self._deactivate_infraction(infraction_object)
if infraction_object["expires_at"] is not None:
@@ -694,11 +794,16 @@ class Moderation(Scheduler):
icon_url=Icons.user_unmute
)
- dm_result = ":incoming_envelope: " if notified else ""
- await ctx.send(f"{dm_result}:ok_hand: Un-muted {user.mention}.")
+ if notified:
+ dm_status = "Sent"
+ dm_emoji = ":incoming_envelope: "
+ log_content = None
+ else:
+ dm_status = "**Failed**"
+ dm_emoji = ""
+ log_content = ctx.author.mention
- if not notified:
- await self.log_notify_failure(user, ctx.author, "unmute")
+ await ctx.send(f"{dm_emoji}:ok_hand: Un-muted {user.mention}.")
# Send a log message to the mod log
await self.mod_log.send_log_message(
@@ -710,19 +815,23 @@ class Moderation(Scheduler):
Member: {user.mention} (`{user.id}`)
Actor: {ctx.message.author}
Intended expiry: {infraction_object['expires_at']}
- """)
+ DM: {dm_status}
+ """),
+ footer=infraction_object["id"],
+ content=log_content
)
- except Exception:
- log.exception("There was an error removing an infraction.")
+
+ except Exception as e:
+ log.exception("There was an error removing an infraction.", exc_info=e)
await ctx.send(":x: There was an error removing the infraction.")
- return
@with_role(*MODERATION_ROLES)
- @command(name="unban")
+ @command()
async def unban(self, ctx: Context, user: Union[User, proxy_user]):
"""
Deactivates the active ban infraction for a user.
- :param user: Accepts user mention, ID, etc.
+
+ **`user`:** Accepts user mention, ID, etc.
"""
try:
@@ -736,14 +845,17 @@ class Moderation(Scheduler):
)
response_object = await response.json()
if "error_code" in response_object:
- await ctx.send(f":x: There was an error removing the infraction: {response_object['error_message']}")
- return
+ return await ctx.send(
+ ":x: There was an error removing the infraction: "
+ f"{response_object['error_message']}"
+ )
infraction_object = response_object["infraction"]
if infraction_object is None:
# no active infraction
- await ctx.send(f":x: There is no active ban infraction for user {user.mention}.")
- return
+ return await ctx.send(
+ f":x: There is no active ban infraction for user {user.mention}."
+ )
await self._deactivate_infraction(infraction_object)
if infraction_object["expires_at"] is not None:
@@ -766,7 +878,6 @@ class Moderation(Scheduler):
except Exception:
log.exception("There was an error removing an infraction.")
await ctx.send(":x: There was an error removing the infraction.")
- return
# endregion
# region: Edit infraction commands
@@ -789,10 +900,12 @@ class Moderation(Scheduler):
@infraction_edit_group.command(name="duration")
async def edit_duration(self, ctx: Context, infraction_id: str, duration: str):
"""
- Sets the duration of the given infraction, relative to the time of updating.
- :param infraction_id: the id (UUID) of the infraction
- :param duration: the new duration of the infraction, relative to the time of updating. Use "permanent" to mark
- the infraction as permanent.
+ Sets the duration of the given infraction, relative to the time of
+ updating.
+
+ **`infraction_id`:** The ID (UUID) of the infraction.
+ **`duration`:** The new duration of the infraction, relative to the
+ time of updating. Use "permanent" to the infraction as permanent.
"""
try:
@@ -818,8 +931,10 @@ class Moderation(Scheduler):
)
response_object = await response.json()
if "error_code" in response_object or response_object.get("success") is False:
- await ctx.send(f":x: There was an error updating the infraction: {response_object['error_message']}")
- return
+ return await ctx.send(
+ ":x: There was an error updating the infraction: "
+ f"{response_object['error_message']}"
+ )
infraction_object = response_object["infraction"]
# Re-schedule
@@ -830,7 +945,10 @@ class Moderation(Scheduler):
if duration is None:
await ctx.send(f":ok_hand: Updated infraction: marked as permanent.")
else:
- await ctx.send(f":ok_hand: Updated infraction: set to expire on {infraction_object['expires_at']}.")
+ await ctx.send(
+ ":ok_hand: Updated infraction: set to expire on "
+ f"{infraction_object['expires_at']}."
+ )
except Exception:
log.exception("There was an error updating an infraction.")
@@ -873,8 +991,8 @@ class Moderation(Scheduler):
async def edit_reason(self, ctx: Context, infraction_id: str, *, reason: str):
"""
Sets the reason of the given infraction.
- :param infraction_id: the id (UUID) of the infraction
- :param reason: The new reason of the infraction
+ **`infraction_id`:** The ID (UUID) of the infraction.
+ **`reason`:** The new reason of the infraction.
"""
try:
@@ -897,14 +1015,15 @@ class Moderation(Scheduler):
)
response_object = await response.json()
if "error_code" in response_object or response_object.get("success") is False:
- await ctx.send(f":x: There was an error updating the infraction: {response_object['error_message']}")
- return
+ return await ctx.send(
+ ":x: There was an error updating the infraction: "
+ f"{response_object['error_message']}"
+ )
await ctx.send(f":ok_hand: Updated infraction: set reason to \"{reason}\".")
except Exception:
log.exception("There was an error updating an infraction.")
- await ctx.send(":x: There was an error updating the infraction.")
- return
+ return await ctx.send(":x: There was an error updating the infraction.")
new_infraction = response_object["infraction"]
prev_infraction = previous_object["infraction"]
@@ -1038,6 +1157,7 @@ class Moderation(Scheduler):
def schedule_expiration(self, loop: asyncio.AbstractEventLoop, infraction_object: dict):
"""
Schedules a task to expire a temporary infraction.
+
:param loop: the asyncio event loop
:param infraction_object: the infraction object to expire at the end of the task
"""
@@ -1066,9 +1186,10 @@ class Moderation(Scheduler):
async def _scheduled_task(self, infraction_object: dict):
"""
- A co-routine which marks an infraction as expired after the delay from the time of scheduling
- to the time of expiration. At the time of expiration, the infraction is marked as inactive on the website,
- and the expiration task is cancelled.
+ A co-routine which marks an infraction as expired after the delay from the time of
+ scheduling to the time of expiration. At the time of expiration, the infraction is
+ marked as inactive on the website, and the expiration task is cancelled.
+
:param infraction_object: the infraction in question
"""
@@ -1095,8 +1216,9 @@ class Moderation(Scheduler):
async def _deactivate_infraction(self, infraction_object):
"""
- A co-routine which marks an infraction as inactive on the website. This co-routine does not cancel or
- un-schedule an expiration task.
+ A co-routine which marks an infraction as inactive on the website. This co-routine does
+ not cancel or un-schedule an expiration task.
+
:param infraction_object: the infraction in question
"""
@@ -1150,7 +1272,8 @@ class Moderation(Scheduler):
return lines.strip()
async def notify_infraction(
- self, user: Union[User, Member], infr_type: str, duration: str = None, reason: str = None
+ self, user: Union[User, Member], infr_type: str, duration: str = None,
+ reason: str = None
):
"""
Notify a user of their fresh infraction :)
@@ -1184,7 +1307,8 @@ class Moderation(Scheduler):
return await self.send_private_embed(user, embed)
async def notify_pardon(
- self, user: Union[User, Member], title: str, content: str, icon_url: str = Icons.user_verified
+ self, user: Union[User, Member], title: str, content: str,
+ icon_url: str = Icons.user_verified
):
"""
Notify a user that an infraction has been lifted.
@@ -1231,7 +1355,10 @@ class Moderation(Scheduler):
content=actor.mention,
colour=Colour(Colours.soft_red),
title="Notification Failed",
- text=f"Direct message was unable to be sent.\nUser: {target.mention}\nType: {infraction_type}"
+ text=(
+ f"Direct message was unable to be sent.\nUser: {target.mention}\n"
+ f"Type: {infraction_type}"
+ )
)
# endregion
diff --git a/bot/cogs/modlog.py b/bot/cogs/modlog.py
index 55611c5e4..495795b6d 100644
--- a/bot/cogs/modlog.py
+++ b/bot/cogs/modlog.py
@@ -116,7 +116,7 @@ class ModLog:
content: Optional[str] = None,
additional_embeds: Optional[List[Embed]] = None,
timestamp_override: Optional[datetime.datetime] = None,
- footer_override: Optional[str] = None,
+ footer: Optional[str] = None,
):
embed = Embed(description=text)
@@ -127,8 +127,8 @@ class ModLog:
embed.timestamp = timestamp_override or datetime.datetime.utcnow()
- if footer_override:
- embed.set_footer(text=footer_override)
+ if footer:
+ embed.set_footer(text=footer)
if thumbnail:
embed.set_thumbnail(url=thumbnail)
@@ -381,7 +381,8 @@ class ModLog:
await self.send_log_message(
Icons.user_ban, Colour(Colours.soft_red),
"User banned", f"{member.name}#{member.discriminator} (`{member.id}`)",
- thumbnail=member.avatar_url_as(static_format="png")
+ thumbnail=member.avatar_url_as(static_format="png"),
+ channel_id=Channels.modlog
)
async def on_member_join(self, member: Member):
@@ -400,7 +401,8 @@ class ModLog:
await self.send_log_message(
Icons.sign_in, Colour(Colours.soft_green),
"User joined", message,
- thumbnail=member.avatar_url_as(static_format="png")
+ thumbnail=member.avatar_url_as(static_format="png"),
+ channel_id=Channels.userlog
)
async def on_member_remove(self, member: Member):
@@ -414,7 +416,8 @@ class ModLog:
await self.send_log_message(
Icons.sign_out, Colour(Colours.soft_red),
"User left", f"{member.name}#{member.discriminator} (`{member.id}`)",
- thumbnail=member.avatar_url_as(static_format="png")
+ thumbnail=member.avatar_url_as(static_format="png"),
+ channel_id=Channels.userlog
)
async def on_member_unban(self, guild: Guild, member: User):
@@ -428,7 +431,8 @@ class ModLog:
await self.send_log_message(
Icons.user_unban, Colour.blurple(),
"User unbanned", f"{member.name}#{member.discriminator} (`{member.id}`)",
- thumbnail=member.avatar_url_as(static_format="png")
+ thumbnail=member.avatar_url_as(static_format="png"),
+ channel_id=Channels.modlog
)
async def on_member_update(self, before: Member, after: Member):
@@ -516,7 +520,8 @@ class ModLog:
await self.send_log_message(
Icons.user_update, Colour.blurple(),
"Member updated", message,
- thumbnail=after.avatar_url_as(static_format="png")
+ thumbnail=after.avatar_url_as(static_format="png"),
+ channel_id=Channels.userlog
)
async def on_raw_bulk_message_delete(self, event: RawBulkMessageDeleteEvent):
@@ -705,7 +710,7 @@ class ModLog:
await self.send_log_message(
Icons.message_edit, Colour.blurple(), "Message edited (Before)", before_response,
- channel_id=Channels.message_log, timestamp_override=timestamp, footer_override=footer
+ channel_id=Channels.message_log, timestamp_override=timestamp, footer=footer
)
await self.send_log_message(
diff --git a/bot/constants.py b/bot/constants.py
index be713cef2..61f62b09c 100644
--- a/bot/constants.py
+++ b/bot/constants.py
@@ -201,7 +201,7 @@ class Filter(metaclass=YAMLGetter):
filter_zalgo: bool
filter_invites: bool
filter_domains: bool
- filter_rich_embeds: bool
+ watch_rich_embeds: bool
watch_words: bool
watch_tokens: bool
@@ -209,7 +209,6 @@ class Filter(metaclass=YAMLGetter):
notify_user_zalgo: bool
notify_user_invites: bool
notify_user_domains: bool
- notify_user_rich_embeds: bool
ping_everyone: bool
guild_invite_whitelist: List[int]
@@ -352,6 +351,7 @@ class Channels(metaclass=YAMLGetter):
off_topic_3: int
python: int
reddit: int
+ userlog: int
verification: int
diff --git a/config-default.yml b/config-default.yml
index b6427b489..5938ae533 100644
--- a/config-default.yml
+++ b/config-default.yml
@@ -114,6 +114,7 @@ guild:
python: 267624335836053506
reddit: 458224812528238616
staff_lounge: &STAFF_LOUNGE 464905259261755392
+ userlog: 528976905546760203
verification: 352442727016693763
ignored: [*ADMINS, *MESSAGE_LOG, *MODLOG]
@@ -140,7 +141,7 @@ filter:
filter_zalgo: false
filter_invites: true
filter_domains: true
- filter_rich_embeds: false
+ watch_rich_embeds: true
watch_words: true
watch_tokens: true
@@ -149,7 +150,6 @@ filter:
notify_user_zalgo: false
notify_user_invites: true
notify_user_domains: false
- notify_user_rich_embeds: true
# Filter configuration
ping_everyone: true # Ping @everyone when we send a mod-alert?