Compare commits

...

6 Commits

Author SHA1 Message Date
Cristian Le 855966e547 Forgot one 2021-07-30 17:47:01 +09:00
Cristian Le a477d10d29 `require` does not work? 2021-07-30 17:45:11 +09:00
Cristian Le 3fdb6cc8d3 Implement logger for remaining 2021-07-30 17:39:42 +09:00
Cristian Le 0a6609e6c9 Defaults not undefined 2021-07-30 17:02:42 +09:00
Cristian Le 4e639ee2ff Mistakes and nullchecks
Doesn't javascript support
`a?.b?.c`?
2021-07-30 16:54:50 +09:00
Cristian Le b575dba30d Add specific loggers 2021-07-30 16:14:25 +09:00
7 changed files with 286 additions and 267 deletions

View File

@ -1,31 +1,33 @@
{ {
"name": "matrix-puppeteer-line-chrome", "name": "matrix-puppeteer-line-chrome",
"version": "0.1.0", "version": "0.1.0",
"description": "Chrome/Puppeteer backend for matrix-puppeteer-line", "description": "Chrome/Puppeteer backend for matrix-puppeteer-line",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git+https://src.miscworks.net/fair/matrix-puppeteer-line.git" "url": "git+https://src.miscworks.net/fair/matrix-puppeteer-line.git"
}, },
"engines": { "engines": {
"node": ">=10.18.1" "node": ">=10.18.1"
}, },
"type": "module", "type": "module",
"main": "src/main.js", "main": "src/main.js",
"author": "Andrew Ferrazzutti <fair@miscworks.net>", "author": "Andrew Ferrazzutti <fair@miscworks.net>",
"license": "AGPL-3.0-or-later", "license": "AGPL-3.0-or-later",
"homepage": "https://src.miscworks.net/fair/matrix-puppeteer-line", "homepage": "https://src.miscworks.net/fair/matrix-puppeteer-line",
"scripts": { "scripts": {
"start": "node ./src/main.js" "start": "node ./src/main.js"
}, },
"dependencies": { "dependencies": {
"arg": "^4.1.3", "arg": "^4.1.3",
"chrono-node": "^2.1.7", "chrono-node": "^2.1.7",
"systemd-daemon": "^1.1.2", "systemd-daemon": "^1.1.2",
"puppeteer": "9.1.1" "puppeteer": "9.1.1",
}, "loglevel": "^1.7.1",
"devDependencies": { "loglevel-plugin-prefix": "^0.8.4"
"babel-eslint": "^10.1.0", },
"eslint": "^7.7.0", "devDependencies": {
"eslint-plugin-import": "^2.22.0" "babel-eslint": "^10.1.0",
} "eslint": "^7.7.0",
"eslint-plugin-import": "^2.22.0"
}
} }

View File

@ -16,6 +16,7 @@
import net from "net" import net from "net"
import fs from "fs" import fs from "fs"
import path from "path" import path from "path"
import logger from "loglevel";
import Client from "./client.js" import Client from "./client.js"
import { promisify } from "./util.js" import { promisify } from "./util.js"
@ -29,10 +30,7 @@ export default class PuppetAPI {
this.clients = new Map() this.clients = new Map()
this.connIDSequence = 0 this.connIDSequence = 0
this.stopped = false this.stopped = false
} this.log = logger.getLogger("API")
log(...text) {
console.log("[API]", ...text)
} }
acceptConnection = sock => { acceptConnection = sock => {
@ -57,16 +55,16 @@ export default class PuppetAPI {
} catch (err) {} } catch (err) {}
await promisify(cb => this.server.listen(socketPath, cb)) await promisify(cb => this.server.listen(socketPath, cb))
await fs.promises.chmod(socketPath, 0o700) await fs.promises.chmod(socketPath, 0o700)
this.log("Now listening at", socketPath) this.log.info("Now listening at", socketPath)
} }
async startTCP(port, host) { async startTCP(port, host) {
await promisify(cb => this.server.listen(port, host, cb)) await promisify(cb => this.server.listen(port, host, cb))
this.log(`Now listening at ${host || ""}:${port}`) this.log.info(`Now listening at ${host || ""}:${port}`)
} }
async start() { async start() {
this.log("Starting server") this.log.info("Starting server")
if (this.listenConfig.type === "unix") { if (this.listenConfig.type === "unix") {
await this.startUnix(this.listenConfig.path) await this.startUnix(this.listenConfig.path)
@ -84,14 +82,14 @@ export default class PuppetAPI {
socket.end() socket.end()
socket.destroy() socket.destroy()
} }
this.log("Stopping server") this.log.info("Stopping server")
await promisify(cb => this.server.close(cb)) await promisify(cb => this.server.close(cb))
if (this.listenConfig.type === "unix") { if (this.listenConfig.type === "unix") {
try { try {
await fs.promises.unlink(this.listenConfig.path) await fs.promises.unlink(this.listenConfig.path)
} catch (err) {} } catch (err) {}
} }
this.log("Stopping puppets") this.log.info("Stopping puppets")
for (const puppet of this.puppets.values()) { for (const puppet of this.puppets.values()) {
await puppet.stop() await puppet.stop()
} }

View File

@ -14,7 +14,8 @@
// You should have received a copy of the GNU Affero General Public License // 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/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
import MessagesPuppeteer from "./puppet.js" import MessagesPuppeteer from "./puppet.js"
import { emitLines, promisify } from "./util.js" import {emitLines, promisify} from "./util.js"
import logger from "loglevel";
export default class Client { export default class Client {
/** /**
@ -28,31 +29,27 @@ export default class Client {
this.manager = manager this.manager = manager
this.socket = socket this.socket = socket
this.connID = connID this.connID = connID
this.userID = userID
this.puppet = puppet this.puppet = puppet
this.stopped = false this.stopped = false
this.notificationID = 0 this.notificationID = 0
this.maxCommandID = 0 this.maxCommandID = 0
} this.set_userID(userID)
if (!this.userID) {
log(...text) { this.log = logger.getLogger(`API/${this.connID}`)
if (this.userID) { this.log.setLevel(logger.getLogger("API").getLevel())
console.log(`[API/${this.userID}/${this.connID}]`, ...text)
} else {
console.log(`[API/${this.connID}]`, ...text)
} }
} }
error(...text) { set_userID(ID) {
this.userID = ID
if (this.userID) { if (this.userID) {
console.error(`[API/${this.userID}/${this.connID}]`, ...text) this.log = logger.getLogger(`API/${this.userID}/${this.connID}`)
} else { this.log.setLevel(logger.getLogger(`API/${this.connID}`).getLevel())
console.error(`[API/${this.connID}]`, ...text)
} }
} }
start() { start() {
this.log("Received connection", this.connID) this.log.info("Received connection", this.connID)
emitLines(this.socket) emitLines(this.socket)
this.socket.on("line", line => this.handleLine(line) this.socket.on("line", line => this.handleLine(line)
.catch(err => this.log("Error handling line:", err))) .catch(err => this.log("Error handling line:", err)))
@ -60,7 +57,7 @@ export default class Client {
setTimeout(() => { setTimeout(() => {
if (!this.userID && !this.stopped) { if (!this.userID && !this.stopped) {
this.log("Didn't receive register request within 3 seconds, terminating") this.log.warn("Didn't receive register request within 3 seconds, terminating")
this.stop("Register request timeout") this.stop("Register request timeout")
} }
}, 3000) }, 3000)
@ -72,10 +69,10 @@ export default class Client {
} }
this.stopped = true this.stopped = true
try { try {
await this._write({ id: --this.notificationID, command: "quit", error }) await this._write({id: --this.notificationID, command: "quit", error})
await promisify(cb => this.socket.end(cb)) await promisify(cb => this.socket.end(cb))
} catch (err) { } catch (err) {
this.error("Failed to end connection:", err) this.log.error("Failed to end connection:", err)
this.socket.destroy(err) this.socket.destroy(err)
} }
} }
@ -85,7 +82,7 @@ export default class Client {
if (this.userID && this.manager.clients.get(this.userID) === this) { if (this.userID && this.manager.clients.get(this.userID) === this) {
this.manager.clients.delete(this.userID) this.manager.clients.delete(this.userID)
} }
this.log(`Connection closed (user: ${this.userID})`) this.log.info(`Connection closed (user: ${this.userID})`)
} }
/** /**
@ -99,7 +96,7 @@ export default class Client {
} }
sendMessage(message) { sendMessage(message) {
this.log(`Sending message ${message.id || "with no ID"} to client`) this.log.debug(`Sending message ${message.id || "with no ID"} to client`)
return this._write({ return this._write({
id: --this.notificationID, id: --this.notificationID,
command: "message", command: "message",
@ -109,7 +106,7 @@ export default class Client {
} }
sendReceipt(receipt) { sendReceipt(receipt) {
this.log(`Sending read receipt (${receipt.count || "DM"}) of msg ${receipt.id} for chat ${receipt.chat_id}`) this.log.debug(`Sending read receipt (${receipt.count || "DM"}) of msg ${receipt.id} for chat ${receipt.chat_id}`)
return this._write({ return this._write({
id: --this.notificationID, id: --this.notificationID,
command: "receipt", command: "receipt",
@ -118,7 +115,7 @@ export default class Client {
} }
sendQRCode(url) { sendQRCode(url) {
this.log(`Sending QR ${url} to client`) this.log.debug(`Sending QR ${url} to client`)
return this._write({ return this._write({
id: --this.notificationID, id: --this.notificationID,
command: "qr", command: "qr",
@ -127,7 +124,7 @@ export default class Client {
} }
sendPIN(pin) { sendPIN(pin) {
this.log(`Sending PIN ${pin} to client`) this.log.debug(`Sending PIN ${pin} to client`)
return this._write({ return this._write({
id: --this.notificationID, id: --this.notificationID,
command: "pin", command: "pin",
@ -136,7 +133,7 @@ export default class Client {
} }
sendLoginSuccess() { sendLoginSuccess() {
this.log("Sending login success to client") this.log.debug("Sending login success to client")
return this._write({ return this._write({
id: --this.notificationID, id: --this.notificationID,
command: "login_success", command: "login_success",
@ -144,7 +141,7 @@ export default class Client {
} }
sendLoginFailure(reason) { sendLoginFailure(reason) {
this.log(`Sending login failure to client${reason ? `: "${reason}"` : ""}`) this.log.debug(`Sending login failure to client${reason ? `: "${reason}"` : ""}`)
return this._write({ return this._write({
id: --this.notificationID, id: --this.notificationID,
command: "login_failure", command: "login_failure",
@ -153,7 +150,7 @@ export default class Client {
} }
sendLoggedOut() { sendLoggedOut() {
this.log("Sending logout notice to client") this.log.debug("Sending logout notice to client")
return this._write({ return this._write({
id: --this.notificationID, id: --this.notificationID,
command: "logged_out", command: "logged_out",
@ -163,7 +160,7 @@ export default class Client {
handleStart = async (req) => { handleStart = async (req) => {
let started = false let started = false
if (this.puppet === null) { if (this.puppet === null) {
this.log("Opening new puppeteer for", this.userID) this.log.info("Opening new puppeteer for", this.userID)
this.puppet = new MessagesPuppeteer(this.userID, this.ownID, this.sendPlaceholders, this) this.puppet = new MessagesPuppeteer(this.userID, this.ownID, this.sendPlaceholders, this)
this.manager.puppets.set(this.userID, this.puppet) this.manager.puppets.set(this.userID, this.puppet)
await this.puppet.start(!!req.debug) await this.puppet.start(!!req.debug)
@ -179,13 +176,13 @@ export default class Client {
handleStop = async () => { handleStop = async () => {
if (this.puppet === null) { if (this.puppet === null) {
return { stopped: false } return {stopped: false}
} }
this.log("Closing puppeteer for", this.userID) this.log.info("Closing puppeteer for", this.userID)
this.manager.puppets.delete(this.userID) this.manager.puppets.delete(this.userID)
await this.puppet.stop() await this.puppet.stop()
this.puppet = null this.puppet = null
return { stopped: true } return {stopped: true}
} }
handleUnknownCommand = () => { handleUnknownCommand = () => {
@ -193,14 +190,14 @@ export default class Client {
} }
handleRegister = async (req) => { handleRegister = async (req) => {
this.userID = req.user_id this.set_userID(req.user_id)
this.ownID = req.own_id this.ownID = req.own_id
this.sendPlaceholders = req.ephemeral_events this.sendPlaceholders = req.ephemeral_events
this.log(`Registered socket ${this.connID} -> ${this.userID}${!this.sendPlaceholders ? "" : " (with placeholder message support)"}`) this.log.info(`Registered socket ${this.connID} -> ${this.userID}${!this.sendPlaceholders ? "" : " (with placeholder message support)"}`)
if (this.manager.clients.has(this.userID)) { if (this.manager.clients.has(this.userID)) {
const oldClient = this.manager.clients.get(this.userID) const oldClient = this.manager.clients.get(this.userID)
this.manager.clients.set(this.userID, this) this.manager.clients.set(this.userID, this)
this.log(`Terminating previous socket ${oldClient.connID} for ${this.userID}`) this.log.info(`Terminating previous socket ${oldClient.connID} for ${this.userID}`)
await oldClient.stop("Socket replaced by new connection") await oldClient.stop("Socket replaced by new connection")
} else { } else {
this.manager.clients.set(this.userID, this) this.manager.clients.set(this.userID, this)
@ -209,41 +206,41 @@ export default class Client {
if (this.puppet) { if (this.puppet) {
this.puppet.client = this this.puppet.client = this
} }
return { client_exists: this.puppet !== null } return {client_exists: this.puppet !== null}
} }
async handleLine(line) { async handleLine(line) {
if (this.stopped) { if (this.stopped) {
this.log("Ignoring line, client is stopped") this.log.info("Ignoring line, client is stopped")
return return
} }
let req let req
try { try {
req = JSON.parse(line) req = JSON.parse(line)
} catch (err) { } catch (err) {
this.log("Non-JSON request:", line) this.log.error("Non-JSON request:", line)
return return
} }
if (!req.command || !req.id) { if (!req.command || !req.id) {
this.log("Invalid request:", line) this.log.error("Invalid request:", line)
return return
} }
if (req.id <= this.maxCommandID) { if (req.id <= this.maxCommandID) {
this.log("Ignoring old request", req.id) this.log.warn("Ignoring old request", req.id)
return return
} }
if (req.command != "is_connected") { if (req.command != "is_connected") {
this.log("Received request", req.id, "with command", req.command) this.log.info("Received request", req.id, "with command", req.command)
} }
this.maxCommandID = req.id this.maxCommandID = req.id
let handler let handler
if (!this.userID) { if (!this.userID) {
if (req.command !== "register") { if (req.command !== "register") {
this.log("First request wasn't a register request, terminating") this.log.info("First request wasn't a register request, terminating")
await this.stop("Invalid first request") await this.stop("Invalid first request")
return return
} else if (!req.user_id) { } else if (!req.user_id) {
this.log("Register request didn't contain user ID, terminating") this.log.info("Register request didn't contain user ID, terminating")
await this.stop("Invalid register request") await this.stop("Invalid register request")
return return
} }
@ -267,17 +264,17 @@ export default class Client {
get_chat: req => this.puppet.getChatInfo(req.chat_id, req.force_view), get_chat: req => this.puppet.getChatInfo(req.chat_id, req.force_view),
get_messages: req => this.puppet.getMessages(req.chat_id), get_messages: req => this.puppet.getMessages(req.chat_id),
read_image: req => this.puppet.readImage(req.image_url), read_image: req => this.puppet.readImage(req.image_url),
is_connected: async () => ({ is_connected: !await this.puppet.isDisconnected() }), is_connected: async () => ({is_connected: !await this.puppet.isDisconnected()}),
}[req.command] || this.handleUnknownCommand }[req.command] || this.handleUnknownCommand
} }
const resp = { id: req.id } const resp = {id: req.id}
try { try {
resp.command = "response" resp.command = "response"
resp.response = await handler(req) resp.response = await handler(req)
} catch (err) { } catch (err) {
resp.command = "error" resp.command = "error"
resp.error = err.toString() resp.error = err.toString()
this.log("Error handling request", req.id, err) this.log.error("Error handling request", req.id, err)
} }
await this._write(resp) await this._write(resp)
} }

View File

@ -16,6 +16,8 @@
import process from "process" import process from "process"
import fs from "fs" import fs from "fs"
import sd from "systemd-daemon" import sd from "systemd-daemon"
import logger from "loglevel"
import loggerprefix from "loglevel-plugin-prefix"
import arg from "arg" import arg from "arg"
@ -42,6 +44,17 @@ MessagesPuppeteer.extensionDir = config.extension_dir || MessagesPuppeteer.exten
MessagesPuppeteer.cycleDelay = config.cycle_delay || MessagesPuppeteer.cycleDelay MessagesPuppeteer.cycleDelay = config.cycle_delay || MessagesPuppeteer.cycleDelay
MessagesPuppeteer.useXdotool = config.use_xdotool || MessagesPuppeteer.useXdotool MessagesPuppeteer.useXdotool = config.use_xdotool || MessagesPuppeteer.useXdotool
MessagesPuppeteer.jiggleDelay = config.jiggle_delay || MessagesPuppeteer.jiggleDelay MessagesPuppeteer.jiggleDelay = config.jiggle_delay || MessagesPuppeteer.jiggleDelay
let clog = config.logger
let clogpup = clog ? clog.puppeteer : undefined
logger.setLevel(clog ? clog.default_level || 3 : 3)
logger.getLogger("Puppeteer").setLevel(clogpup ? clogpup.level || logger.getLevel() : logger.getLevel())
logger.getLogger("Puppeteer_details").setLevel(clogpup ? clogpup.details || 3 : 3)
logger.getLogger("Puppeteer_spammer").setLevel(clogpup ? clogpup.spammer || 3 : 3)
logger.getLogger("TaskQueue").setLevel(clog ? clog.task_queue || 3 : 3)
logger.getLogger("API").setLevel(clog ? clog.api || 3 : 3)
// Register and specify the logger format. Others will inherit
loggerprefix.reg(logger)
loggerprefix.apply(logger, {template: '[%n] %l:'});
const api = new PuppetAPI(config.listen) const api = new PuppetAPI(config.listen)

View File

@ -15,13 +15,14 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
import process from "process" import process from "process"
import path from "path" import path from "path"
import { exec, execSync } from "child_process" import {exec, execSync} from "child_process"
import puppeteer from "puppeteer" import puppeteer from "puppeteer"
import chrono from "chrono-node" import chrono from "chrono-node"
import logger from "loglevel"
import TaskQueue from "./taskqueue.js" import TaskQueue from "./taskqueue.js"
import { sleep } from "./util.js" import {sleep} from "./util.js"
export default class MessagesPuppeteer { export default class MessagesPuppeteer {
static profileDir = "./profiles" static profileDir = "./profiles"
@ -31,7 +32,7 @@ export default class MessagesPuppeteer {
static jiggleDelay = 30000 static jiggleDelay = 30000
static devtools = false static devtools = false
static noSandbox = false static noSandbox = false
static viewport = { width: 960, height: 840 } static viewport = {width: 960, height: 840}
static url = undefined static url = undefined
static extensionDir = "extension_files" static extensionDir = "extension_files"
@ -59,14 +60,17 @@ export default class MessagesPuppeteer {
this.jiggleTimerID = null this.jiggleTimerID = null
this.taskQueue = new TaskQueue(this.id) this.taskQueue = new TaskQueue(this.id)
this.client = client this.client = client
} // Distinct logger object with same format as previous method
// General logger for overall process
log(...text) { this.log = logger.getLogger(`Puppeteer/${this.id}`)
console.log(`[Puppeteer/${this.id}]`, ...text) // Detailed logger for active run (event based)
} this.dlog = logger.getLogger(`Puppeteer/${this.id}: Details`)
// Seperate logger for more spammy messages (timer based)
error(...text) { this.spammer = logger.getLogger(`Puppeteer/${this.id}: Spam`)
console.error(`[Puppeteer/${this.id}]`, ...text) // Inherit config rules
this.log.setLevel(logger.getLogger("Puppeteer").getLevel())
this.dlog.setLevel(logger.getLogger("Puppeteer_details").getLevel())
this.spammer.setLevel(logger.getLogger("Puppeteer_spammer").getLevel())
} }
/** /**
@ -74,12 +78,12 @@ export default class MessagesPuppeteer {
* This must be called before doing anything else. * This must be called before doing anything else.
*/ */
async start() { async start() {
this.log("Launching browser") this.log.info("Launching browser")
const args = [ let args = [
`--disable-extensions-except=${MessagesPuppeteer.extensionDir}`, `--disable-extensions-except=${MessagesPuppeteer.extensionDir}`,
`--load-extension=${MessagesPuppeteer.extensionDir}`, `--load-extension=${MessagesPuppeteer.extensionDir}`,
`--window-size=${MessagesPuppeteer.viewport.width},${MessagesPuppeteer.viewport.height+120}`, `--window-size=${MessagesPuppeteer.viewport.width},${MessagesPuppeteer.viewport.height + 120}`,
] ]
if (MessagesPuppeteer.noSandbox) { if (MessagesPuppeteer.noSandbox) {
args = args.concat(`--no-sandbox`) args = args.concat(`--no-sandbox`)
@ -94,7 +98,7 @@ export default class MessagesPuppeteer {
devtools: MessagesPuppeteer.devtools, devtools: MessagesPuppeteer.devtools,
timeout: 0, timeout: 0,
}) })
this.log("Opening new tab") this.log.info("Opening new tab")
const pages = await this.browser.pages() const pages = await this.browser.pages()
if (pages.length > 0) { if (pages.length > 0) {
this.page = pages[0] this.page = pages[0]
@ -103,29 +107,29 @@ export default class MessagesPuppeteer {
} }
{ {
this.log("Finding extension UUID") this.log.info("Finding extension UUID")
await this.page.goto("chrome://system") await this.page.goto("chrome://system")
const selector = "#extensions-value" const selector = "#extensions-value"
await this.page.waitForSelector(selector, 0) await this.page.waitForSelector(selector, 0)
const lineDetails = await this.page.$eval(selector, e => e.innerText) const lineDetails = await this.page.$eval(selector, e => e.innerText)
const uuid = lineDetails.match(/(.*) : LINE : version/)[1] const uuid = lineDetails.match(/(.*) : LINE : version/)[1]
this.log(`Found extension UUID ${uuid}`) this.log.info(`Found extension UUID ${uuid}`)
MessagesPuppeteer.url = `chrome-extension://${uuid}/index.html` MessagesPuppeteer.url = `chrome-extension://${uuid}/index.html`
} }
this.blankPage = await this.browser.newPage() this.blankPage = await this.browser.newPage()
if (MessagesPuppeteer.useXdotool) { if (MessagesPuppeteer.useXdotool) {
this.log("Finding window ID") this.log.info("Finding window ID")
const buffer = execSync("xdotool search 'about:blank'") const buffer = execSync("xdotool search 'about:blank'")
this.windowID = Number.parseInt(buffer) this.windowID = Number.parseInt(buffer)
this.log(`Found window ID ${this.windowID}`) this.log.info(`Found window ID ${this.windowID}`)
} }
this.log(`Opening ${MessagesPuppeteer.url}`) this.log.info(`Opening ${MessagesPuppeteer.url}`)
await this.page.setBypassCSP(true) // Needed to load content scripts await this.page.setBypassCSP(true) // Needed to load content scripts
await this._preparePage(true) await this._preparePage(true)
this.log("Exposing functions") this.log.info("Exposing functions")
await this.page.exposeFunction("__mautrixReceiveQR", this._receiveQRChange.bind(this)) await this.page.exposeFunction("__mautrixReceiveQR", this._receiveQRChange.bind(this))
await this.page.exposeFunction("__mautrixSendEmailCredentials", this._sendEmailCredentials.bind(this)) await this.page.exposeFunction("__mautrixSendEmailCredentials", this._sendEmailCredentials.bind(this))
await this.page.exposeFunction("__mautrixReceivePIN", this._receivePIN.bind(this)) await this.page.exposeFunction("__mautrixReceivePIN", this._receivePIN.bind(this))
@ -145,7 +149,7 @@ export default class MessagesPuppeteer {
this.loginRunning = false this.loginRunning = false
this.loginCancelled = false this.loginCancelled = false
this.taskQueue.start() this.taskQueue.start()
this.log("Startup complete") this.log.info("Startup complete")
} }
async _preparePage(navigateTo) { async _preparePage(navigateTo) {
@ -155,8 +159,8 @@ export default class MessagesPuppeteer {
} else { } else {
await this.page.reload() await this.page.reload()
} }
this.log("Injecting content script") this.log.info("Injecting content script")
await this.page.addScriptTag({ path: "./src/contentscript.js", type: "module" }) await this.page.addScriptTag({path: "./src/contentscript.js", type: "module"})
} }
async _interactWithPage(promiser) { async _interactWithPage(promiser) {
@ -164,7 +168,7 @@ export default class MessagesPuppeteer {
try { try {
await promiser() await promiser()
} catch (e) { } catch (e) {
this.error(`Error while interacting with page: ${e}`) this.log.error(`Error while interacting with page: ${e}`)
throw e throw e
} finally { } finally {
await this.blankPage.bringToFront() await this.blankPage.bringToFront()
@ -180,7 +184,7 @@ export default class MessagesPuppeteer {
if (numTries && --numTries == 0) { if (numTries && --numTries == 0) {
throw e throw e
} else if (failMessage) { } else if (failMessage) {
this.log(failMessage) this.log.error(failMessage)
} }
} }
} }
@ -211,51 +215,51 @@ export default class MessagesPuppeteer {
const loginContentArea = await this.page.waitForSelector("#login_content") const loginContentArea = await this.page.waitForSelector("#login_content")
switch (login_type) { switch (login_type) {
case "qr": { case "qr": {
this.log("Running QR login") this.dlog.info("Running QR login")
const qrButton = await this.page.waitForSelector("#login_qr_btn") const qrButton = await this.page.waitForSelector("#login_qr_btn")
await qrButton.click() await qrButton.click()
const qrElement = await this.page.waitForSelector("#login_qrcode_area div[title]", {visible: true}) const qrElement = await this.page.waitForSelector("#login_qrcode_area div[title]", {visible: true})
const currentQR = await this.page.evaluate(element => element.title, qrElement) const currentQR = await this.page.evaluate(element => element.title, qrElement)
this._receiveQRChange(currentQR) this._receiveQRChange(currentQR)
await this.page.evaluate( await this.page.evaluate(
element => window.__mautrixController.addQRChangeObserver(element), qrElement) element => window.__mautrixController.addQRChangeObserver(element), qrElement)
await this.page.evaluate( await this.page.evaluate(
element => window.__mautrixController.addQRAppearObserver(element), loginContentArea) element => window.__mautrixController.addQRAppearObserver(element), loginContentArea)
break break
}
case "email": {
this.log("Running email login")
if (!login_data) {
this._sendLoginFailure("No login credentials provided for email login")
return
} }
case "email": {
this.dlog.info("Running email login")
if (!login_data) {
this._sendLoginFailure("No login credentials provided for email login")
return
}
const emailButton = await this.page.waitForSelector("#login_email_btn") const emailButton = await this.page.waitForSelector("#login_email_btn")
await emailButton.click() await emailButton.click()
await this.page.waitForSelector("#login_email_area", {visible: true}) await this.page.waitForSelector("#login_email_area", {visible: true})
this.login_email = login_data["email"] this.login_email = login_data["email"]
this.login_password = login_data["password"] this.login_password = login_data["password"]
await this._sendEmailCredentials() await this._sendEmailCredentials()
await this.page.evaluate( await this.page.evaluate(
element => window.__mautrixController.addEmailAppearObserver(element), loginContentArea) element => window.__mautrixController.addEmailAppearObserver(element), loginContentArea)
break break
} }
default: default:
this._sendLoginFailure(`Invalid login type: ${login_type}`) this._sendLoginFailure(`Invalid login type: ${login_type}`)
return return
} }
await this.page.evaluate( await this.page.evaluate(
element => window.__mautrixController.addPINAppearObserver(element), loginContentArea) element => window.__mautrixController.addPINAppearObserver(element), loginContentArea)
this.log("Waiting for login response") this.dlog.info("Waiting for login response")
let doneWaiting = false let doneWaiting = false
let loginSuccess = false let loginSuccess = false
const cancelableResolve = (promiseFn) => { const cancelableResolve = (promiseFn) => {
@ -289,7 +293,7 @@ export default class MessagesPuppeteer {
].map(promiseFn => cancelableResolve(promiseFn))) ].map(promiseFn => cancelableResolve(promiseFn)))
if (!this.loginCancelled) { if (!this.loginCancelled) {
this.log("Removing observers") this.dlog.info("Removing observers")
// TODO __mautrixController is undefined when cancelling, why? // TODO __mautrixController is undefined when cancelling, why?
await this.page.evaluate(ownID => window.__mautrixController.setOwnID(ownID), this.ownID) await this.page.evaluate(ownID => window.__mautrixController.setOwnID(ownID), this.ownID)
await this.page.evaluate(() => window.__mautrixController.removeQRChangeObserver()) await this.page.evaluate(() => window.__mautrixController.removeQRChangeObserver())
@ -309,28 +313,28 @@ export default class MessagesPuppeteer {
} }
this._sendLoginSuccess() this._sendLoginSuccess()
this.log("Waiting for sync") this.dlog.info("Waiting for sync")
try { try {
await this.page.waitForFunction( await this.page.waitForFunction(
messageSyncElement => { messageSyncElement => {
const text = messageSyncElement.innerText const text = messageSyncElement.innerText
return text.startsWith("Syncing messages...") return text.startsWith("Syncing messages...")
&& (text.endsWith("100%") || text.endsWith("NaN%")) && (text.endsWith("100%") || text.endsWith("NaN%"))
// TODO Sometimes it gets stuck at 99%...?? // TODO Sometimes it gets stuck at 99%...??
}, },
{timeout: 10000}, // Assume 10 seconds is long enough {timeout: 10000}, // Assume 10 seconds is long enough
messageSyncElement) messageSyncElement)
} catch (err) { } catch (err) {
//this._sendLoginFailure(`Failed to sync: ${err}`) //this._sendLoginFailure(`Failed to sync: ${err}`)
this.log("LINE's sync took too long, assume it's fine and carry on...") this.dlog.warn("LINE's sync took too long, assume it's fine and carry on...")
} finally { } finally {
const syncText = await messageSyncElement.evaluate(e => e.innerText) const syncText = await messageSyncElement.evaluate(e => e.innerText)
this.log(`Final sync text is: "${syncText}"`) this.dlog.info(`Final sync text is: "${syncText}"`)
} }
this.loginRunning = false this.loginRunning = false
await this.blankPage.bringToFront() await this.blankPage.bringToFront()
this.log("Login complete") this.dlog.info("Login complete")
} }
/** /**
@ -366,7 +370,7 @@ export default class MessagesPuppeteer {
if (this.browser) { if (this.browser) {
await this.browser.close() await this.browser.close()
} }
this.log("Everything stopped") this.log.info("Everything stopped")
} }
/** /**
@ -494,7 +498,7 @@ export default class MessagesPuppeteer {
* @return {Promise<{id: number}>} - The ID of the sent message. * @return {Promise<{id: number}>} - The ID of the sent message.
*/ */
async sendMessage(chatID, text) { async sendMessage(chatID, text) {
return { id: await this.taskQueue.push(() => this._sendMessageUnsafe(chatID, text)) } return {id: await this.taskQueue.push(() => this._sendMessageUnsafe(chatID, text))}
} }
/** /**
@ -512,14 +516,14 @@ export default class MessagesPuppeteer {
for (const [chatID, messageID] of Object.entries(msgIDs)) { for (const [chatID, messageID] of Object.entries(msgIDs)) {
this.mostRecentMessages.set(chatID, messageID) this.mostRecentMessages.set(chatID, messageID)
} }
this.log("Updated most recent message ID map:") this.dlog.info("Updated most recent message ID map:")
this.log(JSON.stringify(msgIDs)) this.dlog.debug(JSON.stringify(msgIDs))
for (const [chatID, messageID] of Object.entries(ownMsgIDs)) { for (const [chatID, messageID] of Object.entries(ownMsgIDs)) {
this.mostRecentOwnMessages.set(chatID, messageID) this.mostRecentOwnMessages.set(chatID, messageID)
} }
this.log("Updated most recent own message ID map:") this.dlog.info("Updated most recent own message ID map:")
this.log(JSON.stringify(ownMsgIDs)) this.dlog.debug(JSON.stringify(ownMsgIDs))
this.mostRecentReceipts.clear() this.mostRecentReceipts.clear()
for (const [chatID, receipts] of Object.entries(rctIDs)) { for (const [chatID, receipts] of Object.entries(rctIDs)) {
@ -528,11 +532,11 @@ export default class MessagesPuppeteer {
receiptMap.set(+count, receiptID) receiptMap.set(+count, receiptID)
} }
} }
this.log("Updated most recent receipt ID map") this.dlog.info("Updated most recent receipt ID map")
for (const [chatID, receiptMap] of this.mostRecentReceipts) { for (const [chatID, receiptMap] of this.mostRecentReceipts) {
this.log(`${chatID}:`) this.dlog.debug(`${chatID}:`)
for (const [count, receiptID] of receiptMap) { for (const [count, receiptID] of receiptMap) {
this.log(`Read by ${count}: ${receiptID}`) this.dlog.debug(`Read by ${count}: ${receiptID}`)
} }
} }
} }
@ -552,7 +556,7 @@ export default class MessagesPuppeteer {
} }
async sendFile(chatID, filePath) { async sendFile(chatID, filePath) {
return { id: await this.taskQueue.push(() => this._sendFileUnsafe(chatID, filePath)) } return {id: await this.taskQueue.push(() => this._sendFileUnsafe(chatID, filePath))}
} }
_cycleTimerStart() { _cycleTimerStart() {
@ -562,7 +566,7 @@ export default class MessagesPuppeteer {
} }
async _cycleChatUnsafe() { async _cycleChatUnsafe() {
this.log("Cycling chats") this.dlog.info("Cycling chats")
const initialID = this.cycleTimerID const initialID = this.cycleTimerID
const currentChatID = await this.page.evaluate(() => window.__mautrixController.getCurrentChatID()) const currentChatID = await this.page.evaluate(() => window.__mautrixController.getCurrentChatID())
@ -575,7 +579,7 @@ export default class MessagesPuppeteer {
// - the most recently-sent own message is not fully read // - the most recently-sent own message is not fully read
let chatIDToSync let chatIDToSync
for (let i = 0, n = chatList.length; i < n; i++) { for (let i = 0, n = chatList.length; i < n; i++) {
const chatListItem = chatList[(i+offset) % n] const chatListItem = chatList[(i + offset) % n]
if (chatListItem.notificationCount > 0) { if (chatListItem.notificationCount > 0) {
// Chat has unread notifications, so don't view it // Chat has unread notifications, so don't view it
@ -601,13 +605,13 @@ export default class MessagesPuppeteer {
} }
chatIDToSync = chatListItem.id chatIDToSync = chatListItem.id
this.log(`Viewing chat ${chatIDToSync} to check for new read receipts`) this.dlog.debug(`Viewing chat ${chatIDToSync} to check for new read receipts`)
await this._syncChat(chatIDToSync) await this._syncChat(chatIDToSync)
break break
} }
if (!chatIDToSync) { if (!chatIDToSync) {
this.log("Found no chats in need of read receipt updates") this.dlog.debug("Found no chats in need of read receipt updates")
} }
if (this.cycleTimerID == initialID) { if (this.cycleTimerID == initialID) {
@ -624,27 +628,27 @@ export default class MessagesPuppeteer {
} }
_jiggleMouse() { _jiggleMouse() {
this.log("Jiggling mouse") this.spammer.info("Jiggling mouse")
const initialID = this.jiggleTimerID const initialID = this.jiggleTimerID
exec(`xdotool mousemove --sync --window ${this.windowID} 0 0`, {}, exec(`xdotool mousemove --sync --window ${this.windowID} 0 0`, {},
(error, stdout, stderr) => { (error, stdout, stderr) => {
if (error) { if (error) {
this.log(`Error while jiggling mouse: ${error}`) this.spammer.error(`Error while jiggling mouse: ${error}`)
} else { } else {
this.log("Jiggled mouse") this.spammer.debug("Jiggled mouse")
} }
if (this.jiggleTimerID == initialID) { if (this.jiggleTimerID == initialID) {
this._jiggleTimerStart() this._jiggleTimerStart()
} }
}) })
} }
async startObserving() { async startObserving() {
// TODO Highly consider syncing anything that was missed since stopObserving... // TODO Highly consider syncing anything that was missed since stopObserving...
const chatID = await this.page.evaluate(() => window.__mautrixController.getCurrentChatID()) const chatID = await this.page.evaluate(() => window.__mautrixController.getCurrentChatID())
this.log(`Adding observers for ${chatID || "empty chat"}, and global timers`) this.dlog.info(`Adding observers for ${chatID || "empty chat"}, and global timers`)
await this.page.evaluate( await this.page.evaluate(
() => window.__mautrixController.addChatListObserver()) () => window.__mautrixController.addChatListObserver())
if (chatID) { if (chatID) {
@ -662,7 +666,7 @@ export default class MessagesPuppeteer {
} }
async stopObserving() { async stopObserving() {
this.log("Removing observers and timers") this.dlog.info("Removing observers and timers")
await this.page.evaluate( await this.page.evaluate(
() => window.__mautrixController.removeChatListObserver()) () => window.__mautrixController.removeChatListObserver())
await this.page.evaluate( await this.page.evaluate(
@ -687,12 +691,12 @@ export default class MessagesPuppeteer {
// Best to use this on startup when no chat is viewed. // Best to use this on startup when no chat is viewed.
let ownProfile let ownProfile
await this._interactWithPage(async () => { await this._interactWithPage(async () => {
this.log("Opening settings view") this.dlog.info("Opening settings view")
await this.page.click("button.mdGHD01SettingBtn") await this.page.click("button.mdGHD01SettingBtn")
await this.page.waitForSelector("#context_menu li#settings", {visible: true}).then(e => e.click()) await this.page.waitForSelector("#context_menu li#settings", {visible: true}).then(e => e.click())
await this.page.waitForSelector("#settings_contents", {visible: true}) await this.page.waitForSelector("#settings_contents", {visible: true})
this.log("Getting own profile info") this.dlog.info("Getting own profile info")
ownProfile = { ownProfile = {
id: this.ownID, id: this.ownID,
name: await this.page.$eval("#settings_basic_name_input", e => e.innerText), name: await this.page.$eval("#settings_basic_name_input", e => e.innerText),
@ -727,7 +731,7 @@ export default class MessagesPuppeteer {
async _switchChat(chatID, forceView = false) { async _switchChat(chatID, forceView = false) {
// TODO Allow passing in an element directly // TODO Allow passing in an element directly
this.log(`Switching to chat ${chatID}`) this.dlog.info(`Switching to chat ${chatID}`)
let chatItem = await this.page.$(this._chatItemSelector(chatID)) let chatItem = await this.page.$(this._chatItemSelector(chatID))
let chatName let chatName
@ -745,23 +749,23 @@ export default class MessagesPuppeteer {
if (!!chatItem && await this.page.evaluate(isCorrectChatVisible, chatName)) { if (!!chatItem && await this.page.evaluate(isCorrectChatVisible, chatName)) {
if (!forceView) { if (!forceView) {
this.log("Already viewing chat, no need to switch") this.dlog.debug("Already viewing chat, no need to switch")
} else { } else {
await this._interactWithPage(async () => { await this._interactWithPage(async () => {
this.log("Already viewing chat, but got request to view it") this.dlog.debug("Already viewing chat, but got request to view it")
this.page.waitForTimeout(500) this.page.waitForTimeout(500)
}) })
} }
} else { } else {
this.log("Ensuring msg list observer is removed") this.dlog.info("Ensuring msg list observer is removed")
const hadMsgListObserver = await this.page.evaluate( const hadMsgListObserver = await this.page.evaluate(
() => window.__mautrixController.removeMsgListObserver()) () => window.__mautrixController.removeMsgListObserver())
this.log(hadMsgListObserver ? "Observer was already removed" : "Removed observer") this.dlog.info(hadMsgListObserver ? "Observer was already removed" : "Removed observer")
let switchedTabs = false let switchedTabs = false
let needRealClick = false let needRealClick = false
if (!chatItem) { if (!chatItem) {
this.log(`Chat ${chatID} not in recents list`) this.dlog.info(`Chat ${chatID} not in recents list`)
if (chatID.charAt(0) != "u") { if (chatID.charAt(0) != "u") {
needRealClick = true needRealClick = true
@ -784,13 +788,13 @@ export default class MessagesPuppeteer {
// HTML of friend/group item titles ever diverge // HTML of friend/group item titles ever diverge
chatName = await chatItem.evaluate( chatName = await chatItem.evaluate(
chatID.charAt(0) == "u" chatID.charAt(0) == "u"
? element => window.__mautrixController.getFriendsListItemName(element) ? element => window.__mautrixController.getFriendsListItemName(element)
: element => window.__mautrixController.getGroupListItemName(element)) : element => window.__mautrixController.getGroupListItemName(element))
} }
await this._retryUntilSuccess(3, "Clicking chat item didn't work...try again", await this._retryUntilSuccess(3, "Clicking chat item didn't work...try again",
async () => { async () => {
this.log("Clicking chat item") this.dlog.info("Clicking chat item")
if (!needRealClick) { if (!needRealClick) {
await chatItem.evaluate(e => e.click()) await chatItem.evaluate(e => e.click())
} else { } else {
@ -798,7 +802,7 @@ export default class MessagesPuppeteer {
await chatItem.click() await chatItem.click()
}) })
} }
this.log(`Waiting for chat header title to be "${chatName}"`) this.dlog.debug(`Waiting for chat header title to be "${chatName}"`)
await this.page.waitForFunction( await this.page.waitForFunction(
isCorrectChatVisible, isCorrectChatVisible,
{polling: "mutation", timeout: 1000}, {polling: "mutation", timeout: 1000},
@ -810,7 +814,7 @@ export default class MessagesPuppeteer {
await this._interactWithPage(async () => { await this._interactWithPage(async () => {
// Always show the chat details sidebar, as this makes life easier // Always show the chat details sidebar, as this makes life easier
this.log("Waiting for detail area to be auto-hidden upon entering chat") this.dlog.debug("Waiting for detail area to be auto-hidden upon entering chat")
await this.page.waitForFunction( await this.page.waitForFunction(
detailArea => detailArea.childElementCount == 0, detailArea => detailArea.childElementCount == 0,
{}, {},
@ -818,47 +822,47 @@ export default class MessagesPuppeteer {
await this._retryUntilSuccess(3, "Clicking chat header didn't work...try again", await this._retryUntilSuccess(3, "Clicking chat header didn't work...try again",
async () => { async () => {
this.log("Clicking chat header to show detail area") this.dlog.debug("Clicking chat header to show detail area")
await this.page.click("#_chat_header_area > .mdRGT04Link") await this.page.click("#_chat_header_area > .mdRGT04Link")
this.log("Waiting for detail area") this.dlog.debug("Waiting for detail area")
await this.page.waitForSelector("#_chat_detail_area > .mdRGT02Info", {timeout: 1000}) await this.page.waitForSelector("#_chat_detail_area > .mdRGT02Info", {timeout: 1000})
}) })
}) })
this.log("Waiting for any item to appear in chat") this.dlog.debug("Waiting for any item to appear in chat")
try { try {
await this.page.waitForSelector("#_chat_room_msg_list div", {timeout: 2000}) await this.page.waitForSelector("#_chat_room_msg_list div", {timeout: 2000})
this.log("Waiting for chat to stabilize") this.dlog.debug("Waiting for chat to stabilize")
await this.page.evaluate(() => window.__mautrixController.waitForMessageListStability()) await this.page.evaluate(() => window.__mautrixController.waitForMessageListStability())
} catch (e) { } catch (e) {
this.log("No messages in chat found. Maybe no messages were ever sent yet?") this.dlog.warn("No messages in chat found. Maybe no messages were ever sent yet?")
} }
if (hadMsgListObserver) { if (hadMsgListObserver) {
this.log("Restoring msg list observer") this.dlog.debug("Restoring msg list observer")
await this.page.evaluate( await this.page.evaluate(
(mostRecentMessage) => window.__mautrixController.addMsgListObserver(mostRecentMessage), (mostRecentMessage) => window.__mautrixController.addMsgListObserver(mostRecentMessage),
this.mostRecentMessages.get(chatID)) this.mostRecentMessages.get(chatID))
} else { } else {
this.log("Not restoring msg list observer, as there never was one") this.dlog.debug("Not restoring msg list observer, as there never was one")
} }
} }
} }
async _getChatInfoUnsafe(chatID, forceView) { async _getChatInfoUnsafe(chatID, forceView) {
// TODO Commonize this // TODO Commonize this
let [isDirect, isGroup, isRoom] = [false,false,false] let [isDirect, isGroup, isRoom] = [false, false, false]
switch (chatID.charAt(0)) { switch (chatID.charAt(0)) {
case "u": case "u":
isDirect = true isDirect = true
break break
case "c": case "c":
isGroup = true isGroup = true
break break
case "r": case "r":
isRoom = true isRoom = true
break break
} }
const chatListItem = await this.page.$(this._chatItemSelector(chatID)) const chatListItem = await this.page.$(this._chatItemSelector(chatID))
@ -872,7 +876,7 @@ export default class MessagesPuppeteer {
(element, chatID) => window.__mautrixController.parseFriendsListItem(element, chatID), (element, chatID) => window.__mautrixController.parseFriendsListItem(element, chatID),
chatID) chatID)
this.log(`Found NEW direct chat with ${chatID}`) this.dlog.info(`Found NEW direct chat with ${chatID}`)
return { return {
participants: [friendsListInfo], participants: [friendsListInfo],
id: chatID, id: chatID,
@ -893,7 +897,7 @@ export default class MessagesPuppeteer {
let participants let participants
if (!isDirect) { if (!isDirect) {
this.log("Found multi-user chat, so viewing it to get participants") this.dlog.info("Found multi-user chat, so viewing it to get participants")
// TODO This will mark the chat as "read"! // TODO This will mark the chat as "read"!
await this._switchChat(chatID, forceView) await this._switchChat(chatID, forceView)
const participantList = await this.page.$("#_chat_detail_area > .mdRGT02Info ul.mdRGT13Ul") const participantList = await this.page.$("#_chat_detail_area > .mdRGT02Info ul.mdRGT13Ul")
@ -902,9 +906,9 @@ export default class MessagesPuppeteer {
participants = await participantList.evaluate( participants = await participantList.evaluate(
element => window.__mautrixController.parseParticipantList(element)) element => window.__mautrixController.parseParticipantList(element))
} else { } else {
this.log(`Found direct chat with ${chatID}`) this.dlog.info(`Found direct chat with ${chatID}`)
if (forceView) { if (forceView) {
this.log("Viewing chat on request") this.dlog.info("Viewing chat on request")
await this._switchChat(chatID, forceView) await this._switchChat(chatID, forceView)
} }
//const chatDetailArea = await this.page.waitForSelector("#_chat_detail_area > .mdRGT02Info") //const chatDetailArea = await this.page.waitForSelector("#_chat_detail_area > .mdRGT02Info")
@ -916,9 +920,9 @@ export default class MessagesPuppeteer {
}] }]
} }
this.log("Found participants:") this.dlog.info("Found participants:")
for (const participant of participants) { for (const participant of participants) {
this.log(JSON.stringify(participant)) this.dlog.debug(JSON.stringify(participant))
} }
return {participants, ...chatListInfo} return {participants, ...chatListInfo}
} }
@ -965,16 +969,16 @@ export default class MessagesPuppeteer {
try { try {
this._interactWithPage(async () => { this._interactWithPage(async () => {
this.log(`About to ask for file chooser in ${chatID}`) this.dlog.info(`About to ask for file chooser in ${chatID}`)
const [fileChooser] = await Promise.all([ const [fileChooser] = await Promise.all([
this.page.waitForFileChooser(), this.page.waitForFileChooser(),
this.page.click("#_chat_room_plus_btn") this.page.click("#_chat_room_plus_btn")
]) ])
this.log(`About to upload ${filePath}`) this.dlog.info(`About to upload ${filePath}`)
await fileChooser.accept([filePath]) await fileChooser.accept([filePath])
}) })
} catch (e) { } catch (e) {
this.log(`Failed to upload file to ${chatID}`) this.dlog.error(`Failed to upload file to ${chatID}`)
return -1 return -1
} }
@ -983,16 +987,16 @@ export default class MessagesPuppeteer {
async _waitForSentMessage(chatID) { async _waitForSentMessage(chatID) {
try { try {
this.log("Waiting for message to be sent") this.dlog.debug("Waiting for message to be sent")
const id = await this.page.evaluate( const id = await this.page.evaluate(
() => window.__mautrixController.waitForOwnMessage()) () => window.__mautrixController.waitForOwnMessage())
this.log(`Successfully sent message ${id} to ${chatID}`) this.dlog.debug(`Successfully sent message ${id} to ${chatID}`)
this.mostRecentMessages.set(chatID, id) this.mostRecentMessages.set(chatID, id)
this.mostRecentOwnMessages.set(chatID, id) this.mostRecentOwnMessages.set(chatID, id)
return id return id
} catch (e) { } catch (e) {
// TODO Catch if something other than a timeout // TODO Catch if something other than a timeout
this.error(`Timed out sending message to ${chatID}`) this.dlog.error(`Timed out sending message to ${chatID}`)
// TODO Figure out why e is undefined... // TODO Figure out why e is undefined...
//this.error(e) //this.error(e)
return -1 return -1
@ -1006,19 +1010,19 @@ export default class MessagesPuppeteer {
if (this.client) { if (this.client) {
for (const message of messages) { for (const message of messages) {
this.client.sendMessage(message).catch(err => this.client.sendMessage(message).catch(err =>
this.error("Failed to send message", message.id, "to client:", err)) this.dlog.error("Failed to send message", message.id, "to client:", err))
} }
} else { } else {
this.log("No client connected, not sending messages") this.dlog.info("No client connected, not sending messages")
} }
} }
async _getMessagesUnsafe(chatID) { async _getMessagesUnsafe(chatID) {
// TODO Consider making a wrapper for pausing/resuming the msg list observers // TODO Consider making a wrapper for pausing/resuming the msg list observers
this.log("Ensuring msg list observer is removed") this.dlog.info("Ensuring msg list observer is removed")
const hadMsgListObserver = await this.page.evaluate( const hadMsgListObserver = await this.page.evaluate(
() => window.__mautrixController.removeMsgListObserver()) () => window.__mautrixController.removeMsgListObserver())
this.log(hadMsgListObserver ? "Observer was already removed" : "Removed observer") this.dlog.info(hadMsgListObserver ? "Observer was already removed" : "Removed observer")
// TODO Handle unloaded messages. Maybe scroll up // TODO Handle unloaded messages. Maybe scroll up
// TODO This will mark the chat as "read"! // TODO This will mark the chat as "read"!
@ -1039,14 +1043,14 @@ export default class MessagesPuppeteer {
// Sync receipts seen from newly-synced messages // Sync receipts seen from newly-synced messages
// TODO When user leaves, clear the read-by count for the old number of other participants // TODO When user leaves, clear the read-by count for the old number of other participants
let minCountToFind = 1 let minCountToFind = 1
for (let i = messages.length-1; i >= 0; i--) { for (let i = messages.length - 1; i >= 0; i--) {
const message = messages[i] const message = messages[i]
if (!message.is_outgoing) { if (!message.is_outgoing) {
continue continue
} }
const count = message.receipt_count const count = message.receipt_count
if (count >= minCountToFind && message.id > (receiptMap.get(count) || 0)) { if (count >= minCountToFind && message.id > (receiptMap.get(count) || 0)) {
minCountToFind = count+1 minCountToFind = count + 1
receiptMap.set(count, message.id) receiptMap.set(count, message.id)
} }
// TODO Early exit when count == num other participants // TODO Early exit when count == num other participants
@ -1065,12 +1069,12 @@ export default class MessagesPuppeteer {
if (hadMsgListObserver) { if (hadMsgListObserver) {
this.log("Restoring msg list observer") this.dlog.info("Restoring msg list observer")
await this.page.evaluate( await this.page.evaluate(
mostRecentMessage => window.__mautrixController.addMsgListObserver(mostRecentMessage), mostRecentMessage => window.__mautrixController.addMsgListObserver(mostRecentMessage),
this.mostRecentMessages.get(chatID)) this.mostRecentMessages.get(chatID))
} else { } else {
this.log("Not restoring msg list observer, as there never was one") this.dlog.info("Not restoring msg list observer, as there never was one")
} }
return { return {
@ -1089,7 +1093,7 @@ export default class MessagesPuppeteer {
const newLastID = filteredMessages[filteredMessages.length - 1].id const newLastID = filteredMessages[filteredMessages.length - 1].id
this.mostRecentMessages.set(chatID, newLastID) this.mostRecentMessages.set(chatID, newLastID)
const range = newFirstID === newLastID ? newFirstID : `${newFirstID}-${newLastID}` const range = newFirstID === newLastID ? newFirstID : `${newFirstID}-${newLastID}`
this.log(`Loaded ${messages.length} messages in ${chatID}, got ${filteredMessages.length} newer than ${minID} (${range})`) this.dlog.info(`Loaded ${messages.length} messages in ${chatID}, got ${filteredMessages.length} newer than ${minID} (${range})`)
for (const message of filteredMessages) { for (const message of filteredMessages) {
message.chat_id = chatID message.chat_id = chatID
} }
@ -1130,14 +1134,14 @@ export default class MessagesPuppeteer {
async _processChatListChangeUnsafe(chatListInfo) { async _processChatListChangeUnsafe(chatListInfo) {
const chatID = chatListInfo.id const chatID = chatListInfo.id
this.updatedChats.delete(chatID) this.updatedChats.delete(chatID)
this.log("Processing change to", chatID) this.dlog.info("Processing change to", chatID)
// TODO Also process name/icon changes // TODO Also process name/icon changes
const prevNumNotifications = this.numChatNotifications.get(chatID) || 0 const prevNumNotifications = this.numChatNotifications.get(chatID) || 0
const diffNumNotifications = chatListInfo.notificationCount - prevNumNotifications const diffNumNotifications = chatListInfo.notificationCount - prevNumNotifications
if (chatListInfo.notificationCount == 0 && diffNumNotifications < 0) { if (chatListInfo.notificationCount == 0 && diffNumNotifications < 0) {
this.log("Notifications dropped--must have read messages from another LINE client, skip") this.dlog.info("Notifications dropped--must have read messages from another LINE client, skip")
this.numChatNotifications.set(chatID, 0) this.numChatNotifications.set(chatID, 0)
return return
} }
@ -1146,12 +1150,12 @@ export default class MessagesPuppeteer {
// If >1, a notification was missed. Only way to get them is to view the chat. // 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. // 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. // To play it safe, just sync them. Should be no harm, as they're viewed already.
diffNumNotifications != 1 diffNumNotifications != 1
// Without placeholders, some messages require visiting their chat to be synced. // Without placeholders, some messages require visiting their chat to be synced.
|| !this.sendPlaceholders || !this.sendPlaceholders
&& ( && (
// Can only use previews for DMs, because sender can't be found otherwise! // Can only use previews for DMs, because sender can't be found otherwise!
chatListInfo.id.charAt(0) != 'u' chatListInfo.id.charAt(0) != 'u'
// Sync when lastMsg is a canned message for a non-previewable message type. // Sync when lastMsg is a canned message for a non-previewable message type.
|| chatListInfo.lastMsg.endsWith(" sent a photo.") || chatListInfo.lastMsg.endsWith(" sent a photo.")
|| chatListInfo.lastMsg.endsWith(" sent a sticker.") || chatListInfo.lastMsg.endsWith(" sent a sticker.")
@ -1181,25 +1185,25 @@ export default class MessagesPuppeteer {
const {messages, receipts} = await this._getMessagesUnsafe(chatID) const {messages, receipts} = await this._getMessagesUnsafe(chatID)
if (messages.length == 0) { if (messages.length == 0) {
this.log("No new messages found in", chatID) this.dlog.info("No new messages found in", chatID)
} else { } else {
this._receiveMessages(chatID, messages, true) this._receiveMessages(chatID, messages, true)
} }
if (receipts.length == 0) { if (receipts.length == 0) {
this.log("No new receipts found in", chatID) this.dlog.info("No new receipts found in", chatID)
} else { } else {
this._receiveReceiptMulti(chatID, receipts, true) this._receiveReceiptMulti(chatID, receipts, true)
} }
} }
_receiveChatListChanges(changes) { _receiveChatListChanges(changes) {
this.log(`Received chat list changes: ${changes.map(item => item.id)}`) this.dlog.info(`Received chat list changes: ${changes.map(item => item.id)}`)
for (const item of changes) { for (const item of changes) {
if (!this.updatedChats.has(item.id)) { if (!this.updatedChats.has(item.id)) {
this.updatedChats.add(item.id) this.updatedChats.add(item.id)
this.taskQueue.push(() => this._processChatListChangeUnsafe(item)) this.taskQueue.push(() => this._processChatListChangeUnsafe(item))
.catch(err => this.error("Error handling chat list changes:", err)) .catch(err => this.dlog.error("Error handling chat list changes:", err))
} }
} }
} }
@ -1208,17 +1212,17 @@ export default class MessagesPuppeteer {
const receiptMap = this._getReceiptMap(chat_id) const receiptMap = this._getReceiptMap(chat_id)
const prevReceiptID = (receiptMap.get(1) || 0) const prevReceiptID = (receiptMap.get(1) || 0)
if (receipt_id <= prevReceiptID) { if (receipt_id <= prevReceiptID) {
this.log(`Received OUTDATED read receipt ${receipt_id} (older than ${prevReceiptID}) for chat ${chat_id}`) this.dlog.info(`Received OUTDATED read receipt ${receipt_id} (older than ${prevReceiptID}) for chat ${chat_id}`)
return return
} }
receiptMap.set(1, receipt_id) receiptMap.set(1, receipt_id)
this.log(`Received read receipt ${receipt_id} (since ${prevReceiptID}) for chat ${chat_id}`) this.dlog.info(`Received read receipt ${receipt_id} (since ${prevReceiptID}) for chat ${chat_id}`)
if (this.client) { if (this.client) {
this.client.sendReceipt({chat_id: chat_id, id: receipt_id}) this.client.sendReceipt({chat_id: chat_id, id: receipt_id})
.catch(err => this.error("Error handling read receipt:", err)) .catch(err => this.dlog.error("Error handling read receipt:", err))
} else { } else {
this.log("No client connected, not sending receipts") this.dlog.info("No client connected, not sending receipts")
} }
} }
@ -1236,29 +1240,29 @@ export default class MessagesPuppeteer {
} }
}) })
if (receipts.length == 0) { if (receipts.length == 0) {
this.log(`Received ALL OUTDATED bulk read receipts for chat ${chat_id}:`, receipts) this.dlog.info(`Received ALL OUTDATED bulk read receipts for chat ${chat_id}:`, receipts)
return return
} }
this._trimReceiptMap(receiptMap) this._trimReceiptMap(receiptMap)
} }
this.log(`Received bulk read receipts for chat ${chat_id}:`, receipts) this.dlog.info(`Received bulk read receipts for chat ${chat_id}:`, receipts)
if (this.client) { if (this.client) {
for (const receipt of receipts) { for (const receipt of receipts) {
receipt.chat_id = chat_id receipt.chat_id = chat_id
try { try {
await this.client.sendReceipt(receipt) await this.client.sendReceipt(receipt)
} catch(err) { } catch (err) {
this.error("Error handling read receipt:", err) this.dlog.error("Error handling read receipt:", err)
} }
} }
} else { } else {
this.log("No client connected, not sending receipts") this.dlog.info("No client connected, not sending receipts")
} }
} }
async _sendEmailCredentials() { async _sendEmailCredentials() {
this.log("Inputting login credentials") this.dlog.info("Inputting login credentials")
await this._enterText(await this.page.$("#line_login_email"), this.login_email) await this._enterText(await this.page.$("#line_login_email"), this.login_email)
await this._enterText(await this.page.$("#line_login_pwd"), this.login_password) await this._enterText(await this.page.$("#line_login_pwd"), this.login_password)
await this.page.click("button#login_btn") await this.page.click("button#login_btn")
@ -1267,51 +1271,51 @@ export default class MessagesPuppeteer {
_receiveQRChange(url) { _receiveQRChange(url) {
if (this.client) { if (this.client) {
this.client.sendQRCode(url).catch(err => this.client.sendQRCode(url).catch(err =>
this.error("Failed to send new QR to client:", err)) this.dlog.error("Failed to send new QR to client:", err))
} else { } else {
this.log("No client connected, not sending new QR") this.dlog.warn("No client connected, not sending new QR")
} }
} }
_receivePIN(pin) { _receivePIN(pin) {
if (this.client) { if (this.client) {
this.client.sendPIN(pin).catch(err => this.client.sendPIN(pin).catch(err =>
this.error("Failed to send new PIN to client:", err)) this.dlog.error("Failed to send new PIN to client:", err))
} else { } else {
this.log("No client connected, not sending new PIN") this.dlog.warn("No client connected, not sending new PIN")
} }
} }
_sendLoginSuccess() { _sendLoginSuccess() {
this.error("Login success") this.dlog.info("Login success")
if (this.client) { if (this.client) {
this.client.sendLoginSuccess().catch(err => this.client.sendLoginSuccess().catch(err =>
this.error("Failed to send login success to client:", err)) this.dlog.error("Failed to send login success to client:", err))
} else { } else {
this.log("No client connected, not sending login success") this.dlog.warn("No client connected, not sending login success")
} }
} }
_sendLoginFailure(reason) { _sendLoginFailure(reason) {
this.loginRunning = false this.loginRunning = false
this.error(`Login failure: ${reason ? reason : "cancelled"}`) this.dlog.error(`Login failure: ${reason ? reason : "cancelled"}`)
if (this.client) { if (this.client) {
this.client.sendLoginFailure(reason).catch(err => this.client.sendLoginFailure(reason).catch(err =>
this.error("Failed to send login failure to client:", err)) this.dlog.error("Failed to send login failure to client:", err))
} else { } else {
this.log("No client connected, not sending login failure") this.dlog.warn("No client connected, not sending login failure")
} }
} }
_onLoggedOut() { _onLoggedOut() {
this.log("Got logged out!") this.dlog.info("Got logged out!")
this.stopObserving() this.stopObserving()
this.page.bringToFront() this.page.bringToFront()
if (this.client) { if (this.client) {
this.client.sendLoggedOut().catch(err => this.client.sendLoggedOut().catch(err =>
this.error("Failed to send logout notice to client:", err)) this.dlog.error("Failed to send logout notice to client:", err))
} else { } else {
this.log("No client connected, not sending logout notice") this.dlog.warn("No client connected, not sending logout notice")
} }
} }
} }

View File

@ -13,6 +13,7 @@
// //
// You should have received a copy of the GNU Affero General Public License // 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/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
import logger from "loglevel"
export default class TaskQueue { export default class TaskQueue {
constructor(id) { constructor(id) {
@ -20,21 +21,15 @@ export default class TaskQueue {
this._tasks = [] this._tasks = []
this.running = false this.running = false
this._wakeup = null this._wakeup = null
} this.log = logger.getLogger(`TaskQueue/${this.id}`)
this.log.setLevel(logger.getLogger("TaskQueue").getLevel())
log(...text) {
console.log(`[TaskQueue/${this.id}]`, ...text)
}
error(...text) {
console.error(`[TaskQueue/${this.id}]`, ...text)
} }
async _run() { async _run() {
this.log("Started processing tasks") this.log.info("Started processing tasks")
while (this.running) { while (this.running) {
if (this._tasks.length === 0) { if (this._tasks.length === 0) {
this.log("Sleeping until a new task is received") this.log.debug("Sleeping until a new task is received")
await new Promise(resolve => this._wakeup = () => { await new Promise(resolve => this._wakeup = () => {
resolve() resolve()
this._wakeup = null this._wakeup = null
@ -42,12 +37,12 @@ export default class TaskQueue {
if (!this.running) { if (!this.running) {
break break
} }
this.log("Continuing processing tasks") this.log.debug("Continuing processing tasks")
} }
const { task, resolve, reject } = this._tasks.shift() const { task, resolve, reject } = this._tasks.shift()
await task().then(resolve, reject) await task().then(resolve, reject)
} }
this.log("Stopped processing tasks") this.log.info("Stopped processing tasks")
} }
/** /**
@ -79,7 +74,7 @@ export default class TaskQueue {
return return
} }
this.running = true this.running = true
this._run().catch(err => this.error("Fatal error processing tasks:", err)) this._run().catch(err => this.log.error("Fatal error processing tasks:", err))
} }
/** /**

View File

@ -932,6 +932,16 @@ lodash@^4.17.14, lodash@^4.17.19:
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52"
integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA== integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==
loglevel-plugin-prefix@^0.8.4:
version "0.8.4"
resolved "https://registry.yarnpkg.com/loglevel-plugin-prefix/-/loglevel-plugin-prefix-0.8.4.tgz#2fe0e05f1a820317d98d8c123e634c1bd84ff644"
integrity sha512-WpG9CcFAOjz/FtNht+QJeGpvVl/cdR6P0z6OcXSkr8wFJOsV2GRj2j10JLfjuA4aYkcKCNIEqRGCyTife9R8/g==
loglevel@^1.7.1:
version "1.7.1"
resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.7.1.tgz#005fde2f5e6e47068f935ff28573e125ef72f197"
integrity sha512-Hesni4s5UkWkwCGJMQGAh71PaLUmKFM60dHvq0zi/vDhhrzuk+4GgNbTXJ12YYQJn6ZKBDNIjYcuQGKudvqrIw==
minimatch@^3.0.4: minimatch@^3.0.4:
version "3.0.4" version "3.0.4"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"