Jiggle mouse with xdotool to keep LINE alive

Even if Chrome is the only window in an X session, and even if the
window is focused, the LINE extension stops getting new messages unless
the mouse has moved recently. To work around that, use xdotool to move
the mouse every so often.
This commit is contained in:
Andrew Ferrazzutti 2021-07-01 01:49:43 -04:00
parent 915e3daa06
commit e1b822bd52
5 changed files with 61 additions and 9 deletions

View File

@ -1,6 +1,7 @@
# Minimum Requirements # Minimum Requirements
* Python 3.8 * Python 3.8
* Node 10.18.1 * Node 10.18.1
* xdotool
# Initial setup # Initial setup
## Puppeteer module ## Puppeteer module

View File

@ -12,5 +12,13 @@ The `profile_dir` specifies which directory to put Chromium user data directorie
### Extension directory ### Extension directory
The `extension_dir` specifies which directory contains the files for the LINE extension, which you must download yourself. The `extension_dir` specifies which directory contains the files for the LINE extension, which you must download yourself.
### Cycle delay
`cycle_delay` specifies the period (in milliseconds) at which Puppeteer should view chats to check on their read receipts. Only chats with messages that haven't been fully read need to be checked.
### `xdotool`
Set `use_xdotool` to `true` to allow the Node process to manipulate the mouse cursor of the X server it runs in. Requires the `xdotool` utility to be installed. Highly recommended, especially when running in a virtual X server. Its default value is `false` so that running in a non-virtual X server won't interfere with a real mouse cursor.
`jiggle_delay` specifies the period (in milliseconds) between "jiggling" the mouse cursor (necessary to keep the LINE extension active). Only relevant when `use_xdotool` is `true`.
### DevTools ### DevTools
Set `devtools` to `true` to launch Chromium with DevTools enabled by default. Set `devtools` to `true` to launch Chromium with DevTools enabled by default.

View File

@ -6,5 +6,8 @@
"profile_dir": "./profiles", "profile_dir": "./profiles",
"url": "chrome-extension://<extension-uuid>/index.html", "url": "chrome-extension://<extension-uuid>/index.html",
"extension_dir": "./extension_files", "extension_dir": "./extension_files",
"cycle_delay": 5000,
"use_xdotool": false,
"jiggle_delay": 20000,
"devtools": false "devtools": false
} }

View File

@ -39,6 +39,9 @@ MessagesPuppeteer.profileDir = config.profile_dir || MessagesPuppeteer.profileDi
MessagesPuppeteer.devtools = config.devtools || false MessagesPuppeteer.devtools = config.devtools || false
MessagesPuppeteer.url = config.url MessagesPuppeteer.url = config.url
MessagesPuppeteer.extensionDir = config.extension_dir || MessagesPuppeteer.extensionDir MessagesPuppeteer.extensionDir = config.extension_dir || MessagesPuppeteer.extensionDir
MessagesPuppeteer.cycleDelay = config.cycle_delay || MessagesPuppeteer.cycleDelay
MessagesPuppeteer.useXdotool = config.use_xdotool || MessagesPuppeteer.useXdotool
MessagesPuppeteer.jiggleDelay = config.jiggle_delay || MessagesPuppeteer.jiggleDelay
const api = new PuppetAPI(config.listen) const api = new PuppetAPI(config.listen)

View File

@ -15,6 +15,7 @@
// 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 puppeteer from "puppeteer" import puppeteer from "puppeteer"
import chrono from "chrono-node" import chrono from "chrono-node"
@ -25,6 +26,9 @@ import { sleep } from "./util.js"
export default class MessagesPuppeteer { export default class MessagesPuppeteer {
static profileDir = "./profiles" static profileDir = "./profiles"
static executablePath = undefined static executablePath = undefined
static cycleDelay = 5000
static useXdotool = false
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 }
@ -43,6 +47,7 @@ export default class MessagesPuppeteer {
} }
this.id = id this.id = id
this.ownID = ownID this.ownID = ownID
this.windowID = null
this.sendPlaceholders = sendPlaceholders this.sendPlaceholders = sendPlaceholders
this.profilePath = profilePath this.profilePath = profilePath
this.updatedChats = new Set() this.updatedChats = new Set()
@ -51,6 +56,7 @@ export default class MessagesPuppeteer {
this.mostRecentReceipts = new Map() this.mostRecentReceipts = new Map()
this.numChatNotifications = new Map() this.numChatNotifications = new Map()
this.cycleTimerID = null this.cycleTimerID = null
this.jiggleTimerID = null
this.taskQueue = new TaskQueue(this.id) this.taskQueue = new TaskQueue(this.id)
this.client = client this.client = client
} }
@ -99,7 +105,14 @@ export default class MessagesPuppeteer {
this.blankPage = await this.browser.newPage() this.blankPage = await this.browser.newPage()
await this.page.bringToFront() await this.page.bringToFront()
this.log("Opening", MessagesPuppeteer.url) if (MessagesPuppeteer.useXdotool) {
this.log("Finding window ID")
const buffer = execSync("xdotool search 'about:blank'")
this.windowID = Number.parseInt(buffer)
this.log(`Found window ID ${this.windowID}`)
}
this.log(`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)
@ -472,10 +485,9 @@ export default class MessagesPuppeteer {
} }
_cycleTimerStart() { _cycleTimerStart() {
// TODO Config for cycle delay
this.cycleTimerID = setTimeout( this.cycleTimerID = setTimeout(
() => this.taskQueue.push(() => this._cycleChatUnsafe()), () => this.taskQueue.push(() => this._cycleChatUnsafe()),
5000) MessagesPuppeteer.cycleDelay)
} }
async _cycleChatUnsafe() { async _cycleChatUnsafe() {
@ -515,21 +527,39 @@ export default class MessagesPuppeteer {
} }
chatIDToSync = chatListItem.id chatIDToSync = chatListItem.id
this.log(`Viewing chat ${chatIDToSync} to check for new read receipts`)
await this._syncChat(chatIDToSync)
break break
} }
if (!chatIDToSync) { if (!chatIDToSync) {
// TODO Confirm if this actually works...! this.log("Found no chats in need of read receipt updates")
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() this._cycleTimerStart()
} }
/**
* Jiggle the mouse periodically.
* Have to do this to keep the LINE extension "awake". Ridiculous, but necessary...
*/
_jiggleTimerStart() {
this.jiggleTimerID = setTimeout(() => this._jiggleMouse(), MessagesPuppeteer.jiggleDelay)
}
_jiggleMouse() {
this.log("Jiggling mouse")
exec(`xdotool mousemove --sync --window ${this.windowID} 0 0`, {},
(error, stdout, stderr) => {
if (error) {
this.log(`Error while jiggling mouse: ${error}`)
} else {
this.log("Jiggled mouse")
}
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())
@ -545,6 +575,9 @@ export default class MessagesPuppeteer {
if (this.cycleTimerID == null) { if (this.cycleTimerID == null) {
this._cycleTimerStart() this._cycleTimerStart()
} }
if (MessagesPuppeteer.useXdotool && this.jiggleTimerID == null) {
this._jiggleTimerStart()
}
} }
async stopObserving() { async stopObserving() {
@ -558,6 +591,10 @@ export default class MessagesPuppeteer {
clearTimeout(this.cycleTimerID) clearTimeout(this.cycleTimerID)
this.cycleTimerID = null this.cycleTimerID = null
} }
if (this.jiggleTimerID != null) {
clearTimeout(this.jiggleTimerID)
this.jiggleTimerID = null
}
} }
async getOwnProfile() { async getOwnProfile() {