221 lines
7.7 KiB
Python
221 lines
7.7 KiB
Python
from __future__ import annotations
|
|
|
|
import re
|
|
from datetime import datetime, UTC
|
|
from typing import TYPE_CHECKING, Any
|
|
|
|
from nio import (
|
|
RoomMessageAudio,
|
|
RoomMessageFile,
|
|
RoomMessageFormatted,
|
|
RoomMessageImage,
|
|
RoomMessageText,
|
|
RoomMessageVideo,
|
|
)
|
|
|
|
from yuxi.channels.models import (
|
|
Attachment,
|
|
ChannelIdentity,
|
|
ChannelMessage,
|
|
ChannelType,
|
|
ChatType,
|
|
EventType,
|
|
MentionsInfo,
|
|
MessageType,
|
|
)
|
|
|
|
from .rooms_config import get_room_require_mention
|
|
from .thread_utils import is_thread_event
|
|
from .location import parse_location_event
|
|
from .sticker import is_sticker_event, parse_sticker
|
|
|
|
if TYPE_CHECKING:
|
|
from yuxi.channels.adapters.matrix.adapter import MatrixAdapter
|
|
|
|
_MXID_PATTERN = re.compile(r'data-mx-username="@([^"]+)"')
|
|
_MXID_URL_PATTERN = re.compile(r'href="https://matrix\.to/#/(@[^"]+)"')
|
|
_PLAIN_MXID_PATTERN = re.compile(r"@([\w._=\-/]+):([\w.\-]+(?:\.[\w.\-]+)*)")
|
|
|
|
|
|
class MatrixNormalizer:
|
|
def __init__(self, adapter: MatrixAdapter):
|
|
self._adapter = adapter
|
|
|
|
def normalize(self, event: Any, room_id: str) -> ChannelMessage | None:
|
|
if event.sender == self._adapter.user_id:
|
|
return None
|
|
|
|
content_type, text_content, attachments = self._extract_content(event)
|
|
msg_type = self._map_message_type(content_type)
|
|
chat_type = self._adapter._session.resolve_chat_type(room_id)
|
|
is_thread, root_event_id = is_thread_event(event)
|
|
|
|
mentions = self._extract_mentions(event)
|
|
is_edit = self._is_edit_event(event)
|
|
|
|
if chat_type == "group" and not is_thread:
|
|
require_mention = get_room_require_mention(room_id, self._adapter.config)
|
|
if require_mention and not (mentions and mentions.is_bot_mentioned):
|
|
return None
|
|
|
|
server_ts = getattr(event, "server_timestamp", None)
|
|
timestamp = datetime.fromtimestamp((server_ts / 1000) if server_ts else datetime.now(UTC).timestamp(), tz=UTC)
|
|
|
|
event_type = EventType.MESSAGE_UPDATED if is_edit else EventType.MESSAGE_RECEIVED
|
|
|
|
return ChannelMessage(
|
|
identity=ChannelIdentity(
|
|
channel_id="matrix",
|
|
channel_type=ChannelType.MATRIX,
|
|
channel_user_id=event.sender,
|
|
channel_chat_id=self._adapter._session.resolve_chat_id(room_id, chat_type == "direct"),
|
|
channel_message_id=event.event_id,
|
|
),
|
|
event_type=event_type,
|
|
message_type=msg_type,
|
|
chat_type=ChatType.THREAD if is_thread else (ChatType.DIRECT if chat_type == "direct" else ChatType.GROUP),
|
|
content=text_content,
|
|
attachments=attachments,
|
|
mentions=mentions,
|
|
reply_to_message_id=self._extract_reply_to(event),
|
|
metadata={
|
|
"room_id": room_id,
|
|
"is_thread": is_thread,
|
|
"root_event_id": root_event_id,
|
|
"origin_server_ts": server_ts,
|
|
"is_edit": is_edit,
|
|
},
|
|
timestamp=timestamp,
|
|
)
|
|
|
|
def _extract_content(self, event: Any) -> tuple[str, str, list[Attachment]]:
|
|
if isinstance(event, RoomMessageFormatted):
|
|
return "text", event.body or event.formatted_body or "", []
|
|
if isinstance(event, RoomMessageText):
|
|
return "text", event.body, []
|
|
|
|
if isinstance(event, RoomMessageImage):
|
|
img = event.source.get("content", {})
|
|
mxc_url = img.get("url", "")
|
|
return (
|
|
"image",
|
|
img.get("body", ""),
|
|
[
|
|
Attachment(
|
|
type="image",
|
|
url=mxc_url,
|
|
mime_type=img.get("info", {}).get("mimetype"),
|
|
)
|
|
],
|
|
)
|
|
|
|
if isinstance(event, RoomMessageFile):
|
|
file_content = event.source.get("content", {})
|
|
mxc_url = file_content.get("url", "")
|
|
return (
|
|
"file",
|
|
file_content.get("body", ""),
|
|
[
|
|
Attachment(
|
|
type="file",
|
|
url=mxc_url,
|
|
filename=file_content.get("body"),
|
|
mime_type=file_content.get("info", {}).get("mimetype"),
|
|
)
|
|
],
|
|
)
|
|
|
|
if isinstance(event, RoomMessageAudio):
|
|
media_content = event.source.get("content", {})
|
|
mxc_url = media_content.get("url", "")
|
|
return (
|
|
"audio",
|
|
media_content.get("body", ""),
|
|
[
|
|
Attachment(
|
|
type="audio",
|
|
url=mxc_url,
|
|
mime_type=media_content.get("info", {}).get("mimetype"),
|
|
)
|
|
],
|
|
)
|
|
|
|
if isinstance(event, RoomMessageVideo):
|
|
media_content = event.source.get("content", {})
|
|
mxc_url = media_content.get("url", "")
|
|
return (
|
|
"video",
|
|
media_content.get("body", ""),
|
|
[
|
|
Attachment(
|
|
type="video",
|
|
url=mxc_url,
|
|
mime_type=media_content.get("info", {}).get("mimetype"),
|
|
)
|
|
],
|
|
)
|
|
|
|
event_source = getattr(event, "source", {})
|
|
location = parse_location_event(event_source)
|
|
if location:
|
|
return "location", location["text"], [Attachment(type="location", url=location.get("geo_uri", ""))]
|
|
|
|
if is_sticker_event(event_source):
|
|
sticker = parse_sticker(event_source)
|
|
if sticker:
|
|
return (
|
|
"sticker",
|
|
sticker["text"],
|
|
[Attachment(type="sticker", url=sticker.get("url", ""), mime_type=sticker.get("mimetype", ""))],
|
|
)
|
|
|
|
return "text", "", []
|
|
|
|
def _map_message_type(self, content_type: str) -> MessageType:
|
|
mapping = {
|
|
"text": MessageType.TEXT,
|
|
"image": MessageType.IMAGE,
|
|
"video": MessageType.VIDEO,
|
|
"audio": MessageType.AUDIO,
|
|
"file": MessageType.FILE,
|
|
"location": MessageType.TEXT,
|
|
"sticker": MessageType.IMAGE,
|
|
}
|
|
return mapping.get(content_type, MessageType.TEXT)
|
|
|
|
def _extract_mentions(self, event: Any) -> MentionsInfo | None:
|
|
content = event.source.get("content", {})
|
|
formatted_body = content.get("formatted_body", "")
|
|
body = content.get("body", "")
|
|
|
|
if not formatted_body and not body:
|
|
return None
|
|
|
|
is_at_bot = self._adapter.user_id in formatted_body or self._adapter.user_id in body
|
|
|
|
mentioned = _MXID_PATTERN.findall(formatted_body) + _MXID_URL_PATTERN.findall(formatted_body)
|
|
|
|
if not mentioned:
|
|
plain_body = body if body else formatted_body
|
|
plain_mentions = _PLAIN_MXID_PATTERN.findall(plain_body)
|
|
mentioned = [f"@{local}:{domain}" for local, domain in plain_mentions]
|
|
|
|
mentioned = list(dict.fromkeys(mentioned))
|
|
|
|
return MentionsInfo(
|
|
mentioned_user_ids=mentioned,
|
|
is_bot_mentioned=is_at_bot,
|
|
)
|
|
|
|
def _extract_reply_to(self, event: Any) -> str | None:
|
|
content = event.source.get("content", {})
|
|
relates_to = content.get("m.relates_to", {})
|
|
if relates_to.get("m.in_reply_to"):
|
|
return relates_to["m.in_reply_to"].get("event_id")
|
|
return None
|
|
|
|
def _is_edit_event(self, event: Any) -> bool:
|
|
content = event.source.get("content", {})
|
|
relates_to = content.get("m.relates_to", {})
|
|
return relates_to.get("rel_type") == "m.replace" and "m.new_content" in content
|