-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathWeatherTelegramBot.py
More file actions
257 lines (204 loc) · 11 KB
/
Copy pathWeatherTelegramBot.py
File metadata and controls
257 lines (204 loc) · 11 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
"""
Weather Telegram Bot — Погодний Telegram-бот
============================================
A simple Telegram bot that replies to /weather <city> with current weather
data fetched from the Open-Meteo API (no API key required).
Простий Telegram-бот, який відповідає на /weather <місто> поточними
даними погоди з Open-Meteo API (ключ не потрібен).
Requirements / Залежності:
pip install python-telegram-bot==20.* httpx
Setup / Налаштування:
1. Create a bot via @BotFather and copy the token.
Створи бота через @BotFather і скопіюй токен.
2. Set env variable: export BOT_TOKEN="your_token_here"
Встанови змінну середовища: export BOT_TOKEN="твій_токен"
3. python weather_bot.py
Commands / Команди:
/start — welcome message / привітання
/help — list commands / список команд
/weather <city> — get current weather / поточна погода
"""
import logging
import os
import sys
import httpx
from telegram import Update
from telegram.constants import ParseMode
from telegram.ext import (
Application,
CommandHandler,
ContextTypes,
)
# ─── Logging / Логування ──────────────────────────────────────────────────────
# Configure logging so we can see what the bot is doing in the terminal
# Налаштовуємо логування, щоб бачити роботу бота в терміналі
logging.basicConfig(
format="%(asctime)s | %(levelname)s | %(name)s — %(message)s",
level=logging.INFO,
)
logger = logging.getLogger(__name__)
# ─── Configuration / Конфігурація ─────────────────────────────────────────────
# Read the token from the environment — never hard-code secrets in source files!
# Читаємо токен зі змінної середовища — ніколи не пиши секрети у коді!
BOT_TOKEN = os.getenv("BOT_TOKEN")
# Geocoding API to convert city name → lat/lon (no key needed)
# API геокодування для перетворення назви міста → широта/довгота (без ключа)
GEOCODE_URL = "https://geocoding-api.open-meteo.com/v1/search"
# Weather API (also no key needed)
# API погоди (теж без ключа)
WEATHER_URL = "https://api.open-meteo.com/v1/forecast"
# WMO weather code descriptions (subset) — maps numeric code to human text
# Опис кодів погоди WMO — відображає числовий код у зрозумілий текст
WMO_DESCRIPTIONS: dict[int, str] = {
0: "Clear sky ☀️ / Ясне небо",
1: "Mainly clear 🌤️ / Переважно ясно",
2: "Partly cloudy ⛅ / Хмарно",
3: "Overcast ☁️ / Похмуро",
45: "Foggy 🌫️ / Туман",
51: "Light drizzle 🌦️ / Легка мряка",
61: "Slight rain 🌧️ / Невеликий дощ",
71: "Slight snow 🌨️ / Невеликий сніг",
80: "Rain showers 🌦️ / Зливи",
95: "Thunderstorm ⛈️ / Гроза",
}
# ─── Weather data fetching / Отримання даних погоди ──────────────────────────
async def geocode_city(city: str) -> tuple[float, float, str] | None:
"""
Look up a city name and return (latitude, longitude, display_name).
Returns None if the city was not found.
Шукає місто і повертає (широта, довгота, назва_для_відображення).
Повертає None, якщо місто не знайдено.
"""
async with httpx.AsyncClient(timeout=10) as client:
resp = await client.get(GEOCODE_URL, params={"name": city, "count": 1, "language": "en"})
resp.raise_for_status()
data = resp.json()
results = data.get("results")
if not results:
return None # city unknown / місто невідоме
r = results[0]
# Build a nice display name: "City, Country (Admin)"
# Формуємо красиву назву: "Місто, Країна (Регіон)"
display = r["name"]
if r.get("country"):
display += f", {r['country']}"
return r["latitude"], r["longitude"], display
async def fetch_weather(lat: float, lon: float) -> dict | None:
"""
Call Open-Meteo and return current weather fields.
Звертається до Open-Meteo і повертає поточні поля погоди.
"""
params = {
"latitude": lat,
"longitude": lon,
"current_weather": True,
"hourly": "relative_humidity_2m",
"forecast_days": 1,
}
async with httpx.AsyncClient(timeout=10) as client:
resp = await client.get(WEATHER_URL, params=params)
resp.raise_for_status()
return resp.json()
def format_weather(data: dict, city_name: str) -> str:
"""
Turn the raw API response into a readable Telegram message (Markdown).
Перетворює сирий відповідь API на читабельне Telegram-повідомлення (Markdown).
"""
cw = data.get("current_weather", {})
temp = cw.get("temperature", "—")
wind = cw.get("windspeed", "—")
code = int(cw.get("weathercode", -1))
# First hourly humidity value — current approximation
# Перше значення вологості з погодинних даних — поточне наближення
hourly = data.get("hourly", {})
humidity_list = hourly.get("relative_humidity_2m", [])
humidity = humidity_list[0] if humidity_list else "—"
description = WMO_DESCRIPTIONS.get(code, f"Code {code}")
# Use MarkdownV2 formatting — special characters must be escaped
# Використовуємо форматування MarkdownV2 — спецсимволи треба екранувати
return (
f"📍 *{city_name}*\n\n"
f"🌡 Temperature / Температура: `{temp} °C`\n"
f"💨 Wind / Вітер: `{wind} km/h`\n"
f"💧 Humidity / Вологість: `{humidity} %`\n"
f"🔭 Condition / Стан: {description}\n\n"
f"_Data: Open\\-Meteo \\| No key needed_"
)
# ─── Command handlers / Обробники команд ──────────────────────────────────────
async def cmd_start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""
/start — send a greeting message.
/start — надіслати привітальне повідомлення.
"""
user = update.effective_user
await update.message.reply_text(
f"Hello, {user.first_name}! 👋\n"
f"Use /weather <city> to get current weather.\n"
f"Привіт! Використовуй /weather <місто> для погоди."
)
async def cmd_help(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""
/help — list available commands.
/help — список доступних команд.
"""
await update.message.reply_text(
"📋 *Commands / Команди*\n\n"
"/start — welcome / привітання\n"
"/help — this message / це повідомлення\n"
"/weather <city> — current weather / поточна погода",
parse_mode=ParseMode.MARKDOWN,
)
async def cmd_weather(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""
/weather <city> — fetch and display current weather.
/weather <місто> — отримати і відобразити поточну погоду.
"""
# context.args contains the words after the command
# context.args містить слова після команди
if not context.args:
await update.message.reply_text("Please provide a city: /weather Lviv")
return
city_input = " ".join(context.args) # allow multi-word names / дозволяємо кількасловні назви
# Show a loading indicator while we fetch data
# Показуємо індикатор завантаження під час отримання даних
await update.message.reply_text(f"🔍 Looking up '{city_input}'…")
try:
geo = await geocode_city(city_input)
if geo is None:
await update.message.reply_text(
f"❌ City '{city_input}' not found. / Місто не знайдено.\n"
"Try in English: /weather Lviv"
)
return
lat, lon, display_name = geo
weather_data = await fetch_weather(lat, lon)
if weather_data is None:
await update.message.reply_text("⚠️ Weather service unavailable. Try later.")
return
message = format_weather(weather_data, display_name)
await update.message.reply_text(message, parse_mode=ParseMode.MARKDOWN_V2)
logger.info("Weather sent for %s (%.2f, %.2f)", display_name, lat, lon)
except httpx.HTTPError as exc:
# Log the full error internally, show a friendly message to the user
# Логуємо повну помилку всередині, показуємо зрозуміле повідомлення користувачу
logger.error("HTTP error: %s", exc)
await update.message.reply_text("⚠️ Network error. Please try again.")
# ─── Bot startup / Запуск бота ────────────────────────────────────────────────
def main() -> None:
if not BOT_TOKEN:
logger.critical("BOT_TOKEN env variable not set! / Змінна BOT_TOKEN не встановлена!")
sys.exit(1)
# Build the Application — this is the main bot object from python-telegram-bot v20
# Будуємо Application — головний об'єкт бота з python-telegram-bot v20
app = Application.builder().token(BOT_TOKEN).build()
# Register command handlers — each decorated function handles one command
# Реєструємо обробники команд — кожна функція обробляє одну команду
app.add_handler(CommandHandler("start", cmd_start))
app.add_handler(CommandHandler("help", cmd_help))
app.add_handler(CommandHandler("weather", cmd_weather))
logger.info("Bot is running… / Бот запущений…")
# run_polling() keeps the bot alive, polling Telegram for new messages
# run_polling() тримає бота живим, опитуючи Telegram на нові повідомлення
app.run_polling(allowed_updates=Update.ALL_TYPES)
if __name__ == "__main__":
main()