Compare commits
No commits in common. "master" and "master" have entirely different histories.
6
.gitignore
vendored
6
.gitignore
vendored
@ -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
|
||||
|
47
Dockerfile
47
Dockerfile
@ -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"]
|
||||
|
@ -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
|
@ -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)
|
||||
|
||||
|
45
ROADMAP.md
45
ROADMAP.md
@ -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
138
SETUP.md
@ -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.
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -1,3 +1,2 @@
|
||||
from .auth import SECTION_AUTH
|
||||
from .conn import SECTION_CONNECTION
|
||||
from .line import SECTION_CHATS
|
||||
|
@ -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)
|
||||
|
@ -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()
|
||||
|
@ -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.")
|
@ -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
|
||||
|
@ -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"]
|
||||
|
@ -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)
|
@ -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
|
||||
|
@ -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:])
|
||||
|
@ -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
|
||||
|
@ -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]
|
||||
|
@ -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
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
)""")
|
||||
)""")
|
@ -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)
|
||||
|
@ -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?
|
||||
|
@ -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)
|
||||
|
@ -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()
|
||||
|
@ -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]
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
3
puppet/.gitignore
vendored
@ -1,3 +1,2 @@
|
||||
/node_modules
|
||||
/config*.json
|
||||
/*.sock
|
||||
/config.json
|
||||
|
@ -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"]
|
||||
|
@ -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.
|
||||
|
@ -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
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
||||
|
@ -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
14
puppet/prep_helper.js
Normal 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,
|
||||
})
|
||||
})()
|
@ -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
|
||||
|
@ -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
@ -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
@ -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
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
|
@ -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
|
@ -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
|
Loading…
x
Reference in New Issue
Block a user