Перед уроком
Обери свою групу й себе зі списку. Тоді відповіді й перевірки завдань запишуться саме на тебе.
Навчити бота захищатися від спаму (ліміт повідомлень, тимчасове мовчання в таблиці blocked) і поставити окремого сторожа (watch.py), який стежить за сайтом і ботом і пише власнику, коли щось упало й коли відновилося, — по одному повідомленню на подію.
Замість <логін> підставляєш свій
логін виду bohdan.g9: імʼя, крапка, номер групи. Кутові дужки не пишуться.
Кабінет — сторінка, де проти кожного критерію стоїть відмітка, зарахований він чи ні.
У прикладах на цій сторінці скрізь написано bohdan.g9 — читай це як свій
логін і виправляй у кожній команді, яку копіюєш. Це важливо саме сьогодні: у розкладі
команд шляхи доведеться писати повністю, і чужий логін там просто нічого не знайде.
Бот, у якого багато користувачів, рано чи пізно зустрічає двох неприємних гостей. Перший — спам: хтось засипає бота сотнею повідомлень за хвилину (з цікавості, з нудьги або щоб зламати). Другий — збій: служба впала, сайт не відкривається, бот завис, а ти дізнаєшся про це від однокласників через три дні. Сьогодні в тебе з’являться обидва захисти.
До кінця заняття в тебе буде:
guard.py («охоронець» — він стереже бота від спаму): якщо хтось пише надто часто, повторює те саме чи вживає заборонені слова, бот спершу чемно просить зачекати, а потім на 10 хвилин замовкає. Покарання записується в таблицю blocked у базі;/status (скільки працює бот, скільки заблоковано, останні помилки), /blocked і /unblock;watch.py («сторож» — він стереже сайт і самого бота від збоїв) і таймер, які щохвилини перевіряють сайт та самого бота й пишуть тобі в Telegram, коли щось упало і коли знову запрацювало, — але не частіше ніж один раз на подію;Показати можна буде: імітацію спаму, де бот замовкає (python3 spam.py), повідомлення «🔴 … впало» і «🟢 … знову працює» і відповідь на /status.
Якщо в тебе немає TelegramНічого не втрачено. Спам імітує програма spam.py, команди власника перевіряє try_bot.py --id …, а сповіщення сторожа, якщо немає куди писати, потрапляють у журнал ~/logs/club-watch.log з позначкою [без Telegram]. Живе повідомлення в Telegram лише підтверджує викладач.
Спам — це небажані повідомлення, які надсилають масово й швидко. Для бота це три різні ситуації, і кожну можна впізнати за цифрами:
BAD_WORDS у guard.py (кортеж — це список, який не змінюють під час роботи), і його можна доповнювати.Є тонкість: охоронець не має ображати звичайних людей. Друг, який розсердився й надіслав шість повідомлень, — не спамер. Тому покарання йде сходинками: перше порушення — лише чемне «зачекай хвилину», і це повідомлення бот не обробляє; наступне порушення — мовчання без жодної відповіді (щоб спамер не отримував «нагороди» у вигляді відповідей); третє порушення за хвилину — тимчасове мовчання на 10 хвилин. Не назавжди: помилитися може кожен, а вічний бан без права на помилку — несправедливий.
Чому «ліміт на хвилину», а не «ліміт на весь день»? Бо цікавить швидкість. Стільки ж повідомлень за день — нормальна розмова, за хвилину — атака.
Кнопки під повідомленнями бота (заняття 10) теж можна «засипати» натисканнями, і кожне натискання — це запис у базу. Тому охоронець рахує й їх, але щедріше: до 20 натискань за хвилину (CLICK_LIMIT), бо кнопки — звичайна частина розмови.
Це спрощена схемаСправжні захисти складніші. Наш охоронець, наприклад, вважає повтором навіть п’ять разів «ок» поспіль. Для навчання цього досить: головне — зрозуміти ідею «лічильник у пам’яті плюс покарання в базі».
Друг написав боту 6 повідомлень за хвилину, бо розгнівався. Що має зробити охоронець за нашими правилами?
Охоронцю потрібно пам’ятати двоє різних речей, і зберігаються вони по-різному.
Лічильники повідомлень («цей 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.
Блокувати можна лише за chat_id — тим самим числом, яким ми визначали власника на занятті 07. Ім’я та нік змінює собі кожен за секунду. Якби ми блокували за іменем «Спамер», то по-перше, спамер змінив би ім’я й продовжив; по-друге, тисячі цілком чемних людей, що теж звуться «Саша», потрапили б під удар. Номер чату видає Telegram, і підробити його не вдасться.
Власника не блокуємо ніколи. Це перший рядок у guard.check: якщо chat_id збігається з номером власника (перший у ADMIN_IDS) — пропускаємо без розмов. Уяви, що ти тестуєш бота й надсилаєш двадцять команд поспіль, а власний захист заблокував тебе самого. Саме тому для власника ще й існують /unblock і /blocked: якщо охоронець помилково заблокував гарну людину, ти її розблокуєш однією командою.
Спамер, якого заблоковано, змінив своє ім’я в Telegram на «Оля» й знову пише. Що зробить охоронець?
Досі ми дізнавалися про поломки так: бот не відповідає — отже, щось не так. Це найгірший спосіб: ти дізнаєшся останнім. Краще, щоб про збій тобі сказала програма-сторож. Для цього їй потрібно вміти перевіряти, чи все гаразд. Такі перевірки бувають двох видів.
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. Таку перевірку називають перевіркою «живості».
Висновок, який варто запам’ятати: стежити треба за результатом (чи відповідає сайт, чи рухається бот), а не за тим, що «процес існує».
Чому сторожа не можна вбудувати в самого бота? Бо коли бот упав, він уже нічого не скаже: мертвий сторож нікого не попередить. Тому watch.py — окрема програма, яку будить таймер systemd щохвилини (так само, як morning.py на занятті 09). Бот може лежати, а сторож усе одно прокинеться, подивиться і напише.
Але сторож, який пише щохвилини «сайт лежить, сайт лежить, сайт лежить…», за годину набридне, і ти його вимкнеш. Тоді він марний. Тому дві умови:
~/data/watch_state.json: для кожної перевірки — скільки разів поспіль вона провалилася (fails) і чи вже сповіщено (alerted). Ідея знайома: стан живе у файлі, а не в пам’яті програми (заняття 09, поле last).FAILS = 2 невдач поспіль — приблизно через дві хвилини. Саме тому kill -9 з заняття 08 (служба впала, але сама піднялася за пару секунд) жодного сповіщення не викличе: це і є правильна поведінка.Є ще одна дрібниця: повідомлення позначається «надісланим» лише тоді, коли Telegram справді його прийняв. Якщо інтернет зник, сторож спробує ще раз за хвилину.
Школа вимикає службу вночіТвоя служба працює лише в години занять і ввечері. Поза цим часом школа її зупиняє, і сторож напише «🔴 впало», а потім «🟢 знову працює», коли службу ввімкнуть. Це не збій, а розклад, але сторож цього не знає й сповістить про «аварію». Щоб не отримувати повідомлень уночі, після заняття можна зупинити таймер: systemctl --user stop club-watch.timer. Після перезавантаження сервера таймер знову запуститься сам.
Сайт «лежить» уже годину. Скільки повідомлень про це має прийти від правильного сторожа за цю годину?
Служба club-bot показує active, а last_poll у bot_state.json — десятихвилинної давності. У club-bot.log за ці десять хвилин — жодного нового рядка. Що це означає?
Коли ти за кермом, тобі потрібна не вся механіка автомобіля, а кілька приладів на панелі: швидкість, пальне, лампочки несправностей. Для бота такою «панеллю» буде команда /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 …: так помилка знайдеться одразу.
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/git status --short нічого не друкує — усе із заняття 09 збережено. Видно файли — спершу git add -A і git commit -m "Заняття 09: доробки";active — служба працює; inactive — systemctl --user start club-bot;1 означає, що власник бота заданий (заняття 09). 0 — повернися до кроку 4 заняття 09: без цього сторож не знатиме, кого не блокувати;bot.py club-watch.service club-watch.timer guard.py spam.py watch.py. No such file or directory — скажи викладачеві.cp /opt/club/kurs/11/guard.py /opt/club/kurs/11/spam.py ~/app/
cd ~/app
less guard.pyГортай стрілками, вийти — q. Знайди у файлі те, про що була теорія:
LIMIT = 5, WINDOW = 60, REPEAT = 3, STRIKES = 3 (стільки порушень за хвилину — і довге мовчання), BAN_SEC = 600, CLICK_LIMIT = 20 і список BAD_WORDS;clock = time.time — «годинник», який підміняють перевірки;check(chat_id, text, owner) — головну. Вона повертає пару: (True, None) — «пропустити», (False, "текст") — «не обробляти, а відповісти цим текстом» і (False, None) — «не обробляти й мовчати»;banned_until() і ban() — читання й запис таблиці blocked; check_click() — те саме для кнопок.Файл можна використовувати вже зараз, окремо від бота:
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.
Відкрий ~/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 файл не скопійовано.
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.
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--pause 20). Пауза умовна: годинник підмінено, тож команда виконається миттєво, а не за 160 секунд. Усі повідомлення з ✔, а в кінці Зараз нікого не заблоковано. Звичайна людина захищена.OWNER береться твій номер із .env (grep знаходить у файлі потрібний рядок, а cut -d= -f2 залишає лише те, що після знака =), і ти «спамиш» від свого імені 20 повідомлень. Команда grep -c ⛔ рахує рядки з ⛔ і друкує 0: жодне повідомлення не зупинено. Власника охоронець не чіпає.Минуло 11 хв. Людина пише знову:, потім ✔, а таблиця знову порожня: мовчання закінчилося, а рядок із таблиці видалено.Підказка: годинник перемотується, бо spam.py підміняє guard.clock (теорія, розділ 2). У справжньому боті час іде звичайним порядком.
Усе виконуємо від твого номера, змінна 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".
systemctl --user restart club-bot
tail -n 5 ~/logs/club-bot.logУ журналі не має бути Traceback чи ModuleNotFoundError. Якщо у тебе є Telegram: напиши своєму боту /status і подивися відповідь — тепер аптайм справжній (кілька секунд). Попроси друга швидко надіслати боту 7–8 повідомлень поспіль і подивися, що бот йому відповів і що показує /blocked. Без Telegram цей крок можна пропустити: ти вже бачив те саме у кроці 4.
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: там зашита логіка «один раз на подію».
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.
Зараз ти сам зупиниш службу свого бота й побачиш, як сторож про це повідомить. Нічого не зламається: службу запустимо назад.
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.jsonsleep 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.
Перша. Нагадаю: службу можна вбити, і вона сама підніметься (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.
Усі цифри в guard.py — просто змінні вгорі, і їх можна міняти. Відкрий файл (nano ~/app/guard.py) і зроби три експерименти. До кожного — своя команда й свій очікуваний результат:
BAD_WORDS одне своє слово, яке не хочеш бачити в чаті, збережи й виконай python3 try_bot.py --id 555 "привіт, моє-слово". Має прийти Такі слова я не приймаю. Напиши інакше.BAN_SEC = 60 (мовчання на хвилину), збережи й виконай python3 spam.py --count 12 --minutes 2. Спершу бот замовкне, а після рядка Минуло 2 хв. Людина пише знову: з’явиться ✔: мовчання закінчилося.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.
У репозиторій потраплять guard.py, watch.py, spam.py і змінений bot.py. Стан сторожа (~/data/watch_state.json), база з таблицею blocked, журнали та файли таймера лишаються поза репозиторієм.
cd ~/app
git status --short
git add -A
git commit -m "Заняття 11: охоронець від спаму, сторож watch.py, /status"
git push origin main
git push github maingit 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.6 | watch.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 | практика-аварія: зупинили службу — сповіщення про збій і про відновлення прийшли викладач | Позначку ставить викладач на занятті або після нього. |
Кожне питання оцінюється відразу в браузері — це лишається так само. Ця кнопка лише надсилає підсумок учителю, щоб відповіді не зникли разом із закритою вкладкою.
Список критеріїв і що саме вони означають — у таблиці вище, «Що перевіряє автоматика». Тут — та сама перевірка, але прямо зараз, без очікування на найближчий прохід за розкладом.
tail -n 20 ~/logs/club-watch.log і cat ~/data/watch_state.json. Школа вимикає служби груп уночі, тож, якщо таймер не зупиняли, там буде «🔴» і «🟢». Запиши в README, о котрій годині це сталося і скільки часу минуло між ними. Не хочеш нічних повідомлень — systemctl --user stop club-watch.timer.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() сам підхопить нову перевірку.active, а бот — ні. На початку заняття 12 викладач спитає щось одне з цього.git add -A, git commit -m "Заняття 11: домашня робота", git push origin main і git push github main.Подивись журнал: tail -n 30 ~/logs/club-bot.log. Найчастіше причина в одному з трьох:
ModuleNotFoundError: No module named 'guard' — немає файлу ~/app/guard.py (крок 2);SyntaxError чи IndentationError після вставок — зсунуто відступ або забуто двокрапку; порівняй: diff ~/app/bot.py /opt/club/kurs/11/bot.py;/status відповідає «Не знаю такої команди» — вставка 3 стоїть після загального if text.startswith("/"); її треба перед ним;callback_query.python3 spam.py показує лише ✔ — у handle_update немає виклику guard.check(...) (вставка 2) або він стоїть після обробки команд. Охоронець має бути першим;ban() у guard.py; оригінал: /opt/club/kurs/11/guard.py;BAD_WORDS — це кортеж рядків з комами, не один великий рядок, і що слово написано малими літерами.Найчастіше причина — змінений LIMIT. Якщо ти поставив 1–2, людина, що пише раз на 20 секунд, швидко потрапить під обмеження; поверни LIMIT = 5, WINDOW = 60, REPEAT = 3. Друга причина — блокування за іменем замість chat_id: у guard.py ключем скрізь має бути chat_id.
Перший рядок у 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; власник тоді не відомий, і охоронець блокує всіх.
banned_until() порівнюй until з clock(), а не з time.time(): перевірка підміняє саме clock;DELETE FROM blocked …);BAN_SEC менше 60 — перевірка цього не пропустить; за завданням — 600.status() у guard.py або поверни оригінал;~/logs/club-bot.log (його можна підмінити змінною CLUB_BOT_LOG);guard.command(text), а має бути перед ним.немає ~/app/watch.py — cp /opt/club/kurs/11/watch.py ~/app/;--dry немає слів «Сайт», «Бот» або знаків ✅ ❌ — ти змінив main(); поверни оригінал;--dry створює watch_state.json — у гілці --dry не має бути виклику run() чи save();alerted і умову and not st["alerted"] у run();FAILS має бути не менше 2, інакше будь-який перезапуск служби поверне хибну тривогу;alerted знову стає False.Unit club-watch.timer not found — файли не скопійовано в ~/.config/systemd/user/ або забуто daemon-reload;timers.target.wants/club-watch.timer — не виконано systemctl --user enable --now club-watch.timer. Зупинка таймера (stop) перевірку не ламає, а вимкнення (disable) — ламає;club-watch.timer має бути рядок OnCalendar=minutely (або OnUnitActiveSec=).