Просмотр исходного кода

Add support for mentioning users from Matrix

Tulir Asokan 2 лет назад
Родитель
Сommit
efdda635d9

+ 4 - 0
mauigpapi/mqtt/conn.py

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

+ 15 - 0
mauigpapi/types/thread_item.py

@@ -575,6 +575,19 @@ class FetchedClipInfo(SerializableAttrs):
     status: str
     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)
 @dataclass(kw_only=True)
 class ThreadItem(SerializableAttrs):
 class ThreadItem(SerializableAttrs):
     item_id: Optional[str] = None
     item_id: Optional[str] = None
@@ -585,6 +598,8 @@ class ThreadItem(SerializableAttrs):
     new_reaction: Optional[Reaction] = None
     new_reaction: Optional[Reaction] = None
 
 
     text: Optional[str] = None
     text: Optional[str] = None
+    text_entities: Optional[TextEntities] = None
+    mentioned_entities: List[MentionedEntity] = None
     client_context: Optional[str] = None
     client_context: Optional[str] = None
     show_forward_attribution: Optional[bool] = None
     show_forward_attribution: Optional[bool] = None
     action_log: Optional[ThreadItemActionLog] = 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 import background_task, ffmpeg
 from mautrix.util.message_send_checkpoint import MessageSendCheckpointStatus
 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 .config import Config
 from .db import Backfill, Message as DBMessage, Portal as DBPortal, Reaction as DBReaction
 from .db import Backfill, Message as DBMessage, Portal as DBPortal, Reaction as DBReaction
 
 
@@ -582,7 +582,10 @@ class Portal(DBPortal, BasePortal):
             return
             return
 
 
         if message.msgtype in (MessageType.EMOTE, MessageType.TEXT, MessageType.NOTICE):
         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:
             if message.msgtype == MessageType.EMOTE:
                 text = f"/me {text}"
                 text = f"/me {text}"
             self.log.trace(f"Sending Matrix text from {event_id} with request ID {request_id}")
             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:
     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_name(info) or update
         update = await self._update_avatar(info, source) or update
         update = await self._update_avatar(info, source) or update
         if update:
         if update:
             await self.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:
         if not self.bridge.homeserver_software.is_hungry:
             return False
             return False
 
 
@@ -147,8 +151,8 @@ class Puppet(DBPuppet, BasePuppet):
                 "com.beeper.bridge.service": self.bridge.beeper_service_name,
                 "com.beeper.bridge.service": self.bridge.beeper_service_name,
                 "com.beeper.bridge.network": self.bridge.beeper_network_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)
             await self.default_mxid_intent.beeper_update_profile(contact_info)
             self.contact_info_set = True
             self.contact_info_set = True