Files
matrix-vona/vona/federation/__init__.py

441 lines
10 KiB
Python

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/<media_id>")
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/<media_id>")
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/<room>/<path:eventId>", 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/<room>/<path:eventId>", methods=["PUT"])
async def send_join_v2(room, eventId):
return jsonify(send_join(request, room))
@server.route("/_matrix/federation/v1/make_join/<room>/<user>")
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": {
"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, int(room_ver)),
"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/<txnId>", 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/<user>")
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/<room>/<txnId>", 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/<room>/<txnId>", 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/<room>")
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/<room>")
async def backfill(room):
# TODO: burger king foot lettuce
return jsonify({
"origin": config.server_name,
"origin_server_ts": globals.time(),
"pdus": send_join(bullshit, room)["state"]
})
@server.route("/_matrix/federation/unstable/org.matrix.msc4370/extremities/<room>")
@server.route("/_matrix/federation/v1/extremities/<room>")
async def extremities(room):
if ":" in room:
if room.split(":")[1] != config.server_name:
return jsonify({
"errcode": "M_NOT_FOUND",
"error": "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/<room>")
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 Exception:
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,
})