feat: add PUT /api/settings/sync federation endpoint with newer-wins logic

This commit is contained in:
Brian Krabach
2026-04-08 21:03:23 -07:00
parent 349f49824b
commit a0ad4921cf
2 changed files with 158 additions and 1 deletions
+37 -1
View File
@@ -30,7 +30,7 @@ import websockets
from websockets.typing import Subprotocol
from fastapi import FastAPI, Form, HTTPException, Request, WebSocket
from fastapi.responses import HTMLResponse
from fastapi.responses import HTMLResponse, JSONResponse
from fastapi.staticfiles import StaticFiles
from pydantic import BaseModel, field_validator
from starlette.responses import RedirectResponse
@@ -65,6 +65,7 @@ from muxplex.state import (
state_lock,
)
from muxplex.settings import (
apply_synced_settings,
get_syncable_settings,
load_federation_key,
load_settings,
@@ -359,6 +360,11 @@ class CreateSessionPayload(BaseModel):
return stripped
class SettingsSyncPayload(BaseModel):
settings: dict
settings_updated_at: float
# ---------------------------------------------------------------------------
# Frontend directory + hostname
# ---------------------------------------------------------------------------
@@ -739,6 +745,36 @@ async def get_settings_sync() -> dict:
return {"settings": settings, "settings_updated_at": ts}
@app.put("/api/settings/sync")
async def put_settings_sync(payload: SettingsSyncPayload):
"""Accept synced settings from a remote server (newer-wins).
Compares the incoming timestamp against the local settings_updated_at.
If the incoming timestamp is strictly newer, applies only the syncable
keys via apply_synced_settings() and returns 200 with the final state.
If the incoming timestamp is equal to or older than the local one, returns
409 (Conflict) with the current local state so the caller can see what
this instance has.
"""
current = load_settings()
local_ts: float = current.get("settings_updated_at", 0.0)
if payload.settings_updated_at > local_ts:
apply_synced_settings(payload.settings, payload.settings_updated_at)
syncable = get_syncable_settings()
ts = syncable.get("settings_updated_at", 0.0)
settings_out = {k: v for k, v in syncable.items() if k != "settings_updated_at"}
return {"settings": settings_out, "settings_updated_at": ts}
else:
syncable = get_syncable_settings()
ts = syncable.get("settings_updated_at", 0.0)
settings_out = {k: v for k, v in syncable.items() if k != "settings_updated_at"}
return JSONResponse(
status_code=409,
content={"settings": settings_out, "settings_updated_at": ts},
)
@app.get("/api/instance-info")
async def instance_info() -> dict:
"""Return this instance's display name and version.