Исправил форматирование кода по PEP8
This commit is contained in:
@@ -14,23 +14,23 @@ DB_NAME = os.getenv("DB_NAME", "postgres")
|
|||||||
DB_USER = os.getenv("DB_USER", "postgres")
|
DB_USER = os.getenv("DB_USER", "postgres")
|
||||||
DB_PASS = os.getenv("DB_PASS", "")
|
DB_PASS = os.getenv("DB_PASS", "")
|
||||||
|
|
||||||
|
|
||||||
@contextmanager
|
@contextmanager
|
||||||
def get_db_connection():
|
def get_db_connection():
|
||||||
# Используем RealDictCursor, чтобы код в tg.py мог обращаться к полям как к словарю: user['default_city']
|
# Используем RealDictCursor, чтобы код в tg.py мог обращаться к полям как к словарю: user['default_city']
|
||||||
conn = psycopg2.connect(
|
conn = psycopg2.connect(host=DB_HOST,
|
||||||
host=DB_HOST,
|
port=DB_PORT,
|
||||||
port=DB_PORT,
|
dbname=DB_NAME,
|
||||||
dbname=DB_NAME,
|
user=DB_USER,
|
||||||
user=DB_USER,
|
password=DB_PASS,
|
||||||
password=DB_PASS,
|
cursor_factory=RealDictCursor)
|
||||||
cursor_factory=RealDictCursor
|
|
||||||
)
|
|
||||||
try:
|
try:
|
||||||
yield conn
|
yield conn
|
||||||
finally:
|
finally:
|
||||||
conn.commit()
|
conn.commit()
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
def init_db():
|
def init_db():
|
||||||
try:
|
try:
|
||||||
with get_db_connection() as conn:
|
with get_db_connection() as conn:
|
||||||
@@ -51,12 +51,15 @@ def init_db():
|
|||||||
# Если база обязательна для запуска, можно сделать raise e, чтобы бот не стартовал впустую
|
# Если база обязательна для запуска, можно сделать raise 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:
|
||||||
with conn.cursor() as cursor:
|
with conn.cursor() as cursor:
|
||||||
cursor.execute('SELECT * FROM users WHERE telegram_id = %s', (telegram_id,))
|
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:
|
with conn.cursor() as cursor:
|
||||||
@@ -64,18 +67,18 @@ def add_user(telegram_id, username):
|
|||||||
# В PostgreSQL синтаксис избегания дубликатов - ON CONFLICT DO NOTHING
|
# В PostgreSQL синтаксис избегания дубликатов - ON CONFLICT DO NOTHING
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
'INSERT INTO users (telegram_id, username) VALUES (%s, %s) ON CONFLICT (telegram_id) DO NOTHING',
|
'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:
|
||||||
logger.error(f"Ошибка при добавлении пользователя: {e}")
|
logger.error(f"Ошибка при добавлении пользователя: {e}")
|
||||||
|
|
||||||
|
|
||||||
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:
|
||||||
with conn.cursor() as cursor:
|
with conn.cursor() as cursor:
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
'UPDATE users SET default_city = %s WHERE telegram_id = %s',
|
'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:
|
||||||
|
|||||||
+13
-13
@@ -1,32 +1,32 @@
|
|||||||
from telebot.types import ReplyKeyboardMarkup, KeyboardButton, InlineKeyboardMarkup, InlineKeyboardButton
|
from telebot.types import ReplyKeyboardMarkup, KeyboardButton, InlineKeyboardMarkup, InlineKeyboardButton
|
||||||
|
|
||||||
|
|
||||||
def get_main_menu():
|
def get_main_menu():
|
||||||
markup = ReplyKeyboardMarkup(resize_keyboard=True, row_width=2)
|
markup = ReplyKeyboardMarkup(resize_keyboard=True, row_width=2)
|
||||||
markup.add(
|
markup.add(KeyboardButton("Текущая погода"),
|
||||||
KeyboardButton("Текущая погода"),
|
KeyboardButton("Прогноз на 3 дня"),
|
||||||
KeyboardButton("Прогноз на 3 дня"),
|
KeyboardButton("Прогноз на 5 дней"),
|
||||||
KeyboardButton("Прогноз на 5 дней"),
|
KeyboardButton("Настройки"))
|
||||||
KeyboardButton("Настройки")
|
|
||||||
)
|
|
||||||
return markup
|
return markup
|
||||||
|
|
||||||
|
|
||||||
def get_settings_menu():
|
def get_settings_menu():
|
||||||
markup = ReplyKeyboardMarkup(resize_keyboard=True, row_width=1)
|
markup = ReplyKeyboardMarkup(resize_keyboard=True, row_width=1)
|
||||||
markup.add(
|
markup.add(KeyboardButton("Изменить город"),
|
||||||
KeyboardButton("Изменить город"),
|
KeyboardButton("🔙 Назад в меню"))
|
||||||
KeyboardButton("🔙 Назад в меню")
|
|
||||||
)
|
|
||||||
return markup
|
return markup
|
||||||
|
|
||||||
|
|
||||||
def get_cancel_menu():
|
def get_cancel_menu():
|
||||||
markup = ReplyKeyboardMarkup(resize_keyboard=True, row_width=1)
|
markup = ReplyKeyboardMarkup(resize_keyboard=True, row_width=1)
|
||||||
markup.add(KeyboardButton("Отмена"))
|
markup.add(KeyboardButton("Отмена"))
|
||||||
return markup
|
return markup
|
||||||
|
|
||||||
|
|
||||||
def get_city_confirm_inline(city_name):
|
def get_city_confirm_inline(city_name):
|
||||||
markup = InlineKeyboardMarkup()
|
markup = InlineKeyboardMarkup()
|
||||||
markup.add(
|
markup.add(
|
||||||
InlineKeyboardButton("Да, сохранить", callback_data=f"save_city:{city_name}"),
|
InlineKeyboardButton("Да, сохранить",
|
||||||
InlineKeyboardButton("Нет, отмена", callback_data="cancel_city")
|
callback_data=f"save_city:{city_name}"),
|
||||||
)
|
InlineKeyboardButton("Нет, отмена", callback_data="cancel_city"))
|
||||||
return markup
|
return markup
|
||||||
|
|||||||
@@ -1,14 +1,17 @@
|
|||||||
# test_bot.py
|
# test_bot.py
|
||||||
from weather_utils import get_weather_comment
|
from weather_utils import get_weather_comment
|
||||||
|
|
||||||
|
|
||||||
def test_weather_comment_hot():
|
def test_weather_comment_hot():
|
||||||
# Проверяем, что при +30 будет "Жара! ☀️"
|
# Проверяем, что при +30 будет "Жара! ☀️"
|
||||||
assert get_weather_comment(30) == "Жара! ☀️"
|
assert get_weather_comment(30) == "Жара! ☀️"
|
||||||
|
|
||||||
|
|
||||||
def test_weather_comment_cold():
|
def test_weather_comment_cold():
|
||||||
# Проверяем, что при -10 будет "Дубак! ❄️"
|
# Проверяем, что при -10 будет "Дубак! ❄️"
|
||||||
assert get_weather_comment(-10) == "Дубак! ❄️"
|
assert get_weather_comment(-10) == "Дубак! ❄️"
|
||||||
|
|
||||||
|
|
||||||
def test_weather_comment_normal():
|
def test_weather_comment_normal():
|
||||||
# Проверяем промежуточное значение
|
# Проверяем промежуточное значение
|
||||||
assert get_weather_comment(15) == "Нормальная погода ☁️"
|
assert get_weather_comment(15) == "Нормальная погода ☁️"
|
||||||
|
|||||||
@@ -11,8 +11,7 @@ import weather_api as wa
|
|||||||
# Настройка логирования
|
# Настройка логирования
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
level=logging.INFO,
|
level=logging.INFO,
|
||||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
||||||
)
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# Инициализация переменных окружения
|
# Инициализация переменных окружения
|
||||||
@@ -23,11 +22,13 @@ WEBHOOK_HOST = os.getenv("WEBHOOK_HOST") # e.g., https://my-bot.com
|
|||||||
WEBHOOK_PORT = int(os.getenv("WEBHOOK_PORT", 8080))
|
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:
|
if not WEBHOOK_HOST:
|
||||||
logger.error("Не задана переменная WEBHOOK_HOST! (например: https://mydomain.com)")
|
logger.error(
|
||||||
|
"Не задана переменная WEBHOOK_HOST! (например: https://mydomain.com)")
|
||||||
exit(1)
|
exit(1)
|
||||||
|
|
||||||
# Формируем URL вебхука (скрываем токен в пути для безопасности)
|
# Формируем URL вебхука (скрываем токен в пути для безопасности)
|
||||||
@@ -43,40 +44,50 @@ bot = telebot.TeleBot(TG_TOKEN)
|
|||||||
# Инициализация Flask приложения
|
# Инициализация Flask приложения
|
||||||
app = Flask(__name__)
|
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']:
|
||||||
return user['default_city']
|
return user['default_city']
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
@bot.message_handler(commands=['start', 'help'])
|
@bot.message_handler(commands=['start', 'help'])
|
||||||
def send_welcome(message):
|
def send_welcome(message):
|
||||||
user_id = message.from_user.id
|
user_id = message.from_user.id
|
||||||
username = message.from_user.username
|
username = message.from_user.username
|
||||||
|
|
||||||
# Добавляем юзера в БД, если его еще нет
|
# Добавляем юзера в БД, если его еще нет
|
||||||
db.add_user(user_id, username)
|
db.add_user(user_id, username)
|
||||||
|
|
||||||
welcome_text = (
|
welcome_text = (
|
||||||
"Привет! 🌍 Я твой погодный бот.\n\n"
|
"Привет! 🌍 Я твой погодный бот.\n\n"
|
||||||
"Я могу показывать текущую погоду и прогноз на несколько дней.\n"
|
"Я могу показывать текущую погоду и прогноз на несколько дней.\n"
|
||||||
"Для удобства давай установим твой город по умолчанию."
|
"Для удобства давай установим твой город по умолчанию.")
|
||||||
)
|
|
||||||
|
|
||||||
user_city = get_user_city(user_id)
|
user_city = get_user_city(user_id)
|
||||||
if user_city:
|
if user_city:
|
||||||
welcome_text += f"\n\nТвой текущий город: <b>{user_city}</b>"
|
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")
|
bot.send_message(message.chat.id,
|
||||||
|
welcome_text,
|
||||||
|
reply_markup=kb.get_main_menu(),
|
||||||
|
parse_mode="HTML")
|
||||||
else:
|
else:
|
||||||
bot.send_message(message.chat.id, welcome_text, parse_mode="HTML")
|
bot.send_message(message.chat.id, welcome_text, parse_mode="HTML")
|
||||||
msg = bot.send_message(message.chat.id, "Напиши название города, который хочешь установить по умолчанию:", reply_markup=kb.get_cancel_menu())
|
msg = bot.send_message(
|
||||||
|
message.chat.id,
|
||||||
|
"Напиши название города, который хочешь установить по умолчанию:",
|
||||||
|
reply_markup=kb.get_cancel_menu())
|
||||||
bot.register_next_step_handler(msg, process_city_input)
|
bot.register_next_step_handler(msg, process_city_input)
|
||||||
|
|
||||||
|
|
||||||
@bot.message_handler(commands=['stats'])
|
@bot.message_handler(commands=['stats'])
|
||||||
def admin_stats(message):
|
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}")
|
||||||
|
|
||||||
|
|
||||||
@bot.message_handler(func=lambda message: message.text == "Настройки")
|
@bot.message_handler(func=lambda message: message.text == "Настройки")
|
||||||
def settings_menu(message):
|
def settings_menu(message):
|
||||||
@@ -86,82 +97,98 @@ def settings_menu(message):
|
|||||||
text += f"Текущий город: <b>{user_city}</b>"
|
text += f"Текущий город: <b>{user_city}</b>"
|
||||||
else:
|
else:
|
||||||
text += "Город по умолчанию не установлен."
|
text += "Город по умолчанию не установлен."
|
||||||
bot.send_message(message.chat.id, text, reply_markup=kb.get_settings_menu(), parse_mode="HTML")
|
bot.send_message(message.chat.id,
|
||||||
|
text,
|
||||||
|
reply_markup=kb.get_settings_menu(),
|
||||||
|
parse_mode="HTML")
|
||||||
|
|
||||||
|
|
||||||
@bot.message_handler(func=lambda message: message.text == "🔙 Назад в меню")
|
@bot.message_handler(func=lambda message: message.text == "🔙 Назад в меню")
|
||||||
def back_to_main(message):
|
def back_to_main(message):
|
||||||
bot.send_message(message.chat.id, "Возвращаемся в главное меню.", reply_markup=kb.get_main_menu())
|
bot.send_message(message.chat.id,
|
||||||
|
"Возвращаемся в главное меню.",
|
||||||
|
reply_markup=kb.get_main_menu())
|
||||||
|
|
||||||
|
|
||||||
@bot.message_handler(func=lambda message: message.text == "Изменить город")
|
@bot.message_handler(func=lambda message: message.text == "Изменить город")
|
||||||
def change_city_prompt(message):
|
def change_city_prompt(message):
|
||||||
msg = bot.send_message(message.chat.id, "Напиши название нового города:", reply_markup=kb.get_cancel_menu())
|
msg = bot.send_message(message.chat.id,
|
||||||
|
"Напиши название нового города:",
|
||||||
|
reply_markup=kb.get_cancel_menu())
|
||||||
bot.register_next_step_handler(msg, process_city_input)
|
bot.register_next_step_handler(msg, process_city_input)
|
||||||
|
|
||||||
|
|
||||||
def process_city_input(message):
|
def process_city_input(message):
|
||||||
if message.text == "Отмена":
|
if message.text == "Отмена":
|
||||||
bot.send_message(message.chat.id, "Действие отменено.", reply_markup=kb.get_main_menu())
|
bot.send_message(message.chat.id,
|
||||||
|
"Действие отменено.",
|
||||||
|
reply_markup=kb.get_main_menu())
|
||||||
return
|
return
|
||||||
|
|
||||||
city = message.text.strip()
|
city = message.text.strip()
|
||||||
weather_data = wa.get_current_weather(city)
|
weather_data = wa.get_current_weather(city)
|
||||||
|
|
||||||
if weather_data["success"]:
|
if weather_data["success"]:
|
||||||
real_city_name = weather_data["city"]
|
real_city_name = weather_data["city"]
|
||||||
text = (
|
text = (f"Город <b>{real_city_name}</b> найден!\n"
|
||||||
f"Город <b>{real_city_name}</b> найден!\n"
|
f"Текущая температура: {weather_data['temp']}°C\n\n"
|
||||||
f"Текущая температура: {weather_data['temp']}°C\n\n"
|
f"Сохранить этот город как основной?")
|
||||||
f"Сохранить этот город как основной?"
|
|
||||||
)
|
|
||||||
bot.send_message(
|
bot.send_message(
|
||||||
message.chat.id,
|
message.chat.id,
|
||||||
text,
|
text,
|
||||||
reply_markup=kb.get_city_confirm_inline(real_city_name),
|
reply_markup=kb.get_city_confirm_inline(real_city_name),
|
||||||
parse_mode="HTML"
|
parse_mode="HTML")
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
msg = bot.send_message(
|
msg = bot.send_message(
|
||||||
message.chat.id,
|
message.chat.id,
|
||||||
f"❌ {weather_data['error']}\nПопробуй ввести название еще раз или нажми 'Отмена'.",
|
f"❌ {weather_data['error']}\nПопробуй ввести название еще раз или нажми 'Отмена'.",
|
||||||
reply_markup=kb.get_cancel_menu()
|
reply_markup=kb.get_cancel_menu())
|
||||||
)
|
|
||||||
bot.register_next_step_handler(msg, process_city_input)
|
bot.register_next_step_handler(msg, process_city_input)
|
||||||
|
|
||||||
@bot.callback_query_handler(func=lambda call: call.data.startswith('save_city:'))
|
|
||||||
|
@bot.callback_query_handler(
|
||||||
|
func=lambda call: call.data.startswith('save_city:'))
|
||||||
def callback_save_city(call):
|
def callback_save_city(call):
|
||||||
city_name = call.data.split(':', 1)[1]
|
city_name = call.data.split(':', 1)[1]
|
||||||
user_id = call.from_user.id
|
user_id = call.from_user.id
|
||||||
db.update_default_city(user_id, city_name)
|
db.update_default_city(user_id, city_name)
|
||||||
|
|
||||||
bot.edit_message_text(
|
bot.edit_message_text(chat_id=call.message.chat.id,
|
||||||
chat_id=call.message.chat.id,
|
message_id=call.message.message_id,
|
||||||
message_id=call.message.message_id,
|
text=f"✅ Город <b>{city_name}</b> успешно сохранен!",
|
||||||
text=f"✅ Город <b>{city_name}</b> успешно сохранен!",
|
parse_mode="HTML")
|
||||||
parse_mode="HTML"
|
bot.send_message(call.message.chat.id,
|
||||||
)
|
"Открываю главное меню:",
|
||||||
bot.send_message(call.message.chat.id, "Открываю главное меню:", reply_markup=kb.get_main_menu())
|
reply_markup=kb.get_main_menu())
|
||||||
bot.answer_callback_query(call.id)
|
bot.answer_callback_query(call.id)
|
||||||
|
|
||||||
|
|
||||||
@bot.callback_query_handler(func=lambda call: call.data == 'cancel_city')
|
@bot.callback_query_handler(func=lambda call: call.data == 'cancel_city')
|
||||||
def callback_cancel_city(call):
|
def callback_cancel_city(call):
|
||||||
bot.edit_message_text(
|
bot.edit_message_text(chat_id=call.message.chat.id,
|
||||||
chat_id=call.message.chat.id,
|
message_id=call.message.message_id,
|
||||||
message_id=call.message.message_id,
|
text="❌ Изменение города отменено.")
|
||||||
text="❌ Изменение города отменено."
|
bot.send_message(call.message.chat.id,
|
||||||
)
|
"Возвращаемся в главное меню:",
|
||||||
bot.send_message(call.message.chat.id, "Возвращаемся в главное меню:", reply_markup=kb.get_main_menu())
|
reply_markup=kb.get_main_menu())
|
||||||
bot.answer_callback_query(call.id)
|
bot.answer_callback_query(call.id)
|
||||||
|
|
||||||
@bot.message_handler(func=lambda message: message.text in ["Текущая погода", "Прогноз на 3 дня", "Прогноз на 5 дней"])
|
|
||||||
|
@bot.message_handler(
|
||||||
|
func=lambda message: message.text in
|
||||||
|
["Текущая погода", "Прогноз на 3 дня", "Прогноз на 5 дней"])
|
||||||
def weather_requests(message):
|
def weather_requests(message):
|
||||||
user_city = get_user_city(message.from_user.id)
|
user_city = get_user_city(message.from_user.id)
|
||||||
|
|
||||||
if not user_city:
|
if not user_city:
|
||||||
msg = bot.send_message(message.chat.id, "Сначала укажите город. Напишите название:", reply_markup=kb.get_cancel_menu())
|
msg = bot.send_message(message.chat.id,
|
||||||
|
"Сначала укажите город. Напишите название:",
|
||||||
|
reply_markup=kb.get_cancel_menu())
|
||||||
bot.register_next_step_handler(msg, process_city_input)
|
bot.register_next_step_handler(msg, process_city_input)
|
||||||
return
|
return
|
||||||
|
|
||||||
bot.send_chat_action(message.chat.id, 'typing')
|
bot.send_chat_action(message.chat.id, 'typing')
|
||||||
|
|
||||||
if message.text == "Текущая погода":
|
if message.text == "Текущая погода":
|
||||||
data = wa.get_current_weather(user_city)
|
data = wa.get_current_weather(user_city)
|
||||||
if data["success"]:
|
if data["success"]:
|
||||||
@@ -171,7 +198,7 @@ def weather_requests(message):
|
|||||||
bot.send_message(message.chat.id, answer, parse_mode="HTML")
|
bot.send_message(message.chat.id, answer, parse_mode="HTML")
|
||||||
else:
|
else:
|
||||||
bot.send_message(message.chat.id, f"❌ Ошибка: {data['error']}")
|
bot.send_message(message.chat.id, f"❌ Ошибка: {data['error']}")
|
||||||
|
|
||||||
elif message.text == "Прогноз на 3 дня" or message.text == "Прогноз на 5 дней":
|
elif message.text == "Прогноз на 3 дня" or message.text == "Прогноз на 5 дней":
|
||||||
days = 3 if "3" in message.text else 5
|
days = 3 if "3" in message.text else 5
|
||||||
data = wa.get_forecast(user_city, days)
|
data = wa.get_forecast(user_city, days)
|
||||||
@@ -185,11 +212,12 @@ def weather_requests(message):
|
|||||||
else:
|
else:
|
||||||
bot.send_message(message.chat.id, f"❌ Ошибка: {data['error']}")
|
bot.send_message(message.chat.id, f"❌ Ошибка: {data['error']}")
|
||||||
|
|
||||||
|
|
||||||
@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')
|
||||||
|
|
||||||
data = wa.get_current_weather(city)
|
data = wa.get_current_weather(city)
|
||||||
if data["success"]:
|
if data["success"]:
|
||||||
answer = f"🌡 Текущая погода (разовый запрос) в <b>{data['city']}</b>:\n"
|
answer = f"🌡 Текущая погода (разовый запрос) в <b>{data['city']}</b>:\n"
|
||||||
@@ -197,11 +225,15 @@ def unknown_text(message):
|
|||||||
answer += f"На улице: {data['desc']}."
|
answer += f"На улице: {data['desc']}."
|
||||||
bot.send_message(message.chat.id, answer, parse_mode="HTML")
|
bot.send_message(message.chat.id, answer, parse_mode="HTML")
|
||||||
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) ---
|
# --- ВЕБХУКИ (Flask) ---
|
||||||
|
|
||||||
|
|
||||||
@app.route(WEBHOOK_PATH, methods=['POST'])
|
@app.route(WEBHOOK_PATH, methods=['POST'])
|
||||||
def webhook():
|
def webhook():
|
||||||
if request.headers.get('content-type') == 'application/json':
|
if request.headers.get('content-type') == 'application/json':
|
||||||
@@ -212,14 +244,15 @@ def webhook():
|
|||||||
else:
|
else:
|
||||||
abort(403)
|
abort(403)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
logger.info("Удаляем старый вебхук...")
|
logger.info("Удаляем старый вебхук...")
|
||||||
bot.remove_webhook()
|
bot.remove_webhook()
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
|
|
||||||
logger.info(f"Устанавливаем новый вебхук: {WEBHOOK_URL}")
|
logger.info(f"Устанавливаем новый вебхук: {WEBHOOK_URL}")
|
||||||
bot.set_webhook(url=WEBHOOK_URL)
|
bot.set_webhook(url=WEBHOOK_URL)
|
||||||
|
|
||||||
logger.info(f"Flask сервер запущен на порту {WEBHOOK_PORT}")
|
logger.info(f"Flask сервер запущен на порту {WEBHOOK_PORT}")
|
||||||
# Для продакшена лучше использовать Gunicorn, но для Docker часто достаточно и встроенного (или Waitress)
|
# Для продакшена лучше использовать Gunicorn, но для Docker часто достаточно и встроенного (или Waitress)
|
||||||
app.run(host='0.0.0.0', port=WEBHOOK_PORT)
|
app.run(host='0.0.0.0', port=WEBHOOK_PORT)
|
||||||
|
|||||||
+40
-21
@@ -10,6 +10,7 @@ WEATHER_API_KEY = os.getenv("WEATHER_API_KEY")
|
|||||||
BASE_URL = "https://api.openweathermap.org/data/2.5"
|
BASE_URL = "https://api.openweathermap.org/data/2.5"
|
||||||
TIMEOUT = 10
|
TIMEOUT = 10
|
||||||
|
|
||||||
|
|
||||||
def get_current_weather(city: str) -> dict:
|
def get_current_weather(city: str) -> dict:
|
||||||
url = f"{BASE_URL}/weather"
|
url = f"{BASE_URL}/weather"
|
||||||
params = {
|
params = {
|
||||||
@@ -18,7 +19,7 @@ def get_current_weather(city: str) -> dict:
|
|||||||
"units": "metric",
|
"units": "metric",
|
||||||
"lang": "ru"
|
"lang": "ru"
|
||||||
}
|
}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response = requests.get(url, params=params, timeout=TIMEOUT)
|
response = requests.get(url, params=params, timeout=TIMEOUT)
|
||||||
if response.status_code == 200:
|
if response.status_code == 200:
|
||||||
@@ -32,11 +33,19 @@ def get_current_weather(city: str) -> dict:
|
|||||||
elif response.status_code == 404:
|
elif response.status_code == 404:
|
||||||
return {"success": False, "error": f"Город '{city}' не найден."}
|
return {"success": False, "error": f"Город '{city}' не найден."}
|
||||||
else:
|
else:
|
||||||
logger.error(f"API Error: Code {response.status_code} for city {city}")
|
logger.error(
|
||||||
return {"success": False, "error": "Ошибка связи с сервером погоды."}
|
f"API Error: Code {response.status_code} for city {city}")
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": "Ошибка связи с сервером погоды."
|
||||||
|
}
|
||||||
except requests.exceptions.RequestException as e:
|
except requests.exceptions.RequestException as e:
|
||||||
logger.error(f"Request exception: {e}")
|
logger.error(f"Request exception: {e}")
|
||||||
return {"success": False, "error": "Превышено время ожидания или ошибка сети."}
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": "Превышено время ожидания или ошибка сети."
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def get_forecast(city: str, days: int) -> dict:
|
def get_forecast(city: str, days: int) -> dict:
|
||||||
url = f"{BASE_URL}/forecast"
|
url = f"{BASE_URL}/forecast"
|
||||||
@@ -46,57 +55,67 @@ def get_forecast(city: str, days: int) -> dict:
|
|||||||
"units": "metric",
|
"units": "metric",
|
||||||
"lang": "ru"
|
"lang": "ru"
|
||||||
}
|
}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response = requests.get(url, params=params, timeout=TIMEOUT)
|
response = requests.get(url, params=params, timeout=TIMEOUT)
|
||||||
if response.status_code == 200:
|
if response.status_code == 200:
|
||||||
data = response.json()
|
data = response.json()
|
||||||
city_name = data["city"]["name"]
|
city_name = data["city"]["name"]
|
||||||
|
|
||||||
# Агрегация данных по дням
|
# Агрегация данных по дням
|
||||||
daily_data = defaultdict(lambda: {"temps": [], "descs": []})
|
daily_data = defaultdict(lambda: {"temps": [], "descs": []})
|
||||||
|
|
||||||
for item in data["list"]:
|
for item in data["list"]:
|
||||||
# дата в формате 'YYYY-MM-DD HH:MM:SS'
|
# дата в формате 'YYYY-MM-DD HH:MM:SS'
|
||||||
date_str = item["dt_txt"].split(" ")[0]
|
date_str = item["dt_txt"].split(" ")[0]
|
||||||
daily_data[date_str]["temps"].append(item["main"]["temp"])
|
daily_data[date_str]["temps"].append(item["main"]["temp"])
|
||||||
daily_data[date_str]["descs"].append(item["weather"][0]["description"])
|
daily_data[date_str]["descs"].append(
|
||||||
|
item["weather"][0]["description"])
|
||||||
|
|
||||||
# Сортируем дни
|
# Сортируем дни
|
||||||
sorted_dates = sorted(daily_data.keys())
|
sorted_dates = sorted(daily_data.keys())
|
||||||
|
|
||||||
# Ограничиваем количество дней
|
# Ограничиваем количество дней
|
||||||
target_dates = sorted_dates[:days]
|
target_dates = sorted_dates[:days]
|
||||||
|
|
||||||
result = []
|
result = []
|
||||||
for date_str in target_dates:
|
for date_str in target_dates:
|
||||||
temps = daily_data[date_str]["temps"]
|
temps = daily_data[date_str]["temps"]
|
||||||
descs = daily_data[date_str]["descs"]
|
descs = daily_data[date_str]["descs"]
|
||||||
|
|
||||||
min_temp = min(temps)
|
min_temp = min(temps)
|
||||||
max_temp = max(temps)
|
max_temp = max(temps)
|
||||||
|
|
||||||
# Самое частое описание погоды за день
|
# Самое частое описание погоды за день
|
||||||
most_common_desc = max(set(descs), key=descs.count).capitalize()
|
most_common_desc = max(set(descs),
|
||||||
|
key=descs.count).capitalize()
|
||||||
|
|
||||||
# Перевод даты в читаемый формат
|
# Перевод даты в читаемый формат
|
||||||
date_obj = datetime.strptime(date_str, "%Y-%m-%d")
|
date_obj = datetime.strptime(date_str, "%Y-%m-%d")
|
||||||
formatted_date = date_obj.strftime("%d.%m.%Y")
|
formatted_date = date_obj.strftime("%d.%m.%Y")
|
||||||
|
|
||||||
result.append({
|
result.append({
|
||||||
"date": formatted_date,
|
"date": formatted_date,
|
||||||
"min_temp": min_temp,
|
"min_temp": min_temp,
|
||||||
"max_temp": max_temp,
|
"max_temp": max_temp,
|
||||||
"desc": most_common_desc
|
"desc": most_common_desc
|
||||||
})
|
})
|
||||||
|
|
||||||
return {"success": True, "city": city_name, "forecast": result}
|
return {"success": True, "city": city_name, "forecast": result}
|
||||||
|
|
||||||
elif response.status_code == 404:
|
elif response.status_code == 404:
|
||||||
return {"success": False, "error": f"Город '{city}' не найден."}
|
return {"success": False, "error": f"Город '{city}' не найден."}
|
||||||
else:
|
else:
|
||||||
logger.error(f"API Error (Forecast): Code {response.status_code} for city {city}")
|
logger.error(
|
||||||
return {"success": False, "error": "Ошибка связи с сервером погоды."}
|
f"API Error (Forecast): Code {response.status_code} for city {city}"
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": "Ошибка связи с сервером погоды."
|
||||||
|
}
|
||||||
except requests.exceptions.RequestException as e:
|
except requests.exceptions.RequestException as e:
|
||||||
logger.error(f"Request exception (Forecast): {e}")
|
logger.error(f"Request exception (Forecast): {e}")
|
||||||
return {"success": False, "error": "Превышено время ожидания или ошибка сети."}
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": "Превышено время ожидания или ошибка сети."
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
# weather_utils.py
|
# weather_utils.py
|
||||||
|
|
||||||
|
|
||||||
def get_weather_comment(temperature):
|
def get_weather_comment(temperature):
|
||||||
"""Возвращает текстовый комментарий и эмодзи в зависимости от температуры"""
|
"""Возвращает текстовый комментарий и эмодзи в зависимости от температуры"""
|
||||||
if temperature >= 25:
|
if temperature >= 25:
|
||||||
|
|||||||
Reference in New Issue
Block a user