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 @@ -
Featured Telegram Streaming

A premium OTT-style gateway for your 360Hub library.

Browse channels, collections, and playlists in a cleaner, cinematic interface while keeping every existing Telegram-powered workflow exactly the same.

Start Watching
Dashboard

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.

+
Featured Telegram Streaming

A premium OTT-style gateway for your 360Hub library.

Browse channels, collections, and playlists in a cleaner, cinematic interface while keeping every existing Telegram-powered workflow exactly the same.

Start Watching
Dashboard

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.

\ No newline at end of file diff --git a/bot/server/template/login.html b/bot/server/template/login.html index 10340a7..719a03a 100644 --- a/bot/server/template/login.html +++ b/bot/server/template/login.html @@ -1,228 +1,120 @@ -360Hub: Login + + +
+
+
+ 360Hub +
+
OTT Streaming
+

360Hub Access Verification

+
+
-.ott-app-new, -.ott-app-new body { background: #07080d; } -.ott-app-new .nm-platform-col, -.ott-app-new .nm-rail-col { min-width: 0; } -.ott-app-new .nm-platform-link, -.ott-app-new .nm-media-link { display:block; } -.ott-app-new .nm-platform-card { - display:flex; align-items:center; gap:1rem; min-height: 112px; padding: 1rem 1.1rem; - border-radius: 24px; border: 1px solid rgba(255,255,255,.08); - background: linear-gradient(145deg, rgba(19,22,33,.98), rgba(10,11,18,.96)); - box-shadow: 0 16px 40px rgba(0,0,0,.35); - transition: transform .35s ease, border-color .35s ease, box-shadow .35s ease; -} -.ott-app-new .nm-platform-card:hover { transform: translateY(-6px); border-color: rgba(229,9,20,.45); box-shadow: 0 24px 46px rgba(0,0,0,.5); } -.ott-app-new .nm-platform-logo-wrap { - width: 72px; height: 72px; border-radius: 22px; overflow:hidden; flex: 0 0 72px; - background: linear-gradient(135deg, rgba(229,9,20,.22), rgba(124,58,237,.22)); padding: 3px; -} -.ott-app-new .nm-platform-logo { width:100%; height:100%; object-fit:cover; border-radius: 19px; background:#0b0b0b; } -.ott-app-new .nm-platform-kicker, .ott-app-new .nm-media-tag { color: #d1d5db; font-size:.72rem; letter-spacing:.16em; text-transform:uppercase; } -.ott-app-new .nm-platform-title { margin:.25rem 0; font-size:1rem; font-weight:700; color:#fff; } -.ott-app-new .nm-platform-pill, -.ott-app-new .nm-media-badge { - display:inline-flex; align-items:center; justify-content:center; min-height: 28px; - padding:.3rem .72rem; border-radius:999px; background: rgba(255,255,255,.08); color:#fff; - border: 1px solid rgba(255,255,255,.1); font-size:.75rem; -} -.ott-app-new .nm-media-badge--info { background: rgba(124,58,237,.24); } -.ott-app-new .nm-media-card { - position:relative; height:100%; overflow:hidden; border-radius: 24px; - background: linear-gradient(180deg, rgba(18,20,30,.98), rgba(8,9,14,.98)); - border:1px solid rgba(255,255,255,.08); box-shadow: 0 22px 45px rgba(0,0,0,.34); - transition: transform .35s ease, border-color .35s ease, box-shadow .35s ease; -} -.ott-app-new .nm-media-card:hover { transform: translateY(-8px) scale(1.02); border-color: rgba(229,9,20,.42); box-shadow: 0 28px 54px rgba(0,0,0,.52); } -.ott-app-new .nm-media-poster-wrap { position:relative; aspect-ratio: 2/3; overflow:hidden; background:#111; } -.ott-app-new .nm-media-poster { width:100%; height:100%; object-fit:cover; display:block; } -.ott-app-new .nm-media-overlay { - position:absolute; inset:0; background: linear-gradient(180deg, rgba(7,8,13,.04) 10%, rgba(7,8,13,.36) 55%, rgba(7,8,13,.95) 100%); -} -.ott-app-new .nm-play-chip { - position:absolute; inset:auto auto 1rem 1rem; width: 48px; height: 48px; display:grid; place-items:center; - border-radius:50%; background: linear-gradient(135deg, #e50914, #7c3aed); color:#fff; font-size:1.2rem; - box-shadow: 0 16px 28px rgba(0,0,0,.35); opacity:0; transform: translateY(12px); transition: all .3s ease; -} -.ott-app-new .nm-media-card:hover .nm-play-chip { opacity:1; transform: translateY(0); } -.ott-app-new .nm-media-body { padding: 1rem 1rem 1.1rem; } -.ott-app-new .nm-media-title { - margin: .35rem 0 .7rem; color:#fff; font-weight:700; line-height:1.35; - display:-webkit-box; -webkit-box-orient:vertical; -webkit-line-clamp:2; overflow:hidden; min-height: 2.7em; -} -.ott-app-new .nm-meta-row { display:flex; flex-wrap:wrap; gap:.45rem; } -.ott-app-new .nm-edit-btn, -.ott-app-new .nm-select-box { - position:absolute !important; top: .9rem !important; right: .9rem !important; z-index:3; -} -.ott-app-new .nm-edit-btn { - width: 38px; height: 38px; display:grid; place-items:center; border-radius: 50%; - background: rgba(7,8,13,.72); color:#fff; border:1px solid rgba(255,255,255,.12); - backdrop-filter: blur(8px); -} -.ott-app-new .nm-select-box { width: 1.15rem; height: 1.15rem; } -.ott-app-new .grid-cards { row-gap: 1rem !important; } -.ott-app-new .scroll-strip { display:flex !important; flex-wrap:nowrap !important; overflow-x:auto !important; padding-bottom: .85rem; } -.ott-app-new .scroll-strip > * { flex: 0 0 min(320px, 84vw); } -.ott-app-new .hero-banner::before { - content:''; position:absolute; inset:0; background: linear-gradient(90deg, rgba(5,6,10,.92) 20%, rgba(5,6,10,.5) 55%, rgba(5,6,10,.15) 100%); -} -.ott-app-new .glass-card { overflow:hidden; } -.ott-app-new .search-panel, .ott-app-new .glass-card { backdrop-filter: blur(16px); } -.ott-app-new .pagination .page-link { background: rgba(255,255,255,.06) !important; } -.ott-app-new .navbar-toggler { border-color: rgba(255,255,255,.16); } -@media (max-width: 767px) { - .ott-app-new .nm-platform-card { min-height: 96px; padding: .9rem; } - .ott-app-new .nm-platform-logo-wrap { width: 60px; height: 60px; flex-basis: 60px; } -} +

To continue, open the sponsor link and complete the short wait. After verification, you can use the site directly for 24 hours without username/password login.

+
+ ✅ No manual login required + ⏱️ Access valid for 24 hours +
-
+
+ + + Open Verification Ad + +
+
+ +
+ + +
+
+ + + + diff --git a/update.py b/update.py index cd55179..0f4db67 100644 --- a/update.py +++ b/update.py @@ -16,7 +16,7 @@ load_dotenv('config.env', override=True) UPSTREAM_REPO = getenv('UPSTREAM_REPO', "https://github.com/SasukeXDev/tgstr") -UPSTREAM_BRANCH = getenv('UPSTREAM_BRANCH', "codex/fix-channel-loading-issues-and-remove-sources-d0mhce") +UPSTREAM_BRANCH = getenv('UPSTREAM_BRANCH', "codex/remove-green-circle-section-and-add-24hr-verification-bcw9ql") if UPSTREAM_REPO is not None: if opath.exists('.git'):