More changes

This commit is contained in:
Andrew Ferrazzutti 2021-02-10 02:34:19 -05:00
parent 63dc7f0be3
commit 85814f9793
17 changed files with 535 additions and 143 deletions

1
.gitignore vendored
View File

@ -11,6 +11,7 @@ __pycache__
/.eggs
profiles
puppet/extension_files
/config.yaml
/registration.yaml

View File

@ -13,13 +13,13 @@
#
# 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/>.
from typing import Optional
from typing import Optional, AsyncGenerator, Tuple
import io
import qrcode
import PIL as _
from mautrix.types import MediaMessageEventContent, MessageType, ImageInfo, EventID
from mautrix.types import TextMessageEventContent, MediaMessageEventContent, MessageType, ImageInfo, EventID
from mautrix.bridge.commands import HelpSection, command_handler
from .typehint import CommandEvent
@ -27,29 +27,91 @@ from .typehint import CommandEvent
SECTION_AUTH = HelpSection("Authentication", 10, "")
@command_handler(needs_auth=False, management_only=True, help_section=SECTION_AUTH,
help_text="Log into Android Messages")
async def login(evt: CommandEvent) -> None:
async def login_prep(evt: CommandEvent, login_type: str) -> bool:
status = await evt.sender.client.start()
if status.is_logged_in:
await evt.reply("You're already logged in")
return
qr_event_id: Optional[EventID] = None
async for url in evt.sender.client.login():
buffer = io.BytesIO()
image = qrcode.make(url)
size = image.pixel_size
image.save(buffer, "PNG")
qr = buffer.getvalue()
mxc = await evt.az.intent.upload_media(qr, "image/png", "login-qr.png", len(qr))
content = MediaMessageEventContent(body=url, url=mxc, msgtype=MessageType.IMAGE,
info=ImageInfo(mimetype="image/png", size=len(qr),
width=size, height=size))
if qr_event_id:
content.set_edit(qr_event_id)
await evt.az.intent.send_message(evt.room_id, content)
return False
if evt.sender.command_status is not None:
action = evt.sender.command_status["action"]
if action == "Login":
await evt.reply(
"A login is already in progress. Please follow the login instructions, "
"or use the `$cmdprefix+sp cancel` command to start over.")
else:
content.set_reply(evt.event_id)
qr_event_id = await evt.az.intent.send_message(evt.room_id, content)
await evt.reply("Successfully logged in, now syncing")
await evt.sender.sync()
await evt.reply(f"Cannot login while a {action} command is active.")
return False
evt.sender.command_status = {
"action": "Login",
"login_type": login_type,
}
return True
async def login_do(evt: CommandEvent, gen: AsyncGenerator[Tuple[str, str], None]) -> None:
qr_event_id: Optional[EventID] = None
pin_event_id: Optional[EventID] = None
failure = False
async for item in gen:
if item[0] == "qr":
url = item[1]
buffer = io.BytesIO()
image = qrcode.make(url)
size = image.pixel_size
image.save(buffer, "PNG")
qr = buffer.getvalue()
mxc = await evt.az.intent.upload_media(qr, "image/png", "login-qr.png", len(qr))
content = MediaMessageEventContent(body=url, url=mxc, msgtype=MessageType.IMAGE,
info=ImageInfo(mimetype="image/png", size=len(qr),
width=size, height=size))
if qr_event_id:
content.set_edit(qr_event_id)
await evt.az.intent.send_message(evt.room_id, content)
else:
content.set_reply(evt.event_id)
qr_event_id = await evt.az.intent.send_message(evt.room_id, content)
elif item[0] == "pin":
pin = item[1]
content = TextMessageEventContent(body=pin, msgtype=MessageType.NOTICE)
if pin_event_id:
content.set_edit(pin_event_id)
await evt.az.intent.send_message(evt.room_id, content)
else:
content.set_reply(evt.event_id)
pin_event_id = await evt.az.intent.send_message(evt.room_id, content)
elif item[0] in ("failure", "error"):
# TODO Handle errors differently?
reason = item[1]
failure = True
content = TextMessageEventContent(body=reason, msgtype=MessageType.NOTICE)
await evt.az.intent.send_message(evt.room_id, content)
# else: pass
if not failure and evt.sender.command_status:
await evt.reply("Successfully logged in, now syncing")
await evt.sender.sync()
# else command was cancelled or failed. Don't post message about it, "cancel" command or failure did already
evt.sender.command_status = None
@command_handler(needs_auth=False, management_only=True, help_section=SECTION_AUTH,
help_text="Log into LINE via QR code")
async def login_qr(evt: CommandEvent) -> None:
if not await login_prep(evt, "qr"):
return
gen = evt.sender.client.login(evt.sender)
await login_do(evt, gen)
@command_handler(needs_auth=False, management_only=True, help_section=SECTION_AUTH,
help_text="Log into LINE via email/password",
help_args="<_email_> <_password_>")
async def login_email(evt: CommandEvent) -> None:
if len(evt.args) != 2:
await evt.reply("Usage: `$cmdprefix+sp login <email> <password>`")
return
if not await login_prep(evt, "email"):
return
gen = evt.sender.client.login(
evt.sender,
login_data=dict(email=evt.args[0], password=evt.args[1]))
await login_do(evt, gen)

View File

@ -29,7 +29,7 @@ async def set_notice_room(evt: CommandEvent) -> None:
@command_handler(needs_auth=False, management_only=True, help_section=SECTION_CONNECTION,
help_text="Check if you're logged into Android Messages")
help_text="Check if you're logged into LINE")
async def ping(evt: CommandEvent) -> None:
status = await evt.sender.client.start()
if status.is_logged_in:

View File

@ -39,18 +39,18 @@ appservice:
shared_secret: generate
# The unique ID of this appservice.
id: amp
id: line
# Username of the appservice bot.
bot_username: ampbot
bot_username: linebot
# Display name and avatar for bot. Set to "remove" to remove display name/avatar, leave empty
# to leave display name/avatar as-is.
bot_displayname: Android Messages bridge bot
bot_displayname: LINE bridge bot
bot_avatar: mxc://maunium.net/VuvevQiMRlOxuBVMBNEZZrxi
# Community ID for bridged users (changes registration file) and rooms.
# Must be created manually.
#
# Example: "+amp:example.com". Set to false to disable.
# Example: "+line:example.com". Set to false to disable.
community_id: false
# Authentication tokens for AS <-> HS communication. Autogenerated; do not modify.
@ -66,11 +66,11 @@ metrics:
bridge:
# Localpart template of MXIDs for remote users.
# {userid} is replaced with the user ID (phone or name converted into a mxid-friendly format).
username_template: "amp_{userid}"
username_template: "line_{userid}"
# Displayname template for remote users.
# {displayname} is replaced with the display name of the user.
# {phone} is replaced with the phone number or name of the user.
displayname_template: "{displayname} (SMS)"
displayname_template: "{displayname} (LINE)"
# Maximum length of displayname
displayname_max_length: 100
@ -129,7 +129,7 @@ bridge:
resend_bridge_info: false
# The prefix for commands. Only required in non-management rooms.
command_prefix: "!am"
command_prefix: "!line"
# This bridge only supports a single user
user: "@admin:example.com"

View File

@ -47,4 +47,4 @@ class MatrixHandler(BaseMatrixHandler):
inviter.notice_room = room_id
await inviter.update()
await self.az.intent.send_notice(room_id, "This room has been marked as your "
"Android Messages bridge notice room.")
"LINE bridge notice room.")

View File

@ -268,7 +268,7 @@ class Portal(DBPortal, BasePortal):
@property
def bridge_info_state_key(self) -> str:
return f"net.maunium.amp://androidmessages/{self.chat_id}"
return f"net.maunium.line://line/{self.chat_id}"
@property
def bridge_info(self) -> Dict[str, Any]:
@ -276,8 +276,8 @@ class Portal(DBPortal, BasePortal):
"bridgebot": self.az.bot_mxid,
"creator": self.main_intent.mxid,
"protocol": {
"id": "androidmessages",
"displayname": "Android Messages",
"id": "line",
"displayname": "LINE",
"avatar_url": self.config["appservice.bot_avatar"],
},
"channel": {

View File

@ -56,8 +56,9 @@ class Puppet(DBPuppet, BasePuppet):
cls.mxid_template = SimpleTemplate(cls.config["bridge.username_template"], "userid",
prefix="@", suffix=f":{cls.hs_domain}", type=str)
secret = cls.config["bridge.login_shared_secret"]
cls.login_shared_secret_map[cls.hs_domain] = secret.encode("utf-8") if secret else None
cls.login_device_name = "Android Messages Bridge"
if secret:
cls.login_shared_secret_map[cls.hs_domain] = secret.encode("utf-8")
cls.login_device_name = "LINE Bridge"
async def update_info(self, info: Participant) -> None:
update = False

View File

@ -13,20 +13,17 @@
#
# 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/>.
from typing import AsyncGenerator, TypedDict, List, Dict, Callable, Awaitable, Any
from typing import AsyncGenerator, TypedDict, List, Tuple, Dict, Callable, Awaitable, Any
from collections import deque
import asyncio
from .rpc import RPCClient
from .types import ChatListInfo, ChatInfo, Message, StartStatus
from mautrix_line.rpc.types import RPCError
class QRCommand(TypedDict):
url: str
class LoginComplete(Exception):
pass
class LoginCommand(TypedDict):
content: str
class Client(RPCClient):
@ -66,22 +63,48 @@ class Client(RPCClient):
self.add_event_handler("message", wrapper)
async def login(self) -> AsyncGenerator[str, None]:
# TODO Type hint for sender
async def login(self, sender, **login_data) -> AsyncGenerator[Tuple[str, str], None]:
login_data["login_type"] = sender.command_status["login_type"]
data = deque()
event = asyncio.Event()
async def qr_handler(req: QRCommand) -> None:
data.append(req["url"])
async def qr_handler(req: LoginCommand) -> None:
data.append(("qr", req["url"]))
event.set()
async def pin_handler(req: LoginCommand) -> None:
data.append(("pin", req["pin"]))
event.set()
async def failure_handler(req: LoginCommand) -> None:
data.append(("failure", req["reason"]))
event.set()
async def cancel_watcher() -> None:
try:
while sender.command_status is not None:
await asyncio.sleep(1)
await self._raw_request("cancel_login")
except asyncio.CancelledError:
pass
cancel_watcher_task = asyncio.create_task(cancel_watcher())
def login_handler(_fut: asyncio.Future) -> None:
cancel_watcher_task.cancel()
e = _fut.exception()
if e is not None:
data.append(("error", str(e)))
data.append(None)
event.set()
login_future = await self._raw_request("login")
login_future = await self._raw_request("login", **login_data)
login_future.add_done_callback(login_handler)
self.add_event_handler("qr", qr_handler)
self.add_event_handler("pin", pin_handler)
self.add_event_handler("failure", failure_handler)
try:
while True:
await event.wait()
@ -93,3 +116,5 @@ class Client(RPCClient):
event.clear()
finally:
self.remove_event_handler("qr", qr_handler)
self.remove_event_handler("pin", pin_handler)
self.remove_event_handler("failure", failure_handler)

View File

@ -30,7 +30,7 @@ from . import puppet as pu, portal as po
if TYPE_CHECKING:
from .__main__ import MessagesBridge
METRIC_CONNECTED = Gauge("bridge_connected", "Users connected to Android Messages")
METRIC_CONNECTED = Gauge("bridge_connected", "Users connected to LINE")
class User(DBUser, BaseUser):
@ -49,6 +49,7 @@ class User(DBUser, BaseUser):
def __init__(self, mxid: UserID, notice_room: Optional[RoomID] = None) -> None:
super().__init__(mxid=mxid, notice_room=notice_room)
self._notice_room_lock = asyncio.Lock()
self.command_status = None
self.is_whitelisted = self.is_admin = self.config["bridge.user"] == mxid
self.log = self.log.getChild(self.mxid)
self._metric_value = defaultdict(lambda: False)

View File

@ -64,8 +64,8 @@ class ProvisioningAPI:
return None
for part in auth_parts:
part = part.strip()
if part.startswith("net.maunium.amp.auth-"):
return part[len("net.maunium.amp.auth-"):]
if part.startswith("net.maunium.line.auth-"):
return part[len("net.maunium.line.auth-"):]
return None
def check_token(self, request: web.Request) -> Awaitable['u.User']:
@ -94,7 +94,7 @@ class ProvisioningAPI:
user = await self.check_token(request)
data = {
"mxid": user.mxid,
"amp": {
"line": {
"connected": True,
} if await user.is_logged_in() else None,
}
@ -107,7 +107,7 @@ class ProvisioningAPI:
if status.is_logged_in:
raise web.HTTPConflict(text='{"error": "Already logged in"}', headers=self._headers)
ws = web.WebSocketResponse(protocols=["net.maunium.amp.login"])
ws = web.WebSocketResponse(protocols=["net.maunium.line.login"])
await ws.prepare(request)
try:
async for url in user.client.login():

View File

@ -4,5 +4,6 @@
"path": "/var/run/mautrix-line/puppet.sock"
},
"profile_dir": "./profiles",
"url": "chrome-extension://<extension-uuid>/index.html"
"url": "chrome-extension://<extension-uuid>/index.html",
"extension_dir": "./extension_files"
}

View File

@ -17,7 +17,7 @@
"dependencies": {
"arg": "^4.1.3",
"chrono-node": "^2.1.7",
"puppeteer": "5.1.0"
"puppeteer": "5.5.0"
},
"devDependencies": {
"babel-eslint": "^10.1.0",

View File

@ -116,6 +116,24 @@ export default class Client {
})
}
sendPIN(pin) {
this.log(`Sending PIN ${pin} to client`)
return this._write({
id: --this.notificationID,
command: "pin",
pin,
})
}
sendFailure(reason) {
this.log(`Sending failure "${reason}" to client`)
return this._write({
id: --this.notificationID,
command: "failure",
reason,
})
}
handleStart = async (req) => {
let started = false
if (this.puppet === null) {
@ -205,7 +223,8 @@ export default class Client {
start: this.handleStart,
stop: this.handleStop,
disconnect: () => this.stop(),
login: () => this.puppet.waitForLogin(),
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),
set_last_message_ids: req => this.puppet.setLastMessageIDs(req.msg_ids),
get_chats: () => this.puppet.getRecentChats(),

View File

@ -32,6 +32,20 @@ window.__mautrixReceiveChanges = function (changes) {}
* @return {Promise<void>}
*/
window.__mautrixReceiveQR = function (url) {}
/**
* @return {Promise<void>}
*/
window.__mautrixSendEmailCredentials = function () {}
/**
* @param {string} pin - The login PIN.
* @return {Promise<void>}
*/
window.__mautrixReceivePIN = function (pin) {}
/**
* @param {Element} button - The button to click when a QR code or PIN expires.
* @return {Promise<void>}
*/
window.__mautrixExpiry = function (button) {}
/**
* @param {number} id - The ID of the message that was sent
* @return {Promise<void>}
@ -41,7 +55,11 @@ window.__mautrixReceiveMessageID = function(id) {}
class MautrixController {
constructor() {
this.chatListObserver = null
this.qrCodeObserver = null
this.qrChangeObserver = null
this.qrAppearObserver = null
this.emailAppearObserver = null
this.pinAppearObserver = null
this.expiryObserver = null
}
/**
@ -312,6 +330,7 @@ class MautrixController {
if (this.chatListObserver !== null) {
this.removeChatListObserver()
}
/* TODO
this.chatListObserver = new MutationObserver(mutations => {
try {
this._observeChatListMutations(mutations)
@ -320,6 +339,7 @@ class MautrixController {
}
})
this.chatListObserver.observe(element, { childList: true, subtree: true })
*/
console.debug("Started chat list observer")
}
@ -334,27 +354,132 @@ class MautrixController {
}
}
addQRObserver(element) {
if (this.qrCodeObserver !== null) {
this.removeQRObserver()
addQRChangeObserver(element) {
if (this.qrChangeObserver !== null) {
this.removeQRChangeObserver()
}
this.qrCodeObserver = new MutationObserver(changes => {
this.qrChangeObserver = new MutationObserver(changes => {
for (const change of changes) {
if (change.attributeName === "data-qr-code" && change.target instanceof Element) {
window.__mautrixReceiveQR(change.target.getAttribute("data-qr-code"))
if (change.attributeName === "title" && change.target instanceof Element) {
window.__mautrixReceiveQR(change.target.getAttribute("title"))
}
}
})
this.qrCodeObserver.observe(element, {
this.qrChangeObserver.observe(element, {
attributes: true,
attributeFilter: ["data-qr-code"],
attributeFilter: ["title"],
})
}
removeQRObserver() {
if (this.qrCodeObserver !== null) {
this.qrCodeObserver.disconnect()
this.qrCodeObserver = null
removeQRChangeObserver() {
if (this.qrChangeObserver !== null) {
this.qrChangeObserver.disconnect()
this.qrChangeObserver = null
}
}
addQRAppearObserver(element) {
if (this.qrAppearObserver !== null) {
this.removeQRAppearObserver()
}
this.qrAppearObserver = new MutationObserver(changes => {
for (const change of changes) {
for (const node of change.addedNodes) {
const qrElement = node.querySelector("#login_qrcode_area div[title]")
if (qrElement) {
window.__mautrixReceiveQR(qrElement.title)
window.__mautrixController.addQRChangeObserver(element)
return
}
}
}
})
this.qrAppearObserver.observe(element, {
childList: true,
})
}
removeQRAppearObserver() {
if (this.qrAppearObserver !== null) {
this.qrAppearObserver.disconnect()
this.qrAppearObserver = null
}
}
addEmailAppearObserver(element, login_type) {
if (this.emailAppearObserver !== null) {
this.removeEmailAppearObserver()
}
this.emailAppearObserver = new MutationObserver(changes => {
for (const change of changes) {
for (const node of change.addedNodes) {
const emailElement = node.querySelector("#login_email_btn")
if (emailElement) {
window.__mautrixSendEmailCredentials()
return
}
}
}
})
this.emailAppearObserver.observe(element, {
childList: true,
})
}
removeEmailAppearObserver() {
if (this.emailAppearObserver !== null) {
this.emailAppearObserver.disconnect()
this.emailAppearObserver = null
}
}
addPINAppearObserver(element, login_type) {
if (this.pinAppearObserver !== null) {
this.removePINAppearObserver()
}
this.pinAppearObserver = new MutationObserver(changes => {
for (const change of changes) {
for (const node of change.addedNodes) {
const pinElement = node.querySelector("div.mdCMN01Code")
if (pinElement) {
window.__mautrixReceivePIN(pinElement.innerText)
return
}
}
}
})
this.pinAppearObserver.observe(element, {
childList: true,
})
}
removePINAppearObserver() {
if (this.pinAppearObserver !== null) {
this.pinAppearObserver.disconnect()
this.pinAppearObserver = null
}
}
addExpiryObserver(element) {
if (this.expiryObserver !== null) {
this.removeExpiryObserver()
}
const button = element.querySelector("dialog button")
this.expiryObserver = new MutationObserver(changes => {
if (changes.length == 1 && !changes[0].target.getAttribute("class").includes("MdNonDisp")) {
window.__mautrixExpiry(button)
}
})
this.expiryObserver.observe(element, {
attributes: true,
attributeFilter: ["class"],
})
}
removeExpiryObserver() {
if (this.expiryObserver !== null) {
this.expiryObserver.disconnect()
this.expiryObserver = null
}
}
}

View File

@ -38,6 +38,7 @@ const config = JSON.parse(fs.readFileSync(configPath).toString())
MessagesPuppeteer.profileDir = config.profile_dir || MessagesPuppeteer.profileDir
MessagesPuppeteer.disableDebug = !!config.disable_debug
MessagesPuppeteer.url = config.url
MessagesPuppeteer.extensionDir = config.extension_dir || MessagesPuppeteer.extensionDir
const api = new PuppetAPI(config.listen)

View File

@ -27,8 +27,9 @@ export default class MessagesPuppeteer {
static executablePath = undefined
static disableDebug = false
static noSandbox = false
static viewport = { width: 1920, height: 1080 }
//static viewport = { width: 1920, height: 1080 }
static url = undefined
static extensionDir = 'extension_files'
/**
*
@ -61,14 +62,13 @@ export default class MessagesPuppeteer {
* Start the browser and open the messages for web page.
* This must be called before doing anything else.
*/
async start(debug = false) {
async start() {
this.log("Launching browser")
const pathToExtension = require('path').join(__dirname, 'extension_files');
const extensionArgs = [
`--disable-extensions-except=${pathToExtension}`,
`--load-extension=${pathToExtension}`
];
`--disable-extensions-except=${MessagesPuppeteer.extensionDir}`,
`--load-extension=${MessagesPuppeteer.extensionDir}`
]
this.browser = await puppeteer.launch({
executablePath: MessagesPuppeteer.executablePath,
@ -85,67 +85,182 @@ export default class MessagesPuppeteer {
this.page = await this.browser.newPage()
}
this.log("Opening", MessagesPuppeteer.url)
await this.page.goto(MessagesPuppeteer.url)
await this.page.setBypassCSP(true) // Needed to load content scripts
await this._preparePage(true)
this.log("Injecting content script")
await this.page.addScriptTag({ path: "./src/contentscript.js", type: "module" })
this.log("Exposing functions")
await this.page.exposeFunction("__mautrixReceiveQR", this._receiveQRChange.bind(this))
await this.page.exposeFunction("__mautrixSendEmailCredentials", this._sendEmailCredentials.bind(this))
await this.page.exposeFunction("__mautrixReceivePIN", this._receivePIN.bind(this))
await this.page.exposeFunction("__mautrixExpiry", this._receiveExpiry.bind(this))
/* TODO
await this.page.exposeFunction("__mautrixReceiveMessageID",
id => this.sentMessageIDs.add(id))
await this.page.exposeFunction("__mautrixReceiveChanges",
this._receiveChatListChanges.bind(this))
await this.page.exposeFunction("__chronoParseDate", chrono.parseDate)
*/
this.log("Waiting for load")
// Wait for the page to load (either QR code for login or chat list when already logged in)
await Promise.race([
this.page.waitForSelector("mw-main-container mws-conversations-list .conv-container",
{ visible: true, timeout: 60000 }),
this.page.waitForSelector("mw-authentication-container mw-qr-code",
{ visible: true, timeout: 60000 }),
this.page.waitForSelector("mw-unable-to-connect-container",
{ visible: true, timeout: 60000 }),
])
// NOTE Must *always* re-login on a browser session, so no need to check if already logged in
this.loginRunning = false
this.loginCancelled = false
this.taskQueue.start()
if (await this.isLoggedIn()) {
await this.startObserving()
}
this.log("Startup complete")
}
async _preparePage(navigateTo) {
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" })
}
/**
* Wait for the session to be logged in and monitor QR code changes while it's not.
* Wait for the session to be logged in and monitor changes while it's not.
*/
async waitForLogin() {
async waitForLogin(login_type, login_data) {
if (await this.isLoggedIn()) {
return
}
const qrSelector = "mw-authentication-container mw-qr-code"
if (!await this.page.$("mat-slide-toggle.mat-checked")) {
this.log("Clicking Remember Me button")
await this.page.click("mat-slide-toggle:not(.mat-checked) > label")
} else {
this.log("Remember Me button already clicked")
this.loginRunning = true
this.loginCancelled = false
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
}
this.log("Fetching current QR code")
const currentQR = await this.page.$eval(qrSelector,
element => element.getAttribute("data-qr-code"))
this._receiveQRChange(currentQR)
this.log("Adding QR observer")
await this.page.$eval(qrSelector,
element => window.__mautrixController.addQRObserver(element))
this.log("Waiting for login")
await this.page.waitForSelector("mws-conversations-list .conv-container", {
visible: true,
timeout: 0,
})
this.log("Removing QR observer")
await this.page.evaluate(() => window.__mautrixController.removeQRObserver())
case "email": {
this.log("Running email login")
if (!login_data) {
_sendLoginFailure("No login credentials provided for email login")
return
}
const emailButton = await this.page.waitForSelector("#login_email_btn")
await emailButton.click()
const emailArea = await this.page.waitForSelector("#login_email_area", {visible: true})
this.login_email = login_data["email"]
this.login_password = login_data["password"]
this._sendEmailCredentials()
await this.page.evaluate(
element => window.__mautrixController.addEmailAppearObserver(element), loginContentArea)
break
}
// TODO Phone number login
default:
_sendLoginFailure(`Invalid login type: ${login_type}`)
return
}
await this.page.evaluate(
element => window.__mautrixController.addPINAppearObserver(element), loginContentArea)
await this.page.$eval("#layer_contents",
element => window.__mautrixController.addExpiryObserver(element))
this.log("Waiting for login response")
let doneWaiting = false
let loginSuccess = false
const cancelableResolve = (promiseWithShortTimeout) => {
const executor = (resolve, reject) => {
promiseWithShortTimeout.then(
value => {
this.log(`Done: ${value}`)
doneWaiting = true
resolve(value)
},
reason => {
if (!doneWaiting) {
this.log(`Not done, waiting some more. ${reason}`)
setTimeout(executor, 3000, resolve, reject)
} else {
this.log(`Final fail. ${reason}`)
resolve()
}
}
)
}
return new Promise(executor)
}
const result = await Promise.race([
this.page.waitForSelector("#wrap_message_sync", {timeout: 2000})
.then(element => {
loginSuccess = true
return element
}),
this.page.waitForSelector("#login_incorrect", {visible: true, timeout: 2000})
.then(element => element.innerText),
this._waitForLoginCancel(),
].map(promise => cancelableResolve(promise)))
this.log("Removing observers")
await this.page.evaluate(() => window.__mautrixController.removeQRChangeObserver())
await this.page.evaluate(() => window.__mautrixController.removeLoginChildrenObserver(element))
await this.page.evaluate(() => window.__mautrixController.removeExpiryObserver())
delete this.login_email
delete this.login_password
if (!loginSuccess) {
_sendLoginFailure(result)
return
}
this.log("Waiting for sync")
await this.page.waitForFunction(
messageSyncElement => {
const text = messageSyncElement.innerText
return text == 'Syncing messages... 100%'
},
{},
result)
await this.startObserving()
this.loginRunning = false
this.log("Login complete")
}
/**
* Cancel an ongoing login attempt.
*/
async cancelLogin() {
if (this.loginRunning) {
this.loginCancelled = true
//await this._preparePage(false)
}
}
_waitForLoginCancel() {
return new Promise((resolve, reject) => {
console.log(`>>>>> ${this.loginCancelled}`)
if (this.loginCancelled) {
resolve()
} else {
reject()
}
})
}
/**
* Close the browser.
*/
@ -166,14 +281,17 @@ export default class MessagesPuppeteer {
* @return {Promise<boolean>} - Whether or not the session is logged in.
*/
async isLoggedIn() {
return await this.page.$("mw-main-container mws-conversations-list") !== null
return await this.page.$("#wrap_message_sync") !== null
}
async isPermanentlyDisconnected() {
return await this.page.$("mw-unable-to-connect-container") !== null
// TODO
//return await this.page.$("mw-unable-to-connect-container") !== null
return false
}
async isOpenSomewhereElse() {
/* TODO
try {
const text = await this.page.$eval("mws-dialog mat-dialog-content div",
elem => elem.textContent)
@ -181,16 +299,15 @@ export default class MessagesPuppeteer {
} catch (err) {
return false
}
}
async clickDialogButton() {
await this.page.click("mws-dialog mat-dialog-actions button")
*/
return false
}
async isDisconnected() {
if (!await this.isLoggedIn()) {
return true
}
/* TODO
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']"),
@ -198,6 +315,8 @@ export default class MessagesPuppeteer {
this.isOpenSomewhereElse(),
])
return offlineIndicators.some(indicator => Boolean(indicator))
*/
return false
}
/**
@ -206,8 +325,11 @@ export default class MessagesPuppeteer {
* @return {Promise<[ChatListInfo]>} - List of chat IDs in order of most recent message.
*/
async getRecentChats() {
/* TODO
return await this.page.$eval("mws-conversations-list .conv-container",
elem => window.__mautrixController.parseChatList(elem))
*/
return null
}
/**
@ -266,7 +388,7 @@ export default class MessagesPuppeteer {
async startObserving() {
this.log("Adding chat list observer")
await this.page.$eval("mws-conversations-list .conv-container",
await this.page.$eval("#wrap_chat_list",
element => window.__mautrixController.addChatListObserver(element))
}
@ -276,7 +398,9 @@ export default class MessagesPuppeteer {
}
_listItemSelector(id) {
return `mws-conversation-list-item > a.list-item[href="/web/conversations/${id}"]`
// TODO
//return `mws-conversation-list-item > a.list-item[href="/web/conversations/${id}"]`
return ''
}
async _switchChatUnsafe(id) {
@ -365,6 +489,20 @@ export default class MessagesPuppeteer {
}
}
async _sendEmailCredentials() {
this.log("Inputting login credentials")
// Triple-click email input field to select all existing text and replace it on type
const emailInput = await this.page.$("#line_login_email")
await emailInput.click({clickCount: 3})
await emailInput.type(this.login_email)
// Password input field always starts empty, so no need to select its text first
await this.page.type("#line_login_pwd", this.login_password)
await this.page.click("button#login_btn")
}
_receiveQRChange(url) {
if (this.client) {
this.client.sendQRCode(url).catch(err =>
@ -373,4 +511,28 @@ export default class MessagesPuppeteer {
this.log("No client connected, not sending new QR")
}
}
_receivePIN(pin) {
if (this.client) {
this.client.sendPIN(`Your PIN is: ${pin}`).catch(err =>
this.error("Failed to send new PIN to client:", err))
} else {
this.log("No client connected, not sending new PIN")
}
}
_sendLoginFailure(reason) {
this.error(`Login failure: ${reason ? reason : 'cancelled'}`)
if (this.client) {
this.client.sendFailure(reason).catch(err =>
this.error("Failed to send failure reason to client:", err))
} else {
this.log("No client connected, not sending failure reason")
}
}
async _receiveExpiry(button) {
this.log("Something expired, clicking OK button to continue")
await this.page.click(button)
}
}

View File

@ -364,10 +364,10 @@ define-properties@^1.1.2, define-properties@^1.1.3:
dependencies:
object-keys "^1.0.12"
devtools-protocol@0.0.767361:
version "0.0.767361"
resolved "https://registry.yarnpkg.com/devtools-protocol/-/devtools-protocol-0.0.767361.tgz#5977f2558b84f9df36f62501bdddb82f3ae7b66b"
integrity sha512-ziRTdhEVQ9jEwedaUaXZ7kl9w9TF/7A3SXQ0XuqrJB+hMS62POHZUWTbumDN2ehRTfvWqTPc2Jw4gUl/jggmHA==
devtools-protocol@0.0.818844:
version "0.0.818844"
resolved "https://registry.yarnpkg.com/devtools-protocol/-/devtools-protocol-0.0.818844.tgz#d1947278ec85b53e4c8ca598f607a28fa785ba9e"
integrity sha512-AD1hi7iVJ8OD0aMLQU5VK0XH9LDlA1+BcPIgrAxPfaibx2DbWucuyOhc4oyQCbnvDDO68nN6/LcKfqTP343Jjg==
doctrine@1.5.0:
version "1.5.0"
@ -918,11 +918,6 @@ lodash@^4.17.14, lodash@^4.17.19:
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52"
integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==
mime@^2.0.3:
version "2.4.6"
resolved "https://registry.yarnpkg.com/mime/-/mime-2.4.6.tgz#e5b407c90db442f2beb5b162373d07b69affa4d1"
integrity sha512-RZKhC3EmpBchfTGBVb8fb+RL2cWyw/32lshnsETttkBAyAUXSGHxbEJWWRXc751DrIxG1q04b8QwMbAwkRPpUA==
minimatch@^3.0.4:
version "3.0.4"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
@ -935,11 +930,6 @@ minimist@^1.2.0, minimist@^1.2.5:
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
mitt@^2.0.1:
version "2.1.0"
resolved "https://registry.yarnpkg.com/mitt/-/mitt-2.1.0.tgz#f740577c23176c6205b121b2973514eade1b2230"
integrity sha512-ILj2TpLiysu2wkBbWjAmww7TkZb65aiQO+DkVdUTBpBXq+MHYiETENkKFMtsJZX1Lf4pe4QOrTSjIfUwN5lRdg==
mkdirp-classic@^0.5.2:
version "0.5.3"
resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113"
@ -967,6 +957,11 @@ natural-compare@^1.4.0:
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=
node-fetch@^2.6.1:
version "2.6.1"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052"
integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==
normalize-package-data@^2.3.2:
version "2.5.0"
resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8"
@ -1162,17 +1157,16 @@ punycode@^2.1.0:
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
puppeteer@5.1.0:
version "5.1.0"
resolved "https://registry.yarnpkg.com/puppeteer/-/puppeteer-5.1.0.tgz#e7bae2caa6e3a13a622755e4c27689d9812c38ca"
integrity sha512-IZBFG8XcA+oHxYo5rEpJI/HQignUis2XPijPoFpNxla2O+WufonGsUsSqrhRXgBKOME5zNfhRdUY2LvxAiKlhw==
puppeteer@5.5.0:
version "5.5.0"
resolved "https://registry.yarnpkg.com/puppeteer/-/puppeteer-5.5.0.tgz#331a7edd212ca06b4a556156435f58cbae08af00"
integrity sha512-OM8ZvTXAhfgFA7wBIIGlPQzvyEETzDjeRa4mZRCRHxYL+GNH5WAuYUQdja3rpWZvkX/JKqmuVgbsxDNsDFjMEg==
dependencies:
debug "^4.1.0"
devtools-protocol "0.0.767361"
devtools-protocol "0.0.818844"
extract-zip "^2.0.0"
https-proxy-agent "^4.0.0"
mime "^2.0.3"
mitt "^2.0.1"
node-fetch "^2.6.1"
pkg-dir "^4.2.0"
progress "^2.0.1"
proxy-from-env "^1.0.0"