More changes

TODO: Rebase for cleaner changes
This commit is contained in:
Andrew Ferrazzutti 2021-02-10 02:34:19 -05:00
parent 26e8177f1c
commit d9487e6b12
43 changed files with 643 additions and 251 deletions

1
.gitignore vendored
View File

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

View File

@ -31,19 +31,19 @@ RUN apk add --no-cache \
chmod +x yq && mv yq /usr/bin/yq chmod +x yq && mv yq /usr/bin/yq
COPY requirements.txt /opt/mautrix-amp/requirements.txt COPY requirements.txt /opt/mautrix-line/requirements.txt
COPY optional-requirements.txt /opt/mautrix-amp/optional-requirements.txt COPY optional-requirements.txt /opt/mautrix-line/optional-requirements.txt
WORKDIR /opt/mautrix-amp WORKDIR /opt/mautrix-line
RUN apk add --virtual .build-deps python3-dev libffi-dev build-base \ RUN apk add --virtual .build-deps python3-dev libffi-dev build-base \
&& pip3 install -r requirements.txt -r optional-requirements.txt \ && pip3 install -r requirements.txt -r optional-requirements.txt \
&& apk del .build-deps && apk del .build-deps
COPY . /opt/mautrix-amp COPY . /opt/mautrix-line
RUN apk add git && pip3 install .[e2be] && apk del git \ RUN apk add git && pip3 install .[e2be] && apk del git \
# This doesn't make the image smaller, but it's needed so that the `version` command works properly # This doesn't make the image smaller, but it's needed so that the `version` command works properly
&& cp mautrix_amp/example-config.yaml . && rm -rf mautrix_amp && cp mautrix_line/example-config.yaml . && rm -rf mautrix_line
VOLUME /data VOLUME /data
ENV UID=1337 GID=1337 ENV UID=1337 GID=1337
CMD ["/opt/mautrix-amp/docker-run.sh"] CMD ["/opt/mautrix-line/docker-run.sh"]

View File

@ -2,10 +2,10 @@
# Define functions. # Define functions.
function fixperms { function fixperms {
chown -R $UID:$GID /data /opt/mautrix-amp chown -R $UID:$GID /data /opt/mautrix-line
} }
cd /opt/mautrix-amp cd /opt/mautrix-line
if [ ! -f /data/config.yaml ]; then if [ ! -f /data/config.yaml ]; then
cp example-config.yaml /data/config.yaml cp example-config.yaml /data/config.yaml
@ -18,7 +18,7 @@ if [ ! -f /data/config.yaml ]; then
fi fi
if [ ! -f /data/registration.yaml ]; then if [ ! -f /data/registration.yaml ]; then
python3 -m mautrix_amp -g -c /data/config.yaml -r /data/registration.yaml python3 -m mautrix_line -g -c /data/config.yaml -r /data/registration.yaml
echo "Didn't find a registration file." echo "Didn't find a registration file."
echo "Generated one for you." echo "Generated one for you."
echo "Copy that over to synapses app service directory." echo "Copy that over to synapses app service directory."
@ -27,4 +27,4 @@ if [ ! -f /data/registration.yaml ]; then
fi fi
fixperms fixperms
exec su-exec $UID:$GID python3 -m mautrix_amp -c /data/config.yaml exec su-exec $UID:$GID python3 -m mautrix_line -c /data/config.yaml

View File

@ -1,55 +0,0 @@
# mautrix-amp - A very hacky Matrix-SMS bridge based on using Android Messages for Web in Puppeteer
# Copyright (C) 2020 Tulir Asokan
#
# 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/>.
from typing import Optional
import io
import qrcode
import PIL as _
from mautrix.types import MediaMessageEventContent, MessageType, ImageInfo, EventID
from mautrix.bridge.commands import HelpSection, command_handler
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:
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)
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()

View File

@ -1,5 +1,5 @@
# mautrix-amp - A very hacky Matrix-SMS bridge based on using Android Messages for Web in Puppeteer # mautrix-line - A very hacky Matrix-LINE bridge based on using LINE's Chrome Store Extension in Puppeteer
# Copyright (C) 2020 Tulir Asokan # Copyright (C) 2021 Tulir Asokan, Andrew Ferrazzutti
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU Affero General Public License as published by
@ -30,13 +30,13 @@ from . import commands as _
class MessagesBridge(Bridge): class MessagesBridge(Bridge):
module = "mautrix_amp" module = "mautrix_line"
name = "mautrix-amp" name = "mautrix-line"
command = "python -m mautrix-amp" command = "python -m mautrix-line"
description = ("A very hacky Matrix-SMS bridge based on using " description = ("A very hacky Matrix-LINE bridge based on using"
"Android Messages for Web in Puppeteer.") "LINE's Chrome Store Extension in Puppeteer")
repo_url = "https://github.com/tulir/mautrix-amp" repo_url = "https://github.com/tulir/mautrix-line"
real_user_content_key = "net.maunium.amp.puppet" real_user_content_key = "net.maunium.line.puppet"
version = version version = version
markdown_version = linkified_version markdown_version = linkified_version
config_class = Config config_class = Config

View File

@ -0,0 +1,117 @@
# mautrix-line - A very hacky Matrix-LINE bridge based on using LINE's Chrome Store Extension in Puppeteer
# Copyright (C) 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/>.
from typing import Optional, AsyncGenerator, Tuple
import io
import qrcode
import PIL as _
from mautrix.types import TextMessageEventContent, MediaMessageEventContent, MessageType, ImageInfo, EventID
from mautrix.bridge.commands import HelpSection, command_handler
from .typehint import CommandEvent
SECTION_AUTH = HelpSection("Authentication", 10, "")
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 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:
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

@ -1,5 +1,5 @@
# mautrix-amp - A very hacky Matrix-SMS bridge based on using Android Messages for Web in Puppeteer # mautrix-line - A very hacky Matrix-LINE bridge based on using LINE's Chrome Store Extension in Puppeteer
# Copyright (C) 2020 Tulir Asokan # Copyright (C) 2021 Tulir Asokan, Andrew Ferrazzutti
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU Affero General Public License as published by
@ -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:

View File

@ -1,5 +1,5 @@
# mautrix-amp - A very hacky Matrix-SMS bridge based on using Android Messages for Web in Puppeteer # mautrix-line - A very hacky Matrix-LINE bridge based on using LINE's Chrome Store Extension in Puppeteer
# Copyright (C) 2020 Tulir Asokan # Copyright (C) 2021 Tulir Asokan, Andrew Ferrazzutti
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU Affero General Public License as published by

View File

@ -1,5 +1,5 @@
# mautrix-amp - A very hacky Matrix-SMS bridge based on using Android Messages for Web in Puppeteer # mautrix-line - A very hacky Matrix-LINE bridge based on using LINE's Chrome Store Extension in Puppeteer
# Copyright (C) 2020 Tulir Asokan # Copyright (C) 2021 Tulir Asokan, Andrew Ferrazzutti
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU Affero General Public License as published by

View File

@ -1,5 +1,5 @@
# mautrix-amp - A very hacky Matrix-SMS bridge based on using Android Messages for Web in Puppeteer # mautrix-line - A very hacky Matrix-LINE bridge based on using LINE's Chrome Store Extension in Puppeteer
# Copyright (C) 2020 Tulir Asokan # Copyright (C) 2021 Tulir Asokan, Andrew Ferrazzutti
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU Affero General Public License as published by

View File

@ -1,5 +1,5 @@
# mautrix-amp - A very hacky Matrix-SMS bridge based on using Android Messages for Web in Puppeteer # mautrix-line - A very hacky Matrix-LINE bridge based on using LINE's Chrome Store Extension in Puppeteer
# Copyright (C) 2020 Tulir Asokan # Copyright (C) 2021 Tulir Asokan, Andrew Ferrazzutti
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU Affero General Public License as published by

View File

@ -1,5 +1,5 @@
# mautrix-amp - A very hacky Matrix-SMS bridge based on using Android Messages for Web in Puppeteer # mautrix-line - A very hacky Matrix-LINE bridge based on using LINE's Chrome Store Extension in Puppeteer
# Copyright (C) 2020 Tulir Asokan # Copyright (C) 2021 Tulir Asokan, Andrew Ferrazzutti
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU Affero General Public License as published by

View File

@ -1,5 +1,5 @@
# mautrix-amp - A very hacky Matrix-SMS bridge based on using Android Messages for Web in Puppeteer # mautrix-line - A very hacky Matrix-LINE bridge based on using LINE's Chrome Store Extension in Puppeteer
# Copyright (C) 2020 Tulir Asokan # Copyright (C) 2021 Tulir Asokan, Andrew Ferrazzutti
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU Affero General Public License as published by

View File

@ -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"
@ -139,7 +139,7 @@ puppeteer:
# Either unix or tcp # Either unix or tcp
type: unix type: unix
# Only for type: unix # Only for type: unix
path: /var/run/mautrix-amp/puppet.sock path: /var/run/mautrix-line/puppet.sock
# Only for type: tcp # Only for type: tcp
host: localhost host: localhost
port: 29395 port: 29395
@ -152,7 +152,7 @@ logging:
version: 1 version: 1
formatters: formatters:
colored: colored:
(): mautrix_amp.util.ColorFormatter (): mautrix_line.util.ColorFormatter
format: "[%(asctime)s] [%(levelname)s@%(name)s] %(message)s" format: "[%(asctime)s] [%(levelname)s@%(name)s] %(message)s"
normal: normal:
format: "[%(asctime)s] [%(levelname)s@%(name)s] %(message)s" format: "[%(asctime)s] [%(levelname)s@%(name)s] %(message)s"
@ -160,7 +160,7 @@ logging:
file: file:
class: logging.handlers.RotatingFileHandler class: logging.handlers.RotatingFileHandler
formatter: normal formatter: normal
filename: ./mautrix-amp.log filename: ./mautrix-line.log
maxBytes: 10485760 maxBytes: 10485760
backupCount: 10 backupCount: 10
console: console:

View File

@ -19,7 +19,7 @@ def run(cmd):
if os.path.exists(".git") and shutil.which("git"): if os.path.exists(".git") and shutil.which("git"):
try: try:
git_revision = run(["git", "rev-parse", "HEAD"]).strip().decode("ascii") git_revision = run(["git", "rev-parse", "HEAD"]).strip().decode("ascii")
git_revision_url = f"https://github.com/tulir/mautrix-amp/commit/{git_revision}" git_revision_url = f"https://github.com/tulir/mautrix-line/commit/{git_revision}"
git_revision = git_revision[:8] git_revision = git_revision[:8]
except (subprocess.SubprocessError, OSError): except (subprocess.SubprocessError, OSError):
git_revision = "unknown" git_revision = "unknown"

View File

@ -1,5 +1,5 @@
# mautrix-amp - A very hacky Matrix-SMS bridge based on using Android Messages for Web in Puppeteer # mautrix-line - A very hacky Matrix-LINE bridge based on using LINE's Chrome Store Extension in Puppeteer
# Copyright (C) 2020 Tulir Asokan # Copyright (C) 2021 Tulir Asokan, Andrew Ferrazzutti
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU Affero General Public License as published by
@ -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.")

View File

@ -1,5 +1,5 @@
# mautrix-amp - A very hacky Matrix-SMS bridge based on using Android Messages for Web in Puppeteer # mautrix-line - A very hacky Matrix-LINE bridge based on using LINE's Chrome Store Extension in Puppeteer
# Copyright (C) 2020 Tulir Asokan # Copyright (C) 2021 Tulir Asokan, Andrew Ferrazzutti
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU Affero General Public License as published by
@ -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": {

View File

@ -1,5 +1,5 @@
# mautrix-amp - A very hacky Matrix-SMS bridge based on using Android Messages for Web in Puppeteer # mautrix-line - A very hacky Matrix-LINE bridge based on using LINE's Chrome Store Extension in Puppeteer
# Copyright (C) 2020 Tulir Asokan # Copyright (C) 2021 Tulir Asokan, Andrew Ferrazzutti
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU Affero General Public License as published by
@ -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

View File

@ -1,5 +1,5 @@
# mautrix-amp - A very hacky Matrix-SMS bridge based on using Android Messages for Web in Puppeteer # mautrix-line - A very hacky Matrix-LINE bridge based on using LINE's Chrome Store Extension in Puppeteer
# Copyright (C) 2020 Tulir Asokan # Copyright (C) 2021 Tulir Asokan, Andrew Ferrazzutti
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU Affero General Public License as published by
@ -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)

View File

@ -1,5 +1,5 @@
# mautrix-amp - A very hacky Matrix-SMS bridge based on using Android Messages for Web in Puppeteer # mautrix-line - A very hacky Matrix-LINE bridge based on using LINE's Chrome Store Extension in Puppeteer
# Copyright (C) 2020 Tulir Asokan # Copyright (C) 2021 Tulir Asokan, Andrew Ferrazzutti
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU Affero General Public License as published by

View File

@ -1,5 +1,5 @@
# mautrix-amp - A very hacky Matrix-SMS bridge based on using Android Messages for Web in Puppeteer # mautrix-line - A very hacky Matrix-LINE bridge based on using LINE's Chrome Store Extension in Puppeteer
# Copyright (C) 2020 Tulir Asokan # Copyright (C) 2021 Tulir Asokan, Andrew Ferrazzutti
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU Affero General Public License as published by

View File

@ -1,5 +1,5 @@
# mautrix-amp - A very hacky Matrix-SMS bridge based on using Android Messages for Web in Puppeteer # mautrix-line - A very hacky Matrix-LINE bridge based on using LINE's Chrome Store Extension in Puppeteer
# Copyright (C) 2020 Tulir Asokan # Copyright (C) 2021 Tulir Asokan, Andrew Ferrazzutti
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU Affero General Public License as published by
@ -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)

View File

@ -1,5 +1,5 @@
# mautrix-amp - A very hacky Matrix-SMS bridge based on using Android Messages for Web in Puppeteer # mautrix-line - A very hacky Matrix-LINE bridge based on using LINE's Chrome Store Extension in Puppeteer
# Copyright (C) 2020 Tulir Asokan # Copyright (C) 2021 Tulir Asokan, Andrew Ferrazzutti
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU Affero General Public License as published by

View File

@ -1,5 +1,5 @@
# mautrix-amp - A very hacky Matrix-SMS bridge based on using Android Messages for Web in Puppeteer # mautrix-line - A very hacky Matrix-LINE bridge based on using LINE's Chrome Store Extension in Puppeteer
# Copyright (C) 2020 Tulir Asokan # Copyright (C) 2021 Tulir Asokan, Andrew Ferrazzutti
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU Affero General Public License as published by
@ -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():

View File

@ -7,11 +7,11 @@ RUN echo $'\
RUN apk add --no-cache chromium@edge RUN apk add --no-cache chromium@edge
WORKDIR /opt/mautrix-amp/puppet WORKDIR /opt/mautrix-line/puppet
RUN chown node:node /opt/mautrix-amp/puppet RUN chown node:node /opt/mautrix-line/puppet
USER node USER node
COPY package.json yarn.lock ./ COPY package.json yarn.lock ./
RUN yarn --production && rm -rf node_modules/puppeteer/.local-chromium RUN yarn --production && rm -rf node_modules/puppeteer/.local-chromium
COPY . /opt/mautrix-amp/puppet COPY . /opt/mautrix-line/puppet
CMD ["yarn", "start", "--config", "/data/config.json", "--browser", "/usr/lib/chromium/chrome", "--no-sandbox"] CMD ["yarn", "start", "--config", "/data/config.json", "--browser", "/usr/lib/chromium/chrome", "--no-sandbox"]

View File

@ -4,5 +4,6 @@
"path": "/var/run/mautrix-amp/puppet.sock" "path": "/var/run/mautrix-amp/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"
} }

View File

@ -1,23 +1,23 @@
{ {
"name": "mautrix-amp-puppeteer", "name": "mautrix-line-puppeteer",
"version": "0.1.0", "version": "0.1.0",
"description": "Puppeteer module for mautrix-amp", "description": "Puppeteer module for mautrix-line",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git+https://mau.dev/tulir/mautrix-amp.git" "url": "git+https://mau.dev/tulir/mautrix-line.git"
}, },
"type": "module", "type": "module",
"main": "src/main.js", "main": "src/main.js",
"author": "Tulir Asokan <tulir@maunium.net>", "author": "Tulir Asokan <tulir@maunium.net>",
"license": "AGPL-3.0-or-later", "license": "AGPL-3.0-or-later",
"homepage": "https://mau.dev/tulir/mautrix-amp", "homepage": "https://mau.dev/tulir/mautrix-line",
"scripts": { "scripts": {
"start": "node ./src/main.js" "start": "node ./src/main.js"
}, },
"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",

View File

@ -1,5 +1,5 @@
// mautrix-amp - A very hacky Matrix-SMS bridge based on using Android Messages for Web in Puppeteer // mautrix-line - A very hacky Matrix-LINE bridge based on using LINE's Chrome Store Extension in Puppeteer
// Copyright (C) 2020 Tulir Asokan // Copyright (C) 2021 Tulir Asokan, Andrew Ferrazzutti
// //
// This program is free software: you can redistribute it and/or modify // 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 // it under the terms of the GNU Affero General Public License as published by

View File

@ -1,5 +1,5 @@
// mautrix-amp - A very hacky Matrix-SMS bridge based on using Android Messages for Web in Puppeteer // mautrix-line - A very hacky Matrix-LINE bridge based on using LINE's Chrome Store Extension in Puppeteer
// Copyright (C) 2020 Tulir Asokan // Copyright (C) 2021 Tulir Asokan, Andrew Ferrazzutti
// //
// This program is free software: you can redistribute it and/or modify // 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 // it under the terms of the GNU Affero General Public License as published by
@ -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(),

View File

@ -1,5 +1,5 @@
// mautrix-amp - A very hacky Matrix-SMS bridge based on using Android Messages for Web in Puppeteer // mautrix-line - A very hacky Matrix-LINE bridge based on using LINE's Chrome Store Extension in Puppeteer
// Copyright (C) 2020 Tulir Asokan // Copyright (C) 2021 Tulir Asokan, Andrew Ferrazzutti
// //
// This program is free software: you can redistribute it and/or modify // 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 // it under the terms of the GNU Affero General Public License as published by
@ -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
} }
} }
} }

View File

@ -1,5 +1,5 @@
// mautrix-amp - A very hacky Matrix-LINE bridge based on using LINE's Chrome Store Extension in Puppeteer // mautrix-line - A very hacky Matrix-LINE bridge based on using LINE's Chrome Store Extension in Puppeteer
// Copyright (C) 2020 Tulir Asokan, Andrew Ferrazzutti // Copyright (C) 2021 Tulir Asokan, Andrew Ferrazzutti
// //
// This program is free software: you can redistribute it and/or modify // 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 // it under the terms of the GNU Affero General Public License as published by
@ -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)

View File

@ -1,5 +1,5 @@
// mautrix-amp - A very hacky Matrix-SMS bridge based on using Android Messages for Web in Puppeteer // mautrix-line - A very hacky Matrix-LINE bridge based on using LINE's Chrome Store Extension in Puppeteer
// Copyright (C) 2020 Tulir Asokan // Copyright (C) 2021 Tulir Asokan, Andrew Ferrazzutti
// //
// This program is free software: you can redistribute it and/or modify // 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 // it under the terms of the GNU Affero General Public License as published by
@ -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("Fetching current QR code") this.log("Running QR login")
const currentQR = await this.page.$eval(qrSelector, const qrButton = await this.page.waitForSelector("#login_qr_btn")
element => element.getAttribute("data-qr-code")) 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) this._receiveQRChange(currentQR)
this.log("Adding QR observer")
await this.page.$eval(qrSelector, await this.page.evaluate(
element => window.__mautrixController.addQRObserver(element)) element => window.__mautrixController.addQRChangeObserver(element), qrElement)
this.log("Waiting for login") await this.page.evaluate(
await this.page.waitForSelector("mws-conversations-list .conv-container", { element => window.__mautrixController.addQRAppearObserver(element), loginContentArea)
visible: true,
timeout: 0, break
}) }
this.log("Removing QR observer") case "email": {
await this.page.evaluate(() => window.__mautrixController.removeQRObserver()) 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() 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)
}
} }

View File

@ -1,5 +1,5 @@
// mautrix-amp - A very hacky Matrix-SMS bridge based on using Android Messages for Web in Puppeteer // mautrix-line - A very hacky Matrix-LINE bridge based on using LINE's Chrome Store Extension in Puppeteer
// Copyright (C) 2020 Tulir Asokan // Copyright (C) 2021 Tulir Asokan, Andrew Ferrazzutti
// //
// This program is free software: you can redistribute it and/or modify // 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 // it under the terms of the GNU Affero General Public License as published by

View File

@ -1,5 +1,5 @@
// mautrix-amp - A very hacky Matrix-SMS bridge based on using Android Messages for Web in Puppeteer // mautrix-line - A very hacky Matrix-LINE bridge based on using LINE's Chrome Store Extension in Puppeteer
// Copyright (C) 2020 Tulir Asokan // Copyright (C) 2021 Tulir Asokan, Andrew Ferrazzutti
// //
// This program is free software: you can redistribute it and/or modify // 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 // it under the terms of the GNU Affero General Public License as published by

View File

@ -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"