Wait for outbound images to be sent

Also prevent possible race condition in waiting for inbound images, and
generally tighten up file sending/receiving.
This commit is contained in:
Andrew Ferrazzutti 2021-03-29 01:25:05 -04:00
parent 5a33500765
commit afcc206a93
2 changed files with 85 additions and 40 deletions

View File

@ -150,6 +150,10 @@ class MautrixController {
* @property {string} [image] - The URL to the image in the message. * @property {string} [image] - The URL to the image in the message.
*/ */
_isLoadedImageURL(src) {
return src?.startsWith("blob:")
}
/** /**
* Parse a message element (mws-message-wrapper) * Parse a message element (mws-message-wrapper)
* *
@ -213,14 +217,11 @@ class MautrixController {
} else if (messageElement.classList.contains("mdRGT07Image")) { } else if (messageElement.classList.contains("mdRGT07Image")) {
const img = messageElement.querySelector(".mdRGT07MsgImg > img") const img = messageElement.querySelector(".mdRGT07MsgImg > img")
if (img) { if (img) {
if (img.src.startsWith("blob:")) {
messageData.image_url = img.src
} else {
let resolve let resolve
// TODO Should reject on "#_chat_message_image_failure" // TODO Should reject on "#_chat_message_image_failure"
let observer = new MutationObserver((changes) => { let observer = new MutationObserver((changes) => {
for (const change of changes) { for (const change of changes) {
if (change.target.src.startsWith("blob:")) { if (this._isLoadedImageURL(change.target.src) && observer) {
observer.disconnect() observer.disconnect()
observer = null observer = null
resolve(change.target.src) resolve(change.target.src)
@ -229,6 +230,14 @@ class MautrixController {
} }
}) })
observer.observe(img, { attributes: true, attributeFilter: ["src"] }) observer.observe(img, { attributes: true, attributeFilter: ["src"] })
if (this._isLoadedImageURL(img.src)) {
// Check for this AFTER attaching the observer, in case
// the image loaded after the img element was found but
// before the observer was attached.
messageData.image_url = img.src
observer.disconnect()
} else {
messageData.image_url = await new Promise((realResolve, reject) => { messageData.image_url = await new Promise((realResolve, reject) => {
resolve = realResolve resolve = realResolve
setTimeout(() => { setTimeout(() => {
@ -236,7 +245,7 @@ class MautrixController {
observer.disconnect() observer.disconnect()
resolve(img.src) resolve(img.src)
} }
}, 5000) }, 10000) // Longer timeout for image downloads
}) })
} }
} }
@ -245,10 +254,11 @@ class MautrixController {
} }
promiseOwnMessage() { promiseOwnMessage(timeoutLimitMillis, successSelector, failureSelector=null) {
let observer let observer
let msgID = -1 let msgID = -1
let resolve let resolve
let reject
const resolveMessage = () => { const resolveMessage = () => {
observer.disconnect() observer.disconnect()
@ -257,50 +267,81 @@ class MautrixController {
resolve(msgID) resolve(msgID)
} }
const invisibleTimeCallback = (changes) => { const rejectMessage = failureElement => {
observer.disconnect()
observer = null
reject(failureElement)
}
const changeCallback = (changes) => {
for (const change of changes) { for (const change of changes) {
for (const addedNode of change.addedNodes) { for (const addedNode of change.addedNodes) {
if (addedNode.classList.contains("mdRGT07Own")) { if (addedNode.classList.contains("mdRGT07Own")) {
const timeElement = addedNode.querySelector("time") const successElement = addedNode.querySelector(successSelector)
if (timeElement) { if (successElement) {
console.log("Found success element")
console.log(successElement)
msgID = +addedNode.getAttribute("data-local-id") msgID = +addedNode.getAttribute("data-local-id")
if (timeElement.classList.contains(".MdNonDisp")) { if (successElement.classList.contains("MdNonDisp")) {
console.log("Invisible success, wait")
observer.disconnect() observer.disconnect()
observer = new MutationObserver(visibleTimeCallback) observer = new MutationObserver(getVisibleCallback(true))
observer.observe(timeElement, { attributes: true, attributeFilter: ["class"] }) observer.observe(successElement, { attributes: true, attributeFilter: ["class"] })
} else { } else {
console.log("Already visible success")
resolveMessage() resolveMessage()
} }
return return
} else if (failureSelector) {
const failureElement = addedNode.querySelector(failureSelector)
if (failureElement) {
console.log("Found failure element")
console.log(failureElement)
if (failureElement.classList.contains("MdNonDisp")) {
console.log("Invisible failure, wait")
observer.disconnect()
observer = new MutationObserver(getVisibleCallback(false))
observer.observe(successElement, { attributes: true, attributeFilter: ["class"] })
} else {
console.log("Already visible failure")
rejectMessage(failureElement)
}
return
}
} }
} }
} }
} }
} }
const visibleTimeCallback = (changes) => { const getVisibleCallback = isSuccess => {
return changes => {
for (const change of changes) { for (const change of changes) {
if (!change.target.classList.contains("MdNonDisp")) { if (!change.target.classList.contains("MdNonDisp")) {
resolveMessage() console.log(`Waited for visible ${isSuccess ? "success" : "failure"}`)
console.log(change.target)
isSuccess ? resolveMessage() : rejectMessage(change.target)
return return
} }
} }
} }
}
observer = new MutationObserver(invisibleTimeCallback) observer = new MutationObserver(changeCallback)
observer.observe( observer.observe(
document.querySelector("#_chat_room_msg_list"), document.querySelector("#_chat_room_msg_list"),
{ childList: true }) { childList: true })
return new Promise((realResolve, reject) => { return new Promise((realResolve, realReject) => {
resolve = realResolve resolve = realResolve
// TODO Handle a timeout better than this reject = realReject
setTimeout(() => { setTimeout(() => {
if (observer) { if (observer) {
console.log("Timeout!")
observer.disconnect() observer.disconnect()
reject() reject()
} }
}, 5000) }, timeoutLimitMillis)
}) })
} }

View File

@ -403,7 +403,10 @@ export default class MessagesPuppeteer {
async _sendFileUnsafe(chatID, filePath) { async _sendFileUnsafe(chatID, filePath) {
await this._switchChat(chatID) await this._switchChat(chatID)
const promise = this.page.evaluate( const promise = this.page.evaluate(
() => window.__mautrixController.promiseOwnMessage()) () => window.__mautrixController.promiseOwnMessage(
10000, // Use longer timeout for file uploads
"#_chat_message_success_menu",
"#_chat_message_fail_menu"))
try { try {
this.log(`About to ask for file chooser in ${chatID}`) this.log(`About to ask for file chooser in ${chatID}`)
@ -420,13 +423,14 @@ export default class MessagesPuppeteer {
// TODO Commonize with text message sending // TODO Commonize with text message sending
try { try {
this.log("Waiting for message to be sent") this.log("Waiting for file to be sent")
const id = await promise const id = await promise
this.log(`Successfully sent message ${id} to ${chatID}`) this.log(`Successfully sent file in message ${id} to ${chatID}`)
return id return id
} catch (e) { } catch (e) {
// TODO Handle a timeout better than this this.error(`Error sending file to ${chatID}`)
this.error(`Timed out waiting for message to ${chatID}`) // TODO Figure out why e is undefined...
//this.error(e)
return -1 return -1
} }
} }
@ -551,7 +555,7 @@ export default class MessagesPuppeteer {
async _sendMessageUnsafe(chatID, text) { async _sendMessageUnsafe(chatID, text) {
await this._switchChat(chatID) await this._switchChat(chatID)
const promise = this.page.evaluate( const promise = this.page.evaluate(
() => window.__mautrixController.promiseOwnMessage()) () => window.__mautrixController.promiseOwnMessage(5000, "time"))
const input = await this.page.$("#_chat_room_input") const input = await this.page.$("#_chat_room_input")
await input.click() await input.click()
@ -564,8 +568,8 @@ export default class MessagesPuppeteer {
this.log(`Successfully sent message ${id} to ${chatID}`) this.log(`Successfully sent message ${id} to ${chatID}`)
return id return id
} catch (e) { } catch (e) {
// TODO Handle a timeout better than this // TODO Catch if something other than a timeout
this.error(`Timed out waiting for message to ${chatID}`) this.error(`Timed out sending message to ${chatID}`)
return -1 return -1
} }
} }