Initial commit
This commit is contained in:
commit
7b5c71d832
|
|
@ -0,0 +1,4 @@
|
|||
__pycache__/
|
||||
*.pyc
|
||||
.env
|
||||
venv/
|
||||
|
|
@ -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"]
|
||||
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
||||
[](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:
|
||||
|
||||
[](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
|
||||
|
|
@ -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")
|
||||
|
|
@ -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())
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
aiogram==3.23.0
|
||||
python-dotenv==1.2.1
|
||||
|
|
@ -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]
|
||||
|
|
@ -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
|
||||
|
|
@ -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,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
|
||||
Loading…
Reference in New Issue