Skip to content

Commit 499f9aa

Browse files
committed
Merge
2 parents 16816f3 + 893acc9 commit 499f9aa

File tree

4 files changed

+181
-31
lines changed

4 files changed

+181
-31
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ however, insignificant breaking changes do not guarantee a major version bump, s
3131
- Modmail now uses per-server avatars if applicable. ([GH #3048](https://github.com/kyb3r/modmail/issues/3048))
3232
- Use discord relative timedeltas. ([GH #3046](https://github.com/kyb3r/modmail/issues/3046))
3333
- Use discord native buttons for all paginator sessions.
34+
- Snippets can be used in aliases. ([GH #3108](https://github.com/kyb3r/modmail/issues/3108), [PR #3124](https://github.com/kyb3r/modmail/pull/3124))
3435

3536
### Fixed
3637

bot.py

Lines changed: 66 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@
4646
)
4747
from core.thread import ThreadManager
4848
from core.time import human_timedelta
49-
from core.utils import normalize_alias, truncate, tryint
49+
from core.utils import normalize_alias, parse_alias, truncate, tryint
5050

5151
logger = getLogger(__name__)
5252

@@ -85,6 +85,30 @@ def __init__(self):
8585
self.plugin_db = PluginDatabaseClient(self) # Deprecated
8686
self.startup()
8787

88+
def _resolve_snippet(self, name: str) -> typing.Optional[str]:
89+
"""
90+
Get actual snippet names from direct aliases to snippets.
91+
92+
If the provided name is a snippet, it's returned unchanged.
93+
If there is an alias by this name, it is parsed to see if it
94+
refers only to a snippet, in which case that snippet name is
95+
returned.
96+
97+
If no snippets were found, None is returned.
98+
"""
99+
if name in self.snippets:
100+
return name
101+
102+
try:
103+
(command,) = parse_alias(self.aliases[name])
104+
except (KeyError, ValueError):
105+
# There is either no alias by this name present or the
106+
# alias has multiple steps.
107+
pass
108+
else:
109+
if command in self.snippets:
110+
return command
111+
88112
@property
89113
def uptime(self) -> str:
90114
now = discord.utils.utcnow()
@@ -935,6 +959,16 @@ async def process_dm_modmail(self, message: discord.Message) -> None:
935959
await self.add_reaction(message, sent_emoji)
936960
self.dispatch("thread_reply", thread, False, message, False, False)
937961

962+
def _get_snippet_command(self) -> commands.Command:
963+
"""Get the correct reply command based on the snippet config"""
964+
modifiers = "f"
965+
if self.config["plain_snippets"]:
966+
modifiers += "p"
967+
if self.config["anonymous_snippets"]:
968+
modifiers += "a"
969+
970+
return self.get_command(f"{modifiers}reply")
971+
938972
async def get_contexts(self, message, *, cls=commands.Context):
939973
"""
940974
Returns all invocation contexts from the message.
@@ -956,28 +990,54 @@ async def get_contexts(self, message, *, cls=commands.Context):
956990

957991
invoker = view.get_word().lower()
958992

993+
# Check if a snippet is being called.
994+
# This needs to be done before checking for aliases since
995+
# snippets can have multiple words.
996+
try:
997+
snippet_text = self.snippets[message.content.removeprefix(invoked_prefix)]
998+
except KeyError:
999+
snippet_text = None
1000+
9591001
# Check if there is any aliases being called.
9601002
alias = self.aliases.get(invoker)
961-
if alias is not None:
1003+
if alias is not None and snippet_text is None:
9621004
ctxs = []
9631005
aliases = normalize_alias(alias, message.content[len(f"{invoked_prefix}{invoker}") :])
9641006
if not aliases:
9651007
logger.warning("Alias %s is invalid, removing.", invoker)
9661008
self.aliases.pop(invoker)
9671009

9681010
for alias in aliases:
969-
view = StringView(invoked_prefix + alias)
1011+
command = None
1012+
try:
1013+
snippet_text = self.snippets[alias]
1014+
except KeyError:
1015+
command_invocation_text = alias
1016+
else:
1017+
command = self._get_snippet_command()
1018+
command_invocation_text = f"{invoked_prefix}{command} {snippet_text}"
1019+
view = StringView(invoked_prefix + command_invocation_text)
9701020
ctx_ = cls(prefix=self.prefix, view=view, bot=self, message=message)
9711021
ctx_.thread = thread
9721022
discord.utils.find(view.skip_string, prefixes)
9731023
ctx_.invoked_with = view.get_word().lower()
974-
ctx_.command = self.all_commands.get(ctx_.invoked_with)
1024+
ctx_.command = command or self.all_commands.get(ctx_.invoked_with)
9751025
ctxs += [ctx_]
9761026
return ctxs
9771027

9781028
ctx.thread = thread
979-
ctx.invoked_with = invoker
980-
ctx.command = self.all_commands.get(invoker)
1029+
1030+
if snippet_text is not None:
1031+
# Process snippets
1032+
ctx.command = self._get_snippet_command()
1033+
reply_view = StringView(f"{invoked_prefix}{ctx.command} {snippet_text}")
1034+
discord.utils.find(reply_view.skip_string, prefixes)
1035+
ctx.invoked_with = reply_view.get_word().lower()
1036+
ctx.view = reply_view
1037+
else:
1038+
ctx.command = self.all_commands.get(invoker)
1039+
ctx.invoked_with = invoker
1040+
9811041
return [ctx]
9821042

9831043
async def trigger_auto_triggers(self, message, channel, *, cls=commands.Context):
@@ -1119,20 +1179,6 @@ async def process_commands(self, message):
11191179
if isinstance(message.channel, discord.DMChannel):
11201180
return await self.process_dm_modmail(message)
11211181

1122-
if message.content.startswith(self.prefix):
1123-
cmd = message.content[len(self.prefix) :].strip()
1124-
1125-
# Process snippets
1126-
cmd = cmd.lower()
1127-
if cmd in self.snippets:
1128-
snippet = self.snippets[cmd]
1129-
modifiers = "f"
1130-
if self.config["plain_snippets"]:
1131-
modifiers += "p"
1132-
if self.config["anonymous_snippets"]:
1133-
modifiers += "a"
1134-
message.content = f"{self.prefix}{modifiers}reply {snippet}"
1135-
11361182
ctxs = await self.get_contexts(message)
11371183
for ctx in ctxs:
11381184
if ctx.command:

cogs/modmail.py

Lines changed: 99 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
import discord
99
from discord.ext import commands
10+
from discord.ext.commands.view import StringView
1011
from discord.ext.commands.cooldowns import BucketType
1112
from discord.role import Role
1213
from discord.utils import escape_markdown
@@ -143,12 +144,14 @@ async def snippet(self, ctx, *, name: str.lower = None):
143144
"""
144145

145146
if name is not None:
146-
val = self.bot.snippets.get(name)
147-
if val is None:
147+
snippet_name = self.bot._resolve_snippet(name)
148+
149+
if snippet_name is None:
148150
embed = create_not_found_embed(name, self.bot.snippets.keys(), "Snippet")
149151
else:
152+
val = self.bot.snippets[snippet_name]
150153
embed = discord.Embed(
151-
title=f'Snippet - "{name}":', description=val, color=self.bot.main_color
154+
title=f'Snippet - "{snippet_name}":', description=val, color=self.bot.main_color
152155
)
153156
return await ctx.send(embed=embed)
154157

@@ -177,13 +180,13 @@ async def snippet_raw(self, ctx, *, name: str.lower):
177180
"""
178181
View the raw content of a snippet.
179182
"""
180-
val = self.bot.snippets.get(name)
181-
if val is None:
183+
snippet_name = self.bot._resolve_snippet(name)
184+
if snippet_name is None:
182185
embed = create_not_found_embed(name, self.bot.snippets.keys(), "Snippet")
183186
else:
184-
val = truncate(escape_code_block(val), 2048 - 7)
187+
val = truncate(escape_code_block(self.bot.snippets[snippet_name]), 2048 - 7)
185188
embed = discord.Embed(
186-
title=f'Raw snippet - "{name}":',
189+
title=f'Raw snippet - "{snippet_name}":',
187190
description=f"```\n{val}```",
188191
color=self.bot.main_color,
189192
)
@@ -246,16 +249,103 @@ async def snippet_add(self, ctx, name: str.lower, *, value: commands.clean_conte
246249
)
247250
return await ctx.send(embed=embed)
248251

252+
def _fix_aliases(self, snippet_being_deleted: str) -> tuple[list[str]]:
253+
"""
254+
Remove references to the snippet being deleted from aliases.
255+
256+
Direct aliases to snippets are deleted, and aliases having
257+
other steps are edited.
258+
259+
A tuple of dictionaries are returned. The first dictionary
260+
contains a mapping of alias names which were deleted to their
261+
original value, and the second dictionary contains a mapping
262+
of alias names which were edited to their original value.
263+
"""
264+
deleted = {}
265+
edited = {}
266+
267+
# Using a copy since we might need to delete aliases
268+
for alias, val in self.bot.aliases.copy().items():
269+
values = parse_alias(val)
270+
271+
save_aliases = []
272+
273+
for val in values:
274+
view = StringView(val)
275+
linked_command = view.get_word().lower()
276+
message = view.read_rest()
277+
278+
if linked_command == snippet_being_deleted:
279+
continue
280+
281+
is_valid_snippet = snippet_being_deleted in self.bot.snippets
282+
283+
if not self.bot.get_command(linked_command) and not is_valid_snippet:
284+
alias_command = self.bot.aliases[linked_command]
285+
save_aliases.extend(normalize_alias(alias_command, message))
286+
else:
287+
save_aliases.append(val)
288+
289+
if not save_aliases:
290+
original_value = self.bot.aliases.pop(alias)
291+
deleted[alias] = original_value
292+
else:
293+
original_alias = self.bot.aliases[alias]
294+
new_alias = " && ".join(f'"{a}"' for a in save_aliases)
295+
296+
if original_alias != new_alias:
297+
self.bot.aliases[alias] = new_alias
298+
edited[alias] = original_alias
299+
300+
return deleted, edited
301+
249302
@snippet.command(name="remove", aliases=["del", "delete"])
250303
@checks.has_permissions(PermissionLevel.SUPPORTER)
251304
async def snippet_remove(self, ctx, *, name: str.lower):
252305
"""Remove a snippet."""
253-
254306
if name in self.bot.snippets:
307+
deleted_aliases, edited_aliases = self._fix_aliases(name)
308+
309+
deleted_aliases_string = ",".join(f"`{alias}`" for alias in deleted_aliases)
310+
if len(deleted_aliases) == 1:
311+
deleted_aliases_output = f"The `{deleted_aliases_string}` direct alias has been removed."
312+
elif deleted_aliases:
313+
deleted_aliases_output = (
314+
f"The following direct aliases have been removed: {deleted_aliases_string}."
315+
)
316+
else:
317+
deleted_aliases_output = None
318+
319+
if len(edited_aliases) == 1:
320+
alias, val = edited_aliases.popitem()
321+
edited_aliases_output = (
322+
f"Steps pointing to this snippet have been removed from the `{alias}` alias"
323+
f" (previous value: `{val}`).`"
324+
)
325+
elif edited_aliases:
326+
alias_list = "\n".join(
327+
[
328+
f"- `{alias_name}` (previous value: `{val}`)"
329+
for alias_name, val in edited_aliases.items()
330+
]
331+
)
332+
edited_aliases_output = (
333+
f"Steps pointing to this snippet have been removed from the following aliases:"
334+
f"\n\n{alias_list}"
335+
)
336+
else:
337+
edited_aliases_output = None
338+
339+
description = f"Snippet `{name}` is now deleted."
340+
if deleted_aliases_output:
341+
description += f"\n\n{deleted_aliases_output}"
342+
if edited_aliases_output:
343+
description += f"\n\n{edited_aliases_output}"
344+
255345
embed = discord.Embed(
256346
title="Removed snippet",
257347
color=self.bot.main_color,
258-
description=f"Snippet `{name}` is now deleted.",
348+
description=description,
259349
)
260350
self.bot.snippets.pop(name)
261351
await self.bot.config.update()

cogs/utility.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,18 @@ async def send_error_message(self, error):
184184
val = self.context.bot.snippets.get(command)
185185
if val is not None:
186186
embed = discord.Embed(title=f"{command} is a snippet.", color=self.context.bot.main_color)
187-
embed.add_field(name=f"`{command}` will send:", value=val)
187+
embed.add_field(name=f"`{command}` will send:", value=val, inline=False)
188+
189+
snippet_aliases = []
190+
for alias in self.context.bot.aliases:
191+
if self.context.bot._resolve_snippet(alias) == command:
192+
snippet_aliases.append(f"`{alias}`")
193+
194+
if snippet_aliases:
195+
embed.add_field(
196+
name=f"Aliases to this snippet:", value=",".join(snippet_aliases), inline=False
197+
)
198+
188199
return await self.get_destination().send(embed=embed)
189200

190201
val = self.context.bot.aliases.get(command)
@@ -1070,7 +1081,9 @@ async def make_alias(self, name, value, action):
10701081
linked_command = view.get_word().lower()
10711082
message = view.read_rest()
10721083

1073-
if not self.bot.get_command(linked_command):
1084+
is_snippet = val in self.bot.snippets
1085+
1086+
if not self.bot.get_command(linked_command) and not is_snippet:
10741087
alias_command = self.bot.aliases.get(linked_command)
10751088
if alias_command is not None:
10761089
save_aliases.extend(utils.normalize_alias(alias_command, message))

0 commit comments

Comments
 (0)