Skip to content

Commit 69541b7

Browse files
refactor: use custom event and container for message events
1 parent 63207d3 commit 69541b7

5 files changed

Lines changed: 118 additions & 12 deletions

File tree

monty/events.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# from pydantic import dataclasses
2+
import dataclasses
3+
import enum
4+
import functools
5+
6+
import disnake
7+
8+
from monty.utils.code import prepare_input
9+
from monty.utils.markdown import remove_codeblocks
10+
from monty.utils.messages import extract_urls
11+
12+
13+
class MontyEvent(enum.Enum):
14+
monty_message_processed = "monty_message_processed"
15+
16+
17+
@dataclasses.dataclass(frozen=True)
18+
class MessageContext:
19+
"""A context for a message event."""
20+
21+
content: str
22+
23+
# Lazily initialize attributes
24+
@functools.cached_property
25+
def code(self) -> str | None:
26+
"""Return the code blocks found in the message."""
27+
return prepare_input(self.content, require_fenced=True)
28+
29+
@functools.cached_property
30+
def text(self) -> str:
31+
"""Return the message content with code blocks removed."""
32+
return remove_codeblocks(self.content)
33+
34+
@functools.cached_property
35+
def urls(self) -> list[str]:
36+
"""Return the URLs found in the message."""
37+
return list(extract_urls(self.text))
38+
39+
@classmethod
40+
def from_message_inter(
41+
cls, inter: disnake.MessageInteraction | disnake.MessageCommandInteraction, /
42+
) -> "MessageContext":
43+
"""Create a MessageContext from an interaction."""
44+
if isinstance(inter, disnake.MessageCommandInteraction):
45+
return cls(inter.target.content)
46+
return cls(inter.message.content)
47+
48+
@classmethod
49+
def from_message(cls, message: disnake.Message) -> "MessageContext":
50+
"""Create a MessageContext from a message."""
51+
return cls(content=message.content)

monty/exts/core/events.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import disnake
2+
from disnake.ext import commands
3+
4+
from monty.bot import Monty
5+
from monty.events import MessageContext, MontyEvent
6+
from monty.log import get_logger
7+
from monty.metadata import ExtMetadata
8+
9+
10+
EXT_METADATA = ExtMetadata(core=True)
11+
12+
13+
logger = get_logger(__name__)
14+
15+
16+
class Events(commands.Cog):
17+
"""Global checks for monty."""
18+
19+
def __init__(self, bot: Monty) -> None:
20+
self.bot = bot
21+
22+
@commands.Cog.listener(disnake.Event.message)
23+
async def on_message(self, message: disnake.Message) -> None:
24+
"""Re-dispatch message listener events for on_message commands.
25+
26+
There are multiple listeners that listen for urls and code content
27+
so we pre-process that information here before dispatching.
28+
"""
29+
# Only care about messages from users.
30+
if message.author.bot:
31+
return
32+
33+
context = MessageContext.from_message(message)
34+
self.bot.dispatch(MontyEvent.monty_message_processed.value, message, context)
35+
36+
37+
def setup(bot: Monty) -> None:
38+
"""Add the events cog the bot."""
39+
bot.add_cog(Events(bot))

monty/exts/info/codesnippets.py

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import re
33
import textwrap
44
from datetime import timedelta
5-
from typing import TYPE_CHECKING, Any
5+
from typing import TYPE_CHECKING, Any, overload
66
from urllib.parse import quote_plus
77
from urllib.parse import unquote as urlunquote
88

@@ -14,6 +14,7 @@
1414
from disnake.ext import commands
1515

1616
from monty.bot import Monty
17+
from monty.events import MessageContext, MontyEvent
1718
from monty.log import get_logger
1819
from monty.utils import scheduling
1920
from monty.utils.helpers import EXPAND_BUTTON_PREFIX, block_url_traversal, decode_github_link
@@ -301,11 +302,26 @@ def _snippet_to_codeblock(
301302
# Returns an empty codeblock if the snippet is empty
302303
return f"{ret}```\n```"
303304

304-
async def _parse_snippets(self, content: str) -> str:
305+
@overload
306+
async def _parse_snippets(self, *, content: str) -> str: ...
307+
@overload
308+
async def _parse_snippets(self, *, context: MessageContext) -> str: ...
309+
310+
async def _parse_snippets(self, *, content: str | None = None, context: MessageContext | None = None) -> str:
305311
"""Parse message content and return a string with a code block for each URL found."""
306-
all_snippets = []
312+
if context and content:
313+
msg = "Provide either content or context, not both."
314+
raise ValueError(msg)
315+
316+
if context:
317+
content = "\n".join(context.urls)
318+
elif content:
319+
content = remove_codeblocks(content)
320+
else:
321+
msg = "Either content or context must be provided."
322+
raise ValueError(msg)
307323

308-
content = remove_codeblocks(content)
324+
all_snippets: list[tuple[int, str]] = []
309325

310326
for pattern, handler in self.pattern_handlers:
311327
for match in pattern.finditer(content):
@@ -335,12 +351,9 @@ async def _parse_snippets(self, content: str) -> str:
335351
# Sorts the list of snippets by their match index and joins them into a single message
336352
return "\n".join(m[1] for m in sorted(all_snippets))
337353

338-
@commands.Cog.listener()
339-
async def on_message(self, message: disnake.Message) -> None:
354+
@commands.Cog.listener(MontyEvent.monty_message_processed.value)
355+
async def on_message(self, message: disnake.Message, context: MessageContext) -> None:
340356
"""Checks if the message has a snippet link, removes the embed, then sends the snippet contents."""
341-
if message.author.bot:
342-
return
343-
344357
if not message.guild:
345358
return
346359

@@ -354,7 +367,7 @@ async def on_message(self, message: disnake.Message) -> None:
354367
if not my_perms.send_messages:
355368
return
356369

357-
message_to_send = await self._parse_snippets(message.content)
370+
message_to_send = await self._parse_snippets(content=context.text)
358371
destination = message.channel
359372

360373
if 0 < len(message_to_send) <= 2000 and message_to_send.count("\n") <= 27:
@@ -380,7 +393,7 @@ async def send_expanded_links(self, inter: disnake.MessageInteraction) -> None:
380393

381394
custom_id = inter.data.custom_id[len(EXPAND_BUTTON_PREFIX) :]
382395
link = decode_github_link(custom_id)
383-
snippet = await self._parse_snippets(link)
396+
snippet = await self._parse_snippets(content=link)
384397

385398
def disable_button() -> disnake.ui.View:
386399
view = disnake.ui.View.from_message(inter.message)

monty/utils/code.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,9 @@ def prepare_input(code: str, *, require_fenced: bool = False) -> str | None:
4545
if len(blocks) > 1:
4646
code = "\n".join(block.group("code") for block in blocks)
4747
info = "several code blocks"
48+
elif len(match) > 1:
49+
code = "\n".join(block.group("code") for block in match)
50+
info = "several code blocks"
4851
else:
4952
match = match[0] if len(blocks) == 0 else blocks[0]
5053
code, block, lang, delim = match.group("code", "block", "lang", "delim")

monty/utils/markdown.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717

1818

1919
CODE_BLOCK_RE = re.compile(
20-
r"(?P<delim>`{1,2})([^\n]+)(?P=delim)|```(.+?)```",
20+
r"```(.+?)```|(?P<delim>`{1,2})([^\n]+?)(?P=delim)",
2121
re.DOTALL | re.MULTILINE,
2222
)
2323

0 commit comments

Comments
 (0)