2026-10-05 09:36:20 +02:00
|
|
|
# retoor <retoor@molodetz.nl>
|
|
|
|
|
import logging
|
|
|
|
|
|
|
|
|
|
from molodetz import config
|
|
|
|
|
from molodetz.constants import SETTINGS_DEFAULTS
|
|
|
|
|
from molodetz.database.core import db, generate_uid, now_iso
|
|
|
|
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
|
TABLE_EXAMPLES = {
|
|
|
|
|
"users": {
|
|
|
|
|
"uid": "x",
|
|
|
|
|
"username": "x",
|
|
|
|
|
"email": "x@x.nl",
|
|
|
|
|
"password_hash": "x",
|
|
|
|
|
"role": "Member",
|
|
|
|
|
"api_key": "x",
|
|
|
|
|
"bio": "",
|
|
|
|
|
"avatar_seed": None,
|
|
|
|
|
"is_active": True,
|
|
|
|
|
"last_seen_at": None,
|
|
|
|
|
"suspended_until": None,
|
|
|
|
|
"suspend_reason": None,
|
|
|
|
|
"terms_version": "1",
|
|
|
|
|
"created_at": now_iso(),
|
|
|
|
|
"updated_at": now_iso(),
|
|
|
|
|
"deleted_at": None,
|
|
|
|
|
"deleted_by": None,
|
|
|
|
|
},
|
|
|
|
|
"sessions": {
|
|
|
|
|
"uid": "x",
|
|
|
|
|
"token": "x",
|
|
|
|
|
"user_uid": "x",
|
|
|
|
|
"created_at": now_iso(),
|
|
|
|
|
"expires_at": now_iso(),
|
|
|
|
|
"deleted_at": None,
|
|
|
|
|
"deleted_by": None,
|
|
|
|
|
},
|
|
|
|
|
"posts": {
|
|
|
|
|
"uid": "x",
|
|
|
|
|
"slug": "x",
|
|
|
|
|
"title": "x",
|
|
|
|
|
"body": "x",
|
2026-10-05 11:25:41 +02:00
|
|
|
"topic": "roll",
|
2026-10-05 09:36:20 +02:00
|
|
|
"status": "draft",
|
|
|
|
|
"is_placeholder": False,
|
|
|
|
|
"user_uid": "x",
|
|
|
|
|
"published_at": None,
|
|
|
|
|
"created_at": now_iso(),
|
|
|
|
|
"updated_at": now_iso(),
|
|
|
|
|
"deleted_at": None,
|
|
|
|
|
"deleted_by": None,
|
|
|
|
|
},
|
|
|
|
|
"join_requests": {
|
|
|
|
|
"uid": "x",
|
|
|
|
|
"name": "x",
|
|
|
|
|
"contact": "x",
|
|
|
|
|
"repo_url": None,
|
|
|
|
|
"message": "",
|
|
|
|
|
"status": "open",
|
|
|
|
|
"ip_hash": None,
|
|
|
|
|
"link_title": None,
|
|
|
|
|
"link_checked_at": None,
|
|
|
|
|
"link_status": None,
|
|
|
|
|
"created_at": now_iso(),
|
|
|
|
|
"updated_at": now_iso(),
|
|
|
|
|
"deleted_at": None,
|
|
|
|
|
"deleted_by": None,
|
|
|
|
|
},
|
2026-10-06 02:56:08 +02:00
|
|
|
"member_invites": {
|
|
|
|
|
"uid": "x",
|
|
|
|
|
"token_hash": "x",
|
|
|
|
|
"join_request_uid": "x",
|
|
|
|
|
"email": "",
|
|
|
|
|
"username": "",
|
|
|
|
|
"created_by": "x",
|
|
|
|
|
"created_at": now_iso(),
|
|
|
|
|
"expires_at": now_iso(),
|
|
|
|
|
"used_at": None,
|
|
|
|
|
"used_by": None,
|
|
|
|
|
"revoked_at": None,
|
|
|
|
|
"revoked_by": None,
|
|
|
|
|
},
|
2026-10-05 09:36:20 +02:00
|
|
|
"media_items": {
|
|
|
|
|
"uid": "x",
|
|
|
|
|
"filename": "x.jpg",
|
|
|
|
|
"kind": "flyer",
|
|
|
|
|
"caption": "",
|
|
|
|
|
"position": 0,
|
|
|
|
|
"width": 0,
|
|
|
|
|
"height": 0,
|
|
|
|
|
"phash": None,
|
|
|
|
|
"thumb_directory": None,
|
|
|
|
|
"thumb_name": None,
|
|
|
|
|
"created_at": now_iso(),
|
|
|
|
|
"updated_at": now_iso(),
|
|
|
|
|
"deleted_at": None,
|
|
|
|
|
"deleted_by": None,
|
|
|
|
|
},
|
|
|
|
|
"notifications": {
|
|
|
|
|
"uid": "x",
|
|
|
|
|
"user_uid": "x",
|
|
|
|
|
"kind": "system.maintenance",
|
|
|
|
|
"message": "x",
|
|
|
|
|
"url": "/",
|
|
|
|
|
"read_at": None,
|
|
|
|
|
"created_at": now_iso(),
|
|
|
|
|
"deleted_at": None,
|
|
|
|
|
"deleted_by": None,
|
|
|
|
|
},
|
|
|
|
|
"site_settings": {"uid": "setting_x", "key": "x", "value": "x", "updated_at": now_iso()},
|
|
|
|
|
"cache_state": {"name": "settings", "version": 0, "updated_at": now_iso()},
|
|
|
|
|
"service_state": {
|
|
|
|
|
"name": "backup",
|
|
|
|
|
"status": "stopped",
|
|
|
|
|
"last_run": None,
|
|
|
|
|
"next_run": None,
|
|
|
|
|
"started_at": None,
|
|
|
|
|
"heartbeat": None,
|
|
|
|
|
"logs": "[]",
|
|
|
|
|
"metrics": "{}",
|
|
|
|
|
"updated_at": now_iso(),
|
|
|
|
|
},
|
|
|
|
|
"audit_log": {
|
|
|
|
|
"uid": "x",
|
|
|
|
|
"event_key": "auth.login",
|
|
|
|
|
"category": "auth",
|
|
|
|
|
"actor_kind": "user",
|
|
|
|
|
"actor_uid": None,
|
|
|
|
|
"origin": "web",
|
|
|
|
|
"result": "ok",
|
|
|
|
|
"ip": None,
|
|
|
|
|
"user_agent": None,
|
|
|
|
|
"message": None,
|
|
|
|
|
"payload": "{}",
|
|
|
|
|
"created_at": now_iso(),
|
|
|
|
|
},
|
|
|
|
|
"audit_links": {"audit_uid": "x", "target_type": "user", "target_uid": "x"},
|
|
|
|
|
"visit_stats": {"day": "2026-01-01", "path": "/", "status": 200, "hits": 0},
|
|
|
|
|
"backups": {
|
|
|
|
|
"uid": "x",
|
|
|
|
|
"target": "database",
|
|
|
|
|
"status": "pending",
|
|
|
|
|
"requested_by": "x",
|
|
|
|
|
"directory": None,
|
|
|
|
|
"stored_name": None,
|
|
|
|
|
"size_bytes": None,
|
|
|
|
|
"error": None,
|
|
|
|
|
"created_at": now_iso(),
|
|
|
|
|
"finished_at": None,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _ensure_table(name, example):
|
|
|
|
|
table = db.get_table(name)
|
|
|
|
|
for column, value in example.items():
|
|
|
|
|
table.create_column_by_example(column, value)
|
|
|
|
|
return table
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _index(sql):
|
|
|
|
|
try:
|
|
|
|
|
with db:
|
|
|
|
|
db.query(sql)
|
|
|
|
|
except Exception as exc:
|
|
|
|
|
logger.warning("index ensure failed: %s (%s)", sql, exc)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _seed_settings():
|
|
|
|
|
if "site_settings" not in db.tables:
|
|
|
|
|
return
|
|
|
|
|
for key, value in SETTINGS_DEFAULTS.items():
|
2026-10-05 11:12:18 +02:00
|
|
|
if db["site_settings"].find_one(key=key) is None:
|
|
|
|
|
db["site_settings"].insert({"uid": f"default_{key}", "key": key, "value": value, "updated_at": now_iso()})
|
2026-10-05 09:36:20 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def _seed_notification_key():
|
|
|
|
|
path = config.NOTIFICATION_KEY_FILE
|
|
|
|
|
if not path.exists():
|
|
|
|
|
path.write_text(generate_uid(), encoding="utf-8")
|
|
|
|
|
path.chmod(0o600)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _backfill_api_keys():
|
|
|
|
|
if "users" not in db.tables:
|
|
|
|
|
return
|
|
|
|
|
with db:
|
|
|
|
|
db.query("UPDATE users SET api_key = :key WHERE api_key IS NULL OR api_key = ''", key=generate_uid())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def init_db():
|
|
|
|
|
for name, example in TABLE_EXAMPLES.items():
|
|
|
|
|
_ensure_table(name, example)
|
|
|
|
|
|
|
|
|
|
_index("CREATE UNIQUE INDEX IF NOT EXISTS idx_users_uid ON users(uid)")
|
|
|
|
|
_index("CREATE UNIQUE INDEX IF NOT EXISTS idx_users_username ON users(username)")
|
|
|
|
|
_index("CREATE UNIQUE INDEX IF NOT EXISTS idx_users_email ON users(email)")
|
|
|
|
|
_index("CREATE UNIQUE INDEX IF NOT EXISTS idx_users_api_key ON users(api_key)")
|
|
|
|
|
_index("CREATE INDEX IF NOT EXISTS idx_users_created_at ON users(created_at)")
|
|
|
|
|
_index("CREATE INDEX IF NOT EXISTS idx_users_trash ON users(deleted_at) WHERE deleted_at IS NOT NULL")
|
|
|
|
|
|
|
|
|
|
_index("CREATE UNIQUE INDEX IF NOT EXISTS idx_sessions_uid ON sessions(uid)")
|
|
|
|
|
_index("CREATE UNIQUE INDEX IF NOT EXISTS idx_sessions_token ON sessions(token)")
|
|
|
|
|
_index("CREATE INDEX IF NOT EXISTS idx_sessions_user ON sessions(user_uid, created_at)")
|
|
|
|
|
_index("CREATE INDEX IF NOT EXISTS idx_sessions_trash ON sessions(deleted_at) WHERE deleted_at IS NOT NULL")
|
|
|
|
|
|
|
|
|
|
_index("CREATE UNIQUE INDEX IF NOT EXISTS idx_posts_uid ON posts(uid)")
|
|
|
|
|
_index("CREATE UNIQUE INDEX IF NOT EXISTS idx_posts_slug ON posts(slug)")
|
|
|
|
|
_index(
|
|
|
|
|
"CREATE INDEX IF NOT EXISTS idx_posts_published ON posts(topic, published_at) "
|
|
|
|
|
"WHERE deleted_at IS NULL AND status = 'published'"
|
|
|
|
|
)
|
|
|
|
|
_index("CREATE INDEX IF NOT EXISTS idx_posts_author ON posts(user_uid, created_at)")
|
|
|
|
|
_index("CREATE INDEX IF NOT EXISTS idx_posts_trash ON posts(deleted_at) WHERE deleted_at IS NOT NULL")
|
|
|
|
|
|
2026-10-06 02:56:08 +02:00
|
|
|
_index("CREATE UNIQUE INDEX IF NOT EXISTS idx_member_invites_uid ON member_invites(uid)")
|
|
|
|
|
_index("CREATE UNIQUE INDEX IF NOT EXISTS idx_member_invites_token ON member_invites(token_hash)")
|
|
|
|
|
_index("CREATE INDEX IF NOT EXISTS idx_member_invites_join ON member_invites(join_request_uid, created_at)")
|
2026-10-05 09:36:20 +02:00
|
|
|
_index("CREATE UNIQUE INDEX IF NOT EXISTS idx_join_requests_uid ON join_requests(uid)")
|
|
|
|
|
_index("CREATE INDEX IF NOT EXISTS idx_join_requests_status ON join_requests(status, created_at)")
|
|
|
|
|
_index("CREATE INDEX IF NOT EXISTS idx_join_requests_trash ON join_requests(deleted_at) WHERE deleted_at IS NOT NULL")
|
|
|
|
|
|
|
|
|
|
_index("CREATE UNIQUE INDEX IF NOT EXISTS idx_media_items_uid ON media_items(uid)")
|
|
|
|
|
_index("CREATE UNIQUE INDEX IF NOT EXISTS idx_media_items_filename ON media_items(filename)")
|
|
|
|
|
_index("CREATE INDEX IF NOT EXISTS idx_media_items_kind ON media_items(kind, position)")
|
|
|
|
|
|
|
|
|
|
_index("CREATE UNIQUE INDEX IF NOT EXISTS idx_notifications_uid ON notifications(uid)")
|
|
|
|
|
_index("CREATE INDEX IF NOT EXISTS idx_notifications_user ON notifications(user_uid, created_at)")
|
|
|
|
|
|
|
|
|
|
_index("CREATE UNIQUE INDEX IF NOT EXISTS idx_site_settings_uid ON site_settings(uid)")
|
|
|
|
|
_index("CREATE UNIQUE INDEX IF NOT EXISTS idx_site_settings_key ON site_settings(key)")
|
|
|
|
|
_index("CREATE UNIQUE INDEX IF NOT EXISTS idx_cache_state_name ON cache_state(name)")
|
|
|
|
|
_index("CREATE UNIQUE INDEX IF NOT EXISTS idx_service_state_name ON service_state(name)")
|
|
|
|
|
_index("CREATE UNIQUE INDEX IF NOT EXISTS idx_audit_log_uid ON audit_log(uid)")
|
|
|
|
|
_index("CREATE INDEX IF NOT EXISTS idx_audit_log_created ON audit_log(created_at)")
|
|
|
|
|
_index("CREATE INDEX IF NOT EXISTS idx_audit_links_audit ON audit_links(audit_uid)")
|
|
|
|
|
_index("CREATE UNIQUE INDEX IF NOT EXISTS idx_visit_stats_day_path ON visit_stats(day, path, status)")
|
|
|
|
|
_index("CREATE UNIQUE INDEX IF NOT EXISTS idx_backups_uid ON backups(uid)")
|
|
|
|
|
|
|
|
|
|
_seed_settings()
|
|
|
|
|
_seed_notification_key()
|
|
|
|
|
_backfill_api_keys()
|
|
|
|
|
|
|
|
|
|
with db:
|
|
|
|
|
has_stats = next(iter(db.query("SELECT name FROM sqlite_master WHERE type='table' AND name='sqlite_stat1'")), None)
|
|
|
|
|
if has_stats is None:
|
|
|
|
|
db.query("ANALYZE")
|
|
|
|
|
db.query("PRAGMA optimize")
|