Spaces:
Running
Running
| import os, time, threading, json, re, requests, warnings, email.utils | |
| from datetime import datetime | |
| from zoneinfo import ZoneInfo | |
| from curl_cffi import requests as curl_requests | |
| from bs4 import BeautifulSoup, XMLParsedAsHTMLWarning | |
| from http.server import HTTPServer, BaseHTTPRequestHandler | |
| from huggingface_hub import HfApi, hf_hub_download | |
| from openai import OpenAI | |
| from groq import Groq | |
| warnings.filterwarnings("ignore", category=XMLParsedAsHTMLWarning) | |
| # --- KONFIGURASI GROUNDED MEI 2026 --- | |
| MY_TZ = ZoneInfo("Asia/Kuala_Lumpur") | |
| PORT = int(os.getenv("PORT", "7860")) | |
| WEBHOOK_URL = os.getenv("DISCORD_WEBHOOK", "").strip() | |
| HF_TOKEN = os.getenv("HF_TOKEN", "").strip() | |
| GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY", "").strip() | |
| GROQ_API_KEY = os.getenv("GROQ_API_KEY", "").strip() | |
| NVIDIA_API_KEY = os.getenv("NVIDIA_API_KEY", "").strip() | |
| HF_DATASET = os.getenv("HFDATASET", "").strip() | |
| UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36" | |
| api_hf = HfApi(token=HF_TOKEN) | |
| nvidia_client = OpenAI(base_url="https://integrate.api.nvidia.com/v1", api_key=NVIDIA_API_KEY) | |
| google_client = OpenAI(api_key=GOOGLE_API_KEY, base_url="https://generativelanguage.googleapis.com/v1beta/openai/") | |
| groq_client = Groq(api_key=GROQ_API_KEY) | |
| FEEDS = [ | |
| {"name": "Amanz", "url": "https://cms.amanz.my/feed", "color": 15158332, "icon": "https://amanz.my/wp-content/uploads/2021/01/amanz-logo.png", "interval": 600, "use_proxy": True}, | |
| {"name": "Phys.org", "url": "https://phys.org/rss-feed/", "color": 27558, "icon": "https://i.imgur.com/8YvA5fG.png", "interval": 600, "use_proxy": True}, | |
| {"name": "Defense One", "url": "https://www.defenseone.com/rss/all/", "color": 10038562, "icon": "https://www.defenseone.com/favicon.ico", "interval": 600, "use_proxy": True}, | |
| {"name": "Defence Blog", "url": "https://defence-blog.com/feed/", "color": 7372944, "icon": "https://defence-blog.com/favicon.ico", "interval": 600, "use_proxy": True}, | |
| {"name": "Quanta Magazine", "url": "https://www.quantamagazine.org/feed/", "color": 1150410, "icon": "https://www.quantamagazine.org/favicon.ico", "interval": 600, "use_proxy": True} | |
| ] | |
| LOCAL_MEM = {} | |
| state_lock = threading.Lock() | |
| def log(msg): print(f"[{datetime.now(MY_TZ).strftime('%H:%M:%S')}] {msg}", flush=True) | |
| def clip(text, limit): text = str(text or "").strip(); return text if len(text) <= limit else text[:limit - 1] + "β¦" | |
| def safe_name(name): return re.sub(r"[^a-zA-Z0-9_.-]", "_", name) | |
| def normalize_link(url): return (url or "").strip().split("#")[0].split("?")[0].rstrip("/") | |
| def _resolve(url): return url.strip() | |
| def load_state(feed_name): | |
| fname = f"state_{safe_name(feed_name)}.json" | |
| with state_lock: | |
| if feed_name in LOCAL_MEM: return LOCAL_MEM[feed_name].copy() | |
| try: | |
| path = hf_hub_download(repo_id=HF_DATASET, filename=fname, repo_type="dataset", token=HF_TOKEN, force_download=True) | |
| with open(path, "r", encoding="utf-8") as f: | |
| data = json.load(f) | |
| log(f"π {feed_name}: State loaded dari {path} | seen={len(data.get('seen_links',[]))} | last={data.get('seen_links',['?'])[-1][-8:] if data.get('seen_links') else 'kosong'}") | |
| with state_lock: LOCAL_MEM[feed_name] = data | |
| return data | |
| except: return {"seen_links": [], "etag": "", "last_modified": ""} | |
| def save_state(feed_name, state): | |
| fname = f"state_{safe_name(feed_name)}.json" | |
| with state_lock: | |
| LOCAL_MEM[feed_name] = state.copy() | |
| try: | |
| with open(fname, "w", encoding="utf-8") as f: json.dump(state, f, ensure_ascii=False, indent=2) | |
| api_hf.upload_file(path_or_fileobj=fname, path_in_repo=fname, repo_id=HF_DATASET, repo_type="dataset", token=HF_TOKEN) | |
| log(f"πΎ State {feed_name} disimpan.") | |
| except Exception as e: log(f"β Gagal simpan state: {e}") | |
| # --- FETCH FEED YANG TELAH DIPERBAIKI --- | |
| def fetch_feed(feed, state): | |
| url = feed["url"] | |
| sep = "&" if "?" in url else "?" | |
| cache_buster = f"{sep}t={int(time.time())}" | |
| use_proxy_setting = feed.get("use_proxy", "auto") | |
| if use_proxy_setting is True: | |
| urls_to_try = [ | |
| {"url": f"https://corsproxy.io/?{url}{cache_buster}", "type": "Proxy (CORS Proxy)"}, | |
| {"url": f"https://api.allorigins.win/raw?url={url}{cache_buster}", "type": "Proxy (AllOrigins)"}, | |
| {"url": f"https://api.codetabs.com/v1/proxy?quest={url}{cache_buster}", "type": "Proxy (Codetabs)"}, | |
| {"url": f"https://api.rss2json.com/v1/api.json?rss_url={url}", "type": "Proxy (RSS2JSON)"}, | |
| {"url": url, "type": "Direct (Selesa)"} | |
| ] | |
| elif use_proxy_setting == "auto": | |
| urls_to_try = [ | |
| {"url": url, "type": "Direct"}, | |
| {"url": url + cache_buster, "type": "Direct (Cache Buster)"}, | |
| {"url": f"https://api.codetabs.com/v1/proxy?quest={url}{cache_buster}", "type": "Proxy (Codetabs)"}, | |
| {"url": f"https://corsproxy.io/?{url}{cache_buster}", "type": "Proxy (CORS Proxy)"}, | |
| {"url": f"https://api.allorigins.win/raw?url={url}{cache_buster}", "type": "Proxy (AllOrigins)"} | |
| ] | |
| else: | |
| urls_to_try = [ | |
| {"url": url, "type": "Direct"}, | |
| {"url": url + cache_buster, "type": "Direct (Cache Buster)"} | |
| ] | |
| for item in urls_to_try: | |
| try_url = item["url"] | |
| type_url = item["type"] | |
| log(f"π {feed['name']}: Mencuba {type_url} ({try_url})") | |
| try: | |
| headers = {} | |
| if type_url in ("Direct", "Direct (Selesa)") and state.get("etag"): | |
| headers["If-None-Match"] = state["etag"] | |
| r = curl_requests.get(try_url, headers=headers, impersonate="chrome124", timeout=30) | |
| if r.status_code == 304: | |
| log(f"π {feed['name']}: {type_url} 304, cuba URL seterusnya...") | |
| continue | |
| if r.status_code == 200: | |
| # FIX 4: Validate response β pastikan XML/RSS, bukan HTML Cloudflare | |
| content_text = r.text[:500].strip().lower() | |
| if "<html" in content_text and "<rss" not in content_text and "<feed" not in content_text and "<?xml" not in content_text: | |
| log(f"β οΈ {feed['name']}: {type_url} beri HTML bukan RSS! Skip...") | |
| continue | |
| if "Direct" in type_url: | |
| state["etag"] = r.headers.get("ETag", "") | |
| log(f"β {feed['name']}: Berjaya tembus guna {type_url}!") | |
| # FIX 5c: RSS2JSON return JSON β convert ke XML supaya parser kekal sama | |
| if "rss2json" in type_url.lower(): | |
| try: | |
| jdata = r.json() | |
| if jdata.get("status") == "ok" and "items" in jdata: | |
| xml_parts = ['<?xml version="1.0" encoding="UTF-8"?><rss><channel>'] | |
| for ji in jdata["items"]: | |
| xml_parts.append("<item>") | |
| xml_parts.append(f"<title>{ji.get('title','')}</title>") | |
| xml_parts.append(f"<link>{ji.get('link','')}</link>") | |
| xml_parts.append(f"<description><![CDATA[{ji.get('description','')}]]></description>") | |
| if ji.get("thumbnail"): | |
| xml_parts.append(f'<media:thumbnail url="{ji["thumbnail"]}"/>') | |
| elif ji.get("enclosure",{}).get("link"): | |
| xml_parts.append(f'<media:thumbnail url="{ji["enclosure"]["link"]}"/>') | |
| xml_parts.append("</item>") | |
| xml_parts.append("</channel></rss>") | |
| class XmlResponse: | |
| def __init__(self, content): | |
| self.content = content | |
| self.status_code = 200 | |
| r = XmlResponse("".join(xml_parts).encode("utf-8")) | |
| log(f"π {feed['name']}: RSS2JSON β XML converted ({len(jdata['items'])} items)") | |
| except Exception as e: | |
| log(f"β οΈ {feed['name']}: RSS2JSON parse error: {e}") | |
| return r, state, "ok" | |
| log(f"β οΈ {feed['name']}: {type_url} beri respon {r.status_code}") | |
| except Exception as e: | |
| log(f"β {feed['name']}: {type_url} timeout/error ({type(e).__name__})") | |
| return None, state, "failed" | |
| # --- EKSTRAKSI GAMBAR --- | |
| def extract_image(raw): | |
| media = raw.find("media:thumbnail") or raw.find("thumbnail") or \ | |
| raw.find("media:content") or raw.find("content") or \ | |
| raw.find("enclosure") | |
| if media and media.get("url"): | |
| log(f"πΌοΈ Gambar jumpa (media): {media['url']}") | |
| return media["url"] | |
| desc_tag = raw.find("description") | |
| content_tag = raw.find("content:encoded") or raw.find("encoded") | |
| content_text = "" | |
| for tag in [content_tag, desc_tag]: | |
| if tag and tag.text: | |
| content_text += tag.text | |
| soup = BeautifulSoup(tag.text, "html.parser") | |
| img_tag = soup.find("img") | |
| if img_tag and img_tag.get("src"): | |
| log(f"πΌοΈ Gambar jumpa (HTML): {img_tag['src']}") | |
| return img_tag["src"] | |
| if content_text: | |
| imgs = re.findall(r'<img[^>]+(?:src|data-src|data-lazy|data-original)\s*=\s*["\']([^"\'>\s]+)["\'][^>]*>', content_text, re.I) | |
| if imgs: | |
| priority = [u for u in imgs if any(x in u.lower() for x in ["amanz", "wp-content", "scx2.b-cdn.net", "phys"])] | |
| chosen = priority[-1].strip() if priority else imgs[-1].strip() | |
| return _resolve(chosen) | |
| link_tag = raw.find("link") | |
| link = link_tag.text if link_tag else "" | |
| if link and "http" in link: | |
| try: | |
| base = link.split("/")[0] + "//" + link.split("/")[2] | |
| return base + "/favicon.ico" | |
| except: pass | |
| return None | |
| # --- AI RUMUSAN --- | |
| def jana_rumusan_ai(tajuk, teks, nama_sumber): | |
| is_english = nama_sumber in ["Phys.org", "Defense One", "Defence Blog", "Quanta Magazine"] | |
| if not is_english: | |
| log(f"π²πΎ {nama_sumber}: BM detected, skip AI") | |
| return tajuk, teks or "Tiada ringkasan tersedia." | |
| log(f"π {nama_sumber}: English detected, hantar ke AI...") | |
| prompt = f"""Kau adalah seorang penulis portal berita teknologi dan sains yang popular di Malaysia. | |
| Gaya penulisan kau santai, 'cool', dan senang difahami oleh orang muda tapi tetap padat dengan info. | |
| Tugas kau: | |
| 1. Terjemahkan tajuk ke Bahasa Melayu yang 'catchy' dan santai. | |
| 2. Buat rumusan 4-6 ayat. Jangan guna bahasa skema sangat (elakkan perkataan macam 'adalah', 'merupakan', 'tersebut'). | |
| 3. Gunakan bahasa percakapan profesional (contoh: guna 'buatkan' bukan 'menyebabkan', 'bagitau' bukan 'memaklumkan'). | |
| 4. Fokus terus kepada poin paling 'power' atau penting dalam berita ni. | |
| 5. Pastikan flow ayat tu nampak 'natural' macam kawan tengah ceritakan berita menarik kat kawan lain. | |
| Sumber berita: {nama_sumber} | |
| Tajuk Asal: {tajuk} | |
| Kandungan: {teks[:1200]} | |
| Balas dalam format TEPAT ini sahaja: | |
| TAJUK: [tajuk dalam BM] | |
| RUMUSAN: [4-6 ayat ringkasan padat dalam BM]""" | |
| def parse_hasil(hasil): | |
| tajuk_baru = tajuk | |
| rumusan = teks[:900] | |
| for line in hasil.split("\n"): | |
| line = line.strip() | |
| if line.upper().startswith("TAJUK:"): | |
| extracted = line[6:].strip() | |
| if extracted: | |
| tajuk_baru = extracted | |
| log(f"β Tajuk BM: {clip(tajuk_baru, 50)}") | |
| elif line.upper().startswith("RUMUSAN:"): | |
| extracted = line[8:].strip() | |
| if extracted: | |
| rumusan = extracted | |
| log(f"β Rumusan: {clip(rumusan, 80)}") | |
| return tajuk_baru, rumusan | |
| models = [ | |
| {"client": nvidia_client, "name": "google/gemma-4-31b-it", "label": "NVIDIA", "timeout": 45, "retries": 2}, | |
| {"client": google_client, "name": "gemma-4-26b-a4b-it", "label": "Google", "timeout": 35, "retries": 1}, | |
| {"client": groq_client, "name": "openai/gpt-oss-120b", "label": "Groq", "timeout": 25, "retries": 1} | |
| ] | |
| for m in models: | |
| for attempt in range(m["retries"]): | |
| try: | |
| log(f"π€ [{m['label']}] Cubaan {attempt+1}/{m['retries']}...") | |
| res = m["client"].chat.completions.create( | |
| model=m["name"], | |
| messages=[{"role": "user", "content": prompt}], | |
| timeout=m["timeout"] | |
| ) | |
| return parse_hasil(res.choices[0].message.content) | |
| except Exception as e: | |
| err_type = type(e).__name__ | |
| msg = str(e).lower() | |
| reject_keywords = [ | |
| "500", "internal", "bad gateway", "502", "503", | |
| "429", "rate limit", "too many", "overloaded", | |
| "quota", "balance", "insufficient", "credit", | |
| "capacity", "service_unavailable" | |
| ] | |
| if any(x in msg for x in reject_keywords): | |
| log(f"π₯ {m['label']} REJECT: ({msg[:40]}). Server taknak layan, skip terus!") | |
| break | |
| if any(x in msg or x in err_type.lower() for x in ["timeout", "timed out", "deadline"]): | |
| if attempt + 1 < m["retries"]: | |
| log(f"β³ {m['label']} LAMBAT: AI tengah fikir. Cuba sekali terakhir...") | |
| time.sleep(3) | |
| continue | |
| else: | |
| log(f"π {m['label']} GAGAL: Dah tunggu lama pun senyap. Pass ke model lain...") | |
| break | |
| log(f"β οΈ {m['label']} Error: {msg[:50]}") | |
| break | |
| log("β Semua AI dah mampus/reject. Guna teks asal.") | |
| return tajuk, teks | |
| # --- DISCORD PADU --- | |
| def hantar_discord(payload, image_url=None): | |
| try: | |
| url = WEBHOOK_URL.replace("discord.com", "discordapp.com") | |
| headers = {"User-Agent": UA} | |
| if image_url: | |
| try: | |
| # STEP 1: Cuba direct embed dulu | |
| payload_try = json.loads(json.dumps(payload)) | |
| if "embeds" in payload_try and payload_try["embeds"]: | |
| payload_try["embeds"][0]["image"] = {"url": image_url} | |
| resp = requests.post(url, json=payload_try, headers=headers, timeout=20) | |
| if resp.status_code in (200, 204): | |
| log(f"πΌοΈ Gambar embed URL berjaya: {image_url[:80]}") | |
| return True | |
| log(f"β οΈ Direct embed gagal ({resp.status_code}), cuba upload file...") | |
| # STEP 2: Download guna curl_cffi | |
| rtest = curl_requests.get(image_url, impersonate="chrome124", timeout=15) | |
| log(f"π₯ Download gambar: status={rtest.status_code}, CT={rtest.headers.get('Content-Type','?')}, size={len(rtest.content)//1024}KB") | |
| if rtest.status_code == 200 and "image" in rtest.headers.get("Content-Type", "").lower(): | |
| data = rtest.content | |
| if data.startswith(b'\x89PNG\r\n\x1a\n'): | |
| ext, mime_type = "png", "image/png" | |
| elif data.startswith(b'\xff\xd8\xff'): | |
| ext, mime_type = "jpg", "image/jpeg" | |
| elif data.startswith(b'GIF87a') or data.startswith(b'GIF89a'): | |
| ext, mime_type = "gif", "image/gif" | |
| elif data.startswith(b'RIFF') and b'WEBP' in data[8:14]: | |
| ext, mime_type = "webp", "image/webp" | |
| else: | |
| ct = rtest.headers.get("Content-Type", "").lower() | |
| ext = "webp" if "webp" in ct else "png" if "png" in ct else "gif" if "gif" in ct else "jpg" | |
| mime_type = f"image/{ext}" if ext != "jpg" else "image/jpeg" | |
| if len(rtest.content) < 5000: | |
| log(f"β οΈ Gambar terlalu kecil ({len(rtest.content)}B), skip") | |
| elif len(rtest.content) > 10 * 1024 * 1024: | |
| log(f"β οΈ Gambar besar ({len(rtest.content)//1024//1024}MB). Menggunakan Direct URL untuk bypass had Discord.") | |
| if "embeds" in payload and payload["embeds"]: | |
| payload["embeds"][0]["image"] = {"url": image_url} | |
| resp = requests.post( | |
| url, | |
| data={"payload_json": json.dumps(payload)}, | |
| headers=headers, timeout=30 | |
| ) | |
| if resp.status_code in (200, 204): | |
| log(f"πΌοΈ Gambar besar berjaya dipaparkan (Direct URL)") | |
| return True | |
| else: | |
| log(f"β Discord reject: {resp.status_code}") | |
| else: | |
| if "embeds" in payload and payload["embeds"]: | |
| payload["embeds"][0]["image"] = {"url": f"attachment://img.{ext}"} | |
| resp = requests.post( | |
| url, | |
| data={"payload_json": json.dumps(payload)}, | |
| files={"file": (f"img.{ext}", rtest.content, mime_type)}, | |
| headers=headers, timeout=30 | |
| ) | |
| if resp.status_code in (200, 204): | |
| log(f"πΌοΈ Gambar upload file berjaya ({ext}, {len(rtest.content)//1024}KB)") | |
| return True | |
| else: | |
| log(f"β Discord reject: {resp.status_code} {resp.text[:150]}") | |
| else: | |
| log(f"β Gambar download gagal: status={rtest.status_code}, CT={rtest.headers.get('Content-Type','?')}") | |
| except Exception as e: | |
| log(f"β οΈ Gambar error: {e}") | |
| if image_url: | |
| log("β οΈ Gagal render gambar. Guna cara malas: paste URL terus kat mesej.") | |
| payload["content"] = f"{payload.get('content', '')}\n\nπΌοΈ {image_url}" | |
| if "embeds" in payload and payload["embeds"] and "image" in payload["embeds"][0]: | |
| del payload["embeds"][0]["image"] | |
| return requests.post(url, json=payload, headers=headers, timeout=20).status_code in (200, 204) | |
| except: | |
| return False | |
| # --- CORE LOGIC --- | |
| def process_feed(feed): | |
| state = load_state(feed['name']) | |
| if "seen_links" not in state: | |
| state["seen_links"] = [] | |
| if state.get("last_link"): | |
| state["seen_links"].append(normalize_link(state["last_link"])) | |
| seen_links = state["seen_links"] | |
| r, state, status = fetch_feed(feed, state) | |
| if status == "not_modified": | |
| log(f"π {feed['name']}: Tiada perubahan.") | |
| return | |
| if status != "ok" or r is None: | |
| log(f"β οΈ {feed['name']}: Gagal fetch (Status: {status})") | |
| return | |
| soup = BeautifulSoup(r.content, "xml") | |
| items = soup.find_all("item") | |
| log(f"π’ {feed['name']}: XML parsed, {len(items)} items dijumpai") | |
| new_found = [] | |
| for item in items: | |
| link = normalize_link(item.find("link").text if item.find("link") else "") | |
| if not link: | |
| continue | |
| is_seen = link in seen_links | |
| log(f"π {feed['name']}: {link[-8:]} β {'SKIP' if is_seen else 'BARU'}") | |
| if is_seen: | |
| continue | |
| new_found.append({"title": item.find("title").text if item.find("title") else "Tiada Tajuk", "link": link, "desc": item.find("description").text if item.find("description") else "", "raw": item}) | |
| if not new_found: | |
| log(f"β {feed['name']}: Tiada berita baru (Semua dah pernah dihantar).") | |
| return | |
| # --- FIX 1: LOGIK ANTI-SPAM SMART (HAFAL LAMA, HANTAR 7 TERBARU) --- | |
| if len(new_found) > 20: | |
| log(f"β οΈ {feed['name']}: Transisi dikesan! Jumpa {len(new_found)} berita tunggakan.") | |
| tunggakan = new_found[:-7] | |
| for art in tunggakan: | |
| if art["link"] not in seen_links: | |
| seen_links.append(art["link"]) | |
| log(f"π {feed['name']}: {len(tunggakan)} artikel lama dihafal, {min(7, len(new_found))} terbaru akan dihantar.") | |
| if len(seen_links) > 50: | |
| seen_links = seen_links[-50:] | |
| state["seen_links"] = seen_links | |
| save_state(feed["name"], state) | |
| new_found = new_found[-7:] | |
| # ----------------------------------------------- | |
| log(f"π° {feed['name']}: Jumpa {len(new_found)} berita baru!") | |
| new_found.reverse() | |
| to_process = new_found[:7] | |
| for art in to_process: | |
| # ===================================================== | |
| # FIX 2: AMANZ β Embed style + gambar upload dari RSS | |
| # ===================================================== | |
| if feed["name"] == "Amanz": | |
| img_url = extract_image(art['raw']) | |
| desc_clean = BeautifulSoup(art["desc"], "html.parser").get_text(" ").strip() | |
| log(f"π Amanz: {art['title'][:50]} | img={img_url[:60] if img_url else 'None'}") | |
| payload = { | |
| "content": clip(f"π° {feed['name']}: {art['title']}", 200), | |
| "embeds": [{ | |
| "author": {"name": feed["name"], "icon_url": feed["icon"]}, | |
| "title": clip(art["title"], 256), | |
| "url": art["link"], | |
| "description": clip(desc_clean, 900), | |
| "color": feed["color"], | |
| "footer": {"text": f"π° Amanz β’ {datetime.now(MY_TZ).strftime('%H:%M')}"} | |
| }] | |
| } | |
| sent = False | |
| if img_url: | |
| try: | |
| rimg = curl_requests.get(img_url, impersonate="chrome124", timeout=15) | |
| if rimg.status_code == 200 and "image" in rimg.headers.get("Content-Type", "").lower() and len(rimg.content) >= 5000: | |
| data = rimg.content | |
| if data.startswith(b'\x89PNG\r\n\x1a\n'): ext, mime = "png", "image/png" | |
| elif data.startswith(b'\xff\xd8\xff'): ext, mime = "jpg", "image/jpeg" | |
| elif data.startswith(b'GIF8'): ext, mime = "gif", "image/gif" | |
| elif data.startswith(b'RIFF') and b'WEBP' in data[8:14]: ext, mime = "webp", "image/webp" | |
| else: | |
| ct = rimg.headers.get("Content-Type","").lower() | |
| ext = "webp" if "webp" in ct else "png" if "png" in ct else "gif" if "gif" in ct else "jpg" | |
| mime = f"image/{ext}" if ext != "jpg" else "image/jpeg" | |
| payload["embeds"][0]["image"] = {"url": f"attachment://amanz.{ext}"} | |
| wh_url = WEBHOOK_URL.replace("discord.com", "discordapp.com") | |
| resp = requests.post( | |
| wh_url, | |
| data={"payload_json": json.dumps(payload)}, | |
| files={"file": (f"amanz.{ext}", rimg.content, mime)}, | |
| headers={"User-Agent": UA}, timeout=30 | |
| ) | |
| # FIX 3: Tambah 201 β Discord kadang reply 201 Created | |
| if resp.status_code in (200, 201, 204): | |
| log(f"πΌοΈ Amanz: Embed + gambar berjaya ({ext}, {len(rimg.content)//1024}KB)") | |
| sent = True | |
| else: | |
| log(f"β οΈ Amanz: Discord reject ({resp.status_code})") | |
| else: | |
| log(f"β οΈ Amanz: Gambar download gagal/kecil (status={rimg.status_code})") | |
| except Exception as e: | |
| log(f"β οΈ Amanz: Gambar error ({e})") | |
| if not sent: | |
| sent = hantar_discord(payload) | |
| if sent: | |
| seen_links.append(art["link"]) | |
| if len(seen_links) > 50: | |
| seen_links = seen_links[-50:] | |
| state["seen_links"] = seen_links | |
| if "last_link" in state: | |
| del state["last_link"] | |
| save_state(feed["name"], state) | |
| log(f"π΄ Bot tidur 15 saat sebelum post seterusnya...") | |
| time.sleep(15) | |
| else: | |
| break | |
| continue | |
| # ===================================================== | |
| img_url = extract_image(art['raw']) | |
| desc_clean = BeautifulSoup(art["desc"], "html.parser").get_text(" ").strip() | |
| tajuk_ai, teks_ai = jana_rumusan_ai(art["title"], desc_clean, feed["name"]) | |
| payload = { | |
| "content": clip(f"π° {feed['name']}: {tajuk_ai}", 200), | |
| "embeds": [{ | |
| "author": {"name": feed["name"], "icon_url": feed["icon"]}, | |
| "title": clip(tajuk_ai, 256), | |
| "url": art["link"], | |
| "description": clip(teks_ai, 900), | |
| "color": feed["color"], | |
| "footer": {"text": f"β¨ Dirumuskan oleh AI β’ {datetime.now(MY_TZ).strftime('%H:%M')}"} | |
| }] | |
| } | |
| if hantar_discord(payload, image_url=img_url): | |
| seen_links.append(art["link"]) | |
| if len(seen_links) > 50: | |
| seen_links = seen_links[-50:] | |
| state["seen_links"] = seen_links | |
| if "last_link" in state: | |
| del state["last_link"] | |
| save_state(feed["name"], state) | |
| log(f"π΄ Bot tidur 15 saat sebelum post seterusnya...") | |
| time.sleep(15) | |
| else: | |
| break | |
| # --- RUNNER --- | |
| class H(BaseHTTPRequestHandler): | |
| def do_GET(self): self.send_response(200); self.end_headers(); self.wfile.write(b"Bot 2026 Hybrid Talking Active") | |
| def log_message(self, *args): pass | |
| if __name__ == "__main__": | |
| log("π Bot Berita Padu 2026 Hybrid dimulakan...") | |
| threading.Thread(target=lambda: HTTPServer(("0.0.0.0", PORT), H).serve_forever(), daemon=True).start() | |
| for f in FEEDS: | |
| def loop(fd=f): | |
| log(f"π‘ Thread {fd['name']} aktif.") | |
| while True: | |
| try: process_feed(fd) | |
| except Exception as e: log(f"π₯ Error {fd['name']}: {e}") | |
| time.sleep(fd.get("interval", 1800)) | |
| threading.Thread(target=loop, daemon=True).start() | |
| while True: time.sleep(60) | |