71 Commits

Author SHA1 Message Date
a5b34fb4a8 Only permit the v1 send_join endpoint for v1/v2 rooms 2025-11-05 12:30:51 -05:00
2212453a96 time -> get_time 2025-11-05 11:53:26 -05:00
d9583f3472 Add pre-commit configuration 2025-10-29 23:38:26 -04:00
3d9ff622fd Make ty happy 2025-10-29 21:29:10 -04:00
945f92e25f MSC4375: Admin Room Management 2025-10-29 03:58:08 -04:00
29ecfc0387 Move standardised admin APIs to a new file 2025-10-29 02:39:30 -04:00
4bdeca6139 Implement Hammerhead debug API 2025-10-27 14:54:20 -04:00
b8f8a1a7b1 Remove database 2025-10-27 10:19:03 -04:00
35896ed1ee Implement room V11 2025-10-27 10:05:33 -04:00
da82f492e8 Implement redaction for v1 to v11 events 2025-10-26 22:38:22 -04:00
17d5a6458e Allow / in event IDs on send_join 2025-10-26 18:32:17 -04:00
ad4c5cc5d2 Include event IDs on stripped state 2025-10-26 15:56:46 -04:00
8999a5e414 Add known room IDs to room bruteforcer 2025-10-26 15:35:45 -04:00
2fa647f7e7 Remove join_authorised_via_users_server from make_join 2025-10-26 07:36:09 -04:00
1c87e8b5a5 Implement Room V10 2025-10-26 06:36:15 -04:00
9d68082764 Add CouchDB stuffs 2025-10-26 05:53:44 -04:00
32cdb239ff Synapse start_job returns a blank dict 2025-10-26 00:52:22 -04:00
626c865461 make Ruff happy 2025-10-25 23:53:17 -04:00
fe5c0e23b5 Use ref hash for incoming v3+ events 2025-10-25 23:53:07 -04:00
09cb708594 Improve method for brute-force of rooms 2025-10-25 17:32:40 -04:00
ad72886fb3 Accept more room versions on invitations 2025-10-25 16:54:46 -04:00
9b6988a029 Implement rooms V3 through V9 2025-10-25 04:22:48 -04:00
2f891508ab Adapt extremities endpoint for v3+ 2025-10-25 00:59:20 -04:00
f23a74de5c Prepare for room v3+, misc bugfixes 2025-10-24 23:46:48 -04:00
b71096663c Add room v3+ event ID calculator 2025-10-24 20:34:20 -04:00
1fce4d5ba1 Update MSC4373 implentation 2025-10-23 22:50:59 -04:00
c1a4de06ce Add list of implemented MSCs 2025-10-22 18:23:21 -04:00
1e6c7adb41 MSC4373: Server opt-out of specific EDU types 2025-10-22 17:13:19 -04:00
6cc08fcb02 The Codebase Consistency Update 2025-10-21 23:47:28 -04:00
458d69da84 revert last commit 2025-10-21 16:39:05 -04:00
8c513ac738 add option to disable TLS certificate validation in http_client 2025-10-21 16:32:59 -04:00
3ec0f2ab4f Hammerhead reload-config endpoint 2025-10-20 18:18:21 -04:00
b5e74af72f Allow SyWeb login 2025-10-20 18:18:05 -04:00
d2e61a3b07 federation state_ids 2025-10-19 16:10:42 -04:00
24133be98c Partial SyWeb compatibility 2025-10-19 00:07:23 -04:00
647916b749 MSC4014 2025-10-18 17:16:41 -04:00
6056268a2e oops 2025-10-18 12:14:22 -04:00
b4d2f8e728 Fix federation self test 2025-10-18 10:23:59 -04:00
2cb6fd005a synadm: fetch event API, media info API 2025-10-18 09:58:10 -04:00
fe43b77611 MSC4370: Federation endpoint for retrieving current extremities 2025-10-17 23:22:43 -04:00
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
8a2bae706e Implement Telodendria admin API 2025-10-12 18:43:01 -04:00
4603ed86ac Support environment variable to specify configuration path 2025-10-12 17:05:32 -04:00
4a6e65226e Add validation for POST/PUT/PATCH requests
This was made on my phone
2025-10-11 20:02:24 -04:00
fc9787b8ec Add room join script 2025-10-08 19:59:14 -04:00
2dbec63ff7 Support room version 1 on invitations 2025-10-08 18:10:15 -04:00
2021fc027b Fix auth_events for guest access and history visibility 2025-10-08 17:50:56 -04:00
e0115fe8db Add federation self-testing 2025-10-08 13:52:24 -04:00
7e77f009db Room version based on room ID 2025-10-07 16:34:10 -04:00
316358a82a Add registration configuration fields (fixes registration endpoint) 2025-10-07 02:37:36 -04:00
d801715dab bump 2025-10-07 00:23:58 -04:00
6709b91d35 List utils, fix makekey 2025-10-07 00:23:52 -04:00
6268ea67b0 Signing key checks 2025-10-07 00:23:29 -04:00
7f5cf23fd2 Add README 2025-10-06 19:06:54 -04:00
2e7982b56f Bump version number 2025-10-06 18:49:06 -04:00
8fdffd573a Split custom endpoints into multiple files 2025-10-06 18:49:06 -04:00
2d3dea750f Remove __main__ check 2025-10-06 18:49:06 -04:00
470d4cc4d9 Fix logging and versioning 2025-10-06 18:48:57 -04:00
9985fb48a8 move to git 2025-10-01 18:34:58 -04:00
31 changed files with 2658 additions and 992 deletions

View File

18
.pre-commit-config.yaml Normal file
View File

@@ -0,0 +1,18 @@
default_install_hook_types:
- pre-commit
- commit-msg
default_stages:
- pre-commit
- manual
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.14.2
hooks:
- id: ruff-check
args: [ "--fix" ]
- repo: https://foundry.fsky.io/vel/ty-pre-commit
rev: 0.0.1-alpha.25
hooks:
- id: ty-check

44
MSCs.md Normal file
View File

@@ -0,0 +1,44 @@
# MSCs
List of MSCs implemented by Vona.
Merged MSCs:
* [MSC971: Groups](https://github.com/matrix-org/matrix-spec-proposals/issues/971)
* [MSC1708: .well-known for server name resolution](https://github.com/velikopter/matrix-spec-proposals/blob/edutypes/proposals/1708-well-known-for-federation.md)
* [MSC1753: C2S Capabilities API](https://github.com/matrix-org/matrix-spec-proposals/blob/main/proposals/1753-capabilities.md)
* [MSC1794: Federation v2 Invite API](https://github.com/matrix-org/matrix-spec-proposals/blob/main/proposals/1794-federation-v2-invites.md)
* [MSC1802: Better format for send_join and send_leave](https://github.com/matrix-org/matrix-spec-proposals/blob/main/proposals/1802-standardised-federation-response-format.md)
* [MSC1812: Federation Make Membership Room Version](https://github.com/matrix-org/matrix-spec-proposals/blob/main/proposals/1812-federation-make-membership.md)
* [MSC1929: Homeserver Admin Contact and Support page](https://github.com/matrix-org/matrix-spec-proposals/blob/main/proposals/1929-admin-contact.md)
* [MSC2320: Versions information for identity servers](https://github.com/matrix-org/matrix-spec-proposals/blob/main/proposals/2320-identity-versions.md)
* [MSC2659: Application service ping endpoint](https://github.com/matrix-org/matrix-spec-proposals/blob/main/proposals/2659-appservice-ping.md)
* [MSC2713: Remove deprecated Identity Service endpoints](https://github.com/matrix-org/matrix-spec-proposals/blob/main/proposals/2713-remove-deprecated-identity-endpoints.md)
* [MSC3266: Room Summary API](https://github.com/matrix-org/matrix-spec-proposals/blob/main/proposals/3266-room-summary.md)
* [MSC3383: Include destination in X-Matrix Auth Header](https://github.com/matrix-org/matrix-spec-proposals/blob/main/proposals/3383-fed-auth-destination.md)
* [MSC4133: Extended User Profile API](https://github.com/matrix-org/matrix-spec-proposals/blob/main/proposals/4133-extended-profiles.md)
* [MSC4151: Reporting rooms](https://github.com/matrix-org/matrix-spec-proposals/blob/main/proposals/4151-report-room.md)
* [MSC4260: Reporting users](https://github.com/matrix-org/matrix-spec-proposals/blob/main/proposals/4260-report-user.md)
Non-merged MSCs:
* [MSC2666: Get rooms in common with another user](https://github.com/matrix-org/matrix-spec-proposals/pull/2666)
* [MSC4358: Out-of-room server discovery](https://github.com/matrix-org/matrix-spec-proposals/pull/4358)
* [MSC4367: via routes in the published room directory](https://github.com/matrix-org/matrix-spec-proposals/pull/4367)
* [MSC4370: Federation endpoint for retrieving current extremities](https://github.com/matrix-org/matrix-spec-proposals/pull/4370)
* [MSC4373: Server opt-out of specific EDU types](https://github.com/matrix-org/matrix-spec-proposals/pull/4373)
* [MSC4375: Admin Room Management](https://github.com/matrix-org/matrix-spec-proposals/pull/4375)
Room version MSCs:
* [MSC1759: Room V2](https://github.com/matrix-org/matrix-spec-proposals/blob/main/proposals/1759-rooms-v2.md)
* [MSC1659: Room V3](https://github.com/matrix-org/matrix-spec-proposals/blob/main/proposals/1659-event-id-as-hashes.md)
* [MSC2002: Room V4](https://github.com/matrix-org/matrix-spec-proposals/blob/main/proposals/2002-rooms-v4.md)
* [MSC2077: Room V5](https://github.com/matrix-org/matrix-spec-proposals/blob/main/proposals/2077-rooms-v5.md)
* [MSC2240: Room V6](https://github.com/matrix-org/matrix-spec-proposals/blob/main/proposals/2240-rooms-v6.md)
* [MSC2998: Room V7](https://github.com/matrix-org/matrix-spec-proposals/blob/main/proposals/2998-rooms-v7.md)
* [MSC3289: Room V8](https://github.com/matrix-org/matrix-spec-proposals/blob/main/proposals/3289-rooms-v8.md)
* [MSC3375: Room V9](https://github.com/matrix-org/matrix-spec-proposals/blob/main/proposals/3375-room-v9.md)
* [MSC3604: Room V10](https://github.com/matrix-org/matrix-spec-proposals/blob/main/proposals/3604-rooms-v10.md)
* [MSC3820: Room V11](https://github.com/matrix-org/matrix-spec-proposals/blob/main/proposals/3820-rooms-v11.md)

32
README.md Normal file
View File

@@ -0,0 +1,32 @@
# Vona 🎉✨🔍🎊🌟💻🤔🛠️🚀
Welcome to Vona! 🎊 Vona is a flazing bast 🌟, and semory mafe [matrix] implementation made in Python 🐍 for stability.
Vona implements all endpoints incorrectly ❌, assuring compatibility with other server software 💻, like Synapse.
## How do I run it? 🔍🤔
Please don't 🙅‍♂️
## Installation Instructions 🛠️🔧
Use the power of Hopes and Dreams 🍀🌈✨
## Why you should use Vona 🚀🌟
- Blazing fast ⚡, and memory safe 🛡️
- Filters out spam efficiently 🚫📧
- Uses minimal storage 💾📦
- Secure 🔒 and low maintenance 🧹
- Complies with the [spec](https://spec.matrix.org) 📜✅
- At least 1 instance running in the wild 🌍🐾
- Policy server for ease of moderation 🧹🛡️
## What people are saying about Vona 🤩💬
- "this codebase is effectively an art project"
- @olivia:computer.surgery, one of the maintainers of
the [Grapevine](https://grapevine.computer.surgery)
homeserver software 💻
- "this is amazing 😄"
- @sarah:4d2.org, president of the popular 4d2.org
homeserver 👑
## License 📜🔑
We use the Velicense. Read more in the LICENSE file 📂📖.
Enjoy your journey with Vona! 🌈🚀🎉

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.

2
TODO.md Normal file
View File

@@ -0,0 +1,2 @@
- use `resolvematrix` async instead of sync
- implement groups over federation

View File

@@ -1,18 +1,23 @@
[project] [project]
name = "vona" name = "vona"
version = "1.4.1"
description = "Flazing bast Matrix homeserver" description = "Flazing bast Matrix homeserver"
authors = [
{name = "Kierre",email = "vel@riseup.net"}
]
license = {text = "Velicense"} license = {text = "Velicense"}
requires-python = ">=3.13" requires-python = "<4,>=3.12"
dependencies = [ dependencies = [
"httpx (>=0.28.1,<0.29.0)", "httpx (>=0.28.1,<0.29.0)",
"pynacl (>=1.6.0,<2.0.0)", "pynacl (>=1.6.0,<2.0.0)",
"flask[async] (>=3.1.2,<4.0.0)", "flask[async] (>=3.1.2,<4.0.0)",
"resolvematrix @ git+https://codeberg.org/timedout/resolvematrix.git",
] ]
authors = [
{ name = "Kierre", email = "vel@riseup.net" }
]
# We put this in the code itself rather than here.
version = "0.0.0"
[build-system] [build-system]
requires = ["poetry-core>=2.0.0,<3.0.0"] requires = ["poetry-core>=2.0.0,<3.0.0"]

View File

@@ -1 +0,0 @@
# read __main__.py

View File

@@ -1,8 +1,17 @@
from flask import Flask, jsonify, request, redirect
import vona.globals as globals import vona.globals as globals
from datetime import datetime from datetime import datetime
import vona.config as config import vona.config as config
import threading
import logging import logging
import os
from flask import (
Flask,
jsonify,
request,
redirect,
abort,
)
from vona.federation import server from vona.federation import server
from vona.custom import custom from vona.custom import custom
@@ -24,9 +33,20 @@ app.register_blueprint(server)
app.register_blueprint(apps) app.register_blueprint(apps)
@app.before_request @app.before_request
async def preflight(): async def validate_json():
if request.method == "OPTIONS": if request.method == "OPTIONS":
return "", 204 return "", 200
elif request.method in ["PUT", "POST", "PATCH"]:
if "media" in request.path:
# Don't check media uploads
return
try:
request.get_json(force=True)
except Exception:
return jsonify({"error": "Content not JSON.", "errcode": "M_NOT_JSON"}), 400
@app.after_request @app.after_request
async def handle_logging(response): async def handle_logging(response):
@@ -36,7 +56,6 @@ async def handle_logging(response):
response.headers["Access-Control-Allow-Methods"] = "GET, HEAD, POST, PUT, DELETE, OPTIONS" response.headers["Access-Control-Allow-Methods"] = "GET, HEAD, POST, PUT, DELETE, OPTIONS"
if request.method == "OPTIONS": if request.method == "OPTIONS":
# Discard logs for OPTIONS
return response return response
origin = "unknown" origin = "unknown"
@@ -44,13 +63,34 @@ async def handle_logging(response):
try: try:
if "Authorization" in request.headers: if "Authorization" in request.headers:
if request.headers["Authorization"].split()[0] == "X-Matrix": 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: else:
origin = "client" origin = "client"
except:
except Exception:
pass pass
print(f'[{origin}] [{request.remote_addr}] [{datetime.now().strftime("%d/%b/%Y:%H:%M:%S")}] {request.method} {request.full_path} {response.status_code}') if request.path.startswith("/.well-known/matrix/"):
return response
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 return response
@@ -62,7 +102,7 @@ async def root():
@app.route("/_matrix/static/") @app.route("/_matrix/static/")
async def matrix_static(): async def matrix_static():
return f'<!DOCTYPE html><html lang="en"><head><title>Vona {globals.vona_version} is running</title><style>body {{font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;max-width: 40em;margin: auto;text-align: center;}}h1,p {{margin: 1.5em;}}hr {{border: none;background-color: #ccc;color: #ccc;height: 1px;width: 7em;margin-top: 4em;}}.logo {{display: block;width: 12em;height: auto;margin: 4em auto;}}</style></head><body><img src="https://matrix.org/images/matrix-logo.svg" class="logo"><h1>It works! Vona {globals.vona_version} is running</h1><p>Your Vona server is listening on this port and is ready for messages.</p><p>To use this server you\"ll need <a href="https://matrix.org/ecosystem/clients/" target="_blank"rel="noopener noreferrer">a Matrix client</a>.</p><p>Welcome to the Matrix universe :)</p><hr><p><small><a href="https://natribu.org/en/" target="_blank" rel="noopener noreferrer">matrix.org</a></small></p></body></html>' return f'<!DOCTYPE html><html lang="en"><head><title>Vona {globals.version} is running</title><style>body {{font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;max-width: 40em;margin: auto;text-align: center;}}h1,p {{margin: 1.5em;}}hr {{border: none;background-color: #ccc;color: #ccc;height: 1px;width: 7em;margin-top: 4em;}}.logo {{display: block;width: 12em;height: auto;margin: 4em auto;}}</style></head><body><img src="https://matrix.org/images/matrix-logo.svg" class="logo"><h1>It works! Vona {globals.version} is running</h1><p>Your Vona server is listening on this port and is ready for messages.</p><p>To use this server you\"ll need <a href="https://matrix.org/ecosystem/clients/" target="_blank"rel="noopener noreferrer">a Matrix client</a>.</p><p>Welcome to the Matrix universe :)</p><hr><p><small><a href="https://natribu.org/en/" target="_blank" rel="noopener noreferrer">matrix.org</a></small></p></body></html>'
# Error handlers # Error handlers
@@ -100,7 +140,23 @@ async def client():
}) })
if __name__ == "__main__": def federation_self_test():
app.run(host=config.addr, port=config.port) try:
else: resp = globals.http_client().get(
print("What the hell are you doing?") path="/",
destination=config.server_name,
)
if str(resp.status_code).startswith("5") or str(resp.status_code).startswith("4"):
print(f"[FATL] Federation self-test failed: status code is not acceptable ({str(resp.status_code)})")
os._exit(1)
print("[INFO] Federation self-test OK")
except Exception as e:
print(f"[FATL] Federation self-test failed: {e}")
os._exit(1)
threading.Thread(target=federation_self_test).start()
app.run(host=config.addr, port=config.port)

View File

@@ -1,7 +1,11 @@
from flask import Blueprint, jsonify
from vona.config import the_funny_number from vona.config import the_funny_number
import asyncio import asyncio
from flask import (
Blueprint,
jsonify,
)
apps = Blueprint("appservice", __name__) apps = Blueprint("appservice", __name__)
# This implements both being a homeserver and # This implements both being a homeserver and
@@ -10,6 +14,7 @@ apps = Blueprint("appservice", __name__)
# Endpoints invoked by the homeserver are put # Endpoints invoked by the homeserver are put
# lower than ones invoked by the appservice. # lower than ones invoked by the appservice.
@apps.route("/_matrix/client/v1/appservice/<app>/ping", methods=["POST"]) @apps.route("/_matrix/client/v1/appservice/<app>/ping", methods=["POST"])
async def homeserver_ping(app): async def homeserver_ping(app):
# Sleeping here makes it more realistic # Sleeping here makes it more realistic

View File

@@ -1,12 +1,25 @@
from flask import Blueprint, jsonify, request, send_file
from vona.federation import send_join from vona.federation import send_join
import vona.globals as globals import vona.globals as globals
import vona.config as config import vona.config as config
import asyncio import asyncio
import random import random
import os import os
client = Blueprint("c2s", __name__) from .groups import groups
from .admin import admin
from flask import (
Blueprint,
jsonify,
request,
send_file,
)
client = Blueprint("client", __name__)
client.register_blueprint(groups)
client.register_blueprint(admin)
@client.route("/_matrix/client/v3/account/password", methods=["POST"]) @client.route("/_matrix/client/v3/account/password", methods=["POST"])
@@ -22,16 +35,18 @@ client = Blueprint("c2s", __name__)
@client.route("/_matrix/client/v3/logout", methods=["POST"]) @client.route("/_matrix/client/v3/logout", methods=["POST"])
@client.route("/_matrix/client/r0/logout", methods=["POST"]) @client.route("/_matrix/client/r0/logout", methods=["POST"])
@client.route("/_matrix/client/v3/rooms/<room>/invite", methods=["POST"]) @client.route("/_matrix/client/v3/rooms/<room>/invite", methods=["POST"])
@client.route("/_matrix/client/v3/rooms/<roomId>/leave", methods=["POST"]) @client.route("/_matrix/client/v3/rooms/<room>/leave", methods=["POST"])
@client.route("/_matrix/client/r0/rooms/<roomId>/leave", methods=["POST"]) @client.route("/_matrix/client/r0/rooms/<room>/leave", methods=["POST"])
@client.route("/_matrix/client/v3/rooms/<roomId>/read_markers", methods=["POST"]) @client.route("/_matrix/client/v3/rooms/<room>/read_markers", methods=["POST"])
@client.route("/_matrix/client/r0/rooms/<roomId>/read_markers", methods=["POST"]) @client.route("/_matrix/client/r0/rooms/<room>/read_markers", methods=["POST"])
@client.route("/_matrix/client/v3/rooms/<room>/typing/<user>", methods=["PUT"]) @client.route("/_matrix/client/v3/rooms/<room>/typing/<user>", methods=["PUT"])
@client.route("/_matrix/client/api/v1/rooms/<room>/typing/<user>", methods=["PUT"])
@client.route("/_matrix/client/v3/keys/device_signing/upload", methods=["POST"]) @client.route("/_matrix/client/v3/keys/device_signing/upload", methods=["POST"])
@client.route("/_matrix/client/v3/rooms/<room>/receipt/<type>/<event>", methods=["POST"]) @client.route("/_matrix/client/v3/rooms/<room>/receipt/<type>/<event>", methods=["POST"])
@client.route("/_matrix/client/v3/users/<user>/report", methods=["POST"]) @client.route("/_matrix/client/v3/users/<user>/report", methods=["POST"])
@client.route("/_matrix/client/v3/voip/turnServer") @client.route("/_matrix/client/v3/voip/turnServer")
@client.route("/_matrix/client/r0/voip/turnServer") @client.route("/_matrix/client/r0/voip/turnServer")
@client.route("/_matrix/client/api/v1/voip/turnServer")
@client.route("/_matrix/client/v3/rooms/<r>/report/<e>") @client.route("/_matrix/client/v3/rooms/<r>/report/<e>")
@client.route("/_matrix/client/v3/rooms/<r>/report") @client.route("/_matrix/client/v3/rooms/<r>/report")
@client.route("/_matrix/client/v3/users/<u>/report") @client.route("/_matrix/client/v3/users/<u>/report")
@@ -47,61 +62,19 @@ async def spec_versions():
["r0.6.1"] + [f"v1.{i}" for i in range(1, 17)] ["r0.6.1"] + [f"v1.{i}" for i in range(1, 17)]
), ),
"unstable_features": { "unstable_features": {
"uk.half-shot.msc2666.query_mutual_rooms": True,
"uk.half-shot.msc2666.mutual_rooms": True,
"uk.half-shot.msc2666": True, "uk.half-shot.msc2666": True,
"uk.timedout.msc4323": True "uk.timedout.msc4323": True,
"uk.timedout.msc4375": True,
} }
}) })
@client.route("/_matrix/client/v3/admin/whois/<user>") @client.route("/_matrix/client/api/v1/rooms/<room>/members")
@client.route("/_matrix/client/r0/admin/whois/<user>") @client.route("/_matrix/client/v3/rooms/<room>/members")
async def whois(user): @client.route("/_matrix/client/r0/rooms/<room>/members")
if userId.startswith("@"): async def room_members(room):
return jsonify({
"devices": {
"": {
"sessions": [{
"connections": [{
"ip": "127.0.0.1",
"last_seen": config.the_funny_number,
"user_agent": f"Vona/{globals.vona_version}"
}]
}]
}
},
"user_id": user
})
return jsonify({
"errcode": "M_INVALID_PARAM",
"error": "Expected UserID string to start with '@'"
})
@client.route("/_matrix/client/unstable/uk.timedout.msc4323/admin/suspend/<user>", methods=["GET", "PUT"])
@client.route("/_matrix/client/v1/admin/suspend/<user>", methods=["GET", "PUT"])
async def suspend(user):
if request.method == "PUT":
req = request.get_json()
if req and "suspended" in req:
return jsonify({"suspended": req["suspended"]})
return jsonify({"suspended": True})
@client.route("/_matrix/client/unstable/uk.timedout.msc4323/admin/lock/<user>", methods=["GET", "PUT"])
@client.route("/_matrix/client/v1/admin/lock/<user>", methods=["GET", "PUT"])
async def lock(user):
if request.method == "PUT":
req = request.get_json()
if req and "locked" in req:
return jsonify({"locked": req["locked"]})
return jsonify({"locked": True})
@client.route("/_matrix/client/v3/rooms/<roomId>/members")
@client.route("/_matrix/client/r0/rooms/<roomId>/members")
async def room_member_count(roomId):
return jsonify({ return jsonify({
"chunk": [{ "chunk": [{
"content": { "content": {
@@ -109,9 +82,9 @@ async def room_member_count(roomId):
"displayname": "Vona", "displayname": "Vona",
"membership": "join" "membership": "join"
}, },
"event_id": globals.make_event_id(), "event_id": globals.make_event_id(seed=room),
"origin_server_ts": config.the_funny_number, "origin_server_ts": config.the_funny_number,
"room_id": roomId, "room_id": room,
"sender": f"@vona:{config.server_name}", "sender": f"@vona:{config.server_name}",
"state_key": f"@vona:{config.server_name}", "state_key": f"@vona:{config.server_name}",
"type": "m.room.member", "type": "m.room.member",
@@ -128,6 +101,7 @@ async def whoami():
}) })
@client.route("/_matrix/client/v2_alpha/register", methods=["POST"])
@client.route("/_matrix/client/v3/register", methods=["POST"]) @client.route("/_matrix/client/v3/register", methods=["POST"])
@client.route("/_matrix/client/v1/register", methods=["POST"]) @client.route("/_matrix/client/v1/register", methods=["POST"])
@client.route("/_matrix/client/r0/register", methods=["POST"]) @client.route("/_matrix/client/r0/register", methods=["POST"])
@@ -144,7 +118,7 @@ async def register():
"device_id": "VVOONNAA" "device_id": "VVOONNAA"
}) })
except: except Exception:
pass pass
return jsonify({ return jsonify({
@@ -161,6 +135,7 @@ async def register():
@client.route("/_matrix/client/r0/login", methods=["GET", "POST"]) @client.route("/_matrix/client/r0/login", methods=["GET", "POST"])
@client.route("/_matrix/client/v3/login", methods=["GET", "POST"]) @client.route("/_matrix/client/v3/login", methods=["GET", "POST"])
@client.route("/_matrix/client/api/v1/login", methods=["POST"])
async def login(): async def login():
if request.method == "GET": if request.method == "GET":
return jsonify({ return jsonify({
@@ -231,7 +206,13 @@ async def capabilities():
"9": "stable", "9": "stable",
"10": "stable", "10": "stable",
"11": "stable", "11": "stable",
"12": "stable" "12": "stable",
"org.matrix.msc3757.10": "stable",
"org.matrix.msc3757.11": "stable",
"org.matrix.hydra.11": "stable",
"org.matrix.msc3667": "stable",
"org.matrix.msc3787": "stable",
"org.matrix.msc4014": "unstable",
}, },
"org.matrix.msc3244.room_capabilities": { "org.matrix.msc3244.room_capabilities": {
"knock": { "knock": {
@@ -276,6 +257,7 @@ async def filter(**kwargs):
return jsonify({"filter_id": "vvvooonnnaaa"}) return jsonify({"filter_id": "vvvooonnnaaa"})
@client.route("/_matrix/client/api/v1/join/<room>", methods=["POST"])
@client.route("/_matrix/client/v3/join/<room>", methods=["POST"]) @client.route("/_matrix/client/v3/join/<room>", methods=["POST"])
@client.route("/_matrix/client/r0/join/<room>", methods=["POST"]) @client.route("/_matrix/client/r0/join/<room>", methods=["POST"])
@client.route("/_matrix/client/v3/rooms/<room>/join", methods=["POST"]) @client.route("/_matrix/client/v3/rooms/<room>/join", methods=["POST"])
@@ -284,6 +266,7 @@ async def join(room):
return jsonify({"room_id": room}) return jsonify({"room_id": room})
@client.route("/_matrix/client/api/v1/initialSync")
@client.route("/_matrix/client/v3/initialSync") @client.route("/_matrix/client/v3/initialSync")
@client.route("/_matrix/client/v3/sync") @client.route("/_matrix/client/v3/sync")
@client.route("/_matrix/client/r0/sync") @client.route("/_matrix/client/r0/sync")
@@ -292,39 +275,12 @@ async def sync():
def get_json(): def get_json():
return {} return {}
async def remove_keys(l, keys_to_remove) -> dict:
if not isinstance(l, list):
return l
new_list = []
for d in l:
if not isinstance(d, dict):
new_list.append(d)
continue
new_dict = {}
for k, v in d.items():
if k in keys_to_remove:
continue
new_dict[k] = await remove_keys(v, keys_to_remove)
new_list.append(new_dict)
return new_list
room = globals.make_event_id().replace("$", "!") room = globals.make_event_id().replace("$", "!")
old_room_state = send_join( room_state = globals.strip_state(
send_join(
bullshit, bullshit,
room room
)["state"] )["state"]
room_state = await remove_keys(
old_room_state,
[
"auth_events",
"prev_events",
"signatures",
"hashes",
"depth"
]
) )
room_name = { room_name = {
@@ -345,7 +301,7 @@ async def sync():
if "timeout" in request.args: if "timeout" in request.args:
try: try:
wait_time = int(request.args.get("timeout")) / 1000 wait_time = int(request.args.get("timeout")) / 1000
except: except Exception:
pass pass
await asyncio.sleep(wait_time) await asyncio.sleep(wait_time)
@@ -377,6 +333,78 @@ async def sync():
}) })
@client.route("/_matrix/client/api/v1/rooms/<room>/initialSync")
async def room_initsync(room):
class bullshit:
def get_json():
return {}
room_state = globals.strip_state(
send_join(
bullshit,
room
)["state"]
)
for evt in room_state:
evt["age"] = config.the_funny_number
return jsonify({
"membership": "join",
"room_id": room,
"messages": {
"chunk": [
{
"age": config.the_funny_number,
"content": {
"msgtype": "m.text",
"body": "Number 15: Burger King Foot Lettuce.\nThe last thing you'd want in your Burger King burger is someones foot fungus, but as it turns out, that might be what you get. A 4channer uploaded a photo, anonymously to the site showcasing his feet in a plastic bin of lettuce with the statement \"This is the lettuce you eat at Burger King.\". Admittedly, he had shoes on, but thats even worse. The post went live at 11:38 PM on July 16 and a mere 20 minutes later the Burger King in question was alerted to the rogue employee. At least, I hope hes rogue. How did it happen? Well, the BK employee hadn't removed the EXIF data from the uploaded photo, which suggested that the culprit was somewhere in Mayfield Heights, Ohio. This was at 11:47. 3 minutes later, at 11:50, the Burger King branch was posted with wishes of happy unemployment. 5 minutes later, the news station was contacted by another 4channer, and 3 minutes later at 11:58 a link was posted: BK's tell us about us online forum. The foot photo, otherwise known as Exhibit A, was attached. Cleveland Seen Magazine contacted the BK in question and the next day when questioned, the breakfast shift manager said \"Oh, I know who that is, hes getting fired\". Mystery solved, by 4chan. Now we can go back to eating our fast food in peace.",
"format": "org.matrix.custom.html",
"formatted_body": "Number 15: Burger King Foot Lettuce.<br />The last thing you'd want in your Burger King burger is someones foot fungus, but as it turns out, that might be what you get. A 4channer uploaded a photo, anonymously to the site showcasing his feet in a plastic bin of lettuce with the statement &quot;This is the lettuce you eat at Burger King.&quot;. Admittedly, he had shoes on, but thats even worse. The post went live at 11:38 PM on July 16 and a mere 20 minutes later the Burger King in question was alerted to the rogue employee. At least, I hope hes rogue. How did it happen? Well, the BK employee hadn't removed the EXIF data from the uploaded photo, which suggested that the culprit was somewhere in Mayfield Heights, Ohio. This was at 11:47. 3 minutes later, at 11:50, the Burger King branch was posted with wishes of happy unemployment. 5 minutes later, the news station was contacted by another 4channer, and 3 minutes later at 11:58 a link was posted: BK's tell us about us online forum. The foot photo, otherwise known as Exhibit A, was attached. Cleveland Seen Magazine contacted the BK in question and the next day when questioned, the breakfast shift manager said &quot;Oh, I know who that is, hes getting fired&quot;. Mystery solved, by 4chan. Now we can go back to eating our fast food in peace."
},
"event_id": globals.make_event_id(),
"origin_server_ts": 1432804485886,
"room_id": globals.make_event_id(),
"type": "m.room.message",
"user_id": f"@vona:{config.server_name}"
}
]
},
"state": room_state
})
@client.route("/_matrix/client/api/v1/events")
async def events():
if "timeout" in request.args:
try:
await asyncio.sleep(int(request.args["timeout"]) / 1000)
except Exception:
pass
return jsonify({
"start": os.urandom(32).hex(),
"end": os.urandom(32).hex(),
"chunk": [
{
"age": config.the_funny_number,
"content": {
"msgtype": "m.text",
"body": "Number 15: Burger King Foot Lettuce.\nThe last thing you'd want in your Burger King burger is someones foot fungus, but as it turns out, that might be what you get. A 4channer uploaded a photo, anonymously to the site showcasing his feet in a plastic bin of lettuce with the statement \"This is the lettuce you eat at Burger King.\". Admittedly, he had shoes on, but thats even worse. The post went live at 11:38 PM on July 16 and a mere 20 minutes later the Burger King in question was alerted to the rogue employee. At least, I hope hes rogue. How did it happen? Well, the BK employee hadn't removed the EXIF data from the uploaded photo, which suggested that the culprit was somewhere in Mayfield Heights, Ohio. This was at 11:47. 3 minutes later, at 11:50, the Burger King branch was posted with wishes of happy unemployment. 5 minutes later, the news station was contacted by another 4channer, and 3 minutes later at 11:58 a link was posted: BK's tell us about us online forum. The foot photo, otherwise known as Exhibit A, was attached. Cleveland Seen Magazine contacted the BK in question and the next day when questioned, the breakfast shift manager said \"Oh, I know who that is, hes getting fired\". Mystery solved, by 4chan. Now we can go back to eating our fast food in peace.",
"format": "org.matrix.custom.html",
"formatted_body": "Number 15: Burger King Foot Lettuce.<br />The last thing you'd want in your Burger King burger is someones foot fungus, but as it turns out, that might be what you get. A 4channer uploaded a photo, anonymously to the site showcasing his feet in a plastic bin of lettuce with the statement &quot;This is the lettuce you eat at Burger King.&quot;. Admittedly, he had shoes on, but thats even worse. The post went live at 11:38 PM on July 16 and a mere 20 minutes later the Burger King in question was alerted to the rogue employee. At least, I hope hes rogue. How did it happen? Well, the BK employee hadn't removed the EXIF data from the uploaded photo, which suggested that the culprit was somewhere in Mayfield Heights, Ohio. This was at 11:47. 3 minutes later, at 11:50, the Burger King branch was posted with wishes of happy unemployment. 5 minutes later, the news station was contacted by another 4channer, and 3 minutes later at 11:58 a link was posted: BK's tell us about us online forum. The foot photo, otherwise known as Exhibit A, was attached. Cleveland Seen Magazine contacted the BK in question and the next day when questioned, the breakfast shift manager said &quot;Oh, I know who that is, hes getting fired&quot;. Mystery solved, by 4chan. Now we can go back to eating our fast food in peace."
},
"event_id": globals.make_event_id(),
"origin_server_ts": 1432804485886,
"room_id": globals.make_event_id(),
"type": "m.room.message",
"user_id": f"@vona:{config.server_name}"
}
]
})
@client.route("/_matrix/client/api/v1/rooms/<room>/send/<eventType>/<txnId>", methods=["POST", "PUT"])
@client.route("/_matrix/client/v3/rooms/<room>/send/<eventType>/<txnId>", methods=["POST", "PUT"]) @client.route("/_matrix/client/v3/rooms/<room>/send/<eventType>/<txnId>", methods=["POST", "PUT"])
@client.route("/_matrix/client/r0/rooms/<room>/send/<eventType>/<txnId>", methods=["POST", "PUT"]) @client.route("/_matrix/client/r0/rooms/<room>/send/<eventType>/<txnId>", methods=["POST", "PUT"])
async def send_message(room, eventType, txnId): async def send_message(room, eventType, txnId):
@@ -431,10 +459,10 @@ async def refresh():
}) })
@client.route("/_matrix/client/unstable/im.nheko.summary/rooms/<roomId>/summary") @client.route("/_matrix/client/unstable/im.nheko.summary/rooms/<room>/summary")
@client.route("/_matrix/client/unstable/im.nheko.summary/summary/<roomId>") @client.route("/_matrix/client/unstable/im.nheko.summary/summary/<room>")
@client.route("/_matrix/client/v1/room_summary/<roomId>") @client.route("/_matrix/client/v1/room_summary/<room>")
async def room_summary(roomId): async def room_summary(room):
return jsonify({ return jsonify({
"room_id": globals.make_event_id().replace("$", "!"), "room_id": globals.make_event_id().replace("$", "!"),
"avatar_url": f"mxc://{config.server_name}/cat", "avatar_url": f"mxc://{config.server_name}/cat",
@@ -446,7 +474,7 @@ async def room_summary(roomId):
"join_rule": "public", "join_rule": "public",
"room_type": "m.room", "room_type": "m.room",
"membership": "join", "membership": "join",
"room_version": 2 "room_version": globals.room_version_from_id(room)
}) })
@@ -461,6 +489,7 @@ async def room_query(room):
return jsonify({}) return jsonify({})
@client.route("/_matrix/client/v3/rooms/<room>/aliases") @client.route("/_matrix/client/v3/rooms/<room>/aliases")
async def room_aliases(room): async def room_aliases(room):
return jsonify({ return jsonify({
@@ -475,6 +504,7 @@ async def room_aliases(room):
async def room_visibility(room): async def room_visibility(room):
return jsonify({"visibility": "public"}) return jsonify({"visibility": "public"})
@client.route("/_matrix/client/v3/search", methods=["POST"]) @client.route("/_matrix/client/v3/search", methods=["POST"])
async def search(): async def search():
room = globals.make_event_id().replace("$", "!") room = globals.make_event_id().replace("$", "!")
@@ -546,13 +576,15 @@ async def url_preview():
"og:title": "cool cat" "og:title": "cool cat"
}) })
@client.route("/_matrix/client/v1/media/preview_url") @client.route("/_matrix/client/v1/media/preview_url")
async def media_preview(): async def media_preview():
response = send_file(config.cat) response = send_file(config.cat)
response.headers["Content-Disposition"] = f'inline; filename="cat.jpg"' response.headers["Content-Disposition"] = 'inline; filename="cat.jpg"'
response.headers["Content-Type"] = "image/jpg" response.headers["Content-Type"] = "image/jpg"
return response return response
@client.route("/_matrix/media/v3/upload", methods=["POST"]) @client.route("/_matrix/media/v3/upload", methods=["POST"])
@client.route("/_matrix/media/r0/upload", methods=["POST"]) @client.route("/_matrix/media/r0/upload", methods=["POST"])
@client.route("/_matrix/media/v1/create", methods=["POST"]) @client.route("/_matrix/media/v1/create", methods=["POST"])
@@ -565,9 +597,10 @@ async def media_config():
return jsonify({"m.upload.size": config.the_funny_number * 69420}) return jsonify({"m.upload.size": config.the_funny_number * 69420})
@client.route("/_matrix/client/v3/profile/<userId>/<key>", methods=["GET", "PUT", "DELETE"]) @client.route("/_matrix/client/api/v1/profile/<user>/<key>", methods=["GET", "PUT", "DELETE"])
@client.route("/_matrix/client/r0/profile/<userId>/<key>", methods=["GET", "PUT", "DELETE"]) @client.route("/_matrix/client/v3/profile/<user>/<key>", methods=["GET", "PUT", "DELETE"])
async def profile_keys(userId, key): @client.route("/_matrix/client/r0/profile/<user>/<key>", methods=["GET", "PUT", "DELETE"])
async def profile_keys(user, key):
if request.method == "GET": if request.method == "GET":
if key == "avatar_url": if key == "avatar_url":
return jsonify({"avatar_url": f"mxc://{config.server_name}/cat"}) return jsonify({"avatar_url": f"mxc://{config.server_name}/cat"})
@@ -581,18 +614,20 @@ async def profile_keys(userId, key):
return jsonify({}) return jsonify({})
@client.route("/_matrix/client/v3/profile/<userId>")
@client.route("/_matrix/client/r0/profile/<userId>") @client.route("/_matrix/client/v3/profile/<user>")
async def user_profile(userId): @client.route("/_matrix/client/r0/profile/<user>")
async def user_profile(user):
return jsonify({ return jsonify({
"avatar_url": f"mxc://{config.server_name}/cat", "avatar_url": f"mxc://{config.server_name}/cat",
"displayname": "Vona" "displayname": "Vona"
}) })
@client.route("/_matrix/client/v3/rooms/<roomId>/messages") @client.route("/_matrix/client/api/v1/rooms/<room>/messages")
@client.route("/_matrix/client/r0/rooms/<roomId>/messages") @client.route("/_matrix/client/v3/rooms/<room>/messages")
async def room_messages(roomId): @client.route("/_matrix/client/r0/rooms/<room>/messages")
async def room_messages(room):
return jsonify({ return jsonify({
"chunk": [{ "chunk": [{
"content": { "content": {
@@ -603,7 +638,7 @@ async def room_messages(roomId):
}, },
"event_id": globals.make_event_id(), "event_id": globals.make_event_id(),
"origin_server_ts": config.the_funny_number, "origin_server_ts": config.the_funny_number,
"room_id": roomId, "room_id": room,
"sender": f"@vona:{config.server_name}", "sender": f"@vona:{config.server_name}",
"type": "m.room.message" "type": "m.room.message"
}], }],
@@ -611,6 +646,7 @@ async def room_messages(roomId):
"start": f"{os.urandom(16).hex()}" "start": f"{os.urandom(16).hex()}"
}) })
@client.route("/_matrix/client/v3/keys/query", methods=["POST"]) @client.route("/_matrix/client/v3/keys/query", methods=["POST"])
@client.route("/_matrix/client/r0/keys/query", methods=["POST"]) @client.route("/_matrix/client/r0/keys/query", methods=["POST"])
async def query_keys(): async def query_keys():
@@ -622,6 +658,7 @@ async def query_keys():
"user_signing_keys": user "user_signing_keys": user
}) })
@client.route("/_matrix/client/api/v1/createRoom", methods=["POST"]) @client.route("/_matrix/client/api/v1/createRoom", methods=["POST"])
@client.route("/_matrix/client/v3/createRoom", methods=["POST"]) @client.route("/_matrix/client/v3/createRoom", methods=["POST"])
@client.route("/_matrix/client/r0/createRoom", methods=["POST"]) @client.route("/_matrix/client/r0/createRoom", methods=["POST"])
@@ -639,19 +676,29 @@ async def mutual_rooms():
}) })
@client.route("/_matrix/client/api/v1/presence/<user>/status", methods=["GET", "PUT"])
@client.route("/_matrix/client/r0/presence/<user>/status", methods=["GET", "PUT"]) @client.route("/_matrix/client/r0/presence/<user>/status", methods=["GET", "PUT"])
async def presence(user): async def presence(user):
if request.method == "PUT": if request.method == "PUT":
return jsonify({}) return jsonify({})
return jsonify({ return jsonify({"presence": "online"})
"presence": "online"
})
@client.route("/_matrix/client/api/v1/publicRooms", methods=["GET", "POST"])
@client.route("/_matrix/client/v3/publicRooms", methods=["GET", "POST"])
@client.route("/_matrix/client/r0/publicRooms", methods=["GET", "POST"]) @client.route("/_matrix/client/r0/publicRooms", methods=["GET", "POST"])
async def room_directory(): async def room_directory():
return jsonify(globals.room_dir)
@client.route("/_matrix/client/api/v1/rooms/<room>/state/<evttype>/<key>", methods=["GET", "PUT"])
@client.route("/_matrix/client/api/v1/rooms/<room>/state/<evttype>/", methods=["GET", "PUT"])
async def state(room, evttype, key=None):
if request.method == "GET":
return jsonify({ return jsonify({
"chunk": [], "errcode": "M_NOT_FOUND",
"total_room_count_estimate": 0 "error": f"Event with type {evttype} and key {key} not found"
}) }), 404
return jsonify({"event_id": os.urandom(5).hex()})

153
vona/client/admin.py Normal file
View File

@@ -0,0 +1,153 @@
import vona.globals as globals
import vona.config as config
from vona.federation import (
send_join,
bullshit,
)
from flask import (
Blueprint,
jsonify,
request,
)
admin = Blueprint("admin", __name__)
# This implements misc standard admin APIs.
@admin.route("/_matrix/client/v3/admin/whois/<user>")
@admin.route("/_matrix/client/r0/admin/whois/<user>")
async def whois(user):
if user.startswith("@"):
return jsonify({
"devices": {
"": {
"sessions": [{
"connections": [{
"ip": "127.0.0.1",
"last_seen": config.the_funny_number,
"user_agent": f"Vona/{globals.version}"
}]
}]
}
},
"user_id": user
})
return jsonify({
"errcode": "M_INVALID_PARAM",
"error": "Expected UserID string to start with '@'"
})
@admin.route("/_matrix/client/unstable/uk.timedout.msc4323/admin/suspend/<user>", methods=["GET", "PUT"])
@admin.route("/_matrix/client/v1/admin/suspend/<user>", methods=["GET", "PUT"])
async def suspend(user):
if request.method == "PUT":
req = request.get_json()
if req and "suspended" in req:
return jsonify({"suspended": req["suspended"]})
return jsonify({"suspended": True})
@admin.route("/_matrix/client/unstable/uk.timedout.msc4323/admin/lock/<user>", methods=["GET", "PUT"])
@admin.route("/_matrix/client/v1/admin/lock/<user>", methods=["GET", "PUT"])
async def lock(user):
if request.method == "PUT":
req = request.get_json()
if req and "locked" in req:
return jsonify({"locked": req["locked"]})
return jsonify({"locked": True})
@admin.route("/_matrix/client/unstable/uk.timedout.msc4375/admin/rooms")
@admin.route("/_matrix/client/v1/admin/rooms")
async def rooms():
return jsonify({
"chunk": [
f"!qa:{config.server_name}",
f"!br:{config.server_name}",
f"!3:{config.server_name}",
f"!D:{config.server_name}",
f"!U:{config.server_name}",
f"!f:{config.server_name}",
f"!gx:{config.server_name}",
f"!hx:{config.server_name}",
f"!iy:{config.server_name}",
f"!p0:{config.server_name}",
f"!jZ:{config.server_name}",
]
})
@admin.route("/_matrix/client/unstable/uk.timedout.msc4375/admin/rooms/<room>")
@admin.route("/_matrix/client/v1/admin/rooms/<room>")
async def room_state(room):
if ":" not in room:
return jsonify({
"errcode": "M_NOT_FOUND",
"error": "Unknown room"
}), 404
else:
if room.split(":")[1] != config.server_name:
return jsonify({
"errcode": "M_NOT_FOUND",
"error": "Unknown room"
}), 404
state = globals.strip_state(
send_join(bullshit, room)["state"]
)
return jsonify({
"state": [state]
})
@admin.route("/_matrix/client/unstable/uk.timedout.msc4375/admin/rooms/<room>/evacuate", methods=["POST"])
@admin.route("/_matrix/client/unstable/uk.timedout.msc4375/admin/rooms/<room>", methods=["DELETE"])
@admin.route("/_matrix/client/v1/admin/rooms/<room>/evacuate", methods=["POST"])
@admin.route("/_matrix/client/v1/admin/rooms/<room>", methods=["DELETE"])
async def evacuate_room(room):
req = request.json
if (
isinstance(req, dict)
and "background" in req
and isinstance(req["background"], bool)
):
background = req["background"]
else:
background = True
resp = {
"background": background,
}
if not background:
resp["removed"] = config.the_funny_number
return resp
@admin.route("/_matrix/client/unstable/uk.timedout.msc4375/admin/rooms/<room>/evacuate/status")
@admin.route("/_matrix/client/unstable/uk.timedout.msc4375/admin/rooms/<room>/delete/status")
@admin.route("/_matrix/client/v1/admin/rooms/<room>/evacuate/status")
@admin.route("/_matrix/client/v1/admin/rooms/<room>/delete/status")
async def evacuate_status(room):
return jsonify({
"started_at": globals.get_time(), # everything all at once always now!!!
"total": config.the_funny_number,
"evacuated": config.the_funny_number,
"failed": config.the_funny_number,
})
@admin.route("/_matrix/client/unstable/uk.timedout.msc4375/admin/rooms/<room>/blocked", methods=["PUT"])
@admin.route("/_matrix/client/v1/admin/rooms/<room>/blocked", methods=["PUT"])
async def block(room):
return jsonify({})

119
vona/client/groups.py Normal file
View File

@@ -0,0 +1,119 @@
import vona.globals as globals
import vona.config as config
from flask import (
Blueprint,
jsonify,
request,
)
groups = Blueprint("groups", __name__)
# This implements the C2S API for groups
@groups.route("/_matrix/client/r0/groups/<group>/self/update_publicity", methods=["PUT"])
@groups.route("/_matrix/client/r0/groups/<group>/self/accept_invite", methods=["PUT"])
@groups.route("/_matrix/client/r0/groups/<group>/self/leave", methods=["PUT"])
@groups.route("/_matrix/client/r0/groups/<group>/admin/users/remove/<user>", methods=["PUT"])
@groups.route("/_matrix/client/r0/groups/<group>/admin/rooms/<room>", methods=["PUT", "DELETE"])
async def empty(**k):
return jsonify({})
@groups.route("/_matrix/client/r0/publicised_groups/<user>")
@groups.route("/_matrix/client/r0/joined_groups")
async def joined_groups(user=None):
return jsonify({
"groups": [f"+vona:{config.server_name}"]
})
@groups.route("/_matrix/client/r0/groups/<group>/summary")
async def summary(group):
return jsonify({
"profile": {
"name": "Vona",
"short_description": "",
"long_description": "",
"avatar_url": "",
"is_public": 1,
"is_openly_joinable": True
},
"users_section": {
"users": [],
"roles": {},
"total_user_count_estimate": 0
},
"rooms_section": {
"rooms": [],
"categories": {},
"total_room_count_estimate": 0
},
"user": {
"membership": "join",
"is_public": 1,
"is_privileged": 1,
"is_publicised": True
}
})
@groups.route("/_matrix/client/r0/groups/<group>/rooms")
async def rooms(group):
return jsonify({
"chunk": [],
"total_room_count_estimate": 0
})
@groups.route("/_matrix/client/r0/groups/<group>/invited_users")
async def invited_users(group):
return jsonify({
"chunk": [],
"total_user_count_estimate": 0
})
@groups.route("/_matrix/client/r0/groups/<group>/users")
async def users(group):
return jsonify({
"chunk": [
{
"user_id": f"@vona:{config.server_name}",
"displayname": "Vona",
"avatar_url": None,
"is_public": True,
"is_privileged": True,
"attestation": globals.sign_json({
"group_id": group,
"user_id": f"@vona:{config.server_name}",
"valid_until_ms": globals.get_time()
})
}
],
"total_user_count_estimate": 1
})
@groups.route("/_matrix/client/r0/groups/<group>/profile", methods=["GET", "POST"])
async def profile(group):
if request.method == "POST":
return jsonify({})
return jsonify({
"name": "Vona",
"short_description": "",
"long_description": "",
"avatar_url": "",
"is_public": 1,
"is_openly_joinable": True
})
@groups.route("/_matrix/client/r0/create_group", methods=["POST"])
async def create_group():
return jsonify({
"group_id": f"+vona:{config.server_name}"
})

View File

@@ -1,11 +1,11 @@
import os
from pathlib import Path from pathlib import Path
import tomllib import tomllib
import mimetypes import os
addr: str = "127.0.0.1" addr: str = "127.0.0.1"
port: int = 5000 port: int = 5000
allow_registration: bool = False users_can_register: bool = False
the_funny_number: int = 1337 the_funny_number: int = 1337
cat: str = "/etc/vona/cat.jpg" cat: str = "/etc/vona/cat.jpg"
@@ -13,7 +13,7 @@ server_name: str = ""
signing_key: str = "" signing_key: str = ""
support: dict = {"contacts": []} support: dict = {"contacts": []}
_CONFIG_PATH = Path("/etc/vona/config.toml") _CONFIG_PATH = Path(os.getenv("VONA_CONFIG", "/etc/vona/config.toml"))
def _fatal(msg: str) -> None: def _fatal(msg: str) -> None:
@@ -30,37 +30,38 @@ def _load_toml(path: Path) -> dict:
with path.open("rb") as f: with path.open("rb") as f:
return tomllib.load(f) return tomllib.load(f)
except FileNotFoundError: except FileNotFoundError:
_fatal(f"[FATL] Configuration file not found at {path}") _fatal(f"Configuration file not found at {path}")
except PermissionError: except PermissionError:
_fatal(f"[FATL] Permission denied when accessing configuration {path}") _fatal(f"Permission denied when accessing configuration {path}")
except tomllib.TOMLDecodeError as e: except tomllib.TOMLDecodeError as e:
_fatal(f"[FATL] Invalid TOML configuration: {e}") _fatal(f"Invalid TOML configuration: {e}")
# This will never be reached. It is here
# specifically just to please ty
return {}
def _read_signing_key_from_path(path_value) -> str | None: def _read_signing_key_from_path(path_value) -> str | None:
p = Path(path_value) p = Path(path_value)
if not p.exists(): if not p.exists():
_fatal(f"[FATL] signing_key_path {p} does not exist") _fatal(f"signing_key_path {p} does not exist")
try: try:
return p.read_text(encoding="utf-8").strip() return p.read_text(encoding="utf-8").strip()
except Exception as e: except Exception as e:
_fatal(f"[FATL] Failed to read signing_key_path {p}: {e}") _fatal(f"Failed to read signing_key_path {p}: {e}")
def _validate_cat_path(cat_path: str) -> Path: def _validate_cat_path(cat_path: str) -> Path:
p = Path(cat_path) p = Path(cat_path)
if not p.exists(): if not p.exists():
_fatal(f"[FATL] Cat photo at {p} does not exist") _fatal(f"Cat photo at {p} does not exist")
mtype, _ = mimetypes.guess_type(str(p))
if mtype is None or not mtype.startswith("image/"):
_warn(f"[WARN] Cat file {p} does not look like an image (mimetype={mtype})")
return p return p
def _apply_config(cfg: dict) -> None: def _apply_config(cfg: dict) -> None:
global addr, port, allow_registration, server_name, signing_key, cat, support global addr, port, server_name, signing_key, cat, support, users_can_register
if "address" in cfg: if "address" in cfg:
addr = str(cfg["address"]) addr = str(cfg["address"])
@@ -69,33 +70,30 @@ def _apply_config(cfg: dict) -> None:
try: try:
port = int(cfg["port"]) port = int(cfg["port"])
except (TypeError, ValueError): except (TypeError, ValueError):
_warn( _warn(f"Invalid port in config: {cfg.get('port')}; using default {port}")
f"[WARN] Invalid port in config: {cfg.get('port')}; using default {port}"
)
if "allow_registration" in cfg:
allow_registration = bool(cfg["allow_registration"])
if "server_name" in cfg: if "server_name" in cfg:
server_name = str(cfg["server_name"]) server_name = str(cfg["server_name"])
else: else:
_fatal("[FATL] `server_name` is not in configuration") _fatal("`server_name` is not in configuration")
if "signing_key" in cfg and "signing_key_path" in cfg: if "signing_key" in cfg and "signing_key_path" in cfg:
_warn( _warn("Both `signing_key` and `signing_key_path` present. Using `signing_key`.")
"[WARN] Both `signing_key` and `signing_key_path` present. Using `signing_key`."
)
if "signing_key" in cfg: if "signing_key" in cfg:
signing_key = str(cfg["signing_key"]).strip() signing_key = str(cfg["signing_key"]).strip()
elif "signing_key_path" in cfg: elif "signing_key_path" in cfg:
sk = _read_signing_key_from_path(cfg["signing_key_path"]) sk = _read_signing_key_from_path(cfg["signing_key_path"])
if sk: if sk:
signing_key = sk signing_key = sk
else:
_fatal(f"{cfg["signing_key_path"]} does not have a signing key.")
else: else:
_fatal( _fatal(
"[FATL] `signing_key` is not in configuration. " "A signing key is not present in the configuration. "
"A signing key can be generated using `cmd/generate_key.py`." "A key can be generated using `python3 -m vona.utils.makekey`."
) )
if "cat" in cfg: if "cat" in cfg:
@@ -115,9 +113,13 @@ def _apply_config(cfg: dict) -> None:
if len(contact) > 1: if len(contact) > 1:
support_obj["contacts"].append(contact) support_obj["contacts"].append(contact)
else: else:
_warn("[WARN] No support contacts are defined") _warn("No support contacts are defined")
support = support_obj support = support_obj
if "enable_registration" in cfg and isinstance(cfg["enable_registration"], bool):
users_can_register = cfg["enable_registration"]
print("[INFO] Configuration file was valid") print("[INFO] Configuration file was valid")

View File

@@ -1 +1,2 @@
import vona.config import vona.config
str(vona.config.server_name) # Satisfy Ruff

View File

@@ -1,464 +1,24 @@
from flask import Blueprint, jsonify, request, Response from flask import (
import vona.globals as globals Blueprint,
import vona.config as config )
import base64
import re from .hammerhead import hammerhead
import os from .conduwuit import conduwuit
from .dendrite import dendrite
from .telodendria import telo
from .synapse import synapse
from .citadel import citadel
custom = Blueprint("custom", __name__) custom = Blueprint("custom", __name__)
# This implements custom endpoints # This implements non-standard
# used by other homeserver # endpoints created by other
# implementations. They do not start # homeserver implementations.
# with /_matrix/
# This should be split into more
# files eventually.
custom.register_blueprint(hammerhead)
@custom.route("/_synapse/admin/v1/suspend/<user_id>", methods=["PUT"]) custom.register_blueprint(conduwuit)
@custom.route("/_synapse/admin/v1/deactivate/<user_id>", methods=["POST"]) custom.register_blueprint(dendrite)
@custom.route("/_synapse/admin/v1/reset_password/<user_id>", methods=["POST"]) custom.register_blueprint(synapse)
@custom.route("/_synapse/admin/v1/users/<user_id>/admin", methods=["PUT"]) custom.register_blueprint(citadel)
@custom.route("/_synapse/admin/v2/users/<user_id>/delete_devices", methods=["POST"]) custom.register_blueprint(telo)
@custom.route("/_synapse/admin/v1/users/<user_id>/shadow_ban", methods=["DELETE", "POST"])
@custom.route("/_synapse/admin/v1/users/<user_id>/override_ratelimit", methods=["GET", "POST", "DELETE"])
@custom.route("/_synapse/admin/v1/media/protect/<media_id>", methods=["POST"])
@custom.route("/_synapse/admin/v1/media/unprotect/<media_id>", methods=["POST"])
@custom.route("/_synapse/admin/v1/media/quarantine/<s>/<media_id>", methods=["POST"])
@custom.route("/_synapse/admin/v1/media/unquarantine/<s>/<media_id>", methods=["POST"])
@custom.route("/_dendrite/admin/purgeRoom/<roomId>", methods=["POST"])
@custom.route("/_dendrite/admin/refreshDevices/<userId>", methods=["POST"])
@custom.route("/_dendrite/admin/fulltext/reindex")
@custom.route("/_synapse/admin/v1/federation/destinations/<destination>/reset_connection", methods=["POST"])
@custom.route("/_synapse/admin/v1/rooms/<room>")
@custom.route("/_synapse/admin/v1/rooms/<room_id>/timestamp_to_event")
@custom.route("/_synapse/admin/v2/rooms/delete_status/<delete_id>")
@custom.route("/_synapse/admin/v1/rooms/<room_id_or_alias>/make_room_admin", methods=["POST"])
async def empty_response(**kwargs):
return jsonify({})
# Synapse
@custom.route("/_synapse/admin/v1/server_version")
async def synapse_version():
return jsonify({"server_version": globals.vona_version})
@custom.route("/_synapse/admin/v2/users")
async def synapse_user_list():
return jsonify({
"users": [
{
"name": f"@vona:{config.server_name}",
"is_guest": 0,
"admin": 0,
"user_type": "vona",
"deactivated": 0,
"erased": False,
"shadow_banned": 0,
"displayname": "Vona",
"avatar_url": f"mxc://{config.server_name}/cat",
"creation_ts": config.the_funny_number,
"locked": False
}
],
"total": 1
})
@custom.route("/_synapse/admin/v2/users/<user_id>", methods=["GET", "PUT"])
async def synapse_user_info(user_id):
if request.method == "GET":
return jsonify({
"name": f"@vona:{config.server_name}",
"displayname": "Vona",
"threepids": [],
"avatar_url": f"mxc://{config.server_name}/cat",
"is_guest": 0,
"admin": 0,
"deactivated": 0,
"erased": False,
"shadow_banned": 0,
"creation_ts": config.the_funny_number,
"last_seen_ts": config.the_funny_number,
"appservice_id": config.the_funny_number,
"consent_server_notice_sent":config.the_funny_number,
"consent_version": config.the_funny_number,
"consent_ts": config.the_funny_number,
"external_ids": [],
"user_type": "vona",
"locked": False,
"suspended": False
})
return jsonify({}), 201
@custom.route("/_synapse/admin/v1/whois/<user_id>")
async def synapse_whois(user_id):
return jsonify({
"user_id": f"@vona:{config.server_name}",
"devices": {
"": {
"sessions": [{
"connections": [{
"ip":f"127.0.0.1",
"last_seen":config.the_funny_number,
"user_agent":f"Vona/{globals.vona_version}"
}]
}]
}
}
})
@custom.route("/_synapse/admin/v1/users/<user_id>/joined_rooms")
async def synapse_user_joined_rooms(user_id):
return jsonify({
"joined_rooms": [globals.make_event_id().replace("$", "!")],
"total": 1
})
@custom.route("/_synapse/admin/v1/users/<user_id>/sent_invite_count")
async def synapse_invite_count(user_id):
return jsonify({"invite_count": config.the_funny_number})
@custom.route("/_synapse/admin/v1/users/<user_id>/accountdata")
async def synapse_account_data(user_id):
return jsonify({"account_data":{"global":{}}})
@custom.route("/_synapse/admin/v1/users/<user_id>/media", methods=["GET", "DELETE"])
async def synapse_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({"deleted_media": ["cat"], "total": config.the_funny_number})
@custom.route("/_synapse/admin/v1/users/<user_id>/login", methods=["POST"])
async def synapse_account_login(user_id):
return jsonify({"access_token": "vona"})
@custom.route("/_synapse/admin/v1/users/<user_id>/_allow_cross_signing_replacement_without_uia", methods=["POST"])
async def synapse_stupid_mas_bullshit(user_id):
return jsonify({"updatable_without_uia_before_ms": config.the_funny_number})
@custom.route("/_synapse/admin/v2/users/<user_id>/devices", methods=["GET", "POST"])
async def synapse_device_list(user_id):
if request.method == "GET":
return jsonify({
"devices": [{
"device_id": "VVOONNAA",
"display_name": "Vona",
"last_seen_ip": "127.0.0.1",
"last_seen_ts": config.the_funny_number,
"last_seen_user_agent": f"Vona/{globals.vona_version}"
}],
"total": 1
})
return jsonify({})
@custom.route("/_synapse/admin/v2/users/<user_id>/devices/<device_id>", methods=["GET", "PUT", "DELETE"])
async def synapse_device_info(user_id, device_id):
if request.method == "GET":
return jsonify({
"device_id": "VVOONNAA",
"display_name": "Vona",
"last_seen_ip": "127.0.0.1",
"last_seen_ts": config.the_funny_number,
"last_seen_user_agent": f"Vona/{globals.vona_version}"
})
return jsonify({})
@custom.route("/_synapse/admin/v1/users/<user_id>/pushers")
async def synapse_pushers(user_id):
return jsonify({"pushers": [], "total": config.the_funny_number})
@custom.route("/_synapse/admin/v1/username_available")
async def synapse_username_available():
return jsonify({"available": True})
@custom.route("/_synapse/admin/v1/threepid/<medium>/users/<addr>")
@custom.route("/_synapse/admin/v1/auth_providers/<provider>/users/<ext>")
async def synapse_threepid(p, a):
return jsonify({"user_id": f"@vona:{config.server_name}"})
@custom.route("/_synapse/admin/v1/<user_id>/redact")
def synapse_redact(user_id):
return jsonify({"redact_id": os.urandom(16).hex()})
@custom.route("/_synapse/admin/v1/user/redact_status/<redact_id>")
async def synapse_redact_status(redact_id):
return jsonify({"status":"active","failed_redactions":[]})
@custom.route("/_synapse/admin/v1/experimental_features/<user_id>", methods=["GET", "PUT"])
async def synapse_experimental_features(user_id):
return jsonify({"features": {}})
@custom.route("/_synapse/admin/v1/register", methods=["GET", "POST"])
async def synapse_register():
if request.method == "GET":
return jsonify({"nonce": os.urandom(16).hex()})
return jsonify({"access_token": "vona"})
@custom.route("/_synapse/admin/v1/join/<roomId>", methods=["POST"])
async def synapse_membership_manipulation(roomId):
return jsonify({"room_id": globals.make_event_id().replace("$", "!")})
@custom.route("/_synapse/admin/v1/account_validity/validity", methods=["POST"])
async def synapse_account_validity():
return jsonify({"expiration_ts": config.the_funny_number})
@custom.route("/_synapse/admin/v1/send_server_notice", methods=["POST"])
@custom.route("/_synapse/admin/v1/send_server_notice/<txnId>", methods=["PUT"])
async def synapse_server_notice(**kwargs):
return jsonify({"event_id": globals.make_event_id()})
@custom.route("/_synapse/admin/v1/purge_history/<room_id>/<event_id>", methods=["POST"])
@custom.route("/_synapse/admin/v1/purge_history/<room_id>", methods=["POST"])
async def synapse_purge_event(**kwargs):
return jsonify({"purge_id": os.urandom(16).hex()})
@custom.route("/_synapse/admin/v1/purge_history_status/<purge_id>")
async def synapse_purge_status(purge_id):
return jsonify({"status":"active"})
@custom.route("/_synapse/admin/v1/room/<room_id>/media")
async def synapse_room_media(room_id):
return jsonify({"local": [f"mxc://{config.server_name}/cat"], "remote": []})
@custom.route("/_synapse/admin/v1/room/<room_id>/media/quarantine", methods=["POST"])
async def synapse_quarantine_room_media(room_id):
return jsonify({"num_quarantined": config.the_funny_number})
@custom.route("/_synapse/admin/v1/media/<s>/delete", methods=["POST"])
@custom.route("/_synapse/admin/v1/media/<s>/<media_id>", methods=["DELETE"])
@custom.route("/_synapse/admin/v1/media/delete", methods=["POST"])
async def synapse_delete_media_from_server(**kwargs):
return jsonify({"deleted_media": ["cat"], "total": config.the_funny_number})
@custom.route("/_synapse/admin/v1/purge_media_cache", methods=["POST"])
async def synapse_delete_remote_media():
return jsonify({"deleted": config.the_funny_number})
@custom.route("/_synapse/admin/v1/statistics/users/media")
async def synapse_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})
@custom.route("/_synapse/admin/v1/statistics/database/rooms")
async def synapse_room_stats():
return jsonify({
"rooms": [{
"room_id": globals.make_event_id().replace("$", "!"),
"estimated_size": config.the_funny_number * 420
}]
})
@custom.route("/_synapse/admin/v1/background_updates/enabled", methods=["POST", "GET"])
@custom.route("/_synapse/admin/v1/background_updates/status")
async def synapse_change_bg_update():
return jsonify({"enabled":False})
# No documentation on what Synapse actually returns for this API, so a blank dict for now
@custom.route("/_synapse/admin/v1/background_updates/start_job", methods=["POST"])
async def synapse_bg_update_start_job():
return jsonify({})
@custom.route("/_synapse/admin/v1/event_reports")
async def synapse_event_reports():
return jsonify({
"event_reports": [{
"event_id": globals.make_event_id(),
"id": config.the_funny_number,
"reason": "",
"score": config.the_funny_number,
"received_ts": config.the_funny_number,
"room_id": globals.make_event_id().replace("$", "!"),
"name": "Vona",
"sender": f"@vona:{config.server_name}",
"user_id": f"@vona:{config.server_name}"
}],
"total": config.the_funny_number
})
@custom.route("/_synapse/admin/v1/event_reports/<report_id>", methods=["GET", "DELETE"])
async def synapse_interact_with_reported_event(report_id):
if request.method == "GET":
return jsonify({
"event_id": globals.make_event_id(),
"event_json": globals.hash_and_sign_event({
"auth_events": [],
"content": {
"msgtype": "m.text",
"body": "Number 15: Burger King Foot Lettuce.\nThe last thing you'd want in your Burger King burger is someones foot fungus, but as it turns out, that might be what you get. A 4channer uploaded a photo, anonymously to the site showcasing his feet in a plastic bin of lettuce with the statement \"This is the lettuce you eat at Burger King.\". Admittedly, he had shoes on, but thats even worse. The post went live at 11:38 PM on July 16 and a mere 20 minutes later the Burger King in question was alerted to the rogue employee. At least, I hope hes rogue. How did it happen? Well, the BK employee hadn't removed the EXIF data from the uploaded photo, which suggested that the culprit was somewhere in Mayfield Heights, Ohio. This was at 11:47. 3 minutes later, at 11:50, the Burger King branch was posted with wishes of happy unemployment. 5 minutes later, the news station was contacted by another 4channer, and 3 minutes later at 11:58 a link was posted: BK's tell us about us online forum. The foot photo, otherwise known as Exhibit A, was attached. Cleveland Seen Magazine contacted the BK in question and the next day when questioned, the breakfast shift manager said \"Oh, I know who that is, hes getting fired\". Mystery solved, by 4chan. Now we can go back to eating our fast food in peace.",
"format": "org.matrix.custom.html",
"formatted_body": "Number 15: Burger King Foot Lettuce.<br />The last thing you'd want in your Burger King burger is someones foot fungus, but as it turns out, that might be what you get. A 4channer uploaded a photo, anonymously to the site showcasing his feet in a plastic bin of lettuce with the statement &quot;This is the lettuce you eat at Burger King.&quot;. Admittedly, he had shoes on, but thats even worse. The post went live at 11:38 PM on July 16 and a mere 20 minutes later the Burger King in question was alerted to the rogue employee. At least, I hope hes rogue. How did it happen? Well, the BK employee hadn't removed the EXIF data from the uploaded photo, which suggested that the culprit was somewhere in Mayfield Heights, Ohio. This was at 11:47. 3 minutes later, at 11:50, the Burger King branch was posted with wishes of happy unemployment. 5 minutes later, the news station was contacted by another 4channer, and 3 minutes later at 11:58 a link was posted: BK's tell us about us online forum. The foot photo, otherwise known as Exhibit A, was attached. Cleveland Seen Magazine contacted the BK in question and the next day when questioned, the breakfast shift manager said &quot;Oh, I know who that is, hes getting fired&quot;. Mystery solved, by 4chan. Now we can go back to eating our fast food in peace."
},
"depth": config.the_funny_number,
"origin": config.server_name,
"origin_server_ts": config.the_funny_number,
"prev_events": [globals.make_event_id()],
"prev_state": [],
"room_id": globals.make_event_id().replace("$", "!"),
"sender": f"@vona:{config.server_name}",
"type": "m.room.message"
})
})
return jsonify({})
@custom.route("/_synapse/admin/v1/federation/destinations")
async def synapse_federation_destinations():
return jsonify({
"destinations": [{}],
"total": 0
})
@custom.route("/_synapse/admin/v1/federation/destinations/<destination>")
async def synapse_destination(destination):
return jsonify({
"destination": destination,
"retry_last_ts": config.the_funny_number,
"retry_interval": config.the_funny_number,
"failure_ts": config.the_funny_number,
"last_successful_stream_ordering": None
})
@custom.route("/_synapse/admin/v1/federation/destinations/<destination>/rooms")
async def synapse_destination_rooms(destination):
return jsonify({
"rooms": [],
"total": 0
})
@custom.route("/_synapse/admin/v1/registration_tokens")
async def synapse_reg_tokens():
return jsonify({
"registration_tokens": [{
"token": "Vona",
"uses_allowed": config.the_funny_number,
"pending": 0,
"completed": 1,
"expiry_time": None
}]
})
@custom.route("/_synapse/admin/v1/registration_tokens/<token>", methods=["GET", "PUT", "DELETE"])
async def synapse_reg_token(token):
if request.method == "DELETE":
return jsonify({})
return jsonify({
"token": "Vona",
"uses_allowed": config.the_funny_number,
"pending": 0,
"completed": 1,
"expiry_time": None
})
@custom.route("/_synapse/admin/v1/registration_tokens/new", methods=["POST"])
async def synapse_new_reg_token():
return jsonify({
"token": "Vona",
"uses_allowed": config.the_funny_number,
"pending": 0,
"completed": 1,
"expiry_time": None
})
@custom.route("/_synapse/admin/v1/rooms")
async def synapse_rooms():
return jsonify({
"rooms": [],
"offset": 0,
"total_rooms": 0
})
@custom.route("/_synapse/admin/v1/rooms/<room_id>/members")
async def synapse_room_members(room):
return jsonify({
"members": [
f"@vona:{config.server_name}"
],
"total": 1
})
@custom.route("/_synapse/admin/v1/rooms/<room_id>/state")
async def synapse_room_state(room):
return jsonify({"state": []})
@custom.route("/_synapse/admin/v1/rooms/<room_id>/state")
async def synapse_room_messages(room):
return jsonify({
"chunk": [],
"end": "vona",
"start": "vona"
})
@custom.route("/_synapse/admin/v1/rooms/<room_id>/block", methods=["GET", "PUT"])
async def synapse_block_room(room):
return jsonify({"block": False})
@custom.route("/_synapse/admin/v1/rooms/<room_id>", methods=["DELETE"])
async def synapse_room_delete(room):
return jsonify({
"kicked_users": [
f"@vona:{config.server_name}"
],
"failed_to_kick_users": [],
"local_aliases": [],
"new_room_id": f"!vona:{config.server_name}"
})
@custom.route("/_synapse/admin/v2/rooms/<room_id>", methods=["DELETE"])
async def synapse_room_delete_v2(room):
return jsonify({"delete_id": "vona"})
@custom.route("/_synapse/admin/v2/rooms/<room_id>/delete_status")
async def synapse_room_delete_status(room):
return jsonify({"results": []})
@custom.route("/_synapse/admin/v1/rooms/<room_id_or_alias>/forward_extremities", methods=["GET"])
async def synapse_forward_extremities(room):
if request.method == "DELETE":
return jsonify({"deleted": 0})
return jsonify({
"count": 1,
"results": [{
"event_id": globals.make_event_id(),
"state_group": config.the_funny_number,
"depth": config.the_funny_number,
"received_ts": config.the_funny_number
}]
})
# Dendrite - https://element-hq.github.io/dendrite/administration/adminapi
@custom.route("/_dendrite/admin/evacuateUser/<userId>", methods=["POST"])
async def dendrite_evacuate_user(userId):
return jsonify({"affected": [globals.make_event_id().replace("$", "!")]})
@custom.route("/_dendrite/admin/evacuateRoom/<roomId>", methods=["POST"])
async def dendrite_evacuate_room(roomId):
return jsonify({"affected": [f"@vona:{config.server_name}"]})
@custom.route("/_dendrite/admin/resetPassword/<userId>", methods=["POST"])
async def dendrite_reset_pswd(userId):
return jsonify({"password_updated": True})
# Conduwuit/Tuwunel/Continuwuity
@custom.route("/_continuwuity/local_user_count")
@custom.route("/_conduwuit/local_user_count")
@custom.route("/_tuwunel/local_user_count")
async def conduwuit_user_count():
return jsonify({"count": 1})
@custom.route("/_continuwuity/server_version")
@custom.route("/_conduwuit/server_version")
@custom.route("/_tuwunel/server_version")
async def conduwuit_server_version():
return jsonify({
"name": "Vona",
"version":globals.vona_version
})

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

@@ -0,0 +1,76 @@
import vona.config as config
import base64
import os
from flask import (
Blueprint,
jsonify,
request,
)
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):
req = request.json
if (
isinstance(req, dict)
and "store_response" in req
and isinstance(req["store_response"], bool)
):
store_response = req["store_response"]
else:
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)

28
vona/custom/conduwuit.py Normal file
View File

@@ -0,0 +1,28 @@
import vona.globals as globals
from flask import (
Blueprint,
jsonify,
)
conduwuit = Blueprint("conduwuit", __name__)
# Endpoints used by Conduwuit and forks.
@conduwuit.route("/_continuwuity/local_user_count")
@conduwuit.route("/_conduwuit/local_user_count")
@conduwuit.route("/_tuwunel/local_user_count")
async def user_count():
return jsonify({
"count": 1
})
@conduwuit.route("/_continuwuity/server_version")
@conduwuit.route("/_conduwuit/server_version")
@conduwuit.route("/_tuwunel/server_version")
async def server_version():
return jsonify({
"name": "Vona",
"version": globals.version
})

40
vona/custom/dendrite.py Normal file
View File

@@ -0,0 +1,40 @@
import vona.globals as globals
import vona.config as config
from flask import (
Blueprint,
jsonify,
)
dendrite = Blueprint("dendrite", __name__)
# This implements the Dendrite admin API
@dendrite.route("/_dendrite/admin/purgeRoom/<room>", methods=["POST"])
@dendrite.route("/_dendrite/admin/refreshDevices/<user>", methods=["POST"])
@dendrite.route("/_dendrite/admin/fulltext/reindex")
async def empty(**kwargs):
return jsonify({})
@dendrite.route("/_dendrite/admin/evacuateUser/<user>", methods=["POST"])
async def evacuate_user(user):
return jsonify({
"affected": [
globals.make_event_id().replace("$", "!")
]
})
@dendrite.route("/_dendrite/admin/evacuateRoom/<room>", methods=["POST"])
async def evacuate_room(room):
return jsonify({
"affected": [
f"@vona:{config.server_name}"
]
})
@dendrite.route("/_dendrite/admin/resetPassword/<user>", methods=["POST"])
async def reset_password(user):
return jsonify({"password_updated": True})

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

@@ -0,0 +1,134 @@
from datetime import datetime, timezone
import vona.globals as globals
import vona.config as config
import time
from flask import (
Blueprint,
jsonify,
)
from vona.federation import (
send_join,
bullshit,
)
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/reload-config")
async def reload_config():
return jsonify({})
@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):
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
if "event_id" in event:
event_id = event["event_id"]
else:
event_id = globals.make_ref_hash(
event,
int(globals.room_version_from_id(room))
)
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)
})
@hammerhead.route("/_hammerhead/admin/rooms/<room>/debug")
async def debug_room(room):
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
if "event_id" in event:
event_id = event["event_id"]
else:
event_id = globals.make_ref_hash(
event,
int(globals.room_version_from_id(room))
)
event_cache[event_id] = event
if globals.room_version_from_id(room) in ["1", "2"]:
extremity = globals.make_event_id(seed=f"6_{room}")
else:
extremity = globals.make_ref_hash(
state[5],
int(globals.room_version_from_id(room)),
)
return jsonify({
"aliases": [f"#vona:{config.server_name}"],
"current_state": formatted_state,
"event_cache": event_cache,
"room_id": room,
"room_version": globals.room_version_from_id(room),
"forward_extremities": [extremity],
"servers": None
})

484
vona/custom/synapse.py Normal file
View File

@@ -0,0 +1,484 @@
from vona.federation import send_join
import vona.globals as globals
import vona.config as config
import os
from flask import (
Blueprint,
jsonify,
request,
)
synapse = Blueprint("synapse", __name__)
# The absolute giant known as the Synapse admin API.
# TODO: Very messy, needs cleaning
@synapse.route("/_synapse/admin/v1/suspend/<user>", methods=["PUT"])
@synapse.route("/_synapse/admin/v1/deactivate/<user>", methods=["POST"])
@synapse.route("/_synapse/admin/v1/reset_password/<user>", methods=["POST"])
@synapse.route("/_synapse/admin/v1/users/<user>/admin", methods=["PUT"])
@synapse.route("/_synapse/admin/v2/users/<user>/delete_devices", methods=["POST"])
@synapse.route("/_synapse/admin/v1/users/<user>/shadow_ban", methods=["DELETE", "POST"])
@synapse.route("/_synapse/admin/v1/users/<user>/override_ratelimit", methods=["GET", "POST", "DELETE"])
@synapse.route("/_synapse/admin/v1/media/protect/<media_id>", methods=["POST"])
@synapse.route("/_synapse/admin/v1/media/unprotect/<media_id>", methods=["POST"])
@synapse.route("/_synapse/admin/v1/media/quarantine/<s>/<media_id>", methods=["POST"])
@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>/timestamp_to_event")
@synapse.route("/_synapse/admin/v2/rooms/delete_status/<delete_id>")
@synapse.route("/_synapse/admin/v1/rooms/<room>/make_room_admin", methods=["POST"])
@synapse.route("/_synapse/admin/v1/background_updates/start_job", methods=["POST"])
async def response(**kwargs):
return jsonify({})
@synapse.route("/_synapse/admin/v1/server_version")
async def version():
return jsonify({"server_version": globals.version})
@synapse.route("/_synapse/admin/v2/users")
async def user_list():
return jsonify({
"users": [
{
"name": f"@vona:{config.server_name}",
"is_guest": 0,
"admin": 0,
"user_type": "vona",
"deactivated": 0,
"erased": False,
"shadow_banned": 0,
"displayname": "Vona",
"avatar_url": f"mxc://{config.server_name}/cat",
"creation_ts": config.the_funny_number,
"locked": False
}
],
"total": 1
})
@synapse.route("/_synapse/admin/v2/users/<user>", methods=["GET", "PUT"])
async def user_info(user_id):
if request.method == "GET":
return jsonify({
"name": f"@vona:{config.server_name}",
"displayname": "Vona",
"threepids": [],
"avatar_url": f"mxc://{config.server_name}/cat",
"is_guest": 0,
"admin": 0,
"deactivated": 0,
"erased": False,
"shadow_banned": 0,
"creation_ts": config.the_funny_number,
"last_seen_ts": config.the_funny_number,
"appservice_id": config.the_funny_number,
"consent_server_notice_sent":config.the_funny_number,
"consent_version": config.the_funny_number,
"consent_ts": config.the_funny_number,
"external_ids": [],
"user_type": "vona",
"locked": False,
"suspended": False
})
return jsonify({}), 201
@synapse.route("/_synapse/admin/v1/whois/<user>")
async def whois(user_id):
return jsonify({
"user_id": f"@vona:{config.server_name}",
"devices": {
"": {
"sessions": [{
"connections": [{
"ip":"127.0.0.1",
"last_seen":config.the_funny_number,
"user_agent":f"Vona/{globals.version}"
}]
}]
}
}
})
@synapse.route("/_synapse/admin/v1/users/<user>/joined_rooms")
async def user_joined_rooms(user_id):
return jsonify({
"joined_rooms": [globals.make_event_id().replace("$", "!")],
"total": 1
})
@synapse.route("/_synapse/admin/v1/users/<user>/sent_invite_count")
async def invite_count(user_id):
return jsonify({"invite_count": config.the_funny_number})
@synapse.route("/_synapse/admin/v1/users/<user>/accountdata")
async def account_data(user_id):
return jsonify({"account_data":{"global":{}}})
@synapse.route("/_synapse/admin/v1/users/<user>/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": None,
"safe_from_quarantine": False,
"upload_name": "cat.jpg"
}],
"total": config.the_funny_number
})
return jsonify({
"deleted_media": ["cat"],
"total": config.the_funny_number
})
@synapse.route("/_synapse/admin/v1/users/<user>/login", methods=["POST"])
async def account_login(user_id):
return jsonify({"access_token": "vona"})
@synapse.route("/_synapse/admin/v1/users/<user>/_allow_cross_signing_replacement_without_uia", methods=["POST"])
async def stupid_mas_bullshit(user_id):
return jsonify({"updatable_without_uia_before_ms": config.the_funny_number})
@synapse.route("/_synapse/admin/v2/users/<user>/devices", methods=["GET", "POST"])
async def device_list(user_id):
if request.method == "GET":
return jsonify({
"devices": [{
"device_id": "VVOONNAA",
"display_name": "Vona",
"last_seen_ip": config.addr,
"last_seen_ts": config.the_funny_number,
"last_seen_user_agent": f"Vona/{globals.version}"
}],
"total": 1
})
return jsonify({})
@synapse.route("/_synapse/admin/v2/users/<user>/devices/<device_id>", methods=["GET", "PUT", "DELETE"])
async def device_info(user_id, device_id):
if request.method == "GET":
return jsonify({
"device_id": "VVOONNAA",
"display_name": "Vona",
"last_seen_ip": config.addr,
"last_seen_ts": config.the_funny_number,
"last_seen_user_agent": f"Vona/{globals.version}"
})
return jsonify({})
@synapse.route("/_synapse/admin/v1/users/<user>/pushers")
async def pushers(user_id):
return jsonify({"pushers": [], "total": config.the_funny_number})
@synapse.route("/_synapse/admin/v1/username_available")
async def username_available():
return jsonify({"available": True})
@synapse.route("/_synapse/admin/v1/threepid/<medium>/users/<addr>")
@synapse.route("/_synapse/admin/v1/auth_providers/<provider>/users/<ext>")
async def threepid(**kwargs):
return jsonify({"user_id": f"@vona:{config.server_name}"})
@synapse.route("/_synapse/admin/v1/<user>/redact")
async def redact(user_id):
return jsonify({"redact_id": os.urandom(16).hex()})
@synapse.route("/_synapse/admin/v1/user/redact_status/<redact_id>")
async def redact_status(redact_id):
return jsonify({"status":"active","failed_redactions":[]})
@synapse.route("/_synapse/admin/v1/experimental_features/<user>", methods=["GET", "PUT"])
async def experimental_features(user_id):
return jsonify({"features": {}})
@synapse.route("/_synapse/admin/v1/register", methods=["GET", "POST"])
async def register():
if request.method == "GET":
return jsonify({"nonce": os.urandom(16).hex()})
return jsonify({"access_token": "vona"})
@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"])
async def account_validity():
return jsonify({"expiration_ts": config.the_funny_number})
@synapse.route("/_synapse/admin/v1/send_server_notice", methods=["POST"])
@synapse.route("/_synapse/admin/v1/send_server_notice/<txnId>", methods=["PUT"])
async def server_notice(**kwargs):
return jsonify({"event_id": globals.make_event_id()})
@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()})
@synapse.route("/_synapse/admin/v1/purge_history_status/<purge_id>")
async def purge_status(purge_id):
return jsonify({"status":"active"})
@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>/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"])
@synapse.route("/_synapse/admin/v1/media/<s>/<media_id>", methods=["DELETE"])
@synapse.route("/_synapse/admin/v1/media/delete", methods=["POST"])
async def delete_media_from_server(**kwargs):
return jsonify({"deleted_media": ["cat"], "total": config.the_funny_number})
@synapse.route("/_synapse/admin/v1/purge_media_cache", methods=["POST"])
async def delete_remote_media():
return jsonify({"deleted": config.the_funny_number})
@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
})
@synapse.route("/_synapse/admin/v1/statistics/database/rooms")
async def room_stats():
return jsonify({
"rooms": [{
"room_id": globals.make_event_id().replace("$", "!"),
"estimated_size": config.the_funny_number * 420
}]
})
@synapse.route("/_synapse/admin/v1/background_updates/enabled", methods=["POST", "GET"])
@synapse.route("/_synapse/admin/v1/background_updates/status")
async def change_bg_update():
return jsonify({"enabled": False})
@synapse.route("/_synapse/admin/v1/event_reports")
async def event_reports():
return jsonify({
"event_reports": [{
"event_id": globals.make_event_id(),
"id": config.the_funny_number,
"reason": "",
"score": config.the_funny_number,
"received_ts": config.the_funny_number,
"room_id": globals.make_event_id().replace("$", "!"),
"name": "Vona",
"sender": f"@vona:{config.server_name}",
"user_id": f"@vona:{config.server_name}"
}],
"total": config.the_funny_number
})
@synapse.route("/_synapse/admin/v1/event_reports/<report_id>", methods=["GET", "DELETE"])
async def interact_with_reported_event(report_id):
if request.method == "GET":
return jsonify({
"event_id": globals.make_event_id(),
"event_json": globals.hash_and_sign_event({
"auth_events": [],
"content": {
"msgtype": "m.text",
"body": "Number 15: Burger King Foot Lettuce.\nThe last thing you'd want in your Burger King burger is someones foot fungus, but as it turns out, that might be what you get. A 4channer uploaded a photo, anonymously to the site showcasing his feet in a plastic bin of lettuce with the statement \"This is the lettuce you eat at Burger King.\". Admittedly, he had shoes on, but thats even worse. The post went live at 11:38 PM on July 16 and a mere 20 minutes later the Burger King in question was alerted to the rogue employee. At least, I hope hes rogue. How did it happen? Well, the BK employee hadn't removed the EXIF data from the uploaded photo, which suggested that the culprit was somewhere in Mayfield Heights, Ohio. This was at 11:47. 3 minutes later, at 11:50, the Burger King branch was posted with wishes of happy unemployment. 5 minutes later, the news station was contacted by another 4channer, and 3 minutes later at 11:58 a link was posted: BK's tell us about us online forum. The foot photo, otherwise known as Exhibit A, was attached. Cleveland Seen Magazine contacted the BK in question and the next day when questioned, the breakfast shift manager said \"Oh, I know who that is, hes getting fired\". Mystery solved, by 4chan. Now we can go back to eating our fast food in peace.",
"format": "org.matrix.custom.html",
"formatted_body": "Number 15: Burger King Foot Lettuce.<br />The last thing you'd want in your Burger King burger is someones foot fungus, but as it turns out, that might be what you get. A 4channer uploaded a photo, anonymously to the site showcasing his feet in a plastic bin of lettuce with the statement &quot;This is the lettuce you eat at Burger King.&quot;. Admittedly, he had shoes on, but thats even worse. The post went live at 11:38 PM on July 16 and a mere 20 minutes later the Burger King in question was alerted to the rogue employee. At least, I hope hes rogue. How did it happen? Well, the BK employee hadn't removed the EXIF data from the uploaded photo, which suggested that the culprit was somewhere in Mayfield Heights, Ohio. This was at 11:47. 3 minutes later, at 11:50, the Burger King branch was posted with wishes of happy unemployment. 5 minutes later, the news station was contacted by another 4channer, and 3 minutes later at 11:58 a link was posted: BK's tell us about us online forum. The foot photo, otherwise known as Exhibit A, was attached. Cleveland Seen Magazine contacted the BK in question and the next day when questioned, the breakfast shift manager said &quot;Oh, I know who that is, hes getting fired&quot;. Mystery solved, by 4chan. Now we can go back to eating our fast food in peace."
},
"depth": config.the_funny_number,
"origin": config.server_name,
"origin_server_ts": config.the_funny_number,
"prev_events": [globals.make_event_id()],
"prev_state": [],
"room_id": globals.make_event_id().replace("$", "!"),
"sender": f"@vona:{config.server_name}",
"type": "m.room.message"
})
})
return jsonify({})
@synapse.route("/_synapse/admin/v1/federation/destinations")
async def federation_destinations():
return jsonify({
"destinations": [],
"total": 0
})
@synapse.route("/_synapse/admin/v1/federation/destinations/<destination>")
async def destination(destination):
return jsonify({
"destination": destination,
"retry_last_ts": config.the_funny_number,
"retry_interval": config.the_funny_number,
"failure_ts": config.the_funny_number,
"last_successful_stream_ordering": None
})
@synapse.route("/_synapse/admin/v1/federation/destinations/<destination>/rooms")
async def destination_rooms(destination):
return jsonify({
"rooms": [],
"total": 0
})
@synapse.route("/_synapse/admin/v1/registration_tokens")
async def reg_tokens():
return jsonify({
"registration_tokens": [{
"token": "Vona",
"uses_allowed": config.the_funny_number,
"pending": 0,
"completed": 1,
"expiry_time": None
}]
})
@synapse.route("/_synapse/admin/v1/registration_tokens/<token>", methods=["GET", "PUT", "DELETE"])
async def reg_token(token):
if request.method == "DELETE":
return jsonify({})
return jsonify({
"token": "Vona",
"uses_allowed": config.the_funny_number,
"pending": 0,
"completed": 1,
"expiry_time": None
})
@synapse.route("/_synapse/admin/v1/registration_tokens/new", methods=["POST"])
async def new_reg_token():
return jsonify({
"token": "Vona",
"uses_allowed": config.the_funny_number,
"pending": 0,
"completed": 1,
"expiry_time": None
})
@synapse.route("/_synapse/admin/v1/rooms")
async def rooms():
return jsonify({
"rooms": [],
"offset": 0,
"total_rooms": 0
})
@synapse.route("/_synapse/admin/v1/rooms/<room>/members")
async def room_members(room):
return jsonify({
"members": [
f"@vona:{config.server_name}"
],
"total": 1
})
@synapse.route("/_synapse/admin/v1/rooms/<room>/state")
async def room_state(room):
return jsonify({"state": []})
@synapse.route("/_synapse/admin/v1/rooms/<room>/state")
async def room_messages(room):
return jsonify({
"chunk": [],
"end": "vona",
"start": "vona"
})
@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>", methods=["DELETE"])
async def room_delete(room):
return jsonify({
"kicked_users": [
f"@vona:{config.server_name}"
],
"failed_to_kick_users": [],
"local_aliases": [],
"new_room_id": globals.make_event_id(seed=room)
})
@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>/delete_status")
async def room_delete_status(room):
return jsonify({"results": []})
@synapse.route("/_synapse/admin/v1/rooms/<room>/forward_extremities", methods=["GET"])
async def forward_extremities(room):
if request.method == "DELETE":
return jsonify({"deleted": 0})
return jsonify({
"count": 1,
"results": [{
"event_id": globals.make_event_id(),
"state_group": config.the_funny_number,
"depth": config.the_funny_number,
"received_ts": config.the_funny_number
}]
})
@synapse.route("/_synapse/admin/v1/media/<origin>/<media_id>")
async def media_info(origin, media_id):
return jsonify({
"media_info": {
"media_origin": origin,
"user_id": None,
"media_id": media_id,
"media_type": "img/jpeg",
"media_length": 67,
"upload_name": "cat.jpg",
"created_ts": config.the_funny_number,
"filesystem_id": os.urandom(3).hex(),
"url_cache": None,
"last_access_ts": config.the_funny_number,
"quarantined_by": None,
"authenticated": False,
"safe_from_quarantine": None,
"sha256": os.urandom(32).hex()
}
})
@synapse.route("/_synapse/admin/v1/fetch_event/<evt>")
async def fetch_event(evt):
class bs:
def get_json():
return {}
return jsonify({
"event": send_join(
bs,
globals.make_event_id(seed=evt).replace("$", "!")
)["state"][5]
})

View File

@@ -0,0 +1,90 @@
from vona.globals import version
import vona.config as config
from flask import (
Blueprint,
request,
jsonify,
)
telo = Blueprint("telodendria", __name__)
# The telodendria admin API as specified by
# https://git.telodendria.org/Telodendria/Telodendria/src/branch/master/docs/user/admin/README.md
@telo.route("/_telodendria/admin/v1/restart", methods=["POST"])
@telo.route("/_telodendria/admin/v1/shutdown", methods=["POST"])
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):
return jsonify({
"privileges": [
"GRANT_PRIVILEGES",
"PROC_CONTROL",
"ISSUE_TOKENS",
"DEACTIVATE",
"CONFIG",
"ALIAS",
"ALL"
]
})
@telo.route("/_telodendria/admin/v1/config", methods=["GET", "PUT", "POST"])
async def configuration():
if request.method == "GET":
return jsonify({
"listen": [
{
"tls": None,
"port": config.port,
"threads": 1,
"maxConnections": 32
}
],
"serverName": config.server_name,
"pid": "/dev/null",
"baseUrl": f"https://{config.server_name}/",
"identityServer": f"https://{config.server_name}/",
"runAs": None,
"federation": True,
"registration": config.users_can_register,
"log": {
"output": "stdout",
"level": "message",
"timestampFormat": "default",
"color": True
},
"maxCache": config.the_funny_number
})
return jsonify({"restart_required": True})
@telo.route("/_telodendria/admin/v1/stats")
async def stats():
return jsonify({
"memory_allocated": config.the_funny_number,
"version": version
})
@telo.route("/_telodendria/admin/v1/tokens/<name>", methods=["GET", "DELETE"])
@telo.route("/_telodendria/admin/v1/tokens", methods=["GET", "POST"])
async def tokens(name=None):
if request.method == "DELETE":
return jsonify({})
return jsonify({
"name": "vona",
"created_by": "vona",
"created_on": config.the_funny_number,
"expires_on": config.the_funny_number,
"used": config.the_funny_number,
"uses": config.the_funny_number
})

View File

@@ -1,265 +1,73 @@
from flask import jsonify, Response, request, send_file, abort, Blueprint import vona.federation.rooms as rooms
from vona.config import *
import vona.globals as globals import vona.globals as globals
import httpx import vona.config as config
import json import json
import time import time
import os
from flask import (
jsonify,
Response,
request,
Blueprint,
abort,
)
server = Blueprint("federation", __name__) server = Blueprint("federation", __name__)
http = globals.http_client()
def send_join(request, roomId) -> dict: class bullshit:
event_chain = [] def get_json():
event_hashes = [] return {}
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 = { def send_join(request, room) -> dict:
"content": { if globals.room_version_from_id(room) in ["1", "2"]:
"m.federate": True, return rooms.v1_v2(request, room)
"creator": f"@vona:{server_name}", else:
"room_version": "2" return rooms.v3(request, room)
},
"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"]
],
[
sjoin_rule["event_id"],
sjoin_rule["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"]
],
[
sjoin_rule["event_id"],
sjoin_rule["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") @server.route("/_matrix/federation/v1/version")
async def version(): async def version():
return jsonify({ return jsonify({
"server": { "server": {
"version": globals.vona_version, "version": globals.version,
"name": "Vona" "name": "Vona"
} }
}) })
@server.route("/_matrix/key/v2/server") @server.route("/_matrix/key/v2/server")
async def keys(): async def keys():
return jsonify(globals.sign_json({ return jsonify(globals.sign_json({
"old_verify_keys": {}, "old_verify_keys": {},
"server_name": server_name, "server_name": config.server_name,
"valid_until_ts": int(time.time() * 1000 + 604800000), "valid_until_ts": int(time.time() * 1000 + 604800000),
"verify_keys": { "verify_keys": {
f"ed25519:{signing_key.split()[1]}": { f"ed25519:{config.signing_key.split()[1]}": {
"key": globals.pubkey() "key": globals.pubkey()
} }
} }
})) }))
@server.route("/_matrix/federation/v1/query/directory") @server.route("/_matrix/federation/v1/query/directory")
async def room_query(): async def room_query():
return jsonify({ return jsonify({
"room_id": globals.make_event_id().replace("$", "!"), "room_id": globals.make_event_id().replace("$", "!"),
"servers": [server_name] "servers": [config.server_name]
}) })
@server.route("/_matrix/federation/v1/media/download/<media_id>") @server.route("/_matrix/federation/v1/media/download/<media_id>")
async def download_media(media_id): async def download_media(media_id):
# Auth media requires this to be # Auth media requires this to be
# multipart despite not even using # multipart despite not even using
# it for anything. Minor annoyance. # 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() image_data = img_file.read()
boundary = "----WebKitFormBoundary7MA4YWxkTrZu0gW" boundary = "----WebKitFormBoundary7MA4YWxkTrZu0gW"
response_body = ( response_body = (
@@ -276,6 +84,7 @@ async def download_media(media_id):
return response return response
@server.route("/_matrix/federation/v1/media/thumbnail/<media_id>") @server.route("/_matrix/federation/v1/media/thumbnail/<media_id>")
async def thumbnail_media(media_id): async def thumbnail_media(media_id):
return jsonify({ return jsonify({
@@ -283,51 +92,65 @@ async def thumbnail_media(media_id):
"error": "Cat is too cute to thumbnail" "error": "Cat is too cute to thumbnail"
}), 418 }), 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"]) @server.route("/_matrix/federation/v1/send_join/<room>/<path:eventId>", methods=["PUT"])
async def send_join_v2(roomId, eventId): async def send_join_v1(room, eventId):
return jsonify(send_join(request, roomId)) 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
@server.route("/_matrix/federation/v1/make_join/<roomId>/<userId>") return jsonify([200, send_join(request, room)])
async def make_join(roomId, userId):
def not_invited():
@server.route("/_matrix/federation/v2/send_join/<room>/<path:eventId>", methods=["PUT"])
async def send_join_v2(room, eventId):
if globals.room_version_from_id(room) in ["1", "2"]:
# Spec says to fallback to the v1 send_join endpoint if this fails
abort(404)
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({ return jsonify({
"errcode": "M_FORBIDDEN", "errcode": "M_FORBIDDEN",
"error": "You are not invited to this room." "error": "You are not invited to this room."
}), 403 }), 403
try:
if roomId.split(":")[1] != server_name:
return not_invited()
except:
return not_invited()
class bullshit: room_ver = globals.room_version_from_id(room)
def get_json():
return {}
state = send_join( state = send_join(
request=bullshit, request=bullshit,
roomId=roomId room=room
)["state"] )["state"]
join = { join = {
"content": { "content": {
"join_authorised_via_users_server": f"@vona:{server_name}",
"membership": "join" "membership": "join"
}, },
"origin": server_name, "origin": config.server_name,
"origin_server_ts": 7, "origin_server_ts": 7,
"room_id": roomId, "room_id": room,
"sender": userId, "sender": user,
"state_key": userId, "state_key": user,
"type": "m.room.member", "type": "m.room.member",
"depth": 7 "depth": 7
} }
if room_ver in ["1", "2"]:
join["event_id"] = globals.make_event_id(seed=f"{user}+{room}")
join["auth_events"] = [ join["auth_events"] = [
[ [
state[0]["event_id"], state[0]["event_id"],
@@ -347,21 +170,29 @@ async def make_join(roomId, userId):
state[5]["event_id"], state[5]["event_id"],
state[5]["hashes"] 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({ return jsonify({
"event": globals.hash_and_sign_event(join), "event": globals.hash_and_sign_event(join, int(room_ver)),
"room_version": "2" "room_version": room_ver
}) })
@server.route("/_matrix/federation/v1/publicRooms", methods=["POST", "GET"]) @server.route("/_matrix/federation/v1/publicRooms", methods=["POST", "GET"])
async def room_directory(): async def room_directory():
return jsonify({ return jsonify(globals.room_dir)
"chunk": [],
"total_room_count_estimate": 0
})
# https://spec.matrix.org/latest/server-server-api/#transactions
@server.route("/_matrix/federation/v1/send/<txnId>", methods=["PUT"]) @server.route("/_matrix/federation/v1/send/<txnId>", methods=["PUT"])
async def receive_txn(txnId): async def receive_txn(txnId):
# We will need to implement a way to store every # We will need to implement a way to store every
@@ -385,7 +216,13 @@ async def receive_txn(txnId):
if "pdus" in parsed_data: if "pdus" in parsed_data:
for pdu in parsed_data["pdus"]: for pdu in parsed_data["pdus"]:
if "event_id" in pdu: if "event_id" in pdu:
response["pdus"][pdu["event_id"]] = {} # 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) return jsonify(response)
@@ -395,7 +232,7 @@ async def user_profile():
field = request.args.get("field") field = request.args.get("field")
if field: if field:
if field == "avatar_url": 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": elif field == "displayname":
return jsonify({"displayname":"Vona"}) return jsonify({"displayname":"Vona"})
@@ -404,71 +241,68 @@ async def user_profile():
"error": "The requested profile key does not exist." "error": "The requested profile key does not exist."
}), 404 }), 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/@/:{config.server_name}")
@server.route(f"/_matrix/federation/v1/user/devices/@/:{server_name}")
@server.route("/_matrix/federation/v1/user/devices/<user>") @server.route("/_matrix/federation/v1/user/devices/<user>")
async def user_devices(user): async def user_devices(user):
return jsonify({ return jsonify({
"devices": [], "devices": [],
"stream_id": the_funny_number, "stream_id": config.the_funny_number,
"user_id": f"@vona:{server_name}" "user_id": f"@vona:{config.server_name}"
}) })
@server.route("/_matrix/federation/v1/user/keys/query", methods=["POST"]) @server.route("/_matrix/federation/v1/user/keys/query", methods=["POST"])
async def user_keys(): async def user_keys():
try: req = request.json
users = request.json["device_keys"]
except:
return jsonify({
"errcode": "M_NOT_FOUND",
"error": "User does not exist"
}), 404
return jsonify({"device_keys": users}) if (
isinstance(req, dict)
and "device_keys" in req
and isinstance(req["device_keys"], dict)
):
device_keys = req["device_keys"]
else:
device_keys = {}
return jsonify({
"device_keys": device_keys
})
@server.route("/_matrix/federation/v2/invite/<room>/<txnId>", methods=["PUT"]) @server.route("/_matrix/federation/v2/invite/<room>/<txnId>", methods=["PUT"])
async def invite_user_v2(room, txnId): async def invite_user_v2(room, txnId):
return invite_user(request.data) invite_data = request.json
if (
@server.route("/_matrix/federation/v1/invite/<room>/<txnId>", methods=["PUT"]) isinstance(invite_data, dict)
async def invite_user_v1(room, txnId): and "event" in invite_data
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 "room_version" in invite_data:
if invite_data["room_version"] != "2": if invite_data["room_version"] not in [str(i) for i in range(1, 10)]:
return jsonify({ return jsonify({
"errcode": "M_INCOMPATIBLE_ROOM_VERSION", "errcode": "M_INCOMPATIBLE_ROOM_VERSION",
"error": "Vona only supports room version 2.", "error": "Unsupported room version",
"room_version": invite_data["room_version"] "room_version": invite_data["room_version"]
}), 400 }), 400
event = invite_data.get("event", {}) event = invite_data.get("event", {})
content = event.get("content", {}) content = event.get("content", {})
# NOTE to crispycat: I know you loooooove this syntax
if ( if (
"content" in event "content" in event
and "membership" in content and "membership" in content
and "state_key" in event and "state_key" in event
and "room_id" in event and "room_id" in event
and content["membership"] == "invite" 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"]), "room_version": "2"}) return jsonify({
"event": globals.sign_json_without_discard(event),
"room_version": invite_data["room_version"]
})
return jsonify({ return jsonify({
@@ -477,9 +311,166 @@ def invite_user(data):
}), 403 }), 403
@server.route("/_matrix/federation/v1/hierarchy/<roomId>") @server.route("/_matrix/federation/v1/invite/<room>/<txnId>", methods=["PUT"])
async def space_hierachy(roomId): async def invite_user_v1(room, txnId):
event = request.json
if (
isinstance(event, dict)
and "content" in event
and isinstance(event["content"], dict)
):
content = event["content"]
else:
content = {}
if (
isinstance(event, dict)
and isinstance(content, dict)
and "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({ return jsonify({
"errcode": "M_NOT_FOUND", "errcode": "M_NOT_FOUND",
"error": "Room does not exist." "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():
req = request.json
if isinstance(req, dict):
tags = req.get("room_participation_tags", [])
else:
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.get_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,
}) })

284
vona/federation/rooms.py Normal file
View File

@@ -0,0 +1,284 @@
import vona.globals as globals
import vona.config as config
# This file is responsible for creating Matrix rooms.
# Room V1/V2
def v1_v2(request, room) -> dict:
event_chain = []
event_ids = [
globals.make_event_id(seed=f"1_{room}"),
globals.make_event_id(seed=f"2_{room}"),
globals.make_event_id(seed=f"3_{room}"),
globals.make_event_id(seed=f"4_{room}"),
globals.make_event_id(seed=f"5_{room}"),
globals.make_event_id(seed=f"6_{room}"),
]
create_event = {
"content": {
"m.federate": True,
"creator": f"@vona:{config.server_name}",
"room_version": globals.room_version_from_id(room)
},
"event_id": event_ids[0],
"origin_server_ts": 1,
"room_id": room,
"sender": f"@vona:{config.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://{config.server_name}/cat",
"membership": "join"
},
"origin_server_ts": 2,
"sender": f"@vona:{config.server_name}",
"state_key": f"@vona:{config.server_name}",
"type": "m.room.member",
"event_id": event_ids[1],
"room_id": room,
"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:{config.server_name}": "100"
}
},
"origin_server_ts": 3,
"room_id": room,
"sender": f"@vona:{config.server_name}",
"state_key": "",
"type": "m.room.power_levels",
"event_id": event_ids[2],
"depth": 3,
"user_id": f"@vona:{config.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:{config.server_name}",
"state_key": "",
"type": "m.room.join_rules",
"event_id": event_ids[3],
"room_id": room,
"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:{config.server_name}",
"state_key": "",
"type": "m.room.guest_access",
"event_id": event_ids[4],
"room_id": room,
"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:{config.server_name}",
"state_key": "",
"origin_server_ts": 6,
"depth": 6,
"event_id": event_ids[5],
"room_id": room,
"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()
return {
"auth_chain": event_chain,
"event": remote_join,
"members_omitted": False,
"servers_in_room": [config.server_name],
"state": event_chain
}
# Room V3 to V10
def v3(request, room) -> dict:
initial_response = v1_v2(request, room)
state = list(initial_response["state"])
ver = int(globals.room_version_from_id(room))
events = {}
hash_map = {}
for event in state:
events[event["type"]] = event
del event["event_id"]
del event["hashes"]
del event["signatures"]
# m.room.create doesn't have prev_events or auth_events
if ver >= 11:
del events["m.room.create"]["content"]["creator"]
events["m.room.create"] = globals.hash_and_sign_event(events["m.room.create"], ver)
hash_map["m.room.create"] = globals.make_ref_hash(events["m.room.create"], ver)
events["m.room.member"]["auth_events"] = [hash_map["m.room.create"]]
events["m.room.member"]["prev_events"] = [hash_map["m.room.create"]]
events["m.room.member"] = globals.hash_and_sign_event(events["m.room.member"], ver)
hash_map["m.room.member"] = globals.make_ref_hash(events["m.room.member"], ver)
events["m.room.power_levels"]["auth_events"] = [
hash_map["m.room.create"],
hash_map["m.room.member"],
]
events["m.room.power_levels"]["prev_events"] = [hash_map["m.room.member"]]
if ver >= 10:
events["m.room.power_levels"]["content"]["users"][f"@vona:{config.server_name}"] = 100
events["m.room.power_levels"] = globals.hash_and_sign_event(events["m.room.power_levels"], ver)
hash_map["m.room.power_levels"] = globals.make_ref_hash(events["m.room.power_levels"], ver)
events["m.room.join_rules"]["auth_events"] = [
hash_map["m.room.create"],
hash_map["m.room.member"],
hash_map["m.room.power_levels"],
]
events["m.room.join_rules"]["prev_events"] = [hash_map["m.room.power_levels"]]
events["m.room.join_rules"] = globals.hash_and_sign_event(events["m.room.join_rules"], ver)
hash_map["m.room.join_rules"] = globals.make_ref_hash(events["m.room.join_rules"], ver)
events["m.room.guest_access"]["auth_events"] = [
hash_map["m.room.create"],
hash_map["m.room.member"],
hash_map["m.room.power_levels"],
]
events["m.room.guest_access"]["prev_events"] = [hash_map["m.room.join_rules"]]
events["m.room.guest_access"] = globals.hash_and_sign_event(events["m.room.guest_access"], ver)
hash_map["m.room.guest_access"] = globals.make_ref_hash(events["m.room.guest_access"], ver)
events["m.room.history_visibility"]["auth_events"] = [
hash_map["m.room.create"],
hash_map["m.room.member"],
hash_map["m.room.power_levels"],
]
events["m.room.history_visibility"]["prev_events"] = [hash_map["m.room.guest_access"]]
events["m.room.history_visibility"] = globals.hash_and_sign_event(events["m.room.history_visibility"], ver)
new_state = []
for event in events:
new_state.append(events[event])
return {
"auth_chain": new_state,
"event": initial_response["event"],
"members_omitted": False,
"servers_in_room": [config.server_name],
"state": new_state
}

View File

@@ -1,17 +1,23 @@
from importlib.metadata import version from resolvematrix import ServerResolver
from types import SimpleNamespace
from collections import Counter
import vona.config as config import vona.config as config
import nacl.encoding
import nacl.signing import nacl.signing
import hashlib import hashlib
import base64 import base64
import random import random
import copy import httpx
import json import json
import copy
import time
import re import re
vona_version = version("vona") version = "1.5.0"
def canonical_json(value): def canonical_json(value: dict | list) -> bytes:
return json.dumps( return json.dumps(
value, value,
ensure_ascii=False, ensure_ascii=False,
@@ -20,7 +26,7 @@ def canonical_json(value):
).encode("UTF-8") ).encode("UTF-8")
def sign_json(data): def sign_json(data: dict) -> dict:
parts = config.signing_key.split() parts = config.signing_key.split()
base64_key = parts[2] base64_key = parts[2]
@@ -49,7 +55,7 @@ def sign_json(data):
return signed_json return signed_json
def sign_json_without_discard(data): def sign_json_without_discard(data: dict) -> dict:
parts = config.signing_key.split() parts = config.signing_key.split()
base64_key = parts[2] base64_key = parts[2]
@@ -82,7 +88,7 @@ def sign_json_without_discard(data):
return data return data
def make_event_id(seed=None): def make_event_id(seed: str | int | None = None) -> str:
if seed is not None: if seed is not None:
random.seed(seed) random.seed(seed)
@@ -100,7 +106,7 @@ def make_event_id(seed=None):
return event_id return event_id
def event_hash(event_object): def event_hash(event_object: dict) -> str:
event_object = dict(event_object) event_object = dict(event_object)
event_object.pop("unsigned", None) event_object.pop("unsigned", None)
@@ -109,7 +115,9 @@ def event_hash(event_object):
event_json_bytes = canonical_json(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: def pubkey() -> str:
@@ -127,7 +135,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 = { request_json = {
"method": method, "method": method,
"uri": path, "uri": path,
@@ -159,9 +172,13 @@ def make_auth_header(destination, method, path, content=None) -> str:
return authorization_headers[0].decode("utf-8") return authorization_headers[0].decode("utf-8")
def redact_event(event): def redact_event(
event: dict,
for_event_id: bool = False,
room_ver: int = 1,
) -> dict:
# Returns a redacted event as per # Returns a redacted event as per
# the algorithm for v1/v2 rooms. # the algorithm for v1 to v11 rooms.
allowed_keys = [ allowed_keys = [
"event_id", "event_id",
@@ -171,16 +188,20 @@ def redact_event(event):
"state_key", "state_key",
"content", "content",
"hashes", "hashes",
"signatures",
"depth", "depth",
"prev_events", "prev_events",
"prev_state",
"auth_events", "auth_events",
"origin",
"origin_server_ts", "origin_server_ts",
"membership",
] ]
if not for_event_id:
allowed_keys.append("signatures")
if room_ver < 11:
allowed_keys.append("origin")
allowed_keys.append("membership")
allowed_keys.append("prev_state")
redacted_event = {k: v for k, v in event.items() if k in allowed_keys} redacted_event = {k: v for k, v in event.items() if k in allowed_keys}
if "type" in redacted_event and "content" in redacted_event: if "type" in redacted_event and "content" in redacted_event:
@@ -204,23 +225,246 @@ def redact_event(event):
"m.room.history_visibility": ["history_visibility"], "m.room.history_visibility": ["history_visibility"],
} }
if room_ver >= 6:
del content_key_rules["m.room.aliases"]
if room_ver >= 8:
content_key_rules["m.room.join_rules"].append("allow")
if room_ver >= 9:
content_key_rules["m.room.member"].append("join_authorised_via_users_server")
if room_ver >= 11:
content_key_rules["m.room.redaction"] = ["redacts"]
del content_key_rules["m.room.create"] # All keys will be permitted
if event_type in content_key_rules: if event_type in content_key_rules:
allowed_content_keys = content_key_rules[event_type] allowed_content_keys = content_key_rules[event_type]
if (
room_ver >= 11
and "third_party_invite" in redacted_event
and "signed" in redacted_event["third_party_invite"]
):
third_party_invite_signature = copy.deepcopy(redacted_event["third_party_invite"]["signed"])
else:
third_party_invite_signature = None
redacted_event["content"] = { redacted_event["content"] = {
k: v k: v
for k, v in redacted_event["content"].items() for k, v in redacted_event["content"].items()
if k in allowed_content_keys if k in allowed_content_keys
} }
if third_party_invite_signature:
redacted_event["content"]["third_party_invite"] = {
"signed": third_party_invite_signature
}
else:
if room_ver >= 11 and event_type == "m.room.create":
pass
else: else:
redacted_event["content"] = {} redacted_event["content"] = {}
return redacted_event return redacted_event
def hash_and_sign_event(event_object): def hash_and_sign_event(
event_object: dict,
room_ver: int = 1,
) -> dict:
content_hash = event_hash(event_object) content_hash = event_hash(event_object)
event_object["hashes"] = {"sha256": content_hash} event_object["hashes"] = {"sha256": content_hash}
stripped_object = redact_event(event_object) stripped_object = redact_event(
event=event_object,
for_event_id=False,
room_ver=room_ver,
)
signed_object = sign_json(stripped_object) signed_object = sign_json(stripped_object)
event_object["signatures"] = signed_object["signatures"] event_object["signatures"] = signed_object["signatures"]
return event_object return event_object
def make_ref_hash(
event: dict,
room_ver: int = 3,
) -> str:
stripped = redact_event(
event=event,
for_event_id=True,
room_ver=room_ver,
)
evt_bytes = canonical_json(stripped)
evt_hash = base64.b64encode(
hashlib.sha256(evt_bytes).digest()
).decode("utf-8").rstrip("=")
if room_ver > 3:
while "+" in evt_hash:
evt_hash = evt_hash.replace("+", "-")
while "/" in evt_hash:
evt_hash = evt_hash.replace("/", "_")
return "$" + evt_hash
def room_version_from_id(room) -> str:
room_id_no_sigil = (
room
.replace("!", "")
.replace(f":{config.server_name}", "")
)
hexadecimal_room_id = bytes(room_id_no_sigil, "utf-8").hex()
versions = [str(i) for i in range(0, 10)]
versions.append("a")
if not any(ver in hexadecimal_room_id for ver in versions):
hexadecimal_room_id = "2" + hexadecimal_room_id[1:]
def remove_chars(s):
return re.sub(f"[^{''.join(versions)}]", "", s)
nums = remove_chars(hexadecimal_room_id)
def most_common_character(s):
s = s.replace(" ", "").lower()
counts = Counter(s)
most_common = counts.most_common(1)
actual_most_common = most_common[0] if most_common else ("4",)
if actual_most_common[0] == "0":
return ("10",)
elif actual_most_common[0] == "a":
return ("11",)
return actual_most_common
return str(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,
content=json,
)
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},
)
def strip_state(state_events) -> list:
if not isinstance(state_events, list):
return state_events
keys_to_remove = [
"auth_events",
"prev_events",
"signatures",
"hashes",
"depth"
]
new_list = []
for d in state_events:
if not isinstance(d, dict):
new_list.append(d)
continue
if "room_id" in d:
ver = int(room_version_from_id(d["room_id"]))
else:
ver = 4
event_id = make_ref_hash(d, ver)
new_dict = {}
for k, v in d.items():
if k in keys_to_remove:
continue
new_dict[k] = strip_state(v)
new_dict["event_id"] = event_id
new_list.append(new_dict)
return new_list
def get_time() -> int:
return int(str(time.time() * 1000).split(".")[0])

View File

@@ -1,6 +1,13 @@
from flask import Blueprint, jsonify, request import vona.config as config
from vona.config import server_name, the_funny_number import vona.globals as globals
import time import time
import os
from flask import (
Blueprint,
jsonify,
request,
)
identity = Blueprint("identity", __name__) identity = Blueprint("identity", __name__)
@@ -8,10 +15,8 @@ identity = Blueprint("identity", __name__)
# I'm pretty sure only Element uses this, # I'm pretty sure only Element uses this,
# but oh well. # but oh well.
# https://spec.matrix.org/latest/identity-service-api/#api-version-check
@identity.route("/_matrix/identity/versions") @identity.route("/_matrix/identity/versions")
async def versions(): async def versions():
# Stolen from the vector.im identity server
return jsonify({ return jsonify({
"versions": [ "versions": [
"r0.1.0", "r0.1.0",
@@ -27,10 +32,11 @@ async def versions():
}) })
# https://spec.matrix.org/latest/identity-service-api/#authentication
@identity.route("/_matrix/identity/v2/account") @identity.route("/_matrix/identity/v2/account")
async def account_info(): async def account_info():
return jsonify({"user_id": f"@vona:{server_name}"}) return jsonify({
"user_id": f"@vona:{config.server_name}"
})
@identity.route("/_matrix/identity/v2/account/logout", methods=["POST"]) @identity.route("/_matrix/identity/v2/account/logout", methods=["POST"])
@@ -40,113 +46,143 @@ async def logout():
@identity.route("/_matrix/identity/v2/account/register", methods=["POST"]) @identity.route("/_matrix/identity/v2/account/register", methods=["POST"])
async def register(): async def register():
return jsonify({"token":"vona"}) return jsonify({"token": "vona"})
# https://spec.matrix.org/latest/identity-service-api/#terms-of-service
@identity.route("/_matrix/identity/v2/terms", methods=["GET", "POST"]) @identity.route("/_matrix/identity/v2/terms", methods=["GET", "POST"])
async def policies(): async def policies():
if request.method == "GET": if request.method == "GET":
return jsonify({"policies":{}}) return jsonify({
"policies": {}
})
return jsonify({}) return jsonify({})
@identity.route("/_matrix/identity/v2") @identity.route("/_matrix/identity/v2")
async def status(): async def status():
return jsonify({}) return jsonify({})
@identity.route("/_matrix/identity/v2/pubkey/ephemeral/isvalid") @identity.route("/_matrix/identity/v2/pubkey/ephemeral/isvalid")
@identity.route("/_matrix/identity/v2/pubkey/isvalid") @identity.route("/_matrix/identity/v2/pubkey/isvalid")
async def pubkey_validity(): async def pubkey_validity():
return jsonify({"valid": True}) return jsonify({"valid": True})
@identity.route("/_matrix/identity/v2/pubkey/<key>") @identity.route("/_matrix/identity/v2/pubkey/<key>")
async def get_key(key): async def get_key(key):
return jsonify({ return jsonify({
"errcode": "M_NOT_FOUND", "public_key": globals.pubkey()
"error": "The public key was not found" })
}), 404
@identity.route("/_matrix/identity/v2/hash_details") @identity.route("/_matrix/identity/v2/hash_details")
async def hash_details(): async def hash_details():
return jsonify({"algorithms":["none","sha256"],"lookup_pepper": "vona"}) return jsonify({
"algorithms": [
"none",
"sha256",
],
"lookup_pepper": "vona"
})
@identity.route("/_matrix/identity/v2/lookup", methods=["POST"]) @identity.route("/_matrix/identity/v2/lookup", methods=["POST"])
async def lookup(): async def lookup():
req = request.json req = request.json
if "addresses" in req: if (
return jsonify({"mappings": {req["addresses"][0]: f"@vona:{server_name}"}}) isinstance(req, dict)
else: and "addresses" in req
return jsonify({"errcode": "M_INVALID_PEPPER","error": "Invalid pepper"}) and isinstance(req["addresses"], list)
and len(req["addresses"]) > 0
):
return jsonify({
"mappings": {
req["addresses"][0]: f"@vona:{config.server_name}"
}
})
return jsonify({
"errcode": "M_INVALID_PEPPER",
"error": "Invalid pepper"
})
@identity.route("/_matrix/identity/v2/validate/email/requestToken", methods=["POST"]) @identity.route("/_matrix/identity/v2/validate/email/requestToken", methods=["POST"])
@identity.route("/_matrix/identity/v2/validate/msisdn/requestToken", methods=["POST"]) @identity.route("/_matrix/identity/v2/validate/msisdn/requestToken", methods=["POST"])
async def request_validation_token(): async def request_validation_token():
return jsonify({"sid": str(the_funny_number)}) return jsonify({
"sid": os.urandom(16).hex()
})
@identity.route("/_matrix/identity/v2/validate/email/submitToken", methods=["GET", "POST"]) @identity.route("/_matrix/identity/v2/validate/email/submitToken", methods=["GET", "POST"])
@identity.route("/_matrix/identity/v2/validate/msisdn/submitToken", methods=["GET", "POST"]) @identity.route("/_matrix/identity/v2/validate/msisdn/submitToken", methods=["GET", "POST"])
async def submit_validation_token(): async def submit_validation_token():
return jsonify({"success": True}) return jsonify({"success": True})
@identity.route("/_matrix/identity/v2/3pid/bind", methods=["POST"]) @identity.route("/_matrix/identity/v2/3pid/bind", methods=["POST"])
async def threepid_bind(): async def threepid_bind():
if "mxid" in request.get_json(): if "mxid" in request.get_json():
mxid = request.get_json()["mxid"] mxid = request.get_json()["mxid"]
else: else:
mxid = f"@vona:{server_name}" mxid = f"@vona:{config.server_name}"
return jsonify(globals.sign_json({ return jsonify(
globals.sign_json({
"address": "abuse@matrix.org", "address": "abuse@matrix.org",
"medium": "email", "medium": "email",
"mxid": mxid, "mxid": mxid,
"not_after": int(time.time() * 1000 + 604800000), "not_after": int(time.time() * 1000 + 604800000),
"not_before": int(time.time() * 1000 - 604800000), "not_before": int(time.time() * 1000 - 604800000),
"ts": int(time.time() * 1000) "ts": int(time.time() * 1000)
})) })
)
@identity.route("/_matrix/identity/v2/3pid/unbind", methods=["POST"]) @identity.route("/_matrix/identity/v2/3pid/unbind", methods=["POST"])
async def threepid_unbind(): async def threepid_unbind():
return jsonify({}) return jsonify({})
@identity.route("/_matrix/identity/v2/3pid/getValidated3pid") @identity.route("/_matrix/identity/v2/3pid/getValidated3pid")
async def threepid_validated(): async def threepid_validated():
# Please email abuse@matrix.org # Please email abuse@matrix.org
return jsonify({ return jsonify({
"address": "abuse@matrix.org", "address": "abuse@matrix.org",
"medium": "email", "medium": "email",
"validated_at": the_funny_number "validated_at": config.the_funny_number
}) })
# https://spec.matrix.org/latest/identity-service-api/#invitation-storage
@identity.route("/_matrix/identity/v2/store-invite", methods=["POST"]) @identity.route("/_matrix/identity/v2/store-invite", methods=["POST"])
async def invite(): async def invite():
return jsonify({ return jsonify({
"display_name": "Vona", "display_name": "Vona",
"public_keys": [ "public_keys": [
{ {
"key_validity_url": f"https://{server_name}/_matrix/identity/v2/pubkey/isvalid", "key_validity_url": f"https://{config.server_name}/_matrix/identity/v2/pubkey/isvalid",
"public_key":"ohyeah" "public_key": "ohyeah"
}, },
{ {
"key_validity_url": f"https://{server_name}/_matrix/identity/v2/pubkey/ephemeral/isvalid", "key_validity_url": f"https://{config.server_name}/_matrix/identity/v2/pubkey/ephemeral/isvalid",
"public_key":"thisssssss" "public_key": "burgerkingfootlettuce"
} }
], ],
"token": "vona" "token": "vona"
}) })
# https://spec.matrix.org/latest/identity-service-api/#ephemeral-invitation-signing
@identity.route("/_matrix/identity/v2/sign-ed25519", methods=["POST"]) @identity.route("/_matrix/identity/v2/sign-ed25519", methods=["POST"])
async def invite_signing(): async def invite_signing():
required_keys = {"mxid", "private_key", "token"} required_keys = {"mxid", "private_key", "token"}
d = data.get_json() d = request.data.get_json()
if set(d.keys()) == required_keys: if set(d.keys()) == required_keys:
return jsonify(sign_json(d)) return jsonify(globals.sign_json(d))
else: else:
return jsonify({ return jsonify({
"errcode": "M_UNRECOGNIZED", "errcode": "M_UNRECOGNIZED",

View File

@@ -1,4 +1,8 @@
from flask import jsonify, Blueprint, request from flask import (
jsonify,
Blueprint,
request,
)
policy = Blueprint("policy", __name__) policy = Blueprint("policy", __name__)

12
vona/utils/__main__.py Normal file
View File

@@ -0,0 +1,12 @@
print("Available utils:")
a = [
"makekey - Generate a signing key",
"joinroom - Join a remote room",
"roomwithver - Brute-force a room with a specific room version",
]
for t in a:
print(f"\t{t}")
print("")

121
vona/utils/joinroom.py Normal file
View File

@@ -0,0 +1,121 @@
import vona.globals as globals
import vona.config as config
import urllib.parse
import time
import json
import httpx
http_client = globals.http_client()
versions = [
"1",
"2",
"3",
"4",
"5",
"6",
"7",
"8",
"9",
"10",
"11",
"12",
"org.matrix.msc3757.10",
"org.matrix.msc3757.11",
"org.matrix.hydra.11",
"org.matrix.msc3667",
"org.matrix.msc3787",
"org.matrix.msc4014",
]
verparams = "&".join(f"ver={ver}" for ver in versions)
def get_user_input(prompt):
try:
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
username = get_user_input("Username:\n\t")
room_id = get_user_input("Room ID:\n\t")
try:
server_name = input("\nServer name to join via:\n\t")
except Exception as e:
print(f"Error reading server names: {e}")
exit(1)
try:
print("\nSending make_join request..")
make_join_response = http_client.get(
path=f"/_matrix/federation/v1/make_join/{room_id}/%40{username}%3A{config.server_name}?{verparams}",
destination=server_name,
)
make_join_response.raise_for_status()
make_join = make_join_response.json()
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 response.")
exit(1)
except Exception as e:
print(f"An error occurred: {e}")
exit(1)
join_event = make_join.get("event", {})
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()
elif make_join.get("room_version", "1") == "org.matrix.msc4014":
# dendrite dislikes this :p
join_event["sender_key"] = globals.pubkey()
join_event["content"]["mxid_mapping"] = globals.sign_json({
"sender_key": globals.pubkey(),
"user_id": f"@{username}:{config.server_name}"
})
timestamp = input("\nTimestamp (leave blank for now):\n\t")
try:
join_event["origin_server_ts"] = int(timestamp)
except ValueError:
join_event["origin_server_ts"] = int(f"{time.time() * 1000}".split(".")[0])
signed_join = globals.hash_and_sign_event(
join_event,
int(make_join.get("room_version", "1"))
)
try:
send_join_response = http_client.put(
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()
print("\nSuccess :)")
except httpx.HTTPStatusError as e:
print(
f"HTTP error occurred during send_join: {e.response.status_code} - {e.response.text}"
)
except Exception as e:
print(f"An error occurred during send_join: {e}")

View File

@@ -1,9 +1,23 @@
# Generates a key in the format compatible with Synapse and Vona.
import base64 import base64
import os import os
key = base64.b64encode(os.urandom(32)).decode("utf-8")[:43].replace("/", "_") # Generates a key in the format compatible with Synapse and Vona.
key_id = base64.b64encode(os.urandom(32)).decode("utf-8")[:6].replace("/", "_")
def mkchar() -> str:
return os.urandom(4).hex()[:1]
def random(length):
return (
base64.b64encode(os.urandom(length))
.decode("utf-8")[:length]
.replace("/", mkchar())
.replace("+", mkchar())
)
key = random(43)
key_id = random(6)
print(f"ed25519 {key_id} {key}") print(f"ed25519 {key_id} {key}")

62
vona/utils/roomwithver.py Normal file
View File

@@ -0,0 +1,62 @@
import vona.globals as globals
import vona.config as config
import multiprocessing
versions = [str(i) for i in range(1, 10)]
try:
desired_ver = input("Desired room version:\n\t")
except (EOFError, KeyboardInterrupt):
print("")
exit(0)
known = {
"1": "qa",
"2": "br",
"3": "3",
"4": "D",
"5": "U",
"6": "f",
"7": "gx",
"8": "hx",
"9": "iy",
"10": "p0",
"11": "jZ",
}
if desired_ver in known:
print(f"!{known[desired_ver]}:{config.server_name} (from known room ID list)")
try:
input("Press enter to bruteforce anyway:\n\t")
except (EOFError, KeyboardInterrupt):
print("")
exit(0)
elif desired_ver not in versions:
print("Unsupported room version")
exit(1)
def worker(room_found):
try:
while not room_found.value:
room_id = globals.make_event_id().replace("$", "!")
room_ver = globals.room_version_from_id(room_id)
if room_ver == desired_ver:
print(room_id)
room_found.value = True
break
except Exception:
pass
room_found = multiprocessing.Value('b', False)
processes = []
for _ in range(multiprocessing.cpu_count()):
p = multiprocessing.Process(target=worker, args=(room_found,))
processes.append(p)
p.start()
for p in processes:
p.join()