"""보관함 저장소. 잠긴 봉투를 받는 사람별로 잠시 맡아 둔다. 저장 방식은 이 파일의 MailboxStore 약속만 지키면 갈아 끼울 수 있다 (처음엔 SQLite 파일, 나중에 클라우드 저장소로 옮길 때 이 파일만 바꾼다). """ import sqlite3 import threading import time class MailboxStore: """약속: 서버는 아래 메서드만 쓴다.""" def put(self, to, envelope_id, text): # 맡기기. 같은 id 는 한 번만. raise NotImplementedError def take(self, to): # 받는 사람의 봉투 목록 [(id, text)] raise NotImplementedError def ack(self, to, ids): # 받아 갔으니 지우기 raise NotImplementedError def check_token(self, to, token, strict=False): # 처음 쓴 기기에 묶는다(strict 면 문자 확인으로 묶인 토큰만). 맞으면 True raise NotImplementedError def bind_token(self, to, token): # 문자 확인을 마친 기기의 토큰으로 바꾼다 raise NotImplementedError def purge(self, older_than_seconds): # 오래된 봉투 정리 raise NotImplementedError class SqliteMailboxStore(MailboxStore): def __init__(self, path=":memory:", max_per_recipient=5000): self._db = sqlite3.connect(path, check_same_thread=False) self._lock = threading.Lock() self._max = max_per_recipient with self._lock: self._db.executescript( """ CREATE TABLE IF NOT EXISTS mail( to_phone TEXT NOT NULL, id TEXT NOT NULL, body TEXT NOT NULL, at REAL NOT NULL, PRIMARY KEY(to_phone, id)); CREATE TABLE IF NOT EXISTS tokens( to_phone TEXT PRIMARY KEY, token TEXT NOT NULL); """ ) cols = [r[1] for r in self._db.execute("PRAGMA table_info(tokens)").fetchall()] if "verified" not in cols: # 예전 DB 이어 쓰기 self._db.execute("ALTER TABLE tokens ADD COLUMN verified INTEGER NOT NULL DEFAULT 0") self._db.commit() def put(self, to, envelope_id, text): with self._lock: n = self._db.execute("SELECT COUNT(*) FROM mail WHERE to_phone=?", (to,)).fetchone()[0] if n >= self._max: return False self._db.execute( "INSERT OR IGNORE INTO mail(to_phone,id,body,at) VALUES(?,?,?,?)", (to, envelope_id, text, time.time()), ) self._db.commit() return True def take(self, to): with self._lock: rows = self._db.execute( "SELECT id, body FROM mail WHERE to_phone=? ORDER BY at, rowid", (to,) ).fetchall() return [(r[0], r[1]) for r in rows] def ack(self, to, ids): with self._lock: self._db.executemany("DELETE FROM mail WHERE to_phone=? AND id=?", [(to, i) for i in ids]) self._db.commit() def check_token(self, to, token, strict=False): if not token: return False with self._lock: row = self._db.execute("SELECT token, verified FROM tokens WHERE to_phone=?", (to,)).fetchone() if strict: return row is not None and row[1] == 1 and row[0] == token if row is None: self._db.execute("INSERT INTO tokens(to_phone,token) VALUES(?,?)", (to, token)) self._db.commit() return True return row[0] == token def bind_token(self, to, token): with self._lock: self._db.execute( "INSERT INTO tokens(to_phone,token,verified) VALUES(?,?,1) " "ON CONFLICT(to_phone) DO UPDATE SET token=excluded.token, verified=1", (to, token), ) self._db.commit() def purge(self, older_than_seconds): with self._lock: self._db.execute("DELETE FROM mail WHERE at < ?", (time.time() - older_than_seconds,)) self._db.commit()