Compare commits
25 commits
Author | SHA1 | Date | |
---|---|---|---|
Morgan McMillian | 409b2a5a3c | ||
Morgan McMillian | 84f3882466 | ||
Morgan McMillian | 1a5c9d84b0 | ||
Morgan McMillian | a26759bb7b | ||
Morgan McMillian | 782c3d070b | ||
Morgan McMillian | 037fee7796 | ||
Morgan McMillian | 03ba94ecb9 | ||
Morgan McMillian | de7f4f5c35 | ||
Morgan McMillian | 32d38bc005 | ||
Morgan McMillian | 45f621f9af | ||
Morgan McMillian | edd1ef6212 | ||
Morgan McMillian | 82c2ab105a | ||
Morgan McMillian | 38cd7b347e | ||
Morgan McMillian | ee6baaa579 | ||
Morgan McMillian | 24d1258265 | ||
Morgan McMillian | 05893172f8 | ||
Morgan McMillian | ad49c3a354 | ||
Morgan McMillian | 2d4476b9a3 | ||
Morgan McMillian | c75b7517ca | ||
Morgan McMillian | f04ec6fef9 | ||
Morgan McMillian | 904140320d | ||
Morgan McMillian | 937ad8cea1 | ||
Morgan McMillian | 03949ff67b | ||
Morgan McMillian | 9571238043 | ||
eb78c95ec6 |
15
.dockerignore
Normal file
15
.dockerignore
Normal file
|
@ -0,0 +1,15 @@
|
|||
__pycache__/
|
||||
*.py[cod]
|
||||
*.yaml
|
||||
*.db
|
||||
*.sublime-project
|
||||
*.sublime-workspace
|
||||
.vscode/
|
||||
.git/
|
||||
.gitignore
|
||||
Dockerfile
|
||||
.dockerignore
|
||||
snap/
|
||||
Jenkinsfile
|
||||
.gitlab-ci.yml
|
||||
contrib/
|
56
.gitlab-ci.yml
Normal file
56
.gitlab-ci.yml
Normal file
|
@ -0,0 +1,56 @@
|
|||
# This file is a template, and might need editing before it works on your project.
|
||||
# To contribute improvements to CI/CD templates, please follow the Development guide at:
|
||||
# https://docs.gitlab.com/ee/development/cicd/templates.html
|
||||
# This specific template is located at:
|
||||
# https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Docker.gitlab-ci.yml
|
||||
|
||||
# Build a Docker image with CI/CD and push to the GitLab registry.
|
||||
# Docker-in-Docker documentation: https://docs.gitlab.com/ee/ci/docker/using_docker_build.html
|
||||
#
|
||||
# This template uses one generic job with conditional builds
|
||||
# for the default branch and all other (MR) branches.
|
||||
stages:
|
||||
- build
|
||||
|
||||
docker-build:
|
||||
# Use the official docker image.
|
||||
image: docker:latest
|
||||
stage: build
|
||||
services:
|
||||
- docker:dind
|
||||
before_script:
|
||||
- docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY
|
||||
# Default branch leaves tag empty (= latest tag)
|
||||
# All other branches are tagged with the escaped branch name (commit ref slug)
|
||||
script:
|
||||
- |
|
||||
if [[ "$CI_COMMIT_BRANCH" == "$CI_DEFAULT_BRANCH" ]]; then
|
||||
tag=""
|
||||
echo "Running on default branch '$CI_DEFAULT_BRANCH': tag = 'latest'"
|
||||
else
|
||||
tag=":$CI_COMMIT_REF_SLUG"
|
||||
echo "Running on branch '$CI_COMMIT_BRANCH': tag = $tag"
|
||||
fi
|
||||
- docker build --pull -t "$CI_REGISTRY_IMAGE${tag}" .
|
||||
- docker push "$CI_REGISTRY_IMAGE${tag}"
|
||||
# Run this job in a branch where a Dockerfile exists
|
||||
rules:
|
||||
- if: $CI_COMMIT_BRANCH
|
||||
exists:
|
||||
- Dockerfile
|
||||
|
||||
# snap-build-arm64:
|
||||
# stage: build
|
||||
# tags:
|
||||
# - snap-arm64
|
||||
# script:
|
||||
# - snapcraft
|
||||
# - snapcraft upload --release=edge *.snap
|
||||
#
|
||||
# snap-build-amd64:
|
||||
# stage: build
|
||||
# tags:
|
||||
# - snap-amd64
|
||||
# script:
|
||||
# - snapcraft
|
||||
# - snapcraft upload --release=edge *.snap
|
18
CHANGELOG.md
18
CHANGELOG.md
|
@ -4,6 +4,24 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
|
|||
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [Unreleased]
|
||||
### Added
|
||||
- Docker image
|
||||
|
||||
### Fixed
|
||||
- Self-invite when sending a PM from a pnut app
|
||||
|
||||
## [1.3.0] - 2022-08-20
|
||||
### Added
|
||||
- Support for matrix stickers
|
||||
|
||||
### Fixed
|
||||
- External matrix users unable to DM appservice
|
||||
- Joining a non-public channels on pnut makes matrix room public
|
||||
- Timestamps on generated events
|
||||
|
||||
### Changed
|
||||
- Improved display name for pnut users in matrix rooms
|
||||
- Access tokens redacted from appservice logs
|
||||
|
||||
## [1.2.0] - 2021-03-20
|
||||
### Added
|
||||
|
|
16
Dockerfile
16
Dockerfile
|
@ -1,16 +1,14 @@
|
|||
FROM python:3
|
||||
|
||||
VOLUME /data
|
||||
FROM python:3.11-slim-bookworm
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
COPY requirements.txt ./
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
RUN apt-get update && apt-get install libmagic-dev curl -y
|
||||
|
||||
COPY . .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
ENV CONFIG_FILE=/data/config.yaml
|
||||
VOLUME /data
|
||||
WORKDIR /data
|
||||
|
||||
EXPOSE 5000/tcp
|
||||
|
||||
CMD [ "python", "/usr/src/app/pnut-matrix.py", "-d" ]
|
||||
EXPOSE 5000
|
||||
CMD [ "python", "/usr/src/app/pnut-matrix.py" ]
|
||||
|
|
60
README.md
60
README.md
|
@ -2,17 +2,65 @@
|
|||
|
||||
This is a pnut.io channel bridge for Matrix using the Application Services (AS) API.
|
||||
|
||||
This bridge will pass pnut.io channel messages through to Matrix, and Matrix messages through to pnut.io channels. Currently only public chat channels on pnut.io are supported but work is in progress to support private messages.
|
||||
This bridge will pass pnut.io channel messages through to Matrix, and Matrix messages through to pnut.io channels.
|
||||
|
||||
|
||||
## Usage
|
||||
|
||||
Visit the project [wiki](https://gitlab.dreamfall.space/thrrgilag/pnut-matrix/-/wikis/home) for up to date information on using the bridge hosted on dreamfall.space.
|
||||
The public bridge is once again online!
|
||||
|
||||
See [Using-the-public-bridge](https://gitlab.com/thrrgilag/pnut-matrix/-/wikis/Using-the-public-bridge) for details.
|
||||
|
||||
|
||||
## Questions, comments, or issues
|
||||
## Installation
|
||||
|
||||
Join the chat!
|
||||
Currently pnut-matrix has been only tested with and confirmed to work with [synapse]. Please refer to the [synapse installation instructions] for details on how to setup your homeserver.
|
||||
|
||||
* [#pnut-matrix:dreamfall.space](https://matrix.to/#/#pnut-matrix:dreamfall.space)
|
||||
* [pnut.io](https://patter.chat/room/999)
|
||||
To install the latest version of pnut-matrix from source:
|
||||
|
||||
```sh
|
||||
git clone https://gitlab.com/thrrgilag/pnut-matrix.git
|
||||
cd pnut-matrix
|
||||
python3 -m venv env
|
||||
source env/bin/activate
|
||||
pip3 install -r requirements.txt
|
||||
```
|
||||
|
||||
|
||||
## Configuration
|
||||
|
||||
Copy `config.yaml-sample` to `config.yaml` and edit for your setup. Likewise copy `appservice.yaml-sample` to `appservice.yaml` and edit to match the tokens, prefix, and port listed in `config.yaml`. This is the configuration that synapse will need to reference so that it can connect the bridge. Make sure you modify your [syanpse configuration] accordingly.
|
||||
|
||||
|
||||
## Tweaks
|
||||
|
||||
There exists a bug in synapse which prevents the bridge user from accepting DM invites from users on other homeservers. To work around the issue you can create a profile for the bot user.[^1]
|
||||
|
||||
```sh
|
||||
curl --data '{"type": "m.login.application_service", "username": "your_sender_localpart"}' 'http://yourhomeserver/_matrix/client/r0/register?access_token=your_as_token'
|
||||
```
|
||||
|
||||
|
||||
## Contributing and support
|
||||
|
||||
You can open issues for bugs or feature requests and you can submit merge requests to this project on [GitLab]. You can also submit issues and patches directly to [morgan@mcmillian.dev].
|
||||
|
||||
Join my public chat room for development discussion.
|
||||
|
||||
- [pnut-matrix on pnut.io]
|
||||
- [#pnut_999:pnut-matrix.dreamfall.space]
|
||||
|
||||
|
||||
## License
|
||||
|
||||
GPLv3, see [LICENSE].
|
||||
|
||||
[synapse]: https://github.com/matrix-org/synapse
|
||||
[synapse installation instructions]: https://matrix-org.github.io/synapse/latest/setup/installation.html
|
||||
[syanpse configuration]: https://matrix-org.github.io/synapse/latest/application_services.html
|
||||
[GitLab]: https://gitlab.com/thrrgilag/pnut-matrix/
|
||||
[morgan@mcmillian.dev]: mailto:morgan@mcmillian.dev
|
||||
[pnut-matrix on pnut.io]: https://patter.chat/999
|
||||
[#pnut_999:pnut-matrix.dreamfall.space]: https://matrix.to/#/#pnut_999:pnut-matrix.dreamfall.space
|
||||
[LICENSE]: LICENSE
|
||||
[^1]: https://github.com/matrix-org/matrix-appservice-irc/issues/1270#issuecomment-849765090
|
||||
|
|
158
appservice.py
158
appservice.py
|
@ -29,6 +29,7 @@ def forbidden(error):
|
|||
def shutdown_session(exception=None):
|
||||
db_session.remove()
|
||||
|
||||
@app.route("/_matrix/app/v1/rooms/<alias>")
|
||||
@app.route("/rooms/<alias>")
|
||||
def query_alias(alias):
|
||||
alias_localpart = alias.split(":")[0][1:]
|
||||
|
@ -58,13 +59,17 @@ def query_alias(alias):
|
|||
room['name'] = channel_settings['name']
|
||||
if 'description' in channel_settings:
|
||||
room['topic'] = channel_settings['description']
|
||||
if channel.acl.read.any_user:
|
||||
if channel.acl.read.public:
|
||||
room['preset'] = 'public_chat'
|
||||
room['visibility'] = 'public'
|
||||
else:
|
||||
abort(401)
|
||||
|
||||
url = app.config['MATRIX_HOST'] + '/_matrix/client/api/v1/createRoom?access_token='
|
||||
url += app.config['MATRIX_AS_TOKEN']
|
||||
headers = {"Content-Type":"application/json"}
|
||||
url = app.config['MATRIX_HOST'] + '/_matrix/client/api/v1/createRoom'
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": "Bearer " + app.config['MATRIX_AS_TOKEN']
|
||||
}
|
||||
r = requests.post(url, headers=headers, data=json.dumps(room))
|
||||
if r.status_code == 200:
|
||||
pnutpy.api.subscribe_channel(channel_id)
|
||||
|
@ -77,6 +82,12 @@ def query_alias(alias):
|
|||
db_session.add(rr)
|
||||
db_session.commit()
|
||||
|
||||
else:
|
||||
logger.error("Unable to create room")
|
||||
logger.error(r.status_code)
|
||||
logger.error(r.text)
|
||||
abort(400)
|
||||
|
||||
except pnutpy.errors.PnutPermissionDenied:
|
||||
abort(401)
|
||||
|
||||
|
@ -86,6 +97,7 @@ def query_alias(alias):
|
|||
|
||||
return jsonify({})
|
||||
|
||||
@app.route("/_matrix/app/v1/transactions/<transaction>", methods=["PUT"])
|
||||
@app.route("/transactions/<transaction>", methods=["PUT"])
|
||||
def on_receive_events(transaction):
|
||||
|
||||
|
@ -105,6 +117,9 @@ def on_receive_events(transaction):
|
|||
if event['type'] == 'm.room.message':
|
||||
new_message(event, user)
|
||||
|
||||
elif event['type'] == 'm.sticker':
|
||||
new_sticker(event, user)
|
||||
|
||||
elif event['type'] == 'm.room.redaction':
|
||||
delete_message(event, user)
|
||||
|
||||
|
@ -123,6 +138,63 @@ def on_receive_events(transaction):
|
|||
|
||||
return jsonify({})
|
||||
|
||||
def new_sticker(event, user):
|
||||
if app.config['MATRIX_PNUT_PREFIX'] in event['user_id'] or 'pnut-bridge' in event['user_id']:
|
||||
logger.debug('-skipping dup event-')
|
||||
return
|
||||
|
||||
room = Rooms.query.filter(Rooms.room_id == event['room_id']).one_or_none()
|
||||
if room is None:
|
||||
logger.debug('-room not mapped-')
|
||||
return
|
||||
|
||||
if user is not None:
|
||||
token = user.pnut_user_token
|
||||
prefix = ""
|
||||
else:
|
||||
token = app.config['MATRIX_PNUT_TOKEN']
|
||||
matrix_profile = get_profile(event['user_id'])
|
||||
if "displayname" in matrix_profile:
|
||||
prefix = "[" + matrix_profile['displayname'] + "] (" + event['user_id'] + ")\n"
|
||||
else:
|
||||
prefix = "(" + event['user_id'] + ")\n"
|
||||
|
||||
pnutpy.api.add_authorization_token(token)
|
||||
# evtext, evraw = msg_from_event(event)
|
||||
text = "sticker::" + event['content']['body'] + "\n"
|
||||
value = {'type': "photo", 'version': "1.0"}
|
||||
value['url'] = app.config['MATRIX_URL'] + '/_matrix/media/r0/download/' + event['content']['url'][6:]
|
||||
value['title'] = event['content']['body']
|
||||
if 'h' in event['content']['info'] and 'w' in event['content']['info']:
|
||||
value['height'] = event['content']['info']['h']
|
||||
value['width'] = event['content']['info']['h']
|
||||
else:
|
||||
return
|
||||
raw = {'type': "io.pnut.core.oembed", 'value': value}
|
||||
embed = [raw]
|
||||
text = prefix + text
|
||||
|
||||
try:
|
||||
msg, meta = pnutpy.api.create_message(room.pnut_chan, data={'text': text, 'raw': embed})
|
||||
revent = Events(
|
||||
event_id=event['event_id'],
|
||||
room_id=event['room_id'],
|
||||
pnut_msg_id=msg.id,
|
||||
pnut_user_id=msg.user.id,
|
||||
pnut_chan_id=room.pnut_chan,
|
||||
deleted=False
|
||||
)
|
||||
db_session.add(revent)
|
||||
db_session.commit()
|
||||
|
||||
except pnutpy.errors.PnutAuthAPIException:
|
||||
logger.exception('-unable to post to pnut channel-')
|
||||
return
|
||||
|
||||
except Exception:
|
||||
logger.exception('-something bad happened here-')
|
||||
return
|
||||
|
||||
def new_message(event, user):
|
||||
if app.config['MATRIX_PNUT_PREFIX'] in event['user_id'] or 'pnut-bridge' in event['user_id']:
|
||||
logger.debug('-skipping dup event-')
|
||||
|
@ -320,7 +392,7 @@ def delete_message(event, user):
|
|||
if e is None:
|
||||
logger.debug("- can't find the event to remove -")
|
||||
return
|
||||
|
||||
|
||||
try:
|
||||
r, meta = pnutpy.api.delete_message(e.pnut_chan_id, e.pnut_msg_id)
|
||||
e.deleted = True
|
||||
|
@ -350,7 +422,7 @@ def get_channel_settings(channel_id):
|
|||
|
||||
return channel_settings
|
||||
|
||||
def create_room(channel, invite):
|
||||
def create_room(channel, user):
|
||||
channel_settings = {}
|
||||
for item in channel.raw:
|
||||
if item.type == 'io.pnut.core.chat-settings':
|
||||
|
@ -358,23 +430,27 @@ def create_room(channel, invite):
|
|||
|
||||
# Matrix sdk doesn't include all details in a single call
|
||||
room = {'room_alias_name': app.config['MATRIX_PNUT_PREFIX'] + channel.id}
|
||||
logger.debug(invite)
|
||||
logger.debug(user)
|
||||
logger.debug(room)
|
||||
room['invite'] = [invite]
|
||||
room['invite'] = [user.matrix_id]
|
||||
if 'name' in channel_settings:
|
||||
room['name'] = channel_settings['name']
|
||||
if 'description' in channel_settings:
|
||||
room['topic'] = channel_settings['description']
|
||||
if channel.acl.read.any_user:
|
||||
if channel.acl.read.public:
|
||||
room['preset'] = 'public_chat'
|
||||
room['visibility'] = 'public'
|
||||
else:
|
||||
elif channel.acl.read.any_user or channel.acl.read.you:
|
||||
room['preset'] = 'private_chat'
|
||||
room['visibility'] = 'private'
|
||||
else:
|
||||
abort(401)
|
||||
|
||||
url = app.config['MATRIX_HOST'] + '/_matrix/client/api/v1/createRoom?access_token='
|
||||
url += app.config['MATRIX_AS_TOKEN']
|
||||
headers = {"Content-Type":"application/json"}
|
||||
url = app.config['MATRIX_HOST'] + '/_matrix/client/api/v1/createRoom'
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": "Bearer " + app.config['MATRIX_AS_TOKEN']
|
||||
}
|
||||
r = requests.post(url, headers=headers, data=json.dumps(room))
|
||||
|
||||
if r.status_code == 200:
|
||||
|
@ -387,17 +463,32 @@ def create_room(channel, invite):
|
|||
)
|
||||
db_session.add(rr)
|
||||
db_session.commit()
|
||||
logger.debug(r.status_code)
|
||||
logger.debug(r)
|
||||
|
||||
else:
|
||||
logger.error("Unable to create room")
|
||||
logger.error(r.status_code)
|
||||
logger.error(r.text)
|
||||
abort(400)
|
||||
|
||||
def new_matrix_user(username):
|
||||
matrix_api = MatrixHttpApi(app.config['MATRIX_HOST'],
|
||||
token=app.config['MATRIX_AS_TOKEN'])
|
||||
data = {'type': 'm.login.application_service','username': app.config['MATRIX_PNUT_PREFIX'] + username}
|
||||
try:
|
||||
matrix_api.register(content=data)
|
||||
|
||||
except Exception:
|
||||
errmsg = "- new_matrix_user user already exists -"
|
||||
logger.warning(errmsg)
|
||||
|
||||
def on_admin_event(event):
|
||||
matrix_api = MatrixHttpApi(app.config['MATRIX_HOST'],
|
||||
matrix_api = MatrixHttpApi(app.config['MATRIX_HOST'],
|
||||
token=app.config['MATRIX_AS_TOKEN'])
|
||||
logger.debug("- admin room event recieved -")
|
||||
|
||||
if event['type'] != 'm.room.message':
|
||||
return jsonify({})
|
||||
|
||||
|
||||
msg = event['content']['body'].split(' ')
|
||||
|
||||
try:
|
||||
|
@ -480,7 +571,7 @@ def cmd_admin_list():
|
|||
return text
|
||||
|
||||
def cmd_admin_link(room_id, pnut_chan_id):
|
||||
matrix_api = MatrixHttpApi(app.config['MATRIX_HOST'],
|
||||
matrix_api = MatrixHttpApi(app.config['MATRIX_HOST'],
|
||||
token=app.config['MATRIX_AS_TOKEN'])
|
||||
pnutpy.api.add_authorization_token(app.config['MATRIX_PNUT_TOKEN'])
|
||||
|
||||
|
@ -493,7 +584,7 @@ def cmd_admin_link(room_id, pnut_chan_id):
|
|||
try:
|
||||
channel, meta = pnutpy.api.subscribe_channel(pnut_chan_id)
|
||||
r = matrix_api.join_room(room_id)
|
||||
|
||||
|
||||
rec = Rooms(
|
||||
room_id=room_id,
|
||||
pnut_chan=channel.id,
|
||||
|
@ -512,15 +603,15 @@ def cmd_admin_link(room_id, pnut_chan_id):
|
|||
logger.exception(errmsg)
|
||||
return errmsg
|
||||
|
||||
def cmd_admin_unlink(id):
|
||||
matrix_api = MatrixHttpApi(app.config['MATRIX_HOST'],
|
||||
def cmd_admin_unlink(rid):
|
||||
matrix_api = MatrixHttpApi(app.config['MATRIX_HOST'],
|
||||
token=app.config['MATRIX_AS_TOKEN'])
|
||||
pnutpy.api.add_authorization_token(app.config['MATRIX_PNUT_TOKEN'])
|
||||
|
||||
if id.startswith('!'):
|
||||
room = Rooms.query.filter(Rooms.room_id == id).one_or_none()
|
||||
if rid.startswith('!'):
|
||||
room = Rooms.query.filter(Rooms.room_id == rid).one_or_none()
|
||||
else:
|
||||
room = Rooms.query.filter(Rooms.pnut_chan == id).one_or_none()
|
||||
room = Rooms.query.filter(Rooms.pnut_chan == rid).one_or_none()
|
||||
|
||||
if hasattr(room, 'portal'):
|
||||
if room.portal:
|
||||
|
@ -574,6 +665,7 @@ def on_direct_invite(event):
|
|||
# TODO: need to handle if the user isn't registered
|
||||
pnutpy.api.add_authorization_token(user.pnut_user_token)
|
||||
channel, meta = pnutpy.api.existing_pm(ids=pnut_user)
|
||||
new_matrix_user(pnut_user)
|
||||
|
||||
dm = DirectRooms(room_id=event['room_id'],
|
||||
bridge_user=bridge_user, pnut_chan=channel.id)
|
||||
|
@ -654,20 +746,20 @@ def on_direct_message(event, user, room):
|
|||
|
||||
except pnutpy.errors.PnutAuthAPIException:
|
||||
logger.exception('-unable to post to pnut channel-')
|
||||
|
||||
|
||||
except Exception:
|
||||
logger.exception('-something bad happened here-')
|
||||
|
||||
return jsonify({})
|
||||
|
||||
def on_control_message(event):
|
||||
matrix_api = MatrixHttpApi(app.config['MATRIX_HOST'],
|
||||
matrix_api = MatrixHttpApi(app.config['MATRIX_HOST'],
|
||||
token=app.config['MATRIX_AS_TOKEN'])
|
||||
logger.debug("- direct room event received -")
|
||||
|
||||
if event['type'] != 'm.room.message':
|
||||
return jsonify({})
|
||||
|
||||
|
||||
msg = event['content']['body'].split(' ')
|
||||
|
||||
try:
|
||||
|
@ -763,7 +855,7 @@ def cmd_user_drop(sender=None):
|
|||
db_session.commit()
|
||||
reply = "Success! Your auth token has been removed."
|
||||
else:
|
||||
reply = "You do not appear to be registered."
|
||||
reply = "You do not appear to be registered."
|
||||
|
||||
except Exception as e:
|
||||
logging.exception('!drop')
|
||||
|
@ -803,8 +895,14 @@ def cmd_user_join(sender=None, channel_id=None):
|
|||
else:
|
||||
pnutpy.api.add_authorization_token(user.pnut_user_token)
|
||||
channel, meta = pnutpy.api.get_channel(channel_id, include_raw=1)
|
||||
create_room(channel, user.matrix_id)
|
||||
reply = "is working?"
|
||||
room = Rooms.query.filter(Rooms.pnut_chan == channel_id).one_or_none()
|
||||
if room is None:
|
||||
create_room(channel, user)
|
||||
else:
|
||||
matrix_api = MatrixHttpApi(app.config['MATRIX_HOST'],
|
||||
token=app.config['MATRIX_AS_TOKEN'])
|
||||
matrix_api.invite_user(room.room_id, sender)
|
||||
reply = "ok"
|
||||
|
||||
except pnutpy.errors.PnutAuthAPIException as e:
|
||||
reply = "You are currently not authorized on pnut.io"
|
||||
|
|
13
appservice.yaml-sample
Normal file
13
appservice.yaml-sample
Normal file
|
@ -0,0 +1,13 @@
|
|||
id: "pnut"
|
||||
url: "http://127.0.0.1:5000"
|
||||
as_token: "<AS_AUTH_TOKEN>"
|
||||
hs_token: "<HS_AUTH_TOKEN>"
|
||||
sender_localpart: pnut
|
||||
namespaces:
|
||||
users:
|
||||
- exclusive: true
|
||||
regex: "@pnut_.*"
|
||||
rooms: []
|
||||
aliases:
|
||||
- exclusive: true
|
||||
regex: "#pnut_.*"
|
|
@ -4,12 +4,34 @@ MATRIX_URL: 'https://<DOMAIN_NAME>' # public base URL of the matrix server
|
|||
MATRIX_HOST: 'https://localhost:8448' # client URL of the matrix server
|
||||
MATRIX_DOMAIN: '<DOMAIN_NAME>' # domain of the matrix server (right hand side of a matrix ID)
|
||||
MATRIX_AS_ID: '<MATRIX_ID>' # matrix ID for the app service user
|
||||
MATRIX_AS_TOKEN: '<AUTH_TOKEN>' # auth token for the app service user
|
||||
MATRIX_HS_TOKEN: '<AUTH_TOKEN>' # auth token for the matrix server
|
||||
MATRIX_PNUT_PREFIX: '<APP_SERVICE_PREFIX>' # prefix used for reserving matrix IDs and room aliases
|
||||
MATRIX_AS_TOKEN: '<AS_AUTH_TOKEN>' # auth token for the app service user
|
||||
MATRIX_HS_TOKEN: '<HS_AUTH_TOKEN>' # auth token for the matrix server
|
||||
MATRIX_PNUT_PREFIX: 'pnut_' # prefix used for reserving matrix IDs and room aliases
|
||||
MATRIX_ADMIN_ROOM: '<ROOM ID>' # Administrator control room ID
|
||||
MATRIX_PNUT_USER: '<USERNAME>' # pnut.io username for the matrix bot
|
||||
MATRIX_PNUT_TOKEN: '<AUTH_TOKEN>' # pnut.io auth token for the matrix bot
|
||||
PNUTCLIENT_ID: '<CLIENT_ID>' # pnut.io app client ID
|
||||
PNUT_APPTOKEN: '<APP TOKEN>' # pnut.io app token
|
||||
PNUT_APPKEY: '<APPSTREAM KEY>' # pnut.io app stream key
|
||||
|
||||
logging:
|
||||
version: 1
|
||||
formatters:
|
||||
precise:
|
||||
format: '%(asctime)s - %(name)s - %(lineno)d - %(levelname)s - %(message)s'
|
||||
normal:
|
||||
format: '%(name)s - %(levelname)s - %(message)s'
|
||||
handlers:
|
||||
console:
|
||||
class: logging.StreamHandler
|
||||
formatter: normal
|
||||
loggers:
|
||||
werkzeug:
|
||||
level: DEBUG
|
||||
appservice:
|
||||
level: DEBUG
|
||||
urllib3.connectionpool:
|
||||
level: DEBUG
|
||||
root:
|
||||
level: DEBUG
|
||||
handlers: [console]
|
||||
|
|
|
@ -2,6 +2,7 @@ import websocket
|
|||
import threading
|
||||
import time
|
||||
import logging
|
||||
import logging.config
|
||||
import yaml
|
||||
import json
|
||||
import pnutpy
|
||||
|
@ -9,6 +10,7 @@ import requests
|
|||
import magic
|
||||
import argparse
|
||||
import os
|
||||
import re
|
||||
|
||||
from matrix_client.api import MatrixHttpApi
|
||||
from matrix_client.api import MatrixError, MatrixRequestError
|
||||
|
@ -22,6 +24,21 @@ logger = logging.getLogger()
|
|||
_shutdown = threading.Event()
|
||||
_reconnect = threading.Event()
|
||||
|
||||
class MLogFilter(logging.Filter):
|
||||
|
||||
ACCESS_TOKEN_RE = re.compile(r"(\?.*access(_|%5[Ff])token=)[^&]*(\s.*)$")
|
||||
ACCESS_TOKEN_RE2 = re.compile(r"(\?.*access(_|%5[Ff])token=)[^&]*(.*)$")
|
||||
|
||||
def filter(self, record):
|
||||
if record.name == "werkzeug" and len(record.args) > 0:
|
||||
redacted_uri = MLogFilter.ACCESS_TOKEN_RE.sub(r"\1<redacted>\3", record.args[0])
|
||||
record.args = (redacted_uri, ) + record.args[1:]
|
||||
elif record.name == "urllib3.connectionpool" and len(record.args) > 3:
|
||||
redacted_uri = MLogFilter.ACCESS_TOKEN_RE2.sub(r"\1<redacted>\3", record.args[4])
|
||||
record.args = record.args[:4] + (redacted_uri,) + record.args[5:]
|
||||
|
||||
return True
|
||||
|
||||
def new_message(msg, meta):
|
||||
logger.debug("channel: " + msg.channel_id)
|
||||
logger.debug("username: " + msg.user.username)
|
||||
|
@ -53,6 +70,8 @@ def new_message(msg, meta):
|
|||
invitees=[]
|
||||
for pm_user in meta['subscribed_user_ids']:
|
||||
user = Users.query.filter(Users.pnut_user_id == pm_user).one_or_none()
|
||||
if int(pm_user) == msg.user.id:
|
||||
continue
|
||||
if user is not None:
|
||||
invitees.append(user.matrix_id)
|
||||
if len(invitees) > 0:
|
||||
|
@ -66,7 +85,7 @@ def new_message(msg, meta):
|
|||
return
|
||||
|
||||
matrix_id = matrix_id_from_pnut(msg.user.username)
|
||||
matrix_api = MatrixHttpApi(config['MATRIX_HOST'],
|
||||
matrix_api = MatrixHttpApi(config['MATRIX_HOST'],
|
||||
token=config['MATRIX_AS_TOKEN'],
|
||||
identity=matrix_id)
|
||||
|
||||
|
@ -75,11 +94,11 @@ def new_message(msg, meta):
|
|||
new_matrix_user(msg.user.username)
|
||||
logger.debug('-new_user-')
|
||||
profile = {'displayname': None}
|
||||
|
||||
|
||||
if profile['displayname'] != matrix_display_from_pnut(msg.user):
|
||||
set_matrix_display(msg.user)
|
||||
logger.debug('-set_display-')
|
||||
|
||||
|
||||
avatar = Avatars.query.filter(Avatars.pnut_user == msg.user.username).one_or_none()
|
||||
if avatar is None or avatar.avatar != msg.user.content.avatar_image.url:
|
||||
set_matrix_avatar(msg.user)
|
||||
|
@ -93,7 +112,7 @@ def new_message(msg, meta):
|
|||
|
||||
if 'content' in msg:
|
||||
text = msg.content.text + "\n"
|
||||
ts = int(msg.created_at.strftime('%s')) * 1000
|
||||
ts = int(time.time()) * 1000
|
||||
|
||||
lnktext = ""
|
||||
for link in msg.content.entities.links:
|
||||
|
@ -107,10 +126,10 @@ def new_message(msg, meta):
|
|||
|
||||
r = matrix_api.send_message(room.room_id, text, timestamp=ts)
|
||||
event = Events(
|
||||
event_id=r['event_id'],
|
||||
room_id=room.room_id,
|
||||
pnut_msg_id=msg.id,
|
||||
pnut_user_id=msg.user.id,
|
||||
event_id=r['event_id'],
|
||||
room_id=room.room_id,
|
||||
pnut_msg_id=msg.id,
|
||||
pnut_user_id=msg.user.id,
|
||||
pnut_chan_id=msg.channel_id,
|
||||
deleted=False)
|
||||
db_session.add(event)
|
||||
|
@ -122,10 +141,10 @@ def new_message(msg, meta):
|
|||
|
||||
def new_media(room_id, msg):
|
||||
matrix_id = matrix_id_from_pnut(msg.user.username)
|
||||
matrix_api = MatrixHttpApi(config['MATRIX_HOST'],
|
||||
matrix_api = MatrixHttpApi(config['MATRIX_HOST'],
|
||||
token=config['MATRIX_AS_TOKEN'],
|
||||
identity=matrix_id)
|
||||
ts = int(msg.created_at.strftime('%s')) * 1000
|
||||
ts = int(time.time()) * 1000
|
||||
|
||||
if 'io.pnut.core.oembed' in msg.raw:
|
||||
|
||||
|
@ -167,10 +186,10 @@ def new_media(room_id, msg):
|
|||
|
||||
r = matrix_api.send_content(room_id, ul['content_uri'], title, msgtype, extra_information=info, timestamp=ts)
|
||||
event = Events(
|
||||
event_id=r['event_id'],
|
||||
room_id=room_id,
|
||||
pnut_msg_id=msg.id,
|
||||
pnut_user_id=msg.user.id,
|
||||
event_id=r['event_id'],
|
||||
room_id=room_id,
|
||||
pnut_msg_id=msg.id,
|
||||
pnut_user_id=msg.user.id,
|
||||
pnut_chan_id=msg.channel_id,
|
||||
deleted=False)
|
||||
db_session.add(event)
|
||||
|
@ -178,7 +197,7 @@ def new_media(room_id, msg):
|
|||
|
||||
def delete_message(msg):
|
||||
matrix_id = matrix_id_from_pnut(msg.user.username)
|
||||
matrix_api = MatrixHttpApi(config['MATRIX_HOST'],
|
||||
matrix_api = MatrixHttpApi(config['MATRIX_HOST'],
|
||||
token=config['MATRIX_AS_TOKEN'],
|
||||
identity=matrix_id)
|
||||
|
||||
|
@ -193,7 +212,7 @@ def matrix_id_from_pnut(username):
|
|||
|
||||
def matrix_display_from_pnut(user):
|
||||
if 'name' in user:
|
||||
display = user.name + " <@" + user.username + "> (pnut)"
|
||||
display = user.name + " (@" + user.username + ")"
|
||||
else:
|
||||
display = "@" + user.username
|
||||
return display
|
||||
|
@ -210,14 +229,14 @@ def get_matrix_profile(matrix_id):
|
|||
|
||||
def set_matrix_display(user):
|
||||
matrix_id = matrix_id_from_pnut(user.username)
|
||||
matrix_api = MatrixHttpApi(config['MATRIX_HOST'],
|
||||
matrix_api = MatrixHttpApi(config['MATRIX_HOST'],
|
||||
token=config['MATRIX_AS_TOKEN'],
|
||||
identity=matrix_id)
|
||||
matrix_api.set_display_name(matrix_id, matrix_display_from_pnut(user))
|
||||
|
||||
def set_matrix_avatar(user):
|
||||
matrix_id = matrix_id_from_pnut(user.username)
|
||||
matrix_api = MatrixHttpApi(config['MATRIX_HOST'],
|
||||
matrix_api = MatrixHttpApi(config['MATRIX_HOST'],
|
||||
token=config['MATRIX_AS_TOKEN'],
|
||||
identity=matrix_id)
|
||||
|
||||
|
@ -241,24 +260,24 @@ def set_matrix_avatar(user):
|
|||
logger.exception('failed to set user avatar')
|
||||
|
||||
def new_matrix_user(username):
|
||||
matrix_api = MatrixHttpApi(config['MATRIX_HOST'],
|
||||
matrix_api = MatrixHttpApi(config['MATRIX_HOST'],
|
||||
token=config['MATRIX_AS_TOKEN'])
|
||||
data = {
|
||||
'type': 'm.login.application_service',
|
||||
'user': config['MATRIX_PNUT_PREFIX'] + username
|
||||
'username': config['MATRIX_PNUT_PREFIX'] + username
|
||||
}
|
||||
matrix_api.register(content=data)
|
||||
|
||||
def join_room(room_id, matrix_id):
|
||||
matrix_api_as = MatrixHttpApi(config['MATRIX_HOST'],
|
||||
matrix_api_as = MatrixHttpApi(config['MATRIX_HOST'],
|
||||
token=config['MATRIX_AS_TOKEN'])
|
||||
matrix_api = MatrixHttpApi(config['MATRIX_HOST'],
|
||||
matrix_api = MatrixHttpApi(config['MATRIX_HOST'],
|
||||
token=config['MATRIX_AS_TOKEN'],
|
||||
identity=matrix_id)
|
||||
|
||||
|
||||
try:
|
||||
matrix_api.join_room(room_id)
|
||||
|
||||
|
||||
except MatrixRequestError as e:
|
||||
if e.code == 403:
|
||||
matrix_api_as.invite_user(room_id, matrix_id)
|
||||
|
@ -270,7 +289,7 @@ def join_room(room_id, matrix_id):
|
|||
|
||||
def new_room(pnut_user, invitees, chan):
|
||||
dr = None
|
||||
matrix_api = MatrixHttpApi(config['MATRIX_HOST'],
|
||||
matrix_api = MatrixHttpApi(config['MATRIX_HOST'],
|
||||
token=config['MATRIX_AS_TOKEN'],
|
||||
identity=pnut_user)
|
||||
url = matrix_url + '/createRoom'
|
||||
|
@ -293,7 +312,7 @@ def on_message(ws, message):
|
|||
# logger.debug("on_message: " + message)
|
||||
msg = json.loads(message)
|
||||
logger.debug(msg['meta'])
|
||||
|
||||
|
||||
if 'data' in msg:
|
||||
|
||||
if 'channel_type' in msg['meta']:
|
||||
|
@ -368,17 +387,17 @@ if __name__ == '__main__':
|
|||
# )
|
||||
args = a_parser.parse_args()
|
||||
|
||||
if args.debug:
|
||||
# websocket.enableTrace(True)
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
else:
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
configyaml = os.environ.get("CONFIG_FILE")
|
||||
|
||||
with open(configyaml, "rb") as config_file:
|
||||
config = yaml.load(config_file, Loader=yaml.SafeLoader)
|
||||
|
||||
# websocket.enableTrace(True)
|
||||
logging.config.dictConfig(config['logging'])
|
||||
redact_filter = MLogFilter()
|
||||
logging.getLogger("werkzeug").addFilter(redact_filter)
|
||||
logging.getLogger("urllib3.connectionpool").addFilter(redact_filter)
|
||||
|
||||
ws_url = 'wss://stream.pnut.io/v1/app?access_token='
|
||||
ws_url += config['PNUT_APPTOKEN'] + '&key=' + config['PNUT_APPKEY']
|
||||
ws_url += '&include_raw=1'
|
||||
|
@ -388,7 +407,7 @@ if __name__ == '__main__':
|
|||
init_db()
|
||||
|
||||
# setup the websocket connection
|
||||
ws = websocket.WebSocketApp(ws_url, on_message=on_message,
|
||||
ws = websocket.WebSocketApp(ws_url, on_message=on_message,
|
||||
on_error=on_error, on_close=on_close)
|
||||
ws.on_open = on_open
|
||||
wst = threading.Thread(target=wsthreader(ws.run_forever))
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
pyyaml
|
||||
requests
|
||||
matrix-client
|
||||
matrix-client==0.3.2
|
||||
Flask
|
||||
pnutpy
|
||||
sqlalchemy
|
||||
websocket-client
|
||||
filemagic
|
||||
git+https://github.com/shawnanastasio/python-matrix-bot-api
|
26
wait-for-synapse.sh
Executable file
26
wait-for-synapse.sh
Executable file
|
@ -0,0 +1,26 @@
|
|||
#!/bin/sh
|
||||
# wait-for-synapse.sh
|
||||
#
|
||||
# based on an exmaple from https://docs.docker.com/compose/startup-order/
|
||||
#
|
||||
# command: ["/usr/src/app/wait-for-synapse.sh", "http://synapse:8008", "python", "/usr/src/app/pnut-matrix.py", "-d"]
|
||||
|
||||
set -e
|
||||
|
||||
host="$1"
|
||||
# Shift arguments with mapping:
|
||||
# - $0 => $0
|
||||
# - $1 => <discarded>
|
||||
# - $2 => $1
|
||||
# - $3 => $2
|
||||
# - ...
|
||||
# This is done for `exec "$@"` below to work correctly
|
||||
shift
|
||||
|
||||
until [ "200" -eq $(curl -s -o /dev/null --head -w "%{http_code}" ${host}/_matrix/client/versions) ]; do
|
||||
>&2 echo "synapse is unavailable - sleeping"
|
||||
sleep 3
|
||||
done
|
||||
|
||||
>&2 echo "synapse is up - executing command"
|
||||
exec "$@"
|
Loading…
Reference in a new issue