Skip to content

Commit

Permalink
Group conversation ALPHA #143
Browse files Browse the repository at this point in the history
  • Loading branch information
fourjr committed Jun 2, 2021
1 parent 4f09b92 commit 5648289
Show file tree
Hide file tree
Showing 5 changed files with 197 additions and 50 deletions.
6 changes: 5 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html);
however, insignificant breaking changes do not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319). If you're a plugin developer, note the "BREAKING" section.

# v3.9.5-dev1
# v3.10.0-dev2

## Added

- Ability to have group conversations. ([GH #143](https://github.com/kyb3r/modmail/issues/143))

## Fixed

Expand Down
28 changes: 20 additions & 8 deletions bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -964,6 +964,15 @@ async def process_dm_modmail(self, message: discord.Message) -> None:
logger.error("Failed to send message:", exc_info=True)
await self.add_reaction(message, blocked_emoji)
else:
for user in thread.recipients:
# send to all other recipients
if user != message.author:
try:
await thread.send(message, user)
except Exception:
# silently ignore
logger.error("Failed to send message:", exc_info=True)

await self.add_reaction(message, sent_emoji)
self.dispatch("thread_reply", thread, False, message, False, False)

Expand Down Expand Up @@ -1224,9 +1233,10 @@ async def on_typing(self, channel, user, _):

thread = await self.threads.find(channel=channel)
if thread is not None and thread.recipient:
if await self.is_blocked(thread.recipient):
return
await thread.recipient.trigger_typing()
for user in thread.recipients:
if await self.is_blocked(user):
continue
await user.trigger_typing()

async def handle_reaction_events(self, payload):
user = self.get_user(payload.user_id)
Expand Down Expand Up @@ -1286,20 +1296,22 @@ async def handle_reaction_events(self, payload):
if not thread:
return
try:
_, linked_message = await thread.find_linked_messages(
_, *linked_message = await thread.find_linked_messages(
message.id, either_direction=True
)
except ValueError as e:
logger.warning("Failed to find linked message for reactions: %s", e)
return

if self.config["transfer_reactions"] and linked_message is not None:
if self.config["transfer_reactions"] and linked_message is not [None]:
if payload.event_type == "REACTION_ADD":
if await self.add_reaction(linked_message, reaction):
await self.add_reaction(message, reaction)
for msg in linked_message:
await self.add_reaction(msg, reaction)
await self.add_reaction(message, reaction)
else:
try:
await linked_message.remove_reaction(reaction, self.user)
for msg in linked_message:
await msg.remove_reaction(reaction, self.user)
await message.remove_reaction(reaction, self.user)
except (discord.HTTPException, discord.InvalidArgument) as e:
logger.warning("Failed to remove reaction: %s", e)
Expand Down
59 changes: 55 additions & 4 deletions cogs/modmail.py
Original file line number Diff line number Diff line change
Expand Up @@ -681,6 +681,48 @@ async def title(self, ctx, *, name: str):
await ctx.message.pin()
await self.bot.add_reaction(ctx.message, sent_emoji)

@commands.command(cooldown_after_parsing=True)
@checks.has_permissions(PermissionLevel.SUPPORTER)
@checks.thread_only()
@commands.cooldown(1, 600, BucketType.channel)
async def adduser(self, ctx, *, user: discord.Member):
"""Adds a user to a modmail thread"""

curr_thread = await self.bot.threads.find(recipient=user)
if curr_thread:
em = discord.Embed(
title="Error",
description=f"User is already in a thread: {curr_thread.channel.mention}.",
color=self.bot.error_color,
)
await ctx.send(embed=em)
else:
em = discord.Embed(
title="New Thread (Group)",
description=f"{ctx.author.name} has added you to a Modmail thread.",
color=self.bot.main_color,
)
if self.bot.config["show_timestamp"]:
em.timestamp = datetime.utcnow()
em.set_footer(text=f"{ctx.author}", icon_url=ctx.author.avatar_url)
await user.send(embed=em)

em = discord.Embed(
title="New User",
description=f"{ctx.author.name} has added {user.name} to the Modmail thread.",
color=self.bot.main_color,
)
if self.bot.config["show_timestamp"]:
em.timestamp = datetime.utcnow()
em.set_footer(text=f"{user}", icon_url=user.avatar_url)

for i in ctx.thread.recipients:
await i.send(embed=em)

await ctx.thread.add_user(user)
sent_emoji, _ = await self.bot.retrieve_emoji()
await self.bot.add_reaction(ctx.message, sent_emoji)

@commands.group(invoke_without_command=True)
@checks.has_permissions(PermissionLevel.SUPPORTER)
async def logs(self, ctx, *, user: User = None):
Expand Down Expand Up @@ -1463,15 +1505,19 @@ async def repair(self, ctx):
and message.embeds[0].footer.text
):
user_id = match_user_id(message.embeds[0].footer.text)
other_recipients = match_other_recipients(ctx.channel.topic)
for n, uid in enumerate(other_recipients):
other_recipients[n] = self.bot.get_user(uid) or await self.bot.fetch_user(uid)

if user_id != -1:
recipient = self.bot.get_user(user_id)
if recipient is None:
self.bot.threads.cache[user_id] = thread = Thread(
self.bot.threads, user_id, ctx.channel
self.bot.threads, user_id, ctx.channel, other_recipients
)
else:
self.bot.threads.cache[user_id] = thread = Thread(
self.bot.threads, recipient, ctx.channel
self.bot.threads, recipient, ctx.channel, other_recipients
)
thread.ready = True
logger.info(
Expand Down Expand Up @@ -1516,13 +1562,18 @@ async def repair(self, ctx):
await thread.channel.send(embed=embed)
except discord.HTTPException:
pass

other_recipients = match_other_recipients(ctx.channel.topic)
for n, uid in enumerate(other_recipients):
other_recipients[n] = self.bot.get_user(uid) or await self.bot.fetch_user(uid)

if recipient is None:
self.bot.threads.cache[user.id] = thread = Thread(
self.bot.threads, user_id, ctx.channel
self.bot.threads, user_id, ctx.channel, other_recipients
)
else:
self.bot.threads.cache[user.id] = thread = Thread(
self.bot.threads, recipient, ctx.channel
self.bot.threads, recipient, ctx.channel, other_recipients
)
thread.ready = True
logger.info("Setting current channel's topic to User ID and created new thread.")
Expand Down
128 changes: 92 additions & 36 deletions core/thread.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
days,
match_title,
match_user_id,
match_other_recipients,
truncate,
format_channel_name,
)
Expand All @@ -34,6 +35,7 @@ def __init__(
manager: "ThreadManager",
recipient: typing.Union[discord.Member, discord.User, int],
channel: typing.Union[discord.DMChannel, discord.TextChannel] = None,
other_recipients: typing.List[typing.Union[discord.Member, discord.User]] = [],
):
self.manager = manager
self.bot = manager.bot
Expand All @@ -45,6 +47,7 @@ def __init__(
raise CommandError("Recipient cannot be a bot.")
self._id = recipient.id
self._recipient = recipient
self._other_recipients = other_recipients
self._channel = channel
self.genesis_message = None
self._ready_event = asyncio.Event()
Expand All @@ -54,7 +57,7 @@ def __init__(
self._cancelled = False

def __repr__(self):
return f'Thread(recipient="{self.recipient or self.id}", channel={self.channel.id})'
return f'Thread(recipient="{self.recipient or self.id}", channel={self.channel.id}, other_recipienets={len(self._other_recipients)})'

async def wait_until_ready(self) -> None:
"""Blocks execution until the thread is fully set up."""
Expand All @@ -80,6 +83,10 @@ def channel(self) -> typing.Union[discord.TextChannel, discord.DMChannel]:
def recipient(self) -> typing.Optional[typing.Union[discord.User, discord.Member]]:
return self._recipient

@property
def recipients(self) -> typing.List[typing.Union[discord.User, discord.Member]]:
return [self._recipient] + self._other_recipients

@property
def ready(self) -> bool:
return self._ready_event.is_set()
Expand All @@ -103,6 +110,23 @@ def cancelled(self, flag: bool):
for i in self.wait_tasks:
i.cancel()

@classmethod
async def from_channel(
cls, manager: "ThreadManager", channel: discord.TextChannel
) -> "Thread":
recipient_id = match_user_id(
channel.topic
) # there is a chance it grabs from another recipient's main thread
recipient = manager.bot.get_user(recipient_id) or await manager.bot.fetch_user(
recipient_id
)

other_recipients = match_other_recipients(channel.topic)
for n, uid in enumerate(other_recipients):
other_recipients[n] = manager.bot.get_user(uid) or await manager.bot.fetch_user(uid)

return cls(manager, recipient or recipient_id, channel, other_recipients)

async def setup(self, *, creator=None, category=None, initial_message=None):
"""Create the thread channel and other io related initialisation tasks"""
self.bot.dispatch("thread_initiate", self, creator, category, initial_message)
Expand Down Expand Up @@ -619,23 +643,30 @@ async def find_linked_messages(
except ValueError:
raise ValueError("Malformed thread message.")

async for msg in self.recipient.history():
if either_direction:
if msg.id == joint_id:
return message1, msg
messages = [message1]
for user in self.recipients:
async for msg in user.history():
if either_direction:
if msg.id == joint_id:
return message1, msg

if not (msg.embeds and msg.embeds[0].author.url):
continue
try:
if int(msg.embeds[0].author.url.split("#")[-1]) == joint_id:
return message1, msg
except ValueError:
continue
raise ValueError("DM message not found. Plain messages are not supported.")
if not (msg.embeds and msg.embeds[0].author.url):
continue
try:
if int(msg.embeds[0].author.url.split("#")[-1]) == joint_id:
messages.append(msg)
break
except ValueError:
continue

if len(messages) > 1:
return messages

raise ValueError("DM message not found.")

async def edit_message(self, message_id: typing.Optional[int], message: str) -> None:
try:
message1, message2 = await self.find_linked_messages(message_id)
message1, *message2 = await self.find_linked_messages(message_id)
except ValueError:
logger.warning("Failed to edit message.", exc_info=True)
raise
Expand All @@ -644,10 +675,11 @@ async def edit_message(self, message_id: typing.Optional[int], message: str) ->
embed1.description = message

tasks = [self.bot.api.edit_message(message1.id, message), message1.edit(embed=embed1)]
if message2 is not None:
embed2 = message2.embeds[0]
embed2.description = message
tasks += [message2.edit(embed=embed2)]
if message2 is not [None]:
for m2 in message2:
embed2 = message2.embeds[0]
embed2.description = message
tasks += [m2.edit(embed=embed2)]
elif message1.embeds[0].author.name.startswith("Persistent Note"):
tasks += [self.bot.api.edit_note(message1.id, message)]

Expand All @@ -657,14 +689,16 @@ async def delete_message(
self, message: typing.Union[int, discord.Message] = None, note: bool = True
) -> None:
if isinstance(message, discord.Message):
message1, message2 = await self.find_linked_messages(message1=message, note=note)
message1, *message2 = await self.find_linked_messages(message1=message, note=note)
else:
message1, message2 = await self.find_linked_messages(message, note=note)
message1, *message2 = await self.find_linked_messages(message, note=note)
print(message1, message2)
tasks = []
if not isinstance(message, discord.Message):
tasks += [message1.delete()]
elif message2 is not None:
tasks += [message2.delete()]
elif message2 is not [None]:
for m2 in message2:
tasks += [m2.delete()]
elif message1.embeds[0].author.name.startswith("Persistent Note"):
tasks += [self.bot.api.delete_note(message1.id)]
if tasks:
Expand Down Expand Up @@ -750,16 +784,18 @@ async def reply(
)
)

user_msg_tasks = []
tasks = []

try:
user_msg = await self.send(
message,
destination=self.recipient,
from_mod=True,
anonymous=anonymous,
plain=plain,
for user in self.recipients:
user_msg_tasks.append(
self.send(
message, destination=user, from_mod=True, anonymous=anonymous, plain=plain,
)
)

try:
user_msg = await asyncio.gather(*user_msg_tasks)
except Exception as e:
logger.error("Message delivery failed:", exc_info=True)
if isinstance(e, discord.Forbidden):
Expand Down Expand Up @@ -1063,9 +1099,23 @@ def get_notifications(self) -> str:

return " ".join(mentions)

async def set_title(self, title) -> None:
async def set_title(self, title: str) -> None:
user_id = match_user_id(self.channel.topic)
ids = ",".join(i.id for i in self._other_recipients)

await self.channel.edit(
topic=f"Title: {title}\nUser ID: {user_id}\nOther Recipients: {ids}"
)

async def add_user(self, user: typing.Union[discord.Member, discord.User]) -> None:
title = match_title(self.channel.topic)
user_id = match_user_id(self.channel.topic)
await self.channel.edit(topic=f"Title: {title}\nUser ID: {user_id}")
self._other_recipients.append(user)

ids = ",".join(str(i.id) for i in self._other_recipients)
await self.channel.edit(
topic=f"Title: {title}\nUser ID: {user_id}\nOther Recipients: {ids}"
)


class ThreadManager:
Expand Down Expand Up @@ -1127,11 +1177,13 @@ async def find(
await thread.close(closer=self.bot.user, silent=True, delete_channel=False)
thread = None
else:
channel = discord.utils.get(
self.bot.modmail_guild.text_channels, topic=f"User ID: {recipient_id}"
channel = discord.utils.find(
lambda x: str(recipient_id) in x.topic if x.topic else False,
self.bot.modmail_guild.text_channels,
)

if channel:
thread = Thread(self, recipient or recipient_id, channel)
thread = await Thread.from_channel(self, channel)
if thread.recipient:
# only save if data is valid
self.cache[recipient_id] = thread
Expand Down Expand Up @@ -1161,10 +1213,14 @@ async def _find_from_channel(self, channel):
except discord.NotFound:
recipient = None

other_recipients = match_other_recipients(channel.topic)
for n, uid in enumerate(other_recipients):
other_recipients[n] = self.bot.get_user(uid) or await self.bot.fetch_user(uid)

if recipient is None:
thread = Thread(self, user_id, channel)
thread = Thread(self, user_id, channel, other_recipients)
else:
self.cache[user_id] = thread = Thread(self, recipient, channel)
self.cache[user_id] = thread = Thread(self, recipient, channel, other_recipients)
thread.ready = True

return thread
Expand Down
Loading

2 comments on commit 5648289

@lorenzo132
Copy link
Member

@lorenzo132 lorenzo132 commented on 5648289 Jun 24, 2021

Choose a reason for hiding this comment

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

Thinking about group conversations in threads, wouldnt it also be nice to add a command where u can add a specific role to the thread by command?

@fourjr
Copy link
Collaborator Author

@fourjr fourjr commented on 5648289 Jun 26, 2021

Choose a reason for hiding this comment

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

@lorenzo132 please create a gh issue

Please sign in to comment.