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
8 changed files with 102 additions and 103 deletions
File diff suppressed because one or more lines are too long
-31
View File
@@ -360,37 +360,6 @@ def create_comment_record(
comment_url, comment_url,
) )
if target_type == "post":
posts_table = get_table("posts")
post_record = posts_table.find_one(uid=target_uid)
if not post_record:
post_record = posts_table.find_one(slug=target_uid)
commenter_uids = set()
for c in get_table("comments").find(
target_type="post", target_uid=target_uid, deleted_at=None
):
commenter_uids.add(c["user_uid"])
commenter_uids.discard(user["uid"])
if post_record:
commenter_uids.discard(post_record["user_uid"])
if parent_uid:
parent_comment = get_table("comments").find_one(
uid=parent_uid, deleted_at=None
)
if parent_comment and parent_comment["user_uid"] != user["uid"]:
commenter_uids.discard(parent_comment["user_uid"])
for commenter_uid in commenter_uids:
create_notification(
commenter_uid,
"thread_comment",
f"{user['username']} also commented on a post you commented on",
user["uid"],
comment_url,
)
create_mention_notifications(content, user["uid"], comment_url) create_mention_notifications(content, user["uid"], comment_url)
schedule_correction(user, "comments", comment_uid, request) schedule_correction(user, "comments", comment_uid, request)
schedule_modification(user, "comments", comment_uid, request) schedule_modification(user, "comments", comment_uid, request)
-1
View File
@@ -19,7 +19,6 @@ NOTIFICATION_TYPES = [
{"key": "harvest_stolen", "label": "Farm raids", "description": "Someone steals a ready build from your Code Farm"}, {"key": "harvest_stolen", "label": "Farm raids", "description": "Someone steals a ready build from your Code Farm"},
{"key": "award", "label": "Awards", "description": "Someone gives you an award on your profile"}, {"key": "award", "label": "Awards", "description": "Someone gives you an award on your profile"},
{"key": "system", "label": "System alerts", "description": "Platform infrastructure alerts (e.g. the AI gateway going down)"}, {"key": "system", "label": "System alerts", "description": "Platform infrastructure alerts (e.g. the AI gateway going down)"},
{"key": "thread_comment", "label": "Thread comments", "description": "Someone else comments on a post you commented on"},
] ]
+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"])
+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",
+50
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,9 +88,32 @@ 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()
try:
messages_table.insert( messages_table.insert(
{ {
"uid": msg_uid, "uid": msg_uid,
@@ -94,6 +124,24 @@ def persist_message(
"created_at": created_at, "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,
+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"
-60
View File
@@ -384,66 +384,6 @@ def test_comment_notification_on_post(app_server, browser, seeded_db):
ctx_b.close() ctx_b.close()
def test_thread_comment_notification(app_server, browser, seeded_db):
from tests.conftest import login_user
ctx_a = browser.new_context(viewport={"width": 1400, "height": 900})
ctx_b = browser.new_context(viewport={"width": 1400, "height": 900})
pa = ctx_a.new_page()
pb = ctx_b.new_page()
pa.set_default_timeout(15000)
pb.set_default_timeout(15000)
login_user(pa, seeded_db["alice"])
login_user(pb, seeded_db["bob"])
pa.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
pa.locator(".feed-fab").first.wait_for(state="visible", timeout=10000)
pa.locator(".feed-fab").first.click()
pa.fill("#post-content", "Post for thread comment notification test")
pa.locator("#create-post-modal button.btn-primary:has-text('Post')").click()
pa.wait_for_url("**/posts/*", timeout=10000, wait_until="domcontentloaded")
post_url = pa.url
pb.goto(post_url, wait_until="domcontentloaded")
pb.wait_for_timeout(1000)
comment_textarea = pb.locator("form.comment-form textarea[name='content']").first
comment_textarea.wait_for(state="visible", timeout=10000)
comment_textarea.fill("Bob's top-level comment")
pb.locator("button.comment-form-submit").first.click()
pb.wait_for_timeout(1500)
pb_body = pb.locator("body").text_content()
assert "Internal Server Error" not in pb_body, f"Bob got 500: {pb_body[:300]}"
pa.goto(post_url, wait_until="domcontentloaded")
pa.wait_for_timeout(1000)
comment_textarea = pa.locator("form.comment-form textarea[name='content']").first
comment_textarea.wait_for(state="visible", timeout=10000)
comment_textarea.fill("Alice also comments on her own post")
pa.locator("button.comment-form-submit").first.click()
pa.wait_for_timeout(1500)
pa_body = pa.locator("body").text_content()
assert "Internal Server Error" not in pa_body, f"Alice got 500: {pa_body[:300]}"
pb.goto(f"{BASE_URL}/notifications", wait_until="domcontentloaded")
pb.wait_for_timeout(2000)
body = pb.locator("body").text_content()
assert "Internal Server Error" not in body, (
f"Got 500 error on notifications: {body[:500]}"
)
assert "alice_test" in body, (
f"Expected 'alice_test' in notifications, got: {body[:500]}"
)
assert "also commented" in body, (
f"Expected 'also commented' in notifications, got: {body[:500]}"
)
ctx_a.close()
ctx_b.close()
def test_follow_notification(app_server, browser, seeded_db): def test_follow_notification(app_server, browser, seeded_db):
from tests.conftest import login_user from tests.conftest import login_user