← До списку уроків
Заняття 11 з 13Бот-сторож: спам і збої
курс 0% виконано0%
Заняття 11 · місяць 3

Бот-сторож: спам і збої

90 хвилин

Навчити бота захищатися від спаму (ліміт повідомлень, тимчасове мовчання в таблиці blocked) і поставити окремого сторожа (watch.py), який стежить за сайтом і ботом і пише власнику, коли щось упало й коли відновилося, — по одному повідомленню на подію.

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

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

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

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

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

Показати можна буде: імітацію спаму, де бот замовкає (python3 spam.py), повідомлення «🔴 … впало» і «🟢 … знову працює» і відповідь на /status.

Якщо в тебе немає TelegramНічого не втрачено. Спам імітує програма spam.py, команди власника перевіряє try_bot.py --id …, а сповіщення сторожа, якщо немає куди писати, потрапляють у журнал ~/logs/club-watch.log з позначкою [без Telegram]. Живе повідомлення в Telegram лише підтверджує викладач.

Теорія

1. Що таке спам і як відрізнити його від людини

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

Є тонкість: охоронець не має ображати звичайних людей. Друг, який розсердився й надіслав шість повідомлень, — не спамер. Тому покарання йде сходинками: перше порушення — лише чемне «зачекай хвилину», і це повідомлення бот не обробляє; наступне порушення — мовчання без жодної відповіді (щоб спамер не отримував «нагороди» у вигляді відповідей); третє порушення за хвилину — тимчасове мовчання на 10 хвилин. Не назавжди: помилитися може кожен, а вічний бан без права на помилку — несправедливий.

Чому «ліміт на хвилину», а не «ліміт на весь день»? Бо цікавить швидкість. Стільки ж повідомлень за день — нормальна розмова, за хвилину — атака.

Кнопки під повідомленнями бота (заняття 10) теж можна «засипати» натисканнями, і кожне натискання — це запис у базу. Тому охоронець рахує й їх, але щедріше: до 20 натискань за хвилину (CLICK_LIMIT), бо кнопки — звичайна частина розмови.

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

Друг написав боту 6 повідомлень за хвилину, бо розгнівався. Що має зробити охоронець за нашими правилами?

2. Де зберігати лічильники, а де — покарання

Охоронцю потрібно пам’ятати двоє різних речей, і зберігаються вони по-різному.

Лічильники повідомлень («цей chat_id писав о 14:01:05, 14:01:06, 14:01:06…») потрібні лише на хвилину-дві. Їх тримаємо в оперативній пам’яті програми — у словнику _recent: для кожного chat_id — черга останніх повідомлень з часом (тип deque, «двостороння черга»: старе легко викидати спереду, нове додавати ззаду). Якщо бот перезапуститься, лічильники обнуляться — і це нормально.

Покарання («замовкни до 14:11») має жити довше й не зникати від перезапуску бота. Інакше спамеру достатньо дочекатися, поки ти оновиш бота, — і він знову на волі. Тому мовчання записуємо в базу, в таблицю blocked:

CREATE TABLE IF NOT EXISTS blocked(
  chat_id INTEGER PRIMARY KEY,   -- кого мовчимо (один рядок на людину)
  until   INTEGER NOT NULL,      -- до якої миті, у секундах
  reason  TEXT NOT NULL,         -- чому: fast, repeat чи word
  created TEXT NOT NULL          -- коли записали
)

Рядок додається командою INSERT OR REPLACE («додай, а якщо такий chat_id вже є — заміни»). Коли строк минув, охоронець сам видаляє рядок (DELETE). Час записуємо числом — «секунди від 1 січня 1970» (так робить time.time()), бо числа легко порівнювати: until > clock() означає «ще мовчимо».

І остання деталь: охоронець бере час не прямо з time.time(), а через clock — спершу це просто time.time під іншим іменем. Це маленька хитрість для перевірок. Щоб довести, що блокування закінчується через 10 хвилин, не треба чекати 10 хвилин: перевірка підміняє clock на «годинник», який можна перемотати. У програмуванні так роблять часто, і ти бачитимеш це в тестах на занятті 12.

3. Кого блокувати: номер чату, а не ім’я

Блокувати можна лише за chat_id — тим самим числом, яким ми визначали власника на занятті 07. Ім’я та нік змінює собі кожен за секунду. Якби ми блокували за іменем «Спамер», то по-перше, спамер змінив би ім’я й продовжив; по-друге, тисячі цілком чемних людей, що теж звуться «Саша», потрапили б під удар. Номер чату видає Telegram, і підробити його не вдасться.

Власника не блокуємо ніколи. Це перший рядок у guard.check: якщо chat_id збігається з номером власника (перший у ADMIN_IDS) — пропускаємо без розмов. Уяви, що ти тестуєш бота й надсилаєш двадцять команд поспіль, а власний захист заблокував тебе самого. Саме тому для власника ще й існують /unblock і /blocked: якщо охоронець помилково заблокував гарну людину, ти її розблокуєш однією командою.

Спамер, якого заблоковано, змінив своє ім’я в Telegram на «Оля» й знову пише. Що зробить охоронець?

4. Два види перевірок: «чи відповідає?» і «чи не завис?»

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

1. Постукати в сайт. Відкрити сторінку сайту й подивитися, що повернув сервер. Відповідь 200 означає «усе гаразд» (ти вже бачив коди відповідей на занятті 06). Сторож робить HTTP-запит до http://127.0.0.1/u/<логін>/. Ця сторінка — звичайні файли з ~/www, їх роздає nginx (програма, що приймає запити з інтернету), і окремої програми для цього не потрібно. Якщо сторінки немає, nginx віддає код 403 або 404. Запам’ятай, він знадобиться в кроках 8 і 11.

Бот не відкриває порту, тому «постукати» в нього не вийде. Його перевіряють по-іншому (друга перевірка).

2. Перевірити, чи не завис бот. Тут команда systemctl --user is-active club-bot не допоможе: служба може бути «активна», а бот усередині застряг — наприклад, назавжди чекає відповіді від Telegram. Пам’ятаєш last_poll у файлі ~/data/bot_state.json? Бот записує туди час кожної відповіді, яку отримав від Telegram, тож якщо цей час давніший за дві хвилини, бот не працює, хоч би що показував is-active. Таку перевірку називають перевіркою «живості».

Висновок, який варто запам’ятати: стежити треба за результатом (чи відповідає сайт, чи рухається бот), а не за тим, що «процес існує».

5. Сторож — окрема програма, і він не кричить даремно

Чому сторожа не можна вбудувати в самого бота? Бо коли бот упав, він уже нічого не скаже: мертвий сторож нікого не попередить. Тому watch.py — окрема програма, яку будить таймер systemd щохвилини (так само, як morning.py на занятті 09). Бот може лежати, а сторож усе одно прокинеться, подивиться і напише.

Але сторож, який пише щохвилини «сайт лежить, сайт лежить, сайт лежить…», за годину набридне, і ти його вимкнеш. Тоді він марний. Тому дві умови:

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

Школа вимикає службу вночіТвоя служба працює лише в години занять і ввечері. Поза цим часом школа її зупиняє, і сторож напише «🔴 впало», а потім «🟢 знову працює», коли службу ввімкнуть. Це не збій, а розклад, але сторож цього не знає й сповістить про «аварію». Щоб не отримувати повідомлень уночі, після заняття можна зупинити таймер: systemctl --user stop club-watch.timer. Після перезавантаження сервера таймер знову запуститься сам.

Сайт «лежить» уже годину. Скільки повідомлень про це має прийти від правильного сторожа за цю годину?

Служба club-bot показує active, а last_poll у bot_state.json — десятихвилинної давності. У club-bot.log за ці десять хвилин — жодного нового рядка. Що це означає?

6. Журнал як панель приладів: команда /status

Коли ти за кермом, тобі потрібна не вся механіка автомобіля, а кілька приладів на панелі: швидкість, пальне, лампочки несправностей. Для бота такою «панеллю» буде команда /status — вона працює лише для власника:

📊 Стан бота
Бот працює: 2 год 14 хв
Останнє опитування Telegram: 12 с тому
Заблоковано зараз: 1
Останні помилки у журналі:
• 2026-10-25T14:03:11 помилка обробки: KeyError('name')

«Бот працює: 2 год 14 хв» — це аптайм (від англ. up — «вгору», «працює»): скільки часу програма працює без перезапуску. Якщо вчора було 20 годин, а сьогодні 3 хвилини — бот перезапускався, і варто з’ясувати чому. Останні помилки /status бере з журналу ~/logs/club-bot.log: охоронець переглядає кінець файлу й вибирає рядки зі словами «помилка», «Traceback», «не вдалося». Власний журнал, який ти ще з заняття 02 читаєш руками, тепер приходить у Telegram сам.

Якщо ти запускаєш /status через try_bot.py, аптайм буде «0 с»: програма try_bot.py щойно стартувала. Справжній аптайм видно лише у відповіді живого бота в Telegram.

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

СловоЩо означає
спамНебажані повідомлення, які надсилають швидко й масово
лімітНайбільше, що дозволено: наприклад, 5 повідомлень за хвилину
тимчасове мовчання (бан)Бот на певний час не відповідає людині. Записано в таблиці blocked разом з часом, до якого мовчимо
chat_idНомер чату, який присвоїв Telegram. За ним блокують, а не за іменем
перевірка живостіПеревірка, чи служба справді працює: відповідає сайт, оновлюється last_poll
охоронецьЧастина бота (guard.py), яка рахує повідомлення й мовчить зі спамером
сторож (watchdog)Окрема програма (watch.py), яка стежить за сайтом і ботом та повідомляє про збої
сповіщенняПовідомлення від програми людині: «щось сталося»
хибна тривогаСповіщення про збій, якого не було (наприклад, служба просто перезапускалася)
аптаймСкільки часу програма працює без перезапуску
панель приладів (дашборд)Одне місце, де видно головні числа про стан системи

Практика

Виконуй кроки по черзі. У прикладах <логін> заміни на свій, наприклад bohdan.g9 (дужки теж прибери). Усе, що змінюєш у bot.py, роби поступово й після кожної правки запускай python3 try_bot.py …: так помилка знайдеться одразу.

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

ssh <логін>@91.219.61.4
cd ~/app
git status --short
systemctl --user is-active club-bot
grep -c '^ADMIN_IDS=[0-9]' ~/app/.env
ls /opt/club/kurs/11/

2 Скопіювати guard.py і прочитати його

cp /opt/club/kurs/11/guard.py /opt/club/kurs/11/spam.py ~/app/
cd ~/app
less guard.py

Гортай стрілками, вийти — q. Знайди у файлі те, про що була теорія:

Файл можна використовувати вже зараз, окремо від бота:

python3 -c "import guard; print(guard.check(555, 'привіт', ''))"
sqlite3 ~/data/app.db ".schema blocked"

Перша команда друкує (True, None): нова людина, нормальне повідомлення — пропустити. Вона ж створила таблицю blocked (функція ensure()), і друга команда показує її опис: чотири стовпці chat_id, until, reason, created. Якщо No module named 'yaml' — запускай /opt/club/venv/bin/python3 замість python3.

3 Підключити охоронця до bot.py

Відкрий ~/app/bot.py (nano ~/app/bot.py). Потрібні чотири невеликі вставки. Готовий приклад лежить у /opt/club/kurs/11/bot.py: це бот із заняття 10 плюс ці вставки. Порівняй свій файл з ним: diff ~/app/bot.py /opt/club/kurs/11/bot.py. Порожній вивід означає «однакові». Якщо ти додавав у бот щось від себе, ці рядки теж з’являться в diff — це нормально. Не копіюй еталон поверх свого файлу «для швидкості»: власні правки пропадуть.

Вставка 1. Під import scripts додай імпорт, а під OWNER_COMMANDS — список команд сторожа:

import guard  # заняття 11
...
GUARD_COMMANDS = ("/status", "/blocked", "/unblock")   # теж лише для власника

Вставка 2. У handle_update, одразу після if not text: return None і до перевірки access.check та remember(sender):

    ok, reply = guard.check((msg.get("chat") or {}).get("id"), text, OWNER)   # заняття 11
    if not ok:
        return reply                                   # спамера не слухаємо

Місце важливе: охоронець стоїть першим. Якби він стояв після remember(), спамер встиг би засмітити базу користувачів, а після команд — виконати їх. Функція повертає пару, тому ми одразу розкладаємо її на дві змінні: ok («пропускати?») і reply («що відповісти, якщо ні»).

Вставка 3. Там само, де в занятті 09 стоїть перевірка OWNER_COMMANDS (одразу перед нею):

    if text.split()[0].split("@")[0].lower() in GUARD_COMMANDS:      # заняття 11
        if not OWNER or str(chat_id) != OWNER:
            return "Ця команда лише для власника бота."
        return guard.command(text)

Вставка 4. У циклі poll_forever, у гілці if update.get("callback_query"): (де заняття 10 обробляє натискання кнопок), одразу під цим рядком:

                who = (update["callback_query"].get("from") or {}).get("id")
                if not guard.check_click(who, OWNER)[0]:       # заняття 11: кнопки теж рахуємо
                    continue

[0] бере з пари лише першу частину («пропускати?»), а continue означає «це натискання пропусти й іди до наступного». Відступи важливі: ці рядки стоять на тому самому рівні, що й наступний за ними try:.

Збережи (Ctrl+O, Enter, Ctrl+X) і перевір:

python3 try_bot.py --id 555 "/whoami"
python3 try_bot.py --id 555 "/status"

Очікувано: Твій chat_id: 555, а потім Ця команда лише для власника бота. Помилка SyntaxError чи IndentationError — подивись рядок, який назвав Python: найчастіше зсунуто відступ або забуто двокрапку. ModuleNotFoundError: No module named 'guard' — на кроці 2 файл не скопійовано.

4 Влаштувати спам і подивитися, що робить охоронець

Telegram-спам ми влаштовувати не будемо, а скористаємося імітатором spam.py. Він працює на копії бази, тож твоїх справжніх даних не чіпає і в Telegram нічого не надсилає. «Спамер» — номер чату 666, який пише 12 повідомлень кожні 0,2 секунди:

python3 spam.py

Побачиш приблизно таке:

#1   ✔  Твій chat_id: 666
...
#5   ✔  Твій chat_id: 666
#6   ⚠  Ти пишеш надто швидко. Зачекай хвилину — і продовжимо.
#7   ⛔ (бот мовчить)
#8   ⚠  Забагато повідомлень. Я помовчу 10 хв — повертайся пізніше.
#9   ⛔ (бот мовчить)
...
Таблиця blocked після спаму:
Заблоковані:
666 — до 14:51 (надто часто)

Розбери по сходинках. Перші п’ять повідомлень пройшли (✔) — це ліміт LIMIT = 5. Шосте — перше порушення: чемне прохання зачекати (⚠). Сьоме — друге порушення: тиша (⛔). Восьме — третє: бот повідомляє про мовчання на 10 хвилин, і відтоді мовчить. Час «до 14:51» — київський. Якщо замість цього всі 12 повідомлень пройшли — перевір вставку 2 з кроку 3. Хочеш побачити, як це виглядає для іншого номера: python3 spam.py --id 777 --count 30.

5 Перевірити, кого охоронець не чіпає, і що мовчання закінчується

python3 spam.py --pause 20 --id 888 --name Оля --count 8
OWNER=$(grep '^ADMIN_IDS=' ~/app/.env | cut -d= -f2 | cut -d, -f1)
python3 spam.py --id $OWNER --count 20 | grep -c ⛔
python3 spam.py --minutes 11

Підказка: годинник перемотується, бо spam.py підміняє guard.clock (теорія, розділ 2). У справжньому боті час іде звичайним порядком.

6 Команди власника: /status, /blocked, /unblock

Усе виконуємо від твого номера, змінна OWNER з минулого кроку (якщо ти вийшов із сервера, задай її знову):

python3 try_bot.py --id $OWNER "/status"
python3 -c "import guard; guard.ban(123456, 'word')"
python3 try_bot.py --id $OWNER "/blocked"
python3 try_bot.py --id $OWNER "/unblock 123456"
python3 try_bot.py --id $OWNER "/blocked"
echo "$(date +%FT%T) помилка обробки: тест для /status" > ~/logs/probe.log
CLUB_BOT_LOG=~/logs/probe.log python3 try_bot.py --id $OWNER "/status"
rm ~/logs/probe.log

Що буде: перший /status показує п’ять рядків (Бот працює: 0 с — пояснення в теорії, розділ 6; Заблоковано зараз: 0). Друга команда записує в справжню базу вигаданого порушника з номером 123456 — але п’ята команда його одразу прибирає, тож нічого не лишається. /blocked показує його: 123456 — до 14:55 (заборонене слово). /unblock 123456 відповідає Розблоковано., і наступний /blocked знову порожній. Далі ми створили окремий пробний журнал probe.log з рядком зі словом «помилка», підказали охоронцю читати саме його (змінна CLUB_BOT_LOG діє лише для цієї однієї команди) і побачили, як /status показує рядок під «Останні помилки у журналі». Справжній журнал club-bot.log ми не чіпали, а rm прибрав пробний. Справжні помилки бота з’являтимуться в /status самі.

Усе це від чужого номера не працює: python3 try_bot.py --id 555 "/blocked" відповідає Ця команда лише для власника бота. — перевір.

Пробні номери 555 (кроки 3 і 12) бот записав у таблицю users як нових людей. Прибери їх, щоб вони не потрапили до звітів: sqlite3 ~/data/app.db "DELETE FROM users WHERE chat_id = 555".

7 Запустити охоронця в справжньому боті

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

У журналі не має бути Traceback чи ModuleNotFoundError. Якщо у тебе є Telegram: напиши своєму боту /status і подивися відповідь — тепер аптайм справжній (кілька секунд). Попроси друга швидко надіслати боту 7–8 повідомлень поспіль і подивися, що бот йому відповів і що показує /blocked. Без Telegram цей крок можна пропустити: ти вже бачив те саме у кроці 4.

8 Другий захист: сторож служб, пробний запуск

cp /opt/club/kurs/11/watch.py ~/app/
cd ~/app
python3 watch.py --dry

--dry («сухий прогін») — лише показати стан, нічого не надсилаючи й не запам’ятовуючи. Побачиш три рядки:

Сайт  ✅ код 200
Бот   ✅ last_poll 14 с тому
Усе гаразд.

Якщо щось не так, шукай свій рядок у таблиці:

Що бачишЩо це означаєЩо робити
Бот ➖ пропущено: у .env немає BOT_TOKENТокен бота ще не вписано, перевірити бота нічимНічого: це нормально
Бот ❌ last_poll 600 с томуБот давно не питав TelegramЗачекай хвилину після перезапуску служби; не минає — systemctl --user start club-bot і tail -n 30 ~/logs/club-bot.log
Сайт ❌ код 403 або 404У ~/www немає головної сторінкиls ~/www: має бути index.html

Відкрий less ~/app/watch.py і знайди функції probe_url, probe_bot, run, а в run — слова fails і alerted: там зашита логіка «один раз на подію».

9 Поставити сторожа на таймер

cp /opt/club/kurs/11/club-watch.service /opt/club/kurs/11/club-watch.timer ~/.config/systemd/user/
systemctl --user daemon-reload
systemctl --user enable --now club-watch.timer
systemctl --user list-timers club-watch.timer
systemctl --user start club-watch.service
tail -n 3 ~/logs/club-watch.log

Ці два файли ти вже знаєш із заняття 09: .service описує, що запускати (watch.py), а .timer — коли. Різниця від ранкового звіту: замість OnCalendar=*-*-* 08:00 тут OnCalendar=minutely — «щохвилини». list-timers покаже рядок із club-watch.timer і часом наступного запуску (NEXT — за хвилину). Остання команда: у журналі з’явиться рядок на зразок 14:41:07 перевірено — тихо: сторож перевірив, усе гаразд, повідомляти не про що.

Якщо list-timers порожній або команди пишуть Unit … not found — файли не скопійовано або забуто daemon-reload.

10 Аварія: зупиняємо службу самі

Зараз ти сам зупиниш службу свого бота й побачиш, як сторож про це повідомить. Нічого не зламається: службу запустимо назад.

systemctl --user stop club-bot
python3 ~/app/watch.py --dry

Спершу Бот ще ✅: last_poll свіжий, і він застаріє лише за дві хвилини. Рядок «Сайт» лишиться ✅ весь час: сторінки з ~/www роздає nginx, йому служба бота не потрібна. Повтори --dry через дві-три хвилини — побачиш Бот ❌ last_poll 150 с тому (той самий, що в таблиці з кроку 8). Якщо вийшло інакше — запиши в README, що саме ти побачив: це теж результат.

Тепер чекай, поки сторож «побачить» збій двічі поспіль: приблизно 2–4 хвилини. Час від часу дивись у журнал сторожа: tail -n 3 ~/logs/club-watch.log. Коли сторож збій помітить, у Telegram прийде повідомлення:

🔴 Бот: last_poll 150 с тому. Так вже 1 хв.

«1 хв» — це час між першою невдалою перевіркою й другою, на якій сторож і повідомив. Тож справжня поломка триває трохи довше. Без Telegram ті самі рядки ти побачиш у журналі: tail -n 6 ~/logs/club-watch.log (з позначкою [без Telegram]). Зверни увагу: якщо ти чекаєш уже п’ять хвилин, нових повідомлень про той самий збій немає. Це і є «один раз на подію». Тепер піднімаємо:

systemctl --user start club-bot
cat ~/data/watch_state.json

У файлі watch_state.json видно стан кожної перевірки: "alerted": true там, де сторож уже повідомив про збій. Служба вже працює, але сторож про це ще не знає: він дізнається під час наступної перевірки. Зачекай:

sleep 120
tail -n 4 ~/logs/club-watch.log
cat ~/data/watch_state.json

sleep 120 просто чекає дві хвилини, і термінал здаватиметься завислим — це нормально. Потім з’явиться друге повідомлення: 🟢 Бот знову працює (не працював 4 хв)., і в файлі alerted знову false. Збій закрито: два повідомлення на одну подію, жодного зайвого.

Не прийшло нічого за 5 хвилин? Перевір: systemctl --user list-timers club-watch.timer (таймер має мати час NEXT), tail -n 20 ~/logs/club-watch.log (там помилка) і що в .env є BOT_TOKEN та OWNER_CHAT_ID.

11 Ще дві поломки: аварійне вбивство і зникла сторінка

Перша. Нагадаю: службу можна вбити, і вона сама підніметься (Restart=always, заняття 08). Команда kill -s KILL — це те саме аварійне вбивство, що kill -9 у занятті 08, але вона зачіпає лише процеси служби club-bot, а не твою сесію.

systemctl --user kill -s KILL club-bot; sleep 8
systemctl --user is-active club-bot
python3 ~/app/watch.py --dry

Усе ✅, і сповіщення немає — служба підвелася швидше, ніж сторож встиг побачити збій двічі. Це не вада, а захист від хибних тривог (теорія, розділ 5).

Друга — зникла сторінка сайту. Тимчасово сховай головну сторінку й подивись. Команда mv перейменовує файл: index.html стає index.off, і nginx не знаходить головної сторінки. Третя команда повертає ім’я назад, тому не пропусти її:

mv ~/www/index.html ~/www/index.off
python3 ~/app/watch.py --dry
mv ~/www/index.off ~/www/index.html
python3 ~/app/watch.py --dry

Після першого --dry побачиш Сайт ❌ код 403 (або 404): сторінки немає, а бот при цьому ✅ — бачиш, чому сайт і бот перевіряються окремо. Третя команда повертає сторінку назад, четверта знову показує ✅. Не забудь повернути файл, інакше твій сайт лишиться без головної сторінки; у разі чого: ls ~/www покаже, чи є index.html.

12 Налаштувати охоронця під себе

Усі цифри в guard.py — просто змінні вгорі, і їх можна міняти. Відкрий файл (nano ~/app/guard.py) і зроби три експерименти. До кожного — своя команда й свій очікуваний результат:

  1. Додай у BAD_WORDS одне своє слово, яке не хочеш бачити в чаті, збережи й виконай python3 try_bot.py --id 555 "привіт, моє-слово". Має прийти Такі слова я не приймаю. Напиши інакше.
  2. Постав BAN_SEC = 60 (мовчання на хвилину), збережи й виконай python3 spam.py --count 12 --minutes 2. Спершу бот замовкне, а після рядка Минуло 2 хв. Людина пише знову: з’явиться ✔: мовчання закінчилося.
  3. Поверни BAN_SEC = 600 (перевірка вимагає не менше 60 с) і LIMIT = 5, збережи й виконай python3 spam.py. Має вийти те саме, що в кроці 4 (⚠ на шостому повідомленні, ⛔ на сьомому). Надто низький ліміт образить звичайних людей, надто високий не врятує від спаму.

Збережи й перезапусти службу: systemctl --user restart club-bot. Не змінюй у guard.py назви clock, check, check_click, ban і таблицю blocked: на них спираються перевірки. Після експериментів ще раз прибери пробного «користувача» 555 командою sqlite3 з кроку 6.

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

У репозиторій потраплять guard.py, watch.py, spam.py і змінений bot.py. Стан сторожа (~/data/watch_state.json), база з таблицею blocked, журнали та файли таймера лишаються поза репозиторієм.

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

cd ~/app
git status --short
git add -A
git commit -m "Заняття 11: охоронець від спаму, сторож watch.py, /status"
git push origin main
git push github main

git status --short покаже M bot.py і нові ?? guard.py, ?? watch.py, ?? spam.py. Бачиш .env чи app.db — зупинись: їх у списку не мало бути (заняття 03). Кожен git push закінчується рядком main -> main. Якщо в тебе гілка master, пиши master.

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

Автоперевірка нічого не пише в твій Telegram, не чіпає справжньої бази й не зупиняє твоїх служб: вона запускає бота на копії бази, з підставним годинником і підставним сайтом. Спершу засипає бота повідомленнями від чужого номера й чекає, що він замовкне, а в таблиці blocked з’явиться запис; так само перевіряє кнопки й заборонене слово. Потім дивиться, що звичайну людину й власника не блокують, а мовчання закінчується через BAN_SEC. Далі перевіряє /status і сторожа: на підставному сайті, який «лежить», watch.py має написати рівно одне повідомлення про збій і одне про відновлення. Наприкінці дивиться, що таймер сторожа встановлено й увімкнено. Аварію зі зупиненою службою (крок 10) бачить викладач.

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

КодЩо перевіряєтьсяЯкщо не зараховано
L11.1спамера ігнорують після ліміту, заборонене слово не проходить
автоперевірка
Скопіюй guard.py з /opt/club/kurs/11/ у ~/app і зроби вставки в bot.py (крок 3 уроку): guard.check(...) у handle_update і guard.check_click(...) для кнопок у poll_forever. Перевір: python3 spam.py має показати ⚠ і ⛔.
L11.2звичайну людину не блокують, навіть із тим самим іменем, що в спамера
автоперевірка
Блокувати можна лише за chat_id. Не міняй у guard.py LIMIT на 1-2 і не став REPEAT менше трьох.
L11.3власника не блокують
автоперевірка
У guard.check першим рядком: якщо chat_id збігається з OWNER (аргумент owner) — return True, None.
L11.4блокування закінчується через BAN_SEC
автоперевірка
У banned_until порівнюй until з clock(), а не з time.time(); коли строк вийшов, видаляй рядок з blocked.
L11.5/status: аптайм, скільки заблоковано, помилки з журналу — лише власнику
автоперевірка
GUARD_COMMANDS і блок із guard.command(text) у handle_update (вставка 3); перевірка власника має стояти в ньому, перед guard.command.
L11.6watch.py --dry показує стан Сайт, Бот
автоперевірка
cp /opt/club/kurs/11/watch.py ~/app/ ; cd ~/app && python3 watch.py --dry. Не прибирай з виводу слова Сайт, Бот і ❌.
L11.7коли сайт лежить, сторож пише рівно один раз, і ще раз — коли він підвівся
автоперевірка
Не змінюй логіку run() у watch.py: лічильник fails, прапорець alerted і файл ~/data/watch_state.json. Оригінал: /opt/club/kurs/11/watch.py
L11.8таймер сторожа встановлено й увімкнено
автоперевірка
cp /opt/club/kurs/11/club-watch.* ~/.config/systemd/user/ ; systemctl --user daemon-reload && systemctl --user enable --now club-watch.timer
L11.9практика-аварія: зупинили службу — сповіщення про збій і про відновлення прийшли
викладач
Позначку ставить викладач на занятті або після нього.

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

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

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

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

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

  1. Подивись, що сторож записав за ніч. Завтра вранці виконай tail -n 20 ~/logs/club-watch.log і cat ~/data/watch_state.json. Школа вимикає служби груп уночі, тож, якщо таймер не зупиняли, там буде «🔴» і «🟢». Запиши в README, о котрій годині це сталося і скільки часу минуло між ними. Не хочеш нічних повідомлень — systemctl --user stop club-watch.timer.
  2. Четверта перевірка. Додай у watch.py перевірку вільного місця на диску: новий пункт у функції look(). Угорі файлу, до інших import, додай import shutil, а look() зроби такою (два перші пункти вже є, не забудь кому після ("bot", …)):
    def look():
        free = shutil.disk_usage(str(HOME)).free // 10**9      # вільно, ГБ
        return [("site", "Сайт", *probe_url(SITE)),
                ("bot", "Бот", *probe_bot()),
                ("disk", "Диск", free >= 1, f"вільно {free} ГБ")]
    Перевір: python3 watch.py --dry має показати рядок «Диск». Більше нічого міняти не треба: подумай, чому run() сам підхопить нову перевірку.
  3. Придумай обхід — і не користуйся ним. Як би спамер міг обійти твого охоронця (підказка: що, якщо писати раз на 13 секунд і по-різному?). Запиши ідею в README одним реченням і поясни, чому охоронець усе одно потрібен.
  4. Підготуйся пояснити. Чому блокують за chat_id, а не за іменем; навіщо блокування записано в таблицю, а не лише в пам’ять; чому сторож пише один раз на подію; чому служба може бути active, а бот — ні. На початку заняття 12 викладач спитає щось одне з цього.
  5. Збережи. git add -A, git commit -m "Заняття 11: домашня робота", git push origin main і git push github main.

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

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

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

L11.1: спамера не зупинили

L11.2: звичайну людину заблоковано

Найчастіше причина — змінений LIMIT. Якщо ти поставив 1–2, людина, що пише раз на 20 секунд, швидко потрапить під обмеження; поверни LIMIT = 5, WINDOW = 60, REPEAT = 3. Друга причина — блокування за іменем замість chat_id: у guard.py ключем скрізь має бути chat_id.

L11.3: власника заблоковано

Перший рядок у guard.check має пропускати власника: if chat_id is None or (owner and str(chat_id) == str(owner)): return True, None. У bot.py третім аргументом має йти OWNER: guard.check(chat_id, text, OWNER). Якщо на кроці 1 було 0 замість 1 — у .env немає OWNER_CHAT_ID; власник тоді не відомий, і охоронець блокує всіх.

L11.4: блокування не закінчується

L11.5: /status

L11.6 або L11.7: сторож

L11.8: таймер сторожа