frindlymap/server/mailbox_store.py

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()