Compare commits

..

No commits in common. "master" and "master" have entirely different histories.

52 changed files with 675 additions and 2620 deletions

6
.gitignore vendored
View File

@ -1,6 +1,4 @@
/.idea/
/.*project
/.settings/
/.venv
/env/
@ -15,8 +13,8 @@ __pycache__
profiles
puppet/extension_files
/config*.yaml
/registration*.yaml
/config.yaml
/registration.yaml
*.log*
*.db
*.pickle

View File

@ -1,46 +1,49 @@
FROM alpine:3.14
FROM alpine:3.12
ARG TARGETARCH=amd64
RUN echo $'\
@edge http://dl-cdn.alpinelinux.org/alpine/edge/main\n\
@edge http://dl-cdn.alpinelinux.org/alpine/edge/testing\n\
@edge http://dl-cdn.alpinelinux.org/alpine/edge/community' >> /etc/apk/repositories
RUN apk add --no-cache \
python3 py3-pip py3-setuptools py3-wheel \
py3-virtualenv \
py3-pillow \
py3-aiohttp \
py3-magic \
py3-ruamel.yaml \
py3-commonmark \
# encryption
py3-olm \
py3-cffi \
py3-pycryptodome \
py3-unpaddedbase64 \
py3-future \
py3-commonmark@edge \
# Other dependencies
ca-certificates \
su-exec \
# encryption
olm-dev \
py3-cffi \
py3-pycryptodome \
py3-unpaddedbase64 \
py3-future \
bash \
curl \
jq \
yq
jq && \
curl -sLo yq https://github.com/mikefarah/yq/releases/download/3.3.2/yq_linux_${TARGETARCH} && \
chmod +x yq && mv yq /usr/bin/yq
COPY requirements.txt /opt/matrix-puppeteer-line/requirements.txt
COPY optional-requirements.txt /opt/matrix-puppeteer-line/optional-requirements.txt
WORKDIR /opt/matrix-puppeteer-line
COPY requirements.txt optional-requirements.txt ./
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 LICENSE setup.py ./
COPY matrix_puppeteer_line matrix_puppeteer_line
RUN apk add --no-cache git && pip3 install .[e2be] && apk del git \
COPY . /opt/matrix-puppeteer-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 matrix_puppeteer_line/example-config.yaml . && rm -rf matrix_puppeteer_line
VOLUME /data
ENV UID=1337 GID=1337
# Needed to prevent "KeyError: 'getpwuid(): uid not found: 1337'" when connecting to postgres
RUN adduser -DHu 1337 --gecos "" line
COPY docker-run.sh ./
RUN chown -R 1337:1337 .
USER 1337
CMD ["./docker-run.sh"]
CMD ["/opt/matrix-puppeteer-line/docker-run.sh"]

View File

@ -1,28 +0,0 @@
# Limitations & missing features
Not all features of LINE are supported by the LINE Chrome extension on which this bridge relies, and not all Matrix features are available in LINE. This section documents all known features missing from LINE that this bridge cannot provide.
## Missing from LINE
* Typing notifications
* Message edits
* Formatted messages
* Presence
* Timestamped read receipts
* Read receipts between users other than yourself
* Identity of who read a message in a multi-user chat
## Missing from LINE on Chrome
* Unlimited message history
* Messages that are very old may not be available in LINE on Chrome at all, even after a full sync
* Voice/video calls
* No notification is sent when a call begins
* When a call ends, an automated message of "Your OS version doesn't support this feature" is sent as an ordinary text message from the user who began the call
* Message redaction (delete/unsend)
* But messages unsent from other LINE clients do disappear from LINE on Chrome
* Replies
* Appear as ordinary messages
* Mentions
* Appear as ordinary text
* Audio message sending
* But audio messages can be received
* Location sending
* But locations can be received

View File

@ -1,13 +1,10 @@
# matrix-puppeteer-line
A Matrix-LINE puppeting bridge based on running LINE's Chrome extension in [Puppeteer](https://github.com/puppeteer/puppeteer).
Fork of [mautrix-amp](https://mau.dev/tulir/mautrix-amp/).
A very hacky Matrix-LINE bridge based on running LINE's Chrome extension in Puppeteer.
Fork of [https://mau.dev/tulir/mautrix-amp/](mautrix-amp).
## Features & roadmap
## Features, roadmap, and limitations
[ROADMAP.md](ROADMAP.md)
## Limitations & missing features
[LIMITATIONS.md](LIMITATIONS.md)
## Setup
[SETUP.md](SETUP.md)

View File

@ -6,7 +6,7 @@
* [x] Images
* [ ] Files
* [x] Stickers
* [x] Read receipts<sup>[1]</sup>
* [ ] Read receipts (currently eagerly-sent since message sync requires "reading" a chat)
* [ ] Room metadata changes
* [ ] Name
* [ ] Avatar
@ -24,7 +24,9 @@
* [x] Stickers
* [x] Emoji
* [ ] Message unsend
* [x] Read receipts<sup>[2]</sup>
* [ ] Read receipts
* [x] For most recently active chat
* [ ] For any chat
* [x] User metadata
* [ ] Name
* [x] On sync
@ -49,13 +51,13 @@
* [x] Groups (named chats)
* [x] Rooms (unnamed chats / "multi-user direct chats")
* [ ] Membership actions
* [x] Join
* [ ] Join
* [x] When message is sent by new participant
* [x] On sync
* [x] At join time
* [x] Leave
* [ ] At join time
* [ ] Leave
* [x] On sync
* [x] At leave time
* [ ] At leave time
* [ ] Invite
* [ ] Remove
* [ ] Friend actions
@ -67,7 +69,6 @@
* [x] At startup
* [x] When receiving invite or message
* [ ] When sending message in new chat from LINE app
* [x] Private chat creation by inviting Matrix puppet of LINE user to new room
* [x] Notification for message send failure
* [ ] Provisioning API for logging in
* [x] Use bridge bot for messages sent from LINE app (when double-puppeting is disabled and `bridge.invite_own_puppet_to_pm` is enabled)
@ -76,5 +77,31 @@
* [ ] Multiple bridge users
* [ ] Relay bridging
<sup>[1]</sup> Requires [MSC2409](https://github.com/matrix-org/matrix-doc/pull/2409). Without it, the bridge will always view incoming LINE messages on your behalf.
<sup>[2]</sup> LINE read receipts may be bridged later than they actually occur. The more unread chats there are, the longer this delay will be.
# Missing features
## Missing from LINE
* Typing notifications
* Message edits
* Formatted messages
* Presence
* Timestamped read receipts
* Read receipts between users other than yourself
## Missing from LINE on Chrome
* Unlimited message history
* Messages that are very old may not be available in LINE on Chrome at all, even after a full sync
* Voice/video calls
* No notification is sent when a call begins
* When a call ends, an automated message of "Your OS version doesn't support this feature" is sent as an ordinary text message from the user who began the call
* Message redaction (delete/unsend)
* But messages unsent from other LINE clients do disappear from LINE on Chrome
* Replies
* Appear as ordinary messages
* Mentions
* Appear as ordinary text
* Audio message sending
* But audio messages can be received
* Location sending
* But locations can be received
## Missing from matrix-puppeteer-line
* TODO

138
SETUP.md
View File

@ -1,132 +1,42 @@
* [Obtaining the LINE Chrome extension](#obtaining-the-line-chrome-extension)
* [Manual setup](#manual-setup)
* [systemd](#systemd)
* [Docker](#docker)
# Minimum Requirements
* Python 3.8
* Node 10.18.1
---
# Obtaining the LINE Chrome extension
For all modes of deploying the bridge, it is first required to manually download a .crx or .zip file of the [LINE Chrome extension](https://chrome.google.com/webstore/detail/line/ophjlpahpchlmihnnnihgmmeilfjmjjc) (current version: 2.5.0).
The recommended way of doing this is to use the [CRX Extractor/Downloader](https://chrome.google.com/webstore/detail/crx-extractordownloader/ajkhmmldknmfjnmeedkbkkojgobmljda) extension for Chrome/Chromium:
1. Install that extension in a Chrome/Chromium instance of your choice
1. Navigate to the Web Store page for the LINE extension
1. Click the "CRX" button in the browser toolbar
1. Select "Download as CRX" or "Download as ZIP"
The downloaded .crx/.zip can then be extracted with `unzip` or with a GUI tool like GNOME File Roller.
To install updated versions of the LINE extension, simply download the .crx/.zip of the latest version of the extension, and extract it in the same location as for initial setup.
# Manual setup
These instructions describe how to install and run the bridge manually from a clone of this repository.
## Minimum requirements
* Python 3.7
* Node 14
* yarn 1.22.x (from either your distribution or `npm`)
* postgresql 11
* A LINE account on a smartphone (Android or iOS)
## Optional requirements
* `xvfb-run` for easily running the Puppeteer module in a background X server
* `xdotool` for keeping the Puppeteer module responsive when run in a background X server (see [puppet/README.md](puppet/README.md))
* Native dependencies for [end-to-bridge](https://docs.mau.fi/bridges/general/end-to-bridge-encryption.html): https://docs.mau.fi/bridges/python/optional-dependencies.html#all-python-bridges
## Initial setup
### Puppeteer module
1. Extract the downloaded .crx/.zip of the LINE Chrome extension to `puppet/extension_files`
# Initial setup
## Puppeteer module
1. Download the .crx file of the [LINE Chrome extension](https://chrome.google.com/webstore/detail/line/ophjlpahpchlmihnnnihgmmeilfjmjjc) (current version: 2.4.4)
* The recommended way of doing this is with the [CRX Extractor/Downloader](https://chrome.google.com/webstore/detail/crx-extractordownloader/ajkhmmldknmfjnmeedkbkkojgobmljda) extension for Chrome/Chromium. Simply install that extension in a Chrome/Chromium instance of your choice, then navigate to the Web Store page for the LINE extension, click the "CRX" button in the toolbar, and select "Download as CRX"
1. Extract the downloaded .crx file to `puppet/extension_files`
* This can be done with `unzip *.crx -d puppet/extension_files`, or with a GUI tool like GNOME File Roller
1. `cd` to the `puppet` directory and run `yarn --production`
1. Copy `puppet/example-config.json` to `puppet/config.json`
1. If your system's CPU architecture is not x86\_64/amd64, the version of Chromium bundled with Puppeteer will not work, and the following additional steps are required:
1. Install Chrome/Chromium from your distribution's package manager
1. Edit `puppet/package.json` to specify the version of Puppeteer that is compatible with the version of Chrome/Chromium that you just installed, and rerun `yarn --production` (see [Puppeteer documentation](https://github.com/puppeteer/puppeteer/blob/main/docs/api.md) for a map of Puppeteer/Chromium compatibility)
1. Set `executable_path` in `puppet/config.json` to the path to the installed Chrome/Chromium binary
1. Edit `puppet/config.json` with desired settings (see [puppet/README.md](puppet/README.md) for details)
1. Run `node prep_helper.js` to open the version of Chrome downloaded by Puppeteer, and click on the LINE icon next to the URL bar
1. Once the LINE popup appears, press F12 to show DevTools, which will reveal the LINE extension's UUID
1. Copy `puppet/example-config.json` to `puppet/config.json`, and set some important settings:
* set `"url"` to the UUID found in the previous step
* set the `"listen"` settings to the socket to use for communication with the bridge (see [puppet/README.md](puppet/README.md) for details)
### Bridge module
1. `cd` to the repository root and create a Python virtual environment with `python3 -m venv .venv`, and enter it with `source .venv/bin/activate`
## Bridge
1. `cd` to the project root directory and create a Python virtual environment with `python -m venv .venv`, and enter it with `source .venv/bin/activate`
1. Install Python requirements:
* `pip install -Ur requirements.txt` for base functionality
* `pip install -Ur optional-requirements.txt` for [end-to-bridge](https://docs.mau.fi/bridges/general/end-to-bridge-encryption.html) encryption and metrics
* Note that end-to-bridge encryption requires some native dependencies. For details, see https://docs.mau.fi/bridges/python/optional-dependencies.html#all-python-bridges
* `pip install -Ur optional_requirements.txt` for [end-to-bridge](https://docs.mau.fi/bridges/general/end-to-bridge-encryption.html) encryption and metrics
1. Copy `matrix_puppeteer_line/example-config.yaml` to `config.yaml`, and update it with the proper settings to connect to your homeserver
* In particular, be sure to set the `puppeteer.connection` settings to use the socket you chose in `puppet/config.json`
1. Run `python -m matrix_puppeteer_line -g` to generate an appservice registration file, and update your homeserver configuration to accept it
## Running manually
# Running
1. In the `puppet` directory, launch the Puppeteer module with `yarn start` or `node src/main.js`
1. In the project root directory, run the bridge module with `python -m matrix_puppeteer_line`
1. Start a chat with the bot, and use one of the `login-email` or `login-qr` commands to sync your LINE account
* Note that on first use, you must enter a verification code on a smartphone version of LINE in order for the login to complete
1. In the project root directory, run the bridge with `python -m matrix_puppeteer_line`
1. Start a chat with the bot and follow the instructions
### Running the Puppeteer module headless
Puppeteer cannot be run in headless mode when using Chrome/Chromium with extensions (including the LINE extension).
# Running the Puppeteer module headless
Puppeteer cannot be run in headless mode when using Chromium with extensions (including the LINE extension).
As a workaround, it may be run in a background X server. This allows running the Puppeteer module on a GUI-less server.
An easy way to do so is to install `xvfb` from your distribution, and run the Puppeteer module with `xvfb-run yarn start`.
## systemd
The [systemd](systemd) directory provides sample service unit configuration files for running the bridge & Puppeteer modules:
* `matrix-puppeteer-line.service` for the bridge module
* `matrix-puppeteer-line-chrome.service` for the Puppeteer module
To use them as-is, follow these steps after [initial setup](#initial-setup):
1. Install `xfvb-run`, ideally from your distribution
1. Place/link your clone of this repository in `/opt/matrix-puppeteer-line`
* If moving your repo directory after having already created a Python virtual environment for the bridge module, re-create the virtual environment after moving to ensure its paths are up-to-date
* Alternatively, clone it to `/opt/matrix-puppeteer-line` in the first place
1. Install the services as either system or user units
* To install as system units:
1. Copy/link the service files to a directory in the system unit search path, such as `/etc/systemd/system/`
1. Create the services' configuration directory with `sudo mkdir /etc/matrix-puppeteer-line`
1. RECOMMENDED: Create the `matrix-puppeteer-line` user on your system with `adduser` or an equivalent command, then uncomment the `User` and `Group` lines in the service files
* To install as user units:
1. Copy/link the service files to a directory in the user unit search path, such as `~/.config/systemd/user`
1. Create the services' configuration directory with `mkdir $XDG_CONFIG_HOME/matrix-puppeteer-line`
1. Copy the bridge & Puppeteer module configuration files to the services' configuration directory as `config.yaml` and `puppet-config.json`, respectively
1. Start the services now and on every boot boot with `[sudo] systemd [--user] enable --now matrix-puppeteer-line{,-chrome}`
Note that stopping/restarting the bridge module service `matrix-puppeteer-line.service` does not affect the Puppeteer module service `matrix-puppeteer-line-chrome.service`, but stopping/restarting the latter will also stop/restart the former.
Thus, to shut down the bridge entirely, either stop `matrix-puppeteer-line-chrome.service`, or stop both services at once.
## Upgrading
# Upgrading
Simply `git pull` or `git rebase` the latest changes, and rerun any installation commands (`yarn --production`, `pip install -Ur ...`).
# Docker
These instructions describe how to run the bridge with Docker containers.
## Notes
* Any `docker` commands mentioned below need to be run with `sudo` unless you have configured your system otherwise. See [Docker docs](https://docs.docker.com/engine/install/linux-postinstall/) for details.
* All configuration files created by the Docker containers will be `chown`ed to UID/GID 1337. Use `sudo` access on the host to edit them.
* The `docker` commands below mount the working directory as `/data`, so make sure you always run them in the correct directory.
## Limitations
* Images must be built manually for now. It is planned for there to be prebuilt images available to pull.
* amd64/x86\_64 is the only architecture the current Dockerfiles have been tested with. For other architectures, it is necessary to change the base image of `puppet/Dockerfile` to one that provides Chrome/Chromium for your architecture.
## Initial setup
1. `cd` to the directory where you cloned this repository
1. Build the image for the bridge module with `docker build . -t matrix-puppeteer-line`
1. `cd` to the `puppet` directory, and build the image for the Puppeteer module with `docker build . -t matrix-puppeteer-line-chrome`
1. Create a new directory outside of the repository directory, and `cd` into it
1. Extract the downloaded .crx/.zip of the LINE Chrome extension to this directory
1. Run a container for the Puppeteer module for the first time, so it can create a config file for you: `docker run --rm -v $(pwd):/data:z matrix-puppeteer-line-chrome`
1. Update the config to your liking, but leave the `"executable_path"` setting as-is (unless you need to use a version of Chrome/Chromium from the host or another container).
1. Run the Puppeteer module with `docker run --restart unless-stopped -v $(pwd):/data:z matrix-puppeteer-line-chrome`
1. Run a container for the bridge module for the first time, so it can create a config file for you: `docker run --rm -v $(pwd):/data:z matrix-puppeteer-line`
1. Update the config to your liking. You'll at least need to change the homeserver settings, appservice address and permissions, as well as the socket connection to the Puppeteer module
* Note that the Puppeteer module's default config uses a unix socket at `/data/puppet.sock`
1. Generate the appservice registration by running the container again, and update your homeserver configuration to accept it
1. Run the bridge module with `docker run --restart unless-stopped -v $(pwd):/data:z matrix-puppeteer-line`
Additionally, you should either add the bridge to the same Docker network as your homeserver and datapase with `--network=<name>`, or expose the correct port(s) with `-p <port>:<port>.` (A quick-and-dirty option is to use `--network="host"`.)
## Upgrading
Simply `git pull` or `git rebase` the latest changes, rerun all `docker build` commands, then run new containers for the freshly-built images.
To upgrade the LINE extension used by Puppeteer, simply download and extract the latest .crx in the same location as for initial setup.

View File

@ -1,9 +1,11 @@
#!/bin/sh
if [ ! -w . ]; then
echo "Please ensure the /data volume of this container is writable for user:group $UID:$GID." >&2
exit
fi
# Define functions.
function fixperms {
chown -R $UID:$GID /data /opt/matrix-puppeteer-line
}
cd /opt/matrix-puppeteer-line
if [ ! -f /data/config.yaml ]; then
cp example-config.yaml /data/config.yaml
@ -11,17 +13,18 @@ if [ ! -f /data/config.yaml ]; then
echo "Copied default config file to /data/config.yaml"
echo "Modify that config file to your liking."
echo "Start the container again after that to generate the registration file."
fixperms
exit
fi
if [ ! -f /data/registration.yaml ]; then
if ! python3 -m matrix_puppeteer_line -g -c /data/config.yaml -r /data/registration.yaml; then
exit
fi
python3 -m matrix_puppeteer_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 Synapse's app service directory."
echo "Copy that over to synapses app service directory."
fixperms
exit
fi
python3 -m matrix_puppeteer_line -c /data/config.yaml
fixperms
exec su-exec $UID:$GID python3 -m matrix_puppeteer_line -c /data/config.yaml

View File

@ -1,5 +1,5 @@
# matrix-puppeteer-line - A very hacky Matrix-LINE bridge based on running LINE's Chrome extension in Puppeteer
# Copyright (C) 2020-2022 Tulir Asokan, Andrew Ferrazzutti
# Copyright (C) 2020-2021 Tulir Asokan, Andrew Ferrazzutti
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by

View File

@ -1,3 +1,2 @@
from .auth import SECTION_AUTH
from .conn import SECTION_CONNECTION
from .line import SECTION_CHATS

View File

@ -1,5 +1,5 @@
# matrix-puppeteer-line - A very hacky Matrix-LINE bridge based on running LINE's Chrome extension in Puppeteer
# Copyright (C) 2020-2022 Tulir Asokan, Andrew Ferrazzutti
# Copyright (C) 2020-2021 Tulir Asokan, Andrew Ferrazzutti
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
@ -13,7 +13,7 @@
#
# 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, TYPE_CHECKING
from typing import Optional, AsyncGenerator, Tuple
import io
import qrcode
@ -26,13 +26,8 @@ from .typehint import CommandEvent
SECTION_AUTH = HelpSection("Authentication", 10, "")
from ..db import LoginCredential
if TYPE_CHECKING:
from ..user import User
async def _login_prep(evt: CommandEvent, login_type: str) -> bool:
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")
@ -54,36 +49,16 @@ async def _login_prep(evt: CommandEvent, login_type: str) -> bool:
}
return True
async def _login_do(
gen: AsyncGenerator[Tuple[str, str], None],
*,
evt: Optional[CommandEvent] = None,
sender: Optional["User"] = None,
) -> bool:
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
if not evt and not sender:
raise ValueError("Must set either a CommandEvent or a User")
if evt:
sender = evt.sender
az = evt.az
room_id = evt.room_id
else:
az = sender.az
room_id = sender.notice_room
if not room_id:
sender.log.warning("Cannot auto-loggin: must have a notice room to do so")
return False
async for item in gen:
if item[0] == "qr":
message = "Open LINE on your smartphone and scan this QR code:"
message = "Open LINE on your primary device and scan this QR code:"
content = TextMessageEventContent(body=message, msgtype=MessageType.NOTICE)
if evt:
content.set_reply(evt.event_id)
await az.intent.send_message(room_id, content)
content.set_reply(evt.event_id)
await evt.az.intent.send_message(evt.room_id, content)
url = item[1]
buffer = io.BytesIO()
@ -91,131 +66,60 @@ async def _login_do(
size = image.pixel_size
image.save(buffer, "PNG")
qr = buffer.getvalue()
mxc = await az.intent.upload_media(qr, "image/png", "login-qr.png", len(qr))
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 az.intent.send_message(room_id, content)
await evt.az.intent.send_message(evt.room_id, content)
else:
qr_event_id = await az.intent.send_message(room_id, content)
qr_event_id = await evt.az.intent.send_message(evt.room_id, content)
elif item[0] == "pin":
pin = item[1]
message = f"Enter this PIN in LINE on your smartphone:\n{pin}"
message = f"Enter this PIN in LINE on your primary device:\n{pin}"
content = TextMessageEventContent(body=message, msgtype=MessageType.NOTICE)
if pin_event_id:
content.set_edit(pin_event_id)
await az.intent.send_message(room_id, content)
await evt.az.intent.send_message(evt.room_id, content)
else:
pin_event_id = await az.intent.send_message(room_id, content)
pin_event_id = await evt.az.intent.send_message(evt.room_id, content)
elif item[0] == "login_success":
await az.intent.send_notice(room_id, "Successfully logged in, waiting for LINE to load...")
await evt.reply("Successfully logged in, waiting for LINE to load...")
elif item[0] in ("login_failure", "error"):
# TODO Handle errors differently?
failure = True
reason = item[1]
if reason:
await az.intent.send_notice(room_id, reason)
content = TextMessageEventContent(body=reason, msgtype=MessageType.NOTICE)
await evt.az.intent.send_message(evt.room_id, content)
# else: pass
login_success = not failure and sender.command_status
if login_success:
await az.intent.send_notice(room_id, "LINE loading complete")
await sender.sync()
if not failure and evt.sender.command_status:
await evt.reply("LINE loading complete")
await evt.sender.sync()
# else command was cancelled or failed. Don't post message about it, "cancel" command or failure did already
sender.command_status = None
return login_success
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"):
if not await login_prep(evt, "qr"):
return
gen = evt.sender.client.login(evt.sender)
await _login_do(gen, evt=evt)
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, and optionally save credentials for auto-login",
help_args="[--save] <_email_> <_password_>")
help_text="Log into LINE via email/password",
help_args="<_email_> <_password_>")
async def login_email(evt: CommandEvent) -> None:
await evt.az.intent.redact(evt.room_id, evt.event_id)
if evt.args and evt.args[0] == "--save":
save = True
evt.args.pop(0)
else:
save = False
if len(evt.args) != 2:
await evt.reply("Usage: `$cmdprefix+sp login-email [--save] <email> <password>`")
await evt.reply("Usage: `$cmdprefix+sp login <email> <password>`")
return
if not await _login_prep(evt, "email"):
if not await login_prep(evt, "email"):
return
await evt.reply("Logging in...")
login_data = {
"email": evt.args[0],
"password": evt.args[1]
}
gen = evt.sender.client.login(
evt.sender,
login_data=login_data
)
login_success = await _login_do(gen, evt=evt)
if login_success and save:
if not evt.sender.notice_room:
await evt.reply("WARNING: You do not have a notice room, but auto-login requires one.")
await _save_password_helper(evt)
async def auto_login(sender: "User") -> bool:
status = await sender.client.start()
if status.is_logged_in:
return True
if sender.command_status is not None:
return False
creds = await LoginCredential.get_by_mxid(sender.mxid)
if not creds:
return False
sender.command_status = {
"action": "Login",
"login_type": "email",
}
gen = sender.client.login(
sender,
login_data={
"email": creds.email,
"password": creds.password,
}
)
return await _login_do(gen, sender=sender)
@command_handler(needs_auth=True, management_only=True, help_section=SECTION_AUTH,
help_text="Remember email/password credentials for auto-login",
help_args="<_email_> <_password_>")
async def save_password(evt: CommandEvent) -> None:
await evt.az.intent.redact(evt.room_id, evt.event_id)
if len(evt.args) != 2:
await evt.reply("Usage: `$cmdprefix+sp save_password <email> <password>`")
return
await _save_password_helper(evt)
async def _save_password_helper(evt: CommandEvent) -> None:
creds = await LoginCredential.get_by_mxid(evt.sender.mxid)
if creds:
creds.email = evt.args[0]
creds.password = evt.args[1]
await creds.update()
else:
await LoginCredential(evt.sender.mxid, email=evt.args[0], password=evt.args[1]).insert()
await evt.reply("Login email/password saved, and will be used to log you back in if your LINE connection ends.")
@command_handler(needs_auth=False, management_only=True, help_section=SECTION_AUTH,
help_text="Delete saved email/password credentials")
async def forget_password(evt: CommandEvent) -> None:
creds = await LoginCredential.get_by_mxid(evt.sender.mxid)
if not creds:
await evt.reply("The bridge wasn't storing your email/password, so there was nothing to forget.")
else:
await creds.delete()
await evt.reply(
"This bridge is no longer storing your email/password. \n"
"You will have to log in manually the next time your LINE connection ends."
)
evt.sender,
login_data=dict(email=evt.args[0], password=evt.args[1]))
await login_do(evt, gen)

View File

@ -1,5 +1,5 @@
# matrix-puppeteer-line - A very hacky Matrix-LINE bridge based on running LINE's Chrome extension in Puppeteer
# Copyright (C) 2020-2022 Tulir Asokan, Andrew Ferrazzutti
# Copyright (C) 2020-2021 Tulir Asokan, Andrew Ferrazzutti
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
@ -41,18 +41,6 @@ async def ping(evt: CommandEvent) -> None:
@command_handler(needs_auth=True, management_only=False, help_section=SECTION_CONNECTION,
help_text="Synchronize contacts and chats", help_args="[_limit_]")
help_text="Synchronize portals")
async def sync(evt: CommandEvent) -> None:
limit = 0
for arg in evt.args:
try:
limit = int(arg)
except ValueError:
pass
await evt.sender.sync(limit)
@command_handler(needs_auth=True, management_only=False, help_section=SECTION_CONNECTION,
help_text="Synchronize contacts only")
async def sync_contacts(evt: CommandEvent) -> None:
await evt.sender.sync_contacts()
await evt.sender.sync()

View File

@ -1,35 +0,0 @@
# matrix-puppeteer-line - A very hacky Matrix-LINE bridge based on running LINE's Chrome extension in Puppeteer
# Copyright (C) 2020-2022 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 mautrix.bridge.commands import HelpSection, command_handler
from .. import puppet as pu
from .typehint import CommandEvent
SECTION_CHATS = HelpSection("Contacts & Chats", 40, "")
@command_handler(needs_auth=True, management_only=False, help_section=SECTION_CHATS,
help_text="List all LINE contacts")
async def list_contacts(evt: CommandEvent) -> None:
# TODO Use a generator if it's worth it
puppets = await pu.Puppet.get_all()
puppets.sort(key=lambda puppet: puppet.name)
results = "".join(f"* [{puppet.name}](https://matrix.to/#/{puppet.default_mxid})\n"
for puppet in puppets)
if results:
await evt.reply(f"Contacts:\n\n{results}")
else:
await evt.reply("No contacts found.")

View File

@ -1,5 +1,5 @@
# matrix-puppeteer-line - A very hacky Matrix-LINE bridge based on running LINE's Chrome extension in Puppeteer
# Copyright (C) 2020-2022 Tulir Asokan, Andrew Ferrazzutti
# Copyright (C) 2020-2021 Tulir Asokan, Andrew Ferrazzutti
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by

View File

@ -7,25 +7,12 @@ from .stranger import Stranger
from .portal import Portal
from .message import Message
from .media import Media
from .receipt import Receipt
from .receipt_reaction import ReceiptReaction
from .login_credential import LoginCredential
def init(db: Database) -> None:
for table in (User, Puppet, Stranger, Portal, Message, Media, Receipt, ReceiptReaction, LoginCredential):
for table in (User, Puppet, Stranger, Portal, Message, Media, ReceiptReaction):
table.db = db
__all__ = [
"upgrade_table",
"User",
"Puppet",
"Stranger",
"Portal",
"Message",
"Media",
"Receipt",
"ReceiptReaction",
"LoginCredential"
]
__all__ = ["upgrade_table", "User", "Puppet", "Stranger", "Portal", "Message", "Media", "ReceiptReaction"]

View File

@ -1,54 +0,0 @@
# matrix-puppeteer-line - A very hacky Matrix-LINE bridge based on running LINE's Chrome extension in Puppeteer
# Copyright (C) 2020-2022 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, ClassVar, TYPE_CHECKING
from attr import dataclass
from mautrix.types import UserID
from mautrix.util.async_db import Database
fake_db = Database("") if TYPE_CHECKING else None
@dataclass
class LoginCredential:
db: ClassVar[Database] = fake_db
mxid: UserID
email: str
password: str
async def insert(self) -> None:
q = ("INSERT INTO login_credential (mxid, email, password) "
"VALUES ($1, $2, $3)")
await self.db.execute(q, self.mxid, self.email, self.password)
async def update(self) -> None:
await self.db.execute("UPDATE login_credential SET email=$2, password=$3 WHERE mxid=$1",
self.mxid, self.email, self.password)
@classmethod
async def get_by_mxid(cls, mxid: UserID) -> Optional["LoginCredential"]:
q = ("SELECT mxid, email, password "
"FROM login_credential WHERE mxid=$1")
row = await cls.db.fetchrow(q, mxid)
if not row:
return None
return cls(**row)
async def delete(self) -> None:
await self.db.execute("DELETE FROM login_credential WHERE mxid=$1",
self.mxid)

View File

@ -1,5 +1,5 @@
# matrix-puppeteer-line - A very hacky Matrix-LINE bridge based on running LINE's Chrome extension in Puppeteer
# Copyright (C) 2020-2022 Tulir Asokan, Andrew Ferrazzutti
# Copyright (C) 2020-2021 Tulir Asokan, Andrew Ferrazzutti
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by

View File

@ -1,5 +1,5 @@
# matrix-puppeteer-line - A very hacky Matrix-LINE bridge based on running LINE's Chrome extension in Puppeteer
# Copyright (C) 2020-2022 Tulir Asokan, Andrew Ferrazzutti
# Copyright (C) 2020-2021 Tulir Asokan, Andrew Ferrazzutti
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
@ -13,7 +13,7 @@
#
# 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, ClassVar, Dict, List, TYPE_CHECKING
from typing import Optional, ClassVar, Dict, TYPE_CHECKING
from attr import dataclass
@ -27,21 +27,22 @@ fake_db = Database("") if TYPE_CHECKING else None
class Message:
db: ClassVar[Database] = fake_db
mxid: Optional[EventID]
mxid: EventID
mx_room: RoomID
mid: Optional[int]
mid: int
chat_id: str
is_outgoing: bool
async def insert(self) -> None:
q = "INSERT INTO message (mxid, mx_room, mid, chat_id, is_outgoing) VALUES ($1, $2, $3, $4, $5)"
await self.db.execute(q, self.mxid, self.mx_room, self.mid, self.chat_id, self.is_outgoing)
q = "INSERT INTO message (mxid, mx_room, mid, chat_id) VALUES ($1, $2, $3, $4)"
await self.db.execute(q, self.mxid, self.mx_room, self.mid, self.chat_id)
async def update_ids(self, new_mxid: EventID, new_mid: int) -> None:
q = ("UPDATE message SET mxid=$1, mid=$2 "
"WHERE mxid=$3 AND mx_room=$4 AND chat_id=$5")
await self.db.execute(q, new_mxid, new_mid,
self.mxid, self.mx_room, self.chat_id)
async def delete(self) -> None:
q = "DELETE FROM message WHERE mid=$1"
await self.db.execute(q, self.mid)
@classmethod
async def delete_all(cls, room_id: RoomID) -> None:
await cls.db.execute("DELETE FROM message WHERE mx_room=$1", room_id)
@classmethod
async def get_max_mid(cls, room_id: RoomID) -> int:
@ -56,35 +57,9 @@ class Message:
data[row["chat_id"]] = row["max_mid"]
return data
@classmethod
async def get_max_outgoing_mids(cls) -> Dict[str, int]:
rows = await cls.db.fetch("SELECT chat_id, MAX(mid) AS max_mid "
"FROM message WHERE is_outgoing GROUP BY chat_id")
data = {}
for row in rows:
data[row["chat_id"]] = row["max_mid"]
return data
@classmethod
async def get_num_noid_msgs(cls, room_id: RoomID) -> int:
return await cls.db.fetchval("SELECT COUNT(*) FROM message "
"WHERE mid IS NULL AND mx_room=$1", room_id)
@classmethod
async def is_last_by_mxid(cls, mxid: EventID, room_id: RoomID) -> bool:
q = ("SELECT mxid "
"FROM message INNER JOIN ( "
" SELECT mx_room, MAX(mid) AS max_mid "
" FROM message GROUP BY mx_room "
") by_room "
"ON mid=max_mid "
"WHERE by_room.mx_room=$1")
last_mxid = await cls.db.fetchval(q, room_id)
return last_mxid == mxid
@classmethod
async def get_by_mxid(cls, mxid: EventID, mx_room: RoomID) -> Optional['Message']:
row = await cls.db.fetchrow("SELECT mxid, mx_room, mid, chat_id, is_outgoing "
row = await cls.db.fetchrow("SELECT mxid, mx_room, mid, chat_id "
"FROM message WHERE mxid=$1 AND mx_room=$2", mxid, mx_room)
if not row:
return None
@ -92,30 +67,8 @@ class Message:
@classmethod
async def get_by_mid(cls, mid: int) -> Optional['Message']:
row = await cls.db.fetchrow("SELECT mxid, mx_room, mid, chat_id, is_outgoing FROM message WHERE mid=$1",
row = await cls.db.fetchrow("SELECT mxid, mx_room, mid, chat_id FROM message WHERE mid=$1",
mid)
if not row:
return None
return cls(**row)
@classmethod
async def get_all_since(cls, chat_id: str, min_mid: int, max_mid: int) -> List['Message']:
rows = await cls.db.fetch("SELECT mxid, mx_room, mid, chat_id, is_outgoing FROM message "
"WHERE chat_id=$1 AND $2<mid AND mid<=$3",
chat_id, min_mid, max_mid)
return [cls(**row) for row in rows]
@classmethod
async def get_next_noid_msg(cls, room_id: RoomID) -> Optional['Message']:
row = await cls.db.fetchrow("SELECT mxid, mx_room, mid, chat_id, is_outgoing FROM message "
"WHERE mid IS NULL AND mx_room=$1", room_id)
if not row:
return None
return cls(**row)
@classmethod
async def delete_all_noid_msgs(cls, room_id: RoomID) -> None:
status = await cls.db.execute("DELETE FROM message "
"WHERE mid IS NULL AND mx_room=$1", room_id)
# Skip leading "DELETE "
return int(status[7:])

View File

@ -1,5 +1,5 @@
# matrix-puppeteer-line - A very hacky Matrix-LINE bridge based on running LINE's Chrome extension in Puppeteer
# Copyright (C) 2020-2022 Tulir Asokan, Andrew Ferrazzutti
# Copyright (C) 2020-2021 Tulir Asokan, Andrew Ferrazzutti
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by

View File

@ -1,5 +1,5 @@
# matrix-puppeteer-line - A very hacky Matrix-LINE bridge based on running LINE's Chrome extension in Puppeteer
# Copyright (C) 2020-2022 Tulir Asokan, Andrew Ferrazzutti
# Copyright (C) 2020-2021 Tulir Asokan, Andrew Ferrazzutti
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
@ -13,7 +13,7 @@
#
# 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, ClassVar, List, TYPE_CHECKING
from typing import Optional, ClassVar, TYPE_CHECKING
from attr import dataclass
@ -61,10 +61,3 @@ class Puppet:
if not row:
return None
return cls(**row)
@classmethod
async def get_all(cls) -> List['Puppet']:
q = ("SELECT mid, name, avatar_path, avatar_mxc, name_set, avatar_set, is_registered "
"FROM puppet")
rows = await cls.db.fetch(q)
return [cls(**row) for row in rows]

View File

@ -1,72 +0,0 @@
# matrix-puppeteer-line - A very hacky Matrix-LINE bridge based on running LINE's Chrome extension in Puppeteer
# Copyright (C) 2020-2022 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 List, ClassVar, Dict, Optional, TYPE_CHECKING
from attr import dataclass
from mautrix.util.async_db import Database
fake_db = Database("") if TYPE_CHECKING else None
@dataclass
class Receipt:
db: ClassVar[Database] = fake_db
mid: int
chat_id: str
num_read: int
async def insert_or_update(self) -> None:
q = ("INSERT INTO receipt (mid, chat_id, num_read) "
"VALUES ($1, $2, $3) "
"ON CONFLICT (chat_id, num_read) "
"DO UPDATE SET mid=EXCLUDED.mid, num_read=EXCLUDED.num_read")
await self.db.execute(q, self.mid, self.chat_id, self.num_read)
# Delete lower counts for earlier messages
# TODO Consider using a CHECK for this instead
q = ("DELETE FROM receipt "
"WHERE chat_id=$1 AND mid<$2 AND num_read<$3")
await self.db.execute(q, self.chat_id, self.mid, self.num_read)
@classmethod
async def get_max_mid(cls, chat_id: str, num_read: int) -> Optional[int]:
q = ("SELECT mid FROM receipt "
"WHERE chat_id=$1 AND num_read=$2")
return await cls.db.fetchval(q, chat_id, num_read)
@classmethod
async def get_max_mid_per_num_read(cls, chat_id: str) -> Dict[int, int]:
rows = await cls.db.fetch("SELECT chat_id, mid, num_read FROM receipt WHERE chat_id=$1", chat_id)
data = {}
for row in rows:
data[row["num_read"]] = row["mid"]
return data
@classmethod
async def get_max_mids_per_num_read(cls) -> Dict[str, Dict[int, int]]:
rows = await cls.db.fetch("SELECT chat_id, mid, num_read FROM receipt")
data = {}
for row in rows:
chat_id = row["chat_id"]
if chat_id not in data:
inner_data = {}
data[chat_id] = inner_data
else:
inner_data = data[chat_id]
inner_data[row["num_read"]] = row["mid"]
return data

View File

@ -1,5 +1,5 @@
# matrix-puppeteer-line - A very hacky Matrix-LINE bridge based on running LINE's Chrome extension in Puppeteer
# Copyright (C) 2020-2022 Tulir Asokan, Andrew Ferrazzutti
# Copyright (C) 2020-2021 Tulir Asokan, Andrew Ferrazzutti
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by

View File

@ -1,5 +1,5 @@
# matrix-puppeteer-line - A very hacky Matrix-LINE bridge based on running LINE's Chrome extension in Puppeteer
# Copyright (C) 2020-2022 Tulir Asokan, Andrew Ferrazzutti
# Copyright (C) 2020-2021 Tulir Asokan, Andrew Ferrazzutti
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by

View File

@ -1,5 +1,5 @@
# matrix-puppeteer-line - A very hacky Matrix-LINE bridge based on running LINE's Chrome extension in Puppeteer
# Copyright (C) 2020-2022 Tulir Asokan, Andrew Ferrazzutti
# Copyright (C) 2020-2021 Tulir Asokan, Andrew Ferrazzutti
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
@ -72,22 +72,20 @@ async def upgrade_media(conn: Connection) -> None:
@upgrade_table.register(description="Helpful table constraints")
async def upgrade_table_constraints(conn: Connection) -> None:
table_name = "portal"
constraint_name = f"{table_name}_mxid_key"
constraint_name = "portal_mxid_key"
q = ( "SELECT EXISTS(SELECT FROM information_schema.constraint_table_usage "
f"WHERE table_name='{table_name}' AND constraint_name='{constraint_name}')")
has_constraint = await conn.fetchval(q)
if not has_constraint:
await conn.execute(f"ALTER TABLE {table_name} ADD CONSTRAINT {constraint_name} UNIQUE(mxid)")
f"WHERE table_name='portal' AND constraint_name='{constraint_name}')")
has_unique_mxid = await conn.fetchval(q)
if not has_unique_mxid:
await conn.execute(f"ALTER TABLE portal ADD CONSTRAINT {constraint_name} UNIQUE(mxid)")
table_name = "message"
constraint_name = f"{table_name}_chat_id_fkey"
constraint_name = "message_chat_id_fkey"
q = ( "SELECT EXISTS(SELECT FROM information_schema.table_constraints "
f"WHERE table_name='{table_name}' AND constraint_name='{constraint_name}')")
has_constraint = await conn.fetchval(q)
if not has_constraint:
f"WHERE table_name='message' AND constraint_name='{constraint_name}')")
has_fkey = await conn.fetchval(q)
if not has_fkey:
await conn.execute(
f"ALTER TABLE {table_name} ADD CONSTRAINT {constraint_name} "
f"ALTER TABLE message ADD CONSTRAINT {constraint_name} "
"FOREIGN KEY (chat_id) "
"REFERENCES portal (chat_id) "
"ON DELETE CASCADE")
@ -130,56 +128,4 @@ async def upgrade_strangers(conn: Connection) -> None:
FOREIGN KEY (fake_mid)
REFERENCES puppet (mid)
ON DELETE CASCADE
)""")
@upgrade_table.register(description="Track messages that lack an ID")
async def upgrade_noid_msgs(conn: Connection) -> None:
await conn.execute("ALTER TABLE message DROP CONSTRAINT IF EXISTS message_pkey")
await conn.execute("ALTER TABLE message ALTER COLUMN mid DROP NOT NULL")
table_name = "message"
constraint_name = f"{table_name}_mid_key"
q = ( "SELECT EXISTS(SELECT FROM information_schema.constraint_table_usage "
f"WHERE table_name='{table_name}' AND constraint_name='{constraint_name}')")
has_constraint = await conn.fetchval(q)
if not has_constraint:
await conn.execute(f"ALTER TABLE {table_name} ADD UNIQUE (mid)")
@upgrade_table.register(description="Track LINE read receipts")
async def upgrade_latest_read_receipts(conn: Connection) -> None:
await conn.execute("ALTER TABLE message DROP CONSTRAINT IF EXISTS message_mid_key")
await conn.execute("ALTER TABLE message ADD UNIQUE (mid, chat_id)")
await conn.execute("ALTER TABLE message "
"ADD COLUMN IF NOT EXISTS "
"is_outgoing BOOLEAN NOT NULL DEFAULT false")
await conn.execute("""CREATE TABLE IF NOT EXISTS receipt (
mid INTEGER NOT NULL,
chat_id TEXT NOT NULL,
num_read INTEGER NOT NULL DEFAULT 1,
PRIMARY KEY (chat_id, num_read),
FOREIGN KEY (mid, chat_id)
REFERENCES message (mid, chat_id)
ON DELETE CASCADE
)""")
@upgrade_table.register(description="Allow messages with no mxid")
async def upgrade_nomxid_msgs(conn: Connection) -> None:
await conn.execute("ALTER TABLE message ALTER COLUMN mxid DROP NOT NULL")
@upgrade_table.register(description="Allow storing email/password login credentials")
async def upgrade_login_credentials(conn: Connection) -> None:
await conn.execute("""CREATE TABLE IF NOT EXISTS login_credential (
mxid TEXT PRIMARY KEY,
email TEXT NOT NULL,
password TEXT NOT NULL,
FOREIGN KEY (mxid)
REFERENCES "user" (mxid)
ON DELETE CASCADE
)""")
)""")

View File

@ -1,5 +1,5 @@
# matrix-puppeteer-line - A very hacky Matrix-LINE bridge based on running LINE's Chrome extension in Puppeteer
# Copyright (C) 2020-2022 Tulir Asokan, Andrew Ferrazzutti
# Copyright (C) 2020-2021 Tulir Asokan, Andrew Ferrazzutti
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
@ -47,8 +47,3 @@ class User:
if not row:
return None
return cls(**row)
@classmethod
async def discard_notice_room(cls, notice_room: RoomID) -> None:
await cls.db.execute('DELETE FROM "user" WHERE notice_room=$1',
notice_room)

View File

@ -45,7 +45,7 @@ appservice:
# 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: LINE bridge bot
bot_avatar: mxc://miscworks.net/vkVOqyfLTQTfRvlEgEoampPW
bot_avatar: mxc://maunium.net/VuvevQiMRlOxuBVMBNEZZrxi
# Community ID for bridged users (changes registration file) and rooms.
# Must be created manually.
@ -57,12 +57,6 @@ appservice:
as_token: "This value is generated when generating the registration"
hs_token: "This value is generated when generating the registration"
# Whether or not to receive ephemeral events via appservice transactions.
# Requires MSC2409 support (i.e. Synapse 1.22+).
# This is REQUIRED in order to bypass Puppeteer needing to "view" a LINE chat
# (thus triggering a LINE read receipt on your behalf) to sync its messages.
ephemeral_events: false
# Prometheus telemetry config. Requires prometheus-client to be installed.
metrics:
enabled: false
@ -81,9 +75,8 @@ bridge:
# Maximum length of displayname
displayname_max_length: 100
# Number of conversations to sync (and create portals for) on login
# and with the "sync" command with no numeric argument passed to it.
# Set 0 to disable automatic syncing on login.
# Number of conversations to sync (and create portals for) on login.
# Set 0 to disable automatic syncing.
initial_conversation_sync: 10
# Whether or not the LINE users of logged in Matrix users should be
# invited to rooms when the user sends a message from another client.
@ -98,7 +91,7 @@ bridge:
# Whether or not created rooms should have federation enabled.
# If false, created portal rooms will never be federated.
federate_rooms: true
# Settings for backfilling messages.
# Settings for backfilling messages from the Messages app.
backfill:
# If using double puppeting, should notifications be disabled
# while the initial backfill is in progress?

View File

@ -1,5 +1,5 @@
# matrix-puppeteer-line - A very hacky Matrix-LINE bridge based on running LINE's Chrome extension in Puppeteer
# Copyright (C) 2020-2022 Tulir Asokan, Andrew Ferrazzutti
# Copyright (C) 2020-2021 Tulir Asokan, Andrew Ferrazzutti
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
@ -16,13 +16,10 @@
from typing import TYPE_CHECKING
from mautrix.bridge import BaseMatrixHandler
from mautrix.types import (Event, EventType, MessageEvent, StateEvent, EncryptedEvent,
ReceiptEvent, SingleReceiptEventContent, TextMessageEventContent,
from mautrix.types import (Event, ReactionEvent, MessageEvent, StateEvent, EncryptedEvent, RedactionEvent,
EventID, RoomID, UserID)
from mautrix.errors import MatrixError
from . import portal as po, puppet as pu, user as u
from .db import User as DBUser
if TYPE_CHECKING:
from .__main__ import MessagesBridge
@ -38,9 +35,8 @@ class MatrixHandler(BaseMatrixHandler):
super().__init__(bridge=bridge)
def filter_matrix_event(self, evt: Event) -> bool:
if isinstance(evt, ReceiptEvent):
return False
if not isinstance(evt, (MessageEvent, StateEvent, EncryptedEvent)):
if not isinstance(evt, (ReactionEvent, MessageEvent, StateEvent, EncryptedEvent,
RedactionEvent)):
return True
return (evt.sender == self.az.bot_mxid
or pu.Puppet.get_id_from_mxid(evt.sender) is not None)
@ -53,113 +49,9 @@ class MatrixHandler(BaseMatrixHandler):
await self.az.intent.send_notice(room_id, "This room has been marked as your "
"LINE bridge notice room.")
async def handle_puppet_invite(self, room_id: RoomID, puppet: 'pu.Puppet',
invited_by: 'u.User', _: EventID) -> None:
intent = puppet.intent
self.log.debug(f"{invited_by.mxid} invited puppet for {puppet.mid} to {room_id}")
if not await invited_by.is_logged_in():
await intent.error_and_leave(room_id, text="Please log in before inviting "
"LINE puppets to private chats.")
return
portal = await po.Portal.get_by_mxid(room_id)
if portal:
if portal.is_direct:
await intent.error_and_leave(room_id, text="You can not invite additional users "
"to private chats.")
else:
# TODO Send invite in LINE
await intent.error_and_leave(room_id, text="Inviting additional users to an existing "
"group chat is not yet supported.")
return
await intent.join_room(room_id)
try:
members = await intent.get_room_members(room_id)
except MatrixError:
self.log.exception(f"Failed to get member list after joining {room_id}")
await intent.leave_room(room_id)
return
if len(members) > 2:
# TODO Add LINE group/room creating. Must also distinguish between the two!
await intent.send_notice(room_id, "You can not invite LINE puppets to "
"multi-user rooms.")
await intent.leave_room(room_id)
return
portal = await po.Portal.get_by_chat_id(puppet.mid, create=True)
if portal.mxid:
try:
await intent.invite_user(portal.mxid, invited_by.mxid, check_cache=False)
await intent.send_notice(room_id,
text=("You already have a private chat with me "
f"in room {portal.mxid}"),
html=("You already have a private chat with me: "
f"<a href='https://matrix.to/#/{portal.mxid}'>"
"Link to room"
"</a>"))
await intent.leave_room(room_id)
return
except MatrixError:
pass
portal.mxid = room_id
e2be_ok = await portal.check_dm_encryption()
# TODO Consider setting other power levels that get set on portal creation,
# but they're of little use when the inviting user has an equal PL...
await portal.save()
if e2be_ok is True:
evt_type, content = await self.e2ee.encrypt(
room_id, EventType.ROOM_MESSAGE,
TextMessageEventContent(msgtype=MessageType.NOTICE,
body="Portal to private chat created and end-to-bridge"
" encryption enabled."))
await intent.send_message_event(room_id, evt_type, content)
else:
message = "Portal to private chat created."
if e2be_ok is False:
message += "\n\nWarning: Failed to enable end-to-bridge encryption"
await intent.send_notice(room_id, message)
# TODO Put pause/resume in portal methods, with a lock or something
# TODO Consider not backfilling on invite.
# To do so, must set the last-seen message ID appropriately
await invited_by.client.pause()
try:
chat_info = await invited_by.client.get_chat(puppet.mid)
await portal.update_matrix_room(invited_by, chat_info)
finally:
await invited_by.client.resume()
async def handle_join(self, room_id: RoomID, user_id: UserID, _: EventID) -> None:
user = await u.User.get_by_mxid(user_id)
portal = await po.Portal.get_by_mxid(room_id)
if not portal:
return
if not user.is_whitelisted:
await portal.main_intent.kick_user(room_id, user.mxid,
"You are not whitelisted on this LINE bridge.")
return
elif not await user.is_logged_in():
await portal.main_intent.kick_user(room_id, user.mxid,
"You are not logged in to this LINE bridge.")
return
self.log.debug(f"{user.mxid} joined {room_id}")
async def handle_leave(self, room_id: RoomID, user_id: UserID, event_id: EventID) -> None:
portal = await po.Portal.get_by_mxid(room_id)
if not portal:
intent = self.bridge.az.intent
try:
if len(await intent.get_room_members(room_id)) == 1:
self.log.info(f"Bridge bot leaving empty room")
await intent.leave_room(room_id)
await DBUser.discard_notice_room(room_id)
except MatrixError:
self.log.exception(f"Failed to get member list of {room_id}")
return
user = await u.User.get_by_mxid(user_id, create=False)
@ -167,20 +59,3 @@ class MatrixHandler(BaseMatrixHandler):
return
await portal.handle_matrix_leave(user)
async def handle_reject(self, room_id: RoomID, user_id: UserID, reason: str, event_id: EventID) -> None:
await self.handle_leave(room_id, user_id, event_id)
async def handle_read_receipt(self, user: 'u.User', portal: 'po.Portal', event_id: EventID,
data: SingleReceiptEventContent) -> None:
# When reading a bridged message, view its chat in LINE, to make it send a read receipt.
# TODO Use *null* mids for last messages in a chat!!
# Only visit a LINE chat when its LAST bridge message has been read,
# because LINE lacks per-message read receipts--it's all or nothing!
# TODO Also view if message is non-last but for media, so it can be loaded.
#if await DBMessage.is_last_by_mxid(event_id, portal.mxid):
# Viewing a chat by updating it whole-hog, lest a ninja arrives
if not user.is_syncing:
await user.sync_portal(portal)

View File

@ -1,5 +1,5 @@
# matrix-puppeteer-line - A very hacky Matrix-LINE bridge based on running LINE's Chrome extension in Puppeteer
# Copyright (C) 2020-2022 Tulir Asokan, Andrew Ferrazzutti
# Copyright (C) 2020-2021 Tulir Asokan, Andrew Ferrazzutti
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
@ -30,10 +30,9 @@ from mautrix.types import (EventID, MessageEventContent, RoomID, EventType, Mess
ContentURI, EncryptedFile, ImageInfo,
RelatesTo, RelationType)
from mautrix.errors import IntentError
from mautrix.errors.request import MatrixRequestError
from mautrix.util.simple_lock import SimpleLock
from .db import Portal as DBPortal, Message as DBMessage, Receipt as DBReceipt, ReceiptReaction as DBReceiptReaction, Media as DBMedia
from .db import Portal as DBPortal, Message as DBMessage, ReceiptReaction as DBReceiptReaction, Media as DBMedia
from .config import Config
from .rpc import ChatInfo, Participant, Message, Receipt, Client, PathImage
from .rpc.types import RPCError
@ -98,10 +97,6 @@ class Portal(DBPortal, BasePortal):
# Reminder that the bridgebot's intent is used for non-DM rooms
return not self.is_direct or (self.encrypted and self.matrix.e2ee)
@property
def needs_portal_meta(self) -> bool:
return self.encrypted or not self.is_direct or self.config["bridge.private_chat_portal_meta"]
@property
def main_intent(self) -> IntentAPI:
if not self._main_intent:
@ -129,20 +124,14 @@ class Portal(DBPortal, BasePortal):
except Exception:
self.log.exception("Failed to send delivery receipt for %s", event_id)
async def _cleanup_noid_msgs(self) -> None:
num_noid_msgs = await DBMessage.delete_all_noid_msgs(self.mxid)
if num_noid_msgs > 0:
self.log.warn(f"Found {num_noid_msgs} messages in chat {self.chat_id} with no ID that could not be matched with a real ID")
async def handle_matrix_message(self, sender: 'u.User', message: MessageEventContent,
event_id: EventID) -> None:
if not await sender.is_logged_in():
self.log.debug(f"Ignoring message {event_id} as sender {sender.mxid} is not connected")
if not sender.client:
self.log.debug(f"Ignoring message {event_id} as user is not connected")
return
elif ((message.get(self.bridge.real_user_content_key,
False) and await p.Puppet.get_by_custom_mxid(sender.mxid))):
self.log.debug(f"Ignoring puppet-sent message by confirmed puppet user {sender.mxid}")
await self._send_delivery_receipt(event_id)
return
# TODO deduplication of outgoing messages
text = message.body
@ -173,12 +162,10 @@ class Portal(DBPortal, BasePortal):
self.log.warning(f"Failed to upload media {event_id} to chat {self.chat_id}: {e}")
message_id = -1
remove(file_path)
await self._cleanup_noid_msgs()
msg = None
if message_id != -1:
try:
msg = DBMessage(mxid=event_id, mx_room=self.mxid, mid=message_id, chat_id=self.chat_id, is_outgoing=True)
msg = DBMessage(mxid=event_id, mx_room=self.mxid, mid=message_id, chat_id=self.chat_id)
await msg.insert()
await self._send_delivery_receipt(event_id)
self.log.debug(f"Handled Matrix message {event_id} -> {message_id}")
@ -187,20 +174,20 @@ class Portal(DBPortal, BasePortal):
if not msg and self.config["bridge.delivery_error_reports"]:
await self.main_intent.send_notice(
self.mxid,
"Posting this message to LINE may have failed.",
relates_to=RelatesTo(rel_type=RelationType.REPLY, event_id=event_id))
"Posting this message to LINE may have failed.",
relates_to=RelatesTo(rel_type=RelationType.REPLY, event_id=event_id))
async def handle_matrix_leave(self, user: 'u.User') -> None:
self.log.info(f"{user.mxid} left portal to {self.chat_id}, "
f"cleaning up and deleting...")
if await user.is_logged_in():
await user.client.forget_chat(self.chat_id)
await self.cleanup_and_delete()
async def _bridge_own_message_pm(self, source: 'u.User', puppet: Optional['p.Puppet'], mid: str,
async def _bridge_own_message_pm(self, source: 'u.User', sender: Optional['p.Puppet'], mid: str,
invite: bool = True) -> Optional[IntentAPI]:
intent = puppet.intent if puppet else (await source.get_own_puppet()).intent
if self.is_direct and (not puppet or puppet.mid == source.mid and not puppet.is_real_user):
# Use bridge bot as puppet for own user when puppet for own user is unavailable
# TODO Use own LINE puppet instead, and create it if it's not available yet
intent = sender.intent if sender else self.az.intent
if self.is_direct and (sender is None or sender.mid == source.mid and not sender.is_real_user):
if self.invite_own_puppet_to_pm and invite:
try:
await intent.ensure_joined(self.mxid)
@ -218,66 +205,36 @@ class Portal(DBPortal, BasePortal):
intent = None
return intent
async def handle_remote_message(self, source: 'u.User', evt: Message, handle_receipt: bool = True) -> None:
async def handle_remote_message(self, source: 'u.User', evt: Message) -> None:
if await DBMessage.get_by_mid(evt.id):
self.log.debug(f"Ignoring duplicate message {evt.id}")
return
if evt.is_outgoing:
if source.intent:
sender = None
intent = source.intent
else:
if not self.invite_own_puppet_to_pm:
self.log.warning(f"Ignoring message {evt.id}: double puppeting isn't enabled")
return
puppet = await p.Puppet.get_by_mid(evt.sender.id) if evt.sender else None
intent = await self._bridge_own_message_pm(source, puppet, f"message {evt.id}")
sender = p.Puppet.get_by_mid(evt.sender.id) if not self.is_direct else None
intent = await self._bridge_own_message_pm(source, sender, f"message {evt.id}")
if not intent:
return
else:
if self.is_direct:
# TODO Respond to name/avatar changes of users in a DM
intent = (await p.Puppet.get_by_mid(self.other_user)).intent
elif evt.sender:
puppet = await p.Puppet.get_by_mid(evt.sender.id)
if puppet:
await puppet.update_info(evt.sender, source.client)
sender = await p.Puppet.get_by_mid(self.other_user if self.is_direct else evt.sender.id)
# TODO Respond to name/avatar changes of users in a DM
if not self.is_direct:
if sender:
await sender.update_info(evt.sender, source.client)
else:
self.log.warning(f"Could not find ID of LINE user who sent message {evt.id or 'with no ID'}")
puppet = await p.Puppet.get_by_profile(evt.sender, source.client)
intent = puppet.intent
else:
self.log.info(f"Using bridgebot for unknown sender of message {evt.id or 'with no ID'}")
intent = self.az.intent
if not evt.member_info:
await intent.ensure_joined(self.mxid)
self.log.warning(f"Could not find ID of LINE user who sent event {evt.id}")
sender = await p.Puppet.get_by_profile(evt.sender, source.client)
intent = sender.intent
intent.ensure_joined(self.mxid)
if evt.id:
msg = await DBMessage.get_next_noid_msg(self.mxid)
if not msg:
self.log.info(f"Handling new message {evt.id} in chat {self.mxid}")
prev_event_id = None
elif not msg.mxid:
self.log.error(f"Preseen message {evt.id} in chat {self.mxid} has no mxid")
return
else:
self.log.info(f"Handling preseen message {evt.id} in chat {self.mxid}: {msg.mxid}")
if not self.is_direct:
# Non-DM previews are always sent by bridgebot.
# Must delete the bridgebot message and send a new message from the correct puppet.
await self.az.intent.redact(self.mxid, msg.mxid, "Found actual sender")
prev_event_id = None
else:
prev_event_id = msg.mxid
else:
self.log.info(f"Handling new message with no ID in chat {self.mxid}")
msg = None
prev_event_id = None
if prev_event_id and evt.html:
# No need to update a previewed text message, as their previews are accurate
event_id = prev_event_id
elif evt.image and evt.image.url:
if evt.image and evt.image.url:
if not evt.image.is_sticker or self.config["bridge.receive_stickers"]:
media_info = await self._handle_remote_media(
source, intent, evt.image.url,
@ -294,11 +251,9 @@ class Portal(DBPortal, BasePortal):
else:
media_info = None
send_sticker = self.config["bridge.use_sticker_events"] and evt.image.is_sticker and not self.encrypted and media_info
# TODO Element Web messes up text->sticker edits!!
# File a case on it
if send_sticker and not prev_event_id:
#relates_to = RelatesTo(rel_type=RelationType.REPLACE, event_id=prev_event_id) if prev_event_id else None
event_id = await intent.send_sticker(self.mxid, media_info.mxc, image_info, "<sticker>", timestamp=evt.timestamp)
if send_sticker:
event_id = await intent.send_sticker(
self.mxid, media_info.mxc, image_info, "<sticker>", timestamp=evt.timestamp)
else:
if media_info:
content = MediaMessageEventContent(
@ -310,11 +265,8 @@ class Portal(DBPortal, BasePortal):
content = TextMessageEventContent(
msgtype=MessageType.NOTICE,
body=f"<{'sticker' if evt.image.is_sticker else 'image'}>")
if prev_event_id:
content.set_edit(prev_event_id)
event_id = await self._send_message(intent, content, timestamp=evt.timestamp)
elif evt.html and not evt.html.isspace():
chunks = []
def handle_data(data):
@ -371,86 +323,51 @@ class Portal(DBPortal, BasePortal):
format=Format.HTML if msg_html else None,
body=msg_text, formatted_body=msg_html)
event_id = await self._send_message(intent, content, timestamp=evt.timestamp)
elif evt.member_info:
# TODO Track invites. Both LINE->LINE and Matrix->LINE
# TODO Make use of evt.timestamp, but how?
if evt.member_info.joined:
await intent.ensure_joined(self.mxid)
elif evt.member_info.left:
try:
await intent.leave_room(self.mxid)
except MatrixRequestError as e:
self.log.warn(f"Puppet for user {evt.sender.id} already left portal {self.mxid}")
event_id = None
# TODO Joins/leaves/invites/rejects, which are sent as LINE message events after all!
# Also keep track of strangers who leave / get blocked / become friends
# (maybe not here for all of that)
else:
content = TextMessageEventContent(
msgtype=MessageType.NOTICE,
body="<Unbridgeable message>")
if prev_event_id:
content.set_edit(prev_event_id)
event_id = await self._send_message(intent, content, timestamp=evt.timestamp)
if not msg:
msg = DBMessage(mxid=event_id, mx_room=self.mxid, mid=evt.id, chat_id=self.chat_id, is_outgoing=evt.is_outgoing)
try:
await msg.insert()
self.log.debug(f"Handled remote message {evt.id or 'with no ID'} -> {event_id or 'with no mxid'}")
except UniqueViolationError as e:
self.log.debug(f"Failed to handle remote message {evt.id or 'with no ID'} -> {event_id or 'with no mxid'}: {e}")
else:
await msg.update_ids(new_mxid=event_id, new_mid=evt.id)
self.log.debug(f"Handled preseen remote message {evt.id} -> {event_id}")
if handle_receipt and evt.is_outgoing and evt.receipt_count:
await self._handle_receipt(event_id, evt.id, evt.receipt_count)
if evt.is_outgoing and evt.receipt_count:
await self._handle_receipt(event_id, evt.receipt_count)
msg = DBMessage(mxid=event_id, mx_room=self.mxid, mid=evt.id, chat_id=self.chat_id)
try:
await msg.insert()
await self._send_delivery_receipt(event_id)
self.log.debug(f"Handled remote message {evt.id} -> {event_id}")
except UniqueViolationError as e:
self.log.debug(f"Failed to handle remote message {evt.id} -> {event_id}: {e}")
async def handle_remote_receipt(self, receipt: Receipt) -> None:
msg = await DBMessage.get_by_mid(receipt.id)
if msg:
await self._handle_receipt(msg.mxid, receipt.id, receipt.count)
await self._handle_receipt(msg.mxid, receipt.count)
else:
self.log.debug(f"Could not find message for read receipt {receipt.id}")
async def _handle_receipt(self, event_id: EventID, receipt_id: int, receipt_count: int) -> None:
async def _handle_receipt(self, event_id: EventID, receipt_count: int) -> None:
if self.is_direct:
await self.main_intent.send_receipt(self.mxid, event_id)
else:
# Update receipts not only for this message, but also for
# all messages before it with an equivalent "read by" count.
prev_receipt_id = await DBReceipt.get_max_mid(self.chat_id, receipt_count) or 0
messages = await DBMessage.get_all_since(self.chat_id, prev_receipt_id, receipt_id)
# Remove reactions for outdated "read by" counts.
for message in messages:
reaction = await DBReceiptReaction.get_by_relation(message.mxid, self.mxid)
if reaction:
await self.main_intent.redact(self.mxid, reaction.mxid)
await reaction.delete()
reaction = await DBReceiptReaction.get_by_relation(event_id, self.mxid)
if reaction:
await self.main_intent.redact(self.mxid, reaction.mxid)
await reaction.delete()
# If there are as many receipts as there are chat participants, then everyone
# must have read the message, so send real read receipts from each puppet.
# TODO Not just -1 if there are multiple _OWN_ puppets...
is_fully_read = receipt_count >= len(self._last_participant_update) - 1
if is_fully_read:
if receipt_count == len(self._last_participant_update) - 1:
for mid in filter(lambda mid: not p.Puppet.is_mid_for_own_puppet(mid), self._last_participant_update):
intent = (await p.Puppet.get_by_mid(mid)).intent
await intent.send_receipt(self.mxid, event_id)
else:
# TODO messages list should exclude non-outgoing messages,
# but include them just to get rid of potential stale reactions
for message in (msg for msg in messages if msg.is_outgoing):
# TODO Translatable string for "Read by"
try:
reaction_mxid = await self.main_intent.react(self.mxid, message.mxid, f"(Read by {receipt_count})")
await DBReceiptReaction(reaction_mxid, self.mxid, message.mxid, receipt_count).insert()
except Exception as e:
self.log.warning(f"Failed to send read receipt reaction for message {message.mxid} in {self.chat_id}: {e}")
try:
await DBReceipt(mid=receipt_id, chat_id=self.chat_id, num_read=receipt_count).insert_or_update()
self.log.debug(f"Handled read receipt for message {receipt_id} read by {receipt_count}")
except Exception as e:
self.log.debug(f"Failed to handle read receipt for message {receipt_id} read by {receipt_count}: {e}")
# TODO Translatable string for "Read by"
reaction_mxid = await self.main_intent.react(self.mxid, event_id, f"(Read by {receipt_count})")
await DBReceiptReaction(reaction_mxid, self.mxid, event_id, receipt_count).insert()
async def _handle_remote_media(self, source: 'u.User', intent: IntentAPI,
media_url: str, media_id: Optional[str] = None,
@ -506,6 +423,10 @@ class Portal(DBPortal, BasePortal):
return MediaInfo(mxc, decryption_info, mime_type, file_name, len(data))
async def update_info(self, conv: ChatInfo, client: Optional[Client]) -> None:
if self.is_direct:
self.other_user = conv.participants[0].id
if self._main_intent is self.az.intent:
self._main_intent = (await p.Puppet.get_by_mid(self.other_user)).intent
for participant in conv.participants:
# REMINDER: multi-user chats include your own LINE user in the participant list
if participant.id != None:
@ -514,14 +435,17 @@ class Portal(DBPortal, BasePortal):
else:
self.log.warning(f"Could not find ID of LINE user {participant.name}")
puppet = await p.Puppet.get_by_profile(participant, client)
if self.needs_portal_meta:
changed = await self._update_name(f"{conv.name} (LINE)")
path_image = conv.icon if not self.is_direct else participant.avatar
changed = await self._update_icon(path_image, client) or changed
else:
changed = await self._update_name(None)
changed = await self._update_icon(None, client) or changed
# TODO Consider setting no room name for non-group chats.
# But then the LINE bot itself may appear in the title...
changed = await self._update_name(f"{conv.name} (LINE)")
if client:
if not self.is_direct:
changed = await self._update_icon(conv.icon, client) or changed
elif puppet and puppet.avatar_mxc != self.icon_mxc:
changed = True
self.icon_mxc = puppet.avatar_mxc
if self.mxid:
await self.main_intent.set_room_avatar(self.mxid, self.icon_mxc)
if changed:
await self.update_bridge_info()
await self.update()
@ -529,7 +453,7 @@ class Portal(DBPortal, BasePortal):
# when their user actually joined or sent a message.
#await self._update_participants(conv.participants)
async def _update_name(self, name: Optional[str]) -> bool:
async def _update_name(self, name: str) -> bool:
if self.name != name:
self.name = name
if self.mxid:
@ -537,10 +461,10 @@ class Portal(DBPortal, BasePortal):
return True
return False
async def _update_icon(self, icon: Optional[PathImage], client: Optional[Client]) -> bool:
async def _update_icon(self, icon: Optional[PathImage], client: Client) -> bool:
if icon:
if icon.url and not icon.path:
self.log.warn(f"Using URL as path for room icon of {self.name or self.chat_id}")
self.log.warn(f"Using URL as path for room icon of {self.name}")
icon_path = icon_url = icon.url
else:
icon_path = icon.path
@ -549,12 +473,9 @@ class Portal(DBPortal, BasePortal):
icon_path = icon_url = None
if icon_path != self.icon_path:
self.log.info(f"Updating room icon of {self.name or self.chat_id}")
self.log.info(f"Updating room icon of {self.name}")
self.icon_path = icon_path
if icon_url:
if not client:
self.log.error(f"Cannot update room icon: no connection to LINE")
return
resp = await client.read_image(icon.url)
self.icon_mxc = await self.main_intent.upload_media(resp.data, mime_type=resp.mime)
else:
@ -566,7 +487,7 @@ class Portal(DBPortal, BasePortal):
self.log.exception(f"Failed to set room icon: {e}")
return True
else:
self.log.debug(f"No need to update room icon of {self.name or self.chat_id}, new icon has same path as old one")
self.log.debug(f"No need to update room icon of {self.name}, new icon has same path as old one")
return False
async def _update_participants(self, participants: List[Participant]) -> None:
@ -599,7 +520,7 @@ class Portal(DBPortal, BasePortal):
print(current_members)
# Puppets who shouldn't be here should leave
# Kick puppets who shouldn't be here
for user_id in await self.main_intent.get_room_members(self.mxid):
if user_id == self.az.bot_mxid:
if forbid_own_puppets and not self.needs_bridgebot:
@ -607,67 +528,41 @@ class Portal(DBPortal, BasePortal):
continue
mid = p.Puppet.get_id_from_mxid(user_id)
is_own_puppet = p.Puppet.is_mid_for_own_puppet(mid)
if mid and mid not in current_members and not is_own_puppet \
or forbid_own_puppets and is_own_puppet:
if mid and mid not in current_members:
print(mid)
puppet = await p.Puppet.get_by_mxid(user_id)
await puppet.intent.leave_room(self.mxid)
await self.main_intent.kick_user(self.mxid, user_id,
reason="User had left this chat")
elif forbid_own_puppets and p.Puppet.is_mid_for_own_puppet(mid):
await self.main_intent.kick_user(self.mxid, user_id,
reason="Kicking own puppet")
async def backfill(self, source: 'u.User', info: ChatInfo) -> None:
async def backfill(self, source: 'u.User') -> None:
try:
with self.backfill_lock:
await self._backfill(source, info)
await self._backfill(source)
except Exception:
self.log.exception("Failed to backfill portal")
async def _backfill(self, source: 'u.User', info: ChatInfo) -> None:
async def _backfill(self, source: 'u.User') -> None:
self.log.debug("Backfilling history through %s", source.mxid)
events = await source.client.get_messages(self.chat_id)
max_mid = await DBMessage.get_max_mid(self.mxid) or 0
messages = [msg for msg in events.messages
messages = [msg for msg in await source.client.get_messages(self.chat_id)
if msg.id > max_mid]
if not messages:
self.log.debug("Didn't get any messages from server")
else:
self.log.debug("Got %d messages from server", len(messages))
async with NotificationDisabler(self.mxid, source):
for evt in messages:
await self.handle_remote_message(source, evt, handle_receipt=self.is_direct)
self.log.info("Backfilled %d messages through %s", len(messages), source.mxid)
await self._cleanup_noid_msgs()
self.log.debug("Didn't get any entries from server")
return
# Need to update participants even for DMs, to kick own puppet if needed
await self._update_participants(info.participants)
if not self.is_direct:
# Update participants before sending any receipts
# TODO Joins and leaves are (usually) shown after all, so track them properly.
# In the meantime, just check the participants list after backfilling.
self.log.debug("Got %d messages from server", len(messages))
async with NotificationDisabler(self.mxid, source):
for evt in messages:
if evt.is_outgoing and evt.receipt_count:
await self.handle_remote_message(source, evt, handle_receipt=False)
max_mid_per_num_read = await DBReceipt.get_max_mid_per_num_read(self.chat_id)
receipts = [rct for rct in events.receipts
if rct.id > max_mid_per_num_read.get(rct.count, 0)]
if not receipts:
self.log.debug("Didn't get any receipts from server")
else:
self.log.debug("Got %d receipts from server", len(receipts))
for rct in receipts:
await self.handle_remote_receipt(rct)
self.log.info("Backfilled %d receipts through %s", len(receipts), source.mxid)
await self.handle_remote_message(source, evt)
self.log.info("Backfilled %d messages through %s", len(messages), source.mxid)
@property
def bridge_info_state_key(self) -> str:
return f"net.miscworks.line://line/{self.chat_id}"
return f"net.maunium.line://line/{self.chat_id}"
@property
def bridge_info(self) -> Dict[str, Any]:
@ -713,21 +608,22 @@ class Portal(DBPortal, BasePortal):
return await self._create_matrix_room(source, info)
async def _update_matrix_room(self, source: 'u.User', info: ChatInfo) -> None:
await self.update_info(info, source.client)
await self.main_intent.invite_user(self.mxid, source.mxid, check_cache=True)
puppet = await p.Puppet.get_by_custom_mxid(source.mxid)
if puppet and puppet.intent:
await puppet.intent.ensure_joined(self.mxid)
await self.backfill(source, info)
await self.update_info(info, source.client)
await self.backfill(source)
await self._update_participants(info.participants)
async def _create_matrix_room(self, source: 'u.User', info: ChatInfo) -> Optional[RoomID]:
if self.mxid:
await self._update_matrix_room(source, info)
return self.mxid
await self.update_info(info, source.client)
self.log.debug("Creating Matrix room")
name: Optional[str] = None
initial_state = [{
"type": str(StateBridge),
"state_key": self.bridge_info_state_key,
@ -748,13 +644,15 @@ class Portal(DBPortal, BasePortal):
})
if self.is_direct:
invites.append(self.az.bot_mxid)
# NOTE Set the room title even for direct chats, because
# the LINE bot itself may appear in the title otherwise.
#if self.encrypted or not self.is_direct:
name = self.name
if self.config["appservice.community_id"]:
initial_state.append({
"type": "m.room.related_groups",
"content": {"groups": [self.config["appservice.community_id"]]},
})
initial_state.append({
"type": str(EventType.ROOM_POWER_LEVELS),
"content": {
@ -769,8 +667,6 @@ class Portal(DBPortal, BasePortal):
}
}
})
await self.update_info(info, source.client)
if self.icon_mxc:
initial_state.append({
"type": str(EventType.ROOM_AVATAR),
@ -782,7 +678,7 @@ class Portal(DBPortal, BasePortal):
# We lock backfill lock here so any messages that come between the room being created
# and the initial backfill finishing wouldn't be bridged before the backfill messages.
with self.backfill_lock:
self.mxid = await self.main_intent.create_room(name=self.name, is_direct=self.is_direct,
self.mxid = await self.main_intent.create_room(name=name, is_direct=self.is_direct,
initial_state=initial_state,
invitees=invites)
if not self.mxid:
@ -798,7 +694,11 @@ class Portal(DBPortal, BasePortal):
await self.update()
self.log.debug(f"Matrix room created: {self.mxid}")
self.by_mxid[self.mxid] = self
await self.backfill(source, info)
await self.backfill(source)
if not self.is_direct:
# TODO Joins and leaves are (usually) shown after all, so track them properly.
# In the meantime, just check the participants list after backfilling.
await self._update_participants(info.participants)
return self.mxid
@ -806,13 +706,15 @@ class Portal(DBPortal, BasePortal):
self.by_chat_id[self.chat_id] = self
if self.mxid:
self.by_mxid[self.mxid] = self
if self.is_direct:
self.other_user = self.chat_id
if self.other_user:
self._main_intent = (await p.Puppet.get_by_mid(self.other_user)).intent
else:
self._main_intent = self.az.intent
async def delete(self) -> None:
if self.mxid:
# TODO Handle this with db foreign keys instead
await DBMessage.delete_all(self.mxid)
self.by_chat_id.pop(self.chat_id, None)
self.by_mxid.pop(self.mxid, None)
await super().delete()

View File

@ -1,5 +1,5 @@
# matrix-puppeteer-line - A very hacky Matrix-LINE bridge based on running LINE's Chrome extension in Puppeteer
# Copyright (C) 2020-2022 Tulir Asokan, Andrew Ferrazzutti
# Copyright (C) 2020-2021 Tulir Asokan, Andrew Ferrazzutti
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
@ -13,7 +13,7 @@
#
# 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, Dict, List, TYPE_CHECKING, cast
from typing import Optional, Dict, TYPE_CHECKING, cast
from mautrix.bridge import BasePuppet
from mautrix.types import UserID, ContentURI
@ -196,16 +196,8 @@ class Puppet(DBPuppet, BasePuppet):
def is_mid_for_own_puppet(cls, mid) -> bool:
return mid and mid.startswith("_OWN_")
@property
def is_own_puppet(self) -> bool:
return self.mid.startswith("_OWN_")
@classmethod
async def get_by_custom_mxid(cls, mxid: UserID) -> Optional['u.User']:
if mxid == cls.config["bridge.user"]:
return await cls.bridge.get_user(mxid)
return None
@classmethod
async def get_all(cls) -> List['Puppet']:
return [p for p in await super().get_all() if not p.is_own_puppet]

View File

@ -1,5 +1,5 @@
# matrix-puppeteer-line - A very hacky Matrix-LINE bridge based on running LINE's Chrome extension in Puppeteer
# Copyright (C) 2020-2022 Tulir Asokan, Andrew Ferrazzutti
# Copyright (C) 2020-2021 Tulir Asokan, Andrew Ferrazzutti
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
@ -13,13 +13,17 @@
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from typing import AsyncGenerator, List, Tuple, Dict, Callable, Awaitable, Any
from typing import AsyncGenerator, TypedDict, List, Tuple, Dict, Callable, Awaitable, Any
from collections import deque
from base64 import b64decode
import asyncio
from .rpc import RPCClient
from .types import ChatEvents, ChatListInfo, ChatInfo, ImageData, Message, Participant, Receipt, StartStatus
from .types import ChatListInfo, ChatInfo, Message, Receipt, ImageData, StartStatus
class LoginCommand(TypedDict):
content: str
class Client(RPCClient):
@ -37,22 +41,16 @@ class Client(RPCClient):
async def resume(self) -> None:
await self.request("resume")
async def get_own_profile(self) -> Participant:
return Participant.deserialize(await self.request("get_own_profile"))
async def get_contacts(self) -> List[Participant]:
resp = await self.request("get_contacts")
return [Participant.deserialize(data) for data in resp]
async def get_chats(self) -> List[ChatListInfo]:
resp = await self.request("get_chats")
return [ChatListInfo.deserialize(data) for data in resp]
async def get_chat(self, chat_id: str, force_view: bool = False) -> ChatInfo:
return ChatInfo.deserialize(await self.request("get_chat", chat_id=chat_id, force_view=force_view))
async def get_chat(self, chat_id: str) -> ChatInfo:
return ChatInfo.deserialize(await self.request("get_chat", chat_id=chat_id))
async def get_messages(self, chat_id: str) -> ChatEvents:
return ChatEvents.deserialize(await self.request("get_messages", chat_id=chat_id))
async def get_messages(self, chat_id: str) -> List[Message]:
resp = await self.request("get_messages", chat_id=chat_id)
return [Message.deserialize(data) for data in resp]
async def read_image(self, image_url: str) -> ImageData:
resp = await self.request("read_image", image_url=image_url)
@ -85,11 +83,8 @@ class Client(RPCClient):
resp = await self.request("send_file", chat_id=chat_id, file_path=file_path)
return resp["id"]
async def set_last_message_ids(self, msg_ids: Dict[str, int], own_msg_ids: Dict[str, int], rct_ids: Dict[str, Dict[int, int]]) -> None:
await self.request("set_last_message_ids", msg_ids=msg_ids, own_msg_ids=own_msg_ids, rct_ids=rct_ids)
async def forget_chat(self, chat_id: str) -> None:
await self.request("forget_chat", chat_id=chat_id)
async def set_last_message_ids(self, msg_ids: Dict[str, int]) -> None:
await self.request("set_last_message_ids", msg_ids=msg_ids)
async def on_message(self, func: Callable[[Message], Awaitable[None]]) -> None:
async def wrapper(data: Dict[str, Any]) -> None:
@ -103,9 +98,9 @@ class Client(RPCClient):
self.add_event_handler("receipt", wrapper)
async def on_logged_out(self, func: Callable[[str], Awaitable[None]]) -> None:
async def wrapper(data: Dict[str, str]) -> None:
await func(data.get("message"))
async def on_logged_out(self, func: Callable[[], Awaitable[None]]) -> None:
async def wrapper(data: Dict[str, Any]) -> None:
await func()
self.add_event_handler("logged_out", wrapper)
@ -116,19 +111,19 @@ class Client(RPCClient):
data = deque()
event = asyncio.Event()
async def qr_handler(req: Dict[str, str]) -> None:
async def qr_handler(req: LoginCommand) -> None:
data.append(("qr", req["url"]))
event.set()
async def pin_handler(req: Dict[str, str]) -> None:
async def pin_handler(req: LoginCommand) -> None:
data.append(("pin", req["pin"]))
event.set()
async def success_handler(req: Dict[str, str]) -> None:
async def success_handler(req: LoginCommand) -> None:
data.append(("login_success", None))
event.set()
async def failure_handler(req: Dict[str, str]) -> None:
async def failure_handler(req: LoginCommand) -> None:
data.append(("login_failure", req.get("reason")))
event.set()

View File

@ -1,5 +1,5 @@
# matrix-puppeteer-line - A very hacky Matrix-LINE bridge based on running LINE's Chrome extension in Puppeteer
# Copyright (C) 2020-2022 Tulir Asokan, Andrew Ferrazzutti
# Copyright (C) 2020-2021 Tulir Asokan, Andrew Ferrazzutti
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
@ -13,7 +13,7 @@
#
# 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 Dict, Any, Callable, Awaitable, List, Optional
from typing import Dict, Any, Callable, Awaitable, List, Optional, Tuple
import logging
import asyncio
import json
@ -33,7 +33,6 @@ class RPCClient:
log: TraceLogger = logging.getLogger("mau.rpc")
user_id: UserID
ephemeral_events: bool
_reader: Optional[asyncio.StreamReader]
_writer: Optional[asyncio.StreamWriter]
_req_id: int
@ -41,12 +40,10 @@ class RPCClient:
_response_waiters: Dict[int, asyncio.Future]
_event_handlers: Dict[str, List[EventHandler]]
def __init__(self, user_id: UserID, own_id: str, ephemeral_events: bool) -> None:
def __init__(self, user_id: UserID) -> None:
self.log = self.log.getChild(user_id)
self.loop = asyncio.get_running_loop()
self.user_id = user_id
self.own_id = own_id
self.ephemeral_events = ephemeral_events
self._req_id = 0
self._min_broadcast_id = 0
self._event_handlers = {}
@ -60,32 +57,17 @@ class RPCClient:
return
if self.config["puppeteer.connection.type"] == "unix":
while True:
try:
r, w = await asyncio.open_unix_connection(self.config["puppeteer.connection.path"])
break
except:
self.log.warn(f'No unix socket available at {self.config["puppeteer.connection.path"]}, wait for it to exist...')
await asyncio.sleep(10)
r, w = await asyncio.open_unix_connection(self.config["puppeteer.connection.path"])
elif self.config["puppeteer.connection.type"] == "tcp":
while True:
try:
r, w = await asyncio.open_connection(self.config["puppeteer.connection.host"],
self.config["puppeteer.connection.port"])
break
except:
self.log.warn(f'No TCP connection open at {self.config["puppeteer.connection.host"]}:{self.config["puppeteer.connection.path"]}, wait for it to become available...')
await asyncio.sleep(10)
r, w = await asyncio.open_connection(self.config["puppeteer.connection.host"],
self.config["puppeteer.connection.port"])
else:
raise RuntimeError("invalid puppeteer connection type")
self._reader = r
self._writer = w
self.loop.create_task(self._try_read_loop())
self.loop.create_task(self._command_loop())
await self.request("register",
user_id=self.user_id,
own_id = self.own_id,
ephemeral_events=self.ephemeral_events)
await self.request("register", user_id=self.user_id)
async def disconnect(self) -> None:
self._writer.write_eof()
@ -170,10 +152,10 @@ class RPCClient:
try:
line += await self._reader.readuntil()
break
except asyncio.IncompleteReadError as e:
except asyncio.exceptions.IncompleteReadError as e:
line += e.partial
break
except asyncio.LimitOverrunError as e:
except asyncio.exceptions.LimitOverrunError as e:
self.log.warning(f"Buffer overrun: {e}")
line += await self._reader.read(self._reader._limit)
if not line:

View File

@ -1,5 +1,5 @@
# matrix-puppeteer-line - A very hacky Matrix-LINE bridge based on running LINE's Chrome extension in Puppeteer
# Copyright (C) 2020-2022 Tulir Asokan, Andrew Ferrazzutti
# Copyright (C) 2020-2021 Tulir Asokan, Andrew Ferrazzutti
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
@ -35,8 +35,8 @@ class ChatListInfo(SerializableAttrs['ChatListInfo']):
id: int
name: str
icon: Optional[PathImage]
lastMsg: Optional[str]
lastMsgDate: Optional[str]
lastMsg: str
lastMsgDate: str
@dataclass
@ -58,23 +58,15 @@ class MessageImage(SerializableAttrs['MessageImage']):
is_animated: bool
@dataclass
class MemberInfo(SerializableAttrs['MemberInfo']):
invited: bool
joined: bool
left: bool
@dataclass
class Message(SerializableAttrs['Message']):
id: Optional[int]
id: int
chat_id: int
is_outgoing: bool
sender: Optional[Participant]
timestamp: Optional[int] = None
timestamp: int = None
html: Optional[str] = None
image: Optional[MessageImage] = None
member_info: Optional[MemberInfo] = None
receipt_count: Optional[int] = None
@ -85,12 +77,6 @@ class Receipt(SerializableAttrs['Receipt']):
count: int = 1
@dataclass
class ChatEvents(SerializableAttrs['ChatEvents']):
messages: List[Message]
receipts: List[Receipt]
@dataclass
class ImageData:
mime: str

View File

@ -1,5 +1,5 @@
# matrix-puppeteer-line - A very hacky Matrix-LINE bridge based on running LINE's Chrome extension in Puppeteer
# Copyright (C) 2020-2022 Tulir Asokan, Andrew Ferrazzutti
# Copyright (C) 2020-2021 Tulir Asokan, Andrew Ferrazzutti
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
@ -22,9 +22,7 @@ from mautrix.types import UserID, RoomID
from mautrix.appservice import AppService, IntentAPI
from mautrix.util.opt_prometheus import Gauge
from .commands.auth import auto_login
from .db import User as DBUser, Portal as DBPortal, Message as DBMessage, Receipt as DBReceipt
from .db import User as DBUser, Portal as DBPortal, Message as DBMessage
from .config import Config
from .rpc import Client, Message, Receipt
from . import puppet as pu, portal as po
@ -44,7 +42,6 @@ class User(DBUser, BaseUser):
client: Optional[Client]
intent: Optional[IntentAPI]
is_real_user = True
is_syncing: bool
_notice_room_lock: asyncio.Lock
_connection_check_task: Optional[asyncio.Task]
@ -59,7 +56,6 @@ class User(DBUser, BaseUser):
self._connection_check_task = None
self.client = None
self.intent = None
self.is_syncing = False
@classmethod
def init_cls(cls, bridge: 'MessagesBridge') -> None:
@ -73,14 +69,6 @@ class User(DBUser, BaseUser):
self.log.debug(f"Sending bridge notice: {text}")
await self.az.intent.send_notice(self.notice_room, text)
@property
def own_id(self) -> str:
# Remove characters that will conflict with mxid grammar
return f"_OWN_{self.mxid[1:].replace(':', '_ON_')}"
async def get_own_puppet(self) -> 'pu.Puppet':
return await pu.Puppet.get_by_mid(self.own_id)
async def is_logged_in(self) -> bool:
try:
return self.client and (await self.client.start()).is_logged_in
@ -107,7 +95,7 @@ class User(DBUser, BaseUser):
async def connect(self) -> None:
self.loop.create_task(self.connect_double_puppet())
self.client = Client(self.mxid, self.own_id, self.config["appservice.ephemeral_events"])
self.client = Client(self.mxid)
self.log.debug("Starting client")
await self.send_bridge_notice("Starting up...")
state = await self.client.start()
@ -119,8 +107,6 @@ class User(DBUser, BaseUser):
if state.is_logged_in:
await self.send_bridge_notice("Already logged in to LINE")
self.loop.create_task(self._try_sync())
elif await auto_login(self):
await self.send_bridge_notice("Auto-logged in to LINE")
else:
await self.send_bridge_notice("Ready to log in to LINE")
@ -135,64 +121,28 @@ class User(DBUser, BaseUser):
self._track_metric(METRIC_CONNECTED, await self.client.is_connected())
await asyncio.sleep(5)
async def sync(self, limit: int = 0) -> None:
await self.sync_contacts()
# TODO Use some kind of async lock / event to queue syncing actions
self.is_syncing = True
async def sync(self) -> None:
if self._connection_check_task:
self._connection_check_task.cancel()
self._connection_check_task = self.loop.create_task(self._check_connection_loop())
await self.client.pause()
try:
await self.sync_own_profile()
await self.client.set_last_message_ids(
await DBMessage.get_max_mids(),
await DBMessage.get_max_outgoing_mids(),
await DBReceipt.get_max_mids_per_num_read())
if limit <= 0:
limit = self.config["bridge.initial_conversation_sync"]
if limit == 0:
self.log.info("Skipping chat sync")
return
self.log.info("Syncing chats")
await self.send_bridge_notice("Synchronizing chats...")
# TODO Since only chat ID is used, retrieve only that
chat_infos = await self.client.get_chats()
for chat_info in chat_infos[:limit]:
portal = await po.Portal.get_by_chat_id(chat_info.id, create=True)
chat_info_full = await self.client.get_chat(chat_info.id)
await portal.create_matrix_room(self, chat_info_full)
await self.send_bridge_notice("Chat synchronization complete")
finally:
await self.client.resume()
self.is_syncing = False
async def sync_contacts(self) -> None:
await self.send_bridge_notice("Synchronizing contacts...")
contacts = await self.client.get_contacts()
for contact in contacts:
puppet = await pu.Puppet.get_by_mid(contact.id)
await puppet.update_info(contact, self.client)
await self.send_bridge_notice("Contact synchronization complete")
async def sync_portal(self, portal: 'po.Portal') -> None:
chat_id = portal.chat_id
self.log.info(f"Viewing (and syncing) chat {chat_id}")
await self.client.pause()
try:
chat = await self.client.get_chat(chat_id, True)
await portal.update_matrix_room(self, chat)
finally:
await self.client.resume()
async def sync_own_profile(self) -> None:
self.log.info("Syncing own LINE profile info")
own_profile = await self.client.get_own_profile()
puppet = await self.get_own_puppet()
await puppet.update_info(own_profile, self.client)
await self.client.set_last_message_ids(await DBMessage.get_max_mids())
limit = self.config["bridge.initial_conversation_sync"]
self.log.info("Syncing chats")
await self.send_bridge_notice("Synchronizing chats...")
chats = await self.client.get_chats()
num_created = 0
for index, chat in enumerate(chats):
portal = await po.Portal.get_by_chat_id(chat.id, create=True)
if portal.mxid or num_created < limit:
chat = await self.client.get_chat(chat.id)
if portal.mxid:
await portal.update_matrix_room(self, chat)
else:
await portal.create_matrix_room(self, chat)
num_created += 1
await self.send_bridge_notice("Synchronization complete")
await self.client.resume()
async def stop(self) -> None:
# TODO Notices for shutdown messages
@ -213,10 +163,7 @@ class User(DBUser, BaseUser):
self.log.trace("Received message %s", evt)
portal = await po.Portal.get_by_chat_id(evt.chat_id, create=True)
if not portal.mxid:
await self.client.set_last_message_ids(
await DBMessage.get_max_mids(),
await DBMessage.get_max_outgoing_mids(),
await DBReceipt.get_max_mids_per_num_read())
await self.client.set_last_message_ids(await DBMessage.get_max_mids())
chat_info = await self.client.get_chat(evt.chat_id)
await portal.create_matrix_room(self, chat_info)
await portal.handle_remote_message(self, evt)
@ -229,22 +176,11 @@ class User(DBUser, BaseUser):
await portal.create_matrix_room(self, chat_info)
await portal.handle_remote_receipt(receipt)
async def handle_logged_out(self, message: str) -> None:
async def handle_logged_out(self) -> None:
await self.send_bridge_notice("Logged out of LINE. Please run either \"login-qr\" or \"login-email\" to log back in.")
if self._connection_check_task:
self._connection_check_task.cancel()
self._connection_check_task = None
newline = "\n"
# TODO Use an asyncio.Task for auto-login
# TODO Don't auto-login if logout was intentional!
if await auto_login(self):
await self.send_bridge_notice(
f"Auto-logged back in to LINE{'' if not message else ' (logout reason: ' + message.replace(newline, ' ') + ')'}"
)
else:
await self.send_bridge_notice(
f"Logged out of LINE{'.' if not message else ' with message: ' + message.replace(newline, ' ') + newline} "
"Please run either \"login-qr\" or \"login-email\" to log back in."
)
def _add_to_cache(self) -> None:
self.by_mxid[self.mxid] = self

View File

@ -1,5 +1,5 @@
# matrix-puppeteer-line - A very hacky Matrix-LINE bridge based on running LINE's Chrome extension in Puppeteer
# Copyright (C) 2020-2022 Tulir Asokan, Andrew Ferrazzutti
# Copyright (C) 2020-2021 Tulir Asokan, Andrew Ferrazzutti
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by

View File

@ -1,5 +1,5 @@
# matrix-puppeteer-line - A very hacky Matrix-LINE bridge based on running LINE's Chrome extension in Puppeteer
# Copyright (C) 2020-2022 Tulir Asokan, Andrew Ferrazzutti
# Copyright (C) 2020-2021 Tulir Asokan, Andrew Ferrazzutti
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
@ -64,8 +64,8 @@ class ProvisioningAPI:
return None
for part in auth_parts:
part = part.strip()
if part.startswith("net.miscworks.line.auth-"):
return part[len("net.miscworks.line.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']:
@ -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.miscworks.line.login"])
ws = web.WebSocketResponse(protocols=["net.maunium.line.login"])
await ws.prepare(request)
try:
async for url in user.client.login():

3
puppet/.gitignore vendored
View File

@ -1,3 +1,2 @@
/node_modules
/config*.json
/*.sock
/config.json

View File

@ -1,22 +1,17 @@
FROM node:16-alpine3.14
FROM node:14-alpine3.12
ARG TARGETARCH=amd64
RUN echo $'\
@edge http://dl-cdn.alpinelinux.org/alpine/edge/main\n\
@edge http://dl-cdn.alpinelinux.org/alpine/edge/testing\n\
@edge http://dl-cdn.alpinelinux.org/alpine/edge/community' >> /etc/apk/repositories
RUN apk add --no-cache chromium xvfb-run xdotool
RUN apk add --no-cache chromium@edge
WORKDIR /opt/matrix-puppeteer-line/puppet
# Want to use same UID as Python process so the Unix socket can be shared.
# But yarn hits snags if there is no user for the UID it's run under.
RUN adduser -Du 1337 --gecos "" line
VOLUME /data
RUN chown 1337:1337 .
USER 1337
RUN chown node:node /opt/matrix-puppeteer-line/puppet
USER node
COPY package.json yarn.lock ./
RUN yarn --production && rm -rf node_modules/puppeteer/.local-chromium
COPY src src
COPY docker-run.sh example-config-docker.json ./
CMD ["./docker-run.sh"]
COPY . /opt/matrix-puppeteer-line/puppet
CMD ["yarn", "start", "--config", "/data/config.json", "--browser", "/usr/lib/chromium/chrome", "--no-sandbox"]

View File

@ -3,22 +3,14 @@ If `type` is `unix`, `path` is the path where to create the socket.
If `type` is `tcp`, `port` and `host` are the host/port where to listen.
### Executable path
The `executable_path` specifies the path to the Chromium binary for Puppeteer to use. Leaving this setting blank will use the x86_64 Chromium installation bundled with Puppeteer. For other architectures, it is necessary to install a compatible version of Chromium (ideally via your distribution's package manager), and to set `executable_path` to the path of its binary (typically `/usr/bin/chromium`).
### Profile directory
The `profile_dir` specifies which directory to put Chromium user data directories.
### URL
`url` specifies the URL of the index page of the LINE extension for your Chromium installation.
### Extension directory
The `extension_dir` specifies which directory contains the files for the LINE extension, which you must download yourself.
### Cycle delay
`cycle_delay` specifies the period (in milliseconds) at which Puppeteer should view chats to check on their read receipts. Only chats with messages that haven't been fully read need to be checked. Set to a negative value to disable this checking.
### `xdotool`
Set `use_xdotool` to `true` to allow the Node process to manipulate the mouse cursor of the X server it runs in. Requires the `xdotool` utility to be installed. Highly recommended, especially when running in a background X server. Its default value is `false` so that running in a non-background X server won't interfere with a real mouse cursor.
`jiggle_delay` specifies the period (in milliseconds) for "jiggling" the mouse cursor (necessary to keep the LINE extension active). Only relevant when `use_xdotool` is `true`.
### DevTools
Set `devtools` to `true` to launch Chromium with DevTools enabled by default.

View File

@ -1,18 +0,0 @@
#!/bin/sh
if [ ! -w . ]; then
echo "Please ensure the /data volume of this container is writable for user:group $UID:$GID." >&2
exit
fi
if [ ! -f /data/config.json ]; then
cp example-config-docker.json /data/config.json
echo "Didn't find a config file."
echo "Copied default config file to /data/config.json"
echo "Modify that config file to your liking, then restart the container."
exit
fi
# Allow setting custom browser path via "executable_path" config setting
# TODO Decide if --no-sandbox is needed
xvfb-run yarn start --config /data/config.json

View File

@ -1,13 +0,0 @@
{
"listen": {
"type": "unix",
"path": "/data/puppet.sock"
},
"executable_path": "/usr/lib/chromium/chrome",
"profile_dir": "./profiles",
"extension_dir": "/data/extension_files",
"cycle_delay": 5000,
"use_xdotool": true,
"jiggle_delay": 20000,
"devtools": false
}

View File

@ -3,11 +3,8 @@
"type": "unix",
"path": "/var/run/matrix-puppeteer-line/puppet.sock"
},
"executable_path": "",
"profile_dir": "./profiles",
"url": "chrome-extension://<extension-uuid>/index.html",
"extension_dir": "./extension_files",
"cycle_delay": 5000,
"use_xdotool": false,
"jiggle_delay": 20000,
"devtools": false
}

View File

@ -1,7 +1,7 @@
{
"name": "matrix-puppeteer-line-chrome",
"name": "matrix-puppeteer-line-puppeteer",
"version": "0.1.0",
"description": "Chrome/Puppeteer backend for matrix-puppeteer-line",
"description": "Puppeteer module for matrix-puppeteer-line",
"repository": {
"type": "git",
"url": "git+https://src.miscworks.net/fair/matrix-puppeteer-line.git"
@ -20,8 +20,7 @@
"dependencies": {
"arg": "^4.1.3",
"chrono-node": "^2.1.7",
"systemd-daemon": "^1.1.2",
"puppeteer": "9.1.1"
"puppeteer": "5.5.0"
},
"devDependencies": {
"babel-eslint": "^10.1.0",

14
puppet/prep_helper.js Normal file
View File

@ -0,0 +1,14 @@
import puppeteer from "puppeteer"
(async () =>
{
const pathToExtension = "extension_files"
const browser = await puppeteer.launch({
headless: false,
args: [
`--disable-extensions-except=${pathToExtension}`,
`--load-extension=${pathToExtension}`
],
timeout: 0,
})
})()

View File

@ -1,5 +1,5 @@
// matrix-puppeteer-line - A very hacky Matrix-LINE bridge based on running LINE's Chrome extension in Puppeteer
// Copyright (C) 2020-2022 Tulir Asokan, Andrew Ferrazzutti
// Copyright (C) 2020-2021 Tulir Asokan, Andrew Ferrazzutti
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by

View File

@ -1,5 +1,5 @@
// matrix-puppeteer-line - A very hacky Matrix-LINE bridge based on running LINE's Chrome extension in Puppeteer
// Copyright (C) 2020-2022 Tulir Asokan, Andrew Ferrazzutti
// Copyright (C) 2020-2021 Tulir Asokan, Andrew Ferrazzutti
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
@ -99,7 +99,7 @@ export default class Client {
}
sendMessage(message) {
this.log(`Sending message ${message.id || "with no ID"} to client`)
this.log(`Sending message ${message.id} to client`)
return this._write({
id: --this.notificationID,
command: "message",
@ -152,12 +152,11 @@ export default class Client {
})
}
sendLoggedOut(message) {
this.log(`Sending logout notice to client${!message ? "" : " with message: " + message}`)
sendLoggedOut() {
this.log("Sending logout notice to client")
return this._write({
id: --this.notificationID,
command: "logged_out",
message,
})
}
@ -165,7 +164,7 @@ export default class Client {
let started = false
if (this.puppet === null) {
this.log("Opening new puppeteer for", this.userID)
this.puppet = new MessagesPuppeteer(this.userID, this.ownID, this.sendPlaceholders, this)
this.puppet = new MessagesPuppeteer(this.userID, this)
this.manager.puppets.set(this.userID, this.puppet)
await this.puppet.start(!!req.debug)
started = true
@ -195,13 +194,11 @@ export default class Client {
handleRegister = async (req) => {
this.userID = req.user_id
this.ownID = req.own_id
this.sendPlaceholders = req.ephemeral_events
this.log(`Registered socket ${this.connID} -> ${this.userID}${!this.sendPlaceholders ? "" : " (with placeholder message support)"}`)
this.log("Registered socket", this.connID, "->", this.userID)
if (this.manager.clients.has(this.userID)) {
const oldClient = this.manager.clients.get(this.userID)
this.manager.clients.set(this.userID, this)
this.log(`Terminating previous socket ${oldClient.connID} for ${this.userID}`)
this.log("Terminating previous socket", oldClient.connID, "for", this.userID)
await oldClient.stop("Socket replaced by new connection")
} else {
this.manager.clients.set(this.userID, this)
@ -258,14 +255,11 @@ export default class Client {
cancel_login: () => this.puppet.cancelLogin(),
send: req => this.puppet.sendMessage(req.chat_id, req.text),
send_file: req => this.puppet.sendFile(req.chat_id, req.file_path),
set_last_message_ids: req => this.puppet.setLastMessageIDs(req.msg_ids, req.own_msg_ids, req.rct_ids),
forget_chat: req => this.puppet.forgetChat(req.chat_id),
set_last_message_ids: req => this.puppet.setLastMessageIDs(req.msg_ids),
pause: () => this.puppet.stopObserving(),
resume: () => this.puppet.startObserving(),
get_own_profile: () => this.puppet.getOwnProfile(),
get_contacts: () => this.puppet.getContacts(),
get_chats: () => this.puppet.getRecentChats(),
get_chat: req => this.puppet.getChatInfo(req.chat_id, req.force_view),
get_chat: req => this.puppet.getChatInfo(req.chat_id),
get_messages: req => this.puppet.getMessages(req.chat_id),
read_image: req => this.puppet.readImage(req.image_url),
is_connected: async () => ({ is_connected: !await this.puppet.isDisconnected() }),

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,5 @@
// matrix-puppeteer-line - A very hacky Matrix-LINE bridge based on running LINE's Chrome extension in Puppeteer
// Copyright (C) 2020-2022 Tulir Asokan, Andrew Ferrazzutti
// Copyright (C) 2020-2021 Tulir Asokan, Andrew Ferrazzutti
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
@ -15,7 +15,6 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
import process from "process"
import fs from "fs"
import sd from "systemd-daemon"
import arg from "arg"
@ -31,17 +30,15 @@ const args = arg({
})
const configPath = args["--config"] || "config.json"
MessagesPuppeteer.executablePath = args["--browser"] || MessagesPuppeteer.executablePath
MessagesPuppeteer.noSandbox = args["--no-sandbox"]
console.log("[Main] Reading config from", configPath)
const config = JSON.parse(fs.readFileSync(configPath).toString())
MessagesPuppeteer.executablePath = args["--browser"] || config.executable_path || MessagesPuppeteer.executablePath
MessagesPuppeteer.noSandbox = args["--no-sandbox"] || MessagesPuppeteer.noSandbox
MessagesPuppeteer.profileDir = config.profile_dir || MessagesPuppeteer.profileDir
MessagesPuppeteer.devtools = config.devtools || false
MessagesPuppeteer.url = config.url
MessagesPuppeteer.extensionDir = config.extension_dir || MessagesPuppeteer.extensionDir
MessagesPuppeteer.cycleDelay = config.cycle_delay || MessagesPuppeteer.cycleDelay
MessagesPuppeteer.useXdotool = config.use_xdotool || MessagesPuppeteer.useXdotool
MessagesPuppeteer.jiggleDelay = config.jiggle_delay || MessagesPuppeteer.jiggleDelay
const api = new PuppetAPI(config.listen)
@ -58,7 +55,6 @@ function stop() {
api.start().then(() => {
process.once("SIGINT", stop)
process.once("SIGTERM", stop)
sd.notify("READY=1")
}, err => {
console.error("[Main] Error starting:", err)
process.exit(2)

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,5 @@
// matrix-puppeteer-line - A very hacky Matrix-LINE bridge based on running LINE's Chrome extension in Puppeteer
// Copyright (C) 2020-2022 Tulir Asokan, Andrew Ferrazzutti
// Copyright (C) 2020-2021 Tulir Asokan, Andrew Ferrazzutti
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by

View File

@ -1,5 +1,5 @@
// matrix-puppeteer-line - A very hacky Matrix-LINE bridge based on running LINE's Chrome extension in Puppeteer
// Copyright (C) 2020-2022 Tulir Asokan, Andrew Ferrazzutti
// Copyright (C) 2020-2021 Tulir Asokan, Andrew Ferrazzutti
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by

View File

@ -125,12 +125,10 @@ acorn@^7.3.1:
resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.0.tgz#e1ad486e6c54501634c6c397c5c121daa383607c"
integrity sha512-+G7P8jJmCHr+S+cLfQxygbWhXy+8YTVGzAkpEbcLo2mLoL7tij/VG41QSHACSf5QgYRhMZYHuNc6drJaO0Da+w==
agent-base@6:
version "6.0.2"
resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77"
integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==
dependencies:
debug "4"
agent-base@5:
version "5.1.1"
resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-5.1.1.tgz#e8fb3f242959db44d63be665db7a8e739537a32c"
integrity sha512-TMeqbNl2fMW0nMjTEPOwe3J/PRFP4vqeoNuQMG0HlMrtm5QxKqdvAkZ1pRBQ/ulIyDD5Yq0nJ7YbdD8ey0TO3g==
ajv@^6.10.0, ajv@^6.10.2:
version "6.12.4"
@ -228,13 +226,6 @@ base64-js@^1.0.2:
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.1.tgz#58ece8cb75dd07e71ed08c736abc5fac4dbf8df1"
integrity sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==
bindings@^1.3.0:
version "1.5.0"
resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.5.0.tgz#10353c9e945334bc0511a6d90b38fbc7c9c504df"
integrity sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==
dependencies:
file-uri-to-path "1.0.0"
bl@^4.0.1:
version "4.0.2"
resolved "https://registry.yarnpkg.com/bl/-/bl-4.0.2.tgz#52b71e9088515d0606d9dd9cc7aa48dc1f98e73a"
@ -373,10 +364,10 @@ define-properties@^1.1.2, define-properties@^1.1.3:
dependencies:
object-keys "^1.0.12"
devtools-protocol@0.0.869402:
version "0.0.869402"
resolved "https://registry.yarnpkg.com/devtools-protocol/-/devtools-protocol-0.0.869402.tgz#03ade701761742e43ae4de5dc188bcd80f156d8d"
integrity sha512-VvlVYY+VDJe639yHs5PHISzdWTLL3Aw8rO4cvUtwvoxFd6FHbE4OpHHcde52M6096uYYazAmd4l0o5VuFRO2WA==
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"
@ -630,11 +621,6 @@ file-entry-cache@^5.0.1:
dependencies:
flat-cache "^2.0.1"
file-uri-to-path@1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd"
integrity sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==
find-up@^2.0.0, find-up@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7"
@ -754,12 +740,12 @@ hosted-git-info@^2.1.4:
resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.8.tgz#7539bd4bc1e0e0a895815a2e0262420b12858488"
integrity sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==
https-proxy-agent@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz#e2a90542abb68a762e0a0850f6c9edadfd8506b2"
integrity sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==
https-proxy-agent@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-4.0.0.tgz#702b71fb5520a132a66de1f67541d9e62154d82b"
integrity sha512-zoDhWrkR3of1l9QAL8/scJZyLu8j/gBkcwcaQOZh7Gyh/+uJQzGVETdgT30akuwkpL8HTRfssqI3BZuV18teDg==
dependencies:
agent-base "6"
agent-base "5"
debug "4"
ieee754@^1.1.4:
@ -966,11 +952,6 @@ ms@^2.1.1:
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
nan@^2.13.2:
version "2.14.2"
resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.2.tgz#f5376400695168f4cc694ac9393d0c9585eeea19"
integrity sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==
natural-compare@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
@ -1158,7 +1139,7 @@ progress@^2.0.0, progress@^2.0.1:
resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8"
integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==
proxy-from-env@^1.1.0:
proxy-from-env@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2"
integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==
@ -1176,19 +1157,19 @@ 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@9.1.1:
version "9.1.1"
resolved "https://registry.yarnpkg.com/puppeteer/-/puppeteer-9.1.1.tgz#f74b7facf86887efd6c6b9fabb7baae6fdce012c"
integrity sha512-W+nOulP2tYd/ZG99WuZC/I5ljjQQ7EUw/jQGcIb9eu8mDlZxNY2SgcJXTLG9h5gRvqA3uJOe4hZXYsd3EqioMw==
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.869402"
devtools-protocol "0.0.818844"
extract-zip "^2.0.0"
https-proxy-agent "^5.0.0"
https-proxy-agent "^4.0.0"
node-fetch "^2.6.1"
pkg-dir "^4.2.0"
progress "^2.0.1"
proxy-from-env "^1.1.0"
proxy-from-env "^1.0.0"
rimraf "^3.0.2"
tar-fs "^2.0.0"
unbzip2-stream "^1.3.3"
@ -1393,13 +1374,6 @@ supports-color@^7.1.0:
dependencies:
has-flag "^4.0.0"
systemd-daemon@^1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/systemd-daemon/-/systemd-daemon-1.1.2.tgz#63171f4353e0f96ef2d2257a5e6258cb89136cc3"
integrity sha512-1s3JH5W78WYQI6iAQdsgoz9LMO5Sj5OtanjeNopJ15iX2q6QupRvkG5SQPJIj+YN3IgUMqPbtzfWxweCVKe28g==
optionalDependencies:
unix-dgram "^2.0.2"
table@^5.2.3:
version "5.4.6"
resolved "https://registry.yarnpkg.com/table/-/table-5.4.6.tgz#1292d19500ce3f86053b05f0e8e7e4a3bb21079e"
@ -1476,14 +1450,6 @@ unbzip2-stream@^1.3.3:
buffer "^5.2.1"
through "^2.3.8"
unix-dgram@^2.0.2:
version "2.0.4"
resolved "https://registry.yarnpkg.com/unix-dgram/-/unix-dgram-2.0.4.tgz#14d4fc21e539742b8fb027de16eccd4e5503a344"
integrity sha512-7tpK6x7ls7J7pDrrAU63h93R0dVhRbPwiRRCawR10cl+2e1VOvF3bHlVJc6WI1dl/8qk5He673QU+Ogv7bPNaw==
dependencies:
bindings "^1.3.0"
nan "^2.13.2"
uri-js@^4.2.2:
version "4.2.2"
resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.2.2.tgz#94c540e1ff772956e2299507c010aea6c8838eb0"

View File

@ -1,18 +0,0 @@
[Unit]
Description=Chrome/Puppeteer backend for matrix-puppeteer-line
After=multi-user.target network.target
[Service]
; User=matrix-puppeteer-line
; Group=matrix-puppeteer-line
Type=notify
NotifyAccess=all
WorkingDirectory=/opt/matrix-puppeteer-line/puppet
ConfigurationDirectory=matrix-puppeteer-line
RuntimeDirectory=matrix-puppeteer-line
ExecStart=/usr/bin/xvfb-run -a yarn start --config ${CONFIGURATION_DIRECTORY}/puppet-config.json
Restart=on-failure
RestartSec=3
[Install]
WantedBy=multi-user.target

View File

@ -1,18 +0,0 @@
[Unit]
Description=matrix-puppeteer-line bridge
BindsTo=matrix-puppeteer-line-chrome.service
PartOf=matrix-puppeteer-line-chrome.service
After=matrix-puppeteer-line-chrome.service
[Service]
; User=matrix-puppeteer-line
; Group=matrix-puppeteer-line
WorkingDirectory=/opt/matrix-puppeteer-line
ConfigurationDirectory=matrix-puppeteer-line
RuntimeDirectory=matrix-puppeteer-line
ExecStart=/opt/matrix-puppeteer-line/.venv/bin/python -m matrix_puppeteer_line -c ${CONFIGURATION_DIRECTORY}/config.yaml
Restart=on-failure
RestartSec=3
[Install]
WantedBy=multi-user.target