Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -275,3 +275,67 @@ will. Specifically you can redistribute and/or modify it under the terms of the
[GNU Affero General Public License](https://www.gnu.org/licenses/agpl-3.0.en.html) as
published by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version. Also keep in mind that all the forks of this repository MUST BE OPEN-SOURCE and MUST BE UNDER THE SAME LICENSE.

---

## Telegram Bot (Production Workflow)

This repository now includes a production-ready **Pyrogram + MongoDB** bot workflow with:

- Force subscription flow (`/start` + join check + retry button + pending content delivery)
- Movie/series keyword search with pagination and inline result buttons
- Admin commands: `/stats`, `/users`, `/broadcast`
- Mongo persistence for users, content index, global metrics, and broadcast logs
- Channel auto-indexing for new files and `/index` command for history indexing

### Folder Structure (Telegram Features)

```text
bot/
├── config.py
├── helper/
│ ├── bot_database.py
│ └── retry.py
└── telegram/
├── keyboards.py
├── services.py
└── plugins/
└── start.py
```

### Required Environment Variables

```env
BOT_TOKEN=
MONGO_URI= # fallback: DATABASE_URL
FORCE_SUB_CHANNELS=@channelA,@channelB
START_IMAGE_URL=https://...
ADMIN_IDS=123456789,987654321
```

Existing values like `AUTH_CHANNEL` remain supported for indexing and fallback force-sub channels.

### Commands

- `/start [payload]` → force-sub check + welcome photo + optional content delivery
- `/search <keyword>` or plain text in private chat → content search
- `/index` (in indexed channel) → import channel history
- `/stats` (admin) → users + request metrics
- `/users` (admin) → user count + preview
- `/broadcast` (admin, reply required) → mass broadcast with progress

> Security restriction: bot startup is locked to **@Stream4u_bot**. If token/username does not match, the process exits.

### Deployment (VPS / Render / Koyeb)

1. Set env vars above and existing Telegram API variables (`API_ID`, `API_HASH`, `BOT_TOKEN`).
2. Install dependencies:
```bash
pip install -r requirements.txt
```
3. Run:
```bash
python -m bot
```
4. Ensure MongoDB is reachable and bot is admin in required channels.

60 changes: 36 additions & 24 deletions bot/__main__.py
Original file line number Diff line number Diff line change
@@ -1,59 +1,71 @@
from asyncio import get_event_loop, sleep as asleep, gather
from asyncio import get_event_loop, sleep as asleep
from traceback import format_exc

from aiohttp import web
from pyrogram import idle

from bot import __version__, LOGGER
from bot.config import Telegram
from bot.helper.bot_database import bot_db
from bot.server import web_server
from bot.telegram import StreamBot, UserBot
from bot.telegram.clients import initialize_clients

loop = get_event_loop()

async def start_services():
LOGGER.info(f'Initializing Surf-TG v-{__version__}')
await asleep(1.2)


def _validate_restricted_bot() -> None:
username = (StreamBot.me.username or "").lower()
if username != Telegram.REQUIRED_BOT_USERNAME:
raise RuntimeError(
f"Restricted bot check failed. Expected @{Telegram.REQUIRED_BOT_USERNAME}, got @{username or 'unknown'}."
)

token_prefix = Telegram.BOT_TOKEN.split(":", 1)[0].strip()
if token_prefix and token_prefix != str(StreamBot.me.id):
raise RuntimeError("BOT_TOKEN does not belong to the connected bot account.")


async def start_services() -> None:
LOGGER.info(f"Initializing Surf-TG v-{__version__}")
await bot_db.ensure_indexes()
await asleep(0.8)

await StreamBot.start()
_validate_restricted_bot()
StreamBot.username = StreamBot.me.username
LOGGER.info(f"Bot Client : [@{StreamBot.username}]")
if len(Telegram.SESSION_STRING) != 0:

if Telegram.SESSION_STRING:
await UserBot.start()
UserBot.username = UserBot.me.username or UserBot.me.first_name or UserBot.me.id
LOGGER.info(f"User Client : {UserBot.username}")
await asleep(1.2)

await asleep(0.8)
LOGGER.info("Initializing Multi Clients")
await initialize_clients()

await asleep(2)
LOGGER.info('Initalizing Surf Web Server..')

LOGGER.info("Initializing Surf Web Server..")
server = web.AppRunner(await web_server())
LOGGER.info("Server CleanUp!")
await server.cleanup()

await asleep(2)
LOGGER.info("Server Setup Started !")

await server.setup()
await web.TCPSite(server, '0.0.0.0', Telegram.PORT).start()
await web.TCPSite(server, "0.0.0.0", Telegram.PORT).start()

LOGGER.info("Surf-TG Started Revolving !")
LOGGER.info("Surf-TG Started")
await idle()

async def stop_clients():
await StreamBot.stop()
if len(Telegram.SESSION_STRING) != 0:

async def stop_clients() -> None:
if StreamBot.is_connected:
await StreamBot.stop()
if Telegram.SESSION_STRING and UserBot.is_connected:
await UserBot.stop()


if __name__ == '__main__':
if __name__ == "__main__":
try:
loop.run_until_complete(start_services())
except KeyboardInterrupt:
LOGGER.info('Service Stopping...')
LOGGER.info("Service Stopping...")
except Exception:
LOGGER.error(format_exc())
finally:
Expand Down
48 changes: 40 additions & 8 deletions bot/config.py
Original file line number Diff line number Diff line change
@@ -1,25 +1,57 @@
from os import getenv
from dotenv import load_dotenv
from pathlib import Path

from dotenv import load_dotenv

if Path("config.env").exists():
load_dotenv("config.env")


def _csv_list(key: str, default: str = "") -> list[str]:
raw = getenv(key, default)
return [item.strip() for item in raw.split(",") if item.strip()]


def _int_list(key: str) -> list[int]:
values: list[int] = []
for item in _csv_list(key):
try:
values.append(int(item))
except ValueError:
continue
return values


class Telegram:
API_ID = int(getenv("API_ID", "0"))
API_HASH = getenv("API_HASH", "")
BOT_TOKEN = getenv("BOT_TOKEN", "")
PORT = int(getenv("PORT", 8080))
SESSION_STRING = getenv("SESSION_STRING", "")
BASE_URL = getenv("BASE_URL", "").rstrip('/')
DATABASE_URL = getenv("DATABASE_URL", "")
AUTH_CHANNEL = [channel.strip() for channel in getenv("AUTH_CHANNEL", "").split(",") if channel.strip()]

BASE_URL = getenv("BASE_URL", "").rstrip("/")
MONGO_URI = getenv("MONGO_URI") or getenv("DATABASE_URL", "")
DATABASE_URL = MONGO_URI # backward compatibility for existing modules

AUTH_CHANNEL = _csv_list("AUTH_CHANNEL")
FORCE_SUB_CHANNELS = _csv_list("FORCE_SUB_CHANNELS") or AUTH_CHANNEL
START_IMAGE_URL = getenv(
"START_IMAGE_URL",
"https://placehold.co/1280x720/png?text=Welcome+to+Movie+Bot",
)
ADMIN_IDS = _int_list("ADMIN_IDS")

REQUIRED_BOT_USERNAME = "stream4u_bot"

THEME = getenv("THEME", "vapor").lower()
USERNAME = getenv("USERNAME", "admin")
PASSWORD = getenv("PASSWORD", "admin")
ADMIN_USERNAME = getenv("ADMIN_USERNAME", "surfTG")
ADMIN_PASSWORD = getenv("ADMIN_PASSWORD", "surfTG")
SLEEP_THRESHOLD = int(getenv('SLEEP_THRESHOLD', '60'))
WORKERS = int(getenv('WORKERS', '10'))
MULTI_CLIENT = getenv('MULTI_CLIENT', 'False')
HIDE_CHANNEL = getenv('HIDE_CHANNEL', 'False')

SLEEP_THRESHOLD = int(getenv("SLEEP_THRESHOLD", "60"))
WORKERS = int(getenv("WORKERS", "10"))
MULTI_CLIENT = getenv("MULTI_CLIENT", "False")
HIDE_CHANNEL = getenv("HIDE_CHANNEL", "False")

SEARCH_PAGE_SIZE = int(getenv("SEARCH_PAGE_SIZE", "8"))
Loading