matrix-puppeteer-line/puppet/src/client.js
Andrew Ferrazzutti 555b19c289 Always use LINE puppet for own messages
...that are sent from another client.

Also look up the profile data for the user's LINE account on sync,
including at startup, so that there's always a puppet available.
2021-06-15 02:55:55 -04:00

282 lines
7.7 KiB
JavaScript

// matrix-puppeteer-line - A very hacky Matrix-LINE bridge based on running LINE's Chrome extension in Puppeteer
// Copyright (C) 2020-2021 Tulir Asokan, Andrew Ferrazzutti
//
// 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 MessagesPuppeteer from "./puppet.js"
import { emitLines, promisify } from "./util.js"
export default class Client {
/**
* @param {PuppetAPI} manager
* @param {import("net").Socket} socket
* @param {number} connID
* @param {?string} [userID]
* @param {?MessagesPuppeteer} [puppet]
*/
constructor(manager, socket, connID, userID = null, puppet = null) {
this.manager = manager
this.socket = socket
this.connID = connID
this.userID = userID
this.puppet = puppet
this.stopped = false
this.notificationID = 0
this.maxCommandID = 0
}
log(...text) {
if (this.userID) {
console.log(`[API/${this.userID}/${this.connID}]`, ...text)
} else {
console.log(`[API/${this.connID}]`, ...text)
}
}
error(...text) {
if (this.userID) {
console.error(`[API/${this.userID}/${this.connID}]`, ...text)
} else {
console.error(`[API/${this.connID}]`, ...text)
}
}
start() {
this.log("Received connection", this.connID)
emitLines(this.socket)
this.socket.on("line", line => this.handleLine(line)
.catch(err => this.log("Error handling line:", err)))
this.socket.on("end", this.handleEnd)
setTimeout(() => {
if (!this.userID && !this.stopped) {
this.log("Didn't receive register request within 3 seconds, terminating")
this.stop("Register request timeout")
}
}, 3000)
}
async stop(error = null) {
if (this.stopped) {
return
}
this.stopped = true
try {
await this._write({ id: --this.notificationID, command: "quit", error })
await promisify(cb => this.socket.end(cb))
} catch (err) {
this.error("Failed to end connection:", err)
this.socket.destroy(err)
}
}
handleEnd = () => {
this.stopped = true
if (this.userID && this.manager.clients.get(this.userID) === this) {
this.manager.clients.delete(this.userID)
}
this.log(`Connection closed (user: ${this.userID})`)
}
/**
* Write JSON data to the socket.
*
* @param {object} data - The data to write.
* @return {Promise<void>}
*/
_write(data) {
return promisify(cb => this.socket.write(JSON.stringify(data) + "\n", cb))
}
sendMessage(message) {
this.log(`Sending message ${message.id} to client`)
return this._write({
id: --this.notificationID,
command: "message",
is_sequential: true,
message,
})
}
sendReceipt(receipt) {
this.log(`Sending read receipt (${receipt.count || "DM"}) of msg ${receipt.id} for chat ${receipt.chat_id}`)
return this._write({
id: --this.notificationID,
command: "receipt",
receipt
})
}
sendQRCode(url) {
this.log(`Sending QR ${url} to client`)
return this._write({
id: --this.notificationID,
command: "qr",
url,
})
}
sendPIN(pin) {
this.log(`Sending PIN ${pin} to client`)
return this._write({
id: --this.notificationID,
command: "pin",
pin,
})
}
sendLoginSuccess() {
this.log("Sending login success to client")
return this._write({
id: --this.notificationID,
command: "login_success",
})
}
sendLoginFailure(reason) {
this.log(`Sending login failure to client${reason ? `: "${reason}"` : ""}`)
return this._write({
id: --this.notificationID,
command: "login_failure",
reason,
})
}
sendLoggedOut() {
this.log("Sending logout notice to client")
return this._write({
id: --this.notificationID,
command: "logged_out",
})
}
handleStart = async (req) => {
let started = false
if (this.puppet === null) {
this.log("Opening new puppeteer for", this.userID)
this.puppet = new MessagesPuppeteer(this.userID, this.ownID, this)
this.manager.puppets.set(this.userID, this.puppet)
await this.puppet.start(!!req.debug)
started = true
}
return {
started,
is_logged_in: await this.puppet.isLoggedIn(),
is_connected: !await this.puppet.isDisconnected(),
is_permanently_disconnected: await this.puppet.isPermanentlyDisconnected(),
}
}
handleStop = async () => {
if (this.puppet === null) {
return { stopped: false }
}
this.log("Closing puppeteer for", this.userID)
this.manager.puppets.delete(this.userID)
await this.puppet.stop()
this.puppet = null
return { stopped: true }
}
handleUnknownCommand = () => {
throw new Error("Unknown command")
}
handleRegister = async (req) => {
this.userID = req.user_id
this.ownID = req.own_id
this.log(`Registered socket ${this.connID} -> ${this.userID}`)
if (this.manager.clients.has(this.userID)) {
const oldClient = this.manager.clients.get(this.userID)
this.manager.clients.set(this.userID, this)
this.log(`Terminating previous socket ${oldClient.connID} for ${this.userID}`)
await oldClient.stop("Socket replaced by new connection")
} else {
this.manager.clients.set(this.userID, this)
}
this.puppet = this.manager.puppets.get(this.userID) || null
if (this.puppet) {
this.puppet.client = this
}
return { client_exists: this.puppet !== null }
}
async handleLine(line) {
if (this.stopped) {
this.log("Ignoring line, client is stopped")
return
}
let req
try {
req = JSON.parse(line)
} catch (err) {
this.log("Non-JSON request:", line)
return
}
if (!req.command || !req.id) {
this.log("Invalid request:", line)
return
}
if (req.id <= this.maxCommandID) {
this.log("Ignoring old request", req.id)
return
}
if (req.command != "is_connected") {
this.log("Received request", req.id, "with command", req.command)
}
this.maxCommandID = req.id
let handler
if (!this.userID) {
if (req.command !== "register") {
this.log("First request wasn't a register request, terminating")
await this.stop("Invalid first request")
return
} else if (!req.user_id) {
this.log("Register request didn't contain user ID, terminating")
await this.stop("Invalid register request")
return
}
handler = this.handleRegister
} else {
handler = {
start: this.handleStart,
stop: this.handleStop,
disconnect: () => this.stop(),
login: req => this.puppet.waitForLogin(req.login_type, req.login_data),
cancel_login: () => this.puppet.cancelLogin(),
send: req => this.puppet.sendMessage(req.chat_id, req.text),
send_file: req => this.puppet.sendFile(req.chat_id, req.file_path),
set_last_message_ids: req => this.puppet.setLastMessageIDs(req.msg_ids),
pause: () => this.puppet.stopObserving(),
resume: () => this.puppet.startObserving(),
get_own_profile: () => this.puppet.getOwnProfile(),
get_chats: () => this.puppet.getRecentChats(),
get_chat: req => this.puppet.getChatInfo(req.chat_id),
get_messages: req => this.puppet.getMessages(req.chat_id),
read_image: req => this.puppet.readImage(req.image_url),
is_connected: async () => ({ is_connected: !await this.puppet.isDisconnected() }),
}[req.command] || this.handleUnknownCommand
}
const resp = { id: req.id }
try {
resp.command = "response"
resp.response = await handler(req)
} catch (err) {
resp.command = "error"
resp.error = err.toString()
this.log("Error handling request", req.id, err)
}
await this._write(resp)
}
}