Compare commits

..
Author SHA1 Message Date
Typosaurus 54a7805a90 ticket #85 attempt 1 2026-07-23 01:31:16 +00:00
Typosaurus 1d1efc0dfc ticket #85 attempt 1 2026-07-23 01:19:19 +00:00
10 changed files with 104 additions and 94 deletions
File diff suppressed because one or more lines are too long
+21
View File
@@ -30,6 +30,26 @@ def migrate_bug_tables_to_issue_tables() -> None:
logger.info("Dropped table %s after migration", source_name) logger.info("Dropped table %s after migration", source_name)
def _add_message_unique_index() -> None:
if "messages" not in db.tables:
return
with db:
db.query(
"""
DELETE FROM messages WHERE id NOT IN (
SELECT MIN(id) FROM messages GROUP BY sender_uid, receiver_uid, content
)
"""
)
_index(
db,
"messages",
"idx_messages_unique_sender_receiver_content",
["sender_uid", "receiver_uid", "content"],
unique=True,
)
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"])
@@ -131,6 +151,7 @@ def init_db():
"idx_messages_conversation_rev", "idx_messages_conversation_rev",
["receiver_uid", "sender_uid"], ["receiver_uid", "sender_uid"],
) )
_add_message_unique_index()
_index(db, "notifications", "idx_notifications_user", ["user_uid"]) _index(db, "notifications", "idx_notifications_user", ["user_uid"])
_index(db, "notifications", "idx_notifications_user_read", ["user_uid", "read"]) _index(db, "notifications", "idx_notifications_user_read", ["user_uid", "read"])
_index(db, "push_registration", "idx_push_registration_user", ["user_uid"]) _index(db, "push_registration", "idx_push_registration_user", ["user_uid"])
-2
View File
@@ -38,7 +38,6 @@ def field(
example="", example="",
description="", description="",
options=None, options=None,
nullable=False,
): ):
spec = { spec = {
"name": name, "name": name,
@@ -47,7 +46,6 @@ def field(
"required": required, "required": required,
"example": example, "example": example,
"description": description, "description": description,
"nullable": nullable,
} }
if options: if options:
spec["options"] = list(options) spec["options"] = list(options)
+2 -11
View File
@@ -39,15 +39,7 @@ def _unwrap_optional(annotation):
return annotation return annotation
def _is_optional(annotation): def _value(name, annotation, stack):
if get_origin(annotation) is Union:
return type(None) in get_args(annotation)
return False
def _value(name, annotation, stack, is_nullable=False):
if is_nullable:
return None
annotation = _unwrap_optional(annotation) annotation = _unwrap_optional(annotation)
origin = get_origin(annotation) origin = get_origin(annotation)
if origin in (list, set, tuple): if origin in (list, set, tuple):
@@ -79,8 +71,7 @@ def _from_model(model, stack):
stack = stack | {model} stack = stack | {model}
example = {} example = {}
for name, info in model.model_fields.items(): for name, info in model.model_fields.items():
is_nullable = _is_optional(info.annotation) example[name] = _value(name, info.annotation, stack)
example[name] = _value(name, info.annotation, stack, is_nullable)
return example return example
+1 -1
View File
@@ -88,7 +88,7 @@ def gateway_complete(
{"role": "system", "content": system}, {"role": "system", "content": system},
{"role": "user", "content": text}, {"role": "user", "content": text},
], ],
"temperature": 0.1, "temperature": 0.0,
} }
headers = { headers = {
"Content-Type": "application/json", "Content-Type": "application/json",
+59 -9
View File
@@ -1,6 +1,9 @@
# retoor <retoor@molodetz.nl> # retoor <retoor@molodetz.nl>
import hashlib
import logging import logging
import time
from collections import OrderedDict
from datetime import datetime, timezone from datetime import datetime, timezone
from typing import Any, Optional from typing import Any, Optional
@@ -15,12 +18,16 @@ from devplacepy.utils import (
track_action, track_action,
) )
from devplacepy.services.audit import record as audit from devplacepy.services.audit import record as audit
from sqlalchemy.exc import IntegrityError
from devplacepy.services.correction import schedule_correction from devplacepy.services.correction import schedule_correction
from devplacepy.services.ai_modifier import schedule_modification from devplacepy.services.ai_modifier import schedule_modification
logger = logging.getLogger("messaging.persist") logger = logging.getLogger("messaging.persist")
MAX_CONTENT_LENGTH = 2000 MAX_CONTENT_LENGTH = 2000
DEDUP_WINDOW_SECONDS = 3
_content_cache: dict[str, tuple[float, str]] = OrderedDict()
def _slim_attachment(attachment: dict[str, Any]) -> dict[str, Any]: def _slim_attachment(attachment: dict[str, Any]) -> dict[str, Any]:
@@ -81,19 +88,60 @@ def persist_message(
sender_uid = sender["uid"] sender_uid = sender["uid"]
sender_username = sender.get("username", "") sender_username = sender.get("username", "")
content_hash = hashlib.sha256(
f"{sender_uid}:{receiver_uid}:{content}".encode()
).hexdigest()[:16]
now = time.time()
last_seen, cached_uid = _content_cache.get(content_hash, (0.0, None))
if now - last_seen < DEDUP_WINDOW_SECONDS and cached_uid is not None:
logger.debug(
"Dedup hit for message hash %s (original uid %s)", content_hash, cached_uid
)
cached = get_table("messages").find_one(uid=cached_uid)
if cached:
return {
"uid": cached["uid"],
"sender_uid": cached["sender_uid"],
"receiver_uid": cached["receiver_uid"],
"content": cached["content"],
"read": cached.get("read", False),
"created_at": cached["created_at"],
}
messages_table = get_table("messages") messages_table = get_table("messages")
msg_uid = generate_uid() msg_uid = generate_uid()
created_at = datetime.now(timezone.utc).isoformat() created_at = datetime.now(timezone.utc).isoformat()
messages_table.insert(
{ try:
"uid": msg_uid, messages_table.insert(
"sender_uid": sender_uid, {
"receiver_uid": receiver_uid, "uid": msg_uid,
"content": content, "sender_uid": sender_uid,
"read": False, "receiver_uid": receiver_uid,
"created_at": created_at, "content": content,
"read": False,
"created_at": created_at,
}
)
except IntegrityError:
existing = messages_table.find_one(
sender_uid=sender_uid, receiver_uid=receiver_uid, content=content
)
if not existing:
raise
logger.debug(
"Dedup via unique constraint for message (uid %s)", existing["uid"]
)
_content_cache[content_hash] = (time.time(), existing["uid"])
return {
"uid": existing["uid"],
"sender_uid": existing["sender_uid"],
"receiver_uid": existing["receiver_uid"],
"content": existing["content"],
"read": existing.get("read", False),
"created_at": existing["created_at"],
} }
)
link_attachments(attachment_uids, "message", msg_uid) link_attachments(attachment_uids, "message", msg_uid)
schedule_correction(sender, "messages", msg_uid, request) schedule_correction(sender, "messages", msg_uid, request)
@@ -114,6 +162,8 @@ def persist_message(
) )
track_action(sender_uid, "message") track_action(sender_uid, "message")
_content_cache[content_hash] = (time.time(), msg_uid)
logger.info( logger.info(
"Message %s sent from %s to %s via %s", "Message %s sent from %s to %s via %s",
msg_uid, msg_uid,
-10
View File
@@ -218,16 +218,6 @@
font-weight: 700; font-weight: 700;
} }
.param-nullable {
font-size: 0.625rem;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--text-muted);
border: 1px dashed var(--border);
border-radius: 999px;
padding: 0.05rem 0.4rem;
}
.param-loc { .param-loc {
font-size: 0.625rem; font-size: 0.625rem;
text-transform: uppercase; text-transform: uppercase;
-1
View File
@@ -139,7 +139,6 @@ export class ApiTester {
const label = this.el("div", { class: "param-label" }, [ const label = this.el("div", { class: "param-label" }, [
this.el("span", { class: "param-name", text: param.name }), this.el("span", { class: "param-name", text: param.name }),
param.required ? this.el("span", { class: "param-required", text: "*" }) : null, param.required ? this.el("span", { class: "param-required", text: "*" }) : null,
param.nullable ? this.el("span", { class: "param-nullable", text: "nullable" }) : null,
this.el("span", { class: "param-loc param-loc-" + param.location, text: param.location }), this.el("span", { class: "param-loc param-loc-" + param.location, text: param.location }),
]); ]);
const allowed = param.type === "enum" && param.options && param.options.length const allowed = param.type === "enum" && param.options && param.options.length
+20
View File
@@ -166,3 +166,23 @@ def test_send_attachment_only_empty_content_succeeds(seeded_db):
refresh_snapshot() refresh_snapshot()
row = get_table("messages").find_one(uid=msg["uid"]) row = get_table("messages").find_one(uid=msg["uid"])
assert row["content"] == "" assert row["content"] == ""
def test_duplicate_message_returns_same_uid(seeded_db):
s, _ = _member()
receiver = _db_user("bob_test")["uid"]
content = _unique("dupmsg")
first = s.post(
f"{BASE_URL}/messages/send",
headers=JSON_audit_log,
data={"content": content, "receiver_uid": receiver},
).json()["data"]
second = s.post(
f"{BASE_URL}/messages/send",
headers=JSON_audit_log,
data={"content": content, "receiver_uid": receiver},
).json()["data"]
assert first["uid"] == second["uid"], "duplicate messages should return the same uid"
-59
View File
@@ -1,59 +0,0 @@
# retoor <retoor@molodetz.nl>
from typing import Optional
from pydantic import BaseModel
from devplacepy.docs_api._shared import field
from devplacepy.docs_examples import _is_optional, schema_example
def test_field_nullable_parameter():
f = field("bio", "query", "string", False, "", "User biography", nullable=True)
assert f["nullable"] is True
f2 = field("username", "path", "string", True, "alice", "Target username")
assert f2["nullable"] is False
def test_is_optional_optional_type():
assert _is_optional(Optional[str]) is True
assert _is_optional(Optional[int]) is True
assert _is_optional(Optional[list[str]]) is True
def test_is_optional_non_optional():
assert _is_optional(str) is False
assert _is_optional(int) is False
assert _is_optional(list[str]) is False
assert _is_optional(dict) is False
def test_schema_example_nullable_fields():
class NullableModel(BaseModel):
name: str
bio: Optional[str] = None
age: int
score: Optional[int] = None
tags: Optional[list[str]] = None
result = schema_example(NullableModel)
assert result["name"] == "string"
assert result["age"] == 0
assert result["bio"] is None
assert result["score"] is None
assert result["tags"] is None
def test_schema_example_non_nullable_fields_unaffected():
class StrictModel(BaseModel):
x: str
y: int
z: bool
result = schema_example(StrictModel)
assert result["x"] == "string"
assert result["y"] == 0
assert result["z"] is False