diff --git a/mautrix_line/config.py b/mautrix_line/config.py index d18cb0a..6c5a99b 100644 --- a/mautrix_line/config.py +++ b/mautrix_line/config.py @@ -58,6 +58,7 @@ class Config(BaseBridgeConfig): copy("bridge.displayname_max_length") copy("bridge.initial_conversation_sync") + copy("bridge.invite_own_puppet_to_pm") copy("bridge.login_shared_secret") copy("bridge.federate_rooms") copy("bridge.backfill.invite_own_puppet") diff --git a/mautrix_line/portal.py b/mautrix_line/portal.py index 5e747d3..d5fd015 100644 --- a/mautrix_line/portal.py +++ b/mautrix_line/portal.py @@ -49,6 +49,7 @@ ReuploadedMediaInfo = NamedTuple('ReuploadedMediaInfo', mxc=Optional[ContentURI] class Portal(DBPortal, BasePortal): + invite_own_puppet_to_pm: bool = False by_mxid: Dict[RoomID, 'Portal'] = {} by_chat_id: Dict[int, 'Portal'] = {} config: Config @@ -77,7 +78,15 @@ class Portal(DBPortal, BasePortal): @property def is_direct(self) -> bool: - return self.other_user is not None + return self.chat_id[0] == "u" + + @property + def is_group(self) -> bool: + return self.chat_id[0] == "c" + + @property + def is_room(self) -> bool: + return self.chat_id[0] == "r" @property def main_intent(self) -> IntentAPI: @@ -92,6 +101,7 @@ class Portal(DBPortal, BasePortal): cls.az = bridge.az cls.loop = bridge.loop cls.bridge = bridge + cls.invite_own_puppet_to_pm = cls.config["bridge.invite_own_puppet_to_pm"] NotificationDisabler.puppet_cls = p.Puppet NotificationDisabler.config_enabled = cls.config["bridge.backfill.disable_notifications"] @@ -145,36 +155,54 @@ class Portal(DBPortal, BasePortal): self.log.debug(f"{user.mxid} left portal to {self.chat_id}") # TODO cleanup if empty - async def handle_remote_message(self, source: 'u.User', evt: Message) -> None: - if evt.is_outgoing: + async def _bridge_own_message_pm(self, source: 'u.User', sender: 'p.Puppet', mid: str, + invite: bool = True) -> bool: + if self.is_direct and sender.fbid == source.fbid and not sender.is_real_user: + if self.invite_own_puppet_to_pm and invite: + await self.main_intent.invite_user(self.mxid, sender.mxid) + elif await self.az.state_store.get_membership(self.mxid, + sender.mxid) != Membership.JOIN: + self.log.warning(f"Ignoring own {mid} in private chat because own puppet is not in" + " room.") + return False + return True + + async def handle_remote_message(self, source: 'u.User', message: Message) -> None: + # TODO + if message.is_outgoing: if not source.intent: - self.log.warning(f"Ignoring message {evt.id}: double puppeting isn't enabled") - return - intent = source.intent + if not await self._bridge_own_message_pm(source, sender, mid, invite): + return + if not self.invite_own_puppet_to_pm: + self.log.warning(f"Ignoring message {message.id}: double puppeting isn't enabled") + return + intent = self.main_intent + else: + intent = source.intent elif self.other_user: intent = (await p.Puppet.get_by_mid(self.other_user)).intent else: # TODO group chats - self.log.warning(f"Ignoring message {evt.id}: group chats aren't supported yet") + self.log.warning(f"Ignoring message {message.id}: group chats aren't supported yet") return - if await DBMessage.get_by_mid(evt.id): - self.log.debug(f"Ignoring duplicate message {evt.id}") + if await DBMessage.get_by_mid(message.id): + self.log.debug(f"Ignoring duplicate message {message.id}") return event_id = None - if evt.image: - content = await self._handle_remote_photo(source, intent, evt) + if message.image: + content = await self._handle_remote_photo(source, intent, message) if content: - event_id = await self._send_message(intent, content, timestamp=evt.timestamp) - if evt.text and not evt.text.isspace(): - content = TextMessageEventContent(msgtype=MessageType.TEXT, body=evt.text) - event_id = await self._send_message(intent, content, timestamp=evt.timestamp) + event_id = await self._send_message(intent, content, timestamp=message.timestamp) + if message.text and not message.text.isspace(): + content = TextMessageEventContent(msgtype=MessageType.TEXT, body=message.text) + event_id = await self._send_message(intent, content, timestamp=message.timestamp) if event_id: - msg = DBMessage(mxid=event_id, mx_room=self.mxid, mid=evt.id, chat_id=self.chat_id) + msg = DBMessage(mxid=event_id, mx_room=self.mxid, mid=message.id, chat_id=self.chat_id) await msg.insert() await self._send_delivery_receipt(event_id) - self.log.debug(f"Handled remote message {evt.id} -> {event_id}") + self.log.debug(f"Handled remote message {message.id} -> {event_id}") async def _handle_remote_photo(self, source: 'u.User', intent: IntentAPI, message: Message ) -> Optional[MediaMessageEventContent]: @@ -200,15 +228,16 @@ class Portal(DBPortal, BasePortal): return ReuploadedMediaInfo(mxc, decryption_info, mime_type, file_name, len(data)) async def update_info(self, conv: ChatInfo) -> None: - # TODO Not true: a single-participant chat could be a group! - if len(conv.participants) == 1: + 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: puppet = await p.Puppet.get_by_mid(participant.id) await puppet.update_info(participant) - changed = await self._update_name(conv.name) + # TODO Consider setting no room name for non-group chats. + # But then the LINE bot itself may appear in the title... + changed = await self._update_name(f"{conv.name} (LINE)") if changed: await self.update_bridge_info() await self.update() @@ -267,8 +296,10 @@ class Portal(DBPortal, BasePortal): self.log.debug("Got %d messages from server", len(messages)) async with NotificationDisabler(self.mxid, source): - for evt in messages: - await self.handle_remote_message(source, evt) + for message in messages: + # TODO + #puppet = await p.Puppet.get_by_mid(message.) + await self.handle_remote_message(source, message) self.log.info("Backfilled %d messages through %s", len(messages), source.mxid) @property diff --git a/mautrix_line/rpc/types.py b/mautrix_line/rpc/types.py index 193a875..987e303 100644 --- a/mautrix_line/rpc/types.py +++ b/mautrix_line/rpc/types.py @@ -48,6 +48,8 @@ class Message(SerializableAttrs['Message']): id: int chat_id: int is_outgoing: bool + # TODO + sender: Optional[str] timestamp: int = None text: Optional[str] = None image: Optional[str] = None diff --git a/puppet/src/contentscript.js b/puppet/src/contentscript.js index 0d4eb4d..f04846a 100644 --- a/puppet/src/contentscript.js +++ b/puppet/src/contentscript.js @@ -104,6 +104,7 @@ class MautrixController { * @property {number} id - The ID of the message. Seems to be sequential. * @property {number} timestamp - The unix timestamp of the message. Not very accurate. * @property {boolean} is_outgoing - Whether or not this user sent the message. + * @property {null|string} sender - The ID of the user who sent the message, or null if outgoing. * @property {string} [text] - The text in the message. * @property {string} [image] - The URL to the image in the message. */ @@ -117,10 +118,17 @@ class MautrixController { * @private */ _tryParseMessage(date, element) { + const is_outgoing: element.classList.contains("mdRGT07Own") const messageData = { id: +element.getAttribute("data-local-id"), timestamp: date ? date.getTime() : null, - is_outgoing: element.classList.contains("mdRGT07Own"), + is_outgoing: is_outgoing, + // TODO The sender's mid isn't available, so must validate their display name somehow... + // Get it from contact list? It's always available in "#contact_wrap_friends > ul" + //
  • + // But what about non-friends? + // TODO Also, sender is always there, but not actually needed for DMs + sender: !is_outgoing ? element.querySelector(".mdRGT07Body > .mdRGT07Ttl") : null } const messageElement = element.querySelector(".mdRGT07Body > .mdRGT07Msg") if (messageElement.classList.contains("mdRGT07Text")) { @@ -226,10 +234,8 @@ class MautrixController { * @return {[Participant]} - The list of participants. */ parseParticipantList(element) { - // TODO Slice to exclude first member, which is always yourself (right?) - // TODO Only slice if double-puppeting is enabled! - //return Array.from(element.children).slice(1).map(child => { - return Array.from(element.children).map(child => { + // TODO The first member is always yourself, right? + return Array.from(element.children).slice(1).map(child => { return { id: child.getAttribute("data-mid"), // TODO avatar: child.querySelector("img").src, @@ -331,7 +337,6 @@ class MautrixController { * @private */ _observeChatListMutations(mutations) { - /* TODO const changedChatIDs = new Set() for (const change of mutations) { console.debug("Chat list mutation:", change) @@ -350,7 +355,6 @@ class MautrixController { () => console.debug("Chat list mutations dispatched"), err => console.error("Error dispatching chat list mutations:", err)) } - */ } /** diff --git a/puppet/src/puppet.js b/puppet/src/puppet.js index 89822bd..8f7f51c 100644 --- a/puppet/src/puppet.js +++ b/puppet/src/puppet.js @@ -95,10 +95,8 @@ export default class MessagesPuppeteer { await this.page.exposeFunction("__mautrixExpiry", this._receiveExpiry.bind(this)) await this.page.exposeFunction("__mautrixReceiveMessageID", id => this.sentMessageIDs.add(id)) - /* TODO await this.page.exposeFunction("__mautrixReceiveChanges", this._receiveChatListChanges.bind(this)) - */ await this.page.exposeFunction("__chronoParseDate", chrono.parseDate) // NOTE Must *always* re-login on a browser session, so no need to check if already logged in @@ -509,6 +507,7 @@ export default class MessagesPuppeteer { return messages.filter(msg => msg.id > minID && !this.sentMessageIDs.has(msg.id)) } + // TODO async _processChatListChangeUnsafe(id) { this.updatedChats.delete(id) this.log("Processing change to", id) @@ -535,6 +534,7 @@ export default class MessagesPuppeteer { } } + // TODO _receiveChatListChanges(changes) { this.log("Received chat list changes:", changes) for (const item of changes) {