ticket #65 attempt 1

This commit is contained in:
Typosaurus 2026-07-19 18:16:23 +00:00
parent 818568c609
commit 7cc39076bf
15 changed files with 82 additions and 12 deletions

View File

@ -39,6 +39,10 @@ NGINX_MAX_BODY_SIZE=50m
NGINX_CACHE_ENABLED=false NGINX_CACHE_ENABLED=false
NGINX_CACHE_MAX_SIZE=1g NGINX_CACHE_MAX_SIZE=1g
# Max length of comment text (applied server-side on write and client-side on input).
# Change at any time via the env var; the DB has no length constraint.
DEVPLACE_COMMENT_MAX_LENGTH=1000
# Run the app container as this host user so shared files keep dev ownership. # Run the app container as this host user so shared files keep dev ownership.
DEVPLACE_UID=1000 DEVPLACE_UID=1000
DEVPLACE_GID=1000 DEVPLACE_GID=1000

View File

@ -169,6 +169,7 @@ The log is **administrator-only**. `/admin/audit-log` is a paginated, filterable
| `DEVPLACE_PRESENCE_TIMEOUT_SECONDS` | `60` | Online-presence window: a user counts as online for this many seconds after their last activity. `last_seen` is refreshed by a throttled in-place update at most once per half this interval per worker (no per-load inserts, no data growth) | | `DEVPLACE_PRESENCE_TIMEOUT_SECONDS` | `60` | Online-presence window: a user counts as online for this many seconds after their last activity. `last_seen` is refreshed by a throttled in-place update at most once per half this interval per worker (no per-load inserts, no data growth) |
| `DEVPLACE_PRESENCE_ONLINE_LIMIT` | `30` | Maximum avatars shown in the feed's live "Online now" panel (ordered alphabetically by username) | | `DEVPLACE_PRESENCE_ONLINE_LIMIT` | `30` | Maximum avatars shown in the feed's live "Online now" panel (ordered alphabetically by username) |
| `DEVPLACE_PRESENCE_ONLINE_MARGIN_SECONDS` | `20` | Grace margin before an online user drops to offline (hysteresis): online at the timeout, offline only after timeout + this. Prevents online/offline flicker for users hovering at the boundary | | `DEVPLACE_PRESENCE_ONLINE_MARGIN_SECONDS` | `20` | Grace margin before an online user drops to offline (hysteresis): online at the timeout, offline only after timeout + this. Prevents online/offline flicker for users hovering at the boundary |
| `DEVPLACE_COMMENT_MAX_LENGTH` | `1000` | Maximum character length for comment body text. Applied server-side via Pydantic validation and client-side via `maxLength` on textareas. The database has no column-length constraint, so this can be changed at any time via the environment variable (requires restart) |
### Runtime settings ### Runtime settings

View File

@ -68,6 +68,7 @@ INTERNAL_GATEWAY_URL = f"{INTERNAL_BASE_URL}/openai/v1/chat/completions"
INTERNAL_EMBED_URL = f"{INTERNAL_BASE_URL}/openai/v1/embeddings" INTERNAL_EMBED_URL = f"{INTERNAL_BASE_URL}/openai/v1/embeddings"
INTERNAL_MODEL = "molodetz" INTERNAL_MODEL = "molodetz"
INTERNAL_EMBED_MODEL = "molodetz~embed" INTERNAL_EMBED_MODEL = "molodetz~embed"
COMMENT_MAX_LENGTH = int(environ.get("DEVPLACE_COMMENT_MAX_LENGTH", "1000"))
DEFAULT_CORRECTION_PROMPT = "Leave literary as is, only do punctuation and casing" DEFAULT_CORRECTION_PROMPT = "Leave literary as is, only do punctuation and casing"
DEFAULT_MODIFIER_PROMPT = ( DEFAULT_MODIFIER_PROMPT = (
"Execute what is behind `@ai` (the prompt) and replace that part including `@ai`" "Execute what is behind `@ai` (the prompt) and replace that part including `@ai`"

View File

@ -1,5 +1,6 @@
# retoor <retoor@molodetz.nl> # retoor <retoor@molodetz.nl>
from devplacepy.config import COMMENT_MAX_LENGTH
from .._shared import COMMENT_TARGETS, GIST_LANGUAGES, PROJECT_TYPES, endpoint, field from .._shared import COMMENT_TARGETS, GIST_LANGUAGES, PROJECT_TYPES, endpoint, field
from devplacepy.constants import TOPICS from devplacepy.constants import TOPICS
@ -229,7 +230,7 @@ four ways to sign requests.
"textarea", "textarea",
True, True,
"Nice work.", "Nice work.",
"Body, 3-1000 characters.", f"Body, 3-{COMMENT_MAX_LENGTH} characters.",
), ),
field( field(
"target_uid", "target_uid",
@ -290,7 +291,7 @@ four ways to sign requests.
"textarea", "textarea",
True, True,
"Edited body.", "Edited body.",
"New body, 3-1000 characters.", f"New body, 3-{COMMENT_MAX_LENGTH} characters.",
), ),
], ],
sample_response={ sample_response={

View File

@ -1,5 +1,7 @@
# retoor <retoor@molodetz.nl> # retoor <retoor@molodetz.nl>
from devplacepy.config import COMMENT_MAX_LENGTH
ROLE_LABELS = {"public": "Public", "user": "Member"} ROLE_LABELS = {"public": "Public", "user": "Member"}
@ -237,7 +239,7 @@ DEVRANT_GROUPS = {
encoding="form", encoding="form",
params=[ params=[
field("rant_id", "path", type="int", required=True, example="1", description="Rant id."), field("rant_id", "path", type="int", required=True, example="1", description="Rant id."),
field("comment", "body", type="textarea", required=True, example="Great rant!", description="Comment text, 1-1000 chars."), field("comment", "body", type="textarea", required=True, example="Great rant!", description=f"Comment text, 1-{COMMENT_MAX_LENGTH} chars."),
], ],
sample_response={"success": True}, sample_response={"success": True},
), ),

View File

@ -6,7 +6,7 @@ from datetime import datetime
from typing import Literal, Optional from typing import Literal, Optional
from pydantic import BaseModel, Field, field_validator, model_validator from pydantic import BaseModel, Field, field_validator, model_validator
from devplacepy.constants import TOPICS, REACTION_EMOJI from devplacepy.constants import TOPICS, REACTION_EMOJI
from devplacepy.config import DEFAULT_CORRECTION_PROMPT, DEFAULT_MODIFIER_PROMPT from devplacepy.config import COMMENT_MAX_LENGTH, DEFAULT_CORRECTION_PROMPT, DEFAULT_MODIFIER_PROMPT
def normalize_european_date(value): def normalize_european_date(value):
@ -157,7 +157,7 @@ class PostEditForm(BaseModel):
class CommentForm(BaseModel): class CommentForm(BaseModel):
content: str = Field(min_length=3, max_length=1000) content: str = Field(min_length=3, max_length=COMMENT_MAX_LENGTH)
target_uid: str = Field(default="", max_length=36) target_uid: str = Field(default="", max_length=36)
post_uid: str = Field(default="", max_length=36) post_uid: str = Field(default="", max_length=36)
target_type: Literal["post", "project", "news", "issue", "gist"] = "post" target_type: Literal["post", "project", "news", "issue", "gist"] = "post"
@ -172,7 +172,7 @@ class CommentForm(BaseModel):
class CommentEditForm(BaseModel): class CommentEditForm(BaseModel):
content: str = Field(min_length=3, max_length=1000) content: str = Field(min_length=3, max_length=COMMENT_MAX_LENGTH)
class ProjectForm(BaseModel): class ProjectForm(BaseModel):

View File

@ -69,7 +69,8 @@ async def edit_comment(request: Request, comment_id: str):
if not is_owner(comment, user): if not is_owner(comment, user):
return dr_error("You can only edit your own comments.", fail_reason="not_owner") return dr_error("You can only edit your own comments.", fail_reason="not_owner")
text = (params.get("comment") or "").strip() text = (params.get("comment") or "").strip()
if len(text) < 1 or len(text) > 1000: from devplacepy.config import COMMENT_MAX_LENGTH
if len(text) < 1 or len(text) > COMMENT_MAX_LENGTH:
return dr_error("Invalid comment length.", fail_reason="length") return dr_error("Invalid comment length.", fail_reason="length")
get_table("comments").update( get_table("comments").update(
{ {

View File

@ -227,7 +227,8 @@ async def comment_rant(request: Request, rant_id: str):
text = (params.get("comment") or "").strip() text = (params.get("comment") or "").strip()
if len(text) < 1: if len(text) < 1:
return dr_error("Your comment is too short.") return dr_error("Your comment is too short.")
if len(text) > 1000: from devplacepy.config import COMMENT_MAX_LENGTH
if len(text) > COMMENT_MAX_LENGTH:
return dr_error("Your comment is too long.") return dr_error("Your comment is too long.")
create_comment_record(request, user, "post", post["uid"], text) create_comment_record(request, user, "post", post["uid"], text)
return dr_ok() return dr_ok()

View File

@ -2,6 +2,7 @@
from __future__ import annotations from __future__ import annotations
from devplacepy.config import COMMENT_MAX_LENGTH
from ..spec import Action from ..spec import Action
from ._shared import ATTACHMENTS, TARGET_TYPE, body, confirm, path from ._shared import ATTACHMENTS, TARGET_TYPE, body, confirm, path
@ -28,7 +29,7 @@ COMMENTS_ACTIONS: tuple[Action, ...] = (
summary="Edit the body of one of your own comments", summary="Edit the body of one of your own comments",
params=( params=(
path("comment_uid", "Uid of the comment."), path("comment_uid", "Uid of the comment."),
body("content", "New comment body, 3-1000 characters.", required=True), body("content", f"New comment body, 3-{COMMENT_MAX_LENGTH} characters.", required=True),
), ),
), ),
Action( Action(

View File

@ -83,6 +83,8 @@ export class CommentManager {
textarea.className = "emoji-picker-target"; textarea.className = "emoji-picker-target";
textarea.value = text.dataset.raw || text.textContent; textarea.value = text.dataset.raw || text.textContent;
textarea.rows = 3; textarea.rows = 3;
const ref = document.querySelector("[data-comment-maxlength]");
textarea.maxLength = ref ? parseInt(ref.dataset.commentMaxlength, 10) : 1000;
form.appendChild(textarea); form.appendChild(textarea);
const actions = document.createElement("div"); const actions = document.createElement("div");

View File

@ -1,9 +1,9 @@
{% if user %} {% if user %}
<form class="comment-form" method="POST" action="/comments/create"> <form class="comment-form" method="POST" action="/comments/create" data-comment-maxlength="{{ comment_max_length }}">
<input type="hidden" name="target_uid" value="{{ _comment_target_uid }}"> <input type="hidden" name="target_uid" value="{{ _comment_target_uid }}">
<input type="hidden" name="target_type" value="{{ _comment_target_type }}"> <input type="hidden" name="target_type" value="{{ _comment_target_type }}">
{% set _user = user %}{% set _size = 32 %}{% set _size_class = "sm" %}{% include "_avatar_link.html" %} {% set _user = user %}{% set _size = 32 %}{% set _size_class = "sm" %}{% include "_avatar_link.html" %}
<textarea name="content" placeholder="Your opinion goes here..." required aria-required="true" aria-label="Comment" maxlength="1000" class="emoji-picker-target" data-mention></textarea> <textarea name="content" placeholder="Your opinion goes here..." required aria-required="true" aria-label="Comment" maxlength="{{ comment_max_length }}" class="emoji-picker-target" data-mention></textarea>
<div class="comment-form-actions"> <div class="comment-form-actions">
<dp-upload multiple <dp-upload multiple
max-size="{{ max_upload_size_mb() }}" max-size="{{ max_upload_size_mb() }}"

View File

@ -6,7 +6,7 @@ import jinja2
from fastapi.templating import Jinja2Templates from fastapi.templating import Jinja2Templates
from markupsafe import Markup, escape from markupsafe import Markup, escape
from devplacepy.cache import TTLCache from devplacepy.cache import TTLCache
from devplacepy.config import STATIC_VERSION, TEMPLATES_DIR, TEMPLATE_AUTO_RELOAD, PRESENCE_TIMEOUT_SECONDS from devplacepy.config import COMMENT_MAX_LENGTH, STATIC_VERSION, TEMPLATES_DIR, TEMPLATE_AUTO_RELOAD, PRESENCE_TIMEOUT_SECONDS
from devplacepy.constants import TOPICS, REACTION_EMOJI from devplacepy.constants import TOPICS, REACTION_EMOJI
from devplacepy.database import get_int_setting, get_setting, get_table from devplacepy.database import get_int_setting, get_setting, get_table
from devplacepy.avatar import avatar_url, avatar_seed from devplacepy.avatar import avatar_url, avatar_seed
@ -67,6 +67,7 @@ templates.env.globals["is_self"] = is_self
templates.env.globals["guest_disabled"] = guest_disabled templates.env.globals["guest_disabled"] = guest_disabled
templates.env.globals["is_online"] = presence.is_online templates.env.globals["is_online"] = presence.is_online
templates.env.globals["presence_timeout"] = PRESENCE_TIMEOUT_SECONDS templates.env.globals["presence_timeout"] = PRESENCE_TIMEOUT_SECONDS
templates.env.globals["comment_max_length"] = COMMENT_MAX_LENGTH
from devplacepy.docs_devrant import devrant_endpoints from devplacepy.docs_devrant import devrant_endpoints

20
dpc.log Normal file
View File

@ -0,0 +1,20 @@
2026-07-19T08:11:05 INFO logging initialised at /workspace/repo/dpc.log
2026-07-19T08:11:05 DEBUG model=molodetz-pro fps=30
2026-07-19T08:11:05 INFO read task from file: /workspace/prompts/research-1.txt
2026-07-19T08:11:05 INFO settings merged: model=<default> allow=0 deny=0 ask=0
2026-07-19T15:03:09 INFO logging initialised at /workspace/repo/dpc.log
2026-07-19T15:03:09 DEBUG model=molodetz-pro fps=30
2026-07-19T15:03:09 INFO read task from file: /workspace/prompts/research-2.txt
2026-07-19T15:03:09 INFO settings merged: model=<default> allow=0 deny=0 ask=0
2026-07-19T15:40:16 INFO logging initialised at /workspace/repo/dpc.log
2026-07-19T15:40:16 DEBUG model=molodetz-pro fps=30
2026-07-19T15:40:16 INFO read task from file: /workspace/prompts/research-3.txt
2026-07-19T15:40:16 INFO settings merged: model=<default> allow=0 deny=0 ask=0
2026-07-19T17:35:33 INFO logging initialised at /workspace/repo/dpc.log
2026-07-19T17:35:33 DEBUG model=molodetz-pro fps=30
2026-07-19T17:35:33 INFO read task from file: /workspace/prompts/research-4.txt
2026-07-19T17:35:33 INFO settings merged: model=<default> allow=0 deny=0 ask=0
2026-07-19T18:11:47 INFO logging initialised at /workspace/repo/dpc.log
2026-07-19T18:11:47 DEBUG model=molodetz-pro fps=30
2026-07-19T18:11:47 INFO read task from file: /workspace/prompts/execution-1.txt
2026-07-19T18:11:47 INFO settings merged: model=<default> allow=0 deny=0 ask=0

View File

@ -345,3 +345,22 @@ def test_comment_links_multiple_attachments(app_server):
assert u1 in r.text and u2 in r.text, ( assert u1 in r.text and u2 in r.text, (
"both attachments must be linked and displayed on the comment" "both attachments must be linked and displayed on the comment"
) )
def test_create_comment_too_long_rejected(app_server):
from devplacepy.config import COMMENT_MAX_LENGTH
s, _ = _session_comments()
post_uid = _create_post_comments(s, f"cmt-long-{int(time.time() * 1000)}")
r = s.post(
f"{BASE_URL}/comments/create",
data={
"content": "x" * (COMMENT_MAX_LENGTH + 1),
"target_type": "post",
"post_uid": post_uid,
"target_uid": post_uid,
},
allow_redirects=False,
)
assert r.status_code in (400, 422), r.text[:300]
refresh_snapshot()
assert get_table("comments").find_one(target_uid=post_uid) is None

View File

@ -129,3 +129,19 @@ def test_edit_comment_too_short_rejected(app_server):
assert r.status_code in (400, 422) assert r.status_code in (400, 422)
refresh_snapshot() refresh_snapshot()
assert get_table("comments").find_one(uid=comment["uid"])["content"] == "Long enough body" assert get_table("comments").find_one(uid=comment["uid"])["content"] == "Long enough body"
def test_edit_comment_too_long_rejected(app_server):
from devplacepy.config import COMMENT_MAX_LENGTH
s, _ = _session_edit()
post_uid = _create_post_edit(s, f"edit-long-{int(time.time() * 1000)}")
comment = _create_comment_edit(s, post_uid, "Normal body length")
r = s.post(
f"{BASE_URL}/comments/edit/{comment['uid']}",
headers=JSON_edit,
data={"content": "x" * (COMMENT_MAX_LENGTH + 1)},
allow_redirects=False,
)
assert r.status_code in (400, 422), r.text[:300]
refresh_snapshot()
assert get_table("comments").find_one(uid=comment["uid"])["content"] == "Normal body length"