From 1280916455b05d22decfd1b2fc0bd90a5730f724 Mon Sep 17 00:00:00 2001 From: Andrew Ferrazzutti Date: Tue, 16 Feb 2021 02:49:54 -0500 Subject: [PATCH] Attempt at syncing list of chats and their participants --- mautrix_line/commands/auth.py | 6 +-- mautrix_line/db/portal.py | 2 +- mautrix_line/db/puppet.py | 1 + mautrix_line/db/upgrade.py | 4 +- mautrix_line/portal.py | 5 +- mautrix_line/puppet.py | 1 + mautrix_line/rpc/types.py | 1 - puppet/src/contentscript.js | 78 ++++++++++++++++-------------- puppet/src/puppet.js | 90 +++++++++++++++++++++++------------ 9 files changed, 112 insertions(+), 76 deletions(-) diff --git a/mautrix_line/commands/auth.py b/mautrix_line/commands/auth.py index 5b12982..86c18b0 100644 --- a/mautrix_line/commands/auth.py +++ b/mautrix_line/commands/auth.py @@ -90,10 +90,8 @@ async def login_do(evt: CommandEvent, gen: AsyncGenerator[Tuple[str, str], None] # else: pass if not failure and evt.sender.command_status: - await evt.reply("Successfully logged in, now go home") - # TODO - #await evt.reply("Successfully logged in, now syncing") - #await evt.sender.sync() + await evt.reply("Successfully logged in, now syncing") + await evt.sender.sync() # else command was cancelled or failed. Don't post message about it, "cancel" command or failure did already evt.sender.command_status = None diff --git a/mautrix_line/db/portal.py b/mautrix_line/db/portal.py index 175c54d..fc57761 100644 --- a/mautrix_line/db/portal.py +++ b/mautrix_line/db/portal.py @@ -27,7 +27,7 @@ fake_db = Database("") if TYPE_CHECKING else None class Portal: db: ClassVar[Database] = fake_db - chat_id: int + chat_id: str other_user: str mxid: Optional[RoomID] name: Optional[str] diff --git a/mautrix_line/db/puppet.py b/mautrix_line/db/puppet.py index aa5f1f2..4795e44 100644 --- a/mautrix_line/db/puppet.py +++ b/mautrix_line/db/puppet.py @@ -28,6 +28,7 @@ class Puppet: mid: str name: Optional[str] + # TODO avatar: Optional[str] is_registered: bool async def insert(self) -> None: diff --git a/mautrix_line/db/upgrade.py b/mautrix_line/db/upgrade.py index d330606..33694ca 100644 --- a/mautrix_line/db/upgrade.py +++ b/mautrix_line/db/upgrade.py @@ -23,7 +23,7 @@ upgrade_table = UpgradeTable() @upgrade_table.register(description="Initial revision") async def upgrade_v1(conn: Connection) -> None: await conn.execute("""CREATE TABLE portal ( - chat_id INTEGER PRIMARY KEY, + chat_id TEXT PRIMARY KEY, other_user TEXT, mxid TEXT, name TEXT, @@ -42,7 +42,7 @@ async def upgrade_v1(conn: Connection) -> None: mxid TEXT NOT NULL, mx_room TEXT NOT NULL, mid INTEGER PRIMARY KEY, - chat_id INTEGER NOT NULL, + chat_id TEXT NOT NULL, UNIQUE (mxid, mx_room) )""") diff --git a/mautrix_line/portal.py b/mautrix_line/portal.py index b122501..dd4f0d6 100644 --- a/mautrix_line/portal.py +++ b/mautrix_line/portal.py @@ -247,7 +247,8 @@ class Portal(DBPortal, BasePortal): async def backfill(self, source: 'u.User') -> None: with self.backfill_lock: - await self._backfill(source) + self.log.debug("Backfill: TODO!") + #await self._backfill(source) async def _backfill(self, source: 'u.User') -> None: self.log.debug("Backfilling history through %s", source.mxid) @@ -362,7 +363,7 @@ class Portal(DBPortal, BasePortal): self.main_intent.mxid: 9001, }, "events": {}, - "events_default": 100 if info.readonly else 0, + "events_default": 100, "state_default": 50, "invite": 50, "redact": 0 diff --git a/mautrix_line/puppet.py b/mautrix_line/puppet.py index f84fcaf..748b6ec 100644 --- a/mautrix_line/puppet.py +++ b/mautrix_line/puppet.py @@ -63,6 +63,7 @@ class Puppet(DBPuppet, BasePuppet): async def update_info(self, info: Participant) -> None: update = False update = await self._update_name(info.name) or update + # TODO Update avatar if update: await self.update() diff --git a/mautrix_line/rpc/types.py b/mautrix_line/rpc/types.py index 61933e2..918add4 100644 --- a/mautrix_line/rpc/types.py +++ b/mautrix_line/rpc/types.py @@ -41,7 +41,6 @@ class Participant(SerializableAttrs['Participant']): @dataclass class ChatInfo(ChatListInfo, SerializableAttrs['ChatInfo']): participants: List[Participant] - readonly: bool @dataclass diff --git a/puppet/src/contentscript.js b/puppet/src/contentscript.js index 9996236..0818117 100644 --- a/puppet/src/contentscript.js +++ b/puppet/src/contentscript.js @@ -191,31 +191,28 @@ class MautrixController { /** * @typedef Participant * @type object - * @property {string} id - The unique-ish identifier for the participant + * @property {string} id - The member ID for the participant + * TODO @property {string} avatar - The URL of the participant's avatar * @property {string} name - The contact list name of the participant */ /** - * Parse a mw-conversation-details .participants list. + * Parse a group participants list. + * TODO Find what works for a *room* participants list...! * * @param {Element} element - The participant list element. * @return {[Participant]} - The list of participants. */ parseParticipantList(element) { - const participants = [] - for (const participantElem of element.getElementsByClassName("participant")) { - const nameElem = participantElem.querySelector(".participant-name") - const name = nameElem.innerText.trim() - let id = name - if (nameElem.nextElementSibling && nameElem.nextElementSibling.hasAttribute("data-e2e-details-participant-number")) { - id = nameElem.nextElementSibling.innerText + // TODO Slice to exclude first member, which is always yourself (right?) + //return Array.from(element.children).slice(1).map(child => { + return Array.from(element.children).map(child => { + return { + id: child.getAttribute("data-mid"), + // TODO avatar: child.querySelector("img").src, + name: child.querySelector(".mdRGT13Ttl").innerText, } - // For phone numbers, remove the + prefix - // For non-number IDs, prepend name_ and force-lowercase - id = /^\+\d+$/.test(id) ? id.substr(1) : `name_${id.toLowerCase()}` - participants.push({ name, id }) - } - return participants + }) } /** @@ -223,27 +220,42 @@ class MautrixController { * @type object * @property {number} id - The ID of the chat. * @property {string} name - The name of the chat. + * TODO @property {string} icon - The icon of the chat. * @property {string} lastMsg - The most recent message in the chat. * May be prefixed by sender name. * @property {string} lastMsgDate - An imprecise date for the most recent message * (e.g. "7:16 PM", "Thu" or "Aug 4") */ + getChatListItemId(element) { + return element.getAttribute("data-chatid") + } + + getChatListItemName(element) { + return element.querySelector(".mdCMN04Ttl").innerText + } + + getChatListItemLastMsg(element) { + return element.querySelector(".mdCMN04Desc").innerText + } + + getChatListItemLastMsgDate(element) { + return element.querySelector("time").innerText + } + /** - * Parse a mws-conversation-list-item element. + * Parse a conversation list item element. * * @param {Element} element - The element to parse. * @return {ChatListInfo} - The info in the element. */ parseChatListItem(element) { - if (element.tagName.toLowerCase() === "mws-conversation-list-item") { - element = element.querySelector("a.list-item") - } return { - id: +element.getAttribute("href").split("/").pop(), - name: element.querySelector("h3.name").innerText, - lastMsg: element.querySelector("mws-conversation-snippet").innerText, - lastMsgDate: element.querySelector("mws-relative-timestamp").innerText, + id: this.getChatListItemId(element), + name: this.getChatListItemName(element), + // TODO icon, but only for groups + lastMsg: this.getChatListItemLastMsg(element), + lastMsgDate: this.getChatListItemLastMsgDate(element), } } @@ -253,14 +265,8 @@ class MautrixController { * @return {[ChatListInfo]} - The list of chats. */ parseChatList(element) { - const chats = [] - for (const child of element.children) { - if (child.tagName.toLowerCase() !== "mws-conversation-list-item") { - continue - } - chats.push(this.parseChatListItem(child)) - } - return chats + return Array.from(element.children).map( + child => this.parseChatListItem(child.firstElementChild)) } /** @@ -301,6 +307,7 @@ class MautrixController { * @private */ _observeChatListMutations(mutations) { + /* TODO const changedChatIDs = new Set() for (const change of mutations) { console.debug("Chat list mutation:", change) @@ -319,6 +326,7 @@ class MautrixController { () => console.debug("Chat list mutations dispatched"), err => console.error("Error dispatching chat list mutations:", err)) } + */ } /** @@ -330,16 +338,16 @@ class MautrixController { if (this.chatListObserver !== null) { this.removeChatListObserver() } - /* TODO this.chatListObserver = new MutationObserver(mutations => { + /* TODO try { this._observeChatListMutations(mutations) } catch (err) { console.error("Error observing chat list mutations:", err) } + */ }) this.chatListObserver.observe(element, { childList: true, subtree: true }) - */ console.debug("Started chat list observer") } @@ -406,7 +414,7 @@ class MautrixController { } } - addEmailAppearObserver(element, login_type) { + addEmailAppearObserver(element) { if (this.emailAppearObserver !== null) { this.removeEmailAppearObserver() } @@ -433,7 +441,7 @@ class MautrixController { } } - addPINAppearObserver(element, login_type) { + addPINAppearObserver(element) { if (this.pinAppearObserver !== null) { this.removePINAppearObserver() } diff --git a/puppet/src/puppet.js b/puppet/src/puppet.js index 4e8dd1e..c372e39 100644 --- a/puppet/src/puppet.js +++ b/puppet/src/puppet.js @@ -229,7 +229,8 @@ export default class MessagesPuppeteer { await this.page.waitForFunction( messageSyncElement => { const text = messageSyncElement.innerText - return text == 'Syncing messages... 100%' + return text.startsWith("Syncing messages...") + && (text.endsWith("100%") || text.endsWith("NaN%")) }, {}, result) @@ -328,18 +329,14 @@ export default class MessagesPuppeteer { * @return {Promise<[ChatListInfo]>} - List of chat IDs in order of most recent message. */ async getRecentChats() { - /* TODO - return await this.page.$eval("mws-conversations-list .conv-container", + return await this.page.$eval("#_chat_list_body", elem => window.__mautrixController.parseChatList(elem)) - */ - return null } /** * @typedef ChatInfo * @type object * @property {[Participant]} participants - * @property {boolean} readonly */ /** @@ -391,7 +388,7 @@ export default class MessagesPuppeteer { async startObserving() { this.log("Adding chat list observer") - await this.page.$eval("#wrap_chat_list", + await this.page.$eval("#_chat_list_body", element => window.__mautrixController.addChatListObserver(element)) } @@ -401,38 +398,69 @@ export default class MessagesPuppeteer { } _listItemSelector(id) { - // TODO - //return `mws-conversation-list-item > a.list-item[href="/web/conversations/${id}"]` - return '' + return `#_chat_list_body div[data-chatid="${id}"]` } async _switchChatUnsafe(id) { this.log("Switching to chat", id) - await this.page.click(this._listItemSelector(id)) + const chatListItem = await this.page.$(this._listItemSelector(id)) + await chatListItem.click() + return chatListItem } async _getChatInfoUnsafe(id) { - await this._switchChatUnsafe(id) - await this.page.waitForSelector("mw-conversation-menu button", { timeout: 500 }) - await this.page.click("mw-conversation-menu button") - await this.page.waitForSelector(".mat-menu-panel button.mat-menu-item.details", - { timeout: 500 }) - const readonly = await this.page.$("mw-conversation-container .compose-readonly") !== null - // There's a 250ms animation and I don't know how to wait for it properly - await sleep(250) - await this.page.click(".mat-menu-panel button.mat-menu-item.details") - await this.page.waitForSelector("mws-dialog mw-conversation-details .participants", - { timeout: 500 }) - const participants = await this.page.$eval( - "mws-dialog mw-conversation-details .participants", - elem => window.__mautrixController.parseParticipantList(elem)) - await this.page.click("mws-dialog mat-dialog-actions button.confirm") - return { - participants, - readonly, - ...await this.page.$eval(this._listItemSelector(id), - elem => window.__mautrixController.parseChatListItem(elem)), + // TODO This will mark the chat as "read"! + const chatListItem = await this._switchChatUnsafe(id) + const chatHeader = await this.page.waitForSelector("#_chat_header_area > .mdRGT04Link") + + /* TODO Make this work + const chatListName = await chatListItem.evaluate(e => window.__mautrixController.getChatListItemName(e)) + this.log(`Waiting for chat header title to be "${chatListName}"`) + const chatHeaderTitleElement = await chatHeader.$(".mdRGT04Ttl") + await this.page.waitForFunction((element, targetText) => { + element.innerText == targetText + }, + {}, + chatHeaderTitleElement, chatListName) + */await this.page.waitForTimeout(3000) + + this.log("Clicking chat header") + await chatHeader.click() + const chatDetailArea = await this.page.waitForSelector("#_chat_detail_area > .mdRGT02Info") + + this.log("Gathering participants") + let participants + const participantList = await chatDetailArea.$("ul.mdRGT13Ul") + if (participantList) { + if (await chatDetailArea.$("#leaveGroup")) { + this.log("Found group") + // This is a *group* (like a Matrix room) + // 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 (await chatDetailArea.$("ul [data-click-name='leave_room'")) { + this.log("Found room") + // This is a *room* (canonical multi-user DM) + // TODO Find a way to get participant IDs from a room member list!! + participants = [] + } } + else + { + this.log("Found chat") + //await chatDetailArea.$(".MdTxtDesc02") || // 1:1 chat with custom title - get participant's real name + participants = [{ + id: id, // the ID of a 1:1 chat is the other user's member ID + name: await participantElement.$eval( + "#_chat_contact_detail_view > a", + element => element.innerText), + }] + // TODO Or just look up the member ID in the contact list? + } + + this.log(`Found participants: ${participants}`) + return participants } async _sendMessageUnsafe(chatID, text) {