mirror of
https://github.com/EDeev/y.calendarkin.git
synced 2026-10-08 00:39:27 +03:00
code/sql.py на psycopg 3 с пулом соединений (переподключается сам); таблицы users, urls, alarms — в PostgreSQL user и url зарезервированы. Внутренние номера пользователей сохраняются: на них ссылаются файлы календарей data/icals/<номер>.ics. scripts/migrate_sqlite.py переносит старые базы со сверкой (проверено на копии рабочих: 25 пользователей, 9 подписок). Тесты хранилища и переноса — на PostgreSQL в CI; compose поднимает бота вместе с базой.
149 lines
6 KiB
Python
149 lines
6 KiB
Python
"""Хранилище бота — PostgreSQL (до 2026-10 — два файла SQLite users.db и clock.db).
|
||
|
||
Таблица users хранит короткий внутренний номер пользователя (id) и его id в Telegram (tg_id);
|
||
остальные таблицы ссылаются на внутренний номер, как и раньше."""
|
||
from psycopg_pool import ConnectionPool
|
||
from pytz import timezone
|
||
|
||
SCHEMA = """
|
||
CREATE TABLE IF NOT EXISTS users (
|
||
id SERIAL PRIMARY KEY,
|
||
tg_id BIGINT NOT NULL UNIQUE
|
||
);
|
||
CREATE TABLE IF NOT EXISTS urls (
|
||
user_id INTEGER PRIMARY KEY REFERENCES users (id) ON DELETE CASCADE,
|
||
status BOOLEAN NOT NULL DEFAULT TRUE,
|
||
url_ical TEXT,
|
||
time_zone TEXT
|
||
);
|
||
CREATE TABLE IF NOT EXISTS alarms (
|
||
user_id INTEGER PRIMARY KEY REFERENCES users (id) ON DELETE CASCADE,
|
||
daily BOOLEAN NOT NULL DEFAULT FALSE,
|
||
start BOOLEAN NOT NULL DEFAULT TRUE,
|
||
alarm_1 INTEGER,
|
||
alarm_2 INTEGER,
|
||
status_2 BOOLEAN NOT NULL DEFAULT TRUE
|
||
);
|
||
"""
|
||
|
||
|
||
def connect(dsn):
|
||
"""Пул соединений сам переподключается, если PostgreSQL перезапускали"""
|
||
pool = ConnectionPool(dsn, min_size=1, max_size=4, kwargs={"autocommit": True}, open=True)
|
||
with pool.connection() as conn:
|
||
conn.execute(SCHEMA)
|
||
return pool
|
||
|
||
|
||
class _Base:
|
||
def __init__(self, pool):
|
||
self.pool = pool
|
||
|
||
def _one(self, query, args=()):
|
||
with self.pool.connection() as conn:
|
||
row = conn.execute(query, args).fetchone()
|
||
return row[0] if row else None
|
||
|
||
def _all(self, query, args=()):
|
||
with self.pool.connection() as conn:
|
||
return conn.execute(query, args).fetchall()
|
||
|
||
def _run(self, query, args=()):
|
||
with self.pool.connection() as conn:
|
||
conn.execute(query, args)
|
||
|
||
|
||
class Users(_Base):
|
||
# КОМАНДЫ USER
|
||
def user_exists(self, user_id):
|
||
"""Проверяем, есть ли уже пользователь в базе (по id Telegram)"""
|
||
return bool(self._one("SELECT 1 FROM users WHERE tg_id = %s", (user_id,)))
|
||
|
||
def all_users(self):
|
||
"""Список внутренних номеров"""
|
||
return self._all("SELECT id FROM users ORDER BY id")
|
||
|
||
def add_user(self, user_id):
|
||
"""Добавляем нового пользователя"""
|
||
self._run("INSERT INTO users (tg_id) VALUES (%s) ON CONFLICT DO NOTHING", (user_id,))
|
||
|
||
def get_user_id(self, user_id):
|
||
"""Внутренний номер по id Telegram"""
|
||
return self._one("SELECT id FROM users WHERE tg_id = %s", (user_id,))
|
||
|
||
def get_first_user_id(self, user_id):
|
||
"""id Telegram по внутреннему номеру"""
|
||
return self._one("SELECT tg_id FROM users WHERE id = %s", (user_id,))
|
||
|
||
# КОМАНДЫ URL
|
||
def url_exists(self, user_id):
|
||
return bool(self._one("SELECT 1 FROM urls WHERE user_id = %s", (user_id,)))
|
||
|
||
def add_url(self, user_id, url_ical, time_zone):
|
||
"""Добавляем ссылку на календарь"""
|
||
self._run("INSERT INTO urls (user_id, url_ical, time_zone) VALUES (%s, %s, %s)", (user_id, url_ical, time_zone))
|
||
|
||
def update_status(self, user_id):
|
||
"""Переключаем рассылку уведомлений"""
|
||
self._run("UPDATE urls SET status = NOT status WHERE user_id = %s", (user_id,))
|
||
|
||
def update_url(self, user_id, url_ical, time_zone):
|
||
"""Обновляем ссылку и часовой пояс"""
|
||
self._run("UPDATE urls SET url_ical = %s, time_zone = %s WHERE user_id = %s", (url_ical, time_zone, user_id))
|
||
|
||
def get_status(self, user_id):
|
||
return self._one("SELECT status FROM urls WHERE user_id = %s", (user_id,))
|
||
|
||
def get_url(self, user_id):
|
||
return self._one("SELECT url_ical FROM urls WHERE user_id = %s", (user_id,))
|
||
|
||
def delete_url(self, user_id):
|
||
"""Удаляем подписку на календарь"""
|
||
self._run("DELETE FROM urls WHERE user_id = %s", (user_id,))
|
||
|
||
def get_tz(self, user_id):
|
||
"""Часовой пояс календаря"""
|
||
return timezone(self._one("SELECT time_zone FROM urls WHERE user_id = %s", (user_id,)))
|
||
|
||
|
||
class Clock(_Base):
|
||
# КОМАНДЫ ALARM
|
||
def clock_exists(self, user_id):
|
||
return bool(self._one("SELECT 1 FROM alarms WHERE user_id = %s", (user_id,)))
|
||
|
||
def add_clock(self, user_id):
|
||
"""Добавляем параметры уведомления: напоминания за 15 и 5 минут"""
|
||
self._run("INSERT INTO alarms (user_id, alarm_1, alarm_2) VALUES (%s, 15, 5) ON CONFLICT DO NOTHING", (user_id,))
|
||
|
||
def delete_clock(self, user_id):
|
||
self._run("DELETE FROM alarms WHERE user_id = %s", (user_id,))
|
||
|
||
def get_alarm(self, user_id):
|
||
"""Задержки напоминаний (первое, второе)"""
|
||
rows = self._all("SELECT alarm_1, alarm_2 FROM alarms WHERE user_id = %s", (user_id,))
|
||
return tuple(rows[0]) if rows else None
|
||
|
||
def get_start(self, user_id):
|
||
return self._one("SELECT start FROM alarms WHERE user_id = %s", (user_id,))
|
||
|
||
def get_daily(self, user_id):
|
||
return self._one("SELECT daily FROM alarms WHERE user_id = %s", (user_id,))
|
||
|
||
def get_status2(self, user_id):
|
||
return self._one("SELECT status_2 FROM alarms WHERE user_id = %s", (user_id,))
|
||
|
||
# ПЕРЕКЛЮЧАТЕЛИ
|
||
def update_daily(self, user_id):
|
||
self._run("UPDATE alarms SET daily = NOT daily WHERE user_id = %s", (user_id,))
|
||
|
||
def update_start(self, user_id):
|
||
self._run("UPDATE alarms SET start = NOT start WHERE user_id = %s", (user_id,))
|
||
|
||
def update_alarm1(self, user_id, alarm_1):
|
||
self._run("UPDATE alarms SET alarm_1 = %s WHERE user_id = %s", (alarm_1, user_id))
|
||
|
||
def update_alarm2(self, user_id, alarm_2):
|
||
self._run("UPDATE alarms SET alarm_2 = %s WHERE user_id = %s", (alarm_2, user_id))
|
||
|
||
def update_status2(self, user_id):
|
||
self._run("UPDATE alarms SET status_2 = NOT status_2 WHERE user_id = %s", (user_id,))
|