diff --git a/bin/update.sh b/bin/update.sh index 9655dac..dcacb94 100755 --- a/bin/update.sh +++ b/bin/update.sh @@ -1,27 +1,38 @@ #!/usr/bin/env bash set -Eeuo pipefail -SERVICE="bot" # имя сервиса в docker-compose -APP_DIR="/app" # рабочая директория в контейнере -HOST_DB_DIR="./db" # каталог БД на хосте -HOST_DB_FILE="./db/bot.db" # файл БД на хосте -DB_URL_DEFAULT="sqlite+aiosqlite:////app/db/bot.db" # единый путь БД в контейнере +# === Настройки === +SERVICE="bot" # имя сервиса из docker-compose.yml +APP_DIR="/app" # рабочая директория кода в контейнере +HOST_DB_DIR="./db" # каталог БД на хосте +HOST_DB_FILE="./db/bot.db" # файл БД на хосте +DB_URL_DEFAULT="sqlite+aiosqlite:////app/db/bot.db" # ЕДИНЫЙ URL БД в контейнере log(){ echo -e "[update.sh] $*"; } die(){ echo -e "[update.sh][ERROR] $*" >&2; exit 1; } +# Запускать из корня репо, даже если скрипт лежит в bin/ cd "$(dirname "${BASH_SOURCE[0]}")/.." -# --- 0) Приводим БД к ./db/bot.db и .env к единому URL --- +# --- Утилиты для alembic в "смонтированном" контейнере (чтобы файлы миграций попадали в репо) --- +alembic_run_mounted() { + # использование: alembic_run_mounted "upgrade head" + docker compose run --rm -T \ + -v "$PWD":/app \ + -w /app \ + "${SERVICE}" sh -lc "alembic $*" +} + +# --- 0) Приводим БД и .env к единому виду --- log "Проверка каталога БД ${HOST_DB_DIR} ..." mkdir -p "${HOST_DB_DIR}" -# гашим древний конфликт ./bot.db (файл/папка) -if [[ -d "./bot.db" ]]; then +# Если в проекте остался прежний конфликтный объект ./bot.db — убираем/переносим +if [[ -d ./bot.db ]]; then log "Удаляю конфликтующую ПАПКУ ./bot.db" rm -rf ./bot.db fi -if [[ -f "./bot.db" && ! -f "${HOST_DB_FILE}" ]]; then +if [[ -f ./bot.db && ! -f "${HOST_DB_FILE}" ]]; then log "Переношу старый файл ./bot.db -> ${HOST_DB_FILE}" mv ./bot.db "${HOST_DB_FILE}" fi @@ -30,43 +41,64 @@ if [[ ! -f "${HOST_DB_FILE}" ]]; then :> "${HOST_DB_FILE}" fi -# .env: гарантируем DATABASE_URL +# .env: зафиксировать DATABASE_URL if [[ -f .env ]]; then - if ! grep -q '^DATABASE_URL=' .env; then - log "В .env не найден DATABASE_URL — дописываю ${DB_URL_DEFAULT}" - echo "DATABASE_URL=${DB_URL_DEFAULT}" >> .env - else - # аккуратно заменим на нужный абсолютный путь, если другой + if grep -q '^DATABASE_URL=' .env; then sed -i "s|^DATABASE_URL=.*$|DATABASE_URL=${DB_URL_DEFAULT}|g" .env log "DATABASE_URL в .env → ${DB_URL_DEFAULT}" + else + echo "DATABASE_URL=${DB_URL_DEFAULT}" >> .env + log "Добавил DATABASE_URL в .env → ${DB_URL_DEFAULT}" fi else - log "Создаю .env с DATABASE_URL=${DB_URL_DEFAULT}" echo "DATABASE_URL=${DB_URL_DEFAULT}" > .env + log "Создал .env с DATABASE_URL=${DB_URL_DEFAULT}" fi -# --- 1) git pull + build --- +# --- 1) git pull + сборка --- log "git pull --rebase --autostash ..." git pull --rebase --autostash || die "git pull не удался" log "Пересборка образа ..." -docker compose build --no-cache +docker compose build --no-cache || die "docker compose build не удался" -# --- 2) Функция безопасного апгрейда Alembic --- +# --- 2) Безопасный upgrade: выравниваем БД до HEAD; при «потере» ревизии чиним alembic_version --- safe_upgrade() { log "alembic upgrade head ..." - if docker compose run --rm -T "${SERVICE}" sh -lc "cd '${APP_DIR}' && alembic upgrade head"; then + set +e + UPG_LOG="$(alembic_run_mounted 'upgrade head' 2>&1)" + RC=$? + set -e + if [[ $RC -eq 0 ]]; then return 0 fi - log "upgrade head не прошёл. Пытаюсь выровнять ревизии: alembic stamp head → upgrade head" - docker compose run --rm -T "${SERVICE}" sh -lc "cd '${APP_DIR}' && alembic stamp head" || die "alembic stamp head провалился" - docker compose run --rm -T "${SERVICE}" sh -lc "cd '${APP_DIR}' && alembic upgrade head" || die "alembic upgrade head провалился повторно" + + echo "$UPG_LOG" + if grep -q "Can't locate revision identified by" <<< "$UPG_LOG"; then + log "Обнаружена «потерянная» ревизия. Автопочинка: подшиваю БД к актуальному HEAD ..." + # Узнаём актуальный HEAD из каталога миграций + set +e + HEADREV="$(alembic_run_mounted 'heads -v' | awk '/^Rev:/{print $2; exit}')" + set -e + [[ -n "${HEADREV:-}" ]] || die "Не удалось определить HEAD ревизию" + + # Переписываем alembic_version в файле БД (внутри контейнера сервиса) + docker compose run --rm -T \ + -v "$PWD":/app \ + -w /app \ + "${SERVICE}" sh -lc "sqlite3 /app/db/bot.db \"UPDATE alembic_version SET version_num='${HEADREV}';\" || true" + + # Повторяем апгрейд + alembic_run_mounted 'upgrade head' || die "Повторный upgrade head не удался" + else + die "alembic upgrade head не удался" + fi } -# --- 3) Сначала выравниваем цепочку миграций --- +# --- 3) Выравниваем миграции до текущего HEAD --- safe_upgrade -# --- 4) (Опционально) создаём новую ревизию с твоим комментарием --- +# --- 4) (опционально) создаём ревизию с комментарием и применяем её --- MIG_MSG="${1-}" if [[ -z "${MIG_MSG}" ]]; then read -rp "[update.sh] Комментарий для новой миграции (Enter — пропустить): " MIG_MSG || true @@ -74,9 +106,8 @@ fi if [[ -n "${MIG_MSG}" ]]; then log "Создаю ревизию Alembic с комментарием: ${MIG_MSG}" - docker compose run --rm -T "${SERVICE}" sh -lc "cd '${APP_DIR}' && alembic revision --autogenerate -m \"${MIG_MSG}\"" \ - || die "alembic revision --autogenerate не удался" - # применяем новую ревизию + alembic_run_mounted "revision --autogenerate -m \"${MIG_MSG}\"" || die "alembic revision --autogenerate не удался" + # Сразу применяем safe_upgrade else log "Создание ревизии пропущено." @@ -98,6 +129,18 @@ docker compose exec -T "${SERVICE}" sh -lc " else echo 'Внимание: /app/db/bot.db отсутствует!'; fi -" +" || true log "Готово ✅" +log "Проверка переменных и таблиц внутри контейнера ..." +docker compose exec -T "${SERVICE}" sh -lc " + echo 'DATABASE_URL='\"\$DATABASE_URL\"; + cd '${APP_DIR}'; + echo 'Alembic HEADS:'; alembic heads -v || true; + echo 'Alembic CURRENT:'; alembic current -v || true; + if [ -f /app/db/bot.db ]; then + echo 'Таблицы SQLite (/app/db/bot.db):'; + sqlite3 /app/db/bot.db '.tables' || true; + else + echo 'Внимание: /app/db/bot.db отсутствует!'; + fi \ No newline at end of file diff --git a/handlers/permissions.py b/handlers/permissions.py index fe3ca10..8f34b88 100644 --- a/handlers/permissions.py +++ b/handlers/permissions.py @@ -3,6 +3,9 @@ import hashlib, secrets from datetime import datetime, timedelta from sqlalchemy import select from models import Admin, Channel, ChannelAccess, SCOPE_POST, SCOPE_SHARE +from sqlalchemy.exc import OperationalError + + def make_token(nbytes: int = 9) -> str: # Короткий URL-safe токен (<= ~12-16 символов укладывается в /start payload) @@ -41,28 +44,25 @@ async def has_scope_on_channel(session, admin_id: int, channel_id: int, scope: i return (acc.scopes & scope) == scope async def list_channels_for_admin(session, admin_id: int): - """Каналы, куда можно постить: владелец + активные доступы с SCOPE_POST.""" - # Владелец q1 = await session.execute(select(Channel).where(Channel.admin_id == admin_id)) owned = q1.scalars().all() - - # Доступы - q2 = await session.execute( - select(ChannelAccess).where( - ChannelAccess.invited_admin_id == admin_id, - ChannelAccess.status == "active", + try: + q2 = await session.execute( + select(ChannelAccess).where( + ChannelAccess.invited_admin_id == admin_id, + ChannelAccess.status == "active", + ) ) - ) - access_rows = q2.scalars().all() - access_map = {ar.channel_id for ar in access_rows if (ar.scopes & SCOPE_POST)} - if not access_map: + rows = q2.scalars().all() + except OperationalError: + return owned # таблицы ещё нет — просто вернём свои каналы + + can_post_ids = {r.channel_id for r in rows if (r.scopes & SCOPE_POST)} + if not can_post_ids: return owned - - q3 = await session.execute(select(Channel).where(Channel.id.in_(access_map))) + q3 = await session.execute(select(Channel).where(Channel.id.in_(can_post_ids))) shared = q3.scalars().all() - - # Уникальный список (owner + shared) - all_channels = {c.id: c for c in owned} + d = {c.id: c for c in owned} for c in shared: - all_channels[c.id] = c - return list(all_channels.values()) + d[c.id] = c + return list(d.values())