Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add more information to helpdm embeds. #2056

Closed
wants to merge 6 commits into from
Closed
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
8 changes: 5 additions & 3 deletions bot/exts/help_channels/_caches.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@
# RedisCache[discord.User.id, bool]
help_dm = RedisCache(namespace="HelpChannels.help_dm")

# This cache tracks member who are participating and opted in to help channel dms.
# This cache keeps track of some attributes of the messages sent to members opted
# in for help channel DMs when they participate in help channels.
# It maps help channel IDs to sets of strings of the pattern <userid-dmchannel_id-messageid>.
# serialise the set as a comma separated string to allow usage with redis
# RedisCache[discord.TextChannel.id, str[set[discord.User.id]]]
session_participants = RedisCache(namespace="HelpChannels.session_participants")
# RedisCache[discord.TextChannel.id, str[set[DMMessageInfo]]]
helpdm_messages = RedisCache(namespace="HelpChannels.helpdm_messages")
92 changes: 70 additions & 22 deletions bot/exts/help_channels/_cog.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from bot.constants import Channels, RedirectOutput
from bot.exts.help_channels import _caches, _channel, _message, _name, _stats
from bot.log import get_logger
from bot.utils import channel as channel_utils, lock, members, scheduling
from bot.utils import channel as channel_utils, lock, members, messages, scheduling, users

log = get_logger(__name__)

Expand All @@ -25,6 +25,29 @@
AVAILABLE_HELP_CHANNELS = "**Currently available help channel(s):** {available}"


class DMMessageInfo(t.NamedTuple):
"""
Stores the recipient's id, the DM channel id and a message id.

This is used for easy access to the values stored in the
helpdm_messages cache.
"""

recipient_id: int
dm_channel_id: int
message_id: int

def get_serialised_string(self) -> str:
"""Serialise the ids as a string for storing in redis."""
return f"{self.recipient_id}-{self.dm_channel_id}-{self.message_id}"

@classmethod
def from_string(cls, info_string: str) -> "DMMessageInfo":
"""Deserialise ids from a hyphen-separated string."""
ids = (int(id_) for id_ in info_string.split("-"))
return cls(*ids)


class HelpChannels(commands.Cog):
"""
Manage the help channel system of the guild.
Expand Down Expand Up @@ -408,6 +431,7 @@ async def unclaim_channel(self, channel: discord.TextChannel, *, closed_on: _cha

Unpin the claimant's question message and move the channel to the Dormant category.
Remove the cooldown role from the channel claimant if they have no other channels claimed.
Edit the messages sent to members who participated in the channel and had help DMs enabled.
Cancel the scheduled cooldown role removal task.

`closed_on` is the reason that the channel was closed. See _channel.ClosingReason for possible values.
Expand All @@ -431,7 +455,8 @@ async def _unclaim_channel(
) -> None:
"""Actual implementation of `unclaim_channel`. See that for full documentation."""
await _caches.claimants.delete(channel.id)
await _caches.session_participants.delete(channel.id)
await self.edit_helpdm_messages(channel.id)
await _caches.helpdm_messages.delete(channel.id)

claimant = await members.get_or_fetch_member(self.guild, claimant_id)
if claimant is None:
Expand Down Expand Up @@ -542,14 +567,20 @@ async def update_available_help_channels(self) -> None:
await _caches.dynamic_message.set("message_id", self.dynamic_message)

@staticmethod
def _serialise_session_participants(participants: set[int]) -> str:
"""Convert a set to a comma separated string."""
return ','.join(str(p) for p in participants)
async def _set_helpdm_messages(help_channel_id: int, ids: t.Iterable[DMMessageInfo]) -> None:
"""Set the value for help dm messages associated with the help channel."""
info_string = ",".join(info.get_serialised_string() for info in ids)
await _caches.helpdm_messages.set(help_channel_id, info_string)

@staticmethod
def _deserialise_session_participants(s: str) -> set[int]:
"""Convert a comma separated string into a set."""
return set(int(user_id) for user_id in s.split(",") if user_id != "")
async def _get_helpdm_messages(help_channel_id: int) -> set[DMMessageInfo]:
"""Get parsed help dm messages associated with the help channel."""
info_string = await _caches.helpdm_messages.get(help_channel_id) or ""
return {
DMMessageInfo.from_string(message_info)
for message_info in info_string.split(",")
if message_info
}

@lock.lock_arg(NAMESPACE, "message", attrgetter("channel.id"))
@lock.lock_arg(NAMESPACE, "message", attrgetter("author.id"))
Expand All @@ -559,7 +590,9 @@ async def notify_session_participants(self, message: discord.Message) -> None:

If they meet the requirements they are notified.
"""
if await _caches.claimants.get(message.channel.id) == message.author.id:
claimant_id = await _caches.claimants.get(message.channel.id)
claimant = await users.get_or_fetch_user(claimant_id)
if claimant == message.author:
return # Ignore messages sent by claimants

if not await _caches.help_dm.get(message.author.id):
Expand All @@ -568,23 +601,28 @@ async def notify_session_participants(self, message: discord.Message) -> None:
if (await self.bot.get_context(message)).command == self.close_command:
return # Ignore messages that are closing the channel

session_participants = self._deserialise_session_participants(
await _caches.session_participants.get(message.channel.id) or ""
)
helpdm_messages = await self._get_helpdm_messages(message.channel.id)
session_participants = {message_info.recipient_id for message_info in helpdm_messages}

if message.author.id not in session_participants:
session_participants.add(message.author.id)

# Changes made to the construction of this embed will be
# reflected in the embed that gets sent when the help
# channel is closed.
embed = discord.Embed(
title="Currently Helping",
description=f"You're currently helping in {message.channel.mention}",
description=f"You're currently helping {claimant.mention} ({claimant}) in {message.channel.mention}",
color=constants.Colours.bright_green,
timestamp=message.created_at
)
embed.set_thumbnail(url=claimant.display_avatar.url)

formatted_message = _message.shorten_text(message.content)
if formatted_message:
embed.add_field(name="Their message", value=formatted_message, inline=False)
embed.add_field(name="Conversation", value=f"[Jump to message]({message.jump_url})")

try:
await message.author.send(embed=embed)
dm_message = await message.author.send(embed=embed)
except discord.Forbidden:
log.trace(
f"Failed to send helpdm message to {message.author.id}. DMs Closed/Blocked. "
Expand All @@ -597,12 +635,22 @@ async def notify_session_participants(self, message: discord.Message) -> None:
"To receive updates on help channels you're active in, enable your DMs.",
delete_after=RedirectOutput.delete_delay
)
return

await _caches.session_participants.set(
message.channel.id,
self._serialise_session_participants(session_participants)
)
else:
info = DMMessageInfo(message.author.id, dm_message.channel.id, dm_message.id)
helpdm_messages.add(info)
await self._set_helpdm_messages(message.channel.id, helpdm_messages)

async def edit_helpdm_messages(self, help_channel_id: int) -> None:
"""Edit help DM messages upon help channel closure."""
for message_info in await self._get_helpdm_messages(help_channel_id):
dm_message = await messages.get_or_fetch_message(message_info.message_id, message_info.dm_channel_id)

# This embed was created in notify_session_participants
embed = dm_message.embeds[0].copy()
embed.color = constants.Colours.soft_orange
embed.title = "Previously helped"
embed.description = embed.description.replace("You're currently helping", "You helped")
await dm_message.edit(embed=embed)

@commands.command(name="helpdm")
async def helpdm_command(
Expand Down
7 changes: 6 additions & 1 deletion bot/exts/help_channels/_message.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,11 @@ async def is_empty(channel: discord.TextChannel) -> bool:
return False


def shorten_text(message: str) -> str:
"""Shorten the message if needed to fit within 100 characters."""
return textwrap.shorten(message, width=100, placeholder="...")


async def dm_on_open(message: discord.Message) -> None:
"""
DM claimant with a link to the claimed channel's first message, with a 100 letter preview of the message.
Expand All @@ -106,7 +111,7 @@ async def dm_on_open(message: discord.Message) -> None:
)

embed.set_thumbnail(url=constants.Icons.green_questionmark)
formatted_message = textwrap.shorten(message.content, width=100, placeholder="...")
formatted_message = shorten_text(message.content)
if formatted_message:
embed.add_field(name="Your message", value=formatted_message, inline=False)
embed.add_field(
Expand Down
4 changes: 3 additions & 1 deletion bot/utils/channel.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,9 @@ def is_in_category(channel: discord.TextChannel, category_id: int) -> bool:
return getattr(channel, "category_id", None) == category_id


async def get_or_fetch_channel(channel_id: int) -> discord.abc.GuildChannel:
async def get_or_fetch_channel(
channel_id: int,
) -> Union[discord.abc.GuildChannel, discord.abc.PrivateChannel, discord.Thread]:
"""Attempt to get or fetch a channel and return it."""
log.trace(f"Getting the channel {channel_id}.")

Expand Down
16 changes: 15 additions & 1 deletion bot/utils/messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
import bot
from bot.constants import Emojis, MODERATION_ROLES, NEGATIVE_REPLIES
from bot.log import get_logger
from bot.utils import scheduling
from bot.utils import channel as channel_util, scheduling

log = get_logger(__name__)

Expand Down Expand Up @@ -235,6 +235,20 @@ async def send_denial(ctx: Context, reason: str) -> discord.Message:
return await ctx.send(embed=embed)


async def get_or_fetch_message(message_id: int, channel_id: int) -> discord.Message:
"""Get a message from the cache or fetch it if needed."""
log.trace(f"Getting message {message_id}.")

message = discord.utils.get(bot.instance.cached_messages, id=message_id)
if not message:
log.debug(f"Message {message_id} is not in cache; fetching from API.")
channel = await channel_util.get_or_fetch_channel(channel_id)
message = await channel.fetch_message(message_id)

log.trace(f"Message {channel_id}-{message_id} retrieved.")
return message


def format_user(user: discord.abc.User) -> str:
"""Return a string for `user` which has their mention and ID."""
return f"{user.mention} (`{user.id}`)"
19 changes: 19 additions & 0 deletions bot/utils/users.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import discord

import bot
from bot.log import get_logger

log = get_logger(__name__)


async def get_or_fetch_user(user_id: int) -> discord.User:
"""Get a user from the cache or fetch the user if needed."""
log.trace(f"Getting the user {user_id}.")

user = bot.instance.get_user(user_id)
if not user:
log.debug(f"User {user_id} is not in cache; fetching from API.")
user = await bot.instance.fetch_user(user_id)

log.trace(f"User {user} ({user.id}) retrieved.")
return user