Attempt at syncing list of chats and their participants

This commit is contained in:
Andrew Ferrazzutti 2021-02-16 02:49:54 -05:00
parent cc0c355c9a
commit 76f2478c8c
9 changed files with 112 additions and 76 deletions

View File

@ -90,10 +90,8 @@ async def login_do(evt: CommandEvent, gen: AsyncGenerator[Tuple[str, str], None]
# else: pass # else: pass
if not failure and evt.sender.command_status: if not failure and evt.sender.command_status:
await evt.reply("Successfully logged in, now go home") await evt.reply("Successfully logged in, now syncing")
# TODO 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 # else command was cancelled or failed. Don't post message about it, "cancel" command or failure did already
evt.sender.command_status = None evt.sender.command_status = None

View File

@ -27,7 +27,7 @@ fake_db = Database("") if TYPE_CHECKING else None
class Portal: class Portal:
db: ClassVar[Database] = fake_db db: ClassVar[Database] = fake_db
chat_id: int chat_id: str
other_user: str other_user: str
mxid: Optional[RoomID] mxid: Optional[RoomID]
name: Optional[str] name: Optional[str]

View File

@ -28,6 +28,7 @@ class Puppet:
mid: str mid: str
name: Optional[str] name: Optional[str]
# TODO avatar: Optional[str]
is_registered: bool is_registered: bool
async def insert(self) -> None: async def insert(self) -> None:

View File

@ -23,7 +23,7 @@ upgrade_table = UpgradeTable()
@upgrade_table.register(description="Initial revision") @upgrade_table.register(description="Initial revision")
async def upgrade_v1(conn: Connection) -> None: async def upgrade_v1(conn: Connection) -> None:
await conn.execute("""CREATE TABLE portal ( await conn.execute("""CREATE TABLE portal (
chat_id INTEGER PRIMARY KEY, chat_id TEXT PRIMARY KEY,
other_user TEXT, other_user TEXT,
mxid TEXT, mxid TEXT,
name TEXT, name TEXT,
@ -42,7 +42,7 @@ async def upgrade_v1(conn: Connection) -> None:
mxid TEXT NOT NULL, mxid TEXT NOT NULL,
mx_room TEXT NOT NULL, mx_room TEXT NOT NULL,
mid INTEGER PRIMARY KEY, mid INTEGER PRIMARY KEY,
chat_id INTEGER NOT NULL, chat_id TEXT NOT NULL,
UNIQUE (mxid, mx_room) UNIQUE (mxid, mx_room)
)""") )""")

View File

@ -247,7 +247,8 @@ class Portal(DBPortal, BasePortal):
async def backfill(self, source: 'u.User') -> None: async def backfill(self, source: 'u.User') -> None:
with self.backfill_lock: 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: async def _backfill(self, source: 'u.User') -> None:
self.log.debug("Backfilling history through %s", source.mxid) self.log.debug("Backfilling history through %s", source.mxid)
@ -362,7 +363,7 @@ class Portal(DBPortal, BasePortal):
self.main_intent.mxid: 9001, self.main_intent.mxid: 9001,
}, },
"events": {}, "events": {},
"events_default": 100 if info.readonly else 0, "events_default": 100,
"state_default": 50, "state_default": 50,
"invite": 50, "invite": 50,
"redact": 0 "redact": 0

View File

@ -63,6 +63,7 @@ class Puppet(DBPuppet, BasePuppet):
async def update_info(self, info: Participant) -> None: async def update_info(self, info: Participant) -> None:
update = False update = False
update = await self._update_name(info.name) or update update = await self._update_name(info.name) or update
# TODO Update avatar
if update: if update:
await self.update() await self.update()

View File

@ -41,7 +41,6 @@ class Participant(SerializableAttrs['Participant']):
@dataclass @dataclass
class ChatInfo(ChatListInfo, SerializableAttrs['ChatInfo']): class ChatInfo(ChatListInfo, SerializableAttrs['ChatInfo']):
participants: List[Participant] participants: List[Participant]
readonly: bool
@dataclass @dataclass

View File

@ -191,31 +191,28 @@ class MautrixController {
/** /**
* @typedef Participant * @typedef Participant
* @type object * @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 * @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. * @param {Element} element - The participant list element.
* @return {[Participant]} - The list of participants. * @return {[Participant]} - The list of participants.
*/ */
parseParticipantList(element) { parseParticipantList(element) {
const participants = [] // TODO Slice to exclude first member, which is always yourself (right?)
for (const participantElem of element.getElementsByClassName("participant")) { //return Array.from(element.children).slice(1).map(child => {
const nameElem = participantElem.querySelector(".participant-name") return Array.from(element.children).map(child => {
const name = nameElem.innerText.trim() return {
let id = name id: child.getAttribute("data-mid"),
if (nameElem.nextElementSibling && nameElem.nextElementSibling.hasAttribute("data-e2e-details-participant-number")) { // TODO avatar: child.querySelector("img").src,
id = nameElem.nextElementSibling.innerText 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 * @type object
* @property {number} id - The ID of the chat. * @property {number} id - The ID of the chat.
* @property {string} name - The name 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. * @property {string} lastMsg - The most recent message in the chat.
* May be prefixed by sender name. * May be prefixed by sender name.
* @property {string} lastMsgDate - An imprecise date for the most recent message * @property {string} lastMsgDate - An imprecise date for the most recent message
* (e.g. "7:16 PM", "Thu" or "Aug 4") * (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. * @param {Element} element - The element to parse.
* @return {ChatListInfo} - The info in the element. * @return {ChatListInfo} - The info in the element.
*/ */
parseChatListItem(element) { parseChatListItem(element) {
if (element.tagName.toLowerCase() === "mws-conversation-list-item") {
element = element.querySelector("a.list-item")
}
return { return {
id: +element.getAttribute("href").split("/").pop(), id: this.getChatListItemId(element),
name: element.querySelector("h3.name").innerText, name: this.getChatListItemName(element),
lastMsg: element.querySelector("mws-conversation-snippet").innerText, // TODO icon, but only for groups
lastMsgDate: element.querySelector("mws-relative-timestamp").innerText, lastMsg: this.getChatListItemLastMsg(element),
lastMsgDate: this.getChatListItemLastMsgDate(element),
} }
} }
@ -253,14 +265,8 @@ class MautrixController {
* @return {[ChatListInfo]} - The list of chats. * @return {[ChatListInfo]} - The list of chats.
*/ */
parseChatList(element) { parseChatList(element) {
const chats = [] return Array.from(element.children).map(
for (const child of element.children) { child => this.parseChatListItem(child.firstElementChild))
if (child.tagName.toLowerCase() !== "mws-conversation-list-item") {
continue
}
chats.push(this.parseChatListItem(child))
}
return chats
} }
/** /**
@ -301,6 +307,7 @@ class MautrixController {
* @private * @private
*/ */
_observeChatListMutations(mutations) { _observeChatListMutations(mutations) {
/* TODO
const changedChatIDs = new Set() const changedChatIDs = new Set()
for (const change of mutations) { for (const change of mutations) {
console.debug("Chat list mutation:", change) console.debug("Chat list mutation:", change)
@ -319,6 +326,7 @@ class MautrixController {
() => console.debug("Chat list mutations dispatched"), () => console.debug("Chat list mutations dispatched"),
err => console.error("Error dispatching chat list mutations:", err)) err => console.error("Error dispatching chat list mutations:", err))
} }
*/
} }
/** /**
@ -330,16 +338,16 @@ class MautrixController {
if (this.chatListObserver !== null) { if (this.chatListObserver !== null) {
this.removeChatListObserver() this.removeChatListObserver()
} }
/* TODO
this.chatListObserver = new MutationObserver(mutations => { this.chatListObserver = new MutationObserver(mutations => {
/* TODO
try { try {
this._observeChatListMutations(mutations) this._observeChatListMutations(mutations)
} catch (err) { } catch (err) {
console.error("Error observing chat list mutations:", err) console.error("Error observing chat list mutations:", err)
} }
*/
}) })
this.chatListObserver.observe(element, { childList: true, subtree: true }) this.chatListObserver.observe(element, { childList: true, subtree: true })
*/
console.debug("Started chat list observer") console.debug("Started chat list observer")
} }
@ -406,7 +414,7 @@ class MautrixController {
} }
} }
addEmailAppearObserver(element, login_type) { addEmailAppearObserver(element) {
if (this.emailAppearObserver !== null) { if (this.emailAppearObserver !== null) {
this.removeEmailAppearObserver() this.removeEmailAppearObserver()
} }
@ -433,7 +441,7 @@ class MautrixController {
} }
} }
addPINAppearObserver(element, login_type) { addPINAppearObserver(element) {
if (this.pinAppearObserver !== null) { if (this.pinAppearObserver !== null) {
this.removePINAppearObserver() this.removePINAppearObserver()
} }

View File

@ -229,7 +229,8 @@ export default class MessagesPuppeteer {
await this.page.waitForFunction( await this.page.waitForFunction(
messageSyncElement => { messageSyncElement => {
const text = messageSyncElement.innerText const text = messageSyncElement.innerText
return text == 'Syncing messages... 100%' return text.startsWith("Syncing messages...")
&& (text.endsWith("100%") || text.endsWith("NaN%"))
}, },
{}, {},
result) result)
@ -328,18 +329,14 @@ export default class MessagesPuppeteer {
* @return {Promise<[ChatListInfo]>} - List of chat IDs in order of most recent message. * @return {Promise<[ChatListInfo]>} - List of chat IDs in order of most recent message.
*/ */
async getRecentChats() { async getRecentChats() {
/* TODO return await this.page.$eval("#_chat_list_body",
return await this.page.$eval("mws-conversations-list .conv-container",
elem => window.__mautrixController.parseChatList(elem)) elem => window.__mautrixController.parseChatList(elem))
*/
return null
} }
/** /**
* @typedef ChatInfo * @typedef ChatInfo
* @type object * @type object
* @property {[Participant]} participants * @property {[Participant]} participants
* @property {boolean} readonly
*/ */
/** /**
@ -391,7 +388,7 @@ export default class MessagesPuppeteer {
async startObserving() { async startObserving() {
this.log("Adding chat list observer") 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)) element => window.__mautrixController.addChatListObserver(element))
} }
@ -401,39 +398,70 @@ export default class MessagesPuppeteer {
} }
_listItemSelector(id) { _listItemSelector(id) {
// TODO return `#_chat_list_body div[data-chatid="${id}"]`
//return `mws-conversation-list-item > a.list-item[href="/web/conversations/${id}"]`
return ''
} }
async _switchChatUnsafe(id) { async _switchChatUnsafe(id) {
this.log("Switching to chat", 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) { async _getChatInfoUnsafe(id) {
await this._switchChatUnsafe(id) // TODO This will mark the chat as "read"!
await this.page.waitForSelector("mw-conversation-menu button", { timeout: 500 }) const chatListItem = await this._switchChatUnsafe(id)
await this.page.click("mw-conversation-menu button") const chatHeader = await this.page.waitForSelector("#_chat_header_area > .mdRGT04Link")
await this.page.waitForSelector(".mat-menu-panel button.mat-menu-item.details",
{ timeout: 500 }) /* TODO Make this work
const readonly = await this.page.$("mw-conversation-container .compose-readonly") !== null const chatListName = await chatListItem.evaluate(e => window.__mautrixController.getChatListItemName(e))
// There's a 250ms animation and I don't know how to wait for it properly this.log(`Waiting for chat header title to be "${chatListName}"`)
await sleep(250) const chatHeaderTitleElement = await chatHeader.$(".mdRGT04Ttl")
await this.page.click(".mat-menu-panel button.mat-menu-item.details") await this.page.waitForFunction((element, targetText) => {
await this.page.waitForSelector("mws-dialog mw-conversation-details .participants", element.innerText == targetText
{ timeout: 500 }) },
const participants = await this.page.$eval( {},
"mws-dialog mw-conversation-details .participants", 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)) elem => window.__mautrixController.parseParticipantList(elem))
await this.page.click("mws-dialog mat-dialog-actions button.confirm") } else if (await chatDetailArea.$("ul [data-click-name='leave_room'")) {
return { this.log("Found room")
participants, // This is a *room* (canonical multi-user DM)
readonly, // TODO Find a way to get participant IDs from a room member list!!
...await this.page.$eval(this._listItemSelector(id), participants = []
elem => window.__mautrixController.parseChatListItem(elem)),
} }
} }
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) { async _sendMessageUnsafe(chatID, text) {
await this._switchChatUnsafe(chatID) await this._switchChatUnsafe(chatID)