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
|
# 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
|
||||||
|
|
||||||
|
|
|
@ -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]
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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)
|
||||||
)""")
|
)""")
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Loading…
Reference in New Issue