| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470 |
- 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/<int:record_id>')
- 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"<div style='background:#ffeb3b; color:#000; padding:15px; font-family:sans-serif;'><strong>WiSearch Cached View</strong><br>Αυτή είναι η αποθηκευμένη μορφή της σελίδας <b>{res['url']}</b> όπως ήταν στις {res['last_crawled']}. Το πραγματικό site μπορεί να είναι offline.</div><hr><div style='padding:20px; font-family:sans-serif; line-height:1.6;'>{res['full_content']}</div>"
- 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/<int:link_id>')
- @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/<int:index>')
- @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/<int:b_id>')
- @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)
|