Compare commits
16 commits
Author | SHA1 | Date | |
---|---|---|---|
af411677a7 | |||
13e714422a | |||
4e2a90d6e3 | |||
88b02977c6 | |||
4f15f7751b | |||
0a75b962d0 | |||
691d920c2b | |||
666198e27d | |||
a5695069bc | |||
fece9465e0 | |||
7dad1fef3b | |||
eef37d1360 | |||
3d96f910ee | |||
734be1edda | |||
d2a3d031ec | |||
5638805481 |
17 changed files with 1933 additions and 1609 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -60,6 +60,7 @@ target/
|
||||||
|
|
||||||
# my other cruft
|
# my other cruft
|
||||||
*.yaml
|
*.yaml
|
||||||
|
*.yml
|
||||||
*.db
|
*.db
|
||||||
*.sublime-project
|
*.sublime-project
|
||||||
*.sublime-workspace
|
*.sublime-workspace
|
||||||
|
|
|
@ -1,56 +0,0 @@
|
||||||
# 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
|
|
24
Dockerfile
24
Dockerfile
|
@ -1,14 +1,28 @@
|
||||||
FROM python:3.11-slim-bookworm
|
FROM python:3.13-slim-bookworm AS builder
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y wget unzip
|
||||||
|
|
||||||
|
RUN pip install --no-cache-dir --upgrade pip setuptools wheel
|
||||||
|
|
||||||
WORKDIR /usr/src/app
|
WORKDIR /usr/src/app
|
||||||
|
|
||||||
RUN apt-get update && apt-get install libmagic-dev curl -y
|
# Work around for the unreleased version of PNUTpy
|
||||||
|
RUN wget https://github.com/spacenerdmo/PNUTpy/archive/refs/heads/api_v1.zip
|
||||||
|
RUN unzip api_v1.zip
|
||||||
|
RUN cd PNUTpy-api_v1 && pip wheel . --wheel-dir /wheels --find-links /wheels
|
||||||
|
|
||||||
|
WORKDIR /usr/src/app/pnut-matrix
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN pip install --no-cache-dir -r requirements.txt
|
|
||||||
|
|
||||||
ENV CONFIG_FILE=/data/config.yaml
|
RUN pip wheel . --wheel-dir /wheels --find-links /wheels
|
||||||
|
|
||||||
|
FROM python:3.13-slim-bookworm AS run
|
||||||
|
|
||||||
|
COPY --from=builder /wheels /wheels
|
||||||
|
|
||||||
|
RUN pip --no-cache-dir install --find-links /wheels --no-index pnut-matrix
|
||||||
|
|
||||||
VOLUME /data
|
VOLUME /data
|
||||||
WORKDIR /data
|
WORKDIR /data
|
||||||
EXPOSE 5000
|
EXPOSE 5000
|
||||||
CMD [ "python", "/usr/src/app/pnut-matrix.py" ]
|
CMD [ "bash", "/usr/src/app/run.sh" ]
|
||||||
|
|
31
Jenkinsfile
vendored
Normal file
31
Jenkinsfile
vendored
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
pipeline {
|
||||||
|
agent none
|
||||||
|
environment {
|
||||||
|
CHAT = "spacenerdmo@clacks.network"
|
||||||
|
}
|
||||||
|
stages {
|
||||||
|
stage('Build docker image') {
|
||||||
|
agent { label 'docker-build' }
|
||||||
|
environment {
|
||||||
|
TAG = "${env.BRANCH_NAME == "main" ? "latest" : env.BRANCH_NAME}"
|
||||||
|
}
|
||||||
|
steps {
|
||||||
|
jabberNotify buildToChatNotifier: [$class: 'ExtraMessageOnlyBuildToChatNotifier'], extraMessage: 'Build Started', notifySuspects: true, targets: "${CHAT}"
|
||||||
|
script {
|
||||||
|
docker.withRegistry('https://git.dreamfall.space/spacenerdmo', 'spacenerdmo-pkg-pub') {
|
||||||
|
def customImage = docker.build("git.dreamfall.space/spacenerdmo/pnut-matrix:${TAG}")
|
||||||
|
customImage.push()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
post {
|
||||||
|
success {
|
||||||
|
jabberNotify buildToChatNotifier: [$class: 'ExtraMessageOnlyBuildToChatNotifier'], extraMessage: 'Build Succeeded', notifySuspects: true, targets: "${CHAT}"
|
||||||
|
}
|
||||||
|
failure {
|
||||||
|
jabberNotify buildToChatNotifier: [$class: 'ExtraMessageOnlyBuildToChatNotifier'], extraMessage: 'Build Failed', notifySuspects: true, targets: "${CHAT}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
19
README.md
19
README.md
|
@ -7,19 +7,19 @@ This bridge will pass pnut.io channel messages through to Matrix, and Matrix mes
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
The public bridge is once again online!
|
The public bridge is not yet online pending uplift of this code base. Stay tuned!
|
||||||
|
|
||||||
See [Using-the-public-bridge](https://gitlab.com/thrrgilag/pnut-matrix/-/wikis/Using-the-public-bridge) for details.
|
|
||||||
|
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
|
**Warning! This code is extremely unstable and not yet ready for use on your matrix server.**
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
To install the latest version of pnut-matrix from source:
|
To install the latest version of pnut-matrix from source:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
git clone https://gitlab.com/thrrgilag/pnut-matrix.git
|
git clone https://git.dreamfall.space/spacenerdmo/pnut-matrix.git
|
||||||
cd pnut-matrix
|
cd pnut-matrix
|
||||||
python3 -m venv env
|
python3 -m venv env
|
||||||
source env/bin/activate
|
source env/bin/activate
|
||||||
|
@ -43,12 +43,11 @@ curl --data '{"type": "m.login.application_service", "username": "your_sender_lo
|
||||||
|
|
||||||
## Contributing and support
|
## 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].
|
Please submit bugs, feature requests, and patches to [morgan@mcmillian.dev].
|
||||||
|
|
||||||
Join my public chat room for development discussion.
|
Join my public chat room on pnut.io for development discussion.
|
||||||
|
|
||||||
- [pnut-matrix on pnut.io]
|
- [pnut-matrix]
|
||||||
- [#pnut_999:pnut-matrix.dreamfall.space]
|
|
||||||
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
@ -58,9 +57,7 @@ GPLv3, see [LICENSE].
|
||||||
[synapse]: https://github.com/matrix-org/synapse
|
[synapse]: https://github.com/matrix-org/synapse
|
||||||
[synapse installation instructions]: https://matrix-org.github.io/synapse/latest/setup/installation.html
|
[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
|
[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
|
[morgan@mcmillian.dev]: mailto:morgan@mcmillian.dev
|
||||||
[pnut-matrix on pnut.io]: https://patter.chat/999
|
[pnut-matrix]: https://patter.chat/999
|
||||||
[#pnut_999:pnut-matrix.dreamfall.space]: https://matrix.to/#/#pnut_999:pnut-matrix.dreamfall.space
|
|
||||||
[LICENSE]: LICENSE
|
[LICENSE]: LICENSE
|
||||||
[^1]: https://github.com/matrix-org/matrix-appservice-irc/issues/1270#issuecomment-849765090
|
[^1]: https://github.com/matrix-org/matrix-appservice-irc/issues/1270#issuecomment-849765090
|
||||||
|
|
917
appservice.py
917
appservice.py
|
@ -1,917 +0,0 @@
|
||||||
import json
|
|
||||||
import requests
|
|
||||||
import logging
|
|
||||||
import re
|
|
||||||
import pnutpy
|
|
||||||
import textwrap
|
|
||||||
import time
|
|
||||||
|
|
||||||
from matrix_client.api import MatrixHttpApi
|
|
||||||
from matrix_client.api import MatrixError, MatrixRequestError
|
|
||||||
from models import Avatars, Rooms, Events, Users, DirectRooms, ControlRooms
|
|
||||||
from database import db_session
|
|
||||||
from sqlalchemy import and_
|
|
||||||
from flask import Flask, jsonify, request, abort
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
app = Flask(__name__)
|
|
||||||
|
|
||||||
@app.errorhandler(404)
|
|
||||||
def not_found(error):
|
|
||||||
return jsonify({'errcode':'COM.MONKEYSTEW.PNUT_NOT_FOUND'}), 404
|
|
||||||
|
|
||||||
@app.errorhandler(403)
|
|
||||||
def forbidden(error):
|
|
||||||
return jsonify({'errcode':'COM.MONKEYSTEW.PNUT_FORBIDDEN'}), 403
|
|
||||||
|
|
||||||
@app.teardown_appcontext
|
|
||||||
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:]
|
|
||||||
channel_id = int(alias_localpart.split('_')[1])
|
|
||||||
|
|
||||||
room = Rooms.query.filter(Rooms.pnut_chan == channel_id).one_or_none()
|
|
||||||
if room is not None:
|
|
||||||
abort(404)
|
|
||||||
|
|
||||||
token = app.config['MATRIX_PNUT_TOKEN']
|
|
||||||
pnutpy.api.add_authorization_token(token)
|
|
||||||
try:
|
|
||||||
channel, meta = pnutpy.api.get_channel(channel_id, include_raw=1)
|
|
||||||
|
|
||||||
if 'is_active' in channel and channel.is_active == False:
|
|
||||||
logger.debug("-channel isn't active-")
|
|
||||||
abort(404)
|
|
||||||
|
|
||||||
channel_settings = {}
|
|
||||||
for item in channel.raw:
|
|
||||||
if item.type == 'io.pnut.core.chat-settings':
|
|
||||||
channel_settings = item.value
|
|
||||||
|
|
||||||
# Matrix sdk doesn't include all details in a single call
|
|
||||||
room = {'room_alias_name': alias_localpart}
|
|
||||||
if 'name' in channel_settings:
|
|
||||||
room['name'] = channel_settings['name']
|
|
||||||
if 'description' in channel_settings:
|
|
||||||
room['topic'] = channel_settings['description']
|
|
||||||
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'
|
|
||||||
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)
|
|
||||||
rdata = r.json()
|
|
||||||
rr = Rooms(
|
|
||||||
room_id=rdata['room_id'],
|
|
||||||
pnut_chan=channel_id,
|
|
||||||
portal=True
|
|
||||||
)
|
|
||||||
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)
|
|
||||||
|
|
||||||
except Exception:
|
|
||||||
logger.exception("-couldn't get the pnut channel-")
|
|
||||||
abort(404)
|
|
||||||
|
|
||||||
return jsonify({})
|
|
||||||
|
|
||||||
@app.route("/_matrix/app/v1/transactions/<transaction>", methods=["PUT"])
|
|
||||||
@app.route("/transactions/<transaction>", methods=["PUT"])
|
|
||||||
def on_receive_events(transaction):
|
|
||||||
|
|
||||||
access_token = request.args.get('access_token', '')
|
|
||||||
if access_token != app.config['MATRIX_HS_TOKEN']:
|
|
||||||
abort(403)
|
|
||||||
|
|
||||||
events = request.get_json()["events"]
|
|
||||||
for event in events:
|
|
||||||
logger.debug(event)
|
|
||||||
|
|
||||||
if app.config['MATRIX_ADMIN_ROOM'] and app.config['MATRIX_ADMIN_ROOM'] == event['room_id']:
|
|
||||||
return on_admin_event(event)
|
|
||||||
|
|
||||||
user = Users.query.filter(Users.matrix_id == event['user_id']).one_or_none()
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
elif event['type'] == 'm.room.member':
|
|
||||||
if 'is_direct' in event['content'] and 'membership' in event['content']:
|
|
||||||
if event['content']['membership'] == "invite" and event['content']['is_direct']:
|
|
||||||
return on_direct_invite(event)
|
|
||||||
|
|
||||||
if 'membership' in event['content']:
|
|
||||||
if event['content']['membership'] == "leave":
|
|
||||||
return on_leave_event(event)
|
|
||||||
|
|
||||||
logger.debug("----room member event----")
|
|
||||||
logger.debug(user)
|
|
||||||
logger.debug(event)
|
|
||||||
|
|
||||||
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-')
|
|
||||||
return
|
|
||||||
|
|
||||||
if 'msgtype' not in event['content']:
|
|
||||||
logger.debug('-unknown message type-')
|
|
||||||
return
|
|
||||||
|
|
||||||
control = ControlRooms.query.filter(ControlRooms.room_id == event['room_id']).one_or_none()
|
|
||||||
if control is not None:
|
|
||||||
return on_control_message(event)
|
|
||||||
|
|
||||||
direct = DirectRooms.query.filter(DirectRooms.room_id == event['room_id']).one_or_none()
|
|
||||||
if direct is not None:
|
|
||||||
return on_direct_message(event, user, direct)
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
embed = [crosspost_raw(event)]
|
|
||||||
evtext, evraw = msg_from_event(event)
|
|
||||||
text = prefix + evtext
|
|
||||||
if len(evraw) > 0:
|
|
||||||
embed.append(evraw)
|
|
||||||
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()
|
|
||||||
|
|
||||||
if user is not None:
|
|
||||||
cctag = re.search('##$', text)
|
|
||||||
if cctag:
|
|
||||||
raw = []
|
|
||||||
cname = get_channel_settings(room.pnut_chan)['name']
|
|
||||||
text = text[:-2]
|
|
||||||
ftext = '\n\n[' + cname + "](https://patter.chat/room.html?channel=" + str(room.pnut_chan) + ")"
|
|
||||||
mtext = textwrap.wrap(text + ftext, 254)
|
|
||||||
if len(mtext) > 1:
|
|
||||||
longpost = {
|
|
||||||
'title': "",
|
|
||||||
'body': text,
|
|
||||||
'tstamp': time.time() * 1000
|
|
||||||
}
|
|
||||||
pretext = textwrap.wrap(text, 100)
|
|
||||||
text = pretext[0]
|
|
||||||
text += "... - https://longpo.st/p/{object_id} - #longpost"
|
|
||||||
raw.append({'type':"nl.chimpnut.blog.post", 'value': longpost})
|
|
||||||
|
|
||||||
text += ftext
|
|
||||||
r, meta = pnutpy.api.create_post(data={'text': text, 'raw': raw})
|
|
||||||
|
|
||||||
except pnutpy.errors.PnutAuthAPIException:
|
|
||||||
logger.exception('-unable to post to pnut channel-')
|
|
||||||
return
|
|
||||||
|
|
||||||
except Exception:
|
|
||||||
logger.exception('-something bad happened here-')
|
|
||||||
return
|
|
||||||
|
|
||||||
def msg_from_event(event):
|
|
||||||
text = None
|
|
||||||
raw = {}
|
|
||||||
if event['content']['msgtype'] == 'm.text' or event['content']['msgtype'] == 'm.notice':
|
|
||||||
text = event['content']['body']
|
|
||||||
|
|
||||||
elif event['content']['msgtype'] == 'm.emote':
|
|
||||||
text = "* " + event['content']['body']
|
|
||||||
|
|
||||||
elif event['content']['msgtype'] == 'm.image':
|
|
||||||
text = event['content']['body'] + "\n"
|
|
||||||
text += app.config['MATRIX_URL'] + '/_matrix/media/r0/download/' + event['content']['url'][6:]
|
|
||||||
raw = raw_from_event(event)
|
|
||||||
|
|
||||||
elif event['content']['msgtype'] == 'm.video':
|
|
||||||
text = event['content']['body'] + "\n"
|
|
||||||
text += app.config['MATRIX_URL'] + '/_matrix/media/r0/download/' + event['content']['url'][6:]
|
|
||||||
|
|
||||||
elif event['content']['msgtype'] == 'm.audio':
|
|
||||||
text = event['content']['body'] + "\n"
|
|
||||||
text += app.config['MATRIX_URL'] + '/_matrix/media/r0/download/' + event['content']['url'][6:]
|
|
||||||
|
|
||||||
elif event['content']['msgtype'] == 'm.file':
|
|
||||||
text = event['content']['body'] + "\n"
|
|
||||||
text += app.config['MATRIX_URL'] + '/_matrix/media/r0/download/' + event['content']['url'][6:]
|
|
||||||
|
|
||||||
else:
|
|
||||||
logger.debug('-unknown msg type- ' + event['content']['msgtype'])
|
|
||||||
return
|
|
||||||
|
|
||||||
return text, raw
|
|
||||||
|
|
||||||
def crosspost_raw(event):
|
|
||||||
cross_profile = {'username': event['user_id']}
|
|
||||||
matrix_profile = get_profile(event['user_id'])
|
|
||||||
if "avatar_url" in matrix_profile:
|
|
||||||
cross_profile['avatar_image'] = app.config['MATRIX_URL'] + '/_matrix/media/r0/download/' + matrix_profile['avatar_url'][6:]
|
|
||||||
|
|
||||||
embed = {
|
|
||||||
'type': "io.pnut.core.crosspost",
|
|
||||||
'value': {
|
|
||||||
'canonical_url': "https://matrix.to/#/" + event['room_id'] + "/" + event['event_id'] + ":" + app.config['MATRIX_DOMAIN'],
|
|
||||||
'source': {
|
|
||||||
'name': "matrix",
|
|
||||||
'url': "https://matrix.org"
|
|
||||||
},
|
|
||||||
'user': cross_profile
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return embed
|
|
||||||
|
|
||||||
def raw_from_event(event):
|
|
||||||
url = app.config['MATRIX_URL'] + '/_matrix/media/r0/download/' + event['content']['url'][6:]
|
|
||||||
|
|
||||||
if event['content']['msgtype'] == 'm.image':
|
|
||||||
value = {'type': "photo", 'version': "1.0"}
|
|
||||||
value['url'] = url
|
|
||||||
value['title'] = event['content']['body']
|
|
||||||
if 'info' in event['content']:
|
|
||||||
if 'w' in event['content']['info']:
|
|
||||||
value['width'] = event['content']['info']['w']
|
|
||||||
else:
|
|
||||||
value['width'] = 200
|
|
||||||
if 'h' in event['content']['info']:
|
|
||||||
value['height'] = event['content']['info']['h']
|
|
||||||
else:
|
|
||||||
value['height'] = 200
|
|
||||||
if 'thumbnail_info' in event['content']['info']:
|
|
||||||
if 'thumbnail_url' in event['content']['info']:
|
|
||||||
value['thumbnail_url'] = app.config['MATRIX_URL'] + '/_matrix/media/r0/download/' + event['content']['info']['thumbnail_url'][6:]
|
|
||||||
if 'w' in event['content']['info']['thumbnail_info']:
|
|
||||||
value['thumbnail_width'] = event['content']['info']['thumbnail_info']['w']
|
|
||||||
else:
|
|
||||||
value['thumbnail_width'] = 200
|
|
||||||
if 'h' in event['content']['info']['thumbnail_info']:
|
|
||||||
value['thumbnail_height'] = event['content']['info']['thumbnail_info']['h']
|
|
||||||
else:
|
|
||||||
value['thumbnail_height'] = 200
|
|
||||||
|
|
||||||
elif event['content']['msgtype'] == 'm.video':
|
|
||||||
# TODO: Need to sort out the oembed for this media type
|
|
||||||
value = {'type': "html5video", 'version': "1.0"}
|
|
||||||
source = {'url': url}
|
|
||||||
value['title'] = event['content']['body']
|
|
||||||
if 'info' in event['content']:
|
|
||||||
value['width'] = event['content']['info']['w']
|
|
||||||
value['height'] = event['content']['info']['h']
|
|
||||||
source['type'] = event['content']['info']['mimetype']
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
value['sources'] = [source]
|
|
||||||
|
|
||||||
elif event['content']['msgtype'] == 'm.audio':
|
|
||||||
# TODO: Need to sort out the oembed for this media type
|
|
||||||
value = {'type': "audio", 'version': "1.0"}
|
|
||||||
return None
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
|
|
||||||
return {'type': "io.pnut.core.oembed", 'value': value}
|
|
||||||
|
|
||||||
def delete_message(event, user):
|
|
||||||
|
|
||||||
# TODO: should there be moderator handled redactions?
|
|
||||||
|
|
||||||
if user is not None:
|
|
||||||
token = user.pnut_user_token
|
|
||||||
else:
|
|
||||||
token = app.config['MATRIX_PNUT_TOKEN']
|
|
||||||
pnutpy.api.add_authorization_token(token)
|
|
||||||
|
|
||||||
e = Events.query.filter(and_(Events.event_id == event['redacts'], Events.deleted == False)).one_or_none()
|
|
||||||
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
|
|
||||||
db_session.commit()
|
|
||||||
|
|
||||||
except pnutpy.errors.PnutPermissionDenied as e:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def get_profile(userid):
|
|
||||||
url = app.config['MATRIX_HOST'] + "/_matrix/client/r0/profile/" + userid
|
|
||||||
r = requests.get(url)
|
|
||||||
if r.status_code == 200:
|
|
||||||
return json.loads(r.text)
|
|
||||||
return userid
|
|
||||||
|
|
||||||
def get_channel_settings(channel_id):
|
|
||||||
channel_settings = {}
|
|
||||||
try:
|
|
||||||
channel, meta = pnutpy.api.get_channel(channel_id, include_raw=1)
|
|
||||||
|
|
||||||
for item in channel.raw:
|
|
||||||
if item.type == 'io.pnut.core.chat-settings':
|
|
||||||
channel_settings = item.value
|
|
||||||
|
|
||||||
except Exception:
|
|
||||||
logger.exception('-unable to get channel settings-')
|
|
||||||
|
|
||||||
return channel_settings
|
|
||||||
|
|
||||||
def create_room(channel, user):
|
|
||||||
channel_settings = {}
|
|
||||||
for item in channel.raw:
|
|
||||||
if item.type == 'io.pnut.core.chat-settings':
|
|
||||||
channel_settings = item.value
|
|
||||||
|
|
||||||
# Matrix sdk doesn't include all details in a single call
|
|
||||||
room = {'room_alias_name': app.config['MATRIX_PNUT_PREFIX'] + channel.id}
|
|
||||||
logger.debug(user)
|
|
||||||
logger.debug(room)
|
|
||||||
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.public:
|
|
||||||
room['preset'] = 'public_chat'
|
|
||||||
room['visibility'] = 'public'
|
|
||||||
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'
|
|
||||||
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)
|
|
||||||
rdata = r.json()
|
|
||||||
rr = Rooms(
|
|
||||||
room_id=rdata['room_id'],
|
|
||||||
pnut_chan=channel.id,
|
|
||||||
portal=True
|
|
||||||
)
|
|
||||||
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)
|
|
||||||
|
|
||||||
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'],
|
|
||||||
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:
|
|
||||||
if msg[0] == 'help':
|
|
||||||
if len(msg) > 1:
|
|
||||||
matrix_api.send_message(event['room_id'], cmd_admin_help(msg[1]))
|
|
||||||
else:
|
|
||||||
matrix_api.send_message(event['room_id'], cmd_admin_help())
|
|
||||||
|
|
||||||
elif msg[0] == 'list':
|
|
||||||
matrix_api.send_message(event['room_id'], cmd_admin_list())
|
|
||||||
|
|
||||||
elif msg[0] == 'unlink':
|
|
||||||
if len(msg) > 1:
|
|
||||||
matrix_api.send_message(event['room_id'], cmd_admin_unlink(msg[1]))
|
|
||||||
else:
|
|
||||||
matrix_api.send_message(event['room_id'], cmd_admin_help('unlink'))
|
|
||||||
|
|
||||||
elif msg[0] == 'link':
|
|
||||||
if len(msg) > 2:
|
|
||||||
matrix_api.send_message(event['room_id'], cmd_admin_link(msg[1], msg[2]))
|
|
||||||
else:
|
|
||||||
matrix_api.send_message(event['room_id'], cmd_admin_help('link'))
|
|
||||||
|
|
||||||
except Exception:
|
|
||||||
errmsg = "- on_admin_event -"
|
|
||||||
logger.exception(errmsg)
|
|
||||||
|
|
||||||
return jsonify({})
|
|
||||||
|
|
||||||
def cmd_admin_help(cmd=None):
|
|
||||||
|
|
||||||
help_usage = "help [command]"
|
|
||||||
help_desc = "Show information about available commands."
|
|
||||||
list_usage = "list"
|
|
||||||
list_desc = "List the rooms currently linked with pnut.io."
|
|
||||||
unlink_usage = "unlink <room_id> | <pnut_channel_id>"
|
|
||||||
unlink_desc = "Unlink a room between Matrix and pnut.io."
|
|
||||||
link_usage = "link <room_id> <pnut_channel_id>"
|
|
||||||
link_desc = "Link a room between Matrix and pnut.io."
|
|
||||||
|
|
||||||
if cmd == 'help':
|
|
||||||
text = "usage: " + help_usage + "\n\n"
|
|
||||||
text += help_desc
|
|
||||||
if cmd == 'list':
|
|
||||||
text = "usage: " + list_usage + "\n\n"
|
|
||||||
text += list_desc
|
|
||||||
elif cmd == 'unlink':
|
|
||||||
text = "usage: " + unlink_usage + "\n\n"
|
|
||||||
text += unlink_desc
|
|
||||||
elif cmd == 'link':
|
|
||||||
text = "usage: " + link_usage + "\n\n"
|
|
||||||
text += link_desc
|
|
||||||
else:
|
|
||||||
text = "The following commands are available:\n\n"
|
|
||||||
text += help_usage + "\n"
|
|
||||||
text += list_usage + "\n"
|
|
||||||
text += unlink_usage + "\n"
|
|
||||||
text += link_usage + "\n"
|
|
||||||
|
|
||||||
return text
|
|
||||||
|
|
||||||
def cmd_admin_list():
|
|
||||||
text = ""
|
|
||||||
rooms = Rooms.query.all()
|
|
||||||
|
|
||||||
if len(rooms) > 0:
|
|
||||||
text = "ID\tMATRIX ID\tPNUT CHANNEL\n"
|
|
||||||
else:
|
|
||||||
text = " - no rooms are currently linked - \n"
|
|
||||||
|
|
||||||
for room in rooms:
|
|
||||||
text += str(room.id) + '\t'
|
|
||||||
text += room.room_id + '\t\t\t\t\t'
|
|
||||||
text += str(room.pnut_chan) + '\t'
|
|
||||||
if room.portal:
|
|
||||||
text += "(portal)"
|
|
||||||
text += '\n'
|
|
||||||
|
|
||||||
return text
|
|
||||||
|
|
||||||
def cmd_admin_link(room_id, pnut_chan_id):
|
|
||||||
matrix_api = MatrixHttpApi(app.config['MATRIX_HOST'],
|
|
||||||
token=app.config['MATRIX_AS_TOKEN'])
|
|
||||||
pnutpy.api.add_authorization_token(app.config['MATRIX_PNUT_TOKEN'])
|
|
||||||
|
|
||||||
mrcheck = Rooms.query.filter(Rooms.room_id == room_id).one_or_none()
|
|
||||||
pncheck = Rooms.query.filter(Rooms.pnut_chan == pnut_chan_id).one_or_none()
|
|
||||||
|
|
||||||
if mrcheck is not None or pncheck is not None:
|
|
||||||
return "- room may already be linked -"
|
|
||||||
|
|
||||||
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,
|
|
||||||
portal=False
|
|
||||||
)
|
|
||||||
db_session.add(rec)
|
|
||||||
db_session.commit()
|
|
||||||
|
|
||||||
except pnutpy.errors.PnutAuthAPIException:
|
|
||||||
errmsg = "- unable to subscribe to channel -"
|
|
||||||
logger.exception(errmsg)
|
|
||||||
return errmsg
|
|
||||||
|
|
||||||
except Exception:
|
|
||||||
errmsg = "- unable to link room for some reason -"
|
|
||||||
logger.exception(errmsg)
|
|
||||||
return errmsg
|
|
||||||
|
|
||||||
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 rid.startswith('!'):
|
|
||||||
room = Rooms.query.filter(Rooms.room_id == rid).one_or_none()
|
|
||||||
else:
|
|
||||||
room = Rooms.query.filter(Rooms.pnut_chan == rid).one_or_none()
|
|
||||||
|
|
||||||
if hasattr(room, 'portal'):
|
|
||||||
if room.portal:
|
|
||||||
alias = "#" + app.config['MATRIX_PNUT_PREFIX']
|
|
||||||
alias += str(room.pnut_chan) + ":"
|
|
||||||
alias += app.config['MATRIX_DOMAIN']
|
|
||||||
matrix_api.remove_room_alias(alias)
|
|
||||||
|
|
||||||
# Kicking users needs at least moderator privs
|
|
||||||
members = matrix_api.get_room_members(room.room_id)
|
|
||||||
reason = "Portal room has been unlinked by administrator"
|
|
||||||
for m in members['chunk']:
|
|
||||||
if m['content']['membership'] == 'join' and m['sender'] != app.config['MATRIX_AS_ID']:
|
|
||||||
if room.portal:
|
|
||||||
matrix_api.kick_user(room.room_id, m['sender'], reason=reason)
|
|
||||||
else:
|
|
||||||
if m['sender'].startswith("@" + app.config['MATRIX_PNUT_PREFIX']):
|
|
||||||
matrix_api.kick_user(room.room_id, m['sender'], reason=reason)
|
|
||||||
|
|
||||||
try:
|
|
||||||
channel, meta = pnutpy.api.unsubscribe_channel(room.pnut_chan)
|
|
||||||
matrix_api.leave_room(room.room_id)
|
|
||||||
|
|
||||||
if room is not None:
|
|
||||||
db_session.delete(room)
|
|
||||||
db_session.commit()
|
|
||||||
return "- room has been unlinked -"
|
|
||||||
else:
|
|
||||||
return "- unable to locate room to unlink -"
|
|
||||||
|
|
||||||
except Exception:
|
|
||||||
errmsg = "- error while unlinking room -"
|
|
||||||
logger.exception(errmsg)
|
|
||||||
return errmsg
|
|
||||||
|
|
||||||
def on_direct_invite(event):
|
|
||||||
if event['state_key'] == app.config['MATRIX_AS_ID']:
|
|
||||||
matrix_api = MatrixHttpApi(app.config['MATRIX_HOST'],
|
|
||||||
token=app.config['MATRIX_AS_TOKEN'])
|
|
||||||
dm = ControlRooms(room_id=event['room_id'])
|
|
||||||
|
|
||||||
else:
|
|
||||||
matrix_api = MatrixHttpApi(app.config['MATRIX_HOST'],
|
|
||||||
identity=event['state_key'],
|
|
||||||
token=app.config['MATRIX_AS_TOKEN'])
|
|
||||||
bridge_user = event['state_key']
|
|
||||||
pnut_user = bridge_user.replace(app.config['MATRIX_PNUT_PREFIX'],'').split(':')[0]
|
|
||||||
|
|
||||||
user = Users.query.filter(Users.matrix_id == event['sender']).one_or_none()
|
|
||||||
if user is not None:
|
|
||||||
# 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)
|
|
||||||
|
|
||||||
try:
|
|
||||||
matrix_api.join_room(event['room_id'])
|
|
||||||
|
|
||||||
db_session.add(dm)
|
|
||||||
db_session.commit()
|
|
||||||
|
|
||||||
except Exception:
|
|
||||||
errmsg = "- on_direct_invite -"
|
|
||||||
logger.exception(errmsg)
|
|
||||||
|
|
||||||
return jsonify({})
|
|
||||||
|
|
||||||
def on_leave_event(event):
|
|
||||||
direct = DirectRooms.query.filter(DirectRooms.room_id == event['room_id']).one_or_none()
|
|
||||||
if direct is not None:
|
|
||||||
|
|
||||||
matrix_api = MatrixHttpApi(app.config['MATRIX_HOST'],
|
|
||||||
identity=direct.bridge_user,
|
|
||||||
token=app.config['MATRIX_AS_TOKEN'])
|
|
||||||
try:
|
|
||||||
matrix_api.leave_room(event['room_id'])
|
|
||||||
db_session.delete(direct)
|
|
||||||
db_session.commit()
|
|
||||||
|
|
||||||
except Exception:
|
|
||||||
errmsg = "- on_leave_event -"
|
|
||||||
logger.exception(errmsg)
|
|
||||||
|
|
||||||
control = ControlRooms.query.filter(ControlRooms.room_id == event['room_id']).one_or_none()
|
|
||||||
if control is not None:
|
|
||||||
matrix_api = MatrixHttpApi(app.config['MATRIX_HOST'],
|
|
||||||
token=app.config['MATRIX_AS_TOKEN'])
|
|
||||||
try:
|
|
||||||
matrix_api.leave_room(event['room_id'])
|
|
||||||
db_session.delete(control)
|
|
||||||
db_session.commit()
|
|
||||||
|
|
||||||
except Exception:
|
|
||||||
errmsg = "- on_leave_event -"
|
|
||||||
logger.exception(errmsg)
|
|
||||||
|
|
||||||
return jsonify({})
|
|
||||||
|
|
||||||
def on_direct_message(event, user, room):
|
|
||||||
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)
|
|
||||||
|
|
||||||
embed = [crosspost_raw(event)]
|
|
||||||
evtext, evraw = msg_from_event(event)
|
|
||||||
text = prefix + evtext
|
|
||||||
if len(evraw) > 0:
|
|
||||||
embed.append(evraw)
|
|
||||||
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-')
|
|
||||||
|
|
||||||
except Exception:
|
|
||||||
logger.exception('-something bad happened here-')
|
|
||||||
|
|
||||||
return jsonify({})
|
|
||||||
|
|
||||||
def on_control_message(event):
|
|
||||||
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:
|
|
||||||
if msg[0] == '!help' or msg[0] == 'help':
|
|
||||||
if len(msg) > 1:
|
|
||||||
matrix_api.send_message(event['room_id'], cmd_user_help(msg[1]))
|
|
||||||
else:
|
|
||||||
matrix_api.send_message(event['room_id'], cmd_user_help())
|
|
||||||
|
|
||||||
elif msg[0] == '!auth':
|
|
||||||
matrix_api.send_message(event['room_id'], cmd_user_auth())
|
|
||||||
|
|
||||||
elif msg[0] == '!save':
|
|
||||||
if len(msg) > 1:
|
|
||||||
matrix_api.send_message(event['room_id'], cmd_user_save(event['sender'], msg[1]))
|
|
||||||
else:
|
|
||||||
matrix_api.send_message(event['room_id'], cmd_user_save())
|
|
||||||
|
|
||||||
elif msg[0] == '!drop':
|
|
||||||
matrix_api.send_message(event['room_id'], cmd_user_drop(event['sender']))
|
|
||||||
|
|
||||||
elif msg[0] == '!status':
|
|
||||||
matrix_api.send_message(event['room_id'], cmd_user_status(event['sender']))
|
|
||||||
|
|
||||||
elif msg[0] == '!join':
|
|
||||||
if len(msg) > 1:
|
|
||||||
matrix_api.send_message(event['room_id'], cmd_user_join(event['sender'], msg[1]))
|
|
||||||
else:
|
|
||||||
matrix_api.send_message(event['room_id'], cmd_user_join(event['sender']))
|
|
||||||
|
|
||||||
|
|
||||||
except Exception:
|
|
||||||
errmsg = "- on_direct_message -"
|
|
||||||
logger.exception(errmsg)
|
|
||||||
|
|
||||||
return jsonify({})
|
|
||||||
|
|
||||||
def cmd_user_help(cmd=None):
|
|
||||||
reply = "This is an admin room for controlling your connection to pnut.io\n"
|
|
||||||
reply += "The following commands are available.\n\n"
|
|
||||||
reply += "!auth\t\t\t- Authorize your account on pnut.io\n"
|
|
||||||
reply += "!save <token>\t- Save your pnut.io auth token\n"
|
|
||||||
reply += "!drop\t\t\t- Drop your pnut.io auth token\n"
|
|
||||||
reply += "!status\t\t\t- Status of your pnut.io auth token\n"
|
|
||||||
reply += "!join <channel #>\t- Join a private channel on pnut.io\n"
|
|
||||||
|
|
||||||
return reply
|
|
||||||
|
|
||||||
def cmd_user_auth():
|
|
||||||
reply = "Visit the following URL to authorize your account on pnut.io.\n\n"
|
|
||||||
reply += "https://pnut.io/oauth/authenticate"
|
|
||||||
reply += "?client_id=6SeCRCpCZkmZOKFLFGWbcdAeq2fX1M5t"
|
|
||||||
reply += "&redirect_uri=urn:ietf:wg:oauth:2.0:oob"
|
|
||||||
reply += "&scope=write_post,presence,messages&response_type=token\n\n"
|
|
||||||
reply += "You will be provided with a token that you store with the !save command.\n"
|
|
||||||
|
|
||||||
return reply
|
|
||||||
|
|
||||||
def cmd_user_save(sender=None, token=None):
|
|
||||||
if token is None:
|
|
||||||
reply = "You must provide a token with this command.\n"
|
|
||||||
reply += "!save <token>"
|
|
||||||
return reply
|
|
||||||
|
|
||||||
pnutpy.api.add_authorization_token(token)
|
|
||||||
try:
|
|
||||||
response, meta = pnutpy.api.get_user('me')
|
|
||||||
|
|
||||||
user = Users(
|
|
||||||
matrix_id=sender,
|
|
||||||
pnut_user_id=response.id,
|
|
||||||
pnut_user_token=token
|
|
||||||
)
|
|
||||||
db_session.add(user)
|
|
||||||
db_session.commit()
|
|
||||||
|
|
||||||
reply = "Success! You are now authorized as " + response['username']
|
|
||||||
|
|
||||||
except pnutpy.errors.PnutAuthAPIException as e:
|
|
||||||
reply = "Error! Unable to authorize your account."
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logging.exception('!save')
|
|
||||||
reply = "Error! Problem saving your token."
|
|
||||||
|
|
||||||
return reply
|
|
||||||
|
|
||||||
def cmd_user_drop(sender=None):
|
|
||||||
try:
|
|
||||||
user = Users.query.filter(Users.matrix_id == sender).one_or_none()
|
|
||||||
if user is not None:
|
|
||||||
db_session.delete(user)
|
|
||||||
db_session.commit()
|
|
||||||
reply = "Success! Your auth token has been removed."
|
|
||||||
else:
|
|
||||||
reply = "You do not appear to be registered."
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logging.exception('!drop')
|
|
||||||
reply = "Error! Problem removing your token."
|
|
||||||
|
|
||||||
return reply
|
|
||||||
|
|
||||||
def cmd_user_status(sender=None):
|
|
||||||
try:
|
|
||||||
user = Users.query.filter(Users.matrix_id == sender).one_or_none()
|
|
||||||
if user is None:
|
|
||||||
reply = "You are currently not authorized on pnut.io"
|
|
||||||
else:
|
|
||||||
pnutpy.api.add_authorization_token(user.pnut_user_token)
|
|
||||||
response, meta = pnutpy.api.get_user('me')
|
|
||||||
reply = "You are currently authorized as " + response.username
|
|
||||||
|
|
||||||
except pnutpy.errors.PnutAuthAPIException as e:
|
|
||||||
reply = "You are currently not authorized on pnut.io"
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logging.exception('!status')
|
|
||||||
reply = "Error! There was a problem checking your account."
|
|
||||||
|
|
||||||
return reply
|
|
||||||
|
|
||||||
def cmd_user_join(sender=None, channel_id=None):
|
|
||||||
if channel_id is None:
|
|
||||||
reply = "You must provide a channel id number with this command.\n"
|
|
||||||
reply += "!join <channel #>"
|
|
||||||
return reply
|
|
||||||
|
|
||||||
try:
|
|
||||||
user = Users.query.filter(Users.matrix_id == sender).one_or_none()
|
|
||||||
if user is None:
|
|
||||||
reply = "You are currently not authorized on pnut.io"
|
|
||||||
else:
|
|
||||||
pnutpy.api.add_authorization_token(user.pnut_user_token)
|
|
||||||
channel, meta = pnutpy.api.get_channel(channel_id, include_raw=1)
|
|
||||||
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"
|
|
||||||
|
|
||||||
except pnutpy.errors.PnutPermissionDenied:
|
|
||||||
reply = "You are not authorized for this channel"
|
|
||||||
|
|
||||||
except Exception:
|
|
||||||
logging.exception('!join')
|
|
||||||
reply = "Error! There was a problem joining the channel."
|
|
||||||
|
|
||||||
return reply
|
|
24
database.py
24
database.py
|
@ -1,24 +0,0 @@
|
||||||
from sqlalchemy import create_engine
|
|
||||||
from sqlalchemy.orm import scoped_session, sessionmaker
|
|
||||||
from sqlalchemy.ext.declarative import declarative_base
|
|
||||||
import yaml
|
|
||||||
import os
|
|
||||||
|
|
||||||
configyaml = os.environ.get("CONFIG_FILE")
|
|
||||||
|
|
||||||
with open(configyaml, "rb") as config_file:
|
|
||||||
config = yaml.load(config_file, Loader=yaml.SafeLoader)
|
|
||||||
|
|
||||||
engine = create_engine(config['SERVICE_DB'])
|
|
||||||
db_session = scoped_session(sessionmaker(bind=engine))
|
|
||||||
|
|
||||||
Base = declarative_base()
|
|
||||||
|
|
||||||
Base.query = db_session.query_property()
|
|
||||||
|
|
||||||
def init_db():
|
|
||||||
# import all modules here that might define models so that
|
|
||||||
# they will be registered properly on the metadata. Otherwise
|
|
||||||
# you will have to import them first before calling init_db()
|
|
||||||
import models
|
|
||||||
Base.metadata.create_all(bind=engine)
|
|
44
models.py
44
models.py
|
@ -1,44 +0,0 @@
|
||||||
from sqlalchemy import Column, ForeignKey, Integer, String, Boolean
|
|
||||||
from database import Base
|
|
||||||
|
|
||||||
class Avatars(Base):
|
|
||||||
__tablename__ = 'avatars'
|
|
||||||
id = Column(Integer, primary_key=True)
|
|
||||||
pnut_user = Column(String(250), unique=True)
|
|
||||||
avatar = Column(String(250))
|
|
||||||
|
|
||||||
class Rooms(Base):
|
|
||||||
__tablename__ = 'rooms'
|
|
||||||
id = Column(Integer, primary_key=True)
|
|
||||||
room_id = Column(String(250), unique=True)
|
|
||||||
pnut_chan = Column(Integer, unique=True)
|
|
||||||
portal = Column(Boolean)
|
|
||||||
|
|
||||||
class DirectRooms(Base):
|
|
||||||
__tablename__ = 'direct'
|
|
||||||
id = Column(Integer, primary_key=True)
|
|
||||||
room_id = Column(String(250), unique=True)
|
|
||||||
pnut_chan = Column(Integer, unique=True)
|
|
||||||
bridge_user = Column(String(250))
|
|
||||||
|
|
||||||
class ControlRooms(Base):
|
|
||||||
__tablename__ = 'control'
|
|
||||||
id = Column(Integer, primary_key=True)
|
|
||||||
room_id = Column(String(250), unique=True)
|
|
||||||
|
|
||||||
class Events(Base):
|
|
||||||
__tablename__ = 'events'
|
|
||||||
id = Column(Integer, primary_key=True)
|
|
||||||
event_id = Column(String(250))
|
|
||||||
room_id = Column(String(250))
|
|
||||||
pnut_msg_id = Column(Integer)
|
|
||||||
pnut_user_id = Column(Integer)
|
|
||||||
pnut_chan_id = Column(Integer)
|
|
||||||
deleted = Column(Boolean)
|
|
||||||
|
|
||||||
class Users(Base):
|
|
||||||
__tablename__ = 'users'
|
|
||||||
id = Column(Integer, primary_key=True)
|
|
||||||
matrix_id = Column(String(250))
|
|
||||||
pnut_user_id = Column(Integer)
|
|
||||||
pnut_user_token = Column(String(250))
|
|
|
@ -1,118 +0,0 @@
|
||||||
import logging
|
|
||||||
import yaml
|
|
||||||
import time
|
|
||||||
import pnutpy
|
|
||||||
|
|
||||||
from matrix_bot_api.matrix_bot_api import MatrixBotAPI
|
|
||||||
from matrix_bot_api.mregex_handler import MRegexHandler
|
|
||||||
from matrix_bot_api.mcommand_handler import MCommandHandler
|
|
||||||
from models import Avatars, Rooms, Events, Users
|
|
||||||
from database import db_session
|
|
||||||
from sqlalchemy import and_
|
|
||||||
|
|
||||||
|
|
||||||
def help_cb(room, event):
|
|
||||||
reply = "This is an admin room for controlling your connection to pnut.io\n"
|
|
||||||
reply += "The following commands are available.\n\n"
|
|
||||||
reply += "!auth - Authorize your account on pnut.io\n"
|
|
||||||
reply += "!save <token> - Save your pnut.io auth token\n"
|
|
||||||
reply += "!drop - Drop your pnut.io auth token\n"
|
|
||||||
reply += "!status - Status of your pnut.io auth token\n"
|
|
||||||
room.send_notice(reply)
|
|
||||||
|
|
||||||
def auth_cb(room, event):
|
|
||||||
reply = "Visit the following URL to authorize your account on pnut.io.\n\n"
|
|
||||||
reply += "https://pnut.io/oauth/authenticate"
|
|
||||||
reply += "?client_id=6SeCRCpCZkmZOKFLFGWbcdAeq2fX1M5t"
|
|
||||||
reply += "&redirect_uri=urn:ietf:wg:oauth:2.0:oob"
|
|
||||||
reply += "&scope=write_post,presence,messages&response_type=token\n\n"
|
|
||||||
reply += "You will be provided with a token that you store with the !save command.\n"
|
|
||||||
room.send_notice(reply)
|
|
||||||
|
|
||||||
def save_cb(room, event):
|
|
||||||
args = event['content']['body'].split(' ', maxsplit=1)
|
|
||||||
|
|
||||||
if len(args) < 2:
|
|
||||||
reply = "You must provide a token with this command.\n"
|
|
||||||
reply += "!save <token>"
|
|
||||||
room.send_notice(reply)
|
|
||||||
return
|
|
||||||
|
|
||||||
pnutpy.api.add_authorization_token(args[1])
|
|
||||||
try:
|
|
||||||
response, meta = pnutpy.api.get_user('me')
|
|
||||||
|
|
||||||
user = Users(
|
|
||||||
matrix_id=event['sender'],
|
|
||||||
pnut_user_id=response.id,
|
|
||||||
pnut_user_token=args[1]
|
|
||||||
)
|
|
||||||
db_session.add(user)
|
|
||||||
db_session.commit()
|
|
||||||
|
|
||||||
reply = "Success! You are now authorized as " + response['username']
|
|
||||||
|
|
||||||
except pnutpy.errors.PnutAuthAPIException as e:
|
|
||||||
reply = "Error! Unable to authorize your account."
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logging.exception('!save')
|
|
||||||
reply = "Error! Problem saving your token."
|
|
||||||
|
|
||||||
room.send_notice(reply)
|
|
||||||
|
|
||||||
def drop_cb(room, event):
|
|
||||||
try:
|
|
||||||
user = Users.query.filter(Users.matrix_id == event['sender']).one_or_none()
|
|
||||||
if user is not None:
|
|
||||||
db_session.delete(user)
|
|
||||||
db_session.commit()
|
|
||||||
reply = "Success! Your auth token has been removed."
|
|
||||||
else:
|
|
||||||
reply = "You do not appear to be registered."
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logging.exception('!drop')
|
|
||||||
reply = "Error! Problem removing your token."
|
|
||||||
|
|
||||||
room.send_notice(reply)
|
|
||||||
|
|
||||||
def status_cb(room, event):
|
|
||||||
try:
|
|
||||||
user = Users.query.filter(Users.matrix_id == event['sender']).one_or_none()
|
|
||||||
if user is None:
|
|
||||||
reply = "You are currently not authorized on pnut.io"
|
|
||||||
else:
|
|
||||||
pnutpy.api.add_authorization_token(user.pnut_user_token)
|
|
||||||
response, meta = pnutpy.api.get_user('me')
|
|
||||||
reply = "You are currently authorized as " + response.username
|
|
||||||
|
|
||||||
except pnutpy.errors.PnutAuthAPIException as e:
|
|
||||||
reply = "You are currently not authorized on pnut.io"
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logging.exception('!status')
|
|
||||||
reply = "Error! There was a problem checking your account."
|
|
||||||
|
|
||||||
room.send_notice(reply)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
|
|
||||||
logging.basicConfig(level=logging.INFO)
|
|
||||||
|
|
||||||
with open("config.yaml", "rb") as config_file:
|
|
||||||
config = yaml.load(config_file)
|
|
||||||
|
|
||||||
bot = MatrixBotAPI(config['TBOT_USER'], config['TBOT_PASS'], config['MATRIX_HOST'])
|
|
||||||
|
|
||||||
bot.add_handler(MCommandHandler("help", help_cb))
|
|
||||||
bot.add_handler(MCommandHandler("auth", auth_cb))
|
|
||||||
bot.add_handler(MCommandHandler("save", save_cb))
|
|
||||||
bot.add_handler(MCommandHandler("drop", drop_cb))
|
|
||||||
bot.add_handler(MCommandHandler("status", status_cb))
|
|
||||||
|
|
||||||
bot.start_polling()
|
|
||||||
|
|
||||||
while True:
|
|
||||||
time.sleep(1)
|
|
426
pnut-matrix.py
426
pnut-matrix.py
|
@ -1,426 +0,0 @@
|
||||||
import websocket
|
|
||||||
import threading
|
|
||||||
import time
|
|
||||||
import logging
|
|
||||||
import logging.config
|
|
||||||
import yaml
|
|
||||||
import json
|
|
||||||
import pnutpy
|
|
||||||
import requests
|
|
||||||
import magic
|
|
||||||
import argparse
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
|
|
||||||
from matrix_client.api import MatrixHttpApi
|
|
||||||
from matrix_client.api import MatrixError, MatrixRequestError
|
|
||||||
from models import Avatars, Rooms, Events, DirectRooms, Users
|
|
||||||
from database import db_session, init_db
|
|
||||||
from sqlalchemy import and_
|
|
||||||
from appservice import app
|
|
||||||
|
|
||||||
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)
|
|
||||||
if 'name' in msg.user:
|
|
||||||
logger.debug("name: " + msg.user.name)
|
|
||||||
logger.debug("text: " + msg.content.text)
|
|
||||||
|
|
||||||
# ignore messages posted by the bridge
|
|
||||||
if msg.user.username == config['MATRIX_PNUT_USER']:
|
|
||||||
return
|
|
||||||
|
|
||||||
if msg.source.id == config['PNUTCLIENT_ID']:
|
|
||||||
return
|
|
||||||
|
|
||||||
if meta['channel_type'] == 'io.pnut.core.chat':
|
|
||||||
room = Rooms.query.filter(Rooms.pnut_chan == msg.channel_id).one_or_none()
|
|
||||||
elif meta['channel_type'] == 'io.pnut.core.pm':
|
|
||||||
room = DirectRooms.query.filter(DirectRooms.pnut_chan == msg.channel_id).one_or_none()
|
|
||||||
if room is None:
|
|
||||||
# Do do an invite from the bridge user?
|
|
||||||
logger.debug('new invite?')
|
|
||||||
# create room and included matrix recpient
|
|
||||||
# subscribed_user_ids from meta
|
|
||||||
logger.debug(meta['subscribed_user_ids'])
|
|
||||||
pnut_user = matrix_id_from_pnut(msg.user.username)
|
|
||||||
profile = get_matrix_profile(pnut_user)
|
|
||||||
if not profile:
|
|
||||||
new_matrix_user(msg.user.username)
|
|
||||||
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:
|
|
||||||
room = new_room(pnut_user, invitees, msg.channel_id)
|
|
||||||
else:
|
|
||||||
room = None
|
|
||||||
|
|
||||||
logger.debug(room)
|
|
||||||
if room is None:
|
|
||||||
logger.debug('-not_mapped-')
|
|
||||||
return
|
|
||||||
|
|
||||||
matrix_id = matrix_id_from_pnut(msg.user.username)
|
|
||||||
matrix_api = MatrixHttpApi(config['MATRIX_HOST'],
|
|
||||||
token=config['MATRIX_AS_TOKEN'],
|
|
||||||
identity=matrix_id)
|
|
||||||
|
|
||||||
profile = get_matrix_profile(matrix_id)
|
|
||||||
if not profile:
|
|
||||||
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)
|
|
||||||
logger.debug('-set_avatar-')
|
|
||||||
|
|
||||||
# members = matrix_api.get_room_members(room.room_id)
|
|
||||||
# logger.debug(members)
|
|
||||||
# join_room(room.room_id, config['MATRIX_AS_ID'])
|
|
||||||
# TODO: sort out room invite and join logic
|
|
||||||
join_room(room.room_id, matrix_id)
|
|
||||||
|
|
||||||
if 'content' in msg:
|
|
||||||
text = msg.content.text + "\n"
|
|
||||||
ts = int(time.time()) * 1000
|
|
||||||
|
|
||||||
lnktext = ""
|
|
||||||
for link in msg.content.entities.links:
|
|
||||||
if 'title' in link:
|
|
||||||
lnktext += link.title + "\n"
|
|
||||||
if 'url' in link:
|
|
||||||
lnktext += link.url + "\n"
|
|
||||||
|
|
||||||
if len(lnktext) > 0:
|
|
||||||
text += "\n" + lnktext
|
|
||||||
|
|
||||||
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,
|
|
||||||
pnut_chan_id=msg.channel_id,
|
|
||||||
deleted=False)
|
|
||||||
db_session.add(event)
|
|
||||||
db_session.commit()
|
|
||||||
|
|
||||||
if 'raw' in msg:
|
|
||||||
logger.debug('-handle media uploads-')
|
|
||||||
new_media(room.room_id, msg)
|
|
||||||
|
|
||||||
def new_media(room_id, msg):
|
|
||||||
matrix_id = matrix_id_from_pnut(msg.user.username)
|
|
||||||
matrix_api = MatrixHttpApi(config['MATRIX_HOST'],
|
|
||||||
token=config['MATRIX_AS_TOKEN'],
|
|
||||||
identity=matrix_id)
|
|
||||||
ts = int(time.time()) * 1000
|
|
||||||
|
|
||||||
if 'io.pnut.core.oembed' in msg.raw:
|
|
||||||
|
|
||||||
for oembed in msg.raw['io.pnut.core.oembed']:
|
|
||||||
info = {}
|
|
||||||
|
|
||||||
if oembed.type == 'photo':
|
|
||||||
msgtype = 'm.image'
|
|
||||||
dl_url = oembed.url
|
|
||||||
info['h'] = oembed.height
|
|
||||||
info['w'] = oembed.width
|
|
||||||
elif oembed.type == 'audio':
|
|
||||||
logger.debug("* recieved audio attachment")
|
|
||||||
continue
|
|
||||||
elif oembed.type == 'video':
|
|
||||||
logger.debug("* recieved video attachment")
|
|
||||||
continue
|
|
||||||
elif oembed.type == 'html5video':
|
|
||||||
logger.debug("* recieved html5 video attachment")
|
|
||||||
continue
|
|
||||||
elif oembed.type == 'rich':
|
|
||||||
logger.debug("* recieved video attachment")
|
|
||||||
continue
|
|
||||||
else:
|
|
||||||
logger.debug("* recieved unknown attachment")
|
|
||||||
continue
|
|
||||||
|
|
||||||
dl = requests.get(dl_url, stream=True)
|
|
||||||
dl.raise_for_status()
|
|
||||||
with magic.Magic(flags=magic.MAGIC_MIME_TYPE) as m:
|
|
||||||
info['mimetype'] = m.id_buffer(dl.content)
|
|
||||||
info['size'] = len(dl.content)
|
|
||||||
ul = matrix_api.media_upload(dl.content, info['mimetype'])
|
|
||||||
|
|
||||||
if 'title' in oembed:
|
|
||||||
title = oembed.title
|
|
||||||
else:
|
|
||||||
title = ""
|
|
||||||
|
|
||||||
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,
|
|
||||||
pnut_chan_id=msg.channel_id,
|
|
||||||
deleted=False)
|
|
||||||
db_session.add(event)
|
|
||||||
db_session.commit()
|
|
||||||
|
|
||||||
def delete_message(msg):
|
|
||||||
matrix_id = matrix_id_from_pnut(msg.user.username)
|
|
||||||
matrix_api = MatrixHttpApi(config['MATRIX_HOST'],
|
|
||||||
token=config['MATRIX_AS_TOKEN'],
|
|
||||||
identity=matrix_id)
|
|
||||||
|
|
||||||
events = Events.query.filter(and_(Events.pnut_msg_id == msg.id, Events.deleted == False)).all()
|
|
||||||
for event in events:
|
|
||||||
matrix_api.redact_event(event.room_id, event.event_id)
|
|
||||||
event.deleted = True
|
|
||||||
db_session.commit()
|
|
||||||
|
|
||||||
def matrix_id_from_pnut(username):
|
|
||||||
return "@" + config['MATRIX_PNUT_PREFIX'] + username + ":" + config['MATRIX_DOMAIN']
|
|
||||||
|
|
||||||
def matrix_display_from_pnut(user):
|
|
||||||
if 'name' in user:
|
|
||||||
display = user.name + " (@" + user.username + ")"
|
|
||||||
else:
|
|
||||||
display = "@" + user.username
|
|
||||||
return display
|
|
||||||
# return user.username + " (pnut)"
|
|
||||||
|
|
||||||
def get_matrix_profile(matrix_id):
|
|
||||||
url = matrix_url + '/profile/' + matrix_id
|
|
||||||
r = requests.get(url)
|
|
||||||
|
|
||||||
if r.status_code == 200:
|
|
||||||
return r.json()
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
|
|
||||||
def set_matrix_display(user):
|
|
||||||
matrix_id = matrix_id_from_pnut(user.username)
|
|
||||||
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'],
|
|
||||||
token=config['MATRIX_AS_TOKEN'],
|
|
||||||
identity=matrix_id)
|
|
||||||
|
|
||||||
dl = requests.get(user.content.avatar_image.url, stream=True)
|
|
||||||
dl.raise_for_status()
|
|
||||||
with magic.Magic(flags=magic.MAGIC_MIME_TYPE) as m:
|
|
||||||
mtype = m.id_buffer(dl.content)
|
|
||||||
ul = matrix_api.media_upload(dl.content, mtype)
|
|
||||||
|
|
||||||
try:
|
|
||||||
matrix_api.set_avatar_url(matrix_id, ul['content_uri'])
|
|
||||||
avatar = Avatars.query.filter(Avatars.pnut_user == user.username).one_or_none()
|
|
||||||
if avatar is None:
|
|
||||||
avatar = Avatars(pnut_user=user.username, avatar=user.content.avatar_image.url)
|
|
||||||
db_session.add(avatar)
|
|
||||||
else:
|
|
||||||
avatar.avatar = user.content.avatar_image.url
|
|
||||||
db_session.commit()
|
|
||||||
|
|
||||||
except MatrixRequestError:
|
|
||||||
logger.exception('failed to set user avatar')
|
|
||||||
|
|
||||||
def new_matrix_user(username):
|
|
||||||
matrix_api = MatrixHttpApi(config['MATRIX_HOST'],
|
|
||||||
token=config['MATRIX_AS_TOKEN'])
|
|
||||||
data = {
|
|
||||||
'type': 'm.login.application_service',
|
|
||||||
'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'],
|
|
||||||
token=config['MATRIX_AS_TOKEN'])
|
|
||||||
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)
|
|
||||||
matrix_api.join_room(room_id)
|
|
||||||
else:
|
|
||||||
logger.exception('failed to join room')
|
|
||||||
|
|
||||||
logger.debug('-room_join-')
|
|
||||||
|
|
||||||
def new_room(pnut_user, invitees, chan):
|
|
||||||
dr = None
|
|
||||||
matrix_api = MatrixHttpApi(config['MATRIX_HOST'],
|
|
||||||
token=config['MATRIX_AS_TOKEN'],
|
|
||||||
identity=pnut_user)
|
|
||||||
url = matrix_url + '/createRoom'
|
|
||||||
params = {"access_token": config['MATRIX_AS_TOKEN'], "user_id": pnut_user}
|
|
||||||
content = {"visibility": "private", "is_direct": True, "invite": invitees}
|
|
||||||
headers = {"Content-Type": "application/json"}
|
|
||||||
r = requests.post(url, headers=headers, params=params,
|
|
||||||
data=json.dumps(content))
|
|
||||||
response = r.json()
|
|
||||||
for bridge_user in invitees:
|
|
||||||
dr = DirectRooms(room_id=response['room_id'],
|
|
||||||
bridge_user=pnut_user, pnut_chan=chan)
|
|
||||||
logger.debug(dr)
|
|
||||||
db_session.add(dr)
|
|
||||||
db_session.commit()
|
|
||||||
|
|
||||||
return dr
|
|
||||||
|
|
||||||
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']:
|
|
||||||
|
|
||||||
if msg['meta']['channel_type'] not in ['io.pnut.core.chat',
|
|
||||||
'io.pnut.core.pm']:
|
|
||||||
return
|
|
||||||
|
|
||||||
for d_item in msg['data']:
|
|
||||||
pmsg = pnutpy.models.Message.from_response_data(d_item)
|
|
||||||
|
|
||||||
if 'is_deleted' in msg['meta']:
|
|
||||||
if msg['meta']['is_deleted']:
|
|
||||||
logger.debug("message: delete")
|
|
||||||
delete_message(pmsg)
|
|
||||||
else:
|
|
||||||
new_message(pmsg, msg['meta'])
|
|
||||||
|
|
||||||
def on_error(ws, error):
|
|
||||||
logger.error("on_error: !!! ERROR !!!")
|
|
||||||
logger.error(error)
|
|
||||||
|
|
||||||
def on_close(ws):
|
|
||||||
logger.debug("on_close: ### CLOSED ###")
|
|
||||||
|
|
||||||
def on_open(ws):
|
|
||||||
|
|
||||||
def run(*args):
|
|
||||||
while not _shutdown.isSet() and not _reconnect.isSet():
|
|
||||||
time.sleep(3)
|
|
||||||
try:
|
|
||||||
ws.send(".")
|
|
||||||
except websocket._exceptions.WebSocketConnectionClosedException:
|
|
||||||
logger.debug('websocket closed exception caught...')
|
|
||||||
_reconnect.set()
|
|
||||||
|
|
||||||
time.sleep(1)
|
|
||||||
logger.debug("*** terminate thread ***")
|
|
||||||
|
|
||||||
t = threading.Thread(target=run)
|
|
||||||
t.start()
|
|
||||||
|
|
||||||
def wsthreader(threadfunc):
|
|
||||||
|
|
||||||
def wrapper():
|
|
||||||
|
|
||||||
while not _shutdown.isSet():
|
|
||||||
_reconnect.clear()
|
|
||||||
logger.debug('threadfunc start...')
|
|
||||||
running = threadfunc()
|
|
||||||
logger.debug('threadfunc end...')
|
|
||||||
if running:
|
|
||||||
time.sleep(5)
|
|
||||||
else:
|
|
||||||
_shutdown.set()
|
|
||||||
logger.debug('*** thread stopped ***')
|
|
||||||
|
|
||||||
return wrapper
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
|
|
||||||
|
|
||||||
a_parser = argparse.ArgumentParser()
|
|
||||||
a_parser.add_argument(
|
|
||||||
'-d', action='store_true', dest='debug',
|
|
||||||
help="debug logging"
|
|
||||||
)
|
|
||||||
# TODO: solve the database.py problem and enable this
|
|
||||||
# a_parser.add_argument(
|
|
||||||
# '-c', '--config', default="config.yaml",
|
|
||||||
# help="configuration file"
|
|
||||||
# )
|
|
||||||
args = a_parser.parse_args()
|
|
||||||
|
|
||||||
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'
|
|
||||||
matrix_url = config['MATRIX_HOST'] + '/_matrix/client/r0'
|
|
||||||
|
|
||||||
# setup the database connection
|
|
||||||
init_db()
|
|
||||||
|
|
||||||
# setup the websocket connection
|
|
||||||
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))
|
|
||||||
wst.start()
|
|
||||||
|
|
||||||
# setup the matrix app service
|
|
||||||
if config['MATRIX_ADMIN_ROOM']:
|
|
||||||
logger.debug("- sould join admin room -")
|
|
||||||
join_room(config['MATRIX_ADMIN_ROOM'], config['MATRIX_AS_ID'])
|
|
||||||
app.config.update(config)
|
|
||||||
app.run(host=config['LISTEN_HOST'], port=config['LISTEN_PORT'])
|
|
||||||
|
|
||||||
logger.info('!! shutdown initiated !!')
|
|
||||||
_shutdown.set()
|
|
||||||
ws.close()
|
|
||||||
time.sleep(2)
|
|
38
pyproject.toml
Normal file
38
pyproject.toml
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
[project]
|
||||||
|
name = "pnut-matrix"
|
||||||
|
version = "1.3.99"
|
||||||
|
authors = [
|
||||||
|
{ name="Morgan McMillian", email="morgan@mcmillian.dev" },
|
||||||
|
]
|
||||||
|
description = "A matrix appservice bridge for pnut.io"
|
||||||
|
readme = "README.md"
|
||||||
|
requires-python = ">=3.10"
|
||||||
|
classifiers = [
|
||||||
|
"Programming Language :: Python :: 3",
|
||||||
|
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
|
||||||
|
"Operating System :: OS Independent",
|
||||||
|
]
|
||||||
|
dependencies = [
|
||||||
|
"pyyaml",
|
||||||
|
"requests",
|
||||||
|
"Flask[async]",
|
||||||
|
"pnutpy>=0.5.0",
|
||||||
|
"filemagic",
|
||||||
|
"mautrix>=0.20.6,<0.21",
|
||||||
|
"websockets",
|
||||||
|
"asyncclick",
|
||||||
|
"peewee",
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.urls]
|
||||||
|
"Homepage" = "https://git.dreamfall.space/spacenerdmo/pnut-matrix"
|
||||||
|
"Bug Tracker" = "https://git.dreamfall.space/spacenerdmo/pnut-matrix/issues"
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["setuptools>=61.0"]
|
||||||
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
|
[project.entry-points.console_scripts]
|
||||||
|
pnutservice = "pnut_matrix.pnutservice:main"
|
||||||
|
appservice = "pnut_matrix.appservice:main"
|
||||||
|
ascmd = "pnut_matrix.cmd:cmd"
|
|
@ -1,8 +0,0 @@
|
||||||
pyyaml
|
|
||||||
requests
|
|
||||||
matrix-client==0.3.2
|
|
||||||
Flask
|
|
||||||
pnutpy
|
|
||||||
sqlalchemy
|
|
||||||
websocket-client
|
|
||||||
filemagic
|
|
7
run.sh
Executable file
7
run.sh
Executable file
|
@ -0,0 +1,7 @@
|
||||||
|
!#/bin/bash
|
||||||
|
|
||||||
|
pnutservice -c /data/config.yml &
|
||||||
|
matrixappsvc -c /data/config.yml &
|
||||||
|
wait -n
|
||||||
|
|
||||||
|
exit $?
|
959
src/pnut_matrix/appservice.py
Normal file
959
src/pnut_matrix/appservice.py
Normal file
|
@ -0,0 +1,959 @@
|
||||||
|
import json
|
||||||
|
import yaml
|
||||||
|
import requests
|
||||||
|
import logging
|
||||||
|
import logging.config
|
||||||
|
import re
|
||||||
|
import pnutpy
|
||||||
|
import textwrap
|
||||||
|
import time
|
||||||
|
import os
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
from mautrix.client import ClientAPI
|
||||||
|
from mautrix.types import *
|
||||||
|
from pnut_matrix.models import *
|
||||||
|
from flask import Flask, jsonify, request, abort
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
@app.errorhandler(404)
|
||||||
|
def not_found(error):
|
||||||
|
return jsonify({'errcode':'PNUT_NOT_FOUND'}), 404
|
||||||
|
|
||||||
|
@app.errorhandler(403)
|
||||||
|
def forbidden(error):
|
||||||
|
return jsonify({'errcode':'PNUT_FORBIDDEN'}), 403
|
||||||
|
|
||||||
|
@app.route("/_matrix/app/v1/rooms/<alias>")
|
||||||
|
@app.route("/rooms/<alias>")
|
||||||
|
async def query_alias(alias):
|
||||||
|
logging.debug("--- query alias ---")
|
||||||
|
alias_localpart = alias.split(":")[0][1:]
|
||||||
|
channel_id = int(alias_localpart.split('_')[1])
|
||||||
|
|
||||||
|
room = PnutChannels.select().where(PnutChannels.pnut_chan ==
|
||||||
|
channel_id).first()
|
||||||
|
if room is not None:
|
||||||
|
abort(404)
|
||||||
|
|
||||||
|
token = app.config['MATRIX_PNUT_TOKEN']
|
||||||
|
pnutpy.api.add_authorization_token(token)
|
||||||
|
try:
|
||||||
|
logging.debug("---- getting the channel ----")
|
||||||
|
channel, meta = pnutpy.api.get_channel(channel_id,
|
||||||
|
include_channel_raw=1)
|
||||||
|
|
||||||
|
if 'is_active' in channel and channel.is_active == False:
|
||||||
|
logging.debug("-channel isn't active-")
|
||||||
|
abort(404)
|
||||||
|
|
||||||
|
if 'io.pnut.core.chat-settings' in channel.raw:
|
||||||
|
for setting in channel.raw['io.pnut.core.chat-settings']:
|
||||||
|
if 'name' in setting:
|
||||||
|
name = f"🥜 {setting['name']}"
|
||||||
|
else:
|
||||||
|
name = f"🥜 channel {channel.id}"
|
||||||
|
if 'description' in setting:
|
||||||
|
topic = setting['description']['text']
|
||||||
|
else:
|
||||||
|
topic = None
|
||||||
|
|
||||||
|
matrix_api = ClientAPI(app.config['MATRIX_AS_ID'],
|
||||||
|
base_url=app.config['MATRIX_HOST'],
|
||||||
|
token=app.config['MATRIX_AS_TOKEN'])
|
||||||
|
|
||||||
|
if channel.acl.read.public:
|
||||||
|
visibility = RoomDirectoryVisibility.PUBLIC
|
||||||
|
preset = RoomCreatePreset.PUBLIC
|
||||||
|
else:
|
||||||
|
visibility = RoomDirectoryVisibility.PRIVATE
|
||||||
|
preset = RoomCreatePreset.PRIVATE
|
||||||
|
room_id = await matrix_api.create_room(alias_localpart,
|
||||||
|
visibility=visibility,
|
||||||
|
preset=preset,
|
||||||
|
name=name,
|
||||||
|
topic=topic)
|
||||||
|
|
||||||
|
if not channel.you_subscribed:
|
||||||
|
pnutpy.api.subscribe_channel(channel_id)
|
||||||
|
|
||||||
|
room = PnutChannels(room_id=room_id, pnut_chan=channel_id)
|
||||||
|
room.save()
|
||||||
|
logging.debug(f'-created new room for channel {room.pnut_chan}-')
|
||||||
|
logging.debug(room.room_id)
|
||||||
|
|
||||||
|
except pnutpy.errors.PnutPermissionDenied:
|
||||||
|
logging.debug("-permission denied-")
|
||||||
|
abort(401)
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
logging.exception("-couldn't get the pnut channel-")
|
||||||
|
abort(404)
|
||||||
|
|
||||||
|
return jsonify({})
|
||||||
|
|
||||||
|
@app.route("/_matrix/app/v1/transactions/<transaction>", methods=["PUT"])
|
||||||
|
@app.route("/transactions/<transaction>", methods=["PUT"])
|
||||||
|
async def on_receive_events(transaction):
|
||||||
|
|
||||||
|
access_token = request.args.get('access_token', '')
|
||||||
|
if access_token != app.config['MATRIX_HS_TOKEN']:
|
||||||
|
abort(403)
|
||||||
|
|
||||||
|
events = request.get_json()["events"]
|
||||||
|
for event in events:
|
||||||
|
logging.debug('-----event-----')
|
||||||
|
logging.debug(event)
|
||||||
|
logging.debug('~~~~~~~~~~~~~~~')
|
||||||
|
|
||||||
|
if (app.config['MATRIX_ADMIN_ROOM'] and
|
||||||
|
app.config['MATRIX_ADMIN_ROOM'] == event['room_id']):
|
||||||
|
logging.debug('>----on_admin_event----<')
|
||||||
|
await on_admin_event(event)
|
||||||
|
return jsonify({})
|
||||||
|
|
||||||
|
user = PnutUsers.select().where(PnutUsers.matrix_id ==
|
||||||
|
event['sender']).first()
|
||||||
|
|
||||||
|
if event['type'] == 'm.room.message':
|
||||||
|
logging.debug('>----new_message----<')
|
||||||
|
await new_message(event, user)
|
||||||
|
|
||||||
|
elif event['type'] == 'm.room.redaction':
|
||||||
|
logging.debug('>----delete_message----<')
|
||||||
|
delete_message(event, user)
|
||||||
|
|
||||||
|
elif event['type'] == 'm.room.member':
|
||||||
|
if ('is_direct' in event['content'] and
|
||||||
|
'membership' in event['content']):
|
||||||
|
if (event['content']['membership'] == "invite" and
|
||||||
|
event['content']['is_direct']):
|
||||||
|
logging.debug('>----on_direct_invite----<')
|
||||||
|
await on_direct_invite(event)
|
||||||
|
return jsonify({})
|
||||||
|
|
||||||
|
if 'membership' in event['content']:
|
||||||
|
if event['content']['membership'] == "leave":
|
||||||
|
logging.debug('>----on_leave_event----<')
|
||||||
|
await on_leave_event(event)
|
||||||
|
return jsonify({})
|
||||||
|
|
||||||
|
return jsonify({})
|
||||||
|
|
||||||
|
async def new_message(event, user):
|
||||||
|
|
||||||
|
if event['sender'] == app.config['MATRIX_AS_ID']:
|
||||||
|
return
|
||||||
|
|
||||||
|
if app.config['MATRIX_PNUT_PREFIX'] in event['sender']:
|
||||||
|
return
|
||||||
|
|
||||||
|
if user.room_id == event['room_id']:
|
||||||
|
await on_control_message(event, user)
|
||||||
|
return
|
||||||
|
|
||||||
|
room = PnutChannels.select().where(PnutChannels.room_id ==
|
||||||
|
event['room_id']).first()
|
||||||
|
logging.debug(f'room: {room}')
|
||||||
|
|
||||||
|
if room is None:
|
||||||
|
if event['room_id'] == app.config['MATRIX_GLOBAL_ROOM']:
|
||||||
|
room = PnutChannels(pnut_chan=0,
|
||||||
|
room_id=app.config['MATRIX_GLOBAL_ROOM'])
|
||||||
|
|
||||||
|
else:
|
||||||
|
logging.debug('-room not mapped-')
|
||||||
|
return
|
||||||
|
|
||||||
|
if room.is_direct:
|
||||||
|
logging.debug('>----on_direct_message----<')
|
||||||
|
return on_direct_message(event, user, room)
|
||||||
|
|
||||||
|
if user is not None:
|
||||||
|
token = user.pnut_user_token
|
||||||
|
prefix = ""
|
||||||
|
|
||||||
|
else:
|
||||||
|
token = app.config['MATRIX_PNUT_TOKEN']
|
||||||
|
matrix_profile = get_profile(event['sender'])
|
||||||
|
if ('displayname' in matrix_profile):
|
||||||
|
prefix = (f"[{matrix_profile['displayname']}]"
|
||||||
|
f" ({event['sender']})\n")
|
||||||
|
else:
|
||||||
|
prefix = "(" + event['sender'] + ")\n"
|
||||||
|
|
||||||
|
pnutpy.api.add_authorization_token(token)
|
||||||
|
|
||||||
|
raw = {}
|
||||||
|
raw['io.pnut.core.crosspost'] = [crosspost_raw(event)]
|
||||||
|
text, oembed = await msg_from_event(event, user)
|
||||||
|
if text is None:
|
||||||
|
return
|
||||||
|
text = prefix + text
|
||||||
|
if oembed:
|
||||||
|
raw['io.pnut.core.oembed'] = [oembed]
|
||||||
|
logging.debug(oembed)
|
||||||
|
|
||||||
|
reply_to_id = None
|
||||||
|
ev_content = event['content']
|
||||||
|
if 'm.relates_to' in ev_content:
|
||||||
|
m_relates_to = ev_content['m.relates_to']
|
||||||
|
if 'm.in_reply_to' in m_relates_to:
|
||||||
|
reply_event_id = m_relates_to['m.in_reply_to']['event_id']
|
||||||
|
e = Events.select().where(Events.event_id ==
|
||||||
|
reply_event_id).first()
|
||||||
|
if e is not None:
|
||||||
|
reply_to_id = e.pnut_id
|
||||||
|
|
||||||
|
try:
|
||||||
|
|
||||||
|
payload = {'raw': raw}
|
||||||
|
if room.pnut_chan == 0:
|
||||||
|
if reply_to_id is not None:
|
||||||
|
orig, meta = pnutpy.api.get_post(reply_to_id)
|
||||||
|
if orig.user.id != user.pnut_user_id:
|
||||||
|
author = orig.user.username
|
||||||
|
text = f"@{author} {text}"
|
||||||
|
if 'content' in orig:
|
||||||
|
cc_list = []
|
||||||
|
for m in orig.content.entities.mentions:
|
||||||
|
if m.text == author:
|
||||||
|
continue
|
||||||
|
cc_list.append(f"@{m.text}")
|
||||||
|
if len(cc_list) > 0:
|
||||||
|
copy = " ".join(cc_list)
|
||||||
|
text = f"{text} /{copy}"
|
||||||
|
payload['reply_to'] = reply_to_id
|
||||||
|
|
||||||
|
payload['text'] = text
|
||||||
|
data, meta = pnutpy.api.create_post(data=payload)
|
||||||
|
|
||||||
|
else:
|
||||||
|
if reply_to_id is not None:
|
||||||
|
orig, meta = pnutpy.api.get_message(room.pnut_chan, reply_to_id)
|
||||||
|
if orig.user.id != user.pnut_user_id:
|
||||||
|
author = orig.user.username
|
||||||
|
text = f"@{author} {text}"
|
||||||
|
if 'content' in orig:
|
||||||
|
cc_list = []
|
||||||
|
for m in orig.content.entities.mentions:
|
||||||
|
if m.text == author:
|
||||||
|
continue
|
||||||
|
cc_list.append(f"@{m.text}")
|
||||||
|
if len(cc_list) > 0:
|
||||||
|
copy = " ".join(cc_list)
|
||||||
|
text = f"{text} /{copy}"
|
||||||
|
payload['reply_to'] = reply_to_id
|
||||||
|
|
||||||
|
payload['text'] = text
|
||||||
|
data, meta = pnutpy.api.create_message(room.pnut_chan,
|
||||||
|
data=payload)
|
||||||
|
bridge_event = Events(
|
||||||
|
event_id=event['event_id'],
|
||||||
|
room_id=event['room_id'],
|
||||||
|
pnut_id=data.id,
|
||||||
|
pnut_channel=room.pnut_chan
|
||||||
|
)
|
||||||
|
bridge_event.save()
|
||||||
|
|
||||||
|
except pnutpy.errors.PnutAuthAPIException:
|
||||||
|
if room.pnut_chan != 0:
|
||||||
|
logging.exception('-unable to post to pnut channel-')
|
||||||
|
else:
|
||||||
|
matrix_api = ClientAPI(app.config['MATRIX_AS_ID'],
|
||||||
|
base_url=app.config['MATRIX_HOST'],
|
||||||
|
token=app.config['MATRIX_AS_TOKEN'])
|
||||||
|
await matrix_api.redact(event['room_id'], event['event_id'],
|
||||||
|
reason='user not authenticated')
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
logging.exception('-something bad happened here-')
|
||||||
|
return
|
||||||
|
|
||||||
|
async def msg_from_event(event, user):
|
||||||
|
text = None
|
||||||
|
oembed = None
|
||||||
|
if (event['content']['msgtype'] == 'm.text' or
|
||||||
|
event['content']['msgtype'] == 'm.notice'):
|
||||||
|
text = event['content']['body']
|
||||||
|
|
||||||
|
fregex = re.compile(r'!file\s(\d+)')
|
||||||
|
m = fregex.search(text)
|
||||||
|
if m is not None:
|
||||||
|
file_id = m.group(1)
|
||||||
|
text = fregex.sub('', text)
|
||||||
|
|
||||||
|
pnut_file = PnutUploads.select().where(PnutUploads.pnut_file_id ==
|
||||||
|
file_id).first()
|
||||||
|
if pnut_file is not None:
|
||||||
|
oembed = {
|
||||||
|
'+io.pnut.core.file': {
|
||||||
|
'file_id': pnut_file.pnut_file_id,
|
||||||
|
'file_token': pnut_file.pnut_file_token,
|
||||||
|
'format': 'oembed'}}
|
||||||
|
|
||||||
|
elif event['content']['msgtype'] == 'm.emote':
|
||||||
|
text = "* " + event['content']['body']
|
||||||
|
|
||||||
|
elif ((event['content']['msgtype'] == 'm.image' or
|
||||||
|
event['content']['msgtype'] == 'm.video' or
|
||||||
|
event['content']['msgtype'] == 'm.audio') and
|
||||||
|
user is not None):
|
||||||
|
await media_from_event(event, user)
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
elif (event['content']['msgtype'] == 'm.image' or
|
||||||
|
event['content']['msgtype'] == 'm.video' or
|
||||||
|
event['content']['msgtype'] == 'm.audio'):
|
||||||
|
oembed = oembed_from_event(event)
|
||||||
|
if ('title' in oembed and 'url' in oembed):
|
||||||
|
text = (f"[{oembed['title']}]"
|
||||||
|
f"({oembed['url']})")
|
||||||
|
|
||||||
|
elif event['content']['msgtype'] == 'm.file':
|
||||||
|
file_url = event['content']['url'][6:]
|
||||||
|
file_name = event['content']['body']
|
||||||
|
dl_url = (f"{app.config['MATRIX_URL']}"
|
||||||
|
f"/_matrix/client/v1/media/download/{file_url}"
|
||||||
|
f"/{file_name}")
|
||||||
|
text = (f"[{file_name}]"
|
||||||
|
f"({dl_url})")
|
||||||
|
|
||||||
|
else:
|
||||||
|
logging.debug('-unknown msg type- ' + event['content']['msgtype'])
|
||||||
|
return
|
||||||
|
|
||||||
|
return text, oembed
|
||||||
|
|
||||||
|
def crosspost_raw(event):
|
||||||
|
cross_profile = {'username': event['sender']}
|
||||||
|
matrix_profile = get_profile(event['sender'])
|
||||||
|
if "avatar_url" in matrix_profile:
|
||||||
|
cross_profile['avatar_image'] = (f"{app.config['MATRIX_URL']}"
|
||||||
|
f"/_matrix/media/r0/download/"
|
||||||
|
f"{matrix_profile['avatar_url'][6:]}")
|
||||||
|
|
||||||
|
crosspost = {}
|
||||||
|
crosspost['canonical_url'] = (f"https://matrix.to/#/{event['room_id']}"
|
||||||
|
f"/{event['event_id']}"
|
||||||
|
f":{app.config['MATRIX_DOMAIN']}")
|
||||||
|
crosspost['source'] = {'name': "matrix.", 'url': "https://matrix.org"}
|
||||||
|
crosspost['user'] = cross_profile
|
||||||
|
|
||||||
|
return crosspost
|
||||||
|
|
||||||
|
async def media_from_event(event, user):
|
||||||
|
mxc_url = event['content']['url']
|
||||||
|
if event['content']['msgtype'] == 'm.image':
|
||||||
|
kind = 'image'
|
||||||
|
elif event['content']['msgtype'] == 'm.video':
|
||||||
|
kind = 'video'
|
||||||
|
elif event['content']['msgtype'] == 'm.audio':
|
||||||
|
kind = 'audio'
|
||||||
|
else:
|
||||||
|
kind = 'other'
|
||||||
|
mime_type = event['content']['info']['mimetype']
|
||||||
|
file_name = event['content']['body']
|
||||||
|
file_data = {'type': f'dev.mcmillian.pnut-matrix.{kind}',
|
||||||
|
'name': file_name, 'kind': kind,
|
||||||
|
'mimetype': mime_type, 'is_public': True}
|
||||||
|
|
||||||
|
matrix_api = ClientAPI(app.config['MATRIX_AS_ID'],
|
||||||
|
base_url=app.config['MATRIX_HOST'],
|
||||||
|
token=app.config['MATRIX_AS_TOKEN'])
|
||||||
|
media_file = await matrix_api.download_media(mxc_url)
|
||||||
|
pnutpy.api.add_authorization_token(user.pnut_user_token)
|
||||||
|
|
||||||
|
try:
|
||||||
|
pnut_file, meta = pnutpy.api.create_file(files={'content': media_file},
|
||||||
|
data=file_data)
|
||||||
|
|
||||||
|
qf = PnutUploads(pnut_user_id=user.pnut_user_id,
|
||||||
|
pnut_file_id=pnut_file.id,
|
||||||
|
pnut_file_token=pnut_file.file_token)
|
||||||
|
qf.save()
|
||||||
|
|
||||||
|
reply = f"* Media upload ready, "
|
||||||
|
reply += f"attach to message with !file {pnut_file.id}"
|
||||||
|
|
||||||
|
except pnutpy.errors.PnutPermissionDenied:
|
||||||
|
reply = f"* Permission denied, you may need to authenticate again."
|
||||||
|
|
||||||
|
try:
|
||||||
|
message = TextMessageEventContent(msgtype='m.text', body=reply)
|
||||||
|
await matrix_api.send_message(event['room_id'], message)
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
errmsg = "- media_from_event -"
|
||||||
|
logging.exception(errmsg)
|
||||||
|
|
||||||
|
def oembed_from_event(event):
|
||||||
|
media_url = event['content']['url'][6:]
|
||||||
|
file_name = event['content']['body']
|
||||||
|
dl_url = (f"{app.config['MATRIX_URL']}"
|
||||||
|
f"/_matrix/client/v1/media/download/{media_url}"
|
||||||
|
f"/{file_name}")
|
||||||
|
|
||||||
|
oembed = {}
|
||||||
|
if event['content']['msgtype'] == 'm.image':
|
||||||
|
oembed['provider_name'] = "matrix"
|
||||||
|
oembed['provider_url'] = "https://matrix.org"
|
||||||
|
oembed['version'] = "1.0"
|
||||||
|
oembed['type'] = "photo"
|
||||||
|
oembed['title'] = file_name
|
||||||
|
oembed['url'] = dl_url
|
||||||
|
if 'info' in event['content']:
|
||||||
|
if 'h' in event['content']['info']:
|
||||||
|
oembed['height'] = event['content']['info']['h']
|
||||||
|
if 'w' in event['content']['info']:
|
||||||
|
oembed['width'] = event['content']['info']['w']
|
||||||
|
|
||||||
|
elif event['content']['msgtype'] == 'm.video':
|
||||||
|
oembed['provider_name'] = "matrix"
|
||||||
|
oembed['provider_url'] = "https://matrix.org"
|
||||||
|
oembed['version'] = "1.0"
|
||||||
|
oembed['type'] = "video"
|
||||||
|
oembed['title'] = file_name
|
||||||
|
oembed['url'] = dl_url
|
||||||
|
if 'info' in event['content']:
|
||||||
|
if 'h' in event['content']['info']:
|
||||||
|
oembed['height'] = event['content']['info']['h']
|
||||||
|
if 'w' in event['content']['info']:
|
||||||
|
oembed['width'] = event['content']['info']['w']
|
||||||
|
|
||||||
|
elif event['content']['msgtype'] == 'm.audio':
|
||||||
|
oembed['provider_name'] = "matrix"
|
||||||
|
oembed['provider_url'] = "https://matrix.org"
|
||||||
|
oembed['version'] = "1.0"
|
||||||
|
oembed['type'] = "audio"
|
||||||
|
oembed['title'] = file_name
|
||||||
|
oembed['url'] = dl_url
|
||||||
|
if 'info' in event['content']:
|
||||||
|
if 'duration' in event['content']['info']:
|
||||||
|
oembed['duration'] = event['content']['info']['duration']
|
||||||
|
|
||||||
|
return oembed
|
||||||
|
|
||||||
|
def delete_message(event, user):
|
||||||
|
if user is not None:
|
||||||
|
token = user.pnut_user_token
|
||||||
|
else:
|
||||||
|
token = app.config['MATRIX_PNUT_TOKEN']
|
||||||
|
pnutpy.api.add_authorization_token(token)
|
||||||
|
|
||||||
|
e = Events.select().where((Events.event_id == event['redacts']) &
|
||||||
|
(Events.deleted == False)).first()
|
||||||
|
if e is None:
|
||||||
|
logging.debug("- can't find the event to remove -")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
r, meta = pnutpy.api.delete_message(e.pnut_channel, e.pnut_id)
|
||||||
|
e.deleted = True
|
||||||
|
e.save()
|
||||||
|
|
||||||
|
except pnutpy.errors.PnutPermissionDenied:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def get_profile(userid):
|
||||||
|
url = app.config['MATRIX_HOST'] + "/_matrix/client/r0/profile/" + userid
|
||||||
|
r = requests.get(url)
|
||||||
|
if r.status_code == 200:
|
||||||
|
return json.loads(r.text)
|
||||||
|
return userid
|
||||||
|
|
||||||
|
def get_channel_settings(channel_id):
|
||||||
|
channel_settings = {}
|
||||||
|
try:
|
||||||
|
channel, meta = pnutpy.api.get_channel(channel_id, include_raw=1)
|
||||||
|
|
||||||
|
for item in channel.raw:
|
||||||
|
if item.type == 'io.pnut.core.chat-settings':
|
||||||
|
channel_settings = item.value
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
logging.exception('-unable to get channel settings-')
|
||||||
|
|
||||||
|
return channel_settings
|
||||||
|
|
||||||
|
async def create_pnut_matrix_room(channel, user):
|
||||||
|
name = None
|
||||||
|
topic = None
|
||||||
|
alias_localpart = f"{app.config['MATRIX_PNUT_PREFIX']}{channel.id}"
|
||||||
|
invitees = [user.matrix_id]
|
||||||
|
|
||||||
|
if channel.acl.read.public:
|
||||||
|
visibility = RoomDirectoryVisibility.PUBLIC
|
||||||
|
preset = RoomCreatePreset.PUBLIC
|
||||||
|
is_private = False
|
||||||
|
else:
|
||||||
|
visibility = RoomDirectoryVisibility.PRIVATE
|
||||||
|
preset = RoomCreatePreset.PRIVATE
|
||||||
|
is_private = True
|
||||||
|
|
||||||
|
if 'io.pnut.core.chat-settings' in channel.raw:
|
||||||
|
for setting in channel.raw['io.pnut.core.chat-settings']:
|
||||||
|
if 'name' in setting:
|
||||||
|
name = f"🥜 {setting['name']}"
|
||||||
|
else:
|
||||||
|
name = f"🥜 channel {channel.id}"
|
||||||
|
if 'description' in setting:
|
||||||
|
topic = setting['description']['text']
|
||||||
|
|
||||||
|
matrix_api = ClientAPI(app.config['MATRIX_AS_ID'],
|
||||||
|
base_url=app.config['MATRIX_HOST'],
|
||||||
|
token=app.config['MATRIX_AS_TOKEN'])
|
||||||
|
|
||||||
|
room_id = await matrix_api.create_room(alias_localpart,
|
||||||
|
invitees=invitees,
|
||||||
|
visibility=visibility,
|
||||||
|
preset=preset,
|
||||||
|
name=name,
|
||||||
|
topic=topic)
|
||||||
|
|
||||||
|
room = PnutChannels(room_id=room_id, pnut_chan=channel.id,
|
||||||
|
is_private=is_private)
|
||||||
|
room.save()
|
||||||
|
|
||||||
|
if is_private:
|
||||||
|
chan_member = PnutPrivateChanMembers(pnut_chan=channel.id,
|
||||||
|
room_id=room_id,
|
||||||
|
pnut_user_id=user.pnut_user_id,
|
||||||
|
matrix_id=user.matrix_id)
|
||||||
|
chan_member.save()
|
||||||
|
|
||||||
|
logging.debug('-create_pnut_matrix_room-')
|
||||||
|
logging.debug(f'-created new room for channel {room.pnut_chan}-')
|
||||||
|
logging.debug(room.room_id)
|
||||||
|
|
||||||
|
def new_matrix_user(username):
|
||||||
|
endpoint = "/_matrix/client/v3/register"
|
||||||
|
url = app.config['MATRIX_HOST'] + endpoint
|
||||||
|
params = {'kind': 'user'}
|
||||||
|
data = {
|
||||||
|
'type': 'm.login.application_service',
|
||||||
|
'username': app.config['MATRIX_PNUT_PREFIX'] + username
|
||||||
|
}
|
||||||
|
headers = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Authorization": "Bearer " + app.config['MATRIX_AS_TOKEN']
|
||||||
|
}
|
||||||
|
r = requests.post(url, headers=headers, json=data, params=params)
|
||||||
|
if r.status_code == 200:
|
||||||
|
return
|
||||||
|
|
||||||
|
else:
|
||||||
|
errmsg = f"- unable to register {username} -"
|
||||||
|
logging.warning(errmsg)
|
||||||
|
logging.debug(r.status_code)
|
||||||
|
logging.debug(r.text)
|
||||||
|
return
|
||||||
|
|
||||||
|
async def on_admin_event(event):
|
||||||
|
matrix_api = ClientAPI(app.config['MATRIX_AS_ID'],
|
||||||
|
base_url=app.config['MATRIX_HOST'],
|
||||||
|
token=app.config['MATRIX_AS_TOKEN'])
|
||||||
|
|
||||||
|
logging.debug("- admin room event recieved -")
|
||||||
|
|
||||||
|
if event['type'] != 'm.room.message':
|
||||||
|
return jsonify({})
|
||||||
|
|
||||||
|
msg = event['content']['body'].split(' ')
|
||||||
|
|
||||||
|
try:
|
||||||
|
if msg[0] == 'help':
|
||||||
|
if len(msg) > 1:
|
||||||
|
await matrix_api.send_message(event['room_id'],
|
||||||
|
cmd_admin_help(msg[1]))
|
||||||
|
else:
|
||||||
|
await matrix_api.send_message(event['room_id'],
|
||||||
|
cmd_admin_help())
|
||||||
|
|
||||||
|
elif msg[0] == 'list':
|
||||||
|
await matrix_api.send_message(event['room_id'], cmd_admin_list())
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
errmsg = "- on_admin_event -"
|
||||||
|
logging.exception(errmsg)
|
||||||
|
|
||||||
|
def cmd_admin_help(cmd=None):
|
||||||
|
help_usage = "help [command]"
|
||||||
|
help_desc = "Show information about available commands."
|
||||||
|
list_usage = "list"
|
||||||
|
list_desc = "List the rooms currently linked with pnut.io."
|
||||||
|
|
||||||
|
if cmd == 'help':
|
||||||
|
text = "usage: " + help_usage + "\n\n"
|
||||||
|
text += help_desc
|
||||||
|
if cmd == 'list':
|
||||||
|
text = "usage: " + list_usage + "\n\n"
|
||||||
|
text += list_desc
|
||||||
|
elif cmd == 'unlink':
|
||||||
|
text = "usage: " + unlink_usage + "\n\n"
|
||||||
|
text += unlink_desc
|
||||||
|
elif cmd == 'link':
|
||||||
|
text = "usage: " + link_usage + "\n\n"
|
||||||
|
text += link_desc
|
||||||
|
else:
|
||||||
|
text = "The following commands are available:\n\n"
|
||||||
|
text += help_usage + "\n"
|
||||||
|
text += list_usage + "\n"
|
||||||
|
text += unlink_usage + "\n"
|
||||||
|
text += link_usage + "\n"
|
||||||
|
|
||||||
|
return TextMessageEventContent(msgtype='m.text', body=text)
|
||||||
|
|
||||||
|
def cmd_admin_list():
|
||||||
|
text = ""
|
||||||
|
rooms = PnutChannels.select()
|
||||||
|
|
||||||
|
if len(rooms) > 0:
|
||||||
|
text = "ID\tMATRIX ID\tPNUT CHANNEL\n"
|
||||||
|
else:
|
||||||
|
text = " - no rooms are currently linked - \n"
|
||||||
|
|
||||||
|
for room in rooms:
|
||||||
|
text += str(room.id) + '\t'
|
||||||
|
text += room.room_id + '\t\t\t\t\t'
|
||||||
|
text += str(room.pnut_chan) + '\t'
|
||||||
|
text += '\n'
|
||||||
|
|
||||||
|
return TextMessageEventContent(msgtype='m.text', body=text)
|
||||||
|
|
||||||
|
async def on_direct_invite(event):
|
||||||
|
# direct chat with the appservice user
|
||||||
|
if event['state_key'] == app.config['MATRIX_AS_ID']:
|
||||||
|
matrix_api = ClientAPI(app.config['MATRIX_AS_ID'],
|
||||||
|
base_url=app.config['MATRIX_HOST'],
|
||||||
|
token=app.config['MATRIX_AS_TOKEN'])
|
||||||
|
|
||||||
|
dm = PnutUsers(matrix_id=event['sender'], room_id=event['room_id'])
|
||||||
|
|
||||||
|
# direct chat with another pnut user
|
||||||
|
elif app.config['MATRIX_PNUT_PREFIX'] in event['state_key']:
|
||||||
|
matrix_api = ClientAPI(app.config['MATRIX_AS_ID'],
|
||||||
|
base_url=app.config['MATRIX_HOST'],
|
||||||
|
token=app.config['MATRIX_AS_TOKEN'],
|
||||||
|
as_user_id=event['state_key'])
|
||||||
|
|
||||||
|
bridge_user = event['state_key']
|
||||||
|
pnut_user = bridge_user.replace(app.config['MATRIX_PNUT_PREFIX'],
|
||||||
|
'').split(':')[0]
|
||||||
|
|
||||||
|
user = PnutUsers.select().where(PnutUsers.matrix_id ==
|
||||||
|
event['sender']).first()
|
||||||
|
|
||||||
|
if user is not None:
|
||||||
|
pnutpy.api.add_authorization_token(user.pnut_user_token)
|
||||||
|
|
||||||
|
try:
|
||||||
|
channel, meta = pnutpy.api.existing_pm(ids=pnut_user)
|
||||||
|
new_matrix_user(pnut_user)
|
||||||
|
|
||||||
|
dm = PnutChannels(pnut_chan=channel.id,
|
||||||
|
room_id=event['room_id'],
|
||||||
|
is_direct=True,
|
||||||
|
direct_pnut_user=bridge_user,
|
||||||
|
direct_mtrx_user=user.matrix_id)
|
||||||
|
|
||||||
|
except pnutpy.errors.PnutAuthAPIException:
|
||||||
|
abort(403)
|
||||||
|
|
||||||
|
else:
|
||||||
|
abort(403)
|
||||||
|
|
||||||
|
else:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
logging.debug('--> trying to join room <--')
|
||||||
|
await matrix_api.join_room_by_id(event['room_id'])
|
||||||
|
dm.save()
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
errmsg = "- on_direct_invite -"
|
||||||
|
logging.exception(errmsg)
|
||||||
|
|
||||||
|
async def on_leave_event(event):
|
||||||
|
direct_room = PnutChannels.select().where(
|
||||||
|
(PnutChannels.room_id == event['room_id']) &
|
||||||
|
(PnutChannels.direct_mtrx_user == event['sender']) &
|
||||||
|
(PnutChannels.is_direct == True)).first()
|
||||||
|
|
||||||
|
user = PnutUsers.select().where(PnutUsers.room_id ==
|
||||||
|
event['room_id']).first()
|
||||||
|
|
||||||
|
if direct_room is not None:
|
||||||
|
matrix_api = ClientAPI(app.config['MATRIX_AS_ID'],
|
||||||
|
base_url=app.config['MATRIX_HOST'],
|
||||||
|
token=app.config['MATRIX_AS_TOKEN'],
|
||||||
|
as_user_id=direct_room.direct_pnut_user.lower())
|
||||||
|
|
||||||
|
try:
|
||||||
|
await matrix_api.leave_room(event['room_id'])
|
||||||
|
direct_room.delete_instance()
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
errmsg = "- on_leave_event -"
|
||||||
|
logging.exception(errmsg)
|
||||||
|
|
||||||
|
def on_direct_message(event, user, room):
|
||||||
|
|
||||||
|
if user is not None:
|
||||||
|
token = user.pnut_user_token
|
||||||
|
prefix = ""
|
||||||
|
else:
|
||||||
|
token = app.config['MATRIX_PNUT_TOKEN']
|
||||||
|
matrix_profile = get_profile(event['sender'])
|
||||||
|
if "displayname" in matrix_profile:
|
||||||
|
prefix = (f"[{matrix_profile['displayname']}]"
|
||||||
|
f" ({event['sender']})\n")
|
||||||
|
else:
|
||||||
|
prefix = "(" + event['sender'] + ")\n"
|
||||||
|
pnutpy.api.add_authorization_token(token)
|
||||||
|
|
||||||
|
raw = {}
|
||||||
|
raw['io.pnut.core.crosspost'] = [crosspost_raw(event)]
|
||||||
|
evtext, evraw = msg_from_event(event)
|
||||||
|
text = prefix + evtext
|
||||||
|
|
||||||
|
try:
|
||||||
|
msg, meta = pnutpy.api.create_message(room.pnut_chan,
|
||||||
|
data={'text': text, 'raw': raw})
|
||||||
|
bridge_event = Events(
|
||||||
|
event_id=event['event_id'],
|
||||||
|
room_id=event['room_id'],
|
||||||
|
pnut_id=msg.id,
|
||||||
|
pnut_channel=room.pnut_chan
|
||||||
|
)
|
||||||
|
bridge_event.save()
|
||||||
|
|
||||||
|
except pnutpy.errors.PnutAuthAPIException:
|
||||||
|
logging.exception('-unable to post to pnut channel-')
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
logging.exception('-something bad happened here-')
|
||||||
|
|
||||||
|
return jsonify({})
|
||||||
|
|
||||||
|
async def on_control_message(event, user):
|
||||||
|
matrix_api = ClientAPI(app.config['MATRIX_AS_ID'],
|
||||||
|
base_url=app.config['MATRIX_HOST'],
|
||||||
|
token=app.config['MATRIX_AS_TOKEN'])
|
||||||
|
|
||||||
|
if event['type'] != 'm.room.message':
|
||||||
|
return jsonify({})
|
||||||
|
|
||||||
|
msg = event['content']['body'].split(' ')
|
||||||
|
|
||||||
|
try:
|
||||||
|
if msg[0] == '!help' or msg[0] == 'help':
|
||||||
|
if len(msg) > 1:
|
||||||
|
await matrix_api.send_message(event['room_id'],
|
||||||
|
cmd_user_help(msg[1]))
|
||||||
|
else:
|
||||||
|
await matrix_api.send_message(event['room_id'],
|
||||||
|
cmd_user_help())
|
||||||
|
|
||||||
|
elif msg[0] == '!auth':
|
||||||
|
await matrix_api.send_message(event['room_id'], cmd_user_auth())
|
||||||
|
|
||||||
|
elif msg[0] == '!save':
|
||||||
|
if len(msg) > 1:
|
||||||
|
await matrix_api.send_message(event['room_id'],
|
||||||
|
cmd_user_save(user, msg[1]))
|
||||||
|
else:
|
||||||
|
await matrix_api.send_message(event['room_id'],
|
||||||
|
cmd_user_save())
|
||||||
|
|
||||||
|
elif msg[0] == '!drop':
|
||||||
|
r = await cmd_user_drop(user)
|
||||||
|
await matrix_api.send_message(event['room_id'], r)
|
||||||
|
|
||||||
|
elif msg[0] == '!status':
|
||||||
|
await matrix_api.send_message(event['room_id'],
|
||||||
|
cmd_user_status(user))
|
||||||
|
|
||||||
|
elif msg[0] == '!join':
|
||||||
|
if len(msg) > 1:
|
||||||
|
r = await cmd_user_join(user, msg[1])
|
||||||
|
await matrix_api.send_message(event['room_id'], r)
|
||||||
|
else:
|
||||||
|
r = await cmd_user_join(user)
|
||||||
|
await matrix_api.send_message(event['room_id'], r)
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
errmsg = "- on_direct_message -"
|
||||||
|
logging.exception(errmsg)
|
||||||
|
|
||||||
|
def cmd_user_help(cmd=None):
|
||||||
|
reply = "This is an admin room for controlling your connection to pnut.io\n"
|
||||||
|
reply += "The following commands are available.\n\n"
|
||||||
|
reply += "!auth\t\t\t- Authorize your account on pnut.io\n"
|
||||||
|
reply += "!save <token>\t- Save your pnut.io auth token\n"
|
||||||
|
reply += "!drop\t\t\t- Drop your pnut.io auth token\n"
|
||||||
|
reply += "!status\t\t\t- Status of your pnut.io auth token\n"
|
||||||
|
reply += "!join <channel #>\t- Join a private channel on pnut.io\n"
|
||||||
|
|
||||||
|
return TextMessageEventContent(msgtype='m.text', body=reply)
|
||||||
|
|
||||||
|
def cmd_user_auth():
|
||||||
|
reply = "Visit the following URL to authorize your account on pnut.io.\n\n"
|
||||||
|
reply += "https://pnut.io/oauth/authenticate"
|
||||||
|
reply += "?client_id=6SeCRCpCZkmZOKFLFGWbcdAeq2fX1M5t"
|
||||||
|
reply += "&redirect_uri=urn:ietf:wg:oauth:2.0:oob"
|
||||||
|
reply += "&scope=files,write_post,presence,messages&response_type=token\n\n"
|
||||||
|
reply += "You will be provided with a token that you store with the !save command.\n"
|
||||||
|
|
||||||
|
return TextMessageEventContent(msgtype='m.text', body=reply)
|
||||||
|
|
||||||
|
def cmd_user_save(user, token=None):
|
||||||
|
if token is None:
|
||||||
|
reply = "You must provide a token with this command.\n"
|
||||||
|
reply += "!save <token>"
|
||||||
|
return TextMessageEventContent(msgtype='m.text', body=reply)
|
||||||
|
|
||||||
|
pnutpy.api.add_authorization_token(token)
|
||||||
|
try:
|
||||||
|
response, meta = pnutpy.api.get_user('me')
|
||||||
|
|
||||||
|
user.pnut_user_id = response.id
|
||||||
|
user.pnut_user_token = token
|
||||||
|
user.save()
|
||||||
|
|
||||||
|
reply = "Success! You are now authorized as " + response['username']
|
||||||
|
|
||||||
|
except pnutpy.errors.PnutAuthAPIException as e:
|
||||||
|
reply = "Error! Unable to authorize your account."
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logging.exception('!save')
|
||||||
|
reply = "Error! Problem saving your token."
|
||||||
|
|
||||||
|
return TextMessageEventContent(msgtype='m.text', body=reply)
|
||||||
|
|
||||||
|
async def cmd_user_drop(user):
|
||||||
|
|
||||||
|
direct_rooms = PnutChannels.select().where(PnutChannels.direct_mtrx_user ==
|
||||||
|
user.matrix_id)
|
||||||
|
for dir_room in direct_rooms:
|
||||||
|
matrix_api = ClientAPI(app.config['MATRIX_AS_ID'],
|
||||||
|
base_url=app.config['MATRIX_HOST'],
|
||||||
|
token=app.config['MATRIX_AS_TOKEN'],
|
||||||
|
as_user_id=dir_room.direct_pnut_user.lower())
|
||||||
|
await matrix_api.leave_room(dir_room.room_id)
|
||||||
|
dir_room.delete_instance()
|
||||||
|
|
||||||
|
private_rooms = PnutPrivateChanMembers.select().where(
|
||||||
|
PnutPrivateChanMembers.pnut_user_id == user.pnut_user_id)
|
||||||
|
for priv_room in private_rooms:
|
||||||
|
matrix_api = ClientAPI(app.config['MATRIX_AS_ID'],
|
||||||
|
base_url=app.config['MATRIX_HOST'],
|
||||||
|
token=app.config['MATRIX_AS_TOKEN'])
|
||||||
|
await matrix_api.kick_user(priv_room.room_id, user.matrix_id,
|
||||||
|
reason='user left from bridge')
|
||||||
|
priv_room.delete_instance()
|
||||||
|
|
||||||
|
user.pnut_user_id = None
|
||||||
|
user.pnut_user_token = None
|
||||||
|
user.save()
|
||||||
|
reply = "Success! Your auth token has been removed."
|
||||||
|
|
||||||
|
return TextMessageEventContent(msgtype='m.text', body=reply)
|
||||||
|
|
||||||
|
def cmd_user_status(user):
|
||||||
|
try:
|
||||||
|
if user is None:
|
||||||
|
reply = "You are currently not authorized on pnut.io"
|
||||||
|
else:
|
||||||
|
pnutpy.api.add_authorization_token(user.pnut_user_token)
|
||||||
|
response, meta = pnutpy.api.get_user('me')
|
||||||
|
reply = "You are currently authorized as " + response.username
|
||||||
|
|
||||||
|
except pnutpy.errors.PnutAuthAPIException as e:
|
||||||
|
reply = "You are currently not authorized on pnut.io"
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logging.exception('!status')
|
||||||
|
reply = "Error! There was a problem checking your account."
|
||||||
|
|
||||||
|
return TextMessageEventContent(msgtype='m.text', body=reply)
|
||||||
|
|
||||||
|
async def cmd_user_join(user, channel_id=None):
|
||||||
|
if channel_id is None:
|
||||||
|
reply = "You must provide a channel id number with this command.\n"
|
||||||
|
reply += "!join <channel #>"
|
||||||
|
return TextMessageEventContent(msgtype='m.text', body=reply)
|
||||||
|
|
||||||
|
try:
|
||||||
|
if user is None:
|
||||||
|
reply = "You are currently not authorized on pnut.io"
|
||||||
|
else:
|
||||||
|
pnutpy.api.add_authorization_token(user.pnut_user_token)
|
||||||
|
channel, meta = pnutpy.api.get_channel(channel_id, include_raw=1)
|
||||||
|
room = PnutChannels.select().where(PnutChannels.pnut_chan ==
|
||||||
|
channel_id).first()
|
||||||
|
if room is None:
|
||||||
|
await create_pnut_matrix_room(channel, user)
|
||||||
|
else:
|
||||||
|
matrix_api = ClientAPI(app.config['MATRIX_AS_ID'],
|
||||||
|
base_url=app.config['MATRIX_HOST'],
|
||||||
|
token=app.config['MATRIX_AS_TOKEN'])
|
||||||
|
await matrix_api.invite_user(room.room_id, user.matrix_id)
|
||||||
|
reply = "ok"
|
||||||
|
|
||||||
|
except pnutpy.errors.PnutAuthAPIException as e:
|
||||||
|
reply = "You are currently not authorized on pnut.io"
|
||||||
|
|
||||||
|
except pnutpy.errors.PnutPermissionDenied:
|
||||||
|
reply = "You are not authorized for this channel"
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
logging.exception('!join')
|
||||||
|
reply = "Error! There was a problem joining the channel."
|
||||||
|
|
||||||
|
return TextMessageEventContent(msgtype='m.text', body=reply)
|
||||||
|
|
||||||
|
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 main():
|
||||||
|
a_parser = argparse.ArgumentParser()
|
||||||
|
a_parser.add_argument('-c', '--config', dest='configyaml',
|
||||||
|
default="config.yaml", help="configuration file")
|
||||||
|
args = a_parser.parse_args()
|
||||||
|
|
||||||
|
with open(args.configyaml, "rb") as config_file:
|
||||||
|
config = yaml.load(config_file, Loader=yaml.SafeLoader)
|
||||||
|
|
||||||
|
logging.config.dictConfig(config['logging'])
|
||||||
|
redact_filter = MLogFilter()
|
||||||
|
logging.getLogger("werkzeug").addFilter(redact_filter)
|
||||||
|
logging.getLogger("urllib3.connectionpool").addFilter(redact_filter)
|
||||||
|
|
||||||
|
app.config.update(config)
|
||||||
|
logging.basicConfig(level=logging.DEBUG)
|
||||||
|
|
||||||
|
db.init(config['SERVICE_DB'])
|
||||||
|
db_create_tables()
|
||||||
|
|
||||||
|
app.run(host=config['LISTEN_HOST'], port=config['LISTEN_PORT'])
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
334
src/pnut_matrix/cmd.py
Executable file
334
src/pnut_matrix/cmd.py
Executable file
|
@ -0,0 +1,334 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import asyncio
|
||||||
|
import asyncclick as click
|
||||||
|
import requests
|
||||||
|
import pnutpy
|
||||||
|
import json
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
from mautrix.client import ClientAPI
|
||||||
|
from mautrix.types import TextMessageEventContent, Format, MessageType, EventType
|
||||||
|
from mautrix.errors import MatrixConnectionError
|
||||||
|
from mautrix.errors.request import MNotFound, MForbidden
|
||||||
|
|
||||||
|
PNUT_API="https://api.pnut.io/v1"
|
||||||
|
|
||||||
|
@click.group()
|
||||||
|
@click.option('--debug', '-d', is_flag=True)
|
||||||
|
@click.option('--config', '-c', required=True)
|
||||||
|
@click.pass_context
|
||||||
|
async def cmd(ctx, debug, config):
|
||||||
|
if debug:
|
||||||
|
logging.basicConfig(level=logging.DEBUG)
|
||||||
|
|
||||||
|
ctx.ensure_object(dict)
|
||||||
|
with open(config, "rb") as config_file:
|
||||||
|
ctx.obj['config'] = yaml.load(config_file, Loader=yaml.SafeLoader)
|
||||||
|
|
||||||
|
@cmd.command()
|
||||||
|
@click.pass_context
|
||||||
|
async def get_streams(ctx):
|
||||||
|
config = ctx.obj['config']
|
||||||
|
logging.debug(config)
|
||||||
|
endpoint = "/streams"
|
||||||
|
headers = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Authorization": "Bearer " + config['PNUT_APPTOKEN']
|
||||||
|
}
|
||||||
|
|
||||||
|
url = f"{PNUT_API}{endpoint}"
|
||||||
|
r = requests.get(url, headers=headers)
|
||||||
|
|
||||||
|
if r.status_code == 200:
|
||||||
|
click.echo(json.dumps(r.json(), indent=4))
|
||||||
|
|
||||||
|
else:
|
||||||
|
click.echo(r.status_code)
|
||||||
|
click.echo(r.text)
|
||||||
|
|
||||||
|
@cmd.command()
|
||||||
|
@click.argument('key')
|
||||||
|
@click.pass_context
|
||||||
|
def rm_stream(ctx, key):
|
||||||
|
config = ctx.obj['config']
|
||||||
|
logging.debug(config)
|
||||||
|
endpoint = f"/streams/{key}"
|
||||||
|
headers = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Authorization": "Bearer " + config['PNUT_APPTOKEN']
|
||||||
|
}
|
||||||
|
|
||||||
|
url = f"{PNUT_API}{endpoint}"
|
||||||
|
r = requests.delete(url, headers=headers)
|
||||||
|
|
||||||
|
if r.status_code == 200:
|
||||||
|
click.echo(json.dumps(r.json(), indent=4))
|
||||||
|
|
||||||
|
else:
|
||||||
|
click.echo(r.status_code)
|
||||||
|
click.echo(r.text)
|
||||||
|
|
||||||
|
@cmd.command()
|
||||||
|
@click.pass_context
|
||||||
|
def rm_streams(ctx):
|
||||||
|
config = ctx.obj['config']
|
||||||
|
logging.debug(config)
|
||||||
|
endpoint = f"/streams"
|
||||||
|
headers = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Authorization": "Bearer " + config['PNUT_APPTOKEN']
|
||||||
|
}
|
||||||
|
|
||||||
|
url = f"{PNUT_API}{endpoint}"
|
||||||
|
r = requests.delete(url, headers=headers)
|
||||||
|
|
||||||
|
if r.status_code == 200:
|
||||||
|
click.echo(json.dumps(r.json(), indent=4))
|
||||||
|
|
||||||
|
else:
|
||||||
|
click.echo(r.status_code)
|
||||||
|
click.echo(r.text)
|
||||||
|
|
||||||
|
@cmd.command()
|
||||||
|
@click.argument('key')
|
||||||
|
@click.argument('object_types')
|
||||||
|
@click.pass_context
|
||||||
|
def new_stream(ctx, key, object_types):
|
||||||
|
config = ctx.obj['config']
|
||||||
|
endpoint = f"/streams"
|
||||||
|
headers = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Authorization": "Bearer " + config['PNUT_APPTOKEN']
|
||||||
|
}
|
||||||
|
otypes = [x.strip() for x in object_types.split(',')]
|
||||||
|
data = {
|
||||||
|
'type': "long_poll",
|
||||||
|
'object_types': otypes,
|
||||||
|
}
|
||||||
|
|
||||||
|
url = f"{PNUT_API}{endpoint}"
|
||||||
|
r = requests.post(url, headers=headers, json=data)
|
||||||
|
|
||||||
|
if r.status_code == 201:
|
||||||
|
click.echo(json.dumps(r.json(), indent=4))
|
||||||
|
|
||||||
|
else:
|
||||||
|
click.echo(r.status_code)
|
||||||
|
click.echo(r.text)
|
||||||
|
|
||||||
|
@cmd.command()
|
||||||
|
@click.argument('key')
|
||||||
|
@click.argument('object_types')
|
||||||
|
@click.pass_context
|
||||||
|
def update_stream(ctx, key, object_types):
|
||||||
|
config = ctx.obj['config']
|
||||||
|
endpoint = f"/streams/{key}"
|
||||||
|
headers = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Authorization": "Bearer " + config['PNUT_APPTOKEN']
|
||||||
|
}
|
||||||
|
otypes = [x.strip() for x in object_types.split(',')]
|
||||||
|
data = {
|
||||||
|
'object_types': otypes,
|
||||||
|
}
|
||||||
|
|
||||||
|
url = f"{PNUT_API}{endpoint}"
|
||||||
|
r = requests.put(url, headers=headers, json=data)
|
||||||
|
|
||||||
|
if r.status_code == 200:
|
||||||
|
click.echo(json.dumps(r.json(), indent=4))
|
||||||
|
|
||||||
|
else:
|
||||||
|
click.echo(r.status_code)
|
||||||
|
click.echo(r.text)
|
||||||
|
|
||||||
|
# TODO: this needs debugging
|
||||||
|
@cmd.command()
|
||||||
|
@click.pass_context
|
||||||
|
def pnut_app_token(ctx):
|
||||||
|
config = ctx.obj['config']
|
||||||
|
logging.debug(config)
|
||||||
|
# endpoint = f"/oauth/access_token"
|
||||||
|
# headers = {
|
||||||
|
# "Content-Type": "application/json",
|
||||||
|
# }
|
||||||
|
# data = {
|
||||||
|
# 'client_id': config['PNUTCLIENT_ID'],
|
||||||
|
# 'client_secret': config['PNUTCLIENT_SECRET'],
|
||||||
|
# 'grant_type': "client_credentials"
|
||||||
|
# }
|
||||||
|
#
|
||||||
|
# url = f"{PNUT_API}{endpoint}"
|
||||||
|
# r = requests.post(url, headers=headers, json=data)
|
||||||
|
#
|
||||||
|
# if r.status_code == 200:
|
||||||
|
# click.echo(json.dumps(r.json(), indent=4))
|
||||||
|
#
|
||||||
|
# else:
|
||||||
|
# click.echo(r.status_code)
|
||||||
|
# click.echo(r.text)
|
||||||
|
|
||||||
|
@cmd.command()
|
||||||
|
@click.argument('username')
|
||||||
|
@click.pass_context
|
||||||
|
def new_matrix_asuser(ctx, username):
|
||||||
|
config = ctx.obj['config']
|
||||||
|
endpoint = "/_matrix/client/v3/register"
|
||||||
|
url = config['MATRIX_HOST'] + endpoint
|
||||||
|
headers = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Authorization": "Bearer " + config['MATRIX_AS_TOKEN']
|
||||||
|
}
|
||||||
|
params = {'kind': 'user'}
|
||||||
|
data = {'type': 'm.login.application_service','username': username}
|
||||||
|
|
||||||
|
r = requests.post(url, headers=headers, json=data, params=params)
|
||||||
|
if r.status_code == 200:
|
||||||
|
click.echo("SUCCESS!")
|
||||||
|
|
||||||
|
else:
|
||||||
|
click.echo(r.status_code)
|
||||||
|
click.echo(r.text)
|
||||||
|
|
||||||
|
@cmd.command()
|
||||||
|
@click.argument('invitee')
|
||||||
|
@click.pass_context
|
||||||
|
def new_pnut_admin_room(ctx, invitee):
|
||||||
|
config = ctx.obj['config']
|
||||||
|
endpoint = "/_matrix/client/v3/createRoom"
|
||||||
|
url = config['MATRIX_HOST'] + endpoint
|
||||||
|
headers = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Authorization": "Bearer " + config['MATRIX_AS_TOKEN']
|
||||||
|
}
|
||||||
|
data = {
|
||||||
|
'visibility': "private",
|
||||||
|
'is_direct': False,
|
||||||
|
'name': "Pnut Bridge Admin Room",
|
||||||
|
'room_alias_name': f"{config['MATRIX_PNUT_PREFIX']}admin",
|
||||||
|
'invite': [invitee],
|
||||||
|
'power_level_content_override': {
|
||||||
|
'users': {
|
||||||
|
f"{config['MATRIX_AS_ID']}": 100,
|
||||||
|
f"{invitee}": 100
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
logging.debug(data)
|
||||||
|
r = requests.post(url, headers=headers, json=data)
|
||||||
|
|
||||||
|
if r.status_code == 200:
|
||||||
|
click.echo(json.dumps(r.json(), indent=4))
|
||||||
|
|
||||||
|
else:
|
||||||
|
click.echo(r.status_code)
|
||||||
|
click.echo(r.text)
|
||||||
|
|
||||||
|
@cmd.command()
|
||||||
|
@click.pass_context
|
||||||
|
def new_pnut_global_room(ctx):
|
||||||
|
config = ctx.obj['config']
|
||||||
|
endpoint = "/_matrix/client/v3/createRoom"
|
||||||
|
url = config['MATRIX_HOST'] + endpoint
|
||||||
|
headers = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Authorization": "Bearer " + config['MATRIX_AS_TOKEN']
|
||||||
|
}
|
||||||
|
# data = {
|
||||||
|
# 'visibility': "public",
|
||||||
|
# 'name': "Pnut Global Stream",
|
||||||
|
# 'room_alias_name': "pnut_global",
|
||||||
|
# 'power_level_content_override': {
|
||||||
|
# "events_default": 2,
|
||||||
|
# "invite": 2,
|
||||||
|
# }
|
||||||
|
# }
|
||||||
|
data = {
|
||||||
|
'visibility': "public",
|
||||||
|
'name': "Pnut Global Stream",
|
||||||
|
'room_alias_name': f"{config['MATRIX_PNUT_PREFIX']}global"
|
||||||
|
}
|
||||||
|
logging.debug(data)
|
||||||
|
r = requests.post(url, headers=headers, json=data)
|
||||||
|
|
||||||
|
if r.status_code == 200:
|
||||||
|
click.echo(json.dumps(r.json(), indent=4))
|
||||||
|
|
||||||
|
else:
|
||||||
|
click.echo(r.status_code)
|
||||||
|
click.echo(r.text)
|
||||||
|
|
||||||
|
@cmd.command()
|
||||||
|
@click.argument('room_id')
|
||||||
|
@click.argument('matrix_id')
|
||||||
|
@click.argument('power_level')
|
||||||
|
@click.pass_context
|
||||||
|
def elevate_matrix_user(ctx, room_id, matrix_id, power_level):
|
||||||
|
config = ctx.obj['config']
|
||||||
|
endpoint = f"/_matrix/client/v3/rooms/{room_id}/state/"
|
||||||
|
url = config['MATRIX_HOST'] + endpoint + "m.room.power_levels"
|
||||||
|
headers = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Authorization": "Bearer " + config['MATRIX_AS_TOKEN']
|
||||||
|
}
|
||||||
|
data = {
|
||||||
|
'users': {
|
||||||
|
f"{config['MATRIX_AS_ID']}": 100,
|
||||||
|
f"{matrix_id}": int(power_level)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
logging.debug(data)
|
||||||
|
r = requests.put(url, headers=headers, json=data)
|
||||||
|
|
||||||
|
if r.status_code == 200:
|
||||||
|
click.echo(json.dumps(r.json(), indent=4))
|
||||||
|
|
||||||
|
else:
|
||||||
|
click.echo(r.status_code)
|
||||||
|
click.echo(r.text)
|
||||||
|
|
||||||
|
@cmd.command()
|
||||||
|
@click.pass_context
|
||||||
|
async def list_joined_rooms(ctx):
|
||||||
|
config = ctx.obj['config']
|
||||||
|
matrix_api = ClientAPI(config['MATRIX_AS_ID'],
|
||||||
|
base_url=config['MATRIX_HOST'],
|
||||||
|
token=config['MATRIX_AS_TOKEN'])
|
||||||
|
room_list = await matrix_api.get_joined_rooms()
|
||||||
|
for room_id in room_list:
|
||||||
|
click.echo(room_id)
|
||||||
|
|
||||||
|
try:
|
||||||
|
room_name = await matrix_api.get_state_event(room_id,
|
||||||
|
EventType.ROOM_NAME)
|
||||||
|
click.echo(f'name: {room_name.name}')
|
||||||
|
except MNotFound:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
room_alias = await matrix_api.get_state_event(room_id,
|
||||||
|
EventType.ROOM_CANONICAL_ALIAS)
|
||||||
|
click.echo(f'alias: {room_alias.canonical_alias}')
|
||||||
|
except MNotFound:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# members = await matrix_api.get_joined_members(room_id)
|
||||||
|
# click.echo(members)
|
||||||
|
click.echo('-----------')
|
||||||
|
|
||||||
|
@cmd.command()
|
||||||
|
@click.argument('room_id')
|
||||||
|
@click.pass_context
|
||||||
|
async def part_room(ctx, room_id):
|
||||||
|
config = ctx.obj['config']
|
||||||
|
matrix_api = ClientAPI(config['MATRIX_AS_ID'],
|
||||||
|
base_url=config['MATRIX_HOST'],
|
||||||
|
token=config['MATRIX_AS_TOKEN'])
|
||||||
|
|
||||||
|
# TODO: need to clear alias
|
||||||
|
await matrix_api.leave_room(room_id)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
cmd(_anyio_backend="asyncio")
|
53
src/pnut_matrix/models.py
Normal file
53
src/pnut_matrix/models.py
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from peewee import *
|
||||||
|
from playhouse.migrate import *
|
||||||
|
|
||||||
|
db = SqliteDatabase(None)
|
||||||
|
migrator = SqliteMigrator(db)
|
||||||
|
|
||||||
|
class BaseModel(Model):
|
||||||
|
class Meta:
|
||||||
|
database = db
|
||||||
|
|
||||||
|
class PnutAvatars(BaseModel):
|
||||||
|
pnut_user = CharField(unique=True)
|
||||||
|
avatar_url = CharField()
|
||||||
|
|
||||||
|
class PnutChannels(BaseModel):
|
||||||
|
pnut_chan = IntegerField(unique=True)
|
||||||
|
room_id = CharField()
|
||||||
|
is_private = BooleanField(default=False)
|
||||||
|
is_direct = BooleanField(default=False)
|
||||||
|
direct_pnut_user = CharField(null=True)
|
||||||
|
direct_mtrx_user = CharField(null=True)
|
||||||
|
|
||||||
|
class PnutPrivateChanMembers(BaseModel):
|
||||||
|
pnut_chan = IntegerField()
|
||||||
|
room_id = CharField()
|
||||||
|
pnut_user_id = IntegerField()
|
||||||
|
matrix_id = CharField()
|
||||||
|
|
||||||
|
class Events(BaseModel):
|
||||||
|
event_id = CharField(unique=True)
|
||||||
|
room_id = CharField()
|
||||||
|
pnut_id = IntegerField()
|
||||||
|
pnut_channel = IntegerField()
|
||||||
|
revised = BooleanField(default=False)
|
||||||
|
deleted = BooleanField(default=False)
|
||||||
|
|
||||||
|
class PnutUsers(BaseModel):
|
||||||
|
matrix_id = CharField(unique=True)
|
||||||
|
room_id = CharField()
|
||||||
|
pnut_user_id = IntegerField(unique=True, null=True)
|
||||||
|
pnut_user_token = CharField(null=True)
|
||||||
|
|
||||||
|
class PnutUploads(BaseModel):
|
||||||
|
pnut_user_id = IntegerField()
|
||||||
|
pnut_file_id = IntegerField()
|
||||||
|
pnut_file_token = CharField()
|
||||||
|
|
||||||
|
def db_create_tables():
|
||||||
|
with db:
|
||||||
|
db.create_tables([PnutUsers, Events, PnutChannels, PnutAvatars,
|
||||||
|
PnutPrivateChanMembers, PnutUploads])
|
483
src/pnut_matrix/pnutservice.py
Normal file
483
src/pnut_matrix/pnutservice.py
Normal file
|
@ -0,0 +1,483 @@
|
||||||
|
import time
|
||||||
|
import logging
|
||||||
|
import logging.config
|
||||||
|
import yaml
|
||||||
|
import json
|
||||||
|
import pnutpy
|
||||||
|
import requests
|
||||||
|
import magic
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
from mautrix.client import ClientAPI
|
||||||
|
from mautrix.types import TextMessageEventContent, Format, MessageType
|
||||||
|
from mautrix.errors import MatrixConnectionError
|
||||||
|
from mautrix.errors.request import MNotFound, MForbidden
|
||||||
|
|
||||||
|
from websockets.asyncio.client import connect
|
||||||
|
from websockets.exceptions import ConnectionClosed
|
||||||
|
|
||||||
|
from pnut_matrix.models import *
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger()
|
||||||
|
|
||||||
|
config = None
|
||||||
|
matrix_url = None
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
async def new_pnut_message(msg, meta):
|
||||||
|
logger.debug("channel: " + msg.channel_id)
|
||||||
|
logger.debug("username: " + msg.user.username)
|
||||||
|
if 'name' in msg.user:
|
||||||
|
logger.debug("name: " + msg.user.name)
|
||||||
|
logger.debug("text: " + msg.content.text)
|
||||||
|
|
||||||
|
# ignore messages posted by the bridge
|
||||||
|
if msg.user.username == config['MATRIX_PNUT_USER']:
|
||||||
|
return
|
||||||
|
|
||||||
|
if msg.source.id == config['PNUTCLIENT_ID']:
|
||||||
|
return
|
||||||
|
|
||||||
|
matrix_id = matrix_id_from_pnut(msg.user.username)
|
||||||
|
matrix_api = ClientAPI(config['MATRIX_AS_ID'],
|
||||||
|
base_url=config['MATRIX_HOST'],
|
||||||
|
token=config['MATRIX_AS_TOKEN'],
|
||||||
|
as_user_id=matrix_id.lower())
|
||||||
|
|
||||||
|
channel_id = int(msg.channel_id)
|
||||||
|
room = PnutChannels.select().where(PnutChannels.pnut_chan ==
|
||||||
|
channel_id).first()
|
||||||
|
|
||||||
|
if meta['channel_type'] == 'io.pnut.core.pm':
|
||||||
|
if room is None:
|
||||||
|
# Do do an invite from the bridge user?
|
||||||
|
logger.debug('new invite?')
|
||||||
|
# create room and included matrix recpient
|
||||||
|
# subscribed_user_ids from meta
|
||||||
|
logger.debug(meta['subscribed_user_ids'])
|
||||||
|
pnut_user = matrix_id_from_pnut(msg.user.username)
|
||||||
|
try:
|
||||||
|
profile = await matrix_api.get_profile(matrix_id.lower())
|
||||||
|
|
||||||
|
except MNotFound:
|
||||||
|
new_matrix_user(msg.user.username)
|
||||||
|
|
||||||
|
invitees=[]
|
||||||
|
for pm_user in meta['subscribed_user_ids']:
|
||||||
|
user = PnutUsers.select().where(PnutUsers.pnut_user_id ==
|
||||||
|
pm_user).first()
|
||||||
|
if int(pm_user) == msg.user.id:
|
||||||
|
continue
|
||||||
|
if user is not None:
|
||||||
|
invitees.append(user.matrix_id)
|
||||||
|
if len(invitees) > 0:
|
||||||
|
room = new_room(pnut_user, invitees, msg.channel_id)
|
||||||
|
|
||||||
|
logger.debug(room)
|
||||||
|
if room is None:
|
||||||
|
logger.debug('-not_mapped-')
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
profile = await matrix_api.get_profile(matrix_id.lower())
|
||||||
|
logger.debug(profile)
|
||||||
|
|
||||||
|
except MNotFound:
|
||||||
|
new_matrix_user(msg.user.username)
|
||||||
|
logger.debug('-new_user-')
|
||||||
|
profile = {'displayname': None}
|
||||||
|
|
||||||
|
if profile['displayname'] != matrix_display_from_pnut(msg.user):
|
||||||
|
await set_matrix_display(msg.user)
|
||||||
|
logger.debug('-set_display-')
|
||||||
|
|
||||||
|
avatar = PnutAvatars.select().where(PnutAvatars.pnut_user ==
|
||||||
|
msg.user.username).first()
|
||||||
|
if avatar is None or avatar.avatar_url != msg.user.content.avatar_image.url:
|
||||||
|
await set_matrix_avatar(msg.user)
|
||||||
|
logger.debug('-set_avatar-')
|
||||||
|
|
||||||
|
if room.is_private:
|
||||||
|
matrix_api_as = ClientAPI(config['MATRIX_AS_ID'],
|
||||||
|
base_url=config['MATRIX_HOST'],
|
||||||
|
token=config['MATRIX_AS_TOKEN'])
|
||||||
|
await matrix_api_as.invite_user(room.room_id, matrix_id.lower())
|
||||||
|
|
||||||
|
await matrix_api.join_room(room.room_id)
|
||||||
|
|
||||||
|
if 'content' in msg:
|
||||||
|
eventtext = TextMessageEventContent(msgtype=MessageType.TEXT,
|
||||||
|
format=Format.HTML,
|
||||||
|
body=msg.content.text,
|
||||||
|
formatted_body=msg.content.html)
|
||||||
|
rid = await matrix_api.send_message(room.room_id, eventtext)
|
||||||
|
bridge_event = Events(
|
||||||
|
event_id=rid,
|
||||||
|
room_id=room.room_id,
|
||||||
|
pnut_id=msg.id,
|
||||||
|
pnut_channel=msg.channel_id
|
||||||
|
)
|
||||||
|
bridge_event.save()
|
||||||
|
|
||||||
|
if 'raw' in msg:
|
||||||
|
logger.debug('-handle media uploads-')
|
||||||
|
await new_media(room.room_id, msg)
|
||||||
|
|
||||||
|
async def new_pnut_post(post, meta):
|
||||||
|
|
||||||
|
if not config['PNUT_GLOBAL']:
|
||||||
|
return
|
||||||
|
|
||||||
|
if (config['PNUT_GLOBAL_HUMAN_ONLY'] and
|
||||||
|
post.user.type in ['feed', 'bot']):
|
||||||
|
logging.debug('-skipping non human post-')
|
||||||
|
return
|
||||||
|
|
||||||
|
if 'content' in post:
|
||||||
|
|
||||||
|
text = ""
|
||||||
|
if 'repost_of' in post:
|
||||||
|
text += f"<{post.user.username}> reposted >> "
|
||||||
|
post = post.repost_of
|
||||||
|
|
||||||
|
matrix_id = matrix_id_from_pnut(post.user.username)
|
||||||
|
matrix_api = ClientAPI(config['MATRIX_AS_ID'],
|
||||||
|
base_url=config['MATRIX_HOST'],
|
||||||
|
token=config['MATRIX_AS_TOKEN'],
|
||||||
|
as_user_id=matrix_id.lower())
|
||||||
|
try:
|
||||||
|
profile = await matrix_api.get_profile(matrix_id.lower())
|
||||||
|
|
||||||
|
except MNotFound:
|
||||||
|
new_matrix_user(post.user.username)
|
||||||
|
|
||||||
|
profile = {'displayname': None}
|
||||||
|
|
||||||
|
if profile['displayname'] != matrix_display_from_pnut(post.user):
|
||||||
|
await set_matrix_display(post.user)
|
||||||
|
logger.debug('-set_display-')
|
||||||
|
|
||||||
|
avatar = PnutAvatars.select().where(PnutAvatars.pnut_user ==
|
||||||
|
post.user.username).first()
|
||||||
|
if (avatar is None or
|
||||||
|
avatar.avatar_url != post.user.content.avatar_image.url):
|
||||||
|
await set_matrix_avatar(post.user)
|
||||||
|
logger.debug('-set_avatar-')
|
||||||
|
|
||||||
|
room_id = config['MATRIX_GLOBAL_ROOM']
|
||||||
|
await matrix_api.join_room(room_id)
|
||||||
|
postlink = f"https://posts.pnut.io/{post.id}"
|
||||||
|
plaintext = f"{post.content.text}\n{postlink}"
|
||||||
|
htmltext = (f"{post.content.html}"
|
||||||
|
f" <a href='{postlink}'>[🔗]</a>")
|
||||||
|
eventtext = TextMessageEventContent(msgtype=MessageType.TEXT,
|
||||||
|
format=Format.HTML,
|
||||||
|
body=plaintext,
|
||||||
|
formatted_body=htmltext)
|
||||||
|
rid = await matrix_api.send_message(room_id, eventtext)
|
||||||
|
bridge_event = Events(
|
||||||
|
event_id=rid,
|
||||||
|
room_id=room_id,
|
||||||
|
pnut_id=post.id,
|
||||||
|
pnut_channel=0
|
||||||
|
)
|
||||||
|
bridge_event.save()
|
||||||
|
|
||||||
|
if 'raw' in post:
|
||||||
|
logger.debug('-handle media uploads-')
|
||||||
|
await new_media(room_id, post)
|
||||||
|
|
||||||
|
async def new_media(room_id, msg):
|
||||||
|
matrix_id = matrix_id_from_pnut(msg.user.username)
|
||||||
|
matrix_api = ClientAPI(config['MATRIX_AS_ID'],
|
||||||
|
base_url=config['MATRIX_HOST'],
|
||||||
|
token=config['MATRIX_AS_TOKEN'],
|
||||||
|
as_user_id=matrix_id.lower())
|
||||||
|
|
||||||
|
if 'io.pnut.core.oembed' in msg.raw:
|
||||||
|
|
||||||
|
for oembed in msg.raw['io.pnut.core.oembed']:
|
||||||
|
info = {}
|
||||||
|
|
||||||
|
if oembed.type == 'photo':
|
||||||
|
msgtype = 'm.image'
|
||||||
|
dl_url = oembed.url
|
||||||
|
info['h'] = oembed.height
|
||||||
|
info['w'] = oembed.width
|
||||||
|
elif oembed.type == 'audio':
|
||||||
|
msgtype = 'm.audio'
|
||||||
|
dl_url = oembed.url
|
||||||
|
elif oembed.type == 'video':
|
||||||
|
msgtype = 'm.video'
|
||||||
|
dl_url = oembed.embeddable_url
|
||||||
|
info['h'] = oembed.height
|
||||||
|
info['w'] = oembed.width
|
||||||
|
elif oembed.type == 'html5video':
|
||||||
|
msgtype = 'm.video'
|
||||||
|
dl_url = oembed.url
|
||||||
|
info['h'] = oembed.height
|
||||||
|
info['w'] = oembed.width
|
||||||
|
elif oembed.type == 'rich':
|
||||||
|
logger.debug("* recieved rich attachment")
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
logger.debug("* recieved unknown attachment")
|
||||||
|
continue
|
||||||
|
|
||||||
|
dl = requests.get(dl_url, stream=True)
|
||||||
|
dl.raise_for_status()
|
||||||
|
with magic.Magic(flags=magic.MAGIC_MIME_TYPE) as m:
|
||||||
|
info['mimetype'] = m.id_buffer(dl.content)
|
||||||
|
info['size'] = len(dl.content)
|
||||||
|
ul = await matrix_api.upload_media(dl.content,
|
||||||
|
mime_type=info['mimetype'])
|
||||||
|
|
||||||
|
if 'title' in oembed:
|
||||||
|
title = oembed.title
|
||||||
|
else:
|
||||||
|
title = ""
|
||||||
|
|
||||||
|
rid = await matrix_api.send_file(room_id, ul,
|
||||||
|
file_name=title,
|
||||||
|
file_type=msgtype,
|
||||||
|
info=info)
|
||||||
|
if 'channel_id' in msg:
|
||||||
|
channel_id = msg.channel_id
|
||||||
|
else:
|
||||||
|
channel_id = 0
|
||||||
|
bridge_event = Events(
|
||||||
|
event_id=rid,
|
||||||
|
room_id=room_id,
|
||||||
|
pnut_id=msg.id,
|
||||||
|
pnut_channel=channel_id
|
||||||
|
)
|
||||||
|
bridge_event.save()
|
||||||
|
|
||||||
|
async def delete_message(msg):
|
||||||
|
matrix_id = matrix_id_from_pnut(msg.user.username)
|
||||||
|
matrix_api = ClientAPI(config['MATRIX_AS_ID'],
|
||||||
|
base_url=config['MATRIX_HOST'],
|
||||||
|
token=config['MATRIX_AS_TOKEN'],
|
||||||
|
as_user_id=matrix_id.lower())
|
||||||
|
|
||||||
|
events = Events.select().where((Events.pnut_id == msg.id) &
|
||||||
|
(Events.deleted == False))
|
||||||
|
for event in events:
|
||||||
|
await matrix_api.redact(event.room_id, event.event_id)
|
||||||
|
event.deleted = True
|
||||||
|
event.save()
|
||||||
|
|
||||||
|
def matrix_id_from_pnut(username):
|
||||||
|
matrix_id = (f"@{config['MATRIX_PNUT_PREFIX']}{username}"
|
||||||
|
f":{config['MATRIX_DOMAIN']}")
|
||||||
|
return matrix_id
|
||||||
|
|
||||||
|
def matrix_display_from_pnut(user):
|
||||||
|
if user.type == 'bot':
|
||||||
|
icon = ' 🤖'
|
||||||
|
elif user.type == 'feed':
|
||||||
|
icon = ' 📰'
|
||||||
|
else:
|
||||||
|
icon = ''
|
||||||
|
|
||||||
|
if 'name' in user:
|
||||||
|
display = user.name + " (@" + user.username + ")" + icon
|
||||||
|
else:
|
||||||
|
display = "@" + user.username + icon
|
||||||
|
return display
|
||||||
|
|
||||||
|
async def set_matrix_display(user):
|
||||||
|
matrix_id = matrix_id_from_pnut(user.username)
|
||||||
|
matrix_api = ClientAPI(config['MATRIX_AS_ID'],
|
||||||
|
base_url=config['MATRIX_HOST'],
|
||||||
|
token=config['MATRIX_AS_TOKEN'],
|
||||||
|
as_user_id=matrix_id.lower())
|
||||||
|
await matrix_api.set_displayname(matrix_display_from_pnut(user))
|
||||||
|
|
||||||
|
async def set_matrix_avatar(user):
|
||||||
|
matrix_id = matrix_id_from_pnut(user.username)
|
||||||
|
matrix_api = ClientAPI(config['MATRIX_AS_ID'],
|
||||||
|
base_url=config['MATRIX_HOST'],
|
||||||
|
token=config['MATRIX_AS_TOKEN'],
|
||||||
|
as_user_id=matrix_id.lower())
|
||||||
|
|
||||||
|
dl = requests.get(user.content.avatar_image.url, stream=True)
|
||||||
|
dl.raise_for_status()
|
||||||
|
with magic.Magic(flags=magic.MAGIC_MIME_TYPE) as m:
|
||||||
|
mtype = m.id_buffer(dl.content)
|
||||||
|
ul = await matrix_api.upload_media(dl.content, mtype)
|
||||||
|
logger.debug(ul)
|
||||||
|
|
||||||
|
try:
|
||||||
|
await matrix_api.set_avatar_url(ul)
|
||||||
|
|
||||||
|
avatar = PnutAvatars.select().where(PnutAvatars.pnut_user ==
|
||||||
|
user.username).first()
|
||||||
|
if avatar is None:
|
||||||
|
avatar = PnutAvatars(pnut_user=user.username,
|
||||||
|
avatar_url=user.content.avatar_image.url)
|
||||||
|
|
||||||
|
else:
|
||||||
|
avatar.avatar_url = user.content.avatar_image.url
|
||||||
|
|
||||||
|
avatar.save()
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
logger.exception('failed to set user avatar')
|
||||||
|
|
||||||
|
def new_matrix_user(username):
|
||||||
|
endpoint = "/_matrix/client/v3/register"
|
||||||
|
url = config['MATRIX_HOST'] + endpoint
|
||||||
|
params = {'kind': 'user'}
|
||||||
|
data = {
|
||||||
|
'type': 'm.login.application_service',
|
||||||
|
'username': config['MATRIX_PNUT_PREFIX'] + username
|
||||||
|
}
|
||||||
|
headers = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Authorization": "Bearer " + config['MATRIX_AS_TOKEN']
|
||||||
|
}
|
||||||
|
logger.debug(data)
|
||||||
|
r = requests.post(url, headers=headers, json=data, params=params)
|
||||||
|
if r.status_code == 200:
|
||||||
|
return
|
||||||
|
|
||||||
|
else:
|
||||||
|
errmsg = f"- unable to register {username} -"
|
||||||
|
logger.warning(errmsg)
|
||||||
|
logger.debug(r.status_code)
|
||||||
|
logger.debug(r.text)
|
||||||
|
return
|
||||||
|
|
||||||
|
def new_room(pnut_user, invitees, chan):
|
||||||
|
dr = None
|
||||||
|
url = matrix_url + '/createRoom'
|
||||||
|
params = {
|
||||||
|
"access_token": config['MATRIX_AS_TOKEN'],
|
||||||
|
"user_id": pnut_user.lower()
|
||||||
|
}
|
||||||
|
content = {
|
||||||
|
"visibility": "private",
|
||||||
|
"is_direct": True,
|
||||||
|
"invite": invitees
|
||||||
|
}
|
||||||
|
headers = {"Content-Type": "application/json"}
|
||||||
|
r = requests.post(url, headers=headers, params=params,
|
||||||
|
data=json.dumps(content))
|
||||||
|
response = r.json()
|
||||||
|
logger.debug(r.status_code)
|
||||||
|
logger.debug(r.text)
|
||||||
|
logger.debug(response)
|
||||||
|
for bridge_user in invitees:
|
||||||
|
direct_room = PnutChannels(pnut_chan=chan, room_id=response['room_id'],
|
||||||
|
is_direct=True, direct_pnut_user=pnut_user,
|
||||||
|
direct_mtrx_user=bridge_user)
|
||||||
|
direct_room.save()
|
||||||
|
return direct_room
|
||||||
|
|
||||||
|
async def on_message(message):
|
||||||
|
logger.debug("on_message: " + message)
|
||||||
|
msg = json.loads(message)
|
||||||
|
|
||||||
|
if 'meta' in msg:
|
||||||
|
meta = msg['meta']
|
||||||
|
else:
|
||||||
|
return
|
||||||
|
|
||||||
|
if 'data' in msg:
|
||||||
|
data = msg['data']
|
||||||
|
else:
|
||||||
|
return
|
||||||
|
|
||||||
|
if 'type' in meta:
|
||||||
|
|
||||||
|
if meta['type'] == "message":
|
||||||
|
|
||||||
|
channel_types = ['io.pnut.core.chat', 'io.pnut.core.pm']
|
||||||
|
if meta['channel_type'] not in channel_types:
|
||||||
|
return
|
||||||
|
|
||||||
|
for item in data:
|
||||||
|
pnut_msg = pnutpy.models.Message.from_response_data(item)
|
||||||
|
|
||||||
|
if 'is_deleted' in meta and meta['is_deleted']:
|
||||||
|
logger.debug("-message: delete-")
|
||||||
|
delete_message(pnut_msg)
|
||||||
|
|
||||||
|
else:
|
||||||
|
await new_pnut_message(pnut_msg, meta)
|
||||||
|
|
||||||
|
elif meta['type'] == "post":
|
||||||
|
|
||||||
|
for item in data:
|
||||||
|
pnut_post = pnutpy.models.Post.from_response_data(item)
|
||||||
|
await new_pnut_post(pnut_post, meta)
|
||||||
|
|
||||||
|
async def asmain():
|
||||||
|
if config['MATRIX_ADMIN_ROOM']:
|
||||||
|
logger.debug("- sould join admin room -")
|
||||||
|
matrix_api_as = ClientAPI(config['MATRIX_AS_ID'],
|
||||||
|
base_url=config['MATRIX_HOST'],
|
||||||
|
token=config['MATRIX_AS_TOKEN'])
|
||||||
|
await matrix_api_as.join_room(config['MATRIX_ADMIN_ROOM'])
|
||||||
|
|
||||||
|
ws_url = 'wss://stream.pnut.io/v1/app?access_token='
|
||||||
|
ws_url += config['PNUT_APPTOKEN'] + '&key=' + config['PNUT_APPKEY']
|
||||||
|
ws_url += '&include_raw=1'
|
||||||
|
async for websocket in connect(uri=ws_url):
|
||||||
|
try:
|
||||||
|
async for message in websocket:
|
||||||
|
await on_message(message)
|
||||||
|
|
||||||
|
await websocket.close()
|
||||||
|
|
||||||
|
except ConnectionClosed:
|
||||||
|
continue
|
||||||
|
|
||||||
|
def main():
|
||||||
|
global config
|
||||||
|
global matrix_url
|
||||||
|
a_parser = argparse.ArgumentParser()
|
||||||
|
a_parser.add_argument('-c', '--config', dest='configyaml',
|
||||||
|
default="config.yaml", help="configuration file")
|
||||||
|
args = a_parser.parse_args()
|
||||||
|
|
||||||
|
with open(args.configyaml, "rb") as config_file:
|
||||||
|
config = yaml.load(config_file, Loader=yaml.SafeLoader)
|
||||||
|
|
||||||
|
db.init(config['SERVICE_DB'])
|
||||||
|
db_create_tables()
|
||||||
|
|
||||||
|
logging.config.dictConfig(config['logging'])
|
||||||
|
redact_filter = MLogFilter()
|
||||||
|
logging.getLogger("werkzeug").addFilter(redact_filter)
|
||||||
|
logging.getLogger("urllib3.connectionpool").addFilter(redact_filter)
|
||||||
|
|
||||||
|
matrix_url = config['MATRIX_HOST'] + '/_matrix/client/v3'
|
||||||
|
|
||||||
|
asyncio.run(asmain())
|
||||||
|
|
||||||
|
logger.info('!! shutdown initiated !!')
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
Loading…
Reference in a new issue