From d9487e6b12309133104dbec174e0346050828f7c Mon Sep 17 00:00:00 2001 From: Andrew Ferrazzutti Date: Wed, 10 Feb 2021 02:34:19 -0500 Subject: [PATCH] More changes TODO: Rebase for cleaner changes --- .gitignore | 1 + Dockerfile | 12 +- docker-run.sh | 8 +- mautrix_amp/commands/auth.py | 55 ---- {mautrix_amp => mautrix_line}/__init__.py | 0 {mautrix_amp => mautrix_line}/__main__.py | 18 +- .../commands/__init__.py | 0 mautrix_line/commands/auth.py | 117 ++++++++ .../commands/conn.py | 6 +- .../commands/typehint.py | 0 {mautrix_amp => mautrix_line}/config.py | 4 +- {mautrix_amp => mautrix_line}/db/__init__.py | 0 {mautrix_amp => mautrix_line}/db/message.py | 4 +- {mautrix_amp => mautrix_line}/db/portal.py | 4 +- {mautrix_amp => mautrix_line}/db/puppet.py | 4 +- {mautrix_amp => mautrix_line}/db/upgrade.py | 4 +- {mautrix_amp => mautrix_line}/db/user.py | 4 +- .../example-config.yaml | 20 +- {mautrix_amp => mautrix_line}/get_version.py | 2 +- {mautrix_amp => mautrix_line}/matrix.py | 6 +- {mautrix_amp => mautrix_line}/portal.py | 10 +- {mautrix_amp => mautrix_line}/puppet.py | 9 +- {mautrix_amp => mautrix_line}/rpc/__init__.py | 0 {mautrix_amp => mautrix_line}/rpc/client.py | 51 +++- {mautrix_amp => mautrix_line}/rpc/rpc.py | 4 +- {mautrix_amp => mautrix_line}/rpc/types.py | 4 +- {mautrix_amp => mautrix_line}/user.py | 7 +- .../util/__init__.py | 0 .../util/color_log.py | 4 +- {mautrix_amp => mautrix_line}/version.py | 0 {mautrix_amp => mautrix_line}/web/__init__.py | 0 .../web/provisioning_api.py | 12 +- puppet/Dockerfile | 6 +- puppet/example-config.json | 3 +- puppet/package.json | 10 +- puppet/src/api.js | 4 +- puppet/src/client.js | 25 +- puppet/src/contentscript.js | 155 +++++++++- puppet/src/main.js | 5 +- puppet/src/puppet.js | 272 ++++++++++++++---- puppet/src/taskqueue.js | 4 +- puppet/src/util.js | 4 +- puppet/yarn.lock | 36 +-- 43 files changed, 643 insertions(+), 251 deletions(-) delete mode 100644 mautrix_amp/commands/auth.py rename {mautrix_amp => mautrix_line}/__init__.py (100%) rename {mautrix_amp => mautrix_line}/__main__.py (88%) rename {mautrix_amp => mautrix_line}/commands/__init__.py (100%) create mode 100644 mautrix_line/commands/auth.py rename {mautrix_amp => mautrix_line}/commands/conn.py (89%) rename {mautrix_amp => mautrix_line}/commands/typehint.py (100%) rename {mautrix_amp => mautrix_line}/config.py (95%) rename {mautrix_amp => mautrix_line}/db/__init__.py (100%) rename {mautrix_amp => mautrix_line}/db/message.py (94%) rename {mautrix_amp => mautrix_line}/db/portal.py (94%) rename {mautrix_amp => mautrix_line}/db/puppet.py (90%) rename {mautrix_amp => mautrix_line}/db/upgrade.py (91%) rename {mautrix_amp => mautrix_line}/db/user.py (91%) rename {mautrix_amp => mautrix_line}/example-config.yaml (94%) rename {mautrix_amp => mautrix_line}/get_version.py (91%) rename {mautrix_amp => mautrix_line}/matrix.py (89%) rename {mautrix_amp => mautrix_line}/portal.py (98%) rename {mautrix_amp => mautrix_line}/puppet.py (92%) rename {mautrix_amp => mautrix_line}/rpc/__init__.py (100%) rename {mautrix_amp => mautrix_line}/rpc/client.py (62%) rename {mautrix_amp => mautrix_line}/rpc/rpc.py (97%) rename {mautrix_amp => mautrix_line}/rpc/types.py (90%) rename {mautrix_amp => mautrix_line}/user.py (96%) rename {mautrix_amp => mautrix_line}/util/__init__.py (100%) rename {mautrix_amp => mautrix_line}/util/color_log.py (87%) rename {mautrix_amp => mautrix_line}/version.py (100%) rename {mautrix_amp => mautrix_line}/web/__init__.py (100%) rename {mautrix_amp => mautrix_line}/web/provisioning_api.py (92%) diff --git a/.gitignore b/.gitignore index 363f7dd..08e4297 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ __pycache__ /.eggs profiles +puppet/extension_files /config.yaml /registration.yaml diff --git a/Dockerfile b/Dockerfile index 3c07683..20b6f65 100644 --- a/Dockerfile +++ b/Dockerfile @@ -31,19 +31,19 @@ RUN apk add --no-cache \ chmod +x yq && mv yq /usr/bin/yq -COPY requirements.txt /opt/mautrix-amp/requirements.txt -COPY optional-requirements.txt /opt/mautrix-amp/optional-requirements.txt -WORKDIR /opt/mautrix-amp +COPY requirements.txt /opt/mautrix-line/requirements.txt +COPY optional-requirements.txt /opt/mautrix-line/optional-requirements.txt +WORKDIR /opt/mautrix-line RUN apk add --virtual .build-deps python3-dev libffi-dev build-base \ && pip3 install -r requirements.txt -r optional-requirements.txt \ && apk del .build-deps -COPY . /opt/mautrix-amp +COPY . /opt/mautrix-line 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 - && cp mautrix_amp/example-config.yaml . && rm -rf mautrix_amp + && cp mautrix_line/example-config.yaml . && rm -rf mautrix_line VOLUME /data ENV UID=1337 GID=1337 -CMD ["/opt/mautrix-amp/docker-run.sh"] +CMD ["/opt/mautrix-line/docker-run.sh"] diff --git a/docker-run.sh b/docker-run.sh index 80f4ee3..98f46fe 100755 --- a/docker-run.sh +++ b/docker-run.sh @@ -2,10 +2,10 @@ # Define functions. 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 cp example-config.yaml /data/config.yaml @@ -18,7 +18,7 @@ if [ ! -f /data/config.yaml ]; then fi 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 "Generated one for you." echo "Copy that over to synapses app service directory." @@ -27,4 +27,4 @@ if [ ! -f /data/registration.yaml ]; then fi 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 diff --git a/mautrix_amp/commands/auth.py b/mautrix_amp/commands/auth.py deleted file mode 100644 index 7a55257..0000000 --- a/mautrix_amp/commands/auth.py +++ /dev/null @@ -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 . -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() diff --git a/mautrix_amp/__init__.py b/mautrix_line/__init__.py similarity index 100% rename from mautrix_amp/__init__.py rename to mautrix_line/__init__.py diff --git a/mautrix_amp/__main__.py b/mautrix_line/__main__.py similarity index 88% rename from mautrix_amp/__main__.py rename to mautrix_line/__main__.py index 770c343..4764e76 100644 --- a/mautrix_amp/__main__.py +++ b/mautrix_line/__main__.py @@ -1,5 +1,5 @@ -# mautrix-amp - A very hacky Matrix-SMS bridge based on using Android Messages for Web in Puppeteer -# Copyright (C) 2020 Tulir Asokan +# 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 @@ -30,13 +30,13 @@ from . import commands as _ class MessagesBridge(Bridge): - module = "mautrix_amp" - name = "mautrix-amp" - command = "python -m mautrix-amp" - description = ("A very hacky Matrix-SMS bridge based on using " - "Android Messages for Web in Puppeteer.") - repo_url = "https://github.com/tulir/mautrix-amp" - real_user_content_key = "net.maunium.amp.puppet" + module = "mautrix_line" + name = "mautrix-line" + command = "python -m mautrix-line" + description = ("A very hacky Matrix-LINE bridge based on using" + "LINE's Chrome Store Extension in Puppeteer") + repo_url = "https://github.com/tulir/mautrix-line" + real_user_content_key = "net.maunium.line.puppet" version = version markdown_version = linkified_version config_class = Config diff --git a/mautrix_amp/commands/__init__.py b/mautrix_line/commands/__init__.py similarity index 100% rename from mautrix_amp/commands/__init__.py rename to mautrix_line/commands/__init__.py diff --git a/mautrix_line/commands/auth.py b/mautrix_line/commands/auth.py new file mode 100644 index 0000000..308f913 --- /dev/null +++ b/mautrix_line/commands/auth.py @@ -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 . +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 `") + 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) diff --git a/mautrix_amp/commands/conn.py b/mautrix_line/commands/conn.py similarity index 89% rename from mautrix_amp/commands/conn.py rename to mautrix_line/commands/conn.py index bdc79f6..6a935ff 100644 --- a/mautrix_amp/commands/conn.py +++ b/mautrix_line/commands/conn.py @@ -1,5 +1,5 @@ -# mautrix-amp - A very hacky Matrix-SMS bridge based on using Android Messages for Web in Puppeteer -# Copyright (C) 2020 Tulir Asokan +# 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 @@ -29,7 +29,7 @@ async def set_notice_room(evt: CommandEvent) -> None: @command_handler(needs_auth=False, management_only=True, help_section=SECTION_CONNECTION, - help_text="Check if you're logged into Android Messages") + help_text="Check if you're logged into LINE") async def ping(evt: CommandEvent) -> None: status = await evt.sender.client.start() if status.is_logged_in: diff --git a/mautrix_amp/commands/typehint.py b/mautrix_line/commands/typehint.py similarity index 100% rename from mautrix_amp/commands/typehint.py rename to mautrix_line/commands/typehint.py diff --git a/mautrix_amp/config.py b/mautrix_line/config.py similarity index 95% rename from mautrix_amp/config.py rename to mautrix_line/config.py index 5f09a8a..d18cb0a 100644 --- a/mautrix_amp/config.py +++ b/mautrix_line/config.py @@ -1,5 +1,5 @@ -# mautrix-amp - A very hacky Matrix-SMS bridge based on using Android Messages for Web in Puppeteer -# Copyright (C) 2020 Tulir Asokan +# 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 diff --git a/mautrix_amp/db/__init__.py b/mautrix_line/db/__init__.py similarity index 100% rename from mautrix_amp/db/__init__.py rename to mautrix_line/db/__init__.py diff --git a/mautrix_amp/db/message.py b/mautrix_line/db/message.py similarity index 94% rename from mautrix_amp/db/message.py rename to mautrix_line/db/message.py index b214e23..87db5a5 100644 --- a/mautrix_amp/db/message.py +++ b/mautrix_line/db/message.py @@ -1,5 +1,5 @@ -# mautrix-amp - A very hacky Matrix-SMS bridge based on using Android Messages for Web in Puppeteer -# Copyright (C) 2020 Tulir Asokan +# 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 diff --git a/mautrix_amp/db/portal.py b/mautrix_line/db/portal.py similarity index 94% rename from mautrix_amp/db/portal.py rename to mautrix_line/db/portal.py index ebe5915..b35a97c 100644 --- a/mautrix_amp/db/portal.py +++ b/mautrix_line/db/portal.py @@ -1,5 +1,5 @@ -# mautrix-amp - A very hacky Matrix-SMS bridge based on using Android Messages for Web in Puppeteer -# Copyright (C) 2020 Tulir Asokan +# 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 diff --git a/mautrix_amp/db/puppet.py b/mautrix_line/db/puppet.py similarity index 90% rename from mautrix_amp/db/puppet.py rename to mautrix_line/db/puppet.py index 41f129e..774fedd 100644 --- a/mautrix_amp/db/puppet.py +++ b/mautrix_line/db/puppet.py @@ -1,5 +1,5 @@ -# mautrix-amp - A very hacky Matrix-SMS bridge based on using Android Messages for Web in Puppeteer -# Copyright (C) 2020 Tulir Asokan +# 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 diff --git a/mautrix_amp/db/upgrade.py b/mautrix_line/db/upgrade.py similarity index 91% rename from mautrix_amp/db/upgrade.py rename to mautrix_line/db/upgrade.py index f3f531a..949d5cf 100644 --- a/mautrix_amp/db/upgrade.py +++ b/mautrix_line/db/upgrade.py @@ -1,5 +1,5 @@ -# mautrix-amp - A very hacky Matrix-SMS bridge based on using Android Messages for Web in Puppeteer -# Copyright (C) 2020 Tulir Asokan +# 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 diff --git a/mautrix_amp/db/user.py b/mautrix_line/db/user.py similarity index 91% rename from mautrix_amp/db/user.py rename to mautrix_line/db/user.py index 55e4941..de3ac86 100644 --- a/mautrix_amp/db/user.py +++ b/mautrix_line/db/user.py @@ -1,5 +1,5 @@ -# mautrix-amp - A very hacky Matrix-SMS bridge based on using Android Messages for Web in Puppeteer -# Copyright (C) 2020 Tulir Asokan +# 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 diff --git a/mautrix_amp/example-config.yaml b/mautrix_line/example-config.yaml similarity index 94% rename from mautrix_amp/example-config.yaml rename to mautrix_line/example-config.yaml index 57bbf13..2eeaa93 100644 --- a/mautrix_amp/example-config.yaml +++ b/mautrix_line/example-config.yaml @@ -39,18 +39,18 @@ appservice: shared_secret: generate # The unique ID of this appservice. - id: amp + id: line # Username of the appservice bot. - bot_username: ampbot + bot_username: linebot # Display name and avatar for bot. Set to "remove" to remove display name/avatar, leave empty # to leave display name/avatar as-is. - bot_displayname: Android Messages bridge bot + bot_displayname: LINE bridge bot bot_avatar: mxc://maunium.net/VuvevQiMRlOxuBVMBNEZZrxi # Community ID for bridged users (changes registration file) and rooms. # Must be created manually. # - # Example: "+amp:example.com". Set to false to disable. + # Example: "+line:example.com". Set to false to disable. community_id: false # Authentication tokens for AS <-> HS communication. Autogenerated; do not modify. @@ -66,11 +66,11 @@ metrics: bridge: # Localpart template of MXIDs for remote users. # {userid} is replaced with the user ID (phone or name converted into a mxid-friendly format). - username_template: "amp_{userid}" + username_template: "line_{userid}" # Displayname template for remote users. # {displayname} is replaced with the display name of the user. # {phone} is replaced with the phone number or name of the user. - displayname_template: "{displayname} (SMS)" + displayname_template: "{displayname} (LINE)" # Maximum length of displayname displayname_max_length: 100 @@ -129,7 +129,7 @@ bridge: resend_bridge_info: false # The prefix for commands. Only required in non-management rooms. - command_prefix: "!am" + command_prefix: "!line" # This bridge only supports a single user user: "@admin:example.com" @@ -139,7 +139,7 @@ puppeteer: # Either unix or tcp type: unix # Only for type: unix - path: /var/run/mautrix-amp/puppet.sock + path: /var/run/mautrix-line/puppet.sock # Only for type: tcp host: localhost port: 29395 @@ -152,7 +152,7 @@ logging: version: 1 formatters: colored: - (): mautrix_amp.util.ColorFormatter + (): mautrix_line.util.ColorFormatter format: "[%(asctime)s] [%(levelname)s@%(name)s] %(message)s" normal: format: "[%(asctime)s] [%(levelname)s@%(name)s] %(message)s" @@ -160,7 +160,7 @@ logging: file: class: logging.handlers.RotatingFileHandler formatter: normal - filename: ./mautrix-amp.log + filename: ./mautrix-line.log maxBytes: 10485760 backupCount: 10 console: diff --git a/mautrix_amp/get_version.py b/mautrix_line/get_version.py similarity index 91% rename from mautrix_amp/get_version.py rename to mautrix_line/get_version.py index 1c740d8..ea44856 100644 --- a/mautrix_amp/get_version.py +++ b/mautrix_line/get_version.py @@ -19,7 +19,7 @@ def run(cmd): if os.path.exists(".git") and shutil.which("git"): try: 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] except (subprocess.SubprocessError, OSError): git_revision = "unknown" diff --git a/mautrix_amp/matrix.py b/mautrix_line/matrix.py similarity index 89% rename from mautrix_amp/matrix.py rename to mautrix_line/matrix.py index 4c5e8ea..4d1a3cb 100644 --- a/mautrix_amp/matrix.py +++ b/mautrix_line/matrix.py @@ -1,5 +1,5 @@ -# mautrix-amp - A very hacky Matrix-SMS bridge based on using Android Messages for Web in Puppeteer -# Copyright (C) 2020 Tulir Asokan +# 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 @@ -47,4 +47,4 @@ class MatrixHandler(BaseMatrixHandler): inviter.notice_room = room_id await inviter.update() await self.az.intent.send_notice(room_id, "This room has been marked as your " - "Android Messages bridge notice room.") + "LINE bridge notice room.") diff --git a/mautrix_amp/portal.py b/mautrix_line/portal.py similarity index 98% rename from mautrix_amp/portal.py rename to mautrix_line/portal.py index ce0b751..4a1fa2d 100644 --- a/mautrix_amp/portal.py +++ b/mautrix_line/portal.py @@ -1,5 +1,5 @@ -# mautrix-amp - A very hacky Matrix-SMS bridge based on using Android Messages for Web in Puppeteer -# Copyright (C) 2020 Tulir Asokan +# 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 @@ -268,7 +268,7 @@ class Portal(DBPortal, BasePortal): @property def bridge_info_state_key(self) -> str: - return f"net.maunium.amp://androidmessages/{self.chat_id}" + return f"net.maunium.line://line/{self.chat_id}" @property def bridge_info(self) -> Dict[str, Any]: @@ -276,8 +276,8 @@ class Portal(DBPortal, BasePortal): "bridgebot": self.az.bot_mxid, "creator": self.main_intent.mxid, "protocol": { - "id": "androidmessages", - "displayname": "Android Messages", + "id": "line", + "displayname": "LINE", "avatar_url": self.config["appservice.bot_avatar"], }, "channel": { diff --git a/mautrix_amp/puppet.py b/mautrix_line/puppet.py similarity index 92% rename from mautrix_amp/puppet.py rename to mautrix_line/puppet.py index da47cbe..2301d28 100644 --- a/mautrix_amp/puppet.py +++ b/mautrix_line/puppet.py @@ -1,5 +1,5 @@ -# mautrix-amp - A very hacky Matrix-SMS bridge based on using Android Messages for Web in Puppeteer -# Copyright (C) 2020 Tulir Asokan +# 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 @@ -56,8 +56,9 @@ class Puppet(DBPuppet, BasePuppet): cls.mxid_template = SimpleTemplate(cls.config["bridge.username_template"], "userid", prefix="@", suffix=f":{cls.hs_domain}", type=str) secret = cls.config["bridge.login_shared_secret"] - cls.login_shared_secret_map[cls.hs_domain] = secret.encode("utf-8") if secret else None - cls.login_device_name = "Android Messages Bridge" + if secret: + cls.login_shared_secret_map[cls.hs_domain] = secret.encode("utf-8") + cls.login_device_name = "LINE Bridge" async def update_info(self, info: Participant) -> None: update = False diff --git a/mautrix_amp/rpc/__init__.py b/mautrix_line/rpc/__init__.py similarity index 100% rename from mautrix_amp/rpc/__init__.py rename to mautrix_line/rpc/__init__.py diff --git a/mautrix_amp/rpc/client.py b/mautrix_line/rpc/client.py similarity index 62% rename from mautrix_amp/rpc/client.py rename to mautrix_line/rpc/client.py index 12b243e..e5232ed 100644 --- a/mautrix_amp/rpc/client.py +++ b/mautrix_line/rpc/client.py @@ -1,5 +1,5 @@ -# mautrix-amp - A very hacky Matrix-SMS bridge based on using Android Messages for Web in Puppeteer -# Copyright (C) 2020 Tulir Asokan +# 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 @@ -13,20 +13,17 @@ # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from typing import AsyncGenerator, TypedDict, List, Dict, Callable, Awaitable, Any +from typing import AsyncGenerator, TypedDict, List, Tuple, Dict, Callable, Awaitable, Any from collections import deque import asyncio from .rpc import RPCClient from .types import ChatListInfo, ChatInfo, Message, StartStatus +from mautrix_line.rpc.types import RPCError -class QRCommand(TypedDict): - url: str - - -class LoginComplete(Exception): - pass +class LoginCommand(TypedDict): + content: str class Client(RPCClient): @@ -66,22 +63,48 @@ class Client(RPCClient): self.add_event_handler("message", wrapper) - async def login(self) -> AsyncGenerator[str, None]: + # TODO Type hint for sender + async def login(self, sender, **login_data) -> AsyncGenerator[Tuple[str, str], None]: + login_data["login_type"] = sender.command_status["login_type"] + data = deque() event = asyncio.Event() - async def qr_handler(req: QRCommand) -> None: - data.append(req["url"]) + async def qr_handler(req: LoginCommand) -> None: + data.append(("qr", req["url"])) event.set() + async def pin_handler(req: LoginCommand) -> None: + data.append(("pin", req["pin"])) + event.set() + + async def failure_handler(req: LoginCommand) -> None: + data.append(("failure", req["reason"])) + event.set() + + async def cancel_watcher() -> None: + try: + while sender.command_status is not None: + await asyncio.sleep(1) + await self._raw_request("cancel_login") + except asyncio.CancelledError: + pass + cancel_watcher_task = asyncio.create_task(cancel_watcher()) + def login_handler(_fut: asyncio.Future) -> None: + cancel_watcher_task.cancel() + e = _fut.exception() + if e is not None: + data.append(("error", str(e))) data.append(None) event.set() - login_future = await self._raw_request("login") + login_future = await self._raw_request("login", **login_data) login_future.add_done_callback(login_handler) self.add_event_handler("qr", qr_handler) + self.add_event_handler("pin", pin_handler) + self.add_event_handler("failure", failure_handler) try: while True: await event.wait() @@ -93,3 +116,5 @@ class Client(RPCClient): event.clear() finally: self.remove_event_handler("qr", qr_handler) + self.remove_event_handler("pin", pin_handler) + self.remove_event_handler("failure", failure_handler) diff --git a/mautrix_amp/rpc/rpc.py b/mautrix_line/rpc/rpc.py similarity index 97% rename from mautrix_amp/rpc/rpc.py rename to mautrix_line/rpc/rpc.py index 077e0ba..0603f1e 100644 --- a/mautrix_amp/rpc/rpc.py +++ b/mautrix_line/rpc/rpc.py @@ -1,5 +1,5 @@ -# mautrix-amp - A very hacky Matrix-SMS bridge based on using Android Messages for Web in Puppeteer -# Copyright (C) 2020 Tulir Asokan +# 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 diff --git a/mautrix_amp/rpc/types.py b/mautrix_line/rpc/types.py similarity index 90% rename from mautrix_amp/rpc/types.py rename to mautrix_line/rpc/types.py index cf8138a..9705cd6 100644 --- a/mautrix_amp/rpc/types.py +++ b/mautrix_line/rpc/types.py @@ -1,5 +1,5 @@ -# mautrix-amp - A very hacky Matrix-SMS bridge based on using Android Messages for Web in Puppeteer -# Copyright (C) 2020 Tulir Asokan +# 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 diff --git a/mautrix_amp/user.py b/mautrix_line/user.py similarity index 96% rename from mautrix_amp/user.py rename to mautrix_line/user.py index 2584fcc..934dc90 100644 --- a/mautrix_amp/user.py +++ b/mautrix_line/user.py @@ -1,5 +1,5 @@ -# mautrix-amp - A very hacky Matrix-SMS bridge based on using Android Messages for Web in Puppeteer -# Copyright (C) 2020 Tulir Asokan +# 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 @@ -30,7 +30,7 @@ from . import puppet as pu, portal as po if TYPE_CHECKING: from .__main__ import MessagesBridge -METRIC_CONNECTED = Gauge("bridge_connected", "Users connected to Android Messages") +METRIC_CONNECTED = Gauge("bridge_connected", "Users connected to LINE") class User(DBUser, BaseUser): @@ -49,6 +49,7 @@ class User(DBUser, BaseUser): def __init__(self, mxid: UserID, notice_room: Optional[RoomID] = None) -> None: super().__init__(mxid=mxid, notice_room=notice_room) self._notice_room_lock = asyncio.Lock() + self.command_status = None self.is_whitelisted = self.is_admin = self.config["bridge.user"] == mxid self.log = self.log.getChild(self.mxid) self._metric_value = defaultdict(lambda: False) diff --git a/mautrix_amp/util/__init__.py b/mautrix_line/util/__init__.py similarity index 100% rename from mautrix_amp/util/__init__.py rename to mautrix_line/util/__init__.py diff --git a/mautrix_amp/util/color_log.py b/mautrix_line/util/color_log.py similarity index 87% rename from mautrix_amp/util/color_log.py rename to mautrix_line/util/color_log.py index 29e30f3..968cc0a 100644 --- a/mautrix_amp/util/color_log.py +++ b/mautrix_line/util/color_log.py @@ -1,5 +1,5 @@ -# mautrix-amp - A very hacky Matrix-SMS bridge based on using Android Messages for Web in Puppeteer -# Copyright (C) 2020 Tulir Asokan +# 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 diff --git a/mautrix_amp/version.py b/mautrix_line/version.py similarity index 100% rename from mautrix_amp/version.py rename to mautrix_line/version.py diff --git a/mautrix_amp/web/__init__.py b/mautrix_line/web/__init__.py similarity index 100% rename from mautrix_amp/web/__init__.py rename to mautrix_line/web/__init__.py diff --git a/mautrix_amp/web/provisioning_api.py b/mautrix_line/web/provisioning_api.py similarity index 92% rename from mautrix_amp/web/provisioning_api.py rename to mautrix_line/web/provisioning_api.py index e3da47e..7398595 100644 --- a/mautrix_amp/web/provisioning_api.py +++ b/mautrix_line/web/provisioning_api.py @@ -1,5 +1,5 @@ -# mautrix-amp - A very hacky Matrix-SMS bridge based on using Android Messages for Web in Puppeteer -# Copyright (C) 2020 Tulir Asokan +# 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 @@ -64,8 +64,8 @@ class ProvisioningAPI: return None for part in auth_parts: part = part.strip() - if part.startswith("net.maunium.amp.auth-"): - return part[len("net.maunium.amp.auth-"):] + if part.startswith("net.maunium.line.auth-"): + return part[len("net.maunium.line.auth-"):] return None def check_token(self, request: web.Request) -> Awaitable['u.User']: @@ -94,7 +94,7 @@ class ProvisioningAPI: user = await self.check_token(request) data = { "mxid": user.mxid, - "amp": { + "line": { "connected": True, } if await user.is_logged_in() else None, } @@ -107,7 +107,7 @@ class ProvisioningAPI: if status.is_logged_in: raise web.HTTPConflict(text='{"error": "Already logged in"}', headers=self._headers) - ws = web.WebSocketResponse(protocols=["net.maunium.amp.login"]) + ws = web.WebSocketResponse(protocols=["net.maunium.line.login"]) await ws.prepare(request) try: async for url in user.client.login(): diff --git a/puppet/Dockerfile b/puppet/Dockerfile index e9bc1ff..e00d63b 100644 --- a/puppet/Dockerfile +++ b/puppet/Dockerfile @@ -7,11 +7,11 @@ RUN echo $'\ RUN apk add --no-cache chromium@edge -WORKDIR /opt/mautrix-amp/puppet -RUN chown node:node /opt/mautrix-amp/puppet +WORKDIR /opt/mautrix-line/puppet +RUN chown node:node /opt/mautrix-line/puppet USER node COPY package.json yarn.lock ./ 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"] diff --git a/puppet/example-config.json b/puppet/example-config.json index 85b1a28..41cfd46 100644 --- a/puppet/example-config.json +++ b/puppet/example-config.json @@ -4,5 +4,6 @@ "path": "/var/run/mautrix-amp/puppet.sock" }, "profile_dir": "./profiles", - "url": "chrome-extension:///index.html" + "url": "chrome-extension:///index.html", + "extension_dir": "./extension_files" } diff --git a/puppet/package.json b/puppet/package.json index 25fa99f..f0c24ec 100644 --- a/puppet/package.json +++ b/puppet/package.json @@ -1,23 +1,23 @@ { - "name": "mautrix-amp-puppeteer", + "name": "mautrix-line-puppeteer", "version": "0.1.0", - "description": "Puppeteer module for mautrix-amp", + "description": "Puppeteer module for mautrix-line", "repository": { "type": "git", - "url": "git+https://mau.dev/tulir/mautrix-amp.git" + "url": "git+https://mau.dev/tulir/mautrix-line.git" }, "type": "module", "main": "src/main.js", "author": "Tulir Asokan ", "license": "AGPL-3.0-or-later", - "homepage": "https://mau.dev/tulir/mautrix-amp", + "homepage": "https://mau.dev/tulir/mautrix-line", "scripts": { "start": "node ./src/main.js" }, "dependencies": { "arg": "^4.1.3", "chrono-node": "^2.1.7", - "puppeteer": "5.1.0" + "puppeteer": "5.5.0" }, "devDependencies": { "babel-eslint": "^10.1.0", diff --git a/puppet/src/api.js b/puppet/src/api.js index df7058a..ca00e1d 100644 --- a/puppet/src/api.js +++ b/puppet/src/api.js @@ -1,5 +1,5 @@ -// mautrix-amp - A very hacky Matrix-SMS bridge based on using Android Messages for Web in Puppeteer -// Copyright (C) 2020 Tulir Asokan +// 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 diff --git a/puppet/src/client.js b/puppet/src/client.js index 978f0a6..f60a874 100644 --- a/puppet/src/client.js +++ b/puppet/src/client.js @@ -1,5 +1,5 @@ -// mautrix-amp - A very hacky Matrix-SMS bridge based on using Android Messages for Web in Puppeteer -// Copyright (C) 2020 Tulir Asokan +// 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 @@ -116,6 +116,24 @@ export default class Client { }) } + sendPIN(pin) { + this.log(`Sending PIN ${pin} to client`) + return this._write({ + id: --this.notificationID, + command: "pin", + pin, + }) + } + + sendFailure(reason) { + this.log(`Sending failure "${reason}" to client`) + return this._write({ + id: --this.notificationID, + command: "failure", + reason, + }) + } + handleStart = async (req) => { let started = false if (this.puppet === null) { @@ -205,7 +223,8 @@ export default class Client { start: this.handleStart, stop: this.handleStop, disconnect: () => this.stop(), - login: () => this.puppet.waitForLogin(), + login: req => this.puppet.waitForLogin(req.login_type, req.login_data), + cancel_login: () => this.puppet.cancelLogin(), send: req => this.puppet.sendMessage(req.chat_id, req.text), set_last_message_ids: req => this.puppet.setLastMessageIDs(req.msg_ids), get_chats: () => this.puppet.getRecentChats(), diff --git a/puppet/src/contentscript.js b/puppet/src/contentscript.js index c81612a..c19dd42 100644 --- a/puppet/src/contentscript.js +++ b/puppet/src/contentscript.js @@ -1,5 +1,5 @@ -// mautrix-amp - A very hacky Matrix-SMS bridge based on using Android Messages for Web in Puppeteer -// Copyright (C) 2020 Tulir Asokan +// 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 @@ -32,6 +32,20 @@ window.__mautrixReceiveChanges = function (changes) {} * @return {Promise} */ window.__mautrixReceiveQR = function (url) {} +/** + * @return {Promise} + */ +window.__mautrixSendEmailCredentials = function () {} +/** + * @param {string} pin - The login PIN. + * @return {Promise} + */ +window.__mautrixReceivePIN = function (pin) {} +/** + * @param {Element} button - The button to click when a QR code or PIN expires. + * @return {Promise} + */ +window.__mautrixExpiry = function (button) {} /** * @param {number} id - The ID of the message that was sent * @return {Promise} @@ -41,7 +55,11 @@ window.__mautrixReceiveMessageID = function(id) {} class MautrixController { constructor() { this.chatListObserver = null - this.qrCodeObserver = null + this.qrChangeObserver = null + this.qrAppearObserver = null + this.emailAppearObserver = null + this.pinAppearObserver = null + this.expiryObserver = null } /** @@ -312,6 +330,7 @@ class MautrixController { if (this.chatListObserver !== null) { this.removeChatListObserver() } + /* TODO this.chatListObserver = new MutationObserver(mutations => { try { this._observeChatListMutations(mutations) @@ -320,6 +339,7 @@ class MautrixController { } }) this.chatListObserver.observe(element, { childList: true, subtree: true }) + */ console.debug("Started chat list observer") } @@ -334,27 +354,132 @@ class MautrixController { } } - addQRObserver(element) { - if (this.qrCodeObserver !== null) { - this.removeQRObserver() + addQRChangeObserver(element) { + if (this.qrChangeObserver !== null) { + this.removeQRChangeObserver() } - this.qrCodeObserver = new MutationObserver(changes => { + this.qrChangeObserver = new MutationObserver(changes => { for (const change of changes) { - if (change.attributeName === "data-qr-code" && change.target instanceof Element) { - window.__mautrixReceiveQR(change.target.getAttribute("data-qr-code")) + if (change.attributeName === "title" && change.target instanceof Element) { + window.__mautrixReceiveQR(change.target.getAttribute("title")) } } }) - this.qrCodeObserver.observe(element, { + this.qrChangeObserver.observe(element, { attributes: true, - attributeFilter: ["data-qr-code"], + attributeFilter: ["title"], }) } - removeQRObserver() { - if (this.qrCodeObserver !== null) { - this.qrCodeObserver.disconnect() - this.qrCodeObserver = null + removeQRChangeObserver() { + if (this.qrChangeObserver !== null) { + this.qrChangeObserver.disconnect() + this.qrChangeObserver = null + } + } + + addQRAppearObserver(element) { + if (this.qrAppearObserver !== null) { + this.removeQRAppearObserver() + } + this.qrAppearObserver = new MutationObserver(changes => { + for (const change of changes) { + for (const node of change.addedNodes) { + const qrElement = node.querySelector("#login_qrcode_area div[title]") + if (qrElement) { + window.__mautrixReceiveQR(qrElement.title) + window.__mautrixController.addQRChangeObserver(element) + return + } + } + } + }) + this.qrAppearObserver.observe(element, { + childList: true, + }) + } + + removeQRAppearObserver() { + if (this.qrAppearObserver !== null) { + this.qrAppearObserver.disconnect() + this.qrAppearObserver = null + } + } + + addEmailAppearObserver(element, login_type) { + if (this.emailAppearObserver !== null) { + this.removeEmailAppearObserver() + } + this.emailAppearObserver = new MutationObserver(changes => { + for (const change of changes) { + for (const node of change.addedNodes) { + const emailElement = node.querySelector("#login_email_btn") + if (emailElement) { + window.__mautrixSendEmailCredentials() + return + } + } + } + }) + this.emailAppearObserver.observe(element, { + childList: true, + }) + } + + removeEmailAppearObserver() { + if (this.emailAppearObserver !== null) { + this.emailAppearObserver.disconnect() + this.emailAppearObserver = null + } + } + + addPINAppearObserver(element, login_type) { + if (this.pinAppearObserver !== null) { + this.removePINAppearObserver() + } + this.pinAppearObserver = new MutationObserver(changes => { + for (const change of changes) { + for (const node of change.addedNodes) { + const pinElement = node.querySelector("div.mdCMN01Code") + if (pinElement) { + window.__mautrixReceivePIN(pinElement.innerText) + return + } + } + } + }) + this.pinAppearObserver.observe(element, { + childList: true, + }) + } + + removePINAppearObserver() { + if (this.pinAppearObserver !== null) { + this.pinAppearObserver.disconnect() + this.pinAppearObserver = null + } + } + + addExpiryObserver(element) { + if (this.expiryObserver !== null) { + this.removeExpiryObserver() + } + const button = element.querySelector("dialog button") + this.expiryObserver = new MutationObserver(changes => { + if (changes.length == 1 && !changes[0].target.getAttribute("class").includes("MdNonDisp")) { + window.__mautrixExpiry(button) + } + }) + this.expiryObserver.observe(element, { + attributes: true, + attributeFilter: ["class"], + }) + } + + removeExpiryObserver() { + if (this.expiryObserver !== null) { + this.expiryObserver.disconnect() + this.expiryObserver = null } } } diff --git a/puppet/src/main.js b/puppet/src/main.js index 88cee9f..c395ca5 100644 --- a/puppet/src/main.js +++ b/puppet/src/main.js @@ -1,5 +1,5 @@ -// mautrix-amp - A very hacky Matrix-LINE bridge based on using LINE's Chrome Store Extension in Puppeteer -// Copyright (C) 2020 Tulir Asokan, Andrew Ferrazzutti +// 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 @@ -38,6 +38,7 @@ const config = JSON.parse(fs.readFileSync(configPath).toString()) MessagesPuppeteer.profileDir = config.profile_dir || MessagesPuppeteer.profileDir MessagesPuppeteer.disableDebug = !!config.disable_debug MessagesPuppeteer.url = config.url +MessagesPuppeteer.extensionDir = config.extension_dir || MessagesPuppeteer.extensionDir const api = new PuppetAPI(config.listen) diff --git a/puppet/src/puppet.js b/puppet/src/puppet.js index 37428ed..25c5764 100644 --- a/puppet/src/puppet.js +++ b/puppet/src/puppet.js @@ -1,5 +1,5 @@ -// mautrix-amp - A very hacky Matrix-SMS bridge based on using Android Messages for Web in Puppeteer -// Copyright (C) 2020 Tulir Asokan +// 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 @@ -27,8 +27,9 @@ export default class MessagesPuppeteer { static executablePath = undefined static disableDebug = false static noSandbox = false - static viewport = { width: 1920, height: 1080 } + //static viewport = { width: 1920, height: 1080 } static url = undefined + static extensionDir = 'extension_files' /** * @@ -61,14 +62,13 @@ export default class MessagesPuppeteer { * Start the browser and open the messages for web page. * This must be called before doing anything else. */ - async start(debug = false) { + async start() { this.log("Launching browser") - const pathToExtension = require('path').join(__dirname, 'extension_files'); - const extensionArgs = [ - `--disable-extensions-except=${pathToExtension}`, - `--load-extension=${pathToExtension}` - ]; + const extensionArgs = [ + `--disable-extensions-except=${MessagesPuppeteer.extensionDir}`, + `--load-extension=${MessagesPuppeteer.extensionDir}` + ] this.browser = await puppeteer.launch({ executablePath: MessagesPuppeteer.executablePath, @@ -85,67 +85,182 @@ export default class MessagesPuppeteer { this.page = await this.browser.newPage() } this.log("Opening", MessagesPuppeteer.url) - await this.page.goto(MessagesPuppeteer.url) + await this.page.setBypassCSP(true) // Needed to load content scripts + await this._preparePage(true) - this.log("Injecting content script") - await this.page.addScriptTag({ path: "./src/contentscript.js", type: "module" }) this.log("Exposing functions") await this.page.exposeFunction("__mautrixReceiveQR", this._receiveQRChange.bind(this)) + await this.page.exposeFunction("__mautrixSendEmailCredentials", this._sendEmailCredentials.bind(this)) + await this.page.exposeFunction("__mautrixReceivePIN", this._receivePIN.bind(this)) + await this.page.exposeFunction("__mautrixExpiry", this._receiveExpiry.bind(this)) + /* TODO await this.page.exposeFunction("__mautrixReceiveMessageID", id => this.sentMessageIDs.add(id)) await this.page.exposeFunction("__mautrixReceiveChanges", this._receiveChatListChanges.bind(this)) await this.page.exposeFunction("__chronoParseDate", chrono.parseDate) + */ - this.log("Waiting for load") - // Wait for the page to load (either QR code for login or chat list when already logged in) - await Promise.race([ - this.page.waitForSelector("mw-main-container mws-conversations-list .conv-container", - { visible: true, timeout: 60000 }), - this.page.waitForSelector("mw-authentication-container mw-qr-code", - { visible: true, timeout: 60000 }), - this.page.waitForSelector("mw-unable-to-connect-container", - { visible: true, timeout: 60000 }), - ]) + // NOTE Must *always* re-login on a browser session, so no need to check if already logged in + this.loginRunning = false + this.loginCancelled = false this.taskQueue.start() - if (await this.isLoggedIn()) { - await this.startObserving() - } this.log("Startup complete") } + async _preparePage(navigateTo) { + if (navigateTo) { + await this.page.goto(MessagesPuppeteer.url) + } else { + await this.page.reload() + } + this.log("Injecting content script") + await this.page.addScriptTag({ path: "./src/contentscript.js", type: "module" }) + } + /** - * Wait for the session to be logged in and monitor QR code changes while it's not. + * Wait for the session to be logged in and monitor changes while it's not. */ - async waitForLogin() { + async waitForLogin(login_type, login_data) { if (await this.isLoggedIn()) { return } - const qrSelector = "mw-authentication-container mw-qr-code" - if (!await this.page.$("mat-slide-toggle.mat-checked")) { - this.log("Clicking Remember Me button") - await this.page.click("mat-slide-toggle:not(.mat-checked) > label") - } else { - this.log("Remember Me button already clicked") + this.loginRunning = true + this.loginCancelled = false + + const loginContentArea = await this.page.waitForSelector("#login_content") + + switch (login_type) { + case "qr": { + this.log("Running QR login") + const qrButton = await this.page.waitForSelector("#login_qr_btn") + await qrButton.click() + + const qrElement = await this.page.waitForSelector("#login_qrcode_area div[title]", {visible: true}) + const currentQR = await this.page.evaluate(element => element.title, qrElement) + this._receiveQRChange(currentQR) + + await this.page.evaluate( + element => window.__mautrixController.addQRChangeObserver(element), qrElement) + await this.page.evaluate( + element => window.__mautrixController.addQRAppearObserver(element), loginContentArea) + + break } - this.log("Fetching current QR code") - const currentQR = await this.page.$eval(qrSelector, - element => element.getAttribute("data-qr-code")) - this._receiveQRChange(currentQR) - this.log("Adding QR observer") - await this.page.$eval(qrSelector, - element => window.__mautrixController.addQRObserver(element)) - this.log("Waiting for login") - await this.page.waitForSelector("mws-conversations-list .conv-container", { - visible: true, - timeout: 0, - }) - this.log("Removing QR observer") - await this.page.evaluate(() => window.__mautrixController.removeQRObserver()) + case "email": { + this.log("Running email login") + if (!login_data) { + _sendLoginFailure("No login credentials provided for email login") + return + } + + const emailButton = await this.page.waitForSelector("#login_email_btn") + await emailButton.click() + + const emailArea = await this.page.waitForSelector("#login_email_area", {visible: true}) + this.login_email = login_data["email"] + this.login_password = login_data["password"] + this._sendEmailCredentials() + + await this.page.evaluate( + element => window.__mautrixController.addEmailAppearObserver(element), loginContentArea) + + break + } + // TODO Phone number login + default: + _sendLoginFailure(`Invalid login type: ${login_type}`) + return + } + + await this.page.evaluate( + element => window.__mautrixController.addPINAppearObserver(element), loginContentArea) + await this.page.$eval("#layer_contents", + element => window.__mautrixController.addExpiryObserver(element)) + + this.log("Waiting for login response") + let doneWaiting = false + let loginSuccess = false + const cancelableResolve = (promiseWithShortTimeout) => { + const executor = (resolve, reject) => { + promiseWithShortTimeout.then( + value => { + this.log(`Done: ${value}`) + doneWaiting = true + resolve(value) + }, + reason => { + if (!doneWaiting) { + this.log(`Not done, waiting some more. ${reason}`) + setTimeout(executor, 3000, resolve, reject) + } else { + this.log(`Final fail. ${reason}`) + resolve() + } + } + ) + } + return new Promise(executor) + } + + const result = await Promise.race([ + this.page.waitForSelector("#wrap_message_sync", {timeout: 2000}) + .then(element => { + loginSuccess = true + return element + }), + this.page.waitForSelector("#login_incorrect", {visible: true, timeout: 2000}) + .then(element => element.innerText), + this._waitForLoginCancel(), + ].map(promise => cancelableResolve(promise))) + + this.log("Removing observers") + await this.page.evaluate(() => window.__mautrixController.removeQRChangeObserver()) + await this.page.evaluate(() => window.__mautrixController.removeLoginChildrenObserver(element)) + await this.page.evaluate(() => window.__mautrixController.removeExpiryObserver()) + delete this.login_email + delete this.login_password + + if (!loginSuccess) { + _sendLoginFailure(result) + return + } + + this.log("Waiting for sync") + await this.page.waitForFunction( + messageSyncElement => { + const text = messageSyncElement.innerText + return text == 'Syncing messages... 100%' + }, + {}, + result) + await this.startObserving() + this.loginRunning = false this.log("Login complete") } + /** + * Cancel an ongoing login attempt. + */ + async cancelLogin() { + if (this.loginRunning) { + this.loginCancelled = true + //await this._preparePage(false) + } + } + + _waitForLoginCancel() { + return new Promise((resolve, reject) => { + console.log(`>>>>> ${this.loginCancelled}`) + if (this.loginCancelled) { + resolve() + } else { + reject() + } + }) + } + /** * Close the browser. */ @@ -166,14 +281,17 @@ export default class MessagesPuppeteer { * @return {Promise} - Whether or not the session is logged in. */ async isLoggedIn() { - return await this.page.$("mw-main-container mws-conversations-list") !== null + return await this.page.$("#wrap_message_sync") !== null } async isPermanentlyDisconnected() { - return await this.page.$("mw-unable-to-connect-container") !== null + // TODO + //return await this.page.$("mw-unable-to-connect-container") !== null + return false } async isOpenSomewhereElse() { + /* TODO try { const text = await this.page.$eval("mws-dialog mat-dialog-content div", elem => elem.textContent) @@ -181,16 +299,15 @@ export default class MessagesPuppeteer { } catch (err) { return false } - } - - async clickDialogButton() { - await this.page.click("mws-dialog mat-dialog-actions button") + */ + return false } async isDisconnected() { if (!await this.isLoggedIn()) { return true } + /* TODO const offlineIndicators = await Promise.all([ this.page.$("mw-main-nav mw-banner mw-error-banner"), this.page.$("mw-main-nav mw-banner mw-information-banner[title='Connecting']"), @@ -198,6 +315,8 @@ export default class MessagesPuppeteer { this.isOpenSomewhereElse(), ]) return offlineIndicators.some(indicator => Boolean(indicator)) + */ + return false } /** @@ -206,8 +325,11 @@ export default class MessagesPuppeteer { * @return {Promise<[ChatListInfo]>} - List of chat IDs in order of most recent message. */ async getRecentChats() { + /* TODO return await this.page.$eval("mws-conversations-list .conv-container", elem => window.__mautrixController.parseChatList(elem)) + */ + return null } /** @@ -266,7 +388,7 @@ export default class MessagesPuppeteer { async startObserving() { this.log("Adding chat list observer") - await this.page.$eval("mws-conversations-list .conv-container", + await this.page.$eval("#wrap_chat_list", element => window.__mautrixController.addChatListObserver(element)) } @@ -276,7 +398,9 @@ export default class MessagesPuppeteer { } _listItemSelector(id) { - return `mws-conversation-list-item > a.list-item[href="/web/conversations/${id}"]` + // TODO + //return `mws-conversation-list-item > a.list-item[href="/web/conversations/${id}"]` + return '' } async _switchChatUnsafe(id) { @@ -365,6 +489,20 @@ export default class MessagesPuppeteer { } } + async _sendEmailCredentials() { + this.log("Inputting login credentials") + + // Triple-click email input field to select all existing text and replace it on type + const emailInput = await this.page.$("#line_login_email") + await emailInput.click({clickCount: 3}) + await emailInput.type(this.login_email) + + // Password input field always starts empty, so no need to select its text first + await this.page.type("#line_login_pwd", this.login_password) + + await this.page.click("button#login_btn") + } + _receiveQRChange(url) { if (this.client) { this.client.sendQRCode(url).catch(err => @@ -373,4 +511,28 @@ export default class MessagesPuppeteer { this.log("No client connected, not sending new QR") } } + + _receivePIN(pin) { + if (this.client) { + this.client.sendPIN(`Your PIN is: ${pin}`).catch(err => + this.error("Failed to send new PIN to client:", err)) + } else { + this.log("No client connected, not sending new PIN") + } + } + + _sendLoginFailure(reason) { + this.error(`Login failure: ${reason ? reason : 'cancelled'}`) + if (this.client) { + this.client.sendFailure(reason).catch(err => + this.error("Failed to send failure reason to client:", err)) + } else { + this.log("No client connected, not sending failure reason") + } + } + + async _receiveExpiry(button) { + this.log("Something expired, clicking OK button to continue") + await this.page.click(button) + } } diff --git a/puppet/src/taskqueue.js b/puppet/src/taskqueue.js index bf97a9a..77d07a6 100644 --- a/puppet/src/taskqueue.js +++ b/puppet/src/taskqueue.js @@ -1,5 +1,5 @@ -// mautrix-amp - A very hacky Matrix-SMS bridge based on using Android Messages for Web in Puppeteer -// Copyright (C) 2020 Tulir Asokan +// 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 diff --git a/puppet/src/util.js b/puppet/src/util.js index 9362dc0..efbcfe4 100644 --- a/puppet/src/util.js +++ b/puppet/src/util.js @@ -1,5 +1,5 @@ -// mautrix-amp - A very hacky Matrix-SMS bridge based on using Android Messages for Web in Puppeteer -// Copyright (C) 2020 Tulir Asokan +// 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 diff --git a/puppet/yarn.lock b/puppet/yarn.lock index eeffcd0..6c7af9c 100644 --- a/puppet/yarn.lock +++ b/puppet/yarn.lock @@ -364,10 +364,10 @@ define-properties@^1.1.2, define-properties@^1.1.3: dependencies: object-keys "^1.0.12" -devtools-protocol@0.0.767361: - version "0.0.767361" - resolved "https://registry.yarnpkg.com/devtools-protocol/-/devtools-protocol-0.0.767361.tgz#5977f2558b84f9df36f62501bdddb82f3ae7b66b" - integrity sha512-ziRTdhEVQ9jEwedaUaXZ7kl9w9TF/7A3SXQ0XuqrJB+hMS62POHZUWTbumDN2ehRTfvWqTPc2Jw4gUl/jggmHA== +devtools-protocol@0.0.818844: + version "0.0.818844" + resolved "https://registry.yarnpkg.com/devtools-protocol/-/devtools-protocol-0.0.818844.tgz#d1947278ec85b53e4c8ca598f607a28fa785ba9e" + integrity sha512-AD1hi7iVJ8OD0aMLQU5VK0XH9LDlA1+BcPIgrAxPfaibx2DbWucuyOhc4oyQCbnvDDO68nN6/LcKfqTP343Jjg== doctrine@1.5.0: version "1.5.0" @@ -918,11 +918,6 @@ lodash@^4.17.14, lodash@^4.17.19: resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52" integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA== -mime@^2.0.3: - version "2.4.6" - resolved "https://registry.yarnpkg.com/mime/-/mime-2.4.6.tgz#e5b407c90db442f2beb5b162373d07b69affa4d1" - integrity sha512-RZKhC3EmpBchfTGBVb8fb+RL2cWyw/32lshnsETttkBAyAUXSGHxbEJWWRXc751DrIxG1q04b8QwMbAwkRPpUA== - minimatch@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" @@ -935,11 +930,6 @@ minimist@^1.2.0, minimist@^1.2.5: resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== -mitt@^2.0.1: - version "2.1.0" - resolved "https://registry.yarnpkg.com/mitt/-/mitt-2.1.0.tgz#f740577c23176c6205b121b2973514eade1b2230" - integrity sha512-ILj2TpLiysu2wkBbWjAmww7TkZb65aiQO+DkVdUTBpBXq+MHYiETENkKFMtsJZX1Lf4pe4QOrTSjIfUwN5lRdg== - mkdirp-classic@^0.5.2: version "0.5.3" resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113" @@ -967,6 +957,11 @@ natural-compare@^1.4.0: resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= +node-fetch@^2.6.1: + version "2.6.1" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052" + integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw== + normalize-package-data@^2.3.2: version "2.5.0" resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8" @@ -1162,17 +1157,16 @@ punycode@^2.1.0: resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== -puppeteer@5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/puppeteer/-/puppeteer-5.1.0.tgz#e7bae2caa6e3a13a622755e4c27689d9812c38ca" - integrity sha512-IZBFG8XcA+oHxYo5rEpJI/HQignUis2XPijPoFpNxla2O+WufonGsUsSqrhRXgBKOME5zNfhRdUY2LvxAiKlhw== +puppeteer@5.5.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/puppeteer/-/puppeteer-5.5.0.tgz#331a7edd212ca06b4a556156435f58cbae08af00" + integrity sha512-OM8ZvTXAhfgFA7wBIIGlPQzvyEETzDjeRa4mZRCRHxYL+GNH5WAuYUQdja3rpWZvkX/JKqmuVgbsxDNsDFjMEg== dependencies: debug "^4.1.0" - devtools-protocol "0.0.767361" + devtools-protocol "0.0.818844" extract-zip "^2.0.0" https-proxy-agent "^4.0.0" - mime "^2.0.3" - mitt "^2.0.1" + node-fetch "^2.6.1" pkg-dir "^4.2.0" progress "^2.0.1" proxy-from-env "^1.0.0"