1
0
Fork 0
mirror of https://github.com/EDeev/y.calendarkin.git synced 2026-10-08 00:39:27 +03:00
y.calendarkin/code/sql.py
Egor Deev abafd7a55d Хранилище — PostgreSQL вместо двух файлов SQLite
code/sql.py на psycopg 3 с пулом соединений (переподключается сам); таблицы users, urls,
alarms — в PostgreSQL user и url зарезервированы. Внутренние номера пользователей сохраняются:
на них ссылаются файлы календарей data/icals/<номер>.ics. scripts/migrate_sqlite.py переносит
старые базы со сверкой (проверено на копии рабочих: 25 пользователей, 9 подписок).
Тесты хранилища и переноса — на PostgreSQL в CI; compose поднимает бота вместе с базой.
2026-10-06 11:46:39 +00:00

149 lines
6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Хранилище бота — 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,))