This commit is contained in:
@@ -1,27 +1,91 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
#!/usr/bin/env bash
|
||||
set -Eeuo pipefail
|
||||
|
||||
echo "[update.sh] Проверка bot.db..."
|
||||
if [ -d "bot.db" ]; then
|
||||
echo "Удаляю папку bot.db..."
|
||||
rm -rf bot.db
|
||||
# === Настройки ===
|
||||
SERVICE="bot" # имя сервиса в docker-compose.yml
|
||||
APP_DIR="/app" # рабочая директория в контейнере
|
||||
HOST_DB_DIR="./db" # каталог БД на хосте
|
||||
HOST_DB_FILE="./db/bot.db" # файл БД на хосте
|
||||
DEFAULT_DB_URL="sqlite+aiosqlite:////app/db/bot.db" # единый путь БД внутри контейнера
|
||||
|
||||
log() { echo -e "[update.sh] $*"; }
|
||||
|
||||
# === Работаем из директории скрипта ===
|
||||
cd "$(dirname "${BASH_SOURCE[0]}")"
|
||||
|
||||
# === Приводим БД к каталогу ./db/bot.db ===
|
||||
log "Проверка корректности БД (./db/bot.db)..."
|
||||
mkdir -p "${HOST_DB_DIR}"
|
||||
|
||||
# если раньше был конфликтный объект ./bot.db
|
||||
if [[ -d "./bot.db" ]]; then
|
||||
log "Найдена ПАПКА ./bot.db → удаляю, чтобы не конфликтовала с файлом БД."
|
||||
rm -rf ./bot.db
|
||||
fi
|
||||
if [ ! -f "bot.db" ]; then
|
||||
echo "Создаю пустой файл bot.db..."
|
||||
touch bot.db
|
||||
if [[ -f "./bot.db" && ! -f "${HOST_DB_FILE}" ]]; then
|
||||
log "Переношу старый файл ./bot.db в ${HOST_DB_FILE} ..."
|
||||
mv ./bot.db "${HOST_DB_FILE}"
|
||||
fi
|
||||
if [[ ! -f "${HOST_DB_FILE}" ]]; then
|
||||
log "Создаю пустой файл БД: ${HOST_DB_FILE}"
|
||||
:> "${HOST_DB_FILE}"
|
||||
fi
|
||||
|
||||
echo "[update.sh] Получение свежего кода..."
|
||||
git pull
|
||||
# === .env: страхуем DATABASE_URL ===
|
||||
if [[ -f ".env" ]]; then
|
||||
if ! grep -q '^DATABASE_URL=' .env; then
|
||||
log "В .env не найден DATABASE_URL — дописываю с ${DEFAULT_DB_URL}"
|
||||
echo "DATABASE_URL=${DEFAULT_DB_URL}" >> .env
|
||||
fi
|
||||
else
|
||||
log "Файл .env не найден — создаю и прописываю DATABASE_URL=${DEFAULT_DB_URL}"
|
||||
echo "DATABASE_URL=${DEFAULT_DB_URL}" > .env
|
||||
fi
|
||||
|
||||
echo "[update.sh] Пересборка контейнера..."
|
||||
# === Git pull ===
|
||||
log "Получение свежего кода (git pull --rebase --autostash)..."
|
||||
git pull --rebase --autostash
|
||||
|
||||
# === Пересборка образа ===
|
||||
log "Пересборка контейнера..."
|
||||
docker compose build --no-cache
|
||||
|
||||
echo "[update.sh] Применение миграций Alembic..."
|
||||
docker compose run --rm bot alembic revision --autogenerate -m "update"
|
||||
docker compose run --rm bot alembic upgrade head
|
||||
# === (Опционально) создаём ревизию Alembic с комментарием ===
|
||||
MIG_MSG="${1-}"
|
||||
if [[ -z "${MIG_MSG}" ]]; then
|
||||
read -rp "[update.sh] Комментарий для новой миграции Alembic (Enter — пропустить создание ревизии): " MIG_MSG
|
||||
fi
|
||||
|
||||
echo "[update.sh] Запуск контейнера..."
|
||||
if [[ -n "${MIG_MSG}" ]]; then
|
||||
log "Создание ревизии Alembic с комментарием: ${MIG_MSG}"
|
||||
docker compose run --rm -T \
|
||||
-e MIG_MSG="${MIG_MSG}" \
|
||||
"${SERVICE}" sh -lc "cd '${APP_DIR}' && alembic revision --autogenerate -m \"\${MIG_MSG}\""
|
||||
else
|
||||
log "Создание ревизии пропущено."
|
||||
fi
|
||||
|
||||
# === Применяем миграции ===
|
||||
log "Применение миграций Alembic (upgrade head)..."
|
||||
docker compose run --rm -T "${SERVICE}" sh -lc "cd '${APP_DIR}' && alembic upgrade head"
|
||||
|
||||
# === Запуск приложения ===
|
||||
log "Запуск контейнера в фоне..."
|
||||
docker compose up -d
|
||||
|
||||
echo "[update.sh] Готово!"
|
||||
# === Мини-проверка окружения и таблиц ===
|
||||
log "Проверка DATABASE_URL внутри контейнера и списка таблиц..."
|
||||
docker compose exec -T "${SERVICE}" sh -lc "
|
||||
echo 'DATABASE_URL='\"\$DATABASE_URL\";
|
||||
if [ -f /app/db/bot.db ]; then
|
||||
echo '[DB] /app/db/bot.db найден. Таблицы:';
|
||||
sqlite3 /app/db/bot.db '.tables' || true;
|
||||
elif [ -f /db/bot.db ]; then
|
||||
echo '[DB] /db/bot.db найден (проверь docker-compose volume!). Таблицы:';
|
||||
sqlite3 /db/bot.db '.tables' || true;
|
||||
else
|
||||
echo 'Файл БД не найден в стандартных путях /app/db/bot.db или /db/bot.db';
|
||||
fi
|
||||
"
|
||||
|
||||
log "Готово!"
|
||||
|
||||
@@ -140,149 +140,6 @@ async def select_text(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
await update.message.reply_text('Выберите, куда отправить пост:', reply_markup=InlineKeyboardMarkup(keyboard))
|
||||
return SELECT_TARGET
|
||||
|
||||
# async def select_target(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
# query = update.callback_query
|
||||
# if not query:
|
||||
# return ConversationHandler.END
|
||||
# await query.answer()
|
||||
|
||||
# data = query.data
|
||||
# async with AsyncSessionLocal() as session:
|
||||
# chat_id = None
|
||||
# markup = None
|
||||
# selected_kind = None # "channel" | "group"
|
||||
# selected_title = None
|
||||
|
||||
# if data and data.startswith('channel_'):
|
||||
# selected_kind = "channel"
|
||||
# channel_id = int(data.split('_')[1])
|
||||
|
||||
# # ACL: право постинга
|
||||
# me = await get_or_create_admin(session, update.effective_user.id)
|
||||
# allowed = await has_scope_on_channel(session, me.id, channel_id, SCOPE_POST)
|
||||
# if not allowed:
|
||||
# await query.edit_message_text("У вас нет права постить в этот канал.")
|
||||
# return ConversationHandler.END
|
||||
|
||||
# channel = (await session.execute(sa_select(Channel).where(Channel.id == channel_id))).scalar_one_or_none()
|
||||
# if not channel:
|
||||
# await query.edit_message_text("Канал не найден.")
|
||||
# return ConversationHandler.END
|
||||
# chat_id = (channel.link or "").strip()
|
||||
# selected_title = channel.name
|
||||
|
||||
# # кнопки канала
|
||||
# btns = (await session.execute(sa_select(Button).where(Button.channel_id == channel_id))).scalars().all()
|
||||
# if btns:
|
||||
# markup = InlineKeyboardMarkup(
|
||||
# [[InlineKeyboardButton(str(b.name), url=str(b.url))] for b in btns]
|
||||
# )
|
||||
|
||||
# elif data and data.startswith('group_'):
|
||||
# selected_kind = "group"
|
||||
# group_id = int(data.split('_')[1])
|
||||
# group = (await session.execute(sa_select(Group).where(Group.id == group_id))).scalar_one_or_none()
|
||||
# if not group:
|
||||
# await query.edit_message_text("Группа не найдена.")
|
||||
# return ConversationHandler.END
|
||||
# chat_id = (group.link or "").strip()
|
||||
# selected_title = group.name
|
||||
|
||||
# # кнопки группы
|
||||
# btns = (await session.execute(sa_select(Button).where(Button.group_id == group_id))).scalars().all()
|
||||
# if btns:
|
||||
# markup = InlineKeyboardMarkup(
|
||||
# [[InlineKeyboardButton(str(b.name), url=str(b.url))] for b in btns]
|
||||
# )
|
||||
|
||||
# if not chat_id or not (chat_id.startswith('@') or chat_id.startswith('-')):
|
||||
# await query.edit_message_text('Ошибка: ссылка должна быть username (@channel) или числовой ID (-100...)')
|
||||
# return ConversationHandler.END
|
||||
|
||||
# ud = context.user_data or {}
|
||||
# text: str = ud.get("text", "") or ""
|
||||
# entities: List[MessageEntity] = ud.get("entities", []) or []
|
||||
|
||||
# # санация для custom_emoji
|
||||
# entities = _strip_broken_entities(entities)
|
||||
# entities = _split_custom_emoji_by_utf16(text, entities)
|
||||
|
||||
# # если есть клавиатура, ПРЕДПОЧИТАЕМ отправку "вручную" (а не copyMessage),
|
||||
# # т.к. в некоторых кейсах клавиатура при копировании может не приклеиться
|
||||
# prefer_manual_send = markup is not None
|
||||
|
||||
# # определим, были ли “части медиа”
|
||||
# has_media_parts = any(k in ud for k in ("photo","animation","video","document","audio","voice","sticker"))
|
||||
|
||||
# try:
|
||||
# if not prefer_manual_send and ud.get("src_chat_id") and ud.get("src_msg_id") and not has_media_parts:
|
||||
# # 1) copyMessage без медиа и без клавиатуры — максимальная идентичность
|
||||
# msg_id_obj = await context.bot.copy_message(
|
||||
# chat_id=chat_id,
|
||||
# from_chat_id=ud["src_chat_id"],
|
||||
# message_id=ud["src_msg_id"],
|
||||
# reply_markup=markup # если вдруг есть, попробуем сразу
|
||||
# )
|
||||
# # страховка: если клавиатуры нет/не приклеилась — прикрутим edit_message_reply_markup
|
||||
# if markup and getattr(msg_id_obj, "message_id", None):
|
||||
# try:
|
||||
# await context.bot.edit_message_reply_markup(
|
||||
# chat_id=chat_id,
|
||||
# message_id=msg_id_obj.message_id,
|
||||
# reply_markup=markup
|
||||
# )
|
||||
# except Exception:
|
||||
# pass
|
||||
# await query.edit_message_text(f'Пост отправлен{" в: " + selected_title if selected_title else "!"}')
|
||||
# return ConversationHandler.END
|
||||
|
||||
# # 2) fallback / manual — ВСЕГДА прикладываем reply_markup
|
||||
# sent = False
|
||||
# if "photo" in ud:
|
||||
# await context.bot.send_photo(chat_id=chat_id, photo=ud["photo"],
|
||||
# caption=(text or None), caption_entities=(entities if text else None),
|
||||
# reply_markup=markup)
|
||||
# sent = True
|
||||
# elif "animation" in ud:
|
||||
# await context.bot.send_animation(chat_id=chat_id, animation=ud["animation"],
|
||||
# caption=(text or None), caption_entities=(entities if text else None),
|
||||
# reply_markup=markup)
|
||||
# sent = True
|
||||
# elif "video" in ud:
|
||||
# await context.bot.send_video(chat_id=chat_id, video=ud["video"],
|
||||
# caption=(text or None), caption_entities=(entities if text else None),
|
||||
# reply_markup=markup)
|
||||
# sent = True
|
||||
# elif "document" in ud:
|
||||
# await context.bot.send_document(chat_id=chat_id, document=ud["document"],
|
||||
# caption=(text or None), caption_entities=(entities if text else None),
|
||||
# reply_markup=markup)
|
||||
# sent = True
|
||||
# elif "audio" in ud:
|
||||
# await context.bot.send_audio(chat_id=chat_id, audio=ud["audio"],
|
||||
# caption=(text or None), caption_entities=(entities if text else None),
|
||||
# reply_markup=markup)
|
||||
# sent = True
|
||||
# elif "voice" in ud:
|
||||
# await context.bot.send_voice(chat_id=chat_id, voice=ud["voice"],
|
||||
# caption=(text or None), caption_entities=(entities if text else None),
|
||||
# reply_markup=markup)
|
||||
# sent = True
|
||||
# elif "sticker" in ud:
|
||||
# await context.bot.send_sticker(chat_id=chat_id, sticker=ud["sticker"], reply_markup=markup)
|
||||
# if text:
|
||||
# await context.bot.send_message(chat_id=chat_id, text=text, entities=entities)
|
||||
# sent = True
|
||||
# else:
|
||||
# await context.bot.send_message(chat_id=chat_id, text=text, entities=entities, reply_markup=markup)
|
||||
# sent = True
|
||||
|
||||
# await query.edit_message_text(f'Пост отправлен{" в: " + selected_title if selected_title else "!"}' if sent else 'Ошибка: не удалось отправить сообщение.')
|
||||
|
||||
# except BadRequest as e:
|
||||
# await query.edit_message_text(f'Ошибка отправки поста: {e}')
|
||||
|
||||
# return ConversationHandler.END
|
||||
|
||||
|
||||
async def select_target(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
@@ -296,11 +153,11 @@ async def select_target(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
chat_id = None
|
||||
markup = None
|
||||
selected_title = None
|
||||
btns = []
|
||||
|
||||
if data and data.startswith('channel_'):
|
||||
channel_id = int(data.split('_')[1])
|
||||
|
||||
# ACL
|
||||
me = await get_or_create_admin(session, update.effective_user.id)
|
||||
if not await has_scope_on_channel(session, me.id, channel_id, SCOPE_POST):
|
||||
await query.edit_message_text("У вас нет права постить в этот канал.")
|
||||
@@ -310,10 +167,10 @@ async def select_target(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
if not channel:
|
||||
await query.edit_message_text("Канал не найден.")
|
||||
return ConversationHandler.END
|
||||
|
||||
chat_id = (channel.link or "").strip()
|
||||
selected_title = channel.name
|
||||
|
||||
# КНОПКИ
|
||||
btns = (await session.execute(sa_select(Button).where(Button.channel_id == channel_id))).scalars().all()
|
||||
if btns:
|
||||
markup = InlineKeyboardMarkup([[InlineKeyboardButton(str(b.name), url=str(b.url))] for b in btns])
|
||||
@@ -324,74 +181,79 @@ async def select_target(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
if not group:
|
||||
await query.edit_message_text("Группа не найдена.")
|
||||
return ConversationHandler.END
|
||||
|
||||
chat_id = (group.link or "").strip()
|
||||
selected_title = group.name
|
||||
|
||||
btns = (await session.execute(sa_select(Button).where(Button.group_id == group_id))).scalars().all()
|
||||
if btns:
|
||||
markup = InlineKeyboardMarkup([[InlineKeyboardButton(str(b.name), url=str(b.url))] for b in btns])
|
||||
markup = InlineKeyboardMarkup([[InlineKeyboardButton(str(b.name), url=str(b.url))] for b in btns]])
|
||||
|
||||
if not chat_id or not (chat_id.startswith('@') or chat_id.startswith('-')):
|
||||
await query.edit_message_text('Ошибка: ссылка должна быть @username или -100...')
|
||||
await query.edit_message_text('Ошибка: ссылка должна быть @username или -100…')
|
||||
return ConversationHandler.END
|
||||
|
||||
# DEBUG: покажем, сколько кнопок нашли и куда шлём
|
||||
print(f"[DEBUG] send -> chat_id={chat_id} title={selected_title!r} buttons={len(btns)} has_markup={bool(markup)}")
|
||||
|
||||
ud = context.user_data or {}
|
||||
text: str = ud.get("text", "") or ""
|
||||
entities: List[MessageEntity] = ud.get("entities", []) or []
|
||||
|
||||
# санация custom_emoji/UTF-16
|
||||
entities = _strip_broken_entities(entities)
|
||||
entities = _split_custom_emoji_by_utf16(text, entities)
|
||||
|
||||
# >>> ВАЖНО: НИКАКОГО copyMessage. Всегда manual send с reply_markup <<<
|
||||
try:
|
||||
sent = False
|
||||
sent_msg = None
|
||||
|
||||
# ВСЕГДА ручная отправка (никакого copyMessage), чтобы приклеить reply_markup
|
||||
if "photo" in ud:
|
||||
await context.bot.send_photo(chat_id=chat_id, photo=ud["photo"],
|
||||
caption=(text or None),
|
||||
caption_entities=(entities if text else None),
|
||||
reply_markup=markup)
|
||||
sent = True
|
||||
sent_msg = await context.bot.send_photo(chat_id=chat_id, photo=ud["photo"],
|
||||
caption=(text or None),
|
||||
caption_entities=(entities if text else None),
|
||||
reply_markup=markup)
|
||||
elif "animation" in ud:
|
||||
await context.bot.send_animation(chat_id=chat_id, animation=ud["animation"],
|
||||
caption=(text or None),
|
||||
caption_entities=(entities if text else None),
|
||||
reply_markup=markup)
|
||||
sent = True
|
||||
sent_msg = await context.bot.send_animation(chat_id=chat_id, animation=ud["animation"],
|
||||
caption=(text or None),
|
||||
caption_entities=(entities if text else None),
|
||||
reply_markup=markup)
|
||||
elif "video" in ud:
|
||||
await context.bot.send_video(chat_id=chat_id, video=ud["video"],
|
||||
caption=(text or None),
|
||||
caption_entities=(entities if text else None),
|
||||
reply_markup=markup)
|
||||
sent = True
|
||||
sent_msg = await context.bot.send_video(chat_id=chat_id, video=ud["video"],
|
||||
caption=(text or None),
|
||||
caption_entities=(entities if text else None),
|
||||
reply_markup=markup)
|
||||
elif "document" in ud:
|
||||
await context.bot.send_document(chat_id=chat_id, document=ud["document"],
|
||||
caption=(text or None),
|
||||
caption_entities=(entities if text else None),
|
||||
reply_markup=markup)
|
||||
sent = True
|
||||
sent_msg = await context.bot.send_document(chat_id=chat_id, document=ud["document"],
|
||||
caption=(text or None),
|
||||
caption_entities=(entities if text else None),
|
||||
reply_markup=markup)
|
||||
elif "audio" in ud:
|
||||
await context.bot.send_audio(chat_id=chat_id, audio=ud["audio"],
|
||||
caption=(text or None),
|
||||
caption_entities=(entities if text else None),
|
||||
reply_markup=markup)
|
||||
sent = True
|
||||
sent_msg = await context.bot.send_audio(chat_id=chat_id, audio=ud["audio"],
|
||||
caption=(text or None),
|
||||
caption_entities=(entities if text else None),
|
||||
reply_markup=markup)
|
||||
elif "voice" in ud:
|
||||
await context.bot.send_voice(chat_id=chat_id, voice=ud["voice"],
|
||||
caption=(text or None),
|
||||
caption_entities=(entities if text else None),
|
||||
reply_markup=markup)
|
||||
sent = True
|
||||
sent_msg = await context.bot.send_voice(chat_id=chat_id, voice=ud["voice"],
|
||||
caption=(text or None),
|
||||
caption_entities=(entities if text else None),
|
||||
reply_markup=markup)
|
||||
elif "sticker" in ud:
|
||||
await context.bot.send_sticker(chat_id=chat_id, sticker=ud["sticker"], reply_markup=markup)
|
||||
sent_msg = await context.bot.send_sticker(chat_id=chat_id, sticker=ud["sticker"], reply_markup=markup)
|
||||
if text:
|
||||
await context.bot.send_message(chat_id=chat_id, text=text, entities=entities)
|
||||
sent = True
|
||||
else:
|
||||
await context.bot.send_message(chat_id=chat_id, text=text, entities=entities, reply_markup=markup)
|
||||
sent = True
|
||||
sent_msg = await context.bot.send_message(chat_id=chat_id, text=text, entities=entities, reply_markup=markup)
|
||||
|
||||
# страховка: если вдруг Telegram проглотил клаву — доклеим через edit_message_reply_markup
|
||||
if markup and getattr(sent_msg, "message_id", None):
|
||||
try:
|
||||
await context.bot.edit_message_reply_markup(chat_id=chat_id,
|
||||
message_id=sent_msg.message_id,
|
||||
reply_markup=markup)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
await query.edit_message_text(f'Пост отправлен{(" в: " + selected_title) if selected_title else "!"}')
|
||||
|
||||
await query.edit_message_text(f'Пост отправлен{(" в: " + selected_title) if selected_title else "!"}' if sent else 'Ошибка: не удалось отправить сообщение.')
|
||||
except BadRequest as e:
|
||||
await query.edit_message_text(f'Ошибка отправки поста: {e}')
|
||||
|
||||
|
||||
Reference in New Issue
Block a user