МУНИЦИПАЛЬНОЕ АВТОНОМНОЕ ОБРАЗОВАТЕЛЬНОЕ УЧРЕЖДЕНИЕ ДОПОЛНИТЕЛЬНОГО ОБРАЗОВАНИЯ
«ЦЕНТР ДЕТСКОГО ТЕХНИЧЕСКОГО ТВОРЧЕСТВА»
Методическая разработка
«Создание Telegram-бота для Юных натуралистов»
к дополнительной общеобразовательной
общеразвивающей программе
технической направленности
«Программирование на Python»
Возраст детей: 10-17 лет
Автор: Костычев Вадим Александрович
г. Заречный Пензенской области
2025 г.
Данный методический материал разработан для изучения программирования на Python через создание Telegram-бота для Юных натуралистов. Бот поможет школьникам и педагогам изучить основы работы с растениями, базами данных и асинхронным программированием. Он позволяет добавлять растения, вести их учёт, получать напоминания о поливе и управлять данными через удобный интерфейс Telegram.
Цели:
- освоить основы Python и асинхронного программирования;
- научиться работать с базами данных SQLite;
- понять принципы разработки чат-ботов;
- развить навыки ухода за растениями через цифровые инструменты.
Описание бота
Telegram-бот для Юных натуралистов выполняет следующие функции:
Добавление растений: пользователи могут добавлять растения с названием, описанием, фотографией и интервалом полива.
Глоссарий растений: отображает список всех растений с информацией о том, кто их добавил, и позволяет просмотреть подробности.
Напоминания о поливе: отправляет уведомления в 15:00, если растение требует полива.
Проверка полива: показывает даты следующего полива для растений.
Удаление растений: доступно только администраторам с подтверждением действия.
Бот использует SQLite для хранения данных и Telegram API для взаимодействия с пользователями.
Необходимое программное обеспечение:
Python: Версия 3.8 или выше.
Библиотека python-telegram-bot: Версия 20.7 с поддержкой job-queue (pip install "python-telegram-bot[job-queue]").
SQLite: Встроен в Python (модуль sqlite3).
Операционная система: Windows, macOS или Linux.
Подготовка:
Установите Python:
sudo apt install python3 python3-pip # Для Linux
Установите библиотеку:
pip install "python-telegram-bot[job-queue]"
Создайте токен бота через BotFather в Telegram:
Отправьте /start и /newbot.
Следуйте инструкциям, чтобы получить токен (например, 123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11).
Структура кода
Код бота организован следующим образом:
Библиотеки:
sqlite3: для работы с базой данных;
logging: для логирования событий;
datetime, timedelta: для расчёта времени полива;
модули telegram и telegram;ext: для взаимодействия с telegram api;
os: для работы с файловой системой.
Инициализация базы данных:
Функция init_db() создаёт таблицу plants с полями: id, name, description, photo, user_id, user_name, watering_interval, last_watered, next_watering.
Обработчики:
/start: Выводит приветствие и главное меню.
Добавление растения: Последовательный ввод данных через состояния (add_plant, handle_message, handle_photo).
Глоссарий: Отображение списка растений (glossary, show_plant).
Полив: Проверка растений, требующих полива (watering).
Проверка дат полива: Показывает расписание полива (check_watering_dates).
Удаление: Только для админов с подтверждением (delete_plant, confirm_delete, execute_delete).
Уведомления: Отправка напоминаний в 15:00 (check_watering).
Главное меню:
Реализовано через InlineKeyboardMarkup с кнопками для всех функций. Администраторам доступна дополнительная кнопка "Удалить растение".
Ключевые функции
Добавление растения
Процесс:
Пользователь нажимает "Добавить растение".
Бот запрашивает название, описание, интервал полива и фото.
Данные сохраняются в базе через save_plant.
Код:
async def add_plant(update: Update, context: ContextTypes.DEFAULT_TYPE):
query = update.callback_query
await query.answer()
context.user_data["state"] = "add_plant_name"
await query.message.reply_text("Введите название растения:")
Использует context.user_data для хранения временных данных. Фото сохраняется в папку photos.
Глоссарий
Процесс:
Код:
async def glossary(update: Update, context: ContextTypes.DEFAULT_TYPE):
conn = sqlite3.connect("plants.db")
c = conn.cursor()
c.execute("SELECT id, name, user_name FROM plants")
plants = c.fetchall()
conn.close()
Напоминания о поливе
Процесс:
Ежедневно в 15:00 бот проверяет растения, где next_watering
Отправляет уведомления и обновляет время полива.
Код:
async def check_watering(context: ContextTypes.DEFAULT_TYPE):
c.execute("SELECT user_id, name, watering_interval FROM plants WHERE next_watering
plants = c.fetchall()
for user_id, name, watering_interval in plants:
await context.bot.send_message(chat_id=user_id, text=f"🌱 Напоминание: пора полить растение '{name}'!")
Проверка полива
Процесс: показывает даты следующего полива для растений пользователя или всех (для админов).
Код:
async def check_watering_dates(update: Update, context: ContextTypes.DEFAULT_TYPE):
if is_admin(user.id):
c.execute("SELECT name, next_watering, user_name FROM plants")
else:
c.execute("SELECT name, next_watering, user_name FROM plants WHERE user_id = ?", (user.id,))
Удаление растений
Процесс:
Доступно только админам.
Требует подтверждения через кнопки "Да, удалить" или "Отмена".
Код:
async def delete_plant(update: Update, context: ContextTypes.DEFAULT_TYPE):
if not is_admin(user.id):
await query.message.reply_text("Только администратор может удалять растения.")
return
Полный листинг программы:
import sqlite3
import logging
from datetime import datetime, timedelta
from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
from telegram.ext import (
Application,
CommandHandler,
MessageHandler,
filters,
ContextTypes,
CallbackQueryHandler,
JobQueue,
)
import os
# Настройка логирования
logging.basicConfig(format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.INFO)
logger = logging.getLogger(__name__)
# Инициализация базы данных
def init_db():
conn = sqlite3.connect("plants.db")
c = conn.cursor()
c.execute("""
CREATE TABLE IF NOT EXISTS plants (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
description TEXT,
photo TEXT,
user_id INTEGER,
user_name TEXT,
watering_interval INTEGER,
last_watered TIMESTAMP,
next_watering TIMESTAMP
)
""")
conn.commit()
conn.close()
# Проверка, является ли пользователь администратором
def is_admin(user_id):
ADMIN_IDS = [123456789] # Замените на реальные ID администраторов
return user_id in ADMIN_IDS
# Главное меню
def get_main_menu(is_admin=False):
keyboard = [
[InlineKeyboardButton("Добавить растение", callback_data="add_plant")],
[InlineKeyboardButton("Глоссарий растений", callback_data="glossary")],
[InlineKeyboardButton("Полив", callback_data="watering")],
[InlineKeyboardButton("Проверка полива", callback_data="check_watering_dates")],
]
if is_admin:
keyboard.append([InlineKeyboardButton("Удалить растение", callback_data="delete_plant")])
return InlineKeyboardMarkup(keyboard)
# Обработчик команды /start
async def start(update: Update, context: ContextTypes.DEFAULT_TYPE):
user = update.effective_user
welcome_text = (
f"Привет, {user.first_name}! Добро пожаловать в бот Юных натуралистов 🌱\n"
"Выбери, что хочешь сделать:"
)
await update.message.reply_text(welcome_text, reply_markup=get_main_menu(is_admin(user.id)))
# Обработчик добавления растения
async def add_plant(update: Update, context: ContextTypes.DEFAULT_TYPE):
query = update.callback_query
await query.answer()
context.user_data["state"] = "add_plant_name"
await query.message.reply_text("Введите название растения:")
# Обработчик текстовых сообщений для добавления растения
async def handle_message(update: Update, context: ContextTypes.DEFAULT_TYPE):
user = update.effective_user
text = update.message.text
state = context.user_data.get("state")
if state == "add_plant_name":
context.user_data["plant_name"] = text
context.user_data["state"] = "add_plant_description"
await update.message.reply_text("Введите описание растения:")
elif state == "add_plant_description":
context.user_data["plant_description"] = text
context.user_data["state"] = "add_plant_interval"
await update.message.reply_text("Укажите интервал полива (в днях, например, 7):")
elif state == "add_plant_interval":
try:
interval = int(text)
context.user_data["watering_interval"] = interval
context.user_data["state"] = "add_plant_photo"
await update.message.reply_text("Отправьте фотографию растения (или напишите 'без фото'):")
except ValueError:
await update.message.reply_text("Пожалуйста, введите число (например, 7):")
elif state == "add_plant_photo" and text.lower() == "без фото":
save_plant(context, user, None)
await update.message.reply_text("Растение добавлено!", reply_markup=get_main_menu(is_admin(user.id)))
context.user_data.clear()
else:
await update.message.reply_text("Пожалуйста, следуйте инструкциям.")
# Обработчик фотографий
async def handle_photo(update: Update, context: ContextTypes.DEFAULT_TYPE):
user = update.effective_user
if context.user_data.get("state") == "add_plant_photo":
photo = update.message.photo[-1]
file = await photo.get_file()
photo_path = f"photos/{photo.file_id}.jpg"
os.makedirs("photos", exist_ok=True)
await file.download_to_drive(photo_path)
save_plant(context, user, photo_path)
await update.message.reply_text("Растение добавлено!", reply_markup=get_main_menu(is_admin(user.id)))
context.user_data.clear()
# Сохранение растения в базе данных
def save_plant(context, user, photo_path):
plant_name = context.user_data["plant_name"]
description = context.user_data["plant_description"]
interval = context.user_data["watering_interval"]
last_watered = datetime.now()
next_watering = last_watered + timedelta(days=interval)
conn = sqlite3.connect("plants.db")
c = conn.cursor()
c.execute(
"""
INSERT INTO plants (name, description, photo, user_id, user_name, watering_interval, last_watered, next_watering)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
""",
(
plant_name,
description,
photo_path,
user.id,
user.first_name,
interval,
last_watered,
next_watering,
),
)
conn.commit()
conn.close()
# Обработчик глоссария
async def glossary(update: Update, context: ContextTypes.DEFAULT_TYPE):
query = update.callback_query
await query.answer()
conn = sqlite3.connect("plants.db")
c = conn.cursor()
c.execute("SELECT id, name, user_name FROM plants")
plants = c.fetchall()
conn.close()
if not plants:
await query.message.reply_text("Глоссарий пуст. Добавьте растение!")
return
keyboard = [
[InlineKeyboardButton(f"{name} (добавил: {user_name})", callback_data=f"plant_{id}")]
for id, name, user_name in plants
]
keyboard.append([InlineKeyboardButton("Назад", callback_data="back")])
await query.message.reply_text("Выберите растение:", reply_markup=InlineKeyboardMarkup(keyboard))
# Обработчик выбора растения в глоссарии
async def show_plant(update: Update, context: ContextTypes.DEFAULT_TYPE):
query = update.callback_query
await query.answer()
plant_id = int(query.data.split("_")[1])
conn = sqlite3.connect("plants.db")
c = conn.cursor()
c.execute("SELECT name, description, photo, user_name FROM plants WHERE id = ?", (plant_id,))
plant = c.fetchone()
conn.close()
name, description, photo, user_name = plant
text = f"🌱 {name}\nОписание: {description}\nДобавил: {user_name}"
if photo:
await query.message.reply_photo(photo=open(photo, "rb"), caption=text)
else:
await query.message.reply_text(text)
await query.message.reply_text("Выберите действие:", reply_markup=get_main_menu(is_admin(query.from_user.id)))
# Обработчик полива
async def watering(update: Update, context: ContextTypes.DEFAULT_TYPE):
query = update.callback_query
await query.answer()
user = query.from_user
conn = sqlite3.connect("plants.db")
c = conn.cursor()
if is_admin(user.id):
c.execute("SELECT name, next_watering FROM plants WHERE next_watering
else:
c.execute(
"SELECT name, next_watering FROM plants WHERE user_id = ? AND next_watering
(user.id, datetime.now()),
)
plants = c.fetchall()
conn.close()
if not plants:
await query.message.reply_text("Нет растений, которые нужно полить.")
return
text = "Растения, которые нужно полить:\n"
for name, next_watering in plants:
text += f"- {name} (полить сейчас)\n"
await query.message.reply_text(text, reply_markup=get_main_menu(is_admin(user.id)))
# Обработчик проверки дат полива
async def check_watering_dates(update: Update, context: ContextTypes.DEFAULT_TYPE):
query = update.callback_query
await query.answer()
user = query.from_user
conn = sqlite3.connect("plants.db")
c = conn.cursor()
if is_admin(user.id):
c.execute("SELECT name, next_watering, user_name FROM plants")
else:
c.execute(
"SELECT name, next_watering, user_name FROM plants WHERE user_id = ?",
(user.id,)
)
plants = c.fetchall()
conn.close()
if not plants:
await query.message.reply_text("У вас нет растений для проверки.")
return
text = "📅 Даты следующего полива:\n"
for name, next_watering, user_name in plants:
try:
next_watering_date = datetime.strptime(next_watering, "%Y-%m-%d %H:%M:%S.%f")
except ValueError:
next_watering_date = datetime.now()
formatted_date = next_watering_date.strftime("%d.%m.%Y %H:%M")
text += f"- {name} (добавил: {user_name}): {formatted_date}\n"
await query.message.reply_text(text, reply_markup=get_main_menu(is_admin(user.id)))
# Обработчик удаления растения (только для администратора)
async def delete_plant(update: Update, context: ContextTypes.DEFAULT_TYPE):
query = update.callback_query
await query.answer()
user = query.from_user
if not is_admin(user.id):
await query.message.reply_text("Только администратор может удалять растения.")
return
conn = sqlite3.connect("plants.db")
c = conn.cursor()
c.execute("SELECT id, name FROM plants")
plants = c.fetchall()
conn.close()
if not plants:
await query.message.reply_text("Нет растений для удаления.")
return
keyboard = [
[InlineKeyboardButton(name, callback_data=f"confirm_delete_{id}")]
for id, name in plants
]
keyboard.append([InlineKeyboardButton("Назад", callback_data="back")])
await query.message.reply_text("Выберите растение для удаления:", reply_markup=InlineKeyboardMarkup(keyboard))
# Подтверждение удаления
async def confirm_delete(update: Update, context: ContextTypes.DEFAULT_TYPE):
query = update.callback_query
await query.answer()
plant_id = int(query.data.split("_")[2])
conn = sqlite3.connect("plants.db")
c = conn.cursor()
c.execute("SELECT name FROM plants WHERE id = ?", (plant_id,))
plant_name = c.fetchone()[0]
conn.close()
keyboard = [
[InlineKeyboardButton("Да, удалить", callback_data=f"delete_{plant_id}")],
[InlineKeyboardButton("Отмена", callback_data="back")],
]
await query.message.reply_text(
f"Вы уверены, что хотите удалить растение '{plant_name}'?",
reply_markup=InlineKeyboardMarkup(keyboard)
)
# Окончательное удаление
async def execute_delete(update: Update, context: ContextTypes.DEFAULT_TYPE):
query = update.callback_query
await query.answer()
plant_id = int(query.data.split("_")[1])
conn = sqlite3.connect("plants.db")
c = conn.cursor()
c.execute("DELETE FROM plants WHERE id = ?", (plant_id,))
conn.commit()
conn.close()
await query.message.reply_text("Растение удалено!", reply_markup=get_main_menu(is_admin(query.from_user.id)))
# Обработчик кнопки "Назад"
async def back(update: Update, context: ContextTypes.DEFAULT_TYPE):
query = update.callback_query
await query.answer()
await query.message.reply_text("Главное меню:", reply_markup=get_main_menu(is_admin(query.from_user.id)))
# Функция для отправки уведомлений о поливе
async def check_watering(context: ContextTypes.DEFAULT_TYPE):
conn = sqlite3.connect("plants.db")
c = conn.cursor()
c.execute("SELECT user_id, name, watering_interval FROM plants WHERE next_watering
plants = c.fetchall()
conn.close()
for user_id, name, watering_interval in plants:
await context.bot.send_message(
chat_id=user_id,
text=f"🌱 Напоминание: пора полить растение '{name}'!"
)
# Обновляем время следующего полива
conn = sqlite3.connect("plants.db")
c = conn.cursor()
c.execute(
"""
UPDATE plants
SET last_watered = ?, next_watering = ?
WHERE name = ? AND user_id = ?
""",
(
datetime.now(),
datetime.now() + timedelta(days=watering_interval),
name,
user_id,
),
)
conn.commit()
conn.close()
def main():
init_db()
# Замените YOUR_BOT_TOKEN на токен вашего бота
application = Application.builder().token("YOUR_BOT_TOKEN").build()
# Обработчики
application.add_handler(CommandHandler("start", start))
application.add_handler(CallbackQueryHandler(add_plant, pattern="add_plant"))
application.add_handler(CallbackQueryHandler(glossary, pattern="glossary"))
application.add_handler(CallbackQueryHandler(watering, pattern="watering"))
application.add_handler(CallbackQueryHandler(check_watering_dates, pattern="check_watering_dates"))
application.add_handler(CallbackQueryHandler(delete_plant, pattern="delete_plant"))
application.add_handler(CallbackQueryHandler(confirm_delete, pattern="confirm_delete_"))
application.add_handler(CallbackQueryHandler(execute_delete, pattern="delete_"))
application.add_handler(CallbackQueryHandler(show_plant, pattern="plant_"))
application.add_handler(CallbackQueryHandler(back, pattern="back"))
application.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, handle_message))
application.add_handler(MessageHandler(filters.PHOTO, handle_photo))
# Ежедневные уведомления в 15:00
application.job_queue.run_daily(
check_watering,
time=datetime.strptime("15:00:00", "%H:%M:%S").time(),
days=(0, 1, 2, 3, 4, 5, 6) # Все дни недели
)
# Запуск бота
application.run_polling()
if __name__ == "__main__":
main()
Бот для Юных натуралистов — это практический проект, объединяющий программирование и биологию. Он помогает учащимся освоить Python, базы данных и разработку ботов, а также развивает ответственность за уход за растениями. Проект легко адаптировать под разные образовательные задачи, добавляя новые функции или упрощая интерфейс.