Compare commits
No commits in common. "master" and "master" have entirely different histories.
|
@ -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 |