fix: correct typo in commit message from "UPDATXE" to proper format
This commit is contained in:
+48
-6
@@ -402,6 +402,10 @@ def backfill_api_keys() -> int:
|
||||
users = db["users"]
|
||||
if not users.has_column("api_key"):
|
||||
users.create_column_by_example("api_key", "")
|
||||
if not users.has_column("cust_disable_global"):
|
||||
users.create_column_by_example("cust_disable_global", 0)
|
||||
if not users.has_column("cust_disable_pagetype"):
|
||||
users.create_column_by_example("cust_disable_pagetype", 0)
|
||||
import uuid_utils
|
||||
|
||||
updated = 0
|
||||
@@ -493,13 +497,14 @@ def _in_clause(uids, prefix="p"):
|
||||
|
||||
|
||||
def get_users_by_uids(uids):
|
||||
if not uids:
|
||||
if not uids or "users" not in db.tables:
|
||||
return {}
|
||||
users = db["users"]
|
||||
if "uid" not in users.columns:
|
||||
return {}
|
||||
seen = set()
|
||||
unique = [u for u in uids if u not in seen and not seen.add(u)]
|
||||
return {
|
||||
u["uid"]: u for u in db["users"].find(db["users"].table.columns.uid.in_(unique))
|
||||
}
|
||||
return {u["uid"]: u for u in users.find(users.table.columns.uid.in_(unique))}
|
||||
|
||||
|
||||
def get_comment_counts_by_post_uids(post_uids):
|
||||
@@ -874,6 +879,37 @@ def _customization_key(owner_kind: str, owner_id: str, page_type: str) -> str:
|
||||
return f"{owner_kind}\x1f{owner_id}\x1f{page_type}"
|
||||
|
||||
|
||||
CUSTOMIZATION_PREF_COLUMNS = {
|
||||
"global": "cust_disable_global",
|
||||
"pagetype": "cust_disable_pagetype",
|
||||
}
|
||||
|
||||
|
||||
def get_customization_prefs(owner_kind: str, owner_id: str) -> dict:
|
||||
if owner_kind != "user" or "users" not in db.tables:
|
||||
return {"disable_global": False, "disable_pagetype": False}
|
||||
user = db["users"].find_one(uid=owner_id)
|
||||
if user is None:
|
||||
return {"disable_global": False, "disable_pagetype": False}
|
||||
return {
|
||||
"disable_global": bool(user.get("cust_disable_global", 0)),
|
||||
"disable_pagetype": bool(user.get("cust_disable_pagetype", 0)),
|
||||
}
|
||||
|
||||
|
||||
def set_customization_pref(owner_id: str, category: str, disabled: bool) -> None:
|
||||
column = CUSTOMIZATION_PREF_COLUMNS.get(category)
|
||||
if column is None:
|
||||
raise ValueError(f"Unknown customization category: {category}")
|
||||
from devplacepy.utils import clear_user_cache
|
||||
|
||||
get_table("users").update(
|
||||
{"uid": owner_id, column: 1 if disabled else 0}, ["uid"]
|
||||
)
|
||||
clear_user_cache(owner_id)
|
||||
bump_cache_version("customizations")
|
||||
|
||||
|
||||
def get_custom_overrides(owner_kind: str, owner_id: str, page_type: str) -> dict:
|
||||
sync_local_cache("customizations", _customizations_cache)
|
||||
key = _customization_key(owner_kind, owner_id, page_type)
|
||||
@@ -882,6 +918,7 @@ def get_custom_overrides(owner_kind: str, owner_id: str, page_type: str) -> dict
|
||||
return cached
|
||||
result = {"css": "", "js": ""}
|
||||
if "user_customizations" in db.tables:
|
||||
prefs = get_customization_prefs(owner_kind, owner_id)
|
||||
scopes = (CUSTOMIZATION_GLOBAL_SCOPE, page_type)
|
||||
rows = db["user_customizations"].find(
|
||||
owner_kind=owner_kind,
|
||||
@@ -892,8 +929,13 @@ def get_custom_overrides(owner_kind: str, owner_id: str, page_type: str) -> dict
|
||||
for row in rows:
|
||||
lang = row.get("lang")
|
||||
scope = row.get("scope")
|
||||
if lang in pieces and scope in scopes:
|
||||
pieces[lang][scope] = row.get("code") or ""
|
||||
if lang not in pieces or scope not in scopes:
|
||||
continue
|
||||
if scope == CUSTOMIZATION_GLOBAL_SCOPE and prefs["disable_global"]:
|
||||
continue
|
||||
if scope != CUSTOMIZATION_GLOBAL_SCOPE and prefs["disable_pagetype"]:
|
||||
continue
|
||||
pieces[lang][scope] = row.get("code") or ""
|
||||
for lang in CUSTOMIZATION_LANGS:
|
||||
ordered = [pieces[lang][scope] for scope in scopes if scope in pieces[lang]]
|
||||
result[lang] = "\n".join(part for part in ordered if part.strip())
|
||||
|
||||
@@ -1422,6 +1422,62 @@ four ways to sign requests.
|
||||
],
|
||||
sample_response={"api_key": "NEW_UUID"},
|
||||
),
|
||||
endpoint(
|
||||
id="profile-customization-global",
|
||||
method="POST",
|
||||
path="/profile/{username}/customization/global",
|
||||
title="Toggle site-wide customizations",
|
||||
summary="Show or suppress your site-wide custom CSS and JS without deleting it. Admins may target any user.",
|
||||
auth="user",
|
||||
encoding="form",
|
||||
destructive=True,
|
||||
params=[
|
||||
field(
|
||||
"username",
|
||||
"path",
|
||||
"string",
|
||||
True,
|
||||
"bob_test",
|
||||
"Profile owner. Must be yourself unless you are an admin.",
|
||||
),
|
||||
field(
|
||||
"value",
|
||||
"form",
|
||||
"boolean",
|
||||
False,
|
||||
"0",
|
||||
"1 to show your site-wide customizations, 0 to suppress them.",
|
||||
),
|
||||
],
|
||||
),
|
||||
endpoint(
|
||||
id="profile-customization-pagetype",
|
||||
method="POST",
|
||||
path="/profile/{username}/customization/pagetype",
|
||||
title="Toggle per-page customizations",
|
||||
summary="Show or suppress your per-page custom CSS and JS without deleting it. Admins may target any user.",
|
||||
auth="user",
|
||||
encoding="form",
|
||||
destructive=True,
|
||||
params=[
|
||||
field(
|
||||
"username",
|
||||
"path",
|
||||
"string",
|
||||
True,
|
||||
"bob_test",
|
||||
"Profile owner. Must be yourself unless you are an admin.",
|
||||
),
|
||||
field(
|
||||
"value",
|
||||
"form",
|
||||
"boolean",
|
||||
False,
|
||||
"0",
|
||||
"1 to show your per-page customizations, 0 to suppress them.",
|
||||
),
|
||||
],
|
||||
),
|
||||
endpoint(
|
||||
id="follow-user",
|
||||
method="POST",
|
||||
@@ -3185,6 +3241,14 @@ _ACTION_RESPONSES = {
|
||||
),
|
||||
"gists-delete": ("/gists", None),
|
||||
"profile-update": ("/profile/YOUR_USERNAME", None),
|
||||
"profile-customization-global": (
|
||||
"/profile/YOUR_USERNAME",
|
||||
{"url": "/profile/YOUR_USERNAME", "cust_disable_global": True},
|
||||
),
|
||||
"profile-customization-pagetype": (
|
||||
"/profile/YOUR_USERNAME",
|
||||
{"url": "/profile/YOUR_USERNAME", "cust_disable_pagetype": True},
|
||||
),
|
||||
"follow-user": ("/profile/bob_test", None),
|
||||
"unfollow-user": ("/profile/bob_test", None),
|
||||
"messages-send": ("/messages?with_uid=RECEIVER_UID", {"uid": "MESSAGE_UID"}),
|
||||
|
||||
@@ -169,6 +169,10 @@ class ProjectFlagForm(BaseModel):
|
||||
value: bool = False
|
||||
|
||||
|
||||
class CustomizationToggleForm(BaseModel):
|
||||
value: bool = False
|
||||
|
||||
|
||||
class ForkForm(BaseModel):
|
||||
title: str = Field(min_length=1, max_length=200)
|
||||
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import logging
|
||||
from typing import Annotated
|
||||
from fastapi import APIRouter, Request, Form
|
||||
from devplacepy.models import ProfileForm
|
||||
from fastapi.responses import HTMLResponse, JSONResponse
|
||||
from devplacepy.models import ProfileForm, CustomizationToggleForm
|
||||
from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse
|
||||
from devplacepy.database import (
|
||||
get_table,
|
||||
db,
|
||||
get_customization_prefs,
|
||||
set_customization_pref,
|
||||
get_user_stars,
|
||||
get_user_rank,
|
||||
get_comment_counts_by_post_uids,
|
||||
@@ -250,6 +252,12 @@ async def profile_page(
|
||||
if (is_owner or viewer_is_admin)
|
||||
else None
|
||||
)
|
||||
can_manage_customization = is_owner or viewer_is_admin
|
||||
customization_prefs = (
|
||||
get_customization_prefs("user", profile_user["uid"])
|
||||
if can_manage_customization
|
||||
else {"disable_global": False, "disable_pagetype": False}
|
||||
)
|
||||
|
||||
base = site_url(request)
|
||||
robots = "noindex,follow" if posts_count < 2 else "index,follow"
|
||||
@@ -296,6 +304,9 @@ async def profile_page(
|
||||
"is_owner": is_owner,
|
||||
"can_view_api_key": can_view_api_key,
|
||||
"api_key": api_key,
|
||||
"can_manage_customization": can_manage_customization,
|
||||
"cust_disable_global": customization_prefs["disable_global"],
|
||||
"cust_disable_pagetype": customization_prefs["disable_pagetype"],
|
||||
"ai_quota": ai_quota,
|
||||
"activities": activities,
|
||||
"rank": rank,
|
||||
@@ -339,3 +350,52 @@ async def regenerate_api_key(request: Request):
|
||||
clear_user_cache(user["uid"])
|
||||
logger.info(f"API key regenerated for {user['username']}")
|
||||
return JSONResponse({"api_key": new_key})
|
||||
|
||||
|
||||
def _resolve_customization_target(request: Request, username: str):
|
||||
from devplacepy.responses import wants_json, json_error
|
||||
|
||||
current_user = require_user(request)
|
||||
target = get_table("users").find_one(username=username)
|
||||
if not target:
|
||||
raise not_found("User not found")
|
||||
is_owner = current_user["uid"] == target["uid"]
|
||||
is_admin = current_user.get("role") == "Admin"
|
||||
if not (is_owner or is_admin):
|
||||
if wants_json(request):
|
||||
return None, json_error(403, "Not allowed")
|
||||
return None, RedirectResponse(url=f"/profile/{username}", status_code=302)
|
||||
return target, None
|
||||
|
||||
|
||||
def _toggle_customization(
|
||||
request: Request, username: str, category: str, value: bool
|
||||
):
|
||||
target, denied = _resolve_customization_target(request, username)
|
||||
if denied is not None:
|
||||
return denied
|
||||
set_customization_pref(target["uid"], category, disabled=not value)
|
||||
column = "cust_disable_global" if category == "global" else "cust_disable_pagetype"
|
||||
logger.info(
|
||||
f"Customization {category} {'enabled' if value else 'disabled'} for {target['username']}"
|
||||
)
|
||||
url = f"/profile/{target['username']}"
|
||||
return action_result(request, url, data={"url": url, column: not value})
|
||||
|
||||
|
||||
@router.post("/{username}/customization/global")
|
||||
async def set_customization_global(
|
||||
request: Request,
|
||||
username: str,
|
||||
data: Annotated[CustomizationToggleForm, Form()],
|
||||
):
|
||||
return _toggle_customization(request, username, "global", data.value)
|
||||
|
||||
|
||||
@router.post("/{username}/customization/pagetype")
|
||||
async def set_customization_pagetype(
|
||||
request: Request,
|
||||
username: str,
|
||||
data: Annotated[CustomizationToggleForm, Form()],
|
||||
):
|
||||
return _toggle_customization(request, username, "pagetype", data.value)
|
||||
|
||||
@@ -505,6 +505,9 @@ class ProfileOut(_Out):
|
||||
is_owner: bool = False
|
||||
can_view_api_key: bool = False
|
||||
api_key: Optional[str] = None
|
||||
can_manage_customization: bool = False
|
||||
cust_disable_global: bool = False
|
||||
cust_disable_pagetype: bool = False
|
||||
ai_quota: Optional[dict] = None
|
||||
activities: list[Any] = []
|
||||
rank: Optional[int] = None
|
||||
|
||||
@@ -131,4 +131,31 @@ CUSTOMIZATION_ACTIONS: tuple[Action, ...] = (
|
||||
),
|
||||
),
|
||||
),
|
||||
Action(
|
||||
name="customize_set_enabled",
|
||||
method="LOCAL",
|
||||
path="",
|
||||
summary="Show or suppress the user's customizations without deleting them",
|
||||
description=(
|
||||
"Toggles whether the user's saved customizations render, keeping the stored code intact. "
|
||||
"category='global' controls the site-wide customizations; category='pagetype' controls every "
|
||||
"per-page-type customization at once. enabled=false hides them; enabled=true shows them again. "
|
||||
"Mirrors the toggle buttons on the user's profile page."
|
||||
),
|
||||
handler="customization",
|
||||
requires_auth=True,
|
||||
params=(
|
||||
arg(
|
||||
"category",
|
||||
"Which set to toggle: 'global' (whole site) or 'pagetype' (all per-page customizations).",
|
||||
required=True,
|
||||
),
|
||||
arg(
|
||||
"enabled",
|
||||
"true to show the customizations, false to suppress them.",
|
||||
required=True,
|
||||
kind="boolean",
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -137,7 +137,9 @@ SYSTEM_PROMPT = (
|
||||
"after the application boots, with access to window, document and the global 'app'. After saving, "
|
||||
"call reload_page so the server-applied version is shown. List existing customizations with "
|
||||
"customize_list, and remove them with customize_reset (confirm=true; scope='all' removes "
|
||||
"everything) to restore the default look and behaviour.\n\n"
|
||||
"everything) to restore the default look and behaviour. To hide saved customizations WITHOUT "
|
||||
"deleting them (the same toggles on the user's profile page), call customize_set_enabled with "
|
||||
"category='global' or 'pagetype' and enabled=false; enabled=true brings them back.\n\n"
|
||||
"USER-DEFINED TOOLS (VIBE TOOLS)\n"
|
||||
"The user can invent new tools for you by describing them ('when I say X, do Y'). When they do, "
|
||||
"call tool_create with a short trigger-oriented description (so you know when to use it), the "
|
||||
|
||||
@@ -8,10 +8,13 @@ from typing import Any
|
||||
|
||||
from devplacepy.database import (
|
||||
CUSTOMIZATION_LANGS,
|
||||
CUSTOMIZATION_PREF_COLUMNS,
|
||||
delete_custom_override,
|
||||
get_custom_override,
|
||||
get_customization_prefs,
|
||||
list_custom_overrides,
|
||||
set_custom_override,
|
||||
set_customization_pref,
|
||||
)
|
||||
from devplacepy.services.devii.errors import ToolInputError
|
||||
|
||||
@@ -36,6 +39,8 @@ class CustomizationController:
|
||||
return self._set(arguments, "js", "js")
|
||||
if name == "customize_reset":
|
||||
return self._reset(arguments)
|
||||
if name == "customize_set_enabled":
|
||||
return self._set_enabled(arguments)
|
||||
raise ToolInputError(f"Unknown customization tool: {name}")
|
||||
|
||||
def _scope(self, arguments: dict[str, Any]) -> str:
|
||||
@@ -68,8 +73,15 @@ class CustomizationController:
|
||||
}
|
||||
for row in rows
|
||||
]
|
||||
prefs = get_customization_prefs(self._owner_kind, self._owner_id)
|
||||
return json.dumps(
|
||||
{"status": "success", "customizations": items}, ensure_ascii=False
|
||||
{
|
||||
"status": "success",
|
||||
"customizations": items,
|
||||
"disabled_global": prefs["disable_global"],
|
||||
"disabled_pagetype": prefs["disable_pagetype"],
|
||||
},
|
||||
ensure_ascii=False,
|
||||
)
|
||||
|
||||
def _get(self, arguments: dict[str, Any]) -> str:
|
||||
@@ -111,6 +123,36 @@ class CustomizationController:
|
||||
ensure_ascii=False,
|
||||
)
|
||||
|
||||
def _category(self, arguments: dict[str, Any]) -> str:
|
||||
category = str(arguments.get("category", "")).strip().lower()
|
||||
if category not in CUSTOMIZATION_PREF_COLUMNS:
|
||||
raise ToolInputError("'category' must be 'global' or 'pagetype'.")
|
||||
return category
|
||||
|
||||
def _enabled(self, arguments: dict[str, Any]) -> bool:
|
||||
raw = arguments.get("enabled")
|
||||
if isinstance(raw, bool):
|
||||
return raw
|
||||
return str(raw).strip().lower() in ("true", "1", "yes", "on")
|
||||
|
||||
def _set_enabled(self, arguments: dict[str, Any]) -> str:
|
||||
if self._owner_kind != "user":
|
||||
raise ToolInputError(
|
||||
"Suppressing customizations is only available for signed-in users."
|
||||
)
|
||||
category = self._category(arguments)
|
||||
enabled = self._enabled(arguments)
|
||||
set_customization_pref(self._owner_id, category, disabled=not enabled)
|
||||
return json.dumps(
|
||||
{
|
||||
"status": "success",
|
||||
"category": category,
|
||||
"enabled": enabled,
|
||||
"note": "Saved. Call reload_page so the user sees it applied by the server.",
|
||||
},
|
||||
ensure_ascii=False,
|
||||
)
|
||||
|
||||
def _reset(self, arguments: dict[str, Any]) -> str:
|
||||
scope = self._scope(arguments)
|
||||
lang = self._lang(arguments)
|
||||
|
||||
@@ -367,6 +367,49 @@ a.profile-stat-value:hover {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.customization-card {
|
||||
margin-top: 1rem;
|
||||
padding: 0.875rem;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
background: var(--bg-elevated, var(--bg-card));
|
||||
}
|
||||
|
||||
.customization-head {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.customization-title {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.customization-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.5rem;
|
||||
padding: 0.4rem 0;
|
||||
}
|
||||
|
||||
.customization-label {
|
||||
font-size: 0.8125rem;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.customization-row form {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.customization-hint {
|
||||
margin-top: 0.5rem;
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.follow-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -16,6 +16,7 @@ import { ReactionBar } from "./ReactionBar.js";
|
||||
import { BookmarkManager } from "./BookmarkManager.js";
|
||||
import { PollManager } from "./PollManager.js";
|
||||
import { ApiKeyManager } from "./ApiKeyManager.js";
|
||||
import { CustomizationToggle } from "./CustomizationToggle.js";
|
||||
import { UserAiUsage } from "./UserAiUsage.js";
|
||||
import { Tabs } from "./Tabs.js";
|
||||
import { DeviiTerminal } from "./DeviiTerminal.js";
|
||||
@@ -47,6 +48,7 @@ class Application {
|
||||
this.bookmarks = new BookmarkManager();
|
||||
this.polls = new PollManager();
|
||||
this.apiKey = new ApiKeyManager();
|
||||
this.customizationToggle = new CustomizationToggle();
|
||||
this.userAiUsage = new UserAiUsage();
|
||||
this.tabs = new Tabs();
|
||||
this.windows = new WindowManager();
|
||||
|
||||
@@ -160,6 +160,29 @@
|
||||
<p class="api-key-hint">Use this key with <code>X-API-KEY</code>, <code>Authorization: Bearer</code>, or HTTP Basic auth on any request. {% if is_owner %}Regenerating immediately invalidates the old key.{% endif %}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if can_manage_customization %}
|
||||
<div class="customization-card" data-customization data-username="{{ profile_user['username'] }}">
|
||||
<div class="customization-head">
|
||||
<span class="customization-title">{% if is_owner %}My customizations{% else %}{{ profile_user['username'] }}'s customizations{% endif %}</span>
|
||||
</div>
|
||||
<div class="customization-row">
|
||||
<span class="customization-label">Site-wide custom CSS and JS</span>
|
||||
<form method="POST" action="/profile/{{ profile_user['username'] }}/customization/global" data-customization-form>
|
||||
<input type="hidden" name="value" value="{{ 1 if cust_disable_global else 0 }}">
|
||||
<button type="submit" class="btn btn-sm {% if cust_disable_global %}btn-primary{% endif %}" data-customization-toggle="global" data-confirm="{% if cust_disable_global %}Re-enable your site-wide custom CSS and JS?{% else %}Disable your site-wide custom CSS and JS? Your saved code is kept and can be re-enabled.{% endif %}">{% if cust_disable_global %}Enable{% else %}Disable{% endif %}</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="customization-row">
|
||||
<span class="customization-label">Per-page custom CSS and JS</span>
|
||||
<form method="POST" action="/profile/{{ profile_user['username'] }}/customization/pagetype" data-customization-form>
|
||||
<input type="hidden" name="value" value="{{ 1 if cust_disable_pagetype else 0 }}">
|
||||
<button type="submit" class="btn btn-sm {% if cust_disable_pagetype %}btn-primary{% endif %}" data-customization-toggle="pagetype" data-confirm="{% if cust_disable_pagetype %}Re-enable your per-page custom CSS and JS?{% else %}Disable your per-page custom CSS and JS? Your saved code is kept and can be re-enabled.{% endif %}">{% if cust_disable_pagetype %}Enable{% else %}Disable{% endif %}</button>
|
||||
</form>
|
||||
</div>
|
||||
<p class="customization-hint">Disabling hides your custom code without deleting it.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</aside>
|
||||
|
||||
<div class="profile-content">
|
||||
|
||||
Reference in New Issue
Block a user