Update
This commit is contained in:
@@ -14,7 +14,7 @@ Prefixes are wired in `main.py`:
|
||||
| `/comments` | comments.py |
|
||||
| `/projects` | projects/ package - `index.py` (listing/detail/create/delete plus owner-only visibility toggles `POST /{slug}/private` and `POST /{slug}/readonly`, and async `POST /{slug}/fork`), `files.py`, and the `containers/` subpackage (below). `main.py` mounts the whole `/projects` tree from this one package. See `routers/projects/CLAUDE.md` for the deep detail on this tree |
|
||||
| `/projects/{slug}/files` | projects/files.py - per-project virtual filesystem (CRUD dirs/files, upload, inline edit, plus line-range ops: `lines` read, `replace-lines`, `insert-lines`, `delete-lines`, `append` for large text files) |
|
||||
| `/profile` | profile/ package - `index.py` (page/search/followers/update/api-key; the **Posts** tab shows only the 10 most recent posts, while the sidebar `posts_count` stat always uses the real `count()` so it is never capped to the rendered 10), `customization.py`, `notifications.py`, `ai_correction.py` (`POST /{username}/ai-correction`, owner-or-admin AI content correction toggle + prompt), `ai_modifier.py` (`POST /{username}/ai-modifier`, owner-or-admin AI modifier toggle + prompt), `telegram.py` (`POST /{username}/telegram`, owner-or-admin Telegram pairing-code request/unpair), `usage.py` (`_ai_quota`, re-exported from the package) |
|
||||
| `/profile` | profile/ package - `index.py` (page/search/followers/update/api-key; the **Posts** tab shows only the 10 most recent posts, while the sidebar `posts_count` stat always uses the real `count()` so it is never capped to the rendered 10), `customization.py`, `notifications.py`, `ai_correction.py` (`POST /{username}/ai-correction`, owner-or-admin AI content correction toggle + prompt), `ai_modifier.py` (`POST /{username}/ai-modifier`, owner-or-admin AI modifier toggle + prompt), `interactions.py` (`POST /{username}/interactions`, owner-or-admin Devii interactive widgets preference; admin default via `devii_interactions_default`), `telegram.py` (`POST /{username}/telegram`, owner-or-admin Telegram pairing-code request/unpair), `usage.py` (`_ai_quota`, re-exported from the package) |
|
||||
| `/messages` | messages.py - real-time DM chat. `GET ""` (page; the conversation sidebar is one windowed SQL aggregate and an opened thread loads only the newest `CONVERSATION_MESSAGE_LIMIT` = 500 messages - keep these queries bounded), `GET /search`, `POST /send` (no-JS fallback), and `WS /messages/ws` (NOT lock-owner gated - accepts on every worker; closes `1008` for guests). The WS adds live bidirectional delivery, typing, read receipts, and in-process presence on top of the existing `messages` table; both the WS `send` and HTTP `POST /send` route through the shared `services/messaging/persist.py` `persist_message` choke point (identical audit/notification/mention) and then through `_finalize_and_broadcast`, which awaits any pending SYNC AI correction/modifier futures (DM content runs through both, so typing `@ai <instruction>` executes live) and broadcasts the FINAL corrected/modified content (the WS path passes `request=websocket` and awaits directly since HTTP middleware does not run for websockets; the frontend swaps the sender's optimistic bubble to the authoritative content). Same-worker delivery is instant via the per-worker `message_hub`; cross-worker delivery is filled by `services/messaging/relay.py` `message_relay` (per-worker ~1s DB poll on the `messages.id` watermark, deduped against the hub's delivered set). See `devplacepy/services/messaging/CLAUDE.md` |
|
||||
| `/notifications` | notifications.py |
|
||||
| `/votes` | votes.py |
|
||||
|
||||
@@ -17,15 +17,14 @@ _CACHE_CONTROL = f"public, max-age={SECONDS_PER_DAY}, immutable"
|
||||
|
||||
@router.get("/{style}/{seed}")
|
||||
async def avatar_proxy(request: Request, style: str, seed: str, size: int = 128):
|
||||
cache_key = f"{seed}:{size}"
|
||||
etag = '"' + hashlib.md5(cache_key.encode("utf-8")).hexdigest() + '"'
|
||||
etag = '"' + hashlib.md5(f"{seed}:{size}".encode("utf-8")).hexdigest() + '"'
|
||||
headers = {"ETag": etag, "Cache-Control": _CACHE_CONTROL}
|
||||
|
||||
if request.headers.get("if-none-match") == etag:
|
||||
return Response(status_code=304, headers=headers)
|
||||
|
||||
svg = _cache.get(cache_key)
|
||||
svg = _cache.get(seed)
|
||||
if svg is None:
|
||||
svg = generate_avatar_svg(seed)
|
||||
_cache.set(cache_key, svg)
|
||||
_cache.set(seed, svg)
|
||||
return Response(content=svg, media_type="image/svg+xml", headers=headers)
|
||||
|
||||
@@ -185,7 +185,10 @@ async def clippy_proxy(request: Request):
|
||||
return JSONResponse({"error": "Devii is unavailable"}, status_code=503)
|
||||
cfg = svc.effective_config()
|
||||
body = await request.body()
|
||||
headers = {"Content-Type": "application/json"}
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"X-App-Reference": "devplace-devii-v-1-0-0",
|
||||
}
|
||||
if cfg.get("devii_ai_key"):
|
||||
headers["Authorization"] = f"Bearer {cfg['devii_ai_key']}"
|
||||
async with stealth.stealth_async_client(timeout=45.0) as client:
|
||||
@@ -260,6 +263,8 @@ async def devii_ws(websocket: WebSocket):
|
||||
if command == "reset":
|
||||
await session.reset()
|
||||
continue
|
||||
if await session.try_answer_interaction(text):
|
||||
continue
|
||||
if svc.quota_exceeded(owner_kind, owner_id, owner_is_admin):
|
||||
limit = svc.daily_limit_for(owner_kind, owner_is_admin)
|
||||
audit.record_system(
|
||||
@@ -302,6 +307,11 @@ async def devii_ws(websocket: WebSocket):
|
||||
)
|
||||
elif kind in ("avatar_result", "client_result"):
|
||||
session.resolve_query(str(data.get("id", "")), data.get("result"))
|
||||
elif kind == "interaction_result":
|
||||
session.resolve_interaction(
|
||||
str(data.get("id", data.get("interaction_id", ""))),
|
||||
data.get("result") or data,
|
||||
)
|
||||
except WebSocketDisconnect:
|
||||
pass
|
||||
except Exception: # noqa: BLE001 - never let the socket loop crash the worker
|
||||
|
||||
@@ -57,6 +57,7 @@ LANGUAGES = [
|
||||
("yaml", "YAML"),
|
||||
("json", "JSON"),
|
||||
("markdown", "Markdown"),
|
||||
("markdown_rendered", "Markdown Rendered"),
|
||||
("swift", "Swift"),
|
||||
("php", "PHP"),
|
||||
("ruby", "Ruby"),
|
||||
|
||||
@@ -231,7 +231,7 @@ async def send_message(request: Request, data: Annotated[MessageForm, Depends(js
|
||||
async def broadcast_message(
|
||||
sender: dict, message: dict, client_id: Optional[str] = None
|
||||
) -> None:
|
||||
frame = message_frame(message, sender.get("username", ""), client_id)
|
||||
frame = message_frame(message, sender.get("username", ""), client_id, sender_role=sender.get("role"))
|
||||
message_hub.mark_delivered(message["uid"])
|
||||
targets = [message["sender_uid"], message["receiver_uid"]]
|
||||
await message_hub.send_to_users(targets, frame)
|
||||
|
||||
@@ -6,6 +6,7 @@ from devplacepy.routers.profile import (
|
||||
avatar,
|
||||
award,
|
||||
customization,
|
||||
interactions,
|
||||
notifications,
|
||||
telegram,
|
||||
)
|
||||
@@ -17,6 +18,7 @@ router.include_router(customization.router)
|
||||
router.include_router(notifications.router)
|
||||
router.include_router(ai_correction.router)
|
||||
router.include_router(ai_modifier.router)
|
||||
router.include_router(interactions.router)
|
||||
router.include_router(avatar.router)
|
||||
router.include_router(telegram.router)
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ from devplacepy.database import (
|
||||
get_notification_prefs,
|
||||
get_user_stars,
|
||||
get_user_rank,
|
||||
get_user_post_count,
|
||||
get_comment_counts_by_post_uids,
|
||||
get_reactions_by_targets,
|
||||
get_user_bookmarks,
|
||||
@@ -211,9 +212,7 @@ async def profile_page(
|
||||
)
|
||||
for g in gists_raw:
|
||||
gists.append({"gist": g, "time_ago": time_ago(g["created_at"])})
|
||||
posts_count = get_table("posts").count(
|
||||
user_uid=profile_user["uid"], deleted_at=None
|
||||
)
|
||||
posts_count = get_user_post_count(profile_user["uid"])
|
||||
|
||||
activities = []
|
||||
if tab == "activity":
|
||||
@@ -302,6 +301,18 @@ async def profile_page(
|
||||
if is_owner
|
||||
else None
|
||||
)
|
||||
from devplacepy.services.devii.interaction import prefs as interaction_prefs
|
||||
|
||||
interactions_snap = (
|
||||
interaction_prefs.snapshot("user", profile_user["uid"], profile_user)
|
||||
if is_owner
|
||||
else {
|
||||
"enabled": True,
|
||||
"source": None,
|
||||
"default": True,
|
||||
"override": None,
|
||||
}
|
||||
)
|
||||
from devplacepy.services.telegram import store as telegram_store
|
||||
|
||||
telegram_paired = (
|
||||
@@ -402,6 +413,10 @@ async def profile_page(
|
||||
"ai_modifier_enabled": ai_modifier_enabled,
|
||||
"ai_modifier_sync": ai_modifier_sync,
|
||||
"ai_modifier_prompt": ai_modifier_prompt,
|
||||
"interactions_enabled": interactions_snap["enabled"],
|
||||
"interactions_source": interactions_snap["source"],
|
||||
"interactions_default": interactions_snap["default"],
|
||||
"interactions_override": interactions_snap["override"],
|
||||
"telegram_paired": telegram_paired,
|
||||
"notif_telegram_paired": notif_telegram_paired,
|
||||
"can_manage_customization": can_manage_customization,
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
|
||||
import logging
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import Depends, APIRouter, Request
|
||||
|
||||
from devplacepy.models import InteractionsForm
|
||||
from devplacepy.responses import action_result
|
||||
from devplacepy.services.audit import record as audit
|
||||
from devplacepy.services.devii.interaction import prefs
|
||||
from devplacepy.routers.profile._shared import resolve_customization_target
|
||||
from devplacepy.dependencies import json_or_form
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/{username}/interactions")
|
||||
async def set_interactions(
|
||||
request: Request,
|
||||
username: str,
|
||||
data: Annotated[InteractionsForm, Depends(json_or_form(InteractionsForm))],
|
||||
):
|
||||
target, denied = resolve_customization_target(request, username)
|
||||
if denied is not None:
|
||||
return denied
|
||||
if data.reset:
|
||||
snap = prefs.set_user_pref(target["uid"], None)
|
||||
summary = f"reset interactive widgets to admin default for {target['username']}"
|
||||
new_value = -1
|
||||
else:
|
||||
snap = prefs.set_user_pref(target["uid"], bool(data.enabled))
|
||||
summary = (
|
||||
f"{'enabled' if data.enabled else 'disabled'} interactive widgets "
|
||||
f"for {target['username']}"
|
||||
)
|
||||
new_value = 1 if data.enabled else 0
|
||||
logger.info(summary)
|
||||
audit.record(
|
||||
request,
|
||||
"profile.interactions",
|
||||
target_type="user",
|
||||
target_uid=target["uid"],
|
||||
target_label=target["username"],
|
||||
new_value=new_value,
|
||||
summary=summary,
|
||||
links=[audit.target("user", target["uid"], target["username"])],
|
||||
)
|
||||
url = f"/profile/{target['username']}"
|
||||
return action_result(
|
||||
request,
|
||||
url,
|
||||
data={
|
||||
"url": url,
|
||||
"enabled": snap["enabled"],
|
||||
"source": snap["source"],
|
||||
"default": snap["default"],
|
||||
"override": snap["override"],
|
||||
},
|
||||
)
|
||||
@@ -4,6 +4,7 @@ import logging
|
||||
|
||||
import httpx
|
||||
from fastapi import APIRouter, Request
|
||||
from starlette.requests import ClientDisconnect
|
||||
from starlette.responses import PlainTextResponse, Response
|
||||
|
||||
from devplacepy.config import XMLRPC_BIND, XMLRPC_PORT
|
||||
@@ -49,7 +50,11 @@ async def info(request: Request, path: str = "") -> PlainTextResponse:
|
||||
@router.post("/")
|
||||
@router.post("/{path:path}")
|
||||
async def proxy(request: Request, path: str = "") -> Response:
|
||||
body = await request.body()
|
||||
try:
|
||||
body = await request.body()
|
||||
except ClientDisconnect:
|
||||
logger.warning("XML-RPC client disconnected before request body was read")
|
||||
return PlainTextResponse("Client disconnected", status_code=400)
|
||||
headers = {
|
||||
key: value
|
||||
for key, value in request.headers.items()
|
||||
|
||||
Reference in New Issue
Block a user