from flask import jsonify, Response, request, send_file, abort, Blueprint import vona.globals as globals from vona.config import * import json import time import os server = Blueprint("federation", __name__) def send_join(request, roomId) -> dict: event_chain = [] event_hashes = [] event_ids = [ globals.make_event_id(seed=f"1_{roomId}"), globals.make_event_id(seed=f"2_{roomId}"), globals.make_event_id(seed=f"3_{roomId}"), globals.make_event_id(seed=f"4_{roomId}"), globals.make_event_id(seed=f"5_{roomId}"), globals.make_event_id(seed=f"6_{roomId}"), ] create_event = { "content": { "m.federate": True, "creator": f"@vona:{server_name}", "room_version": globals.room_version_from_id(roomId) }, "event_id": event_ids[0], "origin_server_ts": 1, "room_id": roomId, "sender": f"@vona:{server_name}", "state_key": "", "depth": 1, "type": "m.room.create", "auth_events": [], "prev_events": [] } screate_event = globals.hash_and_sign_event(create_event) event_chain.append(screate_event) our_join = { "content": { "displayname": "Vona", "avatar_url": f"mxc://{server_name}/cat", "membership": "join" }, "origin_server_ts": 2, "sender": f"@vona:{server_name}", "state_key": f"@vona:{server_name}", "type": "m.room.member", "event_id": event_ids[1], "room_id": roomId, "depth": 2, "auth_events": [[ screate_event["event_id"], screate_event["hashes"] ]], "prev_events": [[ screate_event["event_id"], screate_event["hashes"] ]] } sour_join = globals.hash_and_sign_event(our_join) event_chain.append(sour_join) pls = { "content": { "users": { f"@vona:{server_name}": "100" } }, "origin_server_ts": 3, "room_id": roomId, "sender": f"@vona:{server_name}", "state_key": "", "type": "m.room.power_levels", "event_id": event_ids[2], "depth": 3, "user_id": f"@vona:{server_name}", "auth_events": [ [ screate_event["event_id"], screate_event["hashes"] ], [ sour_join["event_id"], sour_join["hashes"] ] ], "prev_events": [[ sour_join["event_id"], sour_join["hashes"] ]] } spls = globals.hash_and_sign_event(pls) event_chain.append(spls) join_rule = { "content": { "join_rule": "public" }, "origin_server_ts": 4, "sender": f"@vona:{server_name}", "state_key": "", "type": "m.room.join_rules", "event_id": event_ids[3], "room_id": roomId, "depth": 4, "auth_events": [ [ screate_event["event_id"], screate_event["hashes"] ], [ sour_join["event_id"], sour_join["hashes"] ], [ spls["event_id"], spls["hashes"] ] ], "prev_events": [[ spls["event_id"], spls["hashes"] ]] } sjoin_rule = globals.hash_and_sign_event(join_rule) event_chain.append(sjoin_rule) guest_access = { "content": { "guest_access": "forbidden" }, "origin_server_ts": 5, "depth": 5, "sender": f"@vona:{server_name}", "state_key": "", "type": "m.room.guest_access", "event_id": event_ids[4], "room_id": roomId, "auth_events": [ [ screate_event["event_id"], screate_event["hashes"] ], [ sour_join["event_id"], sour_join["hashes"] ], [ spls["event_id"], spls["hashes"] ] ], "prev_events": [[ sjoin_rule["event_id"], sjoin_rule["hashes"] ]] } sguest_access = globals.hash_and_sign_event(guest_access) event_chain.append(sguest_access) history = { "content": { "history_visibility": "shared" }, "type": "m.room.history_visibility", "sender": f"@vona:{server_name}", "state_key": "", "origin_server_ts": 6, "depth": 6, "event_id": event_ids[5], "room_id": roomId, "auth_events": [ [ screate_event["event_id"], screate_event["hashes"] ], [ sour_join["event_id"], sour_join["hashes"] ], [ spls["event_id"], spls["hashes"] ] ], "prev_events": [[ sguest_access["event_id"], sguest_access["hashes"] ]] } shistory = globals.hash_and_sign_event(history) event_chain.append(shistory) remote_join = request.get_json() response = { "auth_chain": event_chain, "event": remote_join, "members_omitted": False, "servers_in_room": [server_name], "state": event_chain } return response @server.route("/_matrix/federation/v1/version") async def version(): return jsonify({ "server": { "version": globals.version, "name": "Vona" } }) @server.route("/_matrix/key/v2/server") async 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") async def room_query(): return jsonify({ "room_id": globals.make_event_id().replace("$", "!"), "servers": [server_name] }) @server.route("/_matrix/federation/v1/media/download/") async 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/") async def thumbnail_media(media_id): return jsonify({ "errcode": "M_TOO_CUTE", "error": "Cat is too cute to thumbnail" }), 418 @server.route("/_matrix/federation/v1/send_join//", methods=["PUT"]) async def send_join_v1(roomId, eventId): return jsonify([200, send_join(request, roomId)]) @server.route("/_matrix/federation/v2/send_join//", methods=["PUT"]) async def send_join_v2(roomId, eventId): return jsonify(send_join(request, roomId)) @server.route("/_matrix/federation/v1/make_join//") async def make_join(roomId, userId): def not_invited(): return jsonify({ "errcode": "M_FORBIDDEN", "error": "You are not invited to this room." }), 403 try: if roomId.split(":")[1] != server_name: return not_invited() except: return not_invited() class bullshit: def get_json(): return {} state = send_join( request=bullshit, roomId=roomId )["state"] join = { "content": { "join_authorised_via_users_server": f"@vona:{server_name}", "membership": "join" }, "origin": server_name, "origin_server_ts": 7, "room_id": roomId, "sender": userId, "state_key": userId, "type": "m.room.member", "depth": 7 } join["auth_events"] = [ [ state[0]["event_id"], state[0]["hashes"] ], [ state[2]["event_id"], state[2]["hashes"] ], [ state[3]["event_id"], state[3]["hashes"] ] ] join["prev_events"] = [[ state[5]["event_id"], state[5]["hashes"] ]] return jsonify({ "event": globals.hash_and_sign_event(join), "room_version": globals.room_version_from_id(roomId) }) @server.route("/_matrix/federation/v1/publicRooms", methods=["POST", "GET"]) async def room_directory(): return jsonify({ "chunk": [], "total_room_count_estimate": 0 }) # https://spec.matrix.org/latest/server-server-api/#transactions @server.route("/_matrix/federation/v1/send/", methods=["PUT"]) async def receive_txn(txnId): # 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. # These events are: # - m.room.create # - m.room.member # - m.room.join_rules # - m.room.power_levels # - m.room.third_party_invite # # As per https://spec.matrix.org/latest/rooms/v2/#authorization-rules data = request.data.decode("utf-8") parsed_data = json.loads(data) response = {"pdus": {}} if "pdus" in parsed_data: for pdu in parsed_data["pdus"]: if "event_id" in pdu: response["pdus"][pdu["event_id"]] = {} return jsonify(response) @server.route("/_matrix/federation/v1/query/profile") async 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"}) 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"}) # https://spec.matrix.org/latest/server-server-api/#device-management @server.route(f"/_matrix/federation/v1/user/devices/@/:{server_name}") @server.route("/_matrix/federation/v1/user/devices/") async 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"]) async def user_keys(): try: users = request.json["device_keys"] except: return jsonify({ "errcode": "M_NOT_FOUND", "error": "User does not exist" }), 404 return jsonify({"device_keys": users}) @server.route("/_matrix/federation/v2/invite//", methods=["PUT"]) async def invite_user_v2(room, txnId): return invite_user(request.data) @server.route("/_matrix/federation/v1/invite//", methods=["PUT"]) async def invite_user_v1(room, txnId): return [200, invite_user(request.data)] def invite_user(data): try: invite_data = json.loads(data) except: return jsonify({"errcode":"M_NOT_JSON","error":"Content not JSON."}), 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 event = invite_data.get("event", {}) content = event.get("content", {}) # NOTE to crispycat: I know you loooooove this syntax if ( "content" in event and "membership" in content and "state_key" in event and "room_id" in event and content["membership"] == "invite" and event["state_key"] == f"@vona:{server_name}" ): return jsonify({"event": globals.sign_json_without_discard(invite_data["event"]), "room_version": "2"}) return jsonify({ "errcode": "M_FORBIDDEN", "error": "Invalid invitation PDU" }), 403 @server.route("/_matrix/federation/v1/hierarchy/") async def space_hierachy(roomId): return jsonify({ "errcode": "M_NOT_FOUND", "error": "Room does not exist." })