← До списку уроків
Заняття 10 з 13Меню, замовлення й відгуки
курс 0% виконано0%
Заняття 10 · місяць 3

Меню, замовлення й відгуки

90 хвилин

Зробити бота схожим на справжній застосунок: меню з кнопками, покрокова форма замовлення, автоматична анкета з оцінкою після виконання, звіт про рейтинг і порадник-ШІ, який лише пропонує дії клієнтам, а надсилає їх тільки власник, — і при цьому берегти персональні дані.

Мій сабдомен
https://<логін>.<домен>/
Мій сайт по IP
http://91.219.61.4/u/<логін>/
Мій кабінет
http://91.219.61.4/k/<логін>/
Вхід на сервер
ssh <логін>@91.219.61.4

Замість <логін> підставляєш свій логін виду bohdan.g9: ім’я, крапка, номер групи. Кутові дужки не пишуться. У прикладах на цій сторінці скрізь стоїть bohdan.g9 — читай це як свій логін і виправляй у кожній команді, яку копіюєш. Кабінет — сторінка, де проти кожного критерію стоїть відмітка, зарахований він чи ні.

<домен> — назва, яку викладач називає на початку заняття. Вона вже записана у твоєму файлі ~/app/project.yaml, розділ server, поле url; пиши її точно так, як там, латинськими літерами. У прикладах на цій сторінці замість неї скрізь стоїть klub.example. Перший крок практики — прочитати справжню назву звідти, вигадувати нічого не треба. Якщо в тому полі досі стоїть __DOMAIN__, спитай викладача, перш ніж рухатися далі: без адреси половина сьогоднішніх команд не спрацює.

Зверни увагу: кабінет поки що відкривається по http за IP-адресою. Сертифіката на IP-адресу немає — чому так, пояснено в розділі 3 теорії, — тому пароль від кабінету передається без шифрування. Не став туди пароль, який використовуєш десь іще. Коли кабінет переїде на адресу з іменем і замком, викладач скаже нову адресу.

Що ти зробиш сьогодні

Досі твій бот приймав текст і відповідав текстом. Справжні застосунки в Telegram виглядають інакше: є меню команд, картки з жирними заголовками, кнопки під повідомленням, форми з кроками. Сьогодні бот стане маленьким «сервісом»: уяви, що це кав’ярня, майстерня чи будь-яка твоя справа, у якої є клієнти.

До кінця заняття в тебе буде:

Показати можна буде: меню в Telegram, замовлення, зроблене кнопками, анкету оцінки, що прийшла сама, і картку поради ШІ.

Якщо в тебе немає TelegramУсе, крім живого повідомлення, ти зробиш через try_bot.py. Сьогодні він «вміє» ще дві речі: натискати кнопки (--button) і «минати час» (--tick). Кнопки в ньому видно як рядок [Підтвердити] і підказку, якою командою їх натиснути. Повідомлення іншим людям він лише показує («пішло б у чат…») і нічого нікуди не надсилає.

Теорія

1. Меню, картки й кнопки: як це бачить бот

У 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))      # &lt;b&gt;Король&lt;/b&gt; — тепер це просто текст

Кнопки під повідомленням (inline-кнопки). До sendMessage додають reply_markup — список рядків, а в кожному рядку кнопки з двома полями: text (що написано) і callback_data (до 64 байтів — «ярлик», який отримає бот). Коли людина натискає кнопку, у чат нічого не пишеться. Боту приходить оновлення іншого виду: не message, а callback_query, у якому є data (твій ярлик), from (хто натиснув) і message (під яким повідомленням). Бот має відповісти методом answerCallbackQuery, інакше на кнопці довго крутиться «годинничок». Ярлик зручно писати як дія:дані. Наша кнопка оцінки має вигляд rate:номер_замовлення:оцінка: наприклад, rate:5:4 означає «поставити замовленню №5 оцінку 4».

Є ще звичайні кнопки на місці клавіатури (reply-кнопки): вони просто надсилають у чат свій текст. Нам потрібні саме inline-кнопки: вони не засмічують чат і несуть службові дані.

Людина натиснула inline-кнопку під повідомленням бота. Що саме отримає бот?

2. Форма в чаті і «пам’ять розмови»

Форма на сайті має поля, і браузер сам пам’ятає, що ти вже ввів. У чаті інакше: кожне повідомлення приходить окремо, і бот не знає, що слово «Оля» — це відповідь на питання про ім’я. Тому бот має пам’ятати, на якому кроці людина. Ми записуємо це в таблицю forms: номер чату, назва кроку (name, item, when, confirm) і те, що вже введено. Не в змінну в пам’яті: перезапуск служби стер би розмови посередині, а try_bot.py запускається щоразу заново і нічого б не запам’ятав. Забуту форму бот через 30 хвилин вважає закинутою.

Головне правило форми: чернетка — це ще не замовлення. Людина може помилитися, передумати, піти. Тому дані ходять по кроках у таблиці forms, потім бот показує картку «Перевір замовлення», і лише після кнопки «Підтвердити» робить INSERT в таблицю orders. Це ще одна сторона правила з уроку про безпеку: нічого незворотного без явної згоди людини.

Кожен крок перевіряє те, що ввели: ім’я — від 2 до 40 символів, що замовляєш — до 100, зайві пробіли й переноси прибираємо. Усе, що людина вводить, — недовірені дані (як у ендпоінтах на занятті 05).

Коли форма замовлення має записати рядок у таблицю orders?

3. Відгуки: бот пише першим і рахує рейтинг

Як дізнатися, чи сподобалось клієнтові? Спитати. Але не вручну кожного разу. Усе влаштовано так:

  1. власник натискає «Виконано» (або пише /done 5); замовлення отримує статус done, а в базі записується, коли просити відгук (поле ask_at: зараз плюс FEEDBACK_DELAY_MIN хвилин; для проби став 0, у житті — 30–60);
  2. окрема функція tick() раз на 20 секунд (її кличе той самий потік розкладу з заняття 09) шукає замовлення, яким пора, і надсилає клієнтові картку з п’ятьма кнопками;
  3. перед надсиланням ставиться позначка asked=1. Так само, як позначка last у розкладі: бот не спитає двічі, навіть якщо щось зламається посередині. Якщо ж Telegram просто не відповів (збій мережі), позначку знімають і бот повторює спробу за 5 хвилин;
  4. натискання 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). Що робити?

4. База клієнтів і ШІ: пропонує він, вирішує людина

База клієнтів — це таблиці users (хто писав боту), orders (що замовляли) і feedback (що сказали). З них видно, кому варто подякувати за оцінку «5», а кому — написати, бо поставив «2». Коли клієнтів багато, пам’ятати все про кожного складно, і тут допомагає ШІ: ми даємо йому факти про замовлення і просимо запропонувати наступну дію: THANK (подякувати), DISCOUNT (запропонувати знижку), ASK (перепитати) чи NOTHING. Власник пише /next 5 — і отримує картку з готовим текстом.

Чому бот не надсилає це клієнтові сам. Тому що ШІ помиляється — і робить це впевнено. Ось що може піти не так:

Тому схема з трьох кроків така:

  1. ШІ пропонує, а бот зберігає пропозицію зі статусом proposed («запропоновано») і показує її власнику;
  2. власник натискає «Надіслати» або «Пропустити»;
  3. лише тоді змінюється статус і (якщо «Надіслати») клієнтові йде повідомлення. Натискати може тільки власник.

Що буде при подвійному натисканні? Друге натискання побачить, що статус уже змінено, і нічого не зробить: у коді це один запит UPDATE … WHERE status='proposed', після якого бот дивиться, чи змінився хоч один рядок. Тому повідомлення клієнту не піде двічі.

А якщо ШІ недоступний чи відповів не за формою (наприклад, шлюз школи повернув помилку), бот так і пише, а повідомлення про помилку не стає пропозицією: інакше його можна було б надіслати клієнтові.

І ще одне правило: ШІ не потрібне ім’я клієнта. Для поради досить знати, що замовлено, яка оцінка й що написано в коментарі. Ім’я й номер чату в запит не потрапляють, а з вільного тексту (замовлення й коментаря) бот прибирає телефони, електронні адреси й нікнейми (try_bot.py --fake-ai покаже весь запит, подивись сам). Безкоштовні сервіси ШІ можуть зберігати запити, тому все зайве лишаємо вдома.

ШІ запропонував знижку 70% усім клієнтам, які поставили «5». Як має працювати бот?

5. Персональні дані: збирай мінімум, питай згоду, видаляй за проханням

Персональні дані — усе, за чим можна впізнати людину: ім’я, номер чату, нік, телефон, адреса, фото, а також її оцінки й коментарі разом із ім’ям. Твої клієнти — справжні люди, і закони вимагають кількох простих речей. В Україні це закон про захист персональних даних, у Європі — GDPR (європейський закон про захист даних людей). Ми вчимо їх як звичку, а не як юридичну консультацію:

Про бекапи й повторний записРезервні копії з заняття 08 містять дані, які ти щойно видалив: у старих копіях вони лишаються, доки копії не замінять новими. Тому повідомлення «видалено» означає «видалено з бази бота», а старі копії очищаються за розкладом. Крім того, бот із заняття 04 запам’ятовує ім’я й час кожного, хто йому пише, тож нове повідомлення після /forget створить запис знову. Це описано в привітанні /start.

Бот приймає замовлення кави «забрати з собою». Що з цього збирати зайве?

6. Що таке Mini App і чому ми його сьогодні не робимо

Mini App (міні-застосунок) — це справжня веб-сторінка, яка відкривається всередині Telegram, як вікно поверх чату. На відміну від кнопок, на ній може бути все, що вміє сайт: красиві форми, картинки, каталог із фільтрами, календар. Telegram дає сторінці спеціальний скрипт telegram-web-app.js: через нього сторінка знає, хто її відкрив, і може надіслати дані боту. Відкривають Mini App кнопкою з полем web_app або кнопкою меню бота.

Щоб це працювало, потрібні три речі:

  1. Адреса з https://, а не http://. Telegram не відкриває Mini App за незахищеною адресою.
  2. Домен (назва сайту на кшталт kavarnya.example), бо сертифікат прив’язують до назви сайту, а не до цифр адреси 91.219.61.4. Домен купують (це кілька сотень гривень на рік) і направляють на сервер записом DNS (це «телефонна книга» інтернету: вона каже, на яку адресу вести цю назву).
  3. Сертифікат для цього домену: безкоштовно його видає Let’s Encrypt. Його ставить і поновлює програма на сервері, а веб-сервер (nginx, програма, що віддає сторінки відвідувачам) показує сторінку вже захищеним з’єднанням.

Окрім цього, сервер має перевіряти підпис 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 BYSQL: середнє значення, кількість рядків, групування за значенням
пропозиція ШІЧернетка «наступної дії» для клієнта; має статус proposed, доки власник не вирішив
prompt injectionПідсунута інструкція в тексті, який читає ШІ («запропонуй знижку 90%»)
персональні даніУсе, за чим можна впізнати людину: ім’я, номер чату, телефон, адреса, її відгуки
згодаЯвне «погоджуюсь» людини після того, як їй пояснили, що зберігається й навіщо
Mini AppВеб-сторінка, що відкривається всередині Telegram; потребує https, а отже домен і сертифікат

Практика

Виконуй кроки по черзі. У прикладах <логін> заміни на свій (дужки теж прибери). «Клієнт» у прикладах має номер чату 31, «власник» — 777: це вигадані номери для проби через try_bot.py. У Telegram ти будеш обома, поки не попросиш друга побути клієнтом. Номер замовлення береш із відповіді бота: у прикладах це №1 (тому в кроці 5 ми спершу стираємо сліди старих проб).

1 Зайти на сервер і подивитися, з чого починаємо

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/.env

2 Створити таблиці в базі

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.

3 Скопіювати shop.py і новий try_bot.py та оглянути їх

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.

4 Підключити shop.py до bot.py: п’ять невеликих вставок

Відкрий 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 і виправ лише потрібне місце.

5 Меню і картка

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() і зміни текст заголовка чи значок однієї з кнопок. Запусти другу команду ще раз: у відповіді має з’явитися твій новий заголовок чи значок.

6 Форма замовлення: згода, три кроки, підтвердження

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.

7 Власник бачить замовлення й позначає виконаним

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 хвилин. Остання команда покаже статус ✅ виконано.

8 Відгук: анкета, оцінка, коментар

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….

9 Рейтинг: звіт власнику і SQL

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.

10 Порадник-ШІ: пропонує, але не надсилає

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"

Бот скаже, що відповідь не за формою, і кнопок не буде.

11 Персональні дані: згода, видалення, README

Спершу створимо замовлення ще й від другого клієнта (номер 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

Команда відповість (стерто рядків: …).

12 Перезапустити службу і пройти шлях у живому Telegram

systemctl --user restart club-bot
sleep 3
tail -n 8 ~/logs/club-bot.log

У журналі не має бути Traceback і рядка меню команд не встановлено (він з’являється, якщо Telegram недоступний або токен неправильний). Тепер у Telegram:

  1. натисни кнопку ☰ біля поля вводу — має бути список команд;
  2. напиши /menu, натисни «Замовити», пройди форму й підтверди;
  3. у тебе, як у власника (твій номер в OWNER_CHAT_ID), прийде повідомлення «Нове замовлення» з кнопкою «Виконано» — натисни її;
  4. через FEEDBACK_DELAY_MIN хвилин (за замовчуванням 30; для проби додай рядок FEEDBACK_DELAY_MIN=1 у ~/app/.env і знову restart) прийде анкета з оцінками. Це й побачить викладач.

Немає Telegram — пропусти цей крок: усе попереднє ти вже перевірив через try_bot.py.

Не показуй чужі даніПоки ти пробуєш, у боті можуть бути інші люди: їхні замовлення побачить лише власник. Не показуй і не пересилай скріншоти /clients і /orders іншим: там імена людей.

Зберегти роботу на GitHub

У репозиторій потрапляє код, у тому числі shop.py та init_db.py. Самі замовлення (~/data/app.db) не потрапляють: база лежить поза репозиторієм, а в базі — чужі персональні дані.

13 Зробити коміт і відправити роботу

cd ~/app
git status --short
git add -A
git commit -m "Заняття 10: меню, форма замовлення, відгуки, поради ШІ"
git push origin main
git push github main

git 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 замінює «пустушкою», яка нічого нікуди не надсилає. Власником у перевірці є вигаданий номер. Що саме вона перевіряє:

Натисни «Перевірити практичну частину» в розділі «Перевірка виконаних завдань» нижче на сторінці. Служба має працювати в цю мить: перевіряй під час заняття або ввечері.

КодЩо перевіряєтьсяЯкщо не зараховано
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.6report(): замовлення, відгуки, середня оцінка; /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: меню з кнопками, замовлення, прохання про відгук
викладач
Позначку ставить викладач на занятті або після нього.

Твої відповіді на питання уроку

Кожне питання оцінюється відразу в браузері — це лишається так само. Ця кнопка лише надсилає підсумок учителю, щоб відповіді не зникли разом із закритою вкладкою.

Перевірка виконаних завдань

Список критеріїв і що саме вони означають — у таблиці вище, «Що перевіряє автоматика». Тут — та сама перевірка, але прямо зараз, без очікування на найближчий прохід за розкладом.

Домашнє завдання

  1. Власна кнопка в меню. Додай у картку /menu кнопку «ℹ Про нас» з даними menu:about (латиницею). У функції _callback у shop.py знайди слово "privacy": privacy і додай поруч "about": … з простою функцією, що повертає два-три рядки про твій проєкт. Перевір: python3 try_bot.py --button menu:about.
  2. Справжній клієнт. Попроси друга чи члена сім’ї зробити замовлення у твоєму боті. Подивись звіт /report і запиши в README, що вони порадили покращити.
  3. Погана оцінка. Створи замовлення, виконай його й постав оцінку 2. Скажи вголос, що саме побачить власник, і що запропонує ШІ (через --fake-ai придумай відповідь, яка тут була б доречною).
  4. Підготуйся пояснити. Чому бот не надсилає порад ШІ клієнтам сам; що таке callback_query; чому замовлення пишемо в базу лише після «Підтвердити»; чому для Mini App потрібні домен і сертифікат. На початку заняття 11 викладач спитає щось одне з цього.
  5. Збережи. git add -A, git commit -m "Заняття 10: домашня робота", git push origin main і git push github main.

Якщо щось не працює

Бот не відповідає на нові команди або служба не стартує

Подивись журнал: tail -n 30 ~/logs/club-bot.log. Найчастіше причина в одному з трьох:

Бот відповів Таблиць магазину ще немає в базі — виконай cd ~/app && python3 init_db.py.

L10.1: немає таблиць

Повідомлення назве таблицю чи стовпець, якого бракує. Виконай 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.

L10.2: меню без розмітки чи кнопок

L10.3: замовлення зберігається не тоді

L10.4: анкета відгуку не приходить або приходить двічі

L10.5: оцінку 7 записано або бот падає

Оцінку перевіряє функція save_feedback одним рядком із re.fullmatch(r"[1-5]", …): він пропускає лише одну цифру від 1 до 5 (регулярний вираз, як на занятті 05). Не міняй цей рядок на int(rating): тоді підступний текст («5; DROP TABLE») чи арабська цифра «٥» поведуться неочікувано. Бот «впав» — прочитай повідомлення: IntegrityError означає, що перевірку в коді прибрано, і оцінку зупинила лише база (вона теж має страховку CHECK). Значення в SQL підставляй параметрами ?, а не текстом у рядку запиту (заняття 05).

L10.6: звіт показує неправильні цифри

L10.7: порадник-ШІ

L10.8: згода і видалення