← До списку уроків
Заняття 07 з 13Хто має доступ до бота
курс 0% виконано0%
Заняття 07 · місяць 2

Хто має доступ до бота

90 хвилин

Закрити бота на замок: список людей і зміни в базі бачить і робить лише власник за своїм номером чату (ADMIN_IDS у .env), чужим бот ввічливо відмовляє, а файли з секретами не можуть прочитати інші учні на сервері.

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

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

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

Зараз твій бот віддає список усіх, хто йому писав, кожному, хто знає команду, а /add, /edit і /del виконує для будь-кого: хто завгодно може написати боту й стерти запис у базі. Адреси бота ніхто не ховає — його знайдуть за назвою в пошуку Telegram. Сьогодні ти зачиниш ці двері. Список і всі зміни працюватимуть лише для тебе, а чужим бот відповість ввічливою відмовою. Для всіх лишиться команда /stats із двома числами: скільки людей писали боту і скільки повідомлень вони надіслали.

Як бот дізнається, що це ти? У кожного повідомлення Telegram дописує номер того, хто його написав (chat_id), і підробити цей номер не можна. Ти запишеш свій номер у .env як ADMIN_IDS, а бот перед кожною небезпечною командою звірятиме номер зі списком. Пароль і сторінка входу для цього не потрібні. Для сміливих є ще необов’язковий PIN: кілька цифр, які бот зберігає не як є, а у вигляді «відбитка» (хеша; пояснимо в розділі 3).

А ще ти розберешся з правами файлів у Linux і переконаєшся, що .env із токеном і база з іменами однокласників не читаються нікому, крім тебе.

Наприкінці покажеш: відмову чужому на /users, /add і /del (у try_bot.py з чужим номером і в Telegram з другого акаунта), список для себе, числа з /stats без жодного імені, права -rw------- у .env і app.db.

Перевіряємо лише своєУсі перевірки сьогодні — на твоєму власному боті: чужий номер ти імітуєш сам, через try_bot.py --id …. Бота однокласника з другого акаунта «пробувати на міцність» не можна без його згоди: ти не знаєш, що саме він зберігає в базі, і чужі дані дивитися так само заборонено, як змінювати. Помітив, що в когось команди досі відкриті, — скажи йому про це, а не користуйся.

Теорія

1. Витік даних: що бачить людина, яка знає назву бота

Витік даних — це коли чужі люди бачать те, що їм бачити не можна. Не обов’язково через злам: найчастіше дані просто лежать відкрито, і хтось на них натрапляє. Саме так зараз із твоєю командою /users. Хто написав її боту, отримає для кожної людини ім’я, нік у Telegram, коли вона писала вперше й востаннє, скільки разів, і твою примітку про неї. Незнайомець, однокласник, бот-перебирач — не має значення: бот відповідає всім однаково.

«Але ж мого бота ніхто не знає» — не захист. Назва бота лежить у пошуку Telegram, її видно на скриншотах і в чаті класу, а нік бота можна просто вгадати. Адреса і назва — не пароль.

Чим це погано, якщо там «лише імена»? Ім’я, нік і час, коли людина пише, разом — це вже персональні дані: відомості, за якими можна впізнати конкретну людину. Незнайомець дізнається, що Оля з ніком @olya_kosmos щовечора о десятій пише боту про космос, — і може знайти її в Telegram, назвавшись «другом із гуртка». Люди писали твоєму боту, довіряючи тобі, а не всьому інтернету. На занятті 04 ти вирішив зберігати лише мінімум; сьогодні — показувати цей мінімум лише тому, кому він потрібен.

Що закрити, а що лишити відкритим? Правило таке: усе, що стосується конкретних людей або змінює дані, — лише власнику. Усе, з чого про конкретну людину нічого не дізнаєшся, — можна всім.

КомандаХто може після сьогодніЧому
/start, /help, /askусіЗвичайна розмова з ботом, жодних чужих даних
/statsусіЛише два числа: скільки людей і скільки повідомлень. Чиїх — не видно
/users, /listлише власникІмена й ніки — персональні дані
/add, /edit, /delлише власникЗмінюють базу

Замок ставимо у самому боті, на початку обробки кожного повідомлення, а не «десь збоку». Пояснення — просте: чужа людина може написати будь-яку команду будь-коли. Приховати команду з підказки /help нічого не захистить, а от перевірка в коді, яку відвідувач не може обійти, — захищає.

Тарас каже: «Мій /users можна не закривати. Назву бота я нікому не давав, а в /help цієї команди немає». Чи правий він?

2. «Хто ти?»: номер чату замість пароля

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

{"message": {"from": {"id": 100200300, "first_name": "Богдан"},
             "chat": {"id": 100200300},
             "text": "/users"}}

Число 100200300 — це і є from.id (номер людини) та chat.id (номер чату; у приватній розмові вони збігаються). Ім’я та нік людина може змінити на які завгодно — написати «Богдан» може будь-хто. А от номер Telegram ставить сам, і підробити його в повідомленні, яке приходить боту, не можна. Тому доступ дають за номером, а не за іменем.

Правило авторизації в нас одне: номер у списку ADMIN_IDS — адмін, може все; решта — лише відкрите. Список лежить у ~/app/.env, поруч із токеном:

ADMIN_IDS=100200300
ADMIN_IDS=100200300,777888999

Один рядок із одним номером або кількома через кому (другий рядок — лише приклад, у файлі має бути один рядок ADMIN_IDS). Готовий модуль access.py читає цей рядок і дає боту одну функцію:

refusal = access.check(sender.get("id"), text)
if refusal:
    return refusal

Якщо людині цю команду не можна, check() повертає текст відмови, і бот просто відповідає ним та більше нічого не робить: до бази справа не доходить. Якщо можна — повертає None («нічого заважати»), і бот працює як раніше. Відмова — це зрозуміла відповідь: «Ця команда лише для власника бота», а не мовчання і не рядки з помилкою. Порядок важливий: перевірка стоїть першою, ще до того, як бот розбереться, що саме в команді (скажімо, чи є в ній номер запису). З тим, хто не має права, бот не обговорює, що не так у його даних.

Чому не просто один власник? Бот може обслуговувати вчителя й помічника, тому в ADMIN_IDS можна кілька номерів. А якщо номер змінити в .env і перезапустити службу, право отримує інша людина: пароля передавати не треба.

Номер — не секрет, але й не прикрасаСвій номер можна показувати: самого по собі він нічого не відкриває. Небезпечно інше: якщо хтось дізнається токен бота. Тоді він зможе писати від імені бота. Тому токен ніколи не публікують, а файл .env закривають від сусідів (розділ 4).

Як користуватися ШІ-помічникомБезпека — тема, де ШІ добре пояснює: «Що таке chat_id у Telegram?», «Чим автентифікація відрізняється від авторизації?». Але в чат ніколи не вставляють вміст .env, токени, PIN, хеші й справжні номери чужих людей — навіть «щоб ШІ подивився, чи все правильно». Замість значень показуй назви: ADMIN_IDS=…. Кожну відповідь перевіряй на своєму боті, а на занятті пояснюй своїми словами: викладач може показати будь-який рядок access.py.

Зловмисник змінив своє ім’я в Telegram на «Богдан» і поставив таку саму аватарку. Чи отримає він доступ до /users?

3. Хеш замість пароля: PIN для небезпечних команд (необов’язково)

Номер чату відповідає на питання «хто пише», але не на питання «чи пише справді він». Телефон адміна можна залишити без нагляду, і тоді хтось інший відкриє розмову з ботом і напише /del. Тому для найнебезпечнішої команди (видалення) додають ще й PIN: кілька цифр, які ти вводиш командою /pin і які діють п’ять хвилин. Цей PIN бот зберігає не як є, а як хеш.

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

python3 -c "import hashlib; print(hashlib.sha256(b'sonechko7').hexdigest())"
python3 -c "import hashlib; print(hashlib.sha256(b'sonechko7').hexdigest())"
python3 -c "import hashlib; print(hashlib.sha256(b'Sonechko7').hexdigest())"

Перші дві надрукують однаковий рядок із 64 символів — 020320950c4f…71a00e9a: той самий текст завжди дає той самий результат. Третя відрізняється однією великою літерою, а рядок вийде зовсім інший: 7c1fe0e31d5d…4834712a. На цьому тримається перевірка: бот рахує хеш введеного PIN і порівнює з тим, що лежить у .env. Збіглося — PIN правильний, хоча сам PIN бот ніде не зберігав.

Але одного sha256 мало. Він дуже швидкий, тож той, хто вкрав файл, перебирає варіанти за долі секунди. І однаковий PIN дає однаковий хеш. Від цього рятують сіль (випадковий рядок, свій для кожного хеша, який додають перед хешуванням) і спеціальний повільний алгоритм scrypt, що є в стандартній бібліотеці Python. У нашому pin.py до хеша додано і те, і те:

$scrypt$n=16384,r=8,p=1$226c93a469f6d5426ab0078b91e8f3e4$2d8545f1eb174ad008d4fc1f0eb15095150879886fb0a9459bb134af922fb5e9

scrypt              назва алгоритму
n=16384,r=8,p=1     налаштування: скільки роботи й пам’яті
226c93a4…f3e4       сіль: 16 випадкових байтів, записаних 32 символами
2d8545f1…5e9        сам хеш: 32 байти, записані 64 символами

Для цікавихПеревірку check_pin() написано так, щоб вона порівнювала хеші функцією hmac.compare_digest(): вона витрачає однаковий час, хоч би де рядки розійшлися, а звичайне == зупиняється на першому відмінному символі, і за часом відповіді можна було б підбирати символ за символом.

Скільки захисту дає такий PIN? Менше, ніж довгий пароль. Чотири цифри — лише 10 000 варіантів, і якщо хтось отримає твій файл .env, він переб’є їх за хвилини, навіть із scrypt. Тому PIN захищає від людини, яка взяла твій телефон, але не від викрадення файлів: для цього є права файлів (розділ 4) і правило «токен не в git». Навіщо тоді хеш? Щоб PIN не лежав у файлі відкритим текстом, а файл не був готовою відповіддю.

Ще одна деталь. PIN вводять у терміналі через getpass.getpass(), а не пишуть у команді (pin.py 1234). Усе, що набираєш у терміналі, записується в ~/.bash_history, а поки команда працює, її рядок видно в списку процесів сервера (ps) усім, хто на нього зайшов. А ще екран може бачити весь клас. І команду /pin у Telegram після введення видали з чату.

Ти й однокласниця поставили однаковий PIN 482916. У ваших .env рядки ADMIN_PIN_HASH=… починаються однаково ($scrypt$n=16384,r=8,p=1$), а далі зовсім різні. Чому?

4. Права файлів у Linux: хто що може на сервері

Усі замки в боті марні, якщо сусід по серверу просто прочитає файл ~/app/.env з твоїм BOT_TOKEN і списком ADMIN_IDS або скопіює собі ~/data/app.db з іменами. На нашому сервері працює вся група, і в кожного свій користувач Linux — обліковий запис із власним іменем (твій логін) і номером. Подивись, хто ти для системи:

id

Відповідь буде схожа на uid=1023(bohdan.g9) gid=1023(bohdan.g9) groups=1023(bohdan.g9),1001(klub): номер і ім’я користувача, твоя основна група (у Linux у кожного користувача є група з тим самим ім’ям, і в ній лише він) і додаткова група klub, у якій усі учні, — через неї сервер дозволяє вам входити за паролем. Числа в тебе будуть інші.

Кожен файл і кожна тека мають власника, групу і права для трьох кіл: власника, групи й усіх інших. Їх показує ls -l:

-rw-------. 1 bohdan.g9 bohdan.g9  312 … .env
drwxr-x---. 2 bohdan.g9 bohdan.g9 4096 … data

-  або  d       тип: звичайний файл чи тека (directory)
rw-             власник: читати (r), писати (w), запускати (x) — тут без x
---             група: нічого
---             усі інші: нічого
.               позначка SELinux — додаткового захисту на нашому AlmaLinux; на неї можна не звертати уваги
bohdan.g9       власник
bohdan.g9       група

Для теки букви означають трохи інше: r — бачити список файлів, w — створювати й видаляти в ній файли, x — заходити в неї й діставати файл за назвою.

Права часто записують трьома цифрами — по одній на коло. Кожна буква має вагу: r = 4, w = 2, x = 1, і ваги додають. rw- = 4 + 2 = 6, r-x = 4 + 1 = 5, --x = 1, --- = 0. Змінює права команда chmod: chmod 600 .env — «власникові читати й писати, решті нічого».

ЩоПраваЩо це дає
~/app/.env, ~/data/app.db600, -rw-------Читаєш і змінюєш лише ти — і твоя служба, бо вона працює від твого імені
~/app700, drwx------У теку з кодом не зайде ніхто, крім тебе
~/data, ~/logs, ~/backups750, drwxr-x---Тобі — усе, групі — заходити й читати (але група тут лише твоя), іншим — нічого
домашня тека ~711, drwx--x--xІнші можуть пройти крізь неї до ~/www, але не бачать, що в ній лежить
~/www і сторінки в ній755 і 644Читати можуть усі — так nginx віддає твій сайт світові

Права на теці й права на файлі — два різні замки. .env лежить у закритій теці ~/app, але якщо колись ти відчиниш теку (скажімо, chmod 755 ~/app, щоб показати другові файл), файл із правами 644 одразу стане читабельним для всіх. Тому закривають і теку, і сам файл. Автоперевірки L07.6 і L07.7 дивляться саме на права файлів: у групи й інших не має бути жодної букви.

Хто ще є на сервері? nginx — окремий користувач, програма, що віддає сайти: вона читає твою ~/www, бо там права «читати всім», і не дістає до ~/app і ~/data. І root — головний користувач, адміністратор сервера. Для нього права файлів не діють, він може прочитати будь-що. Тому на шкільному сервері не тримають нічого справді особистого: адміністратор технічно бачить усе, і це треба знати.

Linux так само береже паролі власних користувачів — і твій пароль для входу по SSH теж:

ls -l /etc/passwd /etc/shadow

У /etc/passwd (права -rw-r--r--, читати всім) — список користувачів, а на місці пароля стоїть x: «шукай деінде». Хеші паролів лежать в /etc/shadow, і в нього права ----------: не прочитає ніхто, крім root. Та сама ідея, що й у твоїй адмінці: хеш замість пароля і файл, закритий від сторонніх.

ls -l ~/app/.env показує -rw-r--r--. 1 bohdan.g9 bohdan.g9 312 … .env. Що кажуть ці права?

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

СловоЩо означає
витік данихДані потрапили до тих, кому не призначалися; найчастіше — просто лежали відкрито
персональні даніВідомості, за якими можна впізнати конкретну людину: ім’я, нік, коли вона буває в мережі
автентифікація і авторизаціяПеревірка «хто ти» (тут — номер чату) і рішення «що тобі можна» (тут — список ADMIN_IDS)
chat_id, from.idНомер чату й номер людини, які Telegram сам дописує до кожного повідомлення; підробити їх не можна
ADMIN_IDSРядок у .env зі списком номерів тих, кому можна небезпечні команди
відмоваЗрозуміла відповідь боту на команду, яку людині не можна, — без помилки й без виконання
хеш«Відбиток» тексту: рядок однакової довжини, обчислений за незмінним правилом; назад у текст не перетворюється
сільВипадковий рядок, який додають перед хешуванням; свій для кожного хеша, лежить поруч із ним
scryptНавмисно повільний алгоритм хешування, якому потрібно багато пам’яті; є в hashlib
hmac.compare_digestПорівняння рядків, яке за часом не видає, де саме вони розійшлися
getpassФункція Python, що питає пароль чи PIN, не показуючи його на екрані
користувач і група LinuxОбліковий запис на сервері й набір користувачів, яким можна дати спільні права
права доступу, rwxЧитати, писати, запускати (для теки — заходити) окремо для власника, групи й інших
chmod 600, 700, 644, 755, 711Змінити права; кожна цифра — сума r = 4, w = 2, x = 1 для власника, групи, інших
rootАдміністратор сервера; права файлів його не зупиняють

Практика

Виконуй кроки по черзі. Де в команді стоїть $USER, нічого не підставляй — ця змінна вже містить твій логін. А <логін> разом із кутовими дужками заміни на свій, наприклад bohdan.g9. Номери й числа в прикладах умовні — у тебе будуть свої. Команди бота нижче названо так, як у нашому курсі (/users — з заняття 04, /list, /add, /edit, /del — з заняття 05). Якщо в твого бота вони звуться інакше, підстав свої назви.

1 Подивитися на витік своїми очима

ssh <логін>@91.219.61.4
cd ~/app
systemctl --user is-active club-bot
python3 try_bot.py --id 777 --name Чужий "/users"

Має бути active, а потім список людей, які писали твоєму боту: try_bot.py --id 777 імітує чужу людину з номером 777 (у Telegram тебе звуть інакше), і бот віддає їй усе. Саме це ти сьогодні закриваєш. inactive — служба зупинена: systemctl --user start club-bot. Ще дві перевірки перед роботою:

git status --short
cut -d= -f1 .env

git status --short має не надрукувати нічого; якщо видно файли, спершу збережи їх: git add -A і git commit -m "Заняття 06: доробки". cut -d= -f1 .env друкує з кожного рядка .env лише те, що стоїть до знака =, — назви без значень, тож жоден токен на екрані не з’явиться. Має бути щонайменше BOT_TOKEN, LLM_BASE_URL, LLM_API_KEY і LLM_MODEL.

2 Дізнатися свій номер

Напиши своєму боту в Telegram /start — бот запам’ятав тебе в базі (заняття 04), тож номер можна прочитати звідти:

sqlite3 ~/data/app.db "SELECT chat_id, name, last_seen FROM users WHERE chat_id IS NOT NULL ORDER BY last_seen DESC LIMIT 5"

Перший рядок — ти (найсвіжіший last_seen): наприклад 100200300|Богдан|2026-11-04T16:20:11. Число перед іменем — твій номер. Інший спосіб — написати в Telegram боту @userinfobot. Без Telegram номер вигадай сам — наприклад 1: його використовує try_bot.py, коли не вказано --id. Усі перевірки нижче вдома робляться саме так.

3 Записати ADMIN_IDS у .env

cd ~/app
echo "" >> .env
echo "ADMIN_IDS=100200300" >> .env
cut -d= -f1 .env
grep -c "^ADMIN_IDS=" .env
ls -l .env

Друга покаже назви, серед них ADMIN_IDS. Третя — 1. Остання має починатися з -rw-------: >> прав не змінює, тож 600 з заняття 02 лишилося. Якщо там інакше — chmod 600 .env. Показало 2 чи більше — рядок записано двічі: пункт «Секрет записано двічі або .env затерто» внизу сторінки.

4 Скопіювати access.py і pin.py та прочитати їх

cp /opt/club/kurs/07/access.py /opt/club/kurs/07/pin.py ~/app/
less ~/app/access.py

Коли cp отримує кілька файлів, останнє слово — тека, куди їх скласти. cp: cannot stat — подивись ls /opt/club/kurs/07/ і, якщо файлів там немає, скажи викладачеві. У less гортай стрілками й Пробілом, вийти — q. Знайди в access.py: рядок ADMIN_COMMANDS — список команд, які тепер лише для адмінів; функцію admin_ids(), що читає ADMIN_IDS з оточення; функцію check(), яка повертає відмову або None; і stats_text() із двома числами. Сам файл не змінюй: хіба що доповни ADMIN_COMMANDS власними командами, які показують людей чи змінюють базу.

5 Підключити перевірку в bot.py

Відкрий nano ~/app/bot.py. Треба три невеликі вставки. Перша — разом з іншими імпортами на початку файлу (за абеткою, поруч з import env):

import access

Друга — у функції handle_update(), одразу після рядка if not text: return None (там, де вже є sender), але перед першим if text.startswith(...):

    refusal = access.check(sender.get("id"), text)       # заняття 07
    if refusal:
        return refusal

Третя — публічна статистика. Поруч з іншими командами (наприклад, під обробкою /help) додай:

    if text.startswith("/stats"):                        # заняття 07
        return access.stats_text()

Відступ у функції — чотири пробіли, як у сусідніх рядків. Збережи (Ctrl+O, Enter), вийди (Ctrl+X) і перевір файл тим самим Python, яким його запускає служба:

cd ~/app
/opt/club/venv/bin/python3 -c "import bot" && echo готово

Має бути готово. ModuleNotFoundError: No module named 'access' — access.py не лежить у ~/app, повернись до кроку 4. NameError: name 'access' is not defined — пропущено import access. IndentationError — відступ у вставці не збігається з сусідніми рядками.

Ще одне: автоперевірка шукає команди за назвами з паспорта проєкту. Відкрий nano ~/app/project.yaml і в розділі commands: заповни порожній рядок stats:, а не дописуй новий (два однакові ключі зіпсують файл):

commands:
  ...
  stats: "/stats"               # заняття 07

Відступи — по два пробіли, без Tab. Перевір: python3 -c "import yaml; print(yaml.safe_load(open('project.yaml'))['commands']['stats'])" надрукує /stats. ScannerError чи ParserError — помилка формату в рядку з номером після слова line (найчастіше Tab замість пробілів).

6 Перевірити очима чужого

Повтори запити з кроку 1 — від чужого номера 777, який не в ADMIN_IDS:

cd ~/app
python3 try_bot.py --id 777 --name Чужий "/users"
python3 try_bot.py --id 777 --name Чужий "/add Хакер | зламано"
python3 try_bot.py --id 777 --name Чужий "/del 1"
python3 try_bot.py --id 777 --name Чужий "/stats"
python3 try_bot.py --id 777 --name Чужий "/ask що таке порт?"

Перші три мають закінчитися відповіддю Ця команда лише для власника бота.: і список, і зміни зачинено. Четверта — Людей, які писали боту: 7. Повідомлень: 42. з твоїми числами: статистику бачать усі. П’ята — звичайна відповідь ШІ (або чемне «ШІ не підключено») — розмова з ботом працює як раніше. Бот запам’ятав і «Чужого» з номером 777 у таблиці users (так працює remember() з заняття 04) — ми приберемо його в кроці 7. Тепер те саме від тебе (підстав свій номер з кроку 2 або 1, якщо Telegram немає і ти записав ADMIN_IDS=1):

python3 try_bot.py --id 100200300 --name Богдан "/users"

Тепер замість відмови — список. Якщо чужому показано список — у access.py немає цієї команди в ADMIN_COMMANDS або вставку з кроку 5 зроблено нижче за обробку команди. Якщо відмову отримуєш ти — номер у ADMIN_IDS інший, ніж --id: grep ^ADMIN_IDS= .env.

7 Переконатися, що база не змінилася

sqlite3 ~/data/app.db "SELECT id, name FROM users WHERE name='Хакер' OR id=1"
sqlite3 ~/data/app.db "DELETE FROM users WHERE chat_id=777"

Перша команда: рядка «Хакер» немає, а запис з id=1 (якщо він був) на місці. Жоден із чужих запитів не дійшов до бази: відмова мусить спрацювати до зміни, а не після. Друга прибирає з таблиці «Чужого» з номером 777, якого бот запам’ятав, поки ти його імітував. Відмови бот ще й записує у журнал служби (номер і команда, без тексту повідомлення):

tail -n 5 ~/logs/club-bot.log

Рядки на зразок 2026-11-04T16:25:02 відмова: 777 спробував /add з’являться, коли відповідати починає справжня служба (крок 8), а не try_bot.py: він пише у ваш термінал.

8 Перезапустити службу й перевірити в Telegram

systemctl --user restart club-bot
sleep 3
systemctl --user is-active club-bot
tail -n 5 ~/logs/club-bot.log

Має бути active. Служба читає код і .env лише під час старту, і без перезапуску працювала б по-старому, з відчиненими дверима. У Telegram напиши своєму боту /users — список; а тепер попроси друга чи подругу (або візьми другий акаунт) написати боту /users і /stats: у відповідь — відмова і два числа. Це критерій L07.9, його оцінює викладач. Без Telegram — покажи викладачеві вивід try_bot.py з кроку 6.

9 Необов’язково: PIN для /del

cd ~/app
python3 pin.py
grep -c "^ADMIN_PIN_HASH=" .env
ls -l .env
systemctl --user restart club-bot

Скрипт спитає PIN (4–10 цифр): — набери і натисни Enter; на екрані не з’явиться нічого, навіть зірочок: так працює getpass. Потім ще раз:. Має бути PIN збережено, 1 і -rw-------. У Telegram: /del 5 відповість Це небезпечна команда. Спершу введи /pin і свій PIN.; після /pin 482916 (твій PIN) — PIN вірний…, і п’ять хвилин /del працює. Хибний PIN — PIN хибний., а після п’яти спроб бот просить зачекати. Видали повідомлення з PIN з чату. Зміна PIN — той самий pin.py ще раз. Подивитися, що саме лежить у файлі, можна так: grep ^ADMIN_PIN_HASH= .env | cut -c1-40 — буде початок хеша ADMIN_PIN_HASH=$scrypt$n=16384,r=8, а самого PIN у файлі немає.

10 Закрити файли від сусідів по серверу

id
ls -ld ~ ~/app ~/data ~/www
ls -l ~/app/.env ~/data/app.db

id покаже тебе й твої групи, як у теорії. ls -ld показує права самих тек (без -d ls показав би їхній вміст): drwx--x--x у домашньої теки (711; на цьому сервері в кінці може стояти ще крапка — це службова позначка захисту системи), drwx------ у app (700), drwxr-x--- у data (750), drwxr-xr-x у www (755). Останній рядок — головні файли: в обох на початку має бути -rw-------. Якщо хоч в одного інакше — закрий обидва (повторний chmod нічого не зламає):

chmod 600 ~/data/app.db ~/app/.env
ls -l ~/app/.env ~/data/app.db
cd ~/app && python3 try_bot.py --id 777 "/stats"

chmod нічого не друкує, а ls -l тепер показує -rw------- в обох. Остання команда — твої два числа, як у кроці 6: бот читає базу як раніше, бо працює від твого імені, а власникові 600 дозволяє все. Уважно з назвами: chmod 600 ~/data — без /app.db — забрав би з теки букву x, і бот уже не зміг би відкрити базу. Тепер подивись, як Linux береже власні паролі:

ls -l /etc/passwd /etc/shadow
cat /etc/shadow

/etc/passwd — -rw-r--r--, /etc/shadow — ----------, а cat відповість cat: /etc/shadow: Permission denied. Хешів паролів усіх користувачів сервера не прочитає ніхто, крім root. Та сама ідея, що й у твого PIN: хеш замість самого значення і файл, закритий від сторонніх.

11 Токен не в Git

cd ~/app
git check-ignore -v .env
git log --all --oneline -- .env

Перша має назвати правило, через яке Git ігнорує .env, наприклад .gitignore:12:.env .env (номер рядка в тебе може бути інший). Друга шукає .env в усій історії, в усіх гілках, і має не надрукувати нічого: файл ніколи не потрапляв у коміт. Якщо перша мовчить або друга надрукувала хоч один рядок — пункт «Автоперевірка: L07.6–L07.8 не зараховано» внизу сторінки.

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

Сьогодні в коміт підуть access.py, pin.py і зміни в bot.py. А от секрети лишаються на сервері: .env Git ігнорує, а база лежить у ~/data, поза репозиторієм.

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

cd ~/app
git status --short

Мають бути M bot.py і нові файли ?? access.py, ?? pin.py (?? — Git бачить файл уперше). Файлу .env у списку бути не повинно: сьогодні в ньому з’явилися нові рядки, і саме тому перед кожним git add -A дивляться на цей список. Якщо .env там є — нічого не додавай і дивись пункт «Автоперевірка: L07.6–L07.8 не зараховано».

git add -A
git commit -m "Заняття 07: доступ за chat_id (ADMIN_IDS), чужим відмова, публічна /stats"
git push origin main
git push

git commit відповість рядком [main …] Заняття 07: … і підсумком 3 files changed. Перший push оновлює ~/repo.git на сервері, другий — GitHub. Онови сторінку репозиторію: там з’явилися access.py і pin.py, а .env і бази немає — і не має бути. git push без слів відповідає помилкою про upstream — виконай один раз git push -u github main, як на занятті 05.

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

Сьогодні автоперевірка дивиться на твого бота двома парами очей. Перша — очі чужого: вона створює копію твоєї бази, кладе в неї людину зі «своїм» іменем і питає бота від імені людини, якої немає в ADMIN_IDS. L07.2: на /users і /list чужий не має побачити це ім’я і має отримати відповідь, яка відрізняється від «Не знаю такої команди». L07.4: після спроб /add, /edit і /del від чужого записи в копії бази мають лишитися без змін. L07.5: чужий на /stats отримує числа без жодного імені. Друга — очі адміна: L07.3 питає від імені людини з ADMIN_IDS і вимагає, щоб /users чи /list показали записи. L07.1 читає ~/app/.env і вимагає в ньому рядок ADMIN_IDS із номерами. L07.6 і L07.7 дивляться на права ~/app/.env і ~/data/app.db: у групи й інших не має бути жодної букви. L07.8 перевіряє, що .env існує, Git його ігнорує і в жодному коміті його ніколи не було. L07.9 — живу відмову в Telegram — оцінює викладач.

Перевірка не змінює твою справжню базу, не пише тобі в Telegram і не витрачає ліміт ШІ. Натисни «Перевірити практичну частину» у розділі «Перевірка виконаних завдань» нижче на сторінці. PIN у автоперевірці не бере участі: він необов’язковий.

КодЩо перевіряєтьсяЯкщо не зараховано
L07.1у .env задано ADMIN_IDS — номери адмінів
автоперевірка
У ~/app/.env немає рядка ADMIN_IDS=число (кілька номерів — через кому, один рядок). Свій номер: sqlite3 ~/data/app.db "SELECT chat_id, name FROM users ORDER BY last_seen DESC LIMIT 3"
L07.2чужому бот не показує список людей
автоперевірка
Від чужого номера /users або /list показують людей. Додай import access, виклик access.check(sender.get("id"), text) перед першим if text.startswith(...) у handle_update (крок 5) і перезапусти службу.
L07.3власнику список працює
автоперевірка
Від номера з ADMIN_IDS команди /users чи /list не показали записів. Перевір, що команда зі списком є в боті (заняття 04–05) і що номер у .env — той, з яким пробуєш.
L07.4чужий не змінює базу
автоперевірка
Чужий номер змінив базу командою /add, /edit чи /del. Перевірка access.check має стояти ПЕРЕД обробкою цих команд, а їхні назви — в ADMIN_COMMANDS у access.py.
L07.5публічна /stats дає лише числа без імен
автоперевірка
Від чужого номера /stats має показати кількість людей, але без жодного імені. Додай у handle_update if text.startswith("/stats"): return access.stats_text() і не клади /stats в ADMIN_COMMANDS.
L07.6.env не читається сусідами
автоперевірка
chmod 600 ~/app/.env
L07.7база не читається сусідами
автоперевірка
Файл бази можуть прочитати інші учні. chmod 600 ~/data/app.db
L07.8.env ніколи не потрапляв у Git
автоперевірка
Секрет у Git-історії вважай украденим: новий BOT_TOKEN у @BotFather, а .env — у .gitignore.
L07.9живий бот: чужий акаунт отримує відмову на /users, а власник бачить список
викладач
Позначку ставить викладач на занятті або після нього.

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

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

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

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

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

  1. Другий адмін. Попроси друга-учня з групи дати тобі свій номер (він дізнається його так само, як ти в кроці 2, і це не секрет). Але в ADMIN_IDS його не додавай, а лише подумай: яку одну команду ти йому довірив би, а яку ні? Допиши в ~/app/README.md розділ ## Хто має доступ: що відкрито для всіх, що лише для власника, чому, і як власник додає ще одного адміна (без токена й PIN, звісно). Три-п’ять речень своїми словами. Розділи ## Що це і ## Як запустити не чіпай.
  2. Нова закрита команда. Придумай для бота ще одну команду, яка показує людей чи щось змінює (скажімо, /export зі списком у вигляді тексту), і додай її в ADMIN_COMMANDS. Перевір через try_bot.py --id 777 (відмова) і зі своїм номером (працює).
  3. Свої PIN-команди. (за бажанням, після кроку 9) Додай до PIN_COMMANDS ще одну команду, яку варто захистити. Поясни, чому PIN із чотирьох цифр захищає від чужих рук, але не від викрадення файлу .env.
  4. Поясни свій код. Підготуйся пояснити будь-який рядок check(), admin_ids() і check_pin(). Зокрема: чому перевірка стоїть першою у handle_update(); чому доступ тримається на номері, а не на імені; навіщо відмова — текст, а не помилка; що означають цифри в 600, 700, 711; чому токен бота треба закривати і правами, і Git.
  5. Збережи. git add -A, git commit -m "Заняття 07: розділ «Хто має доступ» у README", git push origin main і git push. Перед git add — git status --short: .env у списку бути не повинно.

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

Після правки bot.py служба не стартує

Ознаки: systemctl --user is-active club-bot пише activating або failed, а бот у Telegram мовчить. Причину назве Python — сам або в журналі:

cd ~/app
/opt/club/venv/bin/python3 -c "import bot"
tail -n 30 ~/logs/club-bot.log

Дивись на останній рядок звіту Traceback:

Виправ і зроби systemctl --user restart club-bot. Якщо служба надто часто падала, systemd перестає її підіймати й пише start request repeated too quickly — тоді спершу systemctl --user reset-failed club-bot, потім restart.

Автоперевірка: L07.1 «ADMIN_IDS» не зараховано

Перевірка читає ~/app/.env і вимагає один рядок ADMIN_IDS= з числами через кому. Найчастіші причини: рядок не дописано (крок 3); у номері є літери, лапки або пробіл усередині; замість номера стоїть ім’я чи нік (@bohdan — це не номер); порожнє значення. Перевір: grep -c "^ADMIN_IDS=" ~/app/.env має дати 1. Номер — це лише цифри, наприклад 100200300.

Чужий бачить список або змінює базу (L07.2, L07.4)

Перевірка пише, яка саме команда пройшла. Що робити:

Власник не бачить список (L07.3)

Перевірка питає від імені людини з ADMIN_IDS і чекає, що /users або /list покажуть записи. Найчастіше: команди зі списком немає зовсім (заняття 04–05), у access.py зайва команда, а в боті її нема, або номер у .env не збігається з номером, з яким ти пробуєш. Спробуй python3 try_bot.py --id <свій номер> "/users": має бути список, а не відмова.

Статистика порожня чи не зараховано (L07.5)

Чужий на /stats має отримати відповідь із числом людей, але без жодного імені. Ця команда лише для власника на /stats — ти додав /stats у ADMIN_COMMANDS, прибери. Не знаю такої команди — немає вставки if text.startswith("/stats") (крок 5) або вона стоїть нижче за загальне «невідома команда». у project.yaml порожнє commands.stats — не заповнено рядок у кроці 5. Помилка no such table: users — таблиці немає: python3 init_db.py.

Секрет записано двічі або .env затерто

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

cd ~/app
sed -i '/^ADMIN_IDS=/d' .env
echo "ADMIN_IDS=100200300" >> .env
grep -c "^ADMIN_IDS=" .env
ls -l .env
systemctl --user restart club-bot

sed -i редагує файл на місці, а /^ADMIN_IDS=/d означає «видали рядки, що починаються з ADMIN_IDS=»; решту .env він не чіпає. grep -c має сказати 1, ls -l — -rw------- (інакше chmod 600 .env).

Якщо ж замість >> вийшло > і в .env лишився один рядок, відновлюй решту. BOT_TOKEN візьми в @BotFather (/mybots → твій бот → API Token). Рядки ШІ з заняття 06 для учнів до 18 років такі: LLM_BASE_URL=http://127.0.0.1/llm/v1, LLM_API_KEY=school, LLM_MODEL=openrouter/free. Вписуй їх через nano ~/app/.env, кожен з нового рядка, потім ls -l .env (має бути -rw-------) і restart. Перевір бота: python3 try_bot.py "/start".

Автоперевірка: L07.6–L07.8 не зараховано

Кожна перевірка пише причину. Що вона означає: