#!/usr/bin/env python3 """ ChannelReact — WhatsApp Channel reaction sender (pure Python). Sends emoji reactions to a WhatsApp Channel post via the public worker satriareact.satriadeveloperz.workers.dev. Uses curl_cffi with Chrome impersonation because the worker mis-routes non-browser fingerprints (undici / httpx / curl) to an upstream 502. Flow (mirrors the site): 1. POST /api/handshake -> token (valid ~60s). 2. POST /api/react {url, reactions, token} -> queued to VIP API backend. Usage: python3 channelreact.py [more emoji...] python3 channelreact.py -n 3 --delay 1500 👍 ❤️ Dependency: pip install curl_cffi """ import sys import time import re import json from curl_cffi import requests BASE = "https://satriareact.satriadeveloperz.workers.dev" UA = ( "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " "(KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36" ) EMOJI_RE = re.compile(r"[\U0001F300-\U0001FAFF\u2600-\u27BF\uFE0F]", re.UNICODE) TEXT_OK = { "santuy", "sad", "nice", "siap", "ok", "good", "wow", "keren", "marempu", "mantap", "ganteng", "cantik", "anjay", "mantul", "limit", "by", "tenkyu", "dengerin", "sell", "gajelas", "kepo", "jelek", } def is_emoji(s): v = str(s or "").strip() if not v: return False return len(v) <= 8 and (bool(EMOJI_RE.search(v)) or v.lower() in TEXT_OK) class ChannelReact: def __init__(self, session=None): self.session = session or self._new_session() @staticmethod def _new_session(): s = requests.Session(impersonate="chrome") s.headers.update({ "User-Agent": UA, "Origin": BASE, "Referer": BASE + "/", }) return s def handshake(self, retries=8): """Handshake sampai dapat token. Token TERIKAT ke session (cookies) — session yang berhasil harus dipakai untuk react.""" last = None for i in range(retries + 1): if i > 0: # Backoff: 3s, 6s, 9s ... plus fresh session (upstream flakes). time.sleep(3 * i) self.session = self._new_session() try: r = self.session.post(BASE + "/api/handshake", json={}, timeout=30) data = r.json() if r.headers.get("content-type", "").startswith("application/json") else None if r.status_code == 200 and data and data.get("success"): return { "token": data["token"], "clientId": data.get("clientId"), "username": (data.get("step1") or {}).get("username") or data.get("username"), "expiresInMs": data.get("expiresInMs", 60000), } last = "HTTP %s: %s" % (r.status_code, str(data)[:120]) except Exception as e: last = str(e) raise RuntimeError("Handshake gagal (%s)" % last) def react(self, url, reactions, token=None, retries=3): """Kirim reaksi memakai session yang sama dengan handshake (token terikat session). 401 invalid_handshake / 403 / 502 -> handshake ulang & retry.""" tok = token if tok is None: tok = self.handshake()["token"] # self.session ikut ter-set for attempt in range(1, retries + 1): try: r = self.session.post( BASE + "/api/react", json={"url": url, "reactions": reactions, "token": tok}, timeout=45, ) except Exception: self.session = self._new_session() tok = self.handshake()["token"] continue data = r.json() if r.headers.get("content-type", "").startswith("application/json") else None if r.status_code == 200 and data and data.get("success"): return { "success": True, "count": data.get("count", len(reactions)), "message": data.get("message"), "task": (data.get("task") or {}).get("status"), "vip": (data.get("vip") or {}).get("packageName"), } # Token invalid / expired -> handshake ulang pada session baru, retry. if r.status_code in (401, 403) and attempt < retries: time.sleep(2.0 * attempt) self.session = self._new_session() tok = self.handshake()["token"] continue if r.status_code == 502 and attempt < retries: time.sleep(2.5 * attempt) self.session = self._new_session() tok = self.handshake()["token"] continue raise RuntimeError("Reaksi gagal (HTTP %s): %s" % (r.status_code, str(data)[:140])) raise RuntimeError("Reaksi gagal setelah %d percobaan" % retries) def main(): args = sys.argv[1:] url = next((a for a in args if not a.startswith("-")), None) n = 1 delay = 1000 if "-n" in args: n = int(args[args.index("-n") + 1]) if "--delay" in args: delay = int(args[args.index("--delay") + 1]) reactions = [a for a in args if a != url and not a.startswith("-") and not a.isdigit()] if not url: print("Usage: python3 satriareact.py [more...]") print(" python3 satriareact.py -n 3 --delay 1500 👍 ❤️") return 1 if not reactions: reactions = ["santuy"] for r in reactions: if not is_emoji(r): print("ERROR: `%s` bukan emoji — gunakan emoji seperti 👍 ❤️ 🎉" % r) return 1 print("== ChannelReact (channel reaction sender) ==") print("url : %s" % url) print("react : [%s] x %d" % (", ".join(reactions), n)) try: client = ChannelReact() token = client.handshake()["token"] print("handshake: ok") total = 0 for i in range(n): res = client.react(url, reactions, token) total += res["count"] print(" #%d: ok (count %s, task %s)" % (i + 1, res["count"], res["task"])) if i < n - 1 and delay > 0: time.sleep(delay / 1000.0) print("done : %d reaction(s) dikirim" % total) return 0 except Exception as e: print("ERROR: %s" % e) return 1 if __name__ == "__main__": sys.exit(main())