from flask import Flask, render_template, request, redirect, session, url_for, flash, Response import psycopg2 from psycopg2 import extensions, extras import requests import re import functools import json import urllib3 import os urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) extensions.register_type(extensions.UNICODE) extensions.register_type(extensions.UNICODEARRAY) app = Flask(__name__) app.secret_key = 'wisearch_ultra_secret_key' CONFIG_FILE = 'config.json' def load_config(): if os.path.exists(CONFIG_FILE): with open(CONFIG_FILE, 'r', encoding='utf-8') as f: return json.load(f) return {} def save_config(cfg): with open(CONFIG_FILE, 'w', encoding='utf-8') as f: json.dump(cfg, f, indent=2, ensure_ascii=False) def get_db_connection(): cfg = load_config() db = cfg.get('database', {}) conn = psycopg2.connect( host=db.get('host', '192.168.131.124'), port=db.get('port', '5432'), user=db.get('user', 'postgres'), password=db.get('password', 'postpass'), dbname=db.get('dbname', 'wisearch'), sslmode=db.get('sslmode', 'disable') ) conn.set_client_encoding('UTF8') return conn # ΑΥΤΟΜΑΤΗ ΔΗΜΙΟΥΡΓΙΑ ΝΕΩΝ ΠΙΝΑΚΩΝ (Blacklist & Submitted URLs) def init_db(): try: conn = get_db_connection(); cur = conn.cursor() cur.execute("CREATE TABLE IF NOT EXISTS blacklisted_domains (id SERIAL PRIMARY KEY, domain TEXT UNIQUE)") cur.execute("CREATE TABLE IF NOT EXISTS submitted_urls (id SERIAL PRIMARY KEY, url TEXT, submitted_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP)") conn.commit(); cur.close(); conn.close() except Exception as e: print("DB Init Error:", e) init_db() # --- Decorators --- def login_required(f): @functools.wraps(f) def decorated_function(*args, **kwargs): if 'logged_in' not in session: return redirect(url_for('login')) return f(*args, **kwargs) return decorated_function def admin_required(f): @functools.wraps(f) def decorated_function(*args, **kwargs): if session.get('user_role') != 'admin': flash('Δεν έχετε δικαιώματα πρόσβασης.') return redirect(url_for('admin_dashboard')) return f(*args, **kwargs) return decorated_function # --- AI Helper --- def expand_query_with_ai(original_query): if not original_query: return [] cfg = load_config() creds = cfg.get('cf_credentials', []) # ΝΕΟ ΠΙΟ ΕΞΥΠΝΟ PROMPT: Απαγορεύει γενικούς όρους και αγνοεί τα παιχνίδια/ονόματα system_prompt = """You are a search assistant for a local network. Return ONLY a JSON array of up to 2 related terms/synonyms. If the query is a specific game (like minecraft, csgo), an app, a brand, or a proper noun, return an empty array []. NEVER return generic words like 'Network', 'Interface', 'Server', or 'Internet'. Return ONLY valid JSON. No markdown, no explanations.""" payload = { "messages": [ {"role": "system", "content": system_prompt}, {"role": "user", "content": f"Query: {original_query}"} ] } for cred in creds: try: api_url = f"https://api.cloudflare.com/client/v4/accounts/{cred['account_id']}/ai/run/{cred['model']}" r = requests.post(api_url, json=payload, headers={"Authorization": f"Bearer {cred['api_token']}"}, timeout=2.5) if r.status_code == 200: content = re.sub(r'```json|```', '', r.json()['result']['response']).strip() return [w for w in json.loads(content) if len(str(w)) > 1] except: continue return [] # --- Public Routes --- @app.route('/') def home(): return render_template('index.html') # --- ΝΕΟ: AI Chat Endpoint για τη σελίδα αποτελεσμάτων --- @app.route('/api/ai_chat', methods=['POST']) def ai_chat(): data = request.get_json() query = data.get('query', '') user_msg = data.get('message', '') context_data = data.get('context', []) # Φτιάχνουμε ένα κείμενο με τα top αποτελέσματα για να τα διαβάσει το AI ctx_str = "\n".join([f"- {item['title']}: {item['snippet']}" for item in context_data]) cfg = load_config() creds = cfg.get('cf_credentials', []) if not creds: return {"response": "Δεν έχουν ρυθμιστεί τα AI Accounts στο Admin Panel."} # Το ΝΕΟ, πολύ πιο αυστηρό prompt system_prompt = f"""Είσαι ο 'WiSearch AI', ένας βοηθός της μηχανής αναζήτησης του δικτύου AWMN. Ο χρήστης έψαξε για: '{query}'. Αποτελέσματα αναζήτησης: {ctx_str} ΑΥΣΤΗΡΕΣ ΟΔΗΓΙΕΣ: 1. Απαντας ΠΑΝΤΑ ΑΥΣΤΗΡΑ στην γλωσσα που σε ρωτησαν (Θα καταλαβαινεις την γλωσσα απο τις 4 προτες λεξεις) 3. ΜΗΝ παπαγαλίζεις/επαναλαμβάνεις τις οδηγίες (π.χ. μην γράφεις "Γεια σας, σύντομα και φιλικά" ή "Ελπίζω να βοήθησα"). 4. Δώσε κατευθείαν την απάντηση/περίληψη, βασισμένος ΜΟΝΟ στα αποτελέσματα. Αν δεν ξέρεις, πες "Δεν προκύπτει από τα αποτελέσματα".""" payload = { "messages": [ {"role": "system", "content": system_prompt}, {"role": "user", "content": user_msg} ] } for cred in creds: try: api_url = f"https://api.cloudflare.com/client/v4/accounts/{cred['account_id']}/ai/run/{cred['model']}" r = requests.post(api_url, json=payload, headers={"Authorization": f"Bearer {cred['api_token']}"}, timeout=10) if r.status_code == 200: return {"response": r.json()['result']['response'].strip()} except: continue return {"response": "Παρουσιάστηκε σφάλμα κατά την επικοινωνία με το AI."} @app.route('/submit_url', methods=['POST']) def submit_url(): url = request.form.get('url', '').strip() if url: try: conn = get_db_connection(); cur = conn.cursor() cur.execute("INSERT INTO submitted_urls (url) VALUES (%s)", (url,)) conn.commit(); cur.close(); conn.close() flash('Το URL υποβλήθηκε επιτυχώς για σκανάρισμα!') except: pass return redirect(request.referrer or '/') @app.route('/api/autocomplete') def autocomplete(): q = request.args.get('q', '').strip() if len(q) < 2: return Response("[]", mimetype='application/json') try: conn = get_db_connection(); cur = conn.cursor(cursor_factory=extras.RealDictCursor) cur.execute("SELECT search_query FROM activity_logs WHERE search_query ILIKE %s GROUP BY search_query ORDER BY COUNT(*) DESC LIMIT 5", (f"{q}%",)) res = [r['search_query'] for r in cur.fetchall()] cur.close(); conn.close() return Response(json.dumps(res), mimetype='application/json') except: return Response("[]", mimetype='application/json') @app.route('/cached/') def cached_view(record_id): try: conn = get_db_connection(); cur = conn.cursor(cursor_factory=extras.RealDictCursor) cur.execute("SELECT url, full_content, last_crawled FROM web_health WHERE id = %s", (record_id,)) res = cur.fetchone() cur.close(); conn.close() if res: html = f"
WiSearch Cached View
Αυτή είναι η αποθηκευμένη μορφή της σελίδας {res['url']} όπως ήταν στις {res['last_crawled']}. Το πραγματικό site μπορεί να είναι offline.

{res['full_content']}
" return Response(html, mimetype='text/html') except: pass return "Not found", 404 @app.route('/go') def go_to_url(): target_url, record_id = request.args.get('url'), request.args.get('id') if target_url and record_id: try: conn = get_db_connection(); cur = conn.cursor() cur.execute("UPDATE web_health SET clicks = COALESCE(clicks, 0) + 1 WHERE id = %s", (record_id,)) conn.commit(); cur.close(); conn.close() except: pass return redirect(target_url) return redirect('/') @app.route('/proxy_media') def proxy_media(): url = request.args.get('url') if not url: return "No URL provided", 400 try: r = requests.get(url, verify=False, timeout=5) if r.status_code == 200: return Response(r.content, mimetype=r.headers.get('Content-Type', 'image/jpeg')) except: pass return "Failed", 404 @app.route('/search') def search(): query = request.args.get('q', '').strip() tab = request.args.get('tab', 'web') category_filter = request.args.get('category', '') page = request.args.get('page', 1, type=int) offset = (page - 1) * 10 if not query: return redirect('/') try: conn = get_db_connection(); cur = conn.cursor() cur.execute("INSERT INTO activity_logs (ip_address, user_id, search_query) VALUES (%s, %s, %s)", (request.remote_addr, session.get('user_id'), query)) conn.commit(); cur.close(); conn.close() except: pass sponsored = [] try: conn = get_db_connection(); cur = conn.cursor(cursor_factory=extras.RealDictCursor) cur.execute("SELECT * FROM sponsored_links WHERE is_active = TRUE AND keywords ILIKE %s", (f"%{query}%",)) sponsored = cur.fetchall(); cur.close(); conn.close() except: pass patterns = expand_query_with_ai(query) results, image_results, video_results = [], [], [] has_next = False available_categories = [] try: conn = get_db_connection() cur = conn.cursor(cursor_factory=extras.RealDictCursor) search_term = f"%{query}%" if tab == 'web': cat_sql = """ SELECT DISTINCT CASE WHEN dr.hostname ~* '^[a-z0-9-]+\\.awmn$' THEN 'Core Services' ELSE wh.category END as category FROM web_health wh JOIN dns_records dr ON wh.dns_record_id = dr.id WHERE dr.hostname NOT IN (SELECT domain FROM blacklisted_domains) AND (unaccent(wh.page_title) ILIKE unaccent(%s) OR dr.hostname ILIKE %s OR unaccent(wh.full_content) ILIKE unaccent(%s) """ cat_params = [search_term, search_term, search_term] for p in patterns: cat_sql += " OR unaccent(wh.full_content) ILIKE unaccent(%s)" cat_params.append(f"%{p}%") cat_sql += ")" cur.execute(cat_sql, tuple(cat_params)) available_categories = [r['category'] for r in cur.fetchall() if r['category']] sql = """ WITH RankedResults AS ( SELECT wh.id, dr.hostname, wh.url, wh.page_title, CASE WHEN dr.hostname ~* '^[a-z0-9-]+\\.awmn$' THEN 'Core Services' ELSE wh.category END as category, wh.ai_summary, wh.activity_status, wh.pagerank_score, wh.clicks, COALESCE(SUBSTRING(wh.full_content, 1, 400), '') AS snippet, (SELECT media_url FROM media_files WHERE source_url = wh.url AND media_type = 'image' LIMIT 1) as thumbnail_url, ( CASE WHEN dr.hostname ~* '^[a-z0-9-]+\\.awmn$' THEN 50000 ELSE 0 END + COALESCE(wh.pagerank_score, 1.0) + (COALESCE(wh.clicks, 0) * 0.5) + (COALESCE(EXTRACT(EPOCH FROM wh.last_crawled), 0) / 100000.0) ) as total_score, ROW_NUMBER() OVER( PARTITION BY dr.hostname ORDER BY ( CASE WHEN dr.hostname ~* '^[a-z0-9-]+\\.awmn$' THEN 50000 ELSE 0 END + COALESCE(wh.pagerank_score, 1.0) + (COALESCE(wh.clicks, 0) * 0.5) + (COALESCE(EXTRACT(EPOCH FROM wh.last_crawled), 0) / 100000.0) ) DESC ) as rn FROM web_health wh JOIN dns_records dr ON wh.dns_record_id = dr.id WHERE dr.hostname NOT IN (SELECT domain FROM blacklisted_domains) AND (unaccent(wh.page_title) ILIKE unaccent(%s) OR dr.hostname ILIKE %s OR unaccent(wh.full_content) ILIKE unaccent(%s) """ params = [search_term, search_term, search_term] for p in patterns: sql += " OR unaccent(wh.full_content) ILIKE unaccent(%s)" params.append(f"%{p}%") sql += ")" if category_filter: sql += " AND (CASE WHEN dr.hostname ~* '^[a-z0-9-]+\\.awmn$' THEN 'Core Services' ELSE wh.category END) = %s " params.append(category_filter) sql += """ ) SELECT * FROM RankedResults WHERE rn = 1 ORDER BY total_score DESC LIMIT 11 OFFSET %s """ params.append(offset) cur.execute(sql, tuple(params)) fetched = cur.fetchall() if fetched: results = list(fetched) if len(results) > 10: has_next = True; results = results[:10] elif tab in ['images', 'videos']: m_type = 'image' if tab == 'images' else 'video' sql_media = "SELECT m.* FROM media_files m JOIN web_health wh ON m.source_url = wh.url JOIN dns_records dr ON wh.dns_record_id = dr.id WHERE m.media_type = %s AND dr.hostname NOT IN (SELECT domain FROM blacklisted_domains) AND (m.alt_text ILIKE %s OR m.source_url ILIKE %s" params_media = [m_type, search_term, search_term] for p in patterns: sql_media += " OR m.alt_text ILIKE %s" params_media.append(f"%{p}%") sql_media += ") LIMIT 51 OFFSET %s" params_media.append(offset) cur.execute(sql_media, tuple(params_media)) fetched_media = cur.fetchall() if fetched_media: media_list = list(fetched_media) if len(media_list) > 50: has_next = True; media_list = media_list[:50] if tab == 'images': image_results = media_list else: video_results = media_list cur.close(); conn.close() except Exception as e: print(f"SEARCH ERROR: {e}") return render_template('results.html', results=results, image_results=image_results, video_results=video_results, sponsored=sponsored, query=query, page=page, has_next=has_next, tab=tab, smart_query=", ".join(patterns), category=category_filter, available_categories=available_categories) # --- Auth & Admin Routes --- @app.route('/login', methods=['GET', 'POST']) def login(): if request.method == 'POST': u, p = request.form.get('username'), request.form.get('password') conn = get_db_connection(); cur = conn.cursor(cursor_factory=extras.RealDictCursor) cur.execute("SELECT * FROM admin_users WHERE username = %s AND password_hash = %s", (u, p)) user = cur.fetchone(); cur.close(); conn.close() if user: session.update({'logged_in': True, 'user_id': user['id'], 'username': u, 'user_role': user.get('role', 'user')}) return redirect(url_for('admin_dashboard')) flash('Λάθος στοιχεία πρόσβασης') return render_template('login.html') @app.route('/logout') def logout(): session.clear() return redirect(url_for('home')) @app.route('/admin') @login_required def admin_dashboard(): conn = get_db_connection(); cur = conn.cursor(cursor_factory=extras.RealDictCursor) if session.get('user_role') == 'admin': cur.execute("SELECT s.*, u.username as owner_name FROM sponsored_links s LEFT JOIN admin_users u ON s.user_id = u.id ORDER BY s.id DESC") else: cur.execute("SELECT * FROM sponsored_links WHERE user_id = %s ORDER BY id DESC", (session.get('user_id'),)) sponsored = cur.fetchall() analytics = {'top_queries': [], 'categories': [], 'blacklists': []} if session.get('user_role') == 'admin': cur.execute("SELECT search_query, COUNT(*) as count FROM activity_logs GROUP BY search_query ORDER BY count DESC LIMIT 10") analytics['top_queries'] = cur.fetchall() cur.execute("SELECT category, COUNT(*) as count FROM web_health WHERE category IS NOT NULL GROUP BY category") analytics['categories'] = cur.fetchall() cur.execute("SELECT * FROM blacklisted_domains ORDER BY id DESC") analytics['blacklists'] = cur.fetchall() cur.close(); conn.close() cfg = load_config() cf_creds = cfg.get('cf_credentials', []) if session.get('user_role') == 'admin' else [] ai_prompt = cfg.get('ai_prompt', '') if session.get('user_role') == 'admin' else '' return render_template('admin_dashboard.html', sponsored=sponsored, cf_creds=cf_creds, ai_prompt=ai_prompt, analytics=analytics) @app.route('/admin/sponsored/add', methods=['POST']) @login_required def add_sponsored(): user_id = session.get('user_id') data = (request.form.get('title'), request.form.get('url'), request.form.get('description'), request.form.get('keywords'), user_id) try: conn = get_db_connection(); cur = conn.cursor() cur.execute("INSERT INTO sponsored_links (title, url, description, keywords, user_id) VALUES (%s, %s, %s, %s, %s)", data) conn.commit(); cur.close(); conn.close() except: pass return redirect(url_for('admin_dashboard')) @app.route('/admin/sponsored/delete/') @login_required def delete_sponsored(link_id): conn = get_db_connection(); cur = conn.cursor() if session.get('user_role') == 'admin': cur.execute("DELETE FROM sponsored_links WHERE id = %s", (link_id,)) else: cur.execute("DELETE FROM sponsored_links WHERE id = %s AND user_id = %s", (link_id, session.get('user_id'))) conn.commit(); cur.close(); conn.close() return redirect(url_for('admin_dashboard')) @app.route('/admin/cf/add', methods=['POST']) @login_required @admin_required def add_cf_cred(): cfg = load_config(); creds = cfg.get('cf_credentials', []) new_cred = {"comment": request.form.get('comment', ''), "account_id": request.form.get('account_id', ''), "api_token": request.form.get('api_token', ''), "model": request.form.get('model', '')} if new_cred["account_id"]: creds.append(new_cred); cfg['cf_credentials'] = creds; save_config(cfg) return redirect(url_for('admin_dashboard')) @app.route('/admin/cf/delete/') @login_required @admin_required def delete_cf_cred(index): cfg = load_config(); creds = cfg.get('cf_credentials', []) if 0 <= index < len(creds): del creds[index]; cfg['cf_credentials'] = creds; save_config(cfg) return redirect(url_for('admin_dashboard')) @app.route('/admin/cf/prompt', methods=['POST']) @login_required @admin_required def update_ai_prompt(): cfg = load_config(); cfg['ai_prompt'] = request.form.get('ai_prompt', ''); save_config(cfg) return redirect(url_for('admin_dashboard')) @app.route('/admin/blacklist/add', methods=['POST']) @login_required @admin_required def add_blacklist(): domain = request.form.get('domain', '').strip() if domain: try: conn = get_db_connection(); cur = conn.cursor() cur.execute("INSERT INTO blacklisted_domains (domain) VALUES (%s) ON CONFLICT DO NOTHING", (domain,)) conn.commit(); cur.close(); conn.close() except: pass return redirect(url_for('admin_dashboard')) @app.route('/admin/blacklist/delete/') @login_required @admin_required def delete_blacklist(b_id): conn = get_db_connection(); cur = conn.cursor() cur.execute("DELETE FROM blacklisted_domains WHERE id = %s", (b_id,)) conn.commit(); cur.close(); conn.close() return redirect(url_for('admin_dashboard')) @app.route('/admin/logs') @login_required @admin_required def admin_logs(): conn = get_db_connection(); cur = conn.cursor(cursor_factory=extras.RealDictCursor) cur.execute("SELECT l.*, u.username FROM activity_logs l LEFT JOIN admin_users u ON l.user_id = u.id ORDER BY l.timestamp DESC LIMIT 100") logs = cur.fetchall(); cur.close(); conn.close() return render_template('admin_logs.html', logs=logs) @app.route('/admin/users') @login_required @admin_required def admin_users(): conn = get_db_connection(); cur = conn.cursor(cursor_factory=extras.RealDictCursor) cur.execute("SELECT id, username, role FROM admin_users ORDER BY id ASC") users = cur.fetchall(); cur.close(); conn.close() return render_template('admin_users.html', users=users) if __name__ == '__main__': app.run(host='0.0.0.0', port=5000, debug=True)