aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGravatar Johannes Christ <[email protected]>2018-12-30 15:05:16 +0100
committerGravatar GitHub <[email protected]>2018-12-30 15:05:16 +0100
commitc4ffaed9813c389b107710dc05c83e3dd062a245 (patch)
treeb4c77a9f856be2dd72bdc4f7fd05ec47a5ab02ba
parentAdd missing awaits (diff)
parentMerge pull request #223 from python-discord/mod-dm-status (diff)
Merge branch 'master' into defcon-channel-title
-rw-r--r--bot/cogs/filtering.py14
-rw-r--r--bot/cogs/moderation.py133
-rw-r--r--bot/cogs/modlog.py12
-rw-r--r--bot/cogs/snekbox.py26
-rw-r--r--bot/cogs/utils.py63
-rw-r--r--bot/constants.py2
-rw-r--r--bot/decorators.py44
-rw-r--r--config-default.yml2
8 files changed, 215 insertions, 81 deletions
diff --git a/bot/cogs/filtering.py b/bot/cogs/filtering.py
index b6ce501fc..0ba1e49c5 100644
--- a/bot/cogs/filtering.py
+++ b/bot/cogs/filtering.py
@@ -238,15 +238,13 @@ class Filtering:
f"{URLs.discord_invite_api}/{invite}"
)
response = await response.json()
- if response.get("guild") is None:
- # If we have a valid invite which is not a guild invite
- # it might be a DM channel invite
- if response.get("channel") is not None:
- # We don't have whitelisted Group DMs so we can
- # go ahead and return a positive for any group DM
- return True
+ guild = response.get("guild")
+ if guild is None:
+ # We don't have whitelisted Group DMs so we can
+ # go ahead and return a positive for any group DM
+ return True
- guild_id = int(response.get("guild").get("id"))
+ guild_id = int(guild.get("id"))
if guild_id not in Filter.guild_invite_whitelist:
return True
diff --git a/bot/cogs/moderation.py b/bot/cogs/moderation.py
index 6e958b912..ac08d3dd4 100644
--- a/bot/cogs/moderation.py
+++ b/bot/cogs/moderation.py
@@ -82,7 +82,7 @@ class Moderation(Scheduler):
:param reason: The reason for the warning.
"""
- await self.notify_infraction(
+ notified = await self.notify_infraction(
user=user,
infr_type="Warning",
reason=reason
@@ -92,12 +92,29 @@ class Moderation(Scheduler):
if response_object is None:
return
+ dm_result = ":incoming_envelope: " if notified else ""
+ action = f"{dm_result}:ok_hand: warned {user.mention}"
+
if reason is None:
- result_message = f":ok_hand: warned {user.mention}."
+ await ctx.send(f"{action}.")
else:
- result_message = f":ok_hand: warned {user.mention} ({reason})."
+ await ctx.send(f"{action} ({reason}).")
- await ctx.send(result_message)
+ if not notified:
+ await self.log_notify_failure(user, ctx.author, "warning")
+
+ # 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 warned",
+ thumbnail=user.avatar_url_as(static_format="png"),
+ text=textwrap.dedent(f"""
+ Member: {user.mention} (`{user.id}`)
+ Actor: {ctx.message.author}
+ Reason: {reason}
+ """)
+ )
@with_role(*MODERATION_ROLES)
@command(name="kick")
@@ -108,7 +125,7 @@ class Moderation(Scheduler):
:param reason: The reason for the kick.
"""
- await self.notify_infraction(
+ notified = await self.notify_infraction(
user=user,
infr_type="Kick",
reason=reason
@@ -121,12 +138,16 @@ class Moderation(Scheduler):
self.mod_log.ignore(Event.member_remove, user.id)
await user.kick(reason=reason)
+ dm_result = ":incoming_envelope: " if notified else ""
+ action = f"{dm_result}:ok_hand: kicked {user.mention}"
+
if reason is None:
- result_message = f":ok_hand: kicked {user.mention}."
+ await ctx.send(f"{action}.")
else:
- result_message = f":ok_hand: kicked {user.mention} ({reason})."
+ await ctx.send(f"{action} ({reason}).")
- await ctx.send(result_message)
+ if not notified:
+ await self.log_notify_failure(user, ctx.author, "kick")
# Send a log message to the mod log
await self.mod_log.send_log_message(
@@ -150,7 +171,7 @@ class Moderation(Scheduler):
:param reason: The reason for the ban.
"""
- await self.notify_infraction(
+ notified = await self.notify_infraction(
user=user,
infr_type="Ban",
duration="Permanent",
@@ -165,12 +186,16 @@ class Moderation(Scheduler):
self.mod_log.ignore(Event.member_remove, user.id)
await ctx.guild.ban(user, reason=reason, delete_message_days=0)
+ dm_result = ":incoming_envelope: " if notified else ""
+ action = f"{dm_result}:ok_hand: permanently banned {user.mention}"
+
if reason is None:
- result_message = f":ok_hand: permanently banned {user.mention}."
+ await ctx.send(f"{action}.")
else:
- result_message = f":ok_hand: permanently banned {user.mention} ({reason})."
+ await ctx.send(f"{action} ({reason}).")
- await ctx.send(result_message)
+ if not notified:
+ await self.log_notify_failure(user, ctx.author, "ban")
# Send a log message to the mod log
await self.mod_log.send_log_message(
@@ -194,7 +219,7 @@ class Moderation(Scheduler):
:param reason: The reason for the mute.
"""
- await self.notify_infraction(
+ notified = await self.notify_infraction(
user=user,
infr_type="Mute",
duration="Permanent",
@@ -209,12 +234,16 @@ class Moderation(Scheduler):
self.mod_log.ignore(Event.member_update, user.id)
await user.add_roles(self._muted_role, reason=reason)
+ dm_result = ":incoming_envelope: " if notified else ""
+ action = f"{dm_result}:ok_hand: permanently muted {user.mention}"
+
if reason is None:
- result_message = f":ok_hand: permanently muted {user.mention}."
+ await ctx.send(f"{action}.")
else:
- result_message = f":ok_hand: permanently muted {user.mention} ({reason})."
+ await ctx.send(f"{action} ({reason}).")
- await ctx.send(result_message)
+ if not notified:
+ await self.log_notify_failure(user, ctx.author, "mute")
# Send a log message to the mod log
await self.mod_log.send_log_message(
@@ -242,7 +271,7 @@ class Moderation(Scheduler):
:param reason: The reason for the temporary mute.
"""
- await self.notify_infraction(
+ notified = await self.notify_infraction(
user=user,
infr_type="Mute",
duration=duration,
@@ -262,12 +291,16 @@ class Moderation(Scheduler):
loop = asyncio.get_event_loop()
self.schedule_task(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}"
+
if reason is None:
- result_message = f":ok_hand: muted {user.mention} until {infraction_expiration}."
+ await ctx.send(f"{action}.")
else:
- result_message = f":ok_hand: muted {user.mention} until {infraction_expiration} ({reason})."
+ await ctx.send(f"{action} ({reason}).")
- await ctx.send(result_message)
+ if not notified:
+ await self.log_notify_failure(user, ctx.author, "mute")
# Send a log message to the mod log
await self.mod_log.send_log_message(
@@ -294,7 +327,7 @@ class Moderation(Scheduler):
:param reason: The reason for the temporary ban.
"""
- await self.notify_infraction(
+ notified = await self.notify_infraction(
user=user,
infr_type="Ban",
duration=duration,
@@ -316,12 +349,16 @@ class Moderation(Scheduler):
loop = asyncio.get_event_loop()
self.schedule_task(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}"
+
if reason is None:
- result_message = f":ok_hand: banned {user.mention} until {infraction_expiration}."
+ await ctx.send(f"{action}.")
else:
- result_message = f":ok_hand: banned {user.mention} until {infraction_expiration} ({reason})."
+ await ctx.send(f"{action} ({reason}).")
- await ctx.send(result_message)
+ if not notified:
+ await self.log_notify_failure(user, ctx.author, "ban")
# Send a log message to the mod log
await self.mod_log.send_log_message(
@@ -361,6 +398,19 @@ class Moderation(Scheduler):
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",
+ thumbnail=user.avatar_url_as(static_format="png"),
+ text=textwrap.dedent(f"""
+ Member: {user.mention} (`{user.id}`)
+ Actor: {ctx.message.author}
+ Reason: {reason}
+ """)
+ )
+
@with_role(*MODERATION_ROLES)
@command(name="shadow_kick", hidden=True, aliases=['shadowkick', 'skick'])
async def shadow_kick(self, ctx: Context, user: Member, *, reason: str = None):
@@ -603,7 +653,18 @@ class Moderation(Scheduler):
if infraction_object["expires_at"] is not None:
self.cancel_expiration(infraction_object["id"])
- await ctx.send(f":ok_hand: Un-muted {user.mention}.")
+ notified = await self.notify_pardon(
+ user=user,
+ title="You have been unmuted.",
+ content="You may now send messages in the server.",
+ 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 not notified:
+ await self.log_notify_failure(user, ctx.author, "unmute")
# Send a log message to the mod log
await self.mod_log.send_log_message(
@@ -617,13 +678,6 @@ class Moderation(Scheduler):
Intended expiry: {infraction_object['expires_at']}
""")
)
-
- await self.notify_pardon(
- user=user,
- title="You have been unmuted.",
- content="You may now send messages in the server.",
- icon_url=Icons.user_unmute
- )
except Exception:
log.exception("There was an error removing an infraction.")
await ctx.send(":x: There was an error removing the infraction.")
@@ -1093,7 +1147,7 @@ class Moderation(Scheduler):
embed.title = f"Please review our rules over at {RULES_URL}"
embed.url = RULES_URL
- await self.send_private_embed(user, embed)
+ 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
@@ -1114,7 +1168,7 @@ class Moderation(Scheduler):
embed.set_author(name=title, icon_url=icon_url)
- await self.send_private_embed(user, embed)
+ return await self.send_private_embed(user, embed)
async def send_private_embed(self, user: Union[User, Member], embed: Embed):
"""
@@ -1129,11 +1183,22 @@ class Moderation(Scheduler):
try:
await user.send(embed=embed)
+ return True
except (HTTPException, Forbidden):
log.debug(
f"Infraction-related information could not be sent to user {user} ({user.id}). "
"They've probably just disabled private messages."
)
+ return False
+
+ async def log_notify_failure(self, target: str, actor: Member, infraction_type: str):
+ await self.mod_log.send_log_message(
+ icon_url=Icons.token_removed,
+ 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}"
+ )
# endregion
diff --git a/bot/cogs/modlog.py b/bot/cogs/modlog.py
index 1d1546d5b..905f114c1 100644
--- a/bot/cogs/modlog.py
+++ b/bot/cogs/modlog.py
@@ -104,8 +104,9 @@ class ModLog:
self._ignored[event].append(item)
async def send_log_message(
- self, icon_url: Optional[str], colour: Colour, title: Optional[str], text: str, thumbnail: str = None,
- channel_id: int = Channels.modlog, ping_everyone: bool = False, files: List[File] = None
+ self, icon_url: Optional[str], colour: Colour, title: Optional[str], text: str,
+ thumbnail: str = None, channel_id: int = Channels.modlog, ping_everyone: bool = False,
+ files: List[File] = None, content: str = None
):
embed = Embed(description=text)
@@ -118,10 +119,11 @@ class ModLog:
if thumbnail is not None:
embed.set_thumbnail(url=thumbnail)
- content = None
-
if ping_everyone:
- content = "@everyone"
+ if content:
+ content = f"@everyone\n{content}"
+ else:
+ content = "@everyone"
await self.bot.get_channel(channel_id).send(content=content, embed=embed, files=files)
diff --git a/bot/cogs/snekbox.py b/bot/cogs/snekbox.py
index 1b51da843..cb0454249 100644
--- a/bot/cogs/snekbox.py
+++ b/bot/cogs/snekbox.py
@@ -6,12 +6,12 @@ import textwrap
from discord import Colour, Embed
from discord.ext.commands import (
- Bot, CommandError, Context, MissingPermissions,
- NoPrivateMessage, check, command, guild_only
+ Bot, CommandError, Context, NoPrivateMessage, command, guild_only
)
from bot.cogs.rmq import RMQ
from bot.constants import Channels, ERROR_REPLIES, NEGATIVE_REPLIES, Roles, URLs
+from bot.decorators import InChannelCheckFailure, in_channel
from bot.utils.messages import wait_for_deletion
@@ -51,22 +51,8 @@ RAW_CODE_REGEX = re.compile(
r"\s*$", # any trailing whitespace until the end of the string
re.DOTALL # "." also matches newlines
)
-BYPASS_ROLES = (Roles.owner, Roles.admin, Roles.moderator, Roles.helpers)
-WHITELISTED_CHANNELS = (Channels.bot,)
-WHITELISTED_CHANNELS_STRING = ', '.join(f"<#{channel_id}>" for channel_id in WHITELISTED_CHANNELS)
-
-
-async def channel_is_whitelisted_or_author_can_bypass(ctx: Context):
- """
- Checks that the author is either helper or above
- or the channel is a whitelisted channel.
- """
- if ctx.channel.id in WHITELISTED_CHANNELS:
- return True
- if any(r.id in BYPASS_ROLES for r in ctx.author.roles):
- return True
- raise MissingPermissions("You are not allowed to do that here.")
+BYPASS_ROLES = (Roles.owner, Roles.admin, Roles.moderator, Roles.helpers)
class Snekbox:
@@ -84,7 +70,7 @@ class Snekbox:
@command(name='eval', aliases=('e',))
@guild_only()
- @check(channel_is_whitelisted_or_author_can_bypass)
+ @in_channel(Channels.bot, bypass_roles=BYPASS_ROLES)
async def eval_command(self, ctx: Context, *, code: str = None):
"""
Run some code. get the result back. We've done our best to make this safe, but do let us know if you
@@ -205,9 +191,9 @@ class Snekbox:
embed.description = "You're not allowed to use this command in private messages."
await ctx.send(embed=embed)
- elif isinstance(error, MissingPermissions):
+ elif isinstance(error, InChannelCheckFailure):
embed.title = random.choice(NEGATIVE_REPLIES)
- embed.description = f"Sorry, but you may only use this command within {WHITELISTED_CHANNELS_STRING}."
+ embed.description = str(error)
await ctx.send(embed=embed)
else:
diff --git a/bot/cogs/utils.py b/bot/cogs/utils.py
index b101b8816..65c729414 100644
--- a/bot/cogs/utils.py
+++ b/bot/cogs/utils.py
@@ -1,16 +1,20 @@
import logging
+import random
+import re
+import unicodedata
from email.parser import HeaderParser
from io import StringIO
-
from discord import Colour, Embed
from discord.ext.commands import AutoShardedBot, Context, command
-from bot.constants import Roles
-from bot.decorators import with_role
+from bot.constants import Channels, NEGATIVE_REPLIES, Roles
+from bot.decorators import InChannelCheckFailure, in_channel
log = logging.getLogger(__name__)
+BYPASS_ROLES = (Roles.owner, Roles.admin, Roles.moderator, Roles.helpers)
+
class Utils:
"""
@@ -24,7 +28,6 @@ class Utils:
self.base_github_pep_url = "https://raw.githubusercontent.com/python/peps/master/pep-"
@command(name='pep', aliases=('get_pep', 'p'))
- @with_role(Roles.verified)
async def pep_command(self, ctx: Context, pep_number: str):
"""
Fetches information about a PEP and sends it to the channel.
@@ -87,6 +90,58 @@ class Utils:
await ctx.message.channel.send(embed=pep_embed)
+ @command()
+ @in_channel(Channels.bot, bypass_roles=BYPASS_ROLES)
+ async def charinfo(self, ctx, *, characters: str):
+ """
+ Shows you information on up to 25 unicode characters.
+ """
+
+ match = re.match(r"<(a?):(\w+):(\d+)>", characters)
+ if match:
+ embed = Embed(
+ title="Non-Character Detected",
+ description=(
+ "Only unicode characters can be processed, but a custom Discord emoji "
+ "was found. Please remove it and try again."
+ )
+ )
+ embed.colour = Colour.red()
+ return await ctx.send(embed=embed)
+
+ if len(characters) > 25:
+ embed = Embed(title=f"Too many characters ({len(characters)}/25)")
+ embed.colour = Colour.red()
+ return await ctx.send(embed=embed)
+
+ def get_info(char):
+ digit = f"{ord(char):x}"
+ if len(digit) <= 4:
+ u_code = f"\\u{digit:>04}"
+ else:
+ u_code = f"\\U{digit:>08}"
+ url = f"https://www.compart.com/en/unicode/U+{digit:>04}"
+ name = f"[{unicodedata.name(char, '')}]({url})"
+ info = f"`{u_code.ljust(10)}`: {name} - {char}"
+ return info, u_code
+
+ charlist, rawlist = zip(*(get_info(c) for c in characters))
+
+ embed = Embed(description="\n".join(charlist))
+ embed.set_author(name="Character Info")
+
+ if len(characters) > 1:
+ embed.add_field(name='Raw', value=f"`{''.join(rawlist)}`", inline=False)
+
+ await ctx.send(embed=embed)
+
+ async def __error(self, ctx, error):
+ embed = Embed(colour=Colour.red())
+ if isinstance(error, InChannelCheckFailure):
+ embed.title = random.choice(NEGATIVE_REPLIES)
+ embed.description = str(error)
+ await ctx.send(embed=embed)
+
def setup(bot):
bot.add_cog(Utils(bot))
diff --git a/bot/constants.py b/bot/constants.py
index b4eca7e1d..05d2abf81 100644
--- a/bot/constants.py
+++ b/bot/constants.py
@@ -292,6 +292,8 @@ class Icons(metaclass=YAMLGetter):
user_unmute: str
user_verified: str
+ user_warn: str
+
pencil: str
remind_blurple: str
diff --git a/bot/decorators.py b/bot/decorators.py
index fe974cbd3..87877ecbf 100644
--- a/bot/decorators.py
+++ b/bot/decorators.py
@@ -1,18 +1,51 @@
import logging
import random
+import typing
from asyncio import Lock
from functools import wraps
from weakref import WeakValueDictionary
from discord import Colour, Embed
from discord.ext import commands
-from discord.ext.commands import Context
+from discord.ext.commands import CheckFailure, Context
from bot.constants import ERROR_REPLIES
log = logging.getLogger(__name__)
+class InChannelCheckFailure(CheckFailure):
+ pass
+
+
+def in_channel(*channels: int, bypass_roles: typing.Container[int] = None):
+ """
+ Checks that the message is in a whitelisted channel or optionally has a bypass role.
+ """
+ def predicate(ctx: Context):
+ if ctx.channel.id in channels:
+ log.debug(f"{ctx.author} tried to call the '{ctx.command.name}' command. "
+ f"The command was used in a whitelisted channel.")
+ return True
+
+ if bypass_roles:
+ if any(r.id in bypass_roles for r in ctx.author.roles):
+ log.debug(f"{ctx.author} tried to call the '{ctx.command.name}' command. "
+ f"The command was not used in a whitelisted channel, "
+ f"but the author had a role to bypass the in_channel check.")
+ return True
+
+ log.debug(f"{ctx.author} tried to call the '{ctx.command.name}' command. "
+ f"The in_channel check failed.")
+
+ channels_str = ', '.join(f"<#{c_id}>" for c_id in channels)
+ raise InChannelCheckFailure(
+ f"Sorry, but you may only use this command within {channels_str}."
+ )
+
+ return commands.check(predicate)
+
+
def with_role(*role_ids: int):
async def predicate(ctx: Context):
if not ctx.guild: # Return False in a DM
@@ -46,15 +79,6 @@ def without_role(*role_ids: int):
return commands.check(predicate)
-def in_channel(channel_id):
- async def predicate(ctx: Context):
- check = ctx.channel.id == channel_id
- log.debug(f"{ctx.author} tried to call the '{ctx.command.name}' command. "
- f"The result of the in_channel check was {check}.")
- return check
- return commands.check(predicate)
-
-
def locked():
"""
Allows the user to only run one instance of the decorated command at a time.
diff --git a/config-default.yml b/config-default.yml
index 6d301048f..7a5960987 100644
--- a/config-default.yml
+++ b/config-default.yml
@@ -72,6 +72,8 @@ style:
user_unmute: "https://cdn.discordapp.com/emojis/472472639206719508.png"
user_verified: "https://cdn.discordapp.com/emojis/470326274519334936.png"
+ user_warn: "https://cdn.discordapp.com/emojis/470326274238447633.png"
+
pencil: "https://cdn.discordapp.com/emojis/470326272401211415.png"
remind_blurple: "https://cdn.discordapp.com/emojis/477907609215827968.png"