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
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

View File

@ -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]

View File

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

View File

@ -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)
)""")

View File

@ -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

View File

@ -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()

View File

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

View File

@ -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()
}

View File

@ -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) {