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
30 changes: 24 additions & 6 deletions bot/cogs/dm_relay.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import logging
from typing import Optional

import discord
from discord import Color
Expand All @@ -7,6 +8,8 @@

from bot import constants
from bot.bot import Bot
from bot.converters import UserMentionOrID
from bot.utils import RedisCache
from bot.utils.checks import in_whitelist_check, with_role_check
from bot.utils.messages import send_attachments
from bot.utils.webhooks import send_webhook
Expand All @@ -17,33 +20,46 @@
class DMRelay(Cog):
"""Relay direct messages to and from the bot."""

# RedisCache[str, t.Union[discord.User.id, discord.Member.id]]
dm_cache = RedisCache()

def __init__(self, bot: Bot):
self.bot = bot
self.webhook_id = constants.Webhooks.dm_log
self.webhook = None
self.bot.loop.create_task(self.fetch_webhook())

@commands.command(aliases=("reply",))
async def send_dm(self, ctx: commands.Context, member: discord.Member, *, message: str) -> None:
async def send_dm(self, ctx: commands.Context, member: Optional[UserMentionOrID], *, message: str) -> None:
"""
Allows you to send a DM to a user from the bot.

A `member` must be provided.
If `member` is not provided, it will send to the last user who DM'd the bot.

This feature should be used extremely sparingly. Use ModMail if you need to have a serious
conversation with a user. This is just for responding to extraordinary DMs, having a little
fun with users, and telling people they are DMing the wrong bot.

NOTE: This feature will be removed if it is overused.
"""
try:
await member.send(message)
await ctx.message.add_reaction("✅")
if not member:
user_id = await self.dm_cache.get("last_user")
member = ctx.guild.get_member(user_id) if user_id else None

# If we still don't have a Member at this point, give up
if not member:
log.debug("This bot has never gotten a DM, or the RedisCache has been cleared.")
await ctx.message.add_reaction("❌")
return

try:
await member.send(message)
except discord.errors.Forbidden:
log.debug("User has disabled DMs.")
await ctx.message.add_reaction("❌")
else:
await ctx.message.add_reaction("✅")
self.bot.stats.incr("dm_relay.dm_sent")

async def fetch_webhook(self) -> None:
"""Fetches the webhook object, so we can post to it."""
Expand All @@ -65,9 +81,11 @@ async def on_message(self, message: discord.Message) -> None:
await send_webhook(
webhook=self.webhook,
content=message.clean_content,
username=message.author.display_name,
username=f"{message.author.display_name} ({message.author.id})",
avatar_url=message.author.avatar_url
)
await self.dm_cache.set("last_user", message.author.id)
self.bot.stats.incr("dm_relay.dm_received")

# Handle any attachments
if message.attachments:
Expand Down
19 changes: 19 additions & 0 deletions bot/converters.py
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,25 @@ def proxy_user(user_id: str) -> discord.Object:
return user


class UserMentionOrID(UserConverter):
"""
Converts to a `discord.User`, but only if a mention or userID is provided.

Unlike the default `UserConverter`, it does allow conversion from name, or name#descrim.

This is useful in cases where that lookup strategy would lead to ambiguity.
"""

async def convert(self, ctx: Context, argument: str) -> discord.User:
"""Convert the `arg` to a `discord.User`."""
match = self._get_id_match(argument) or re.match(r'<@!?([0-9]+)>$', argument)

if match is not None:
return await super().convert(ctx, argument)
else:
raise BadArgument(f"`{argument}` is not a User mention or a User ID.")


class FetchedUser(UserConverter):
"""
Converts to a `discord.User` or, if it fails, a `discord.Object`.
Expand Down