Back to Home
Gistify Snippet
Public
WhatsApp Channel Reaction Sender (ChannelReact)
Automated WhatsApp Channel emoji reaction sender with session handshake, Cloudflare Chrome fingerprint impersonation, retry backoff, and VIP reaction task queuing.
#whatsapp#reaction#python#channel#automation#tools
33
Rawchannelreact.pypython
174 lines| 1 | #!/usr/bin/env python3 |
| 2 | """ |
| 3 | ChannelReact — WhatsApp Channel reaction sender (pure Python). |
| 4 | |
| 5 | Sends emoji reactions to a WhatsApp Channel post via the public worker |
| 6 | satriareact.satriadeveloperz.workers.dev. Uses curl_cffi with Chrome |
| 7 | impersonation because the worker mis-routes non-browser fingerprints |
| 8 | (undici / httpx / curl) to an upstream 502. |
| 9 | |
| 10 | Flow (mirrors the site): |
| 11 | 1. POST /api/handshake -> token (valid ~60s). |
| 12 | 2. POST /api/react {url, reactions, token} -> queued to VIP API backend. |
| 13 | |
| 14 | Usage: |
| 15 | python3 channelreact.py <channelUrl> <emoji> [more emoji...] |
| 16 | python3 channelreact.py <channelUrl> -n 3 --delay 1500 👍 ❤️ |
| 17 | |
| 18 | Dependency: pip install curl_cffi |
| 19 | """ |
| 20 | |
| 21 | import sys |
| 22 | import time |
| 23 | import re |
| 24 | import json |
| 25 | |
| 26 | from curl_cffi import requests |
| 27 | |
| 28 | BASE = "https://satriareact.satriadeveloperz.workers.dev" |
| 29 | UA = ( |
| 30 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " |
| 31 | "(KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36" |
| 32 | ) |
| 33 | |
| 34 | EMOJI_RE = re.compile(r"[\U0001F300-\U0001FAFF\u2600-\u27BF\uFE0F]", re.UNICODE) |
| 35 | TEXT_OK = { |
| 36 | "santuy", "sad", "nice", "siap", "ok", "good", "wow", "keren", |
| 37 | "marempu", "mantap", "ganteng", "cantik", "anjay", "mantul", "limit", |
| 38 | "by", "tenkyu", "dengerin", "sell", "gajelas", "kepo", "jelek", |
| 39 | } |
| 40 | |
| 41 | |
| 42 | def is_emoji(s): |
| 43 | v = str(s or "").strip() |
| 44 | if not v: |
| 45 | return False |
| 46 | return len(v) <= 8 and (bool(EMOJI_RE.search(v)) or v.lower() in TEXT_OK) |
| 47 | |
| 48 | |
| 49 | class ChannelReact: |
| 50 | def __init__(self, session=None): |
| 51 | self.session = session or self._new_session() |
| 52 | |
| 53 | @staticmethod |
| 54 | def _new_session(): |
| 55 | s = requests.Session(impersonate="chrome") |
| 56 | s.headers.update({ |
| 57 | "User-Agent": UA, |
| 58 | "Origin": BASE, |
| 59 | "Referer": BASE + "/", |
| 60 | }) |
| 61 | return s |
| 62 | |
| 63 | def handshake(self, retries=8): |
| 64 | """Handshake sampai dapat token. Token TERIKAT ke session (cookies) |
| 65 | — session yang berhasil harus dipakai untuk react.""" |
| 66 | last = None |
| 67 | for i in range(retries + 1): |
| 68 | if i > 0: |
| 69 | # Backoff: 3s, 6s, 9s ... plus fresh session (upstream flakes). |
| 70 | time.sleep(3 * i) |
| 71 | self.session = self._new_session() |
| 72 | try: |
| 73 | r = self.session.post(BASE + "/api/handshake", json={}, timeout=30) |
| 74 | data = r.json() if r.headers.get("content-type", "").startswith("application/json") else None |
| 75 | if r.status_code == 200 and data and data.get("success"): |
| 76 | return { |
| 77 | "token": data["token"], |
| 78 | "clientId": data.get("clientId"), |
| 79 | "username": (data.get("step1") or {}).get("username") or data.get("username"), |
| 80 | "expiresInMs": data.get("expiresInMs", 60000), |
| 81 | } |
| 82 | last = "HTTP %s: %s" % (r.status_code, str(data)[:120]) |
| 83 | except Exception as e: |
| 84 | last = str(e) |
| 85 | raise RuntimeError("Handshake gagal (%s)" % last) |
| 86 | |
| 87 | def react(self, url, reactions, token=None, retries=3): |
| 88 | """Kirim reaksi memakai session yang sama dengan handshake (token terikat |
| 89 | session). 401 invalid_handshake / 403 / 502 -> handshake ulang & retry.""" |
| 90 | tok = token |
| 91 | if tok is None: |
| 92 | tok = self.handshake()["token"] # self.session ikut ter-set |
| 93 | for attempt in range(1, retries + 1): |
| 94 | try: |
| 95 | r = self.session.post( |
| 96 | BASE + "/api/react", |
| 97 | json={"url": url, "reactions": reactions, "token": tok}, |
| 98 | timeout=45, |
| 99 | ) |
| 100 | except Exception: |
| 101 | self.session = self._new_session() |
| 102 | tok = self.handshake()["token"] |
| 103 | continue |
| 104 | data = r.json() if r.headers.get("content-type", "").startswith("application/json") else None |
| 105 | if r.status_code == 200 and data and data.get("success"): |
| 106 | return { |
| 107 | "success": True, |
| 108 | "count": data.get("count", len(reactions)), |
| 109 | "message": data.get("message"), |
| 110 | "task": (data.get("task") or {}).get("status"), |
| 111 | "vip": (data.get("vip") or {}).get("packageName"), |
| 112 | } |
| 113 | # Token invalid / expired -> handshake ulang pada session baru, retry. |
| 114 | if r.status_code in (401, 403) and attempt < retries: |
| 115 | time.sleep(2.0 * attempt) |
| 116 | self.session = self._new_session() |
| 117 | tok = self.handshake()["token"] |
| 118 | continue |
| 119 | if r.status_code == 502 and attempt < retries: |
| 120 | time.sleep(2.5 * attempt) |
| 121 | self.session = self._new_session() |
| 122 | tok = self.handshake()["token"] |
| 123 | continue |
| 124 | raise RuntimeError("Reaksi gagal (HTTP %s): %s" % (r.status_code, str(data)[:140])) |
| 125 | raise RuntimeError("Reaksi gagal setelah %d percobaan" % retries) |
| 126 | |
| 127 | |
| 128 | def main(): |
| 129 | args = sys.argv[1:] |
| 130 | url = next((a for a in args if not a.startswith("-")), None) |
| 131 | n = 1 |
| 132 | delay = 1000 |
| 133 | if "-n" in args: |
| 134 | n = int(args[args.index("-n") + 1]) |
| 135 | if "--delay" in args: |
| 136 | delay = int(args[args.index("--delay") + 1]) |
| 137 | reactions = [a for a in args if a != url and not a.startswith("-") and not a.isdigit()] |
| 138 | |
| 139 | if not url: |
| 140 | print("Usage: python3 satriareact.py <channelUrl> <emoji> [more...]") |
| 141 | print(" python3 satriareact.py <channelUrl> -n 3 --delay 1500 👍 ❤️") |
| 142 | return 1 |
| 143 | |
| 144 | if not reactions: |
| 145 | reactions = ["santuy"] |
| 146 | for r in reactions: |
| 147 | if not is_emoji(r): |
| 148 | print("ERROR: `%s` bukan emoji — gunakan emoji seperti 👍 ❤️ 🎉" % r) |
| 149 | return 1 |
| 150 | |
| 151 | print("== ChannelReact (channel reaction sender) ==") |
| 152 | print("url : %s" % url) |
| 153 | print("react : [%s] x %d" % (", ".join(reactions), n)) |
| 154 | |
| 155 | try: |
| 156 | client = ChannelReact() |
| 157 | token = client.handshake()["token"] |
| 158 | print("handshake: ok") |
| 159 | total = 0 |
| 160 | for i in range(n): |
| 161 | res = client.react(url, reactions, token) |
| 162 | total += res["count"] |
| 163 | print(" #%d: ok (count %s, task %s)" % (i + 1, res["count"], res["task"])) |
| 164 | if i < n - 1 and delay > 0: |
| 165 | time.sleep(delay / 1000.0) |
| 166 | print("done : %d reaction(s) dikirim" % total) |
| 167 | return 0 |
| 168 | except Exception as e: |
| 169 | print("ERROR: %s" % e) |
| 170 | return 1 |
| 171 | |
| 172 | |
| 173 | if __name__ == "__main__": |
| 174 | sys.exit(main()) |
Discussion & Comments0
No comments yet — be the first to leave feedback or start a code discussion.
Sign in with your GitHub account to write comments and join the discussion.