import vona.federation.rooms as rooms import vona.globals as globals import vona.config as config import json import time from flask import ( jsonify, Response, request, Blueprint, ) server = Blueprint("federation", __name__) http = globals.http_client() class bullshit: def get_json(): return {} def send_join(request, room) -> dict: if globals.room_version_from_id(room) in ["1", "2"]: return rooms.v1_v2(request, room) else: return rooms.v3(request, room) @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": config.server_name, "valid_until_ts": int(time.time() * 1000 + 604800000), "verify_keys": { f"ed25519:{config.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": [config.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(config.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(room, eventId): if globals.room_version_from_id(room) not in ["1", "2"]: return jsonify({ "errcode": "M_INCOMPATIBLE_ROOM_VERSION", "error": "This room is not v1 or v2." }), 400 return jsonify([200, send_join(request, room)]) @server.route("/_matrix/federation/v2/send_join//", methods=["PUT"]) async def send_join_v2(room, eventId): return jsonify(send_join(request, room)) @server.route("/_matrix/federation/v1/make_join//") async def make_join(room, user): if ":" in room: if room.split(":")[1] != config.server_name: return jsonify({ "errcode": "M_FORBIDDEN", "error": "You are not invited to this room." }), 403 else: return jsonify({ "errcode": "M_FORBIDDEN", "error": "You are not invited to this room." }), 403 room_ver = globals.room_version_from_id(room) state = send_join( request=bullshit, room=room )["state"] join = { "content": { "join_authorised_via_users_server": f"@vona:{config.server_name}", "membership": "join" }, "origin": config.server_name, "origin_server_ts": 7, "room_id": room, "sender": user, "state_key": user, "type": "m.room.member", "depth": 7 } if room_ver in ["1", "2"]: join["event_id"] = globals.make_event_id(seed=f"{user}+{room}") 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"] ]] else: join["auth_events"] = [ globals.make_ref_hash(state[0], int(room_ver)), globals.make_ref_hash(state[2], int(room_ver)), globals.make_ref_hash(state[3], int(room_ver)), ] join["prev_events"] = [ globals.make_ref_hash(state[5], int(room_ver)), ] return jsonify({ "event": globals.hash_and_sign_event(join), "room_version": room_ver }) @server.route("/_matrix/federation/v1/publicRooms", methods=["POST", "GET"]) async def room_directory(): return jsonify(globals.room_dir) # 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: # For v1 and v2 rooms event_id = pdu["event_id"] else: # Assume room v4 or over as most rooms will be anyway event_id = globals.make_ref_hash(pdu, 4) response["pdus"][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://{config.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://{config.server_name}/cat","displayname": "Vona"}) @server.route(f"/_matrix/federation/v1/user/devices/@/:{config.server_name}") @server.route("/_matrix/federation/v1/user/devices/") async def user_devices(user): return jsonify({ "devices": [], "stream_id": config.the_funny_number, "user_id": f"@vona:{config.server_name}" }) @server.route("/_matrix/federation/v1/user/keys/query", methods=["POST"]) async def user_keys(): return jsonify({ "device_keys": request.json.get("device_keys", {}) }) @server.route("/_matrix/federation/v2/invite//", methods=["PUT"]) async def invite_user_v2(room, txnId): invite_data = request.json if "event" in invite_data: if "room_version" in invite_data: if invite_data["room_version"] not in [str(i) for i in range(1, 10)]: return jsonify({ "errcode": "M_INCOMPATIBLE_ROOM_VERSION", "error": "Unsupported room version", "room_version": invite_data["room_version"] }), 400 event = invite_data.get("event", {}) content = event.get("content", {}) 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:{config.server_name}" ): return jsonify({ "event": globals.sign_json_without_discard(event), "room_version": invite_data["room_version"] }) return jsonify({ "errcode": "M_FORBIDDEN", "error": "Invalid invitation PDU" }), 403 @server.route("/_matrix/federation/v1/invite//", methods=["PUT"]) async def invite_user_v1(room, txnId): event = request.json content = event.get("content", {}) 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:{config.server_name}" and "event_id" in event and ":" in event["event_id"] ): return jsonify({ "event": globals.sign_json_without_discard(event) }) return jsonify({ "errcode": "M_FORBIDDEN", "error": "Invalid invitation PDU" }), 403 @server.route("/_matrix/federation/v1/hierarchy/") async def space_hierachy(room): return jsonify({ "errcode": "M_NOT_FOUND", "error": "Room does not exist." }), 404 @server.route("/_matrix/federation/v1/org.matrix.msc4358/discover_common_rooms", methods=["POST"]) @server.route("/_matrix/federation/v1/discover_common_rooms", methods=["POST"]) async def discover_common_rooms(): tags = request.json.get("room_participation_tags", []) return jsonify({"recognised_tags": tags}) @server.route("/_matrix/federation/v1/backfill/") async def backfill(room): # TODO: burger king foot lettuce return jsonify({ "origin": config.server_name, "origin_server_ts": int(str(time.time() * 1000).split(".")[0]), "pdus": send_join(bullshit, room)["state"] }) @server.route("/_matrix/federation/unstable/org.matrix.msc4370/extremities/") @server.route("/_matrix/federation/v1/extremities/") async def extremities(room): if ":" in room: if room.split(":")[1] != config.server_name: return jsonify({ "errcode": "M_NOT_FOUND", "error": f"Room is unknown to this server" }), 404 room_ver = globals.room_version_from_id(room) if room_ver in ["1", "2"]: event_id = globals.make_event_id(seed=f"6_{room}") else: event_id = globals.make_ref_hash( send_join(bullshit, room)["state"][5], int(room_ver) ) return jsonify({ "prev_events": [event_id] }) @server.route("/_matrix/federation/v1/state_ids/") async def state_ids(room): if ( "event_id" in request.args and request.args["event_id"].strip() != "" ): evt = request.args["event_id"] def explode(): return jsonify({ "errcode": "M_NOT_FOUND", "error": f"Could not find event {evt}" }), 404 if ":" not in evt: return explode() if ":" in room: if room.split(":")[1] != config.server_name: return explode() else: return explode() server_name = evt.split(":")[1] if server_name == config.server_name: state = send_join(bullshit, room)["state"] event_ids = [] for event in state: if "event_id" in event: event_ids.append(event["event_id"]) else: event_ids.append( globals.make_ref_hash(event) ) if evt in event_ids: return jsonify({ "auth_chain_ids": [event_ids], "pdu_ids": [event_ids] }) else: return explode() try: resp = http.get( path=request.full_path, destination=server_name, ) if resp.status_code != 200: raise return jsonify(resp.json()) except: pass return explode() return jsonify({ "errcode": "M_MISSING_PARAM", "error": "Query parameter 'event_id' was not specified" }), 400 @server.route("/_matrix/federation/unstable/io.fsky.vel/edutypes") @server.route("/_matrix/federation/v1/edutypes") async def edutypes(): return jsonify({ "m.presence": False, "m.receipt": False, "m.typing": False, })