Skip to content

Commit 7328198

Browse files
authored
Merge branch 'development' into users/martinbndr/selfcontactcooldown
2 parents aea81e6 + 3ff4261 commit 7328198

10 files changed

Lines changed: 517 additions & 109 deletions

File tree

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
66
This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html);
77
however, insignificant breaking changes do not guarantee a major version bump, see the reasoning [here](https://github.com/modmail-dev/modmail/issues/319). If you're a plugin developer, note the "BREAKING" section.
88

9+
# Unreleased
10+
11+
### Fixed
12+
* Confirm thread creation (react to contact) no longer leaves a thread stuck in a "not ready" cache state when the recipient has DMs disabled. The bot now catches `discord.Forbidden` when sending the confirmation prompt, cancels the thread, and clears the cache entry immediately instead of requiring a bot restart. (#3442)
13+
914
# v4.2.1
1015

1116
### Added

bot.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -377,6 +377,10 @@ async def wait_for_connected(self) -> None:
377377
def snippets(self) -> typing.Dict[str, str]:
378378
return self.config["snippets"]
379379

380+
@property
381+
def args(self) -> typing.Dict[str, str]:
382+
return self.config["args"]
383+
380384
@property
381385
def aliases(self) -> typing.Dict[str, str]:
382386
return self.config["aliases"]

cogs/modmail.py

Lines changed: 275 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
from dateutil import parser
1717

1818
from core import checks
19-
from core.models import DMDisabled, PermissionLevel, SimilarCategoryConverter, getLogger
19+
from core.models import DMDisabled, PermissionLevel, SimilarCategoryConverter, UnseenFormatter, getLogger
2020
from core.paginator import EmbedPaginatorSession
2121
from core.thread import Thread
2222
from core.time import UserFriendlyTime, human_timedelta
@@ -25,6 +25,10 @@
2525
logger = getLogger(__name__)
2626

2727

28+
# Arg names reserved by formatreply commands (channel, recipient, author).
29+
RESERVED_ARG_NAMES = {"channel", "recipient", "author"}
30+
31+
2832
class Modmail(commands.Cog):
2933
"""Commands directly related to Modmail functionality."""
3034

@@ -209,7 +213,7 @@ async def snippet(self, ctx, *, name: str.lower = None):
209213
210214
When `{prefix}snippet` is used by itself, this will retrieve
211215
a list of snippets that are currently set. `{prefix}snippet-name` will show what the
212-
snippet point to.
216+
snippet points to.
213217
214218
To create a snippet:
215219
- `{prefix}snippet add snippet-name A pre-defined text.`
@@ -535,6 +539,214 @@ async def snippet_rename(self, ctx, name: str.lower, *, value):
535539
embed = create_not_found_embed(name, self.bot.snippets.keys(), "Snippet")
536540
await ctx.send(embed=embed)
537541

542+
@commands.group(invoke_without_command=True)
543+
@checks.has_permissions(PermissionLevel.SUPPORTER)
544+
async def args(self, ctx, *, name: str.lower = None):
545+
"""
546+
Create dynamic args for use in replies.
547+
548+
When `{prefix}args` is used by itself, this will retrieve
549+
a list of args that are currently set. `{prefix}args name` will show what the
550+
arg points to.
551+
552+
To create an arg:
553+
- `{prefix}args add arg-name A value.`
554+
555+
You can use your arg in a reply with `{arg-name}`.
556+
"""
557+
558+
if name is not None:
559+
if name == "compact":
560+
embeds = []
561+
562+
for i, names in enumerate(zip_longest(*(iter(sorted(self.bot.args)),) * 15)):
563+
description = format_description(i, names)
564+
embed = discord.Embed(color=self.bot.main_color, description=description)
565+
embed.set_author(name="Args", icon_url=self.bot.get_guild_icon(guild=ctx.guild, size=128))
566+
embeds.append(embed)
567+
568+
session = EmbedPaginatorSession(ctx, *embeds)
569+
await session.run()
570+
return
571+
572+
if name not in self.bot.args:
573+
embed = create_not_found_embed(name, self.bot.args.keys(), "Arg")
574+
else:
575+
val = self.bot.args[name]
576+
embed = discord.Embed(
577+
title=f'Arg - "{name}":',
578+
description=val,
579+
color=self.bot.main_color,
580+
)
581+
return await ctx.send(embed=embed)
582+
583+
if not self.bot.args:
584+
embed = discord.Embed(
585+
color=self.bot.error_color,
586+
description="You don't have any args at the moment.",
587+
)
588+
embed.set_footer(text=f'See "{self.bot.prefix}help args add" for how to add an arg.')
589+
embed.set_author(
590+
name="Args",
591+
icon_url=self.bot.get_guild_icon(guild=ctx.guild, size=128),
592+
)
593+
return await ctx.send(embed=embed)
594+
595+
embeds = [discord.Embed(color=self.bot.main_color) for _ in range((len(self.bot.args) + 9) // 10)]
596+
for embed in embeds:
597+
embed.set_author(name="Args", icon_url=self.bot.get_guild_icon(guild=ctx.guild, size=128))
598+
599+
for i, arg in enumerate(sorted(self.bot.args.items())):
600+
embeds[i // 10].add_field(name=arg[0], value=return_or_truncate(arg[1], 350), inline=False)
601+
602+
session = EmbedPaginatorSession(ctx, *embeds)
603+
await session.run()
604+
605+
@args.command(name="raw")
606+
@checks.has_permissions(PermissionLevel.SUPPORTER)
607+
async def args_raw(self, ctx, *, name: str.lower):
608+
"""
609+
View the raw content of an arg.
610+
"""
611+
if name not in self.bot.args:
612+
embed = create_not_found_embed(name, self.bot.args.keys(), "Arg")
613+
else:
614+
val = truncate(escape_code_block(self.bot.args[name]), 2048 - 7)
615+
embed = discord.Embed(
616+
title=f'Raw arg - "{name}":',
617+
description=f"```\n{val}```",
618+
color=self.bot.main_color,
619+
)
620+
621+
return await ctx.send(embed=embed)
622+
623+
@args.command(name="add", aliases=["create", "make"])
624+
@checks.has_permissions(PermissionLevel.SUPPORTER)
625+
async def args_add(self, ctx, name: str.lower, *, value: commands.clean_content):
626+
"""
627+
Add an arg.
628+
629+
To add an arg, simply do: ```
630+
{prefix}args add name value
631+
```
632+
"""
633+
if name in self.bot.args:
634+
embed = discord.Embed(
635+
title="Error",
636+
color=self.bot.error_color,
637+
description=f"Arg `{name}` already exists.",
638+
)
639+
return await ctx.send(embed=embed)
640+
641+
if name in RESERVED_ARG_NAMES:
642+
embed = discord.Embed(
643+
title="Error",
644+
color=self.bot.error_color,
645+
description=f"Arg name `{name}` is reserved (used by formatreply commands). "
646+
f"Reserved names: {', '.join(f'`{n}`' for n in sorted(RESERVED_ARG_NAMES))}.",
647+
)
648+
return await ctx.send(embed=embed)
649+
650+
if len(name) > 120:
651+
embed = discord.Embed(
652+
title="Error",
653+
color=self.bot.error_color,
654+
description="Arg names cannot be longer than 120 characters.",
655+
)
656+
return await ctx.send(embed=embed)
657+
658+
self.bot.args[name] = value
659+
await self.bot.config.update()
660+
661+
embed = discord.Embed(
662+
title="Added arg",
663+
color=self.bot.main_color,
664+
description="Successfully created arg.",
665+
)
666+
return await ctx.send(embed=embed)
667+
668+
@args.command(name="remove", aliases=["del", "delete"])
669+
@checks.has_permissions(PermissionLevel.SUPPORTER)
670+
async def args_remove(self, ctx, *, name: str.lower):
671+
"""Remove an arg."""
672+
if name in self.bot.args:
673+
self.bot.args.pop(name)
674+
await self.bot.config.update()
675+
embed = discord.Embed(
676+
title="Removed arg",
677+
color=self.bot.main_color,
678+
description=f"Arg `{name}` is now deleted.",
679+
)
680+
else:
681+
embed = create_not_found_embed(name, self.bot.args.keys(), "Arg")
682+
await ctx.send(embed=embed)
683+
684+
@args.command(name="edit")
685+
@checks.has_permissions(PermissionLevel.SUPPORTER)
686+
async def args_edit(self, ctx, name: str.lower, *, value):
687+
"""
688+
Edit an arg.
689+
"""
690+
if name in self.bot.args:
691+
self.bot.args[name] = value
692+
await self.bot.config.update()
693+
694+
embed = discord.Embed(
695+
title="Edited arg",
696+
color=self.bot.main_color,
697+
description=f'`{name}` will now be replaced with "{value}".',
698+
)
699+
else:
700+
embed = create_not_found_embed(name, self.bot.args.keys(), "Arg")
701+
await ctx.send(embed=embed)
702+
703+
@args.command(name="rename")
704+
@checks.has_permissions(PermissionLevel.SUPPORTER)
705+
async def args_rename(self, ctx, name: str.lower, *, value: commands.clean_content):
706+
"""
707+
Rename an arg.
708+
"""
709+
if name not in self.bot.args:
710+
embed = create_not_found_embed(name, self.bot.args.keys(), "Arg")
711+
return await ctx.send(embed=embed)
712+
713+
if value in self.bot.args:
714+
embed = discord.Embed(
715+
title="Error",
716+
color=self.bot.error_color,
717+
description=f"Arg `{value}` already exists.",
718+
)
719+
return await ctx.send(embed=embed)
720+
721+
if value in RESERVED_ARG_NAMES:
722+
embed = discord.Embed(
723+
title="Error",
724+
color=self.bot.error_color,
725+
description=f"Arg name `{value}` is reserved (used by formatreply commands). "
726+
f"Reserved names: {', '.join(f'`{n}`' for n in sorted(RESERVED_ARG_NAMES))}.",
727+
)
728+
return await ctx.send(embed=embed)
729+
730+
if len(value) > 120:
731+
embed = discord.Embed(
732+
title="Error",
733+
color=self.bot.error_color,
734+
description="Arg names cannot be longer than 120 characters.",
735+
)
736+
return await ctx.send(embed=embed)
737+
738+
old_arg_value = self.bot.args[name]
739+
self.bot.args.pop(name)
740+
self.bot.args[value] = old_arg_value
741+
await self.bot.config.update()
742+
743+
embed = discord.Embed(
744+
title="Renamed arg",
745+
color=self.bot.main_color,
746+
description=f'`{name}` has been renamed to "{value}".',
747+
)
748+
await ctx.send(embed=embed)
749+
538750
@commands.command(usage="<category> [options]")
539751
@checks.has_permissions(PermissionLevel.MODERATOR)
540752
@checks.thread_only()
@@ -1510,6 +1722,19 @@ async def reply(self, ctx, *, msg: str = ""):
15101722
automatically embedding image URLs.
15111723
"""
15121724

1725+
if self.bot.args:
1726+
msg = UnseenFormatter().format(msg, **self.bot.args)
1727+
1728+
if len(msg) > 4096:
1729+
return await ctx.send(
1730+
embed=discord.Embed(
1731+
title="Error",
1732+
color=self.bot.error_color,
1733+
description="The resulting message is too long to fit in an embed description "
1734+
f"({len(msg)}/4096 characters). Please shorten your message or args.",
1735+
)
1736+
)
1737+
15131738
# Ensure logs record only the reply text, not the command.
15141739
ctx.message.content = msg
15151740
async with safe_typing(ctx):
@@ -1532,10 +1757,22 @@ async def freply(self, ctx, *, msg: str = ""):
15321757
"""
15331758
msg = self.bot.formatter.format(
15341759
msg,
1760+
**self.bot.args,
15351761
channel=ctx.channel,
15361762
recipient=ctx.thread.recipient,
15371763
author=ctx.message.author,
15381764
)
1765+
1766+
if len(msg) > 4096:
1767+
return await ctx.send(
1768+
embed=discord.Embed(
1769+
title="Error",
1770+
color=self.bot.error_color,
1771+
description="The resulting message is too long to fit in an embed description "
1772+
f"({len(msg)}/4096 characters). Please shorten your message or args.",
1773+
)
1774+
)
1775+
15391776
# Ensure logs record only the reply text, not the command.
15401777
ctx.message.content = msg
15411778
async with safe_typing(ctx):
@@ -1558,10 +1795,22 @@ async def fareply(self, ctx, *, msg: str = ""):
15581795
"""
15591796
msg = self.bot.formatter.format(
15601797
msg,
1798+
**self.bot.args,
15611799
channel=ctx.channel,
15621800
recipient=ctx.thread.recipient,
15631801
author=ctx.message.author,
15641802
)
1803+
1804+
if len(msg) > 4096:
1805+
return await ctx.send(
1806+
embed=discord.Embed(
1807+
title="Error",
1808+
color=self.bot.error_color,
1809+
description="The resulting message is too long to fit in an embed description "
1810+
f"({len(msg)}/4096 characters). Please shorten your message or args.",
1811+
)
1812+
)
1813+
15651814
# Ensure logs record only the reply text, not the command.
15661815
ctx.message.content = msg
15671816
async with safe_typing(ctx):
@@ -1584,10 +1833,22 @@ async def fpreply(self, ctx, *, msg: str = ""):
15841833
"""
15851834
msg = self.bot.formatter.format(
15861835
msg,
1836+
**self.bot.args,
15871837
channel=ctx.channel,
15881838
recipient=ctx.thread.recipient,
15891839
author=ctx.message.author,
15901840
)
1841+
1842+
if len(msg) > 4096:
1843+
return await ctx.send(
1844+
embed=discord.Embed(
1845+
title="Error",
1846+
color=self.bot.error_color,
1847+
description="The resulting message is too long to fit in an embed description "
1848+
f"({len(msg)}/4096 characters). Please shorten your message or args.",
1849+
)
1850+
)
1851+
15911852
# Ensure logs record only the reply text, not the command.
15921853
ctx.message.content = msg
15931854
async with safe_typing(ctx):
@@ -1610,10 +1871,22 @@ async def fpareply(self, ctx, *, msg: str = ""):
16101871
"""
16111872
msg = self.bot.formatter.format(
16121873
msg,
1874+
**self.bot.args,
16131875
channel=ctx.channel,
16141876
recipient=ctx.thread.recipient,
16151877
author=ctx.message.author,
16161878
)
1879+
1880+
if len(msg) > 4096:
1881+
return await ctx.send(
1882+
embed=discord.Embed(
1883+
title="Error",
1884+
color=self.bot.error_color,
1885+
description="The resulting message is too long to fit in an embed description "
1886+
f"({len(msg)}/4096 characters). Please shorten your message or args.",
1887+
)
1888+
)
1889+
16171890
# Ensure logs record only the reply text, not the command.
16181891
ctx.message.content = msg
16191892
async with safe_typing(ctx):

0 commit comments

Comments
 (0)