Ver Fonte

Add support for mentioning users from Matrix

Tulir Asokan há 2 anos atrás
pai
commit
efdda635d9

+ 4 - 0
mauigpapi/mqtt/conn.py

@@ -961,6 +961,7 @@ class AndroidMQTT:
         client_context: str | None = None,
         replied_to_item_id: str | None = None,
         replied_to_client_context: str | None = None,
+        mentioned_user_ids: list[int] | None = None,
     ) -> Awaitable[CommandResponse]:
         args = {
             "text": text,
@@ -972,6 +973,9 @@ class AndroidMQTT:
                 "link_urls": json.dumps(urls or []),
             }
             item_type = ThreadItemType.LINK
+        if mentioned_user_ids:
+            args["mentioned_user_ids"] = json.dumps([str(x) for x in mentioned_user_ids])
+            args["sampled"] = True
         return self.send_item(
             thread_id,
             **args,

+ 15 - 0
mauigpapi/types/thread_item.py

@@ -575,6 +575,19 @@ class FetchedClipInfo(SerializableAttrs):
     status: str
 
 
+@dataclass
+class TextEntities(SerializableAttrs):
+    mentioned_user_ids: List[int] = attr.ib(factory=lambda: [])
+
+
+@dataclass
+class MentionedEntity(SerializableAttrs):
+    fbid: str
+    offset: int
+    length: int
+    interop_user_type: int
+
+
 @dataclass(kw_only=True)
 class ThreadItem(SerializableAttrs):
     item_id: Optional[str] = None
@@ -585,6 +598,8 @@ class ThreadItem(SerializableAttrs):
     new_reaction: Optional[Reaction] = None
 
     text: Optional[str] = None
+    text_entities: Optional[TextEntities] = None
+    mentioned_entities: List[MentionedEntity] = None
     client_context: Optional[str] = None
     show_forward_attribution: Optional[bool] = None
     action_log: Optional[ThreadItemActionLog] = None

+ 99 - 0
mautrix_instagram/formatter.py

@@ -0,0 +1,99 @@
+# mautrix-instagram - A Matrix-Instagram puppeting bridge.
+# Copyright (C) 2023 Tulir Asokan
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program.  If not, see <https://www.gnu.org/licenses/>.
+from __future__ import annotations
+
+from typing import NamedTuple
+
+from mautrix.types import MessageEventContent, UserID
+from mautrix.util.formatter import (
+    EntityString,
+    EntityType,
+    MarkdownString,
+    MatrixParser as BaseMatrixParser,
+    SimpleEntity,
+)
+
+from . import puppet as pu, user as u
+
+
+class SendParams(NamedTuple):
+    text: str
+    mentions: list[int]
+
+
+class FacebookFormatString(EntityString[SimpleEntity, EntityType], MarkdownString):
+    def format(self, entity_type: EntityType, **kwargs) -> FacebookFormatString:
+        prefix = suffix = ""
+        if entity_type == EntityType.USER_MENTION:
+            self.entities.append(
+                SimpleEntity(
+                    type=entity_type,
+                    offset=0,
+                    length=len(self.text),
+                    extra_info={"igpk": kwargs["igpk"]},
+                )
+            )
+            return self
+        elif entity_type == EntityType.BOLD:
+            prefix = suffix = "*"
+        elif entity_type == EntityType.ITALIC:
+            prefix = suffix = "_"
+        elif entity_type == EntityType.STRIKETHROUGH:
+            prefix = suffix = "~"
+        elif entity_type == EntityType.URL:
+            if kwargs["url"] != self.text:
+                suffix = f" ({kwargs['url']})"
+        elif entity_type == EntityType.PREFORMATTED:
+            prefix = f"```{kwargs['language']}\n"
+            suffix = "\n```"
+        elif entity_type == EntityType.INLINE_CODE:
+            prefix = suffix = "`"
+        elif entity_type == EntityType.BLOCKQUOTE:
+            children = self.trim().split("\n")
+            children = [child.prepend("> ") for child in children]
+            return self.join(children, "\n")
+        elif entity_type == EntityType.HEADER:
+            prefix = "#" * kwargs["size"] + " "
+        else:
+            return self
+
+        self._offset_entities(len(prefix))
+        self.text = f"{prefix}{self.text}{suffix}"
+        return self
+
+
+class MatrixParser(BaseMatrixParser[FacebookFormatString]):
+    fs = FacebookFormatString
+
+    async def user_pill_to_fstring(
+        self, msg: FacebookFormatString, user_id: UserID
+    ) -> FacebookFormatString | None:
+        entity = await u.User.get_by_mxid(user_id, create=False)
+        if not entity:
+            entity = await pu.Puppet.get_by_mxid(user_id, create=False)
+        if entity and entity.igpk and entity.username:
+            return FacebookFormatString(f"@{entity.username}").format(
+                EntityType.USER_MENTION, igpk=entity.igpk
+            )
+        return msg
+
+
+async def matrix_to_instagram(content: MessageEventContent) -> SendParams:
+    parsed = await MatrixParser().parse(content["formatted_body"])
+    return SendParams(
+        text=parsed.text,
+        mentions=[mention.extra_info["igpk"] for mention in parsed.entities],
+    )

+ 5 - 2
mautrix_instagram/portal.py

@@ -100,7 +100,7 @@ from mautrix.types import (
 from mautrix.util import background_task, ffmpeg
 from mautrix.util.message_send_checkpoint import MessageSendCheckpointStatus
 
-from . import matrix as m, puppet as p, user as u
+from . import formatter as fmt, matrix as m, puppet as p, user as u
 from .config import Config
 from .db import Backfill, Message as DBMessage, Portal as DBPortal, Reaction as DBReaction
 
@@ -582,7 +582,10 @@ class Portal(DBPortal, BasePortal):
             return
 
         if message.msgtype in (MessageType.EMOTE, MessageType.TEXT, MessageType.NOTICE):
-            text = message.body
+            if message.format == Format.HTML:
+                text, reply_to["mentioned_user_ids"] = await fmt.matrix_to_instagram(message)
+            else:
+                text = message.body
             if message.msgtype == MessageType.EMOTE:
                 text = f"/me {text}"
             self.log.trace(f"Sending Matrix text from {event_id} with request ID {request_id}")

+ 8 - 4
mautrix_instagram/puppet.py

@@ -128,13 +128,17 @@ class Puppet(DBPuppet, BasePuppet):
         )
 
     async def update_info(self, info: BaseResponseUser, source: u.User) -> None:
-        update = await self.update_contact_info(info)
+        update = False
+        if info.username and self.username != info.username:
+            self.username = info.username
+            update = True
+        update = await self.update_contact_info() or update
         update = await self._update_name(info) or update
         update = await self._update_avatar(info, source) or update
         if update:
             await self.update()
 
-    async def update_contact_info(self, info: BaseResponseUser | None = None) -> bool:
+    async def update_contact_info(self) -> bool:
         if not self.bridge.homeserver_software.is_hungry:
             return False
 
@@ -147,8 +151,8 @@ class Puppet(DBPuppet, BasePuppet):
                 "com.beeper.bridge.service": self.bridge.beeper_service_name,
                 "com.beeper.bridge.network": self.bridge.beeper_network_name,
             }
-            if info and info.username:
-                contact_info["com.beeper.bridge.identifiers"] = [f"instagram:{info.username}"]
+            if self.username:
+                contact_info["com.beeper.bridge.identifiers"] = [f"instagram:{self.username}"]
 
             await self.default_mxid_intent.beeper_update_profile(contact_info)
             self.contact_info_set = True