Spaces:
Sleeping
Sleeping
| import os | |
| import io | |
| import json | |
| import urllib.parse | |
| import requests | |
| from fastapi import FastAPI, File, UploadFile, Form, Request, BackgroundTasks | |
| from fastapi.responses import HTMLResponse | |
| from PIL import Image | |
| import imagehash | |
| from playwright.sync_api import sync_playwright | |
| app = FastAPI() | |
| DATABASE_DIR = "scam_database" | |
| VOTES_FILE = "votes.json" | |
| # ฟังก์ชันโหลดข้อมูลโหวตจากไฟล์ JSON | |
| def load_votes(): | |
| if os.path.exists(VOTES_FILE): | |
| try: | |
| with open(VOTES_FILE, "r", encoding="utf-8") as f: | |
| return json.load(f) | |
| except Exception: | |
| return {} | |
| return {} | |
| # ฟังก์ชันบันทึกข้อมูลโหวตลงไฟล์ JSON | |
| def save_votes(votes_data): | |
| with open(VOTES_FILE, "w", encoding="utf-8") as f: | |
| json.dump(votes_data, f, ensure_ascii=False, indent=2) | |
| # ฟังก์ชันดึง Hash ของรูปทั้งหมดในฐานข้อมูล | |
| def get_database_hashes(): | |
| hashes = {} | |
| if not os.path.exists(DATABASE_DIR): | |
| os.makedirs(DATABASE_DIR) | |
| for filename in os.listdir(DATABASE_DIR): | |
| if filename.lower().endswith(('.png', '.jpg', '.jpeg', '.webp')): | |
| filepath = os.path.join(DATABASE_DIR, filename) | |
| try: | |
| img = Image.open(filepath) | |
| h = str(imagehash.phash(img)) | |
| hashes[filename] = h | |
| except Exception as e: | |
| print(f"Error loading {filename}: {e}") | |
| return hashes | |
| # ฟังก์ชันดึง IP ของผู้ใช้ | |
| def get_client_ip(request: Request): | |
| forwarded_for = request.headers.get("x-forwarded-for") | |
| if forwarded_for: | |
| return forwarded_for.split(",")[0].strip() | |
| return request.client.host if request.client else "127.0.0.1" | |
| # --------------------------------------------------------- | |
| # ระบบบอทสแกนรูปภาพจาก TikTok (เพิ่มระบบกันตรวจจับบอท) | |
| # --------------------------------------------------------- | |
| def run_tiktok_bot_task(keyword: str): | |
| db_hashes = get_database_hashes() | |
| existing_hash_objs = [imagehash.hex_to_hash(h) for h in db_hashes.values()] | |
| MATCH_THRESHOLD = 12 | |
| encoded_keyword = urllib.parse.quote(keyword) | |
| search_url = f"https://www.tiktok.com/search?q={encoded_keyword}" | |
| try: | |
| with sync_playwright() as p: | |
| # เพิ่ม args ปลอมตัวไม่ให้ TikTok บล็อกบอท | |
| browser = p.chromium.launch( | |
| headless=True, | |
| args=[ | |
| "--no-sandbox", | |
| "--disable-setuid-sandbox", | |
| "--disable-blink-features=AutomationControlled", | |
| "--disable-dev-shm-usage" | |
| ] | |
| ) | |
| context = browser.new_context( | |
| user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36", | |
| viewport={"width": 1280, "height": 800}, | |
| locale="th-TH" | |
| ) | |
| page = context.new_page() | |
| # ซ่อนสถานะ webdriver | |
| page.add_init_script("Object.defineProperty(navigator, 'webdriver', {get: () => undefined})") | |
| print(f"[Bot] กำลังเปิด TikTok ค้นหาคำว่า: '{keyword}'") | |
| page.goto(search_url, wait_until="domcontentloaded", timeout=45000) | |
| page.wait_for_timeout(5000) | |
| # เลื่อนหน้าจอลงหลายๆ ครั้งกระตุ้นให้คลิปโหลด | |
| for _ in range(4): | |
| page.mouse.wheel(0, 1000) | |
| page.wait_for_timeout(2000) | |
| # ค้นหาลิงก์คลิปทั้งหมดในหน้าค้นหา | |
| video_links = set() | |
| links = page.query_selector_all("a") | |
| for a in links: | |
| href = a.get_attribute("href") | |
| if href and ("/video/" in href or "/photo/" in href): | |
| if href.startswith("/"): | |
| href = "https://www.tiktok.com" + href | |
| clean_url = href.split("?")[0] | |
| video_links.add(clean_url) | |
| video_list = list(video_links) | |
| print(f"[Bot] เจอวิดีโอจากหน้าค้นหาทั้งหมด {len(video_list)} คลิป") | |
| # สแกนทีละคลิป (สูงสุด 5 คลิปแรก) | |
| for v_idx, v_url in enumerate(video_list[:5]): | |
| try: | |
| print(f"[Bot] ({v_idx+1}/{min(len(video_list), 5)}) กำลังเข้าไปสแกนคอมเมนต์คลิป: {v_url}") | |
| page.goto(v_url, wait_until="domcontentloaded", timeout=30000) | |
| page.wait_for_timeout(4000) | |
| # สกรอลล์ลงไปส่วนคอมเมนต์ | |
| page.mouse.wheel(0, 1000) | |
| page.wait_for_timeout(2500) | |
| # ดึงเฉพาะรูปภาพที่แนบมาในคอมเมนต์ | |
| comment_imgs = page.query_selector_all( | |
| "[data-e2e='comment-list'] img, [class*='comment'] img, div[class*='Comment'] img" | |
| ) | |
| print(f"[Bot] พบรูปในคอมเมนต์จำนวน {len(comment_imgs)} รูป") | |
| for img_el in comment_imgs: | |
| src = img_el.get_attribute("src") | |
| if not src or not src.startswith("http"): | |
| continue | |
| try: | |
| resp = requests.get(src, timeout=5) | |
| if resp.status_code == 200: | |
| img = Image.open(io.BytesIO(resp.content)) | |
| # กรองรูปโปรไฟล์ขนาดเล็กออก (รูปในคอมเมนต์มักจะใหญ่กว่า 120px) | |
| if img.width < 120 or img.height < 120: | |
| continue | |
| new_hash_obj = imagehash.phash(img) | |
| is_dup = False | |
| for existing_h in existing_hash_objs: | |
| if (new_hash_obj - existing_h) <= MATCH_THRESHOLD: | |
| is_dup = True | |
| break | |
| if not is_dup: | |
| filename = f"bot_{str(new_hash_obj)}.jpg" | |
| filepath = os.path.join(DATABASE_DIR, filename) | |
| img.convert("RGB").save(filepath, "JPEG") | |
| existing_hash_objs.append(new_hash_obj) | |
| print(f"[Bot] ✅ บันทึกรูปใหม่จากคอมเมนต์: {filename}") | |
| else: | |
| print(f"[Bot] ❌ รูปในคอมเมนต์ซ้ำแล้ว - ข้าม") | |
| except Exception: | |
| continue | |
| except Exception as ve: | |
| print(f"[Bot Error] เข้าดูคลิปไม่ได้: {ve}") | |
| continue | |
| browser.close() | |
| print(f"[Bot] สแกนเสร็จสิ้นสำหรับคำว่า '{keyword}'") | |
| except Exception as e: | |
| print(f"[Bot Error] {e}") | |
| # Endpoint สำหรับสั่งให้บอทเริ่มทำงาน | |
| async def trigger_bot( | |
| keyword: str = Form(...), | |
| background_tasks: BackgroundTasks = BackgroundTasks() | |
| ): | |
| background_tasks.add_task(run_tiktok_bot_task, keyword) | |
| return { | |
| "success": True, | |
| "message": f"บอทเริ่มค้นหาคำว่า '{keyword}' และสแกนรูปในคอมเมนต์เรียบร้อยแล้ว" | |
| } | |
| # --------------------------------------------------------- | |
| # Endpoint เดิม (100% ตามโค้ดเดิม) | |
| # --------------------------------------------------------- | |
| def serve_index(): | |
| if os.path.exists("index.html"): | |
| with open("index.html", "r", encoding="utf-8") as f: | |
| return f.read() | |
| return "<h1>index.html not found</h1>" | |
| async def check_image(file: UploadFile = File(...)): | |
| try: | |
| contents = await file.read() | |
| uploaded_img = Image.open(io.BytesIO(contents)) | |
| uploaded_hash_obj = imagehash.phash(uploaded_img) | |
| uploaded_hash_str = str(uploaded_hash_obj) | |
| db_hashes = get_database_hashes() | |
| votes_data = load_votes() | |
| MATCH_THRESHOLD = 12 | |
| matched_hash_key = None | |
| is_in_db = False | |
| for filename, db_hash_str in db_hashes.items(): | |
| db_hash_obj = imagehash.hex_to_hash(db_hash_str) | |
| distance = uploaded_hash_obj - db_hash_obj | |
| if distance <= MATCH_THRESHOLD: | |
| matched_hash_key = db_hash_str | |
| is_in_db = True | |
| break | |
| if not matched_hash_key: | |
| matched_hash_key = uploaded_hash_str | |
| image_vote = votes_data.get(matched_hash_key, {"likes": 0, "dislikes": 0, "users": {}}) | |
| likes = image_vote.get("likes", 0) | |
| dislikes = image_vote.get("dislikes", 0) | |
| if likes >= 100: | |
| is_scam = False | |
| result_text = "ปลอดภัย" | |
| detail_text = f"รูปภาพนี้ได้รับการยืนยันว่าปลอดภัยจากผู้ใช้งาน (กดไลก์ {likes} คน)" | |
| elif is_in_db: | |
| is_scam = True | |
| result_text = "มิจแน่นอน100%" | |
| detail_text = "พบรูปภาพตรงกับฐานข้อมูลรูปภาพโปรโมทหลอกลวง" | |
| else: | |
| is_scam = False | |
| result_text = "ไม่สามารถตรวจสอบได้" | |
| detail_text = "ยังไม่พบรูปภาพนี้ในฐานข้อมูลมิจฉาชีพ" | |
| return { | |
| "is_scam": is_scam, | |
| "result": result_text, | |
| "detail": detail_text, | |
| "image_hash": matched_hash_key, | |
| "likes": likes, | |
| "dislikes": dislikes | |
| } | |
| except Exception as e: | |
| return {"error": f"เกิดข้อผิดพลาดในการประมวลผล: {str(e)}"} | |
| async def vote_image( | |
| request: Request, | |
| image_hash: str = Form(...), | |
| vote_type: str = Form(...), | |
| user_id: str = Form(None) | |
| ): | |
| try: | |
| votes_data = load_votes() | |
| if image_hash not in votes_data: | |
| votes_data[image_hash] = {"likes": 0, "dislikes": 0, "users": {}} | |
| elif "users" not in votes_data[image_hash]: | |
| votes_data[image_hash]["users"] = {} | |
| client_ip = get_client_ip(request) | |
| user_key = f"{client_ip}_{user_id}" if user_id else client_ip | |
| if user_key in votes_data[image_hash]["users"]: | |
| return { | |
| "success": False, | |
| "error": "คุณเคยโหวตรูปภาพนี้ไปแล้ว สามารถโหวตได้เพียง 1 ครั้งเท่านั้น" | |
| } | |
| votes_data[image_hash]["users"][user_key] = vote_type | |
| if vote_type == "like": | |
| votes_data[image_hash]["likes"] += 1 | |
| elif vote_type == "dislike": | |
| votes_data[image_hash]["dislikes"] += 1 | |
| save_votes(votes_data) | |
| likes = votes_data[image_hash]["likes"] | |
| dislikes = votes_data[image_hash]["dislikes"] | |
| db_hashes = get_database_hashes() | |
| is_in_db = image_hash in db_hashes.values() | |
| if likes >= 100: | |
| is_scam = False | |
| result_text = "ปลอดภัย" | |
| detail_text = f"รูปภาพนี้ได้รับการยืนยันว่าปลอดภัยจากผู้ใช้งาน (กดไลก์ {likes} คน)" | |
| elif is_in_db: | |
| is_scam = True | |
| result_text = "มิจแน่นอน100%" | |
| detail_text = "พบรูปภาพตรงกับฐานข้อมูลรูปภาพโปรโมทหลอกลวง" | |
| else: | |
| is_scam = False | |
| result_text = "ไม่สามารถตรวจสอบได้" | |
| detail_text = "ยังไม่พบรูปภาพนี้ในฐานข้อมูลมิจฉาชีพ" | |
| return { | |
| "success": True, | |
| "likes": likes, | |
| "dislikes": dislikes, | |
| "is_scam": is_scam, | |
| "result": result_text, | |
| "detail": detail_text | |
| } | |
| except Exception as e: | |
| return {"error": f"เกิดข้อผิดพลาดในการโหวต: {str(e)}"} | |