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
9 changes: 9 additions & 0 deletions bot/exts/help_channels/_caches.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,12 @@
# This cache keeps track of the dynamic message ID for
# the continuously updated message in the #How-to-get-help channel.
dynamic_message = RedisCache(namespace="HelpChannels.dynamic_message")

# This cache keeps track of who has help-dms on.
# 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.
# 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")
78 changes: 77 additions & 1 deletion bot/exts/help_channels/_cog.py
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,7 @@ 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)

claimant = self.bot.get_guild(constants.Guild.id).get_member(claimant_id)
if claimant is None:
Expand Down Expand Up @@ -466,7 +467,9 @@ async def on_message(self, message: discord.Message) -> None:
if channel_utils.is_in_category(message.channel, constants.Categories.help_available):
if not _channel.is_excluded_channel(message.channel):
await self.claim_channel(message)
else:

elif channel_utils.is_in_category(message.channel, constants.Categories.help_in_use):
await self.notify_session_participants(message)
await _message.update_message_caches(message)

@commands.Cog.listener()
Expand Down Expand Up @@ -535,3 +538,76 @@ async def update_available_help_channels(self) -> None:
)
self.dynamic_message = new_dynamic_message["id"]
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)

@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 != "")
Comment on lines +542 to +550
Copy link
Copy Markdown
Contributor

@MarkKoz MarkKoz Jun 12, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aren't sets in redis more appropriate for this sort of thing? It would remove the need to serialise and deserialise (unless you count str -> int as deserialisation). It wouldn't even need to retrieve all items since there are commands to check set membership.

Copy link
Copy Markdown
Contributor Author

@jacobmonck jacobmonck Jun 12, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Async Rediscache doesn't support sets. We were thinking about using sets but when we figured out it doesn't support it, we switched to that.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

aioredis does support it and async-redis cache can trivially be extended to support it (yes I think it's worth doing that).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note I'm not suggesting implementing the set operations with all the type conversion stuff. Just a basic SADD call or whatever that will store a string as given.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not too familiar with Redis can you please give more information about how I would implement this? Also I think it would be best to do this as a separate feature.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's alright. Let's just leave your implementation for now. I can try to merge set support into async-rediscache in the future.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alright, sounds good!


@lock.lock_arg(NAMESPACE, "message", attrgetter("channel.id"))
@lock.lock_arg(NAMESPACE, "message", attrgetter("author.id"))
async def notify_session_participants(self, message: discord.Message) -> None:
"""
Check if the message author meets the requirements to be notified.

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

if not await _caches.help_dm.get(message.author.id):
return # Ignore message if user is opted out of help dms

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 ""
)

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

embed = discord.Embed(
title="Currently Helping",
description=f"You're currently helping in {message.channel.mention}",
color=constants.Colours.soft_green,
timestamp=message.created_at
)
embed.add_field(name="Conversation", value=f"[Jump to message]({message.jump_url})")
await message.author.send(embed=embed)

await _caches.session_participants.set(
message.channel.id,
self._serialise_session_participants(session_participants)
)

@commands.command(name="helpdm")
async def helpdm_command(
self,
ctx: commands.Context,
state_bool: bool
) -> None:
"""
Allows user to toggle "Helping" dms.

If this is set to on the user will receive a dm for the channel they are participating in.

If this is set to off the user will not receive a dm for channel that they are participating in.
"""
state_str = "ON" if state_bool else "OFF"

if state_bool == await _caches.help_dm.get(ctx.author.id, False):
await ctx.send(f"{constants.Emojis.cross_mark} {ctx.author.mention} Help DMs are already {state_str}")
return

if state_bool:
await _caches.help_dm.set(ctx.author.id, True)
else:
await _caches.help_dm.delete(ctx.author.id)
await ctx.send(f"{constants.Emojis.ok_hand} {ctx.author.mention} Help DMs {state_str}!")
27 changes: 12 additions & 15 deletions bot/exts/help_channels/_message.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
import bot
from bot import constants
from bot.exts.help_channels import _caches
from bot.utils.channel import is_in_category

log = logging.getLogger(__name__)

Expand Down Expand Up @@ -47,23 +46,21 @@ async def update_message_caches(message: discord.Message) -> None:
"""Checks the source of new content in a help channel and updates the appropriate cache."""
channel = message.channel

# Confirm the channel is an in use help channel
if is_in_category(channel, constants.Categories.help_in_use):
log.trace(f"Checking if #{channel} ({channel.id}) has had a reply.")
log.trace(f"Checking if #{channel} ({channel.id}) has had a reply.")

claimant_id = await _caches.claimants.get(channel.id)
if not claimant_id:
# The mapping for this channel doesn't exist, we can't do anything.
return
claimant_id = await _caches.claimants.get(channel.id)
if not claimant_id:
# The mapping for this channel doesn't exist, we can't do anything.
return

# datetime.timestamp() would assume it's local, despite d.py giving a (naïve) UTC time.
timestamp = Arrow.fromdatetime(message.created_at).timestamp()
# datetime.timestamp() would assume it's local, despite d.py giving a (naïve) UTC time.
timestamp = Arrow.fromdatetime(message.created_at).timestamp()

# Overwrite the appropriate last message cache depending on the author of the message
if message.author.id == claimant_id:
await _caches.claimant_last_message_times.set(channel.id, timestamp)
else:
await _caches.non_claimant_last_message_times.set(channel.id, timestamp)
# Overwrite the appropriate last message cache depending on the author of the message
if message.author.id == claimant_id:
await _caches.claimant_last_message_times.set(channel.id, timestamp)
else:
await _caches.non_claimant_last_message_times.set(channel.id, timestamp)


async def get_last_message(channel: discord.TextChannel) -> t.Optional[discord.Message]:
Expand Down