from __future__ import annotations import hashlib import json import sqlite3 from datetime import datetime class StateStore: """State lokal (SQLite) tracking submission_id -> event_id + content_hash. Sengaja TIDAK di doxid2022 — kalau file ini hilang/reset, paling buruk event lama ke-recreate di Calendar, tidak merusak apa pun di DB eDOXID.""" def __init__(self, path: str): self._conn = sqlite3.connect(path) self._conn.execute( """ CREATE TABLE IF NOT EXISTS sync_state ( submission_id INTEGER PRIMARY KEY, event_id TEXT NOT NULL, content_hash TEXT NOT NULL, updated_at TEXT NOT NULL, notified_h1 INTEGER NOT NULL DEFAULT 0, notified_1h INTEGER NOT NULL DEFAULT 0 ) """ ) self._conn.commit() def close(self) -> None: self._conn.close() @staticmethod def compute_hash(submission: dict) -> str: payload = json.dumps( [ str(submission["schedule"]), submission["room"], submission["title"], submission["type_id"], submission["student_email"], submission["student_nim"], submission["verificators"], ], sort_keys=True, default=str, ) return hashlib.sha256(payload.encode("utf-8")).hexdigest() def find(self, submission_id: int) -> dict | None: row = self._conn.execute( "SELECT event_id, content_hash FROM sync_state WHERE submission_id = ?", (submission_id,), ).fetchone() if row is None: return None return {"event_id": row[0], "content_hash": row[1]} def save(self, submission_id: int, event_id: str, content_hash: str) -> None: self._conn.execute( """ INSERT INTO sync_state (submission_id, event_id, content_hash, updated_at) VALUES (?, ?, ?, ?) """, (submission_id, event_id, content_hash, datetime.utcnow().isoformat()), ) self._conn.commit() def update_hash(self, submission_id: int, content_hash: str) -> None: self._conn.execute( "UPDATE sync_state SET content_hash = ?, updated_at = ? WHERE submission_id = ?", (content_hash, datetime.utcnow().isoformat(), submission_id), ) self._conn.commit() def remove(self, submission_id: int) -> None: self._conn.execute("DELETE FROM sync_state WHERE submission_id = ?", (submission_id,)) self._conn.commit() def is_notified(self, submission_id: int, which: str) -> bool: column = "notified_h1" if which == "h1" else "notified_1h" row = self._conn.execute( f"SELECT {column} FROM sync_state WHERE submission_id = ?", (submission_id,) ).fetchone() return bool(row and row[0]) def mark_notified(self, submission_id: int, which: str) -> None: column = "notified_h1" if which == "h1" else "notified_1h" self._conn.execute( f"UPDATE sync_state SET {column} = 1 WHERE submission_id = ?", (submission_id,) ) self._conn.commit() def find_orphaned(self, active_submission_ids: set[int]) -> dict[int, str]: """Submission yang tercatat di state tapi schedule-nya sudah di-null-kan (dibatalkan) atau baris submission-nya sudah dihapus — bukan sekadar tanggalnya lewat. `active_submission_ids` harus dari query TANPA filter tanggal (lihat Database.get_ids_with_active_schedule).""" rows = self._conn.execute("SELECT submission_id, event_id FROM sync_state").fetchall() return {sid: eid for sid, eid in rows if sid not in active_submission_ids}