Initial commit

This commit is contained in:
Kaki Filem Team 2026-01-31 20:35:11 +08:00
commit 7b5c71d832
13 changed files with 350 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
__pycache__/
*.pyc
.env
venv/

10
Dockerfile Normal file
View File

@ -0,0 +1,10 @@
FROM python:3.13-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["python", "main.py"]

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Aman
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

85
README.md Normal file
View File

@ -0,0 +1,85 @@
# Telegram Gatekeeper Bot
A Telegram bot that verifies new users when they join a group and
automatically removes unverified users after a timeout.
[![Deploy on Railway](https://railway.com/button.svg)](https://railway.com/deploy/telegram-gatekeeper-bot?referralCode=nIQTyp&utm_medium=integration&utm_source=template&utm_campaign=generic)
---
## ✨ Features
- 🛡️ Restricts new users on join (supergroups)
- 😀 Emoji-based human verification
- ⏱️ Auto-kicks users who fail to verify in time
- 👮 Admins are automatically bypassed
- 🧹 Cleans up join / leave / kick service messages
- 🔄 Automatically detects group type (supergroup vs normal group)
---
## Group Type Behavior
Telegram has different capabilities for normal groups and supergroups.
This bot automatically detects the group type and adjusts its behavior.
- ✅ **Supergroup** → Full gatekeeper protection (verification, timeout, cleanup)
- ⚠️ **Normal group** → Limited mode (no user restrictions)
To enable full protection, convert your group into a supergroup:
- Make the group public (set a username), or
- Let Telegram auto-upgrade it automatically
## 🚀 Deploy on Railway
Click the button below to deploy instantly:
[![Deploy on Railway](https://railway.com/button.svg)](https://railway.com/deploy/telegram-gatekeeper-bot?referralCode=nIQTyp&utm_medium=integration&utm_source=template&utm_campaign=generic)
### Required Environment Variables
| Variable | Description |
|--------|------------|
| BOT_TOKEN | Telegram bot token from @BotFather |
| VERIFY_TIMEOUT | Seconds before kicking unverified users (default: 60) |
---
## 🤖 Bot Setup
1. Create a bot using **@BotFather**
2. Copy the bot token
3. Deploy this project on Railway
4. Set the BOT_TOKEN environment variable
5. Add the bot to your Telegram group as **admin**
6. Grant permissions:
- Restrict users
- Ban users
- Delete messages
---
## 🧪 How It Works
1. User joins the group
2. Bot restricts the user
3. Emoji verification message is sent
4. User clicks the correct emoji
5. Restrictions are removed
6. If the user does nothing → bot kicks them after the timeout
---
## 🛠️ Local Development (Optional)
```bash
pip install -r requirements.txt
export BOT_TOKEN=your_token_here
python main.py
```
---
## 📜 License
MIT License

7
config.py Normal file
View File

@ -0,0 +1,7 @@
import os
BOT_TOKEN = os.getenv("BOT_TOKEN")
VERIFY_TIMEOUT = int(os.getenv("VERIFY_TIMEOUT", "60"))
if not BOT_TOKEN:
raise RuntimeError("BOT_TOKEN is required")

33
main.py Normal file
View File

@ -0,0 +1,33 @@
import asyncio
import logging
from aiogram import Bot, Dispatcher
from routers import join, verify, service_cleanup
from config import BOT_TOKEN
from routers import join, verify
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s | %(levelname)s | %(name)s | %(message)s",
)
logging.getLogger("aiogram").setLevel(logging.WARNING)
logging.getLogger("aiogram.event").setLevel(logging.WARNING)
logging.getLogger("aiogram.dispatcher").setLevel(logging.WARNING)
logging.getLogger("aiogram.client").setLevel(logging.WARNING)
async def main():
logging.info("Starting Gatekeeper Bot...")
bot = Bot(BOT_TOKEN)
dp = Dispatcher()
dp.include_router(join.router)
dp.include_router(verify.router)
dp.include_router(service_cleanup.router)
logging.info("Bot started. Waiting for updates...")
await dp.start_polling(bot)
if __name__ == "__main__":
asyncio.run(main())

2
requirements.txt Normal file
View File

@ -0,0 +1,2 @@
aiogram==3.23.0
python-dotenv==1.2.1

0
routers/__init__.py Normal file
View File

100
routers/join.py Normal file
View File

@ -0,0 +1,100 @@
import asyncio
import random
import logging
from aiogram import Router
from config import VERIFY_TIMEOUT
from aiogram.types import ChatMemberUpdated, InlineKeyboardButton, InlineKeyboardMarkup
from aiogram.enums import ChatMemberStatus
from services.restrict import restrict
router = Router()
pending = {}
EMOJIS = ["🔥", "🍕", "🐶", "🚀", "🎯", "🍀", ""]
@router.chat_member()
async def on_chat_member_update(event: ChatMemberUpdated):
old = event.old_chat_member.status
new = event.new_chat_member.status
chat_type = event.chat.type
logging.info(f"Bot mode: {'FULL' if event.chat.type=='supergroup' else 'LIMITED'}")
if new in (ChatMemberStatus.ADMINISTRATOR, ChatMemberStatus.CREATOR):
logging.info("Admin joined → bypass")
return
if chat_type != "supergroup":
logging.info("Normal group detected → limited mode")
return
if old in (ChatMemberStatus.LEFT, ChatMemberStatus.KICKED) and new == ChatMemberStatus.MEMBER:
user = event.new_chat_member.user
chat_id = event.chat.id
logging.info(f"New user joined (gatekeeper): {user.id}")
await restrict(event.bot, chat_id, user.id)
correct = random.choice(EMOJIS)
wrong = random.sample([e for e in EMOJIS if e != correct], 2)
options = wrong + [correct]
random.shuffle(options)
kb = InlineKeyboardMarkup(
inline_keyboard=[
[
InlineKeyboardButton(
text=e,
callback_data=f"verify:{user.id}:{e}"
) for e in options
]
]
)
msg = await event.bot.send_message(
chat_id,
(
f"👋 Welcome {user.mention_html()}!\n\n"
f"To continue, please tap the {correct} emoji below.\n"
f"This helps keep spam out 🤖🚫"
),
reply_markup=kb,
parse_mode="HTML",
)
pending[(chat_id, user.id)] = {
"message_id": msg.message_id,
"answer": correct,
}
asyncio.create_task(
verification_timeout(event.bot, chat_id, user.id, timeout=VERIFY_TIMEOUT)
)
logging.info(f"Verification timeout started for {user.id}")
async def verification_timeout(bot, chat_id: int, user_id: int, timeout: int = 60):
await asyncio.sleep(timeout)
key = (chat_id, user_id)
data = pending.get(key)
if not data:
return
logging.info(f"Verification timeout → kick {user_id}")
try:
await bot.ban_chat_member(chat_id, user_id)
await bot.unban_chat_member(chat_id, user_id)
except:
pass
try:
await bot.delete_message(chat_id, data["message_id"])
except:
pass
del pending[key]

View File

@ -0,0 +1,14 @@
from aiogram import Router
from aiogram.types import Message
router = Router()
join_messages = {}
@router.message()
async def capture_service_messages(msg: Message):
if msg.new_chat_members:
join_messages[msg.chat.id] = msg.message_id
elif msg.left_chat_member:
join_messages[msg.chat.id] = msg.message_id

46
routers/verify.py Normal file
View File

@ -0,0 +1,46 @@
from aiogram import Router
from aiogram.types import CallbackQuery
from services.restrict import unrestrict
from routers.join import pending
from routers.service_cleanup import join_messages
router = Router()
@router.callback_query(lambda c: c.data.startswith("verify:"))
async def verify_user(call: CallbackQuery):
_, user_id, emoji = call.data.split(":")
user_id = int(user_id)
chat_id = call.message.chat.id
if call.from_user.id != user_id:
await call.answer("This button is not for you.", show_alert=True)
return
key = (chat_id, user_id)
data = pending.get(key)
if not data:
await call.answer("Verification expired.", show_alert=True)
return
if emoji != data["answer"]:
await call.answer("❌ Wrong emoji. Try again.", show_alert=True)
return
del pending[key]
await call.answer("✅ Verified! You can now chat.")
await unrestrict(call.bot, chat_id, user_id)
try:
await call.message.delete()
except:
pass
join_msg = join_messages.pop(chat_id, None)
if join_msg:
try:
await call.bot.delete_message(chat_id, join_msg)
except:
pass

0
services/__init__.py Normal file
View File

28
services/restrict.py Normal file
View File

@ -0,0 +1,28 @@
from aiogram import Bot
from aiogram.types import ChatPermissions
READ_ONLY = ChatPermissions(
can_send_messages=False,
can_send_media_messages=False,
can_send_polls=False,
can_send_other_messages=False,
)
FULL = ChatPermissions(
can_send_messages=True,
can_send_media_messages=True,
can_send_polls=True,
can_send_other_messages=True,
)
async def restrict(bot: Bot, chat_id: int, user_id: int):
try:
await bot.restrict_chat_member(chat_id, user_id, READ_ONLY)
except Exception:
pass
async def unrestrict(bot: Bot, chat_id: int, user_id: int):
try:
await bot.restrict_chat_member(chat_id, user_id, FULL)
except Exception:
pass