from flask import Flask, jsonify, Response, request, send_file, abort, Blueprint from config import * import globals import json import time import os server = Blueprint('matrix_server', __name__) @server.route('/_matrix/federation/v1/version') def version(): return jsonify({"server": {"version": globals.vona_version,"name": "Vona"}}) @server.route('/_matrix/key/v2/server') def keys(): return jsonify(globals.sign_json({ "old_verify_keys": {}, "server_name": server_name, "valid_until_ts": int(time.time() * 1000 + 604800000), "verify_keys": { f"ed25519:{signing_key.split()[1]}": { "key": globals.pubkey() } } })) @server.route('/_matrix/federation/v1/query/directory') def room_query(): return jsonify({ "room_id": room_dir_room['chunk'][0]['room_id'], "servers": [server_name] }) @server.route('/_matrix/federation/v1/media/download/') def download_media(media_id): # Auth media requires this to be # multipart despite not even using # it for anything. Minor annoyance. with open(cat, 'rb') as img_file: image_data = img_file.read() boundary = '----WebKitFormBoundary7MA4YWxkTrZu0gW' response_body = ( f'--{boundary}\r\n' f'Content-Type: application/json\r\n\r\n' f'{{}}\r\n' f'--{boundary}\r\n' f'Content-Type: image/jpeg\r\n' f'Content-Disposition: attachment; filename="cat.jpg"\r\n\r\n' ).encode() + image_data + f'\r\n--{boundary}--\r\n'.encode() response = Response(response_body, content_type=f'multipart/mixed; boundary={boundary}') response.status_code = 200 return response @server.route('/_matrix/federation/v1/media/thumbnail/') def thumbnail_media(media_id): return jsonify({"errcode": "M_TOO_CUTE","error": "Cat is too cute to thumbnail"}), 403 @server.route('/_matrix/federation/v1/send_join//', methods=['PUT']) def send_join_v1(roomId, eventId): abort(500) @server.route('/_matrix/federation/v2/send_join//', methods=['PUT']) def send_join_v2(roomId, eventId): abort(500) @server.route('/_matrix/federation/v1/make_join//') def make_join(roomId, userId): return jsonify({ "event": { "content": { "join_authorised_via_users_server": f"@vona:{server_name}", "membership": "join" }, "origin": server_name, "origin_server_ts": str(the_funny_number), "room_id": roomId, "sender": userId, "state_key": userId, "type": "m.room.member" }, "room_version": "2" }) @server.route('/_matrix/federation/v1/publicRooms', methods=['POST', 'GET']) def room_directory(): return jsonify(room_dir_room) # https://spec.matrix.org/v1.15/server-server-api/#transactions @server.route('/_matrix/federation/v1/send/', methods=['PUT']) def federation_send(txnId): # This only works for v2 rooms, but anything # bigger than v2 isn't real, so not a problem # We will need to implement a way to store every # event we need if we want to send events in the # future. We don't send events currently, however. data = request.data.decode('utf-8') parsed_data = json.loads(data) response = {"pdus": {}} if 'pdus' in parsed_data and parsed_data['pdus']: for pdu in parsed_data['pdus']: if 'hashes' in pdu and 'sha256' in pdu['hashes']: pdu_hash = pdu['hashes']['sha256'] response_key = f"${pdu_hash}:{parsed_data['origin']}" response["pdus"][response_key] = {} return jsonify(response) @server.route('/_matrix/federation/v1/query/profile') def user_profile(): field = request.args.get('field') if field: if field == 'avatar_url': return jsonify({"avatar_url":f"mxc://{server_name}/cat"}) elif field == 'displayname': return jsonify({"displayname":"Vona"}) else: return jsonify({"errcode": "M_NOT_FOUND","error": "The requested profile key does not exist."}), 404 return jsonify({"avatar_url": f"mxc://{server_name}/cat","displayname": "Vona"}) @server.route('/_matrix/federation/v1/user/devices/') def user_devices(user): return jsonify({ "devices": [], "stream_id": the_funny_number, "user_id": f"@vona:{server_name}" }) @server.route('/_matrix/federation/v1/user/keys/query', methods=['POST']) def user_keys(): return jsonify({"device_keys":{f"@vona:{server_name}":{}}}) # https://spec.matrix.org/v1.15/server-server-api/#inviting-to-a-room @server.route('/_matrix/federation/v2/invite//', methods=['PUT']) def invite_user_v2(room, txnId): return invite_user(request.data.decode('utf-8'), 2) @server.route('/_matrix/federation/v1/invite//', methods=['PUT']) def invite_user_v1(room, txnId): return invite_user(request.data.decode('utf-8'), 1) def invite_user(data, endpVer): try: invite_data = json.loads(data) except: return jsonify({"errcode":"M_NOT_JSON","error":"Content not JSON."}), 400 if "event" in invite_data: if "room_version" in invite_data: if invite_data["room_version"] != "2": return jsonify({ "errcode": "M_INCOMPATIBLE_ROOM_VERSION", "error": "Vona only supports room version 2.", "room_version": invite_data["room_version"] }), 400 else: if "content" in invite_data["event"]: if "membership" in invite_data["event"]["content"] and invite_data["event"]["content"]["membership"] == "invite": if endpVer == 1: return jsonify([ 200, {"event": globals.sign_json(invite_data["event"])} ]) else: return jsonify({"event": globals.sign_json(invite_data["event"]), "room_version": "2"}) return jsonify({ "errcode": "M_FORBIDDEN", "error": "Invalid invitation PDU" }), 403 @server.route('/_matrix/federation/v1/hierarchy/') def space_hierachy(roomId): room = room_dir_room['chunk'][0] return jsonify({ "children": [{ "avatar_url": room['avatar_url'], "children_state": [{ "content": {"via": [server_name]}, "origin_server_ts": the_funny_number, "sender": f"@vona:{server_name}", "state_key": room['room_id'], "type": "m.space.child" }], "guest_can_join": room['guest_can_join'], "join_rule": room['join_rule'], "name": room['name'], "num_joined_members": room['num_joined_members'], "room_id": room['room_id'], "room_type": room['room_type'], "topic": room['topic'], "world_readable": room['world_readable'] }], "inaccessible_children": [], "room": { "allowed_room_ids": [], "avatar_url": room['avatar_url'], "children_state": [{ "content": {"via": [server_name]}, "origin_server_ts": the_funny_number, "sender": f"@vona:{server_name}", "state_key": room['room_id'], "type": "m.space.child" }], "guest_can_join": room['guest_can_join'], "join_rule": room['join_rule'], "name": room['name'], "num_joined_members": room['num_joined_members'], "room_id": room['room_id'], "room_type": room['room_type'], "topic": room['topic'], "world_readable": room['world_readable'] } })