This commit is contained in:
2025-09-05 15:08:49 -04:00
commit 4f17450992
14 changed files with 1428 additions and 0 deletions

3
.hgignore Normal file
View File

@@ -0,0 +1,3 @@
__pycache__
servers.json
src/config.py

11
LICENSE Normal file
View File

@@ -0,0 +1,11 @@
Velicense
Copyright (c) 2025 Kierre Sametti
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
1. The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
2. Any person or entity releasing a proprietary version of the Software must clearly state, in an easily accessible location, that they are a massive cuck. This statement does not require an explanation of any kind.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

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! 🌈🚀🎉

18
TODO.md Normal file
View File

@@ -0,0 +1,18 @@
- add the entirety of Synapse Admin (higher priority to lower priority)
- [X] [Users](https://element-hq.github.io/synapse/latest/admin_api/user_admin_api.html)
- [X] [Server Version](https://element-hq.github.io/synapse/latest/admin_api/version_api.html)
- [X] [Manipulate room membership](https://element-hq.github.io/synapse/latest/admin_api/room_membership.html)
- [X] [Account validity](https://element-hq.github.io/synapse/latest/admin_api/account_validity.html)
- [X] [Server notices](https://element-hq.github.io/synapse/latest/admin_api/server_notices.html)
- [X] [Experimental features](https://element-hq.github.io/synapse/latest/admin_api/experimental_features.html)
- [X] [Register users](https://element-hq.github.io/synapse/latest/admin_api/register_api.html)
- [X] [Purge history](https://element-hq.github.io/synapse/latest/admin_api/purge_history_api.html)
- [X] [Media](https://element-hq.github.io/synapse/latest/admin_api/media_admin_api.html)
- [X] [Statistics](https://element-hq.github.io/synapse/latest/admin_api/statistics.html)
- [X] [Background updates](https://element-hq.github.io/synapse/latest/usage/administration/admin_api/background_updates.html)
- [X] [Reported events](https://element-hq.github.io/synapse/latest/admin_api/event_reports.html)
- [ ] [Federation](https://element-hq.github.io/synapse/latest/usage/administration/admin_api/federation.html)
- [ ] [Registration tokens](https://element-hq.github.io/synapse/latest/usage/administration/admin_api/registration_tokens.html)
- [ ] [Rooms](https://element-hq.github.io/synapse/latest/admin_api/rooms.html)
- implement `/_matrix/client/v1/auth_metadata`

BIN
cat.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

71
src/appservice.py Normal file
View File

@@ -0,0 +1,71 @@
from flask import Blueprint, jsonify
from config import the_funny_number
import asyncio
apps = Blueprint("matrix_appservice", __name__)
# This implements both being a homeserver and
# being an appservice. Why? Maximum carnage.
# Endpoints invoked by the homeserver are put
# lower than ones invoked by the appservice.
# This code is untested! Please test with
# another homeserver and appservice, thanks!
# https://spec.matrix.org/v1.15/application-service-api/#pinging
# https://spec.matrix.org/v1.15/application-service-api/#pinging-1
@apps.route("/_matrix/client/v1/appservice/<app>/ping", methods=["POST"])
async def homeserver_ping(app):
# Sleeping here makes it more realistic
await asyncio.sleep(the_funny_number / 1000)
return jsonify({"duration_ms": the_funny_number})
@apps.route("/_matrix/app/v1/ping", methods=["POST"])
async def app_ping():
return jsonify({})
# https://spec.matrix.org/v1.15/application-service-api/#querying
@apps.route('/_matrix/app/v1/users/<user>')
async def query_user(user):
return jsonify({})
@apps.route('/_matrix/app/v1/rooms/<room>')
async def query_room(room):
return jsonify({})
# https://spec.matrix.org/v1.15/application-service-api/#pushing-events
@apps.route('/_matrix/app/v1/transactions/<txnId>', methods=['PUT'])
async def send_transaction(txnId):
return jsonify({})
# https://spec.matrix.org/v1.15/application-service-api/#third-party-networks
@apps.route('/_matrix/app/v1/thirdparty/location')
async def location():
return jsonify([])
@apps.route('/_matrix/app/v1/thirdparty/location/<protocol>')
async def location_from_protocol(protocol):
return jsonify([])
@apps.route('/_matrix/app/v1/thirdparty/protocol/<protocol>')
async def protocol_info(protocol):
return jsonify({})
@apps.route('/_matrix/app/v1/thirdparty/user')
async def thirdparty_user():
return jsonify([])
@apps.route('/_matrix/app/v1/thirdparty/user/<protocol>')
async def user_from_protocol(protocol):
return jsonify([])
# https://spec.matrix.org/v1.15/application-service-api/#published-room-directories
@apps.route('/_matrix/client/v3/directory/list/appservice/<net>/<room>', methods=['PUT'])
async def publish_room(net, room):
return jsonify({})

410
src/c2s.py Normal file
View File

@@ -0,0 +1,410 @@
from config import server_name, users_can_register, room_dir_room, cat, the_funny_number
from flask import Blueprint, jsonify, request, send_file
import asyncio
import random
import os
client = Blueprint('matrix_client', __name__)
@client.route('/_matrix/client/versions')
def client_version():
return jsonify({"versions":["r0.0.1","r0.1.0","r0.2.0","r0.3.0","r0.4.0","r0.5.0","r0.6.0","r0.6.1","v1.1","v1.2","v1.3","v1.4","v1.5","v1.6","v1.7","v1.8","v1.9","v1.10","v1.11"],"unstable_features":{"org.matrix.label_based_filtering":True,"org.matrix.e2e_cross_signing":True,"org.matrix.msc2432":True,"uk.half-shot.msc2666.query_mutual_rooms":True,"io.element.e2ee_forced.public":False,"io.element.e2ee_forced.private":False,"io.element.e2ee_forced.trusted_private":False,"org.matrix.msc3026.busy_presence":False,"org.matrix.msc2285.stable":True,"org.matrix.msc3827.stable":True,"org.matrix.msc3440.stable":True,"org.matrix.msc3771":True,"org.matrix.msc3773":False,"fi.mau.msc2815":True,"fi.mau.msc2659.stable":True,"org.matrix.msc3882":False,"org.matrix.msc3881":False,"org.matrix.msc3874":False,"org.matrix.msc3912":False,"org.matrix.msc3981":True,"org.matrix.msc3391":False,"org.matrix.msc4069":False,"org.matrix.msc4028":False,"org.matrix.msc4108":False,"org.matrix.msc4140":False,"org.matrix.simplified_msc3575":True,"uk.tcpip.msc4133":True}})
@client.route('/_matrix/client/v3/admin/whois/<userId>')
def admin_whois(userId):
if userId.startswith('@'):
return jsonify({"errcode":"M_MISSING_TOKEN","error":"Missing access token"})
else:
return jsonify({"errcode":"M_INVALID_PARAM","error":"Expected UserID string to start with '@'"})
@client.route('/_matrix/client/v3/rooms/<roomId>/members')
def room_member_count(roomId):
return jsonify({"errcode":"M_MISSING_TOKEN","error":"M_MISSING_TOKEN: Missing access token."})
@client.route('/_matrix/client/v3/account/whoami')
def whoami():
return jsonify({
"device_id": "VVOONNAA",
"user_id": f"@vona:{server_name}"
})
@client.route('/_matrix/client/v3/register', methods=['POST'])
def register():
if users_can_register:
data = request.get_json()
if data and 'auth' in data:
return jsonify({
"user_id": f"@vona:{server_name}",
"home_server": f"{server_name}",
"access_token": "vona",
"device_id": "VVOONNAA"
})
else:
return jsonify({
"session": os.urandom(32).hex(),
"flows": [{"stages": ["m.login.dummy"]}],
"params": {}
}), 401
else:
return jsonify({"errcode": "M_FORBIDDEN", "error": "M_FORBIDDEN: Registration has been disabled."}), 403
@client.route('/_matrix/client/v3/login', methods=['GET', 'POST'])
def login():
if request.method == 'GET':
return jsonify({"flows":[{"type":"m.login.password"},{"type":"m.login.application_service"},{"type":"m.login.token","get_login_token":True}]})
elif request.method == 'POST':
return jsonify({"access_token": "vona","device_id": "VVOONNAA","user_id": f"@vona:{server_name}"})
@client.route('/_matrix/client/v3/account/password/email/requestToken', methods=['POST'])
def pswd_reset():
return jsonify({"errcode":"M_THREEPID_NOT_FOUND","error":"Email not found"}), 400
@client.route('/_matrix/client/v3/keys/upload', methods=['POST'])
def key_upload():
return jsonify({"one_time_key_counts":{"signed_curve25519":50}})
@client.route('/_matrix/client/v3/room_keys/version', methods=['POST', 'GET'])
def roomkeys():
if request.method == 'POST':
return jsonify({
"algorithm": "m.megolm_backup.v1.curve25519-aes-sha2",
"auth_data":
{
"public_key":"vona",
"signatures": {
f"@vona:{server_name}": {
"ed25519:deviceid": "vona"
}
}
},
"count": the_funny_number,
"etag": "vona",
"version":str(the_funny_number)
})
return jsonify({})
@client.route('/_matrix/client/v3/capabilities')
def capabilities():
return jsonify({"capabilities":{"m.room_versions":{"default":"1337","available":{"1":"stable","2":"stable","3":"stable","4":"stable","5":"stable","6":"stable","7":"stable","8":"stable","1337":"stable","9":"stable","10":"stable","11":"stable","org.matrix.msc3757.10":"stable","org.matrix.msc3757.11":"stable"},"org.matrix.msc3244.room_capabilities":{"knock":{"preferred":"7","support":["7","8","9","10","11","org.matrix.msc3757.10","org.matrix.msc3757.11"]},"restricted":{"preferred":"9","support":["8","9","10","11","org.matrix.msc3757.10","org.matrix.msc3757.11"]}}},"m.change_password":{"enabled":True},"m.3pid_changes":{"enabled":True},"m.get_login_token":{"enabled":False},"m.profile_fields":{"enabled":True,"allowed":["na.vo.hafjagger"],"disallowed":["org.example.secret_field"]}}})
@client.route('/_matrix/client/v3/pushrules/')
def pushrules():
# TODO: Actually implement this
return jsonify({"errcode":"M_MISSING_TOKEN","error":"Missing access token"})
@client.route('/_matrix/client/v3/user/<user>/filter', methods=['POST'])
def filter(user):
return jsonify({"filter_id": "vvvooonnnaaa"})
@client.route('/_matrix/client/v3/user/<user>/filter/<data>')
def filter_two(user, data):
return jsonify({"filter_id": "vvvooonnnaaa"})
@client.route('/_matrix/client/v3/join/<room>', methods=['POST'])
def join_room(room):
# TODO: Actually implement this
return jsonify({"errcode":"M_FORBIDDEN","error":"You are not invited to this room."}), 403
@client.route('/_matrix/client/v3/rooms/<room>/invite', methods=['POST'])
def invite_room(room):
return jsonify({})
@client.route('/_matrix/client/v3/knock/<room>', methods=['POST'])
def knock_room(room):
return jsonify({"errcode":"M_FORBIDDEN","error":"You are not allowed to knock on this room."}), 403
@client.route('/_matrix/client/v3/rooms/<room>/join', methods=['POST'])
def join_roomId(room):
return jsonify({"errcode":"M_FORBIDDEN","error":"You are not invited to this room."}), 403
@client.route('/_matrix/client/v3/sync')
async def sync():
wait_time = 0
if 'timeout' in request.args:
if 'timeout' in request.args:
try:
wait_time = int(request.args.get('timeout')) / 1000
except:
pass
await asyncio.sleep(wait_time)
return jsonify({
"next_batch": f"{os.urandom(64).hex()}",
"presence": {},
"device_one_time_keys_count": {"signed_curve25519": 50},
"org.matrix.msc2732.device_unused_fallback_key_types": ["signed_curve25519"],
"device_unused_fallback_key_types": ["signed_curve25519"],
"rooms": {
"join": {
f"!{os.urandom(64).hex()}:{server_name}": {
"timeline": {
"events": [],
"prev_batch": f"{random.randint(32095,309390)}",
"limited": False
},
"state": {"events": []},
"account_data": {"events": []},
"ephemeral": {"events": []},
"unread_notifications": {
"notification_count": 0,
"highlight_count": 0
},
"summary": {}
}
}
}
})
@client.route('/_matrix/client/v3/rooms/<room>/send/<eventType>/<txnId>', methods=['POST', 'PUT'])
def send_message(room, eventType, txnId):
return jsonify({"event_id": f"${os.urandom(16).hex()}:{server_name}"}), 200
@client.route('/_matrix/client/v3/user_directory/search', methods=['POST'])
def user_directory():
return jsonify({"limited":False,"results":[{"avatar_url":f"mxc://{server_name}/cat","display_name":"Vona","user_id":f"@vona:{server_name}"}]})
@client.route('/_matrix/client/v3/devices')
def devices():
return jsonify({"devices":[{"device_id":"VVOONNAA","display_name":"Vona","last_seen_ip":"127.0.0.1","last_seen_ts":the_funny_number}]})
@client.route('/_matrix/client/v3/devices/<deviceId>', methods=['GET', 'PUT', 'DELETE'])
def get_device():
if request.method == 'GET':
return jsonify({"device_id": "VVOONNAA","display_name": "Vona","last_seen_ip": "127.0.0.1","last_seen_ts": the_funny_number})
elif request.method == 'DELETE':
return jsonify({})
elif request.method == 'PUT':
return jsonify({})
@client.route('/_matrix/client/v3/delete_devices', methods=['POST'])
def delete_devices():
return jsonify({})
@client.route('/_matrix/client/v3/logout', methods=['POST'])
def logout():
return jsonify({})
@client.route('/_matrix/client/v3/logout/all', methods=['POST'])
def logout_all():
return jsonify({})
@client.route('/_matrix/client/v3/refresh', methods=['POST'])
def refresh():
return jsonify({"errcode": "M_UNKNOWN_TOKEN","error": "Soft logged out","soft_logout": True})
@client.route('/_matrix/client/v3/voip/turnServer')
def turnserver():
return jsonify({"errcode": "M_LIMIT_EXCEEDED","error": "Too many requests","retry_after_ms": 99999999999999999999999999999999999999999999999999999999999999999999999999999999999}), 429
@client.route('/_matrix/client/unstable/im.nheko.summary/rooms/<roomId>/summary')
def nheko_room_summary(roomId):
room = room_dir_room['chunk'][0]
return jsonify(
{
"room_id": room['room_id'],
"avatar_url": room['avatar_url'],
"guest_can_join": room['guest_can_join'],
"name": room['name'],
"num_joined_members": room['num_joined_members'],
"topic": room['topic'],
"world_readable": room['world_readable'],
"join_rule": room['join_rule'],
"room_type": room['room_type'],
"membership": "join",
"room_version": str(the_funny_number),
}
)
@client.route('/_matrix/client/v1/room_summary/<roomId>')
def msc_room_summary(roomId):
room = room_dir_room['chunk'][0]
return jsonify(
{
"room_id": room['room_id'],
"avatar_url": room['avatar_url'],
"guest_can_join": room['guest_can_join'],
"name": room['name'],
"num_joined_members": room['num_joined_members'],
"topic": room['topic'],
"world_readable": room['world_readable'],
"join_rule": room['join_rule'],
"room_type": room['room_type'],
"membership": "join",
"room_version": str(the_funny_number),
}
)
@client.route('/_matrix/client/r0/directory/room/<roomId>')
def r0_room_query(roomId):
return jsonify(
{
"room_id": room_dir_room['chunk'][0]['room_id'],
"servers": [
"matrix.org",
"envs.net",
"4d2.org"
]
}
)
@client.route('/_matrix/client/r0/directory/list/room/<roomId>')
def r0_room_vis(roomId):
return jsonify({"visibility": "public"})
@client.route('/_matrix/client/r0/publicRooms')
def r0_room_directory():
return jsonify(room_dir_room)
@client.route('/_matrix/media/r0/thumbnail/<server>/<file>')
def r0_media_thumbnail(server, file):
return send_file(cat)
@client.route('/_matrix/media/v1/thumbnail/<server>/<file>')
def v1_media_thumbnail(server, file):
return send_file(cat)
@client.route('/_matrix/client/v1/media/thumbnail/<s>/<f>')
def media_thumbnail(s, f):
return send_file(cat)
@client.route('/_matrix/client/r0/admin/whois/<userId>')
def whois(userId):
return jsonify({"user_id":f"@vona:{server_name}","devices":{"":{"sessions":[{"connections":[{"ip":f"{the_funny_number}.{the_funny_number}.{the_funny_number}.{the_funny_number}","last_seen":the_funny_number,"user_agent":f"Vona/{the_funny_number}.0"},{"ip":f"{the_funny_number}.{the_funny_number}.{the_funny_number}.{the_funny_number}","last_seen":the_funny_number,"user_agent":f"Vona/{the_funny_number}.0"}]}]}}})
@client.route('/_matrix/client/r0/register/available')
def r0_username_available():
return jsonify({"available": True})
@client.route('/_matrix/client/v3/register/available')
def v3_username_available():
return jsonify({"available": True})
@client.route('/_matrix/client/v3/thirdparty/protocols')
def thirdparty_protocols():
return jsonify({})
@client.route('/_matrix/media/v3/preview_url')
def url_preview():
return jsonify({
"matrix:image:size": 102400,
"og:description": "look at this cool cat",
"og:image": f"mxc://{server_name}/ascERGshawAWawugaAcauga",
"og:image:height": 48,
"og:image:type": "image/jpg",
"og:image:width": 48,
"og:title": "cool cat"
})
@client.route('/_matrix/client/v1/media/preview_url')
def media_preview():
response = send_file(cat)
response.headers['Content-Disposition'] = f'inline; filename="cat.jpg"'
response.headers['Content-Type'] = 'image/jpg'
return response
@client.route('/_matrix/media/v1/create', methods=['POST'])
def create_mxc():
return jsonify({"content_uri": f"mxc://vona.squarebowl.club/{os.urandom(10).hex()}"})
@client.route('/_matrix/media/v3/upload', methods=['POST'])
def upload_media():
return jsonify({"content_uri": f"mxc://vona.squarebowl.club/{os.urandom(10).hex()}"})
@client.route('/_matrix/media/v3/upload/<server>/<media>', methods=['PUT'])
def upload_media_to_mxc(server, media):
return jsonify({})
@client.route('/_matrix/media/v3/config')
def media_config():
return jsonify({"m.upload.size": the_funny_number})
@client.route('/_matrix/media/v3/download/<server>/<media>')
def v3_download_media(server, media):
return send_file(cat)
@client.route('/_matrix/media/v3/download/<server>/<media>/<file>')
def v3_download_file(server, media, file):
return send_file(cat)
@client.route('/_matrix/media/v3/thumbnail/<server>/<media>')
def v3_thumbnail_media(server, media):
return send_file(cat)
@client.route('/_matrix/client/v3/sendToDevice/<event>/<txnId>', methods=['PUT'])
def send_to_device(event, txnId):
return jsonify({})
@client.route('/_matrix/client/v3/profile/<userId>/<key>', methods=['GET', 'PUT', 'DELETE'])
def user_profile_keys(userId, key):
if request.method == 'PUT' or request.method == 'DELETE':
return jsonify({})
elif request.method == 'GET':
if key == 'avatar_url':
return jsonify({"avatar_url":f"mxc://{server_name}/cat"})
elif key == 'displayname':
return jsonify({"displayname":"Vona"})
else:
return jsonify({"errcode": "M_NOT_FOUND","error": "The requested profile key does not exist."})
@client.route('/_matrix/client/v3/profile/<userId>')
def user_profile(userId):
return jsonify({"avatar_url":f"mxc://{server_name}/cat","displayname":"Vona"})
@client.route('/_matrix/client/v3/rooms/<roomId>/messages')
def room_messages(roomId):
return jsonify({
"chunk": [
{
"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": f"{os.urandom(16).hex()}:{server_name}",
"origin_server_ts": the_funny_number,
"room_id": roomId,
"sender": f"@vona:{server_name}",
"type": "m.room.message",
"unsigned": {}
},
{
"content": {
"name": "Vona"
},
"event_id": f"{os.urandom(16).hex()}:{server_name}",
"origin_server_ts": the_funny_number,
"room_id": roomId,
"sender": f"@vona:{server_name}",
"state_key": "",
"type": "m.room.name",
"unsigned": {}
}
],
"end": f"{os.urandom(16).hex()}",
"start": f"{os.urandom(16).hex()}"
})
@client.route('/_matrix/client/v3/rooms/<roomId>/leave', methods=['POST'])
def leave_room(roomId):
return jsonify({})
@client.route('/_matrix/client/v3/rooms/<roomId>/read_markers', methods=['POST'])
def read_markers(roomId):
return jsonify({})
@client.route('/_matrix/client/v3/user/<user>/account_data/<type>', methods=['PUT'])
async def set_custom_account_data(user, type):
return jsonify({})
@client.route('/_matrix/client/v3/keys/device_signing/upload', methods=['POST'])
async def upload_device_signing_keys():
return jsonify({})
@client.route('/_matrix/client/v3/keys/query', methods=['POST'])
async def query_keys():
# Should be replaced with
# something proper eventually.
return jsonify({})

57
src/config-example.py Normal file
View File

@@ -0,0 +1,57 @@
# Cat picture to use. Must be a JPEG (most camera photos are JPEG already anyway).
cat = '../cat.jpg'
# Your server name.
server_name = "example.org"
# Room directory room/space.
room_dir_room = {
"chunk": [
{
"avatar_url": f"mxc://{server_name}/T2RoTUs3QyZUeT4kOC9NVzs4b",
"guest_can_join": False,
"join_rule": "public",
"name": "A [matrix] space",
"num_joined_members": 43502,
"room_id": f"!QDckPFoubl1NS2leKkpk:{server_name}",
"room_type": "m.space",
"topic": "A [matrix] space topic",
"world_readable": False
}
],
"total_room_count_estimate": 43502
}
# Where users should reach out for support.
support = {
"contacts": [
{
"email_address": "admin@i-didnt-change-my-vona-config.invalid",
"matrix_id": "@admin:i-didnt-change-my-vona-config.invalid",
"role": "m.role.admin"
},
{
"email_address": "admin@i-didnt-change-my-vona-config.invalid",
"role": "m.role.security"
}
]
}
# The port to listen on.
port = 5000
# The address to listen on.
addr = '127.0.0.1'
# Whether or not to enable registration.
users_can_register = False
# The funny number.
the_funny_number = 1337
# Your private key for Vona.
#
# To generate a private key:
# openssl genpkey -algorithm Ed25519 -out privkey.pem
#
ed25519_key_path = "/etc/vonakey.pem"

310
src/custom.py Normal file
View File

@@ -0,0 +1,310 @@
from config import server_name, the_funny_number, room_dir_room
from flask import Blueprint, jsonify, request, Response
from globals import vona_version, make_event_id
import base64
import re
import os
custom = Blueprint('matrix_custom', __name__)
# This implements custom endpoints
# used by other homeserver
# implementations. They do not start
# with /_matrix/
# This should be split into more
# files eventually.
# Synapse
@custom.route('/_synapse/admin/v1/server_version')
def synapse_version():
return jsonify({'server_version': vona_version})
@custom.route('/_synapse/admin/v2/users')
def synapse_user_list():
return jsonify({
"users": [
{
"name": f"@vona:{server_name}",
"is_guest": 0,
"admin": 0,
"user_type": "vona",
"deactivated": 0,
"erased": False,
"shadow_banned": 0,
"displayname": "Vona",
"avatar_url": f"mxc://{server_name}/cat",
"creation_ts": the_funny_number,
"locked": False
}
],
"total": 1
})
@custom.route('/_synapse/admin/v2/users/<user_id>', methods=['GET', 'PUT'])
def synapse_user_info(user_id):
if request.method == 'GET':
return jsonify({"name":f"@vona:{server_name}","displayname":"Vona","threepids":[],"avatar_url":f"mxc://{server_name}/cat","is_guest":0,"admin":0,"deactivated":0,"erased":False,"shadow_banned":0,"creation_ts":the_funny_number,"last_seen_ts":the_funny_number,"appservice_id":the_funny_number,"consent_server_notice_sent":the_funny_number,"consent_version":the_funny_number,"consent_ts":the_funny_number,"external_ids":[],"user_type":"vona","locked":False,"suspended":False})
elif request.method == 'PUT':
return jsonify({}), 201
@custom.route('/_synapse/admin/v1/whois/<user_id>')
def synapse_whois(user_id):
return jsonify({"user_id":f"@vona:{server_name}","devices":{"":{"sessions":[{"connections":[{"ip":f"{the_funny_number}.{the_funny_number}.{the_funny_number}.{the_funny_number}","last_seen":the_funny_number,"user_agent":f"Vona/{vona_version}"},{"ip":f"{the_funny_number}.{the_funny_number}.{the_funny_number}.{the_funny_number}","last_seen":the_funny_number,"user_agent":f"Vona/{vona_version}"}]}]}}})
@custom.route('/_synapse/admin/v1/deactivate/<user_id>', methods=['POST'])
def synapse_deactivate(user_id):
return jsonify({})
@custom.route('/_synapse/admin/v1/suspend/<user_id>', methods=['PUT'])
def synapse_suspend(user_id):
return jsonify({})
@custom.route('/_synapse/admin/v1/reset_password/<user_id>', methods=['POST'])
def synapse_reset_pswd(user_id):
return jsonify({})
@custom.route('/_synapse/admin/v1/users/<user_id>/admin', methods=['PUT'])
def synapse_change_admin(user_id):
return jsonify({})
@custom.route('/_synapse/admin/v1/users/<user_id>/joined_rooms')
def synapse_user_joined_rooms(user_id):
return jsonify({"joined_rooms":[room_dir_room['chunk'][0]['room_id']],"total":1})
@custom.route('/_synapse/admin/v1/users/<user_id>/sent_invite_count')
def synapse_invite_count(user_id):
return jsonify({"invite_count": the_funny_number})
@custom.route('/_synapse/admin/v1/users/<user_id>/accountdata')
def synapse_account_data(user_id):
return jsonify({"account_data":{"global":{}}})
@custom.route('/_synapse/admin/v1/users/<user_id>/media', methods=['GET', 'DELETE'])
def synapse_account_media(user_id):
if request.method == 'GET':
return jsonify({"media": [{"created_ts":the_funny_number,"last_access_ts":the_funny_number,"media_id":"cat","media_length":the_funny_number,"media_type":"image/jpeg","quarantined_by":"null","safe_from_quarantine":False,"upload_name":"cat.jpg"}], "total": the_funny_number})
elif request.method == 'DELETE':
return jsonify({"deleted_media": ["cat"], "total": the_funny_number})
@custom.route('/_synapse/admin/v1/users/<user_id>/login', methods=['POST'])
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'])
def synapse_stupid_mas_bullshit(user_id):
return jsonify({"updatable_without_uia_before_ms": the_funny_number})
@custom.route('/_synapse/admin/v2/users/<user_id>/devices', methods=['GET', 'POST'])
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":the_funny_number, "last_seen_user_agent": f"Vona/{vona_version}"}], "total": the_funny_number})
elif request.method == 'POST':
return jsonify({})
@custom.route('/_synapse/admin/v2/users/<user_id>/delete_devices', methods=['POST'])
def synapse_delete_devices(user_id):
return jsonify({})
@custom.route('/_synapse/admin/v2/users/<user_id>/devices/<device_id>')
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":the_funny_number, "last_seen_user_agent": f"Vona/{vona_version}"})
else:
return jsonify({})
@custom.route('/_synapse/admin/v1/users/<user_id>/pushers')
def synapse_pushers(user_id):
return jsonify({"pushers": [], "total": the_funny_number})
@custom.route('/_synapse/admin/v1/users/<user_id>/shadow_ban', methods=['DELETE', 'POST'])
def synapse_shadow_ban(user_id):
return jsonify({})
@custom.route('/_synapse/admin/v1/users/<user_id>/override_ratelimit', methods=['GET', 'POST', 'DELETE'])
def synapse_override_ratelimit(user_id):
return jsonify({})
@custom.route('/_synapse/admin/v1/username_available')
def synapse_username_available():
return jsonify({"available": True})
@custom.route('/_synapse/admin/v1/auth_providers/<provider>/users/<ext>')
def synapse_auth_providers(provider, ext):
return jsonify({"user_id": f"@vona:{server_name}"})
@custom.route('/_synapse/admin/v1/threepid/<medium>/users/<addr>')
def synapse_threepid(medium, addr):
return jsonify({"user_id": f"@vona:{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>')
def synapse_redact_status(redact_id):
return jsonify({"status":"active","failed_redactions":[]})
@custom.route('/_synapse/admin/v1/experimental_features/<user_id>', methods=['GET', 'PUT'])
def synapse_experimental_features(user_id):
return jsonify({"features": {}})
@custom.route('/_synapse/admin/v1/register', methods=['GET', 'POST'])
def synapse_register():
if request.method == 'GET':
return jsonify({"nonce": os.urandom(16).hex()})
elif request.method == 'POST':
return jsonify({"access_token": f"@vona:{server_name}"})
@custom.route('/_synapse/admin/v1/join/<roomId>', methods=['POST'])
def synapse_membership_manipulation(roomId):
return jsonify({"room_id": room_dir_room['chunk'][0]['room_id']})
@custom.route('/_synapse/admin/v1/account_validity/validity', methods=['POST'])
def synapse_account_validity():
return jsonify({"expiration_ts": the_funny_number})
@custom.route('/_synapse/admin/v1/send_server_notice', methods=['POST'])
def synapse_server_notice():
return jsonify({"event_id": make_event_id()})
@custom.route('/_synapse/admin/v1/send_server_notice/<txnId>', methods=['PUT'])
def synapse_server_notice_second(txnId):
return jsonify({"event_id": make_event_id()})
@custom.route('/_synapse/admin/v1/purge_history/<room_id>', methods=['POST'])
def synapse_purge_room_history(room_id):
return jsonify({"purge_id": os.urandom(16).hex()})
@custom.route('/_synapse/admin/v1/purge_history/<room_id>/<event_id>', methods=['POST'])
def synapse_purge_event(room_id, event_id):
return jsonify({"purge_id": os.urandom(16).hex()})
@custom.route('/_synapse/admin/v1/purge_history_status/<purge_id>')
def synapse_purge_status(purge_id):
return jsonify({"status":"active"})
@custom.route('/_synapse/admin/v1/room/<room_id>/media')
def synapse_room_media(room_id):
return jsonify({"local": [f"mxc://{server_name}/cat"], "remote": []})
@custom.route('/_synapse/admin/v1/media/quarantine/<server_name>/<media_id>', methods=['POST'])
def synapse_quarantine_media(server_name, media_id):
return jsonify({})
@custom.route('/_synapse/admin/v1/media/unquarantine/<server_name>/<media_id>', methods=['POST'])
def synapse_unquarantine_media(server_name, media_id):
return jsonify({})
@custom.route('/_synapse/admin/v1/room/<room_id>/media/quarantine', methods=['POST'])
def synapse_quarantine_room_media(room_id):
return jsonify({"num_quarantined": the_funny_number})
@custom.route('/_synapse/admin/v1/media/protect/<media_id>', methods=['POST'])
def synapse_protect_media(media_id):
return jsonify({})
@custom.route('/_synapse/admin/v1/media/unprotect/<media_id>', methods=['POST'])
def synapse_unprotect_media(media_id):
return jsonify({})
@custom.route('/_synapse/admin/v1/media/<server_name>/<media_id>', methods=['DELETE'])
def synapse_delete_single_media(media_id):
return jsonify({"deleted_media": ["cat"], "total": the_funny_number})
@custom.route('/_synapse/admin/v1/media/delete', methods=['POST'])
def synapse_delete_media():
return jsonify({"deleted_media": ["cat"], "total": the_funny_number})
@custom.route('/_synapse/admin/v1/media/<server_name>/delete', methods=['POST'])
def synapse_delete_media_from_server(server_name):
return jsonify({"deleted_media": ["cat"], "total": the_funny_number})
@custom.route('/_synapse/admin/v1/purge_media_cache', methods=['POST'])
def synapse_delete_remote_media():
return jsonify({"deleted": the_funny_number})
@custom.route('/_synapse/admin/v1/statistics/users/media')
def synapse_media_stats():
return jsonify({"users":[{"displayname":"Vona","media_count":the_funny_number,"media_length":the_funny_number,"user_id":f"@vona:{server_name}"}],"total":the_funny_number})
@custom.route('/_synapse/admin/v1/statistics/database/rooms')
def synapse_room_stats():
return jsonify({"rooms": [{"room_id":room_dir_room['chunk'][0]['room_id'],"estimated_size":the_funny_number}]})
@custom.route('/_synapse/admin/v1/background_updates/status')
def synapse_background_upate_status():
return jsonify({"enabled":False})
@custom.route('/_synapse/admin/v1/background_updates/enabled', methods=['POST', 'GET'])
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'])
def synapse_bg_update_start_job():
return jsonify({})
@custom.route('/_synapse/admin/v1/event_reports')
def synapse_event_reports():
return jsonify({"event_reports": [{"event_id": f"${make_event_id()}","id": the_funny_number,"reason": "vona","score": the_funny_number,"received_ts": the_funny_number,"room_id": room_dir_room['chunk'][0]['room_id'],"name": room_dir_room['chunk'][0]['name'],"sender": f"@vona:{server_name}","user_id": f"@vona:{server_name}"}],"total": the_funny_number})
@custom.route('/_synapse/admin/v1/event_reports/<report_id>', methods=['GET', 'DELETE'])
def synapse_interact_with_reported_event(report_id):
if request.method == 'GET':
return jsonify({"event_id": f"${make_event_id()}","event_json": {"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": the_funny_number,"hashes": {"sha256": f"${make_event_id()}"},"origin": server_name,"origin_server_ts": the_funny_number,"prev_events": [f"${make_event_id()}"],"prev_state": [],"room_id": room_dir_room['chunk'][0]['room_id'],"sender": f"@vona:{server_name}","signatures": {server_name: {"ed25519:a_JaEG": base64.b64encode(os.urandom(32)).decode('utf-8')[:87]}}},"type": "m.room.message","unsigned": {"age_ts": the_funny_number},"id": the_funny_number,"reason": "vona","score": the_funny_number,"received_ts": the_funny_number,"room_id": room_dir_room['chunk'][0]['room_id'],"name": room_dir_room['chunk'][0]['name'],"sender": f"@vona:{server_name}","user_id": f"@vona:{server_name}"})
else:
return jsonify({})
# Dendrite - https://element-hq.github.io/dendrite/administration/adminapi
@custom.route('/_dendrite/admin/evacuateUser/<userId>', methods=['POST'])
def dendrite_evacuate_user(userId):
return jsonify({"affected":[room_dir_room['chunk'][0]['room_id']]})
@custom.route('/_dendrite/admin/evacuateRoom/<roomId>', methods=['POST'])
def dendrite_evacuate_room(roomId):
return jsonify({"affected":[f"@vona:{server_name}"]})
@custom.route('/_dendrite/admin/resetPassword/<userId>', methods=['POST'])
def dendrite_reset_pswd(userId):
return jsonify({"password_updated":true})
@custom.route('/_dendrite/admin/purgeRoom/<roomId>', methods=['POST'])
def dendrite_purge_room(roomId):
return jsonify({})
@custom.route('/_dendrite/admin/refreshDevices/<userId>', methods=['POST'])
def dendrite_refresh_devices(userId):
return jsonify({})
@custom.route('/_dendrite/admin/fulltext/reindex')
def dendrite_reindex():
return jsonify({})
# Conduwuit
@custom.route('/_conduwuit/local_user_count')
def conduwuit_user_count():
return jsonify({"count": the_funny_number})
@custom.route('/_conduwuit/server_version')
def conduwuit_server_version():
return jsonify({"name":"Vona","version":vona_version})
# Continuwuity
@custom.route('/_continuwuity/local_user_count')
def continuwuity_user_count():
return jsonify({"count": the_funny_number})
@custom.route('/_continuwuity/server_version')
def continuwuity_server_version():
return jsonify({"name":"Vona","version":vona_version})
# Tuwunel
@custom.route('/_tuwunel/local_user_count')
def tuwunel_user_count():
return jsonify({"count": the_funny_number})
@custom.route('/_tuwunel/server_version')
def tuwunel_server_version():
return jsonify({"name":"Vona","version":vona_version})

50
src/globals.py Normal file
View File

@@ -0,0 +1,50 @@
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization
import hashlib
import base64
import json
import re
import os
def canonical_json(value):
return json.dumps(
value,
ensure_ascii=False,
separators=(',',':'),
sort_keys=True
).encode("UTF-8")
'''
def encode_base64(data: bytes) -> str:
return base64.b64encode(data).decode('utf-8')
def sign_json(json_object, signing_key, signing_name):
signatures = json_object.pop("signatures", {})
unsigned = json_object.pop("unsigned", None)
signed = signing_key.sign(canonical_json(json_object))
signature_base64 = encode_base64(signed)
key_id = "ed25519:VonaA"
signatures.setdefault(signing_name, {})[key_id] = signature_base64
json_object["signatures"] = signatures
if unsigned is not None:
json_object["unsigned"] = unsigned
return json_object
'''
vona_version = '1.2.3'
def make_event_id():
return re.sub(r'[\/+=]', '_', base64.b64encode(os.urandom(32)).decode('utf-8'))[:44]
def hash_event(input) -> str:
input.pop('signatures', None)
input.pop('unsigned', None)
sha256_hash = hashlib.sha256(canonical_json(input)).digest()
base64_encoded = base64.b64encode(sha256_hash)
return base64_encoded.decode().rstrip('=')

122
src/identity.py Normal file
View File

@@ -0,0 +1,122 @@
from flask import Blueprint, jsonify, request
from config import server_name, the_funny_number
identity = Blueprint("matrix_identity", __name__)
# This implements being an identity server.
# I'm pretty sure only Element uses this,
# but oh well.
# https://spec.matrix.org/v1.15/identity-service-api/#api-version-check
@identity.route("/_matrix/identity/versions")
async def versions():
return jsonify({"versions":["r0.1.0","r0.2.0","r0.2.1","r0.3.0","v1.1","v1.2","v1.3","v1.4","v1.5"]})
# https://spec.matrix.org/v1.15/identity-service-api/#authentication
@identity.route("/_matrix/identity/v2/account")
async def account_info():
return jsonify({"user_id": f"@vona:{server_name}"})
@identity.route("/_matrix/identity/v2/account/logout", methods=['POST'])
async def logout():
return jsonify({})
@identity.route("/_matrix/identity/v2/account/register", methods=['POST'])
async def register():
return jsonify({"token":"vona"})
# https://spec.matrix.org/v1.15/identity-service-api/#terms-of-service
@identity.route('/_matrix/identity/v2/terms', methods=['GET', 'POST'])
async def policies():
if request.method == 'GET':
return jsonify({"policies":{}})
return jsonify({})
# https://spec.matrix.org/v1.15/identity-service-api/#status-check
@identity.route('/_matrix/identity/v2')
async def status():
return jsonify({})
# https://spec.matrix.org/v1.15/identity-service-api/#key-management
@identity.route('/_matrix/identity/v2/pubkey/ephemeral/isvalid')
async def pubkey_eph_validity():
return jsonify({"valid":True})
@identity.route('/_matrix/identity/v2/pubkey/isvalid')
async def pubkey_validity():
return jsonify({"valid":True})
@identity.route('/_matrix/identity/v2/pubkey/<key>')
async def get_key(key):
return jsonify({"errcode":"M_NOT_FOUND","error":"The public key was not found"}), 404
# https://spec.matrix.org/v1.15/identity-service-api/#association-lookup
@identity.route('/_matrix/identity/v2/hash_details')
async def hash_details():
return jsonify({"algorithms":["none","sha256"],"lookup_pepper": "vona"})
@identity.route('/_matrix/identity/v2/lookup', methods=['POST'])
async def lookup():
req = request.json
if 'addresses' in req:
return jsonify({"mappings": {req['addresses'][0]: f"@vona:{server_name}"}})
else:
return jsonify({"errcode": "M_INVALID_PEPPER","error": "Invalid pepper"})
# https://spec.matrix.org/v1.15/identity-service-api/#establishing-associations
@identity.route('/_matrix/identity/v2/validate/email/requestToken', methods=['POST'])
async def request_email_token():
return jsonify({"sid":str(the_funny_number)})
@identity.route('/_matrix/identity/v2/validate/email/submitToken', methods=['GET', 'POST'])
async def submit_email_token():
return jsonify({"success":True})
@identity.route('/_matrix/identity/v2/validate/msisdn/requestToken', methods=['POST'])
async def request_phone_token():
return jsonify({"sid":str(the_funny_number)})
@identity.route('/_matrix/identity/v2/validate/msisdn/submitToken', methods=['GET', 'POST'])
async def submit_phone_token():
return jsonify({"success":True})
@identity.route('/_matrix/identity/v2/3pid/bind', methods=['POST'])
async def threepid_bind():
# We have to do signature stuff here,
# this will be implemented properly
# when I actually get sigs working.
return jsonify({})
@identity.route('/_matrix/identity/v2/3pid/unbind', methods=['POST'])
async def threepid_unbind():
return jsonify({})
@identity.route('/_matrix/identity/v2/3pid/getValidated3pid')
async def threepid_validated():
# Please email abuse@matrix.org
return jsonify({"address":"abuse@matrix.org","medium":"email","validated_at":the_funny_number})
# https://spec.matrix.org/v1.15/identity-service-api/#invitation-storage
@identity.route('/_matrix/identity/v2/store-invite', methods=['POST'])
async def invite():
return jsonify({"display_name":"Vona","public_keys":[{"key_validity_url":"https://example.com/_matrix/identity/v2/pubkey/isvalid","public_key":"ohyeah"},{"key_validity_url":"https://example.com/_matrix/identity/v2/pubkey/ephemeral/isvalid","public_key":"thisssssss"}],"token":"vona"})
# https://spec.matrix.org/v1.15/identity-service-api/#ephemeral-invitation-signing
@identity.route('/_matrix/identity/v2/sign-ed25519', methods=['POST'])
async def invite_signing():
# the spec sez this is insecure :trole:
return jsonify({
"errcode":"M_TERMS_NOT_SIGNED","error":"Please accept our updated terms of service before continuing"
}), 403

70
src/main.py Normal file
View File

@@ -0,0 +1,70 @@
from flask import Flask, jsonify, request, redirect
import globals
import config
from identity import identity
from appservice import apps
from custom import custom
from policy import policy
from c2s import client
from s2s import server
app = Flask(__name__)
app.register_blueprint(identity)
app.register_blueprint(policy)
app.register_blueprint(client)
app.register_blueprint(custom)
app.register_blueprint(server)
app.register_blueprint(apps)
# Landing page
@app.route("/")
async def root():
return redirect("/_matrix/static/", 308)
@app.route("/_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://matrix.org" target="_blank" rel="noopener noreferrer">matrix.org</a></small></p></body></html>'
# Error handlers
@app.errorhandler(404)
async def not_found(error):
return jsonify({"errcode": "M_UNRECOGNIZED", "error": "Unrecognized request"}), 404
@app.errorhandler(405)
async def invalid_request_method(error):
return jsonify({"errcode": "M_UNRECOGNIZED", "error": "Unrecognized request"}), 405
@app.errorhandler(500)
async def internal_error(error):
return jsonify({"errcode": "M_UNKNOWN", "error": "Internal server error"}), 500
# Well-known endpoints for federation,
# clients, and support information
@app.route("/.well-known/matrix/server")
async def server():
return jsonify({"m.server": f"{config.server_name}:443"})
@app.route("/.well-known/matrix/support")
async def support():
if config.support:
return jsonify(config.support)
else:
abort(404)
@app.route("/.well-known/matrix/client")
async def client():
return jsonify({
"m.homeserver": {"base_url": f"https://{config.server_name}"},
"m.identity_server": {"base_url": f"https://{config.server_name}"},
})
if __name__ == "__main__":
app.run(host=config.addr, port=config.port)
else:
print("What the hell are you doing?")

29
src/policy.py Normal file
View File

@@ -0,0 +1,29 @@
from flask import jsonify, Blueprint, request
policy = Blueprint("policy_server", __name__)
matrix_org = [
"dendrite.matrix.org",
"matrix.org",
"element.io",
"t2l.io",
"t2bot.io"
]
@policy.route("/_matrix/policy/v1/event/<eventId>/check", methods=["POST"])
def check_event(eventId):
if request.get_json()["origin"] in matrix_org:
return jsonify({"recommendation": "spam"})
else:
return jsonify({"recommendation": "ok"})
@policy.route(
"/_matrix/policy/unstable/org.matrix.msc4284/event/<eventId>/check",
methods=["POST"],
)
def check_event_unstable(eventId):
if request.get_json()["origin"] in matrix_org:
return jsonify({"recommendation": "spam"})
else:
return jsonify({"recommendation": "ok"})

245
src/s2s.py Normal file
View File

@@ -0,0 +1,245 @@
from flask import Flask, jsonify, Response, request, send_file, abort, Blueprint
from globals import vona_version, make_event_id, hash_event
from config import *
import requests
import json
import os
server = Blueprint('matrix_server', __name__)
def send_join(request, roomId):
# We may have to include signatures here
# as well, which will be a pain
eventIds = [
f"${make_event_id()}:{server_name}",
f"${make_event_id()}:{server_name}",
f"${make_event_id()}:{server_name}",
f"${make_event_id()}:{server_name}"
]
events = {
"auth_chain": [eventIds[2], eventIds[3]],
"event": {},
"members_omitted": True,
"servers_in_room": [server_name],
"state": [
{
"content": {
"room_version": the_funny_number,
"creator": f"@vona:{server_name}"
},
"room_id": roomId,
"event_id": eventIds[0],
"state_key": "",
"origin_server_ts": 1,
"type": "m.room.create",
"sender": f"@vona:{server_name}"
},
{
"content": {"membership": "join"},
"room_id": roomId,
"event_id": eventIds[1],
"state_key": f"@vona:{server_name}",
"origin_server_ts": 2,
"type": "m.room.member",
"sender": f"@vona:{server_name}"
},
{
"content": {"users": {f"@vona:{server_name}": 100}},
"room_id": roomId,
"event_id": eventIds[2],
"state_key": "",
"origin_server_ts": 3,
"type": "m.room.power_levels",
"sender": f"@vona:{server_name}"
},
{
"content": {"join_rule": "public"},
"room_id": roomId,
"event_id": eventIds[3],
"state_key": "",
"origin_server_ts": 4,
"type": "m.room.join_rules",
"sender": f"@vona:{server_name}"
}
]
}
for event in events['state']:
event_hash = hash_event(event)
if event['type'] != "m.room.create":
event['prev_events'] = [eventIds[event['origin_server_ts'] - 1], {"sha256": event_hash}]
join_event = request.json
join_event['prev_events'] = [eventIds[3], {"sha256": events['state'][3]['prev_events'][1]['sha256']}]
events['event'] = join_event
print(json.dumps(events, indent='\t'))
print(json.dumps(eventIds, indent='\t'))
return events
@server.route('/_matrix/federation/v1/version')
def version():
return jsonify({"server": {"version": vona_version,"name": "Vona"}})
@server.route('/_matrix/key/v2/server')
def keys():
# todo
return jsonify({})
@server.route('/_matrix/federation/v1/query/directory')
def room_query():
return jsonify({
"room_id": room_dir_room['chunk'][0]['room_id'],
"servers": [server_name]
})
@server.route('/_matrix/federation/v1/media/download/<media_id>')
def download_media(media_id):
# Auth media requires this to be
# multipart despite not even using
# it for anything. Minor annoyance.
with open(cat, 'rb') as img_file:
image_data = img_file.read()
boundary = '----WebKitFormBoundary7MA4YWxkTrZu0gW'
response_body = (
f'--{boundary}\r\n'
f'Content-Type: application/json\r\n\r\n'
f'{{}}\r\n'
f'--{boundary}\r\n'
f'Content-Type: image/jpeg\r\n'
f'Content-Disposition: attachment; filename="cat.jpg"\r\n\r\n'
).encode() + image_data + f'\r\n--{boundary}--\r\n'.encode()
response = Response(response_body, content_type=f'multipart/mixed; boundary={boundary}')
response.status_code = 200
return response
@server.route('/_matrix/federation/v1/media/thumbnail/<media_id>')
def thumbnail_media(media_id):
return jsonify({"errcode": "M_TOO_CUTE","error": "Cat is too cute to thumbnail"}), 403
@server.route('/_matrix/federation/v1/send_join/<roomId>/<eventId>', methods=['PUT'])
def send_join_v1(roomId, eventId):
return jsonify(send_join(request, roomId))
@server.route('/_matrix/federation/v2/send_join/<roomId>/<eventId>', methods=['PUT'])
def send_join_v2(roomId, eventId):
return jsonify(send_join(request, roomId))
@server.route('/_matrix/federation/v1/make_join/<roomId>/<userId>')
def make_join(roomId, userId):
return jsonify({
"event": {
"content": {
"join_authorised_via_users_server": f"@vona:{server_name}",
"membership": "join"
},
"origin": server_name,
"origin_server_ts": str(the_funny_number),
"room_id": roomId,
"sender": userId,
"state_key": userId,
"type": "m.room.member"
},
"room_version": "2"
})
@server.route('/_matrix/federation/v1/publicRooms', methods=['POST', 'GET'])
def room_directory():
return jsonify(room_dir_room)
# https://spec.matrix.org/v1.15/server-server-api/#transactions
@server.route('/_matrix/federation/v1/send/<txnId>', methods=['PUT'])
def federation_send(txnId):
# This only works for v2 rooms, but anything
# bigger than v2 isn't real, so not a problem
data = request.data.decode('utf-8')
parsed_data = json.loads(data)
response = {"pdus": {}}
if 'pdus' in parsed_data and parsed_data['pdus']:
for pdu in parsed_data['pdus']:
if 'hashes' in pdu and 'sha256' in pdu['hashes']:
pdu_hash = pdu['hashes']['sha256']
response_key = f"${pdu_hash}:{parsed_data['origin']}"
response["pdus"][response_key] = {}
return jsonify(response)
@server.route('/_matrix/federation/v1/query/profile')
def user_profile():
field = request.args.get('field')
if field:
if field == 'avatar_url':
return jsonify({"avatar_url":f"mxc://{server_name}/cat"})
elif field == 'displayname':
return jsonify({"displayname":"Vona"})
else:
return jsonify({"errcode": "M_NOT_FOUND","error": "The requested profile key does not exist."}), 404
return jsonify({"avatar_url": f"mxc://{server_name}/cat","displayname": "Vona"})
@server.route('/_matrix/federation/v1/user/devices/<user>')
def user_devices(user):
return jsonify({
"devices": [],
"stream_id": the_funny_number,
"user_id": f"@vona:{server_name}"
})
@server.route('/_matrix/federation/v1/user/keys/query', methods=['POST'])
def user_keys():
return jsonify({"device_keys":{f"@vona:{server_name}":{}}})
@server.route('/_matrix/federation/v2/invite/<room>/<txnId>', methods=['PUT'])
def fed_invite_user(room, txnId):
return jsonify({"errcode":"M_INCOMPATIBLE_ROOM_VERSION","error": "Don't touch my users mofo","room_version": str(the_funny_number)}), 400
@server.route('/_matrix/federation/v1/hierarchy/<roomId>')
def space_hierachy(roomId):
room = room_dir_room['chunk'][0]
return jsonify({
"children": [{
"avatar_url": room['avatar_url'],
"children_state": [{
"content": {"via": [server_name]},
"origin_server_ts": the_funny_number,
"sender": f"@vona:{server_name}",
"state_key": room['room_id'],
"type": "m.space.child"
}],
"guest_can_join": room['guest_can_join'],
"join_rule": room['join_rule'],
"name": room['name'],
"num_joined_members": room['num_joined_members'],
"room_id": room['room_id'],
"room_type": room['room_type'],
"topic": room['topic'],
"world_readable": room['world_readable']
}],
"inaccessible_children": [],
"room": {
"allowed_room_ids": [],
"avatar_url": room['avatar_url'],
"children_state": [{
"content": {"via": [server_name]},
"origin_server_ts": the_funny_number,
"sender": f"@vona:{server_name}",
"state_key": room['room_id'],
"type": "m.space.child"
}],
"guest_can_join": room['guest_can_join'],
"join_rule": room['join_rule'],
"name": room['name'],
"num_joined_members": room['num_joined_members'],
"room_id": room['room_id'],
"room_type": room['room_type'],
"topic": room['topic'],
"world_readable": room['world_readable']
}
})