441 lines
10 KiB
Python
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,
|
|
})
|