Trending OTT Platforms
Swipe or scroll through your featured 360Hub destinations and jump straight into a channel.
Collections & playlists
Folders are shown as binge-ready rows with rich card styling.
diff --git a/bot/helper/database.py b/bot/helper/database.py index d5fb403..117b8c3 100644 --- a/bot/helper/database.py +++ b/bot/helper/database.py @@ -12,6 +12,7 @@ def __init__(self): self.collection = self.db["playlist"] self.config = self.db["config"] self.files = self.db["files"] + self.verifications = self.db["verifications"] async def create_folder(self, parent_id, folder_name, thumbnail): folder = {"parent_folder": parent_id, "name": folder_name, @@ -123,3 +124,18 @@ async def search_tgfiles(self, id, query, page=1, per_page=50): async def add_btgfiles(self, data): result = self.files.insert_many(data) + + async def save_verification(self, visitor_id, ip_address, user_agent, verified_at, expires_at): + payload = { + "visitor_id": visitor_id, + "ip_address": ip_address, + "user_agent": user_agent, + "verified_at": verified_at, + "expires_at": expires_at, + } + result = self.verifications.update_one( + {"visitor_id": visitor_id}, + {"$set": payload}, + upsert=True + ) + return result.acknowledged diff --git a/bot/server/render_template.py b/bot/server/render_template.py index 3a4c964..d5cec07 100644 --- a/bot/server/render_template.py +++ b/bot/server/render_template.py @@ -39,6 +39,8 @@ async def render_page( redirect_url="", msg="", chat_id="", + verify_nonce="", + verify_wait=0, ): theme = await db.get_variable("theme") if theme is None or theme == "": @@ -51,6 +53,8 @@ async def render_page( .replace("", msg or "") .replace("", theme.lower()) .replace("", redirect_url) + .replace("", verify_nonce) + .replace("", str(verify_wait)) ) elif route == "home": async with aiopen(ospath.join(tpath, "home.html"), "r") as f: diff --git a/bot/server/stream_routes.py b/bot/server/stream_routes.py index 1356f1e..ab23f4d 100644 --- a/bot/server/stream_routes.py +++ b/bot/server/stream_routes.py @@ -3,6 +3,7 @@ import math import mimetypes import secrets +import time from aiohttp import web from aiohttp.http_exceptions import BadStatusLine from bot.helper.chats import get_chats, post_playlist, posts_chat, posts_db_file @@ -24,19 +25,59 @@ routes = web.RouteTableDef() db = Database() +VERIFICATION_WINDOW_SECONDS = 24 * 60 * 60 + + +def is_verified_session(session): + username = session.get('user') + verified_at = session.get('verified_at') + verified_user_id = session.get('verified_user_id') + if not username or not verified_at or not verified_user_id: + return False + return (int(time.time()) - int(verified_at)) < VERIFICATION_WINDOW_SECONDS + + +def get_client_ip(request): + forwarded_for = request.headers.get('X-Forwarded-For', '') + if forwarded_for: + return forwarded_for.split(',')[0].strip() + return request.headers.get('X-Real-IP') or request.remote or 'unknown' + + +async def get_verified_user(request): + session = await get_session(request) + if is_verified_session(session): + return session, session.get('user') + session.pop('user', None) + session.pop('verified_at', None) + session['redirect_url'] = request.path_qs + raise web.HTTPFound('/login') @routes.get('/login') async def login_form(request): session = await get_session(request) + if is_verified_session(session): + redirect_url = session.pop('redirect_url', '/') + raise web.HTTPFound(redirect_url) redirect_url = session.get('redirect_url', '/') - return web.Response(text=await render_page(None, None, route='login', redirect_url=redirect_url), content_type='text/html') + verify_nonce = secrets.token_urlsafe(24) + verify_wait = secrets.randbelow(16) + 10 + session['verify_nonce'] = verify_nonce + session['verify_wait'] = verify_wait + session['verify_started_at'] = None + return web.Response( + text=await render_page( + None, None, route='login', redirect_url=redirect_url, verify_nonce=verify_nonce, verify_wait=verify_wait + ), + content_type='text/html' + ) @routes.post('/login') async def login_route(request): session = await get_session(request) - if 'user' in session: + if is_verified_session(session): return web.HTTPFound('/') data = await request.post() username = data.get('username') @@ -44,6 +85,9 @@ async def login_route(request): error_message = None if (username == Telegram.USERNAME and password == Telegram.PASSWORD) or (username == Telegram.ADMIN_USERNAME and password == Telegram.ADMIN_PASSWORD): session['user'] = username + now = int(time.time()) + session['verified_at'] = now + session['verified_user_id'] = f"admin:{username}" if 'redirect_url' not in session: session['redirect_url'] = '/' redirect_url = session['redirect_url'] @@ -51,20 +95,88 @@ async def login_route(request): return web.HTTPFound(redirect_url) else: error_message = "Invalid username or password" - return web.Response(text=await render_page(None, None, route='login', msg=error_message), content_type='text/html') + verify_nonce = secrets.token_urlsafe(24) + verify_wait = secrets.randbelow(16) + 10 + session['verify_nonce'] = verify_nonce + session['verify_wait'] = verify_wait + session['verify_started_at'] = None + return web.Response( + text=await render_page(None, None, route='login', msg=error_message, verify_nonce=verify_nonce, verify_wait=verify_wait), + content_type='text/html' + ) + + +@routes.post('/verify/start') +async def verification_start_route(request): + session = await get_session(request) + if is_verified_session(session): + return web.json_response({"ok": True, "already_verified": True}) + data = await request.post() + nonce = data.get('nonce') + if not nonce or nonce != session.get('verify_nonce'): + return web.json_response({"ok": False, "error": "Invalid verification session"}, status=400) + session['verify_started_at'] = int(time.time()) + return web.json_response({"ok": True, "wait_seconds": int(session.get('verify_wait', 0))}) + + +@routes.post('/verify-access') +async def verify_access_route(request): + session = await get_session(request) + if is_verified_session(session): + return web.HTTPFound('/') + data = await request.post() + nonce = data.get('nonce') + if not nonce or nonce != session.get('verify_nonce'): + return web.HTTPForbidden(text='Verification failed. Please restart verification.') + wait_seconds = int(session.get('verify_wait') or 0) + verify_started_at = session.get('verify_started_at') + if not verify_started_at: + return web.HTTPForbidden(text='Verification is incomplete. Please open the ad first.') + now = int(time.time()) + if now - int(verify_started_at) < wait_seconds: + return web.HTTPForbidden(text='Verification timer not finished yet.') + + visitor_id = session.get('visitor_id') + if not visitor_id: + visitor_id = secrets.token_urlsafe(18) + session['visitor_id'] = visitor_id + user_agent = request.headers.get('User-Agent', '') + ip_address = get_client_ip(request) + expires_at = now + VERIFICATION_WINDOW_SECONDS + saved = await db.save_verification( + visitor_id=visitor_id, + ip_address=ip_address, + user_agent=user_agent, + verified_at=now, + expires_at=expires_at, + ) + if not saved: + raise web.HTTPInternalServerError(text='Unable to save verification state.') + + session['user'] = Telegram.USERNAME + session['verified_at'] = now + session['verified_user_id'] = visitor_id + session.pop('verify_nonce', None) + session.pop('verify_wait', None) + session.pop('verify_started_at', None) + redirect_url = data.get('redirect_url') or session.get('redirect_url', '/') + session.pop('redirect_url', None) + return web.HTTPFound(redirect_url) @routes.post('/logout') async def logout_route(request): session = await get_session(request) session.pop('user', None) + session.pop('verified_at', None) + session.pop('verified_user_id', None) return web.HTTPFound('/login') @routes.post('/create') async def create_route(request): - session = await get_session(request) - if (username := session.get('user')) != Telegram.ADMIN_USERNAME: + _, username = await get_verified_user(request) + if username != Telegram.ADMIN_USERNAME: return web.json_response({'msg': 'Who the hell you are'}) data = await request.post() folderName = data.get('folderName') @@ -80,8 +192,8 @@ async def create_route(request): @routes.post('/delete') async def delete_route(request): - session = await get_session(request) - if (username := session.get('user')) != Telegram.ADMIN_USERNAME: + _, username = await get_verified_user(request) + if username != Telegram.ADMIN_USERNAME: return web.json_response({'msg': 'Who the hell you are'}) data = await request.json() id = data.get('delete_id') @@ -96,8 +208,8 @@ async def delete_route(request): @routes.post('/edit') async def editFolder_route(request): - session = await get_session(request) - if (username := session.get('user')) != Telegram.ADMIN_USERNAME: + _, username = await get_verified_user(request) + if username != Telegram.ADMIN_USERNAME: return web.json_response({'msg': 'Who the hell you are'}) data = await request.post() folderName = data.get('folderName') @@ -115,8 +227,8 @@ async def editFolder_route(request): @routes.post('/edit_post') async def editPost_route(request): - session = await get_session(request) - if (username := session.get('user')) != Telegram.ADMIN_USERNAME: + _, username = await get_verified_user(request) + if username != Telegram.ADMIN_USERNAME: return web.json_response({'msg': 'Who the hell you are'}) data = await request.post() fileName = data.get('fileName') @@ -134,8 +246,8 @@ async def editPost_route(request): @routes.get('/searchDbFol') async def searchDbFolder_route(request): - session = await get_session(request) - if (username := session.get('user')) != Telegram.ADMIN_USERNAME: + _, username = await get_verified_user(request) + if username != Telegram.ADMIN_USERNAME: return web.json_response({'msg': 'Who the hell you are'}) query = request.query.get('query', '') folder_names = await db.search_DbFolder(query) @@ -144,6 +256,9 @@ async def searchDbFolder_route(request): @routes.post('/send') async def send_route(request): + _, username = await get_verified_user(request) + if username != Telegram.ADMIN_USERNAME: + return web.json_response({'msg': 'Who the hell you are'}) data = await request.post() chat_id = data.get('chatId') chat_id = f"-100{chat_id}" @@ -178,8 +293,8 @@ async def send_route(request): @routes.get('/reload') async def reload_route(request): - session = await get_session(request) - if (username := session.get('user')) != Telegram.ADMIN_USERNAME: + _, username = await get_verified_user(request) + if username != Telegram.ADMIN_USERNAME: return web.json_response({'msg': 'Who the hell you are'}) chat_id = request.query.get('chatId', '') @@ -193,8 +308,8 @@ async def reload_route(request): @routes.post('/config') async def editConfig_route(request): - session = await get_session(request) - if (username := session.get('user')) != Telegram.ADMIN_USERNAME: + _, username = await get_verified_user(request) + if username != Telegram.ADMIN_USERNAME: return web.json_response({'msg': 'Who the hell you are'}) data = await request.post() channel = data.get('channel') @@ -208,109 +323,89 @@ async def editConfig_route(request): @routes.get('/') async def home_route(request): - session = await get_session(request) - if username := session.get('user'): - try: - channels = await get_chats() - playlists = await db.get_Dbfolder() - phtml = await posts_chat(channels) - dhtml = await post_playlist(playlists) - is_admin = username == Telegram.ADMIN_USERNAME - return web.Response(text=await render_page(None, None, route='home', html=phtml, playlist=dhtml, is_admin=is_admin), content_type='text/html') - except Exception as e: - logging.critical(e.with_traceback(None)) - raise web.HTTPInternalServerError(text=str(e)) from e - else: - session['redirect_url'] = request.path_qs - return web.HTTPFound('/login') + _, username = await get_verified_user(request) + try: + channels = await get_chats() + playlists = await db.get_Dbfolder() + phtml = await posts_chat(channels) + dhtml = await post_playlist(playlists) + is_admin = username == Telegram.ADMIN_USERNAME + return web.Response(text=await render_page(None, None, route='home', html=phtml, playlist=dhtml, is_admin=is_admin), content_type='text/html') + except Exception as e: + logging.critical(e.with_traceback(None)) + raise web.HTTPInternalServerError(text=str(e)) from e @routes.get('/playlist') async def playlist_route(request): - session = await get_session(request) - if username := session.get('user'): - try: - parent_id = request.query.get('db') - page = request.query.get('page', '1') - playlists = await db.get_Dbfolder(parent_id, page=page) - files = await db.get_dbFiles(parent_id, page=page) - text = await db.get_info(parent_id) - dhtml = await post_playlist(playlists) - dphtml = await posts_db_file(files) - is_admin = username == Telegram.ADMIN_USERNAME - return web.Response(text=await render_page(parent_id, None, route='playlist', playlist=dhtml, database=dphtml, msg=text, is_admin=is_admin), content_type='text/html') - except Exception as e: - logging.critical(e.with_traceback(None)) - raise web.HTTPInternalServerError(text=str(e)) from e - else: - session['redirect_url'] = request.path_qs - return web.HTTPFound('/login') + _, username = await get_verified_user(request) + try: + parent_id = request.query.get('db') + page = request.query.get('page', '1') + playlists = await db.get_Dbfolder(parent_id, page=page) + files = await db.get_dbFiles(parent_id, page=page) + text = await db.get_info(parent_id) + dhtml = await post_playlist(playlists) + dphtml = await posts_db_file(files) + is_admin = username == Telegram.ADMIN_USERNAME + return web.Response(text=await render_page(parent_id, None, route='playlist', playlist=dhtml, database=dphtml, msg=text, is_admin=is_admin), content_type='text/html') + except Exception as e: + logging.critical(e.with_traceback(None)) + raise web.HTTPInternalServerError(text=str(e)) from e @routes.get('/search/db/{parent}') async def dbsearch_route(request): - session = await get_session(request) - if username := session.get('user'): - parent = request.match_info['parent'] - page = request.query.get('page', '1') - query = request.query.get('q') - is_admin = username == Telegram.ADMIN_USERNAME - try: - files = await db.search_dbfiles(id=parent, page=page, query=query) - dphtml = await posts_db_file(files) - name = await db.get_info(parent) - text = f"{name} - {query}" - return web.Response(text=await render_page(parent, None, route='playlist', database=dphtml, msg=text, is_admin=is_admin), content_type='text/html') - except Exception as e: - logging.critical(e.with_traceback(None)) - raise web.HTTPInternalServerError(text=str(e)) from e - else: - session['redirect_url'] = request.path_qs - return web.HTTPFound('/login') + _, username = await get_verified_user(request) + parent = request.match_info['parent'] + page = request.query.get('page', '1') + query = request.query.get('q') + is_admin = username == Telegram.ADMIN_USERNAME + try: + files = await db.search_dbfiles(id=parent, page=page, query=query) + dphtml = await posts_db_file(files) + name = await db.get_info(parent) + text = f"{name} - {query}" + return web.Response(text=await render_page(parent, None, route='playlist', database=dphtml, msg=text, is_admin=is_admin), content_type='text/html') + except Exception as e: + logging.critical(e.with_traceback(None)) + raise web.HTTPInternalServerError(text=str(e)) from e @routes.get('/channel/{chat_id}') async def channel_route(request): - session = await get_session(request) - if username := session.get('user'): - chat_id = request.match_info['chat_id'] - chat_id = f"-100{chat_id}" - page = request.query.get('page', '1') - is_admin = username == Telegram.ADMIN_USERNAME - try: - posts = await get_files(chat_id, page=page) - phtml = await posts_file(posts, chat_id) - chat = await StreamBot.get_chat(int(chat_id)) - return web.Response(text=await render_page(None, None, route='index', html=phtml, msg=chat.title, chat_id=chat_id.replace("-100", ""), is_admin=is_admin), content_type='text/html') - except Exception as e: - logging.critical(e.with_traceback(None)) - raise web.HTTPInternalServerError(text=str(e)) from e - else: - session['redirect_url'] = request.path_qs - return web.HTTPFound('/login') + _, username = await get_verified_user(request) + chat_id = request.match_info['chat_id'] + chat_id = f"-100{chat_id}" + page = request.query.get('page', '1') + is_admin = username == Telegram.ADMIN_USERNAME + try: + posts = await get_files(chat_id, page=page) + phtml = await posts_file(posts, chat_id) + chat = await StreamBot.get_chat(int(chat_id)) + return web.Response(text=await render_page(None, None, route='index', html=phtml, msg=chat.title, chat_id=chat_id.replace("-100", ""), is_admin=is_admin), content_type='text/html') + except Exception as e: + logging.critical(e.with_traceback(None)) + raise web.HTTPInternalServerError(text=str(e)) from e @routes.get('/search/{chat_id}') async def search_route(request): - session = await get_session(request) - if username := session.get('user'): - chat_id = request.match_info['chat_id'] - chat_id = f"-100{chat_id}" - page = request.query.get('page', '1') - query = request.query.get('q') - is_admin = username == Telegram.ADMIN_USERNAME - try: - posts = await search(chat_id, page=page, query=query) - phtml = await posts_file(posts, chat_id) - chat = await StreamBot.get_chat(int(chat_id)) - text = f"{chat.title} - {query}" - return web.Response(text=await render_page(None, None, route='index', html=phtml, msg=text, chat_id=chat_id.replace("-100", ""), is_admin=is_admin), content_type='text/html') - except Exception as e: - logging.critical(e.with_traceback(None)) - raise web.HTTPInternalServerError(text=str(e)) from e - else: - session['redirect_url'] = request.path_qs - return web.HTTPFound('/login') + _, username = await get_verified_user(request) + chat_id = request.match_info['chat_id'] + chat_id = f"-100{chat_id}" + page = request.query.get('page', '1') + query = request.query.get('q') + is_admin = username == Telegram.ADMIN_USERNAME + try: + posts = await search(chat_id, page=page, query=query) + phtml = await posts_file(posts, chat_id) + chat = await StreamBot.get_chat(int(chat_id)) + text = f"{chat.title} - {query}" + return web.Response(text=await render_page(None, None, route='index', html=phtml, msg=text, chat_id=chat_id.replace("-100", ""), is_admin=is_admin), content_type='text/html') + except Exception as e: + logging.critical(e.with_traceback(None)) + raise web.HTTPInternalServerError(text=str(e)) from e @routes.get('/api/thumb/{chat_id}', allow_head=True) @@ -327,27 +422,23 @@ async def get_thumbnail(request): @routes.get('/watch/{chat_id}', allow_head=True) async def stream_handler_watch(request: web.Request): - session = await get_session(request) - if username := session.get('user'): - try: - chat_id = request.match_info['chat_id'] - chat_id = f"-100{chat_id}" - message_id = request.query.get('id') - secure_hash = request.query.get('hash') - return web.Response(text=await render_page(message_id, secure_hash, chat_id=chat_id), content_type='text/html') - except InvalidHash as e: - raise web.HTTPForbidden(text=e.message) from e - except FIleNotFound as e: - db.delete_file(chat_id=chat_id, msg_id=message_id, hash=secure_hash) - raise web.HTTPNotFound(text=e.message) from e - except (AttributeError, BadStatusLine, ConnectionResetError): - pass - except Exception as e: - logging.critical(e.with_traceback(None)) - raise web.HTTPInternalServerError(text=str(e)) from e - else: - session['redirect_url'] = request.path_qs - return web.HTTPFound('/login') + await get_verified_user(request) + try: + chat_id = request.match_info['chat_id'] + chat_id = f"-100{chat_id}" + message_id = request.query.get('id') + secure_hash = request.query.get('hash') + return web.Response(text=await render_page(message_id, secure_hash, chat_id=chat_id), content_type='text/html') + except InvalidHash as e: + raise web.HTTPForbidden(text=e.message) from e + except FIleNotFound as e: + db.delete_file(chat_id=chat_id, msg_id=message_id, hash=secure_hash) + raise web.HTTPNotFound(text=e.message) from e + except (AttributeError, BadStatusLine, ConnectionResetError): + pass + except Exception as e: + logging.critical(e.with_traceback(None)) + raise web.HTTPInternalServerError(text=str(e)) from e @routes.get('/{chat_id}/{encoded_name}', allow_head=True) diff --git a/bot/server/template/home.html b/bot/server/template/home.html index 3ab433d..9a203e9 100644 --- a/bot/server/template/home.html +++ b/bot/server/template/home.html @@ -252,5 +252,5 @@
-Swipe or scroll through your featured 360Hub destinations and jump straight into a channel.
Folders are shown as binge-ready rows with rich card styling.
Swipe or scroll through your featured 360Hub destinations and jump straight into a channel.
Folders are shown as binge-ready rows with rich card styling.