""" Flask collaboration server example for ribbit. Demonstrates: WebSocket relay, presence, revisions, and locking. Requires: flask, flask-sock pip install flask flask-sock python server.py Then open http://localhost:5000 in multiple browser tabs. """ import json import time import uuid from pathlib import Path from threading import Lock from flask import Flask, jsonify, render_template, request from flask_sock import Sock app = Flask(__name__) sock = Sock(app) # In-memory state (replace with a database in production) document = {"content": "# Hello\n\nEdit this page collaboratively.\n\n- Try opening multiple tabs\n- Watch edits appear in real time\n"} revisions = [] lock_holder = None lock_mutex = Lock() clients = {} # ws -> user info # ── Pages ──────────────────────────────────────────────── @app.route("/") def index(): return render_template("index.html", content=document["content"]) # ── Revisions API ──────────────────────────────────────── @app.route("/api/revisions", methods=["GET"]) def list_revisions(): return jsonify([{k: v for k, v in r.items() if k != "content"} for r in revisions]) @app.route("/api/revisions/", methods=["GET"]) def get_revision(revision_id): for r in revisions: if r["id"] == revision_id: return jsonify(r) return jsonify({"error": "not found"}), 404 @app.route("/api/revisions", methods=["POST"]) def create_revision(): data = request.json rev = { "id": str(uuid.uuid4())[:8], "timestamp": time.strftime("%Y-%m-%dT%H:%M:%SZ"), "author": data.get("author", "anonymous"), "summary": data.get("summary", ""), "content": data.get("content", document["content"]), } revisions.append(rev) broadcast_json({"type": "revision", "revision": {k: v for k, v in rev.items() if k != "content"}}) return jsonify(rev), 201 # ── Locking API ────────────────────────────────────────── @app.route("/api/lock", methods=["POST"]) def acquire_lock(): global lock_holder with lock_mutex: if lock_holder is None: lock_holder = request.json broadcast_json({"type": "lock", "holder": lock_holder}) return jsonify({"ok": True}) return jsonify({"ok": False, "holder": lock_holder}), 409 @app.route("/api/lock", methods=["DELETE"]) def release_lock(): global lock_holder with lock_mutex: lock_holder = None broadcast_json({"type": "lock", "holder": None}) return jsonify({"ok": True}) @app.route("/api/lock/force", methods=["POST"]) def force_lock(): global lock_holder with lock_mutex: lock_holder = request.json broadcast_json({"type": "lock", "holder": lock_holder}) return jsonify({"ok": True}) # ── WebSocket relay ────────────────────────────────────── @sock.route("/ws") def websocket(ws): client_id = str(uuid.uuid4())[:8] clients[client_id] = {"ws": ws, "user": None} try: while True: data = ws.receive() if isinstance(data, bytes): # Binary = document update, relay to all other clients document["content"] = data.decode("utf-8") for cid, client in clients.items(): if cid != client_id: try: client["ws"].send(data) except Exception: pass elif isinstance(data, str): msg = json.loads(data) if msg.get("type") == "join": clients[client_id]["user"] = msg.get("user") # Send current document state ws.send(document["content"].encode("utf-8")) # Send current lock state ws.send(json.dumps({"type": "lock", "holder": lock_holder})) # Broadcast updated peer list broadcast_peers() elif msg.get("type") == "presence": clients[client_id]["user"] = msg broadcast_peers() except Exception: pass finally: del clients[client_id] broadcast_peers() def broadcast_json(msg): data = json.dumps(msg) for client in clients.values(): try: client["ws"].send(data) except Exception: pass def broadcast_peers(): peers = [c["user"] for c in clients.values() if c["user"]] broadcast_json({"type": "peers", "peers": peers}) if __name__ == "__main__": app.run(debug=True, port=5000)