Upload folder using huggingface_hub
Browse files- Dockerfile +36 -0
- README.md +4 -6
- app/main.py +274 -0
- app/static/index.html +276 -0
- conduit.toml +13 -0
- nginx.conf +38 -0
- requirements.txt +4 -0
- start.sh +20 -0
- supervisord.conf +43 -0
- sync_loop.sh +12 -0
Dockerfile
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM docker.io/matrixconduit/matrix-conduit:latest AS conduit
|
| 2 |
+
|
| 3 |
+
FROM python:3.11-slim
|
| 4 |
+
|
| 5 |
+
# Install system deps
|
| 6 |
+
RUN apt-get update && \
|
| 7 |
+
apt-get install -y nginx supervisor curl && \
|
| 8 |
+
rm -rf /var/lib/apt/lists/*
|
| 9 |
+
|
| 10 |
+
# Copy conduit binary from official image
|
| 11 |
+
COPY --from=conduit /srv/conduit/conduit /usr/local/bin/conduit
|
| 12 |
+
RUN chmod +x /usr/local/bin/conduit
|
| 13 |
+
|
| 14 |
+
# Install Python deps
|
| 15 |
+
COPY requirements.txt /tmp/requirements.txt
|
| 16 |
+
RUN pip install --no-cache-dir -r /tmp/requirements.txt
|
| 17 |
+
|
| 18 |
+
# Config files
|
| 19 |
+
COPY conduit.toml /etc/conduit/conduit.toml
|
| 20 |
+
COPY nginx.conf /etc/nginx/nginx.conf
|
| 21 |
+
COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf
|
| 22 |
+
|
| 23 |
+
# Scripts
|
| 24 |
+
COPY start.sh /start.sh
|
| 25 |
+
COPY sync_loop.sh /sync_loop.sh
|
| 26 |
+
RUN chmod +x /start.sh /sync_loop.sh
|
| 27 |
+
|
| 28 |
+
# Application
|
| 29 |
+
COPY app/ /app/
|
| 30 |
+
|
| 31 |
+
# Create directories
|
| 32 |
+
RUN mkdir -p /data/conduit /var/log/supervisor
|
| 33 |
+
|
| 34 |
+
EXPOSE 7860
|
| 35 |
+
|
| 36 |
+
CMD ["/start.sh"]
|
README.md
CHANGED
|
@@ -1,10 +1,8 @@
|
|
| 1 |
---
|
| 2 |
title: Messenger
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: docker
|
| 7 |
-
|
| 8 |
---
|
| 9 |
-
|
| 10 |
-
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
|
|
|
| 1 |
---
|
| 2 |
title: Messenger
|
| 3 |
+
emoji: "\U0001F4AC"
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: purple
|
| 6 |
sdk: docker
|
| 7 |
+
app_port: 7860
|
| 8 |
---
|
|
|
|
|
|
app/main.py
ADDED
|
@@ -0,0 +1,274 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Messenger API — thin layer over Matrix/Conduit for zero-friction chat."""
|
| 2 |
+
|
| 3 |
+
import hashlib
|
| 4 |
+
import time
|
| 5 |
+
import urllib.parse
|
| 6 |
+
import uuid
|
| 7 |
+
|
| 8 |
+
import httpx
|
| 9 |
+
from fastapi import FastAPI, HTTPException
|
| 10 |
+
from fastapi.responses import FileResponse, HTMLResponse
|
| 11 |
+
from fastapi.staticfiles import StaticFiles
|
| 12 |
+
from pydantic import BaseModel
|
| 13 |
+
|
| 14 |
+
app = FastAPI(title="Messenger")
|
| 15 |
+
|
| 16 |
+
CONDUIT = "http://127.0.0.1:6167"
|
| 17 |
+
SERVER_NAME = "lvwerra-messenger.hf.space"
|
| 18 |
+
# Used to derive deterministic passwords for auto-created accounts
|
| 19 |
+
SECRET_SALT = "messenger-salt-v1"
|
| 20 |
+
|
| 21 |
+
# Cache: display_name -> {user_id, access_token}
|
| 22 |
+
_user_cache: dict[str, dict] = {}
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
# --- Models ---
|
| 26 |
+
|
| 27 |
+
class RegisterRequest(BaseModel):
|
| 28 |
+
name: str
|
| 29 |
+
|
| 30 |
+
class SendRequest(BaseModel):
|
| 31 |
+
text: str
|
| 32 |
+
name: str | None = None
|
| 33 |
+
token: str | None = None
|
| 34 |
+
|
| 35 |
+
class CreateRoomRequest(BaseModel):
|
| 36 |
+
name: str
|
| 37 |
+
token: str
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
# --- Matrix helpers ---
|
| 41 |
+
|
| 42 |
+
def _make_password(username: str) -> str:
|
| 43 |
+
return hashlib.sha256(f"{username}:{SECRET_SALT}".encode()).hexdigest()[:32]
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
async def _matrix_register(username: str, password: str) -> dict:
|
| 47 |
+
"""Register a new Matrix user. Returns {user_id, access_token}."""
|
| 48 |
+
async with httpx.AsyncClient(timeout=10) as c:
|
| 49 |
+
# First call to get the session
|
| 50 |
+
resp = await c.post(f"{CONDUIT}/_matrix/client/v3/register", json={
|
| 51 |
+
"username": username,
|
| 52 |
+
"password": password,
|
| 53 |
+
"inhibit_login": False,
|
| 54 |
+
"initial_device_display_name": "Messenger",
|
| 55 |
+
})
|
| 56 |
+
data = resp.json()
|
| 57 |
+
|
| 58 |
+
# Conduit may require a UIAA step
|
| 59 |
+
if resp.status_code == 401 and "session" in data:
|
| 60 |
+
session = data["session"]
|
| 61 |
+
resp = await c.post(f"{CONDUIT}/_matrix/client/v3/register", json={
|
| 62 |
+
"username": username,
|
| 63 |
+
"password": password,
|
| 64 |
+
"auth": {"type": "m.login.dummy", "session": session},
|
| 65 |
+
"inhibit_login": False,
|
| 66 |
+
"initial_device_display_name": "Messenger",
|
| 67 |
+
})
|
| 68 |
+
data = resp.json()
|
| 69 |
+
|
| 70 |
+
if "access_token" in data:
|
| 71 |
+
return data
|
| 72 |
+
raise HTTPException(400, f"Registration failed: {data}")
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
async def _matrix_login(username: str, password: str) -> dict:
|
| 76 |
+
"""Login to an existing Matrix account. Returns {user_id, access_token}."""
|
| 77 |
+
async with httpx.AsyncClient(timeout=10) as c:
|
| 78 |
+
resp = await c.post(f"{CONDUIT}/_matrix/client/v3/login", json={
|
| 79 |
+
"type": "m.login.password",
|
| 80 |
+
"identifier": {"type": "m.id.user", "user": username},
|
| 81 |
+
"password": password,
|
| 82 |
+
"initial_device_display_name": "Messenger",
|
| 83 |
+
})
|
| 84 |
+
data = resp.json()
|
| 85 |
+
if "access_token" in data:
|
| 86 |
+
return data
|
| 87 |
+
raise HTTPException(401, f"Login failed: {data}")
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
async def get_or_create_user(display_name: str) -> dict:
|
| 91 |
+
"""Get or create a Matrix user from just a display name."""
|
| 92 |
+
if display_name in _user_cache:
|
| 93 |
+
return _user_cache[display_name]
|
| 94 |
+
|
| 95 |
+
username = display_name.lower().replace(" ", "_")
|
| 96 |
+
# Remove non-alphanumeric chars (Matrix usernames are restrictive)
|
| 97 |
+
username = "".join(c for c in username if c.isalnum() or c == "_")
|
| 98 |
+
if not username:
|
| 99 |
+
username = f"user_{uuid.uuid4().hex[:8]}"
|
| 100 |
+
|
| 101 |
+
password = _make_password(username)
|
| 102 |
+
|
| 103 |
+
# Try register first, fall back to login
|
| 104 |
+
try:
|
| 105 |
+
data = await _matrix_register(username, password)
|
| 106 |
+
except HTTPException:
|
| 107 |
+
data = await _matrix_login(username, password)
|
| 108 |
+
|
| 109 |
+
# Set display name
|
| 110 |
+
try:
|
| 111 |
+
async with httpx.AsyncClient(timeout=10) as c:
|
| 112 |
+
await c.put(
|
| 113 |
+
f"{CONDUIT}/_matrix/client/v3/profile/{data['user_id']}/displayname",
|
| 114 |
+
headers={"Authorization": f"Bearer {data['access_token']}"},
|
| 115 |
+
json={"displayname": display_name},
|
| 116 |
+
)
|
| 117 |
+
except Exception:
|
| 118 |
+
pass # Non-critical
|
| 119 |
+
|
| 120 |
+
result = {"user_id": data["user_id"], "access_token": data["access_token"]}
|
| 121 |
+
_user_cache[display_name] = result
|
| 122 |
+
return result
|
| 123 |
+
|
| 124 |
+
|
| 125 |
+
async def _resolve_room(room_alias: str, token: str) -> str:
|
| 126 |
+
"""Resolve a room alias to a room_id."""
|
| 127 |
+
encoded = urllib.parse.quote(room_alias)
|
| 128 |
+
async with httpx.AsyncClient(timeout=10) as c:
|
| 129 |
+
resp = await c.get(
|
| 130 |
+
f"{CONDUIT}/_matrix/client/v3/directory/room/{encoded}",
|
| 131 |
+
headers={"Authorization": f"Bearer {token}"},
|
| 132 |
+
)
|
| 133 |
+
if resp.status_code == 200:
|
| 134 |
+
return resp.json()["room_id"]
|
| 135 |
+
return ""
|
| 136 |
+
|
| 137 |
+
|
| 138 |
+
async def _ensure_room(room_name: str, token: str) -> str:
|
| 139 |
+
"""Get or create a room. Returns room_id."""
|
| 140 |
+
alias = f"#{room_name}:{SERVER_NAME}"
|
| 141 |
+
room_id = await _resolve_room(alias, token)
|
| 142 |
+
if room_id:
|
| 143 |
+
return room_id
|
| 144 |
+
|
| 145 |
+
# Create the room
|
| 146 |
+
async with httpx.AsyncClient(timeout=10) as c:
|
| 147 |
+
resp = await c.post(
|
| 148 |
+
f"{CONDUIT}/_matrix/client/v3/createRoom",
|
| 149 |
+
headers={"Authorization": f"Bearer {token}"},
|
| 150 |
+
json={
|
| 151 |
+
"room_alias_name": room_name,
|
| 152 |
+
"name": room_name,
|
| 153 |
+
"visibility": "public",
|
| 154 |
+
"preset": "public_chat",
|
| 155 |
+
},
|
| 156 |
+
)
|
| 157 |
+
data = resp.json()
|
| 158 |
+
if "room_id" in data:
|
| 159 |
+
return data["room_id"]
|
| 160 |
+
raise HTTPException(500, f"Failed to create room: {data}")
|
| 161 |
+
|
| 162 |
+
|
| 163 |
+
async def _join_room(room_id: str, token: str) -> None:
|
| 164 |
+
"""Join a room (idempotent)."""
|
| 165 |
+
async with httpx.AsyncClient(timeout=10) as c:
|
| 166 |
+
await c.post(
|
| 167 |
+
f"{CONDUIT}/_matrix/client/v3/join/{urllib.parse.quote(room_id)}",
|
| 168 |
+
headers={"Authorization": f"Bearer {token}"},
|
| 169 |
+
)
|
| 170 |
+
|
| 171 |
+
|
| 172 |
+
async def _get_token(req: SendRequest) -> str:
|
| 173 |
+
"""Extract token from request — either provided or auto-created from name."""
|
| 174 |
+
if req.token:
|
| 175 |
+
return req.token
|
| 176 |
+
if req.name:
|
| 177 |
+
user = await get_or_create_user(req.name)
|
| 178 |
+
return user["access_token"]
|
| 179 |
+
raise HTTPException(400, "Either 'name' or 'token' required")
|
| 180 |
+
|
| 181 |
+
|
| 182 |
+
# --- API endpoints ---
|
| 183 |
+
|
| 184 |
+
@app.post("/api/register")
|
| 185 |
+
async def register(req: RegisterRequest):
|
| 186 |
+
"""Register or login with just a display name. Returns token."""
|
| 187 |
+
user = await get_or_create_user(req.name)
|
| 188 |
+
return {"user_id": user["user_id"], "token": user["access_token"]}
|
| 189 |
+
|
| 190 |
+
|
| 191 |
+
@app.post("/api/rooms")
|
| 192 |
+
async def create_room(req: CreateRoomRequest):
|
| 193 |
+
"""Create a new room. Returns a shareable link."""
|
| 194 |
+
room_id = await _ensure_room(req.name, req.token)
|
| 195 |
+
return {
|
| 196 |
+
"room_id": room_id,
|
| 197 |
+
"link": f"https://{SERVER_NAME}/ch/{req.name}",
|
| 198 |
+
}
|
| 199 |
+
|
| 200 |
+
|
| 201 |
+
@app.post("/api/ch/{room}/join")
|
| 202 |
+
async def join_room(room: str, req: RegisterRequest):
|
| 203 |
+
"""Join a room by name. Auto-creates room if it doesn't exist."""
|
| 204 |
+
user = await get_or_create_user(req.name)
|
| 205 |
+
room_id = await _ensure_room(room, user["access_token"])
|
| 206 |
+
await _join_room(room_id, user["access_token"])
|
| 207 |
+
return {"ok": True, "token": user["access_token"], "room_id": room_id}
|
| 208 |
+
|
| 209 |
+
|
| 210 |
+
@app.post("/api/ch/{room}/send")
|
| 211 |
+
async def send_message(room: str, req: SendRequest):
|
| 212 |
+
"""Send a message to a room."""
|
| 213 |
+
token = await _get_token(req)
|
| 214 |
+
room_id = await _ensure_room(room, token)
|
| 215 |
+
await _join_room(room_id, token)
|
| 216 |
+
|
| 217 |
+
txn_id = uuid.uuid4().hex
|
| 218 |
+
async with httpx.AsyncClient(timeout=10) as c:
|
| 219 |
+
resp = await c.put(
|
| 220 |
+
f"{CONDUIT}/_matrix/client/v3/rooms/{room_id}/send/m.room.message/{txn_id}",
|
| 221 |
+
headers={"Authorization": f"Bearer {token}"},
|
| 222 |
+
json={"msgtype": "m.text", "body": req.text},
|
| 223 |
+
)
|
| 224 |
+
data = resp.json()
|
| 225 |
+
if "event_id" in data:
|
| 226 |
+
return {"event_id": data["event_id"]}
|
| 227 |
+
raise HTTPException(500, f"Failed to send: {data}")
|
| 228 |
+
|
| 229 |
+
|
| 230 |
+
@app.get("/api/ch/{room}/messages")
|
| 231 |
+
async def get_messages(room: str, token: str, limit: int = 50):
|
| 232 |
+
"""Get recent messages from a room."""
|
| 233 |
+
alias = f"#{room}:{SERVER_NAME}"
|
| 234 |
+
room_id = await _resolve_room(alias, token)
|
| 235 |
+
if not room_id:
|
| 236 |
+
return {"messages": []}
|
| 237 |
+
|
| 238 |
+
async with httpx.AsyncClient(timeout=30) as c:
|
| 239 |
+
resp = await c.get(
|
| 240 |
+
f"{CONDUIT}/_matrix/client/v3/rooms/{room_id}/messages",
|
| 241 |
+
headers={"Authorization": f"Bearer {token}"},
|
| 242 |
+
params={"dir": "b", "limit": limit},
|
| 243 |
+
)
|
| 244 |
+
data = resp.json()
|
| 245 |
+
|
| 246 |
+
messages = []
|
| 247 |
+
for event in reversed(data.get("chunk", [])):
|
| 248 |
+
if event.get("type") != "m.room.message":
|
| 249 |
+
continue
|
| 250 |
+
content = event.get("content", {})
|
| 251 |
+
if content.get("msgtype") != "m.text":
|
| 252 |
+
continue
|
| 253 |
+
messages.append({
|
| 254 |
+
"id": event["event_id"],
|
| 255 |
+
"sender": event["sender"],
|
| 256 |
+
"sender_name": event.get("sender_name", event["sender"].split(":")[0].lstrip("@")),
|
| 257 |
+
"text": content["body"],
|
| 258 |
+
"timestamp": event.get("origin_server_ts", 0),
|
| 259 |
+
})
|
| 260 |
+
return {"messages": messages}
|
| 261 |
+
|
| 262 |
+
|
| 263 |
+
# --- Web UI ---
|
| 264 |
+
|
| 265 |
+
@app.get("/ch/{room}")
|
| 266 |
+
async def chat_page(room: str):
|
| 267 |
+
"""Serve the chat UI for a room."""
|
| 268 |
+
return FileResponse("/app/static/index.html", media_type="text/html")
|
| 269 |
+
|
| 270 |
+
|
| 271 |
+
@app.get("/")
|
| 272 |
+
async def landing():
|
| 273 |
+
"""Landing page."""
|
| 274 |
+
return FileResponse("/app/static/index.html", media_type="text/html")
|
app/static/index.html
ADDED
|
@@ -0,0 +1,276 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>Messenger</title>
|
| 7 |
+
<style>
|
| 8 |
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
| 9 |
+
body {
|
| 10 |
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
| 11 |
+
background: #0f0f0f; color: #e0e0e0; height: 100dvh;
|
| 12 |
+
display: flex; flex-direction: column;
|
| 13 |
+
}
|
| 14 |
+
header {
|
| 15 |
+
padding: 12px 16px; background: #1a1a1a; border-bottom: 1px solid #2a2a2a;
|
| 16 |
+
font-weight: 600; font-size: 16px; flex-shrink: 0;
|
| 17 |
+
}
|
| 18 |
+
header span { color: #888; font-weight: 400; font-size: 13px; margin-left: 8px; }
|
| 19 |
+
|
| 20 |
+
/* Landing */
|
| 21 |
+
#landing {
|
| 22 |
+
display: flex; align-items: center; justify-content: center;
|
| 23 |
+
flex: 1; padding: 20px;
|
| 24 |
+
}
|
| 25 |
+
#landing .card {
|
| 26 |
+
background: #1a1a1a; border-radius: 12px; padding: 32px;
|
| 27 |
+
max-width: 400px; width: 100%; text-align: center;
|
| 28 |
+
}
|
| 29 |
+
#landing h1 { font-size: 24px; margin-bottom: 8px; }
|
| 30 |
+
#landing p { color: #888; margin-bottom: 24px; font-size: 14px; }
|
| 31 |
+
#landing input {
|
| 32 |
+
width: 100%; padding: 12px; border-radius: 8px; border: 1px solid #333;
|
| 33 |
+
background: #0f0f0f; color: #e0e0e0; font-size: 15px; margin-bottom: 12px;
|
| 34 |
+
outline: none;
|
| 35 |
+
}
|
| 36 |
+
#landing input:focus { border-color: #5b7bd5; }
|
| 37 |
+
#landing button {
|
| 38 |
+
width: 100%; padding: 12px; border-radius: 8px; border: none;
|
| 39 |
+
background: #5b7bd5; color: white; font-size: 15px; font-weight: 600;
|
| 40 |
+
cursor: pointer;
|
| 41 |
+
}
|
| 42 |
+
#landing button:hover { background: #4a6bc4; }
|
| 43 |
+
|
| 44 |
+
/* Join dialog */
|
| 45 |
+
#join-dialog {
|
| 46 |
+
display: flex; align-items: center; justify-content: center;
|
| 47 |
+
flex: 1; padding: 20px;
|
| 48 |
+
}
|
| 49 |
+
#join-dialog .card {
|
| 50 |
+
background: #1a1a1a; border-radius: 12px; padding: 32px;
|
| 51 |
+
max-width: 400px; width: 100%; text-align: center;
|
| 52 |
+
}
|
| 53 |
+
#join-dialog h2 { font-size: 20px; margin-bottom: 16px; }
|
| 54 |
+
#join-dialog input {
|
| 55 |
+
width: 100%; padding: 12px; border-radius: 8px; border: 1px solid #333;
|
| 56 |
+
background: #0f0f0f; color: #e0e0e0; font-size: 15px; margin-bottom: 12px;
|
| 57 |
+
outline: none;
|
| 58 |
+
}
|
| 59 |
+
#join-dialog input:focus { border-color: #5b7bd5; }
|
| 60 |
+
#join-dialog button {
|
| 61 |
+
width: 100%; padding: 12px; border-radius: 8px; border: none;
|
| 62 |
+
background: #5b7bd5; color: white; font-size: 15px; font-weight: 600;
|
| 63 |
+
cursor: pointer;
|
| 64 |
+
}
|
| 65 |
+
#join-dialog button:hover { background: #4a6bc4; }
|
| 66 |
+
|
| 67 |
+
/* Chat */
|
| 68 |
+
#chat { display: none; flex-direction: column; flex: 1; min-height: 0; }
|
| 69 |
+
#messages {
|
| 70 |
+
flex: 1; overflow-y: auto; padding: 16px; display: flex;
|
| 71 |
+
flex-direction: column; gap: 4px;
|
| 72 |
+
}
|
| 73 |
+
.msg {
|
| 74 |
+
padding: 6px 0; line-height: 1.4;
|
| 75 |
+
}
|
| 76 |
+
.msg .name { font-weight: 600; margin-right: 8px; }
|
| 77 |
+
.msg .time { color: #555; font-size: 11px; margin-left: 6px; }
|
| 78 |
+
.msg.self .name { color: #5b7bd5; }
|
| 79 |
+
.msg.system { color: #666; font-style: italic; font-size: 13px; }
|
| 80 |
+
#input-bar {
|
| 81 |
+
padding: 12px 16px; background: #1a1a1a; border-top: 1px solid #2a2a2a;
|
| 82 |
+
display: flex; gap: 8px; flex-shrink: 0;
|
| 83 |
+
}
|
| 84 |
+
#msg-input {
|
| 85 |
+
flex: 1; padding: 10px 14px; border-radius: 8px; border: 1px solid #333;
|
| 86 |
+
background: #0f0f0f; color: #e0e0e0; font-size: 15px; outline: none;
|
| 87 |
+
}
|
| 88 |
+
#msg-input:focus { border-color: #5b7bd5; }
|
| 89 |
+
#send-btn {
|
| 90 |
+
padding: 10px 20px; border-radius: 8px; border: none;
|
| 91 |
+
background: #5b7bd5; color: white; font-size: 15px; font-weight: 600;
|
| 92 |
+
cursor: pointer;
|
| 93 |
+
}
|
| 94 |
+
#send-btn:hover { background: #4a6bc4; }
|
| 95 |
+
.hidden { display: none !important; }
|
| 96 |
+
</style>
|
| 97 |
+
</head>
|
| 98 |
+
<body>
|
| 99 |
+
|
| 100 |
+
<header id="header">
|
| 101 |
+
Messenger <span id="room-label"></span>
|
| 102 |
+
</header>
|
| 103 |
+
|
| 104 |
+
<!-- Landing: create or go to a room -->
|
| 105 |
+
<div id="landing" class="hidden">
|
| 106 |
+
<div class="card">
|
| 107 |
+
<h1>Messenger</h1>
|
| 108 |
+
<p>Create a chat room and share the link</p>
|
| 109 |
+
<input type="text" id="room-name-input" placeholder="Room name (e.g. my-project)">
|
| 110 |
+
<button onclick="goToRoom()">Create Room</button>
|
| 111 |
+
</div>
|
| 112 |
+
</div>
|
| 113 |
+
|
| 114 |
+
<!-- Join dialog: pick a display name -->
|
| 115 |
+
<div id="join-dialog" class="hidden">
|
| 116 |
+
<div class="card">
|
| 117 |
+
<h2 id="join-title">Join room</h2>
|
| 118 |
+
<input type="text" id="display-name" placeholder="Your name" autofocus>
|
| 119 |
+
<button onclick="joinRoom()">Join</button>
|
| 120 |
+
</div>
|
| 121 |
+
</div>
|
| 122 |
+
|
| 123 |
+
<!-- Chat view -->
|
| 124 |
+
<div id="chat">
|
| 125 |
+
<div id="messages"></div>
|
| 126 |
+
<div id="input-bar">
|
| 127 |
+
<input type="text" id="msg-input" placeholder="Type a message..." autofocus>
|
| 128 |
+
<button id="send-btn" onclick="sendMessage()">Send</button>
|
| 129 |
+
</div>
|
| 130 |
+
</div>
|
| 131 |
+
|
| 132 |
+
<script>
|
| 133 |
+
const path = window.location.pathname;
|
| 134 |
+
const roomMatch = path.match(/^\/ch\/([^/]+)/);
|
| 135 |
+
const roomName = roomMatch ? roomMatch[1] : null;
|
| 136 |
+
|
| 137 |
+
let token = null;
|
| 138 |
+
let userId = null;
|
| 139 |
+
let lastMessageId = null;
|
| 140 |
+
let pollTimer = null;
|
| 141 |
+
|
| 142 |
+
// Determine which view to show
|
| 143 |
+
if (!roomName) {
|
| 144 |
+
document.getElementById('landing').classList.remove('hidden');
|
| 145 |
+
document.getElementById('chat').style.display = 'none';
|
| 146 |
+
} else {
|
| 147 |
+
document.getElementById('room-label').textContent = '#' + roomName;
|
| 148 |
+
|
| 149 |
+
// Check for saved session
|
| 150 |
+
const saved = localStorage.getItem('messenger_' + roomName);
|
| 151 |
+
if (saved) {
|
| 152 |
+
const s = JSON.parse(saved);
|
| 153 |
+
token = s.token;
|
| 154 |
+
userId = s.userId;
|
| 155 |
+
startChat();
|
| 156 |
+
} else {
|
| 157 |
+
document.getElementById('join-dialog').classList.remove('hidden');
|
| 158 |
+
document.getElementById('join-title').textContent = 'Join #' + roomName;
|
| 159 |
+
document.getElementById('chat').style.display = 'none';
|
| 160 |
+
}
|
| 161 |
+
}
|
| 162 |
+
|
| 163 |
+
function goToRoom() {
|
| 164 |
+
const name = document.getElementById('room-name-input').value.trim()
|
| 165 |
+
.toLowerCase().replace(/[^a-z0-9_-]/g, '-');
|
| 166 |
+
if (name) window.location.href = '/ch/' + name;
|
| 167 |
+
}
|
| 168 |
+
|
| 169 |
+
async function joinRoom() {
|
| 170 |
+
const name = document.getElementById('display-name').value.trim();
|
| 171 |
+
if (!name) return;
|
| 172 |
+
|
| 173 |
+
try {
|
| 174 |
+
const resp = await fetch('/api/ch/' + roomName + '/join', {
|
| 175 |
+
method: 'POST',
|
| 176 |
+
headers: {'Content-Type': 'application/json'},
|
| 177 |
+
body: JSON.stringify({name}),
|
| 178 |
+
});
|
| 179 |
+
const data = await resp.json();
|
| 180 |
+
if (!resp.ok) throw new Error(data.detail || 'Join failed');
|
| 181 |
+
|
| 182 |
+
token = data.token;
|
| 183 |
+
userId = name;
|
| 184 |
+
localStorage.setItem('messenger_' + roomName, JSON.stringify({token, userId}));
|
| 185 |
+
startChat();
|
| 186 |
+
} catch (e) {
|
| 187 |
+
alert('Failed to join: ' + e.message);
|
| 188 |
+
}
|
| 189 |
+
}
|
| 190 |
+
|
| 191 |
+
function startChat() {
|
| 192 |
+
document.getElementById('landing').classList.add('hidden');
|
| 193 |
+
document.getElementById('join-dialog').classList.add('hidden');
|
| 194 |
+
document.getElementById('chat').style.display = 'flex';
|
| 195 |
+
document.getElementById('msg-input').focus();
|
| 196 |
+
loadMessages();
|
| 197 |
+
pollTimer = setInterval(loadMessages, 2000);
|
| 198 |
+
}
|
| 199 |
+
|
| 200 |
+
async function loadMessages() {
|
| 201 |
+
try {
|
| 202 |
+
const resp = await fetch(`/api/ch/${roomName}/messages?token=${encodeURIComponent(token)}&limit=100`);
|
| 203 |
+
const data = await resp.json();
|
| 204 |
+
renderMessages(data.messages || []);
|
| 205 |
+
} catch (e) {
|
| 206 |
+
console.error('Failed to load messages:', e);
|
| 207 |
+
}
|
| 208 |
+
}
|
| 209 |
+
|
| 210 |
+
function renderMessages(msgs) {
|
| 211 |
+
const container = document.getElementById('messages');
|
| 212 |
+
const wasAtBottom = container.scrollHeight - container.scrollTop - container.clientHeight < 50;
|
| 213 |
+
|
| 214 |
+
// Check if new messages arrived
|
| 215 |
+
const lastId = msgs.length ? msgs[msgs.length - 1].id : null;
|
| 216 |
+
if (lastId === lastMessageId) return;
|
| 217 |
+
lastMessageId = lastId;
|
| 218 |
+
|
| 219 |
+
container.innerHTML = '';
|
| 220 |
+
for (const msg of msgs) {
|
| 221 |
+
const div = document.createElement('div');
|
| 222 |
+
const senderName = msg.sender_name || msg.sender.split(':')[0].replace('@', '');
|
| 223 |
+
const isSelf = senderName.toLowerCase() === userId?.toLowerCase() ||
|
| 224 |
+
msg.sender_name === userId;
|
| 225 |
+
div.className = 'msg' + (isSelf ? ' self' : '');
|
| 226 |
+
|
| 227 |
+
const time = new Date(msg.timestamp).toLocaleTimeString([], {hour: '2-digit', minute: '2-digit'});
|
| 228 |
+
div.innerHTML = `<span class="name">${esc(senderName)}</span>${esc(msg.text)}<span class="time">${time}</span>`;
|
| 229 |
+
container.appendChild(div);
|
| 230 |
+
}
|
| 231 |
+
|
| 232 |
+
if (wasAtBottom) container.scrollTop = container.scrollHeight;
|
| 233 |
+
}
|
| 234 |
+
|
| 235 |
+
async function sendMessage() {
|
| 236 |
+
const input = document.getElementById('msg-input');
|
| 237 |
+
const text = input.value.trim();
|
| 238 |
+
if (!text || !token) return;
|
| 239 |
+
|
| 240 |
+
input.value = '';
|
| 241 |
+
try {
|
| 242 |
+
await fetch(`/api/ch/${roomName}/send`, {
|
| 243 |
+
method: 'POST',
|
| 244 |
+
headers: {'Content-Type': 'application/json'},
|
| 245 |
+
body: JSON.stringify({token, text}),
|
| 246 |
+
});
|
| 247 |
+
loadMessages();
|
| 248 |
+
} catch (e) {
|
| 249 |
+
console.error('Send failed:', e);
|
| 250 |
+
}
|
| 251 |
+
}
|
| 252 |
+
|
| 253 |
+
function esc(s) {
|
| 254 |
+
const d = document.createElement('div');
|
| 255 |
+
d.textContent = s;
|
| 256 |
+
return d.innerHTML;
|
| 257 |
+
}
|
| 258 |
+
|
| 259 |
+
// Enter to send
|
| 260 |
+
document.addEventListener('keydown', (e) => {
|
| 261 |
+
if (e.key === 'Enter' && !e.shiftKey) {
|
| 262 |
+
if (document.activeElement === document.getElementById('msg-input')) {
|
| 263 |
+
e.preventDefault();
|
| 264 |
+
sendMessage();
|
| 265 |
+
} else if (document.activeElement === document.getElementById('display-name')) {
|
| 266 |
+
e.preventDefault();
|
| 267 |
+
joinRoom();
|
| 268 |
+
} else if (document.activeElement === document.getElementById('room-name-input')) {
|
| 269 |
+
e.preventDefault();
|
| 270 |
+
goToRoom();
|
| 271 |
+
}
|
| 272 |
+
}
|
| 273 |
+
});
|
| 274 |
+
</script>
|
| 275 |
+
</body>
|
| 276 |
+
</html>
|
conduit.toml
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[global]
|
| 2 |
+
server_name = "lvwerra-messenger.hf.space"
|
| 3 |
+
database_backend = "sqlite"
|
| 4 |
+
database_path = "/data/conduit/"
|
| 5 |
+
port = 6167
|
| 6 |
+
max_request_size = 20_000_000
|
| 7 |
+
allow_registration = true
|
| 8 |
+
allow_federation = false
|
| 9 |
+
allow_check_for_updates = false
|
| 10 |
+
trusted_servers = ["matrix.org"]
|
| 11 |
+
|
| 12 |
+
[global.well_known]
|
| 13 |
+
client = "https://lvwerra-messenger.hf.space"
|
nginx.conf
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
worker_processes 1;
|
| 2 |
+
events { worker_connections 1024; }
|
| 3 |
+
|
| 4 |
+
http {
|
| 5 |
+
include /etc/nginx/mime.types;
|
| 6 |
+
default_type application/octet-stream;
|
| 7 |
+
sendfile on;
|
| 8 |
+
keepalive_timeout 65;
|
| 9 |
+
|
| 10 |
+
server {
|
| 11 |
+
listen 7860;
|
| 12 |
+
|
| 13 |
+
# Matrix client-server API → Conduit
|
| 14 |
+
location /_matrix/ {
|
| 15 |
+
proxy_pass http://127.0.0.1:6167;
|
| 16 |
+
proxy_set_header Host $host;
|
| 17 |
+
proxy_set_header X-Real-IP $remote_addr;
|
| 18 |
+
proxy_buffering off;
|
| 19 |
+
proxy_read_timeout 600s;
|
| 20 |
+
client_max_body_size 20M;
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
# .well-known for Matrix client discovery
|
| 24 |
+
location /.well-known/matrix/ {
|
| 25 |
+
proxy_pass http://127.0.0.1:6167;
|
| 26 |
+
proxy_set_header Host $host;
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
# Everything else → FastAPI
|
| 30 |
+
location / {
|
| 31 |
+
proxy_pass http://127.0.0.1:8000;
|
| 32 |
+
proxy_set_header Host $host;
|
| 33 |
+
proxy_set_header X-Real-IP $remote_addr;
|
| 34 |
+
proxy_buffering off;
|
| 35 |
+
proxy_read_timeout 120s;
|
| 36 |
+
}
|
| 37 |
+
}
|
| 38 |
+
}
|
requirements.txt
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi==0.115.0
|
| 2 |
+
uvicorn[standard]==0.30.0
|
| 3 |
+
httpx==0.27.0
|
| 4 |
+
huggingface_hub[cli,hf_xet]>=0.30.0
|
start.sh
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
set -e
|
| 3 |
+
|
| 4 |
+
echo "=== Messenger starting ==="
|
| 5 |
+
|
| 6 |
+
# Create data directories
|
| 7 |
+
mkdir -p /data/conduit
|
| 8 |
+
|
| 9 |
+
# Sync database from HF bucket (if exists)
|
| 10 |
+
if [ -n "$HF_TOKEN" ]; then
|
| 11 |
+
echo "Syncing database from bucket..."
|
| 12 |
+
huggingface-cli download \
|
| 13 |
+
lvwerra/messenger-storage \
|
| 14 |
+
--repo-type dataset \
|
| 15 |
+
--local-dir /data/conduit/ \
|
| 16 |
+
2>/dev/null || echo "No existing data in bucket (first run)"
|
| 17 |
+
fi
|
| 18 |
+
|
| 19 |
+
echo "Starting services..."
|
| 20 |
+
exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf
|
supervisord.conf
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[supervisord]
|
| 2 |
+
nodaemon=true
|
| 3 |
+
logfile=/var/log/supervisor/supervisord.log
|
| 4 |
+
pidfile=/var/run/supervisord.pid
|
| 5 |
+
user=root
|
| 6 |
+
|
| 7 |
+
[program:conduit]
|
| 8 |
+
command=/usr/local/bin/conduit
|
| 9 |
+
environment=CONDUIT_CONFIG="/etc/conduit/conduit.toml"
|
| 10 |
+
autostart=true
|
| 11 |
+
autorestart=true
|
| 12 |
+
stdout_logfile=/dev/fd/1
|
| 13 |
+
stdout_logfile_maxbytes=0
|
| 14 |
+
stderr_logfile=/dev/fd/2
|
| 15 |
+
stderr_logfile_maxbytes=0
|
| 16 |
+
|
| 17 |
+
[program:fastapi]
|
| 18 |
+
command=uvicorn app.main:app --host 0.0.0.0 --port 8000
|
| 19 |
+
directory=/app
|
| 20 |
+
autostart=true
|
| 21 |
+
autorestart=true
|
| 22 |
+
stdout_logfile=/dev/fd/1
|
| 23 |
+
stdout_logfile_maxbytes=0
|
| 24 |
+
stderr_logfile=/dev/fd/2
|
| 25 |
+
stderr_logfile_maxbytes=0
|
| 26 |
+
|
| 27 |
+
[program:nginx]
|
| 28 |
+
command=nginx -g "daemon off;"
|
| 29 |
+
autostart=true
|
| 30 |
+
autorestart=true
|
| 31 |
+
stdout_logfile=/dev/fd/1
|
| 32 |
+
stdout_logfile_maxbytes=0
|
| 33 |
+
stderr_logfile=/dev/fd/2
|
| 34 |
+
stderr_logfile_maxbytes=0
|
| 35 |
+
|
| 36 |
+
[program:sync]
|
| 37 |
+
command=/sync_loop.sh
|
| 38 |
+
autostart=true
|
| 39 |
+
autorestart=true
|
| 40 |
+
stdout_logfile=/dev/fd/1
|
| 41 |
+
stdout_logfile_maxbytes=0
|
| 42 |
+
stderr_logfile=/dev/fd/2
|
| 43 |
+
stderr_logfile_maxbytes=0
|
sync_loop.sh
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
# Periodic sync of Conduit database to HF bucket
|
| 3 |
+
while true; do
|
| 4 |
+
sleep 60
|
| 5 |
+
if [ -n "$HF_TOKEN" ] && [ -d /data/conduit ]; then
|
| 6 |
+
huggingface-cli upload \
|
| 7 |
+
lvwerra/messenger-storage \
|
| 8 |
+
/data/conduit/ \
|
| 9 |
+
--repo-type dataset \
|
| 10 |
+
2>/dev/null || echo "Sync failed, will retry"
|
| 11 |
+
fi
|
| 12 |
+
done
|