Skip to content

Commit 1564e1f

Browse files
committed
Multistep alias
1 parent b3e8f62 commit 1564e1f

File tree

6 files changed

+273
-82
lines changed

6 files changed

+273
-82
lines changed

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,10 @@ dmypy.json
124124
# VS Code
125125
.vscode/
126126

127+
# Node
128+
package-lock.json
129+
node_modules/
130+
127131
# Modmail
128132
config.json
129133
plugins/

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1919
- New configuration variable `thread_self_closable_creation_footer` — the footer when `recipient_thread_close` is enabled.
2020
- Added a minimalistic version of requirements.txt (named requirements.min.txt) that contains only the absolute minimum of Modmail.
2121
- For users having trouble with pipenv or any other reason.
22+
- Multi-step alias, see `?help alias add`. Public beta testing, might be unstable.
2223

2324
### Changes
2425

bot.py

Lines changed: 89 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@
66
import re
77
import sys
88
import typing
9-
109
from datetime import datetime
10+
from itertools import zip_longest
1111
from types import SimpleNamespace
1212

1313
import discord
@@ -30,7 +30,7 @@
3030

3131
from core.clients import ApiClient, PluginDatabaseClient
3232
from core.config import ConfigManager
33-
from core.utils import human_join, strtobool
33+
from core.utils import human_join, strtobool, parse_alias
3434
from core.models import PermissionLevel, ModmailLogger
3535
from core.thread import ThreadManager
3636
from core.time import human_timedelta
@@ -669,24 +669,76 @@ async def _process_blocked(self, message: discord.Message) -> bool:
669669
logger.warning("Failed to add reaction %s.", reaction, exc_info=True)
670670
return str(message.author.id) in self.blocked_users
671671

672-
async def process_modmail(self, message: discord.Message) -> None:
672+
async def process_dm_modmail(self, message: discord.Message) -> None:
673673
"""Processes messages sent to the bot."""
674-
await self.wait_for_connected()
675-
676674
blocked = await self._process_blocked(message)
677675
if not blocked:
678676
thread = await self.threads.find_or_create(message.author)
679677
await thread.send(message)
680678

679+
async def get_contexts(self, message, *, cls=commands.Context):
680+
"""
681+
Returns all invocation contexts from the message.
682+
Supports getting the prefix from database as well as command aliases.
683+
"""
684+
685+
view = StringView(message.content)
686+
ctx = cls(prefix=self.prefix, view=view, bot=self, message=message)
687+
ctx.thread = await self.threads.find(channel=ctx.channel)
688+
689+
if self._skip_check(message.author.id, self.user.id):
690+
return [ctx]
691+
692+
prefixes = await self.get_prefix()
693+
694+
invoked_prefix = discord.utils.find(view.skip_string, prefixes)
695+
if invoked_prefix is None:
696+
return [ctx]
697+
698+
invoker = view.get_word().lower()
699+
700+
# Check if there is any aliases being called.
701+
alias = self.aliases.get(invoker)
702+
if alias is not None:
703+
aliases = parse_alias(alias)
704+
if not aliases:
705+
logger.warning("Alias %s is invalid, removing.", invoker)
706+
self.aliases.pop(invoker)
707+
else:
708+
len_ = len(f"{invoked_prefix}{invoker}")
709+
contents = parse_alias(message.content[len_:])
710+
if not contents:
711+
contents = [message.content[len_:]]
712+
713+
ctxs = []
714+
for alias, content in zip_longest(aliases, contents):
715+
if alias is None:
716+
break
717+
ctx = cls(prefix=self.prefix, view=view, bot=self, message=message)
718+
ctx.thread = await self.threads.find(channel=ctx.channel)
719+
720+
if content is not None:
721+
view = StringView(f"{alias} {content.strip()}")
722+
else:
723+
view = StringView(alias)
724+
ctx.view = view
725+
ctx.invoked_with = view.get_word()
726+
ctx.command = self.all_commands.get(ctx.invoked_with)
727+
ctxs += [ctx]
728+
return ctxs
729+
730+
ctx.invoked_with = invoker
731+
ctx.command = self.all_commands.get(invoker)
732+
return [ctx]
733+
681734
async def get_context(self, message, *, cls=commands.Context):
682735
"""
683736
Returns the invocation context from the message.
684-
Supports getting the prefix from database as well as command aliases.
737+
Supports getting the prefix from database.
685738
"""
686-
await self.wait_for_connected()
687739

688740
view = StringView(message.content)
689-
ctx = cls(prefix=None, view=view, bot=self, message=message)
741+
ctx = cls(prefix=self.prefix, view=view, bot=self, message=message)
690742

691743
if self._skip_check(message.author.id, self.user.id):
692744
return ctx
@@ -701,17 +753,7 @@ async def get_context(self, message, *, cls=commands.Context):
701753

702754
invoker = view.get_word().lower()
703755

704-
# Check if there is any aliases being called.
705-
alias = self.aliases.get(invoker)
706-
if alias is not None:
707-
ctx._alias_invoked = True # pylint: disable=W0212
708-
len_ = len(f"{invoked_prefix}{invoker}")
709-
view = StringView(f"{alias}{ctx.message.content[len_:]}")
710-
ctx.view = view
711-
invoker = view.get_word()
712-
713756
ctx.invoked_with = invoker
714-
ctx.prefix = self.prefix # Sane prefix (No mentions)
715757
ctx.command = self.all_commands.get(invoker)
716758

717759
return ctx
@@ -739,47 +781,52 @@ async def update_perms(
739781

740782
async def on_message(self, message):
741783
await self.wait_for_connected()
742-
743784
if message.type == discord.MessageType.pins_add and message.author == self.user:
744785
await message.delete()
786+
await self.process_commands(message)
745787

788+
async def process_commands(self, message):
746789
if message.author.bot:
747790
return
748791

749792
if isinstance(message.channel, discord.DMChannel):
750-
return await self.process_modmail(message)
793+
return await self.process_dm_modmail(message)
751794

752-
prefix = self.prefix
795+
if message.content.startswith(self.prefix):
796+
cmd = message.content[len(self.prefix) :].strip()
753797

754-
if message.content.startswith(prefix):
755-
cmd = message.content[len(prefix) :].strip()
798+
# Process snippets
756799
if cmd in self.snippets:
757800
thread = await self.threads.find(channel=message.channel)
758801
snippet = self.snippets[cmd]
759802
if thread:
760803
snippet = snippet.format(recipient=thread.recipient)
761-
message.content = f"{prefix}reply {snippet}"
804+
message.content = f"{self.prefix}reply {snippet}"
762805

763-
ctx = await self.get_context(message)
764-
if ctx.command:
765-
return await self.invoke(ctx)
806+
ctxs = await self.get_contexts(message)
807+
for ctx in ctxs:
808+
if ctx.command:
809+
await self.invoke(ctx)
810+
continue
766811

767-
thread = await self.threads.find(channel=ctx.channel)
768-
if thread is not None:
769-
try:
770-
reply_without_command = strtobool(self.config["reply_without_command"])
771-
except ValueError:
772-
reply_without_command = self.config.remove("reply_without_command")
812+
thread = await self.threads.find(channel=ctx.channel)
813+
if thread is not None:
814+
try:
815+
reply_without_command = strtobool(
816+
self.config["reply_without_command"]
817+
)
818+
except ValueError:
819+
reply_without_command = self.config.remove("reply_without_command")
773820

774-
if reply_without_command:
775-
await thread.reply(message)
776-
else:
777-
await self.api.append_log(message, type_="internal")
778-
elif ctx.invoked_with:
779-
exc = commands.CommandNotFound(
780-
'Command "{}" is not found'.format(ctx.invoked_with)
781-
)
782-
self.dispatch("command_error", ctx, exc)
821+
if reply_without_command:
822+
await thread.reply(message)
823+
else:
824+
await self.api.append_log(message, type_="internal")
825+
elif ctx.invoked_with:
826+
exc = commands.CommandNotFound(
827+
'Command "{}" is not found'.format(ctx.invoked_with)
828+
)
829+
self.dispatch("command_error", ctx, exc)
783830

784831
async def on_typing(self, channel, user, _):
785832
await self.wait_for_connected()

cogs/modmail.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,9 @@ async def snippet(self, ctx, *, name: str.lower = None):
127127
if name is not None:
128128
val = self.bot.snippets.get(name)
129129
if val is None:
130-
embed = create_not_found_embed(name, self.bot.snippets.keys(), 'Snippet')
130+
embed = create_not_found_embed(
131+
name, self.bot.snippets.keys(), "Snippet"
132+
)
131133
return await ctx.send(embed=embed)
132134
return await ctx.send(escape_mentions(val))
133135

@@ -163,9 +165,9 @@ async def snippet(self, ctx, *, name: str.lower = None):
163165
async def snippet_raw(self, ctx, *, name: str.lower):
164166
val = self.bot.snippets.get(name)
165167
if val is None:
166-
embed = create_not_found_embed(name, self.bot.snippets.keys(), 'Snippet')
168+
embed = create_not_found_embed(name, self.bot.snippets.keys(), "Snippet")
167169
return await ctx.send(embed=embed)
168-
return await ctx.send(escape_markdown(escape_mentions(val)).replace('<', '\\<'))
170+
return await ctx.send(escape_markdown(escape_mentions(val)).replace("<", "\\<"))
169171

170172
@snippet.command(name="add")
171173
@checks.has_permissions(PermissionLevel.SUPPORTER)
@@ -207,7 +209,7 @@ async def snippet_add(self, ctx, name: str.lower, *, value: commands.clean_conte
207209
embed = discord.Embed(
208210
title="Added snippet",
209211
color=self.bot.main_color,
210-
description=f'Successfully created snippet.',
212+
description=f"Successfully created snippet.",
211213
)
212214
return await ctx.send(embed=embed)
213215

@@ -225,7 +227,7 @@ async def snippet_remove(self, ctx, *, name: str.lower):
225227
self.bot.snippets.pop(name)
226228
await self.bot.config.update()
227229
else:
228-
embed = create_not_found_embed(name, self.bot.snippets.keys(), 'Snippet')
230+
embed = create_not_found_embed(name, self.bot.snippets.keys(), "Snippet")
229231
await ctx.send(embed=embed)
230232

231233
@snippet.command(name="edit")
@@ -248,7 +250,7 @@ async def snippet_edit(self, ctx, name: str.lower, *, value):
248250
description=f'`{name}` will now send "{value}".',
249251
)
250252
else:
251-
embed = create_not_found_embed(name, self.bot.snippets.keys(), 'Snippet')
253+
embed = create_not_found_embed(name, self.bot.snippets.keys(), "Snippet")
252254
await ctx.send(embed=embed)
253255

254256
@commands.command()

0 commit comments

Comments
 (0)