webengine.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470
  1. from flask import Flask, render_template, request, redirect, session, url_for, flash, Response
  2. import psycopg2
  3. from psycopg2 import extensions, extras
  4. import requests
  5. import re
  6. import functools
  7. import json
  8. import urllib3
  9. import os
  10. urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
  11. extensions.register_type(extensions.UNICODE)
  12. extensions.register_type(extensions.UNICODEARRAY)
  13. app = Flask(__name__)
  14. app.secret_key = 'wisearch_ultra_secret_key'
  15. CONFIG_FILE = 'config.json'
  16. def load_config():
  17. if os.path.exists(CONFIG_FILE):
  18. with open(CONFIG_FILE, 'r', encoding='utf-8') as f:
  19. return json.load(f)
  20. return {}
  21. def save_config(cfg):
  22. with open(CONFIG_FILE, 'w', encoding='utf-8') as f:
  23. json.dump(cfg, f, indent=2, ensure_ascii=False)
  24. def get_db_connection():
  25. cfg = load_config()
  26. db = cfg.get('database', {})
  27. conn = psycopg2.connect(
  28. host=db.get('host', '192.168.131.124'),
  29. port=db.get('port', '5432'),
  30. user=db.get('user', 'postgres'),
  31. password=db.get('password', 'postpass'),
  32. dbname=db.get('dbname', 'wisearch'),
  33. sslmode=db.get('sslmode', 'disable')
  34. )
  35. conn.set_client_encoding('UTF8')
  36. return conn
  37. # ΑΥΤΟΜΑΤΗ ΔΗΜΙΟΥΡΓΙΑ ΝΕΩΝ ΠΙΝΑΚΩΝ (Blacklist & Submitted URLs)
  38. def init_db():
  39. try:
  40. conn = get_db_connection(); cur = conn.cursor()
  41. cur.execute("CREATE TABLE IF NOT EXISTS blacklisted_domains (id SERIAL PRIMARY KEY, domain TEXT UNIQUE)")
  42. cur.execute("CREATE TABLE IF NOT EXISTS submitted_urls (id SERIAL PRIMARY KEY, url TEXT, submitted_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP)")
  43. conn.commit(); cur.close(); conn.close()
  44. except Exception as e: print("DB Init Error:", e)
  45. init_db()
  46. # --- Decorators ---
  47. def login_required(f):
  48. @functools.wraps(f)
  49. def decorated_function(*args, **kwargs):
  50. if 'logged_in' not in session: return redirect(url_for('login'))
  51. return f(*args, **kwargs)
  52. return decorated_function
  53. def admin_required(f):
  54. @functools.wraps(f)
  55. def decorated_function(*args, **kwargs):
  56. if session.get('user_role') != 'admin':
  57. flash('Δεν έχετε δικαιώματα πρόσβασης.')
  58. return redirect(url_for('admin_dashboard'))
  59. return f(*args, **kwargs)
  60. return decorated_function
  61. # --- AI Helper ---
  62. def expand_query_with_ai(original_query):
  63. if not original_query: return []
  64. cfg = load_config()
  65. creds = cfg.get('cf_credentials', [])
  66. # ΝΕΟ ΠΙΟ ΕΞΥΠΝΟ PROMPT: Απαγορεύει γενικούς όρους και αγνοεί τα παιχνίδια/ονόματα
  67. system_prompt = """You are a search assistant for a local network.
  68. Return ONLY a JSON array of up to 2 related terms/synonyms.
  69. If the query is a specific game (like minecraft, csgo), an app, a brand, or a proper noun, return an empty array [].
  70. NEVER return generic words like 'Network', 'Interface', 'Server', or 'Internet'.
  71. Return ONLY valid JSON. No markdown, no explanations."""
  72. payload = {
  73. "messages": [
  74. {"role": "system", "content": system_prompt},
  75. {"role": "user", "content": f"Query: {original_query}"}
  76. ]
  77. }
  78. for cred in creds:
  79. try:
  80. api_url = f"https://api.cloudflare.com/client/v4/accounts/{cred['account_id']}/ai/run/{cred['model']}"
  81. r = requests.post(api_url, json=payload, headers={"Authorization": f"Bearer {cred['api_token']}"}, timeout=2.5)
  82. if r.status_code == 200:
  83. content = re.sub(r'```json|```', '', r.json()['result']['response']).strip()
  84. return [w for w in json.loads(content) if len(str(w)) > 1]
  85. except: continue
  86. return []
  87. # --- Public Routes ---
  88. @app.route('/')
  89. def home():
  90. return render_template('index.html')
  91. # --- ΝΕΟ: AI Chat Endpoint για τη σελίδα αποτελεσμάτων ---
  92. @app.route('/api/ai_chat', methods=['POST'])
  93. def ai_chat():
  94. data = request.get_json()
  95. query = data.get('query', '')
  96. user_msg = data.get('message', '')
  97. context_data = data.get('context', [])
  98. # Φτιάχνουμε ένα κείμενο με τα top αποτελέσματα για να τα διαβάσει το AI
  99. ctx_str = "\n".join([f"- {item['title']}: {item['snippet']}" for item in context_data])
  100. cfg = load_config()
  101. creds = cfg.get('cf_credentials', [])
  102. if not creds:
  103. return {"response": "Δεν έχουν ρυθμιστεί τα AI Accounts στο Admin Panel."}
  104. # Το ΝΕΟ, πολύ πιο αυστηρό prompt
  105. system_prompt = f"""Είσαι ο 'WiSearch AI', ένας βοηθός της μηχανής αναζήτησης του δικτύου AWMN.
  106. Ο χρήστης έψαξε για: '{query}'.
  107. Αποτελέσματα αναζήτησης:
  108. {ctx_str}
  109. ΑΥΣΤΗΡΕΣ ΟΔΗΓΙΕΣ:
  110. 1. Απαντας ΠΑΝΤΑ ΑΥΣΤΗΡΑ στην γλωσσα που σε ρωτησαν (Θα καταλαβαινεις την γλωσσα απο τις 4 προτες λεξεις)
  111. 3. ΜΗΝ παπαγαλίζεις/επαναλαμβάνεις τις οδηγίες (π.χ. μην γράφεις "Γεια σας, σύντομα και φιλικά" ή "Ελπίζω να βοήθησα").
  112. 4. Δώσε κατευθείαν την απάντηση/περίληψη, βασισμένος ΜΟΝΟ στα αποτελέσματα. Αν δεν ξέρεις, πες "Δεν προκύπτει από τα αποτελέσματα"."""
  113. payload = {
  114. "messages": [
  115. {"role": "system", "content": system_prompt},
  116. {"role": "user", "content": user_msg}
  117. ]
  118. }
  119. for cred in creds:
  120. try:
  121. api_url = f"https://api.cloudflare.com/client/v4/accounts/{cred['account_id']}/ai/run/{cred['model']}"
  122. r = requests.post(api_url, json=payload, headers={"Authorization": f"Bearer {cred['api_token']}"}, timeout=10)
  123. if r.status_code == 200:
  124. return {"response": r.json()['result']['response'].strip()}
  125. except:
  126. continue
  127. return {"response": "Παρουσιάστηκε σφάλμα κατά την επικοινωνία με το AI."}
  128. @app.route('/submit_url', methods=['POST'])
  129. def submit_url():
  130. url = request.form.get('url', '').strip()
  131. if url:
  132. try:
  133. conn = get_db_connection(); cur = conn.cursor()
  134. cur.execute("INSERT INTO submitted_urls (url) VALUES (%s)", (url,))
  135. conn.commit(); cur.close(); conn.close()
  136. flash('Το URL υποβλήθηκε επιτυχώς για σκανάρισμα!')
  137. except: pass
  138. return redirect(request.referrer or '/')
  139. @app.route('/api/autocomplete')
  140. def autocomplete():
  141. q = request.args.get('q', '').strip()
  142. if len(q) < 2: return Response("[]", mimetype='application/json')
  143. try:
  144. conn = get_db_connection(); cur = conn.cursor(cursor_factory=extras.RealDictCursor)
  145. 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}%",))
  146. res = [r['search_query'] for r in cur.fetchall()]
  147. cur.close(); conn.close()
  148. return Response(json.dumps(res), mimetype='application/json')
  149. except: return Response("[]", mimetype='application/json')
  150. @app.route('/cached/<int:record_id>')
  151. def cached_view(record_id):
  152. try:
  153. conn = get_db_connection(); cur = conn.cursor(cursor_factory=extras.RealDictCursor)
  154. cur.execute("SELECT url, full_content, last_crawled FROM web_health WHERE id = %s", (record_id,))
  155. res = cur.fetchone()
  156. cur.close(); conn.close()
  157. if res:
  158. 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>"
  159. return Response(html, mimetype='text/html')
  160. except: pass
  161. return "Not found", 404
  162. @app.route('/go')
  163. def go_to_url():
  164. target_url, record_id = request.args.get('url'), request.args.get('id')
  165. if target_url and record_id:
  166. try:
  167. conn = get_db_connection(); cur = conn.cursor()
  168. cur.execute("UPDATE web_health SET clicks = COALESCE(clicks, 0) + 1 WHERE id = %s", (record_id,))
  169. conn.commit(); cur.close(); conn.close()
  170. except: pass
  171. return redirect(target_url)
  172. return redirect('/')
  173. @app.route('/proxy_media')
  174. def proxy_media():
  175. url = request.args.get('url')
  176. if not url: return "No URL provided", 400
  177. try:
  178. r = requests.get(url, verify=False, timeout=5)
  179. if r.status_code == 200: return Response(r.content, mimetype=r.headers.get('Content-Type', 'image/jpeg'))
  180. except: pass
  181. return "Failed", 404
  182. @app.route('/search')
  183. def search():
  184. query = request.args.get('q', '').strip()
  185. tab = request.args.get('tab', 'web')
  186. category_filter = request.args.get('category', '')
  187. page = request.args.get('page', 1, type=int)
  188. offset = (page - 1) * 10
  189. if not query: return redirect('/')
  190. try:
  191. conn = get_db_connection(); cur = conn.cursor()
  192. cur.execute("INSERT INTO activity_logs (ip_address, user_id, search_query) VALUES (%s, %s, %s)", (request.remote_addr, session.get('user_id'), query))
  193. conn.commit(); cur.close(); conn.close()
  194. except: pass
  195. sponsored = []
  196. try:
  197. conn = get_db_connection(); cur = conn.cursor(cursor_factory=extras.RealDictCursor)
  198. cur.execute("SELECT * FROM sponsored_links WHERE is_active = TRUE AND keywords ILIKE %s", (f"%{query}%",))
  199. sponsored = cur.fetchall(); cur.close(); conn.close()
  200. except: pass
  201. patterns = expand_query_with_ai(query)
  202. results, image_results, video_results = [], [], []
  203. has_next = False
  204. available_categories = []
  205. try:
  206. conn = get_db_connection()
  207. cur = conn.cursor(cursor_factory=extras.RealDictCursor)
  208. search_term = f"%{query}%"
  209. if tab == 'web':
  210. cat_sql = """
  211. SELECT DISTINCT CASE WHEN dr.hostname ~* '^[a-z0-9-]+\\.awmn$' THEN 'Core Services' ELSE wh.category END as category
  212. FROM web_health wh
  213. JOIN dns_records dr ON wh.dns_record_id = dr.id
  214. WHERE dr.hostname NOT IN (SELECT domain FROM blacklisted_domains)
  215. AND (unaccent(wh.page_title) ILIKE unaccent(%s)
  216. OR dr.hostname ILIKE %s
  217. OR unaccent(wh.full_content) ILIKE unaccent(%s)
  218. """
  219. cat_params = [search_term, search_term, search_term]
  220. for p in patterns:
  221. cat_sql += " OR unaccent(wh.full_content) ILIKE unaccent(%s)"
  222. cat_params.append(f"%{p}%")
  223. cat_sql += ")"
  224. cur.execute(cat_sql, tuple(cat_params))
  225. available_categories = [r['category'] for r in cur.fetchall() if r['category']]
  226. sql = """
  227. WITH RankedResults AS (
  228. SELECT wh.id, dr.hostname, wh.url, wh.page_title,
  229. CASE WHEN dr.hostname ~* '^[a-z0-9-]+\\.awmn$' THEN 'Core Services' ELSE wh.category END as category,
  230. wh.ai_summary, wh.activity_status, wh.pagerank_score, wh.clicks,
  231. COALESCE(SUBSTRING(wh.full_content, 1, 400), '') AS snippet,
  232. (SELECT media_url FROM media_files WHERE source_url = wh.url AND media_type = 'image' LIMIT 1) as thumbnail_url,
  233. (
  234. CASE WHEN dr.hostname ~* '^[a-z0-9-]+\\.awmn$' THEN 50000 ELSE 0 END
  235. + COALESCE(wh.pagerank_score, 1.0)
  236. + (COALESCE(wh.clicks, 0) * 0.5)
  237. + (COALESCE(EXTRACT(EPOCH FROM wh.last_crawled), 0) / 100000.0)
  238. ) as total_score,
  239. ROW_NUMBER() OVER(
  240. PARTITION BY dr.hostname
  241. ORDER BY (
  242. CASE WHEN dr.hostname ~* '^[a-z0-9-]+\\.awmn$' THEN 50000 ELSE 0 END
  243. + COALESCE(wh.pagerank_score, 1.0)
  244. + (COALESCE(wh.clicks, 0) * 0.5)
  245. + (COALESCE(EXTRACT(EPOCH FROM wh.last_crawled), 0) / 100000.0)
  246. ) DESC
  247. ) as rn
  248. FROM web_health wh
  249. JOIN dns_records dr ON wh.dns_record_id = dr.id
  250. WHERE dr.hostname NOT IN (SELECT domain FROM blacklisted_domains)
  251. AND (unaccent(wh.page_title) ILIKE unaccent(%s)
  252. OR dr.hostname ILIKE %s
  253. OR unaccent(wh.full_content) ILIKE unaccent(%s)
  254. """
  255. params = [search_term, search_term, search_term]
  256. for p in patterns:
  257. sql += " OR unaccent(wh.full_content) ILIKE unaccent(%s)"
  258. params.append(f"%{p}%")
  259. sql += ")"
  260. if category_filter:
  261. sql += " AND (CASE WHEN dr.hostname ~* '^[a-z0-9-]+\\.awmn$' THEN 'Core Services' ELSE wh.category END) = %s "
  262. params.append(category_filter)
  263. sql += """
  264. )
  265. SELECT * FROM RankedResults WHERE rn = 1 ORDER BY total_score DESC LIMIT 11 OFFSET %s
  266. """
  267. params.append(offset)
  268. cur.execute(sql, tuple(params))
  269. fetched = cur.fetchall()
  270. if fetched:
  271. results = list(fetched)
  272. if len(results) > 10: has_next = True; results = results[:10]
  273. elif tab in ['images', 'videos']:
  274. m_type = 'image' if tab == 'images' else 'video'
  275. 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"
  276. params_media = [m_type, search_term, search_term]
  277. for p in patterns:
  278. sql_media += " OR m.alt_text ILIKE %s"
  279. params_media.append(f"%{p}%")
  280. sql_media += ") LIMIT 51 OFFSET %s"
  281. params_media.append(offset)
  282. cur.execute(sql_media, tuple(params_media))
  283. fetched_media = cur.fetchall()
  284. if fetched_media:
  285. media_list = list(fetched_media)
  286. if len(media_list) > 50: has_next = True; media_list = media_list[:50]
  287. if tab == 'images': image_results = media_list
  288. else: video_results = media_list
  289. cur.close(); conn.close()
  290. except Exception as e: print(f"SEARCH ERROR: {e}")
  291. return render_template('results.html', results=results, image_results=image_results, video_results=video_results,
  292. sponsored=sponsored, query=query, page=page, has_next=has_next, tab=tab,
  293. smart_query=", ".join(patterns), category=category_filter, available_categories=available_categories)
  294. # --- Auth & Admin Routes ---
  295. @app.route('/login', methods=['GET', 'POST'])
  296. def login():
  297. if request.method == 'POST':
  298. u, p = request.form.get('username'), request.form.get('password')
  299. conn = get_db_connection(); cur = conn.cursor(cursor_factory=extras.RealDictCursor)
  300. cur.execute("SELECT * FROM admin_users WHERE username = %s AND password_hash = %s", (u, p))
  301. user = cur.fetchone(); cur.close(); conn.close()
  302. if user:
  303. session.update({'logged_in': True, 'user_id': user['id'], 'username': u, 'user_role': user.get('role', 'user')})
  304. return redirect(url_for('admin_dashboard'))
  305. flash('Λάθος στοιχεία πρόσβασης')
  306. return render_template('login.html')
  307. @app.route('/logout')
  308. def logout():
  309. session.clear()
  310. return redirect(url_for('home'))
  311. @app.route('/admin')
  312. @login_required
  313. def admin_dashboard():
  314. conn = get_db_connection(); cur = conn.cursor(cursor_factory=extras.RealDictCursor)
  315. if session.get('user_role') == 'admin':
  316. 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")
  317. else:
  318. cur.execute("SELECT * FROM sponsored_links WHERE user_id = %s ORDER BY id DESC", (session.get('user_id'),))
  319. sponsored = cur.fetchall()
  320. analytics = {'top_queries': [], 'categories': [], 'blacklists': []}
  321. if session.get('user_role') == 'admin':
  322. cur.execute("SELECT search_query, COUNT(*) as count FROM activity_logs GROUP BY search_query ORDER BY count DESC LIMIT 10")
  323. analytics['top_queries'] = cur.fetchall()
  324. cur.execute("SELECT category, COUNT(*) as count FROM web_health WHERE category IS NOT NULL GROUP BY category")
  325. analytics['categories'] = cur.fetchall()
  326. cur.execute("SELECT * FROM blacklisted_domains ORDER BY id DESC")
  327. analytics['blacklists'] = cur.fetchall()
  328. cur.close(); conn.close()
  329. cfg = load_config()
  330. cf_creds = cfg.get('cf_credentials', []) if session.get('user_role') == 'admin' else []
  331. ai_prompt = cfg.get('ai_prompt', '') if session.get('user_role') == 'admin' else ''
  332. return render_template('admin_dashboard.html', sponsored=sponsored, cf_creds=cf_creds, ai_prompt=ai_prompt, analytics=analytics)
  333. @app.route('/admin/sponsored/add', methods=['POST'])
  334. @login_required
  335. def add_sponsored():
  336. user_id = session.get('user_id')
  337. data = (request.form.get('title'), request.form.get('url'), request.form.get('description'), request.form.get('keywords'), user_id)
  338. try:
  339. conn = get_db_connection(); cur = conn.cursor()
  340. cur.execute("INSERT INTO sponsored_links (title, url, description, keywords, user_id) VALUES (%s, %s, %s, %s, %s)", data)
  341. conn.commit(); cur.close(); conn.close()
  342. except: pass
  343. return redirect(url_for('admin_dashboard'))
  344. @app.route('/admin/sponsored/delete/<int:link_id>')
  345. @login_required
  346. def delete_sponsored(link_id):
  347. conn = get_db_connection(); cur = conn.cursor()
  348. if session.get('user_role') == 'admin': cur.execute("DELETE FROM sponsored_links WHERE id = %s", (link_id,))
  349. else: cur.execute("DELETE FROM sponsored_links WHERE id = %s AND user_id = %s", (link_id, session.get('user_id')))
  350. conn.commit(); cur.close(); conn.close()
  351. return redirect(url_for('admin_dashboard'))
  352. @app.route('/admin/cf/add', methods=['POST'])
  353. @login_required
  354. @admin_required
  355. def add_cf_cred():
  356. cfg = load_config(); creds = cfg.get('cf_credentials', [])
  357. 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', '')}
  358. if new_cred["account_id"]: creds.append(new_cred); cfg['cf_credentials'] = creds; save_config(cfg)
  359. return redirect(url_for('admin_dashboard'))
  360. @app.route('/admin/cf/delete/<int:index>')
  361. @login_required
  362. @admin_required
  363. def delete_cf_cred(index):
  364. cfg = load_config(); creds = cfg.get('cf_credentials', [])
  365. if 0 <= index < len(creds): del creds[index]; cfg['cf_credentials'] = creds; save_config(cfg)
  366. return redirect(url_for('admin_dashboard'))
  367. @app.route('/admin/cf/prompt', methods=['POST'])
  368. @login_required
  369. @admin_required
  370. def update_ai_prompt():
  371. cfg = load_config(); cfg['ai_prompt'] = request.form.get('ai_prompt', ''); save_config(cfg)
  372. return redirect(url_for('admin_dashboard'))
  373. @app.route('/admin/blacklist/add', methods=['POST'])
  374. @login_required
  375. @admin_required
  376. def add_blacklist():
  377. domain = request.form.get('domain', '').strip()
  378. if domain:
  379. try:
  380. conn = get_db_connection(); cur = conn.cursor()
  381. cur.execute("INSERT INTO blacklisted_domains (domain) VALUES (%s) ON CONFLICT DO NOTHING", (domain,))
  382. conn.commit(); cur.close(); conn.close()
  383. except: pass
  384. return redirect(url_for('admin_dashboard'))
  385. @app.route('/admin/blacklist/delete/<int:b_id>')
  386. @login_required
  387. @admin_required
  388. def delete_blacklist(b_id):
  389. conn = get_db_connection(); cur = conn.cursor()
  390. cur.execute("DELETE FROM blacklisted_domains WHERE id = %s", (b_id,))
  391. conn.commit(); cur.close(); conn.close()
  392. return redirect(url_for('admin_dashboard'))
  393. @app.route('/admin/logs')
  394. @login_required
  395. @admin_required
  396. def admin_logs():
  397. conn = get_db_connection(); cur = conn.cursor(cursor_factory=extras.RealDictCursor)
  398. 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")
  399. logs = cur.fetchall(); cur.close(); conn.close()
  400. return render_template('admin_logs.html', logs=logs)
  401. @app.route('/admin/users')
  402. @login_required
  403. @admin_required
  404. def admin_users():
  405. conn = get_db_connection(); cur = conn.cursor(cursor_factory=extras.RealDictCursor)
  406. cur.execute("SELECT id, username, role FROM admin_users ORDER BY id ASC")
  407. users = cur.fetchall(); cur.close(); conn.close()
  408. return render_template('admin_users.html', users=users)
  409. if __name__ == '__main__':
  410. app.run(host='0.0.0.0', port=5000, debug=True)