← До списку уроків
Заняття 02 з 13Бот, який не вимикається
курс 0% виконано0%
Місяць 1 · Заняття 02

Бот, який не вимикається

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

90 хвилин теорія 25 хв практика з ботом 45 хв GitHub і перевірка 20 хв

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

У тебе з’явиться власний Telegram-бот — акаунт у Telegram, якому відповідає не людина, а твоя програма. Ти зареєструєш його в @BotFather, отримаєш токен — пароль бота, — сховаєш токен у закритий файл на сервері й підключиш бота до своєї служби club-bot (служба — програма, яку сервер сам запускає й перезапускає; докладніше в розділі 6). Бот вітатиметься на /start, підказуватиме на /help, розповідатиме про твій проєкт на /about і повторюватиме все, що йому напишуть.

Головне — бот житиме на сервері, а не на твоєму комп’ютері. Наприкінці покажеш: друг пише боту й отримує відповідь, хоча ти вже вийшов із сервера; python3 try_bot.py "/start" друкує відповідь без Telegram; на GitHub лежить код бота — без токена.

Якщо в тебе немає TelegramTelegram — з 13 років і з номером телефона. Якщо цього немає або батьки не дозволили, усе, крім розмови з живим ботом, ти зробиш через try_bot.py. Скажи викладачеві на початку заняття: пункти, яким потрібен справжній токен (L02.11, L02.13, L02.15), він оцінить сам.

Теорія

1. Бот і Bot API: програма, яка розмовляє з Telegram

Бот — звичайний з вигляду контакт у Telegram, тільки відповідає на повідомлення програма. Живе вона не в телефоні, а на комп’ютері з інтернетом — у нас це сервер 91.219.61.4 — і розмовляє з Telegram через Bot API. API — набір адрес, за якими одна програма щось просить в іншої. Адреса Bot API складається з токена бота й методу — назви дії:

https://api.telegram.org/bot<токен>/<метод>

Сьогодні знадобляться три методи: getMe — «хто я?» (так перевіряють, чи справжній токен: це зробиш ти сам і автоперевірка), а також getUpdates — «чи написав мені хтось?» і sendMessage — «надішли цей текст у цей чат»; ці два викликає сам бот. Кожне звернення — звичайний HTTP-запит, як на занятті 01, тільки у відповідь приходить не сторінка, а дані у форматі JSON.

JSON — текстовий запис даних, який читає будь-яка мова програмування. Ось одне повідомлення від Telegram; таку порцію даних називають оновленням (update):

{
  "update_id": 815204311,
  "message": {
    "message_id": 17,
    "from": {"id": 123456789, "is_bot": false, "first_name": "Оля"},
    "chat": {"id": 123456789, "type": "private"},
    "date": 1792335600,
    "text": "/start"
  }
}

У фігурних дужках — пари «ключ: значення», ключ завжди в подвійних лапках. Значенням буває число, рядок у лапках, true чи false («так» і «ні»), список у квадратних дужках або ще одна пара фігурних — це вкладеність. Усередині message лежать from (хто написав), chat (куди відповідати) і text (що написали), а date — час як кількість секунд від 1 січня 1970 року.

У Python JSON стає словником: json.loads() робить словник із тексту, json.dumps() — текст зі словника. Вкладене дістають квадратними дужками рівень за рівнем: update["message"]["from"]["first_name"] дасть "Оля".

2. Порти, API і хто до кого звертається

На сервері одночасно працює багато програм. Деякі з них приймають запити з мережі, і щоб запити не плуталися, кожна така програма сідає на свій порт — число від 1 до 65535. Порт — як номер квартири в будинку: IP-адреса (91.219.61.4) — адреса будинку, а порт — номер квартири. Браузер за замовчуванням стукає в порт 80 (звичайний сайт) або 443 (із замком, HTTPS), тому ти їх не бачиш в адресі. Програму, яка постійно сидить на своєму порту й чекає, що хтось постукає, називають сервером; ту, що стукає сама, — клієнтом.

API — це «двері для програм»: набір адрес, за якими одна програма просить іншу щось зробити й отримує відповідь у вигляді даних (JSON), а не гарної сторінки. Сторінку читає людина, API читає програма. Найближчий приклад — Bot API Telegram (api.telegram.org), про який ти щойно читав. Тут сервер — Telegram, а клієнт — твій бот. Ось увесь шлях повідомлення, читай зверху вниз:

Оля пише боту в Telegram
   │
   ▼
сервери Telegram  ── тримають повідомлення в черзі
   ▲
   │  getUpdates  «є щось нове?»        (бот питає сам, порт 443, назовні)
   │  sendMessage «надішли цю відповідь»
   │
твій бот — служба club-bot на нашому сервері (run.py + bot.py)

Зверни увагу на напрямок стрілок. Telegram ніколи не стукає до твого сервера. Усі запити починає бот: спитав — отримав — відповів. Бот — це клієнт, що питає, коли треба. Порівняй з телефоном: клієнт — це ти, коли сам набираєш номер, коли тобі потрібно; сервер — це людина, яка цілодобово чергує біля телефона, бо дзвінок може надійти будь-коли. Чергування коштує ресурсів, навіть коли ніхто не дзвонить.

Чому в нашому курсі немає власного API-сервера. Є три причини.

Коли боту знадобляться дані ззовні, він знову стукатиме сам і лише тоді, коли треба: Telegram — щоразу, шлюз ШІ — на занятті 06, інший сервіс — коли ти додаси команду, якій він потрібен. Такий запит «на вимогу» (англійською on demand) триває кілька секунд і не займає пам’ять між запитами. Різницю між «питати, коли треба» й «слухати постійно» ти використаєш у кожному наступному занятті.

У кроці 9 побачиш це на власному сервері: команда ss покаже, що твій бот нічого не слухає, а лише тримає з’єднання з Telegram.

Оля пише твоєму боту в Telegram. Хто до кого звертається?

Друг пропонує: «Давай зробимо боту ще й власний API-сервер, нехай постійно слухає порт». Чому в нашому курсі так не роблять?

Як користуватися ШІ-помічником на цьому занятті

Сьогодні тобі допомагає ШІ (штучний інтелект) (ChatGPT, Gemini, Claude — що відкрито на твоєму пристрої). Правила прості: ШІ пише чернетку, а розумієш і відповідаєш за неї ти. Перед тим як вставити будь-який код чи команду на сервер, попроси ШІ пояснити її рядок за рядком — і лише тоді запускай. Не вставляй у ШІ токен бота й паролі: у запитах пиши ТОКЕН замість справжнього.

Готові запити, які можна скопіювати (підстав своє в кутових дужках):

1. Поясни мені простими словами для 14-річного, що таке порт і чим він відрізняється від IP-адреси.
2. Ось мій bot.py: <встав код>. Поясни по рядках, що він робить, і де тут «токен».
3. Мій бот не відповідає. Ось що показує команда tail -n 30 ~/logs/club-bot.log: <встав>. Що це означає і що перевірити першим?
4. Допоможи придумати 3 цікаві команди для мого бота на тему <моя тема>. Пиши код для функції handle_update.
5. Як зробити, щоб служба сама запускалася після перезавантаження сервера? Поясни кожен рядок файлу .service.

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

3. Як повідомлення доходить до бота: long polling

Повідомлення Олі йде не на наш сервер, а в Telegram. Telegram не знає, де живе твоя програма, тому тримає повідомлення в черзі, поки бот сам не спитає. Питати щосекунди — тисячі марних запитів, тож бот — клієнт, що питає, — користується long polling, «довгим опитуванням»:

бот:       getUpdates, timeout=25    «є щось нове? якщо ні — почекаю до 25 секунд»
Telegram:  ...тримає запит відкритим...
           на 7-й секунді Оля пише «привіт»
Telegram:  ось оновлення 815204312 з текстом «привіт»
бот:       sendMessage               «Ти написав: привіт»
бот:       getUpdates, offset=815204313, timeout=25

Запит висить відкритим, доки не з’явиться повідомлення, і тоді відповідь приходить одразу; за 25 секунд нічого — Telegram віддає порожній список, і бот питає знову. Як «я почекаю на лінії» замість дзвінка щохвилини.

У кожного оновлення є номер update_id, і кожне наступне — більше. Питаючи з offset («зсув»), рівним останньому номеру плюс один, бот каже: «усе до цього я обробив, можеш забути». Номер бот зберігає в ~/data/bot_state.json, а після кожної відповіді Telegram пише туди час — last_poll:

{"last_poll": "2026-10-21T15:04:27", "offset": 815204313}

За свіжим last_poll автоперевірка бачить, що бот живий, а збережений offset рятує від подвійних відповідей після перезапуску. Непрочитані оновлення Telegram зберігає до 24 годин. Служби груп працюють за розкладом — під час заняття і щодня, крім суботи, з 17:00 до 23:00 (точний розклад спитай у викладача; у кабінеті видно, чи служба зараз увімкнена). Написали боту о дев’ятій ранку — він мовчить, а щойно служба підніметься, відповість на все накопичене.

О 9:30 ранку, коли служба твоєї групи вимкнена, Оля написала твоєму боту «привіт». Що станеться з повідомленням?

4. Токен — пароль бота: файл .env і права 600

@BotFather — офіційний бот, через якого реєструють інших ботів, — видасть токен на зразок 123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11 (приклад із документації Telegram). До двокрапки — номер бота, далі — секрет. За токеном у кожній адресі Bot API Telegram вирішує, від імені якого бота запит. Хто знає токен — той і є твій бот: читає все, що йому пишуть, і пише людям від його імені.

Тому токен не пишуть у код: код ти показуєш на проєкторі, однокласникам, ШІ, а сьогодні відправиш на GitHub. Токен живе у файлі ~/app/.env, лише на сервері: рядок BOT_TOKEN=… без пробілів і лапок; рядки з # — коментарі. Крапка на початку імені робить файл прихованим: ls його не показує, ls -a — показує.

На початку bot.py стоїть import env. Файл env.py читає .env і кладе значення у змінні оточення — іменовані рядки, які програма отримує ззовні. Звідти bot.py і бере токен: TOKEN = os.environ.get("BOT_TOKEN", "") — «дай значення, а якщо його немає — порожній рядок». Без токена бот не падає, а просто не опитує Telegram. Служба сама .env не читає — це робить програма, і вже задані в оточенні значення не перезаписує: так автоперевірка підставляє свої.

Друга половина захисту — права доступу: у файлу є три набори прав — для власника, для групи й для всіх інших користувачів сервера. Кожен набір — три літери: r — читати, w — змінювати, x — запускати, риска — «не можна». ls -l показує їх на початку рядка: -rw-r--r-- — власник читає й змінює, інші читають (такі права матиме .env, щойно скопійований зі зразка); -rw------- — усе може лише власник. Це права 600: читати — 4, змінювати — 2, запускати — 1, тож 6 = 4 + 2 для власника, а нулі — нічого групі й іншим. Ставить їх chmod 600 ~/app/.env. Тека ~/app і так закрита від сусідів, але права тек можуть змінитися (скажімо, під час перенесення на інший сервер), і тоді закритий файл лишиться закритим. Автоперевірка L02.12 дивиться саме на права файлу.

Якщо токен потрапив до чужих (скриншот, чат, GitHub), його вважають украденим і замінюють: /revoke в @BotFather — старий токен перестає діяти, новий іде в .env, службу перезапускаєш.

Ти зробив скриншот термінала з відкритим .env і вже надіслав його в чат класу. Що тепер правильно зробити?

5. Як влаштований bot.py: логіка окремо від доставки

Готовий bot.py лежить у /opt/club/kurs/02/, але кожен його рядок треба розуміти. Файл має три частини: Bot API — tg() робить один запит до Telegram, now() дає поточний час; логіка — handle_update() вирішує, що відповісти; доставка — poll_forever(), start_polling(), get_state(), set_state() носять повідомлення туди й назад. Логіку ти змінюватимеш на кожному занятті:

HELP = ("Що я вмію:\n"
        "/start — привітання\n"
        "/help — ця підказка")


def handle_update(update):
    """Одне повідомлення від Telegram → текст відповіді (або None)."""
    msg = update.get("message") or {}
    text = (msg.get("text") or "").strip()
    sender = msg.get("from") or {}
    if not text:
        return None
    name = sender.get("first_name") or "друже"
    if text.startswith("/start"):
        return (f"Привіт, {name}! Я бот проєкту «{PRODUCT}». "
                "Напиши /help.")
    if text.startswith("/help"):
        return HELP
    if text.startswith("/"):
        return "Не знаю такої команди. Напиши /help."
    return f"Ти написав: {text}"

handle_update() не знає про мережу: на вході словник, на виході текст. Тому try_bot.py складає такий словник, як у розділі 1, і друкує відповідь — бот перевіряється без Telegram. Автоперевірка L02.14 робить те саме. tg(method, **params) перетворює параметри на JSON і надсилає методом POST через urllib зі стандартної бібліотеки Python. А poll_forever() — це діалог із розділу 2 мовою Python:

def poll_forever(stop):
    """Питає Telegram «чи є нові повідомлення?» і відповідає на кожне.

    offset зберігаємо у файлі: якщо службу вбили посеред роботи, після
    перезапуску бот продовжить з того самого місця й не відповість двічі."""
    offset = int(get_state("offset", 0) or 0)
    while not stop.is_set():
        try:
            data = tg("getUpdates", offset=offset, timeout=25)
        except Exception as e:                        # мережа, обірвана чи зіпсована відповідь — не вмираємо
            print(f"{now()} Telegram недоступний: {e!r}", flush=True)
            stop.wait(10)
            continue
        # «Живий» = Telegram щойно відповів. Записуємо ПІСЛЯ відповіді: з поганим
        # токеном (401) чи з другою копією бота (409) час не оновиться.
        set_state("last_poll", now())
        for update in data.get("result", []):
            offset = update["update_id"] + 1
            set_state("offset", offset)
            try:
                reply = handle_update(update)
            except Exception as e:                    # помилка в логіці не валить бота
                print(f"{now()} помилка обробки: {e!r}", flush=True)
                reply = "Ой, щось пішло не так. Спробуй ще раз."
            chat = (update.get("message") or {}).get("chat", {}).get("id")
            if reply and chat:
                try:
                    tg("sendMessage", chat_id=chat, text=reply)
                except (urllib.error.URLError, OSError) as e:
                    print(f"{now()} не вдалося надіслати: {e}", flush=True)

set_state() пише спершу в тимчасовий файл і лише потім підміняє ним старий (os.replace) — обірваний запис не псує стан. start_polling() запускає poll_forever() в окремому потоці — другій лінії роботи в тій самій програмі: потік опитує Telegram, а головна частина програми (run.py) чекає сигналу зупинки. Без токена або з BOT_POLLING=0 (як у try_bot.py і автоперевірці) потік не запускається.

Як користуватися ШІ-помічникомПопросити ШІ пояснити рядок можна — у тому чаті, який дозволений за твоїм віком (заняття 01). Вставляй лише код, ніколи не .env і не токен. Пояснення перевір через try_bot.py, а на занятті пояснюй своїми словами.

Команда python3 try_bot.py "/start" друкує привітання, а в Telegram бот мовчить. Де шукати причину?

6. Служба: чому бот працює, коли твій комп’ютер вимкнено

Процес — запущена програма. Запустиш бота руками в терміналі — він зупиниться разом із твоїм сеансом SSH. Служба — програма, якою керує сама система: запускає її разом із сервером і перезапускає, якщо та завершилась. У Linux цим займається systemd; твоя служба — club-bot, а --user у командах означає «моя, а не загальносерверна». Чотири рядки з її опису ~/.config/systemd/user/club-bot.service:

ExecStart=/opt/club/venv/bin/python3 run.py
Restart=always
RestartSec=3
MemoryMax=100M

ExecStart — команда запуску: Python зі спільного оточення виконує файл run.py з теки ~/app. Ні веб-сервера, ні порту тут немає: служба нічого не слухає. Restart=always — «завершився з будь-якої причини — запусти знову», RestartSec=3 — «через три секунди». MemoryMax=100M — стеля пам’яті: захоче програма більше ста мегабайт — система її зупинить, і один бот із помилкою не з’їсть пам’ять усім. enable вмикає автозапуск разом із сервером, а linger (його вмикає викладач) дозволяє службам працювати, коли тебе немає на сервері. Усе, що служба друкує, потрапляє в журнал: tail -n 30 ~/logs/club-bot.log.

Сам бот запускає невеликий файл run.py. Він лежить у ~/app із заготовки (копія є й в /opt/club/kurs/02/), і міняти його не треба, але прочитати варто — він короткий. Ось його робочі рядки (пояснення в коментарях на початку файлу пропущено):

import signal
import threading

import bot

quit_event = threading.Event()


def on_signal(signum, frame):
    quit_event.set()


signal.signal(signal.SIGTERM, on_signal)
signal.signal(signal.SIGINT, on_signal)

stop = bot.start_polling()
if stop is None:       # немає BOT_TOKEN у .env
    print("Бот не запущено: додай BOT_TOKEN у ~/app/.env і перезапусти службу.", flush=True)
quit_event.wait()
if stop is not None:
    stop.set()

Так бот живе стільки ж, скільки служба. Усе перед wait() виконується під час запуску, усе після — під час зупинки. Що з цього випливає:

А якщо перезавантажать сам сервер? Тоді зникнуть усі процеси, але systemd при старті системи знову запустить усе, що позначено enable, і за тим самим описом club-bot.service. Для цього в описі є рядок WantedBy=default.target («запускати при старті»), а linger дозволяє твоїм службам стартувати одразу, не чекаючи, поки ти зайдеш по SSH. Тобто окремого «скрипта, що запускає бота» писати не потрібно: цим скриптом і є файл служби. Перевір його в кроці 12: enabled + Linger=yes означає, що бот сам підніметься після перезавантаження.

Автоперевірка вбиває процес служби сигналом SIGKILL, і за три секунди systemd запускає її знову. Чому бот після цього не відповідає вдруге на вже оброблені повідомлення?

7. GitHub: копія роботи поза сервером і ключ замість пароля

Уся робота лежить на одному сервері: вимкнуть його — і показати бота нікому. Тому з’явиться друга копія — на GitHub (про нього було на занятті 01). Тека ~/app уже є git-репозиторієм: програма git веде в ній історію змін, і перший коміт — знімок файлів із підписом «Заготовка проєкту» — зробив сервер, створюючи твій акаунт. Докладно про коміти — на наступному занятті; сьогодні знадобляться три команди: git add -A (позначити зміни), git commit -m "…" (зробити знімок) і git push (відправити його). Іншу копію репозиторію, куди відправляють коміти, називають віддаленим репозиторієм, мовою git — remote: один уже є — origin, тека ~/repo.git на цьому ж сервері, а сьогодні додаси github.

GitHub упізнає тебе за SSH-ключем — парою файлів. Секретний ~/.ssh/id_ed25519 лишається на сервері й нікому не показується, відкритий ~/.ssh/id_ed25519.pub ти віддаси GitHub. Під час з’єднання GitHub дає серверу завдання, яке розв’язується лише секретним ключем, і за відповіддю бачить, що це ти; сам секретний ключ мережею не йде. А щоб на GitHub не поїхав .env, є файл .gitignore — список того, чого git не помічає. У твоєму вже стоїть рядок .env, тому git status цього файлу не покаже. Перед відправкою ти перевіриш це сам.

Слова цього уроку

СловоЩо означає
ботАкаунт у Telegram, на повідомлення якому відповідає програма
Bot API, методАдреси api.telegram.org для керування ботом і назва дії: getMe, getUpdates, sendMessage
токен, @BotFatherПароль бота і офіційний бот Telegram, який його видає
JSONТекстовий формат даних; у Python перетворюється на словник
оновлення, offsetОдна подія для бота з номером і число, яким бот каже: «усе до цього я обробив»
long pollingОпитування, коли запит висить відкритим, доки не з’явиться нове
.env, змінна оточенняПрихований файл із секретами лише на сервері і рядок, який програма отримує ззовні
права 600, chmodЧитати й змінювати файл може лише власник; команда, що змінює права
handle_updateФункція логіки бота: повідомлення на вході, текст відповіді на виході
потік, сигналОкрема лінія роботи в програмі і коротке повідомлення від системи програмі (наприклад, SIGTERM — «закінчуй»)
служба, systemdПрограма під наглядом системи і частина Linux, що за нею наглядає
enable, lingerАвтозапуск служби і дозвіл їй працювати, коли тебе немає на сервері
журналУсе, що надрукувала служба; лежить у файлі ~/logs/club-bot.log, читають командою tail
коміт, віддалений репозиторій (remote)Знімок файлів із підписом і інша копія репозиторію, куди відправляють коміти
бекап, правило 3-2-1Запасна копія, з якої відновлюють роботу; три копії, на двох носіях, одна — далеко
порт, сервер, клієнтНомер «дверей» програми на сервері; сервер постійно слухає порт, клієнт сам звертається, коли треба (наш бот — клієнт Telegram)
APIАдреси, за якими одна програма просить іншу щось зробити й отримує дані. Власного API в нас немає — є Bot API Telegram
SSH-ключ, .gitignoreПара файлів (секретний лишається, відкритий — для GitHub) і список того, чого git не помічає

Практика

Виконуй кроки по черзі. Де в команді є $USER, нічого не підставляй — ця змінна вже містить твій логін. А <логін> разом із кутовими дужками заміни на свій, наприклад bohdan.g9.

1 Зайти на сервер і перевірити службу

ssh <логін>@91.219.61.4
systemctl --user is-active club-bot
ls -a ~/app

Пароль під час набору не показується — набирай і тисни Enter. Друга команда друкує стан служби твого бота. Поки в теці немає bot.py, службі нічого запускати: вона може показати activating чи failed — це нормально, у кроці 3 ти додаси бота. Якщо вже active, файли бота в тебе є з минулого разу. У списку файлів, серед інших, мають бути run.py (запуск бота службою), db.py, project.yaml, .gitignore і тека .git з історією. Параметр -a показує й приховані файли, чиї імена починаються з крапки.

inactive — служба зупинена: systemctl --user start club-bot. Якщо в теці вже є bot.py, а стан усе одно failed чи activating, служба падає; прочитай журнал (tail -n 30 ~/logs/club-bot.log) і поклич викладача: сьогодні ти будуєш саме на ній.

2 Створити бота в @BotFather

Знайди в Telegram @BotFather — справжній має синю галочку, тож звір нік до літери. Натисни Start («Почати») і надішли /newbot. BotFather розмовляє англійською й спитає дві речі. «How are we going to call it?» — ім’я, яке бачитимуть люди, можна українською: Бот-довідник про космос. «Choose a username for your bot. It must end in bot» — нік: латинські літери, цифри й підкреслення, від 5 до 32 символів, у кінці bot, без прізвища, наприклад kosmos_bohdan_bot.

Нік зайнятий — прийде Sorry, this username is already taken, вигадай інший. Успіх — повідомлення Done! Congratulations on your new bot з посиланням t.me/…, а після Use this token to access the HTTP API: — токен. Ти вставиш його в кроці 4, тож відкрий Telegram на тому самому комп’ютері, де термінал (Telegram Desktop або web.telegram.org): переписати понад сорок символів вручну без помилки майже неможливо. Скриншотів із токеном не роби й нікому його не пересилай.

Без TelegramПропусти цей крок. У кроці 4 лиши BOT_TOKEN= порожнім, у кроці 5 — username: "" з порожніми лапками, а в кроці 11 напиши на сторінці лише назву проєкту, без ніка: чужий чи вигаданий нік повів би людей до чужого бота.

3 Скопіювати готові файли уроку

cp /opt/club/kurs/02/{env.py,bot.py,try_bot.py,.env.example} ~/app/
cp /opt/club/kurs/02/run.py ~/app/
ls -a ~/app

Фігурні дужки термінал сам розгорне в чотири шляхи (пробілів усередині бути не повинно). Друга команда замінює заготовку run.py, яка була в ~/app від початку, на версію цього уроку. cp мовчить, якщо все гаразд, а ls -a покаже нові bot.py, env.py, try_bot.py і .env.example (і run.py) — зразок файлу із секретами, але без секретів, тож його можна зберігати в Git. No such file or directory — подивись ls -a /opt/club/kurs/02/: файли є — шукай пробіл у дужках, немає — скажи викладачеві.

4 Сховати токен у .env і закрити файл

cd ~/app
cp .env.example .env
chmod 600 .env
nano .env

Ти переходиш у теку проєкту (далі всі команди — звідти), робиш .env зі зразка й закриваєш його ще до того, як у ньому з’явився токен. nano — простий редактор у терміналі: дійди стрілками до кінця рядка BOT_TOKEN= і встав токен (правою кнопкою миші, Ctrl+V у Windows Terminal або Cmd+V на Mac) — один рядок без пробілів і лапок. Зберегти — Ctrl+O і Enter, вийти — Ctrl+X. Перевір права й вміст так, щоб секрет не з’явився на екрані:

ls -l .env
grep -o "^BOT_TOKEN=[0-9]*:" .env

Перша має почати рядок з -rw------- і двічі назвати твій логін (власник і група); на крапку одразу після прав не зважай — це позначка системи захисту SELinux. -rw-r--r-- — ти пропустив chmod 600 .env. Друга друкує лише номер бота, наприклад BOT_TOKEN=123456:, — секрет після двокрапки лишився невидимим. Не надрукувала нічого — перед токеном пробіл або рядок зіпсовано: nano .env і виправ.

5 Записати в паспорт проєкту назву, автора й бота

nano ~/app/project.yaml

project.yaml — маніфест, «паспорт» проєкту, який читають і програма, і автоперевірка; у форматі YAML вкладеність показують відступом. Якщо в product досі ЗМІНИ МЕНЕ чи Ім'я — впиши назву з брифу й своє ім’я без прізвища. Потім знайди розділ bot: якщо він уже є з username: "", впиши нік із кроку 2 між лапками; якщо його немає — додай два рядки від лівого краю, як у зразку. Розділ bot має бути лише один: з двох однакових програма мовчки візьме нижній. Решту не чіпай. Назва й ім’я потрібні відразу: з них складе відповідь команда /about у кроці 7.

product:
  name: "Бот-довідник про космос"
  author: "Богдан"
  ...

bot:                            # заняття 02
  username: "@kosmos_bohdan_bot"

server:
  ...

Відступ — лише пробілами, по два на рівень: Tab YAML забороняє. Після двокрапки — пробіл. Нік — обов’язково в лапках: знак @ у YAML зарезервований, і без лапок зламається весь файл. Збережи, вийди й перевір:

cd ~/app
python3 -c "import yaml; d = yaml.safe_load(open('project.yaml')); print(d['product']['name'], '|', d['product']['author'], '|', d['bot']['username'])"

Має бути Бот-довідник про космос | Богдан | @kosmos_bohdan_bot з твоїми значеннями. ScannerError чи ParserError — помилка формату в рядку, номер якого стоїть після line (або рядком вище). KeyError: 'bot' — розділу немає або рядок bot: зсунуто вправо.

6 Перевірити бота без Telegram

Спершу прочитай less ~/app/bot.py (гортати — стрілки й Пробіл, вийти — q) і знайди все, що було в теорії. Потім поговори з ботом. Лапки навколо тексту обов’язкові: так термінал розуміє, що це одне повідомлення.

python3 try_bot.py "/start"
python3 try_bot.py "/help"
python3 try_bot.py "/котик"
python3 try_bot.py "привіт, боте"
python3 try_bot.py ""
python3 try_bot.py --name Оля "/start"

По черзі: Привіт, Тест! Я бот проєкту «Бот-довідник про космос». Напиши /help. («Тест» підставляє try_bot.py, назва — з project.yaml); три рядки підказки; Не знаю такої команди. Напиши /help.; Ти написав: привіт, боте; (бот нічого не відповів — на порожній текст так і має бути) — на порожнє бот не відповідає; Привіт, Оля! ….

Кілька рядків, перший з яких — Traceback, — це помилка; читай останній рядок, там її назва зі словом Error. can't open file — ти не в ~/app; KeyError: 'product' чи ScannerError — повернись до кроку 5; No module named 'yaml' — запусти Python зі спільного оточення: /opt/club/venv/bin/python3 try_bot.py "/start".

7 Додати команду /about і переписати відповіді своїми словами

Відкрий nano ~/app/bot.py. Спершу допиши в підказку HELP два рядки: про /about і про відлуння. Потім переглянь привітання на /start і перепиши його під свій проєкт. Нарешті, у handle_update одразу під перевіркою /help і перед рядком if text.startswith("/"): встав нову команду:

HELP = ("Що я вмію:\n"
        "/start — привітання\n"
        "/help — ця підказка\n"
        "/about — хто мене зробив\n"
        "будь-який інший текст — повторю його")
...
    if text.startswith("/start"):
        return (f"Привіт, {name}! Я бот проєкту «{PRODUCT}». "
                "Скоро я знатиму багато про космос, а поки вчуся. Напиши /help.")
    if text.startswith("/help"):
        return HELP
    if text.startswith("/about"):
        return f"Проєкт «{PRODUCT}», автор — {CFG['product']['author']}."
    if text.startswith("/"):
        return "Не знаю такої команди. Напиши /help."

Кожен шматок тексту — у своїх лапках, \n усередині означає «новий рядок», а сусідні рядки в дужках Python склеює сам — без коми й плюса. Перед лапками з {name} лиши букву f, відступи — пробілами, як у сусідів. Про нову команду: CFG — уміст project.yaml, який bot.py читає на самому початку файлу, а PRODUCT — назва продукту звідти ж. Фігурні дужки в f-рядку Python замінює значеннями, а назви полів усередині беруться в одинарні лапки, бо сам рядок — у подвійних. Порядок важливий: рядок if text.startswith("/") ловить будь-яку команду, якої бот не знає, тож /about має стояти вище за нього, інакше до неї черга не дійде.

Залиш у привітанні {name}, а у відповіді на невідому команду — слова «Не знаю такої команди»: на них спиратимуться перевірки наступних занять. Автоперевірка L02.2 викличе /about і шукатиме у відповіді назву й ім’я з project.yaml. Збережи й перевір:

python3 try_bot.py "/about"
python3 try_bot.py "/help"
python3 try_bot.py "/start"

Має бути Проєкт «Бот-довідник про космос», автор — Богдан. (з твоєю назвою й ім’ям), потім п’ять рядків підказки і твоє привітання. Відповідь Не знаю такої команди на /about означає, що команду вставлено нижче за перевірку /. Ти написав: /about — забута сама перевірка. KeyError: 'author' — у project.yaml немає поля author (крок 5). SyntaxError з номером рядка — незакриті лапки чи дужка там або вище; IndentationError — відступи.

8 Переконатися, що run.py побачить бота

Служба запускає не bot.py, а run.py, який підключає його рядком import bot. Тому спершу прочитай run.py і перевір, що він не впаде на імпорті:

cd ~/app
cat run.py
/opt/club/venv/bin/python3 -c "import bot; print('готово:', bot.PRODUCT)"

cat друкує файл із розділу 6, run.py ти не змінюєш. Остання команда імпортує бота тим самим Python, яким його запускає служба, але Telegram не опитує. Має бути готово: і назва твого проєкту. No module named 'bot' — немає bot.py у теці: повтори крок 3; SyntaxError чи IndentationError з номером рядка — описка в bot.py, виправ; KeyError: 'product' чи ScannerError — повернись до кроку 5. Якщо файлу run.py у теці немає, виконай cp -n /opt/club/kurs/02/run.py ~/app/.

9 Перезапустити службу й переконатися, що бот опитує Telegram

Спершу переконайся, що служба пише свій журнал у файл, який ти можеш читати: системний журнал сервера учням закритий.

mkdir -p ~/logs
grep -n "^Standard" ~/.config/systemd/user/club-bot.service

Має бути два рядки: StandardOutput=append:%h/logs/club-bot.log і StandardError=inherit. Це значить: усе, що друкує служба, дописується в ~/logs/club-bot.log. Інакше — скажи викладачеві. Тепер перезапусти:

systemctl --user restart club-bot
systemctl --user status club-bot
tail -n 15 ~/logs/club-bot.log

У status шукай Active: active (running) (вийти — q). Журнал, коли все гаразд, майже порожній: бот мовчить. Рядок Бот не запущено: додай BOT_TOKEN… означає, що токен не прочитано (крок 4). Що означає інше:

cat ~/data/bot_state.json

Покаже щось на зразок {"last_poll": "2026-10-21T15:04:27"}; повтори за пів хвилини — час має змінитися (бот записує його після кожної відповіді Telegram, а запит триває до 25 секунд). No such file or directory — Telegram ще жодного разу не відповів: порожній чи неправильний BOT_TOKEN (подивись tail ~/logs/club-bot.log: там буде причина).

Тепер глянь, хто куди звертається. Дві команди показують мережеві з’єднання; -t — лише TCP, -n — адреси цифрами, -p — яка програма:

ss -tlnp
ss -tnp

Перша команда друкує програми, що слухають порти (літера l від listen). У рядках із твоїми процесами не буде жодного python3: твій бот нічого не слухає, у нього немає відчинених дверей. Друга друкує з’єднання, які існують зараз. Серед них має знайтися рядок із python3 і адресою, що закінчується на :443: це твій бот тримає запит до Telegram відкритим і чекає на повідомлення. Рядка немає — повтори команду, бо між двома запитами з’єднання на мить закривається. Бот — клієнт, а не сервер, саме як у розділі 2.

10 Поговорити з ботом у Telegram

Відкрий посилання t.me/… від BotFather або знайди бота за ніком і натисни Start — Telegram сам надішле боту /start. За секунду-дві прийде привітання, цього разу з твоїм справжнім іменем. Надішли /help, будь-який текст і стікер — на стікер бот промовчить, бо тексту в ньому немає. Тепер cat ~/data/bot_state.json: з’явилося поле offset з великим числом, і з кожним твоїм повідомленням воно росте. Попроси сусіда знайти бота за ніком і написати — бот привітається з ним на його ім’я: відповідає бот будь-кому, хто знайде нік.

Відповіді немає, а try_bot.py працює — дивись «Якщо щось не працює», пункт «try_bot.py відповідає, а в Telegram бот мовчить».

11 Розповісти про проєкт на своїй сторінці

Автоперевірка L02.6 шукає на твоїй сторінці назву продукту — точно таку, як name у project.yaml. Відкрий nano ~/www/index.html і перед </body> встав блок зі своєю назвою й ніком. Текст про себе із заняття 01 не видаляй — його й далі перевіряють.

<h2>Мій проєкт на курсі</h2>
<p>Я роблю Telegram-бота «Бот-довідник про космос». Напиши йому: @kosmos_bohdan_bot</p>

Перезапуск не потрібен: сторінку віддає nginx прямо з файлу. Перевір, підставивши свою назву:

curl -s http://91.219.61.4/u/$USER/ | grep -c "Бот-довідник про космос"

-s прибирає службові рядки curl, grep -c рахує рядки зі збігом. 1 чи більше — гаразд; 0 — назва на сторінці хоч трохи інша: літера, пробіл, лапки.

12 Перевірити автозапуск і linger

systemctl --user is-enabled club-bot
loginctl show-user $USER -p Linger --value
grep -E "ExecStart|Restart|MemoryMax" ~/.config/systemd/user/club-bot.service

Правильно: enabled, yes і рядки ExecStart=… python3 run.py, Restart=always, RestartSec=3, MemoryMax=100M (рядки з # на початку — коментарі, на них не зважай). disabled — виправ сам: systemctl --user enable --now club-bot (--now — «і запусти одразу»). no або немає Restart=always — це до викладача: linger і файл служби налаштовує він.

13 Зупинити бота найгрубіше — і подивитися, як він повертається

Зробимо те саме, що автоперевірка L02.5: уб’ємо процес служби сигналом SIGKILL — найжорсткішою командою «негайно припинись». Такий сигнал програма навіть не отримує — система просто прибирає її з пам’яті.

systemctl --user kill -s SIGKILL club-bot
systemctl --user is-active club-bot

Найімовірніше, побачиш activating: systemd відраховує свої три секунди. Саме зараз напиши боту «ти тут?» — відповідь прийде за кілька секунд, бо повідомлення почекало на серверах Telegram. Повтори systemctl --user is-active club-bot — має бути active. На старі повідомлення бот удруге не відповів: він продовжив зі збереженого offset.

Тепер вийди із сервера (exit) і напиши боту з телефона /start — він відповідає, хоча твого сеансу вже немає: це працюють служба, linger і Restart=always. Без Telegram переконайся в цьому інакше: зайди на сервер знову, виконай systemctl --user is-active club-bot (має бути active) і cat ~/data/bot_state.json (last_poll свіжий), хоч тебе тут довго не було. Потім повернись:

ssh <логін>@91.219.61.4
cd ~/app

14 Завести акаунт на GitHub і назватися для git

На github.com натисни Sign up; пошта — та, до якої маєш доступ, на неї прийде код. Ім’я користувача — без прізвища: воно стане частиною адреси твоєї роботи. GitHub може попросити ввімкнути двоетапний вхід (2FA) — код із застосунку на телефоні; незрозуміло — клич викладача. GitHub — з 13 років і, поки тобі немає 18, з дозволу батьків (про це йшлося на занятті 01). Якщо тобі менше 13 чи дозволу немає, не реєструйся на GitHub, а команди git config нижче виконай усе одно. Кроки 15–16 пропусти, а в кроці 18 зроби лише коміт, без git push.

Тепер скажи git, як підписувати твої коміти (ім’я — своє):

git config --global user.name "Богдан"
git config --global user.email "$(whoami)@club.local"
git config --global --list

$(whoami) термінал замінить твоїм логіном, і остання команда покаже user.name=Богдан і user.email=bohdan.g9@club.local. Справжню пошту сюди не пиши: вона потрапила б у кожен коміт.

15 Дати серверу ключ від GitHub

ssh-keygen -t ed25519 -C "$(whoami)@club" -f ~/.ssh/id_ed25519 -N ""
cat ~/.ssh/id_ed25519.pub

-t ed25519 — тип ключа, -C — підпис, щоб на GitHub було видно, звідки ключ, -f — куди записати, -N "" — без окремого пароля на сам ключ. У відповідь буде Your public key has been saved in …/id_ed25519.pub і квадратна картинка із символів — візерунок ключа. Питання Overwrite (y/n)? означає, що ключ уже є: відповідай n. Друга команда друкує відкритий ключ — один рядок ssh-ed25519 AAAA… <логін>@club; скопіюй його цілком. На GitHub: аватарка вгорі праворуч → Settings → SSH and GPG keys → New SSH key; Title — club-server, Key type — Authentication Key, у Key встав рядок → Add SSH key.

Лише файл .pubВіддавати можна тільки відкритий ключ. ~/.ssh/id_ed25519 без .pub — секрет, як токен: його не показують нікому, ні викладачеві, ні ШІ.

ssh -T git@github.com

Першого разу програма покаже відбиток сервера GitHub, як наш сервер на занятті 01. Перед відбитком написано тип ключа: для ED25519 правильний — SHA256:+DiY3wvvV6TuJJhbpZisF/zLDA0zPMSvHdkr4UvCOqU, для ECDSA — SHA256:p2QAMXNIC1TJYWeIOttrVc98/R1BUFWu3/LiyKgUfQM (GitHub публікує їх у своїй документації). Збігається — набери yes; не збігається — не підтверджуй і поклич викладача. Відповідь Hi <логін-github>! You've successfully authenticated, but GitHub does not provide shell access. — «впізнав, але командного рядка не дам», тобто все добре. Permission denied (publickey) — ключ не додався: скопіюй рядок ще раз повністю.

16 Створити приватний репозиторій і підключити його

На GitHub: + угорі праворуч → New repository, назва club-app, Private — бачитимеш лише ти й ті, кого запросиш. Нічого не додавай — ні README, ні .gitignore, ні ліцензії: усе потрібне вже на сервері, а зайві файли заважатимуть першій відправці. Create repository. На сторінці, що відкриється, перемкни адресу на SSH — вона починається з git@github.com:. На сервері:

git remote add github git@github.com:<логін-github>/club-app.git
git remote -v

Друга команда покаже чотири рядки: двічі github з адресою GitHub і двічі origin з /home/<логін>/repo.git; (fetch) і (push) — «звідки забирати» і «куди відправляти». error: remote github already exists — ти вже додавав його раніше; онови адресу: git remote set-url github git@github.com:<логін-github>/club-app.git.

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

Спершу — головна перевірка уроку: токен не має поїхати разом із кодом.

17 Переконатися, що .env нікуди не поїде

cd ~/app
git status
git check-ignore -v .env

git status показує, що змінилося від останнього коміту: серед modified має бути project.yaml, серед Untracked files (нові файли) — .env.example, bot.py, env.py, try_bot.py, можливо, ще BRIEF.md із заняття 01. Головне — .env там немає. Друга команда каже чому, наприклад .gitignore:12:.env .env — «файл .gitignore, рядок 12, правило .env» (номер у тебе може бути інший).

Якщо .env видно в git statusНічого не відправляй. Допиши правило — echo ".env" >> .gitignore (два знаки >> дописують рядок у кінець файлу) — і знову виконай git status. Ніколи не додавай .env силою через git add -f, хоч би що радили в інтернеті.

18 Зробити коміт і вперше відправити роботу

git add -A
git commit -m "Заняття 02: бот у службі, токен у .env"
git branch --show-current
git push -u github master

git add -A позначає всі зміни, крім того, що в .gitignore. git commit відповість рядком на зразок [master 3f2a1c4] Заняття 02: бот у службі, токен у .env і підсумком змінених файлів. git branch --show-current друкує назву гілки — лінії історії, куди пишуться коміти; найімовірніше, це master. Якщо там main, у git push пиши main. Наприкінці відповіді push має бути * [new branch] master -> master і branch 'master' set up to track 'github/master': ключ -u запам’ятав пару, тож далі досить git push.

Онови сторінку репозиторію в браузері: там bot.py, run.py, project.yaml, .env.example з порожнім BOT_TOKEN= — і немає .env. Відтепер кожне заняття закінчується трьома командами: git add -A, git commit -m "Заняття NN: що зробив", git push.

19 Покласти на GitHub і сайт — другий репозиторій

Бот — у ~/app, а твоя сторінка з заняття 01 — у ~/www. Сайт теж треба зберегти окремо. На GitHub створи ще один Private-репозиторій з назвою club-site (без README), а потім:

cd ~/www
git init -b master
git add -A
git commit -m "Мій сайт: сторінка про себе"
git remote add github git@github.com:<логін-github>/club-site.git
git push -u github master
cd ~/app

Тепер у тебе два репозиторії: club-app (бот) і club-site (сайт). У ~/www немає паролів і токенів, тому секретів там не загубиш; усе одно відкрий сторінку репозиторію й переконайся, що там лише index.html та картинки.

Якщо тобі немає 13 чи немає дозволу батьків і GitHub ти не заводив, пропусти цей крок разом із кроками 15–16. Команди git config із кроку 14 виконай усе одно: без них не вийде зберегти коміт. Бекап із кроку 20 теж зробиш.

20 Зробити бекап проєкту й перевірити, що з нього можна відновитися

Бекап — запасна копія, з якої можна відновити роботу, якщо файли зіпсувалися, сервер упав або ти щось видалив. Є правило 3-2-1: 3 копії, на 2 різних місцях, 1 — далеко від основної. У нас: основний проєкт на сервері; копія в ~/repo.git (теж сервер); копія на GitHub — це вже «далеко». Додамо четверту, повну, яку можна забрати собі на комп'ютер, — архів:

mkdir -p ~/backups
tar czf ~/backups/proekt-$(date +%F).tar.gz -C ~ app www data
ls -lh ~/backups

tar czf пакує теки в один стиснений файл (.tar.gz), $(date +%F) підставляє сьогоднішню дату, -C ~ — «працюй із домашньої теки». У архів потрапляють app (бот, разом із .env!), www (сайт) і data (база й стан бота). Бо в ньому є токен, архів нікуди не публікуй і на GitHub не вантаж; забирати його можна лише собі, тим самим способом, яким ти завантажував файли на сервер на занятті 01.

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

mkdir -p /tmp/perevirka-$USER
tar xzf ~/backups/proekt-$(date +%F).tar.gz -C /tmp/perevirka-$USER
ls /tmp/perevirka-$USER /tmp/perevirka-$USER/app | head
rm -rf /tmp/perevirka-$USER

Мають з'явитися app, www, data і в app — bot.py, run.py. Так: бекап зроблено й перевірено. Правило на життя: зробив важливу зміну — збережи її в git і на GitHub, а раз на тиждень роби архів.

Що перевіряє автоматика

Автоперевірка — програма, яка дивиться на твою роботу, — заходить на сервер від твого імені. Балами оцінюються сім пунктів. L02.2 — команда /about відповідає назвою продукту й твоїм ім’ям з project.yaml (перевірка бере твій bot.py і викликає handle_update() з тестовим повідомленням, як try_bot.py). L02.6 — на сторінці ~/www/index.html є назва твого продукту. Решта п’ять — про бота: у ~/app/.env є рядок BOT_TOKEN=… (L02.11) і файл закритий правами 600 (L02.12); Telegram визнає токен — перевірка питає getMe і сам токен нікуди не друкує (L02.13); handle_update() відповідає на /start (L02.14); а в ~/data/bot_state.json за останні три хвилини оновлювався запис last_poll (L02.15).

Є ще чотири пункти «справності служби»: L02.1 (служба club-bot працює, запускає run.py і не перезапускається щосекунди), L02.3 (автозапуск), L02.4 (linger) і L02.5 (служба сама піднімається після примусової зупинки). Їх показано окремо, балів вони не дають, але без них бот не житиме цілодобово. L02.5 кнопка не запускає, щоб натисканням не можна було зупинити чужу службу. Щоночі сервер сам зупиняє твій процес, як ти в кроці 13, і дивиться, чи служба піднялася; результат побачиш у кабінеті наступного дня.

Пункти, де потрібен справжній токен Telegram (L02.11, L02.13, L02.15): якщо Telegram у тебе немає, їх оцінить викладач, і автоперевірка цю оцінку не перезаписує. Натисни «Перевірити практичну частину» нижче на сторінці. Якщо служба твоєї групи зараз вимкнена за розкладом, перевірка сама ненадовго її підніме, а потім вимкне знову.

КодЩо перевіряєтьсяЯкщо не зараховано
L02.1служба club-bot працює і не перезапускається щосекунди
справність сервісу, без балів
Служба club-bot не працює або падає по колу. Перевір по черзі: systemctl --user status club-bot (стан active?), tail -n 30 ~/logs/club-bot.log (останні рядки журналу), cd ~/app && /opt/club/venv/bin/python3 -c "import bot" (чи не падає bot.py на імпорті?). Потрібні файли ~/app/bot.py і ~/app/run.py.
L02.2/about відповідає назвою продукту й автором з project.yaml
автоперевірка
Команда /about має відповісти назвою продукту й автором з project.yaml. Перевір, що в project.yaml product.name і product.author заповнено (не «ЗМІНИ МЕНЕ» і не «Ім'я»), а в bot.py є if text.startswith("/about") вище за перевірку if text.startswith("/"). Спробуй: python3 try_bot.py "/about"
L02.3служба ввімкнена в автозапуск
справність сервісу, без балів
Служба не в автозапуску. Виконай: systemctl --user enable --now club-bot
L02.4служба переживає вихід із SSH (linger)
справність сервісу, без балів
Для акаунта не ввімкнено linger. Це налаштовує викладач: loginctl enable-linger ЛОГІН
L02.5служба сама піднімається після kill
справність сервісу, без балів
Після примусової зупинки процес не піднявся. У юніт-файлі має бути Restart=always.
L02.6на сторінці учня є назва продукту з project.yaml
автоперевірка
Головна сторінка не відкривається або на ній немає назви продукту з project.yaml (~/www/index.html).
L02.11токен бота лежить у ~/app/.env
автоперевірка
У ~/app/.env немає рядка BOT_TOKEN=… Скопіюй токен від @BotFather у .env — не в код.
L02.12.env не читається сусідами по серверу
автоперевірка
Файл ~/app/.env можуть прочитати інші учні. Виконай: chmod 600 ~/app/.env
L02.13Telegram визнає токен (getMe)
автоперевірка
Telegram не прийняв токен. Перевір, що скопіював його повністю, без пробілів. Якщо токен загубився чи потрапив у чужі руки — @BotFather, /token, і новий токен у .env.
L02.14бот відповідає на /start
автоперевірка
handle_update() на /start нічого не повернула або впала. Перевір: python3 try_bot.py "/start"
L02.15служба опитує Telegram (polling працює)
автоперевірка
Бот не опитує Telegram. Перевір, що служба club-bot active (systemctl --user status club-bot), що в .env є BOT_TOKEN, і перезапусти службу: systemctl --user restart club-bot. У журналі ~/logs/club-bot.log не має бути «Telegram недоступний».

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

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

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

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

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

  1. Опис і меню бота. В @BotFather надішли /setdescription, вибери бота й напиши, що він уміє, — цей текст люди бачать до натискання «Почати». Потім /setcommands і два рядки у форматі «команда - опис»: start - привітання і help - що я вмію. У чаті з ботом з’явиться кнопка меню. Код і токен для цього не потрібні.
  2. Перевірка «без мене». Увечері, між 17:00 і 23:00 (з неділі по п’ятницю), коли твій комп’ютер вимкнено, попроси когось із рідних чи друзів написати боту /start і будь-яке речення. Запиши, що бот відповів і як швидко.
  3. Дослід із чергою. Напиши боту, коли служба групи вимкнена, — наприклад, зранку перед школою. Зафіксуй, о котрій прийшла відповідь, і поясни двома реченнями, чому саме тоді (підказка — теорія, розділ 3).
  4. Поясни рядок. Підготуйся пояснити своїми словами будь-який рядок handle_update() і poll_forever(): що він робить і що станеться, якщо його прибрати. На початку наступного заняття викладач покаже рядок і спитає.
  5. Бекап раз на тиждень. Повтори крок 20 удома: зроби новий архів, перевір його й заведи звичку робити так після кожної великої зміни. Запитай себе: що я втрачу, якщо завтра цей сервер зникне, і де в мене друга копія?
  6. Ще один коміт. Якщо вдома змінював тексти бота, перевір їх через try_bot.py, перезапусти службу й збережи роботу: git add -A, git commit -m "Заняття 02: нові тексти бота", git push. На GitHub має бути вже два твої коміти.
  7. Схема «хто до кого». На аркуші намалюй шлях повідомлення від друга до відповіді бота: хто звертається до кого, у який бік іде стрілка і де нічого не слухає порт. Поясни двома реченнями, чому в нашого бота немає власного API-сервера.

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

Після правки bot.py служба не працює (L02.1): activating, failed або перезапускається по колу

Служба club-bot запускає run.py, а той підключає bot.py. Тому описка в тексті бота валить усю службу, і systemd кожні три секунди пробує запустити її знову. Почни з цього:

systemctl --user status club-bot
tail -n 40 ~/logs/club-bot.log
cd ~/app && /opt/club/venv/bin/python3 -c "import bot" && echo готово

Остання команда показує помилку імпорту відразу. SyntaxError чи IndentationError з File "…/app/bot.py", line 47 — лапки, дужка чи відступи там або рядком вище; No module named 'bot' або 'env' — файлу немає в ~/app (крок 3); ScannerError — project.yaml (крок 5). Виправ і зроби systemctl --user restart club-bot. Якщо стан failed з start request repeated too quickly, спершу systemctl --user reset-failed club-bot, потім restart. Службу також не підніме відсутній run.py: ls ~/app/run.py, а в ExecStart (крок 12) має бути run.py. І ще одна причина, у якій ти не винен: поза розкладом служби групи вимкнено — тоді is-active каже inactive, а помилок у журналі немає.

try_bot.py друкує помилку замість відповіді

can't open file — виконай cd ~/app. No module named 'yaml' — запусти /opt/club/venv/bin/python3 try_bot.py "/start". KeyError: 'product' чи ScannerError — перевір project.yaml командою з кроку 5. SyntaxError чи NameError — описка в bot.py; порівняй свій файл з оригіналом:

diff /opt/club/kurs/02/bot.py ~/app/bot.py

diff друкує лише рядки, що відрізняються: з < — як в оригіналі, з > — як у тебе. Так видно все, що ти міняв. Якщо заплутався зовсім, почни спочатку — cp /opt/club/kurs/02/bot.py ~/app/bot.py, — але свої тексти доведеться написати знову.

try_bot.py відповідає, а в Telegram бот мовчить

systemctl --user is-active club-bot
cat ~/data/bot_state.json
tail -n 30 ~/logs/club-bot.log | grep -E "Telegram|надіслати|обробки|BOT_TOKEN"
curl -s "https://api.telegram.org/bot$(grep '^BOT_TOKEN=' ~/app/.env | cut -d= -f2)/getMe"

{"ok":true,"result":{…,"is_bot":true,…,"username":"kosmos_bohdan_bot",…}} — токен справжній, і видно, якого він бота. {"ok":false,"error_code":401,"description":"Unauthorized"} — візьми токен в @BotFather ще раз (/token і вибери свого бота, або /mybots → свій бот → API Token), впиши в .env і перезапусти службу. Якщо токен міг потрапити до чужих — не бери старий, а заміни його через /revoke. Temporary failure in name resolution чи timed out у журналі — у сервера зараз немає зв’язку з Telegram: скажи викладачеві, бот повернеться сам.

Бот пише «Ой, щось пішло не так», відповідає двічі або через раз

«Ой, щось пішло не так» — handle_update() впала. Причину покаже tail -n 30 ~/logs/club-bot.log | grep "помилка обробки", наприклад помилка обробки: NameError("name 'nmae' is not defined") — описка в імені. Повтори те саме повідомлення через try_bot.py: там буде й номер рядка. Виправ bot.py і перезапусти службу.

Відповідає двічі, через раз або в журналі HTTP Error 409: Conflict — з твоїм токеном опитує Telegram ще якась програма: ти запускав бота ще десь або комусь дав токен. Зупини ту копію; не знаєш, де вона, — /revoke в @BotFather, новий токен у .env, systemctl --user restart club-bot. Стара копія отримуватиме лише 401.

L02.2 або L02.6 не зараховано, хоча все заповнено

grep -nE "name:|author:" ~/app/project.yaml
python3 try_bot.py "/about"

Для L02.2 у відповіді на /about мають бути і назва, і ім’я з project.yaml, написані точно так само. Заготовки ЗМІНИ МЕНЕ та Ім'я перевірка не приймає. Ти написав: /about — команду не додано або вона стоїть нижче за перевірку if text.startswith("/") (крок 7). Відповідь без твого імені — ти змінив project.yaml після запуску: перевірка читає файл щоразу заново, тож досить виправити текст. Для L02.6 назва на сторінці має збігатися з name до символу: заважають інші лапки, подвійний пробіл, велика літера, апостроф. Перевірка шукає назву в коді сторінки, а не в тому, що малює браузер, тому апостроф пиши символом, а не кодом на зразок &#39;. Найпростіше — скопіювати назву з project.yaml і вставити в index.html.

ssh -T чи git push до GitHub не проходять

Host github.com
    Hostname ssh.github.com
    Port 443
    User git

git status показує .env або .env уже потрапив у коміт

Видно серед Untracked files, а коміту ще не було, — допиши правило: cd ~/app && echo ".env" >> .gitignore, потім git check-ignore -v .env назве його, а git status файлу не покаже. Якщо коміт уже був, перевір історію:

git log --oneline --all -- .env

Порожня відповідь — усе гаразд. Надрукувався хоч один коміт — токен лежить в історії git. Нічого не відправляй і скажи викладачеві: разом приберете файл з історії. А токен вважай украденим, навіть якщо до GitHub він не доїхав: /revoke в @BotFather, новий токен у .env, перезапуск служби. Чому секрет, що побував у коміті, уже не секрет, розберемо на наступному занятті.