mirror of
https://github.com/matrix-org/pantalaimon.git
synced 2025-04-25 09:59:31 -04:00
Compare commits
No commits in common. "master" and "0.7.0" have entirely different histories.
45
.github/workflows/ci.yml
vendored
45
.github/workflows/ci.yml
vendored
@ -1,45 +0,0 @@
|
|||||||
name: Build Status
|
|
||||||
|
|
||||||
on: [push, pull_request]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
python-version: ['3.8', '3.9', '3.10']
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
- uses: actions/setup-python@v2
|
|
||||||
with:
|
|
||||||
python-version: ${{ matrix.python-version }}
|
|
||||||
- name: Install Tox and any other packages
|
|
||||||
run: |
|
|
||||||
wget https://gitlab.matrix.org/matrix-org/olm/-/archive/master/olm-master.tar.bz2
|
|
||||||
tar -xvf olm-master.tar.bz2
|
|
||||||
pushd olm-master && make && sudo make PREFIX="/usr" install && popd
|
|
||||||
rm -r olm-master
|
|
||||||
pip install tox
|
|
||||||
- name: Run Tox
|
|
||||||
run: tox -e py
|
|
||||||
|
|
||||||
coverage:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
- name: Setup Python
|
|
||||||
uses: actions/setup-python@v2
|
|
||||||
with:
|
|
||||||
python-version: "3.10"
|
|
||||||
- name: Install Tox and any other packages
|
|
||||||
run: |
|
|
||||||
wget https://gitlab.matrix.org/matrix-org/olm/-/archive/master/olm-master.tar.bz2
|
|
||||||
tar -xvf olm-master.tar.bz2
|
|
||||||
pushd olm-master && make && sudo make PREFIX="/usr" install && popd
|
|
||||||
rm -r olm-master
|
|
||||||
pip install tox
|
|
||||||
- name: Run Tox
|
|
||||||
run: tox -e coverage
|
|
12
.travis.yml
12
.travis.yml
@ -16,14 +16,14 @@ before_install:
|
|||||||
|
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- python: 3.8
|
- python: 3.6
|
||||||
env: TOXENV=py38
|
env: TOXENV=py36
|
||||||
- python: 3.9
|
- python: 3.7
|
||||||
env: TOXENV=py39
|
env: TOXENV=py37
|
||||||
- python: 3.9
|
- python: 3.7
|
||||||
env: TOXENV=coverage
|
env: TOXENV=coverage
|
||||||
|
|
||||||
install: pip install tox-travis aioresponses
|
install: pip install tox-travis PyGObject dbus-python aioresponses
|
||||||
script: tox
|
script: tox
|
||||||
|
|
||||||
after_success:
|
after_success:
|
||||||
|
119
CHANGELOG.md
119
CHANGELOG.md
@ -4,125 +4,6 @@ All notable changes to this project will be documented in this file.
|
|||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
## 0.10.5 2022-09-28
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- [[#137]] Proxy the v3 endpoints as well
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- [[#130]] Make sure the token variable is declared
|
|
||||||
|
|
||||||
[#137]: https://github.com/matrix-org/pantalaimon/pull/137
|
|
||||||
[#130]: https://github.com/matrix-org/pantalaimon/pull/130
|
|
||||||
|
|
||||||
## 0.10.4 2022-02-04
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- [[#122]] Fix the GLib import for panctl on some distributions
|
|
||||||
- [[#120]] Don't use strip to filter Bearer from the auth header
|
|
||||||
- [[#118]] Don't use the raw path if we need to sanitize filters, fixing room
|
|
||||||
history fetching for Fractal
|
|
||||||
|
|
||||||
[#122]: https://github.com/matrix-org/pantalaimon/pull/122
|
|
||||||
[#120]: https://github.com/matrix-org/pantalaimon/pull/120
|
|
||||||
[#118]: https://github.com/matrix-org/pantalaimon/pull/118
|
|
||||||
|
|
||||||
## 0.10.3 2021-09-02
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- [[#105]] Use the raw_path when forwarding requests, avoiding URL
|
|
||||||
decoding/encoding issues.
|
|
||||||
|
|
||||||
[#105]: https://github.com/matrix-org/pantalaimon/pull/105
|
|
||||||
|
|
||||||
|
|
||||||
## 0.10.2 2021-07-14
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- [[#103]] Prevent E2EE downgrade on failed syncs
|
|
||||||
|
|
||||||
[#103]: https://github.com/matrix-org/pantalaimon/pull/103
|
|
||||||
|
|
||||||
|
|
||||||
## 0.10.1 2021-07-06
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- [[#100]] Don't require the rooms dicts in the sync response
|
|
||||||
- [[#99]] Thumbnails not generating for media uploaded in unencrypted rooms
|
|
||||||
whole LRU cache when it shouldn't
|
|
||||||
|
|
||||||
[#100]: https://github.com/matrix-org/pantalaimon/pull/100
|
|
||||||
[#99]: https://github.com/matrix-org/pantalaimon/pull/99
|
|
||||||
|
|
||||||
|
|
||||||
## 0.10.0 2021-05-14
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- [[#98]] Add the ability to remove old room keys
|
|
||||||
- [[#95]] Encrypt thumbnails uploaded by a client
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- [[#96]] Split out the media cache loading logic to avoid returning the
|
|
||||||
whole LRU cache when it shouldn't
|
|
||||||
|
|
||||||
[#98]: https://github.com/matrix-org/pantalaimon/pull/98
|
|
||||||
[#96]: https://github.com/matrix-org/pantalaimon/pull/96
|
|
||||||
[#95]: https://github.com/matrix-org/pantalaimon/pull/95
|
|
||||||
|
|
||||||
## 0.9.3 2021-05-14
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
|
|
||||||
- [[#73f68c7]] Bump the allowed nio version
|
|
||||||
|
|
||||||
[73f68c7]: https://github.com/matrix-org/pantalaimon/commit/73f68c76fb05037bd7fe71688ce39eb1f526a385
|
|
||||||
|
|
||||||
## 0.9.2 2021-03-10
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
|
|
||||||
- [[#89]] Bump the allowed nio version
|
|
||||||
|
|
||||||
[#89]: https://github.com/matrix-org/pantalaimon/pull/89
|
|
||||||
|
|
||||||
## 0.9.1 2021-01-19
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
|
|
||||||
- [[3baae08]] Bump the allowed nio version
|
|
||||||
|
|
||||||
[3baae08]: https://github.com/matrix-org/pantalaimon/commit/3baae08ac36e258632e224b655e177a765a939f3
|
|
||||||
|
|
||||||
## 0.9.0 2021-01-19
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- [[59051c5]] Fix the notification initialization allowing the DBUS thread to
|
|
||||||
start again
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- [[#79]] Support media uploads, thanks to @aspacca
|
|
||||||
|
|
||||||
[59051c5]: https://github.com/matrix-org/pantalaimon/commit/59051c530a343a6887ea0f9ccddd6f6964f6d923
|
|
||||||
[#79]: https://github.com/matrix-org/pantalaimon/pull/79
|
|
||||||
|
|
||||||
## 0.8.0 2020-09-30
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
|
|
||||||
- [[#69]] If no password is provided to /login, the daemon will re-use the original login response.
|
|
||||||
|
|
||||||
[#69]: https://github.com/matrix-org/pantalaimon/pull/69
|
|
||||||
|
|
||||||
## 0.7.0 2020-09-02
|
## 0.7.0 2020-09-02
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
56
README.md
56
README.md
@ -25,19 +25,11 @@ Installing pantalaimon works like usually with python packages:
|
|||||||
|
|
||||||
python setup.py install
|
python setup.py install
|
||||||
|
|
||||||
or you can use `pip` and install it with:
|
|
||||||
```
|
|
||||||
pip install .[ui]
|
|
||||||
```
|
|
||||||
|
|
||||||
It is recommended that you create a virtual environment first or install dependencies
|
|
||||||
via your package manager. They are usually found with `python-<package-name>`.
|
|
||||||
|
|
||||||
Pantalaimon can also be found on pypi:
|
Pantalaimon can also be found on pypi:
|
||||||
|
|
||||||
pip install pantalaimon
|
pip install pantalaimon
|
||||||
|
|
||||||
Pantalaimon contains a dbus based UI that can be used to control the daemon.
|
Pantalaimon contains a dbus based UI that can be used to controll the daemon.
|
||||||
The dbus based UI is completely optional and needs to be installed with the
|
The dbus based UI is completely optional and needs to be installed with the
|
||||||
daemon:
|
daemon:
|
||||||
|
|
||||||
@ -85,10 +77,6 @@ docker build -t pantalaimon .
|
|||||||
# volume below is for where Pantalaimon should dump some data.
|
# volume below is for where Pantalaimon should dump some data.
|
||||||
docker run -it --rm -v /path/to/pantalaimon/dir:/data -p 8008:8008 pantalaimon
|
docker run -it --rm -v /path/to/pantalaimon/dir:/data -p 8008:8008 pantalaimon
|
||||||
```
|
```
|
||||||
The Docker image in the above example can alternatively be built straight from any branch or tag without the need to clone the repo, just by using this syntax:
|
|
||||||
```bash
|
|
||||||
docker build -t pantalaimon github.com/matrix-org/pantalaimon#master
|
|
||||||
```
|
|
||||||
|
|
||||||
An example `pantalaimon.conf` for Docker is:
|
An example `pantalaimon.conf` for Docker is:
|
||||||
```conf
|
```conf
|
||||||
@ -108,7 +96,7 @@ IgnoreVerification = True
|
|||||||
Usage
|
Usage
|
||||||
=====
|
=====
|
||||||
|
|
||||||
While pantalaimon is a daemon, it is meant to be run as the same user as the app it is proxying for. It won't
|
While pantalaimon is a daemon, it is meant to be run as your own user. It won't
|
||||||
verify devices for you automatically, unless configured to do so, and requires
|
verify devices for you automatically, unless configured to do so, and requires
|
||||||
user interaction to verify, ignore or blacklist devices. A more complete
|
user interaction to verify, ignore or blacklist devices. A more complete
|
||||||
description of Pantalaimon can be found in the [man page](docs/man/pantalaimon.8.md).
|
description of Pantalaimon can be found in the [man page](docs/man/pantalaimon.8.md).
|
||||||
@ -119,7 +107,7 @@ specifies one or more homeservers for pantalaimon to connect to.
|
|||||||
A minimal pantalaimon configuration looks like this:
|
A minimal pantalaimon configuration looks like this:
|
||||||
```dosini
|
```dosini
|
||||||
[local-matrix]
|
[local-matrix]
|
||||||
Homeserver = https://localhost:443
|
Homeserver = https://localhost:8448
|
||||||
ListenAddress = localhost
|
ListenAddress = localhost
|
||||||
ListenPort = 8009
|
ListenPort = 8009
|
||||||
```
|
```
|
||||||
@ -148,41 +136,3 @@ To control the daemon an interactive utility is provided in the form of
|
|||||||
`panctl` can be used to verify, blacklist or ignore devices, import or export
|
`panctl` can be used to verify, blacklist or ignore devices, import or export
|
||||||
session keys, or to introspect devices of users that we share encrypted rooms
|
session keys, or to introspect devices of users that we share encrypted rooms
|
||||||
with.
|
with.
|
||||||
|
|
||||||
### Setup
|
|
||||||
This is all coming from an excellent comment that you can find [here](https://github.com/matrix-org/pantalaimon/issues/154#issuecomment-1951591191).
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
1) Ensure you have an OS keyring installed. In my case I installed `gnome-keyring`. You may also want a GUI like `seahorse` to inspect the keyring. (pantalaimon will work without a keyring but your client will have to log in with the password every time `pantalaimon` is restarted, instead of being able to reuse the access token from the previous successful login.)
|
|
||||||
|
|
||||||
2) In case you have prior attempts, clean the slate by deleting the `~/.local/share/pantalaimon` directory.
|
|
||||||
|
|
||||||
3) Start `pantalaimon`.
|
|
||||||
|
|
||||||
4) Connect a client to the `ListenAddress:ListenPort` you specified in `pantalaimon.conf`, eg to `127.0.0.1:8009`, using the same username and password you would've used to login to your homeserver directly.
|
|
||||||
|
|
||||||
5) The login should succeed, but at this point all encrypted messages will fail to decrypt. This is fine.
|
|
||||||
|
|
||||||
6) Start another client that you were already using for your encrypted chats previously. In my case this was `app.element.io`, so the rest of the steps here assume that.
|
|
||||||
|
|
||||||
7) Run `panctl`. At the prompt, run `start-verification <user ID> <user ID> <Element's device ID>`. `<user ID>` here is the full user ID like `@arnavion:arnavion.dev`. If you only have the one Element session, `panctl` will show you the device ID as an autocomplete hint so you don't have to look it up. If you do need to look it up, go to Element -> profile icon -> All Settings -> Sessions, expand the "Current session" item, and the "Session ID" is the device ID.
|
|
||||||
|
|
||||||
8) In Element you will see a popup "Incoming Verification Request". Click "Continue". It will change to a popup containing some emojis, and `panctl` will print the same emojis. Click the "They match" button. It will now change to a popup like "Waiting for other client to confirm..."
|
|
||||||
|
|
||||||
9) In `panctl`, run `confirm-verification <user ID> <user ID> <Element's device ID>`, ie the same command as before but with `confirm-verification` instead of `start-verification`.
|
|
||||||
|
|
||||||
10) At this point, if you look at all your sessions in Element (profile icon -> All Settings -> Sessions), you should see "pantalaimon" in the "Other sessions" list as a "Verified" session.
|
|
||||||
|
|
||||||
11) Export the E2E room keys that Element was using via profile icon -> Security & Privacy -> Export E2E room keys. Pick any password and then save the file to some path.
|
|
||||||
|
|
||||||
12) Back in `panctl`, run `import-keys <user ID> <path of file> <password you used to encrypt the file>`. After a few seconds, in the output of `pantalaimon`, you should see a log like `INFO: pantalaimon: Successfully imported keys for <user ID> from <path of file>`.
|
|
||||||
|
|
||||||
13) Close and restart the client you had used in step 5, ie the one you want to connect to `pantalaimon`. Now, finally, you should be able to see the encrypted chats be decrypted.
|
|
||||||
|
|
||||||
14) Delete the E2E room keys backup file from step 12. You don't need it any more.
|
|
||||||
|
|
||||||
|
|
||||||
15) If in step 11 you had other unverified sessions from pantalaimon from your prior attempts, you can sign out of them too.
|
|
||||||
|
|
||||||
You will probably have to repeat steps 11-14 any time you start a new encrypted chat in Element.
|
|
||||||
|
@ -12,4 +12,3 @@ Proxy = http://localhost:8080
|
|||||||
SSL = False
|
SSL = False
|
||||||
IgnoreVerification = False
|
IgnoreVerification = False
|
||||||
UseKeyring = True
|
UseKeyring = True
|
||||||
DropOldKeys = False
|
|
||||||
|
@ -51,7 +51,7 @@ The message will be sent away after all devices are marked as ignored.
|
|||||||
In contrast to the
|
In contrast to the
|
||||||
.Cm send-anyways
|
.Cm send-anyways
|
||||||
command this command cancels the sending of a message to an encrypted room with
|
command this command cancels the sending of a message to an encrypted room with
|
||||||
unverified devices and gives the user the opportunity to verify or blacklist
|
unverified devices and gives the user the oportunity to verify or blacklist
|
||||||
devices as they see fit.
|
devices as they see fit.
|
||||||
.It Cm import-keys Ar pan-user Ar file Ar passphrase
|
.It Cm import-keys Ar pan-user Ar file Ar passphrase
|
||||||
Import end-to-end encryption keys from the given file for the given pan-user.
|
Import end-to-end encryption keys from the given file for the given pan-user.
|
||||||
|
@ -74,7 +74,7 @@ are as follows:
|
|||||||
> In contrast to the
|
> In contrast to the
|
||||||
> **send-anyways**
|
> **send-anyways**
|
||||||
> command this command cancels the sending of a message to an encrypted room with
|
> command this command cancels the sending of a message to an encrypted room with
|
||||||
> unverified devices and gives the user the opportunity to verify or blacklist
|
> unverified devices and gives the user the oportunity to verify or blacklist
|
||||||
> devices as they see fit.
|
> devices as they see fit.
|
||||||
|
|
||||||
**import-keys** *pan-user* *file* *passphrase*
|
**import-keys** *pan-user* *file* *passphrase*
|
||||||
|
@ -51,11 +51,6 @@ This option configures if a proxy instance should use the OS keyring to store
|
|||||||
its own access tokens. The access tokens are required for the daemon to resume
|
its own access tokens. The access tokens are required for the daemon to resume
|
||||||
operation. If this is set to "No", access tokens are stored in the pantalaimon
|
operation. If this is set to "No", access tokens are stored in the pantalaimon
|
||||||
database in plaintext. Defaults to "Yes".
|
database in plaintext. Defaults to "Yes".
|
||||||
.It Cm DropOldKeys
|
|
||||||
This option configures if a proxy instance should only keep the latest version
|
|
||||||
of a room key from a certain user around. This effectively means that only newly
|
|
||||||
incoming messages will be decryptable, the proxy will be unable to decrypt the
|
|
||||||
room history. Defaults to "No".
|
|
||||||
.It Cm SearchRequests
|
.It Cm SearchRequests
|
||||||
This option configures if the proxy should make additional HTTP requests to the
|
This option configures if the proxy should make additional HTTP requests to the
|
||||||
server when clients use the search API endpoint. Some data that is required to
|
server when clients use the search API endpoint. Some data that is required to
|
||||||
@ -86,7 +81,7 @@ The amount of time to wait between room message history requests to the
|
|||||||
Homeserver in ms. Defaults to 3000.
|
Homeserver in ms. Defaults to 3000.
|
||||||
.El
|
.El
|
||||||
.Pp
|
.Pp
|
||||||
Additional to the homeserver section a special section with the name
|
Aditional to the homeserver section a special section with the name
|
||||||
.Cm Default
|
.Cm Default
|
||||||
can be used to configure the following values for all homeservers:
|
can be used to configure the following values for all homeservers:
|
||||||
.Cm ListenAddress ,
|
.Cm ListenAddress ,
|
||||||
@ -133,7 +128,7 @@ Default location of the configuration file.
|
|||||||
The following example shows a configured pantalaimon proxy with the name
|
The following example shows a configured pantalaimon proxy with the name
|
||||||
.Em Clocktown ,
|
.Em Clocktown ,
|
||||||
the homeserver URL is set to
|
the homeserver URL is set to
|
||||||
.Em https://localhost:8448 ,
|
.Em https://example.org ,
|
||||||
the pantalaimon proxy is listening for client connections on the address
|
the pantalaimon proxy is listening for client connections on the address
|
||||||
.Em localhost ,
|
.Em localhost ,
|
||||||
and port
|
and port
|
||||||
|
@ -62,14 +62,7 @@ The following keys are optional in the proxy instance sections:
|
|||||||
> operation. If this is set to "No", access tokens are stored in the pantalaimon
|
> operation. If this is set to "No", access tokens are stored in the pantalaimon
|
||||||
> database in plaintext. Defaults to "Yes".
|
> database in plaintext. Defaults to "Yes".
|
||||||
|
|
||||||
**DropOldKeys**
|
Aditional to the homeserver section a special section with the name
|
||||||
|
|
||||||
> This option configures if a proxy instance should only keep the latest version
|
|
||||||
> of a room key from a certain user around. This effectively means that only newly
|
|
||||||
> incoming messages will be decryptable, the proxy will be unable to decrypt the
|
|
||||||
> room history. Defaults to "No".
|
|
||||||
|
|
||||||
Additional to the homeserver section a special section with the name
|
|
||||||
**Default**
|
**Default**
|
||||||
can be used to configure the following values for all homeservers:
|
can be used to configure the following values for all homeservers:
|
||||||
**ListenAddress**,
|
**ListenAddress**,
|
||||||
@ -118,7 +111,7 @@ overridden using appropriate environment variables.
|
|||||||
The following example shows a configured pantalaimon proxy with the name
|
The following example shows a configured pantalaimon proxy with the name
|
||||||
*Clocktown*,
|
*Clocktown*,
|
||||||
the homeserver URL is set to
|
the homeserver URL is set to
|
||||||
*https://localhost:8448*,
|
*https://example.org*,
|
||||||
the pantalaimon proxy is listening for client connections on the address
|
the pantalaimon proxy is listening for client connections on the address
|
||||||
*localhost*,
|
*localhost*,
|
||||||
and port
|
and port
|
||||||
@ -157,4 +150,4 @@ pantalaimon(8)
|
|||||||
was written by
|
was written by
|
||||||
Damir Jelić <[poljar@termina.org.uk](mailto:poljar@termina.org.uk)>.
|
Damir Jelić <[poljar@termina.org.uk](mailto:poljar@termina.org.uk)>.
|
||||||
|
|
||||||
Linux 5.11.16-arch1-1 - May 8, 2019
|
Linux 5.1.3-arch2-1-ARCH - May 8, 2019
|
||||||
|
@ -24,7 +24,7 @@ behalf of the client.
|
|||||||
is supposed to run as your own user and listen to connections on a
|
is supposed to run as your own user and listen to connections on a
|
||||||
non-privileged port. A client needs to log in using the standard Matrix HTTP
|
non-privileged port. A client needs to log in using the standard Matrix HTTP
|
||||||
calls to register itself to the daemon, such a registered user is called a pan
|
calls to register itself to the daemon, such a registered user is called a pan
|
||||||
user and will have its own sync loop to keep up with the server. Multiple matrix
|
user and will have it's own sync loop to keep up with the server. Multiple matrix
|
||||||
clients can connect and use the same pan user.
|
clients can connect and use the same pan user.
|
||||||
|
|
||||||
If user interaction is required
|
If user interaction is required
|
||||||
|
@ -16,6 +16,7 @@ import asyncio
|
|||||||
import os
|
import os
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from pprint import pformat
|
from pprint import pformat
|
||||||
|
from typing import Any, Dict, Optional
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
from aiohttp.client_exceptions import ClientConnectionError
|
from aiohttp.client_exceptions import ClientConnectionError
|
||||||
@ -134,7 +135,7 @@ class InvalidLimit(Exception):
|
|||||||
class SqliteQStore(SqliteStore):
|
class SqliteQStore(SqliteStore):
|
||||||
def _create_database(self):
|
def _create_database(self):
|
||||||
return SqliteQueueDatabase(
|
return SqliteQueueDatabase(
|
||||||
self.database_path, pragmas=(("foreign_keys", 1), ("secure_delete", 1))
|
self.database_path, pragmas=(("foregign_keys", 1), ("secure_delete", 1))
|
||||||
)
|
)
|
||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
@ -409,10 +410,6 @@ class PanClient(AsyncClient):
|
|||||||
except (asyncio.CancelledError, KeyboardInterrupt):
|
except (asyncio.CancelledError, KeyboardInterrupt):
|
||||||
return
|
return
|
||||||
|
|
||||||
@property
|
|
||||||
def has_been_synced(self) -> bool:
|
|
||||||
self.last_sync_token is not None
|
|
||||||
|
|
||||||
async def sync_tasks(self, response):
|
async def sync_tasks(self, response):
|
||||||
if self.index:
|
if self.index:
|
||||||
await self.index.commit_events()
|
await self.index.commit_events()
|
||||||
@ -543,6 +540,7 @@ class PanClient(AsyncClient):
|
|||||||
timeout = 30000
|
timeout = 30000
|
||||||
sync_filter = {"room": {"state": {"lazy_load_members": True}}}
|
sync_filter = {"room": {"state": {"lazy_load_members": True}}}
|
||||||
next_batch = self.pan_store.load_token(self.server_name, self.user_id)
|
next_batch = self.pan_store.load_token(self.server_name, self.user_id)
|
||||||
|
self.last_sync_token = next_batch
|
||||||
|
|
||||||
# We don't store any room state so initial sync needs to be with the
|
# We don't store any room state so initial sync needs to be with the
|
||||||
# full_state parameter. Subsequent ones are normal.
|
# full_state parameter. Subsequent ones are normal.
|
||||||
@ -553,7 +551,6 @@ class PanClient(AsyncClient):
|
|||||||
full_state=True,
|
full_state=True,
|
||||||
since=next_batch,
|
since=next_batch,
|
||||||
loop_sleep_time=loop_sleep_time,
|
loop_sleep_time=loop_sleep_time,
|
||||||
set_presence="offline",
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
self.task = task
|
self.task = task
|
||||||
@ -708,6 +705,7 @@ class PanClient(AsyncClient):
|
|||||||
for share in self.get_active_key_requests(
|
for share in self.get_active_key_requests(
|
||||||
message.user_id, message.device_id
|
message.user_id, message.device_id
|
||||||
):
|
):
|
||||||
|
|
||||||
continued = True
|
continued = True
|
||||||
|
|
||||||
if not self.continue_key_share(share):
|
if not self.continue_key_share(share):
|
||||||
@ -734,7 +732,7 @@ class PanClient(AsyncClient):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
response = (
|
response = (
|
||||||
f"Successfully continued the key requests from "
|
f"Succesfully continued the key requests from "
|
||||||
f"{message.user_id} via {message.device_id}"
|
f"{message.user_id} via {message.device_id}"
|
||||||
)
|
)
|
||||||
ret = "m.ok"
|
ret = "m.ok"
|
||||||
@ -759,7 +757,7 @@ class PanClient(AsyncClient):
|
|||||||
|
|
||||||
if cancelled:
|
if cancelled:
|
||||||
response = (
|
response = (
|
||||||
f"Successfully cancelled key requests from "
|
f"Succesfully cancelled key requests from "
|
||||||
f"{message.user_id} via {message.device_id}"
|
f"{message.user_id} via {message.device_id}"
|
||||||
)
|
)
|
||||||
ret = "m.ok"
|
ret = "m.ok"
|
||||||
@ -809,9 +807,8 @@ class PanClient(AsyncClient):
|
|||||||
|
|
||||||
if not isinstance(event, MegolmEvent):
|
if not isinstance(event, MegolmEvent):
|
||||||
logger.warn(
|
logger.warn(
|
||||||
"Encrypted event is not a megolm event:" "\n{}".format(
|
"Encrypted event is not a megolm event:"
|
||||||
pformat(event_dict)
|
"\n{}".format(pformat(event_dict))
|
||||||
)
|
|
||||||
)
|
)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@ -835,9 +832,9 @@ class PanClient(AsyncClient):
|
|||||||
decrypted_event.source["content"]["url"] = decrypted_event.url
|
decrypted_event.source["content"]["url"] = decrypted_event.url
|
||||||
|
|
||||||
if decrypted_event.thumbnail_url:
|
if decrypted_event.thumbnail_url:
|
||||||
decrypted_event.source["content"]["info"]["thumbnail_url"] = (
|
decrypted_event.source["content"]["info"][
|
||||||
decrypted_event.thumbnail_url
|
"thumbnail_url"
|
||||||
)
|
] = decrypted_event.thumbnail_url
|
||||||
|
|
||||||
event_dict.update(decrypted_event.source)
|
event_dict.update(decrypted_event.source)
|
||||||
event_dict["decrypted"] = True
|
event_dict["decrypted"] = True
|
||||||
@ -908,7 +905,7 @@ class PanClient(AsyncClient):
|
|||||||
|
|
||||||
self.handle_to_device_from_sync_body(body)
|
self.handle_to_device_from_sync_body(body)
|
||||||
|
|
||||||
for room_id, room_dict in body.get("rooms", {}).get("join", {}).items():
|
for room_id, room_dict in body["rooms"]["join"].items():
|
||||||
try:
|
try:
|
||||||
if not self.rooms[room_id].encrypted:
|
if not self.rooms[room_id].encrypted:
|
||||||
logger.info(
|
logger.info(
|
||||||
@ -923,7 +920,7 @@ class PanClient(AsyncClient):
|
|||||||
# pan sync stream did. Let's assume that the room is encrypted.
|
# pan sync stream did. Let's assume that the room is encrypted.
|
||||||
pass
|
pass
|
||||||
|
|
||||||
for event in room_dict.get("timeline", {}).get("events", []):
|
for event in room_dict["timeline"]["events"]:
|
||||||
if "type" not in event:
|
if "type" not in event:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
@ -31,7 +31,7 @@ class PanConfigParser(configparser.ConfigParser):
|
|||||||
"IgnoreVerification": "False",
|
"IgnoreVerification": "False",
|
||||||
"ListenAddress": "localhost",
|
"ListenAddress": "localhost",
|
||||||
"ListenPort": "8009",
|
"ListenPort": "8009",
|
||||||
"LogLevel": "warning",
|
"LogLevel": "warnig",
|
||||||
"Notifications": "on",
|
"Notifications": "on",
|
||||||
"UseKeyring": "yes",
|
"UseKeyring": "yes",
|
||||||
"SearchRequests": "off",
|
"SearchRequests": "off",
|
||||||
@ -39,7 +39,6 @@ class PanConfigParser(configparser.ConfigParser):
|
|||||||
"IndexingBatchSize": "100",
|
"IndexingBatchSize": "100",
|
||||||
"HistoryFetchDelay": "3000",
|
"HistoryFetchDelay": "3000",
|
||||||
"DebugEncryption": "False",
|
"DebugEncryption": "False",
|
||||||
"DropOldKeys": "False",
|
|
||||||
},
|
},
|
||||||
converters={
|
converters={
|
||||||
"address": parse_address,
|
"address": parse_address,
|
||||||
@ -113,7 +112,7 @@ class ServerConfig:
|
|||||||
E2E encrypted messages.
|
E2E encrypted messages.
|
||||||
keyring (bool): Enable or disable the OS keyring for the storage of
|
keyring (bool): Enable or disable the OS keyring for the storage of
|
||||||
access tokens.
|
access tokens.
|
||||||
search_requests (bool): Enable or disable additional Homeserver requests
|
search_requests (bool): Enable or disable aditional Homeserver requests
|
||||||
for the search API endpoint.
|
for the search API endpoint.
|
||||||
index_encrypted_only (bool): Enable or disable message indexing fro
|
index_encrypted_only (bool): Enable or disable message indexing fro
|
||||||
non-encrypted rooms.
|
non-encrypted rooms.
|
||||||
@ -122,8 +121,6 @@ class ServerConfig:
|
|||||||
the room history.
|
the room history.
|
||||||
history_fetch_delay (int): The delay between room history fetching
|
history_fetch_delay (int): The delay between room history fetching
|
||||||
requests in seconds.
|
requests in seconds.
|
||||||
drop_old_keys (bool): Should Pantalaimon only keep the most recent
|
|
||||||
decryption key around.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = attr.ib(type=str)
|
name = attr.ib(type=str)
|
||||||
@ -140,7 +137,6 @@ class ServerConfig:
|
|||||||
index_encrypted_only = attr.ib(type=bool, default=True)
|
index_encrypted_only = attr.ib(type=bool, default=True)
|
||||||
indexing_batch_size = attr.ib(type=int, default=100)
|
indexing_batch_size = attr.ib(type=int, default=100)
|
||||||
history_fetch_delay = attr.ib(type=int, default=3)
|
history_fetch_delay = attr.ib(type=int, default=3)
|
||||||
drop_old_keys = attr.ib(type=bool, default=False)
|
|
||||||
|
|
||||||
|
|
||||||
@attr.s
|
@attr.s
|
||||||
@ -186,6 +182,7 @@ class PanConfig:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
for section_name, section in config.items():
|
for section_name, section in config.items():
|
||||||
|
|
||||||
if section_name == "Default":
|
if section_name == "Default":
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@ -232,7 +229,6 @@ class PanConfig:
|
|||||||
f"already defined before."
|
f"already defined before."
|
||||||
)
|
)
|
||||||
listen_set.add(listen_tuple)
|
listen_set.add(listen_tuple)
|
||||||
drop_old_keys = section.getboolean("DropOldKeys")
|
|
||||||
|
|
||||||
server_conf = ServerConfig(
|
server_conf = ServerConfig(
|
||||||
section_name,
|
section_name,
|
||||||
@ -247,7 +243,6 @@ class PanConfig:
|
|||||||
index_encrypted_only,
|
index_encrypted_only,
|
||||||
indexing_batch_size,
|
indexing_batch_size,
|
||||||
history_fetch_delay / 1000,
|
history_fetch_delay / 1000,
|
||||||
drop_old_keys,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
self.servers[section_name] = server_conf
|
self.servers[section_name] = server_conf
|
||||||
|
@ -17,10 +17,8 @@ import json
|
|||||||
import os
|
import os
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
import concurrent.futures
|
import concurrent.futures
|
||||||
from io import BufferedReader, BytesIO
|
|
||||||
from json import JSONDecodeError
|
from json import JSONDecodeError
|
||||||
from typing import Any, Dict
|
from typing import Any, Dict
|
||||||
from urllib.parse import urlparse
|
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
@ -37,7 +35,6 @@ from nio import (
|
|||||||
OlmTrustError,
|
OlmTrustError,
|
||||||
SendRetryError,
|
SendRetryError,
|
||||||
DownloadResponse,
|
DownloadResponse,
|
||||||
UploadResponse,
|
|
||||||
)
|
)
|
||||||
from nio.crypto import decrypt_attachment
|
from nio.crypto import decrypt_attachment
|
||||||
|
|
||||||
@ -51,7 +48,7 @@ from pantalaimon.client import (
|
|||||||
)
|
)
|
||||||
from pantalaimon.index import INDEXING_ENABLED, InvalidQueryError
|
from pantalaimon.index import INDEXING_ENABLED, InvalidQueryError
|
||||||
from pantalaimon.log import logger
|
from pantalaimon.log import logger
|
||||||
from pantalaimon.store import ClientInfo, PanStore, MediaInfo
|
from pantalaimon.store import ClientInfo, PanStore
|
||||||
from pantalaimon.thread_messages import (
|
from pantalaimon.thread_messages import (
|
||||||
AcceptSasMessage,
|
AcceptSasMessage,
|
||||||
CancelSasMessage,
|
CancelSasMessage,
|
||||||
@ -83,12 +80,6 @@ CORS_HEADERS = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class NotDecryptedAvailableError(Exception):
|
|
||||||
"""Exception that signals that no decrypted upload is available"""
|
|
||||||
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
@attr.s
|
@attr.s
|
||||||
class ProxyDaemon:
|
class ProxyDaemon:
|
||||||
name = attr.ib()
|
name = attr.ib()
|
||||||
@ -111,7 +102,6 @@ class ProxyDaemon:
|
|||||||
client_info = attr.ib(init=False, default=attr.Factory(dict), type=dict)
|
client_info = attr.ib(init=False, default=attr.Factory(dict), type=dict)
|
||||||
default_session = attr.ib(init=False, default=None)
|
default_session = attr.ib(init=False, default=None)
|
||||||
media_info = attr.ib(init=False, default=None)
|
media_info = attr.ib(init=False, default=None)
|
||||||
upload_info = attr.ib(init=False, default=None)
|
|
||||||
database_name = "pan.db"
|
database_name = "pan.db"
|
||||||
|
|
||||||
def __attrs_post_init__(self):
|
def __attrs_post_init__(self):
|
||||||
@ -121,12 +111,9 @@ class ProxyDaemon:
|
|||||||
self.hostname = self.homeserver.hostname
|
self.hostname = self.homeserver.hostname
|
||||||
self.store = PanStore(self.data_dir)
|
self.store = PanStore(self.data_dir)
|
||||||
accounts = self.store.load_users(self.name)
|
accounts = self.store.load_users(self.name)
|
||||||
self.media_info = self.store.load_media_cache(self.name)
|
self.media_info = self.store.load_media(self.name)
|
||||||
self.upload_info = self.store.load_upload(self.name)
|
|
||||||
|
|
||||||
for user_id, device_id in accounts:
|
for user_id, device_id in accounts:
|
||||||
token = None
|
|
||||||
|
|
||||||
if self.conf.keyring:
|
if self.conf.keyring:
|
||||||
try:
|
try:
|
||||||
token = keyring.get_password(
|
token = keyring.get_password(
|
||||||
@ -227,8 +214,7 @@ class ProxyDaemon:
|
|||||||
|
|
||||||
if ret:
|
if ret:
|
||||||
msg = (
|
msg = (
|
||||||
f"Device {device.id} of user "
|
f"Device {device.id} of user " f"{device.user_id} succesfully verified."
|
||||||
f"{device.user_id} successfully verified."
|
|
||||||
)
|
)
|
||||||
await client.send_update_device(device)
|
await client.send_update_device(device)
|
||||||
else:
|
else:
|
||||||
@ -243,7 +229,7 @@ class ProxyDaemon:
|
|||||||
if ret:
|
if ret:
|
||||||
msg = (
|
msg = (
|
||||||
f"Device {device.id} of user "
|
f"Device {device.id} of user "
|
||||||
f"{device.user_id} successfully unverified."
|
f"{device.user_id} succesfully unverified."
|
||||||
)
|
)
|
||||||
await client.send_update_device(device)
|
await client.send_update_device(device)
|
||||||
else:
|
else:
|
||||||
@ -258,7 +244,7 @@ class ProxyDaemon:
|
|||||||
if ret:
|
if ret:
|
||||||
msg = (
|
msg = (
|
||||||
f"Device {device.id} of user "
|
f"Device {device.id} of user "
|
||||||
f"{device.user_id} successfully blacklisted."
|
f"{device.user_id} succesfully blacklisted."
|
||||||
)
|
)
|
||||||
await client.send_update_device(device)
|
await client.send_update_device(device)
|
||||||
else:
|
else:
|
||||||
@ -275,7 +261,7 @@ class ProxyDaemon:
|
|||||||
if ret:
|
if ret:
|
||||||
msg = (
|
msg = (
|
||||||
f"Device {device.id} of user "
|
f"Device {device.id} of user "
|
||||||
f"{device.user_id} successfully unblacklisted."
|
f"{device.user_id} succesfully unblacklisted."
|
||||||
)
|
)
|
||||||
await client.send_update_device(device)
|
await client.send_update_device(device)
|
||||||
else:
|
else:
|
||||||
@ -310,6 +296,7 @@ class ProxyDaemon:
|
|||||||
DeviceUnblacklistMessage,
|
DeviceUnblacklistMessage,
|
||||||
),
|
),
|
||||||
):
|
):
|
||||||
|
|
||||||
device = client.device_store[message.user_id].get(message.device_id, None)
|
device = client.device_store[message.user_id].get(message.device_id, None)
|
||||||
|
|
||||||
if not device:
|
if not device:
|
||||||
@ -358,7 +345,7 @@ class ProxyDaemon:
|
|||||||
|
|
||||||
else:
|
else:
|
||||||
info_msg = (
|
info_msg = (
|
||||||
f"Successfully exported keys for {client.user_id} " f"to {path}"
|
f"Succesfully exported keys for {client.user_id} " f"to {path}"
|
||||||
)
|
)
|
||||||
logger.info(info_msg)
|
logger.info(info_msg)
|
||||||
await self.send_response(
|
await self.send_response(
|
||||||
@ -381,7 +368,7 @@ class ProxyDaemon:
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
info_msg = (
|
info_msg = (
|
||||||
f"Successfully imported keys for {client.user_id} " f"from {path}"
|
f"Succesfully imported keys for {client.user_id} " f"from {path}"
|
||||||
)
|
)
|
||||||
logger.info(info_msg)
|
logger.info(info_msg)
|
||||||
await self.send_response(
|
await self.send_response(
|
||||||
@ -420,9 +407,7 @@ class ProxyDaemon:
|
|||||||
access_token = request.query.get("access_token", "")
|
access_token = request.query.get("access_token", "")
|
||||||
|
|
||||||
if not access_token:
|
if not access_token:
|
||||||
access_token = request.headers.get("Authorization", "").replace(
|
access_token = request.headers.get("Authorization", "").strip("Bearer ")
|
||||||
"Bearer ", "", 1
|
|
||||||
)
|
|
||||||
|
|
||||||
return access_token
|
return access_token
|
||||||
|
|
||||||
@ -464,7 +449,6 @@ class ProxyDaemon:
|
|||||||
data=None, # type: bytes
|
data=None, # type: bytes
|
||||||
session=None, # type: aiohttp.ClientSession
|
session=None, # type: aiohttp.ClientSession
|
||||||
token=None, # type: str
|
token=None, # type: str
|
||||||
use_raw_path=True, # type: bool
|
|
||||||
):
|
):
|
||||||
# type: (...) -> aiohttp.ClientResponse
|
# type: (...) -> aiohttp.ClientResponse
|
||||||
"""Forward the given request to our configured homeserver.
|
"""Forward the given request to our configured homeserver.
|
||||||
@ -479,10 +463,6 @@ class ProxyDaemon:
|
|||||||
should be used to forward the request.
|
should be used to forward the request.
|
||||||
token (str, optional): The access token that should be used for the
|
token (str, optional): The access token that should be used for the
|
||||||
request.
|
request.
|
||||||
use_raw_path (str, optional): Should the raw path be used from the
|
|
||||||
request or should we use the path and re-encode it. Some may need
|
|
||||||
their filters to be sanitized, this requires the parsed version of
|
|
||||||
the path, otherwise we leave the path as is.
|
|
||||||
"""
|
"""
|
||||||
if not session:
|
if not session:
|
||||||
if not self.default_session:
|
if not self.default_session:
|
||||||
@ -491,7 +471,9 @@ class ProxyDaemon:
|
|||||||
|
|
||||||
assert session
|
assert session
|
||||||
|
|
||||||
path = request.raw_path if use_raw_path else urllib.parse.quote(request.path)
|
path = urllib.parse.quote(
|
||||||
|
request.path
|
||||||
|
) # re-encode path stuff like room aliases
|
||||||
method = request.method
|
method = request.method
|
||||||
|
|
||||||
headers = CIMultiDict(request.headers)
|
headers = CIMultiDict(request.headers)
|
||||||
@ -571,9 +553,7 @@ class ProxyDaemon:
|
|||||||
|
|
||||||
return user
|
return user
|
||||||
|
|
||||||
async def start_pan_client(
|
async def start_pan_client(self, access_token, user, user_id, password):
|
||||||
self, access_token, user, user_id, password, device_id=None
|
|
||||||
):
|
|
||||||
client = ClientInfo(user_id, access_token)
|
client = ClientInfo(user_id, access_token)
|
||||||
self.client_info[access_token] = client
|
self.client_info[access_token] = client
|
||||||
self.store.save_server_user(self.name, user_id)
|
self.store.save_server_user(self.name, user_id)
|
||||||
@ -598,27 +578,13 @@ class ProxyDaemon:
|
|||||||
store_class=self.client_store_class,
|
store_class=self.client_store_class,
|
||||||
media_info=self.media_info,
|
media_info=self.media_info,
|
||||||
)
|
)
|
||||||
|
response = await pan_client.login(password, "pantalaimon")
|
||||||
|
|
||||||
if password == "":
|
if not isinstance(response, LoginResponse):
|
||||||
if device_id is None:
|
await pan_client.close()
|
||||||
logger.warn(
|
return
|
||||||
"Empty password provided and device_id was also None, not "
|
|
||||||
"starting background sync client "
|
|
||||||
)
|
|
||||||
return
|
|
||||||
# If password is blank, we cannot login normally and must
|
|
||||||
# fall back to using the provided device_id.
|
|
||||||
pan_client.restore_login(user_id, device_id, access_token)
|
|
||||||
else:
|
|
||||||
response = await pan_client.login(password, "pantalaimon")
|
|
||||||
|
|
||||||
if not isinstance(response, LoginResponse):
|
logger.info(f"Succesfully started new background sync client for " f"{user_id}")
|
||||||
await pan_client.close()
|
|
||||||
return
|
|
||||||
|
|
||||||
logger.info(
|
|
||||||
f"Successfully started new background sync client for " f"{user_id}"
|
|
||||||
)
|
|
||||||
|
|
||||||
await self.send_ui_message(
|
await self.send_ui_message(
|
||||||
UpdateUsersMessage(self.name, user_id, pan_client.device_id)
|
UpdateUsersMessage(self.name, user_id, pan_client.device_id)
|
||||||
@ -680,16 +646,13 @@ class ProxyDaemon:
|
|||||||
if response.status == 200 and json_response:
|
if response.status == 200 and json_response:
|
||||||
user_id = json_response.get("user_id", None)
|
user_id = json_response.get("user_id", None)
|
||||||
access_token = json_response.get("access_token", None)
|
access_token = json_response.get("access_token", None)
|
||||||
device_id = json_response.get("device_id", None)
|
|
||||||
|
|
||||||
if user_id and access_token:
|
if user_id and access_token:
|
||||||
logger.info(
|
logger.info(
|
||||||
f"User: {user} successfully logged in, starting "
|
f"User: {user} succesfully logged in, starting "
|
||||||
f"a background sync client."
|
f"a background sync client."
|
||||||
)
|
)
|
||||||
await self.start_pan_client(
|
await self.start_pan_client(access_token, user, user_id, password)
|
||||||
access_token, user, user_id, password, device_id
|
|
||||||
)
|
|
||||||
|
|
||||||
return web.Response(
|
return web.Response(
|
||||||
status=response.status,
|
status=response.status,
|
||||||
@ -735,7 +698,7 @@ class ProxyDaemon:
|
|||||||
return decryption_method(body, ignore_failures=False)
|
return decryption_method(body, ignore_failures=False)
|
||||||
except EncryptionError:
|
except EncryptionError:
|
||||||
logger.info("Error decrypting sync, waiting for next pan " "sync")
|
logger.info("Error decrypting sync, waiting for next pan " "sync")
|
||||||
(await client.synced.wait(),)
|
await client.synced.wait(),
|
||||||
logger.info("Pan synced, retrying decryption.")
|
logger.info("Pan synced, retrying decryption.")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -772,7 +735,7 @@ class ProxyDaemon:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
response = await self.forward_request(
|
response = await self.forward_request(
|
||||||
request, params=query, token=client.access_token, use_raw_path=False
|
request, params=query, token=client.access_token
|
||||||
)
|
)
|
||||||
except ClientConnectionError as e:
|
except ClientConnectionError as e:
|
||||||
return web.Response(status=500, text=str(e))
|
return web.Response(status=500, text=str(e))
|
||||||
@ -795,27 +758,6 @@ class ProxyDaemon:
|
|||||||
body=await response.read(),
|
body=await response.read(),
|
||||||
)
|
)
|
||||||
|
|
||||||
async def createRoom(self, request):
|
|
||||||
try:
|
|
||||||
content = await request.json()
|
|
||||||
except (JSONDecodeError, ContentTypeError):
|
|
||||||
return self._not_json
|
|
||||||
|
|
||||||
invite = content.get("invite", ())
|
|
||||||
if invite:
|
|
||||||
access_token = self.get_access_token(request)
|
|
||||||
|
|
||||||
if not access_token:
|
|
||||||
return self._missing_token
|
|
||||||
|
|
||||||
client = await self._find_client(access_token)
|
|
||||||
if not client:
|
|
||||||
return self._unknown_token
|
|
||||||
|
|
||||||
client.users_for_key_query.update(invite)
|
|
||||||
|
|
||||||
return await self.forward_to_web(request)
|
|
||||||
|
|
||||||
async def messages(self, request):
|
async def messages(self, request):
|
||||||
access_token = self.get_access_token(request)
|
access_token = self.get_access_token(request)
|
||||||
|
|
||||||
@ -841,9 +783,7 @@ class ProxyDaemon:
|
|||||||
query["filter"] = request_filter
|
query["filter"] = request_filter
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response = await self.forward_request(
|
response = await self.forward_request(request, params=query)
|
||||||
request, params=query, use_raw_path=False
|
|
||||||
)
|
|
||||||
except ClientConnectionError as e:
|
except ClientConnectionError as e:
|
||||||
return web.Response(status=500, text=str(e))
|
return web.Response(status=500, text=str(e))
|
||||||
|
|
||||||
@ -867,56 +807,6 @@ class ProxyDaemon:
|
|||||||
body=await response.read(),
|
body=await response.read(),
|
||||||
)
|
)
|
||||||
|
|
||||||
def _get_upload_and_media_info(self, content_uri: str):
|
|
||||||
try:
|
|
||||||
upload_info = self.upload_info[content_uri]
|
|
||||||
except KeyError:
|
|
||||||
upload_info = self.store.load_upload(self.name, content_uri)
|
|
||||||
if not upload_info:
|
|
||||||
return None, None
|
|
||||||
|
|
||||||
self.upload_info[content_uri] = upload_info
|
|
||||||
|
|
||||||
mxc = urlparse(content_uri)
|
|
||||||
mxc_server = mxc.netloc.strip("/")
|
|
||||||
mxc_path = mxc.path.strip("/")
|
|
||||||
|
|
||||||
media_info = self.store.load_media(self.name, mxc_server, mxc_path)
|
|
||||||
if not media_info:
|
|
||||||
return None, None
|
|
||||||
|
|
||||||
self.media_info[(mxc_server, mxc_path)] = media_info
|
|
||||||
|
|
||||||
return upload_info, media_info
|
|
||||||
|
|
||||||
async def _decrypt_uri(self, content_uri, client):
|
|
||||||
upload_info, media_info = self._get_upload_and_media_info(content_uri)
|
|
||||||
if not upload_info or not media_info:
|
|
||||||
raise NotDecryptedAvailableError
|
|
||||||
|
|
||||||
response, decrypted_file = await self._load_decrypted_file(
|
|
||||||
media_info.mxc_server, media_info.mxc_path, upload_info.filename
|
|
||||||
)
|
|
||||||
|
|
||||||
if response is None and decrypted_file is None:
|
|
||||||
raise NotDecryptedAvailableError
|
|
||||||
|
|
||||||
if not isinstance(response, DownloadResponse):
|
|
||||||
raise NotDecryptedAvailableError
|
|
||||||
|
|
||||||
decrypted_upload, _ = await client.upload(
|
|
||||||
data_provider=BufferedReader(BytesIO(decrypted_file)),
|
|
||||||
content_type=upload_info.mimetype,
|
|
||||||
filename=upload_info.filename,
|
|
||||||
encrypt=False,
|
|
||||||
filesize=len(decrypted_file),
|
|
||||||
)
|
|
||||||
|
|
||||||
if not isinstance(decrypted_upload, UploadResponse):
|
|
||||||
raise NotDecryptedAvailableError
|
|
||||||
|
|
||||||
return decrypted_upload.content_uri
|
|
||||||
|
|
||||||
async def send_message(self, request):
|
async def send_message(self, request):
|
||||||
access_token = self.get_access_token(request)
|
access_token = self.get_access_token(request)
|
||||||
|
|
||||||
@ -929,36 +819,12 @@ class ProxyDaemon:
|
|||||||
|
|
||||||
room_id = request.match_info["room_id"]
|
room_id = request.match_info["room_id"]
|
||||||
|
|
||||||
|
# The room is not in the joined rooms list, just forward it.
|
||||||
try:
|
try:
|
||||||
room = client.rooms[room_id]
|
room = client.rooms[room_id]
|
||||||
encrypt = room.encrypted
|
encrypt = room.encrypted
|
||||||
except KeyError:
|
except KeyError:
|
||||||
# The room is not in the joined rooms list, either the pan client
|
return await self.forward_to_web(request, token=client.access_token)
|
||||||
# didn't manage to sync the state or we're not joined, in either
|
|
||||||
# case send an error response.
|
|
||||||
if client.has_been_synced:
|
|
||||||
return web.json_response(
|
|
||||||
{
|
|
||||||
"errcode": "M_FORBIDDEN",
|
|
||||||
"error": "You do not have permission to send the event.",
|
|
||||||
},
|
|
||||||
headers=CORS_HEADERS,
|
|
||||||
status=403,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
logger.error(
|
|
||||||
"The internal Pantalaimon client did not manage "
|
|
||||||
"to sync with the server."
|
|
||||||
)
|
|
||||||
return web.json_response(
|
|
||||||
{
|
|
||||||
"errcode": "M_UNKNOWN",
|
|
||||||
"error": "The pantalaimon client did not manage to sync with "
|
|
||||||
"the server",
|
|
||||||
},
|
|
||||||
headers=CORS_HEADERS,
|
|
||||||
status=500,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Don't encrypt reactions for now - they are weird and clients
|
# Don't encrypt reactions for now - they are weird and clients
|
||||||
# need to support them like this.
|
# need to support them like this.
|
||||||
@ -966,84 +832,23 @@ class ProxyDaemon:
|
|||||||
if request.match_info["event_type"] == "m.reaction":
|
if request.match_info["event_type"] == "m.reaction":
|
||||||
encrypt = False
|
encrypt = False
|
||||||
|
|
||||||
|
# The room isn't encrypted just forward the message.
|
||||||
|
if not encrypt:
|
||||||
|
return await self.forward_to_web(request, token=client.access_token)
|
||||||
|
|
||||||
msgtype = request.match_info["event_type"]
|
msgtype = request.match_info["event_type"]
|
||||||
|
txnid = request.match_info.get("txnid", uuid4())
|
||||||
|
|
||||||
try:
|
try:
|
||||||
content = await request.json()
|
content = await request.json()
|
||||||
except (JSONDecodeError, ContentTypeError):
|
except (JSONDecodeError, ContentTypeError):
|
||||||
return self._not_json
|
return self._not_json
|
||||||
|
|
||||||
# The room isn't encrypted just forward the message.
|
|
||||||
if not encrypt:
|
|
||||||
content_msgtype = content.get("msgtype")
|
|
||||||
if (
|
|
||||||
content_msgtype in ["m.image", "m.video", "m.audio", "m.file"]
|
|
||||||
or msgtype == "m.room.avatar"
|
|
||||||
):
|
|
||||||
try:
|
|
||||||
content["url"] = await self._decrypt_uri(content["url"], client)
|
|
||||||
if (
|
|
||||||
"info" in content
|
|
||||||
and "thumbnail_url" in content["info"]
|
|
||||||
and content["info"]["thumbnail_url"] is not None
|
|
||||||
):
|
|
||||||
content["info"]["thumbnail_url"] = await self._decrypt_uri(
|
|
||||||
content["info"]["thumbnail_url"], client
|
|
||||||
)
|
|
||||||
return await self.forward_to_web(
|
|
||||||
request, data=json.dumps(content), token=client.access_token
|
|
||||||
)
|
|
||||||
except ClientConnectionError as e:
|
|
||||||
return web.Response(status=500, text=str(e))
|
|
||||||
except (KeyError, NotDecryptedAvailableError):
|
|
||||||
return await self.forward_to_web(request, token=client.access_token)
|
|
||||||
|
|
||||||
return await self.forward_to_web(request, token=client.access_token)
|
|
||||||
|
|
||||||
txnid = request.match_info.get("txnid", uuid4())
|
|
||||||
|
|
||||||
async def _send(ignore_unverified=False):
|
async def _send(ignore_unverified=False):
|
||||||
try:
|
try:
|
||||||
content_msgtype = content.get("msgtype")
|
response = await client.room_send(
|
||||||
if (
|
room_id, msgtype, content, txnid, ignore_unverified
|
||||||
content_msgtype in ["m.image", "m.video", "m.audio", "m.file"]
|
)
|
||||||
or msgtype == "m.room.avatar"
|
|
||||||
):
|
|
||||||
upload_info, media_info = self._get_upload_and_media_info(
|
|
||||||
content["url"]
|
|
||||||
)
|
|
||||||
if not upload_info or not media_info:
|
|
||||||
response = await client.room_send(
|
|
||||||
room_id, msgtype, content, txnid, ignore_unverified
|
|
||||||
)
|
|
||||||
|
|
||||||
return web.Response(
|
|
||||||
status=response.transport_response.status,
|
|
||||||
content_type=response.transport_response.content_type,
|
|
||||||
headers=CORS_HEADERS,
|
|
||||||
body=await response.transport_response.read(),
|
|
||||||
)
|
|
||||||
|
|
||||||
media_info.to_content(content, upload_info.mimetype)
|
|
||||||
if content["info"].get("thumbnail_url", False):
|
|
||||||
(
|
|
||||||
thumb_upload_info,
|
|
||||||
thumb_media_info,
|
|
||||||
) = self._get_upload_and_media_info(
|
|
||||||
content["info"]["thumbnail_url"]
|
|
||||||
)
|
|
||||||
if thumb_upload_info and thumb_media_info:
|
|
||||||
thumb_media_info.to_thumbnail(
|
|
||||||
content, thumb_upload_info.mimetype
|
|
||||||
)
|
|
||||||
|
|
||||||
response = await client.room_send(
|
|
||||||
room_id, msgtype, content, txnid, ignore_unverified
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
response = await client.room_send(
|
|
||||||
room_id, msgtype, content, txnid, ignore_unverified
|
|
||||||
)
|
|
||||||
|
|
||||||
return web.Response(
|
return web.Response(
|
||||||
status=response.transport_response.status,
|
status=response.transport_response.status,
|
||||||
@ -1056,7 +861,7 @@ class ProxyDaemon:
|
|||||||
except SendRetryError as e:
|
except SendRetryError as e:
|
||||||
return web.Response(status=503, text=str(e))
|
return web.Response(status=503, text=str(e))
|
||||||
|
|
||||||
# Acquire a semaphore here so we only send out one
|
# Aquire a semaphore here so we only send out one
|
||||||
# UnverifiedDevicesSignal
|
# UnverifiedDevicesSignal
|
||||||
sem = client.send_semaphores[room_id]
|
sem = client.send_semaphores[room_id]
|
||||||
|
|
||||||
@ -1217,60 +1022,11 @@ class ProxyDaemon:
|
|||||||
|
|
||||||
return web.json_response(result, headers=CORS_HEADERS, status=200)
|
return web.json_response(result, headers=CORS_HEADERS, status=200)
|
||||||
|
|
||||||
async def upload(self, request):
|
async def download(self, request):
|
||||||
file_name = request.query.get("filename", "")
|
server_name = request.match_info["server_name"]
|
||||||
content_type = request.headers.get("Content-Type", "application/octet-stream")
|
media_id = request.match_info["media_id"]
|
||||||
client = next(iter(self.pan_clients.values()))
|
file_name = request.match_info.get("file_name")
|
||||||
|
|
||||||
body = await request.read()
|
|
||||||
try:
|
|
||||||
response, maybe_keys = await client.upload(
|
|
||||||
data_provider=BufferedReader(BytesIO(body)),
|
|
||||||
content_type=content_type,
|
|
||||||
filename=file_name,
|
|
||||||
encrypt=True,
|
|
||||||
filesize=len(body),
|
|
||||||
)
|
|
||||||
|
|
||||||
if not isinstance(response, UploadResponse):
|
|
||||||
return web.Response(
|
|
||||||
status=response.transport_response.status,
|
|
||||||
content_type=response.transport_response.content_type,
|
|
||||||
headers=CORS_HEADERS,
|
|
||||||
body=await response.transport_response.read(),
|
|
||||||
)
|
|
||||||
|
|
||||||
self.store.save_upload(
|
|
||||||
self.name, response.content_uri, file_name, content_type
|
|
||||||
)
|
|
||||||
|
|
||||||
mxc = urlparse(response.content_uri)
|
|
||||||
mxc_server = mxc.netloc.strip("/")
|
|
||||||
mxc_path = mxc.path.strip("/")
|
|
||||||
|
|
||||||
logger.info(f"Adding media info for {mxc_server}/{mxc_path} to the store")
|
|
||||||
media_info = MediaInfo(
|
|
||||||
mxc_server,
|
|
||||||
mxc_path,
|
|
||||||
maybe_keys["key"],
|
|
||||||
maybe_keys["iv"],
|
|
||||||
maybe_keys["hashes"],
|
|
||||||
)
|
|
||||||
self.store.save_media(self.name, media_info)
|
|
||||||
|
|
||||||
return web.Response(
|
|
||||||
status=response.transport_response.status,
|
|
||||||
content_type=response.transport_response.content_type,
|
|
||||||
headers=CORS_HEADERS,
|
|
||||||
body=await response.transport_response.read(),
|
|
||||||
)
|
|
||||||
|
|
||||||
except ClientConnectionError as e:
|
|
||||||
return web.Response(status=500, text=str(e))
|
|
||||||
except SendRetryError as e:
|
|
||||||
return web.Response(status=503, text=str(e))
|
|
||||||
|
|
||||||
async def _load_decrypted_file(self, server_name, media_id, file_name):
|
|
||||||
try:
|
try:
|
||||||
media_info = self.media_info[(server_name, media_id)]
|
media_info = self.media_info[(server_name, media_id)]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
@ -1278,86 +1034,28 @@ class ProxyDaemon:
|
|||||||
|
|
||||||
if not media_info:
|
if not media_info:
|
||||||
logger.info(f"No media info found for {server_name}/{media_id}")
|
logger.info(f"No media info found for {server_name}/{media_id}")
|
||||||
return None, None
|
return await self.forward_to_web(request)
|
||||||
|
|
||||||
self.media_info[(server_name, media_id)] = media_info
|
self.media_info[(server_name, media_id)] = media_info
|
||||||
|
|
||||||
try:
|
try:
|
||||||
key = media_info.key["k"]
|
key = media_info.key["k"]
|
||||||
hash = media_info.hashes["sha256"]
|
hash = media_info.hashes["sha256"]
|
||||||
except KeyError as e:
|
except KeyError:
|
||||||
logger.warn(
|
logger.warn(
|
||||||
f"Media info for {server_name}/{media_id} doesn't contain a key or hash."
|
f"Media info for {server_name}/{media_id} doesn't contain a key or hash."
|
||||||
)
|
)
|
||||||
raise e
|
return await self.forward_to_web(request)
|
||||||
|
|
||||||
if not self.pan_clients:
|
if not self.pan_clients:
|
||||||
return None, None
|
return await self.forward_to_web(request)
|
||||||
|
|
||||||
client = next(iter(self.pan_clients.values()))
|
client = next(iter(self.pan_clients.values()))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response = await client.download(
|
response = await client.download(server_name, media_id, file_name)
|
||||||
server_name=server_name, media_id=media_id, filename=file_name
|
|
||||||
)
|
|
||||||
except ClientConnectionError as e:
|
|
||||||
raise e
|
|
||||||
|
|
||||||
if not isinstance(response, DownloadResponse):
|
|
||||||
return response, None
|
|
||||||
|
|
||||||
logger.info(f"Decrypting media {server_name}/{media_id}")
|
|
||||||
|
|
||||||
loop = asyncio.get_running_loop()
|
|
||||||
with concurrent.futures.ProcessPoolExecutor() as pool:
|
|
||||||
decrypted_file = await loop.run_in_executor(
|
|
||||||
pool, decrypt_attachment, response.body, key, hash, media_info.iv
|
|
||||||
)
|
|
||||||
|
|
||||||
return response, decrypted_file
|
|
||||||
|
|
||||||
async def profile(self, request):
|
|
||||||
access_token = self.get_access_token(request)
|
|
||||||
|
|
||||||
if not access_token:
|
|
||||||
return self._missing_token
|
|
||||||
|
|
||||||
client = await self._find_client(access_token)
|
|
||||||
if not client:
|
|
||||||
return self._unknown_token
|
|
||||||
|
|
||||||
try:
|
|
||||||
content = await request.json()
|
|
||||||
except (JSONDecodeError, ContentTypeError):
|
|
||||||
return self._not_json
|
|
||||||
|
|
||||||
try:
|
|
||||||
content["avatar_url"] = await self._decrypt_uri(
|
|
||||||
content["avatar_url"], client
|
|
||||||
)
|
|
||||||
return await self.forward_to_web(
|
|
||||||
request, data=json.dumps(content), token=client.access_token
|
|
||||||
)
|
|
||||||
except ClientConnectionError as e:
|
except ClientConnectionError as e:
|
||||||
return web.Response(status=500, text=str(e))
|
return web.Response(status=500, text=str(e))
|
||||||
except (KeyError, NotDecryptedAvailableError):
|
|
||||||
return await self.forward_to_web(request, token=client.access_token)
|
|
||||||
|
|
||||||
async def download(self, request):
|
|
||||||
server_name = request.match_info["server_name"]
|
|
||||||
media_id = request.match_info["media_id"]
|
|
||||||
file_name = request.match_info.get("file_name")
|
|
||||||
|
|
||||||
try:
|
|
||||||
response, decrypted_file = await self._load_decrypted_file(
|
|
||||||
server_name, media_id, file_name
|
|
||||||
)
|
|
||||||
|
|
||||||
if response is None and decrypted_file is None:
|
|
||||||
return await self.forward_to_web(request)
|
|
||||||
except ClientConnectionError as e:
|
|
||||||
return web.Response(status=500, text=str(e))
|
|
||||||
except KeyError:
|
|
||||||
return await self.forward_to_web(request)
|
|
||||||
|
|
||||||
if not isinstance(response, DownloadResponse):
|
if not isinstance(response, DownloadResponse):
|
||||||
return web.Response(
|
return web.Response(
|
||||||
@ -1367,6 +1065,14 @@ class ProxyDaemon:
|
|||||||
body=await response.transport_response.read(),
|
body=await response.transport_response.read(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
logger.info(f"Decrypting media {server_name}/{media_id}")
|
||||||
|
|
||||||
|
loop = asyncio.get_running_loop()
|
||||||
|
with concurrent.futures.ProcessPoolExecutor() as pool:
|
||||||
|
decrypted_file = await loop.run_in_executor(
|
||||||
|
pool, decrypt_attachment, response.body, key, hash, media_info.iv
|
||||||
|
)
|
||||||
|
|
||||||
return web.Response(
|
return web.Response(
|
||||||
status=response.transport_response.status,
|
status=response.transport_response.status,
|
||||||
content_type=response.transport_response.content_type,
|
content_type=response.transport_response.content_type,
|
||||||
|
@ -23,6 +23,7 @@ if False:
|
|||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
from functools import partial
|
from functools import partial
|
||||||
|
from typing import Any, Dict, List, Optional, Tuple
|
||||||
|
|
||||||
import attr
|
import attr
|
||||||
import tantivy
|
import tantivy
|
||||||
@ -229,6 +230,7 @@ if False:
|
|||||||
)
|
)
|
||||||
|
|
||||||
for message in query:
|
for message in query:
|
||||||
|
|
||||||
event = message.event
|
event = message.event
|
||||||
|
|
||||||
event_dict = {
|
event_dict = {
|
||||||
@ -499,5 +501,6 @@ if False:
|
|||||||
|
|
||||||
return search_result
|
return search_result
|
||||||
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
INDEXING_ENABLED = False
|
INDEXING_ENABLED = False
|
||||||
|
@ -15,6 +15,7 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import os
|
import os
|
||||||
import signal
|
import signal
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
import click
|
import click
|
||||||
import janus
|
import janus
|
||||||
@ -22,13 +23,12 @@ import keyring
|
|||||||
import logbook
|
import logbook
|
||||||
import nio
|
import nio
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
from platformdirs import user_config_dir, user_data_dir
|
from appdirs import user_config_dir, user_data_dir
|
||||||
from logbook import StderrHandler
|
from logbook import StderrHandler
|
||||||
|
|
||||||
from pantalaimon.config import PanConfig, PanConfigError, parse_log_level
|
from pantalaimon.config import PanConfig, PanConfigError, parse_log_level
|
||||||
from pantalaimon.daemon import ProxyDaemon
|
from pantalaimon.daemon import ProxyDaemon
|
||||||
from pantalaimon.log import logger
|
from pantalaimon.log import logger
|
||||||
from pantalaimon.store import KeyDroppingSqliteStore
|
|
||||||
from pantalaimon.thread_messages import DaemonResponse
|
from pantalaimon.thread_messages import DaemonResponse
|
||||||
from pantalaimon.ui import UI_ENABLED
|
from pantalaimon.ui import UI_ENABLED
|
||||||
|
|
||||||
@ -47,8 +47,6 @@ def create_dirs(data_dir, conf_dir):
|
|||||||
|
|
||||||
async def init(data_dir, server_conf, send_queue, recv_queue):
|
async def init(data_dir, server_conf, send_queue, recv_queue):
|
||||||
"""Initialize the proxy and the http server."""
|
"""Initialize the proxy and the http server."""
|
||||||
store_class = KeyDroppingSqliteStore if server_conf.drop_old_keys else None
|
|
||||||
|
|
||||||
proxy = ProxyDaemon(
|
proxy = ProxyDaemon(
|
||||||
server_conf.name,
|
server_conf.name,
|
||||||
server_conf.homeserver,
|
server_conf.homeserver,
|
||||||
@ -58,56 +56,36 @@ async def init(data_dir, server_conf, send_queue, recv_queue):
|
|||||||
recv_queue=recv_queue.async_q if recv_queue else None,
|
recv_queue=recv_queue.async_q if recv_queue else None,
|
||||||
proxy=server_conf.proxy.geturl() if server_conf.proxy else None,
|
proxy=server_conf.proxy.geturl() if server_conf.proxy else None,
|
||||||
ssl=None if server_conf.ssl is True else False,
|
ssl=None if server_conf.ssl is True else False,
|
||||||
client_store_class=store_class,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# 100 MB max POST size
|
# 100 MB max POST size
|
||||||
app = web.Application(client_max_size=1024**2 * 100)
|
app = web.Application(client_max_size=1024 ** 2 * 100)
|
||||||
|
|
||||||
app.add_routes(
|
app.add_routes(
|
||||||
[
|
[
|
||||||
web.post("/_matrix/client/r0/login", proxy.login),
|
web.post("/_matrix/client/r0/login", proxy.login),
|
||||||
web.post("/_matrix/client/v3/login", proxy.login),
|
|
||||||
web.get("/_matrix/client/r0/sync", proxy.sync),
|
web.get("/_matrix/client/r0/sync", proxy.sync),
|
||||||
web.get("/_matrix/client/v3/sync", proxy.sync),
|
|
||||||
web.post("/_matrix/client/r0/createRoom", proxy.createRoom),
|
|
||||||
web.post("/_matrix/client/v3/createRoom", proxy.createRoom),
|
|
||||||
web.get("/_matrix/client/r0/rooms/{room_id}/messages", proxy.messages),
|
web.get("/_matrix/client/r0/rooms/{room_id}/messages", proxy.messages),
|
||||||
web.get("/_matrix/client/v3/rooms/{room_id}/messages", proxy.messages),
|
|
||||||
web.put(
|
web.put(
|
||||||
r"/_matrix/client/r0/rooms/{room_id}/send/{event_type}/{txnid}",
|
r"/_matrix/client/r0/rooms/{room_id}/send/{event_type}/{txnid}",
|
||||||
proxy.send_message,
|
proxy.send_message,
|
||||||
),
|
),
|
||||||
web.put(
|
|
||||||
r"/_matrix/client/v3/rooms/{room_id}/send/{event_type}/{txnid}",
|
|
||||||
proxy.send_message,
|
|
||||||
),
|
|
||||||
web.post(
|
web.post(
|
||||||
r"/_matrix/client/r0/rooms/{room_id}/send/{event_type}",
|
r"/_matrix/client/r0/rooms/{room_id}/send/{event_type}",
|
||||||
proxy.send_message,
|
proxy.send_message,
|
||||||
),
|
),
|
||||||
web.post("/_matrix/client/r0/user/{user_id}/filter", proxy.filter),
|
web.post("/_matrix/client/r0/user/{user_id}/filter", proxy.filter),
|
||||||
web.post("/_matrix/client/v3/user/{user_id}/filter", proxy.filter),
|
|
||||||
web.post("/.well-known/matrix/client", proxy.well_known),
|
web.post("/.well-known/matrix/client", proxy.well_known),
|
||||||
web.get("/.well-known/matrix/client", proxy.well_known),
|
web.get("/.well-known/matrix/client", proxy.well_known),
|
||||||
web.post("/_matrix/client/r0/search", proxy.search),
|
web.post("/_matrix/client/r0/search", proxy.search),
|
||||||
web.post("/_matrix/client/v3/search", proxy.search),
|
|
||||||
web.options("/_matrix/client/r0/search", proxy.search_opts),
|
web.options("/_matrix/client/r0/search", proxy.search_opts),
|
||||||
web.options("/_matrix/client/v3/search", proxy.search_opts),
|
|
||||||
web.get(
|
web.get(
|
||||||
"/_matrix/media/v1/download/{server_name}/{media_id}", proxy.download
|
"/_matrix/media/v1/download/{server_name}/{media_id}", proxy.download
|
||||||
),
|
),
|
||||||
web.get(
|
|
||||||
"/_matrix/media/v3/download/{server_name}/{media_id}", proxy.download
|
|
||||||
),
|
|
||||||
web.get(
|
web.get(
|
||||||
"/_matrix/media/v1/download/{server_name}/{media_id}/{file_name}",
|
"/_matrix/media/v1/download/{server_name}/{media_id}/{file_name}",
|
||||||
proxy.download,
|
proxy.download,
|
||||||
),
|
),
|
||||||
web.get(
|
|
||||||
"/_matrix/media/v3/download/{server_name}/{media_id}/{file_name}",
|
|
||||||
proxy.download,
|
|
||||||
),
|
|
||||||
web.get(
|
web.get(
|
||||||
"/_matrix/media/r0/download/{server_name}/{media_id}", proxy.download
|
"/_matrix/media/r0/download/{server_name}/{media_id}", proxy.download
|
||||||
),
|
),
|
||||||
@ -115,22 +93,6 @@ async def init(data_dir, server_conf, send_queue, recv_queue):
|
|||||||
"/_matrix/media/r0/download/{server_name}/{media_id}/{file_name}",
|
"/_matrix/media/r0/download/{server_name}/{media_id}/{file_name}",
|
||||||
proxy.download,
|
proxy.download,
|
||||||
),
|
),
|
||||||
web.post(
|
|
||||||
r"/_matrix/media/r0/upload",
|
|
||||||
proxy.upload,
|
|
||||||
),
|
|
||||||
web.post(
|
|
||||||
r"/_matrix/media/v3/upload",
|
|
||||||
proxy.upload,
|
|
||||||
),
|
|
||||||
web.put(
|
|
||||||
r"/_matrix/client/r0/profile/{userId}/avatar_url",
|
|
||||||
proxy.profile,
|
|
||||||
),
|
|
||||||
web.put(
|
|
||||||
r"/_matrix/client/v3/profile/{userId}/avatar_url",
|
|
||||||
proxy.profile,
|
|
||||||
),
|
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
app.router.add_route("*", "/" + "{proxyPath:.*}", proxy.router)
|
app.router.add_route("*", "/" + "{proxyPath:.*}", proxy.router)
|
||||||
@ -288,7 +250,7 @@ async def daemon(context, log_level, debug_encryption, config, data_path):
|
|||||||
"connect to pantalaimon."
|
"connect to pantalaimon."
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@click.version_option(version="0.10.5", prog_name="pantalaimon")
|
@click.version_option(version="0.7.0", prog_name="pantalaimon")
|
||||||
@click.option(
|
@click.option(
|
||||||
"--log-level",
|
"--log-level",
|
||||||
type=click.Choice(["error", "warning", "info", "debug"]),
|
type=click.Choice(["error", "warning", "info", "debug"]),
|
||||||
|
@ -20,16 +20,10 @@ import sys
|
|||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from itertools import zip_longest
|
from itertools import zip_longest
|
||||||
from typing import List
|
from typing import List
|
||||||
from shlex import split
|
|
||||||
|
|
||||||
import attr
|
import attr
|
||||||
import click
|
import click
|
||||||
|
from gi.repository import GLib
|
||||||
try:
|
|
||||||
from gi.repository import GLib
|
|
||||||
except ModuleNotFoundError:
|
|
||||||
from pgi.repository import GLib
|
|
||||||
|
|
||||||
from prompt_toolkit import __version__ as ptk_version
|
from prompt_toolkit import __version__ as ptk_version
|
||||||
from prompt_toolkit import HTML, PromptSession, print_formatted_text
|
from prompt_toolkit import HTML, PromptSession, print_formatted_text
|
||||||
from prompt_toolkit.completion import Completer, Completion, PathCompleter
|
from prompt_toolkit.completion import Completer, Completion, PathCompleter
|
||||||
@ -465,7 +459,7 @@ class PanCtl:
|
|||||||
def sas_done(self, pan_user, user_id, device_id, _):
|
def sas_done(self, pan_user, user_id, device_id, _):
|
||||||
print(
|
print(
|
||||||
f"Device {device_id} of user {user_id}"
|
f"Device {device_id} of user {user_id}"
|
||||||
f" successfully verified for pan user {pan_user}."
|
f" succesfully verified for pan user {pan_user}."
|
||||||
)
|
)
|
||||||
|
|
||||||
def show_sas_invite(self, pan_user, user_id, device_id, _):
|
def show_sas_invite(self, pan_user, user_id, device_id, _):
|
||||||
@ -590,7 +584,7 @@ class PanCtl:
|
|||||||
parser = PanctlParser(self.commands)
|
parser = PanctlParser(self.commands)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
args = parser.parse_args(split(result, posix=False))
|
args = parser.parse_args(result.split())
|
||||||
except ParseError:
|
except ParseError:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@ -696,9 +690,9 @@ class PanCtl:
|
|||||||
"the pantalaimon daemon."
|
"the pantalaimon daemon."
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@click.version_option(version="0.10.5", prog_name="panctl")
|
@click.version_option(version="0.7.0", prog_name="panctl")
|
||||||
def main():
|
def main():
|
||||||
loop = asyncio.new_event_loop()
|
loop = asyncio.get_event_loop()
|
||||||
glib_loop = GLib.MainLoop()
|
glib_loop = GLib.MainLoop()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -15,15 +15,13 @@
|
|||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from typing import Any, Dict
|
from typing import List, Optional, Tuple
|
||||||
|
|
||||||
import attr
|
import attr
|
||||||
from nio.crypto import TrustState, GroupSessionStore
|
from nio.crypto import TrustState
|
||||||
from nio.store import (
|
from nio.store import (
|
||||||
Accounts,
|
Accounts,
|
||||||
MegolmInboundSessions,
|
|
||||||
DeviceKeys,
|
DeviceKeys,
|
||||||
SqliteStore,
|
|
||||||
DeviceTrustState,
|
DeviceTrustState,
|
||||||
use_database,
|
use_database,
|
||||||
use_database_atomic,
|
use_database_atomic,
|
||||||
@ -31,8 +29,8 @@ from nio.store import (
|
|||||||
from peewee import SQL, DoesNotExist, ForeignKeyField, Model, SqliteDatabase, TextField
|
from peewee import SQL, DoesNotExist, ForeignKeyField, Model, SqliteDatabase, TextField
|
||||||
from cachetools import LRUCache
|
from cachetools import LRUCache
|
||||||
|
|
||||||
|
|
||||||
MAX_LOADED_MEDIA = 10000
|
MAX_LOADED_MEDIA = 10000
|
||||||
MAX_LOADED_UPLOAD = 10000
|
|
||||||
|
|
||||||
|
|
||||||
@attr.s
|
@attr.s
|
||||||
@ -49,33 +47,6 @@ class MediaInfo:
|
|||||||
iv = attr.ib(type=str)
|
iv = attr.ib(type=str)
|
||||||
hashes = attr.ib(type=dict)
|
hashes = attr.ib(type=dict)
|
||||||
|
|
||||||
def to_content(self, content: Dict, mime_type: str) -> Dict[Any, Any]:
|
|
||||||
content["file"] = {
|
|
||||||
"v": "v2",
|
|
||||||
"key": self.key,
|
|
||||||
"iv": self.iv,
|
|
||||||
"hashes": self.hashes,
|
|
||||||
"url": content["url"],
|
|
||||||
"mimetype": mime_type,
|
|
||||||
}
|
|
||||||
|
|
||||||
def to_thumbnail(self, content: Dict, mime_type: str) -> Dict[Any, Any]:
|
|
||||||
content["info"]["thumbnail_file"] = {
|
|
||||||
"v": "v2",
|
|
||||||
"key": self.key,
|
|
||||||
"iv": self.iv,
|
|
||||||
"hashes": self.hashes,
|
|
||||||
"url": content["info"]["thumbnail_url"],
|
|
||||||
"mimetype": mime_type,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@attr.s
|
|
||||||
class UploadInfo:
|
|
||||||
content_uri = attr.ib(type=str)
|
|
||||||
filename = attr.ib(type=str)
|
|
||||||
mimetype = attr.ib(type=str)
|
|
||||||
|
|
||||||
|
|
||||||
class DictField(TextField):
|
class DictField(TextField):
|
||||||
def python_value(self, value): # pragma: no cover
|
def python_value(self, value): # pragma: no cover
|
||||||
@ -142,18 +113,6 @@ class PanMediaInfo(Model):
|
|||||||
constraints = [SQL("UNIQUE(server_id, mxc_server, mxc_path)")]
|
constraints = [SQL("UNIQUE(server_id, mxc_server, mxc_path)")]
|
||||||
|
|
||||||
|
|
||||||
class PanUploadInfo(Model):
|
|
||||||
server = ForeignKeyField(
|
|
||||||
model=Servers, column_name="server_id", backref="upload", on_delete="CASCADE"
|
|
||||||
)
|
|
||||||
content_uri = TextField()
|
|
||||||
filename = TextField()
|
|
||||||
mimetype = TextField()
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
constraints = [SQL("UNIQUE(server_id, content_uri)")]
|
|
||||||
|
|
||||||
|
|
||||||
@attr.s
|
@attr.s
|
||||||
class ClientInfo:
|
class ClientInfo:
|
||||||
user_id = attr.ib(type=str)
|
user_id = attr.ib(type=str)
|
||||||
@ -176,7 +135,6 @@ class PanStore:
|
|||||||
PanSyncTokens,
|
PanSyncTokens,
|
||||||
PanFetcherTasks,
|
PanFetcherTasks,
|
||||||
PanMediaInfo,
|
PanMediaInfo,
|
||||||
PanUploadInfo,
|
|
||||||
]
|
]
|
||||||
|
|
||||||
def __attrs_post_init__(self):
|
def __attrs_post_init__(self):
|
||||||
@ -204,43 +162,6 @@ class PanStore:
|
|||||||
except DoesNotExist:
|
except DoesNotExist:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@use_database
|
|
||||||
def save_upload(self, server, content_uri, filename, mimetype):
|
|
||||||
server = Servers.get(name=server)
|
|
||||||
|
|
||||||
PanUploadInfo.insert(
|
|
||||||
server=server,
|
|
||||||
content_uri=content_uri,
|
|
||||||
filename=filename,
|
|
||||||
mimetype=mimetype,
|
|
||||||
).on_conflict_ignore().execute()
|
|
||||||
|
|
||||||
@use_database
|
|
||||||
def load_upload(self, server, content_uri=None):
|
|
||||||
server, _ = Servers.get_or_create(name=server)
|
|
||||||
|
|
||||||
if not content_uri:
|
|
||||||
upload_cache = LRUCache(maxsize=MAX_LOADED_UPLOAD)
|
|
||||||
|
|
||||||
for i, u in enumerate(server.upload):
|
|
||||||
if i > MAX_LOADED_UPLOAD:
|
|
||||||
break
|
|
||||||
|
|
||||||
upload = UploadInfo(u.content_uri, u.filename, u.mimetype)
|
|
||||||
upload_cache[u.content_uri] = upload
|
|
||||||
|
|
||||||
return upload_cache
|
|
||||||
else:
|
|
||||||
u = PanUploadInfo.get_or_none(
|
|
||||||
PanUploadInfo.server == server,
|
|
||||||
PanUploadInfo.content_uri == content_uri,
|
|
||||||
)
|
|
||||||
|
|
||||||
if not u:
|
|
||||||
return None
|
|
||||||
|
|
||||||
return UploadInfo(u.content_uri, u.filename, u.mimetype)
|
|
||||||
|
|
||||||
@use_database
|
@use_database
|
||||||
def save_media(self, server, media):
|
def save_media(self, server, media):
|
||||||
server = Servers.get(name=server)
|
server = Servers.get(name=server)
|
||||||
@ -254,34 +175,32 @@ class PanStore:
|
|||||||
hashes=media.hashes,
|
hashes=media.hashes,
|
||||||
).on_conflict_ignore().execute()
|
).on_conflict_ignore().execute()
|
||||||
|
|
||||||
@use_database
|
|
||||||
def load_media_cache(self, server):
|
|
||||||
server, _ = Servers.get_or_create(name=server)
|
|
||||||
media_cache = LRUCache(maxsize=MAX_LOADED_MEDIA)
|
|
||||||
|
|
||||||
for i, m in enumerate(server.media):
|
|
||||||
if i > MAX_LOADED_MEDIA:
|
|
||||||
break
|
|
||||||
|
|
||||||
media = MediaInfo(m.mxc_server, m.mxc_path, m.key, m.iv, m.hashes)
|
|
||||||
media_cache[(m.mxc_server, m.mxc_path)] = media
|
|
||||||
|
|
||||||
return media_cache
|
|
||||||
|
|
||||||
@use_database
|
@use_database
|
||||||
def load_media(self, server, mxc_server=None, mxc_path=None):
|
def load_media(self, server, mxc_server=None, mxc_path=None):
|
||||||
server, _ = Servers.get_or_create(name=server)
|
server, _ = Servers.get_or_create(name=server)
|
||||||
|
|
||||||
m = PanMediaInfo.get_or_none(
|
if not mxc_path:
|
||||||
PanMediaInfo.server == server,
|
media_cache = LRUCache(maxsize=MAX_LOADED_MEDIA)
|
||||||
PanMediaInfo.mxc_server == mxc_server,
|
|
||||||
PanMediaInfo.mxc_path == mxc_path,
|
|
||||||
)
|
|
||||||
|
|
||||||
if not m:
|
for i, m in enumerate(server.media):
|
||||||
return None
|
if i > MAX_LOADED_MEDIA:
|
||||||
|
break
|
||||||
|
|
||||||
return MediaInfo(m.mxc_server, m.mxc_path, m.key, m.iv, m.hashes)
|
media = MediaInfo(m.mxc_server, m.mxc_path, m.key, m.iv, m.hashes)
|
||||||
|
media_cache[(m.mxc_server, m.mxc_path)] = media
|
||||||
|
|
||||||
|
return media_cache
|
||||||
|
else:
|
||||||
|
m = PanMediaInfo.get_or_none(
|
||||||
|
PanMediaInfo.server == server,
|
||||||
|
PanMediaInfo.mxc_server == mxc_server,
|
||||||
|
PanMediaInfo.mxc_path == mxc_path,
|
||||||
|
)
|
||||||
|
|
||||||
|
if not m:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return MediaInfo(m.mxc_server, m.mxc_path, m.key, m.iv, m.hashes)
|
||||||
|
|
||||||
@use_database_atomic
|
@use_database_atomic
|
||||||
def replace_fetcher_task(self, server, pan_user, old_task, new_task):
|
def replace_fetcher_task(self, server, pan_user, old_task, new_task):
|
||||||
@ -307,7 +226,6 @@ class PanStore:
|
|||||||
user=user, room_id=task.room_id, token=task.token
|
user=user, room_id=task.room_id, token=task.token
|
||||||
).execute()
|
).execute()
|
||||||
|
|
||||||
@use_database
|
|
||||||
def load_fetcher_tasks(self, server, pan_user):
|
def load_fetcher_tasks(self, server, pan_user):
|
||||||
server = Servers.get(name=server)
|
server = Servers.get(name=server)
|
||||||
user = ServerUsers.get(server=server, user_id=pan_user)
|
user = ServerUsers.get(server=server, user_id=pan_user)
|
||||||
@ -431,6 +349,7 @@ class PanStore:
|
|||||||
device_store = defaultdict(dict)
|
device_store = defaultdict(dict)
|
||||||
|
|
||||||
for d in account.device_keys:
|
for d in account.device_keys:
|
||||||
|
|
||||||
if d.deleted:
|
if d.deleted:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@ -453,47 +372,3 @@ class PanStore:
|
|||||||
store[account.user_id] = device_store
|
store[account.user_id] = device_store
|
||||||
|
|
||||||
return store
|
return store
|
||||||
|
|
||||||
|
|
||||||
class KeyDroppingSqliteStore(SqliteStore):
|
|
||||||
@use_database
|
|
||||||
def save_inbound_group_session(self, session):
|
|
||||||
"""Save the provided Megolm inbound group session to the database.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
session (InboundGroupSession): The session to save.
|
|
||||||
"""
|
|
||||||
account = self._get_account()
|
|
||||||
assert account
|
|
||||||
|
|
||||||
MegolmInboundSessions.delete().where(
|
|
||||||
MegolmInboundSessions.sender_key == session.sender_key,
|
|
||||||
MegolmInboundSessions.account == account,
|
|
||||||
MegolmInboundSessions.room_id == session.room_id,
|
|
||||||
).execute()
|
|
||||||
|
|
||||||
super().save_inbound_group_session(session)
|
|
||||||
|
|
||||||
@use_database
|
|
||||||
def load_inbound_group_sessions(self):
|
|
||||||
store = super().load_inbound_group_sessions()
|
|
||||||
|
|
||||||
return KeyDroppingGroupSessionStore.from_group_session_store(store)
|
|
||||||
|
|
||||||
|
|
||||||
class KeyDroppingGroupSessionStore(GroupSessionStore):
|
|
||||||
def from_group_session_store(store):
|
|
||||||
new_store = KeyDroppingGroupSessionStore()
|
|
||||||
new_store._entries = store._entries
|
|
||||||
|
|
||||||
return new_store
|
|
||||||
|
|
||||||
def add(self, session) -> bool:
|
|
||||||
room_id = session.room_id
|
|
||||||
sender_key = session.sender_key
|
|
||||||
if session in self._entries[room_id][sender_key].values():
|
|
||||||
return False
|
|
||||||
|
|
||||||
self._entries[room_id][sender_key].clear()
|
|
||||||
self._entries[room_id][sender_key][session.id] = session
|
|
||||||
return True
|
|
||||||
|
@ -30,7 +30,6 @@ if UI_ENABLED:
|
|||||||
from gi.repository import GLib
|
from gi.repository import GLib
|
||||||
from pydbus import SessionBus
|
from pydbus import SessionBus
|
||||||
from pydbus.generic import signal
|
from pydbus.generic import signal
|
||||||
from dbus.mainloop.glib import DBusGMainLoop
|
|
||||||
|
|
||||||
from nio import RoomKeyRequest, RoomKeyRequestCancellation
|
from nio import RoomKeyRequest, RoomKeyRequestCancellation
|
||||||
|
|
||||||
@ -448,7 +447,6 @@ if UI_ENABLED:
|
|||||||
config = attr.ib()
|
config = attr.ib()
|
||||||
|
|
||||||
loop = attr.ib(init=False)
|
loop = attr.ib(init=False)
|
||||||
dbus_loop = attr.ib(init=False)
|
|
||||||
store = attr.ib(init=False)
|
store = attr.ib(init=False)
|
||||||
users = attr.ib(init=False)
|
users = attr.ib(init=False)
|
||||||
devices = attr.ib(init=False)
|
devices = attr.ib(init=False)
|
||||||
@ -459,7 +457,6 @@ if UI_ENABLED:
|
|||||||
|
|
||||||
def __attrs_post_init__(self):
|
def __attrs_post_init__(self):
|
||||||
self.loop = None
|
self.loop = None
|
||||||
self.dbus_loop = None
|
|
||||||
|
|
||||||
id_counter = IdCounter()
|
id_counter = IdCounter()
|
||||||
|
|
||||||
@ -470,14 +467,14 @@ if UI_ENABLED:
|
|||||||
self.bus.publish("org.pantalaimon1", self.control_if, self.device_if)
|
self.bus.publish("org.pantalaimon1", self.control_if, self.device_if)
|
||||||
|
|
||||||
def unverified_notification(self, message):
|
def unverified_notification(self, message):
|
||||||
notification = notify2.Notification(
|
notificaton = notify2.Notification(
|
||||||
"Unverified devices.",
|
"Unverified devices.",
|
||||||
message=(
|
message=(
|
||||||
f"There are unverified devices in the room "
|
f"There are unverified devices in the room "
|
||||||
f"{message.room_display_name}."
|
f"{message.room_display_name}."
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
notification.set_category("im")
|
notificaton.set_category("im")
|
||||||
|
|
||||||
def send_cb(notification, action_key, user_data):
|
def send_cb(notification, action_key, user_data):
|
||||||
message = user_data
|
message = user_data
|
||||||
@ -488,20 +485,20 @@ if UI_ENABLED:
|
|||||||
self.control_if.CancelSending(message.pan_user, message.room_id)
|
self.control_if.CancelSending(message.pan_user, message.room_id)
|
||||||
|
|
||||||
if "actions" in notify2.get_server_caps():
|
if "actions" in notify2.get_server_caps():
|
||||||
notification.add_action("send", "Send anyways", send_cb, message)
|
notificaton.add_action("send", "Send anyways", send_cb, message)
|
||||||
notification.add_action("cancel", "Cancel sending", cancel_cb, message)
|
notificaton.add_action("cancel", "Cancel sending", cancel_cb, message)
|
||||||
|
|
||||||
notification.show()
|
notificaton.show()
|
||||||
|
|
||||||
def sas_invite_notification(self, message):
|
def sas_invite_notification(self, message):
|
||||||
notification = notify2.Notification(
|
notificaton = notify2.Notification(
|
||||||
"Key verification invite",
|
"Key verification invite",
|
||||||
message=(
|
message=(
|
||||||
f"{message.user_id} via {message.device_id} has started "
|
f"{message.user_id} via {message.device_id} has started "
|
||||||
f"a key verification process."
|
f"a key verification process."
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
notification.set_category("im")
|
notificaton.set_category("im")
|
||||||
|
|
||||||
def accept_cb(notification, action_key, user_data):
|
def accept_cb(notification, action_key, user_data):
|
||||||
message = user_data
|
message = user_data
|
||||||
@ -516,17 +513,17 @@ if UI_ENABLED:
|
|||||||
)
|
)
|
||||||
|
|
||||||
if "actions" in notify2.get_server_caps():
|
if "actions" in notify2.get_server_caps():
|
||||||
notification.add_action("accept", "Accept", accept_cb, message)
|
notificaton.add_action("accept", "Accept", accept_cb, message)
|
||||||
notification.add_action("cancel", "Cancel", cancel_cb, message)
|
notificaton.add_action("cancel", "Cancel", cancel_cb, message)
|
||||||
|
|
||||||
notification.show()
|
notificaton.show()
|
||||||
|
|
||||||
def sas_show_notification(self, message):
|
def sas_show_notification(self, message):
|
||||||
emojis = [x[0] for x in message.emoji]
|
emojis = [x[0] for x in message.emoji]
|
||||||
|
|
||||||
emoji_str = " ".join(emojis)
|
emoji_str = " ".join(emojis)
|
||||||
|
|
||||||
notification = notify2.Notification(
|
notificaton = notify2.Notification(
|
||||||
"Short authentication string",
|
"Short authentication string",
|
||||||
message=(
|
message=(
|
||||||
f"Short authentication string for the key verification of"
|
f"Short authentication string for the key verification of"
|
||||||
@ -534,7 +531,7 @@ if UI_ENABLED:
|
|||||||
f"{emoji_str}"
|
f"{emoji_str}"
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
notification.set_category("im")
|
notificaton.set_category("im")
|
||||||
|
|
||||||
def confirm_cb(notification, action_key, user_data):
|
def confirm_cb(notification, action_key, user_data):
|
||||||
message = user_data
|
message = user_data
|
||||||
@ -549,21 +546,21 @@ if UI_ENABLED:
|
|||||||
)
|
)
|
||||||
|
|
||||||
if "actions" in notify2.get_server_caps():
|
if "actions" in notify2.get_server_caps():
|
||||||
notification.add_action("confirm", "Confirm", confirm_cb, message)
|
notificaton.add_action("confirm", "Confirm", confirm_cb, message)
|
||||||
notification.add_action("cancel", "Cancel", cancel_cb, message)
|
notificaton.add_action("cancel", "Cancel", cancel_cb, message)
|
||||||
|
|
||||||
notification.show()
|
notificaton.show()
|
||||||
|
|
||||||
def sas_done_notification(self, message):
|
def sas_done_notification(self, message):
|
||||||
notification = notify2.Notification(
|
notificaton = notify2.Notification(
|
||||||
"Device successfully verified.",
|
"Device successfully verified.",
|
||||||
message=(
|
message=(
|
||||||
f"Device {message.device_id} of user {message.user_id} "
|
f"Device {message.device_id} of user {message.user_id} "
|
||||||
f"successfully verified."
|
f"successfully verified."
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
notification.set_category("im")
|
notificaton.set_category("im")
|
||||||
notification.show()
|
notificaton.show()
|
||||||
|
|
||||||
def message_callback(self):
|
def message_callback(self):
|
||||||
try:
|
try:
|
||||||
@ -635,12 +632,11 @@ if UI_ENABLED:
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
self.dbus_loop = DBusGMainLoop()
|
|
||||||
self.loop = GLib.MainLoop()
|
self.loop = GLib.MainLoop()
|
||||||
|
|
||||||
if self.config.notifications:
|
if self.config.notifications:
|
||||||
try:
|
try:
|
||||||
notify2.init("pantalaimon", mainloop=self.dbus_loop)
|
notify2.init("pantalaimon", mainloop=self.loop)
|
||||||
self.notifications = True
|
self.notifications = True
|
||||||
except dbus.DBusException:
|
except dbus.DBusException:
|
||||||
logger.error(
|
logger.error(
|
||||||
@ -650,7 +646,6 @@ if UI_ENABLED:
|
|||||||
self.notifications = False
|
self.notifications = False
|
||||||
|
|
||||||
GLib.timeout_add(100, self.message_callback)
|
GLib.timeout_add(100, self.message_callback)
|
||||||
|
|
||||||
if not self.loop:
|
if not self.loop:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
29
setup.py
29
setup.py
@ -7,11 +7,12 @@ with open("README.md", encoding="utf-8") as f:
|
|||||||
|
|
||||||
setup(
|
setup(
|
||||||
name="pantalaimon",
|
name="pantalaimon",
|
||||||
version="0.10.5",
|
version="0.7.0",
|
||||||
url="https://github.com/matrix-org/pantalaimon",
|
url="https://github.com/matrix-org/pantalaimon",
|
||||||
author="The Matrix.org Team",
|
author="The Matrix.org Team",
|
||||||
author_email="poljar@termina.org.uk",
|
author_email="poljar@termina.org.uk",
|
||||||
description=("A Matrix proxy daemon that adds E2E encryption " "capabilities."),
|
description=("A Matrix proxy daemon that adds E2E encryption "
|
||||||
|
"capabilities."),
|
||||||
long_description=long_description,
|
long_description=long_description,
|
||||||
long_description_content_type="text/markdown",
|
long_description_content_type="text/markdown",
|
||||||
license="Apache License, Version 2.0",
|
license="Apache License, Version 2.0",
|
||||||
@ -19,30 +20,28 @@ setup(
|
|||||||
install_requires=[
|
install_requires=[
|
||||||
"attrs >= 19.3.0",
|
"attrs >= 19.3.0",
|
||||||
"aiohttp >= 3.6, < 4.0",
|
"aiohttp >= 3.6, < 4.0",
|
||||||
"platformdirs >= 4.3.6",
|
"appdirs >= 1.4.4",
|
||||||
"click >= 7.1.2",
|
"click >= 7.1.2",
|
||||||
"keyring >= 21.2.1",
|
"keyring >= 21.2.1",
|
||||||
"logbook >= 1.5.3",
|
"logbook >= 1.5.3",
|
||||||
"peewee >= 3.13.1",
|
"peewee >= 3.13.1",
|
||||||
"janus >= 0.5",
|
"janus >= 0.5",
|
||||||
"cachetools >= 3.0.0",
|
"cachetools >= 3.0.0"
|
||||||
"prompt_toolkit > 2, < 4",
|
"prompt_toolkit>2<4",
|
||||||
"typing;python_version<'3.5'",
|
"typing;python_version<'3.5'",
|
||||||
"matrix-nio[e2e] >= 0.24, < 0.25.2",
|
"matrix-nio[e2e] >= 0.14, < 0.16"
|
||||||
],
|
],
|
||||||
extras_require={
|
extras_require={
|
||||||
"ui": [
|
"ui": [
|
||||||
"dbus-python >= 1.2, < 1.3",
|
"dbus-python <= 1.2",
|
||||||
"PyGObject >= 3.46, < 3.50",
|
"PyGObject <= 3.36",
|
||||||
"pydbus >= 0.6, < 0.7",
|
"pydbus <= 0.6",
|
||||||
"notify2 >= 0.3, < 0.4",
|
"notify2 <= 0.3",
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
entry_points={
|
entry_points={
|
||||||
"console_scripts": [
|
"console_scripts": ["pantalaimon=pantalaimon.main:main",
|
||||||
"pantalaimon=pantalaimon.main:main",
|
"panctl=pantalaimon.panctl:main"],
|
||||||
"panctl=pantalaimon.panctl:main",
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
zip_safe=False,
|
zip_safe=False
|
||||||
)
|
)
|
||||||
|
@ -34,9 +34,11 @@ class Provider(BaseProvider):
|
|||||||
def client(self):
|
def client(self):
|
||||||
return ClientInfo(faker.mx_id(), faker.access_token())
|
return ClientInfo(faker.mx_id(), faker.access_token())
|
||||||
|
|
||||||
|
|
||||||
def avatar_url(self):
|
def avatar_url(self):
|
||||||
return "mxc://{}/{}#auto".format(
|
return "mxc://{}/{}#auto".format(
|
||||||
faker.hostname(), "".join(choices(ascii_letters) for i in range(24))
|
faker.hostname(),
|
||||||
|
"".join(choices(ascii_letters) for i in range(24))
|
||||||
)
|
)
|
||||||
|
|
||||||
def olm_key_pair(self):
|
def olm_key_pair(self):
|
||||||
@ -54,6 +56,7 @@ class Provider(BaseProvider):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
faker.add_provider(Provider)
|
faker.add_provider(Provider)
|
||||||
|
|
||||||
|
|
||||||
@ -77,7 +80,13 @@ def tempdir():
|
|||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def panstore(tempdir):
|
def panstore(tempdir):
|
||||||
for _ in range(10):
|
for _ in range(10):
|
||||||
store = SqliteStore(faker.mx_id(), faker.device_id(), tempdir, "", "pan.db")
|
store = SqliteStore(
|
||||||
|
faker.mx_id(),
|
||||||
|
faker.device_id(),
|
||||||
|
tempdir,
|
||||||
|
"",
|
||||||
|
"pan.db"
|
||||||
|
)
|
||||||
account = OlmAccount()
|
account = OlmAccount()
|
||||||
store.save_account(account)
|
store.save_account(account)
|
||||||
|
|
||||||
@ -121,23 +130,21 @@ async def pan_proxy_server(tempdir, aiohttp_server):
|
|||||||
recv_queue=ui_queue.async_q,
|
recv_queue=ui_queue.async_q,
|
||||||
proxy=None,
|
proxy=None,
|
||||||
ssl=False,
|
ssl=False,
|
||||||
client_store_class=SqliteStore,
|
client_store_class=SqliteStore
|
||||||
)
|
)
|
||||||
|
|
||||||
app.add_routes(
|
app.add_routes([
|
||||||
[
|
web.post("/_matrix/client/r0/login", proxy.login),
|
||||||
web.post("/_matrix/client/r0/login", proxy.login),
|
web.get("/_matrix/client/r0/sync", proxy.sync),
|
||||||
web.get("/_matrix/client/r0/sync", proxy.sync),
|
web.get("/_matrix/client/r0/rooms/{room_id}/messages", proxy.messages),
|
||||||
web.get("/_matrix/client/r0/rooms/{room_id}/messages", proxy.messages),
|
web.put(
|
||||||
web.put(
|
r"/_matrix/client/r0/rooms/{room_id}/send/{event_type}/{txnid}",
|
||||||
r"/_matrix/client/r0/rooms/{room_id}/send/{event_type}/{txnid}",
|
proxy.send_message
|
||||||
proxy.send_message,
|
),
|
||||||
),
|
web.post("/_matrix/client/r0/user/{user_id}/filter", proxy.filter),
|
||||||
web.post("/_matrix/client/r0/user/{user_id}/filter", proxy.filter),
|
web.post("/_matrix/client/r0/search", proxy.search),
|
||||||
web.post("/_matrix/client/r0/search", proxy.search),
|
web.options("/_matrix/client/r0/search", proxy.search_opts),
|
||||||
web.options("/_matrix/client/r0/search", proxy.search_opts),
|
])
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
server = await aiohttp_server(app)
|
server = await aiohttp_server(app)
|
||||||
|
|
||||||
@ -154,7 +161,7 @@ async def running_proxy(pan_proxy_server, aioresponse, aiohttp_client):
|
|||||||
"access_token": "abc123",
|
"access_token": "abc123",
|
||||||
"device_id": "GHTYAJCE",
|
"device_id": "GHTYAJCE",
|
||||||
"home_server": "example.org",
|
"home_server": "example.org",
|
||||||
"user_id": "@example:example.org",
|
"user_id": "@example:example.org"
|
||||||
}
|
}
|
||||||
|
|
||||||
aioclient = await aiohttp_client(server)
|
aioclient = await aiohttp_client(server)
|
||||||
@ -163,7 +170,7 @@ async def running_proxy(pan_proxy_server, aioresponse, aiohttp_client):
|
|||||||
"https://example.org/_matrix/client/r0/login",
|
"https://example.org/_matrix/client/r0/login",
|
||||||
status=200,
|
status=200,
|
||||||
payload=login_response,
|
payload=login_response,
|
||||||
repeat=True,
|
repeat=True
|
||||||
)
|
)
|
||||||
|
|
||||||
await aioclient.post(
|
await aioclient.post(
|
||||||
@ -172,7 +179,7 @@ async def running_proxy(pan_proxy_server, aioresponse, aiohttp_client):
|
|||||||
"type": "m.login.password",
|
"type": "m.login.password",
|
||||||
"user": "example",
|
"user": "example",
|
||||||
"password": "wordpass",
|
"password": "wordpass",
|
||||||
},
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
yield server, aioclient, proxy, queues
|
yield server, aioclient, proxy, queues
|
||||||
|
@ -25,10 +25,10 @@ ALICE_ID = "@alice:example.org"
|
|||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
async def client(tmpdir):
|
async def client(tmpdir, loop):
|
||||||
store = PanStore(tmpdir)
|
store = PanStore(tmpdir)
|
||||||
queue = janus.Queue()
|
queue = janus.Queue()
|
||||||
conf = ServerConfig("example", "https://example.org")
|
conf = ServerConfig("example", "https://exapmle.org")
|
||||||
conf.history_fetch_delay = 0.1
|
conf.history_fetch_delay = 0.1
|
||||||
|
|
||||||
store.save_server_user("example", "@example:example.org")
|
store.save_server_user("example", "@example:example.org")
|
||||||
@ -371,7 +371,7 @@ class TestClass(object):
|
|||||||
|
|
||||||
await client.loop_stop()
|
await client.loop_stop()
|
||||||
|
|
||||||
async def test_history_fetching_tasks(self, client, aioresponse):
|
async def test_history_fetching_tasks(self, client, aioresponse, loop):
|
||||||
if not INDEXING_ENABLED:
|
if not INDEXING_ENABLED:
|
||||||
pytest.skip("Indexing needs to be enabled to test this")
|
pytest.skip("Indexing needs to be enabled to test this")
|
||||||
|
|
||||||
@ -380,9 +380,7 @@ class TestClass(object):
|
|||||||
)
|
)
|
||||||
|
|
||||||
aioresponse.get(
|
aioresponse.get(
|
||||||
sync_url,
|
sync_url, status=200, payload=self.initial_sync_response,
|
||||||
status=200,
|
|
||||||
payload=self.initial_sync_response,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
aioresponse.get(sync_url, status=200, payload=self.empty_sync, repeat=True)
|
aioresponse.get(sync_url, status=200, payload=self.empty_sync, repeat=True)
|
||||||
@ -423,7 +421,7 @@ class TestClass(object):
|
|||||||
tasks = client.pan_store.load_fetcher_tasks(client.server_name, client.user_id)
|
tasks = client.pan_store.load_fetcher_tasks(client.server_name, client.user_id)
|
||||||
assert len(tasks) == 1
|
assert len(tasks) == 1
|
||||||
|
|
||||||
# Check that the task is our prev_batch from the sync response
|
# Check that the task is our prev_batch from the sync resposne
|
||||||
assert tasks[0].room_id == TEST_ROOM_ID
|
assert tasks[0].room_id == TEST_ROOM_ID
|
||||||
assert tasks[0].token == "t392-516_47314_0_7_1_1_1_11444_1"
|
assert tasks[0].token == "t392-516_47314_0_7_1_1_1_11444_1"
|
||||||
|
|
||||||
@ -433,7 +431,7 @@ class TestClass(object):
|
|||||||
tasks = client.pan_store.load_fetcher_tasks(client.server_name, client.user_id)
|
tasks = client.pan_store.load_fetcher_tasks(client.server_name, client.user_id)
|
||||||
assert len(tasks) == 1
|
assert len(tasks) == 1
|
||||||
|
|
||||||
# Check that the task is our end token from the messages response
|
# Check that the task is our end token from the messages resposne
|
||||||
assert tasks[0].room_id == TEST_ROOM_ID
|
assert tasks[0].room_id == TEST_ROOM_ID
|
||||||
assert tasks[0].token == "t47409-4357353_219380_26003_2265"
|
assert tasks[0].token == "t47409-4357353_219380_26003_2265"
|
||||||
|
|
||||||
@ -447,7 +445,7 @@ class TestClass(object):
|
|||||||
|
|
||||||
await client.loop_stop()
|
await client.loop_stop()
|
||||||
|
|
||||||
async def test_history_fetching_resume(self, client, aioresponse):
|
async def test_history_fetching_resume(self, client, aioresponse, loop):
|
||||||
if not INDEXING_ENABLED:
|
if not INDEXING_ENABLED:
|
||||||
pytest.skip("Indexing needs to be enabled to test this")
|
pytest.skip("Indexing needs to be enabled to test this")
|
||||||
|
|
||||||
@ -456,9 +454,7 @@ class TestClass(object):
|
|||||||
)
|
)
|
||||||
|
|
||||||
aioresponse.get(
|
aioresponse.get(
|
||||||
sync_url,
|
sync_url, status=200, payload=self.initial_sync_response,
|
||||||
status=200,
|
|
||||||
payload=self.initial_sync_response,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
aioresponse.get(sync_url, status=200, payload=self.empty_sync, repeat=True)
|
aioresponse.get(sync_url, status=200, payload=self.empty_sync, repeat=True)
|
||||||
@ -523,7 +519,7 @@ class TestClass(object):
|
|||||||
)
|
)
|
||||||
assert len(tasks) == 1
|
assert len(tasks) == 1
|
||||||
|
|
||||||
# Check that the task is our end token from the messages response
|
# Check that the task is our end token from the messages resposne
|
||||||
assert tasks[0].room_id == TEST_ROOM_ID
|
assert tasks[0].room_id == TEST_ROOM_ID
|
||||||
assert tasks[0].token == "t47409-4357353_219380_26003_2265"
|
assert tasks[0].token == "t47409-4357353_219380_26003_2265"
|
||||||
|
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
|
import asyncio
|
||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
|
||||||
|
from aiohttp import web
|
||||||
from nio.crypto import OlmDevice
|
from nio.crypto import OlmDevice
|
||||||
|
|
||||||
from conftest import faker
|
from conftest import faker
|
||||||
@ -25,7 +27,7 @@ class TestClass(object):
|
|||||||
"access_token": "abc123",
|
"access_token": "abc123",
|
||||||
"device_id": "GHTYAJCE",
|
"device_id": "GHTYAJCE",
|
||||||
"home_server": "example.org",
|
"home_server": "example.org",
|
||||||
"user_id": "@example:example.org",
|
"user_id": "@example:example.org"
|
||||||
}
|
}
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -34,7 +36,12 @@ class TestClass(object):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def keys_upload_response(self):
|
def keys_upload_response(self):
|
||||||
return {"one_time_key_counts": {"curve25519": 10, "signed_curve25519": 20}}
|
return {
|
||||||
|
"one_time_key_counts": {
|
||||||
|
"curve25519": 10,
|
||||||
|
"signed_curve25519": 20
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def example_devices(self):
|
def example_devices(self):
|
||||||
@ -45,7 +52,10 @@ class TestClass(object):
|
|||||||
devices[device.user_id][device.id] = device
|
devices[device.user_id][device.id] = device
|
||||||
|
|
||||||
bob_device = OlmDevice(
|
bob_device = OlmDevice(
|
||||||
BOB_ID, BOB_DEVICE, {"ed25519": BOB_ONETIME, "curve25519": BOB_CURVE}
|
BOB_ID,
|
||||||
|
BOB_DEVICE,
|
||||||
|
{"ed25519": BOB_ONETIME,
|
||||||
|
"curve25519": BOB_CURVE}
|
||||||
)
|
)
|
||||||
|
|
||||||
devices[BOB_ID][BOB_DEVICE] = bob_device
|
devices[BOB_ID][BOB_DEVICE] = bob_device
|
||||||
@ -61,7 +71,7 @@ class TestClass(object):
|
|||||||
"https://example.org/_matrix/client/r0/login",
|
"https://example.org/_matrix/client/r0/login",
|
||||||
status=200,
|
status=200,
|
||||||
payload=self.login_response,
|
payload=self.login_response,
|
||||||
repeat=True,
|
repeat=True
|
||||||
)
|
)
|
||||||
|
|
||||||
assert not daemon.pan_clients
|
assert not daemon.pan_clients
|
||||||
@ -72,7 +82,7 @@ class TestClass(object):
|
|||||||
"type": "m.login.password",
|
"type": "m.login.password",
|
||||||
"user": "example",
|
"user": "example",
|
||||||
"password": "wordpass",
|
"password": "wordpass",
|
||||||
},
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
assert resp.status == 200
|
assert resp.status == 200
|
||||||
@ -95,11 +105,11 @@ class TestClass(object):
|
|||||||
"https://example.org/_matrix/client/r0/login",
|
"https://example.org/_matrix/client/r0/login",
|
||||||
status=200,
|
status=200,
|
||||||
payload=self.login_response,
|
payload=self.login_response,
|
||||||
repeat=True,
|
repeat=True
|
||||||
)
|
)
|
||||||
|
|
||||||
sync_url = re.compile(
|
sync_url = re.compile(
|
||||||
r"^https://example\.org/_matrix/client/r0/sync\?access_token=.*"
|
r'^https://example\.org/_matrix/client/r0/sync\?access_token=.*'
|
||||||
)
|
)
|
||||||
|
|
||||||
aioresponse.get(
|
aioresponse.get(
|
||||||
@ -114,16 +124,14 @@ class TestClass(object):
|
|||||||
"type": "m.login.password",
|
"type": "m.login.password",
|
||||||
"user": "example",
|
"user": "example",
|
||||||
"password": "wordpass",
|
"password": "wordpass",
|
||||||
},
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
# Check that the pan client started to sync after logging in.
|
# Check that the pan client started to sync after logging in.
|
||||||
pan_client = list(daemon.pan_clients.values())[0]
|
pan_client = list(daemon.pan_clients.values())[0]
|
||||||
assert len(pan_client.rooms) == 1
|
assert len(pan_client.rooms) == 1
|
||||||
|
|
||||||
async def test_pan_client_keys_upload(
|
async def test_pan_client_keys_upload(self, pan_proxy_server, aiohttp_client, aioresponse):
|
||||||
self, pan_proxy_server, aiohttp_client, aioresponse
|
|
||||||
):
|
|
||||||
server, daemon, _ = pan_proxy_server
|
server, daemon, _ = pan_proxy_server
|
||||||
|
|
||||||
client = await aiohttp_client(server)
|
client = await aiohttp_client(server)
|
||||||
@ -132,11 +140,11 @@ class TestClass(object):
|
|||||||
"https://example.org/_matrix/client/r0/login",
|
"https://example.org/_matrix/client/r0/login",
|
||||||
status=200,
|
status=200,
|
||||||
payload=self.login_response,
|
payload=self.login_response,
|
||||||
repeat=True,
|
repeat=True
|
||||||
)
|
)
|
||||||
|
|
||||||
sync_url = re.compile(
|
sync_url = re.compile(
|
||||||
r"^https://example\.org/_matrix/client/r0/sync\?access_token=.*"
|
r'^https://example\.org/_matrix/client/r0/sync\?access_token=.*'
|
||||||
)
|
)
|
||||||
|
|
||||||
aioresponse.get(
|
aioresponse.get(
|
||||||
@ -161,7 +169,7 @@ class TestClass(object):
|
|||||||
"type": "m.login.password",
|
"type": "m.login.password",
|
||||||
"user": "example",
|
"user": "example",
|
||||||
"password": "wordpass",
|
"password": "wordpass",
|
||||||
},
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
pan_client = list(daemon.pan_clients.values())[0]
|
pan_client = list(daemon.pan_clients.values())[0]
|
||||||
|
@ -1,12 +1,14 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
|
import pdb
|
||||||
import pprint
|
import pprint
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from nio import RoomMessage, RoomEncryptedMedia
|
from nio import RoomMessage, RoomEncryptedMedia
|
||||||
|
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
from conftest import faker
|
||||||
from pantalaimon.index import INDEXING_ENABLED
|
from pantalaimon.index import INDEXING_ENABLED
|
||||||
from pantalaimon.store import FetchTask, MediaInfo, UploadInfo
|
from pantalaimon.store import FetchTask, MediaInfo
|
||||||
|
|
||||||
TEST_ROOM = "!SVkFJHzfwvuaIEawgC:localhost"
|
TEST_ROOM = "!SVkFJHzfwvuaIEawgC:localhost"
|
||||||
TEST_ROOM2 = "!testroom:localhost"
|
TEST_ROOM2 = "!testroom:localhost"
|
||||||
@ -25,7 +27,7 @@ class TestClass(object):
|
|||||||
"type": "m.room.message",
|
"type": "m.room.message",
|
||||||
"unsigned": {"age": 43289803095},
|
"unsigned": {"age": 43289803095},
|
||||||
"user_id": "@example2:localhost",
|
"user_id": "@example2:localhost",
|
||||||
"age": 43289803095,
|
"age": 43289803095
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -41,44 +43,43 @@ class TestClass(object):
|
|||||||
"type": "m.room.message",
|
"type": "m.room.message",
|
||||||
"unsigned": {"age": 43289803095},
|
"unsigned": {"age": 43289803095},
|
||||||
"user_id": "@example2:localhost",
|
"user_id": "@example2:localhost",
|
||||||
"age": 43289803095,
|
"age": 43289803095
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def encrypted_media_event(self):
|
def encrypted_media_event(self):
|
||||||
return RoomEncryptedMedia.from_dict(
|
return RoomEncryptedMedia.from_dict({
|
||||||
{
|
"room_id": "!testroom:localhost",
|
||||||
"room_id": "!testroom:localhost",
|
"event_id": "$15163622445EBvZK:localhost",
|
||||||
"event_id": "$15163622445EBvZK:localhost",
|
"origin_server_ts": 1516362244030,
|
||||||
"origin_server_ts": 1516362244030,
|
"sender": "@example2:localhost",
|
||||||
"sender": "@example2:localhost",
|
"type": "m.room.message",
|
||||||
"type": "m.room.message",
|
"content": {
|
||||||
"content": {
|
"body": "orange_cat.jpg",
|
||||||
"body": "orange_cat.jpg",
|
"msgtype": "m.image",
|
||||||
"msgtype": "m.image",
|
"file": {
|
||||||
"file": {
|
"v": "v2",
|
||||||
"v": "v2",
|
"key": {
|
||||||
"key": {
|
"alg": "A256CTR",
|
||||||
"alg": "A256CTR",
|
"ext": True,
|
||||||
"ext": True,
|
"k": "yx0QvkgYlasdWEsdalkejaHBzCkKEBAp3tB7dGtWgrs",
|
||||||
"k": "yx0QvkgYlasdWEsdalkejaHBzCkKEBAp3tB7dGtWgrs",
|
"key_ops": ["encrypt", "decrypt"],
|
||||||
"key_ops": ["encrypt", "decrypt"],
|
"kty": "oct"
|
||||||
"kty": "oct",
|
|
||||||
},
|
|
||||||
"iv": "0pglXX7fspIBBBBAEERLFd",
|
|
||||||
"hashes": {
|
|
||||||
"sha256": "eXRDFvh+aXsQRj8a+5ZVVWUQ9Y6u9DYiz4tq1NvbLu8"
|
|
||||||
},
|
|
||||||
"url": "mxc://localhost/maDtasSiPFjROFMnlwxIhhyW",
|
|
||||||
"mimetype": "image/jpeg",
|
|
||||||
},
|
},
|
||||||
},
|
"iv": "0pglXX7fspIBBBBAEERLFd",
|
||||||
|
"hashes": {
|
||||||
|
"sha256": "eXRDFvh+aXsQRj8a+5ZVVWUQ9Y6u9DYiz4tq1NvbLu8"
|
||||||
|
},
|
||||||
|
"url": "mxc://localhost/maDtasSiPFjROFMnlwxIhhyW",
|
||||||
|
"mimetype": "image/jpeg"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)
|
})
|
||||||
|
|
||||||
def test_account_loading(self, panstore):
|
def test_account_loading(self, panstore):
|
||||||
accounts = panstore.load_all_users()
|
accounts = panstore.load_all_users()
|
||||||
|
# pdb.set_trace()
|
||||||
assert len(accounts) == 10
|
assert len(accounts) == 10
|
||||||
|
|
||||||
def test_token_saving(self, panstore, access_token):
|
def test_token_saving(self, panstore, access_token):
|
||||||
@ -129,8 +130,7 @@ class TestClass(object):
|
|||||||
if not INDEXING_ENABLED:
|
if not INDEXING_ENABLED:
|
||||||
pytest.skip("Indexing needs to be enabled to test this")
|
pytest.skip("Indexing needs to be enabled to test this")
|
||||||
|
|
||||||
from pantalaimon.index import IndexStore
|
from pantalaimon.index import Index, IndexStore
|
||||||
|
|
||||||
loop = asyncio.get_event_loop()
|
loop = asyncio.get_event_loop()
|
||||||
|
|
||||||
store = IndexStore("example", tempdir)
|
store = IndexStore("example", tempdir)
|
||||||
@ -148,14 +148,12 @@ class TestClass(object):
|
|||||||
assert len(result["results"]) == 1
|
assert len(result["results"]) == 1
|
||||||
assert result["count"] == 1
|
assert result["count"] == 1
|
||||||
assert result["results"][0]["result"] == self.test_event.source
|
assert result["results"][0]["result"] == self.test_event.source
|
||||||
assert (
|
assert (result["results"][0]["context"]["events_after"][0]
|
||||||
result["results"][0]["context"]["events_after"][0]
|
== self.another_event.source)
|
||||||
== self.another_event.source
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_media_storage(self, panstore):
|
def test_media_storage(self, panstore):
|
||||||
server_name = "test"
|
server_name = "test"
|
||||||
media_cache = panstore.load_media_cache(server_name)
|
media_cache = panstore.load_media(server_name)
|
||||||
assert not media_cache
|
assert not media_cache
|
||||||
|
|
||||||
event = self.encrypted_media_event
|
event = self.encrypted_media_event
|
||||||
@ -173,31 +171,9 @@ class TestClass(object):
|
|||||||
|
|
||||||
panstore.save_media(server_name, media)
|
panstore.save_media(server_name, media)
|
||||||
|
|
||||||
media_cache = panstore.load_media_cache(server_name)
|
media_cache = panstore.load_media(server_name)
|
||||||
|
|
||||||
assert (mxc_server, mxc_path) in media_cache
|
assert (mxc_server, mxc_path) in media_cache
|
||||||
media_info = media_cache[(mxc_server, mxc_path)]
|
media_info = media_cache[(mxc_server, mxc_path)]
|
||||||
assert media_info == media
|
assert media_info == media
|
||||||
assert media_info == panstore.load_media(server_name, mxc_server, mxc_path)
|
assert media_info == panstore.load_media(server_name, mxc_server, mxc_path)
|
||||||
|
|
||||||
def test_upload_storage(self, panstore):
|
|
||||||
server_name = "test"
|
|
||||||
upload_cache = panstore.load_upload(server_name)
|
|
||||||
assert not upload_cache
|
|
||||||
|
|
||||||
filename = "orange_cat.jpg"
|
|
||||||
mimetype = "image/jpeg"
|
|
||||||
event = self.encrypted_media_event
|
|
||||||
|
|
||||||
assert not panstore.load_upload(server_name, event.url)
|
|
||||||
|
|
||||||
upload = UploadInfo(event.url, filename, mimetype)
|
|
||||||
|
|
||||||
panstore.save_upload(server_name, event.url, filename, mimetype)
|
|
||||||
|
|
||||||
upload_cache = panstore.load_upload(server_name)
|
|
||||||
|
|
||||||
assert (event.url) in upload_cache
|
|
||||||
upload_info = upload_cache[event.url]
|
|
||||||
assert upload_info == upload
|
|
||||||
assert upload_info == panstore.load_upload(server_name, event.url)
|
|
||||||
|
13
tox.ini
13
tox.ini
@ -1,14 +1,21 @@
|
|||||||
|
# content of: tox.ini , put in same dir as setup.py
|
||||||
[tox]
|
[tox]
|
||||||
envlist = coverage
|
envlist = py36,py37,coverage
|
||||||
|
|
||||||
[testenv]
|
[testenv]
|
||||||
|
basepython =
|
||||||
|
py36: python3.6
|
||||||
|
py37: python3.7
|
||||||
|
py3: python3.7
|
||||||
|
|
||||||
deps = -rtest-requirements.txt
|
deps = -rtest-requirements.txt
|
||||||
install_command = pip install {opts} {packages}
|
install_command = pip install {opts} {packages}
|
||||||
|
|
||||||
passenv = TOXENV,CI
|
passenv = TOXENV CI TRAVIS TRAVIS_*
|
||||||
commands = pytest
|
commands = pytest
|
||||||
|
usedevelop = True
|
||||||
|
|
||||||
[testenv:coverage]
|
[testenv:coverage]
|
||||||
|
basepython = python3.7
|
||||||
commands =
|
commands =
|
||||||
pytest --cov=pantalaimon --cov-report term-missing
|
pytest --cov=pantalaimon --cov-report term-missing
|
||||||
coverage xml
|
coverage xml
|
||||||
|
Loading…
x
Reference in New Issue
Block a user