Compare commits

..
Author SHA1 Message Date
Typosaurus 7d4ae7dd4c ticket #21 attempt 1 2026-07-19 17:51:09 +00:00
19 changed files with 22 additions and 120 deletions
File diff suppressed because one or more lines are too long
-4
View File
@@ -39,10 +39,6 @@ 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
-1
View File
@@ -169,7 +169,6 @@ 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
-1
View File
@@ -68,7 +68,6 @@ 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`"
+1 -3
View File
@@ -22,7 +22,7 @@ from .comments import _drop_blocked, _build_comment_items, load_comments, get_re
from .content import resolve_by_slug, resolve_object_url, get_uids_by_username_match, text_search_clause, get_daily_topic, get_featured_news from .content import resolve_by_slug, resolve_object_url, get_uids_by_username_match, text_search_clause, get_daily_topic, get_featured_news
from .attachments_data import get_attachments, get_attachments_by_type, get_news_images_by_uids, delete_attachment_record, delete_attachments, _delete_attachment_file, get_user_media, get_deleted_media from .attachments_data import get_attachments, get_attachments_by_type, get_news_images_by_uids, delete_attachment_record, delete_attachments, _delete_attachment_file, get_user_media, get_deleted_media
from .stats import _stats_cache, get_site_stats, _analytics_cache, get_platform_analytics, _gist_languages_cache, get_gist_languages from .stats import _stats_cache, get_site_stats, _analytics_cache, get_platform_analytics, _gist_languages_cache, get_gist_languages
from .schema import BUG_TABLE_RENAMES, migrate_bug_tables_to_issue_tables, init_db, _refresh_query_planner_stats, OLD_GATEWAY_URL, migrate_ai_gateway_settings, backfill_api_keys, _backfill_gamification from .schema import init_db, _refresh_query_planner_stats, OLD_GATEWAY_URL, migrate_ai_gateway_settings, backfill_api_keys, _backfill_gamification
__all__ = [ __all__ = [
"dataset", "dataset",
@@ -222,8 +222,6 @@ __all__ = [
"get_platform_analytics", "get_platform_analytics",
"_gist_languages_cache", "_gist_languages_cache",
"get_gist_languages", "get_gist_languages",
"BUG_TABLE_RENAMES",
"migrate_bug_tables_to_issue_tables",
"init_db", "init_db",
"_refresh_query_planner_stats", "_refresh_query_planner_stats",
"OLD_GATEWAY_URL", "OLD_GATEWAY_URL",
-25
View File
@@ -6,30 +6,6 @@ from .soft_delete import SOFT_DELETE_TABLES, ensure_soft_delete_columns
from .ranking import _authors_cache from .ranking import _authors_cache
BUG_TABLE_RENAMES = (
("bug_tickets", "issue_tickets"),
("bug_comment_authors", "issue_comment_authors"),
)
def migrate_bug_tables_to_issue_tables() -> None:
for source_name, destination_name in BUG_TABLE_RENAMES:
if source_name not in db.tables:
continue
source = db[source_name]
destination = get_table(destination_name)
copied = 0
for row in source.all():
payload = {key: value for key, value in row.items() if key != "id"}
destination.insert_ignore(payload, ["uid"])
copied += 1
logger.info(
"Migrated %s rows from %s into %s", copied, source_name, destination_name
)
source.drop()
logger.info("Dropped table %s after migration", source_name)
def init_db(): def init_db():
tables = db.tables tables = db.tables
_index(db, "users", "idx_users_username", ["username"]) _index(db, "users", "idx_users_username", ["username"])
@@ -356,7 +332,6 @@ def init_db():
_index( _index(
db, "issue_comment_authors", "idx_issue_comment_authors_number", ["gitea_number"] db, "issue_comment_authors", "idx_issue_comment_authors_number", ["gitea_number"]
) )
migrate_bug_tables_to_issue_tables()
_index(db, "service_state", "idx_service_state_name", ["name"]) _index(db, "service_state", "idx_service_state_name", ["name"])
if "devii_conversations" in db.tables: if "devii_conversations" in db.tables:
conversations = get_table("devii_conversations") conversations = get_table("devii_conversations")
+2 -3
View File
@@ -1,6 +1,5 @@
# 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
@@ -230,7 +229,7 @@ four ways to sign requests.
"textarea", "textarea",
True, True,
"Nice work.", "Nice work.",
f"Body, 3-{COMMENT_MAX_LENGTH} characters.", "Body, 3-1000 characters.",
), ),
field( field(
"target_uid", "target_uid",
@@ -291,7 +290,7 @@ four ways to sign requests.
"textarea", "textarea",
True, True,
"Edited body.", "Edited body.",
f"New body, 3-{COMMENT_MAX_LENGTH} characters.", "New body, 3-1000 characters.",
), ),
], ],
sample_response={ sample_response={
+1 -3
View File
@@ -1,7 +1,5 @@
# 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"}
@@ -239,7 +237,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=f"Comment text, 1-{COMMENT_MAX_LENGTH} chars."), field("comment", "body", type="textarea", required=True, example="Great rant!", description="Comment text, 1-1000 chars."),
], ],
sample_response={"success": True}, sample_response={"success": True},
), ),
+3 -3
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 COMMENT_MAX_LENGTH, DEFAULT_CORRECTION_PROMPT, DEFAULT_MODIFIER_PROMPT from devplacepy.config import 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=COMMENT_MAX_LENGTH) content: str = Field(min_length=3, max_length=1000)
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=COMMENT_MAX_LENGTH) content: str = Field(min_length=3, max_length=1000)
class ProjectForm(BaseModel): class ProjectForm(BaseModel):
+1 -2
View File
@@ -69,8 +69,7 @@ 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()
from devplacepy.config import COMMENT_MAX_LENGTH if len(text) < 1 or len(text) > 1000:
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(
{ {
+1 -2
View File
@@ -227,8 +227,7 @@ 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.")
from devplacepy.config import COMMENT_MAX_LENGTH if len(text) > 1000:
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()
+1 -1
View File
@@ -72,7 +72,7 @@ MENTION_POLL_SECONDS = int(os.environ.get("MENTION_POLL_SECONDS", "30"))
DM_POLL_SECONDS = int(os.environ.get("DM_POLL_SECONDS", "10")) DM_POLL_SECONDS = int(os.environ.get("DM_POLL_SECONDS", "10"))
BOT_USERNAME = os.environ.get("BOT_USERNAME", "") BOT_USERNAME = os.environ.get("BOT_USERNAME", "")
COMMENT_CHAR_LIMIT = int(os.environ.get("DEVPLACE_COMMENT_MAX_LENGTH", "1000")) COMMENT_CHAR_LIMIT = 1000
MESSAGE_CHAR_LIMIT = 2000 MESSAGE_CHAR_LIMIT = 2000
PART_SUFFIX_RESERVE = 12 PART_SUFFIX_RESERVE = 12
PART_DELIVERY_DELAY = 0.5 PART_DELIVERY_DELAY = 0.5
@@ -2,7 +2,6 @@
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
@@ -29,7 +28,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", f"New comment body, 3-{COMMENT_MAX_LENGTH} characters.", required=True), body("content", "New comment body, 3-1000 characters.", required=True),
), ),
), ),
Action( Action(
-2
View File
@@ -83,8 +83,6 @@ 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");
+2 -2
View File
@@ -1,9 +1,9 @@
{% if user %} {% if user %}
<form class="comment-form" method="POST" action="/comments/create" data-comment-maxlength="{{ comment_max_length }}"> <form class="comment-form" method="POST" action="/comments/create">
<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="{{ comment_max_length }}" class="emoji-picker-target" data-mention></textarea> <textarea name="content" placeholder="Your opinion goes here..." required aria-required="true" aria-label="Comment" maxlength="1000" 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() }}"
+1 -2
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 COMMENT_MAX_LENGTH, STATIC_VERSION, TEMPLATES_DIR, TEMPLATE_AUTO_RELOAD, PRESENCE_TIMEOUT_SECONDS from devplacepy.config import 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,7 +67,6 @@ 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
+8 -28
View File
@@ -1,28 +1,8 @@
2026-07-19T08:11:05 INFO logging initialised at /workspace/repo/dpc.log 2026-07-19T17:34:12 INFO logging initialised at /workspace/repo/dpc.log
2026-07-19T08:11:05 DEBUG model=molodetz-pro fps=30 2026-07-19T17:34:12 DEBUG model=molodetz-pro fps=30
2026-07-19T08:11:05 INFO read task from file: /workspace/prompts/research-1.txt 2026-07-19T17:34:12 INFO read task from file: /workspace/prompts/research-9.txt
2026-07-19T08:11:05 INFO settings merged: model=<default> allow=0 deny=0 ask=0 2026-07-19T17:34:12 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-19T17:38:50 INFO logging initialised at /workspace/repo/dpc.log
2026-07-19T15:03:09 DEBUG model=molodetz-pro fps=30 2026-07-19T17:38:50 DEBUG model=molodetz-pro fps=30
2026-07-19T15:03:09 INFO read task from file: /workspace/prompts/research-2.txt 2026-07-19T17:38:50 INFO read task from file: /workspace/prompts/execution-1.txt
2026-07-19T15:03:09 INFO settings merged: model=<default> allow=0 deny=0 ask=0 2026-07-19T17:38:50 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
2026-07-19T18:31:31 INFO logging initialised at /workspace/repo/dpc.log
2026-07-19T18:31:31 DEBUG model=molodetz-pro fps=30
2026-07-19T18:31:31 INFO read task from file: /workspace/prompts/execution-2.txt
2026-07-19T18:31:31 INFO settings merged: model=<default> allow=0 deny=0 ask=0
2026-07-19T18:58:51 INFO logging initialised at /workspace/repo/dpc.log
2026-07-19T18:58:51 DEBUG model=molodetz-pro fps=30
2026-07-19T18:58:51 INFO read task from file: /workspace/prompts/execution-3.txt
2026-07-19T18:58:51 INFO settings merged: model=<default> allow=0 deny=0 ask=0
-19
View File
@@ -345,22 +345,3 @@ 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
-16
View File
@@ -129,19 +129,3 @@ 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"