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