← До списку уроків
Заняття 04 з 13База користувачів бота
курс 0% виконано0%
Місяць 1 · Заняття 04

База користувачів бота

Замість текстового файлу — таблиця в SQLite: бот сам записує, хто йому писав, коли й скільки разів, а список цих людей бот показує за командою /users.

90 хвилин теорія 25 хв практика 55 хв GitHub і перевірка 10 хв

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

Досі твій бот нічого не пам’ятає: відповів — і забув, хто писав. Сьогодні в нього з’явиться база даних — файл ~/data/app.db з таблицею users. Кожне повідомлення бот записуватиме туди сам: хто написав (номер чату, ім’я й нік у Telegram), коли вперше, коли востаннє і скільки разів. Оля пише вдруге — бот не заводить нову Олю, а додає одиницю до її лічильника.

Ти створиш таблицю однією командою, навчишся заглядати в неї програмою sqlite3 і писати запити мовою SQL, додаси в bot.py функцію remember(), а ще новий файл contacts.py з командою /users: напишеш її боту — і він відповість списком людей із бази. Наприкінці покажеш три речі: у базі щонайменше п’ятеро людей; ще одне повідомлення боту — і лічильник у рядку цієї людини виріс; у відповіді на /users рівно стільки людей, скільки рядків у таблиці.

Якщо в тебе немає TelegramСьогоднішнє заняття майже повністю робиться без нього: «людей» ти змоделюєш через try_bot.py, і автоперевірка теж пише боту саме так. Пропусти лише розмову з живим ботом у кроці 10.

Теорія

1. Чому бот не може вести облік у текстовому файлі

Найпростіша ідея — файл users.txt, по рядку на людину: 123456789;Оля;7 — номер чату, ім’я, скільки повідомлень. Для п’ятьох однокласників це навіть працюватиме. А тепер подивись, що доведеться робити боту на кожне повідомлення.

  1. Знайти людину. Програма читає файл від першого рядка, розрізає кожен на частини й порівнює номер. Тисяча людей — тисяча розрізань на кожне повідомлення.
  2. Змінити одне число. Оля написала увосьме: 7 треба замінити на 8. Переписати шматок посеред файлу не можна — особливо коли 9 стає 10 і рядок довшає, — тож програма переписує весь файл. Уб’ють процес посеред запису (автоперевірка на занятті 02 саме так і робила) — від файлу лишиться половина.
  3. Двоє одночасно. До твоїх даних ходять кілька програм: потік бота всередині служби, try_bot.py, який ти запускаєш руками, автоперевірка, а сьогодні ще й команда /users, що читає список. Дві програми прочитали файл в одну мить, кожна додала одиницю й записала свою версію — і та, що записала пізніше, стерла зміну першої. Одне повідомлення зникло з обліку, і ніхто про це не дізнався.
  4. Жодних правил. У файл можна записати людину без імені, ім’я з крапкою з комою всередині (і зламати розрізання) чи слово «сім» замість числа. Помітиш через тиждень, коли підсумки не зійдуться.

База даних — програма, яка зберігає дані в таблицях і відповідає на запити до них. На кожну з чотирьох бід у неї є відповідь:

Ми беремо SQLite («ес-кю-лайт») — базу, яка тримає все в одному файлі й не потребує окремої служби: її вміє відкривати сам Python. Твій файл — ~/data/app.db, поруч із bot_state.json із заняття 02. Заглядати в нього руками ти будеш програмою sqlite3, яка вже стоїть на сервері. Не плутай: sqlite3 у терміналі — програма для людини, а import sqlite3 у Python — модуль для програми. Відкривають вони той самий файл.

Бот рахує повідомлення у файлі users.txt: читає весь файл, додає одиницю потрібній людині й записує файл заново. Оля написала боту рівно в ту мить, коли ти запустив try_bot.py від її імені. Що, найімовірніше, станеться з її лічильником?

2. Як людина потрапляє в базу: від повідомлення до рядка таблиці

Коли Оля вперше пише боту, база про неї нічого не знає. Ось що відбувається — крок за кроком, від повідомлення до рядка:

1. Оля пише боту «привіт»
2. Telegram віддає боту оновлення: у ньому chat.id = 123456789 і ім'я «Оля»
3. Бот питає базу: «чи є вже рядок з chat_id = 123456789?»
4. Немає  → бот додає новий рядок: (123456789, "Оля", перший візит = зараз, повідомлень = 1)
   Вже є  → бот лише оновлює рядок: повідомлень = повідомлень + 1, останній візит = зараз
5. Бот відповідає Олі, а в таблиці users тепер видно, хто писав, коли й скільки разів

Тут є важлива ідея: унікальний ідентифікатор людини. Ім'я для цього не годиться — двох Оль може бути багато, а ім'я можна змінити. Тому людину впізнають за chat_id — числом, яке Telegram видає кожному акаунту і яке ніколи не повторюється. У таблиці на цьому стовпці стоїть правило UNIQUE (розділ 4): другої людини з таким самим номером не з’явиться. Саме за ним база знаходить «свою» людину. Чого бот не зберігає: паролів і того, що людина не просила запам'ятати (докладніше — у розділі про те, що бот має право пам'ятати).

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

3. Таблиця users: рядки, стовпці, типи

Дані в базі лежать у таблицях. Одна таблиця — один вид речей; у нас це люди, які писали боту, і таблиця зветься 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
);
СтовпецьТипЩо в ньому
idINTEGERНомер рядка в таблиці: 1, 2, 3… Ставить сама база
chat_idINTEGERНомер чату в Telegram — те саме "id", що в from і chat оновлення із заняття 02
nameTEXTІм’я, яке людина сама вказала в Telegram (first_name)
usernameTEXTНік без @; порожній рядок, якщо ніка немає
noteTEXTТвоя примітка про людину; спершу порожня
first_seenTEXTКоли людина написала вперше
last_seenTEXTКоли написала востаннє
messagesINTEGERСкільки повідомлень надіслала

Назви стовпців — латиницею, малими літерами, через підкреслення: вони стоять у кожному запиті й у коді. І в кожного рядка ті самі стовпці, що й у решти: окремого поля «улюблений колір» лише для Олі в таблиці не буває.

Тип даних — вид значення в стовпці. Нам потрібні два: 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 можна запускати скільки завгодно разів: люди, які вже є в таблиці, нікуди не дінуться.

4. Ключ і правила: PRIMARY KEY, NOT NULL, UNIQUE

Оль у класі буває дві. Щоб база розрізняла рядки навіть тоді, коли всі інші поля однакові, у таблиці є первинний ключ (PRIMARY KEY) — стовпець, значення якого не повторюється й ніколи не буває порожнім. У нас це id. Тип INTEGER PRIMARY KEY має в SQLite зручну властивість: номер нового рядка база ставить сама, тож у запиті на додавання id не передають. Ключ не змінюється за все життя рядка: Оля змінить ім’я в Telegram — name зміниться, а id лишиться.

Порожнє поле в базі — окреме значення NULL, «нічого не записано». Воно не дорівнює ні нулю, ні порожньому рядку: messages = 0 — відомий факт «жодного повідомлення», а NULL — «невідомо». Правила для стовпців:

Чому ключ — окремий id, а не chat_id, адже номер чату й так унікальний? Бо він буде не в кожного рядка. На наступному занятті в таблицю можна буде додати контакт руками — людину, яка боту ще не писала й номера чату не має; про це й коментар у схемі: «порожнє, якщо контакт додано вручну». Первинний ключ мусить бути в кожного рядка, тому ним служить id, а chat_id лише UNIQUE.

Правила перевіряє не твій код, який можна забути чи зламати, а сама база — щоразу, хоч би хто писав: бот, try_bot.py чи ти руками.

Уяви, що в remember() стоїть звичайний INSERT без ON CONFLICT. Оля пише боту вперше — рядок додано. Що станеться, коли вона напише вдруге?

5. SQL: чотири команди й «додай або онови»

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;

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))

Потрійні лапки """…""" — рядок 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()?

6. Знак питання замість склеювання: захист від SQL-ін’єкції

Чому в 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 для роботи з базою є три функції, і всі приймають запит і окремо параметри:

Усі три відкривають базу через connect(), а шлях беруть із DB_PATH: ~/data/ плюс ім’я з database.file у project.yaml. Задано змінну оточення APP_DB — шлях береться з неї: так пізніше тести працюватимуть на окремій базі, не чіпаючи справжньої. А with connect() as conn: — це транзакція з розділу 1: усе вийшло — зміни записано, щось упало — скасовано.

Твій друг «спростив» свою remember(): замість знаків питання вклеїв значення f-рядком — VALUES({chat_id}, '{name}', …). Його боту пише однокласниця, записана в Telegram як Мар'яна. Що станеться?

7. Чому для цього SQL, а що таке NoSQL — на прикладах

SQL-база (SQLite, PostgreSQL, MySQL) — це таблиці з чітко заданими стовпцями: у кожного рядка однаковий набір полів і типів, а між таблицями можна задавати зв’язки (наприклад, «у цього користувача три замовлення»). Для списку людей, що пишуть боту, це ідеально: усі мають chat_id, ім'я й лічильник, а мова SQL дозволяє одним запитом дізнатися «хто писав найбільше» чи «скільки людей писало сьогодні». База сама стежить за правилами (NOT NULL, UNIQUE) і не дає записати сміття. SQLite ще й живе просто в одному файлі — на нашому спільному сервері це найпростіше й найекономніше.

NoSQL («не тільки SQL») — назва для баз, які зберігають дані не таблицями. Замість жорстких стовпців — гнучкі записи, де в кожного може бути свій набір полів. Є кілька видів — ось приклади з життя:

ВидЯк зберігаєПриклад базиДе доречно
документикожен запис — як JSON-«картка» з власними полямиMongoDBпрофілі, товари, де в кожного різні характеристики
ключ → значенняпросто «ключ: значення», як словник у PythonRedisшвидка пам’ять для того, що часто потрібне (кеш), відомості про те, хто зараз увійшов (сесії), лічильники
графвузли й зв'язки між нимиNeo4j«друзі друзів» у соцмережі, маршрути
стовпцевадані по стовпцях на величезних обсягахCassandraжурнали й аналітика мільярдів подій

Приклад: у соцмережі твоє ім'я, пароль і дата реєстрації лежать у SQL-таблиці — це суворі, важливі дані. А стрічка новин, де в кожного допису різні поля (текст, фото, відео, опитування) — зручніше в документній NoSQL-базі. А «хто зараз онлайн» тримають у Redis, бо це треба читати тисячі разів на секунду. Одна велика програма часто користується кількома базами одразу.

Коротка відповідь, що вибрати: немає «кращого» — є «доречніший». Для нашого бота дані прості й однакові (людина, ім'я, лічильник), важлива точність, і треба вміти робити підрахунки, тому ми беремо SQL. NoSQL варто згадати, коли дані дуже різні за формою, коли їх надзвичайно багато або коли потрібна швидкість понад усе.

Бот зберігає рядок «chat_id, ім'я, кількість повідомлень» для кожної людини, а ти хочеш швидко порахувати, хто написав найбільше. Яка база підходить найкраще?

8. Що бот має право пам’ятати про людей

Усе, за чим можна впізнати конкретну людину, — ім’я, нік, номер чату, коли й скільки вона писала, — це персональні дані. Їх захищають закони: в Україні — закон «Про захист персональних даних», у Європейському Союзі — закон 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.

1 Зайти на сервер і перевірити, що бот у порядку

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.

2 Оновити db.py

db.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.py

diff друкує лише рядки, що відрізняються: з < — як у твоєму файлі, з > — як у новому. Найімовірніше, побачиш приблизно таке:

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

3 Записати базу в паспорт проєкту

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: []

Відступ — два пробіли, без 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: зсунуто вправо або в ньому описка.

4 Скопіювати 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/ і, якщо файлу там немає, скажи викладачеві.

5 Створити таблицю й закрити файл бази

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.

6 Заглянути в базу програмою sqlite3

sqlite3 ~/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 не скаже «не знайдено», а мовчки працюватиме з новою порожньою базою — і ти дивуватимешся, куди зникла таблиця.

7 Спробувати порушити правила таблиці

Перевіримо, що база справді себе захищає. Спробуй додати людину без імені й без часу:

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 одним шматком; тексти всередині запиту — в одинарних.

8 Навчити бота запам’ятовувати: 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 "привіт"

Має бути привітання з новим реченням і звичайне відлуння. Що означають помилки (читай останній рядок):

Заплутався — порівняй свій файл із готовим: diff /opt/club/kurs/04/bot.py bot.py. Мають відрізнятися лише твої власні тексти й команди із занять 02–03. Якщо серед відмінностей є рядки з remember() — шукай описку там.

9 Подивитися, як росте лічильник

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() не просто додає, а «додає або оновлює».

10 Перезапустити службу й написати живому боту

Поки що 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 цей крок пропусти — людей ти додаси в наступному.

11 Зібрати в базі п’ятьох людей

Попроси трьох-чотирьох однокласників знайти твого бота за ніком і написати йому /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 "Марта Ю.".

12 Додати примітку командою 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, тож у примітках його не став (у != він безпечний: після нього стоїть =).

13 Додати команду /users

Список людей бот показуватиме у відповідь на команду. Код цієї команди — окремий файл contacts.py: так bot.py не розростається, а наступного заняття ти додаси туди ще команди. Скопіюй файл і прочитай його:

cp /opt/club/kurs/04/contacts.py ~/app/
less contacts.py

Чому тут немає сторінки в браузеріБот не відкриває жодного порту й нікого не «слухає» постійно: він сам, раз на кілька секунд, питає 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 або в іншій функції.

14 Порівняти список із базою

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: поки що цю команду бачить кожен, хто напише боту.

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

Код бота й таблиці — в Git і на GitHub. А сама база — ні: це дані, а не код, вона змінюється з кожним повідомленням і в ній імена однокласників.

15 Переконатися, що база не поїде в Git

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 — нічого не відправляй і дивись розділ «Якщо щось не працює».

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

git add -A
git commit -m "Заняття 04: бот записує користувачів у базу, команда /users"
git push origin master
git push github master

git 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»
викладач
Позначку ставить викладач на занятті або після нього.

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

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

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

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

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

  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 майже ніколи не буває порожнім (підказка: що відбувається з кожним повідомленням ще до перевірки команд?).
  2. Три питання до своєї бази. У sqlite3 напиши запити, які відповідають: хто з людей написав боту найбільше (ORDER BY і LIMIT 1); скільки людей писали сьогодні (WHERE last_seen >= '2026-10-21' — з сьогоднішньою датою); у кого є нік (WHERE username != ''). Запиши запити й відповіді в чернетку — на початку наступного заняття викладач попросить виконати один із них.
  3. Розділ про базу в README. Допиши в README.md розділ ## База даних: яка там таблиця, які в ній поля, що бот про людей не зберігає і чому. Три-п’ять речень своїми словами. Розділи ## Що це і ## Як запустити не чіпай — їх і далі перевіряють.
  4. Поясни remember(). Підготуйся пояснити будь-який її рядок: що таке excluded і чим воно відрізняється від users, чому не змінюється first_seen, навіщо ? замість f-рядка і що станеться, якщо прибрати з таблиці UNIQUE.
  5. Збережи. git add -A, git commit -m "Заняття 04: команда /me і розділ про базу в README", git push origin master і git push github master.

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

Бот відповідає «Ой, щось пішло не так», а try_bot.py пише no such table: users

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 "помилка обробки"

Після виправлення перевір python3 try_bot.py "привіт" і зроби systemctl --user restart club-bot, щоб і живий бот узяв нову версію.

sqlite3 показує порожню базу, хоча init_db.py сказав «таблиці готові»

Майже завжди це інший файл: команду набрано з коротким шляхом, наприклад 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.

L04.3 не зараховано: лічильник не росте

Повтори те, що робить автоперевірка, і подивись, чи зріс лічильник:

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

/users не працює або дає не те число (L04.5)

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

Перша команда — відповідь бота, друга — скільки рядків у таблиці, третя й четверта — підключення команди й рядок маніфесту.

Термінал або sqlite3 чогось чекають і не відповідають

database is locked

Базу зайняла інша програма й не відпускала довше за десять секунд. На нашому сервері так буває, коли в іншому вікні термінала лишилася відкрита розмова 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).