This commit is contained in:
Leo
2026-06-30 18:57:56 +05:00
parent 4308aae8cd
commit 9f724c0754
6 changed files with 375 additions and 48 deletions
+1
View File
@@ -216,3 +216,4 @@ __marimo__/
# Streamlit # Streamlit
.streamlit/secrets.toml .streamlit/secrets.toml
bot_database.db
+57
View File
@@ -0,0 +1,57 @@
import sqlite3
import logging
from datetime import datetime
from contextlib import contextmanager
DB_PATH = 'bot_database.db'
logger = logging.getLogger(__name__)
@contextmanager
def get_db_connection():
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
try:
yield conn
finally:
conn.commit()
conn.close()
def init_db():
with get_db_connection() as conn:
conn.execute('''
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
telegram_id INTEGER UNIQUE NOT NULL,
username TEXT,
default_city TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
''')
logger.info("База данных инициализирована.")
def get_user(telegram_id):
with get_db_connection() as conn:
cursor = conn.execute('SELECT * FROM users WHERE telegram_id = ?', (telegram_id,))
return cursor.fetchone()
def add_user(telegram_id, username):
with get_db_connection() as conn:
try:
conn.execute(
'INSERT OR IGNORE INTO users (telegram_id, username) VALUES (?, ?)',
(telegram_id, username)
)
except Exception as e:
logger.error(f"Ошибка при добавлении пользователя: {e}")
def update_default_city(telegram_id, city):
with get_db_connection() as conn:
conn.execute(
'UPDATE users SET default_city = ? WHERE telegram_id = ?',
(city, telegram_id)
)
def get_users_count():
with get_db_connection() as conn:
cursor = conn.execute('SELECT COUNT(*) FROM users')
return cursor.fetchone()[0]
+32
View File
@@ -0,0 +1,32 @@
from telebot.types import ReplyKeyboardMarkup, KeyboardButton, InlineKeyboardMarkup, InlineKeyboardButton
def get_main_menu():
markup = ReplyKeyboardMarkup(resize_keyboard=True, row_width=2)
markup.add(
KeyboardButton("Текущая погода"),
KeyboardButton("Прогноз на 3 дня"),
KeyboardButton("Прогноз на 5 дней"),
KeyboardButton("Настройки")
)
return markup
def get_settings_menu():
markup = ReplyKeyboardMarkup(resize_keyboard=True, row_width=1)
markup.add(
KeyboardButton("Изменить город"),
KeyboardButton("🔙 Назад в меню")
)
return markup
def get_cancel_menu():
markup = ReplyKeyboardMarkup(resize_keyboard=True, row_width=1)
markup.add(KeyboardButton("Отмена"))
return markup
def get_city_confirm_inline(city_name):
markup = InlineKeyboardMarkup()
markup.add(
InlineKeyboardButton("Да, сохранить", callback_data=f"save_city:{city_name}"),
InlineKeyboardButton("Нет, отмена", callback_data="cancel_city")
)
return markup
+2
View File
@@ -0,0 +1,2 @@
pyTelegramBotAPI==4.14.0
requests==2.31.0
+172 -39
View File
@@ -1,64 +1,197 @@
import os import os
import telebot import telebot
import requests import logging
# 1. Забираем токены из переменных окружения (вспоминаем флаг -e в Docker!) import db
import keyboards as kb
import weather_api as wa
# Настройка логирования
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
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")
# Проверка, что переменные действительно переданы
if not TG_TOKEN or not WEATHER_API_KEY: if not TG_TOKEN or not WEATHER_API_KEY:
print("ВНИМАНИЕ: Не заданы переменные окружения TG_TOKEN или WEATHER_API_KEY!") logger.error("Не заданы переменные окружения TG_TOKEN или WEATHER_API_KEY!")
exit(1) exit(1)
# Инициализация БД
db.init_db()
# Инициализация бота
bot = telebot.TeleBot(TG_TOKEN) bot = telebot.TeleBot(TG_TOKEN)
# 2. Реакция на команду /start def get_user_city(user_id):
user = db.get_user(user_id)
if user and user['default_city']:
return user['default_city']
return None
@bot.message_handler(commands=['start', 'help']) @bot.message_handler(commands=['start', 'help'])
def send_welcome(message): def send_welcome(message):
bot.reply_to(message, "Привет! 🌍 Напиши мне название любого города, и я пришлю прогноз погоды.") user_id = message.from_user.id
username = message.from_user.username
# Добавляем юзера в БД, если его еще нет
db.add_user(user_id, username)
welcome_text = (
"Привет! 🌍 Я твой погодный бот.\n\n"
"Я могу показывать текущую погоду и прогноз на несколько дней.\n"
"Для удобства давай установим твой город по умолчанию."
)
user_city = get_user_city(user_id)
if user_city:
welcome_text += f"\n\nТвой текущий город: <b>{user_city}</b>"
bot.send_message(message.chat.id, welcome_text, reply_markup=kb.get_main_menu(), parse_mode="HTML")
else:
bot.send_message(message.chat.id, welcome_text, parse_mode="HTML")
msg = bot.send_message(message.chat.id, "Напиши название города, который хочешь установить по умолчанию:", reply_markup=kb.get_cancel_menu())
bot.register_next_step_handler(msg, process_city_input)
@bot.message_handler(commands=['stats'])
def admin_stats(message):
if ADMIN_ID and str(message.from_user.id) == str(ADMIN_ID):
count = db.get_users_count()
bot.send_message(message.chat.id, f"📊 Уникальных пользователей в базе: {count}")
else:
# Игнорируем или выдаем сообщение о недостатке прав
pass
@bot.message_handler(func=lambda message: message.text == "Настройки")
def settings_menu(message):
user_city = get_user_city(message.from_user.id)
text = "⚙️ Настройки\n"
if user_city:
text += f"Текущий город: <b>{user_city}</b>"
else:
text += "Город по умолчанию не установлен."
bot.send_message(message.chat.id, text, reply_markup=kb.get_settings_menu(), parse_mode="HTML")
@bot.message_handler(func=lambda message: message.text == "🔙 Назад в меню")
def back_to_main(message):
bot.send_message(message.chat.id, "Возвращаемся в главное меню.", reply_markup=kb.get_main_menu())
@bot.message_handler(func=lambda message: message.text == "Изменить город")
def change_city_prompt(message):
msg = bot.send_message(message.chat.id, "Напиши название нового города:", reply_markup=kb.get_cancel_menu())
bot.register_next_step_handler(msg, process_city_input)
def process_city_input(message):
if message.text == "Отмена":
bot.send_message(message.chat.id, "Действие отменено.", reply_markup=kb.get_main_menu())
return
# 3. Реакция на любой другой текст (названия городов)
@bot.message_handler(func=lambda message: True)
def get_weather(message):
city = message.text.strip() city = message.text.strip()
url = "https://api.openweathermap.org/data/2.5/weather" # Делаем проверочный запрос к API
weather_data = wa.get_current_weather(city)
params = { if weather_data["success"]:
"q": city, real_city_name = weather_data["city"]
"appid": WEATHER_API_KEY, text = (
"units": "metric", f"Город <b>{real_city_name}</b> найден!\n"
"lang": "ru" f"Текущая температура: {weather_data['temp']}°C\n\n"
} f"Сохранить этот город как основной?"
)
bot.send_message(
message.chat.id,
text,
reply_markup=kb.get_city_confirm_inline(real_city_name),
parse_mode="HTML"
)
else:
msg = bot.send_message(
message.chat.id,
f"❌ {weather_data['error']}\nПопробуй ввести название еще раз или нажми 'Отмена'.",
reply_markup=kb.get_cancel_menu()
)
bot.register_next_step_handler(msg, process_city_input)
try: @bot.callback_query_handler(func=lambda call: call.data.startswith('save_city:'))
response = requests.get(url, params=params) def callback_save_city(call):
city_name = call.data.split(':', 1)[1]
user_id = call.from_user.id
db.update_default_city(user_id, city_name)
# Если API вернуло код 200 (Всё Ок) bot.edit_message_text(
if response.status_code == 200: chat_id=call.message.chat.id,
data = response.json() message_id=call.message.message_id,
temp = data["main"]["temp"] text=f"✅ Город <b>{city_name}</b> успешно сохранен!",
desc = data["weather"][0]["description"] parse_mode="HTML"
)
bot.send_message(call.message.chat.id, "Открываю главное меню:", reply_markup=kb.get_main_menu())
bot.answer_callback_query(call.id)
# Формируем красивый ответ @bot.callback_query_handler(func=lambda call: call.data == 'cancel_city')
answer = f"🌡 Погода в городе {city.capitalize()}:\n" def callback_cancel_city(call):
answer += f"Температура: {temp}°C\n" bot.edit_message_text(
answer += f"На улице {desc}." chat_id=call.message.chat.id,
message_id=call.message.message_id,
text="❌ Изменение города отменено."
)
bot.send_message(call.message.chat.id, "Возвращаемся в главное меню:", reply_markup=kb.get_main_menu())
bot.answer_callback_query(call.id)
bot.send_message(message.chat.id, answer) @bot.message_handler(func=lambda message: message.text in ["Текущая погода", "Прогноз на 3 дня", "Прогноз на 5 дней"])
def weather_requests(message):
user_city = get_user_city(message.from_user.id)
# Если API вернуло код 404 (Город не найден) if not user_city:
elif response.status_code == 404: msg = bot.send_message(message.chat.id, "Сначала укажите город. Напишите название:", reply_markup=kb.get_cancel_menu())
bot.send_message(message.chat.id, f"❌ Город '{city}' не найден. Проверь опечатку!") bot.register_next_step_handler(msg, process_city_input)
return
bot.send_chat_action(message.chat.id, 'typing')
if message.text == "Текущая погода":
data = wa.get_current_weather(user_city)
if data["success"]:
answer = f"🌡 Погода в городе <b>{data['city']}</b>:\n"
answer += f"Температура: <b>{data['temp']}°C</b>\n"
answer += f"На улице: {data['desc']}."
bot.send_message(message.chat.id, answer, parse_mode="HTML")
else: else:
bot.send_message(message.chat.id, "⚠️ Ошибка связи с сервером погоды.") bot.send_message(message.chat.id, f"❌ Ошибка: {data['error']}")
print(f"Ошибка API: Код {response.status_code}")
elif message.text == "Прогноз на 3 дня" or message.text == "Прогноз на 5 дней":
days = 3 if "3" in message.text else 5
data = wa.get_forecast(user_city, days)
if data["success"]:
answer = f"📅 Прогноз погоды для <b>{data['city']}</b> на {days} дн.:\n\n"
for day in data["forecast"]:
answer += f"🔹 <b>{day['date']}</b>\n"
answer += f" Мин: {day['min_temp']}°C | Макс: {day['max_temp']}°C\n"
answer += f" {day['desc']}\n\n"
bot.send_message(message.chat.id, answer, parse_mode="HTML")
else:
bot.send_message(message.chat.id, f"❌ Ошибка: {data['error']}")
@bot.message_handler(func=lambda message: True)
def unknown_text(message):
# Если введен какой-то произвольный текст, предполагаем, что юзер хочет узнать погоду для этого города (разово)
city = message.text.strip()
bot.send_chat_action(message.chat.id, 'typing')
data = wa.get_current_weather(city)
if data["success"]:
answer = f"🌡 Текущая погода (разовый запрос) в <b>{data['city']}</b>:\n"
answer += f"Температура: <b>{data['temp']}°C</b>\n"
answer += f"На улице: {data['desc']}."
bot.send_message(message.chat.id, answer, parse_mode="HTML")
else:
bot.send_message(message.chat.id, f"❌ {data['error']}\nИспользуйте меню или введите корректное название города.", reply_markup=kb.get_main_menu())
if __name__ == "__main__":
logger.info("Бот запущен и ждет сообщений...")
try:
bot.polling(none_stop=True)
except Exception as e: except Exception as e:
bot.send_message(message.chat.id, "🔌 Внутренняя ошибка бота.") logger.error(f"Критическая ошибка при поллинге: {e}")
print(f"Ошибка кода: {e}")
# Запуск постоянного опроса серверов Telegram
print("Бот запущен и ждет сообщений...")
bot.polling(none_stop=True)
+102
View File
@@ -0,0 +1,102 @@
import os
import requests
import logging
from collections import defaultdict
from datetime import datetime
logger = logging.getLogger(__name__)
WEATHER_API_KEY = os.getenv("WEATHER_API_KEY")
BASE_URL = "https://api.openweathermap.org/data/2.5"
TIMEOUT = 10
def get_current_weather(city: str) -> dict:
url = f"{BASE_URL}/weather"
params = {
"q": city,
"appid": WEATHER_API_KEY,
"units": "metric",
"lang": "ru"
}
try:
response = requests.get(url, params=params, timeout=TIMEOUT)
if response.status_code == 200:
data = response.json()
return {
"success": True,
"city": data["name"],
"temp": data["main"]["temp"],
"desc": data["weather"][0]["description"].capitalize()
}
elif response.status_code == 404:
return {"success": False, "error": f"Город '{city}' не найден."}
else:
logger.error(f"API Error: Code {response.status_code} for city {city}")
return {"success": False, "error": "Ошибка связи с сервером погоды."}
except requests.exceptions.RequestException as e:
logger.error(f"Request exception: {e}")
return {"success": False, "error": "Превышено время ожидания или ошибка сети."}
def get_forecast(city: str, days: int) -> dict:
url = f"{BASE_URL}/forecast"
params = {
"q": city,
"appid": WEATHER_API_KEY,
"units": "metric",
"lang": "ru"
}
try:
response = requests.get(url, params=params, timeout=TIMEOUT)
if response.status_code == 200:
data = response.json()
city_name = data["city"]["name"]
# Агрегация данных по дням
daily_data = defaultdict(lambda: {"temps": [], "descs": []})
for item in data["list"]:
# дата в формате 'YYYY-MM-DD HH:MM:SS'
date_str = item["dt_txt"].split(" ")[0]
daily_data[date_str]["temps"].append(item["main"]["temp"])
daily_data[date_str]["descs"].append(item["weather"][0]["description"])
# Сортируем дни
sorted_dates = sorted(daily_data.keys())
# Ограничиваем количество дней
target_dates = sorted_dates[:days]
result = []
for date_str in target_dates:
temps = daily_data[date_str]["temps"]
descs = daily_data[date_str]["descs"]
min_temp = min(temps)
max_temp = max(temps)
# Самое частое описание погоды за день
most_common_desc = max(set(descs), key=descs.count).capitalize()
# Перевод даты в читаемый формат
date_obj = datetime.strptime(date_str, "%Y-%m-%d")
formatted_date = date_obj.strftime("%d.%m.%Y")
result.append({
"date": formatted_date,
"min_temp": min_temp,
"max_temp": max_temp,
"desc": most_common_desc
})
return {"success": True, "city": city_name, "forecast": result}
elif response.status_code == 404:
return {"success": False, "error": f"Город '{city}' не найден."}
else:
logger.error(f"API Error (Forecast): Code {response.status_code} for city {city}")
return {"success": False, "error": "Ошибка связи с сервером погоды."}
except requests.exceptions.RequestException as e:
logger.error(f"Request exception (Forecast): {e}")
return {"success": False, "error": "Превышено время ожидания или ошибка сети."}