Compare commits

..

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

52 changed files with 675 additions and 2620 deletions

6
.gitignore vendored
View File

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

View File

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

View File

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

View File

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

View File

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

138
SETUP.md
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,35 +0,0 @@
# matrix-puppeteer-line - A very hacky Matrix-LINE bridge based on running LINE's Chrome extension in Puppeteer
# Copyright (C) 2020-2022 Tulir Asokan, Andrew Ferrazzutti
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from mautrix.bridge.commands import HelpSection, command_handler
from .. import puppet as pu
from .typehint import CommandEvent
SECTION_CHATS = HelpSection("Contacts & Chats", 40, "")
@command_handler(needs_auth=True, management_only=False, help_section=SECTION_CHATS,
help_text="List all LINE contacts")
async def list_contacts(evt: CommandEvent) -> None:
# TODO Use a generator if it's worth it
puppets = await pu.Puppet.get_all()
puppets.sort(key=lambda puppet: puppet.name)
results = "".join(f"* [{puppet.name}](https://matrix.to/#/{puppet.default_mxid})\n"
for puppet in puppets)
if results:
await evt.reply(f"Contacts:\n\n{results}")
else:
await evt.reply("No contacts found.")

View File

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

View File

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

View File

@ -1,54 +0,0 @@
# matrix-puppeteer-line - A very hacky Matrix-LINE bridge based on running LINE's Chrome extension in Puppeteer
# Copyright (C) 2020-2022 Tulir Asokan, Andrew Ferrazzutti
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from typing import Optional, ClassVar, TYPE_CHECKING
from attr import dataclass
from mautrix.types import UserID
from mautrix.util.async_db import Database
fake_db = Database("") if TYPE_CHECKING else None
@dataclass
class LoginCredential:
db: ClassVar[Database] = fake_db
mxid: UserID
email: str
password: str
async def insert(self) -> None:
q = ("INSERT INTO login_credential (mxid, email, password) "
"VALUES ($1, $2, $3)")
await self.db.execute(q, self.mxid, self.email, self.password)
async def update(self) -> None:
await self.db.execute("UPDATE login_credential SET email=$2, password=$3 WHERE mxid=$1",
self.mxid, self.email, self.password)
@classmethod
async def get_by_mxid(cls, mxid: UserID) -> Optional["LoginCredential"]:
q = ("SELECT mxid, email, password "
"FROM login_credential WHERE mxid=$1")
row = await cls.db.fetchrow(q, mxid)
if not row:
return None
return cls(**row)
async def delete(self) -> None:
await self.db.execute("DELETE FROM login_credential WHERE mxid=$1",
self.mxid)

View File

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

View File

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

View File

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

View File

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

View File

@ -1,72 +0,0 @@
# matrix-puppeteer-line - A very hacky Matrix-LINE bridge based on running LINE's Chrome extension in Puppeteer
# Copyright (C) 2020-2022 Tulir Asokan, Andrew Ferrazzutti
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from typing import List, ClassVar, Dict, Optional, TYPE_CHECKING
from attr import dataclass
from mautrix.util.async_db import Database
fake_db = Database("") if TYPE_CHECKING else None
@dataclass
class Receipt:
db: ClassVar[Database] = fake_db
mid: int
chat_id: str
num_read: int
async def insert_or_update(self) -> None:
q = ("INSERT INTO receipt (mid, chat_id, num_read) "
"VALUES ($1, $2, $3) "
"ON CONFLICT (chat_id, num_read) "
"DO UPDATE SET mid=EXCLUDED.mid, num_read=EXCLUDED.num_read")
await self.db.execute(q, self.mid, self.chat_id, self.num_read)
# Delete lower counts for earlier messages
# TODO Consider using a CHECK for this instead
q = ("DELETE FROM receipt "
"WHERE chat_id=$1 AND mid<$2 AND num_read<$3")
await self.db.execute(q, self.chat_id, self.mid, self.num_read)
@classmethod
async def get_max_mid(cls, chat_id: str, num_read: int) -> Optional[int]:
q = ("SELECT mid FROM receipt "
"WHERE chat_id=$1 AND num_read=$2")
return await cls.db.fetchval(q, chat_id, num_read)
@classmethod
async def get_max_mid_per_num_read(cls, chat_id: str) -> Dict[int, int]:
rows = await cls.db.fetch("SELECT chat_id, mid, num_read FROM receipt WHERE chat_id=$1", chat_id)
data = {}
for row in rows:
data[row["num_read"]] = row["mid"]
return data
@classmethod
async def get_max_mids_per_num_read(cls) -> Dict[str, Dict[int, int]]:
rows = await cls.db.fetch("SELECT chat_id, mid, num_read FROM receipt")
data = {}
for row in rows:
chat_id = row["chat_id"]
if chat_id not in data:
inner_data = {}
data[chat_id] = inner_data
else:
inner_data = data[chat_id]
inner_data[row["num_read"]] = row["mid"]
return data

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,5 @@
# matrix-puppeteer-line - A very hacky Matrix-LINE bridge based on running LINE's Chrome extension in Puppeteer
# Copyright (C) 2020-2022 Tulir Asokan, Andrew Ferrazzutti
# Copyright (C) 2020-2021 Tulir Asokan, Andrew Ferrazzutti
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
@ -16,13 +16,10 @@
from typing import TYPE_CHECKING
from mautrix.bridge import BaseMatrixHandler
from mautrix.types import (Event, EventType, MessageEvent, StateEvent, EncryptedEvent,
ReceiptEvent, SingleReceiptEventContent, TextMessageEventContent,
from mautrix.types import (Event, ReactionEvent, MessageEvent, StateEvent, EncryptedEvent, RedactionEvent,
EventID, RoomID, UserID)
from mautrix.errors import MatrixError
from . import portal as po, puppet as pu, user as u
from .db import User as DBUser
if TYPE_CHECKING:
from .__main__ import MessagesBridge
@ -38,9 +35,8 @@ class MatrixHandler(BaseMatrixHandler):
super().__init__(bridge=bridge)
def filter_matrix_event(self, evt: Event) -> bool:
if isinstance(evt, ReceiptEvent):
return False
if not isinstance(evt, (MessageEvent, StateEvent, EncryptedEvent)):
if not isinstance(evt, (ReactionEvent, MessageEvent, StateEvent, EncryptedEvent,
RedactionEvent)):
return True
return (evt.sender == self.az.bot_mxid
or pu.Puppet.get_id_from_mxid(evt.sender) is not None)
@ -53,113 +49,9 @@ class MatrixHandler(BaseMatrixHandler):
await self.az.intent.send_notice(room_id, "This room has been marked as your "
"LINE bridge notice room.")
async def handle_puppet_invite(self, room_id: RoomID, puppet: 'pu.Puppet',
invited_by: 'u.User', _: EventID) -> None:
intent = puppet.intent
self.log.debug(f"{invited_by.mxid} invited puppet for {puppet.mid} to {room_id}")
if not await invited_by.is_logged_in():
await intent.error_and_leave(room_id, text="Please log in before inviting "
"LINE puppets to private chats.")
return
portal = await po.Portal.get_by_mxid(room_id)
if portal:
if portal.is_direct:
await intent.error_and_leave(room_id, text="You can not invite additional users "
"to private chats.")
else:
# TODO Send invite in LINE
await intent.error_and_leave(room_id, text="Inviting additional users to an existing "
"group chat is not yet supported.")
return
await intent.join_room(room_id)
try:
members = await intent.get_room_members(room_id)
except MatrixError:
self.log.exception(f"Failed to get member list after joining {room_id}")
await intent.leave_room(room_id)
return
if len(members) > 2:
# TODO Add LINE group/room creating. Must also distinguish between the two!
await intent.send_notice(room_id, "You can not invite LINE puppets to "
"multi-user rooms.")
await intent.leave_room(room_id)
return
portal = await po.Portal.get_by_chat_id(puppet.mid, create=True)
if portal.mxid:
try:
await intent.invite_user(portal.mxid, invited_by.mxid, check_cache=False)
await intent.send_notice(room_id,
text=("You already have a private chat with me "
f"in room {portal.mxid}"),
html=("You already have a private chat with me: "
f"<a href='https://matrix.to/#/{portal.mxid}'>"
"Link to room"
"</a>"))
await intent.leave_room(room_id)
return
except MatrixError:
pass
portal.mxid = room_id
e2be_ok = await portal.check_dm_encryption()
# TODO Consider setting other power levels that get set on portal creation,
# but they're of little use when the inviting user has an equal PL...
await portal.save()
if e2be_ok is True:
evt_type, content = await self.e2ee.encrypt(
room_id, EventType.ROOM_MESSAGE,
TextMessageEventContent(msgtype=MessageType.NOTICE,
body="Portal to private chat created and end-to-bridge"
" encryption enabled."))
await intent.send_message_event(room_id, evt_type, content)
else:
message = "Portal to private chat created."
if e2be_ok is False:
message += "\n\nWarning: Failed to enable end-to-bridge encryption"
await intent.send_notice(room_id, message)
# TODO Put pause/resume in portal methods, with a lock or something
# TODO Consider not backfilling on invite.
# To do so, must set the last-seen message ID appropriately
await invited_by.client.pause()
try:
chat_info = await invited_by.client