ribbit/examples/flask-collab/server.py

282 lines
6.1 KiB
Python
Raw Permalink Normal View History

"""
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)
2026-04-29 23:54:34 -07:00
document = {"content": """# Ribbit Demo Document
## Inline Formatting
@block(examples
@block(example
### Type this
`**bold**`
### To get this
**bold**
)
@block(example
### Type this
`*italic*`
### To get this
*italic*
)
@block(example
### Type this
`***bold italic***`
### To get this
***bold italic***
)
@block(example
### Type this
`~~strikethrough~~`
### To get this
~~strikethrough~~
)
@block(example
### Type this
`` `inline code` ``
### To get this
`inline code`
)
@block(example
### Type this
`[link](http://example.com)`
### To get this
[link](http://example.com)
)
)
## Block Elements
@block(examples
@block(example
### Type this
```
- apples
- bananas
- cherries
```
### To get this
- apples
- bananas
- cherries
)
@block(example
### Type this
```
1. Step one
2. Step two
3. Step three
```
### To get this
1. Step one
2. Step two
3. Step three
)
@block(example
### Type this
```
> First line
> Second line
> Third line
```
### To get this
> First line
> Second line
> Third line
)
@block(example
### Type this
````
```python
def hello():
print("Hello!")
```
````
### To get this
```python
def hello():
print("Hello!")
```
)
)
## Full Example
Here is a paragraph with **bold**, *italic*, and `code` inline.
A [link](http://example.com) and ~~deleted text~~ too.
> A blockquote with **formatting** inside.
- List with *italic*
- And `code`
***
"""}
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, host="0.0.0.0", port=5000)