Перед уроком
Обери свою групу й себе зі списку. Тоді відповіді й перевірки завдань запишуться саме на тебе.
Зробити бота схожим на справжній застосунок: меню з кнопками, покрокова форма замовлення, автоматична анкета з оцінкою після виконання, звіт про рейтинг і порадник-ШІ, який лише пропонує дії клієнтам, а надсилає їх тільки власник, — і при цьому берегти персональні дані.
Замість <логін> підставляєш свій
логін виду bohdan.g9: ім’я, крапка, номер групи. Кутові дужки не пишуться.
У прикладах на цій сторінці скрізь стоїть bohdan.g9 — читай це як свій логін
і виправляй у кожній команді, яку копіюєш. Кабінет — сторінка, де проти кожного критерію
стоїть відмітка, зарахований він чи ні.
<домен> — назва, яку викладач називає на початку заняття.
Вона вже записана у твоєму файлі ~/app/project.yaml, розділ server,
поле url; пиши її точно так, як там, латинськими літерами. У прикладах на цій
сторінці замість неї скрізь стоїть klub.example. Перший крок практики —
прочитати справжню назву звідти, вигадувати нічого не треба. Якщо в тому полі досі стоїть __DOMAIN__, спитай викладача, перш ніж
рухатися далі: без адреси половина сьогоднішніх команд не спрацює.
Зверни увагу: кабінет поки що відкривається по http за
IP-адресою. Сертифіката на IP-адресу немає — чому так, пояснено в розділі 3 теорії, — тому
пароль від кабінету передається без шифрування. Не став туди пароль, який використовуєш
десь іще. Коли кабінет переїде на адресу з іменем і замком, викладач скаже нову адресу.
Досі твій бот приймав текст і відповідав текстом. Справжні застосунки в Telegram виглядають інакше: є меню команд, картки з жирними заголовками, кнопки під повідомленням, форми з кроками. Сьогодні бот стане маленьким «сервісом»: уяви, що це кав’ярня, майстерня чи будь-яка твоя справа, у якої є клієнти.
До кінця заняття в тебе буде:
/menu з карткою і чотирма кнопками, жирні заголовки й значки./order питає по кроках: ім’я → що замовляєш → коли → «Підтвердити». Замовлення потрапляє в базу (таблиця orders) лише після підтвердження.feedback). Власник бачить звіт: скільки замовлень і яка середня оцінка./forget стирає все про людину.Показати можна буде: меню в Telegram, замовлення, зроблене кнопками, анкету оцінки, що прийшла сама, і картку поради ШІ.
Якщо в тебе немає TelegramУсе, крім живого повідомлення, ти зробиш через try_bot.py. Сьогодні він «вміє» ще дві речі: натискати кнопки (--button) і «минати час» (--tick). Кнопки в ньому видно як рядок [Підтвердити] і підказку, якою командою їх натиснути. Повідомлення іншим людям він лише показує («пішло б у чат…») і нічого нікуди не надсилає.
У Telegram три «прикраси» для бота, і всі вони працюють через той самий Bot API, яким ти вже користуєшся.
Меню команд. Бот може сказати Telegram: «ось мої команди й підписи до них». Для цього є метод setMyCommands. Тоді біля поля вводу з’являється кнопка ☰, а людина, яка набрала /, бачить підказки. Ім’я команди — малі латинські букви, цифри й підкреслення. Власникові можна показати довший список, ніж усім: Telegram дозволяє задати список для окремого чату.
Розмітка тексту. Додай до sendMessage параметр parse_mode="HTML", і в тексті запрацюють теги: <b>жирний</b>, <i>курсив</i>, <code>моноширинний</code>. Є одна небезпека. Усе, що написала людина, перед вставкою в такий текст треба «знешкодити» функцією html.escape: вона замінює <, > і & безпечними символами. Інакше клієнт, який назве себе <b>Король</b>, зламає розмітку, а некоректний тег узагалі змусить Telegram відхилити повідомлення.
import html
name = "<b>Король</b>"
print(html.escape(name)) # <b>Король</b> — тепер це просто текстКнопки під повідомленням (inline-кнопки). До sendMessage додають reply_markup — список рядків, а в кожному рядку кнопки з двома полями: text (що написано) і callback_data (до 64 байтів — «ярлик», який отримає бот). Коли людина натискає кнопку, у чат нічого не пишеться. Боту приходить оновлення іншого виду: не message, а callback_query, у якому є data (твій ярлик), from (хто натиснув) і message (під яким повідомленням). Бот має відповісти методом answerCallbackQuery, інакше на кнопці довго крутиться «годинничок». Ярлик зручно писати як дія:дані. Наша кнопка оцінки має вигляд rate:номер_замовлення:оцінка: наприклад, rate:5:4 означає «поставити замовленню №5 оцінку 4».
Є ще звичайні кнопки на місці клавіатури (reply-кнопки): вони просто надсилають у чат свій текст. Нам потрібні саме inline-кнопки: вони не засмічують чат і несуть службові дані.
Людина натиснула inline-кнопку під повідомленням бота. Що саме отримає бот?
Форма на сайті має поля, і браузер сам пам’ятає, що ти вже ввів. У чаті інакше: кожне повідомлення приходить окремо, і бот не знає, що слово «Оля» — це відповідь на питання про ім’я. Тому бот має пам’ятати, на якому кроці людина. Ми записуємо це в таблицю forms: номер чату, назва кроку (name, item, when, confirm) і те, що вже введено. Не в змінну в пам’яті: перезапуск служби стер би розмови посередині, а try_bot.py запускається щоразу заново і нічого б не запам’ятав. Забуту форму бот через 30 хвилин вважає закинутою.
Головне правило форми: чернетка — це ще не замовлення. Людина може помилитися, передумати, піти. Тому дані ходять по кроках у таблиці forms, потім бот показує картку «Перевір замовлення», і лише після кнопки «Підтвердити» робить INSERT в таблицю orders. Це ще одна сторона правила з уроку про безпеку: нічого незворотного без явної згоди людини.
Кожен крок перевіряє те, що ввели: ім’я — від 2 до 40 символів, що замовляєш — до 100, зайві пробіли й переноси прибираємо. Усе, що людина вводить, — недовірені дані (як у ендпоінтах на занятті 05).
Коли форма замовлення має записати рядок у таблицю orders?
Як дізнатися, чи сподобалось клієнтові? Спитати. Але не вручну кожного разу. Усе влаштовано так:
/done 5); замовлення отримує статус done, а в базі записується, коли просити відгук (поле ask_at: зараз плюс FEEDBACK_DELAY_MIN хвилин; для проби став 0, у житті — 30–60);tick() раз на 20 секунд (її кличе той самий потік розкладу з заняття 09) шукає замовлення, яким пора, і надсилає клієнтові картку з п’ятьма кнопками;asked=1. Так само, як позначка last у розкладі: бот не спитає двічі, навіть якщо щось зламається посередині. Якщо ж Telegram просто не відповів (збій мережі), позначку знімають і бот повторює спробу за 5 хвилин;rate:5:4 (замовлення №5, оцінка 4) записує оцінку в таблицю feedback; наступне текстове повідомлення стає коментарем, а /skip пропускає його.Перевіряй оцінку навіть тоді, коли кнопки створив сам бот. Хто знає Bot API, може надіслати боту будь-який callback_data, не натискаючи нічого. І текстова команда /feedback 7 теж існує. Тому оцінка має бути однією цифрою від 1 до 5, замовлення — твоїм, виконаним і ще без відгуку. У таблиці є і друга лінія захисту: CHECK (rating BETWEEN 1 AND 5). Але перша перевірка в програмі дає людині зрозуміле пояснення, а не помилку бази.
Рейтинг — це SQL. Скільки замовлень і яка середня оцінка — не потрібно рахувати в Python по одному. Достатньо запиту:
SELECT COUNT(*), AVG(rating) FROM feedback;
SELECT rating, COUNT(*) FROM feedback GROUP BY rating ORDER BY rating DESC;Перший дає кількість відгуків і середню оцінку, другий — скільки було п’ятірок, четвірок і так далі (GROUP BY — «згрупуй за значенням»). У shop.py їх обгорнуто функцією report(), яка повертає словник {"orders": …, "feedbacks": …, "avg": …}.
Боту прийшов callback_data = "rate:5:7" (оцінка 7 для замовлення №5). Що робити?
База клієнтів — це таблиці users (хто писав боту), orders (що замовляли) і feedback (що сказали). З них видно, кому варто подякувати за оцінку «5», а кому — написати, бо поставив «2». Коли клієнтів багато, пам’ятати все про кожного складно, і тут допомагає ШІ: ми даємо йому факти про замовлення і просимо запропонувати наступну дію: THANK (подякувати), DISCOUNT (запропонувати знижку), ASK (перепитати) чи NOTHING. Власник пише /next 5 — і отримує картку з готовим текстом.
Чому бот не надсилає це клієнтові сам. Тому що ШІ помиляється — і робить це впевнено. Ось що може піти не так:
Тому схема з трьох кроків така:
proposed («запропоновано») і показує її власнику;Що буде при подвійному натисканні? Друге натискання побачить, що статус уже змінено, і нічого не зробить: у коді це один запит UPDATE … WHERE status='proposed', після якого бот дивиться, чи змінився хоч один рядок. Тому повідомлення клієнту не піде двічі.
А якщо ШІ недоступний чи відповів не за формою (наприклад, шлюз школи повернув помилку), бот так і пише, а повідомлення про помилку не стає пропозицією: інакше його можна було б надіслати клієнтові.
І ще одне правило: ШІ не потрібне ім’я клієнта. Для поради досить знати, що замовлено, яка оцінка й що написано в коментарі. Ім’я й номер чату в запит не потрапляють, а з вільного тексту (замовлення й коментаря) бот прибирає телефони, електронні адреси й нікнейми (try_bot.py --fake-ai покаже весь запит, подивись сам). Безкоштовні сервіси ШІ можуть зберігати запити, тому все зайве лишаємо вдома.
ШІ запропонував знижку 70% усім клієнтам, які поставили «5». Як має працювати бот?
Персональні дані — усе, за чим можна впізнати людину: ім’я, номер чату, нік, телефон, адреса, фото, а також її оцінки й коментарі разом із ім’ям. Твої клієнти — справжні люди, і закони вимагають кількох простих речей. В Україні це закон про захист персональних даних, у Європі — GDPR (європейський закон про захист даних людей). Ми вчимо їх як звичку, а не як юридичну консультацію:
/forget (з підтвердженням кнопкою) стирає замовлення, відгуки, згоду й запис про людину. Видаляти треба лише дані цієї людини: тому в DELETE завжди є WHERE chat_id = ?.Про бекапи й повторний записРезервні копії з заняття 08 містять дані, які ти щойно видалив: у старих копіях вони лишаються, доки копії не замінять новими. Тому повідомлення «видалено» означає «видалено з бази бота», а старі копії очищаються за розкладом. Крім того, бот із заняття 04 запам’ятовує ім’я й час кожного, хто йому пише, тож нове повідомлення після /forget створить запис знову. Це описано в привітанні /start.
Бот приймає замовлення кави «забрати з собою». Що з цього збирати зайве?
Mini App (міні-застосунок) — це справжня веб-сторінка, яка відкривається всередині Telegram, як вікно поверх чату. На відміну від кнопок, на ній може бути все, що вміє сайт: красиві форми, картинки, каталог із фільтрами, календар. Telegram дає сторінці спеціальний скрипт telegram-web-app.js: через нього сторінка знає, хто її відкрив, і може надіслати дані боту. Відкривають Mini App кнопкою з полем web_app або кнопкою меню бота.
Щоб це працювало, потрібні три речі:
https://, а не http://. Telegram не відкриває Mini App за незахищеною адресою.kavarnya.example), бо сертифікат прив’язують до назви сайту, а не до цифр адреси 91.219.61.4. Домен купують (це кілька сотень гривень на рік) і направляють на сервер записом DNS (це «телефонна книга» інтернету: вона каже, на яку адресу вести цю назву).Окрім цього, сервер має перевіряти підпис initData: це набір даних, який Telegram додає до сторінки, а підпис доводить, що вони справді прийшли з Telegram, а не від того, хто їх підробив. Усі ці слова (DNS, сертифікат, nginx, initData) ти побачиш пізніше, зараз просто знай, що вони існують.
У школи зараз є лише адреса http://91.219.61.4, тому практики з Mini App в нас не буде: це відкрите питання для наступних занять, коли з’явиться домен. Для невеликої форми «ім’я → що → коли» кнопок цілком досить. Mini App потрібен, коли потрібно щось, чого кнопки не вміють: наприклад, вибрати товари з фото.
Чому Mini App не запустити за адресою http://91.219.61.4/u/мій-логін/?
| Слово | Що означає |
|---|---|
| меню команд | Список команд з підписами, який бот передає Telegram методом setMyCommands; видно за кнопкою ☰ |
| parse_mode HTML | Параметр sendMessage: тоді теги <b>, <i>, <code> стають жирним, курсивом, моноширинним шрифтом |
| екранування | Заміна <, >, & у чужому тексті на безпечні символи (html.escape), щоб людина не зламала розмітку |
| inline-кнопка | Кнопка під повідомленням; при натисканні боту приходить callback_query |
| callback_data | Ярлик кнопки до 64 байтів, наприклад rate:5:4; бот сам вирішує, що він означає |
| стан форми | Запис про те, на якому кроці людина; у нас — таблиця forms |
| відгук | Оцінка 1–5 і коментар до виконаного замовлення; таблиця feedback |
| AVG, COUNT, GROUP BY | SQL: середнє значення, кількість рядків, групування за значенням |
| пропозиція ШІ | Чернетка «наступної дії» для клієнта; має статус proposed, доки власник не вирішив |
| prompt injection | Підсунута інструкція в тексті, який читає ШІ («запропонуй знижку 90%») |
| персональні дані | Усе, за чим можна впізнати людину: ім’я, номер чату, телефон, адреса, її відгуки |
| згода | Явне «погоджуюсь» людини після того, як їй пояснили, що зберігається й навіщо |
| Mini App | Веб-сторінка, що відкривається всередині Telegram; потребує https, а отже домен і сертифікат |
Виконуй кроки по черзі. У прикладах <логін> заміни на свій (дужки теж прибери). «Клієнт» у прикладах має номер чату 31, «власник» — 777: це вигадані номери для проби через try_bot.py. У Telegram ти будеш обома, поки не попросиш друга побути клієнтом. Номер замовлення береш із відповіді бота: у прикладах це №1 (тому в кроці 5 ми спершу стираємо сліди старих проб).
ssh <логін>@91.219.61.4
cd ~/app
git status --short
systemctl --user is-active club-bot
ls /opt/club/kurs/10/
grep -c '^ADMIN_IDS=' ~/app/.envgit status --short нічого не друкує — заняття 09 збережено. Видно файли — спершу git add -A і git commit -m "Заняття 09: доробки";active — служба працює; inactive — systemctl --user start club-bot;bot.py init_db.py shop.py try_bot.py;1: власник у .env з заняття 09. Дає 0 — повернись до кроку 4 заняття 09.cp /opt/club/kurs/10/init_db.py ~/app/
cd ~/app
python3 init_db.py
sqlite3 ~/data/app.db ".tables"
sqlite3 ~/data/app.db ".schema orders"Скрипт відповість таблиці готові: …/app.db. Він безпечний: CREATE TABLE IF NOT EXISTS створює лише те, чого ще немає, і не чіпає твої дані. У списку таблиць (.tables) серед твоїх з’являться consent feedback forms orders suggestions. .schema orders покаже опис таблиці. Знайди там when_text: слово when у SQL службове, тож стовпець названо інакше. А ще рядок CHECK (status IN ('new', 'done')): база сама не дасть записати інший статус. Бачиш No such file — перевір, що ти в ~/app; No module named 'yaml' — запускай через /opt/club/venv/bin/python3.
cp /opt/club/kurs/10/shop.py /opt/club/kurs/10/try_bot.py ~/app/
cd ~/app
grep -n '^def ' shop.py | head -n 40Усе, що стосується магазину, лежить в одному модулі shop.py, а бот лише передає йому повідомлення. Команда grep надрукує близько тридцяти рядків виду 123:def menu(name): (номер рядка й назва функції). Знайди серед них головні:
| Функція | Що робить |
|---|---|
text() | приймає текст від людини й повертає відповідь |
handle_callback() | приймає натискання кнопки й повертає відповідь |
on_callback() | викликає handle_callback() і додає розмову з Telegram: «ок» на кнопці, прибрати кнопки, надіслати відповідь |
save_order() | єдине місце, де замовлення потрапляє в базу |
tick() | перевіряє, чи не час просити відгук |
report() | рахує рейтинг |
propose(), decide() | пропозиція ШІ й рішення власника |
forget_do() | видаляє дані людини |
Відкрий файл (less shop.py, вихід — q) і знайди рядок INSERT INTO orders: він лише один, і стоїть у save_order. Новий try_bot.py замінив старий: він працює так само, плюс --button, --tick, --fake-ai і --cleanup.
Відкрий nano ~/app/bot.py. Готовий приклад усього файлу лежить у /opt/club/kurs/10/bot.py; порівняти можна командою diff ~/app/bot.py /opt/club/kurs/10/bot.py (порожній вивід означає «однакові»).
Вставка 1. Під рядком import scripts:
import shop # заняття 10Вставка 2. У тексті HELP останній рядок зараз такий: "/whoami — твій номер чату"). Заміни його на шість рядків нижче. Усі, крім останнього, закінчуються на \n", а закривна дужка стоїть лише в останньому:
"/whoami — твій номер чату\n"
"/menu — меню з кнопками\n"
"/order — зробити замовлення\n"
"/myorders — мої замовлення\n"
"/feedback — залишити відгук\n"
"/forget — видалити мої дані")Вставка 3. У handle_update, одразу під remember(sender): усе, що розуміє магазин, бот віддає йому, а решту обробляє, як раніше:
shop_reply = shop.text(update) # заняття 10: меню, форма, відгуки
if shop_reply is not None:
return shop_replyВставка 4 стосується функції poll_forever, у трьох місцях. (а) Перед рядком while not stop.is_set(): — список команд для меню Telegram. Він надсилається один раз, коли бот стартує:
try:
shop.setup_commands() # заняття 10: список команд у меню Telegram
except Exception as e:
print(f"{now()} меню команд не встановлено: {e!r}", flush=True)(б) Усередині циклу for update in …, одразу після set_state("offset", offset) і перед try:: натискання кнопок мають свою гілку, бо це не message:
if update.get("callback_query"): # заняття 10: натиснули кнопку
try:
shop.on_callback(update)
except Exception as e:
print(f"{now()} помилка кнопки: {e!r}", flush=True)
continue(в) Відправка відповіді тепер бере з магазину розмітку й кнопки (якщо вони є), а кнопки внизу екрана з заняття 05 лишаються. У рядку з sendMessage заміни **contacts_extras() на **message_extras(chat), а під функцією contacts_extras додай нову:
tg("sendMessage", chat_id=chat, text=reply, **message_extras(chat))def message_extras(chat):
"""Заняття 10: додатки до відповіді. Кнопки з уроку 05 лишаються,
а коли магазин дав свої кнопки чи HTML-розмітку — вони головніші."""
extra = dict(contacts_extras())
extra.update(shop.extras(chat))
return extraВставка 5. У schedule_forever після блоку try … except із run_due() і перед stop.wait(20):
try:
shop.tick() # заняття 10: час просити відгук?
except Exception as e:
print(f"{now()} помилка відгуків: {e!r}", flush=True)Зверни увагу: handle_update(update) працює як раніше — приймає оновлення й повертає рядок або None. Кнопки й розмітка передаються окремо через message_extras(chat), тому try_bot.py і автоперевірка працюють без Telegram. Перевір, що бот не зламався: python3 try_bot.py "/start" (має з’явитися привітання, а не Traceback). Помилка SyntaxError чи IndentationError — зсунуто відступ вставки; ModuleNotFoundError: shop — не виконано крок 3. Якщо правка не виходить, не копіюй чужого bot.py цілком: це може стерти твої вставки; порівняй за допомогою diff і виправ лише потрібне місце.
python3 try_bot.py --cleanup 31 32 777
python3 try_bot.py --id 31 --name Оля "/menu"
python3 try_bot.py --id 31 --button menu:myordersПерша команда стирає з бази сліди вигаданих клієнтів 31 і 32 та власника 777 (якщо ти вже пробував): вона відповість (стерто рядків: …). Так замовлення в прикладах матимуть номер 1. Друга відповість карткою:
☕ ЗМІНИ МЕНЕ
Привіт, Оля! Обери, що потрібно:
кнопки: [🛒 Замовити] [📋 Мої замовлення]
щоб натиснути: --button menu:order --button menu:myorders
кнопки: [⭐ Залишити відгук] [🔒 Мої дані]
щоб натиснути: --button menu:feedback --button menu:privacyНазва твого продукту береться з project.yaml. У Telegram заголовок буде жирним, а кнопки — під повідомленням: try_bot.py лише показує, як вони називаються і яку команду треба дати, щоб їх «натиснути». Третя команда натискає «Мої замовлення»: поки замовлень немає, бот скаже У тебе ще немає замовлень. Знайди в shop.py функцію menu() і зміни текст заголовка чи значок однієї з кнопок. Запусти другу команду ще раз: у відповіді має з’явитися твій новий заголовок чи значок.
python3 try_bot.py --id 31 --name Оля "/order"
python3 try_bot.py --id 31 --button consent:yes
python3 try_bot.py --id 31 "Оля"
python3 try_bot.py --id 31 "два капучино"
python3 try_bot.py --id 31 --button when:tomorrow
sqlite3 ~/data/app.db "SELECT COUNT(*) FROM orders"Перша команда не питає ім’я, а спершу показує картку 📝 Перш ніж почнемо: що буде збережено й навіщо, з кнопками [Погоджуюсь] і [Ні]. Після згоди бот питає «Крок 1 з 3…», далі «Крок 2 з 3…», далі пропонує кнопки [Сьогодні] [Завтра] [На вихідних]. Кнопка «Завтра» має дані when:tomorrow (латиницею, як радить розділ 1). Після неї показується картка 🧾 Перевір замовлення з кнопками [Підтвердити], [Спочатку], [Скасувати]. Остання команда має надрукувати 0: поки ти не підтвердив, у orders нічого немає. Тепер підтверджуємо:
OWNER_CHAT_ID=777 python3 try_bot.py --id 31 --button order:ok
sqlite3 ~/data/app.db "SELECT id, name, item, when_text, status FROM orders"
OWNER_CHAT_ID=777 python3 try_bot.py --id 31 --button order:okПершим рядком бот виведе → у чат 777 пішло б: 🆕 Нове замовлення №1… — це повідомлення власникові, а далі ✅ Замовлення №1 прийнято!. SQL покаже 1|Оля|два капучино|Завтра|new. Повторне order:ok скаже Ця кнопка вже не діє…: форму вже закрито, другого замовлення не буде.
Спробуй і два інші шляхи. Перший: /order, пройди кроки й натисни --button order:cancel — у базі нічого не з’явиться. Другий: відповідь «ні» на запит згоди для нового номера, наприклад --id 32 "/order", а потім --id 32 --button consent:no — бот нічого не збере. Не спрацювало — дивись розділ «Якщо щось не працює», блок L10.3.
OWNER_CHAT_ID=777 python3 try_bot.py --id 31 "/orders"
OWNER_CHAT_ID=777 python3 try_bot.py --id 777 "/orders"
OWNER_CHAT_ID=777 FEEDBACK_DELAY_MIN=0 python3 try_bot.py --id 777 "/done 1"
python3 try_bot.py --id 31 "/myorders"Перша команда — від клієнта: бот відповість Ця команда лише для власника бота. Друга — від власника: 🧾 Нові замовлення і список, під ним кнопка [✅ №1 виконано] (у Telegram власник натиснув би її). Третя виконує те саме, що кнопка: бот каже ✅ Замовлення №1 виконано… і показує, що клієнту пішло Твоє замовлення №1 готове!. FEEDBACK_DELAY_MIN=0 означає «проси відгук одразу»; без нього бот почекав би 30 хвилин. Остання команда покаже статус ✅ виконано.
python3 try_bot.py --tick
python3 try_bot.py --id 31 "/feedback 7"
python3 try_bot.py --id 31 --button rate:1:5
python3 try_bot.py --id 31 "дуже смачно"
sqlite3 ~/data/app.db "SELECT order_id, rating, comment FROM feedback"--tick імітує потік бота, який раз на 20 секунд питає «чи не пора просити відгук?». Побачиш → у чат 31 пішло б: ⭐ Як тобі замовлення №1?… і (надіслано прохань про відгук: 1). Повтори --tick — буде 0: позначку asked=1 поставлено, вдруге бот не спитає. /feedback 7 бот відхилить: ⚠ Оцінка — число від 1 до 5. Ти написав: 7. Кнопка rate:1:5 («замовлення №1, оцінка 5») збереже п’ятірку, бот подякує й запропонує коментар, а наступне текстове повідомлення збереже коментар. Останній SQL-запит покаже 1|5|дуже смачно.
Для оцінки 3 і нижче бот ще й сам сповістить власника: ⚠ Низька оцінка…. Перевір це на другому замовленні (воно матиме номер 2):
python3 try_bot.py --id 31 "/order"
python3 try_bot.py --id 31 "Оля"
python3 try_bot.py --id 31 "латте"
python3 try_bot.py --id 31 --button when:today
OWNER_CHAT_ID=777 python3 try_bot.py --id 31 --button order:ok
OWNER_CHAT_ID=777 FEEDBACK_DELAY_MIN=0 python3 try_bot.py --id 777 "/done 2"
python3 try_bot.py --tick
OWNER_CHAT_ID=777 python3 try_bot.py --id 31 --button rate:2:2Згоду бот уже має, тому форма одразу питає ім’я. Остання команда збереже оцінку 2 і покаже: → у чат 777 пішло б: ⚠ Низька оцінка 2/5 до замовлення №2….
OWNER_CHAT_ID=777 python3 try_bot.py --id 777 "/report"
OWNER_CHAT_ID=777 python3 try_bot.py --id 777 "/clients"
sqlite3 ~/data/app.db "SELECT COUNT(*), AVG(rating) FROM feedback"
python3 -c "import shop; print(shop.report())"Звіт починається з 📊 Звіт, далі кількість замовлень, кількість відгуків, середня оцінка й «смужки» зі знаків █: чим більше п’ятірок, четвірок і так далі, тим довша смужка. /clients збирає клієнтів у вигляді Оля · замовлень 2 · середня 3.5. Це запит, який з’єднує (по-англійськи JOIN) дві таблиці — замовлення й відгуки — за номером замовлення, щоб до кожного клієнта підставити його оцінки. SQL-запит дає ті самі цифри, що й звіт, а report() повертає словник, наприклад {'orders': 2, 'done': 2, 'feedbacks': 2, 'avg': 3.5, 'by_rating': {5: 1, 2: 1}}. Такі цифри потім перевіряє автоматика. Знайди в shop.py функцію report() і прочитай її SQL.
OWNER_CHAT_ID=777 python3 try_bot.py --id 777 "/next 1"Це справжній запит до шлюзу ШІ школи (заняття 06). Поки шлюз не підключено, відповідь буде чемною: 🤖 ШІ зараз не допоміг… і його повідомлення про помилку. Це нормально і навіть правильно: помилка ШІ не стала «пропозицією». Щоб потренувати кнопки, уявімо відповідь ШІ: дамо її програмі параметром --fake-ai (у ньому \n — новий рядок):
OWNER_CHAT_ID=777 python3 try_bot.py --id 777 --fake-ai 'ДІЯ: THANK\nТЕКСТ: Дякуємо за високу оцінку! Чекаємо вас знову.' "/next 1"Спершу бот покаже весь запит до ШІ. Прочитай його і переконайся: імені клієнта там немає, лише «Замовлення: два капучино… Оцінка: 5. Коментар: дуже смачно». Нижче — картка 🤖 Порада ШІ щодо замовлення №1, текст, попередження ШІ може помилятись. Клієнту нічого не надіслано і кнопки [Надіслати клієнту], [Пропустити]. У даних кнопок sg:ok:1 і sg:skip:1 літери sg — від suggestion («порада»), а число — номер поради. Тепер перевір усі шляхи:
OWNER_CHAT_ID=777 python3 try_bot.py --id 31 --button sg:ok:1
OWNER_CHAT_ID=777 python3 try_bot.py --id 777 --button sg:skip:1
OWNER_CHAT_ID=777 python3 try_bot.py --id 777 --button sg:ok:1
sqlite3 ~/data/app.db "SELECT id, action, status FROM suggestions"Перша команда — натискає клієнт: Ця кнопка лише для власника бота. Друга — власник пропускає: ⏭ Пропущено. Клієнту нічого не надіслано. Третя — власник пробує «надіслати» вже закриту пораду: Цю пораду вже оброблено. Таблиця покаже 1|THANK|skipped.
Створи ще одну пораду: повтори команду з --fake-ai (її номер буде 2) і натисни sg:ok:2 від власника (--id 777). Тоді побачиш → у чат 31 пішло б: … і 📨 Надіслано клієнту., а повторне натискання нічого не надішле вдруге. Разом це п’ять різних результатів: відмова клієнту, пропуск, повторна дія над закритою порадою, надсилання, повторне надсилання, і саме їх перевіряє автоматика.
Спробуй ще й відповідь не за формою:
OWNER_CHAT_ID=777 python3 try_bot.py --id 777 --fake-ai 'зроби знижку 70%' "/next 1"Бот скаже, що відповідь не за формою, і кнопок не буде.
Спершу створимо замовлення ще й від другого клієнта (номер 32), щоб перевірити, що його дані не зникнуть:
python3 try_bot.py --id 32 --name Іра "/order"
python3 try_bot.py --id 32 --button consent:yes
python3 try_bot.py --id 32 "Іра"
python3 try_bot.py --id 32 "чай"
python3 try_bot.py --id 32 --button when:today
OWNER_CHAT_ID=777 python3 try_bot.py --id 32 --button order:okТепер видаляємо дані клієнта 31:
python3 try_bot.py --id 31 "/forget"
python3 try_bot.py --id 31 --button forget:no
sqlite3 ~/data/app.db "SELECT COUNT(*) FROM orders WHERE chat_id=31"
python3 try_bot.py --id 31 --button forget:yes
sqlite3 ~/data/app.db "SELECT (SELECT COUNT(*) FROM orders WHERE chat_id=31), (SELECT COUNT(*) FROM feedback WHERE chat_id=31), (SELECT COUNT(*) FROM consent WHERE chat_id=31)"
sqlite3 ~/data/app.db "SELECT COUNT(*) FROM orders WHERE chat_id=32"Перша перевірка (після «Ні») покаже 2: у клієнта 31 два замовлення, і нічого не стерто. Остання перевірка після видалення покаже 0|0|0 для клієнта 31, а друга — 1: замовлення клієнта 32 на місці. Спершу /forget лише питає підтвердження (кнопки [Так, видалити] й [Ні, залишити]) і нічого не стирає; «Ні» теж нічого не стирає; і лише forget:yes видаляє замовлення, відгуки, згоду й запис про клієнта в users (бот скаже, скільки чого видалено).
Тепер прибери сліди проб (вигадані клієнти не мають потрапити у звіти й резервні копії) і запиши в ~/app/README.md розділ ## Персональні дані з трьох пунктів: що бот зберігає, навіщо, як це видалити (/forget). Автоперевірка його не читає, але ти знатимеш відповідь, якщо спитають.
python3 try_bot.py --cleanup 31 32 777Команда відповість (стерто рядків: …).
systemctl --user restart club-bot
sleep 3
tail -n 8 ~/logs/club-bot.logУ журналі не має бути Traceback і рядка меню команд не встановлено (він з’являється, якщо Telegram недоступний або токен неправильний). Тепер у Telegram:
/menu, натисни «Замовити», пройди форму й підтверди;OWNER_CHAT_ID), прийде повідомлення «Нове замовлення» з кнопкою «Виконано» — натисни її;FEEDBACK_DELAY_MIN хвилин (за замовчуванням 30; для проби додай рядок FEEDBACK_DELAY_MIN=1 у ~/app/.env і знову restart) прийде анкета з оцінками. Це й побачить викладач.Немає Telegram — пропусти цей крок: усе попереднє ти вже перевірив через try_bot.py.
Не показуй чужі даніПоки ти пробуєш, у боті можуть бути інші люди: їхні замовлення побачить лише власник. Не показуй і не пересилай скріншоти /clients і /orders іншим: там імена людей.
У репозиторій потрапляє код, у тому числі shop.py та init_db.py. Самі замовлення (~/data/app.db) не потрапляють: база лежить поза репозиторієм, а в базі — чужі персональні дані.
cd ~/app
git status --short
git add -A
git commit -m "Заняття 10: меню, форма замовлення, відгуки, поради ШІ"
git push origin main
git push github maingit status --short покаже M bot.py, M init_db.py, M try_bot.py і новий ?? shop.py. Бачиш .env чи app.db — зупинись: їх у списку бути не має (заняття 03). Кожен git push закінчується рядком main -> main. Якщо у тебе гілка master, пиши master.
Автоперевірка не пише у твій Telegram і не чіпає твоєї справжньої бази: вона працює з копією бази, а Telegram замінює «пустушкою», яка нічого нікуди не надсилає. Власником у перевірці є вигаданий номер. Що саме вона перевіряє:
/menu бот показує картку з жирним заголовком і щонайменше трьома кнопками, кнопка спрацьовує, а список команд для меню Telegram містить menu, order, myorders, feedback; вставки в bot.py мають бути справжнім кодом, а не коментарем;shop.tick();report() дає точні цифри на відомих оцінках (скільки замовлень і відгуків, яка середня), а /report показує їх лише власнику;/forget нічого не видаляє без підтвердження, а після підтвердження видаляє дані лише цієї людини;Натисни «Перевірити практичну частину» в розділі «Перевірка виконаних завдань» нижче на сторінці. Служба має працювати в цю мить: перевіряй під час заняття або ввечері.
| Код | Що перевіряється | Якщо не зараховано |
|---|---|---|
| L10.1 | у базі є таблиці orders, feedback, consent, suggestions, forms автоперевірка | cp /opt/club/kurs/10/init_db.py ~/app/ && cd ~/app && python3 init_db.py — скрипт безпечно додає лише нові таблиці. |
| L10.2 | /menu: картка з розміткою і кнопками, команди в меню Telegram автоперевірка | Скопіюй shop.py з /opt/club/kurs/10/ і додай у bot.py вставки кроку 4: import shop, shop.text(update) у handle_update, обробку callback_query, **shop.extras(chat) у sendMessage і shop.setup_commands() на старті. |
| L10.3 | форма замовлення зберігає його лише після «Підтвердити» автоперевірка | Замовлення пиши в orders лише у функції, яку викликає кнопка order:ok (save_order). Після «Скасувати» чи без підтвердження — нічого. Повторне order:ok не має створювати друге замовлення. |
| L10.4 | після /done клієнту сама приходить анкета 1–5, оцінка й коментар зберігаються автоперевірка | Перевір: /done лише від власника; tick() шле анкету з кнопками rate:ID:1…5 один раз (відмітка asked=1 ДО надсилання); цикл schedule_forever кличе shop.tick() (вставка 5, виклик має бути кодом, а не коментарем); натискання зберігає оцінку, наступний текст — коментар. |
| L10.5 | оцінка 7, 0 чи текст відхиляється, чужа кнопка нічого не пише автоперевірка | save_feedback має перевіряти оцінку (рядок із однієї цифри 1–5), що замовлення твоє й виконане, що відгуку ще немає. Значення в SQL — лише параметрами (?). |
| L10.6 | report(): замовлення, відгуки, середня оцінка; /report лише власнику автоперевірка | report() повертає словник {orders, done, feedbacks, avg}; avg = AVG(rating) з таблиці feedback, None, якщо відгуків немає. У shop.py є готовий приклад SQL. |
| L10.7 | ШІ лише пропонує: без кнопки власника клієнту нічого не йде, ім'я й номер до ШІ не передаються автоперевірка | /next ID зберігає пропозицію (suggestions, статус proposed) і показує власнику кнопки sg:ok / sg:skip. Клієнту надсилає лише decide(…, True) після кнопки власника, один раз. У build_prompt не має бути name і chat_id, а коментар клієнта (там можуть бути телефон чи нік) іде через redact(). |
| L10.8 | без згоди нічого не зберігається, /forget видаляє дані лише цієї людини автоперевірка | Перед першим замовленням — згода (consent:yes/no). /forget питає підтвердження кнопкою; видалення в усіх таблицях з умовою WHERE chat_id = ? (інакше зникнуть дані всіх). |
| L10.9 | у живому Telegram: меню з кнопками, замовлення, прохання про відгук викладач | Позначку ставить викладач на занятті або після нього. |
Кожне питання оцінюється відразу в браузері — це лишається так само. Ця кнопка лише надсилає підсумок учителю, щоб відповіді не зникли разом із закритою вкладкою.
Список критеріїв і що саме вони означають — у таблиці вище, «Що перевіряє автоматика». Тут — та сама перевірка, але прямо зараз, без очікування на найближчий прохід за розкладом.
/menu кнопку «ℹ Про нас» з даними menu:about (латиницею). У функції _callback у shop.py знайди слово "privacy": privacy і додай поруч "about": … з простою функцією, що повертає два-три рядки про твій проєкт. Перевір: python3 try_bot.py --button menu:about./report і запиши в README, що вони порадили покращити.--fake-ai придумай відповідь, яка тут була б доречною).callback_query; чому замовлення пишемо в базу лише після «Підтвердити»; чому для Mini App потрібні домен і сертифікат. На початку заняття 11 викладач спитає щось одне з цього.git add -A, git commit -m "Заняття 10: домашня робота", git push origin main і git push github main.Подивись журнал: tail -n 30 ~/logs/club-bot.log. Найчастіше причина в одному з трьох:
ModuleNotFoundError: No module named 'shop' — файлу shop.py немає в ~/app (крок 3);SyntaxError чи IndentationError після вставок у bot.py — зсунуто відступ; порівняй: diff ~/app/bot.py /opt/club/kurs/10/bot.py;shop.text(update)) стоїть після загального if text.startswith("/") або взагалі відсутня.Бот відповів Таблиць магазину ще немає в базі — виконай cd ~/app && python3 init_db.py.
Повідомлення назве таблицю чи стовпець, якого бракує. Виконай cp /opt/club/kurs/10/init_db.py ~/app/ && cd ~/app && python3 init_db.py і перевір: sqlite3 ~/data/app.db ".tables". Якщо таблиця orders вже була з іншим набором стовпців (ти створював її раніше сам), перейменуй її: sqlite3 ~/data/app.db "ALTER TABLE orders RENAME TO orders_old" і знову запусти init_db.py.
у bot.py немає import shop — вставка 1;у poll-циклі bot.py бракує вставок — перевір вставку 4: у файлі мають бути рядки shop.setup_commands(, shop.on_callback(update) і **shop.extras(chat) — саме кодом, закоментований рядок не рахується;відповідь на /menu без розмітки — ти змінив заголовок у menu() і прибрав <b>…</b>;callback_data довше 64 байтів — літери кирилиці займають по два байти: пиши дані кнопки латиницею (як when:tomorrow).замовлення збережено ДО підтвердження — десь у shop.py з’явився другий INSERT INTO orders. Він має бути один — у save_order(), і викликається лише з кнопки order:ok (або слова «так» на кроці підтвердження);після «Підтвердити» … має бути рівно 1 замовлення — друге order:ok створює ще одне: save_order має викликатися, лише коли форма на кроці confirm, і одразу закривати форму (form_clear);не ті дані — подивись у базі: sqlite3 ~/data/app.db "SELECT * FROM orders".чужа людина може /done — не власник має отримувати «лише для власника»: перевір, що is_owner порівнює номер чату з OWNER_CHAT_ID;shop.tick() не надіслав прохання — у перевірці FEEDBACK_DELAY_MIN=0; перевір, що mark_done записує ask_at, а tick бере замовлення з status='done' AND asked=0;надіслано вдруге — рядок, що ставить asked=1, стоїть після надсилання: перенеси його вище за надсилання, до нього;у schedule_forever немає виклику shop.tick() або в справжньому боті анкети не приходять — додай вставку 5 (shop.tick()) у schedule_forever кодом, не коментарем;друге /done … «уже позначено» — статус міняє один запит UPDATE … WHERE status='new', і бот перевіряє, що рядок справді змінився.Оцінку перевіряє функція save_feedback одним рядком із re.fullmatch(r"[1-5]", …): він пропускає лише одну цифру від 1 до 5 (регулярний вираз, як на занятті 05). Не міняй цей рядок на int(rating): тоді підступний текст («5; DROP TABLE») чи арабська цифра «٥» поведуться неочікувано. Бот «впав» — прочитай повідомлення: IntegrityError означає, що перевірку в коді прибрано, і оцінку зупинила лише база (вона теж має страховку CHECK). Значення в SQL підставляй параметрами ?, а не текстом у рядку запиту (заняття 05).
report() має повертати словник з ключами orders, done, feedbacks, avg;AVG(rating) з feedback, округлена до одного знака (round(…, 1)); для 5, 4, 4 це 4.3;avg має бути None, а не 0 і не помилка ділення;/report показав цифри не власнику — додай перевірку власника для команд /report, /orders, /clients, /done, /next.клієнту пішло повідомлення ДО підтвердження — у propose() не має бути deliver(…); надсилає лише decide(…, True) після кнопки власника;у запиті до ШІ лишився телефон чи нікнейм — текст замовлення й коментар у build_prompt() мають іти через redact();у запиті до ШІ є ім’я або номер чату — подивись build_prompt(): у facts лише замовлення, статус, оцінка, коментар;кнопку «Надіслати» спрацювала від не власника — перед decide стоїть перевірка is_owner(who) (кнопки sg:… і done:…);клієнт отримав кілька повідомлень — змінюй статус одним UPDATE … WHERE status='proposed' і перевіряй, чи змінився хоч один рядок (на уроці дивись функцію decide());повідомлення про помилку ШІ збережено як пропозицію — спершу parse_advice(), і лише якщо відповідь за формою, INSERT INTO suggestions.без згоди бот щось зберіг — start_order має спершу перевірити has_consent; запис у consent робить лише кнопка «Погоджуюсь» (чи слово «так»);/forget видалив дані до підтвердження — команда має лише показати кнопки, а видаляє тільки forget:yes;видалив дані ІНШОЇ людини — у кожному DELETE має бути WHERE chat_id=?, і значення береться з того, хто натиснув кнопку;forget_do очищає всі шість: feedback, suggestions, orders, consent, forms, users.