Add collaboration support
Real-time collaboration through consumer-provided transport and presence interfaces. Also includes a sample backend app.
This commit is contained in:
@@ -0,0 +1,160 @@
|
||||
"""
|
||||
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)
|
||||
Reference in New Issue
Block a user