Skip to content

Commit 27afe7d

Browse files
committed
Added ?autotrigger to specify keywords to trigger commands, resolve #130, resolve #649
1 parent 69d60ab commit 27afe7d

File tree

7 files changed

+205
-10
lines changed

7 files changed

+205
-10
lines changed

CHANGELOG.md

+4-3
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,10 @@ however, insignificant breaking changes do not guarantee a major version bump, s
2323
- Added a way to block roles. ([GH #2753](https://github.com/kyb3r/modmail/issues/2753))
2424
- Added `cooldown_thread_title`, `cooldown_thread_response` to customise message sent when user is on a creating thread cooldown. ([GH #2865](https://github.com/kyb3r/modmail/issues/2865))
2525
- Added `?selfcontact` to allow users to open a thread. ([GH #2762](https://github.com/kyb3r/modmail/issues/2762))
26-
- Support stickers and reject non-messages (i.e. pin_add)
27-
- Added support for thread titles, `?title` ([GH #2838](https://github.com/kyb3r/modmail/issues/2838))
28-
- Added `data_collection` to specify if bot metadata should be collected by Modmail developers
26+
- Support stickers and reject non-messages. (i.e. pin_add)
27+
- Added support for thread titles, `?title`. ([GH #2838](https://github.com/kyb3r/modmail/issues/2838))
28+
- Added `data_collection` to specify if bot metadata should be collected by Modmail developers.
29+
- Added `?autotrigger` to specify keywords to trigger commands. ([GH #130](https://github.com/kyb3r/modmail/issues/130), [GH #649](https://github.com/kyb3r/modmail/issues/649))
2930

3031
### Fixed
3132

bot.py

+50
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33

44
import asyncio
5+
import copy
56
import logging
67
import os
78
import re
@@ -244,6 +245,10 @@ def snippets(self) -> typing.Dict[str, str]:
244245
def aliases(self) -> typing.Dict[str, str]:
245246
return self.config["aliases"]
246247

248+
@property
249+
def auto_triggers(self) -> typing.Dict[str, str]:
250+
return self.config["auto_triggers"]
251+
247252
@property
248253
def token(self) -> str:
249254
token = self.config["token"]
@@ -852,6 +857,51 @@ async def get_contexts(self, message, *, cls=commands.Context):
852857
ctx.command = self.all_commands.get(invoker)
853858
return [ctx]
854859

860+
async def trigger_auto_triggers(self, message, channel, *, cls=commands.Context):
861+
message.author = self.modmail_guild.me
862+
message.channel = channel
863+
864+
view = StringView(message.content)
865+
ctx = cls(prefix=self.prefix, view=view, bot=self, message=message)
866+
thread = await self.threads.find(channel=ctx.channel)
867+
868+
invoked_prefix = self.prefix
869+
invoker = view.get_word().lower()
870+
871+
# Check if there is any aliases being called.
872+
alias = self.auto_triggers[
873+
next(filter(lambda x: x in message.content, self.auto_triggers.keys()))
874+
]
875+
if alias is None:
876+
ctx.thread = thread
877+
ctx.invoked_with = invoker
878+
ctx.command = self.all_commands.get(invoker)
879+
ctxs = [ctx]
880+
else:
881+
ctxs = []
882+
aliases = normalize_alias(alias, message.content[len(f"{invoked_prefix}{invoker}") :])
883+
if not aliases:
884+
logger.warning("Alias %s is invalid as called in automove.", invoker)
885+
886+
for alias in aliases:
887+
view = StringView(invoked_prefix + alias)
888+
ctx_ = cls(prefix=self.prefix, view=view, bot=self, message=message)
889+
ctx_.thread = thread
890+
discord.utils.find(view.skip_string, await self.get_prefix())
891+
ctx_.invoked_with = view.get_word().lower()
892+
ctx_.command = self.all_commands.get(ctx_.invoked_with)
893+
ctxs += [ctx_]
894+
895+
for ctx in ctxs:
896+
if ctx.command:
897+
old_checks = copy.copy(ctx.command.checks)
898+
ctx.command.checks = [checks.has_permissions(PermissionLevel.INVALID)]
899+
900+
await self.invoke(ctx)
901+
902+
ctx.command.checks = old_checks
903+
continue
904+
855905
async def get_context(self, message, *, cls=commands.Context):
856906
"""
857907
Returns the invocation context from the message.

cogs/modmail.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -338,7 +338,10 @@ async def move(self, ctx, *, arguments):
338338
await thread.channel.send(f"{mention}, thread has been moved.")
339339

340340
sent_emoji, _ = await self.bot.retrieve_emoji()
341-
await self.bot.add_reaction(ctx.message, sent_emoji)
341+
try:
342+
await self.bot.add_reaction(ctx.message, sent_emoji)
343+
except discord.NotFound:
344+
pass
342345

343346
async def send_scheduled_close_message(self, ctx, after, silent=False):
344347
human_delta = human_timedelta(after.dt)

cogs/utility.py

+120-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,13 @@
2222

2323
from core import checks, utils
2424
from core.changelog import Changelog
25-
from core.models import InvalidConfigError, PermissionLevel, UnseenFormatter, getLogger
25+
from core.models import (
26+
InvalidConfigError,
27+
PermissionLevel,
28+
SimilarCategoryConverter,
29+
UnseenFormatter,
30+
getLogger,
31+
)
2632
from core.paginator import EmbedPaginatorSession, MessagePaginatorSession
2733

2834

@@ -1698,6 +1704,119 @@ async def oauth_show(self, ctx):
16981704

16991705
await ctx.send(embed=embed)
17001706

1707+
@commands.group(invoke_without_command=True)
1708+
@checks.has_permissions(PermissionLevel.OWNER)
1709+
async def autotrigger(self, ctx):
1710+
"""Automatically trigger alias-like commands based on a certain keyword"""
1711+
await ctx.send_help(ctx.command)
1712+
1713+
@autotrigger.command(name="add")
1714+
@checks.has_permissions(PermissionLevel.OWNER)
1715+
async def autotrigger_add(self, ctx, keyword, *, command):
1716+
"""Adds a trigger to automatically trigger an alias-like command"""
1717+
if keyword in self.bot.auto_triggers:
1718+
embed = discord.Embed(
1719+
title="Error",
1720+
color=self.bot.error_color,
1721+
description=f"Another autotrigger with the same name already exists: `{name}`.",
1722+
)
1723+
else:
1724+
self.bot.auto_triggers[keyword] = command
1725+
await self.bot.config.update()
1726+
1727+
embed = discord.Embed(
1728+
title="Success",
1729+
color=self.bot.main_color,
1730+
description=f"Keyword `{keyword}` has been linked to `{command}`.",
1731+
)
1732+
1733+
await ctx.send(embed=embed)
1734+
1735+
@autotrigger.command(name="edit")
1736+
@checks.has_permissions(PermissionLevel.OWNER)
1737+
async def autotrigger_edit(self, ctx, keyword, *, command):
1738+
"""Edits a pre-existing trigger to automatically trigger an alias-like command"""
1739+
if keyword not in self.bot.auto_triggers:
1740+
embed = utils.create_not_found_embed(
1741+
keyword, self.bot.auto_triggers.keys(), "Autotrigger"
1742+
)
1743+
else:
1744+
self.bot.auto_triggers[keyword] = command
1745+
await self.bot.config.update()
1746+
1747+
embed = discord.Embed(
1748+
title="Success",
1749+
color=self.bot.main_color,
1750+
description=f"Keyword `{keyword}` has been linked to `{command}`.",
1751+
)
1752+
1753+
await ctx.send(embed=embed)
1754+
1755+
@autotrigger.command(name="remove")
1756+
@checks.has_permissions(PermissionLevel.OWNER)
1757+
async def autotrigger_remove(self, ctx, keyword):
1758+
"""Removes a trigger to automatically trigger an alias-like command"""
1759+
try:
1760+
del self.bot.auto_triggers[keyword]
1761+
except KeyError:
1762+
embed = discord.Embed(
1763+
title="Error",
1764+
color=self.bot.error_color,
1765+
description=f"Keyword `{keyword}` could not be found.",
1766+
)
1767+
await ctx.send(embed=embed)
1768+
else:
1769+
await self.bot.config.update()
1770+
1771+
embed = discord.Embed(
1772+
title="Success",
1773+
color=self.bot.main_color,
1774+
description=f"Keyword `{keyword}` has been removed.",
1775+
)
1776+
await ctx.send(embed=embed)
1777+
1778+
@autotrigger.command(name="test")
1779+
@checks.has_permissions(PermissionLevel.OWNER)
1780+
async def autotrigger_test(self, ctx, *, text):
1781+
"""Tests a string against the current autotrigger setup"""
1782+
for keyword in list(self.bot.auto_triggers):
1783+
if keyword in text:
1784+
alias = self.bot.auto_triggers[keyword]
1785+
embed = discord.Embed(
1786+
title="Keyword Found",
1787+
color=self.bot.main_color,
1788+
description=f"autotrigger keyword `{keyword}` found. Command executed: `{alias}`",
1789+
)
1790+
return await ctx.send(embed=embed)
1791+
1792+
embed = discord.Embed(
1793+
title="Keyword Not Found",
1794+
color=self.bot.error_color,
1795+
description=f"No autotrigger keyword found. Thread will stay in {self.bot.main_category}.",
1796+
)
1797+
return await ctx.send(embed=embed)
1798+
1799+
@autotrigger.command(name="list")
1800+
@checks.has_permissions(PermissionLevel.OWNER)
1801+
async def autotrigger_list(self, ctx):
1802+
"""Lists all autotriggers set up"""
1803+
embeds = []
1804+
for keyword in list(self.bot.auto_triggers):
1805+
command = self.bot.auto_triggers[keyword]
1806+
embed = discord.Embed(title=keyword, color=self.bot.main_color, description=command,)
1807+
embeds.append(embed)
1808+
1809+
if not embeds:
1810+
embeds.append(
1811+
discord.Embed(
1812+
title="No autotrigger set",
1813+
color=self.bot.error_color,
1814+
description=f"Use `{self.bot.prefix}autotrigger add` to add new autotriggers.",
1815+
)
1816+
)
1817+
1818+
await EmbedPaginatorSession(ctx, *embeds).run()
1819+
17011820
@commands.command(hidden=True, name="eval")
17021821
@checks.has_permissions(PermissionLevel.OWNER)
17031822
async def eval_(self, ctx, *, body: str):

core/config.py

+1
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ class ConfigManager:
117117
# misc
118118
"plugins": [],
119119
"aliases": {},
120+
"auto_triggers": {},
120121
}
121122

122123
protected_keys = {

core/models.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -196,8 +196,11 @@ async def convert(self, ctx, argument):
196196
try:
197197
return await super().convert(ctx, argument)
198198
except commands.ChannelNotFound:
199+
199200
def check(c):
200-
return isinstance(c, discord.CategoryChannel) and c.name.lower().startswith(argument.lower())
201+
return isinstance(c, discord.CategoryChannel) and c.name.lower().startswith(
202+
argument.lower()
203+
)
201204

202205
if guild:
203206
result = discord.utils.find(check, guild.categories)

core/thread.py

+22-4
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import asyncio
2+
import copy
23
import io
34
import re
45
import typing
@@ -10,7 +11,8 @@
1011
import discord
1112
from discord.ext.commands import MissingRequiredArgument, CommandError
1213

13-
from core.models import getLogger
14+
from core import checks
15+
from core.models import PermissionLevel, getLogger
1416
from core.time import human_timedelta
1517
from core.utils import (
1618
is_image_url,
@@ -101,7 +103,7 @@ def cancelled(self, flag: bool):
101103
for i in self.wait_tasks:
102104
i.cancel()
103105

104-
async def setup(self, *, creator=None, category=None):
106+
async def setup(self, *, creator=None, category=None, initial_message=None):
105107
"""Create the thread channel and other io related initialisation tasks"""
106108
self.bot.dispatch("thread_initiate", self)
107109
recipient = self.recipient
@@ -197,7 +199,19 @@ async def send_recipient_genesis_message():
197199
close_emoji = await self.bot.convert_emoji(close_emoji)
198200
await self.bot.add_reaction(msg, close_emoji)
199201

200-
await asyncio.gather(send_genesis_message(), send_recipient_genesis_message())
202+
async def activate_auto_triggers():
203+
message = copy.copy(initial_message)
204+
if message:
205+
for keyword in list(self.bot.auto_triggers):
206+
if keyword in message.content:
207+
try:
208+
return await self.bot.trigger_auto_triggers(message, channel)
209+
except StopIteration:
210+
pass
211+
212+
await asyncio.gather(
213+
send_genesis_message(), send_recipient_genesis_message(), activate_auto_triggers(),
214+
)
201215
self.bot.dispatch("thread_ready", self)
202216

203217
def _format_info_embed(self, user, log_url, log_count, color):
@@ -880,6 +894,8 @@ async def send(
880894
if delete_message and destination == self.channel:
881895
try:
882896
await message.delete()
897+
except discord.NotFound:
898+
pass
883899
except Exception as e:
884900
logger.warning("Cannot delete message: %s.", e)
885901

@@ -1138,7 +1154,9 @@ async def create(
11381154
del self.cache[recipient.id]
11391155
return thread
11401156

1141-
self.bot.loop.create_task(thread.setup(creator=creator, category=category))
1157+
self.bot.loop.create_task(
1158+
thread.setup(creator=creator, category=category, initial_message=message)
1159+
)
11421160
return thread
11431161

11441162
async def find_or_create(self, recipient) -> Thread:

0 commit comments

Comments
 (0)