Compare commits

..
Author SHA1 Message Date
Typosaurus aae9c3698c ticket #89 attempt 1 2026-07-23 01:35:14 +00:00
8 changed files with 19 additions and 104 deletions
File diff suppressed because one or more lines are too long
-21
View File
@@ -30,26 +30,6 @@ 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"])
@@ -151,7 +131,6 @@ 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"])
+3
View File
@@ -44,6 +44,7 @@ from devplacepy.templating import templates, jinja_unread_count
from devplacepy.cache import TTLCache from devplacepy.cache import TTLCache
from devplacepy.responses import respond, wants_json, json_error from devplacepy.responses import respond, wants_json, json_error
from devplacepy.schemas import LandingOut, ValidationErrorOut from devplacepy.schemas import LandingOut, ValidationErrorOut
from devplacepy.attachments import get_attachments_batch
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse
from devplacepy.utils import get_current_user, time_ago, safe_next, client_ip from devplacepy.utils import get_current_user, time_ago, safe_next, client_ip
from devplacepy.seo import base_seo_context, site_url, website_schema from devplacepy.seo import base_seo_context, site_url, website_schema
@@ -671,6 +672,7 @@ def _landing_recent_posts(blocked):
authors = get_users_by_uids(author_uids) authors = get_users_by_uids(author_uids)
comment_counts = get_comment_counts_by_post_uids(post_uids) comment_counts = get_comment_counts_by_post_uids(post_uids)
upvotes, downvotes = get_vote_counts(post_uids) upvotes, downvotes = get_vote_counts(post_uids)
attachments_map = get_attachments_batch("post", post_uids)
for p in raw_posts: for p in raw_posts:
posts.append( posts.append(
{ {
@@ -680,6 +682,7 @@ def _landing_recent_posts(blocked):
"comment_count": comment_counts.get(p["uid"], 0), "comment_count": comment_counts.get(p["uid"], 0),
"stars": upvotes.get(p["uid"], 0) - downvotes.get(p["uid"], 0), "stars": upvotes.get(p["uid"], 0) - downvotes.get(p["uid"], 0),
"slug": p.get("slug", "") or p["uid"], "slug": p.get("slug", "") or p["uid"],
"attachments": attachments_map.get(p["uid"], []),
} }
) )
if not blocked: if not blocked:
+3 -1
View File
@@ -7,7 +7,6 @@ from devplacepy.models import ProfileForm
from fastapi.responses import HTMLResponse, JSONResponse from fastapi.responses import HTMLResponse, JSONResponse
from devplacepy.database import ( from devplacepy.database import (
get_table, get_table,
db,
get_customization_prefs, get_customization_prefs,
get_notification_prefs, get_notification_prefs,
get_user_stars, get_user_stars,
@@ -48,6 +47,7 @@ from devplacepy.utils import (
) )
from devplacepy.responses import respond, action_result, wants_json from devplacepy.responses import respond, action_result, wants_json
from devplacepy.schemas import ProfileOut from devplacepy.schemas import ProfileOut
from devplacepy.attachments import get_attachments_batch
from devplacepy.avatar import avatar_url, avatar_seed from devplacepy.avatar import avatar_url, avatar_seed
from devplacepy.seo import ( from devplacepy.seo import (
base_seo_context, base_seo_context,
@@ -188,8 +188,10 @@ async def profile_page(
else set() else set()
) )
polls_map = get_polls_by_post_uids(post_uids, current_user) polls_map = get_polls_by_post_uids(post_uids, current_user)
attachments_map = get_attachments_batch("post", post_uids)
for item in posts: for item in posts:
uid = item["post"]["uid"] uid = item["post"]["uid"]
item["attachments"] = attachments_map.get(uid, [])
item["reactions"] = reactions_map.get(uid, {"counts": {}, "mine": []}) item["reactions"] = reactions_map.get(uid, {"counts": {}, "mine": []})
item["bookmarked"] = uid in bookmark_set item["bookmarked"] = uid in bookmark_set
item["poll"] = polls_map.get(uid) item["poll"] = polls_map.get(uid)
+2 -1
View File
@@ -5,7 +5,7 @@ from __future__ import annotations
from typing import Any, Optional from typing import Any, Optional
from devplacepy.schemas.base import _Out from devplacepy.schemas.base import _Out
from devplacepy.schemas.content import UserOut from devplacepy.schemas.content import AttachmentOut, UserOut
class AuthPageOut(_Out): class AuthPageOut(_Out):
@@ -38,6 +38,7 @@ class LandingPostOut(_Out):
comment_count: int = 0 comment_count: int = 0
stars: int = 0 stars: int = 0
slug: str = "" slug: str = ""
attachments: list[AttachmentOut] = []
class TrendingTopicOut(_Out): class TrendingTopicOut(_Out):
+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.0, "temperature": 0.1,
} }
headers = { headers = {
"Content-Type": "application/json", "Content-Type": "application/json",
+9 -59
View File
@@ -1,9 +1,6 @@
# 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
@@ -18,16 +15,12 @@ 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]:
@@ -88,60 +81,19 @@ 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: {
messages_table.insert( "uid": msg_uid,
{ "sender_uid": sender_uid,
"uid": msg_uid, "receiver_uid": receiver_uid,
"sender_uid": sender_uid, "content": content,
"receiver_uid": receiver_uid, "read": False,
"content": content, "created_at": created_at,
"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)
@@ -162,8 +114,6 @@ 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,
-20
View File
@@ -166,23 +166,3 @@ 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"