13 Commits
1.4.3 ... 1.4.4

Author SHA1 Message Date
492ffc62e9 1.4.4 2025-10-17 17:49:30 -04:00
9ca4c913f3 Make sending Matrix requests easier 2025-10-17 17:44:41 -04:00
6dd4fb04e3 Follow upstream Hammerhead response for state-resolver 2025-10-17 15:49:19 -04:00
f771130a16 Implement MSC4367 room directory 2025-10-15 13:46:00 -04:00
9aa2e062e5 Better logging (for the 3rd time), fix devices list over federation 2025-10-13 17:55:48 -04:00
c8d6f57e5b add security policy 2025-10-13 16:08:17 -04:00
753625ad5c some Citadel endpoints 2025-10-13 15:44:57 -04:00
74a36e0b34 Import configuration properly 2025-10-13 15:20:01 -04:00
b178011ad9 Remove padding from base64 hashes 2025-10-13 11:12:39 -04:00
76f89beb29 Add backfill 2025-10-13 10:38:24 -04:00
7e8eb6de7f msc: 4358 2025-10-13 09:53:56 -04:00
4e28507dea Add Hammerhead endpoints 2025-10-12 23:39:25 -04:00
357d3b7429 Add resolvematrix dependency 2025-10-12 20:36:57 -04:00
12 changed files with 398 additions and 127 deletions

3
SECURITY.md Normal file
View File

@@ -0,0 +1,3 @@
# Security
If you believe you have found a vulnerability in Vona, please email vel@riseup.net or DM `@vel:faelix.im` on Matrix.
Do not disclose the details of the vulnerability until a public fix has been made.

View File

@@ -8,6 +8,7 @@ dependencies = [
"httpx (>=0.28.1,<0.29.0)",
"pynacl (>=1.6.0,<2.0.0)",
"flask[async] (>=3.1.2,<4.0.0)",
"resolvematrix @ git+https://codeberg.org/timedout/resolvematrix.git",
]
authors = [

View File

@@ -49,7 +49,6 @@ async def handle_logging(response):
response.headers["Access-Control-Allow-Methods"] = "GET, HEAD, POST, PUT, DELETE, OPTIONS"
if request.method == "OPTIONS":
# Discard logs for OPTIONS
return response
origin = "unknown"
@@ -57,13 +56,31 @@ async def handle_logging(response):
try:
if "Authorization" in request.headers:
if request.headers["Authorization"].split()[0] == "X-Matrix":
origin = request.headers["Authorization"].split('origin="')[1].split('"')[0]
origin = (
request.headers["Authorization"]
.split("origin=")[1]
.split(",")[0]
)
while '"' in origin:
origin = origin.replace('"', "")
if origin == config.server_name:
return response
else:
origin = "client"
except:
pass
print(f'[{origin}] [{request.remote_addr}] [{datetime.now().strftime("%d/%b/%Y:%H:%M:%S")}] {request.method} {request.full_path} {response.status_code}')
print(
f"[{origin}] " +
f'[{datetime.now().strftime("%d/%b/%Y:%H:%M:%S")}] ' +
request.method + " " +
request.full_path.rstrip("?") + " " +
str(response.status_code)
)
return response
@@ -115,7 +132,11 @@ async def client():
def federation_self_test():
try:
resp = globals.http_client.get(f"https://{config.server_name}/.well-known/matrix/server")
resp = globals.http_client().get(
path="/",
destination=config.server_name,
)
resp.raise_for_status()
print("[INFO] Federation self-test OK")
except Exception as e:

View File

@@ -56,7 +56,7 @@ async def spec_versions():
@client.route("/_matrix/client/v3/admin/whois/<user>")
@client.route("/_matrix/client/r0/admin/whois/<user>")
async def whois(user):
if userId.startswith("@"):
if user.startswith("@"):
return jsonify({
"devices": {
"": {
@@ -644,14 +644,10 @@ async def presence(user):
if request.method == "PUT":
return jsonify({})
return jsonify({
"presence": "online"
})
return jsonify({"presence": "online"})
@client.route("/_matrix/client/v3/publicRooms", methods=["GET", "POST"])
@client.route("/_matrix/client/r0/publicRooms", methods=["GET", "POST"])
async def room_directory():
return jsonify({
"chunk": [],
"total_room_count_estimate": 0
})
return jsonify(globals.room_dir)

View File

@@ -2,18 +2,21 @@ from flask import Blueprint
custom = Blueprint("custom", __name__)
# This implements custom endpoints
# used by other homeserver
# implementations. They do not start
# with /_matrix/
# This implements non-standard
# endpoints created by other
# homeserver implementations.
from .hammerhead import hammerhead
from .conduwuit import conduwuit
from .dendrite import dendrite
from .telodendria import telo
from .synapse import synapse
from .citadel import citadel
custom.register_blueprint(hammerhead)
custom.register_blueprint(conduwuit)
custom.register_blueprint(dendrite)
custom.register_blueprint(synapse)
custom.register_blueprint(citadel)
custom.register_blueprint(telo)

61
vona/custom/citadel.py Normal file
View File

@@ -0,0 +1,61 @@
from flask import Blueprint, jsonify, request
import vona.config as config
import base64
import os
citadel = Blueprint("citadel", __name__)
# These are endpoints made by Thales Citadel
# TODO: Add more endpoints, this likely
# isn't all of them
@citadel.route("/_matrix/client/r0/citadel/stats/m.news/<event>", methods=["GET", "PUT"])
async def news_stats(event):
if request.method == "PUT":
return jsonify({"success": True})
return jsonify({
"total_clicks": config.the_funny_number,
"user_readings": config.the_funny_number
})
@citadel.route("/_matrix/client/r0/citadel/rooms/<room>/closeRoom", methods=["POST"])
async def close_room(room):
store_response = request.json.get("store_response", True)
operation_id = base64.b64encode(
bytes(
os.urandom(8).hex(),
"utf-8"
)
).decode("utf-8")[:14]
if store_response:
resp = {"operation_id": operation_id}
else:
resp = {
"operation_id": operation_id,
"previous_state": {
"progress": {
"steps": {
"step_kick_users": {
"status": "complete"
},
"step_purge_history": {
"status": "complete"
},
"step_purge_media": {
"status": "complete"
}
}
},
"status": "complete"
}
}
return jsonify(resp)

77
vona/custom/hammerhead.py Normal file
View File

@@ -0,0 +1,77 @@
from flask import Blueprint, request, jsonify
from vona.config import the_funny_number
from datetime import datetime, timezone
from vona.federation import send_join
import vona.globals as globals
import vona.config as config
import time
hammerhead = Blueprint("hammerhead", __name__)
# Hammerhead endpoints. Not documented, but code is at:
# https://codeberg.org/timedout/hammerhead/src/branch/dev/hammerhead/router/routes/hammerhead
@hammerhead.route("/_hammerhead/uptime")
async def uptime():
return jsonify({"started_at": config.the_funny_number})
@hammerhead.route("/_hammerhead/version")
async def version():
return jsonify({"version": globals.version})
@hammerhead.route("/_hammerhead/admin/server-info/<server>")
async def server_info(server):
return jsonify({
"destination": {
"expires": datetime.now(timezone.utc).isoformat(),
"host_header": server,
"ip_port": [
f"{server}:443"
],
"server_name": server
},
"keys": globals.sign_json({
"old_verify_keys": {},
"server_name": server,
"valid_until_ts": int(time.time() * 1000 + 604800000),
"verify_keys": {
f"ed25519:{config.signing_key.split()[1]}": {
"key": globals.pubkey()
}
}
}),
"software": {
"server": {
"version": globals.version,
"name": "Vona"
}
}
})
@hammerhead.route("/_hammerhead/admin/rooms/<room>/state-resolver")
async def room_state(room):
class bullshit:
def get_json():
return {}
state = send_join(bullshit, room)["state"]
formatted_state = {}
event_cache = {}
for event in state:
key = f"({event["type"]},'{event["state_key"]}')"
formatted_state[key] = event
event_id = event["event_id"]
event_cache[event_id] = event
return jsonify({
"current_state": formatted_state,
"event_cache": event_cache,
"room_id": room,
"room_version": globals.room_version_from_id(room)
})

View File

@@ -24,9 +24,9 @@ synapse = Blueprint("synapse", __name__)
@synapse.route("/_synapse/admin/v1/media/unquarantine/<s>/<media_id>", methods=["POST"])
@synapse.route("/_synapse/admin/v1/federation/destinations/<destination>/reset_connection", methods=["POST"])
@synapse.route("/_synapse/admin/v1/rooms/<room>")
@synapse.route("/_synapse/admin/v1/rooms/<room_id>/timestamp_to_event")
@synapse.route("/_synapse/admin/v1/rooms/<room>/timestamp_to_event")
@synapse.route("/_synapse/admin/v2/rooms/delete_status/<delete_id>")
@synapse.route("/_synapse/admin/v1/rooms/<room_id_or_alias>/make_room_admin", methods=["POST"])
@synapse.route("/_synapse/admin/v1/rooms/<room>/make_room_admin", methods=["POST"])
async def response(**kwargs):
return jsonify({})
@@ -118,9 +118,24 @@ async def account_data(user_id):
@synapse.route("/_synapse/admin/v1/users/<user_id>/media", methods=["GET", "DELETE"])
async def account_media(user_id):
if request.method == "GET":
return jsonify({"media": [{"created_ts":config.the_funny_number,"last_access_ts":config.the_funny_number,"media_id":"cat","media_length":config.the_funny_number,"media_type":"image/jpeg","quarantined_by":"null","safe_from_quarantine":False,"upload_name":"cat.jpg"}], "total": config.the_funny_number})
return jsonify({
"media": [{
"created_ts": config.the_funny_number,
"last_access_ts": config.the_funny_number,
"media_id": "cat",
"media_length": config.the_funny_number,
"media_type": "image/jpeg",
"quarantined_by": None,
"safe_from_quarantine": False,
"upload_name": "cat.jpg"
}],
"total": config.the_funny_number
})
return jsonify({"deleted_media": ["cat"], "total": config.the_funny_number})
return jsonify({
"deleted_media": ["cat"],
"total": config.the_funny_number
})
@synapse.route("/_synapse/admin/v1/users/<user_id>/login", methods=["POST"])
async def account_login(user_id):
@@ -137,7 +152,7 @@ async def device_list(user_id):
"devices": [{
"device_id": "VVOONNAA",
"display_name": "Vona",
"last_seen_ip": "127.0.0.1",
"last_seen_ip": config.addr,
"last_seen_ts": config.the_funny_number,
"last_seen_user_agent": f"Vona/{globals.version}"
}],
@@ -152,7 +167,7 @@ async def device_info(user_id, device_id):
return jsonify({
"device_id": "VVOONNAA",
"display_name": "Vona",
"last_seen_ip": "127.0.0.1",
"last_seen_ip": config.addr,
"last_seen_ts": config.the_funny_number,
"last_seen_user_agent": f"Vona/{globals.version}"
})
@@ -169,11 +184,11 @@ async def username_available():
@synapse.route("/_synapse/admin/v1/threepid/<medium>/users/<addr>")
@synapse.route("/_synapse/admin/v1/auth_providers/<provider>/users/<ext>")
async def threepid(p, a):
async def threepid(**kwargs):
return jsonify({"user_id": f"@vona:{config.server_name}"})
@synapse.route("/_synapse/admin/v1/<user_id>/redact")
def redact(user_id):
async def redact(user_id):
return jsonify({"redact_id": os.urandom(16).hex()})
@synapse.route("/_synapse/admin/v1/user/redact_status/<redact_id>")
@@ -191,8 +206,8 @@ async def register():
return jsonify({"access_token": "vona"})
@synapse.route("/_synapse/admin/v1/join/<roomId>", methods=["POST"])
async def membership_manipulation(roomId):
@synapse.route("/_synapse/admin/v1/join/<room>", methods=["POST"])
async def membership_manipulation(room):
return jsonify({"room_id": globals.make_event_id().replace("$", "!")})
@synapse.route("/_synapse/admin/v1/account_validity/validity", methods=["POST"])
@@ -204,8 +219,8 @@ async def account_validity():
async def server_notice(**kwargs):
return jsonify({"event_id": globals.make_event_id()})
@synapse.route("/_synapse/admin/v1/purge_history/<room_id>/<event_id>", methods=["POST"])
@synapse.route("/_synapse/admin/v1/purge_history/<room_id>", methods=["POST"])
@synapse.route("/_synapse/admin/v1/purge_history/<room>/<event_id>", methods=["POST"])
@synapse.route("/_synapse/admin/v1/purge_history/<room>", methods=["POST"])
async def purge_event(**kwargs):
return jsonify({"purge_id": os.urandom(16).hex()})
@@ -213,12 +228,12 @@ async def purge_event(**kwargs):
async def purge_status(purge_id):
return jsonify({"status":"active"})
@synapse.route("/_synapse/admin/v1/room/<room_id>/media")
async def room_media(room_id):
@synapse.route("/_synapse/admin/v1/room/<room>/media")
async def room_media(room):
return jsonify({"local": [f"mxc://{config.server_name}/cat"], "remote": []})
@synapse.route("/_synapse/admin/v1/room/<room_id>/media/quarantine", methods=["POST"])
async def quarantine_room_media(room_id):
@synapse.route("/_synapse/admin/v1/room/<room>/media/quarantine", methods=["POST"])
async def quarantine_room_media(room):
return jsonify({"num_quarantined": config.the_funny_number})
@synapse.route("/_synapse/admin/v1/media/<s>/delete", methods=["POST"])
@@ -233,7 +248,15 @@ async def delete_remote_media():
@synapse.route("/_synapse/admin/v1/statistics/users/media")
async def media_stats():
return jsonify({"users":[{"displayname":"Vona","media_count":config.the_funny_number,"media_length":config.the_funny_number,"user_id":f"@vona:{config.server_name}"}],"total":config.the_funny_number})
return jsonify({
"users": [{
"displayname": "Vona",
"media_count": config.the_funny_number,
"media_length": config.the_funny_number,
"user_id": f"@vona:{config.server_name}"
}],
"total": config.the_funny_number
})
@synapse.route("/_synapse/admin/v1/statistics/database/rooms")
async def room_stats():
@@ -302,7 +325,7 @@ async def interact_with_reported_event(report_id):
@synapse.route("/_synapse/admin/v1/federation/destinations")
async def federation_destinations():
return jsonify({
"destinations": [{}],
"destinations": [],
"total": 0
})
@@ -366,7 +389,7 @@ async def rooms():
"total_rooms": 0
})
@synapse.route("/_synapse/admin/v1/rooms/<room_id>/members")
@synapse.route("/_synapse/admin/v1/rooms/<room>/members")
async def room_members(room):
return jsonify({
"members": [
@@ -375,11 +398,11 @@ async def room_members(room):
"total": 1
})
@synapse.route("/_synapse/admin/v1/rooms/<room_id>/state")
@synapse.route("/_synapse/admin/v1/rooms/<room>/state")
async def room_state(room):
return jsonify({"state": []})
@synapse.route("/_synapse/admin/v1/rooms/<room_id>/state")
@synapse.route("/_synapse/admin/v1/rooms/<room>/state")
async def room_messages(room):
return jsonify({
"chunk": [],
@@ -387,11 +410,11 @@ async def room_messages(room):
"start": "vona"
})
@synapse.route("/_synapse/admin/v1/rooms/<room_id>/block", methods=["GET", "PUT"])
@synapse.route("/_synapse/admin/v1/rooms/<room>/block", methods=["GET", "PUT"])
async def block_room(room):
return jsonify({"block": False})
@synapse.route("/_synapse/admin/v1/rooms/<room_id>", methods=["DELETE"])
@synapse.route("/_synapse/admin/v1/rooms/<room>", methods=["DELETE"])
async def room_delete(room):
return jsonify({
"kicked_users": [
@@ -399,18 +422,18 @@ async def room_delete(room):
],
"failed_to_kick_users": [],
"local_aliases": [],
"new_room_id": f"!vona:{config.server_name}"
"new_room_id": globals.make_event_id(seed=room)
})
@synapse.route("/_synapse/admin/v2/rooms/<room_id>", methods=["DELETE"])
@synapse.route("/_synapse/admin/v2/rooms/<room>", methods=["DELETE"])
async def room_delete_v2(room):
return jsonify({"delete_id": "vona"})
@synapse.route("/_synapse/admin/v2/rooms/<room_id>/delete_status")
@synapse.route("/_synapse/admin/v2/rooms/<room>/delete_status")
async def room_delete_status(room):
return jsonify({"results": []})
@synapse.route("/_synapse/admin/v1/rooms/<room_id_or_alias>/forward_extremities", methods=["GET"])
@synapse.route("/_synapse/admin/v1/rooms/<room>/forward_extremities", methods=["GET"])
async def forward_extremities(room):
if request.method == "DELETE":
return jsonify({"deleted": 0})

View File

@@ -14,7 +14,6 @@ async def process_management(**kwargs):
return jsonify({})
@telo.route("/_telodendria/admin/v1/privileges/<lp>", methods=["GET", "PUT", "POST", "DELETE"])
@telo.route("/_telodendria/admin/v1/privileges/", methods=["GET", "POST"])
async def privileges(lp=None):

View File

@@ -1,6 +1,6 @@
from flask import jsonify, Response, request, send_file, abort, Blueprint
import vona.globals as globals
from vona.config import *
import vona.config as config
import json
import time
import os
@@ -24,13 +24,13 @@ def send_join(request, roomId) -> dict:
create_event = {
"content": {
"m.federate": True,
"creator": f"@vona:{server_name}",
"creator": f"@vona:{config.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}",
"sender": f"@vona:{config.server_name}",
"state_key": "",
"depth": 1,
"type": "m.room.create",
@@ -44,12 +44,12 @@ def send_join(request, roomId) -> dict:
our_join = {
"content": {
"displayname": "Vona",
"avatar_url": f"mxc://{server_name}/cat",
"avatar_url": f"mxc://{config.server_name}/cat",
"membership": "join"
},
"origin_server_ts": 2,
"sender": f"@vona:{server_name}",
"state_key": f"@vona:{server_name}",
"sender": f"@vona:{config.server_name}",
"state_key": f"@vona:{config.server_name}",
"type": "m.room.member",
"event_id": event_ids[1],
"room_id": roomId,
@@ -70,17 +70,17 @@ def send_join(request, roomId) -> dict:
pls = {
"content": {
"users": {
f"@vona:{server_name}": "100"
f"@vona:{config.server_name}": "100"
}
},
"origin_server_ts": 3,
"room_id": roomId,
"sender": f"@vona:{server_name}",
"sender": f"@vona:{config.server_name}",
"state_key": "",
"type": "m.room.power_levels",
"event_id": event_ids[2],
"depth": 3,
"user_id": f"@vona:{server_name}",
"user_id": f"@vona:{config.server_name}",
"auth_events": [
[
screate_event["event_id"],
@@ -105,7 +105,7 @@ def send_join(request, roomId) -> dict:
"join_rule": "public"
},
"origin_server_ts": 4,
"sender": f"@vona:{server_name}",
"sender": f"@vona:{config.server_name}",
"state_key": "",
"type": "m.room.join_rules",
"event_id": event_ids[3],
@@ -140,7 +140,7 @@ def send_join(request, roomId) -> dict:
},
"origin_server_ts": 5,
"depth": 5,
"sender": f"@vona:{server_name}",
"sender": f"@vona:{config.server_name}",
"state_key": "",
"type": "m.room.guest_access",
"event_id": event_ids[4],
@@ -173,7 +173,7 @@ def send_join(request, roomId) -> dict:
"history_visibility": "shared"
},
"type": "m.room.history_visibility",
"sender": f"@vona:{server_name}",
"sender": f"@vona:{config.server_name}",
"state_key": "",
"origin_server_ts": 6,
"depth": 6,
@@ -208,7 +208,7 @@ def send_join(request, roomId) -> dict:
"auth_chain": event_chain,
"event": remote_join,
"members_omitted": False,
"servers_in_room": [server_name],
"servers_in_room": [config.server_name],
"state": event_chain
}
@@ -228,10 +228,10 @@ async def version():
async def keys():
return jsonify(globals.sign_json({
"old_verify_keys": {},
"server_name": server_name,
"server_name": config.server_name,
"valid_until_ts": int(time.time() * 1000 + 604800000),
"verify_keys": {
f"ed25519:{signing_key.split()[1]}": {
f"ed25519:{config.signing_key.split()[1]}": {
"key": globals.pubkey()
}
}
@@ -241,7 +241,7 @@ async def keys():
async def room_query():
return jsonify({
"room_id": globals.make_event_id().replace("$", "!"),
"servers": [server_name]
"servers": [config.server_name]
})
@server.route("/_matrix/federation/v1/media/download/<media_id>")
@@ -250,7 +250,7 @@ async def download_media(media_id):
# multipart despite not even using
# it for anything. Minor annoyance.
with open(cat, "rb") as img_file:
with open(config.cat, "rb") as img_file:
image_data = img_file.read()
boundary = "----WebKitFormBoundary7MA4YWxkTrZu0gW"
response_body = (
@@ -291,7 +291,7 @@ async def make_join(roomId, userId):
}), 403
try:
if roomId.split(":")[1] != server_name:
if roomId.split(":")[1] != config.server_name:
return not_invited()
except:
return not_invited()
@@ -307,10 +307,10 @@ async def make_join(roomId, userId):
join = {
"content": {
"join_authorised_via_users_server": f"@vona:{server_name}",
"join_authorised_via_users_server": f"@vona:{config.server_name}",
"membership": "join"
},
"origin": server_name,
"origin": config.server_name,
"origin_server_ts": 7,
"room_id": roomId,
"sender": userId,
@@ -344,12 +344,10 @@ async def make_join(roomId, userId):
"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
})
return jsonify(globals.room_dir)
# https://spec.matrix.org/latest/server-server-api/#transactions
@@ -386,7 +384,7 @@ async def user_profile():
field = request.args.get("field")
if field:
if field == "avatar_url":
return jsonify({"avatar_url":f"mxc://{server_name}/cat"})
return jsonify({"avatar_url":f"mxc://{config.server_name}/cat"})
elif field == "displayname":
return jsonify({"displayname":"Vona"})
@@ -395,31 +393,24 @@ async def user_profile():
"error": "The requested profile key does not exist."
}), 404
return jsonify({"avatar_url": f"mxc://{server_name}/cat","displayname": "Vona"})
return jsonify({"avatar_url": f"mxc://{config.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(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": the_funny_number,
"user_id": f"@vona:{server_name}"
"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():
try:
users = request.json["device_keys"]
except:
return jsonify({
"errcode": "M_NOT_FOUND",
"error": "User does not exist"
}), 404
return jsonify({"device_keys": users})
return jsonify({
"device_keys": request.json.get("device_keys", {})
})
@server.route("/_matrix/federation/v2/invite/<room>/<txnId>", methods=["PUT"])
@@ -429,15 +420,10 @@ async def invite_user_v2(room, txnId):
@server.route("/_matrix/federation/v1/invite/<room>/<txnId>", methods=["PUT"])
async def invite_user_v1(room, txnId):
return [200, invite_user(request.data)]
return [200, invite_user(request.json)]
def invite_user(data):
try:
invite_data = json.loads(data)
except:
return jsonify({"errcode":"M_NOT_JSON","error":"Content not JSON."}),
def invite_user(invite_data):
if "event" in invite_data:
if "room_version" in invite_data:
if invite_data["room_version"] not in ["1", "2"]:
@@ -457,7 +443,7 @@ def invite_user(data):
and "state_key" in event
and "room_id" in event
and content["membership"] == "invite"
and event["state_key"] == f"@vona:{server_name}"
and event["state_key"] == f"@vona:{config.server_name}"
):
return jsonify({
"event": globals.sign_json_without_discard(invite_data["event"]),
@@ -477,3 +463,25 @@ async def space_hierachy(roomId):
"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": config.server_name,
"origin_server_ts": int(str(time.time() * 1000).split(".")[0]),
"pdus": send_join(bullshit, room)["state"]
})

View File

@@ -1,3 +1,5 @@
from resolvematrix import ServerResolver
from types import SimpleNamespace
from collections import Counter
import vona.config as config
import nacl.signing
@@ -9,8 +11,7 @@ import copy
import json
import re
version = "1.4.3"
http_client = httpx.Client(headers={"User-Agent": f"Vona/{version}"})
version = "1.4.4"
def canonical_json(value):
@@ -111,7 +112,9 @@ def event_hash(event_object):
event_json_bytes = canonical_json(event_object)
return base64.b64encode(hashlib.sha256(event_json_bytes).digest()).decode("utf-8")
return base64.b64encode(
hashlib.sha256(event_json_bytes).digest()
).decode("utf-8").rstrip("=")
def pubkey() -> str:
@@ -129,7 +132,12 @@ def pubkey() -> str:
)
def make_auth_header(destination, method, path, content=None) -> str:
def make_auth_header(
destination: str,
method: str,
path: str,
content = None
) -> str:
request_json = {
"method": method,
"uri": path,
@@ -252,3 +260,89 @@ def room_version_from_id(room_id):
return most_common[0] if most_common else None
return most_common_character(nums)[0]
room_dir = {
"chunk": [{
"avatar_url": f"mxc://{config.server_name}/cat",
"guest_can_join": False,
"join_rule": "public",
"name": "Vona",
"num_joined_members": 1,
"room_id": make_event_id().replace("$", "!"),
"room_type": "m.room",
"topic": "",
"world_readable": False,
"via": [config.server_name]
}],
"total_room_count_estimate": 1
}
class http_client:
http = httpx.Client(headers={"User-Agent": f"Vona/{version}"})
resolver = ServerResolver(client=http)
def _resolve(self, target) -> SimpleNamespace:
r = self.resolver.resolve(target)
if r.sni:
sni = r.sni
else:
sni = r.host_header
return SimpleNamespace(
base_url=r.base_url,
host_header=r.host_header,
sni=sni
)
def put(
self,
path: str,
destination: str,
headers: dict = {},
authorize: bool = True,
json: dict = {},
):
resolved = self._resolve(destination)
if authorize:
headers["Authorization"] = make_auth_header(
method="PUT",
destination=destination,
path=path,
)
headers["Host"] = resolved.host_header
return self.http.put(
f"{resolved.base_url}{path}",
headers=headers,
extensions={"sni_hostname": resolved.sni},
json=json
)
def get(
self,
path: str,
destination: str,
headers: dict = {},
authorize: bool = True,
):
resolved = self._resolve(destination)
if authorize:
headers["Authorization"] = make_auth_header(
method="GET",
destination=destination,
path=path,
)
headers["Host"] = resolved.host_header
return self.http.get(
f"{resolved.base_url}{path}",
headers=headers,
extensions={"sni_hostname": resolved.sni}
)

View File

@@ -1,16 +1,18 @@
from resolvematrix import ServerResolver
import urllib.parse, time, json, httpx
import vona.globals as globals
import vona.config as config
import urllib.parse
import httpx
import json
import time
http_client = globals.http_client
http_client = globals.http_client()
def get_user_input(prompt):
try:
return urllib.parse.quote(input(prompt).replace("\\n", "\n", count=32))
answer = input(prompt)
while "\\n" in answer:
answer = answer.replace("\\n", "\n")
return urllib.parse.quote(answer)
except Exception as e:
print(f"Error reading input: {e}")
return None
@@ -21,27 +23,19 @@ room_id = get_user_input("Room ID:\n\t")
try:
unresolved_server_name = input("\nServer name before resolve:\n\t")
resolved_server_name = input("Server name after resolve:\n\t")
server_name = input("\nServer name to join via:\n\t")
except Exception as e:
print(f"Error reading server names: {e}")
exit(1)
resolver = ServerResolver(client=http_client)
try:
make_join_auth = globals.make_auth_header(
unresolved_server_name,
"GET",
f"/_matrix/federation/v1/make_join/{room_id}/%40{username}%3A{config.server_name}?ver=1&ver=2&ver=3&ver=4&ver=5&ver=6&ver=7&ver=8&ver=9&ver=10&ver=11&ver=12",
)
print("\nSending make_join request..")
make_join_response = http_client.get(
f"https://{resolved_server_name}/_matrix/federation/v1/make_join/{room_id}/%40{username}%3A{config.server_name}?ver=1&ver=2&ver=3&ver=4&ver=5&ver=6&ver=7&ver=8&ver=9&ver=10&ver=11&ver=12",
headers={
"Authorization": make_join_auth
},
path=f"/_matrix/federation/v1/make_join/{room_id}/%40{username}%3A{config.server_name}?ver=1&ver=2&ver=3&ver=4&ver=5&ver=6&ver=7&ver=8&ver=9&ver=10&ver=11&ver=12",
destination=server_name,
)
make_join_response.raise_for_status()
@@ -51,7 +45,7 @@ except httpx.HTTPStatusError as e:
print(f"HTTP error occurred: {e.response.status_code} - {e.response.text}")
exit(1)
except json.JSONDecodeError:
print("Failed to decode JSON response.")
print("Failed to decode response.")
exit(1)
except Exception as e:
print(f"An error occurred: {e}")
@@ -59,7 +53,7 @@ except Exception as e:
join_event = make_join.get("event", {})
if make_join.get("room_version") in ["1", "2"]:
if make_join.get("room_version", "1") in ["1", "2"]:
# NOTE: if we always make it opaque than Synapse will 500 lmao
join_event["event_id"] = globals.make_event_id()
@@ -72,19 +66,10 @@ except ValueError:
signed_join = globals.hash_and_sign_event(join_event)
try:
send_join_auth = globals.make_auth_header(
unresolved_server_name,
"PUT",
f"/_matrix/federation/v2/send_join/{room_id}/%24doesntmatter?omit_members=true",
signed_join,
)
send_join_response = http_client.put(
f"https://{resolved_server_name}/_matrix/federation/v2/send_join/{room_id}/%24doesntmatter?omit_members=true",
headers={
"Authorization": send_join_auth
},
path=f"/_matrix/federation/v2/send_join/{room_id}/%24doesntmatter?omit_members=true",
json=signed_join,
destination=server_name,
)
send_join_response.raise_for_status()