Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c8d8aad8c3 | ||
|
|
1c17501476 | ||
|
|
d2324db105 | ||
|
|
1d083e564c |
84
install.sh
84
install.sh
@@ -5,8 +5,28 @@ set -euo pipefail
|
||||
# Telegram Shop — установщик
|
||||
# Скрипт для развёртывания на x86_64 и ARM64 (Orange Pi, RPi)
|
||||
# Совместим с POSIX sh (Alpine ash)
|
||||
#
|
||||
# Режимы:
|
||||
# install.sh — интерактивный (запрашивает BOT_TOKEN/ADMIN_IDS)
|
||||
# install.sh --auto — неинтерактивный: placeholder BOT_TOKEN, ADMIN_IDS=0,
|
||||
# секреты генерируются автоматически (для CI/SSH)
|
||||
# install.sh --clean — очистка старых контейнеров/образов перед установкой
|
||||
# ============================================================
|
||||
|
||||
AUTO=0
|
||||
CLEAN=0
|
||||
for ARG in "$@"; do
|
||||
case "$ARG" in
|
||||
--auto) AUTO=1 ;;
|
||||
--clean) CLEAN=1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Неинтерактивный режим также включается, если stdin не терминал
|
||||
if [ ! -t 0 ]; then
|
||||
AUTO=1
|
||||
fi
|
||||
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
@@ -51,6 +71,27 @@ esac
|
||||
print_ok "Архитектура: $ARCH_NAME"
|
||||
print_info "Docker автоматически соберёт образ под текущую архитектуру"
|
||||
|
||||
# ============================================================
|
||||
# 0.5. Очистка (--clean): удалить старые контейнеры/образы/тома
|
||||
# ============================================================
|
||||
if [ "$CLEAN" -eq 1 ]; then
|
||||
print_step "Очистка старых контейнеров и образов"
|
||||
if [ -f docker-compose.yml ]; then
|
||||
docker compose down --rmi all --volumes 2>/dev/null || true
|
||||
fi
|
||||
# Удалить осиротевшие контейнеры/образы/тома проекта telegram-shop
|
||||
docker ps -aq --filter "name=telegram_shop_prod" | xargs -r docker rm -f 2>/dev/null || true
|
||||
docker ps -aq --filter "name=tg_shop_admin" | xargs -r docker rm -f 2>/dev/null || true
|
||||
docker ps -aq --filter "name=tor-proxy" | xargs -r docker rm -f 2>/dev/null || true
|
||||
docker images -q "git.softuniq.eu/telegram-market/telegram-shop*" | xargs -r docker rmi -f 2>/dev/null || true
|
||||
docker images -q "telegram-shop-*" | xargs -r docker rmi -f 2>/dev/null || true
|
||||
docker volume rm tor_proxy_data 2>/dev/null || true
|
||||
docker network rm tor_proxy_net telegram-shop_default 2>/dev/null || true
|
||||
# Удалить локальную БД и загрузки (чистая переустановка)
|
||||
rm -rf db uploads
|
||||
print_ok "Очистка завершена (tor-ssh-client не тронут)"
|
||||
fi
|
||||
|
||||
# ============================================================
|
||||
# 1. Проверка / установка Docker
|
||||
# ============================================================
|
||||
@@ -200,10 +241,16 @@ generate_random_hex() {
|
||||
# BOT_TOKEN
|
||||
current_bot_token=$(grep -E "^BOT_TOKEN=" .env 2>/dev/null | cut -d'=' -f2- | tr -d '"') || true
|
||||
if [ -z "$current_bot_token" ] || [ "$current_bot_token" = "your_bot_token_here" ]; then
|
||||
print_warn "BOT_TOKEN не задан или содержит placeholder."
|
||||
read_secret "Введите BOT_TOKEN (от @BotFather): "
|
||||
update_env_var "BOT_TOKEN" "$input"
|
||||
bot_token="$input"
|
||||
if [ "$AUTO" -eq 1 ]; then
|
||||
bot_token="REPLACE_ME_LATER"
|
||||
update_env_var "BOT_TOKEN" "$bot_token"
|
||||
print_warn "BOT_TOKEN не задан — установлен placeholder (внесите реальный токен в админке)"
|
||||
else
|
||||
print_warn "BOT_TOKEN не задан или содержит placeholder."
|
||||
read_secret "Введите BOT_TOKEN (от @BotFather): "
|
||||
update_env_var "BOT_TOKEN" "$input"
|
||||
bot_token="$input"
|
||||
fi
|
||||
else
|
||||
bot_token="$current_bot_token"
|
||||
fi
|
||||
@@ -211,11 +258,17 @@ fi
|
||||
# ADMIN_IDS
|
||||
current_admin_ids=$(grep -E "^ADMIN_IDS=" .env 2>/dev/null | cut -d'=' -f2- | tr -d '"') || true
|
||||
if [ -z "$current_admin_ids" ] || [ "$current_admin_ids" = "123456789,987654321" ]; then
|
||||
print_warn "ADMIN_IDS не задан или содержит placeholder."
|
||||
printf "Введите ADMIN_IDS (через запятую, например: 123456789,987654321): "
|
||||
read -r input
|
||||
update_env_var "ADMIN_IDS" "$input"
|
||||
admin_ids="$input"
|
||||
if [ "$AUTO" -eq 1 ]; then
|
||||
admin_ids="0"
|
||||
update_env_var "ADMIN_IDS" "$admin_ids"
|
||||
print_warn "ADMIN_IDS не задан — установлен 0 (настройте в админке)"
|
||||
else
|
||||
print_warn "ADMIN_IDS не задан или содержит placeholder."
|
||||
printf "Введите ADMIN_IDS (через запятую, например: 123456789,987654321): "
|
||||
read -r input
|
||||
update_env_var "ADMIN_IDS" "$input"
|
||||
admin_ids="$input"
|
||||
fi
|
||||
else
|
||||
admin_ids="$current_admin_ids"
|
||||
fi
|
||||
@@ -232,7 +285,7 @@ fi
|
||||
|
||||
# ADMIN_SECRET
|
||||
current_admin_secret=$(grep -E "^ADMIN_SECRET=" .env 2>/dev/null | cut -d'=' -f2- | tr -d '"') || true
|
||||
if [ -z "$current_admin_secret" ] || [ "$current_admin_secret" = "changeme" ]; then
|
||||
if [ -z "$current_admin_secret" ] || [ "$current_admin_secret" = "changeme" ] || [ "$current_admin_secret" = "your_admin_token_here" ]; then
|
||||
admin_secret=$(generate_random_hex)
|
||||
update_env_var "ADMIN_SECRET" "$admin_secret"
|
||||
print_ok "ADMIN_SECRET сгенерирован автоматически"
|
||||
@@ -242,7 +295,7 @@ fi
|
||||
|
||||
# SUPER_ADMIN_SECRET
|
||||
current_super_admin_secret=$(grep -E "^SUPER_ADMIN_SECRET=" .env 2>/dev/null | cut -d'=' -f2- | tr -d '"') || true
|
||||
if [ -z "$current_super_admin_secret" ] || [ "$current_super_admin_secret" = "changeme_super" ]; then
|
||||
if [ -z "$current_super_admin_secret" ] || [ "$current_super_admin_secret" = "changeme_super" ] || [ "$current_super_admin_secret" = "your_super_admin_token_here" ]; then
|
||||
# Генерация до тех пор, пока не получим значение, отличное от ADMIN_SECRET
|
||||
super_admin_secret=""
|
||||
while [ -z "$super_admin_secret" ] || [ "$super_admin_secret" = "$admin_secret" ]; do
|
||||
@@ -266,7 +319,7 @@ fi
|
||||
|
||||
# ADMIN_URL
|
||||
current_admin_url=$(grep -E "^ADMIN_URL=" .env 2>/dev/null | cut -d'=' -f2- | tr -d '"') || true
|
||||
if [ -z "$current_admin_url" ]; then
|
||||
if [ -z "$current_admin_url" ] || [ "$current_admin_url" = "http://your-host:3000" ] || [ "$current_admin_url" = "your-host" ]; then
|
||||
# Попытка определить LAN IP
|
||||
lan_ip=$(hostname -I | awk '{print $1}')
|
||||
if [ -z "$lan_ip" ]; then
|
||||
@@ -307,6 +360,13 @@ print_info "Образы собираются в CI (Gitea Actions, buildx multi
|
||||
IMAGE_TAG="${IMAGE_TAG:-latest}"
|
||||
print_info "Используемая версия образа: $IMAGE_TAG"
|
||||
|
||||
# Логин в приватный Gitea Container Registry (если задан токен)
|
||||
if [ -n "${GITEA_TOKEN:-}" ]; then
|
||||
echo "${GITEA_TOKEN}" | docker login git.softuniq.eu -u oauth2 --password-stdin >/dev/null 2>&1 \
|
||||
&& print_ok "Авторизация в Gitea Container Registry" \
|
||||
|| print_warn "Не удалось авторизоваться в registry (GITEA_TOKEN)"
|
||||
fi
|
||||
|
||||
$COMPOSE_CMD pull telegram_shop_prod tg_shop_admin
|
||||
print_ok "Образы telegram-shop:${IMAGE_TAG} и telegram-shop-admin:${IMAGE_TAG} загружены"
|
||||
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
{
|
||||
"name": "telegram-shop-bot",
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.2",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"start": "node src/index.js",
|
||||
"dev": "nodemon src/index.js",
|
||||
"lint": "find src -name '*.js' -print0 | xargs -0 -n1 node --check"
|
||||
"lint": "find src -name '*.js' -print0 | xargs -0 -n1 node --check",
|
||||
"arch:build": "docker compose -f docker/docker-compose.architect.yml build",
|
||||
"arch:index": "docker compose -f docker/docker-compose.architect.yml run --rm architect-indexer",
|
||||
"arch:index:full": "docker compose -f docker/docker-compose.architect.yml run --rm architect-indexer --mode full",
|
||||
"arch:index:incremental": "docker compose -f docker/docker-compose.architect.yml run --rm architect-indexer --mode incremental",
|
||||
"arch:status": "docker compose -f docker/docker-compose.architect.yml ps"
|
||||
},
|
||||
"dependencies": {
|
||||
"archiver": "^7.0.1",
|
||||
|
||||
@@ -5,7 +5,13 @@ import { dirname } from 'path';
|
||||
import { pathToFileURL } from 'url';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const DB_PATH = new URL('../../db/shop.db', import.meta.url).pathname;
|
||||
// Путь к БД: env SHOP_DB_PATH (для единой БД с админкой) → по умолчанию ../db/shop.db.
|
||||
// Рассинхрон: админка (Next.js/Prisma) писала в custom.db, бот — в shop.db → товары,
|
||||
// добавленные в админке, не были видны боту. Оба должны использовать ОДИН файл.
|
||||
const DB_PATH =
|
||||
process.env.SHOP_DB_PATH && process.env.SHOP_DB_PATH !== 'false'
|
||||
? process.env.SHOP_DB_PATH
|
||||
: new URL('../../db/shop.db', import.meta.url).pathname;
|
||||
|
||||
let betterDb;
|
||||
|
||||
|
||||
@@ -15,7 +15,53 @@ const initBot = async () => {
|
||||
}
|
||||
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
|
||||
try {
|
||||
const instance = new TelegramBot(config.BOT_TOKEN, {polling: true});
|
||||
const instance = new TelegramBot(config.BOT_TOKEN, {polling: false});
|
||||
// ── Проверка на ДУБЛИКАТ токена ─────────────────────────────
|
||||
// Перед запуском polling делаем разовый getUpdates. Если токен уже
|
||||
// используется другой нодой/процессом — Telegram возвращает 409
|
||||
// "Conflict: terminated by other getUpdates request" (или webhook).
|
||||
// Такой бот НЕ должен стартовать: он будет конфликтовать, терять
|
||||
// обновления и «прыгать» между серверами (issue: дубликат BotFather
|
||||
// токена на нескольких нодах).
|
||||
const dupCheck = await new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(() => resolve(null), 10000);
|
||||
instance
|
||||
.getUpdates({ timeout: 0, allowed_updates: [] })
|
||||
.then((updates) => {
|
||||
clearTimeout(timeout);
|
||||
// допустимые значения — любой успешный ответ ([] или список)
|
||||
resolve({ ok: true, updates });
|
||||
})
|
||||
.catch((error) => {
|
||||
clearTimeout(timeout);
|
||||
const status = error?.response?.statusCode ?? error?.response?.status;
|
||||
const description = error?.response?.body?.description ?? error?.message ?? String(error);
|
||||
const is409 = status === 409 || /conflict/i.test(description);
|
||||
resolve({ ok: false, status, description, is409 });
|
||||
});
|
||||
});
|
||||
if (!dupCheck?.ok) {
|
||||
if (dupCheck?.is409) {
|
||||
console.error(
|
||||
'\n====================================================\n' +
|
||||
'🚫 ОБНАРУЖЕН ДУБЛИКАТ ТОКЕНА ДЛЯ ЗАПУСКА\n' +
|
||||
' Telegram вернул 409 Conflict — этот BOT_TOKEN уже\n' +
|
||||
' используется другим экземпляром/нодой.\n' +
|
||||
` (${dupCheck.description})\n` +
|
||||
' Запуск бота остановлен. Создайте новый бот в @BotFather\n' +
|
||||
' или замените BOT_TOKEN в настройках сервиса.\n' +
|
||||
'====================================================\n'
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
if (dupCheck?.status === 404 || (dupCheck?.description || '').includes('Not Found')) {
|
||||
// неверный токен — пробуем дальше; ниже полноценная валидация
|
||||
logger.warn('validate: getUpdates 404 (invalid token?)');
|
||||
}
|
||||
}
|
||||
|
||||
// Реальная инициализация: включаем polling и проверяем getMe
|
||||
await instance.startPolling();
|
||||
await new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
instance.stopPolling();
|
||||
|
||||
@@ -55,7 +55,10 @@ export async function getCommissionWallets() {
|
||||
|
||||
/**
|
||||
* Check if the shop is activated.
|
||||
* Reads site_settings.shop_activated; falls back to env SHOP_ACTIVATED !== 'false' (default true).
|
||||
* Приоритет: env SHOP_ACTIVATED (единый источник — панель Bbox) → БД site_settings.
|
||||
* Раньше БД была первичной, и смена «Магазин активирован» в панели (env) не
|
||||
* влияла на бота (в БД оставалось старое значение) — рассинхрон «пишет true,
|
||||
* а бот говорит not activated». Теперь env главный; БД — зеркало-фоллбэк.
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
export async function getShopActivated() {
|
||||
@@ -64,25 +67,26 @@ export async function getShopActivated() {
|
||||
return shopActivatedCache;
|
||||
}
|
||||
|
||||
try {
|
||||
const row = await db.getAsync(
|
||||
"SELECT value FROM site_settings WHERE key = 'shop_activated'"
|
||||
);
|
||||
|
||||
let activated;
|
||||
if (row) {
|
||||
activated = row.value !== 'false';
|
||||
} else {
|
||||
activated = process.env.SHOP_ACTIVATED !== 'false';
|
||||
let activated;
|
||||
// 1. env (панель Bbox / compose) — единый источник правды
|
||||
if (process.env.SHOP_ACTIVATED !== undefined) {
|
||||
activated = process.env.SHOP_ACTIVATED !== 'false';
|
||||
} else {
|
||||
// 2. БД site_settings (админка магазина может вести туда же)
|
||||
try {
|
||||
const row = await db.getAsync(
|
||||
"SELECT value FROM site_settings WHERE key = 'shop_activated'"
|
||||
);
|
||||
activated = row ? row.value !== 'false' : true;
|
||||
} catch (err) {
|
||||
logger.error({ err }, 'Failed to read shop_activated from site_settings');
|
||||
activated = true;
|
||||
}
|
||||
|
||||
shopActivatedCache = activated;
|
||||
shopActivatedCachedAt = now;
|
||||
return activated;
|
||||
} catch (err) {
|
||||
logger.error({ err }, 'Failed to read shop_activated from site_settings');
|
||||
return process.env.SHOP_ACTIVATED !== 'false';
|
||||
}
|
||||
|
||||
shopActivatedCache = activated;
|
||||
shopActivatedCachedAt = now;
|
||||
return activated;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user