diff --git a/matrix_appservice_line/config.py b/matrix_appservice_line/config.py index f49cf75..5f7b8cd 100644 --- a/matrix_appservice_line/config.py +++ b/matrix_appservice_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/matrix_appservice_line/example-config.yaml b/matrix_appservice_line/example-config.yaml index 1d6ce16..f57b838 100644 --- a/matrix_appservice_line/example-config.yaml +++ b/matrix_appservice_line/example-config.yaml @@ -78,6 +78,9 @@ bridge: # Number of conversations to sync (and create portals for) on login. # Set 0 to disable automatic syncing. initial_conversation_sync: 10 + # Whether or not the LINE users of logged in Matrix users should be + # invited to private chats when the user sends a message from another client. + invite_own_puppet_to_pm: false # Shared secret for https://github.com/devture/matrix-synapse-shared-secret-auth # # If set, custom puppets will be enabled automatically for local users diff --git a/matrix_appservice_line/portal.py b/matrix_appservice_line/portal.py index 34a325e..6e20789 100644 --- a/matrix_appservice_line/portal.py +++ b/matrix_appservice_line/portal.py @@ -22,7 +22,7 @@ import magic from mautrix.appservice import AppService, IntentAPI from mautrix.bridge import BasePortal, NotificationDisabler from mautrix.types import (EventID, MessageEventContent, RoomID, EventType, MessageType, - TextMessageEventContent, MediaMessageEventContent, + TextMessageEventContent, MediaMessageEventContent, Membership, ContentURI, EncryptedFile) from mautrix.errors import MatrixError from mautrix.util.simple_lock import SimpleLock @@ -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 @@ -60,8 +61,6 @@ class Portal(DBPortal, BasePortal): backfill_lock: SimpleLock _last_participant_update: Set[str] - _main_intent: IntentAPI - def __init__(self, chat_id: int, other_user: Optional[str] = None, mxid: Optional[RoomID] = None, name: Optional[str] = None, encrypted: bool = False ) -> None: @@ -77,7 +76,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 +99,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,17 +153,39 @@ 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: + async def _bridge_own_message_pm(self, source: 'u.User', sender: Optional['p.Puppet'], mid: str, + invite: bool = True) -> Optional[IntentAPI]: + # Use bridge bot as puppet for own user when puppet for own user is unavailable + # TODO Use own LINE puppet instead, if it's available + intent = sender.intent if sender else self.az.intent + if self.is_direct and (sender is None or sender.mid == source.mid and not sender.is_real_user): + if self.invite_own_puppet_to_pm and invite: + await self.main_intent.invite_user(self.mxid, intent.mxid) + elif await self.az.state_store.get_membership(self.mxid, + intent.mxid) != Membership.JOIN: + self.log.warning(f"Ignoring own {mid} in private chat because own puppet is not in" + " room.") + intent = None + return intent + + async def handle_remote_message(self, source: 'u.User', sender: Optional['p.Puppet'], + evt: Message) -> None: if evt.is_outgoing: - if not source.intent: - self.log.warning(f"Ignoring message {evt.id}: double puppeting isn't enabled") - return - intent = source.intent + if source.intent: + intent = source.intent + else: + if not self.invite_own_puppet_to_pm: + self.log.warning(f"Ignoring message {evt.id}: double puppeting isn't enabled") + return + intent = await self._bridge_own_message_pm(source, sender, f"message {evt.id}") + if not intent: + return elif self.other_user: intent = (await p.Puppet.get_by_mid(self.other_user)).intent + elif sender: + intent = sender.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 {evt.id}: sender puppet is unavailable") return if await DBMessage.get_by_mid(evt.id): @@ -200,19 +230,22 @@ 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() - await self._update_participants(conv.participants) + # NOTE Don't call this yet, lest puppets join earlier than + # when their user actually joined or sent a message. + #await self._update_participants(conv.participants) async def _update_name(self, name: str) -> bool: if self.name != name: @@ -251,8 +284,11 @@ class Portal(DBPortal, BasePortal): reason="User had left this chat") async def backfill(self, source: 'u.User') -> None: - with self.backfill_lock: - await self._backfill(source) + try: + with self.backfill_lock: + await self._backfill(source) + except Exception: + self.log.exception("Failed to backfill portal") async def _backfill(self, source: 'u.User') -> None: self.log.debug("Backfilling history through %s", source.mxid) @@ -267,8 +303,15 @@ class Portal(DBPortal, BasePortal): self.log.debug("Got %d messages from server", len(messages)) async with NotificationDisabler(self.mxid, source): + # Member joins/leaves are not shown in chat history. + # Best we can do is have a puppet join if its user had sent a message. + members_known = set(await self.main_intent.get_room_members(self.mxid)) if not self.is_direct else None for evt in messages: - await self.handle_remote_message(source, evt) + puppet = await p.Puppet.get_by_mid(evt.sender.id) if not self.is_direct else None + if puppet and evt.sender.id not in members_known: + await puppet.update_info(evt.sender) + members_known.add(evt.sender.id) + await self.handle_remote_message(source, puppet, evt) self.log.info("Backfilled %d messages through %s", len(messages), source.mxid) @property @@ -325,6 +368,8 @@ class Portal(DBPortal, BasePortal): await puppet.az.intent.ensure_joined(self.mxid) await self.update_info(info) + await self.backfill(source) + await self._update_participants(info.participants) async def _create_matrix_room(self, source: 'u.User', info: ChatInfo) -> Optional[RoomID]: if self.mxid: @@ -352,8 +397,10 @@ class Portal(DBPortal, BasePortal): }) if self.is_direct: invites.append(self.az.bot_mxid) - if self.encrypted or not self.is_direct: - name = self.name + # NOTE Set the room title even for direct chats, because + # the LINE bot itself may appear in the title otherwise. + #if self.encrypted or not self.is_direct: + name = self.name if self.config["appservice.community_id"]: initial_state.append({ "type": "m.room.related_groups", @@ -394,6 +441,11 @@ class Portal(DBPortal, BasePortal): self.log.debug(f"Matrix room created: {self.mxid}") self.by_mxid[self.mxid] = self if not self.is_direct: + # For multi-user chats, backfill before updating participants, + # to act as as a best guess of when users actually joined. + # No way to tell when a user actually left, so just check the + # participants list after backfilling. + await self.backfill(source) await self._update_participants(info.participants) else: puppet = await p.Puppet.get_by_custom_mxid(source.mxid) @@ -403,11 +455,7 @@ class Portal(DBPortal, BasePortal): except MatrixError: self.log.debug("Failed to join custom puppet into newly created portal", exc_info=True) - - try: await self.backfill(source) - except Exception: - self.log.exception("Failed to backfill new portal") return self.mxid diff --git a/matrix_appservice_line/puppet.py b/matrix_appservice_line/puppet.py index f2f4116..ff1a171 100644 --- a/matrix_appservice_line/puppet.py +++ b/matrix_appservice_line/puppet.py @@ -98,6 +98,7 @@ class Puppet(DBPuppet, BasePuppet): @classmethod async def get_by_mid(cls, mid: str, create: bool = True) -> Optional['Puppet']: + # TODO Might need to parse a real id from "_OWN" try: return cls.by_mid[mid] except KeyError: diff --git a/matrix_appservice_line/rpc/types.py b/matrix_appservice_line/rpc/types.py index 7848b5a..35ca159 100644 --- a/matrix_appservice_line/rpc/types.py +++ b/matrix_appservice_line/rpc/types.py @@ -35,6 +35,7 @@ class ChatListInfo(SerializableAttrs['ChatListInfo']): @dataclass class Participant(SerializableAttrs['Participant']): id: str + # TODO avatar: str name: str @@ -48,6 +49,7 @@ class Message(SerializableAttrs['Message']): id: int chat_id: int is_outgoing: bool + sender: Optional[Participant] timestamp: int = None text: Optional[str] = None image: Optional[str] = None diff --git a/matrix_appservice_line/user.py b/matrix_appservice_line/user.py index cd8e200..915853b 100644 --- a/matrix_appservice_line/user.py +++ b/matrix_appservice_line/user.py @@ -124,7 +124,6 @@ class User(DBUser, BaseUser): chat = await self.client.get_chat(chat.id) if portal.mxid: await portal.update_matrix_room(self, chat) - await portal.backfill(self) else: await portal.create_matrix_room(self, chat) @@ -145,10 +144,11 @@ class User(DBUser, BaseUser): async def handle_message(self, evt: Message) -> None: self.log.trace("Received message %s", evt) portal = await po.Portal.get_by_chat_id(evt.chat_id, create=True) + puppet = await pu.Puppet.get_by_mid(evt.sender.id) if not portal.is_direct else None if not portal.mxid: chat_info = await self.client.get_chat(evt.chat_id) await portal.create_matrix_room(self, chat_info) - await portal.handle_remote_message(self, evt) + await portal.handle_remote_message(self, puppet, evt) def _add_to_cache(self) -> None: self.by_mxid[self.mxid] = self diff --git a/puppet/src/client.js b/puppet/src/client.js index b10220d..6a472da 100644 --- a/puppet/src/client.js +++ b/puppet/src/client.js @@ -99,7 +99,7 @@ export default class Client { } sendMessage(message) { - this.log("Sending", message, "to client") + this.log(`Sending message ${message.id} to client`) return this._write({ id: --this.notificationID, command: "message", @@ -204,7 +204,9 @@ export default class Client { this.log("Ignoring old request", req.id) return } - this.log("Received request", req.id, "with command", req.command) + if (req.command != "is_connected") { + this.log("Received request", req.id, "with command", req.command) + } this.maxCommandID = req.id let handler if (!this.userID) { diff --git a/puppet/src/contentscript.js b/puppet/src/contentscript.js index 84d77ef..fac450f 100644 --- a/puppet/src/contentscript.js +++ b/puppet/src/contentscript.js @@ -51,15 +51,46 @@ window.__mautrixExpiry = function (button) {} * @return {Promise} */ window.__mautrixReceiveMessageID = function(id) {} +/** + * @return {Promise} + */ +window.__mautrixGetParticipantsList = function() {} + +const ChatTypeEnum = Object.freeze({ + DIRECT: 1, + GROUP: 2, + ROOM: 3, +}) class MautrixController { - constructor() { + constructor(ownID) { this.chatListObserver = null this.qrChangeObserver = null this.qrAppearObserver = null this.emailAppearObserver = null this.pinAppearObserver = null this.expiryObserver = null + this.ownID = null + } + + setOwnID(ownID) { + // Remove characters that will conflict with mxid grammar + const suffix = ownID.slice(1).replace(":", "_ON_") + this.ownID = `_OWN_${suffix}` + } + + // TODO Commonize with Node context + getChatType(id) { + switch (id.charAt(0)) { + case "u": + return ChatTypeEnum.DIRECT + case "c": + return ChatTypeEnum.GROUP + case "r": + return ChatTypeEnum.ROOM + default: + throw `Invalid chat ID: ${id}` + } } /** @@ -98,12 +129,23 @@ class MautrixController { return newDate && newDate <= now ? newDate : null } + /** + * Try to match a user against an entry in the friends list to get their ID. + * + * @param {Element} element - The display name of the user to find the ID for. + * @return {null|str} - The user's ID if found. + */ + getUserIdFromFriendsList(senderName) { + return document.querySelector(`#contact_wrap_friends > ul > li[title='${senderName}']`)?.getAttribute("data-mid") + } + /** * @typedef MessageData * @type {object} * @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|Participant} sender - Full data of the participant who sent the message, if needed and available. * @property {string} [text] - The text in the message. * @property {string} [image] - The URL to the image in the message. */ @@ -113,14 +155,59 @@ class MautrixController { * * @param {Date} date - The most recent date indicator. * @param {Element} element - The message element. + * @param {int} chatType - What kind of chat this message is part of. * @return {MessageData} * @private */ - _tryParseMessage(date, element) { + async _tryParseMessage(date, element, chatType) { + const is_outgoing = element.classList.contains("mdRGT07Own") + let sender = {} + + // TODO Clean up participantsList access... + const participantsListSelector = "#_chat_detail_area > .mdRGT02Info ul.mdRGT13Ul" + + // Don't need sender ID for direct chats, since the portal will have it already. + if (chatType == ChatTypeEnum.DIRECT) { + sender = null + } else if (!is_outgoing) { + sender.name = element.querySelector(".mdRGT07Body > .mdRGT07Ttl").innerText + // Room members are always friends (right?), + // so search the friend list for the sender's name + // and get their ID from there. + // TODO For rooms, allow creating Matrix puppets in case + // a message is sent by someone who since left the + // room and never had a puppet made for them yet. + sender.id = this.getUserIdFromFriendsList(sender.name) + // Group members aren't necessarily friends, + // but the participant list includes their ID. + if (!sender.id) { + await window.__mautrixShowParticipantsList() + const participantsList = document.querySelector(participantsListSelector) + sender.id = participantsList.querySelector(`img[alt='${senderName}'`).parentElement.parentElement.getAttribute("data-mid") + } + // TODO Avatar + } else { + // TODO Get own ID and store it somewhere appropriate. + // Unable to get own ID from a room chat... + // if (chatType == ChatTypeEnum.GROUP) { + // await window.__mautrixShowParticipantsList() + // const participantsList = document.querySelector("#_chat_detail_area > .mdRGT02Info ul.mdRGT13Ul") + // // TODO The first member is always yourself, right? + // // TODO Cache this so own ID can be used later + // sender = participantsList.children[0].getAttribute("data-mid") + // } + await window.__mautrixShowParticipantsList() + const participantsList = document.querySelector(participantsListSelector) + sender.name = this.getParticipantListItemName(participantsList.children[0]) + // TODO avatar + sender.id = this.ownID + } + const messageData = { id: +element.getAttribute("data-local-id"), timestamp: date ? date.getTime() : null, - is_outgoing: element.classList.contains("mdRGT07Own"), + is_outgoing: is_outgoing, + sender: sender, } const messageElement = element.querySelector(".mdRGT07Body > .mdRGT07Msg") if (messageElement.classList.contains("mdRGT07Text")) { @@ -184,9 +271,14 @@ class MautrixController { /** * Parse the message list of whatever the currently-viewed chat is. * + * @param {null|string} chatId - The ID of the currently-viewed chat, if known. * @return {[MessageData]} - A list of messages. */ - async parseMessageList() { + async parseMessageList(chatId) { + if (!chatId) { + chatId = this.getChatListItemId(document.querySelector("#_chat_list_body > .ExSelected > div")) + } + const chatType = this.getChatType(chatId); const msgList = document.querySelector("#_chat_room_msg_list") const messages = [] let refDate = null @@ -202,7 +294,7 @@ class MautrixController { const timeElement = child.querySelector("time") if (timeElement) { const messageDate = await this._tryParseDate(timeElement.innerText, refDate) - messages.push(this._tryParseMessage(messageDate, child)) + messages.push(await this._tryParseMessage(messageDate, child, chatType)) } } } @@ -218,6 +310,15 @@ class MautrixController { * @property {string} name - The contact list name of the participant */ + getParticipantListItemName(element) { + return element.querySelector(".mdRGT13Ttl").innerText + } + + getParticipantListItemId(element) { + // TODO Cache own ID + return element.getAttribute("data-mid") + } + /** * Parse a group participants list. * TODO Find what works for a *room* participants list...! @@ -226,16 +327,26 @@ 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 Might need to explicitly exclude own user if double-puppeting is enabled. + // TODO The first member is always yourself, right? + const ownParticipant = { + // TODO Find way to make this work with multiple mxids using the bridge. + // One idea is to add real ID as suffix if we're in a group, and + // put in the puppet DB table somehow. + id: this.ownID, + // TODO avatar: child.querySelector("img").src, + name: this.getParticipantListItemName(element.children[0]), + } + + return [ownParticipant].concat(Array.from(element.children).slice(1).map(child => { + const name = this.getParticipantListItemName(child) + const id = this.getParticipantListItemId(child) || this.getUserIdFromFriendsList(name) return { - id: child.getAttribute("data-mid"), + id: id, // NOTE Don't want non-own user's ID to ever be null. // TODO avatar: child.querySelector("img").src, - name: child.querySelector(".mdRGT13Ttl").innerText, + name: name, } - }) + })) } /** diff --git a/puppet/src/puppet.js b/puppet/src/puppet.js index 2640eac..47572a0 100644 --- a/puppet/src/puppet.js +++ b/puppet/src/puppet.js @@ -29,7 +29,7 @@ export default class MessagesPuppeteer { static noSandbox = false static viewport = { width: 960, height: 880 } static url = undefined - static extensionDir = 'extension_files' + static extensionDir = "extension_files" /** * @@ -58,6 +58,14 @@ export default class MessagesPuppeteer { console.error(`[Puppeteer/${this.id}]`, ...text) } + /** + * Get the inner text of an element. + * To be called in browser context. + */ + _getInnerText(element) { + return element?.innerText + } + /** * Start the browser and open the messages for web page. * This must be called before doing anything else. @@ -97,6 +105,7 @@ export default class MessagesPuppeteer { id => this.sentMessageIDs.add(id)) await this.page.exposeFunction("__mautrixReceiveChanges", this._receiveChatListChanges.bind(this)) + await this.page.exposeFunction("__mautrixShowParticipantsList", this._showParticipantList.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 @@ -204,11 +213,12 @@ export default class MessagesPuppeteer { return value }), () => this.page.waitForSelector("#login_incorrect", {visible: true, timeout: 2000}) - .then(value => this.page.evaluate(element => element.innerText, value)), + .then(value => this.page.evaluate(_getInnerText, value)), () => this._waitForLoginCancel(), ].map(promiseFn => cancelableResolve(promiseFn))) this.log("Removing observers") + await this.page.evaluate(ownID => window.__mautrixController.setOwnID(ownID), this.id) await this.page.evaluate(() => window.__mautrixController.removeQRChangeObserver()) await this.page.evaluate(() => window.__mautrixController.removeQRAppearObserver()) await this.page.evaluate(() => window.__mautrixController.removeEmailAppearObserver()) @@ -402,68 +412,99 @@ export default class MessagesPuppeteer { } async _switchChat(id) { + // TODO Allow passing in an element directly this.log(`Switching to chat ${id}`) const chatListItem = await this.page.$(this._listItemSelector(id)) - await chatListItem.click() - const chatHeader = await this.page.waitForSelector("#_chat_header_area > .mdRGT04Link") - const chatListInfo = await chatListItem.evaluate( - (e, id) => window.__mautrixController.parseChatListItem(e, id), - id) + const chatName = await chatListItem.evaluate( + element => window.__mautrixController.getChatListItemName(element)) - this.log(`Waiting for chat header title to be "${chatListInfo.name}"`) - const chatHeaderTitleElement = await chatHeader.$(".mdRGT04Ttl") - await this.page.waitForFunction( - (element, targetText) => element.innerText == targetText, - {}, - chatHeaderTitleElement, chatListInfo.name) + const isCorrectChatVisible = (targetText) => { + const chatHeader = document.querySelector("#_chat_header_area > .mdRGT04Link") + if (!chatHeader) return false + const chatHeaderTitleElement = chatHeader.querySelector(".mdRGT04Ttl") + return chatHeaderTitleElement.innerText == targetText + } - return [chatListItem, chatListInfo, chatHeader] + if (await this.page.evaluate(isCorrectChatVisible, chatName)) { + this.log("Already viewing chat, no need to switch") + } else { + await chatListItem.click() + this.log(`Waiting for chat header title to be "${chatName}"`) + await this.page.waitForFunction( + isCorrectChatVisible, + {polling: "mutation"}, + chatName) + + // For consistent behaviour later, wait for the chat details sidebar to be hidden + await this.page.waitForFunction( + detailArea => detailArea.childElementCount == 0, + {}, + await this.page.$("#_chat_detail_area")) + } + } + + // TODO Commonize + async getParticipantList() { + await this._showParticipantList() + return await this.page.$("#_chat_detail_area > .mdRGT02Info ul.mdRGT13Ul") + } + + async _showParticipantList() { + const selector = "#_chat_detail_area > .mdRGT02Info ul.mdRGT13Ul" + let participantList = await this.page.$(selector) + if (!participantList) { + this.log("Participant list hidden, so clicking chat header to show it") + await this.page.click("#_chat_header_area > .mdRGT04Link") + participantList = await this.page.waitForSelector(selector) + } + //return participantList } async _getChatInfoUnsafe(id) { + const chatListItem = await this.page.$(this._listItemSelector(id)) + const chatListInfo = await chatListItem.evaluate( + (element, id) => window.__mautrixController.parseChatListItem(element, id), + id) + let [isDirect, isGroup, isRoom] = [false,false,false] switch (id.charAt(0)) { - case 'u': + case "u": isDirect = true break - case 'c': + case "c": isGroup = true break - case 'r': + case "r": isRoom = true break } - // TODO This will mark the chat as "read"! - const [chatListItem, chatListInfo, chatHeader] = await this._switchChat(id) - let participants - if (isGroup || isRoom) { + if (!isDirect) { this.log("Found multi-user chat, so clicking chat header to get participants") - await chatHeader.click() - const participantList = await this.page.waitForSelector("#_chat_detail_area > .mdRGT02Info ul.mdRGT13Ul") - if (isGroup) { - // TODO Is a group not actually created until a message is sent(?) - // If so, maybe don't create a portal until there is a message. - participants = await participantList.evaluate( - elem => window.__mautrixController.parseParticipantList(elem)) - } else if (isRoom) { - this.log("TODO: Room participant lists don't have user IDs...") - participants = [] - } - } - else - { + // TODO This will mark the chat as "read"! + await this._switchChat(id) + const participantList = await this.getParticipantList() + // TODO Is a group not actually created until a message is sent(?) + // If so, maybe don't create a portal until there is a message. + participants = await participantList.evaluate( + element => window.__mautrixController.parseParticipantList(element)) + } else { this.log(`Found direct chat with ${id}`) //const chatDetailArea = await this.page.waitForSelector("#_chat_detail_area > .mdRGT02Info") //await chatDetailArea.$(".MdTxtDesc02") || // 1:1 chat with custom title - get participant's real name participants = [{ id: id, + // TODO avatar, or leave null since this is a 1:1 chat name: chatListInfo.name, }] } + this.log("Found participants:") + for (const participant of participants) { + this.log(participant) + } return {participants, ...chatListInfo} } @@ -504,7 +545,7 @@ export default class MessagesPuppeteer { await this._switchChat(id) this.log("Waiting for messages to load") const messages = await this.page.evaluate( - () => window.__mautrixController.parseMessageList()) + id => window.__mautrixController.parseMessageList(id), id) return messages.filter(msg => msg.id > minID && !this.sentMessageIDs.has(msg.id)) } @@ -582,7 +623,7 @@ export default class MessagesPuppeteer { _sendLoginFailure(reason) { this.loginRunning = false - this.error(`Login failure: ${reason ? reason : 'cancelled'}`) + this.error(`Login failure: ${reason ? reason : "cancelled"}`) if (this.client) { this.client.sendFailure(reason).catch(err => this.error("Failed to send failure reason to client:", err))