A dead-simple Telegram bot that lets schoolchildren message the school administration with a single button
A contact bot connecting young students with their school administration or teachers.
The guiding rule: a child should not have to learn anything to use it. No tickets, no message-type pickers, no nested menus, no technical vocabulary. One button, then they write or send whatever they want, and it reaches the administration instantly.
The bot's user interface is in Arabic, because its users are Arabic-speaking children. The code, comments and documentation are in English.
/start β π Welcome β [ π¬ Contact us ] β π Write or send anything
β π© Delivered to the admins β π¬ Admin replies β π Conversation continues
| π― A single button | The child's keyboard holds exactly one button: π¬ ΨͺΩΨ§Ψ΅Ω Ω
ΨΉΩΨ§ (Contact us) |
| πͺ Automatic content detection | Text, photo, document, video, voice note, audio, video note, animation, sticker β never a question asked |
| π¬ Continuous conversation | The child stays in contact mode and keeps sending, exactly like a normal chat |
| π¨βπ« Direct admin replies | A Reply button under every message; the admin answers with any content type |
| π₯ Multiple admins | Messages reach every admin, and whoever replies first gets through to the child |
| β‘ Fully asynchronous | asyncio + aiosqlite β dozens of children at once, nobody blocks anybody |
| π Locked down | Children never see the admin panel, and no child can see another child's data |
| πΎ Durable state | Contact mode lives in the database, so it survives a restart |
student_bot/
β
βββ main.py # Startup, handler registration, global error handler
βββ config.py # Reads and validates .env
βββ requirements.txt
βββ .env.example
β
βββ database/ # Data layer, fully isolated from the handlers
β βββ connection.py # aiosqlite connection + schema creation
β βββ users.py # users table
β βββ conversations.py # conversations table
β βββ messages.py # messages table
β
βββ handlers/ # Telegram-facing layer
β βββ start.py # /start, /help, /id
β βββ student.py # Child UI + the catch-all message receiver
β βββ admin.py # Admin panel, replies, announcements
β βββ media.py # Automatic content-type detection
β
βββ keyboards/ # Buttons, kept apart from the logic
β βββ reply.py # The "Contact us" reply keyboard
β βββ inline.py # Inline buttons for child and admin
β
βββ services/ # Business logic
β βββ message_service.py # Sends any content type to any chat
β βββ conversation_service.py # Bridges child and admins
β
βββ utils/
βββ decorators.py # @admin_only
βββ logger.py # Logging setup
Child's message
β
βΌ
handlers/student.py βββΊ handlers/media.py (what type is this?)
β
βΌ
services/conversation_service.py βββΊ database/ (persist it)
β
βΌ
services/message_service.py βββΊ every admin + [π¬ Reply] [π Close] buttons
- Python 3.11 or newer
1. Clone
git clone https://github.com/ixl55/telegram-bot-.git && cd telegram-bot-2. Virtual environment and dependencies
Windows
python -m venv .venv && .venv\Scripts\activate && pip install -r requirements.txtLinux / macOS
python3 -m venv .venv && source .venv/bin/activate && pip install -r requirements.txt3. Create the bot
Open @BotFather β /newbot β copy the token.
4. Configuration
cp .env.example .envThen fill it in:
BOT_TOKEN=your_token_from_botfather
ADMIN_IDS=123456789 # your numeric id; for several: 111111,222222
DB_PATH=student_bot.db
LOG_LEVEL=INFOπ Don't know your id? Start the bot with any placeholder value, send it
/id, then put the number it returns intoADMIN_IDSand restart.
5. Run
python main.pyA healthy start looks like this:
Connected to database: .../student_bot.db
Bot ready, 1 admin(s) configured
Starting the bot...
| Step | What happens |
|---|---|
/start |
Welcome message plus the π¬ ΨͺΩΨ§Ψ΅Ω Ω
ΨΉΩΨ§ button |
| Presses the button | "Go ahead! Write your message or send a photo or a file" with β Cancel and π Home |
| Sends anything | "Your message arrived!" then short per-type confirmations afterwards |
β Cancel |
Contact mode ends and the home screen returns |
Children never see an admin command or panel of any kind.
Every admin receives:
π© New message
π€ Name: Ahmed
π User: 123456789
followed by the content itself and two buttons:
| Button | Effect |
|---|---|
π¬ Ψ§ΩΨ±Ψ― (Reply) |
Enters reply mode; anything sent next (text/photo/file/video/audio) goes straight to the child |
π Ψ₯ΩΩΨ§Ψ‘ Ψ§ΩΩ
ΨΨ§Ψ―Ψ«Ψ© (Close) |
Closes the conversation and tells the child politely |
Leave reply mode with the β Stop button or the /admin command.
| Button | Shows |
|---|---|
π© Ψ§ΩΨ±Ψ³Ψ§Ψ¦Ω |
Open conversations, each with a quick reply button |
π₯ Ψ§ΩΨ·ΩΨ§Ψ¨ |
The most recently registered students |
π Ψ§ΩΨ₯ΨΨ΅Ψ§Ψ¦ΩΨ§Ψͺ |
Students, conversations, total messages, messages today |
π’ Ψ₯ΨΉΩΨ§Ω |
Broadcast one message to every student, batched to respect rate limits |
| Command | Who | Purpose |
|---|---|---|
/start |
Everyone | Start and register |
/help |
Everyone | Short explanation |
/id |
Everyone | Show your Telegram id |
/admin |
Admins only | Open the admin panel |
SQLite through aiosqlite. Every operation is async/await with no blocking call anywhere, using WAL journaling and foreign_keys=ON.
users
| Column | Type | Notes |
|---|---|---|
id |
INTEGER | Primary key |
telegram_id |
INTEGER | Unique |
username |
TEXT | May be empty |
full_name |
TEXT | |
created_at |
TEXT | Automatic |
conversations
| Column | Type | Notes |
|---|---|---|
id |
INTEGER | Primary key |
user_id |
INTEGER | β users.id |
status |
TEXT | open / closed |
created_at |
TEXT | |
updated_at |
TEXT | Last activity |
Contact mode is simply an
openconversation. That is why no global variable holds per-user state and why every child is fully independent of the others.
messages
| Column | Type | Notes |
|---|---|---|
id |
INTEGER | Primary key |
conversation_id |
INTEGER | β conversations.id |
sender_id |
INTEGER | Sender's telegram_id (child or admin) |
message_type |
TEXT | text, photo, document, video, voice, audio, video_note, animation, sticker |
text_content |
TEXT | For text messages |
file_id |
TEXT | For everything else |
caption |
TEXT | Attachment caption |
created_at |
TEXT |
1. Upload
tar -czf - --exclude='__pycache__' --exclude='*.db' --exclude='.venv' student_bot | ssh USER@SERVER 'tar -xzf - -C /opt'2. Environment and dependencies
ssh USER@SERVER "cd /opt/student_bot && python3 -m venv .venv && .venv/bin/pip install -r requirements.txt"3. Unit file at /etc/systemd/system/student-bot.service
[Unit]
Description=Student Contact Telegram Bot
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
WorkingDirectory=/opt/student_bot
Environment=PYTHONUNBUFFERED=1
Environment=PYTHONIOENCODING=utf-8
ExecStart=/opt/student_bot/.venv/bin/python main.py
Restart=always
RestartSec=5
SyslogIdentifier=student-bot
[Install]
WantedBy=multi-user.target4. Enable and watch
systemctl daemon-reload && systemctl enable --now student-bot && systemctl status student-botjournalctl -u student-bot -f
β οΈ Never run two instances on the same token. Telegram allows a single poller; a second one causesConflicterrors and lost messages.
.envis never committed β it is in.gitignore, and on the server it should bechmod 600.- Every admin entry point is guarded by
@admin_only, which checkstelegram_idfor commands and callback buttons alike. - Children are isolated: no command or button exposes another user's data, and every conversation belongs to exactly one child.
- HTML is escaped for every name and body shown to an admin, so no tag can be injected through a display name.
- No global mutable state: the child's mode lives in the database, and each admin's mode lives in their own
context.user_data.
π If your token ever leaks:
/revokein @BotFather, then put the new one in.env.
Logs go to the console and to logs/bot.log (rotating, 2 MB Γ 3 files). Set LOG_LEVEL=DEBUG for more detail.
On Windows the logger forces UTF-8 output, otherwise
cp1252mangles the Arabic interface strings.
| Problem | Fix |
|---|---|
BOT_TOKEN is missing |
Create .env next to main.py and put the token in it |
ADMIN_IDS is missing |
Add your numeric id (find it with /id) |
| Messages never reach the admin | Each admin must send /start to the bot once first |
Conflict: terminated by other getUpdates |
Another instance is running on the same token β stop it |
| Messages lost after a restart | drop_pending_updates=True in main.py discards whatever arrived while the bot was down; set it to False |
| Dependencies will not install | Check for Python 3.11+ and an activated virtual environment |
Python 3.11+ Β· python-telegram-bot 21.6 Β· asyncio Β· aiosqlite Β· python-dotenv Β· logging Β· type hints throughout