import os import sys import json import urllib.parse # Add current folder to sys.path for Hostinger Python App Manager BASE_DIR = os.path.dirname(os.path.abspath(__file__)) if BASE_DIR not in sys.path: sys.path.insert(0, BASE_DIR) from core.scheduler import AgentScheduler scheduler = AgentScheduler(BASE_DIR) def application(environ, start_response): """ Hostinger WSGI application entry point (passenger_wsgi.py). Routes HTTP requests directly to Hostinger hPanel Webserver. """ path_info = environ.get('PATH_INFO', '/') method = environ.get('REQUEST_METHOD', 'GET') query_string = environ.get('QUERY_STRING', '') # --- SERVE STATIC MEDIA (PDFs, PNGs, HTML) --- if path_info.startswith('/media/'): rel_path = path_info.lstrip('/media/') file_path = os.path.join(BASE_DIR, 'storage', 'media', rel_path) if os.path.exists(file_path): content_type = 'application/pdf' if file_path.endswith('.pdf') else ('image/png' if file_path.endswith('.png') else 'text/html') start_response('200 OK', [('Content-Type', content_type)]) with open(file_path, 'rb') as f: return [f.read()] start_response('404 Not Found', [('Content-Type', 'text/plain')]) return [b'File Not Found'] # --- SERVE WEB DASHBOARD FILES --- if path_info == '/' or path_info == '/index.html': file_path = os.path.join(BASE_DIR, 'web', 'index.html') start_response('200 OK', [('Content-Type', 'text/html; charset=utf-8')]) with open(file_path, 'rb') as f: return [f.read()] if path_info == '/styles.css': file_path = os.path.join(BASE_DIR, 'web', 'styles.css') start_response('200 OK', [('Content-Type', 'text/css; charset=utf-8')]) with open(file_path, 'rb') as f: return [f.read()] if path_info == '/app.js': file_path = os.path.join(BASE_DIR, 'web', 'app.js') start_response('200 OK', [('Content-Type', 'application/javascript; charset=utf-8')]) with open(file_path, 'rb') as f: return [f.read()] # --- API GET ROUTES --- if method == 'GET': if path_info == '/api/status': db = scheduler.load_posts_db() config = scheduler.config res_data = { 'status': 'online', 'scheduler_running': True, 'total_batches': len(db.get('posts', [])), 'config': { 'linkedin_sandbox': config.get('linkedin', {}).get('sandbox_mode', True), 'smtp_enabled': config.get('smtp', {}).get('enabled', False) } } start_response('200 OK', [('Content-Type', 'application/json')]) return [json.dumps(res_data).encode('utf-8')] elif path_info == '/api/posts': db = scheduler.load_posts_db() start_response('200 OK', [('Content-Type', 'application/json')]) return [json.dumps(db).encode('utf-8')] elif path_info == '/api/trends': trends = scheduler.trend_fetcher.fetch_all_trends() start_response('200 OK', [('Content-Type', 'application/json')]) return [json.dumps({'trends': trends}).encode('utf-8')] elif path_info == '/api/config': start_response('200 OK', [('Content-Type', 'application/json')]) return [json.dumps(scheduler.config).encode('utf-8')] elif path_info == '/api/linkedin/auth': client_id = scheduler.config.get('linkedin', {}).get('client_id', '77opdiiqro3xhx') redirect_uri = urllib.parse.quote('http://' + environ.get('HTTP_HOST', 'localhost') + '/api/linkedin/callback', safe='') auth_url = f"https://www.linkedin.com/oauth/v2/authorization?response_type=code&client_id={client_id}&redirect_uri={redirect_uri}&scope=w_member_social%20openid%20profile%20email" start_response('302 Found', [('Location', auth_url)]) return [b''] elif path_info == '/api/linkedin/callback': query_params = urllib.parse.parse_qs(query_string) code = query_params.get('code', [None])[0] if code: try: import requests client_id = scheduler.config.get('linkedin', {}).get('client_id', '77opdiiqro3xhx') client_secret = scheduler.config.get('linkedin', {}).get('client_secret', 'WPL_AP1.xcGd78Y8RhPFkXGl.TvgJdg==') redirect_uri = 'http://' + environ.get('HTTP_HOST', 'localhost') + '/api/linkedin/callback' token_url = 'https://www.linkedin.com/oauth/v2/accessToken' payload = { 'grant_type': 'authorization_code', 'code': code, 'client_id': client_id, 'client_secret': client_secret, 'redirect_uri': redirect_uri } token_resp = requests.post(token_url, data=payload, headers={'Content-Type': 'application/x-www-form-urlencoded'}) access_token = token_resp.json().get('access_token') if access_token: profile_resp = requests.get('https://api.linkedin.com/v2/userinfo', headers={'Authorization': f'Bearer {access_token}'}) profile_data = profile_resp.json() sub_id = profile_data.get('sub') author_urn = f"urn:li:person:{sub_id}" if sub_id else "urn:li:person:YOUR_PERSON_URN" scheduler.config['linkedin']['access_token'] = access_token scheduler.config['linkedin']['author_urn'] = author_urn scheduler.config['linkedin']['sandbox_mode'] = False scheduler.save_config(scheduler.config) start_response('302 Found', [('Location', '/?linkedin_auth=success')]) return [b''] except Exception as e: print(f"LinkedIn OAuth Callback Error: {e}") start_response('302 Found', [('Location', '/?linkedin_auth=failed')]) return [b''] elif path_info == '/api/export-csv': csv_path = os.path.join(BASE_DIR, 'storage', 'linkedin_posts_export.csv') if os.path.exists(csv_path): start_response('200 OK', [('Content-Type', 'text/csv; charset=utf-8'), ('Content-Disposition', 'attachment; filename="linkedin_posts_export.csv"')]) with open(csv_path, 'rb') as f: return [f.read()] # --- API POST ROUTES --- if method == 'POST': try: request_body_size = int(environ.get('CONTENT_LENGTH', 0)) except (ValueError): request_body_size = 0 request_body = environ['wsgi.input'].read(request_body_size) if request_body_size > 0 else b'{}' try: body = json.loads(request_body.decode('utf-8')) except Exception: body = {} if path_info == '/api/generate-batch': result = scheduler.trigger_daily_batch() start_response('200 OK', [('Content-Type', 'application/json')]) return [json.dumps({'status': 'success', 'data': result}).encode('utf-8')] elif path_info == '/api/instant-post-trend': trend_item = body.get('trend', {}) post_type = body.get('post_type', 'text') import datetime date_today = datetime.datetime.now().strftime('%Y-%m-%d_%H%M%S') if post_type == 'text': post_obj = scheduler.content_generator.generate_morning_text_post(trend_item) post_obj['slot_name'] = 'Instant Text Post' elif post_type == 'carousel': post_obj = scheduler.content_generator.generate_afternoon_carousel_post(trend_item) post_obj['slot_name'] = 'Instant Carousel Post' slides = post_obj.get('carousel_slides', []) pdf_path = scheduler.carousel_generator.generate_carousel_pdf(f"instant_{date_today}", slides) post_obj['media_path'] = pdf_path post_obj['media_url'] = f"/media/carousel_instant_{date_today}.pdf" elif post_type == 'graphic': post_obj = scheduler.content_generator.generate_evening_graphic_post(trend_item) post_obj['slot_name'] = 'Instant Graphic Post' card_data = post_obj.get('graphic_card', {}) png_path = scheduler.graphic_generator.generate_graphic_png(f"instant_{date_today}", card_data) post_obj['media_path'] = png_path post_obj['media_url'] = f"/media/graphic_instant_{date_today}.png" else: post_obj = scheduler.content_generator.generate_morning_text_post(trend_item) post_obj['approval_status'] = 'INSTANT PUBLISHED' res = scheduler.linkedin_client.publish_or_schedule_post(post_obj) post_obj['linkedin_status'] = res scheduler.sheets_logger.log_single_post(post_obj) start_response('200 OK', [('Content-Type', 'application/json')]) return [json.dumps({'status': 'success', 'post': post_obj, 'linkedin_result': res}).encode('utf-8')] elif path_info == '/api/approve-post': slot_name = body.get('slot_name') import datetime date_str = body.get('date', datetime.datetime.now().strftime('%Y-%m-%d')) db = scheduler.load_posts_db() for record in db.get('posts', []): if record.get('date') == date_str: posts = record.get('posts', {}) for key, p in posts.items(): if key == slot_name or p.get('slot_name') == slot_name: p['approval_status'] = 'APPROVED & SCHEDULED' scheduler.save_posts_db(db) start_response('200 OK', [('Content-Type', 'application/json')]) return [json.dumps({'status': 'success', 'message': f"Post '{slot_name}' approved successfully!"}).encode('utf-8')] elif path_info == '/api/save-config': scheduler.save_config(body) start_response('200 OK', [('Content-Type', 'application/json')]) return [json.dumps({'status': 'success', 'message': 'Configuration updated successfully'}).encode('utf-8')] start_response('404 Not Found', [('Content-Type', 'application/json')]) return [json.dumps({'error': 'Endpoint not found'}).encode('utf-8')]