103 lines
4.0 KiB
Python
103 lines
4.0 KiB
Python
"""보관함 저장소. 잠긴 봉투를 받는 사람별로 잠시 맡아 둔다.
|
|
|
|
저장 방식은 이 파일의 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()
|