Real-time collaboration through consumer-provided transport and presence interfaces. Also includes a sample backend app.
161 lines
4.8 KiB
Python
161 lines
4.8 KiB
Python
"""
|
|
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/<revision_id>", 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)
|