Добавлены Telegram-боты (клиентский + админский)
- Клиентский бот: принимает ссылку на chess-results.com, проверяет наличие результатов/жеребьёвки, считает пары следующего тура, выводит в HTML-формате - Админский бот: /stat — уникальные пользователи, всего запросов, запросов за сегодня - Статистика через SQLite (volume bot-data) - Оба бота в docker-compose с автоперезапуском - .env.example с тестовыми токенами - .env в .gitignore
This commit is contained in:
parent
bbbfe61094
commit
5c4b2536e7
9 changed files with 415 additions and 14 deletions
9
.env.example
Normal file
9
.env.example
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
# Telegram-боты chessCalc
|
||||||
|
CLIENT_BOT_TOKEN=8620149115:AAFVZvdD_Sh9SAtBObWCcn5piijGtDK3-Dw
|
||||||
|
ADMIN_BOT_TOKEN=8637175624:AAFT-_xkPKM1gIAkrEQ71LdaDnDCwAEX858
|
||||||
|
|
||||||
|
# Путь к bbpPairings (опционально, по умолчанию /usr/local/bin/bbpPairings.exe)
|
||||||
|
BBP_PATH=/usr/local/bin/bbpPairings.exe
|
||||||
|
|
||||||
|
# Путь к БД статистики
|
||||||
|
STATS_DB=/app/data/tournaments.db
|
||||||
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -2,3 +2,5 @@
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.pyc
|
*.pyc
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
.env
|
||||||
|
bots/tournaments.db
|
||||||
|
|
|
||||||
12
Dockerfile
12
Dockerfile
|
|
@ -2,27 +2,21 @@ FROM python:3.11-slim
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Install system dependencies
|
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
curl \
|
curl \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# Install Python dependencies
|
|
||||||
COPY requirements.txt .
|
COPY requirements.txt .
|
||||||
RUN pip install --no-cache-dir -r requirements.txt
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
# Copy bbpPairings binary (FIDE Swiss engine)
|
|
||||||
COPY bbpPairings.exe /usr/local/bin/bbpPairings.exe
|
COPY bbpPairings.exe /usr/local/bin/bbpPairings.exe
|
||||||
RUN chmod +x /usr/local/bin/bbpPairings.exe
|
RUN chmod +x /usr/local/bin/bbpPairings.exe
|
||||||
|
|
||||||
# Set path for bbpPairings wrapper
|
|
||||||
ENV BBP_PATH=/usr/local/bin/bbpPairings.exe
|
ENV BBP_PATH=/usr/local/bin/bbpPairings.exe
|
||||||
|
|
||||||
# Copy application code
|
|
||||||
COPY swiss_calc/ /app/swiss_calc/
|
COPY swiss_calc/ /app/swiss_calc/
|
||||||
|
COPY bots/ /app/bots/
|
||||||
|
|
||||||
# Create entry point
|
RUN mkdir -p /app/data
|
||||||
RUN python3 -c "from swiss_calc import parser; print('✅ Module loaded')"
|
|
||||||
|
|
||||||
ENTRYPOINT ["python3", "-m", "swiss_calc"]
|
RUN python3 -c "from swiss_calc import parser; print('Module loaded')"
|
||||||
CMD ["--help"]
|
|
||||||
|
|
|
||||||
0
bots/__init__.py
Normal file
0
bots/__init__.py
Normal file
58
bots/admin_bot.py
Normal file
58
bots/admin_bot.py
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
"""
|
||||||
|
Админский Telegram-бот chessCalc.
|
||||||
|
Показывает статистику использования клиентского бота.
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from telegram import Update
|
||||||
|
from telegram.ext import Application, CommandHandler, ContextTypes
|
||||||
|
from telegram.constants import ParseMode
|
||||||
|
|
||||||
|
from bots.stats import get_stats, record_user
|
||||||
|
|
||||||
|
TOKEN = os.environ.get('ADMIN_BOT_TOKEN', '')
|
||||||
|
|
||||||
|
START_MSG = (
|
||||||
|
'<b>🔧 chessCalc — админский бот</b>\n\n'
|
||||||
|
'Доступные команды:\n'
|
||||||
|
'/stat — статистика использования клиентского бота\n'
|
||||||
|
'/start — эта справка'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def start(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||||
|
user = update.effective_user
|
||||||
|
record_user(user.id, user.username or '', user.first_name or '',
|
||||||
|
user.last_name or '')
|
||||||
|
await update.message.reply_text(START_MSG, parse_mode=ParseMode.HTML)
|
||||||
|
|
||||||
|
|
||||||
|
async def stat(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||||
|
user = update.effective_user
|
||||||
|
record_user(user.id, user.username or '', user.first_name or '',
|
||||||
|
user.last_name or '')
|
||||||
|
stats = get_stats()
|
||||||
|
text = (
|
||||||
|
'<b>📊 Статистика chessCalc</b>\n\n'
|
||||||
|
f'👤 Уникальных пользователей: <b>{stats["unique_users"]}</b>\n'
|
||||||
|
f'🔢 Всего запросов: <b>{stats["total_requests"]}</b>\n'
|
||||||
|
f'📅 За сегодня: <b>{stats["today_requests"]}</b>'
|
||||||
|
)
|
||||||
|
await update.message.reply_text(text, parse_mode=ParseMode.HTML)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
if not TOKEN:
|
||||||
|
print('ADMIN_BOT_TOKEN not set', file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
app = Application.builder().token(TOKEN).build()
|
||||||
|
app.add_handler(CommandHandler('start', start))
|
||||||
|
app.add_handler(CommandHandler('stat', stat))
|
||||||
|
|
||||||
|
print('Admin bot started', file=sys.stderr)
|
||||||
|
app.run_polling()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
223
bots/client_bot.py
Normal file
223
bots/client_bot.py
Normal file
|
|
@ -0,0 +1,223 @@
|
||||||
|
"""
|
||||||
|
Клиентский Telegram-бот chessCalc.
|
||||||
|
Принимает ссылку на турнир chess-results.com, рассчитывает жеребьёвку
|
||||||
|
следующего тура и выводит пары.
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
import asyncio
|
||||||
|
from telegram import Update
|
||||||
|
from telegram.ext import Application, CommandHandler, MessageHandler, filters, ContextTypes
|
||||||
|
from telegram.constants import ParseMode
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
|
||||||
|
from swiss_calc.parser import fetch_tournament
|
||||||
|
from swiss_calc.swiss import calculate_next_round
|
||||||
|
from swiss_calc.display import format_pairings_table
|
||||||
|
from bots.stats import record_user, record_request
|
||||||
|
|
||||||
|
TOKEN = os.environ.get('CLIENT_BOT_TOKEN', '')
|
||||||
|
|
||||||
|
URL_PATTERN = re.compile(r'https?://(?:[\w-]+\.)?chess-results\.com/\S+')
|
||||||
|
START_MSG = (
|
||||||
|
'<b>♟️ chessCalc — жеребьёвка следующего тура</b>\n\n'
|
||||||
|
'Я рассчитываю пары следующего тура шахматного турнира '
|
||||||
|
'<i>до официальной публикации</i> на chess-results.com.\n\n'
|
||||||
|
'Просто пришли мне ссылку на турнир — и я покажу, '
|
||||||
|
'кто с кем играет в следующем туре.\n\n'
|
||||||
|
'Пример ссылки:\n'
|
||||||
|
'<code>https://chess-results.com/tnr1393124.aspx?lan=11</code>\n\n'
|
||||||
|
'Работает для швейцарских турниров. Движок: FIDE Dutch System (bbpPairings).'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def start(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||||
|
user = update.effective_user
|
||||||
|
record_user(user.id, user.username or '', user.first_name or '',
|
||||||
|
user.last_name or '')
|
||||||
|
await update.message.reply_text(START_MSG, parse_mode=ParseMode.HTML)
|
||||||
|
|
||||||
|
|
||||||
|
async def handle_url(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||||
|
user = update.effective_user
|
||||||
|
text = update.message.text.strip()
|
||||||
|
record_user(user.id, user.username or '', user.first_name or '',
|
||||||
|
user.last_name or '')
|
||||||
|
|
||||||
|
match = URL_PATTERN.search(text)
|
||||||
|
if not match:
|
||||||
|
await update.message.reply_text(
|
||||||
|
'❌ Не нашёл ссылку на chess-results.com в сообщении.\n\n'
|
||||||
|
'Пришли ссылку вида:\n'
|
||||||
|
'<code>https://chess-results.com/tnr1393124.aspx?lan=11</code>',
|
||||||
|
parse_mode=ParseMode.HTML)
|
||||||
|
return
|
||||||
|
|
||||||
|
url = match.group(0)
|
||||||
|
msg = await update.message.reply_text('⏳ Загружаю турнир...')
|
||||||
|
|
||||||
|
try:
|
||||||
|
tournament = fetch_tournament(url)
|
||||||
|
except Exception as e:
|
||||||
|
await msg.edit_text(f'❌ Ошибка загрузки: {e}')
|
||||||
|
record_request(user.id, url, '', False, str(e))
|
||||||
|
return
|
||||||
|
|
||||||
|
if tournament['current_round'] >= tournament['num_rounds']:
|
||||||
|
await msg.edit_text('🏁 Турнир уже завершён. Жеребьёвки не будет.')
|
||||||
|
record_request(user.id, url, tournament.get('name', ''),
|
||||||
|
True)
|
||||||
|
return
|
||||||
|
|
||||||
|
status_parts = [
|
||||||
|
f'✅ <b>{tournament["name"]}</b>',
|
||||||
|
f'📅 Сыграно туров: {tournament["current_round"]} из {tournament["num_rounds"]}',
|
||||||
|
f'👥 Участников: {len(tournament["standings"])}',
|
||||||
|
]
|
||||||
|
await msg.edit_text('\n'.join(status_parts), parse_mode=ParseMode.HTML)
|
||||||
|
|
||||||
|
await asyncio.sleep(0.5) # дать прочитать
|
||||||
|
|
||||||
|
# Check if next round pairings already published on site
|
||||||
|
base_url = re.sub(r'[&?]art=\d+', '', url)
|
||||||
|
base_url = re.sub(r'[&?]rd=\d+', '', base_url)
|
||||||
|
base_url = re.sub(r'[&?]turdet=\w+', '', base_url)
|
||||||
|
base_url = re.sub(r'[&?]SNode=\w+', '', base_url)
|
||||||
|
next_rd = tournament['current_round'] + 1
|
||||||
|
|
||||||
|
from swiss_calc.parser import fetch_url as _fetch_url, parse_round_pairings
|
||||||
|
try:
|
||||||
|
rd_html = _fetch_url(f'{base_url}&art=2&rd={next_rd}&turdet=YES')
|
||||||
|
rd_games = parse_round_pairings(rd_html, next_rd)
|
||||||
|
# Check if any game has a result (means round already played)
|
||||||
|
played = any(g.get('white_score') is not None for g in rd_games if g)
|
||||||
|
if played:
|
||||||
|
await update.message.reply_text(
|
||||||
|
f'⚠️ Тур {next_rd} уже сыгран — на сайте есть результаты.',
|
||||||
|
parse_mode=ParseMode.HTML)
|
||||||
|
record_request(user.id, url, tournament.get('name', ''), True)
|
||||||
|
return
|
||||||
|
# Check if pairings are published (unplayed games exist)
|
||||||
|
if rd_games:
|
||||||
|
await update.message.reply_text(
|
||||||
|
f'⚠️ Жеребьёвка тура {next_rd} уже опубликована на chess-results.com.\n'
|
||||||
|
'Я всё равно посчитаю, но пары могут незначительно отличаться '
|
||||||
|
'(разные редакции правил FIDE).',
|
||||||
|
parse_mode=ParseMode.HTML)
|
||||||
|
except Exception:
|
||||||
|
pass # art=2 page not accessible — pairings likely not published yet
|
||||||
|
|
||||||
|
# Calculate next round
|
||||||
|
calc_msg = await update.message.reply_text(
|
||||||
|
f'🧮 Считаю жеребьёвку тура {next_rd}...')
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = calculate_next_round(tournament)
|
||||||
|
except Exception as e:
|
||||||
|
await calc_msg.edit_text(f'❌ Ошибка расчёта: {e}')
|
||||||
|
record_request(user.id, url, tournament.get('name', ''), False, str(e))
|
||||||
|
return
|
||||||
|
|
||||||
|
output = format_pairings(result, tournament['name'])
|
||||||
|
|
||||||
|
# Telegram has 4096 char limit
|
||||||
|
if len(output) > 4000:
|
||||||
|
chunks = _chunk_output(output, 3800)
|
||||||
|
await calc_msg.edit_text(chunks[0], parse_mode=ParseMode.HTML,
|
||||||
|
disable_web_page_preview=True)
|
||||||
|
for chunk in chunks[1:]:
|
||||||
|
await update.message.reply_text(chunk, parse_mode=ParseMode.HTML,
|
||||||
|
disable_web_page_preview=True)
|
||||||
|
else:
|
||||||
|
await calc_msg.edit_text(output, parse_mode=ParseMode.HTML,
|
||||||
|
disable_web_page_preview=True)
|
||||||
|
|
||||||
|
record_request(user.id, url, tournament.get('name', ''), True)
|
||||||
|
|
||||||
|
|
||||||
|
def format_pairings(pairings_data: dict, tournament_name: str) -> str:
|
||||||
|
pairings = pairings_data['pairings']
|
||||||
|
rnd = pairings_data['round']
|
||||||
|
|
||||||
|
lines = [
|
||||||
|
f'<b>{_escape_html(tournament_name)}</b>',
|
||||||
|
f'📋 <b>Тур {rnd}</b> — пары',
|
||||||
|
'',
|
||||||
|
]
|
||||||
|
|
||||||
|
board = 1
|
||||||
|
for pairing in pairings:
|
||||||
|
if len(pairing) != 3:
|
||||||
|
continue
|
||||||
|
p1, p2, color = pairing
|
||||||
|
|
||||||
|
if color == 'bye':
|
||||||
|
lines.append(f' {board}. {_escape_html(p1.name)} — BYE')
|
||||||
|
board += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
if color == 'w':
|
||||||
|
white, black = p1, p2
|
||||||
|
else:
|
||||||
|
white, black = p2, p1
|
||||||
|
|
||||||
|
w_name = _escape_html(white.name)
|
||||||
|
b_name = _escape_html(black.name)
|
||||||
|
w_pts = white.points
|
||||||
|
b_pts = black.points
|
||||||
|
|
||||||
|
lines.append(
|
||||||
|
f' {board}. {w_name} — {b_name} '
|
||||||
|
f'<i>({w_pts}/{b_pts})</i>'
|
||||||
|
)
|
||||||
|
board += 1
|
||||||
|
|
||||||
|
lines.append('')
|
||||||
|
source = pairings_data.get('source', 'algorithm')
|
||||||
|
if source == 'bbp_pairings_fide_2025':
|
||||||
|
lines.append('<i>FIDE 2025 (bbpPairings)</i>')
|
||||||
|
elif source == 'swiss_algorithm_simplified':
|
||||||
|
lines.append('<i>Упрощённый Swiss (bbpPairings недоступен)</i>')
|
||||||
|
|
||||||
|
return '\n'.join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
def _escape_html(text: str) -> str:
|
||||||
|
return text.replace('&', '&').replace('<', '<').replace('>', '>')
|
||||||
|
|
||||||
|
|
||||||
|
def _chunk_output(text: str, size: int) -> list:
|
||||||
|
lines = text.split('\n')
|
||||||
|
chunks = []
|
||||||
|
current = []
|
||||||
|
current_len = 0
|
||||||
|
for line in lines:
|
||||||
|
if current_len + len(line) > size and current:
|
||||||
|
chunks.append('\n'.join(current))
|
||||||
|
current = [line]
|
||||||
|
current_len = len(line)
|
||||||
|
else:
|
||||||
|
current.append(line)
|
||||||
|
current_len += len(line)
|
||||||
|
if current:
|
||||||
|
chunks.append('\n'.join(current))
|
||||||
|
return chunks
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
if not TOKEN:
|
||||||
|
print('CLIENT_BOT_TOKEN not set', file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
app = Application.builder().token(TOKEN).build()
|
||||||
|
app.add_handler(CommandHandler('start', start))
|
||||||
|
app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, handle_url))
|
||||||
|
|
||||||
|
print('Client bot started', file=sys.stderr)
|
||||||
|
app.run_polling()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
95
bots/stats.py
Normal file
95
bots/stats.py
Normal file
|
|
@ -0,0 +1,95 @@
|
||||||
|
"""
|
||||||
|
Статистика использования ботов (SQLite).
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import sqlite3
|
||||||
|
from datetime import datetime, date
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
DB_PATH = os.environ.get('STATS_DB', '/app/data/tournaments.db')
|
||||||
|
|
||||||
|
|
||||||
|
def _get_conn():
|
||||||
|
p = Path(DB_PATH)
|
||||||
|
try:
|
||||||
|
p.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
except PermissionError:
|
||||||
|
# Fallback: use tempdir or cwd (local testing)
|
||||||
|
fallback = Path(__file__).parent.parent / 'data' / 'tournaments.db'
|
||||||
|
fallback.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
return _get_conn_at(fallback)
|
||||||
|
return _get_conn_at(p)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_conn_at(db_path: Path):
|
||||||
|
conn = sqlite3.connect(str(db_path))
|
||||||
|
conn.execute('PRAGMA journal_mode=WAL')
|
||||||
|
conn.execute('''
|
||||||
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
|
user_id INTEGER PRIMARY KEY,
|
||||||
|
username TEXT,
|
||||||
|
first_name TEXT,
|
||||||
|
last_name TEXT,
|
||||||
|
first_seen TEXT DEFAULT (datetime('now')),
|
||||||
|
last_seen TEXT DEFAULT (datetime('now'))
|
||||||
|
)
|
||||||
|
''')
|
||||||
|
conn.execute('''
|
||||||
|
CREATE TABLE IF NOT EXISTS requests (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
user_id INTEGER,
|
||||||
|
url TEXT,
|
||||||
|
tournament_name TEXT,
|
||||||
|
timestamp TEXT DEFAULT (datetime('now')),
|
||||||
|
success INTEGER DEFAULT 1,
|
||||||
|
error TEXT
|
||||||
|
)
|
||||||
|
''')
|
||||||
|
conn.commit()
|
||||||
|
return conn
|
||||||
|
|
||||||
|
|
||||||
|
def record_user(user_id: int, username: str = '', first_name: str = '',
|
||||||
|
last_name: str = ''):
|
||||||
|
conn = _get_conn()
|
||||||
|
now = datetime.utcnow().isoformat()
|
||||||
|
existing = conn.execute('SELECT user_id FROM users WHERE user_id = ?',
|
||||||
|
(user_id,)).fetchone()
|
||||||
|
if existing:
|
||||||
|
conn.execute('UPDATE users SET username=?, first_name=?, '
|
||||||
|
'last_name=?, last_seen=? WHERE user_id=?',
|
||||||
|
(username, first_name, last_name, now, user_id))
|
||||||
|
else:
|
||||||
|
conn.execute('INSERT INTO users (user_id, username, first_name, '
|
||||||
|
'last_name, first_seen, last_seen) VALUES (?,?,?,?,?,?)',
|
||||||
|
(user_id, username, first_name, last_name, now, now))
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
def record_request(user_id: int, url: str = '', tournament_name: str = '',
|
||||||
|
success: bool = True, error: str = ''):
|
||||||
|
conn = _get_conn()
|
||||||
|
conn.execute('INSERT INTO requests (user_id, url, tournament_name, '
|
||||||
|
'timestamp, success, error) VALUES (?,?,?,datetime(\'now\'),?,?)',
|
||||||
|
(user_id, url, tournament_name, 1 if success else 0, error))
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
def get_stats() -> dict:
|
||||||
|
conn = _get_conn()
|
||||||
|
unique_users = conn.execute(
|
||||||
|
'SELECT COUNT(*) FROM users').fetchone()[0]
|
||||||
|
total_requests = conn.execute(
|
||||||
|
'SELECT COUNT(*) FROM requests').fetchone()[0]
|
||||||
|
today = date.today().isoformat()
|
||||||
|
today_requests = conn.execute(
|
||||||
|
"SELECT COUNT(*) FROM requests WHERE date(timestamp) = ?",
|
||||||
|
(today,)).fetchone()[0]
|
||||||
|
conn.close()
|
||||||
|
return {
|
||||||
|
'unique_users': unique_users,
|
||||||
|
'total_requests': total_requests,
|
||||||
|
'today_requests': today_requests,
|
||||||
|
}
|
||||||
|
|
@ -1,9 +1,28 @@
|
||||||
---
|
---
|
||||||
services:
|
services:
|
||||||
chess-calc:
|
client-bot:
|
||||||
build: .
|
build: .
|
||||||
image: chess-calc
|
image: chess-calc
|
||||||
container_name: chess-calc
|
container_name: chess-calc-client-bot
|
||||||
command: ["--help"]
|
restart: unless-stopped
|
||||||
# Example usage with a URL:
|
env_file: .env
|
||||||
# docker compose run --rm chess-calc 'https://chess-results.com/tnr1393124.aspx?lan=11&art=2&rd=3&turdet=YES'
|
environment:
|
||||||
|
- STATS_DB=/app/data/tournaments.db
|
||||||
|
volumes:
|
||||||
|
- bot-data:/app/data
|
||||||
|
command: ["python3", "-m", "bots.client_bot"]
|
||||||
|
|
||||||
|
admin-bot:
|
||||||
|
build: .
|
||||||
|
image: chess-calc
|
||||||
|
container_name: chess-calc-admin-bot
|
||||||
|
restart: unless-stopped
|
||||||
|
env_file: .env
|
||||||
|
environment:
|
||||||
|
- STATS_DB=/app/data/tournaments.db
|
||||||
|
volumes:
|
||||||
|
- bot-data:/app/data
|
||||||
|
command: ["python3", "-m", "bots.admin_bot"]
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
bot-data:
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
requests==2.32.3
|
requests==2.32.3
|
||||||
beautifulsoup4==4.12.3
|
beautifulsoup4==4.12.3
|
||||||
rich==13.7.1
|
rich==13.7.1
|
||||||
|
python-telegram-bot[job-queue]==21.11
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue