Перед уроком
Обери свою групу й себе зі списку. Тоді відповіді й перевірки завдань запишуться саме на тебе.
Навчити бота керувати контактами: додавати, змінювати й видаляти їх командами й кнопками в Telegram, на помилки вводу відповідати зрозуміло, а підсумки виводити на статичну сторінку, яку бот оновлює сам.
Замість <логін> підставляєш свій
логін виду bohdan.g9: ім’я, крапка, номер групи. Кутові дужки не пишуться.
Кабінет — сторінка, де проти кожного критерію стоїть відмітка, зарахований він чи ні.
У прикладах на цій сторінці скрізь написано bohdan.g9 — читай це як свій логін.
Зараз список тих, хто писав твоєму боту, ти бачиш командою /users, а змінити в ньому щось можна лише командою SQL у терміналі. Сьогодні в боту з’явиться «адмінка» — набір дій, якими ти керуєш контактами просто в Telegram, без терміналу й без жодної сторінки в браузері: /list — показати, /add — додати, /edit — змінити, /del — видалити. Контакт — це рядок таблиці users. Він може бути людиною, яка писала боту, а може бути доданий тобою вручну: однокласник, який боту ще не писав.
Окремо ти навчишся робити так, щоб бот на неправильні дані відповідав словами, а не падав: порожнє ім’я, номер, якого немає, «номер», який не є числом — на кожне бот пояснить, що не так і як правильно. Додаси кнопки внизу екрана Telegram, щоб головні команди натискалися пальцем. І зробиш статичну сторінку ~/www/stats.html з цифрами (скільки людей, скільки повідомлень) — її бот переписуватиме сам після кожної зміни, а відкривається вона в будь-якому браузері, навіть на телефоні, без жодної працюючої програми.
Наприкінці покажеш: контакт, який ти додав, змінив і видалив командами; бота, що на порожнє ім’я й на неіснуючий номер відповідає поясненням; сторінку http://91.219.61.4/u/<логін>/stats.html на телефоні; репозиторій на GitHub, де основна гілка тепер називається main.
Якщо в тебе немає TelegramУсе, крім кнопок і розмови з живим ботом, робиться через try_bot.py: python3 try_bot.py "/add Оля | подруга". Автоперевірка теж пише боту саме так.
Поки що команди відкритіЯк і /users із заняття 04, команди /add, /edit і /del сьогодні виконає кожен, хто знайде твого бота: ніхто не перевіряє, що це саме ти. Це тимчасово, для навчання: на занятті 07 ти залишиш керування лише собі. Тому поки що нікому не розсилай нік свого бота й не пиши в контакти справжніх даних, яких не хочеш показувати.
З будь-яким списком даних роблять чотири речі: створити запис, прочитати (показати) його, змінити й видалити. Програмісти називають їх одним словом CRUD — за першими літерами англійських Create, Read, Update, Delete. Майже будь-який застосунок — це ці чотири дії над різними таблицями: записник, магазин, соцмережа. У базі їм відповідають чотири команди SQL, які ти вже знаєш із заняття 04:
| Дія | Команда бота | Що робить у базі |
|---|---|---|
| показати | /list | SELECT: читає рядки, нічого не змінює |
| створити | /add Оля | сидить поруч | INSERT: додає рядок |
| змінити | /edit 7 note нова примітка | UPDATE … WHERE id = 7: міняє лише рядок №7 |
| видалити | /del 7 | DELETE … WHERE id = 7: прибирає рядок №7 |
Така «панель керування» називається адмінкою — від слова «адміністратор», людина, яка керує даними. У багатьох сайтів адмінка — окрема сторінка з формами. У нас її роль виконує сам бот: ти пишеш йому команду — він змінює базу й відповідає, що вийшло. Чому не сторінка? Бо сторінка з формами потребує програми, яка весь час працює і чекає запитів, — вона займає пам’ять сервера, а ми ділимо його на всю групу. Бот уже працює, і додати йому кілька команд майже безкоштовно. До того ж в Telegram уже є вхід, телефон і кнопки, які не треба малювати.
Команда — це повідомлення, що починається з косої риски. Після назви йдуть аргументи — дані, з якими працює команда. У /edit 7 note нова примітка їх три: номер запису (7), назва поля (note) і нове значення (нова примітка). Бот розбирає текст по частинах: слова розділяються пробілами, а в /add Оля | сидить поруч ім’я від примітки відділяє вертикальна риска |, бо ім’я та примітка самі можуть складатися з кількох слів.
Номер, який потрібен для /edit і /del, ти береш зі списку: це id з таблиці — той самий PRIMARY KEY із заняття 04. Саме тому /list друкує його на початку кожного рядка: 7. Оля, повідомлень: 0, …. Номер не змінюється за життя запису, тож по ньому можна впевнено вказати, кого саме міняти.
Контакт, якого нема в TelegramУ людини, яка писала боту, є chat_id. У контакту, якого ти додав сам, його немає: у цьому полі лежить NULL — «нічого не записано» (заняття 04). Правило UNIQUE дозволяє скільки завгодно порожніх значень, тож рядків без chat_id може бути багато. А ось видалена людина, яка писала боту, повернеться, щойно напише знову: бот додасть її як нову.
У списку є рядок 7. Оля, повідомлень: 3, …. Ти пишеш /edit 7 note сидить поруч. Що змінить бот?
Бот отримує просто рядок тексту: "/edit 7 note сидить поруч". Щоб дістати з нього частини, Python має кілька інструментів, які ми й візьмемо:
text.split(None, 1) — розбити на дві частини по першому пропуску: ["/edit", "7 note сидить поруч"]. Перша — назва команди, друга — усі аргументи разом. Слово None означає «роздільник — будь-яка кількість пробілів»; число 1 — «розрізати не більше одного разу»;arg.split(None, 2) — розбити на три частини: ["7", "note", "сидить поруч"]. Решта тексту лишається цілою — у примітці ж можуть бути пробіли;arg.partition("|") — розрізати по першій вертикальній рисці на три частини: те, що до неї, сама риска і те, що після. "Оля | сидить поруч".partition("|") дає ("Оля ", "|", " сидить поруч"). Є риска чи ні — частини завжди три, тож розпаковувати їх можна не боячись помилки;" ".join(text.split()) — «прибрати зайве»: split() без аргументів викидає порожні місця, а join склеює слова одним пробілом. Ім’я " Оля К.\n" перетворюється на "Оля К.". Ми викликаємо це для всього, що людина ввела;word.isdigit() — «в слові лише цифри». Саме так бот перевіряє, що номер — число: "7".isdigit() це True, "абв".isdigit() та "-3".isdigit() — False.Нижче в практиці ти побачиш їх у справі. Усе це — звичайні рядки Python; нічого нового, крім уважності до того, що людина може написати.
Бот отримує дані від людей, а люди вводять усяке: забувають ім’я, плутають номер, пишуть літери замість цифр, натискають кнопку, яка не підходить. Тому програма завжди має план на «неправильно». Є три рівні, і їх легко сплутати:
NOT NULL із заняття 04 зупинив би запис. Але база відповість мовою програм (NOT NULL constraint failed), яку людина не зрозуміє. Це запасний захист, а не заміна першого рівня;Traceback — бот у poll_forever() із заняття 02 перехопить його й напише людині «Ой, щось пішло не так». Бот живий, але людина не отримала ні результату, ні пояснення. У журналі лишився рядок «помилка обробки» — для тебе. Потрапити сюди через неправильне введення не повинно.Хороша відповідь на помилку робить три речі: каже, що не так («немає контакту з номером 999999»), показує, як правильно (приклад команди або «подивись номери: /list») і ніколи не звинувачує людину. «Номер має бути числом» — добре; «Ти ввів дурницю» — ні.
Ще важливо, що саме бот перевіряє. Кожне поле має межі: ім’я — не порожнє й не довше за 100 символів, примітка — до 500. Без меж хтось надішле «ім’я» на 4000 символів, і відповідь з таким іменем не вміститься в одне повідомлення (Telegram приймає не більше 4096 символів). Перевіряти треба в коді бота, бо ніхто, окрім нього, цього не зробить: кнопка Telegram не вміє «заборонити порожнє поле», на відміну від форми на сторінці.
І ще одне: значення з повідомлення не вставляють у SQL (заняття 04). Усюди — через ?. Єдине виключення в сьогоднішньому коді: назву стовпця, який міняє /edit, підставляє f-рядок, але вона береться не з тексту людини, а зі списку «name або note», який бот перевірив раніше. Людина вибирає зі списку; вигадати свою назву стовпця вона не може.
Хтось пише /del абв. Яка відповідь бота найкраща?
Від заняття 04 у тебе є три функції для бази: query (прочитати), execute (записати й дізнатися id нового рядка) і changed (записати й дізнатися, скільки рядків змінилося). Сьогодні вони всі потрібні:
INSERT через execute: дізнаємося id нового контакту й повідомляємо його людині («Додано: 7. Оля»), щоб вона знала номер для /edit і /del;UPDATE … WHERE id = ? через changed. Якщо повернулося 0 — рядка з таким номером немає, і це не збій, а звичайна ситуація «ти помилився номером»: відповідаємо «Контакту з номером 999999 немає»;DELETE … WHERE id = ?. Спершу SELECT — дізнатися ім’я, щоб написати «Видалено: 7. Оля» (після DELETE воно зникне), і лише потім видаляємо.У UPDATE і DELETE умова WHERE id = ? обов’язкова — без неї зміниться чи зникне вся таблиця (заняття 04). Тому в кожній такій команді спершу перевіряємо номер, а лише потім звертаємося до бази.
Видалення не можна скасувати: рядка більше немає ніде. Тому відповідь на /del повторює, кого саме видалено, і підказує команду, якою його можна додати знову. Для великих даних перед видаленням просять підтвердження (кнопкою «Так, видалити») — на занятті 10 ти зробиш і це; для списку з десятка контактів достатньо чіткої відповіді.
Щоб не набирати /list руками, Telegram вміє показувати кнопки. Є два види, і сьогодні ми беремо простіший:
/list надсилає текст /list. Бот навіть не знає, що це була кнопка — тому всі його команди працюють однаково;Щоб показати клавіатуру, бот додає до sendMessage параметр reply_markup — JSON-опис рядів кнопок. Наша клавіатура:
{"keyboard": [[{"text": "/list"}, {"text": "/add"}],
[{"text": "/help"}]],
"resize_keyboard": True}Зовнішній список — ряди, внутрішні — кнопки в ряду; тут два ряди (дві кнопки й одна). resize_keyboard робить кнопки компактними. Клавіатура лишається на екрані, поки її не заберуть, тож досить надіслати її з першою відповіддю. Ми додаємо її до кожної відповіді — і це дешевше, ніж стежити, кому її вже показано.
Кнопка /add без імені надішле боту просто /add — і бот відповість підказкою з прикладом. Це й є «добра відповідь на помилку» з розділу 3. Ще один прийом: у Telegram будь-яку команду в тексті повідомлення (/list, /help) можна натиснути пальцем, тож підказки бота з назвами команд теж працюють як кнопки.
Ти вже знаєш, що сайт у твоїй теці ~/www — це звичайні файли, які віддає nginx. Жодна програма для цього не працює: прийшов запит — nginx відкрив файл і віддав. Така сторінка називається статичною. Протилежність — динамічна: сторінка, яку для кожного запиту складає працююча програма. Ми динамічних не робимо, бо вони потребують процесу, який постійно висить у пам’яті.
Але цифри на сторінці міняються, і їх хочеться бачити свіжими. Рішення просте: бот, який і так працює, сам переписує файл щоразу, коли дані змінилися. Для відвідувача все виглядає так, ніби сторінка жива, а сервер знову не напружується: файл лежить і чекає. Так у нас працюватиме ~/www/stats.html: після кожного нового повідомлення і після кожної команди /add, /edit, /del бот перераховує цифри й записує сторінку заново.
Два правила, як переписувати файл безпечно:
stats.html і в цей момент хтось відкрив сторінку, він побачив би половину тексту. Тому бот пише в stats.html.tmp, а тоді os.replace підміняє файл за один крок: відвідувач бачить або стару повну сторінку, або нову повну. Те саме ми робили з bot_state.json на занятті 02;/add, бот однаково відповість. Тому write_stats() перехоплює помилки сама й лише друкує їх у журнал.Що писати на сторінці? Вона публічна: адресу http://91.219.61.4/u/<логін>/stats.html може відкрити будь-хто, хто її знає, без жодного входу. Тому там тільки цифри: «людей у базі», «повідомлень», «писали за тиждень» — і жодного імені, ніка чи номера чату. Це продовження розмови про персональні дані із заняття 04: те, що лежить у базі, не обов’язково показувати світові. Автоперевірка L05.4 саме це й перевіряє: вона додає контакт із вигаданим ім’ям і дивиться, що на сторінці його немає.
І ще дві речі, без яких сторінку незручно читати. Рядок <meta name="viewport" content="width=device-width, initial-scale=1"> у заголовку каже телефону: «показуй сторінку на ширину екрана, а не як зменшений комп’ютерний сайт» — без нього все стає дрібним. А порожній стан — те, що людина бачить, коли даних ще немає. Порожня сторінка з нулями схожа на зламану; тому, коли людей нуль, сторінка каже: «Поки що тут порожньо. Напиши боту /start — і з’являться перші цифри». Добрий порожній стан каже дві речі: що відбувається і що зробити.
Яку цифру не можна виводити на публічній сторінці stats.html?
Навіщо бот пише сторінку спочатку в stats.html.tmp, а потім перейменовує?
| Слово | Що означає |
|---|---|
| адмінка | Панель, якою керують даними; у нас — команди бота в Telegram |
| CRUD | Чотири дії над списком: створити, прочитати, змінити, видалити |
| команда, аргумент | Повідомлення з косою рискою; дані після назви команди |
split, partition, isdigit | Розрізати рядок на слова; розрізати по першому роздільнику; чи складається рядок із цифр |
| перевірка введення | Код сам дивиться, чи годяться дані, і відповідає зрозуміло, ще до звернення до бази |
| порожній стан | Те, що бачить людина, коли даних ще немає, — з поясненням, що робити |
changed() | Функція db.py: виконати запит і повернути, скільки рядків змінилося (0 — такого запису немає) |
| клавіатура відповіді | Кнопки внизу екрана Telegram; натискання надсилає боту текст кнопки |
reply_markup | Параметр sendMessage з описом кнопок |
| статична сторінка | Готовий файл, який nginx просто віддає; жодної програми для цього не працює |
os.replace | Підмінити файл за один неподільний крок; так сторінка ніколи не лишається напівзаписаною |
viewport | Рядок у заголовку сторінки, що каже телефону показувати її на ширину екрана |
contacts.py | Файл бота з командами для контактів і сторінкою статистики |
stats.html | Публічна сторінка з цифрами, яку бот переписує сам |
Виконуй кроки по черзі. Де в команді стоїть $USER, нічого не підставляй — ця змінна вже містить твій логін. А <логін> разом із кутовими дужками заміни на свій, наприклад bohdan.g9. Номери контактів (6, 7) у прикладах — умовні: у тебе будуть свої, і в командах пиши саме їх. Усі команди виконуються з теки ~/app, якщо не сказано інше.
ssh <логін>@91.219.61.4
cd ~/app
systemctl --user is-active club-bot
python3 try_bot.py "/users" | head -n 3
ls contacts.py
grep -n "def changed" db.py
git status --short
git branch --show-currentМає бути по черзі: active — служба працює; початок списку («Людей у базі: …» і перші рядки з людьми, щонайменше п’ятеро з заняття 04); contacts.py — файл із заняття 04 на місці; рядок на зразок 44:def changed(sql, params=()):; порожній вивід git status --short — усе з заняття 04 закомічено; і назва основної гілки — найімовірніше, master. Сьогодні наприкінці ти її перейменуєш.
inactive — systemctl --user start club-bot;contacts.py або не знаю такої команди /users — поверніться до кроку 13 заняття 04;grep нічого не знайшов — у тебе стара версія db.py: cp /opt/club/kurs/04/db.py ~/app/db.py (свого коду в цьому файлі ти не писав);git status є файли — збережи їх: git add -A і git commit -m "Заняття 04: доробки".contacts.py: імпорти й межіnano contacts.pyУ файлі з заняття 04 вгорі стоїть import sqlite3, порожній рядок і from db import query. Заміни ці рядки на такий блок імпортів (рядок import sqlite3 лишається — просто тепер він серед інших):
import html
import os
import sqlite3
from datetime import datetime, timedelta
from pathlib import Path
from db import CFG, changed, execute, queryНові імпорти знадобляться по черзі: changed і execute — командам, html, os, datetime, Path і CFG — сторінці статистики. Одразу під рядком MAX_REPLY = 3800 … додай межі для введення. А під функцією _clean() — функцію now():
MAX_NAME = 100
MAX_NOTE = 500def now():
return datetime.now().isoformat(timespec="seconds")З nano не виходь: кроки 3–6 працюють у тому ж файлі. Зберегти можна будь-коли (Ctrl+O, Enter).
Знайди в contacts.py рядок HANDLERS = …. Над ним, між функціями, встав п’ять маленьких функцій, які перевіряють те, що ввела людина. Жодна не торкається бази:
def _id(word):
"""«7» → 7; усе інше → None. Номер у базі — додатне ціле."""
if word.isdigit() and len(word) <= 12:
return int(word)
return None
def _bad_id(word):
return (f"Номер має бути числом — його видно на початку рядка в /list. "
f"А ти написав «{_clean(word)[:20]}».")
def _no_such(num):
return f"Контакту з номером {num} немає. Подивись номери: /list"
def _name_error(name):
if not name:
return "Ім’я не може бути порожнім. Приклад: /add Оля | сидить поруч"
if len(name) > MAX_NAME:
return f"Ім’я задовге: {len(name)} символів, а можна до {MAX_NAME}."
return None
def _note_error(note):
if len(note) > MAX_NOTE:
return f"Примітка задовга: {len(note)} символів, а можна до {MAX_NOTE}."
return None_id(word) — перетворює слово на номер: "7" дає 7, усе інше (літери, мінус, порожнє, дуже довге число) дає None — «це не номер». Обмеження на 12 цифр не дає людині передати в базу число, яке не вміститься в неї;_bad_id і _no_such — тексти двох помилок: «не число» і «такого немає». У першій [:20] обрізає те, що написала людина, щоб величезний рядок не повернувся їй назад у відповіді;_name_error і _note_error — повертають текст помилки, якщо ім’я порожнє чи задовге (примітка — лише задовга), а коли все гаразд — None. Така домовленість зручна: «є текст — є проблема».Лапки ’ в слові «Ім’я» — типографський апостроф: у тексті відповіді він безпечний, а прямий ' закрив би рядок Python.
/add — додати контактЩе нижче, також перед HANDLERS, встав:
def add(arg):
name, _, note = arg.partition("|")
name, note = _clean(name), _clean(note)
problem = _name_error(name) or _note_error(note)
if problem:
return problem
t = now()
new_id = execute("""INSERT INTO users(chat_id, name, username, note, first_seen, last_seen, messages)
VALUES(NULL, ?, '', ?, ?, ?, 0)""", (name, note, t, t))
return f"Додано: {new_id}. {name}. Усі контакти: /list"arg.partition("|") — ім’я до риски, примітка після; якщо риски немає, note буде порожнім рядком;_clean — зайві пробіли й переноси рядків перетворюються на один пробіл. Ім’я з самих пробілів стає порожнім, а порожнє — відхиляється;problem = _name_error(name) or _note_error(note) — перша знайдена проблема; якщо є, відповідаємо нею й виходимо, нічого не записавши;? і п’ять значень: ім’я, примітка, два часи та лічильник 0 і NULL замість chat_id уже стоять у самому запиті. У контакту без Telegram немає ні chat_id, ні ніка ('');execute повертає id нового рядка, і ми показуємо його людині./edit і /delНижче add(), також перед HANDLERS:
def edit(arg):
parts = arg.split(None, 2)
if len(parts) < 2:
return "Приклад: /edit 7 note сидить поруч або /edit 7 name Нове ім’я. Номер дивись у /list."
num = _id(parts[0])
if num is None:
return _bad_id(parts[0])
field = parts[1].lower()
value = _clean(parts[2]) if len(parts) > 2 else ""
if field not in ("name", "note"):
return f"Змінити можна name (ім’я) або note (примітку), а не «{_clean(parts[1])[:20]}»."
problem = _name_error(value) if field == "name" else _note_error(value)
if problem:
return problem
# назва стовпця береться лише зі списку вище, значення — через ?
if not changed(f"UPDATE users SET {field} = ? WHERE id = ?", (value, num)):
return _no_such(num)
return f"Змінено контакт {num}: {'ім’я' if field == 'name' else 'примітка'} — «{value}»."
def delete(arg):
if not arg:
return "Приклад: /del 7 — номер дивись у /list."
if len(arg.split()) > 1:
return "Видаляю по одному контакту за раз: /del 7"
word = arg.split()[0]
num = _id(word)
if num is None:
return _bad_id(word)
rows = query("SELECT name FROM users WHERE id = ?", (num,))
if not rows:
return _no_such(num)
changed("DELETE FROM users WHERE id = ?", (num,))
return (f"Видалено: {num}. {_clean(rows[0]['name'])}. "
f"Передумав — додай знову: /add {_clean(rows[0]['name'])}")arg.split(None, 2) — номер, назва поля й решта тексту. Менше за два слова — показуємо приклад;_id до бази; поле — лише name чи note. Значення поля теж проходить перевірку (ім’я не порожнє, обидва не задовгі); для примітки порожнє значення дозволено — так її очищують;changed(…) повертає 0 — отже, рядка з таким номером немає, відповідаємо «Контакту з номером … немає». Назва стовпця підставляється f-рядком, бо field — це вже перевірене слово name або note; значення й номер ідуть через ?;delete() порожній запит і кілька номерів відхиляються окремими поясненнями; потім SELECT — запам’ятати ім’я, потім DELETE … WHERE id = ?.У кінці файлу (там, де були HANDLERS і command із заняття 04) заміни їх на новішу версію. Було два шматки; тепер вони такі:
HANDLERS = {"/users": lambda arg: users_text(), "/list": lambda arg: users_text(),
"/add": add, "/edit": edit, "/del": delete}
CHANGING = ("/add", "/edit", "/del") # після них бот переписує stats.html
def command(text, sender=None):
"""Команда → текст відповіді; None, якщо такої команди тут немає."""
parts = text.split(None, 1)
name = parts[0].split("@")[0].lower()
handler = HANDLERS.get(name)
if handler is None:
return None
try:
reply = handler(parts[1].strip() if len(parts) > 1 else "")
except sqlite3.Error as e: # база зайнята чи зіпсована — не traceback людині
print(f"{now()} помилка бази: {e!r}", flush=True)
return "База зараз недоступна. Спробуй ще раз за хвилину."
if name in CHANGING:
write_stats() # дані могли змінитися — переписуємо сторінку
return reply
# ── кнопки внизу екрана ─────────────────────────────────────────────
KEYBOARD = {"keyboard": [[{"text": "/list"}, {"text": "/add"}], [{"text": "/help"}]],
"resize_keyboard": True}
def extras():
"""Кнопки внизу екрана Telegram: натискання надсилає боту текст кнопки, як звичайне повідомлення."""
return {"reply_markup": KEYBOARD}
Що змінилося: у словнику з’явилися /list (друге ім’я для /users), /add, /edit і /del; кортеж CHANGING зберігає команди, після яких дані могли змінитись; command() після такої команди викликає write_stats() — функцію зі сторінкою статистики, яку ти додаси в наступному кроці. Нижче — клавіатура з розділу 5: словник KEYBOARD і функція extras(), що повертає додатковий параметр для sendMessage. Ще одна дрібниця для порожнього стану: у функції users_text() заміни рядок return "Поки що боту ніхто не писав. Напиши йому сам — і тут з’явиться рядок." на два рядки, у дужках, щоб бот підказував, як додати першого:
return ("Поки що боту ніхто не писав. Напиши йому сам — і тут з’явиться рядок. "
"Або додай контакт сам: /add Оля | подруга")І останнє: щоб у /list було видно примітку, яку ставлять /add Оля | сидить поруч і /edit 7 note …, у функції line() заміни останній рядок (return f"{row['id']}. …") на два:
note = f" — {_clean(row['note'])}" if row["note"] else ""
return f"{row['id']}. {_clean(row['name'])}{nick}, повідомлень: {row['messages']}, востаннє {when}{note}"Примітки немає — рядок такий, як і був; є — додається після тире.
Тепер збережи файл (Ctrl+O, Enter) і вийди (Ctrl+X). Поки write_stats() немає, команди /add, /edit і /del видаватимуть помилку — тому спершу допиши її, а перевіряти команди будемо одразу після.
Готовий блок сторінки лежить у файлі уроку; виведи його й допиши в кінець свого contacts.py. Ти вже вийшов з nano, тож просто в терміналі:
sed -n '/^# ── статична сторінка/,$p' /opt/club/kurs/05/contacts.py | head -n 20
sed -n '/^# ── статична сторінка/,$p' /opt/club/kurs/05/contacts.py >> contacts.py
python3 -c "import contacts; print(contacts.stats_numbers())"Перша команда лише показує початок блоку (head -n 20 — перші двадцять рядків). Друга дописує блок у кінець твого файлу: >> — «додати в кінець», а один > стер би файл. Не виконуй її двічі. Третя перевіряє цифри: вона друкує словник на зразок {'people': 7, 'messages': 12, 'recent': 7, 'manual': 0} — скільки людей, повідомлень, писали за тиждень і скільки додано вручну.
Прочитай блок (less contacts.py, вийти — q) — він невеликий:
stats_path() — куди писати. Звичайно це ~/www/stats.html, а змінна оточення STATS_FILE підставляє інший файл: так автоперевірка пише в тимчасовий, а не у твій;stats_numbers() — один запит SELECT із трьома підрахунками. Імена до нього не потрапляють узагалі: з бази читаються лише числа;stats_html() — складає HTML: чотири «картки» з цифрами або пояснення, якщо людей нуль. Назва продукту береться з project.yaml й проходить крізь html.escape: якщо в назві є < чи &, вони стануть безпечними символами;write_stats() — пише в тимчасовий файл і підміняє os.replace; помилку диска перехоплює й лише друкує.Теки ~/www у тебе вже є. Якби не було — write_stats() створить її сама (mkdir(parents=True, exist_ok=True)).
python3 try_bot.py "/add Оля Кравчук | сидить поруч"
python3 try_bot.py "/list"
python3 try_bot.py "/edit 7 note допомагає тестувати"
python3 try_bot.py "/edit 7 name Оля К."
python3 try_bot.py "/list"
python3 try_bot.py "/del 7"
python3 try_bot.py "/list"Приблизно такі відповіді (номер у тебе свій — це наступний вільний): «Додано: 7. Оля Кравчук. Усі контакти: /list»; список, де нагорі рядок «7. Оля Кравчук, повідомлень: 0, востаннє … — сидить поруч»; «Змінено контакт 7: примітка — «допомагає тестувати»»; «Змінено контакт 7: ім’я — «Оля К.»»; список із новим іменем і приміткою; «Видалено: 7. Оля К. Передумав — додай знову: /add Оля К.»; список без неї. Лічильник повідомлень у доданого вручну контакту — 0: він боту нічого не писав. А ти, коли пишеш боту команди, лічишся як звичайна людина — тому твій рядок (Тест) у списку завжди першим.
Якщо замість відповідей — NameError: name 'write_stats' is not defined, ти пропустив крок 7; SyntaxError чи IndentationError — подивись номер рядка у повідомленні: найчастіше загублена дужка чи лапка у вставленому коді; No module named 'contacts' — ти не в ~/app; Не знаю такої команди — HANDLERS не оновлено. Перевір: grep -n "/edit" contacts.py має знайти рядок словника HANDLERS.
Тепер перевіряємо головне — що бот робить, коли людина помиляється:
python3 try_bot.py "/add"
python3 try_bot.py "/add | примітка без імені"
python3 try_bot.py "/del 999999"
python3 try_bot.py "/edit 999999 note щось"
python3 try_bot.py "/del абв"
python3 try_bot.py "/edit 3 колір синій"
python3 try_bot.py "/edit 3 name"
python3 try_bot.py "/add $(python3 -c 'print("Я" * 101)')"
python3 try_bot.py "/list" | head -n 1Кожна з перших восьми команд має дати одне зрозуміле речення без слова Traceback: «Ім’я не може бути порожнім. Приклад: /add Оля | сидить поруч»; те саме для порожнього імені з приміткою; «Контакту з номером 999999 немає. Подивись номери: /list»; те саме для /edit; «Номер має бути числом — його видно на початку рядка в /list. А ти написав «абв»»; «Змінити можна name (ім’я) або note (примітку), а не «колір»»; «Ім’я не може бути порожнім» для /edit 3 name; «Ім’я задовге: 101 символів, а можна до 100». Остання команда друкує перший рядок списку: кількість людей не змінилася після всіх помилок — жодна не пройшла в базу.
Побачив Traceback — прочитай його останній рядок: це місце, де перевірка введення пропустила випадок. Найчастіше — _id не викликається (int(…) прямо від тексту) або забутий return після повідомлення про помилку.
І ще дві спроби, що показують, чому значення йдуть через ? (заняття 04):
python3 try_bot.py "/add Мар'яна | з \"лапками\""
python3 try_bot.py "/add x'); DROP TABLE users;--"
python3 try_bot.py "/list" | head -n 4
sqlite3 ~/data/app.db ".tables"Обидва «імені» записались як є — з апострофом, лапками й навіть «командою» DROP TABLE усередині. Таблиця users на місці: текст людини ніколи не стає частиною запиту. Видали ці два зайві контакти командами /del з їхніми номерами зі списку.
bot.pyУ try_bot.py усе вже працює, бо виклик contacts.command() у bot.py з'явився на занятті 04. Лишилося три дрібні зміни в самому боті: довідка, свіжа сторінка після кожного повідомлення й кнопки при відправленні відповіді.
nano bot.pyПерша — довідка. У кінці рядка HELP заміни останній рядок "/users — хто писав боту") на чотири (на кінці кожного, крім останнього, — \n усередині лапок):
"/users, /list — хто писав боту\n"
"/add Ім’я | примітка — додати контакт\n"
"/edit номер note текст — змінити\n"
"/del номер — видалити")Закривна дужка ) стоїть лише в останньому рядку — вона закриває HELP = (….
Друга — свіжа сторінка після кожного нового повідомлення. У кінці remember(), після виклику execute(…), додай два рядки (відступ — чотири пробіли):
import contacts # заняття 05: свіжа сторінка статистики
contacts.write_stats()Третя — кнопки. Знайди в poll_forever() рядок tg("sendMessage", chat_id=chat, text=reply) і додай до нього параметр; а перед функцією start_polling() — нову функцію:
tg("sendMessage", chat_id=chat, text=reply, **contacts_extras())def contacts_extras():
"""Заняття 05: кнопки /list, /add, /help внизу екрана Telegram."""
import contacts
return contacts.extras()**contacts_extras() розгортає словник {"reply_markup": …} у додаткові параметри виклику. Звіряй себе з готовим файлом: diff /opt/club/kurs/05/bot.py bot.py — мають відрізнятися лише твої власні тексти й команди із занять 02–03. Потім:
python3 try_bot.py "/help"
python3 try_bot.py "привіт"
ls -l ~/www/stats.htmlДовідка показує нові команди; привіт відповідає як зазвичай; файл stats.html існує й оновився щойно (час у ls -l). Помилка про contacts_extras з'явиться лише в живому боті, не в try_bot.py — її покаже крок 12.
nano project.yamlУ розділі commands заповни рядки (list уже є із заняття 04), а в розділі pages — рядок stats:
commands:
about: "/about"
list: "/users" # заняття 04
create: "/add" # заняття 05
update: "/edit"
delete: "/del"
pages:
index: "index.html"
stats: "stats.html" # заняття 05Решту рядків не чіпай. Автоперевірка бере звідси назви твоїх команд і сторінки. Перевір формат і перезапусти бота:
python3 -c "import yaml; d = yaml.safe_load(open('project.yaml')); print(d['commands']['create'], d['pages']['stats'])"
systemctl --user restart club-bot
systemctl --user is-active club-bot
tail -n 5 ~/logs/club-bot.logМає бути /add stats.html, потім active, а в журналі — жодного рядка з Error. Якщо в Telegram бот відповідає «Ой, щось пішло не так», знайди причину: tail -n 30 ~/logs/club-bot.log | grep "помилка обробки".
Напиши боту /start — під полем вводу з’явилися кнопки /list, /add, /help. Потім:
/add — вона надішле порожню команду, і бот відповість підказкою з прикладом;/add Марко | з гуртка — бот повідомить номер;/list — Марко з’явився; біля рядка видно номер;/edit з його номером та note і новий текст, потім /del з номером. Натисни пальцем на команду в підказці бота — вона спрацює як кнопка;/del 999999 і подивись, що відповість бот.Без Telegram цей крок пропусти: усе, крім самих кнопок, ти вже перевірив через try_bot.py. Не з’явились кнопки — перезапусти службу (крок 11) і напиши щось ще раз: клавіатура приходить разом із відповіддю.
curl -s http://91.219.61.4/u/$USER/stats.html | head -n 14
curl -s -o /dev/null -w "%{http_code}\n" http://91.219.61.4/u/$USER/stats.htmlПерша команда показує початок сторінки: <!doctype html>, рядок viewport і заголовок. Друга друкує лише код відповіді nginx — 200 означає, що сторінку видно. Це відповідь веб-сервера на звичайне відкриття файлу; жодна твоя програма в цьому не бере участі.
Тепер відкрий http://91.219.61.4/u/<логін>/stats.html у браузері, а краще — на телефоні через мобільний інтернет: чотири картки з цифрами мають стояти по дві в ряд і читатися без збільшення. Напиши боту /add Тарас і онови сторінку: «людей у базі» стало на одиницю більше. Імен на сторінці немає — і не повинно бути.
Порожній стан ти зараз не побачиш — у таблиці є люди. Але він є в коді: знайди в stats_html() абзац із <p class="empty"> і прочитай, що там написано. Хороший порожній стан каже, що відбувається («тут порожньо») і що зробити («напиши боту /start»).
Сьогодні, крім звичного коміту, у репозиторію зміниться назва основної гілки. Досі вона звалася master — так її за звичкою називає git, коли створює новий репозиторій. GitHub і більшість нових проєктів уже кілька років називають основну гілку main, і далі в курсі всі команди будуть саме з main. Назва — лише ярлик на останньому коміті: історія, коміти й файли від перейменування не змінюються.
Одне застереження: сторінку stats.html Git не збереже. Вона лежить у ~/www, а репозиторій — це тека ~/app. І це правильно: сторінку створює бот, і вона відновиться першим же повідомленням, а в репозиторії має лежати лише код.
cd ~/app
git status --short
git branch --show-currentgit status --short має показати змінені M bot.py і M project.yaml та новий файл ?? contacts.py — його ти змінив у кроках 2–7, а на занятті 04 він ще не був у коміті, якщо ти його не додавав. (M — modified, «змінено»). Якщо ти ставив нову версію db.py у кроці 1, буде й він. Файлів .env чи .db у списку бути не повинно; якщо вони там є — нічого не відправляй і поклич викладача. Друга команда, найімовірніше, надрукує master. Тоді перейменуй гілку:
git branch -m master main
git branch-m — від move, «перемістити»: так git перейменовує гілку. Команда нічого не друкує, а git branch покаже * main (зірочка — гілка, на якій ти стоїш) і нижче proba з заняття 03. Якщо git branch --show-current одразу сказав main, перейменовувати нічого не треба — переходь до коміту.
git add -A
git commit -m "Заняття 05: адмінка в Telegram — /add, /edit, /del, кнопки і сторінка статистики"
git push origin main
git push -u github maingit commit відповість рядком [main …] Заняття 05: … і підсумком на зразок 3 files changed. Перший git push оновлює ~/repo.git на сервері, другий — GitHub; обидва закінчуються рядком * [new branch] main -> main: для віддалених репозиторіїв main — нова гілка. GitHub ще допише кілька рядків remote: Create a pull request for 'main'… — це пропозиція, яка нам не потрібна, пропускай її.
Параметр -u (від upstream, «основний віддалений») запам’ятовує: гілка main за замовчуванням ходить на github. Останній рядок відповіді це підтверджує: branch 'main' set up to track 'github/main'. Відтепер на GitHub досить писати просто git push, а копію на сервері так само оновлює git push origin main.
error: src refspec main does not match any — гілку не перейменовано: git branch досі показує * master. Permission denied (publickey) на другому push — GitHub не впізнав ключ: ssh -T git@github.com має привітати тебе на ім’я (налаштовано на занятті 02).
Тепер у ~/repo.git і на GitHub лежать обидві гілки, і головною там досі вважається master. Спершу сервер. У bare-репозиторії головну гілку задає файл HEAD — ось як його переписати й видалити стару гілку:
git -C ~/repo.git symbolic-ref HEAD refs/heads/main
git push origin --delete mastergit -C ~/repo.git — «виконай команду в теці ~/repo.git», нікуди не переходячи. Перша команда нічого не друкує; друга відповідає - [deleted] master. Тепер GitHub — там головну гілку міняють на сторінці налаштувань:
club-app на GitHub і вгорі натисни Settings;master натисни кнопку з двома стрілками (Switch to another branch);main, натисни Update, а в попередженні — I understand, update the default branch.Цю команду роби лише після пунктів 1–3 у браузері (інакше GitHub відмовить: «refusing to delete the current branch»).
git push github --delete master
git ls-remote --heads origin
git ls-remote --heads github
git statusПерша знову відповідає - [deleted] master. git ls-remote --heads показує, які гілки лежать у віддаленому репозиторії: в обох має бути рядок … refs/heads/main (і … refs/heads/proba, якщо ти відправляв цю гілку на занятті 03), а master — ніде. git status напише On branch main, Your branch is up to date with 'github/main'. і nothing to commit, working tree clean — усе збережено. Онови сторінку репозиторію на GitHub: у перемикачі гілок стоїть main, а останній коміт — «Заняття 05: адмінка в Telegram…».
! [remote rejected] master (refusing to delete the current branch: refs/heads/master) — GitHub не дає видалити головну гілку: спершу зміни Default branch на main (пункти 1–3), потім повтори команду. error: unable to delete 'master': remote ref does not exist — гілки master там уже немає, і це нормально, якщо в тебе від початку була main.
Сьогодні автоперевірка розмовляє з твоїм ботом так само, як ти через try_bot.py у кроках 8–9: імпортує bot.py і надсилає йому повідомлення від імені Автоперевірка. Всі команди вона бере з project.yaml — commands.create, list, update, delete і pages.stats, — сама нічого не вгадує, тож описка в маніфесті дає червоний пункт навіть тоді, коли бот працює. Усе відбувається на копії твоєї бази й з підставною сторінкою статистики — справжні контакти й справжній stats.html вона не змінює. Служба, Telegram і мережа не потрібні.
L05.1 спершу додає контакт «Контроль05» (якщо /add ще нічого не додає, пункт червоний), а потім надсилає /add без імені, з пробілами й з приміткою, але без імені та вимагає у відповідь пояснення, а не слово «Додано», і ніяких нових рядків у базі. L05.2 надсилає /del 999999, /edit 999999 note x (у відповіді має бути цей номер) і /del абв (будь-яке зрозуміле пояснення, не «не знаю такої команди»); у базі нічого не змінюється. L05.3 проходить цикл: додає контакт, шукає його у відповіді на /list, змінює примітку й ім’я, видаляє й переконується, що інших контактів не зачеплено. L05.4 додає контакт з вигаданим іменем, читає сторінку з STATS_FILE і перевіряє: вона є, у ній немає імені контакту й імені «Автоперевірка», є viewport, є кількість людей у базі, а після ще одного /add число змінилося; також є справжній файл ~/www/stats.html на сервері.
L05.5 оцінює викладач: відкриє бота й сторінку на телефоні, подивиться, чи зрозуміло, що робити, чи пояснено порожній стан, і попросить додати, змінити й видалити контакт. Натисни «Перевірити практичну частину» у розділі «Перевірка виконаних завдань» нижче на сторінці.
| Код | Що перевіряється | Якщо не зараховано |
|---|---|---|
| L05.1 | порожнє ім'я: пояснення, а не запис і не помилка автоперевірка | Команда додавання (commands.create, напр. "/add") має вміти додати контакт, а на порожнє ім'я («/add», «/add | примітка») відповісти поясненням і нічого не записати. Перевір, що перед записом стоїть перевірка name, а відповідь не починається зі слова «Додано». |
| L05.2 | неіснуючий номер: зрозуміла відповідь, а не помилка автоперевірка | /del 999999 і /edit 999999 note x мають відповісти, що контакту з номером 999999 немає (у відповіді має бути цей номер), а /del абв — що номер має бути числом. Перевір changed(): якщо він повернув 0 — такого запису немає; і не лишай необроблених винятків. |
| L05.3 | додати → видно в списку → змінити → видалити автоперевірка | Повний цикл командами не пройшов. Заповни в project.yaml commands.list, create, update, delete і перевір кожну команду вручну: python3 try_bot.py "/add Оля | подруга", потім /list, /edit номер note текст, /del номер. |
| L05.4 | бот сам переписує статичну сторінку stats.html автоперевірка | Після /add бот має переписати сторінку (pages.stats у project.yaml, напр. stats.html у ~/www): цифри змінюються, імен немає, є <meta name="viewport">. Адресу файла бере stats_path() зі змінної STATS_FILE; write_stats() викликають і remember(), і /add, /edit, /del. |
| L05.5 | команди й кнопки зручні: порожній стан пояснено, сторінка статистики читається на телефоні викладач | Позначку ставить викладач на занятті або після нього. |
Кожне питання оцінюється відразу в браузері — це лишається так само. Ця кнопка лише надсилає підсумок учителю, щоб відповіді не зникли разом із закритою вкладкою.
Список критеріїв і що саме вони означають — у таблиці вище, «Що перевіряє автоматика». Тут — та сама перевірка, але прямо зараз, без очікування на найближчий прохід за розкладом.
/find. Напиши в contacts.py пошук: /find Ол показує контакти, у яких ім’я містить «Ол». Підказка: query("SELECT * FROM users WHERE name LIKE ?", ("%" + text + "%",)) — знаки % у LIKE означають «будь-що», а сам шаблон іде через ?, як усі значення. Не знайшов нікого — бот каже «Нікого не знайдено: …». Додай команду в HANDLERS, у HELP і перевір через try_bot.py.README.md розділ ## Адмінка: які є команди й що вони роблять, як бот відповідає на помилки (наведи по одному прикладу) і чому керування поки відкрите всім. Три-п’ять речень своїми словами. Розділи ## Що це і ## Як запустити не чіпай — їх і далі перевіряють.stats_html() заголовок, колір або підписи карток і перевір: python3 try_bot.py "привіт", потім відкрий сторінку на телефоні. Лишай лише цифри: жодних імен.add(), edit() і delete(). Зокрема: чому в delete() стоїть changed() і query(), а не лише execute(); чому f-рядок у edit() тут безпечний, а в SQL загалом — ні; що буде, якщо прибрати перевірку _id.git add -A, git commit -m "Заняття 05: розділ про адмінку в README", git push origin main і git push — тепер без слів, бо гілка main пам’ятає дорогу на GitHub.cd ~/app
python3 try_bot.py "/list"
python3 -c "import contacts"
grep -n "contacts" bot.py
tail -n 30 ~/logs/club-bot.log | grep "помилка"Не знаю такої команди — слова немає в HANDLERS у contacts.py або у bot.py немає виклику contacts.command(text, sender) (заняття 04, крок 13). Назва в словнику має бути з косою рискою й малими літерами: "/add";SyntaxError або IndentationError — після python3 -c "import contacts" дивись номер рядка в повідомленні; найчастіше загублена дужка чи лапка у вставленому коді;NameError: name 'write_stats' is not defined — не додано блок сторінки статистики (крок 7);try_bot.py працює — служба не перезапущена: systemctl --user restart club-bot.cd ~/app
python3 try_bot.py "/add Контроль | перевірка"
python3 try_bot.py "/add"
python3 try_bot.py "/add | примітка без імені"
sqlite3 ~/data/app.db "SELECT id, name FROM users WHERE TRIM(name) = ''"/add працював. Почни з кроку 4;_name_error(name) перед записом або вона не зупиняє функцію: після неї має бути return problem;/add не підключено (див. вище);sqlite3 ~/data/app.db "DELETE FROM users WHERE TRIM(name) = ''" і виправ перевірку.cd ~/app
python3 try_bot.py "/del 999999"
python3 try_bot.py "/edit 999999 note щось"
python3 try_bot.py "/del абв"999999 («Контакту з номером 999999 немає») — перевірка шукає його в тексті. Якщо відповідь інша, подивись _no_such(): у ній має бути {num};Traceback на /del абв — перевірка _id() не викликається перед DELETE;/edit 999999 note щось бот відповідає «Змінено» — не перевіряється результат changed(): умова if not changed(…) має повертати _no_such(num);delete: чи update: у project.yaml — впиши "/del" і "/edit" (крок 11).Повідомлення перевірки каже, на якому кроці вона зупинилась. Повтори цикл руками й порівняй:
cd ~/app
python3 try_bot.py "/add Цикл | примітка1"
python3 try_bot.py "/list" | head -n 3
sqlite3 ~/data/app.db "SELECT id, name, note FROM users WHERE name = 'Цикл'"
python3 try_bot.py "/edit 8 note змінено"
python3 try_bot.py "/del 8"8 підстав номер із /add;/list — у запиті списку є WHERE або LIMIT (заняття 04); список має віддавати всіх, найновіші вгорі;edit() забуто WHERE id = ? або порядок значень у (value, num) не збігається з порядком знаків ?;/del контакт лишився — перевір запит DELETE FROM users WHERE id = ? і чи передано (num,) з комою;/del видалила не лише потрібний контакт — у DELETE загубилась умова WHERE. Одразу перевір, чи не зникли твої контакти: sqlite3 ~/data/app.db "SELECT COUNT(*) FROM users".cd ~/app
python3 try_bot.py "/add Сторінка | перевірка"
ls -l ~/www/stats.html
grep -c "viewport" ~/www/stats.html
grep -c "Сторінка" ~/www/stats.html
grep -n "stats:" project.yamlls не бачить файлу — write_stats() не викликається (див. remember() і command()) або падає: запусти python3 -c "import contacts; contacts.write_stats()" і читай повідомлення, воно друкується в журнал;STATS_FILE — у твоєму коді шлях написано прямо; бери його з stats_path();grep -c "Сторінка" дає більше за нуль — на сторінку потрапили імена. У stats_html() лишайся лише з цифрами з stats_numbers();grep -c "viewport" дає нуль — немає рядка <meta name="viewport" …>;/add число не змінилось — command() не викликає write_stats() (CHANGING);stats: у project.yaml — впиши stats: "stats.html".Тестовий контакт «Сторінка» видали: /del з його номером.
poll_forever() немає **contacts_extras() або функцію не додано; перевір grep -n "contacts_extras" bot.py: рядків має бути два (виклик і def). Перезапусти службу;TypeError: tg() got an unexpected keyword argument — зайві зірочки: у виклику має бути рівно **contacts_extras();/add, а бот відповідає прикладом — так і має бути: кнопка надсилає порожню команду.fatal: no branch named 'master' після git branch -m master main — гілки master у тебе немає. git branch --show-current: якщо там main, усе вже гаразд, переходь до коміту;error: src refspec main does not match any — гілку ще не перейменовано, а push просить надіслати main. git branch -m master main і повтори;! [rejected] main -> main (fetch first) — на GitHub гілка main уже є, і в ній коміти, яких у тебе немає (скажімо, хтось редагував файл просто на сайті GitHub). Не пиши --force: він стер би ті коміти. Поклич викладача — разом подивитеся на них командами git fetch github і git log --oneline main..github/main;git push без слів відповідає fatal: The upstream branch of your current branch does not match the name of your current branch (або The current branch main has no upstream branch) — у кроці 17 пропущено -u, і гілка досі пам’ятає стару дорогу на github/master з заняття 02. Виконай git push -u github main один раз, далі працюватиме просто git push.