Files
matrix-vona/vona/federation/__init__.py
2025-10-13 10:38:24 -04:00

502 lines
11 KiB
Python

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/<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(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/<roomId>/<eventId>", methods=["PUT"])
async def send_join_v1(roomId, eventId):
return jsonify([200, send_join(request, roomId)])
@server.route("/_matrix/federation/v2/send_join/<roomId>/<eventId>", methods=["PUT"])
async def send_join_v2(roomId, eventId):
return jsonify(send_join(request, roomId))
@server.route("/_matrix/federation/v1/make_join/<roomId>/<userId>")
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/<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:
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/<user>")
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/<room>/<txnId>", methods=["PUT"])
async def invite_user_v2(room, txnId):
return invite_user(request.data)
@server.route("/_matrix/federation/v1/invite/<room>/<txnId>", 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"] not in ["1", "2"]:
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", {})
# 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": invite_data["room_version"]
})
return jsonify({
"errcode": "M_FORBIDDEN",
"error": "Invalid invitation PDU"
}), 403
@server.route("/_matrix/federation/v1/hierarchy/<roomId>")
async def space_hierachy(roomId):
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
class bullshit:
def get_json():
return {}
return jsonify({
"origin": server_name,
"origin_server_ts": int(str(time.time() * 1000).split(".")[0]),
"pdus": send_join(bullshit, room)["state"]
})