This commit is contained in:
Leo
2026-07-09 09:20:55 +05:00
parent c36b4f839c
commit 7bc2b84294
4 changed files with 148 additions and 44 deletions
+47 -4
View File
@@ -1,5 +1,48 @@
## pogoda_tg_bot # 🌤️ Weather Telegram Bot (with Ansible Deployment)
### Телеграмм бот, который по названию города отправляет текущую погоду в нём.
___
## **Инструкция по установке выйдет позже** Бот для получения прогноза погоды, упакованный в Docker. В репозитории также находится Ansible-плейбук для автоматического развертывания на любом облачном сервере Ubuntu за 2 минуты.
## 🚀 Быстрый старт за 2 минуты
1. **Склонируйте репозиторий** командой:
```bash
git clone https://github.com/LeoR156/pogoda_tg_bot.git
cd pogoda_tg_bot
```
2. **Получите токены и ключи**:
- Создайте бота(ов) и скопируйте токен(ы) у [@BotFather](https://t.me/BotFather) в Telegram.
- Получите бесплатный API ключ на сайте OpenWeatherMap.
- Узнайте свой Telegram ID у официального бота [@userinfobot](https://t.me/userinfobot) (чтобы получать уведомления от бота как админ).
3. **Создайте файл инвентори**:
Создайте файл `inventory.ini` и добавьте туда серверы для установки. Вы можете задать общие переменные `admin` (ваш ID) и `api_pogoda` (ключ погоды) для всех серверов, чтобы не дублировать их. А вот токен телеграм-бота `tg_token` **должен быть обязательно разным для каждого сервера/бота**, иначе боты будут конфликтовать за входящие сообщения.
*Пример синтаксиса `inventory.ini`:*
```ini
[tg_bot_3x]
# Указываем IP серверов и уникальные токены ботов для каждого сервера
server1 ansible_host=222.222.222.222 tg_token="123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11"
server2 ansible_host=111.111.111.111 tg_token="987654:XYZ-ABC1234ghIkl-zyx57W2v1u123ew11"
[tg_bot_3x:vars]
# Общие переменные для всех хостов в этой группе
api_pogoda="ваш_ключ_openweathermap"
admin=123456789
```
*(Примечание: файл `.env` вам не нужен, так как Ansible сам пробросит переменные из inventory прямо в Docker-контейнеры!)*
4. **Настройте Playbook**:
В файле `playbook.yml` убедитесь, что в поле `hosts:` указана группа из вашего `inventory.ini` (в примере выше это `tg_bot_3x`).
5. **Запуск**:
Проверьте файл на ошибки командой:
```bash
ansible-playbook playbook.yml --syntax-check
```
В случае неудачи исправьте синтаксическую ошибку. Если всё хорошо, запускайте автоматическую установку командой:
```bash
ansible-playbook playbook.yml -i inventory.ini
```
Готово! Через пару минут Ansible самостоятельно установит Docker, скачает образ бота с Docker Hub и запустит контейнеры с вашими переменными.
+45 -15
View File
@@ -1,15 +1,30 @@
import sqlite3 import os
import psycopg2
from psycopg2.extras import RealDictCursor
import logging import logging
from datetime import datetime from datetime import datetime
from contextlib import contextmanager from contextlib import contextmanager
DB_PATH = 'bot_database.db'
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# Читаем параметры подключения из окружения (или используем дефолтные для локальной разработки)
DB_HOST = os.getenv("DB_HOST", "localhost")
DB_PORT = os.getenv("DB_PORT", "5432")
DB_NAME = os.getenv("DB_NAME", "postgres")
DB_USER = os.getenv("DB_USER", "postgres")
DB_PASS = os.getenv("DB_PASS", "")
@contextmanager @contextmanager
def get_db_connection(): def get_db_connection():
conn = sqlite3.connect(DB_PATH) # Используем RealDictCursor, чтобы код в tg.py мог обращаться к полям как к словарю: user['default_city']
conn.row_factory = sqlite3.Row conn = psycopg2.connect(
host=DB_HOST,
port=DB_PORT,
dbname=DB_NAME,
user=DB_USER,
password=DB_PASS,
cursor_factory=RealDictCursor
)
try: try:
yield conn yield conn
finally: finally:
@@ -17,28 +32,38 @@ def get_db_connection():
conn.close() conn.close()
def init_db(): def init_db():
try:
with get_db_connection() as conn: with get_db_connection() as conn:
conn.execute(''' with conn.cursor() as cursor:
# В PostgreSQL используем SERIAL для автоинкремента
cursor.execute('''
CREATE TABLE IF NOT EXISTS users ( CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT, id SERIAL PRIMARY KEY,
telegram_id INTEGER UNIQUE NOT NULL, telegram_id BIGINT UNIQUE NOT NULL,
username TEXT, username TEXT,
default_city TEXT, default_city TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) )
''') ''')
logger.info("База данных инициализирована.") logger.info("База данных PostgreSQL успешно инициализирована.")
except Exception as e:
logger.error(f"Ошибка при инициализации PostgreSQL: {e}")
# Если база обязательна для запуска, можно сделать raise e, чтобы бот не стартовал впустую
raise e
def get_user(telegram_id): def get_user(telegram_id):
with get_db_connection() as conn: with get_db_connection() as conn:
cursor = conn.execute('SELECT * FROM users WHERE telegram_id = ?', (telegram_id,)) with conn.cursor() as cursor:
cursor.execute('SELECT * FROM users WHERE telegram_id = %s', (telegram_id,))
return cursor.fetchone() return cursor.fetchone()
def add_user(telegram_id, username): def add_user(telegram_id, username):
with get_db_connection() as conn: with get_db_connection() as conn:
with conn.cursor() as cursor:
try: try:
conn.execute( # В PostgreSQL синтаксис избегания дубликатов - ON CONFLICT DO NOTHING
'INSERT OR IGNORE INTO users (telegram_id, username) VALUES (?, ?)', cursor.execute(
'INSERT INTO users (telegram_id, username) VALUES (%s, %s) ON CONFLICT (telegram_id) DO NOTHING',
(telegram_id, username) (telegram_id, username)
) )
except Exception as e: except Exception as e:
@@ -46,12 +71,17 @@ def add_user(telegram_id, username):
def update_default_city(telegram_id, city): def update_default_city(telegram_id, city):
with get_db_connection() as conn: with get_db_connection() as conn:
conn.execute( with conn.cursor() as cursor:
'UPDATE users SET default_city = ? WHERE telegram_id = ?', cursor.execute(
'UPDATE users SET default_city = %s WHERE telegram_id = %s',
(city, telegram_id) (city, telegram_id)
) )
def get_users_count(): def get_users_count():
with get_db_connection() as conn: with get_db_connection() as conn:
cursor = conn.execute('SELECT COUNT(*) FROM users') with conn.cursor() as cursor:
return cursor.fetchone()[0] cursor.execute('SELECT COUNT(*) FROM users')
# Так как мы используем RealDictCursor, результат будет {'count': X}
# Но для COUNT(*) безопаснее забрать по индексу или ключу
res = cursor.fetchone()
return res['count'] if res else 0
+3
View File
@@ -1,2 +1,5 @@
pyTelegramBotAPI==4.14.0 pyTelegramBotAPI==4.14.0
requests==2.31.0 requests==2.31.0
psycopg2-binary==2.9.9
Flask==3.0.3
gunicorn==22.0.0
+38 -10
View File
@@ -1,6 +1,8 @@
import os import os
import time
import telebot import telebot
import logging import logging
from flask import Flask, request, abort
import db import db
import keyboards as kb import keyboards as kb
@@ -17,17 +19,30 @@ logger = logging.getLogger(__name__)
TG_TOKEN = os.getenv("TG_TOKEN") TG_TOKEN = os.getenv("TG_TOKEN")
WEATHER_API_KEY = os.getenv("WEATHER_API_KEY") WEATHER_API_KEY = os.getenv("WEATHER_API_KEY")
ADMIN_ID = os.getenv("ADMIN_ID") ADMIN_ID = os.getenv("ADMIN_ID")
WEBHOOK_HOST = os.getenv("WEBHOOK_HOST") # e.g., https://my-bot.com
WEBHOOK_PORT = int(os.getenv("WEBHOOK_PORT", 8080))
if not TG_TOKEN or not WEATHER_API_KEY: if not TG_TOKEN or not WEATHER_API_KEY:
logger.error("Не заданы переменные окружения TG_TOKEN или WEATHER_API_KEY!") logger.error("Не заданы переменные окружения TG_TOKEN или WEATHER_API_KEY!")
exit(1) exit(1)
if not WEBHOOK_HOST:
logger.error("Не задана переменная WEBHOOK_HOST! (например: https://mydomain.com)")
exit(1)
# Формируем URL вебхука (скрываем токен в пути для безопасности)
WEBHOOK_PATH = f"/{TG_TOKEN}/"
WEBHOOK_URL = f"{WEBHOOK_HOST}{WEBHOOK_PATH}"
# Инициализация БД # Инициализация БД
db.init_db() db.init_db()
# Инициализация бота # Инициализация бота
bot = telebot.TeleBot(TG_TOKEN) bot = telebot.TeleBot(TG_TOKEN)
# Инициализация Flask приложения
app = Flask(__name__)
def get_user_city(user_id): def get_user_city(user_id):
user = db.get_user(user_id) user = db.get_user(user_id)
if user and user['default_city']: if user and user['default_city']:
@@ -62,9 +77,6 @@ def admin_stats(message):
if ADMIN_ID and str(message.from_user.id) == str(ADMIN_ID): if ADMIN_ID and str(message.from_user.id) == str(ADMIN_ID):
count = db.get_users_count() count = db.get_users_count()
bot.send_message(message.chat.id, f"📊 Уникальных пользователей в базе: {count}") bot.send_message(message.chat.id, f"📊 Уникальных пользователей в базе: {count}")
else:
# Игнорируем или выдаем сообщение о недостатке прав
pass
@bot.message_handler(func=lambda message: message.text == "Настройки") @bot.message_handler(func=lambda message: message.text == "Настройки")
def settings_menu(message): def settings_menu(message):
@@ -91,7 +103,6 @@ def process_city_input(message):
return return
city = message.text.strip() city = message.text.strip()
# Делаем проверочный запрос к API
weather_data = wa.get_current_weather(city) weather_data = wa.get_current_weather(city)
if weather_data["success"]: if weather_data["success"]:
@@ -176,7 +187,6 @@ def weather_requests(message):
@bot.message_handler(func=lambda message: True) @bot.message_handler(func=lambda message: True)
def unknown_text(message): def unknown_text(message):
# Если введен какой-то произвольный текст, предполагаем, что юзер хочет узнать погоду для этого города (разово)
city = message.text.strip() city = message.text.strip()
bot.send_chat_action(message.chat.id, 'typing') bot.send_chat_action(message.chat.id, 'typing')
@@ -189,9 +199,27 @@ def unknown_text(message):
else: else:
bot.send_message(message.chat.id, f"❌ {data['error']}\nИспользуйте меню или введите корректное название города.", reply_markup=kb.get_main_menu()) bot.send_message(message.chat.id, f"❌ {data['error']}\nИспользуйте меню или введите корректное название города.", reply_markup=kb.get_main_menu())
# --- ВЕБХУКИ (Flask) ---
@app.route(WEBHOOK_PATH, methods=['POST'])
def webhook():
if request.headers.get('content-type') == 'application/json':
json_string = request.get_data().decode('utf-8')
update = telebot.types.Update.de_json(json_string)
bot.process_new_updates([update])
return '', 200
else:
abort(403)
if __name__ == "__main__": if __name__ == "__main__":
logger.info("Бот запущен и ждет сообщений...") logger.info("Удаляем старый вебхук...")
try: bot.remove_webhook()
bot.polling(none_stop=True) time.sleep(1)
except Exception as e:
logger.error(f"Критическая ошибка при поллинге: {e}") logger.info(f"Устанавливаем новый вебхук: {WEBHOOK_URL}")
bot.set_webhook(url=WEBHOOK_URL)
logger.info(f"Flask сервер запущен на порту {WEBHOOK_PORT}")
# Для продакшена лучше использовать Gunicorn, но для Docker часто достаточно и встроенного (или Waitress)
app.run(host='0.0.0.0', port=WEBHOOK_PORT)