Перед уроком
Обери свою групу й себе зі списку. Тоді відповіді й перевірки завдань запишуться саме на тебе.
Замість текстового файлу — таблиця в SQLite: бот сам записує, хто йому писав, коли й скільки разів, а список цих людей бот показує за командою /users.
Замість <логін> підстав свій — той, який тобі видали на першому занятті. Логін має вигляд bohdan.g9: ім’я, крапка, номер групи. Кабінет — сторінка, де видно, які пункти автоперевірки тобі вже зараховано.
Досі твій бот нічого не пам’ятає: відповів — і забув, хто писав. Сьогодні в нього з’явиться база даних — файл ~/data/app.db з таблицею users. Кожне повідомлення бот записуватиме туди сам: хто написав (номер чату, ім’я й нік у Telegram), коли вперше, коли востаннє і скільки разів. Оля пише вдруге — бот не заводить нову Олю, а додає одиницю до її лічильника.
Ти створиш таблицю однією командою, навчишся заглядати в неї програмою sqlite3 і писати запити мовою SQL, додаси в bot.py функцію remember(), а ще новий файл contacts.py з командою /users: напишеш її боту — і він відповість списком людей із бази. Наприкінці покажеш три речі: у базі щонайменше п’ятеро людей; ще одне повідомлення боту — і лічильник у рядку цієї людини виріс; у відповіді на /users рівно стільки людей, скільки рядків у таблиці.
Якщо в тебе немає TelegramСьогоднішнє заняття майже повністю робиться без нього: «людей» ти змоделюєш через try_bot.py, і автоперевірка теж пише боту саме так. Пропусти лише розмову з живим ботом у кроці 10.
Найпростіша ідея — файл users.txt, по рядку на людину: 123456789;Оля;7 — номер чату, ім’я, скільки повідомлень. Для п’ятьох однокласників це навіть працюватиме. А тепер подивись, що доведеться робити боту на кожне повідомлення.
7 треба замінити на 8. Переписати шматок посеред файлу не можна — особливо коли 9 стає 10 і рядок довшає, — тож програма переписує весь файл. Уб’ють процес посеред запису (автоперевірка на занятті 02 саме так і робила) — від файлу лишиться половина.try_bot.py, який ти запускаєш руками, автоперевірка, а сьогодні ще й команда /users, що читає список. Дві програми прочитали файл в одну мить, кожна додала одиницю й записала свою версію — і та, що записала пізніше, стерла зміну першої. Одне повідомлення зникло з обліку, і ніхто про це не дізнався.База даних — програма, яка зберігає дані в таблицях і відповідає на запити до них. На кожну з чотирьох бід у неї є відповідь:
db.py чекання обмежено десятьма секундами (timeout=10); не дочекалася — помилка database is locked, а не тихо зіпсовані дані;Ми беремо SQLite («ес-кю-лайт») — базу, яка тримає все в одному файлі й не потребує окремої служби: її вміє відкривати сам Python. Твій файл — ~/data/app.db, поруч із bot_state.json із заняття 02. Заглядати в нього руками ти будеш програмою sqlite3, яка вже стоїть на сервері. Не плутай: sqlite3 у терміналі — програма для людини, а import sqlite3 у Python — модуль для програми. Відкривають вони той самий файл.
Бот рахує повідомлення у файлі users.txt: читає весь файл, додає одиницю потрібній людині й записує файл заново. Оля написала боту рівно в ту мить, коли ти запустив try_bot.py від її імені. Що, найімовірніше, станеться з її лічильником?
Коли Оля вперше пише боту, база про неї нічого не знає. Ось що відбувається — крок за кроком, від повідомлення до рядка:
1. Оля пише боту «привіт»
2. Telegram віддає боту оновлення: у ньому chat.id = 123456789 і ім'я «Оля»
3. Бот питає базу: «чи є вже рядок з chat_id = 123456789?»
4. Немає → бот додає новий рядок: (123456789, "Оля", перший візит = зараз, повідомлень = 1)
Вже є → бот лише оновлює рядок: повідомлень = повідомлень + 1, останній візит = зараз
5. Бот відповідає Олі, а в таблиці users тепер видно, хто писав, коли й скільки разівТут є важлива ідея: унікальний ідентифікатор людини. Ім'я для цього не годиться — двох Оль може бути багато, а ім'я можна змінити. Тому людину впізнають за chat_id — числом, яке Telegram видає кожному акаунту і яке ніколи не повторюється. У таблиці на цьому стовпці стоїть правило UNIQUE (розділ 4): другої людини з таким самим номером не з’явиться. Саме за ним база знаходить «свою» людину. Чого бот не зберігає: паролів і того, що людина не просила запам'ятати (докладніше — у розділі про те, що бот має право пам'ятати).
Той самий принцип працює в усіх сайтах і застосунках: коли ти заходиш у соцмережу, вона шукає у своїй базі рядок із твоїм ідентифікатором — і або знаходить, і показує твій профіль, або створює новий.
Дані в базі лежать у таблицях. Одна таблиця — один вид речей; у нас це люди, які писали боту, і таблиця зветься users («користувачі»). Далі три слова будуть майже в кожному реченні:
Ось схема нашої таблиці. Вона лежить у файлі init_db.py, який ти сьогодні скопіюєш. Рядки, що починаються з --, — коментарі SQL, як # у Python:
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY,
chat_id INTEGER UNIQUE, -- порожнє, якщо контакт додано вручну
name TEXT NOT NULL,
username TEXT NOT NULL DEFAULT '',
note TEXT NOT NULL DEFAULT '',
first_seen TEXT NOT NULL,
last_seen TEXT NOT NULL,
messages INTEGER NOT NULL DEFAULT 0
);| Стовпець | Тип | Що в ньому |
|---|---|---|
id | INTEGER | Номер рядка в таблиці: 1, 2, 3… Ставить сама база |
chat_id | INTEGER | Номер чату в Telegram — те саме "id", що в from і chat оновлення із заняття 02 |
name | TEXT | Ім’я, яке людина сама вказала в Telegram (first_name) |
username | TEXT | Нік без @; порожній рядок, якщо ніка немає |
note | TEXT | Твоя примітка про людину; спершу порожня |
first_seen | TEXT | Коли людина написала вперше |
last_seen | TEXT | Коли написала востаннє |
messages | INTEGER | Скільки повідомлень надіслала |
Назви стовпців — латиницею, малими літерами, через підкреслення: вони стоять у кожному запиті й у коді. І в кожного рядка ті самі стовпці, що й у решти: окремого поля «улюблений колір» лише для Олі в таблиці не буває.
Тип даних — вид значення в стовпці. Нам потрібні два: INTEGER — ціле число і TEXT — текст будь-якої довжини (є ще REAL для дробових чисел і BLOB для файлів). Лічильник — число, бо до нього додають одиницю: запиши «сім» текстом — і додати вже не вийде. Окремого типу для дати й часу в SQLite немає, тому їх записують текстом, але в строгому порядку: рік-місяць-день, буква T, години:хвилини:секунди. Функція now() у твоєму bot.py дає саме такий запис:
2026-10-21T15:04:27Чому саме так? Такий текст порівнюється й сортується так само, як час: зліва найстарший розряд — рік, далі місяць, день, година. "2026-10-21T09:05" більше за "2026-10-09T23:59" і як текст, і як дата. Тому ORDER BY last_seen DESC — «упорядкуй від найсвіжішого» — працює без жодних перетворень. А в записі 21.10.2026 першим стоїть день, і 21 жовтня опинилося б поруч із 21 березня.
CREATE TABLE IF NOT EXISTS означає «створи таблицю, якщо її ще немає». Є — команда нічого не робить і помилки не видає. Тому init_db.py можна запускати скільки завгодно разів: люди, які вже є в таблиці, нікуди не дінуться.
Оль у класі буває дві. Щоб база розрізняла рядки навіть тоді, коли всі інші поля однакові, у таблиці є первинний ключ (PRIMARY KEY) — стовпець, значення якого не повторюється й ніколи не буває порожнім. У нас це id. Тип INTEGER PRIMARY KEY має в SQLite зручну властивість: номер нового рядка база ставить сама, тож у запиті на додавання id не передають. Ключ не змінюється за все життя рядка: Оля змінить ім’я в Telegram — name зміниться, а id лишиться.
Порожнє поле в базі — окреме значення NULL, «нічого не записано». Воно не дорівнює ні нулю, ні порожньому рядку: messages = 0 — відомий факт «жодного повідомлення», а NULL — «невідомо». Правила для стовпців:
name, first_seen, last_seen: людина без імені й без часу в обліку не має сенсу. Спробуєш додати такий рядок — база відмовить з помилкою NOT NULL constraint failed: users.name;chat_id: одна людина в Telegram — один рядок у таблиці. Другий рядок з тим самим номером база не прийме: UNIQUE constraint failed: users.chat_id. Від первинного ключа відрізняється тим, що порожнє значення дозволене, і навіть у кількох рядках;DEFAULT '' — порожній рядок, DEFAULT 0 — нуль. Тому username і note ніколи не бувають NULL, і програмі не треба окремо перевіряти «а раптом там нічого».Чому ключ — окремий id, а не chat_id, адже номер чату й так унікальний? Бо він буде не в кожного рядка. На наступному занятті в таблицю можна буде додати контакт руками — людину, яка боту ще не писала й номера чату не має; про це й коментар у схемі: «порожнє, якщо контакт додано вручну». Первинний ключ мусить бути в кожного рядка, тому ним служить id, а chat_id лише UNIQUE.
Правила перевіряє не твій код, який можна забути чи зламати, а сама база — щоразу, хоч би хто писав: бот, try_bot.py чи ти руками.
Уяви, що в remember() стоїть звичайний INSERT без ON CONFLICT. Оля пише боту вперше — рядок додано. Що станеться, коли вона напише вдруге?
SQL («ес-кю-ел») — мова запитів до бази. Ти описуєш, що хочеш отримати, а як дістати це з файлу, вирішує база. Ключові слова пишуть великими літерами, щоб їх було видно серед назв стовпців; база великих і малих літер у них не розрізняє. Запит закінчується крапкою з комою. Текст у запиті береться в одинарні лапки — 'Оля', числа пишуть без лапок. Першу команду, CREATE TABLE, ти вже бачив у розділі 2. Ще три:
INSERT — додати рядок. Спершу перелік стовпців, потім значення в тому самому порядку:
INSERT INTO users(chat_id, name, first_seen, last_seen)
VALUES(555, 'Оля', '2026-10-21T15:04:27', '2026-10-21T15:04:27');id не вказано — база поставить сама; username, note і messages не вказано — підставить значення з DEFAULT.
SELECT — дістати рядки:
SELECT * FROM users;
SELECT name, messages FROM users WHERE messages >= 5;
SELECT * FROM users ORDER BY last_seen DESC;
SELECT name FROM users ORDER BY messages DESC LIMIT 3;
SELECT COUNT(*) FROM users;* — усі стовпці; замість зірочки можна перелічити потрібні через кому;FROM users — з якої таблиці;WHERE — умова: у відповідь потраплять лише рядки, для яких вона правдива. Порівнюють знаками =, !=, <, >, <=, >=;ORDER BY … DESC — упорядкувати від більшого до меншого; без DESC — від меншого до більшого;LIMIT 3 — не більше трьох рядків. Зручно для «трійки найактивніших», але в запиті списку йому не місце: список має віддавати всіх;COUNT(*) — не самі рядки, а їхня кількість: відповідь з одного числа.UPDATE — змінити рядки, які вже є:
UPDATE users SET note = 'сидить зі мною за партою' WHERE chat_id = 555;Читається так: «онови таблицю users: у полі note хай буде такий текст — там, де chat_id дорівнює 555». Без WHERE зміниться кожен рядок таблиці, і база нічого не перепитає. Тому спершу продумай WHERE, а вже потім — що міняти.
Тепер головне для бота. На кожне повідомлення йому треба: людини ще немає — додати, уже є — оновити час і лічильник. Можна спершу спитати базу через SELECT, а потім вирішити, — але це два запити, зайвий код, і між ними може втрутитися інша програма. SQLite вміє все це одним запитом: INSERT … ON CONFLICT … DO UPDATE — «спробуй додати, а якщо вийде конфлікт із правилом UNIQUE, онови рядок, який уже є». Ось функція remember(), яку ти сьогодні додаси в bot.py:
# ── заняття 04: база користувачів бота ──────────────────────────────
def remember(sender):
"""Записує або оновлює того, хто написав боту. Зберігаємо мінімум:
chat_id, ім'я, нік, коли вперше й востаннє писав і скільки повідомлень."""
from db import execute
if not sender.get("id"):
return
t = now()
execute("""INSERT INTO users(chat_id, name, username, first_seen, last_seen, messages)
VALUES(?, ?, ?, ?, ?, 1)
ON CONFLICT(chat_id) DO UPDATE SET
name=excluded.name, username=excluded.username,
last_seen=excluded.last_seen, messages=users.messages + 1""",
(sender["id"], sender.get("first_name") or "без імені",
sender.get("username") or "", t, t))sender — словник from з оновлення Telegram: {"id": 123456789, "first_name": "Оля", "username": "olia_k", …};from db import execute — бере з db.py функцію, що виконує запит і зберігає зміни. Стоїть усередині remember(), тож db.py підключається, лише коли бот справді щось записує;if not sender.get("id"): return — запобіжник: немає номера відправника — нема кого записувати. Telegram і try_bot.py номер передають завжди;t = now() — один час для двох полів: у нового рядка first_seen і last_seen однакові. П’ять знаків ? у VALUES — місця для п’яти значень, що лежать окремо, у дужках унизу, у тому самому порядку: номер, ім’я, нік, час, час. Шосте, 1, вписане прямо в запит — це перше повідомлення людини. Чому знаки питання — у наступному розділі;ON CONFLICT(chat_id) DO UPDATE SET … — що робити, коли такий chat_id уже є. excluded — рядок, який ми намагалися додати, а users — той, що вже лежить у таблиці. Тож ім’я, нік і час останнього повідомлення беремо нові, а лічильник — старий плюс один;first_seen у переліку після SET немає, тому він не змінюється ніколи. Як і note: твою примітку бот не зітре;sender.get("first_name") or "без імені" — ім’я з Telegram, а якщо його чомусь немає — запасне слово, бо name у нас NOT NULL.Потрійні лапки """…""" — рядок Python на кілька рядків: увесь запит — один текст, який Python передає базі як є. І ще: ON CONFLICT(chat_id) спрацьовує лише тому, що на chat_id стоїть UNIQUE. Без цього правила базі нема з чим «конфліктувати», і вона відповість помилкою ON CONFLICT clause does not match any PRIMARY KEY or UNIQUE constraint.
Як користуватися ШІ-помічникомШІ добре пояснює SQL: «розбери цей запит по словах», «чим excluded відрізняється від users». Можна попросити й написати запит — але кожен спершу виконай сам у sqlite3 і подивись, що він повернув. Особливо UPDATE: без WHERE він змінить усю таблицю. І ніколи не вставляй у чат вміст своєї бази: там імена й ніки однокласників, а це не твої дані.
Оля вперше написала боту 1 жовтня, а сьогодні, 21 жовтня, надіслала третє повідомлення. Що буде в її рядку після remember()?
Чому в remember() стоять знаки питання, а не f-рядок на зразок f"… VALUES({chat_id}, '{name}', …)"? Згадай, звідки береться name: це ім’я, яке людина сама вписала у свій профіль Telegram. Твоя програма його не вибирає й не перевіряє. Хай однокласниця записана як Мар'яна — зі звичайним прямим апострофом. Склеєний запит вийшов би таким:
INSERT INTO users(chat_id, name, …) VALUES(777, 'Мар'яна', …)Апостроф у імені закрив лапки зарано: для бази текст закінчився на 'Мар', а далі стоїть незрозуміле яна'. Python отримає помилку sqlite3.OperationalError: near "яна": syntax error, і бот замість привітання напише «Ой, щось пішло не так». Це ще безневинний випадок. Якщо дані вміють закривати лапки, ними можна дописати в запит чужі SQL-команди: показати рядки, яких не мали показувати, змінити їх чи стерти. Така атака зветься SQL-ін’єкцією («впорскуванням»), і роками вона була однією з найчастіших причин витоків даних із сайтів.
Захист простий і надійний: значення ніколи не вклеюють у текст запиту. На місці кожного стоїть ? — параметр, а самі значення йдуть окремо, другим аргументом:
execute("UPDATE users SET note = ? WHERE chat_id = ?", ("сидить поруч", 555))База спершу розбирає текст запиту — у ньому немає жодного символу від користувача, — а значення підставляє вже потім, як дані, а не як команду. Хоч би що людина вписала в ім’я, воно ляже в поле name буква в букву, разом з апострофами й крапками з комою. Звідси правило на весь курс: текст запиту пишеш ти, дані приходять через ?. На занятті 08 ти перевірятимеш на міцність свій сервіс — і це правило там знадобиться.
У db.py для роботи з базою є три функції, і всі приймають запит і окремо параметри:
query(sql, params) — для SELECT: повертає список словників, по одному на рядок, де ключі — назви стовпців: [{"id": 1, "name": "Оля", …}, …];execute(sql, params) — для INSERT і UPDATE: виконує запит, зберігає зміни й повертає id доданого рядка;changed(sql, params) — теж змінює, але повертає, скільки рядків змінилося (0 — такого запису немає). Вона з’явилася в новій версії db.py і знадобиться на наступному занятті.Усі три відкривають базу через connect(), а шлях беруть із DB_PATH: ~/data/ плюс ім’я з database.file у project.yaml. Задано змінну оточення APP_DB — шлях береться з неї: так пізніше тести працюватимуть на окремій базі, не чіпаючи справжньої. А with connect() as conn: — це транзакція з розділу 1: усе вийшло — зміни записано, щось упало — скасовано.
Твій друг «спростив» свою remember(): замість знаків питання вклеїв значення f-рядком — VALUES({chat_id}, '{name}', …). Його боту пише однокласниця, записана в Telegram як Мар'яна. Що станеться?
SQL-база (SQLite, PostgreSQL, MySQL) — це таблиці з чітко заданими стовпцями: у кожного рядка однаковий набір полів і типів, а між таблицями можна задавати зв’язки (наприклад, «у цього користувача три замовлення»). Для списку людей, що пишуть боту, це ідеально: усі мають chat_id, ім'я й лічильник, а мова SQL дозволяє одним запитом дізнатися «хто писав найбільше» чи «скільки людей писало сьогодні». База сама стежить за правилами (NOT NULL, UNIQUE) і не дає записати сміття. SQLite ще й живе просто в одному файлі — на нашому спільному сервері це найпростіше й найекономніше.
NoSQL («не тільки SQL») — назва для баз, які зберігають дані не таблицями. Замість жорстких стовпців — гнучкі записи, де в кожного може бути свій набір полів. Є кілька видів — ось приклади з життя:
| Вид | Як зберігає | Приклад бази | Де доречно |
|---|---|---|---|
| документи | кожен запис — як JSON-«картка» з власними полями | MongoDB | профілі, товари, де в кожного різні характеристики |
| ключ → значення | просто «ключ: значення», як словник у Python | Redis | швидка пам’ять для того, що часто потрібне (кеш), відомості про те, хто зараз увійшов (сесії), лічильники |
| граф | вузли й зв'язки між ними | Neo4j | «друзі друзів» у соцмережі, маршрути |
| стовпцева | дані по стовпцях на величезних обсягах | Cassandra | журнали й аналітика мільярдів подій |
Приклад: у соцмережі твоє ім'я, пароль і дата реєстрації лежать у SQL-таблиці — це суворі, важливі дані. А стрічка новин, де в кожного допису різні поля (текст, фото, відео, опитування) — зручніше в документній NoSQL-базі. А «хто зараз онлайн» тримають у Redis, бо це треба читати тисячі разів на секунду. Одна велика програма часто користується кількома базами одразу.
Коротка відповідь, що вибрати: немає «кращого» — є «доречніший». Для нашого бота дані прості й однакові (людина, ім'я, лічильник), важлива точність, і треба вміти робити підрахунки, тому ми беремо SQL. NoSQL варто згадати, коли дані дуже різні за формою, коли їх надзвичайно багато або коли потрібна швидкість понад усе.
Бот зберігає рядок «chat_id, ім'я, кількість повідомлень» для кожної людини, а ти хочеш швидко порахувати, хто написав найбільше. Яка база підходить найкраще?
Усе, за чим можна впізнати конкретну людину, — ім’я, нік, номер чату, коли й скільки вона писала, — це персональні дані. Їх захищають закони: в Україні — закон «Про захист персональних даних», у Європейському Союзі — закон GDPR (загальний регламент про захист даних). Головні вимоги там схожі: збирай лише те, без чого справді не обійтися, кажи людям, що збираєш, і бережи зібране.
Нашому боту для обліку досить кількох речей: номер чату (щоб упізнати людину вдруге), ім’я й нік (щоб ти розумів, хто це), час першого й останнього повідомлення та лічильник. Ось чого в таблиці немає і бути не повинно:
Друга вимога — прямота. Тому сьогодні в привітанні на /start з’явиться речення Я запам’ятовую твоє ім’я, нік, час повідомлень і скільки їх було. Людина, яка пише твоєму боту, має знати, що її записано.
Третя — берегти. І тут поки є слабке місце, про яке треба знати: команда /users, яку ти сьогодні зробиш, відповідає кожному, хто знайде твого бота й напише її. Імена й ніки всіх, хто писав, побачить будь-хто. Зараз так навмисно, лише для навчання: простіше побачити, як рядки з бази стають відповіддю бота. На занятті 07 ти залишиш список лише собі. А до того — дві домовленості: однокласники пишуть твоєму боту, знаючи, що їхнє ім’я з’явиться у списку, а нік свого бота ти не розсилаєш нікому поза групою.
І про chat_id: це не пароль, як токен, — знаючи номер, чужа людина не зайде у твій Telegram. Але номер у людини один для всіх ботів, тож за ним її можна впізнати в різних базах. Тому це теж персональні дані.
Ти хочеш, щоб бот «краще знав» людей. Що з цього варто додати в таблицю users?
| Слово | Що означає |
|---|---|
| база даних | Програма, що зберігає дані в таблицях і відповідає на запити до них |
SQLite, sqlite3 | База в одному файлі, без окремої служби; програма в терміналі, якою в неї заглядають |
~/data/app.db | Файл твоєї бази на сервері |
| таблиця, рядок, стовпець | Набір записів одного виду; один запис; одна характеристика кожного запису |
| схема | Опис таблиць: стовпці, їхні типи й правила |
| тип даних | Вид значення в стовпці: INTEGER — ціле число, TEXT — текст |
| NULL | «Нічого не записано»; не нуль і не порожній рядок |
| PRIMARY KEY | Первинний ключ: номер рядка, що не повторюється й не буває порожнім (у нас id) |
| NOT NULL, UNIQUE, DEFAULT | Правила: не може бути порожнім; не повторюється; що підставити, якщо значення не дали |
| SQL | Мова запитів до бази |
CREATE TABLE IF NOT EXISTS | Створює таблицю, якщо її ще немає |
INSERT, SELECT, UPDATE | Додати рядок; дістати рядки; змінити рядки, які вже є |
WHERE, ORDER BY, LIMIT, COUNT(*) | Умова відбору; порядок; не більше стількох рядків; кількість рядків |
ON CONFLICT … DO UPDATE, excluded | «Додай, а якщо такий уже є — онови»; рядок, який намагалися додати |
| транзакція | Група змін, що виконується вся або ніяк |
| блокування | Черга на запис: друга програма чекає, доки допише перша |
| індекс | Службовий довідник бази, щоб знайти рядок, не переглядаючи всіх |
параметр ? | Місце для значення в запиті; саме значення передають окремо |
| SQL-ін’єкція | Атака, коли дані вклеюються в запит і стають його частиною |
| персональні дані | Усе, за чим можна впізнати конкретну людину |
remember() | Функція бота, що записує нового відправника або оновлює того, хто вже є |
init_db.py | Скрипт, що створює таблиці; його безпечно запускати повторно |
query, execute, changed | Функції db.py: прочитати рядки; записати; записати й дізнатися, скільки рядків змінилося |
/users | Команда бота: відповідає списком усіх людей із таблиці users |
contacts.py | Файл із командами бота, що працюють з контактами; сьогодні в ньому одна — /users |
Виконуй кроки по черзі: кожен спирається на попередній. Де в команді є $USER, нічого не підставляй — ця змінна вже містить твій логін. А <логін> разом із кутовими дужками заміни на свій, наприклад bohdan.g9. Усі команди, якщо не сказано інше, виконуються з теки ~/app.
ssh <логін>@91.219.61.4
cd ~/app
git status
git branch --show-current
systemctl --user is-active club-bot
python3 try_bot.py "/start"Має бути по черзі: nothing to commit, working tree clean — усе з минулого заняття в комітах; master — основна гілка; active — служба працює; і твоє привітання із занять 02–03.
modified: … — лишилися незбережені зміни: потрібні — закоміть, не потрібні — git restore назва_файла. Сьогодні ти правитимеш bot.py і project.yaml, і забуті зміни в них тільки заплутають;proba замість master — git switch master; inactive — systemctl --user start club-bot;try_bot.py друкує помилку — спершу полагодь бота (розділ «Якщо щось не працює» заняття 02 або 03): сьогодні ти будуєш поверх нього.db.pydb.py — готова заготовка для роботи з базою, яка лежить у тебе із самого початку. Її не редагують, лише читають. Для бота потрібна нова версія — подивись спершу, чим вона відрізняється від твоєї, а потім заміни:
diff db.py /opt/club/kurs/04/db.py
cp /opt/club/kurs/04/db.py ~/app/db.py
grep -n "APP_DB\|def " db.pydiff друкує лише рядки, що відрізняються: з < — як у твоєму файлі, з > — як у новому. Найімовірніше, побачиш приблизно таке:
4a5,6
> APP_DB у змінних оточення підставляє іншу базу — так тести (заняття 09)
> не чіпають справжню.
5a8
> import os
13c16
< DB_PATH = Path.home() / "data" / CFG["database"]["file"]
---
> DB_PATH = Path(os.environ.get("APP_DB") or Path.home() / "data" / CFG["database"]["file"])
17c20
< conn = sqlite3.connect(DB_PATH)
---
> conn = sqlite3.connect(DB_PATH, timeout=10)
38a42,49
…
> def changed(sql, params=()):
…13c16 — адреса: «рядок 13 першого файлу змінено (c) на рядок 16 другого», a — «додано». Нове — три речі з теорії: змінна APP_DB, чекання до десяти секунд, якщо базу зайняла інша програма, і функція changed(). Не надрукувалося нічого — у тебе вже нова версія; скопіювати однаково не завадить. cp мовчить, а grep показує шість рядків: опис з APP_DB, DB_PATH = … і чотири функції — connect, query, execute, changed. Прочитати файл цілком — less db.py (вийти — q).
nano project.yamlЗнайди розділ database — він є в маніфесті з першого дня, але порожній. Зроби його таким:
database: # заняття 04
file: app.db
main_table: users
main_table_pk: id
main_table_required: [name, first_seen, last_seen]
secondary_tables: []file — ім’я файлу бази в теці ~/data; лиши app.db, його відкривають і db.py, і автоперевірка;main_table — головна таблиця, записи якої показує список: звідси автоперевірка дізнається, що перевіряти; main_table_pk — стовпець первинного ключа;main_table_required — обов’язкові поля з NOT NULL, у квадратних дужках через кому; secondary_tables — інші таблиці, поки їх немає.Відступ — два пробіли, без Tab. Збережи (Ctrl+O, Enter), вийди (Ctrl+X) і перевір:
python3 -c "import yaml; print(yaml.safe_load(open('project.yaml'))['database'])"Має надрукуватися {'file': 'app.db', 'main_table': 'users', 'main_table_pk': 'id', 'main_table_required': ['name', 'first_seen', 'last_seen'], 'secondary_tables': []}. ScannerError чи ParserError — зламано формат: після слова line стоїть номер рядка, дивись туди й рядком вище. KeyError: 'database' — рядок database: зсунуто вправо або в ньому описка.
init_db.py і прочитати йогоcp /opt/club/kurs/04/init_db.py ~/app/
less init_db.pyФайл короткий: from db import DB_PATH, connect — шлях до бази й функція, що її відкриває; SCHEMA = """…""" — рядок Python, а в ньому SQL зі схемою з теорії; у main() рядок DB_PATH.parent.mkdir(parents=True, exist_ok=True) створює теку ~/data, якщо її немає, а conn.executescript(SCHEMA) виконує весь SQL за раз. Останні два рядки, if __name__ == "__main__": main(), означають «запусти main(), лише якщо файл запустили напряму»: програма, яка підключить init_db.py через import, сама вирішить, коли створювати таблиці. Вийти — q. No such file or directory після cp — подивись ls /opt/club/kurs/04/ і, якщо файлу там немає, скажи викладачеві.
python3 init_db.py
python3 init_db.py
chmod 600 ~/data/app.db
ls -l ~/dataОбидва запуски друкують таблиці готові: /home/bohdan.g9/data/app.db (з твоїм логіном); другий нічого не зламав — так працює IF NOT EXISTS. ls -l покаже app.db розміром 12288 байтів — база одразу відводить місце під таблицю й службовий індекс для chat_id — і, якщо бот опитує Telegram, bot_state.json із заняття 02.
Рядок app.db має починатися з -rw-------: базу читаєш і змінюєш лише ти, а служба працює від твого імені, тож їй цього досить. Скоро там будуть імена однокласників; тека ~/data і так закрита від сусідів, але це другий замок — як .env на занятті 02. Побачив -rw-r--r-- — ти пропустив chmod.
ModuleNotFoundError: No module named 'db' — ти не в ~/app: виконай cd ~/app. KeyError: 'database' — повернись до кроку 3.
sqlite3sqlite3 ~/data/app.db ".tables"
sqlite3 ~/data/app.db ".schema users"
sqlite3 ~/data/app.db "PRAGMA table_info(users)"Після sqlite3 іде файл бази, потім — що зробити. Команди з крапкою, .tables і .schema, належать самій програмі sqlite3, це не SQL. Перша друкує список таблиць: users (інші таблиці від давніх спроб, якщо є, не заважають). Друга показує, як база запам’ятала опис: той самий CREATE TABLE users (…);, що в init_db.py, з коментарем, але без IF NOT EXISTS — ці слова потрібні лише під час створення.
Третя — службовий запит SQLite, що описує стовпці. Значення в рядку розділені рискою |:
0|id|INTEGER|0||1
1|chat_id|INTEGER|0||0
2|name|TEXT|1||0
3|username|TEXT|1|''|0
4|note|TEXT|1|''|0
5|first_seen|TEXT|1||0
6|last_seen|TEXT|1||0
7|messages|INTEGER|1|0|0По порядку: номер стовпця, назва, тип, NOT NULL (1 — так), значення за замовчуванням і первинний ключ (1 — так). Подивись на два стовпці: одиниця в останній — лише в id, а в четвертій — у всіх полів, крім id і chat_id. Саме це читає автоперевірка L04.2. У id там 0, і це правильно: INTEGER PRIMARY KEY порожнім не буває й без окремого правила — номер ставить база.
Завжди повний шляхПиши sqlite3 ~/data/app.db, а не sqlite3 app.db. Якщо файлу за вказаним шляхом немає, sqlite3 не скаже «не знайдено», а мовчки працюватиме з новою порожньою базою — і ти дивуватимешся, куди зникла таблиця.
Перевіримо, що база справді себе захищає. Спробуй додати людину без імені й без часу:
sqlite3 ~/data/app.db "INSERT INTO users(chat_id) VALUES(5)"
sqlite3 ~/data/app.db "SELECT COUNT(*) FROM users"Перша команда закінчиться помилкою, у якій є слова NOT NULL constraint failed: users.name (на початку може стояти Error: або Runtime error — залежить від версії програми). База назвала перше поле, яке ти не заповнив. Друга друкує 0: рядка не додано, таблиця порожня, як і була. Запит SQL ми беремо в подвійні лапки, щоб термінал передав його sqlite3 одним шматком; тексти всередині запиту — в одинарних.
remember()nano bot.pyТри зміни. Перша — нова функція. Знайди кінець функції handle_update() (її останній рядок — відлуння, щось на зразок return f"Ти написав: {text}") і рядок, що йде за ним, — заголовок # ── доставка: long polling ──…. Між ними, відступивши по два порожні рядки зверху й знизу, встав функцію remember() з розділу 4 теорії — від рядка-заголовка # ── заняття 04: база користувачів бота ──… до sender.get("username") or "", t, t)). Набирати можна самому, але в цьому запиті легко загубити кому чи лапку, тож надійніше вивести готовий блок і скопіювати мишею:
sed -n '/^# ── заняття 04/,/username") or ""/p' /opt/club/kurs/04/bot.pyЦю команду виконай до nano або в другому вікні термінала: вона друкує рівно потрібний блок, п’ятнадцять рядків. Виділи їх, скопіюй і встав у nano правою кнопкою миші. Але скопіювати — не значить зрозуміти: кожен рядок цієї функції ти маєш уміти пояснити, як у теорії.
Друга — виклик. У handle_update() одразу після перевірки на порожній текст додай один рядок із відступом у чотири пробіли:
if not text:
return None
remember(sender) # заняття 04
name = sender.get("first_name") or "друже"Місце не випадкове. Вище — лише «порожнє повідомлення», тож стікери й фото бот не рахує. Нижче — усі команди з їхніми return: якби виклик стояв у кінці функції, /start, /help і /about повертали б відповідь раніше, і такі повідомлення не потрапили б у лічильник.
Третя — відкритість. У відповідь на /start перед «Напиши /help.» додай речення про те, що бот запам’ятовує. Якщо привітання в тебе своє, із заняття 03, — свого тексту не стирай, просто додай ще один шматок у лапках. Пробіл між реченнями стоїть усередині лапок:
if text.startswith("/start"):
return (f"Привіт, {name}! Я бот проєкту «{PRODUCT}». "
"Я допомагаю зібратися в похід. "
"Я запам’ятовую твоє ім’я, нік, час повідомлень і скільки їх було. Напиши /help.")Збережи, вийди й перевір:
python3 try_bot.py "/start"
python3 try_bot.py "привіт"Має бути привітання з новим реченням і звичайне відлуння. Що означають помилки (читай останній рядок):
NameError: name 'remember' is not defined — функцію не вставлено або в її назві описка;sqlite3.OperationalError: no such table: users — бот звернувся до бази, а таблиці там немає: повтори крок 5;SyntaxError: unterminated triple-quoted string literal — загублено одну з потрійних лапок """, і Python вважає рядком усю решту файлу;IndentationError — відступи: у тілі функції по чотири пробіли, рядки запиту — як у зразку.Заплутався — порівняй свій файл із готовим: diff /opt/club/kurs/04/bot.py bot.py. Мають відрізнятися лише твої власні тексти й команди із занять 02–03. Якщо серед відмінностей є рядки з remember() — шукай описку там.
sqlite3 ~/data/app.db "SELECT * FROM users"Один рядок, приблизно такий: 1|1|Тест|||2026-10-21T15:12:03|2026-10-21T15:12:09|2. Це людина, від імені якої пише try_bot.py без --id і --name: номер чату 1, ім’я Тест, порожні нік і примітка, два часи й лічильник 2 — /start і «привіт» із кроку 8 (запускав try_bot.py частіше — число буде більше). Щоб читати було зручніше, попросимо підписати й вирівняти стовпці:
sqlite3 -header -column ~/data/app.db "SELECT id, chat_id, name, messages, first_seen, last_seen FROM users"
python3 try_bot.py "ще раз"
sqlite3 -header -column ~/data/app.db "SELECT id, chat_id, name, messages, first_seen, last_seen FROM users"-header додає рядок із назвами, -column вирівнює стовпці. Вийде приблизно так:
id chat_id name messages first_seen last_seen
-- ------- ---- -------- ------------------- -------------------
1 1 Тест 2 2026-10-21T15:12:03 2026-10-21T15:12:09Порівняй два виводи: після «ще раз» messages став 3, last_seen змінився, а first_seen — ні. Це й є ON CONFLICT … DO UPDATE у роботі. Час записано за годинником сервера; якщо він розходиться з твоїм на кілька годин, це часовий пояс сервера — команда date покаже, який він.
А тепер спробуй додати «двійника» — другу людину з тим самим номером чату:
sqlite3 ~/data/app.db "INSERT INTO users(chat_id, name, first_seen, last_seen) VALUES(1, 'Двійник', 'x', 'x')"Помилка зі словами UNIQUE constraint failed: users.chat_id — база не пустила другий рядок із номером 1. Саме тому remember() не просто додає, а «додає або оновлює».
Поки що remember() працює лише в try_bot.py: служба й досі виконує старий bot.py, прочитаний під час запуску. Перезапусти її:
systemctl --user restart club-bot
systemctl --user is-active club-botМає бути active (якщо activating — зачекай пару секунд і повтори). Тепер напиши своєму боту в Telegram /start — у привітанні з’явилося нове речення. Надішли ще два будь-які повідомлення й один стікер, а потім подивись у базу:
sqlite3 -header -column ~/data/app.db "SELECT id, chat_id, name, username, messages FROM users"Додався рядок із тобою: довгий номер чату на зразок 123456789, твоє ім’я з Telegram, нік без @ (порожньо, якщо ніка немає) і messages — 3: стікер не рахується, бо тексту в ньому немає. Це ті самі дані, що приходять у полі from кожного оновлення.
Бот відповідає «Ой, щось пішло не так» — remember() упала всередині служби. Причину покаже журнал: tail -n 30 ~/logs/club-bot.log | grep "помилка обробки", наприклад помилка обробки: OperationalError('no such table: users'). Без Telegram цей крок пропусти — людей ти додаси в наступному.
Попроси трьох-чотирьох однокласників знайти твого бота за ніком і написати йому /start. Спершу скажи їм, що бот запише їхнє ім’я й нік і що список поки відкритий. Хто не хоче — не мусить: людей можна замінити імітацією через try_bot.py:
python3 try_bot.py --id 2 --name Оля "привіт"
python3 try_bot.py --id 3 --name Тарас "/start"
python3 try_bot.py --id 2 --name Оля "як справи?"
python3 try_bot.py --id 4 --name Дмитро "привіт"
python3 try_bot.py --id 5 --name Соломія "/start"
python3 try_bot.py --id 6 --name "Марта Ю." "привіт"
sqlite3 -header -column ~/data/app.db "SELECT id, chat_id, name, messages, last_seen FROM users ORDER BY last_seen DESC"
sqlite3 ~/data/app.db "SELECT COUNT(*) FROM users"Кожна команда try_bot.py друкує відповідь бота. --id — номер чату: один номер — одна людина, тож друге повідомлення «Олі» не додало рядка, а зробило її лічильник 2. Різним людям — різні номери: 2, 3, 4… (у справжніх людей у Telegram номери значно довші, тож не збіжуться). Угорі таблиці — Оля, яка писала останньою; остання команда друкує, скільки всього людей.
Потрібно щонайменше п’ятеро людей, не рахуючи рядка Автоперевірка, який додає сама перевірка. Рядок Тест — це ти через try_bot.py, він рахується. Імена — правдоподібні (Оля, Тарас, Дмитро), а не тест1 чи аааа: на них дивитиметься викладач (L04.6). Ім’я з двох слів бери в лапки: --name "Марта Ю.".
UPDATEПоле note бот не заповнює — воно для тебе. Запиши примітку про «Олю» і переконайся, що бот її не зітре:
sqlite3 ~/data/app.db "UPDATE users SET note = 'сидить поруч, допомагає тестувати' WHERE chat_id = 2"
sqlite3 -header -column ~/data/app.db "SELECT name, messages, note FROM users WHERE chat_id = 2"
python3 try_bot.py --id 2 --name Оля "я знову тут"
sqlite3 -header -column ~/data/app.db "SELECT name, messages, note FROM users WHERE chat_id = 2"UPDATE нічого не друкує — так база каже, що все вдалося. Перший SELECT показує Олю з приміткою, другий — що лічильник виріс на одиницю, а примітка на місці: note немає в переліку після SET у remember(). Перевір себе: чи зміниться примітка, якщо Оля змінить ім’я в Telegram? Ні — зміниться лише name.
WHERE — завждиБез WHERE chat_id = 2 ця сама команда записала б примітку всім людям у таблиці. Ще дві пастки: апостроф усередині тексту в SQL пишуть двічі — 'сім''я', а знак оклику в подвійних лапках термінал сприймає як службовий і відповідає event not found, тож у примітках його не став (у != він безпечний: після нього стоїть =).
/usersСписок людей бот показуватиме у відповідь на команду. Код цієї команди — окремий файл contacts.py: так bot.py не розростається, а наступного заняття ти додаси туди ще команди. Скопіюй файл і прочитай його:
cp /opt/club/kurs/04/contacts.py ~/app/
less contacts.pyusers_text() — читає таблицю запитом SELECT * FROM users ORDER BY last_seen DESC, id DESC (свіжіші вгорі) і складає відповідь: перший рядок «Людей у базі: N», далі по рядку на людину. Ні WHERE, ні LIMIT: список віддає всіх, і саме це звіряє L04.5. Єдиний виняток — дуже довгий список: Telegram не приймає повідомлень понад 4096 символів, тому наприкінці стоїть «…і ще 12»;line(row) — один рядок списку: «3. Оля (@olia_k), повідомлень: 5, востаннє 2026-10-21 15:31». Номер на початку — id із таблиці, він знадобиться на наступному занятті;command(text, sender) — приймає текст повідомлення, дивиться, чи знає таку команду (HANDLERS), і повертає відповідь; якщо не знає, повертає None — «це не моє, питайте далі». А якщо база зайнята, людина отримає «База зараз недоступна», а не помилку з трасуванням.Чому тут немає сторінки в браузеріБот не відкриває жодного порту й нікого не «слухає» постійно: він сам, раз на кілька секунд, питає Telegram, чи є нові повідомлення, і відповідає на них. Окрема програма, що весь час чекає запитів, займає пам’ять, а на нашому спільному сервері її обмаль. Тому все, що ти показуєш людям, — це відповіді бота (а сторінки, які можна відкрити в браузері, — статичні файли, їх віддає nginx, і жодної програми для цього не треба).
Тепер підключи команду в bot.py. Відкрий його: nano bot.py. Знайди в handle_update() два рядки, що відповідають на невідому команду:
if text.startswith("/"):
return "Не знаю такої команди. Напиши /help."Між ними встав чотири нових рядки (відступи — по 8 пробілів усередині, як у зразку), щоб вийшло так:
if text.startswith("/"):
import contacts # заняття 04: /users
reply = contacts.command(text, sender)
if reply is not None:
return reply
return "Не знаю такої команди. Напиши /help."Читається так: «якщо це команда — спитай contacts.py; він відповів — віддай його відповідь; не відповів — тоді вже “не знаю такої команди”». Місце важливе: усі твої власні команди (/start, /help, /about…) стоять вище й спрацьовують раніше. Додай також рядок про команду в HELP ("/users — хто писав боту", не забудь \n та лапки на сусідніх рядках) і впиши команду в маніфест: nano project.yaml, розділ commands:
commands:
about: "/about"
list: "/users" # заняття 04Решту рядків commands не чіпай: вони для наступних занять. Звідси автоперевірка дізнається, як називається твоя команда зі списком. Перевір бота, а потім перезапусти службу:
python3 try_bot.py "/users"
systemctl --user restart club-bot
systemctl --user is-active club-botУ відповіді на /users — «Людей у базі: …» і рядки з людьми; службу видно як active. ModuleNotFoundError: No module named 'contacts' — файл не скопійовано або ти не в ~/app. IndentationError — не збігаються відступи у вставлених рядках. Не знаю такої команди — вставлені рядки стоять нижче за return або в іншій функції.
python3 try_bot.py "/users"
sqlite3 ~/data/app.db "SELECT COUNT(*) FROM users"Відповідь бота виглядає приблизно так (бот склав рядок зі стовпців таблиці):
Людей у базі: 6
1. Тест, повідомлень: 4, востаннє 2026-10-21 15:34
6. Марта Ю., повідомлень: 1, востаннє 2026-10-21 15:31
5. Соломія, повідомлень: 1, востаннє 2026-10-21 15:30
4. Дмитро, повідомлень: 1, востаннє 2026-10-21 15:29
3. Тарас, повідомлень: 1, востаннє 2026-10-21 15:28
2. Оля, повідомлень: 4, востаннє 2026-10-21 15:27Число в першому рядку відповіді й число, яке надрукувала друга команда, мають збігтися, а рядків у списку — стільки ж. Так робить і автоперевірка L04.5. Зверни увагу: сама команда /users теж лічиться повідомленням, тому «Тест» стоїть угорі, а його лічильник виріс на одиницю: бот записує людину до того, як розбирає команду (крок 8).
Різні числа — у SELECT є LIMIT чи WHERE, або в users_text() щось змінено. «Не знаю такої команди» — розділ «Якщо щось не працює». Якщо в тебе є Telegram, напиши своєму боту /users і подивись відповідь на телефоні: це ті самі рядки. Пам’ятай про домовленість із розділу 8: поки що цю команду бачить кожен, хто напише боту.
Код бота й таблиці — в Git і на GitHub. А сама база — ні: це дані, а не код, вона змінюється з кожним повідомленням і в ній імена однокласників.
cd ~/app
git status
git check-ignore -v app.dbУ git status серед modified мають бути bot.py, db.py і project.yaml, серед Untracked files — init_db.py і contacts.py. Файлу .db там немає: база лежить у ~/data, поза репозиторієм ~/app. А на випадок, якщо файл бази колись опиниться всередині ~/app (скажімо, після sqlite3 app.db), друга команда показує запобіжник: .gitignore:8:*.db app.db — «файл .gitignore, рядок 8, правило *.db»; номер рядка в тебе може бути інший. Якщо git status усе ж показує якийсь .db — нічого не відправляй і дивись розділ «Якщо щось не працює».
git add -A
git commit -m "Заняття 04: бот записує користувачів у базу, команда /users"
git push origin master
git push github mastergit commit відповість рядком [master …] Заняття 04: … і підсумком на зразок 5 files changed. Кожен git push закінчується стрілкою master -> master: перший оновлює ~/repo.git на сервері, другий — GitHub. Якщо в кроці 1 git branch --show-current надрукував main, пиши main замість master. Онови сторінку репозиторію на GitHub: там з’явився init_db.py, у bot.py — функція remember(), поруч — contacts.py, а файлу бази немає — і не має бути.
Сьогодні автоперевірка зазирає в базу й розмовляє з ботом. Спершу вона читає project.yaml: назву таблиці бере з database.main_table, назву команди зі списком — з commands.list. Сама вона нічого не вгадує, тож описка в маніфесті дає червоний пункт навіть тоді, коли бот працює. L04.1 шукає цю таблицю в ~/data/app.db, L04.2 читає її опис так само, як ти командою PRAGMA table_info у кроці 6: чи є первинний ключ і хоча б одне поле NOT NULL. L04.3 імпортує твій bot.py, як це робить try_bot.py, передає в handle_update() повідомлення «привіт від автоперевірки» від імені Автоперевірка з номером чату 1000000001 і дивиться, чи зріс у цьому рядку лічильник messages. Тож після першої перевірки в твоїй таблиці з’явиться рядок «Автоперевірка». Це нормально; видаляти його марно — він повернеться з наступною перевіркою.
L04.4 рахує рядки таблиці: їх має бути щонайменше п’ять. L04.5 надсилає боту твою команду зі списку (на копії бази — твої справжні дані вона не змінює), читає перший рядок відповіді «Людей у базі: N», рахує рядки з людьми й порівнює з COUNT(*) у таблиці — числа мають збігтися. L04.6 оцінює викладач: подивиться список і перевірить, чи там люди, а не «тест1» і «аааа». Натисни «Перевірити практичну частину» у розділі «Перевірка виконаних завдань» нижче на сторінці. Бот у цей момент може й не працювати: перевірка запускає твій код сама, без служби і без Telegram.
| Код | Що перевіряється | Якщо не зараховано |
|---|---|---|
| L04.1 | таблиця з project.yaml є в базі автоперевірка | У ~/data/app.db немає таблиці, названої в project.yaml (database.main_table). Запусти python3 init_db.py і звір назву. Перевір також, що в project.yaml стоїть database: main_table: users. |
| L04.2 | у таблиці є PRIMARY KEY і обов'язкові поля автоперевірка | У таблиці немає PRIMARY KEY або жодного поля NOT NULL. Подивись схему: sqlite3 ~/data/app.db ".schema users" |
| L04.3 | повідомлення боту записується в базу автоперевірка | Після повідомлення боту в таблиці не зріс лічильник messages для цього chat_id. Перевір, що handle_update() викликає remember() і що в таблиці є поле chat_id. |
| L04.4 | у базі щонайменше 5 людей автоперевірка | У таблиці менше за п'ять рядків. Попроси однокласників написати твоєму боту або змоделюй різних людей: python3 try_bot.py --id 2 --name Оля "привіт" |
| L04.5 | відповідь /users збігається з базою автоперевірка | Команда зі списку (commands.list у project.yaml, напр. "/users") не відповіла так, як у базі. Перша строка відповіді має бути «Людей у базі: N», далі по рядку на людину («номер. ім'я …»). Перевір, що contacts.py лежить у ~/app, що в bot.py є виклик contacts.command(), а в запиті немає LIMIT чи WHERE. |
| L04.6 | у базі справжні люди або правдоподібно змодельовані, а не «тест1» викладач | Позначку ставить викладач на занятті або після нього. |
Кожне питання оцінюється відразу в браузері — це лишається так само. Ця кнопка лише надсилає підсумок учителю, щоб відповіді не зникли разом із закритою вкладкою.
Список критеріїв і що саме вони означають — у таблиці вище, «Що перевіряє автоматика». Тут — та сама перевірка, але прямо зараз, без очікування на найближчий прохід за розкладом.
/me — «що ти про мене знаєш?». Хай бот показує людині її власний рядок. У handle_update() вище за if text.startswith("/"): додай:
if text.startswith("/me"):
from db import query
rows = query("SELECT messages, first_seen FROM users WHERE chat_id = ?",
(sender.get("id"),))
if not rows:
return "Я тебе ще не знаю."
return (f"Повідомлень від тебе: {rows[0]['messages']}. "
f"Уперше ти написав {rows[0]['first_seen'][:10]}.")?, як у remember(). Кома в (sender.get("id"),) обов’язкова: так Python розуміє, що це набір з одного значення, а не просто дужки. [:10] бере перші десять символів часу — саму дату. Додай рядок /me у HELP, перевір python3 try_bot.py --id 2 --name Оля "/me" і перезапусти службу. Поясни, чому rows майже ніколи не буває порожнім (підказка: що відбувається з кожним повідомленням ще до перевірки команд?).sqlite3 напиши запити, які відповідають: хто з людей написав боту найбільше (ORDER BY і LIMIT 1); скільки людей писали сьогодні (WHERE last_seen >= '2026-10-21' — з сьогоднішньою датою); у кого є нік (WHERE username != ''). Запиши запити й відповіді в чернетку — на початку наступного заняття викладач попросить виконати один із них.README.md розділ ## База даних: яка там таблиця, які в ній поля, що бот про людей не зберігає і чому. Три-п’ять речень своїми словами. Розділи ## Що це і ## Як запустити не чіпай — їх і далі перевіряють.remember(). Підготуйся пояснити будь-який її рядок: що таке excluded і чим воно відрізняється від users, чому не змінюється first_seen, навіщо ? замість f-рядка і що станеться, якщо прибрати з таблиці UNIQUE.git add -A, git commit -m "Заняття 04: команда /me і розділ про базу в README", git push origin master і git push github master.remember() звертається до таблиці, якої в базі немає. Подивись, що є насправді:
ls -l ~/data/app.db
sqlite3 ~/data/app.db ".tables"
grep -n "file:" ~/app/project.yaml
tail -n 30 ~/logs/club-bot.log | grep "помилка обробки".tables нічого не друкує — таблиць не створено: cd ~/app && python3 init_db.py;project.yaml не file: app.db — db.py відкриває інший файл. Поверни app.db, перезапусти службу;no such table: user, — описка в запиті remember(). Порівняй з готовим: diff /opt/club/kurs/04/bot.py ~/app/bot.py.Після виправлення перевір python3 try_bot.py "привіт" і зроби systemctl --user restart club-bot, щоб і живий бот узяв нову версію.
Майже завжди це інший файл: команду набрано з коротким шляхом, наприклад sqlite3 app.db у теці ~/app. Тоді sqlite3 відкриває нову, порожню базу поруч із кодом. Перевір:
ls -l ~/app/*.db ~/data/app.dbРядок cannot access '…/app/*.db' — добре: випадкових файлів бази в ~/app немає, і причину треба шукати в самій команді. Справжня база — лише ~/data/app.db. Якщо поруч є ~/app/app.db, він випадковий: видали саме його, уважно перевіривши шлях, — rm ~/app/app.db — і відтепер пиши повний шлях sqlite3 ~/data/app.db. У Git випадковий файл однаково не потрапив би: .gitignore не пускає туди *.db.
Повтори те, що робить автоперевірка, і подивись, чи зріс лічильник:
cd ~/app
python3 try_bot.py --id 1000000001 --name Автоперевірка "привіт"
sqlite3 ~/data/app.db "SELECT messages FROM users WHERE chat_id = 1000000001"
grep -n "remember" bot.py
grep -n "main_table:" project.yamlremember(sender) не викликається. grep має показати щонайменше два рядки: def remember(sender): і виклик remember(sender). Виклик має стояти одразу після if not text: return None, вище за всі команди (крок 8);main_table: не users — автоперевірка шукає лічильник в іншій таблиці (крок 3);ON CONFLICT clause does not match any PRIMARY KEY or UNIQUE constraint — описка в ON CONFLICT(chat_id) або таблицю колись створено іншою схемою, без UNIQUE. Подивись sqlite3 ~/data/app.db ".schema users". Якщо схема не така, як у init_db.py, а в таблиці поки лише змодельовані люди, створи її заново: sqlite3 ~/data/app.db "DROP TABLE users" (ця команда видаляє таблицю разом з усіма рядками), потім python3 init_db.py і знову п’ятеро людей із кроку 11.cd ~/app
python3 try_bot.py "/users"
sqlite3 ~/data/app.db "SELECT COUNT(*) FROM users"
grep -n "contacts" bot.py
grep -n "list:" project.yamlПерша команда — відповідь бота, друга — скільки рядків у таблиці, третя й четверта — підключення команди й рядок маніфесту.
Не знаю такої команди — бот не передав команду в contacts.py. grep має показати import contacts і contacts.command(text, sender) у handle_update(), причому вище за рядок return "Не знаю такої команди…" (крок 13);ModuleNotFoundError: No module named 'contacts' — файл не скопійовано: cp /opt/club/kurs/04/contacts.py ~/app/;База зараз недоступна — users_text() упала на запиті. Пробуй окремо: sqlite3 ~/data/app.db "SELECT * FROM users ORDER BY last_seen DESC, id DESC"; найімовірніше, таблиці немає (крок 5) або в запиті описка;LIMIT чи WHERE. Список має бути SELECT * FROM users ORDER BY last_seen DESC, id DESC — і нічого більше;project.yaml порожнє list: "" — автоперевірка не знає, яку команду надсилати: впиши list: "/users".> — термінал чекає закривної подвійної лапки: ти відкрив " і не закрив. Натисни Ctrl+C і набери команду знову;sqlite3 ~/data/app.db без запиту — відкрилася розмова з базою із запрошенням sqlite>. Тут теж можна писати запити, але кожен закінчується ;. Забув крапку з комою — програма показує ...> і чекає продовження: набери ; і Enter. Вийти — .quit або Ctrl+D;bash: !…: event not found — у подвійних лапках опинився знак оклику: термінал сприйняв його як свою команду. Прибери ! з тексту.Базу зайняла інша програма й не відпускала довше за десять секунд. На нашому сервері так буває, коли в іншому вікні термінала лишилася відкрита розмова sqlite3 з незакінченою зміною — наприклад, хтось набрав BEGIN;, потім UPDATE …;, а завершального COMMIT; не було. Поки зміну не завершено, база тримає чергу для всіх інших. Повернись у те вікно й набери COMMIT; (зберегти зміну) або .quit (вийти без неї). Потім повтори свою команду: бот і try_bot.py самі чекають на базу до десяти секунд, тож поодинока помилка мине. Якщо повторюється — systemctl --user restart club-bot і скажи викладачеві.
Наприклад, ти вводив --name тест1 або однаковим людям дав різні --id. Знайди номери зайвих рядків і видали їх по одному командою DELETE — вона прибирає рядки, які підходять під WHERE:
sqlite3 -header -column ~/data/app.db "SELECT id, chat_id, name, messages FROM users"
sqlite3 ~/data/app.db "DELETE FROM users WHERE id = 7"Замість 7 — id зайвого рядка. Як і з UPDATE: без WHERE зникне вся таблиця. Справжню людину видаляти марно — щойно вона напише боту знову, remember() додасть її як нову, з лічильником 1. Рядок Автоперевірка теж не видаляй: він повернеться з наступною перевіркою. Потім додай людей, яких бракує до п’ятьох (крок 11).