2021-03-15 01:40:56 -04:00
|
|
|
// matrix-puppeteer-line - A very hacky Matrix-LINE bridge based on running LINE's Chrome extension in Puppeteer
|
2021-02-26 01:28:54 -05:00
|
|
|
// Copyright (C) 2020-2021 Tulir Asokan, Andrew Ferrazzutti
|
2020-08-18 09:47:06 -04:00
|
|
|
//
|
|
|
|
// This program is free software: you can redistribute it and/or modify
|
|
|
|
// it under the terms of the GNU Affero General Public License as published by
|
|
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
|
|
// (at your option) any later version.
|
|
|
|
//
|
|
|
|
// This program is distributed in the hope that it will be useful,
|
|
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
// GNU Affero General Public License for more details.
|
|
|
|
//
|
|
|
|
// You should have received a copy of the GNU Affero General Public License
|
|
|
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
import process from "process"
|
|
|
|
import path from "path"
|
|
|
|
|
|
|
|
import puppeteer from "puppeteer"
|
|
|
|
import chrono from "chrono-node"
|
|
|
|
|
|
|
|
import TaskQueue from "./taskqueue.js"
|
|
|
|
import { sleep } from "./util.js"
|
|
|
|
|
|
|
|
export default class MessagesPuppeteer {
|
2020-08-24 09:45:44 -04:00
|
|
|
static profileDir = "./profiles"
|
2020-08-24 11:26:07 -04:00
|
|
|
static executablePath = undefined
|
2021-04-27 02:59:16 -04:00
|
|
|
static devtools = false
|
2020-08-24 11:53:56 -04:00
|
|
|
static noSandbox = false
|
2021-06-18 00:09:20 -04:00
|
|
|
static viewport = { width: 960, height: 840 }
|
2021-02-04 21:52:14 -05:00
|
|
|
static url = undefined
|
2021-02-25 22:21:11 -05:00
|
|
|
static extensionDir = "extension_files"
|
2020-08-18 09:47:06 -04:00
|
|
|
|
|
|
|
/**
|
|
|
|
*
|
|
|
|
* @param {string} id
|
|
|
|
* @param {?Client} [client]
|
|
|
|
*/
|
2021-06-17 00:42:06 -04:00
|
|
|
constructor(id, ownID, sendPlaceholders, client = null) {
|
2020-08-24 09:45:44 -04:00
|
|
|
let profilePath = path.join(MessagesPuppeteer.profileDir, id)
|
2020-08-18 09:47:06 -04:00
|
|
|
if (!profilePath.startsWith("/")) {
|
|
|
|
profilePath = path.join(process.cwd(), profilePath)
|
|
|
|
}
|
|
|
|
this.id = id
|
2021-06-15 02:48:29 -04:00
|
|
|
this.ownID = ownID
|
2021-06-17 00:42:06 -04:00
|
|
|
this.sendPlaceholders = sendPlaceholders
|
2020-08-18 09:47:06 -04:00
|
|
|
this.profilePath = profilePath
|
|
|
|
this.updatedChats = new Set()
|
|
|
|
this.mostRecentMessages = new Map()
|
2021-06-30 03:04:25 -04:00
|
|
|
this.mostRecentOwnMessages = new Map()
|
|
|
|
this.mostRecentReceipts = new Map()
|
2021-06-10 02:10:18 -04:00
|
|
|
this.numChatNotifications = new Map()
|
2021-06-30 03:04:25 -04:00
|
|
|
this.cycleTimerID = null
|
2020-08-18 09:47:06 -04:00
|
|
|
this.taskQueue = new TaskQueue(this.id)
|
|
|
|
this.client = client
|
|
|
|
}
|
|
|
|
|
|
|
|
log(...text) {
|
|
|
|
console.log(`[Puppeteer/${this.id}]`, ...text)
|
|
|
|
}
|
|
|
|
|
|
|
|
error(...text) {
|
|
|
|
console.error(`[Puppeteer/${this.id}]`, ...text)
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Start the browser and open the messages for web page.
|
|
|
|
* This must be called before doing anything else.
|
|
|
|
*/
|
2021-02-10 02:34:19 -05:00
|
|
|
async start() {
|
2020-08-18 09:47:06 -04:00
|
|
|
this.log("Launching browser")
|
2021-02-04 21:52:14 -05:00
|
|
|
|
2021-06-18 00:09:20 -04:00
|
|
|
const args = [
|
2021-02-10 02:34:19 -05:00
|
|
|
`--disable-extensions-except=${MessagesPuppeteer.extensionDir}`,
|
2021-06-18 00:09:20 -04:00
|
|
|
`--load-extension=${MessagesPuppeteer.extensionDir}`,
|
|
|
|
`--window-size=${MessagesPuppeteer.viewport.width},${MessagesPuppeteer.viewport.height+120}`,
|
2021-02-10 02:34:19 -05:00
|
|
|
]
|
2021-04-27 02:59:16 -04:00
|
|
|
if (MessagesPuppeteer.noSandbox) {
|
2021-06-18 00:09:20 -04:00
|
|
|
args = args.concat(`--no-sandbox`)
|
2021-04-27 02:59:16 -04:00
|
|
|
}
|
2021-02-04 21:52:14 -05:00
|
|
|
|
2020-08-18 09:47:06 -04:00
|
|
|
this.browser = await puppeteer.launch({
|
2020-08-24 11:26:07 -04:00
|
|
|
executablePath: MessagesPuppeteer.executablePath,
|
2020-08-18 09:47:06 -04:00
|
|
|
userDataDir: this.profilePath,
|
2021-06-18 00:09:20 -04:00
|
|
|
args: args,
|
2021-02-04 21:52:14 -05:00
|
|
|
headless: false, // Needed to load extensions
|
2020-08-24 11:26:07 -04:00
|
|
|
defaultViewport: MessagesPuppeteer.viewport,
|
2021-04-27 02:59:16 -04:00
|
|
|
devtools: MessagesPuppeteer.devtools,
|
|
|
|
timeout: 0,
|
2020-08-18 09:47:06 -04:00
|
|
|
})
|
|
|
|
this.log("Opening new tab")
|
|
|
|
const pages = await this.browser.pages()
|
|
|
|
if (pages.length > 0) {
|
|
|
|
this.page = pages[0]
|
|
|
|
} else {
|
|
|
|
this.page = await this.browser.newPage()
|
|
|
|
}
|
2021-06-11 02:53:30 -04:00
|
|
|
|
|
|
|
this.blankPage = await this.browser.newPage()
|
|
|
|
await this.page.bringToFront()
|
|
|
|
|
2020-08-24 11:26:07 -04:00
|
|
|
this.log("Opening", MessagesPuppeteer.url)
|
2021-02-10 02:34:19 -05:00
|
|
|
await this.page.setBypassCSP(true) // Needed to load content scripts
|
|
|
|
await this._preparePage(true)
|
2020-08-18 09:47:06 -04:00
|
|
|
|
|
|
|
this.log("Exposing functions")
|
|
|
|
await this.page.exposeFunction("__mautrixReceiveQR", this._receiveQRChange.bind(this))
|
2021-02-10 02:34:19 -05:00
|
|
|
await this.page.exposeFunction("__mautrixSendEmailCredentials", this._sendEmailCredentials.bind(this))
|
|
|
|
await this.page.exposeFunction("__mautrixReceivePIN", this._receivePIN.bind(this))
|
2020-08-18 09:47:06 -04:00
|
|
|
await this.page.exposeFunction("__mautrixReceiveChanges",
|
|
|
|
this._receiveChatListChanges.bind(this))
|
2021-04-23 03:38:13 -04:00
|
|
|
await this.page.exposeFunction("__mautrixReceiveMessages",
|
|
|
|
this._receiveMessages.bind(this))
|
2021-04-20 20:01:50 -04:00
|
|
|
await this.page.exposeFunction("__mautrixReceiveReceiptDirectLatest",
|
|
|
|
this._receiveReceiptDirectLatest.bind(this))
|
|
|
|
await this.page.exposeFunction("__mautrixReceiveReceiptMulti",
|
|
|
|
this._receiveReceiptMulti.bind(this))
|
2021-05-30 17:41:28 -04:00
|
|
|
await this.page.exposeFunction("__mautrixLoggedOut",
|
|
|
|
this._onLoggedOut.bind(this))
|
2021-02-20 20:00:32 -05:00
|
|
|
await this.page.exposeFunction("__chronoParseDate", chrono.parseDate)
|
2020-08-18 09:47:06 -04:00
|
|
|
|
2021-02-10 02:34:19 -05:00
|
|
|
// NOTE Must *always* re-login on a browser session, so no need to check if already logged in
|
|
|
|
this.loginRunning = false
|
|
|
|
this.loginCancelled = false
|
2020-08-18 09:47:06 -04:00
|
|
|
this.taskQueue.start()
|
|
|
|
this.log("Startup complete")
|
|
|
|
}
|
|
|
|
|
2021-02-10 02:34:19 -05:00
|
|
|
async _preparePage(navigateTo) {
|
2021-06-11 02:53:30 -04:00
|
|
|
await this.page.bringToFront()
|
2021-02-10 02:34:19 -05:00
|
|
|
if (navigateTo) {
|
|
|
|
await this.page.goto(MessagesPuppeteer.url)
|
|
|
|
} else {
|
|
|
|
await this.page.reload()
|
|
|
|
}
|
|
|
|
this.log("Injecting content script")
|
|
|
|
await this.page.addScriptTag({ path: "./src/contentscript.js", type: "module" })
|
|
|
|
}
|
|
|
|
|
2021-06-11 02:53:30 -04:00
|
|
|
async _interactWithPage(promiser) {
|
|
|
|
await this.page.bringToFront()
|
|
|
|
try {
|
|
|
|
await promiser()
|
|
|
|
} catch (e) {
|
|
|
|
this.error(`Error while interacting with page: ${e}`)
|
|
|
|
throw e
|
|
|
|
} finally {
|
|
|
|
await this.blankPage.bringToFront()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-06-14 01:47:20 -04:00
|
|
|
/**
|
|
|
|
* Set the contents of a text input field to the given text.
|
|
|
|
* Works by triple-clicking the input field to select all existing text, to replace it on type.
|
|
|
|
*
|
|
|
|
* @param {ElementHandle} inputElement - The input element to type into.
|
|
|
|
* @param {string} text - The text to input.
|
|
|
|
*/
|
|
|
|
async _enterText(inputElement, text) {
|
|
|
|
await inputElement.click({clickCount: 3})
|
|
|
|
await inputElement.type(text)
|
|
|
|
}
|
|
|
|
|
2020-08-18 09:47:06 -04:00
|
|
|
/**
|
2021-02-10 02:34:19 -05:00
|
|
|
* Wait for the session to be logged in and monitor changes while it's not.
|
2020-08-18 09:47:06 -04:00
|
|
|
*/
|
2021-02-10 02:34:19 -05:00
|
|
|
async waitForLogin(login_type, login_data) {
|
2020-08-18 09:47:06 -04:00
|
|
|
if (await this.isLoggedIn()) {
|
|
|
|
return
|
|
|
|
}
|
2021-02-10 02:34:19 -05:00
|
|
|
this.loginRunning = true
|
2021-06-11 02:53:30 -04:00
|
|
|
await this.page.bringToFront()
|
2021-02-10 02:34:19 -05:00
|
|
|
|
|
|
|
const loginContentArea = await this.page.waitForSelector("#login_content")
|
|
|
|
|
|
|
|
switch (login_type) {
|
|
|
|
case "qr": {
|
|
|
|
this.log("Running QR login")
|
|
|
|
const qrButton = await this.page.waitForSelector("#login_qr_btn")
|
|
|
|
await qrButton.click()
|
|
|
|
|
|
|
|
const qrElement = await this.page.waitForSelector("#login_qrcode_area div[title]", {visible: true})
|
|
|
|
const currentQR = await this.page.evaluate(element => element.title, qrElement)
|
|
|
|
this._receiveQRChange(currentQR)
|
|
|
|
|
|
|
|
await this.page.evaluate(
|
|
|
|
element => window.__mautrixController.addQRChangeObserver(element), qrElement)
|
|
|
|
await this.page.evaluate(
|
|
|
|
element => window.__mautrixController.addQRAppearObserver(element), loginContentArea)
|
|
|
|
|
|
|
|
break
|
2020-12-29 09:33:33 -05:00
|
|
|
}
|
2021-02-10 02:34:19 -05:00
|
|
|
case "email": {
|
|
|
|
this.log("Running email login")
|
|
|
|
if (!login_data) {
|
2021-02-10 03:24:28 -05:00
|
|
|
this._sendLoginFailure("No login credentials provided for email login")
|
2021-02-10 02:34:19 -05:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
const emailButton = await this.page.waitForSelector("#login_email_btn")
|
|
|
|
await emailButton.click()
|
|
|
|
|
2021-02-11 00:04:25 -05:00
|
|
|
await this.page.waitForSelector("#login_email_area", {visible: true})
|
2021-02-10 02:34:19 -05:00
|
|
|
this.login_email = login_data["email"]
|
|
|
|
this.login_password = login_data["password"]
|
2021-02-11 00:04:25 -05:00
|
|
|
await this._sendEmailCredentials()
|
2021-02-10 02:34:19 -05:00
|
|
|
|
|
|
|
await this.page.evaluate(
|
|
|
|
element => window.__mautrixController.addEmailAppearObserver(element), loginContentArea)
|
|
|
|
|
|
|
|
break
|
|
|
|
}
|
|
|
|
default:
|
2021-02-10 03:24:28 -05:00
|
|
|
this._sendLoginFailure(`Invalid login type: ${login_type}`)
|
2021-02-10 02:34:19 -05:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
await this.page.evaluate(
|
|
|
|
element => window.__mautrixController.addPINAppearObserver(element), loginContentArea)
|
|
|
|
|
|
|
|
this.log("Waiting for login response")
|
|
|
|
let doneWaiting = false
|
|
|
|
let loginSuccess = false
|
2021-02-10 03:24:28 -05:00
|
|
|
const cancelableResolve = (promiseFn) => {
|
2021-02-10 02:34:19 -05:00
|
|
|
const executor = (resolve, reject) => {
|
2021-02-10 03:24:28 -05:00
|
|
|
promiseFn().then(
|
2021-02-10 02:34:19 -05:00
|
|
|
value => {
|
|
|
|
doneWaiting = true
|
|
|
|
resolve(value)
|
|
|
|
},
|
|
|
|
reason => {
|
|
|
|
if (!doneWaiting) {
|
2021-02-10 03:24:28 -05:00
|
|
|
setTimeout(executor, 1000, resolve, reject)
|
2021-02-10 02:34:19 -05:00
|
|
|
} else {
|
|
|
|
resolve()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
)
|
|
|
|
}
|
|
|
|
return new Promise(executor)
|
|
|
|
}
|
|
|
|
|
|
|
|
const result = await Promise.race([
|
2021-06-08 00:24:00 -04:00
|
|
|
() => this.page.waitForSelector("#mainApp:not(.MdNonDisp)", {timeout: 2000})
|
2021-02-11 00:04:25 -05:00
|
|
|
.then(value => {
|
2021-02-10 02:34:19 -05:00
|
|
|
loginSuccess = true
|
2021-02-11 00:04:25 -05:00
|
|
|
return value
|
2021-02-10 02:34:19 -05:00
|
|
|
}),
|
2021-02-10 03:24:28 -05:00
|
|
|
() => this.page.waitForSelector("#login_incorrect", {visible: true, timeout: 2000})
|
2021-03-26 02:48:11 -04:00
|
|
|
.then(value => this.page.evaluate(element => element?.innerText, value)),
|
2021-02-10 03:24:28 -05:00
|
|
|
() => this._waitForLoginCancel(),
|
|
|
|
].map(promiseFn => cancelableResolve(promiseFn)))
|
2021-02-10 02:34:19 -05:00
|
|
|
|
|
|
|
this.log("Removing observers")
|
2021-03-26 02:48:11 -04:00
|
|
|
// TODO __mautrixController is undefined when cancelling, why?
|
2021-06-15 02:48:29 -04:00
|
|
|
await this.page.evaluate(ownID => window.__mautrixController.setOwnID(ownID), this.ownID)
|
2021-02-10 02:34:19 -05:00
|
|
|
await this.page.evaluate(() => window.__mautrixController.removeQRChangeObserver())
|
2021-02-10 03:24:28 -05:00
|
|
|
await this.page.evaluate(() => window.__mautrixController.removeQRAppearObserver())
|
|
|
|
await this.page.evaluate(() => window.__mautrixController.removeEmailAppearObserver())
|
|
|
|
await this.page.evaluate(() => window.__mautrixController.removePINAppearObserver())
|
2021-02-10 02:34:19 -05:00
|
|
|
delete this.login_email
|
|
|
|
delete this.login_password
|
|
|
|
|
2021-06-08 00:24:00 -04:00
|
|
|
const messageSyncElement = loginSuccess ? await this.page.waitForSelector("#wrap_message_sync") : null
|
|
|
|
if (!loginSuccess || !messageSyncElement) {
|
2021-02-10 03:24:28 -05:00
|
|
|
this._sendLoginFailure(result)
|
2021-02-10 02:34:19 -05:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2021-06-08 00:24:00 -04:00
|
|
|
this._sendLoginSuccess()
|
2021-02-10 02:34:19 -05:00
|
|
|
this.log("Waiting for sync")
|
2021-02-12 02:16:09 -05:00
|
|
|
try {
|
|
|
|
await this.page.waitForFunction(
|
|
|
|
messageSyncElement => {
|
|
|
|
const text = messageSyncElement.innerText
|
2021-02-16 02:49:54 -05:00
|
|
|
return text.startsWith("Syncing messages...")
|
|
|
|
&& (text.endsWith("100%") || text.endsWith("NaN%"))
|
2021-02-18 01:03:33 -05:00
|
|
|
// TODO Sometimes it gets stuck at 99%...??
|
2021-02-12 02:16:09 -05:00
|
|
|
},
|
2021-02-18 01:03:33 -05:00
|
|
|
{timeout: 10000}, // Assume 10 seconds is long enough
|
2021-06-08 00:24:00 -04:00
|
|
|
messageSyncElement)
|
2021-02-12 02:16:09 -05:00
|
|
|
} catch (err) {
|
2021-02-18 01:03:33 -05:00
|
|
|
//this._sendLoginFailure(`Failed to sync: ${err}`)
|
|
|
|
this.log("LINE's sync took too long, assume it's fine and carry on...")
|
2021-05-30 19:10:52 -04:00
|
|
|
} finally {
|
2021-06-08 00:24:00 -04:00
|
|
|
const syncText = await messageSyncElement.evaluate(e => e.innerText)
|
2021-05-30 19:10:52 -04:00
|
|
|
this.log(`Final sync text is: "${syncText}"`)
|
2021-02-12 02:16:09 -05:00
|
|
|
}
|
2021-02-18 01:03:33 -05:00
|
|
|
|
|
|
|
this.loginRunning = false
|
2021-06-11 02:53:30 -04:00
|
|
|
await this.blankPage.bringToFront()
|
2021-02-18 01:03:33 -05:00
|
|
|
this.log("Login complete")
|
2020-08-18 09:47:06 -04:00
|
|
|
}
|
|
|
|
|
2021-02-10 02:34:19 -05:00
|
|
|
/**
|
|
|
|
* Cancel an ongoing login attempt.
|
|
|
|
*/
|
|
|
|
async cancelLogin() {
|
|
|
|
if (this.loginRunning) {
|
2021-02-12 02:16:09 -05:00
|
|
|
this.loginRunning = false
|
2021-02-10 02:34:19 -05:00
|
|
|
this.loginCancelled = true
|
2021-02-10 03:24:28 -05:00
|
|
|
await this._preparePage(false)
|
2021-02-10 02:34:19 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
_waitForLoginCancel() {
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
if (this.loginCancelled) {
|
2021-02-12 02:16:09 -05:00
|
|
|
this.loginCancelled = false
|
2021-02-10 02:34:19 -05:00
|
|
|
resolve()
|
|
|
|
} else {
|
|
|
|
reject()
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2020-08-18 09:47:06 -04:00
|
|
|
/**
|
|
|
|
* Close the browser.
|
|
|
|
*/
|
|
|
|
async stop() {
|
|
|
|
this.taskQueue.stop()
|
2020-08-24 11:53:56 -04:00
|
|
|
if (this.page) {
|
|
|
|
await this.page.close()
|
|
|
|
}
|
|
|
|
if (this.browser) {
|
|
|
|
await this.browser.close()
|
|
|
|
}
|
2020-08-18 09:47:06 -04:00
|
|
|
this.log("Everything stopped")
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Check if the session is currently logged in.
|
|
|
|
*
|
|
|
|
* @return {Promise<boolean>} - Whether or not the session is logged in.
|
|
|
|
*/
|
|
|
|
async isLoggedIn() {
|
2021-05-30 17:41:28 -04:00
|
|
|
const selectors = [
|
|
|
|
"#mainApp:not(.MdNonDisp)",
|
|
|
|
"#wrap_message_sync",
|
|
|
|
"#_chat_list_body",
|
|
|
|
]
|
|
|
|
for (const selector of selectors) {
|
|
|
|
if (await this.page.$(selector) == null) {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return true
|
2020-08-18 09:47:06 -04:00
|
|
|
}
|
|
|
|
|
2020-08-24 16:29:09 -04:00
|
|
|
async isPermanentlyDisconnected() {
|
2021-02-10 02:34:19 -05:00
|
|
|
// TODO
|
|
|
|
//return await this.page.$("mw-unable-to-connect-container") !== null
|
|
|
|
return false
|
2020-08-24 16:29:09 -04:00
|
|
|
}
|
|
|
|
|
2020-11-03 10:57:51 -05:00
|
|
|
async isOpenSomewhereElse() {
|
2021-02-10 02:34:19 -05:00
|
|
|
/* TODO
|
2020-11-03 10:57:51 -05:00
|
|
|
try {
|
|
|
|
const text = await this.page.$eval("mws-dialog mat-dialog-content div",
|
|
|
|
elem => elem.textContent)
|
|
|
|
return text?.trim() === "Messages for web is open in more than one tab or browser"
|
|
|
|
} catch (err) {
|
|
|
|
return false
|
|
|
|
}
|
2021-02-10 02:34:19 -05:00
|
|
|
*/
|
|
|
|
return false
|
2020-11-03 10:57:51 -05:00
|
|
|
}
|
|
|
|
|
2020-08-24 16:29:09 -04:00
|
|
|
async isDisconnected() {
|
2020-11-09 17:07:01 -05:00
|
|
|
if (!await this.isLoggedIn()) {
|
|
|
|
return true
|
|
|
|
}
|
2021-02-10 02:34:19 -05:00
|
|
|
/* TODO
|
2020-11-03 10:57:51 -05:00
|
|
|
const offlineIndicators = await Promise.all([
|
|
|
|
this.page.$("mw-main-nav mw-banner mw-error-banner"),
|
|
|
|
this.page.$("mw-main-nav mw-banner mw-information-banner[title='Connecting']"),
|
|
|
|
this.page.$("mw-unable-to-connect-container"),
|
|
|
|
this.isOpenSomewhereElse(),
|
|
|
|
])
|
|
|
|
return offlineIndicators.some(indicator => Boolean(indicator))
|
2021-02-10 02:34:19 -05:00
|
|
|
*/
|
|
|
|
return false
|
2020-08-24 16:29:09 -04:00
|
|
|
}
|
|
|
|
|
2020-08-18 09:47:06 -04:00
|
|
|
/**
|
|
|
|
* Get the IDs of the most recent chats.
|
|
|
|
*
|
|
|
|
* @return {Promise<[ChatListInfo]>} - List of chat IDs in order of most recent message.
|
|
|
|
*/
|
|
|
|
async getRecentChats() {
|
2021-04-27 02:59:16 -04:00
|
|
|
return await this.taskQueue.push(() =>
|
|
|
|
this.page.evaluate(() => window.__mautrixController.parseChatList()))
|
2020-08-18 09:47:06 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @typedef ChatInfo
|
|
|
|
* @type object
|
|
|
|
* @property {[Participant]} participants
|
|
|
|
*/
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get info about a chat.
|
|
|
|
*
|
2021-04-22 02:39:52 -04:00
|
|
|
* @param {string} chatID - The chat ID whose info to get.
|
2021-06-11 02:53:30 -04:00
|
|
|
* @param {boolean} forceView - Whether the LINE tab should always be viewed, even if the chat is already active.
|
2020-08-18 09:47:06 -04:00
|
|
|
* @return {Promise<ChatInfo>} - Info about the chat.
|
|
|
|
*/
|
2021-06-11 02:53:30 -04:00
|
|
|
async getChatInfo(chatID, forceView) {
|
|
|
|
return await this.taskQueue.push(() => this._getChatInfoUnsafe(chatID, forceView))
|
2020-08-18 09:47:06 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Send a message to a chat.
|
|
|
|
*
|
2021-04-22 02:39:52 -04:00
|
|
|
* @param {string} chatID - The ID of the chat to send a message to.
|
2020-08-18 09:47:06 -04:00
|
|
|
* @param {string} text - The text to send.
|
2020-08-28 12:34:13 -04:00
|
|
|
* @return {Promise<{id: number}>} - The ID of the sent message.
|
2020-08-18 09:47:06 -04:00
|
|
|
*/
|
|
|
|
async sendMessage(chatID, text) {
|
2020-08-28 12:34:13 -04:00
|
|
|
return { id: await this.taskQueue.push(() => this._sendMessageUnsafe(chatID, text)) }
|
2020-08-18 09:47:06 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get messages in a chat.
|
|
|
|
*
|
2021-04-22 02:39:52 -04:00
|
|
|
* @param {string} chatID The ID of the chat whose messages to get.
|
2021-06-30 03:04:25 -04:00
|
|
|
* @return {Promise<ChatEvents>} - New messages and receipts synced fron the chat.
|
2020-08-18 09:47:06 -04:00
|
|
|
*/
|
2021-04-22 02:39:52 -04:00
|
|
|
async getMessages(chatID) {
|
2021-06-17 23:59:25 -04:00
|
|
|
return await this.taskQueue.push(() => this._getMessagesUnsafe(chatID))
|
2020-08-24 16:29:09 -04:00
|
|
|
}
|
|
|
|
|
2021-06-30 03:04:25 -04:00
|
|
|
setLastMessageIDs(msgIDs, ownMsgIDs, rctIDs) {
|
2021-04-22 02:39:52 -04:00
|
|
|
this.mostRecentMessages.clear()
|
2021-06-30 03:04:25 -04:00
|
|
|
for (const [chatID, messageID] of Object.entries(msgIDs)) {
|
2021-04-22 02:39:52 -04:00
|
|
|
this.mostRecentMessages.set(chatID, messageID)
|
2020-08-24 16:29:09 -04:00
|
|
|
}
|
2021-06-06 18:15:38 -04:00
|
|
|
this.log("Updated most recent message ID map:")
|
2021-06-30 03:04:25 -04:00
|
|
|
this.log(JSON.stringify(msgIDs))
|
|
|
|
|
|
|
|
for (const [chatID, messageID] of Object.entries(ownMsgIDs)) {
|
|
|
|
this.mostRecentOwnMessages.set(chatID, messageID)
|
|
|
|
}
|
|
|
|
this.log("Updated most recent own message ID map:")
|
|
|
|
this.log(JSON.stringify(ownMsgIDs))
|
|
|
|
|
|
|
|
this.mostRecentReceipts.clear()
|
|
|
|
for (const [chatID, receipts] of Object.entries(rctIDs)) {
|
|
|
|
const receiptMap = this._getReceiptMap(chatID)
|
|
|
|
for (const [count, receiptID] of Object.entries(receipts)) {
|
|
|
|
receiptMap.set(+count, receiptID)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
this.log("Updated most recent receipt ID map")
|
|
|
|
for (const [chatID, receiptMap] of this.mostRecentReceipts) {
|
|
|
|
this.log(`${chatID}:`)
|
|
|
|
for (const [count, receiptID] of receiptMap) {
|
|
|
|
this.log(`Read by ${count}: ${receiptID}`)
|
|
|
|
}
|
|
|
|
}
|
2020-08-18 09:47:06 -04:00
|
|
|
}
|
|
|
|
|
2021-03-26 02:27:21 -04:00
|
|
|
async readImage(imageUrl) {
|
|
|
|
return await this.taskQueue.push(() =>
|
|
|
|
this.page.evaluate(
|
|
|
|
url => window.__mautrixController.readImage(url),
|
|
|
|
imageUrl))
|
|
|
|
}
|
|
|
|
|
2021-03-27 03:37:41 -04:00
|
|
|
async sendFile(chatID, filePath) {
|
|
|
|
return { id: await this.taskQueue.push(() => this._sendFileUnsafe(chatID, filePath)) }
|
|
|
|
}
|
|
|
|
|
2021-06-30 03:04:25 -04:00
|
|
|
_cycleTimerStart() {
|
|
|
|
// TODO Config for cycle delay
|
|
|
|
this.cycleTimerID = setTimeout(
|
|
|
|
() => this.taskQueue.push(() => this._cycleChatUnsafe()),
|
|
|
|
5000)
|
|
|
|
}
|
|
|
|
|
|
|
|
async _cycleChatUnsafe() {
|
|
|
|
const currentChatID = await this.page.evaluate(() => window.__mautrixController.getCurrentChatID())
|
|
|
|
const chatList = await this.page.evaluate(() => window.__mautrixController.parseChatListForCycle())
|
|
|
|
// Add 1 to start at the chat after the currently-viewed one
|
|
|
|
const offset = 1 + Math.max(chatList.findIndex(item => item.id == currentChatID), 0)
|
|
|
|
|
|
|
|
// Visit next chat for which:
|
|
|
|
// - there are no unread notifications
|
|
|
|
// - the most recently-sent own message is not fully read
|
|
|
|
let chatIDToSync
|
|
|
|
for (let i = 0, n = chatList.length; i < n; i++) {
|
|
|
|
const chatListItem = chatList[(i+offset) % n]
|
|
|
|
|
|
|
|
if (chatListItem.notificationCount > 0) {
|
|
|
|
// Chat has unread notifications, so don't view it
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
if (chatListItem.otherParticipantCount == 0) {
|
|
|
|
// Chat has no other participants (must be a non-DM with only you), so nothing to sync
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
const mostRecentOwnMsgID = this.mostRecentOwnMessages.get(chatListItem.id)
|
|
|
|
if (mostRecentOwnMsgID == undefined) {
|
|
|
|
// Chat doesn't have any own messages, so no need to view it
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
const receiptMap = this._getReceiptMap(chatListItem.id)
|
|
|
|
const mostRecentFullyReadMsgID = receiptMap.get(chatListItem.otherParticipantCount)
|
|
|
|
if (mostRecentFullyReadMsgID == mostRecentOwnMsgID) {
|
|
|
|
// Latest own message is fully-read, nothing to see here, move along
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
chatIDToSync = chatListItem.id
|
|
|
|
break
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!chatIDToSync) {
|
|
|
|
// TODO Confirm if this actually works...!
|
|
|
|
this.log(`Found no chats in need of read receipt updates, so force-viewing ${currentChatID} just to keep LINE alive`)
|
|
|
|
await this._switchChat(currentChatID, true)
|
|
|
|
} else {
|
|
|
|
this.log(`Viewing chat ${chatIDToSync} to check for new read receipts`)
|
|
|
|
await this._syncChat(chatIDToSync)
|
|
|
|
}
|
|
|
|
|
|
|
|
this._cycleTimerStart()
|
|
|
|
}
|
|
|
|
|
2020-08-18 09:47:06 -04:00
|
|
|
async startObserving() {
|
2021-06-30 03:04:25 -04:00
|
|
|
// TODO Highly consider syncing anything that was missed since stopObserving...
|
2021-06-06 18:15:38 -04:00
|
|
|
const chatID = await this.page.evaluate(() => window.__mautrixController.getCurrentChatID())
|
2021-06-30 03:04:25 -04:00
|
|
|
this.log(`Adding observers for ${chatID || "empty chat"}, and global timers`)
|
2021-02-25 23:59:25 -05:00
|
|
|
await this.page.evaluate(
|
|
|
|
() => window.__mautrixController.addChatListObserver())
|
2021-06-06 18:15:38 -04:00
|
|
|
if (chatID) {
|
|
|
|
await this.page.evaluate(
|
|
|
|
(mostRecentMessage) => window.__mautrixController.addMsgListObserver(mostRecentMessage),
|
|
|
|
this.mostRecentMessages.get(chatID))
|
|
|
|
}
|
2021-06-30 03:04:25 -04:00
|
|
|
|
|
|
|
if (this.cycleTimerID == null) {
|
|
|
|
this._cycleTimerStart()
|
|
|
|
}
|
2020-08-18 09:47:06 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
async stopObserving() {
|
2021-06-30 03:04:25 -04:00
|
|
|
this.log("Removing observers and timers")
|
2021-04-20 20:01:50 -04:00
|
|
|
await this.page.evaluate(
|
|
|
|
() => window.__mautrixController.removeChatListObserver())
|
|
|
|
await this.page.evaluate(
|
|
|
|
() => window.__mautrixController.removeMsgListObserver())
|
2021-06-30 03:04:25 -04:00
|
|
|
|
|
|
|
if (this.cycleTimerID != null) {
|
|
|
|
clearTimeout(this.cycleTimerID)
|
|
|
|
this.cycleTimerID = null
|
|
|
|
}
|
2020-08-18 09:47:06 -04:00
|
|
|
}
|
|
|
|
|
2021-06-15 02:48:29 -04:00
|
|
|
async getOwnProfile() {
|
|
|
|
return await this.taskQueue.push(() => this._getOwnProfileUnsafe())
|
|
|
|
}
|
|
|
|
|
|
|
|
async _getOwnProfileUnsafe() {
|
|
|
|
// NOTE Will send a read receipt if a chat was in view!
|
|
|
|
// Best to use this on startup when no chat is viewed.
|
|
|
|
let ownProfile
|
2021-06-16 22:14:26 -04:00
|
|
|
await this._interactWithPage(async () => {
|
|
|
|
this.log("Opening settings view")
|
|
|
|
await this.page.click("button.mdGHD01SettingBtn")
|
|
|
|
await this.page.waitForSelector("#context_menu li#settings", {visible: true}).then(e => e.click())
|
|
|
|
await this.page.waitForSelector("#settings_contents", {visible: true})
|
|
|
|
|
|
|
|
this.log("Getting own profile info")
|
|
|
|
ownProfile = {
|
|
|
|
id: this.ownID,
|
|
|
|
name: await this.page.$eval("#settings_basic_name_input", e => e.innerText),
|
|
|
|
avatar: {
|
|
|
|
path: null,
|
|
|
|
url: await this.page.$eval(".mdCMN09ImgInput", e => {
|
|
|
|
const imgStr = e.style?.getPropertyValue("background-image")
|
|
|
|
const matches = imgStr.match(/url\("(blob:.*)"\)/)
|
|
|
|
return matches?.length == 2 ? matches[1] : null
|
|
|
|
}),
|
|
|
|
},
|
|
|
|
}
|
2021-06-15 02:48:29 -04:00
|
|
|
|
2021-06-16 22:14:26 -04:00
|
|
|
const backSelector = "#label_setting button"
|
|
|
|
await this.page.click(backSelector)
|
|
|
|
await this.page.waitForSelector(backSelector, {visible: false})
|
|
|
|
})
|
2021-06-15 02:48:29 -04:00
|
|
|
return ownProfile
|
|
|
|
}
|
|
|
|
|
2020-08-18 09:47:06 -04:00
|
|
|
_listItemSelector(id) {
|
2021-02-16 02:49:54 -05:00
|
|
|
return `#_chat_list_body div[data-chatid="${id}"]`
|
2020-08-18 09:47:06 -04:00
|
|
|
}
|
|
|
|
|
2021-06-11 02:53:30 -04:00
|
|
|
async _switchChat(chatID, forceView = false) {
|
2021-02-25 22:21:11 -05:00
|
|
|
// TODO Allow passing in an element directly
|
2021-04-22 02:39:52 -04:00
|
|
|
this.log(`Switching to chat ${chatID}`)
|
|
|
|
const chatListItem = await this.page.$(this._listItemSelector(chatID))
|
2021-02-21 02:07:48 -05:00
|
|
|
|
2021-02-25 22:21:11 -05:00
|
|
|
const chatName = await chatListItem.evaluate(
|
|
|
|
element => window.__mautrixController.getChatListItemName(element))
|
|
|
|
|
|
|
|
const isCorrectChatVisible = (targetText) => {
|
|
|
|
const chatHeader = document.querySelector("#_chat_header_area > .mdRGT04Link")
|
|
|
|
if (!chatHeader) return false
|
|
|
|
const chatHeaderTitleElement = chatHeader.querySelector(".mdRGT04Ttl")
|
|
|
|
return chatHeaderTitleElement.innerText == targetText
|
|
|
|
}
|
|
|
|
|
|
|
|
if (await this.page.evaluate(isCorrectChatVisible, chatName)) {
|
2021-06-11 02:53:30 -04:00
|
|
|
if (!forceView) {
|
|
|
|
this.log("Already viewing chat, no need to switch")
|
|
|
|
} else {
|
|
|
|
await this._interactWithPage(async () => {
|
|
|
|
this.log("Already viewing chat, but got request to view it")
|
|
|
|
this.page.waitForTimeout(500)
|
|
|
|
})
|
|
|
|
}
|
2021-02-25 22:21:11 -05:00
|
|
|
} else {
|
2021-06-06 18:15:38 -04:00
|
|
|
this.log("Ensuring msg list observer is removed")
|
2021-04-23 03:38:13 -04:00
|
|
|
const hadMsgListObserver = await this.page.evaluate(
|
|
|
|
() => window.__mautrixController.removeMsgListObserver())
|
2021-06-06 18:15:38 -04:00
|
|
|
this.log(hadMsgListObserver ? "Observer was already removed" : "Removed observer")
|
2021-04-23 03:38:13 -04:00
|
|
|
|
2021-06-11 02:53:30 -04:00
|
|
|
await this._interactWithPage(async () => {
|
2021-06-17 23:55:19 -04:00
|
|
|
let numTries = 3
|
|
|
|
while (true) {
|
|
|
|
try {
|
|
|
|
this.log("Clicking chat list item")
|
|
|
|
chatListItem.click()
|
|
|
|
this.log(`Waiting for chat header title to be "${chatName}"`)
|
|
|
|
await this.page.waitForFunction(
|
|
|
|
isCorrectChatVisible,
|
|
|
|
{polling: "mutation", timeout: 1000},
|
|
|
|
chatName)
|
|
|
|
break
|
|
|
|
} catch (e) {
|
|
|
|
if (--numTries == 0) {
|
|
|
|
throw e
|
|
|
|
} else {
|
|
|
|
this.log("Clicking chat list item didn't work...try again")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2021-06-11 02:53:30 -04:00
|
|
|
|
|
|
|
// Always show the chat details sidebar, as this makes life easier
|
|
|
|
this.log("Waiting for detail area to be auto-hidden upon entering chat")
|
|
|
|
await this.page.waitForFunction(
|
|
|
|
detailArea => detailArea.childElementCount == 0,
|
|
|
|
{},
|
|
|
|
await this.page.$("#_chat_detail_area"))
|
|
|
|
|
|
|
|
this.log("Clicking chat header to show detail area")
|
|
|
|
await this.page.click("#_chat_header_area > .mdRGT04Link")
|
|
|
|
this.log("Waiting for detail area")
|
|
|
|
await this.page.waitForSelector("#_chat_detail_area > .mdRGT02Info")
|
|
|
|
})
|
2021-04-20 20:01:50 -04:00
|
|
|
|
2021-06-17 23:55:19 -04:00
|
|
|
this.log("Waiting for any item to appear in chat")
|
|
|
|
try {
|
|
|
|
await this.page.waitForSelector("#_chat_room_msg_list div", {timeout: 2000})
|
|
|
|
|
|
|
|
this.log("Waiting for chat to stabilize")
|
|
|
|
await this.page.evaluate(() => window.__mautrixController.waitForMessageListStability())
|
|
|
|
} catch (e) {
|
|
|
|
this.log("No messages in chat found. Maybe no messages were ever sent yet?")
|
|
|
|
}
|
2021-06-16 02:27:27 -04:00
|
|
|
|
2021-04-23 03:38:13 -04:00
|
|
|
if (hadMsgListObserver) {
|
|
|
|
this.log("Restoring msg list observer")
|
|
|
|
await this.page.evaluate(
|
2021-06-06 18:15:38 -04:00
|
|
|
(mostRecentMessage) => window.__mautrixController.addMsgListObserver(mostRecentMessage),
|
|
|
|
this.mostRecentMessages.get(chatID))
|
2021-04-23 03:38:13 -04:00
|
|
|
} else {
|
|
|
|
this.log("Not restoring msg list observer, as there never was one")
|
|
|
|
}
|
2021-02-25 22:21:11 -05:00
|
|
|
}
|
|
|
|
}
|
2021-02-21 02:07:48 -05:00
|
|
|
|
2021-06-11 02:53:30 -04:00
|
|
|
async _getChatInfoUnsafe(chatID, forceView) {
|
2021-06-10 02:52:13 -04:00
|
|
|
const chatListInfo = await this.page.$eval(this._listItemSelector(chatID),
|
2021-04-22 02:39:52 -04:00
|
|
|
(element, chatID) => window.__mautrixController.parseChatListItem(element, chatID),
|
|
|
|
chatID)
|
2021-02-25 22:21:11 -05:00
|
|
|
|
2021-02-21 02:07:48 -05:00
|
|
|
let [isDirect, isGroup, isRoom] = [false,false,false]
|
2021-04-22 02:39:52 -04:00
|
|
|
switch (chatID.charAt(0)) {
|
2021-02-25 22:21:11 -05:00
|
|
|
case "u":
|
2021-02-21 02:07:48 -05:00
|
|
|
isDirect = true
|
|
|
|
break
|
2021-02-25 22:21:11 -05:00
|
|
|
case "c":
|
2021-02-21 02:07:48 -05:00
|
|
|
isGroup = true
|
|
|
|
break
|
2021-02-25 22:21:11 -05:00
|
|
|
case "r":
|
2021-02-21 02:07:48 -05:00
|
|
|
isRoom = true
|
|
|
|
break
|
|
|
|
}
|
|
|
|
|
2021-02-16 02:49:54 -05:00
|
|
|
let participants
|
2021-02-25 22:21:11 -05:00
|
|
|
if (!isDirect) {
|
2021-06-10 02:52:13 -04:00
|
|
|
this.log("Found multi-user chat, so viewing it to get participants")
|
2021-02-25 22:21:11 -05:00
|
|
|
// TODO This will mark the chat as "read"!
|
2021-06-11 02:53:30 -04:00
|
|
|
await this._switchChat(chatID, forceView)
|
2021-06-06 18:15:38 -04:00
|
|
|
const participantList = await this.page.$("#_chat_detail_area > .mdRGT02Info ul.mdRGT13Ul")
|
2021-02-25 22:21:11 -05:00
|
|
|
// TODO Is a group not actually created until a message is sent(?)
|
2021-06-10 02:52:13 -04:00
|
|
|
// If so, maybe don't create a portal until there is a message.
|
2021-02-25 22:21:11 -05:00
|
|
|
participants = await participantList.evaluate(
|
|
|
|
element => window.__mautrixController.parseParticipantList(element))
|
|
|
|
} else {
|
2021-04-22 02:39:52 -04:00
|
|
|
this.log(`Found direct chat with ${chatID}`)
|
2021-06-11 02:53:30 -04:00
|
|
|
if (forceView) {
|
|
|
|
this.log("Viewing chat on request")
|
|
|
|
await this._switchChat(chatID, forceView)
|
|
|
|
}
|
2021-02-21 02:07:48 -05:00
|
|
|
//const chatDetailArea = await this.page.waitForSelector("#_chat_detail_area > .mdRGT02Info")
|
2021-02-16 02:49:54 -05:00
|
|
|
//await chatDetailArea.$(".MdTxtDesc02") || // 1:1 chat with custom title - get participant's real name
|
|
|
|
participants = [{
|
2021-04-22 02:39:52 -04:00
|
|
|
id: chatID,
|
2021-03-26 02:27:21 -04:00
|
|
|
avatar: chatListInfo.icon,
|
2021-02-21 02:07:48 -05:00
|
|
|
name: chatListInfo.name,
|
2021-02-16 02:49:54 -05:00
|
|
|
}]
|
2020-08-18 09:47:06 -04:00
|
|
|
}
|
2021-02-16 02:49:54 -05:00
|
|
|
|
2021-02-25 22:21:11 -05:00
|
|
|
this.log("Found participants:")
|
|
|
|
for (const participant of participants) {
|
2021-06-10 02:52:25 -04:00
|
|
|
this.log(JSON.stringify(participant))
|
2021-02-25 22:21:11 -05:00
|
|
|
}
|
2021-02-21 02:07:48 -05:00
|
|
|
return {participants, ...chatListInfo}
|
2020-08-18 09:47:06 -04:00
|
|
|
}
|
|
|
|
|
2021-02-20 03:47:26 -05:00
|
|
|
// TODO Catch "An error has occurred" dialog
|
2021-06-10 02:52:13 -04:00
|
|
|
// Selector is just "dialog", then "button"
|
|
|
|
// Child of "#layer_contents"
|
|
|
|
// Always present, just made visible via classes
|
2021-02-20 03:47:26 -05:00
|
|
|
|
2020-08-18 09:47:06 -04:00
|
|
|
async _sendMessageUnsafe(chatID, text) {
|
2021-06-10 02:10:18 -04:00
|
|
|
// Sync all messages in this chat first
|
2021-06-30 03:04:25 -04:00
|
|
|
await this._syncChat(chatID)
|
2021-06-06 18:15:38 -04:00
|
|
|
// TODO Initiate the promise in the content script
|
2021-04-27 02:59:16 -04:00
|
|
|
await this.page.evaluate(
|
2021-03-29 01:25:05 -04:00
|
|
|
() => window.__mautrixController.promiseOwnMessage(5000, "time"))
|
2021-02-20 03:47:26 -05:00
|
|
|
|
|
|
|
const input = await this.page.$("#_chat_room_input")
|
2021-06-11 02:53:30 -04:00
|
|
|
await this._interactWithPage(async () => {
|
2021-06-16 22:14:26 -04:00
|
|
|
// Live-typing in the field can have its text mismatch what was requested!!
|
|
|
|
// Probably because the input element is a div instead of a real text input...ugh!
|
|
|
|
// Setting its innerText directly works fine though...
|
2021-06-11 02:53:30 -04:00
|
|
|
await input.click()
|
2021-06-16 22:14:26 -04:00
|
|
|
await input.evaluate((e, text) => e.innerText = text, text)
|
2021-06-11 02:53:30 -04:00
|
|
|
await input.press("Enter")
|
|
|
|
})
|
2021-02-20 03:47:26 -05:00
|
|
|
|
2021-04-27 02:59:16 -04:00
|
|
|
return await this._waitForSentMessage(chatID)
|
|
|
|
}
|
|
|
|
|
|
|
|
async _sendFileUnsafe(chatID, filePath) {
|
2021-06-30 03:04:25 -04:00
|
|
|
await this._syncChat(chatID)
|
2021-04-27 02:59:16 -04:00
|
|
|
await this.page.evaluate(
|
|
|
|
() => window.__mautrixController.promiseOwnMessage(
|
|
|
|
10000, // Use longer timeout for file uploads
|
|
|
|
"#_chat_message_success_menu",
|
|
|
|
"#_chat_message_fail_menu"))
|
|
|
|
|
|
|
|
try {
|
2021-06-11 02:53:30 -04:00
|
|
|
this._interactWithPage(async () => {
|
|
|
|
this.log(`About to ask for file chooser in ${chatID}`)
|
|
|
|
const [fileChooser] = await Promise.all([
|
|
|
|
this.page.waitForFileChooser(),
|
|
|
|
this.page.click("#_chat_room_plus_btn")
|
|
|
|
])
|
|
|
|
this.log(`About to upload ${filePath}`)
|
|
|
|
await fileChooser.accept([filePath])
|
|
|
|
})
|
2021-04-27 02:59:16 -04:00
|
|
|
} catch (e) {
|
|
|
|
this.log(`Failed to upload file to ${chatID}`)
|
|
|
|
return -1
|
|
|
|
}
|
|
|
|
|
|
|
|
return await this._waitForSentMessage(chatID)
|
|
|
|
}
|
|
|
|
|
|
|
|
async _waitForSentMessage(chatID) {
|
2021-02-20 03:47:26 -05:00
|
|
|
try {
|
|
|
|
this.log("Waiting for message to be sent")
|
2021-04-27 02:59:16 -04:00
|
|
|
const id = await this.page.evaluate(
|
|
|
|
() => window.__mautrixController.waitForOwnMessage())
|
2021-02-20 03:47:26 -05:00
|
|
|
this.log(`Successfully sent message ${id} to ${chatID}`)
|
2021-06-30 03:04:25 -04:00
|
|
|
this.mostRecentMessages.set(chatID, id)
|
|
|
|
this.mostRecentOwnMessages.set(chatID, id)
|
2021-02-20 03:47:26 -05:00
|
|
|
return id
|
|
|
|
} catch (e) {
|
2021-03-29 01:25:05 -04:00
|
|
|
// TODO Catch if something other than a timeout
|
|
|
|
this.error(`Timed out sending message to ${chatID}`)
|
2021-04-27 02:59:16 -04:00
|
|
|
// TODO Figure out why e is undefined...
|
|
|
|
//this.error(e)
|
2021-02-20 03:47:26 -05:00
|
|
|
return -1
|
|
|
|
}
|
2020-08-18 09:47:06 -04:00
|
|
|
}
|
|
|
|
|
2021-06-10 02:10:18 -04:00
|
|
|
_receiveMessages(chatID, messages, skipProcessing = false) {
|
2021-06-30 03:04:25 -04:00
|
|
|
if (!skipProcessing) {
|
|
|
|
messages = this._processMessages(chatID, messages)
|
|
|
|
}
|
2021-04-23 03:38:13 -04:00
|
|
|
if (this.client) {
|
2021-06-06 18:15:38 -04:00
|
|
|
for (const message of messages) {
|
|
|
|
this.client.sendMessage(message).catch(err =>
|
|
|
|
this.error("Failed to send message", message.id, "to client:", err))
|
2021-04-23 03:38:13 -04:00
|
|
|
}
|
|
|
|
} else {
|
|
|
|
this.log("No client connected, not sending messages")
|
|
|
|
}
|
|
|
|
}
|
2021-02-20 03:47:26 -05:00
|
|
|
|
2021-04-22 02:39:52 -04:00
|
|
|
async _getMessagesUnsafe(chatID) {
|
2021-06-06 18:15:38 -04:00
|
|
|
// TODO Consider making a wrapper for pausing/resuming the msg list observers
|
|
|
|
this.log("Ensuring msg list observer is removed")
|
|
|
|
const hadMsgListObserver = await this.page.evaluate(
|
|
|
|
() => window.__mautrixController.removeMsgListObserver())
|
|
|
|
this.log(hadMsgListObserver ? "Observer was already removed" : "Removed observer")
|
|
|
|
|
2021-02-20 20:00:32 -05:00
|
|
|
// TODO Handle unloaded messages. Maybe scroll up
|
2021-02-25 23:59:25 -05:00
|
|
|
// TODO This will mark the chat as "read"!
|
2021-04-22 02:39:52 -04:00
|
|
|
await this._switchChat(chatID)
|
2021-06-10 02:10:18 -04:00
|
|
|
// TODO Is it better to reset the notification count in _switchChat instead of here?
|
|
|
|
this.numChatNotifications.set(chatID, 0)
|
2021-06-30 03:04:25 -04:00
|
|
|
|
|
|
|
|
2021-06-10 02:10:18 -04:00
|
|
|
let messages = await this.page.evaluate(
|
2021-06-06 18:15:38 -04:00
|
|
|
mostRecentMessage => window.__mautrixController.parseMessageList(mostRecentMessage),
|
|
|
|
this.mostRecentMessages.get(chatID))
|
|
|
|
// Doing this before restoring the observer since it updates minID
|
2021-06-10 02:10:18 -04:00
|
|
|
messages = this._processMessages(chatID, messages)
|
2021-06-06 18:15:38 -04:00
|
|
|
|
2021-06-30 03:04:25 -04:00
|
|
|
|
|
|
|
const receiptMap = this._getReceiptMap(chatID)
|
|
|
|
|
|
|
|
// Sync receipts seen from newly-synced messages
|
|
|
|
// TODO When user leaves, clear the read-by count for the old number of other participants
|
|
|
|
let minCountToFind = 1
|
|
|
|
for (let i = messages.length-1; i >= 0; i--) {
|
|
|
|
const message = messages[i]
|
|
|
|
if (!message.is_outgoing) {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
const count = message.receipt_count
|
|
|
|
if (count >= minCountToFind && message.id > (receiptMap.get(count) || 0)) {
|
|
|
|
minCountToFind = count+1
|
|
|
|
receiptMap.set(count, message.id)
|
|
|
|
}
|
|
|
|
// TODO Early exit when count == num other participants
|
|
|
|
}
|
|
|
|
|
|
|
|
// Sync receipts from previously-seen messages
|
|
|
|
const receipts = await this.page.evaluate(
|
|
|
|
mostRecentReceipts => window.__mautrixController.parseReceiptList(mostRecentReceipts),
|
|
|
|
Object.fromEntries(receiptMap))
|
|
|
|
for (const receipt of receipts) {
|
|
|
|
receiptMap.set(receipt.count, receipt.id)
|
|
|
|
receipt.chat_id = chatID
|
|
|
|
}
|
|
|
|
|
|
|
|
this._trimReceiptMap(receiptMap)
|
|
|
|
|
|
|
|
|
2021-06-06 18:15:38 -04:00
|
|
|
if (hadMsgListObserver) {
|
|
|
|
this.log("Restoring msg list observer")
|
|
|
|
await this.page.evaluate(
|
|
|
|
mostRecentMessage => window.__mautrixController.addMsgListObserver(mostRecentMessage),
|
|
|
|
this.mostRecentMessages.get(chatID))
|
|
|
|
} else {
|
|
|
|
this.log("Not restoring msg list observer, as there never was one")
|
|
|
|
}
|
|
|
|
|
2021-06-30 03:04:25 -04:00
|
|
|
return {
|
|
|
|
messages: messages,
|
|
|
|
receipts: receipts
|
|
|
|
}
|
2020-08-18 09:47:06 -04:00
|
|
|
}
|
|
|
|
|
2021-06-10 02:10:18 -04:00
|
|
|
_processMessages(chatID, messages) {
|
2021-06-06 18:15:38 -04:00
|
|
|
// TODO Probably don't need minID filtering if Puppeteer context handles it now
|
2021-04-27 02:59:16 -04:00
|
|
|
const minID = this.mostRecentMessages.get(chatID) || 0
|
2021-06-30 03:04:25 -04:00
|
|
|
const filteredMessages = messages.filter(msg => msg.id > minID)
|
2021-04-27 02:59:16 -04:00
|
|
|
|
2021-06-06 18:15:38 -04:00
|
|
|
if (filteredMessages.length > 0) {
|
|
|
|
const newFirstID = filteredMessages[0].id
|
|
|
|
const newLastID = filteredMessages[filteredMessages.length - 1].id
|
2021-04-27 02:59:16 -04:00
|
|
|
this.mostRecentMessages.set(chatID, newLastID)
|
|
|
|
const range = newFirstID === newLastID ? newFirstID : `${newFirstID}-${newLastID}`
|
2021-06-06 18:15:38 -04:00
|
|
|
this.log(`Loaded ${messages.length} messages in ${chatID}, got ${filteredMessages.length} newer than ${minID} (${range})`)
|
|
|
|
for (const message of filteredMessages) {
|
|
|
|
message.chat_id = chatID
|
|
|
|
}
|
2021-06-30 03:04:25 -04:00
|
|
|
for (let i = filteredMessages.length - 1; i >= 0; i--) {
|
|
|
|
const message = filteredMessages[i]
|
|
|
|
if (message.is_outgoing) {
|
|
|
|
this.mostRecentOwnMessages.set(chatID, message.id)
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
2021-06-06 18:15:38 -04:00
|
|
|
return filteredMessages
|
|
|
|
} else {
|
|
|
|
return []
|
2021-04-27 02:59:16 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-06-30 03:04:25 -04:00
|
|
|
_getReceiptMap(chatID) {
|
|
|
|
if (!this.mostRecentReceipts.has(chatID)) {
|
|
|
|
const newMap = new Map()
|
|
|
|
this.mostRecentReceipts.set(chatID, newMap)
|
|
|
|
return newMap
|
|
|
|
} else {
|
|
|
|
return this.mostRecentReceipts.get(chatID)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
_trimReceiptMap(receiptMap) {
|
|
|
|
// Delete lower counts for earlier messages
|
|
|
|
let prevCount = null
|
|
|
|
for (const count of Array.from(receiptMap.keys()).sort()) {
|
|
|
|
if (prevCount != null && receiptMap.get(prevCount) < receiptMap.get(count)) {
|
|
|
|
receiptMap.delete(count)
|
|
|
|
}
|
|
|
|
prevCount = count
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-06-10 02:10:18 -04:00
|
|
|
async _processChatListChangeUnsafe(chatListInfo) {
|
|
|
|
const chatID = chatListInfo.id
|
2021-04-22 02:39:52 -04:00
|
|
|
this.updatedChats.delete(chatID)
|
|
|
|
this.log("Processing change to", chatID)
|
2021-06-10 02:10:18 -04:00
|
|
|
// TODO Also process name/icon changes
|
|
|
|
|
|
|
|
const prevNumNotifications = this.numChatNotifications.get(chatID) || 0
|
|
|
|
const diffNumNotifications = chatListInfo.notificationCount - prevNumNotifications
|
|
|
|
|
|
|
|
if (chatListInfo.notificationCount == 0 && diffNumNotifications < 0) {
|
2021-06-17 00:42:06 -04:00
|
|
|
this.log("Notifications dropped--must have read messages from another LINE client, skip")
|
2021-06-10 02:10:18 -04:00
|
|
|
this.numChatNotifications.set(chatID, 0)
|
2020-08-18 09:47:06 -04:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2021-06-10 02:10:18 -04:00
|
|
|
const mustSync =
|
2021-06-17 00:42:06 -04:00
|
|
|
// If >1, a notification was missed. Only way to get them is to view the chat.
|
|
|
|
// If == 0, might be own message...or just a shuffled chat, or something else.
|
|
|
|
// To play it safe, just sync them. Should be no harm, as they're viewed already.
|
2021-06-17 23:32:45 -04:00
|
|
|
diffNumNotifications != 1
|
2021-06-17 00:42:06 -04:00
|
|
|
// Without placeholders, some messages require visiting their chat to be synced.
|
|
|
|
|| !this.sendPlaceholders
|
|
|
|
&& (
|
2021-06-17 23:32:45 -04:00
|
|
|
// Can only use previews for DMs, because sender can't be found otherwise!
|
|
|
|
chatListInfo.id.charAt(0) != 'u'
|
2021-06-17 00:42:06 -04:00
|
|
|
// Sync when lastMsg is a canned message for a non-previewable message type.
|
2021-06-17 23:32:45 -04:00
|
|
|
|| chatListInfo.lastMsg.endsWith(" sent a photo.")
|
2021-06-17 00:42:06 -04:00
|
|
|
|| chatListInfo.lastMsg.endsWith(" sent a sticker.")
|
|
|
|
|| chatListInfo.lastMsg.endsWith(" sent a location.")
|
|
|
|
// TODO More?
|
|
|
|
)
|
2021-06-10 02:10:18 -04:00
|
|
|
|
|
|
|
let messages
|
|
|
|
if (!mustSync) {
|
|
|
|
messages = [{
|
|
|
|
chat_id: chatListInfo.id,
|
|
|
|
id: null, // because sidebar messages have no ID
|
|
|
|
timestamp: null, // because this message was sent right now
|
2021-06-17 00:42:06 -04:00
|
|
|
is_outgoing: false, // because there's no reliable way to detect own messages...
|
2021-06-17 23:32:45 -04:00
|
|
|
sender: null, // because there's no way to tell who sent a message
|
2021-06-10 02:10:18 -04:00
|
|
|
html: chatListInfo.lastMsg,
|
|
|
|
}]
|
|
|
|
this.numChatNotifications.set(chatID, chatListInfo.notificationCount)
|
2021-06-30 03:04:25 -04:00
|
|
|
this._receiveMessages(chatID, messages, true)
|
2021-06-10 02:10:18 -04:00
|
|
|
} else {
|
|
|
|
this.numChatNotifications.set(chatID, 0)
|
2021-06-30 03:04:25 -04:00
|
|
|
await this._syncChat(chatListInfo.id)
|
2021-06-10 02:10:18 -04:00
|
|
|
}
|
2021-06-30 03:04:25 -04:00
|
|
|
}
|
2021-06-10 02:10:18 -04:00
|
|
|
|
2021-06-30 03:04:25 -04:00
|
|
|
async _syncChat(chatID) {
|
|
|
|
const {messages, receipts} = await this._getMessagesUnsafe(chatID)
|
|
|
|
|
|
|
|
if (messages.length == 0) {
|
|
|
|
this.log("No new messages found in", chatID)
|
2020-08-18 09:47:06 -04:00
|
|
|
} else {
|
2021-06-30 03:04:25 -04:00
|
|
|
this._receiveMessages(chatID, messages, true)
|
|
|
|
}
|
|
|
|
|
|
|
|
if (receipts.length == 0) {
|
|
|
|
this.log("No new receipts found in", chatID)
|
|
|
|
} else {
|
|
|
|
this._receiveReceiptMulti(chatID, receipts, true)
|
2020-08-18 09:47:06 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
_receiveChatListChanges(changes) {
|
2021-06-10 02:10:18 -04:00
|
|
|
this.log(`Received chat list changes: ${changes.map(item => item.id)}`)
|
2020-08-18 09:47:06 -04:00
|
|
|
for (const item of changes) {
|
2021-06-10 02:10:18 -04:00
|
|
|
if (!this.updatedChats.has(item.id)) {
|
|
|
|
this.updatedChats.add(item.id)
|
2020-08-18 09:47:06 -04:00
|
|
|
this.taskQueue.push(() => this._processChatListChangeUnsafe(item))
|
|
|
|
.catch(err => this.error("Error handling chat list changes:", err))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-04-20 20:01:50 -04:00
|
|
|
_receiveReceiptDirectLatest(chat_id, receipt_id) {
|
2021-06-30 03:04:25 -04:00
|
|
|
const receiptMap = this._getReceiptMap(chat_id)
|
|
|
|
const prevReceiptID = (receiptMap.get(1) || 0)
|
|
|
|
if (receipt_id <= prevReceiptID) {
|
|
|
|
this.log(`Received OUTDATED read receipt ${receipt_id} (older than ${prevReceiptID}) for chat ${chat_id}`)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
receiptMap.set(1, receipt_id)
|
|
|
|
|
|
|
|
this.log(`Received read receipt ${receipt_id} (since ${prevReceiptID}) for chat ${chat_id}`)
|
2021-04-27 02:59:16 -04:00
|
|
|
if (this.client) {
|
|
|
|
this.client.sendReceipt({chat_id: chat_id, id: receipt_id})
|
|
|
|
.catch(err => this.error("Error handling read receipt:", err))
|
|
|
|
} else {
|
|
|
|
this.log("No client connected, not sending receipts")
|
|
|
|
}
|
2021-04-20 20:01:50 -04:00
|
|
|
}
|
|
|
|
|
2021-06-30 03:04:25 -04:00
|
|
|
async _receiveReceiptMulti(chat_id, receipts, skipProcessing = false) {
|
2021-04-27 02:59:16 -04:00
|
|
|
// Use async to ensure that receipts are sent in order
|
2021-06-30 03:04:25 -04:00
|
|
|
|
|
|
|
if (!skipProcessing) {
|
|
|
|
const receiptMap = this._getReceiptMap(chat_id)
|
|
|
|
receipts.filter(receipt => {
|
|
|
|
if (receipt.id > (receiptMap.get(receipt.count) || 0)) {
|
|
|
|
receiptMap.set(receipt.count, receipt.id)
|
|
|
|
return true
|
|
|
|
} else {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
})
|
|
|
|
if (receipts.length == 0) {
|
|
|
|
this.log(`Received ALL OUTDATED bulk read receipts for chat ${chat_id}:`, receipts)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
this._trimReceiptMap(receiptMap)
|
|
|
|
}
|
|
|
|
|
2021-04-20 20:01:50 -04:00
|
|
|
this.log(`Received bulk read receipts for chat ${chat_id}:`, receipts)
|
2021-04-27 02:59:16 -04:00
|
|
|
if (this.client) {
|
|
|
|
for (const receipt of receipts) {
|
|
|
|
receipt.chat_id = chat_id
|
|
|
|
try {
|
|
|
|
await this.client.sendReceipt(receipt)
|
|
|
|
} catch(err) {
|
|
|
|
this.error("Error handling read receipt:", err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
this.log("No client connected, not sending receipts")
|
2021-04-20 20:01:50 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-02-10 02:34:19 -05:00
|
|
|
async _sendEmailCredentials() {
|
|
|
|
this.log("Inputting login credentials")
|
2021-06-14 01:47:20 -04:00
|
|
|
await this._enterText(await this.page.$("#line_login_email"), this.login_email)
|
|
|
|
await this._enterText(await this.page.$("#line_login_pwd"), this.login_password)
|
2021-02-10 02:34:19 -05:00
|
|
|
await this.page.click("button#login_btn")
|
|
|
|
}
|
|
|
|
|
2020-08-18 09:47:06 -04:00
|
|
|
_receiveQRChange(url) {
|
|
|
|
if (this.client) {
|
|
|
|
this.client.sendQRCode(url).catch(err =>
|
|
|
|
this.error("Failed to send new QR to client:", err))
|
|
|
|
} else {
|
|
|
|
this.log("No client connected, not sending new QR")
|
|
|
|
}
|
|
|
|
}
|
2021-02-10 02:34:19 -05:00
|
|
|
|
|
|
|
_receivePIN(pin) {
|
|
|
|
if (this.client) {
|
2021-02-20 03:48:08 -05:00
|
|
|
this.client.sendPIN(pin).catch(err =>
|
2021-02-10 02:34:19 -05:00
|
|
|
this.error("Failed to send new PIN to client:", err))
|
|
|
|
} else {
|
|
|
|
this.log("No client connected, not sending new PIN")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-06-08 00:24:00 -04:00
|
|
|
_sendLoginSuccess() {
|
|
|
|
this.error("Login success")
|
|
|
|
if (this.client) {
|
|
|
|
this.client.sendLoginSuccess().catch(err =>
|
|
|
|
this.error("Failed to send login success to client:", err))
|
|
|
|
} else {
|
|
|
|
this.log("No client connected, not sending login success")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-02-10 02:34:19 -05:00
|
|
|
_sendLoginFailure(reason) {
|
2021-02-12 02:16:09 -05:00
|
|
|
this.loginRunning = false
|
2021-02-25 22:21:11 -05:00
|
|
|
this.error(`Login failure: ${reason ? reason : "cancelled"}`)
|
2021-02-10 02:34:19 -05:00
|
|
|
if (this.client) {
|
2021-06-08 00:24:00 -04:00
|
|
|
this.client.sendLoginFailure(reason).catch(err =>
|
|
|
|
this.error("Failed to send login failure to client:", err))
|
2021-02-10 02:34:19 -05:00
|
|
|
} else {
|
2021-06-08 00:24:00 -04:00
|
|
|
this.log("No client connected, not sending login failure")
|
2021-02-10 02:34:19 -05:00
|
|
|
}
|
|
|
|
}
|
2021-05-30 17:41:28 -04:00
|
|
|
|
|
|
|
_onLoggedOut() {
|
|
|
|
this.log("Got logged out!")
|
|
|
|
this.stopObserving()
|
2021-06-11 02:53:30 -04:00
|
|
|
this.page.bringToFront()
|
2021-05-30 17:41:28 -04:00
|
|
|
if (this.client) {
|
|
|
|
this.client.sendLoggedOut().catch(err =>
|
|
|
|
this.error("Failed to send logout notice to client:", err))
|
|
|
|
} else {
|
|
|
|
this.log("No client connected, not sending logout notice")
|
|
|
|
}
|
|
|
|
}
|
2020-08-18 09:47:06 -04:00
|
|
|
}
|