Notifications: a new "thread" type notifies every other commenter on a
post whenever anyone comments on it, disregarding reply hierarchy -
excluding the actor and whoever already got a comment/reply
notification for that same event, so no one is double-notified.
Implemented via a background-deferred fan-out mirroring the existing
mention-notification pattern.
SEO: discussion_forum_posting() now embeds up to 20 of a post's
comments as nested schema.org Comment entities (not just an aggregate
count), and a new /topics hub plus /topics/{topic} pages give the
feed's topic filter real, independently crawlable/indexable URLs -
/feed?topic=X was never indexable since its canonical strips the
query string back to bare /feed. Both are wired end to end (schemas,
Devii actions, docs API, sitemap, locustfile load-test coverage).
Quiz player: the auto-advance to the next question used to hide the
just-answered slide in the same tick as rendering the grade, so on
any multi-question quiz the Correct/Not correct feedback was never
actually visible before the view moved on. Delayed via setTimeout,
with the pending timer cleared on manual navigation and on
disconnect so it can't race or fire on a removed component.
Also includes other local changes already in progress in this
working tree before this session (messaging, push delivery,
deepsearch jobs, game economy, quiz builder) - verified by the full
suite passing (3467 tests) but not authored or individually reviewed
in this session.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VL9Xn57W5UR3HZbbuuzxdK
168 lines
4.6 KiB
Python
168 lines
4.6 KiB
Python
# retoor <retoor@molodetz.nl>
|
|
|
|
import dataset
|
|
import logging
|
|
from pathlib import Path
|
|
from sqlalchemy import or_
|
|
from sqlalchemy.pool import NullPool
|
|
from collections import defaultdict
|
|
from datetime import datetime, timedelta, timezone
|
|
from devplacepy.cache import TTLCache
|
|
from devplacepy.config import (
|
|
AQUALITY_NEWS_GRADING_MODEL,
|
|
AQUALITY_NEWS_GRADING_URL,
|
|
DATABASE_URL,
|
|
DEFAULT_CORRECTION_PROMPT,
|
|
DEFAULT_MODIFIER_PROMPT,
|
|
INTERNAL_GATEWAY_URL,
|
|
ensure_data_dirs,
|
|
)
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
ensure_data_dirs()
|
|
if DATABASE_URL.startswith("sqlite:///"):
|
|
_db_file = DATABASE_URL[len("sqlite:///") :]
|
|
if _db_file and _db_file != ":memory:":
|
|
Path(_db_file).parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
db = dataset.connect(
|
|
DATABASE_URL,
|
|
engine_kwargs={
|
|
"connect_args": {
|
|
"timeout": 30,
|
|
"check_same_thread": False,
|
|
},
|
|
"poolclass": NullPool,
|
|
},
|
|
on_connect_statements=[
|
|
"PRAGMA journal_mode=WAL",
|
|
"PRAGMA synchronous=NORMAL",
|
|
"PRAGMA busy_timeout=30000",
|
|
"PRAGMA cache_size=-8000",
|
|
"PRAGMA temp_store=MEMORY",
|
|
"PRAGMA mmap_size=268435456",
|
|
],
|
|
)
|
|
|
|
|
|
def refresh_snapshot() -> None:
|
|
connection = db.executable
|
|
if connection.in_transaction() and not db.in_transaction:
|
|
connection.commit()
|
|
|
|
|
|
_local_cache_versions: dict = {}
|
|
|
|
|
|
_cache_version_cache = TTLCache(ttl=1)
|
|
|
|
|
|
_cache_state_ready = False
|
|
|
|
|
|
def _ensure_cache_state() -> None:
|
|
global _cache_state_ready
|
|
if _cache_state_ready:
|
|
return
|
|
with db:
|
|
db.query(
|
|
"CREATE TABLE IF NOT EXISTS cache_state "
|
|
"(name TEXT PRIMARY KEY, version INTEGER NOT NULL DEFAULT 0)"
|
|
)
|
|
_cache_state_ready = True
|
|
|
|
|
|
def get_cache_version(name: str) -> int:
|
|
cached = _cache_version_cache.get(name)
|
|
if cached is not None:
|
|
return cached
|
|
try:
|
|
_ensure_cache_state()
|
|
with db:
|
|
rows = list(db.query("SELECT name, version FROM cache_state"))
|
|
versions = {row["name"]: int(row["version"]) for row in rows}
|
|
except Exception as e:
|
|
logger.warning(f"Could not read cache version {name}: {e}")
|
|
return 0
|
|
for key, version in versions.items():
|
|
_cache_version_cache.set(key, version)
|
|
version = versions.get(name, 0)
|
|
_cache_version_cache.set(name, version)
|
|
return version
|
|
|
|
|
|
def bump_cache_version(name: str) -> None:
|
|
try:
|
|
_ensure_cache_state()
|
|
with db:
|
|
db.query(
|
|
"INSERT OR IGNORE INTO cache_state (name, version) VALUES (:name, 0)",
|
|
name=name,
|
|
)
|
|
db.query(
|
|
"UPDATE cache_state SET version = version + 1 WHERE name = :name",
|
|
name=name,
|
|
)
|
|
_cache_version_cache.pop(name)
|
|
except Exception as e:
|
|
logger.warning(f"Could not bump cache version {name}: {e}")
|
|
|
|
|
|
def sync_local_cache(name: str, cache) -> None:
|
|
current = get_cache_version(name)
|
|
if name not in _local_cache_versions:
|
|
_local_cache_versions[name] = current
|
|
return
|
|
if _local_cache_versions[name] != current:
|
|
cache.clear()
|
|
_local_cache_versions[name] = current
|
|
|
|
|
|
def _index(db, table, name, columns, *, where=None, unique=False):
|
|
try:
|
|
if table in db.tables:
|
|
cols = ", ".join(columns)
|
|
kind = "UNIQUE INDEX" if unique else "INDEX"
|
|
clause = f" WHERE {where}" if where else ""
|
|
with db:
|
|
db.query(
|
|
f"CREATE {kind} IF NOT EXISTS {name} ON {table} ({cols}){clause}"
|
|
)
|
|
except Exception as e:
|
|
logger.warning(f"Could not create index {name} on {table}: {e}")
|
|
|
|
|
|
def _drop_index(db, name):
|
|
try:
|
|
with db:
|
|
db.query(f"DROP INDEX IF EXISTS {name}")
|
|
except Exception as e:
|
|
logger.warning(f"Could not drop index {name}: {e}")
|
|
|
|
|
|
def _uid_index(db, table):
|
|
if table not in db.tables or "uid" not in get_table(table).columns:
|
|
return
|
|
name = f"idx_{table}_uid"
|
|
try:
|
|
with db:
|
|
db.query(f"CREATE UNIQUE INDEX IF NOT EXISTS {name} ON {table} (uid)")
|
|
except Exception as e:
|
|
logger.warning(f"Unique uid index on {table} failed ({e}); using non-unique")
|
|
_index(db, table, name, ["uid"])
|
|
|
|
|
|
def get_table(name):
|
|
return db[name]
|
|
|
|
|
|
def _in_clause(uids, prefix="p"):
|
|
placeholders = ", ".join(f":{prefix}{i}" for i in range(len(uids)))
|
|
params = {f"{prefix}{i}": uid for i, uid in enumerate(uids)}
|
|
return placeholders, params
|
|
|
|
|
|
def _now_iso() -> str:
|
|
return datetime.now(timezone.utc).isoformat()
|