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
* Python 3.8
* Node 10.18.1
* xdotool
# Initial setup
## Puppeteer module

View File

@ -12,5 +12,13 @@ The `profile_dir` specifies which directory to put Chromium user data directorie
### Extension directory
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
Set `devtools` to `true` to launch Chromium with DevTools enabled by default.

View File

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

View File

@ -39,6 +39,9 @@ MessagesPuppeteer.profileDir = config.profile_dir || MessagesPuppeteer.profileDi
MessagesPuppeteer.devtools = config.devtools || false
MessagesPuppeteer.url = config.url
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)

View File

@ -15,6 +15,7 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
import process from "process"
import path from "path"
import { exec, execSync } from "child_process"
import puppeteer from "puppeteer"
import chrono from "chrono-node"
@ -25,6 +26,9 @@ import { sleep } from "./util.js"
export default class MessagesPuppeteer {
static profileDir = "./profiles"
static executablePath = undefined
static cycleDelay = 5000
static useXdotool = false
static jiggleDelay = 30000
static devtools = false
static noSandbox = false
static viewport = { width: 960, height: 840 }
@ -43,6 +47,7 @@ export default class MessagesPuppeteer {
}
this.id = id
this.ownID = ownID
this.windowID = null
this.sendPlaceholders = sendPlaceholders
this.profilePath = profilePath
this.updatedChats = new Set()
@ -51,6 +56,7 @@ export default class MessagesPuppeteer {
this.mostRecentReceipts = new Map()
this.numChatNotifications = new Map()
this.cycleTimerID = null
this.jiggleTimerID = null
this.taskQueue = new TaskQueue(this.id)
this.client = client
}
@ -99,7 +105,14 @@ export default class MessagesPuppeteer {
this.blankPage = await this.browser.newPage()
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._preparePage(true)
@ -472,10 +485,9 @@ export default class MessagesPuppeteer {
}
_cycleTimerStart() {
// TODO Config for cycle delay
this.cycleTimerID = setTimeout(
() => this.taskQueue.push(() => this._cycleChatUnsafe()),
5000)
MessagesPuppeteer.cycleDelay)
}
async _cycleChatUnsafe() {
@ -515,21 +527,39 @@ export default class MessagesPuppeteer {
}
chatIDToSync = chatListItem.id
this.log(`Viewing chat ${chatIDToSync} to check for new read receipts`)
await this._syncChat(chatIDToSync)
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.log("Found no chats in need of read receipt updates")
}
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() {
// TODO Highly consider syncing anything that was missed since stopObserving...
const chatID = await this.page.evaluate(() => window.__mautrixController.getCurrentChatID())
@ -545,6 +575,9 @@ export default class MessagesPuppeteer {
if (this.cycleTimerID == null) {
this._cycleTimerStart()
}
if (MessagesPuppeteer.useXdotool && this.jiggleTimerID == null) {
this._jiggleTimerStart()
}
}
async stopObserving() {
@ -558,6 +591,10 @@ export default class MessagesPuppeteer {
clearTimeout(this.cycleTimerID)
this.cycleTimerID = null
}
if (this.jiggleTimerID != null) {
clearTimeout(this.jiggleTimerID)
this.jiggleTimerID = null
}
}
async getOwnProfile() {