forked from fair/matrix-puppeteer-line
More changes
This commit is contained in:
parent
63dc7f0be3
commit
85814f9793
|
@ -11,6 +11,7 @@ __pycache__
|
||||||
/.eggs
|
/.eggs
|
||||||
|
|
||||||
profiles
|
profiles
|
||||||
|
puppet/extension_files
|
||||||
|
|
||||||
/config.yaml
|
/config.yaml
|
||||||
/registration.yaml
|
/registration.yaml
|
||||||
|
|
|
@ -13,13 +13,13 @@
|
||||||
#
|
#
|
||||||
# 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/>.
|
||||||
from typing import Optional
|
from typing import Optional, AsyncGenerator, Tuple
|
||||||
import io
|
import io
|
||||||
|
|
||||||
import qrcode
|
import qrcode
|
||||||
import PIL as _
|
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 mautrix.bridge.commands import HelpSection, command_handler
|
||||||
|
|
||||||
from .typehint import CommandEvent
|
from .typehint import CommandEvent
|
||||||
|
@ -27,29 +27,91 @@ from .typehint import CommandEvent
|
||||||
SECTION_AUTH = HelpSection("Authentication", 10, "")
|
SECTION_AUTH = HelpSection("Authentication", 10, "")
|
||||||
|
|
||||||
|
|
||||||
@command_handler(needs_auth=False, management_only=True, help_section=SECTION_AUTH,
|
async def login_prep(evt: CommandEvent, login_type: str) -> bool:
|
||||||
help_text="Log into Android Messages")
|
|
||||||
async def login(evt: CommandEvent) -> None:
|
|
||||||
status = await evt.sender.client.start()
|
status = await evt.sender.client.start()
|
||||||
if status.is_logged_in:
|
if status.is_logged_in:
|
||||||
await evt.reply("You're already logged in")
|
await evt.reply("You're already logged in")
|
||||||
return
|
return False
|
||||||
qr_event_id: Optional[EventID] = None
|
|
||||||
async for url in evt.sender.client.login():
|
if evt.sender.command_status is not None:
|
||||||
buffer = io.BytesIO()
|
action = evt.sender.command_status["action"]
|
||||||
image = qrcode.make(url)
|
if action == "Login":
|
||||||
size = image.pixel_size
|
await evt.reply(
|
||||||
image.save(buffer, "PNG")
|
"A login is already in progress. Please follow the login instructions, "
|
||||||
qr = buffer.getvalue()
|
"or use the `$cmdprefix+sp cancel` command to start over.")
|
||||||
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:
|
else:
|
||||||
content.set_reply(evt.event_id)
|
await evt.reply(f"Cannot login while a {action} command is active.")
|
||||||
qr_event_id = await evt.az.intent.send_message(evt.room_id, content)
|
return False
|
||||||
await evt.reply("Successfully logged in, now syncing")
|
|
||||||
await evt.sender.sync()
|
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)
|
||||||
|
|
|
@ -29,7 +29,7 @@ async def set_notice_room(evt: CommandEvent) -> None:
|
||||||
|
|
||||||
|
|
||||||
@command_handler(needs_auth=False, management_only=True, help_section=SECTION_CONNECTION,
|
@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:
|
async def ping(evt: CommandEvent) -> None:
|
||||||
status = await evt.sender.client.start()
|
status = await evt.sender.client.start()
|
||||||
if status.is_logged_in:
|
if status.is_logged_in:
|
||||||
|
|
|
@ -39,18 +39,18 @@ appservice:
|
||||||
shared_secret: generate
|
shared_secret: generate
|
||||||
|
|
||||||
# The unique ID of this appservice.
|
# The unique ID of this appservice.
|
||||||
id: amp
|
id: line
|
||||||
# Username of the appservice bot.
|
# 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
|
# Display name and avatar for bot. Set to "remove" to remove display name/avatar, leave empty
|
||||||
# to leave display name/avatar as-is.
|
# to leave display name/avatar as-is.
|
||||||
bot_displayname: Android Messages bridge bot
|
bot_displayname: LINE bridge bot
|
||||||
bot_avatar: mxc://maunium.net/VuvevQiMRlOxuBVMBNEZZrxi
|
bot_avatar: mxc://maunium.net/VuvevQiMRlOxuBVMBNEZZrxi
|
||||||
|
|
||||||
# Community ID for bridged users (changes registration file) and rooms.
|
# Community ID for bridged users (changes registration file) and rooms.
|
||||||
# Must be created manually.
|
# 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
|
community_id: false
|
||||||
|
|
||||||
# Authentication tokens for AS <-> HS communication. Autogenerated; do not modify.
|
# Authentication tokens for AS <-> HS communication. Autogenerated; do not modify.
|
||||||
|
@ -66,11 +66,11 @@ metrics:
|
||||||
bridge:
|
bridge:
|
||||||
# Localpart template of MXIDs for remote users.
|
# Localpart template of MXIDs for remote users.
|
||||||
# {userid} is replaced with the user ID (phone or name converted into a mxid-friendly format).
|
# {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 template for remote users.
|
||||||
# {displayname} is replaced with the display name of the user.
|
# {displayname} is replaced with the display name of the user.
|
||||||
# {phone} is replaced with the phone number or 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
|
# Maximum length of displayname
|
||||||
displayname_max_length: 100
|
displayname_max_length: 100
|
||||||
|
@ -129,7 +129,7 @@ bridge:
|
||||||
resend_bridge_info: false
|
resend_bridge_info: false
|
||||||
|
|
||||||
# The prefix for commands. Only required in non-management rooms.
|
# The prefix for commands. Only required in non-management rooms.
|
||||||
command_prefix: "!am"
|
command_prefix: "!line"
|
||||||
|
|
||||||
# This bridge only supports a single user
|
# This bridge only supports a single user
|
||||||
user: "@admin:example.com"
|
user: "@admin:example.com"
|
||||||
|
|
|
@ -47,4 +47,4 @@ class MatrixHandler(BaseMatrixHandler):
|
||||||
inviter.notice_room = room_id
|
inviter.notice_room = room_id
|
||||||
await inviter.update()
|
await inviter.update()
|
||||||
await self.az.intent.send_notice(room_id, "This room has been marked as your "
|
await self.az.intent.send_notice(room_id, "This room has been marked as your "
|
||||||
"Android Messages bridge notice room.")
|
"LINE bridge notice room.")
|
||||||
|
|
|
@ -268,7 +268,7 @@ class Portal(DBPortal, BasePortal):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def bridge_info_state_key(self) -> str:
|
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
|
@property
|
||||||
def bridge_info(self) -> Dict[str, Any]:
|
def bridge_info(self) -> Dict[str, Any]:
|
||||||
|
@ -276,8 +276,8 @@ class Portal(DBPortal, BasePortal):
|
||||||
"bridgebot": self.az.bot_mxid,
|
"bridgebot": self.az.bot_mxid,
|
||||||
"creator": self.main_intent.mxid,
|
"creator": self.main_intent.mxid,
|
||||||
"protocol": {
|
"protocol": {
|
||||||
"id": "androidmessages",
|
"id": "line",
|
||||||
"displayname": "Android Messages",
|
"displayname": "LINE",
|
||||||
"avatar_url": self.config["appservice.bot_avatar"],
|
"avatar_url": self.config["appservice.bot_avatar"],
|
||||||
},
|
},
|
||||||
"channel": {
|
"channel": {
|
||||||
|
|
|
@ -56,8 +56,9 @@ class Puppet(DBPuppet, BasePuppet):
|
||||||
cls.mxid_template = SimpleTemplate(cls.config["bridge.username_template"], "userid",
|
cls.mxid_template = SimpleTemplate(cls.config["bridge.username_template"], "userid",
|
||||||
prefix="@", suffix=f":{cls.hs_domain}", type=str)
|
prefix="@", suffix=f":{cls.hs_domain}", type=str)
|
||||||
secret = cls.config["bridge.login_shared_secret"]
|
secret = cls.config["bridge.login_shared_secret"]
|
||||||
cls.login_shared_secret_map[cls.hs_domain] = secret.encode("utf-8") if secret else None
|
if secret:
|
||||||
cls.login_device_name = "Android Messages Bridge"
|
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:
|
async def update_info(self, info: Participant) -> None:
|
||||||
update = False
|
update = False
|
||||||
|
|
|
@ -13,20 +13,17 @@
|
||||||
#
|
#
|
||||||
# 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/>.
|
||||||
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
|
from collections import deque
|
||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
from .rpc import RPCClient
|
from .rpc import RPCClient
|
||||||
from .types import ChatListInfo, ChatInfo, Message, StartStatus
|
from .types import ChatListInfo, ChatInfo, Message, StartStatus
|
||||||
|
from mautrix_line.rpc.types import RPCError
|
||||||
|
|
||||||
|
|
||||||
class QRCommand(TypedDict):
|
class LoginCommand(TypedDict):
|
||||||
url: str
|
content: str
|
||||||
|
|
||||||
|
|
||||||
class LoginComplete(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class Client(RPCClient):
|
class Client(RPCClient):
|
||||||
|
@ -66,22 +63,48 @@ class Client(RPCClient):
|
||||||
|
|
||||||
self.add_event_handler("message", wrapper)
|
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()
|
data = deque()
|
||||||
event = asyncio.Event()
|
event = asyncio.Event()
|
||||||
|
|
||||||
async def qr_handler(req: QRCommand) -> None:
|
async def qr_handler(req: LoginCommand) -> None:
|
||||||
data.append(req["url"])
|
data.append(("qr", req["url"]))
|
||||||
event.set()
|
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:
|
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)
|
data.append(None)
|
||||||
event.set()
|
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)
|
login_future.add_done_callback(login_handler)
|
||||||
|
|
||||||
self.add_event_handler("qr", qr_handler)
|
self.add_event_handler("qr", qr_handler)
|
||||||
|
self.add_event_handler("pin", pin_handler)
|
||||||
|
self.add_event_handler("failure", failure_handler)
|
||||||
try:
|
try:
|
||||||
while True:
|
while True:
|
||||||
await event.wait()
|
await event.wait()
|
||||||
|
@ -93,3 +116,5 @@ class Client(RPCClient):
|
||||||
event.clear()
|
event.clear()
|
||||||
finally:
|
finally:
|
||||||
self.remove_event_handler("qr", qr_handler)
|
self.remove_event_handler("qr", qr_handler)
|
||||||
|
self.remove_event_handler("pin", pin_handler)
|
||||||
|
self.remove_event_handler("failure", failure_handler)
|
||||||
|
|
|
@ -30,7 +30,7 @@ from . import puppet as pu, portal as po
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .__main__ import MessagesBridge
|
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):
|
class User(DBUser, BaseUser):
|
||||||
|
@ -49,6 +49,7 @@ class User(DBUser, BaseUser):
|
||||||
def __init__(self, mxid: UserID, notice_room: Optional[RoomID] = None) -> None:
|
def __init__(self, mxid: UserID, notice_room: Optional[RoomID] = None) -> None:
|
||||||
super().__init__(mxid=mxid, notice_room=notice_room)
|
super().__init__(mxid=mxid, notice_room=notice_room)
|
||||||
self._notice_room_lock = asyncio.Lock()
|
self._notice_room_lock = asyncio.Lock()
|
||||||
|
self.command_status = None
|
||||||
self.is_whitelisted = self.is_admin = self.config["bridge.user"] == mxid
|
self.is_whitelisted = self.is_admin = self.config["bridge.user"] == mxid
|
||||||
self.log = self.log.getChild(self.mxid)
|
self.log = self.log.getChild(self.mxid)
|
||||||
self._metric_value = defaultdict(lambda: False)
|
self._metric_value = defaultdict(lambda: False)
|
||||||
|
|
|
@ -64,8 +64,8 @@ class ProvisioningAPI:
|
||||||
return None
|
return None
|
||||||
for part in auth_parts:
|
for part in auth_parts:
|
||||||
part = part.strip()
|
part = part.strip()
|
||||||
if part.startswith("net.maunium.amp.auth-"):
|
if part.startswith("net.maunium.line.auth-"):
|
||||||
return part[len("net.maunium.amp.auth-"):]
|
return part[len("net.maunium.line.auth-"):]
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def check_token(self, request: web.Request) -> Awaitable['u.User']:
|
def check_token(self, request: web.Request) -> Awaitable['u.User']:
|
||||||
|
@ -94,7 +94,7 @@ class ProvisioningAPI:
|
||||||
user = await self.check_token(request)
|
user = await self.check_token(request)
|
||||||
data = {
|
data = {
|
||||||
"mxid": user.mxid,
|
"mxid": user.mxid,
|
||||||
"amp": {
|
"line": {
|
||||||
"connected": True,
|
"connected": True,
|
||||||
} if await user.is_logged_in() else None,
|
} if await user.is_logged_in() else None,
|
||||||
}
|
}
|
||||||
|
@ -107,7 +107,7 @@ class ProvisioningAPI:
|
||||||
if status.is_logged_in:
|
if status.is_logged_in:
|
||||||
raise web.HTTPConflict(text='{"error": "Already logged in"}', headers=self._headers)
|
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)
|
await ws.prepare(request)
|
||||||
try:
|
try:
|
||||||
async for url in user.client.login():
|
async for url in user.client.login():
|
||||||
|
|
|
@ -4,5 +4,6 @@
|
||||||
"path": "/var/run/mautrix-line/puppet.sock"
|
"path": "/var/run/mautrix-line/puppet.sock"
|
||||||
},
|
},
|
||||||
"profile_dir": "./profiles",
|
"profile_dir": "./profiles",
|
||||||
"url": "chrome-extension://<extension-uuid>/index.html"
|
"url": "chrome-extension://<extension-uuid>/index.html",
|
||||||
|
"extension_dir": "./extension_files"
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,7 +17,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"arg": "^4.1.3",
|
"arg": "^4.1.3",
|
||||||
"chrono-node": "^2.1.7",
|
"chrono-node": "^2.1.7",
|
||||||
"puppeteer": "5.1.0"
|
"puppeteer": "5.5.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"babel-eslint": "^10.1.0",
|
"babel-eslint": "^10.1.0",
|
||||||
|
|
|
@ -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) => {
|
handleStart = async (req) => {
|
||||||
let started = false
|
let started = false
|
||||||
if (this.puppet === null) {
|
if (this.puppet === null) {
|
||||||
|
@ -205,7 +223,8 @@ export default class Client {
|
||||||
start: this.handleStart,
|
start: this.handleStart,
|
||||||
stop: this.handleStop,
|
stop: this.handleStop,
|
||||||
disconnect: () => this.stop(),
|
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),
|
send: req => this.puppet.sendMessage(req.chat_id, req.text),
|
||||||
set_last_message_ids: req => this.puppet.setLastMessageIDs(req.msg_ids),
|
set_last_message_ids: req => this.puppet.setLastMessageIDs(req.msg_ids),
|
||||||
get_chats: () => this.puppet.getRecentChats(),
|
get_chats: () => this.puppet.getRecentChats(),
|
||||||
|
|
|
@ -32,6 +32,20 @@ window.__mautrixReceiveChanges = function (changes) {}
|
||||||
* @return {Promise<void>}
|
* @return {Promise<void>}
|
||||||
*/
|
*/
|
||||||
window.__mautrixReceiveQR = function (url) {}
|
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
|
* @param {number} id - The ID of the message that was sent
|
||||||
* @return {Promise<void>}
|
* @return {Promise<void>}
|
||||||
|
@ -41,7 +55,11 @@ window.__mautrixReceiveMessageID = function(id) {}
|
||||||
class MautrixController {
|
class MautrixController {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.chatListObserver = null
|
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) {
|
if (this.chatListObserver !== null) {
|
||||||
this.removeChatListObserver()
|
this.removeChatListObserver()
|
||||||
}
|
}
|
||||||
|
/* TODO
|
||||||
this.chatListObserver = new MutationObserver(mutations => {
|
this.chatListObserver = new MutationObserver(mutations => {
|
||||||
try {
|
try {
|
||||||
this._observeChatListMutations(mutations)
|
this._observeChatListMutations(mutations)
|
||||||
|
@ -320,6 +339,7 @@ class MautrixController {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
this.chatListObserver.observe(element, { childList: true, subtree: true })
|
this.chatListObserver.observe(element, { childList: true, subtree: true })
|
||||||
|
*/
|
||||||
console.debug("Started chat list observer")
|
console.debug("Started chat list observer")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -334,27 +354,132 @@ class MautrixController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
addQRObserver(element) {
|
addQRChangeObserver(element) {
|
||||||
if (this.qrCodeObserver !== null) {
|
if (this.qrChangeObserver !== null) {
|
||||||
this.removeQRObserver()
|
this.removeQRChangeObserver()
|
||||||
}
|
}
|
||||||
this.qrCodeObserver = new MutationObserver(changes => {
|
this.qrChangeObserver = new MutationObserver(changes => {
|
||||||
for (const change of changes) {
|
for (const change of changes) {
|
||||||
if (change.attributeName === "data-qr-code" && change.target instanceof Element) {
|
if (change.attributeName === "title" && change.target instanceof Element) {
|
||||||
window.__mautrixReceiveQR(change.target.getAttribute("data-qr-code"))
|
window.__mautrixReceiveQR(change.target.getAttribute("title"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
this.qrCodeObserver.observe(element, {
|
this.qrChangeObserver.observe(element, {
|
||||||
attributes: true,
|
attributes: true,
|
||||||
attributeFilter: ["data-qr-code"],
|
attributeFilter: ["title"],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
removeQRObserver() {
|
removeQRChangeObserver() {
|
||||||
if (this.qrCodeObserver !== null) {
|
if (this.qrChangeObserver !== null) {
|
||||||
this.qrCodeObserver.disconnect()
|
this.qrChangeObserver.disconnect()
|
||||||
this.qrCodeObserver = null
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,6 +38,7 @@ const config = JSON.parse(fs.readFileSync(configPath).toString())
|
||||||
MessagesPuppeteer.profileDir = config.profile_dir || MessagesPuppeteer.profileDir
|
MessagesPuppeteer.profileDir = config.profile_dir || MessagesPuppeteer.profileDir
|
||||||
MessagesPuppeteer.disableDebug = !!config.disable_debug
|
MessagesPuppeteer.disableDebug = !!config.disable_debug
|
||||||
MessagesPuppeteer.url = config.url
|
MessagesPuppeteer.url = config.url
|
||||||
|
MessagesPuppeteer.extensionDir = config.extension_dir || MessagesPuppeteer.extensionDir
|
||||||
|
|
||||||
const api = new PuppetAPI(config.listen)
|
const api = new PuppetAPI(config.listen)
|
||||||
|
|
||||||
|
|
|
@ -27,8 +27,9 @@ export default class MessagesPuppeteer {
|
||||||
static executablePath = undefined
|
static executablePath = undefined
|
||||||
static disableDebug = false
|
static disableDebug = false
|
||||||
static noSandbox = false
|
static noSandbox = false
|
||||||
static viewport = { width: 1920, height: 1080 }
|
//static viewport = { width: 1920, height: 1080 }
|
||||||
static url = undefined
|
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.
|
* Start the browser and open the messages for web page.
|
||||||
* This must be called before doing anything else.
|
* This must be called before doing anything else.
|
||||||
*/
|
*/
|
||||||
async start(debug = false) {
|
async start() {
|
||||||
this.log("Launching browser")
|
this.log("Launching browser")
|
||||||
|
|
||||||
const pathToExtension = require('path').join(__dirname, 'extension_files');
|
|
||||||
const extensionArgs = [
|
const extensionArgs = [
|
||||||
`--disable-extensions-except=${pathToExtension}`,
|
`--disable-extensions-except=${MessagesPuppeteer.extensionDir}`,
|
||||||
`--load-extension=${pathToExtension}`
|
`--load-extension=${MessagesPuppeteer.extensionDir}`
|
||||||
];
|
]
|
||||||
|
|
||||||
this.browser = await puppeteer.launch({
|
this.browser = await puppeteer.launch({
|
||||||
executablePath: MessagesPuppeteer.executablePath,
|
executablePath: MessagesPuppeteer.executablePath,
|
||||||
|
@ -85,67 +85,182 @@ export default class MessagesPuppeteer {
|
||||||
this.page = await this.browser.newPage()
|
this.page = await this.browser.newPage()
|
||||||
}
|
}
|
||||||
this.log("Opening", MessagesPuppeteer.url)
|
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")
|
this.log("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("__mautrixReceivePIN", this._receivePIN.bind(this))
|
||||||
|
await this.page.exposeFunction("__mautrixExpiry", this._receiveExpiry.bind(this))
|
||||||
|
/* TODO
|
||||||
await this.page.exposeFunction("__mautrixReceiveMessageID",
|
await this.page.exposeFunction("__mautrixReceiveMessageID",
|
||||||
id => this.sentMessageIDs.add(id))
|
id => this.sentMessageIDs.add(id))
|
||||||
await this.page.exposeFunction("__mautrixReceiveChanges",
|
await this.page.exposeFunction("__mautrixReceiveChanges",
|
||||||
this._receiveChatListChanges.bind(this))
|
this._receiveChatListChanges.bind(this))
|
||||||
await this.page.exposeFunction("__chronoParseDate", chrono.parseDate)
|
await this.page.exposeFunction("__chronoParseDate", chrono.parseDate)
|
||||||
|
*/
|
||||||
|
|
||||||
this.log("Waiting for load")
|
// NOTE Must *always* re-login on a browser session, so no need to check if already logged in
|
||||||
// Wait for the page to load (either QR code for login or chat list when already logged in)
|
this.loginRunning = false
|
||||||
await Promise.race([
|
this.loginCancelled = false
|
||||||
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 }),
|
|
||||||
])
|
|
||||||
this.taskQueue.start()
|
this.taskQueue.start()
|
||||||
if (await this.isLoggedIn()) {
|
|
||||||
await this.startObserving()
|
|
||||||
}
|
|
||||||
this.log("Startup complete")
|
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()) {
|
if (await this.isLoggedIn()) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const qrSelector = "mw-authentication-container mw-qr-code"
|
this.loginRunning = true
|
||||||
if (!await this.page.$("mat-slide-toggle.mat-checked")) {
|
this.loginCancelled = false
|
||||||
this.log("Clicking Remember Me button")
|
|
||||||
await this.page.click("mat-slide-toggle:not(.mat-checked) > label")
|
const loginContentArea = await this.page.waitForSelector("#login_content")
|
||||||
} else {
|
|
||||||
this.log("Remember Me button already clicked")
|
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")
|
case "email": {
|
||||||
const currentQR = await this.page.$eval(qrSelector,
|
this.log("Running email login")
|
||||||
element => element.getAttribute("data-qr-code"))
|
if (!login_data) {
|
||||||
this._receiveQRChange(currentQR)
|
_sendLoginFailure("No login credentials provided for email login")
|
||||||
this.log("Adding QR observer")
|
return
|
||||||
await this.page.$eval(qrSelector,
|
}
|
||||||
element => window.__mautrixController.addQRObserver(element))
|
|
||||||
this.log("Waiting for login")
|
const emailButton = await this.page.waitForSelector("#login_email_btn")
|
||||||
await this.page.waitForSelector("mws-conversations-list .conv-container", {
|
await emailButton.click()
|
||||||
visible: true,
|
|
||||||
timeout: 0,
|
const emailArea = await this.page.waitForSelector("#login_email_area", {visible: true})
|
||||||
})
|
this.login_email = login_data["email"]
|
||||||
this.log("Removing QR observer")
|
this.login_password = login_data["password"]
|
||||||
await this.page.evaluate(() => window.__mautrixController.removeQRObserver())
|
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()
|
await this.startObserving()
|
||||||
|
this.loginRunning = false
|
||||||
this.log("Login complete")
|
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.
|
* Close the browser.
|
||||||
*/
|
*/
|
||||||
|
@ -166,14 +281,17 @@ export default class MessagesPuppeteer {
|
||||||
* @return {Promise<boolean>} - Whether or not the session is logged in.
|
* @return {Promise<boolean>} - Whether or not the session is logged in.
|
||||||
*/
|
*/
|
||||||
async isLoggedIn() {
|
async isLoggedIn() {
|
||||||
return await this.page.$("mw-main-container mws-conversations-list") !== null
|
return await this.page.$("#wrap_message_sync") !== null
|
||||||
}
|
}
|
||||||
|
|
||||||
async isPermanentlyDisconnected() {
|
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() {
|
async isOpenSomewhereElse() {
|
||||||
|
/* TODO
|
||||||
try {
|
try {
|
||||||
const text = await this.page.$eval("mws-dialog mat-dialog-content div",
|
const text = await this.page.$eval("mws-dialog mat-dialog-content div",
|
||||||
elem => elem.textContent)
|
elem => elem.textContent)
|
||||||
|
@ -181,16 +299,15 @@ export default class MessagesPuppeteer {
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
*/
|
||||||
|
return false
|
||||||
async clickDialogButton() {
|
|
||||||
await this.page.click("mws-dialog mat-dialog-actions button")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async isDisconnected() {
|
async isDisconnected() {
|
||||||
if (!await this.isLoggedIn()) {
|
if (!await this.isLoggedIn()) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
/* TODO
|
||||||
const offlineIndicators = await Promise.all([
|
const offlineIndicators = await Promise.all([
|
||||||
this.page.$("mw-main-nav mw-banner mw-error-banner"),
|
this.page.$("mw-main-nav mw-banner mw-error-banner"),
|
||||||
this.page.$("mw-main-nav mw-banner mw-information-banner[title='Connecting']"),
|
this.page.$("mw-main-nav mw-banner mw-information-banner[title='Connecting']"),
|
||||||
|
@ -198,6 +315,8 @@ export default class MessagesPuppeteer {
|
||||||
this.isOpenSomewhereElse(),
|
this.isOpenSomewhereElse(),
|
||||||
])
|
])
|
||||||
return offlineIndicators.some(indicator => Boolean(indicator))
|
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.
|
* @return {Promise<[ChatListInfo]>} - List of chat IDs in order of most recent message.
|
||||||
*/
|
*/
|
||||||
async getRecentChats() {
|
async getRecentChats() {
|
||||||
|
/* TODO
|
||||||
return await this.page.$eval("mws-conversations-list .conv-container",
|
return await this.page.$eval("mws-conversations-list .conv-container",
|
||||||
elem => window.__mautrixController.parseChatList(elem))
|
elem => window.__mautrixController.parseChatList(elem))
|
||||||
|
*/
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -266,7 +388,7 @@ export default class MessagesPuppeteer {
|
||||||
|
|
||||||
async startObserving() {
|
async startObserving() {
|
||||||
this.log("Adding chat list observer")
|
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))
|
element => window.__mautrixController.addChatListObserver(element))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -276,7 +398,9 @@ export default class MessagesPuppeteer {
|
||||||
}
|
}
|
||||||
|
|
||||||
_listItemSelector(id) {
|
_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) {
|
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) {
|
_receiveQRChange(url) {
|
||||||
if (this.client) {
|
if (this.client) {
|
||||||
this.client.sendQRCode(url).catch(err =>
|
this.client.sendQRCode(url).catch(err =>
|
||||||
|
@ -373,4 +511,28 @@ export default class MessagesPuppeteer {
|
||||||
this.log("No client connected, not sending new QR")
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -364,10 +364,10 @@ define-properties@^1.1.2, define-properties@^1.1.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
object-keys "^1.0.12"
|
object-keys "^1.0.12"
|
||||||
|
|
||||||
devtools-protocol@0.0.767361:
|
devtools-protocol@0.0.818844:
|
||||||
version "0.0.767361"
|
version "0.0.818844"
|
||||||
resolved "https://registry.yarnpkg.com/devtools-protocol/-/devtools-protocol-0.0.767361.tgz#5977f2558b84f9df36f62501bdddb82f3ae7b66b"
|
resolved "https://registry.yarnpkg.com/devtools-protocol/-/devtools-protocol-0.0.818844.tgz#d1947278ec85b53e4c8ca598f607a28fa785ba9e"
|
||||||
integrity sha512-ziRTdhEVQ9jEwedaUaXZ7kl9w9TF/7A3SXQ0XuqrJB+hMS62POHZUWTbumDN2ehRTfvWqTPc2Jw4gUl/jggmHA==
|
integrity sha512-AD1hi7iVJ8OD0aMLQU5VK0XH9LDlA1+BcPIgrAxPfaibx2DbWucuyOhc4oyQCbnvDDO68nN6/LcKfqTP343Jjg==
|
||||||
|
|
||||||
doctrine@1.5.0:
|
doctrine@1.5.0:
|
||||||
version "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"
|
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52"
|
||||||
integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==
|
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:
|
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"
|
||||||
|
@ -935,11 +930,6 @@ minimist@^1.2.0, minimist@^1.2.5:
|
||||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
|
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
|
||||||
integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
|
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:
|
mkdirp-classic@^0.5.2:
|
||||||
version "0.5.3"
|
version "0.5.3"
|
||||||
resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113"
|
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"
|
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
|
||||||
integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=
|
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:
|
normalize-package-data@^2.3.2:
|
||||||
version "2.5.0"
|
version "2.5.0"
|
||||||
resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8"
|
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"
|
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
|
||||||
integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
|
integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
|
||||||
|
|
||||||
puppeteer@5.1.0:
|
puppeteer@5.5.0:
|
||||||
version "5.1.0"
|
version "5.5.0"
|
||||||
resolved "https://registry.yarnpkg.com/puppeteer/-/puppeteer-5.1.0.tgz#e7bae2caa6e3a13a622755e4c27689d9812c38ca"
|
resolved "https://registry.yarnpkg.com/puppeteer/-/puppeteer-5.5.0.tgz#331a7edd212ca06b4a556156435f58cbae08af00"
|
||||||
integrity sha512-IZBFG8XcA+oHxYo5rEpJI/HQignUis2XPijPoFpNxla2O+WufonGsUsSqrhRXgBKOME5zNfhRdUY2LvxAiKlhw==
|
integrity sha512-OM8ZvTXAhfgFA7wBIIGlPQzvyEETzDjeRa4mZRCRHxYL+GNH5WAuYUQdja3rpWZvkX/JKqmuVgbsxDNsDFjMEg==
|
||||||
dependencies:
|
dependencies:
|
||||||
debug "^4.1.0"
|
debug "^4.1.0"
|
||||||
devtools-protocol "0.0.767361"
|
devtools-protocol "0.0.818844"
|
||||||
extract-zip "^2.0.0"
|
extract-zip "^2.0.0"
|
||||||
https-proxy-agent "^4.0.0"
|
https-proxy-agent "^4.0.0"
|
||||||
mime "^2.0.3"
|
node-fetch "^2.6.1"
|
||||||
mitt "^2.0.1"
|
|
||||||
pkg-dir "^4.2.0"
|
pkg-dir "^4.2.0"
|
||||||
progress "^2.0.1"
|
progress "^2.0.1"
|
||||||
proxy-from-env "^1.0.0"
|
proxy-from-env "^1.0.0"
|
||||||
|
|
Loading…
Reference in New Issue