add edoxid calendar sync dag
Sync jadwal sidang eDOXID ke Google Calendar "Jadwal Sidang" tiap 5 menit (read-only ke doxid2022), plus reminder email H-1/1-jam via Gmail API karena reminder Calendar bawaan tidak ikut ke kalender pribadi attendee.
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
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}
|
||||
Reference in New Issue
Block a user