forked from fair/matrix-puppeteer-line
Attempt at syncing list of chats and their participants
This commit is contained in:
parent
2e07a480c9
commit
1280916455
|
@ -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
|
||||
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -28,6 +28,7 @@ class Puppet:
|
|||
|
||||
mid: str
|
||||
name: Optional[str]
|
||||
# TODO avatar: Optional[str]
|
||||
is_registered: bool
|
||||
|
||||
async def insert(self) -> None:
|
||||
|
|
|
@ -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)
|
||||
)""")
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -41,7 +41,6 @@ class Participant(SerializableAttrs['Participant']):
|
|||
@dataclass
|
||||
class ChatInfo(ChatListInfo, SerializableAttrs['ChatInfo']):
|
||||
participants: List[Participant]
|
||||
readonly: bool
|
||||
|
||||
|
||||
@dataclass
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
Loading…
Reference in New Issue