Skip to content

Commit

Permalink
Merge
Browse files Browse the repository at this point in the history
  • Loading branch information
fourjr committed Apr 28, 2022
2 parents 16816f3 + 893acc9 commit 499f9aa
Show file tree
Hide file tree
Showing 4 changed files with 181 additions and 31 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ however, insignificant breaking changes do not guarantee a major version bump, s
- Modmail now uses per-server avatars if applicable. ([GH #3048](https://github.com/kyb3r/modmail/issues/3048))
- Use discord relative timedeltas. ([GH #3046](https://github.com/kyb3r/modmail/issues/3046))
- Use discord native buttons for all paginator sessions.
- Snippets can be used in aliases. ([GH #3108](https://github.com/kyb3r/modmail/issues/3108), [PR #3124](https://github.com/kyb3r/modmail/pull/3124))

### Fixed

Expand Down
86 changes: 66 additions & 20 deletions bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
)
from core.thread import ThreadManager
from core.time import human_timedelta
from core.utils import normalize_alias, truncate, tryint
from core.utils import normalize_alias, parse_alias, truncate, tryint

logger = getLogger(__name__)

Expand Down Expand Up @@ -85,6 +85,30 @@ def __init__(self):
self.plugin_db = PluginDatabaseClient(self) # Deprecated
self.startup()

def _resolve_snippet(self, name: str) -> typing.Optional[str]:
"""
Get actual snippet names from direct aliases to snippets.
If the provided name is a snippet, it's returned unchanged.
If there is an alias by this name, it is parsed to see if it
refers only to a snippet, in which case that snippet name is
returned.
If no snippets were found, None is returned.
"""
if name in self.snippets:
return name

try:
(command,) = parse_alias(self.aliases[name])
except (KeyError, ValueError):
# There is either no alias by this name present or the
# alias has multiple steps.
pass
else:
if command in self.snippets:
return command

@property
def uptime(self) -> str:
now = discord.utils.utcnow()
Expand Down Expand Up @@ -935,6 +959,16 @@ async def process_dm_modmail(self, message: discord.Message) -> None:
await self.add_reaction(message, sent_emoji)
self.dispatch("thread_reply", thread, False, message, False, False)

def _get_snippet_command(self) -> commands.Command:
"""Get the correct reply command based on the snippet config"""
modifiers = "f"
if self.config["plain_snippets"]:
modifiers += "p"
if self.config["anonymous_snippets"]:
modifiers += "a"

return self.get_command(f"{modifiers}reply")

async def get_contexts(self, message, *, cls=commands.Context):
"""
Returns all invocation contexts from the message.
Expand All @@ -956,28 +990,54 @@ async def get_contexts(self, message, *, cls=commands.Context):

invoker = view.get_word().lower()

# Check if a snippet is being called.
# This needs to be done before checking for aliases since
# snippets can have multiple words.
try:
snippet_text = self.snippets[message.content.removeprefix(invoked_prefix)]
except KeyError:
snippet_text = None

# Check if there is any aliases being called.
alias = self.aliases.get(invoker)
if alias is not None:
if alias is not None and snippet_text is None:
ctxs = []
aliases = normalize_alias(alias, message.content[len(f"{invoked_prefix}{invoker}") :])
if not aliases:
logger.warning("Alias %s is invalid, removing.", invoker)
self.aliases.pop(invoker)

for alias in aliases:
view = StringView(invoked_prefix + alias)
command = None
try:
snippet_text = self.snippets[alias]
except KeyError:
command_invocation_text = alias
else:
command = self._get_snippet_command()
command_invocation_text = f"{invoked_prefix}{command} {snippet_text}"
view = StringView(invoked_prefix + command_invocation_text)
ctx_ = cls(prefix=self.prefix, view=view, bot=self, message=message)
ctx_.thread = thread
discord.utils.find(view.skip_string, prefixes)
ctx_.invoked_with = view.get_word().lower()
ctx_.command = self.all_commands.get(ctx_.invoked_with)
ctx_.command = command or self.all_commands.get(ctx_.invoked_with)
ctxs += [ctx_]
return ctxs

ctx.thread = thread
ctx.invoked_with = invoker
ctx.command = self.all_commands.get(invoker)

if snippet_text is not None:
# Process snippets
ctx.command = self._get_snippet_command()
reply_view = StringView(f"{invoked_prefix}{ctx.command} {snippet_text}")
discord.utils.find(reply_view.skip_string, prefixes)
ctx.invoked_with = reply_view.get_word().lower()
ctx.view = reply_view
else:
ctx.command = self.all_commands.get(invoker)
ctx.invoked_with = invoker

return [ctx]

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

if message.content.startswith(self.prefix):
cmd = message.content[len(self.prefix) :].strip()

# Process snippets
cmd = cmd.lower()
if cmd in self.snippets:
snippet = self.snippets[cmd]
modifiers = "f"
if self.config["plain_snippets"]:
modifiers += "p"
if self.config["anonymous_snippets"]:
modifiers += "a"
message.content = f"{self.prefix}{modifiers}reply {snippet}"

ctxs = await self.get_contexts(message)
for ctx in ctxs:
if ctx.command:
Expand Down
108 changes: 99 additions & 9 deletions cogs/modmail.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import discord
from discord.ext import commands
from discord.ext.commands.view import StringView
from discord.ext.commands.cooldowns import BucketType
from discord.role import Role
from discord.utils import escape_markdown
Expand Down Expand Up @@ -143,12 +144,14 @@ async def snippet(self, ctx, *, name: str.lower = None):
"""

if name is not None:
val = self.bot.snippets.get(name)
if val is None:
snippet_name = self.bot._resolve_snippet(name)

if snippet_name is None:
embed = create_not_found_embed(name, self.bot.snippets.keys(), "Snippet")
else:
val = self.bot.snippets[snippet_name]
embed = discord.Embed(
title=f'Snippet - "{name}":', description=val, color=self.bot.main_color
title=f'Snippet - "{snippet_name}":', description=val, color=self.bot.main_color
)
return await ctx.send(embed=embed)

Expand Down Expand Up @@ -177,13 +180,13 @@ async def snippet_raw(self, ctx, *, name: str.lower):
"""
View the raw content of a snippet.
"""
val = self.bot.snippets.get(name)
if val is None:
snippet_name = self.bot._resolve_snippet(name)
if snippet_name is None:
embed = create_not_found_embed(name, self.bot.snippets.keys(), "Snippet")
else:
val = truncate(escape_code_block(val), 2048 - 7)
val = truncate(escape_code_block(self.bot.snippets[snippet_name]), 2048 - 7)
embed = discord.Embed(
title=f'Raw snippet - "{name}":',
title=f'Raw snippet - "{snippet_name}":',
description=f"```\n{val}```",
color=self.bot.main_color,
)
Expand Down Expand Up @@ -246,16 +249,103 @@ async def snippet_add(self, ctx, name: str.lower, *, value: commands.clean_conte
)
return await ctx.send(embed=embed)

def _fix_aliases(self, snippet_being_deleted: str) -> tuple[list[str]]:
"""
Remove references to the snippet being deleted from aliases.
Direct aliases to snippets are deleted, and aliases having
other steps are edited.
A tuple of dictionaries are returned. The first dictionary
contains a mapping of alias names which were deleted to their
original value, and the second dictionary contains a mapping
of alias names which were edited to their original value.
"""
deleted = {}
edited = {}

# Using a copy since we might need to delete aliases
for alias, val in self.bot.aliases.copy().items():
values = parse_alias(val)

save_aliases = []

for val in values:
view = StringView(val)
linked_command = view.get_word().lower()
message = view.read_rest()

if linked_command == snippet_being_deleted:
continue

is_valid_snippet = snippet_being_deleted in self.bot.snippets

if not self.bot.get_command(linked_command) and not is_valid_snippet:
alias_command = self.bot.aliases[linked_command]
save_aliases.extend(normalize_alias(alias_command, message))
else:
save_aliases.append(val)

if not save_aliases:
original_value = self.bot.aliases.pop(alias)
deleted[alias] = original_value
else:
original_alias = self.bot.aliases[alias]
new_alias = " && ".join(f'"{a}"' for a in save_aliases)

if original_alias != new_alias:
self.bot.aliases[alias] = new_alias
edited[alias] = original_alias

return deleted, edited

@snippet.command(name="remove", aliases=["del", "delete"])
@checks.has_permissions(PermissionLevel.SUPPORTER)
async def snippet_remove(self, ctx, *, name: str.lower):
"""Remove a snippet."""

if name in self.bot.snippets:
deleted_aliases, edited_aliases = self._fix_aliases(name)

deleted_aliases_string = ",".join(f"`{alias}`" for alias in deleted_aliases)
if len(deleted_aliases) == 1:
deleted_aliases_output = f"The `{deleted_aliases_string}` direct alias has been removed."
elif deleted_aliases:
deleted_aliases_output = (
f"The following direct aliases have been removed: {deleted_aliases_string}."
)
else:
deleted_aliases_output = None

if len(edited_aliases) == 1:
alias, val = edited_aliases.popitem()
edited_aliases_output = (
f"Steps pointing to this snippet have been removed from the `{alias}` alias"
f" (previous value: `{val}`).`"
)
elif edited_aliases:
alias_list = "\n".join(
[
f"- `{alias_name}` (previous value: `{val}`)"
for alias_name, val in edited_aliases.items()
]
)
edited_aliases_output = (
f"Steps pointing to this snippet have been removed from the following aliases:"
f"\n\n{alias_list}"
)
else:
edited_aliases_output = None

description = f"Snippet `{name}` is now deleted."
if deleted_aliases_output:
description += f"\n\n{deleted_aliases_output}"
if edited_aliases_output:
description += f"\n\n{edited_aliases_output}"

embed = discord.Embed(
title="Removed snippet",
color=self.bot.main_color,
description=f"Snippet `{name}` is now deleted.",
description=description,
)
self.bot.snippets.pop(name)
await self.bot.config.update()
Expand Down
17 changes: 15 additions & 2 deletions cogs/utility.py
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,18 @@ async def send_error_message(self, error):
val = self.context.bot.snippets.get(command)
if val is not None:
embed = discord.Embed(title=f"{command} is a snippet.", color=self.context.bot.main_color)
embed.add_field(name=f"`{command}` will send:", value=val)
embed.add_field(name=f"`{command}` will send:", value=val, inline=False)

snippet_aliases = []
for alias in self.context.bot.aliases:
if self.context.bot._resolve_snippet(alias) == command:
snippet_aliases.append(f"`{alias}`")

if snippet_aliases:
embed.add_field(
name=f"Aliases to this snippet:", value=",".join(snippet_aliases), inline=False
)

return await self.get_destination().send(embed=embed)

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

if not self.bot.get_command(linked_command):
is_snippet = val in self.bot.snippets

if not self.bot.get_command(linked_command) and not is_snippet:
alias_command = self.bot.aliases.get(linked_command)
if alias_command is not None:
save_aliases.extend(utils.normalize_alias(alias_command, message))
Expand Down

0 comments on commit 499f9aa

Please sign in to comment.