diff --git a/matrix_puppeteer_line/matrix.py b/matrix_puppeteer_line/matrix.py index a9ca885..bc7ed55 100644 --- a/matrix_puppeteer_line/matrix.py +++ b/matrix_puppeteer_line/matrix.py @@ -16,12 +16,12 @@ from typing import TYPE_CHECKING from mautrix.bridge import BaseMatrixHandler -from mautrix.types import (Event, ReactionEvent, MessageEvent, StateEvent, EncryptedEvent, RedactionEvent, - ReceiptEvent, SingleReceiptEventContent, +from mautrix.types import (Event, EventType, MessageEvent, StateEvent, EncryptedEvent, + ReceiptEvent, SingleReceiptEventContent, TextMessageEventContent, EventID, RoomID, UserID) +from mautrix.errors import MatrixError from . import portal as po, puppet as pu, user as u -from .db import Message as DBMessage if TYPE_CHECKING: from .__main__ import MessagesBridge @@ -54,17 +54,78 @@ class MatrixHandler(BaseMatrixHandler): async def handle_puppet_invite(self, room_id: RoomID, puppet: 'pu.Puppet', invited_by: 'u.User', _: EventID) -> None: - chat_id = puppet.mid - portal = await po.Portal.get_by_chat_id(chat_id, create=True) - if portal.mxid: - # TODO Allow creating a LINE group/room from a Matrix invite - await portal.main_intent.error_and_leave(room_id, "You already have an existing chat with me!") + intent = puppet.intent + self.log.debug(f"{invited_by.mxid} invited puppet for {puppet.mid} to {room_id}") + if not await invited_by.is_logged_in(): + await intent.error_and_leave(room_id, text="Please log in before inviting " + "LINE puppets to private chats.") return + + portal = await po.Portal.get_by_mxid(room_id) + if portal: + if portal.is_direct: + await intent.error_and_leave(room_id, text="You can not invite additional users " + "to private chats.") + else: + # TODO Send invite in LINE + await intent.error_and_leave(room_id, text="Inviting additional users to an existing " + "group chat is not yet supported.") + return + + await intent.join_room(room_id) + try: + members = await intent.get_room_members(room_id) + except MatrixError: + self.log.exception(f"Failed to get member list after joining {room_id}") + await intent.leave_room(room_id) + return + if len(members) > 2: + # TODO Add LINE group/room creating. Must also distinguish between the two! + await intent.send_notice(room_id, "You can not invite LINE puppets to " + "multi-user rooms.") + await intent.leave_room(room_id) + return + + portal = await po.Portal.get_by_chat_id(puppet.mid, create=True) + if portal.mxid: + try: + await intent.invite_user(portal.mxid, invited_by.mxid, check_cache=False) + await intent.send_notice(room_id, + text=("You already have a private chat with me " + f"in room {portal.mxid}"), + html=("You already have a private chat with me: " + f"" + "Link to room" + "")) + await intent.leave_room(room_id) + return + except MatrixError: + pass + portal.mxid = room_id + e2be_ok = await portal.check_dm_encryption() + # TODO Consider setting other power levels that get set on portal creation, + # but they're of little use when the inviting user has an equal PL... + await portal.save() + if e2be_ok is True: + evt_type, content = await self.e2ee.encrypt( + room_id, EventType.ROOM_MESSAGE, + TextMessageEventContent(msgtype=MessageType.NOTICE, + body="Portal to private chat created and end-to-bridge" + " encryption enabled.")) + await intent.send_message_event(room_id, evt_type, content) + else: + message = "Portal to private chat created." + if e2be_ok is False: + message += "\n\nWarning: Failed to enable end-to-bridge encryption" + await intent.send_notice(room_id, message) + # TODO Put pause/resume in portal methods, with a lock or something + # TODO Consider not backfilling on invite. + # To do so, must set the last-seen message ID appropriately await invited_by.client.pause() try: - chat_info = await invited_by.client.get_chat(chat_id) + chat_info = await invited_by.client.get_chat(puppet.mid) await portal.update_matrix_room(invited_by, chat_info) finally: await invited_by.client.resume() diff --git a/matrix_puppeteer_line/portal.py b/matrix_puppeteer_line/portal.py index a97a0fe..3043ed6 100644 --- a/matrix_puppeteer_line/portal.py +++ b/matrix_puppeteer_line/portal.py @@ -189,7 +189,8 @@ class Portal(DBPortal, BasePortal): async def handle_matrix_leave(self, user: 'u.User') -> None: self.log.info(f"{user.mxid} left portal to {self.chat_id}, " f"cleaning up and deleting...") - # TODO Delete room history in LINE to prevent a re-sync from happening + if await user.is_logged_in(): + await user.client.forget_chat(self.chat_id) await self.cleanup_and_delete() async def _bridge_own_message_pm(self, source: 'u.User', puppet: Optional['p.Puppet'], mid: str, @@ -501,10 +502,6 @@ class Portal(DBPortal, BasePortal): return MediaInfo(mxc, decryption_info, mime_type, file_name, len(data)) async def update_info(self, conv: ChatInfo, client: Optional[Client]) -> None: - if self.is_direct: - self.other_user = conv.participants[0].id - if self._main_intent is self.az.intent: - self._main_intent = (await p.Puppet.get_by_mid(self.other_user)).intent for participant in conv.participants: # REMINDER: multi-user chats include your own LINE user in the participant list if participant.id != None: @@ -806,7 +803,8 @@ class Portal(DBPortal, BasePortal): self.by_chat_id[self.chat_id] = self if self.mxid: self.by_mxid[self.mxid] = self - if self.other_user: + if self.is_direct: + self.other_user = self.chat_id self._main_intent = (await p.Puppet.get_by_mid(self.other_user)).intent else: self._main_intent = self.az.intent diff --git a/matrix_puppeteer_line/rpc/client.py b/matrix_puppeteer_line/rpc/client.py index 6304385..eae2f0c 100644 --- a/matrix_puppeteer_line/rpc/client.py +++ b/matrix_puppeteer_line/rpc/client.py @@ -88,6 +88,9 @@ class Client(RPCClient): async def set_last_message_ids(self, msg_ids: Dict[str, int], own_msg_ids: Dict[str, int], rct_ids: Dict[str, Dict[int, int]]) -> None: await self.request("set_last_message_ids", msg_ids=msg_ids, own_msg_ids=own_msg_ids, rct_ids=rct_ids) + async def forget_chat(self, chat_id: str) -> None: + await self.request("forget_chat", chat_id=chat_id) + async def on_message(self, func: Callable[[Message], Awaitable[None]]) -> None: async def wrapper(data: Dict[str, Any]) -> None: await func(Message.deserialize(data["message"])) diff --git a/puppet/src/client.js b/puppet/src/client.js index 7ddf600..2a3b280 100644 --- a/puppet/src/client.js +++ b/puppet/src/client.js @@ -258,6 +258,7 @@ export default class Client { send: req => this.puppet.sendMessage(req.chat_id, req.text), send_file: req => this.puppet.sendFile(req.chat_id, req.file_path), set_last_message_ids: req => this.puppet.setLastMessageIDs(req.msg_ids, req.own_msg_ids, req.rct_ids), + forget_chat: req => this.puppet.forgetChat(req.chat_id), pause: () => this.puppet.stopObserving(), resume: () => this.puppet.startObserving(), get_own_profile: () => this.puppet.getOwnProfile(), diff --git a/puppet/src/puppet.js b/puppet/src/puppet.js index 791cf06..96810dc 100644 --- a/puppet/src/puppet.js +++ b/puppet/src/puppet.js @@ -538,6 +538,13 @@ export default class MessagesPuppeteer { } } + forgetChat(chatID) { + this.mostRecentMessages.delete(chatID) + this.mostRecentOwnMessages.delete(chatID) + this.mostRecentReceipts.delete(chatID) + // TODO Delete chat from recents list + } + async readImage(imageUrl) { return await this.taskQueue.push(() => this.page.evaluate(