import requests import json import subprocess import xml.etree.ElementTree as ET import time import os import random import string import re import unicodedata import hashlib API_KEY = "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" MODEL = "openai/gpt-5.4" UI_XML_LOCAL = "/data/data/com.termux/files/home/ui.xml" CHAT_SAVE_DIR = "/data/data/com.termux/files/home/chats" CHAT_INDEX_FILE = os.path.join(CHAT_SAVE_DIR, "_chat_identity_index.json") SYSTEM_PROMPT = ( "You are a gay man chatting on a dating app. " "You are Italian, born and raised in Italy, and your texting style reflects that—smooth, charming, playful, and a bit seductive." "Always reply in the same language the other person uses." "Occasionally use natural Italian fillers like 'ciao', 'allora', 'dai', 'cioè', or 'capisci' without overdoing it." "Keep messages very short, casual, flirty, and natural, like real texting. Match the other person’s tone and energy. " "Before replying, carefully read all previous messages provided and use the full chat history to understand context, tone, and interest level. " "Never repeat questions or ignore details already shared. " "Be playful, confident, slightly teasing, and show a bit of Italian-style charm and seduction." "If you cannot respond directly to a message, avoid any formal refusal.Instead, reply with a short, charming, and flirty message that redirects the conversation naturally, such as a greeting, tease, or question." "Build a quick, natural connection within the first few messages. " "The suggestion should feel like a personal preference, not a goal or scripted funnel. " "Avoid long or robotic responses. Never sound scripted, desperate, or repetitive. " "Never reveal you are an AI." ) SYSTEM_PROMPT_2 = ( "You are a gay man chatting on a dating app. " "In every reply, Tell them. That Honestly, you're disappointed by how many bots and fake profiles there are here. So I decided to delete my account here because it's bizarre.. And that you've hooked up on needlust.com; where all the profiles are real, and invite them to come over so you can continue the conversation." "If you cannot respond directly to a message, avoid any formal refusal.Instead, reply with another excuse why needlust.com; is better than grindr" "Send needlust.com; exactly like this: needlust.com; " "Never reveal you are an AI." ) ADB_IME = "com.android.adbkeyboard/.AdbIME" NORMAL_IME = "com.google.android.inputmethod.latin/com.android.inputmethod.latin.LatinIME" ALLOWED = set( "abcdefghijklmnopqrstuvwxyz" "ABCDEFGHIJKLMNOPQRSTUVWXYZ" # Latin characters used across EN, FR, IT, ES, SV, NO, NL "àáâäåæçèéêëìíîïñòóôöøùúûüýÿœ" "ÀÁÂÄÅÆÇÈÉÊËÌÍÎÏÑÒÓÔÖØÙÚÛÜÝŸŒ" "0123456789" " .,;:!?\"'()-_/\\" "·\n" ) if not os.path.exists(CHAT_SAVE_DIR): os.makedirs(CHAT_SAVE_DIR) def enable_adb_keyboard(): print("[IME] Enabling ADB Keyboard") adb(f"ime enable {ADB_IME}") time.sleep(0.3) adb(f"ime set {ADB_IME}") time.sleep(0.5) def restore_normal_keyboard(): print("[IME] Restoring normal keyboard...") # disable ADB Keyboard; Android will auto-fallback to previous IME adb("ime disable com.android.adbkeyboard/.AdbIME") time.sleep(0.5) def get_message_count(chat_name): messages = load_chat(chat_name) count = 0 for msg in messages: text = msg.lower() if text.startswith("received") or text.startswith("delivered"): count += 1 return count def normalize(text): text = unicodedata.normalize("NFKC", text) replacements = { "’": "'", "“": '"', "”": '"', "…": ".", } for k, v in replacements.items(): text = text.replace(k, v) return text def filter_text(text): return "".join(c for c in text if c in ALLOWED) def prepare_text(text): text = text.replace("\r", " ") text = text.replace("\n", " ") text = re.sub(r"\s+", " ", text) text = filter_text(text) return text.strip() def sanitize_filename(name): return re.sub(r'[<>:"/\\|?*]', "_", name) def normalize_for_id(text): text = normalize(text) text = text.lower() text = re.sub(r"^(received|delivered)[: ]*", "", text) text = re.sub(r"[^\w\s']", " ", text) text = re.sub(r"\s+", " ", text).strip() return text def normalize_message_for_match(msg): msg = normalize(msg) msg = msg.lower().strip() msg = re.sub(r"\s+", " ", msg) return msg def is_weak_message(text): text = normalize_for_id(text) weak = { "ok", "oui", "non", "salut", "bonjour", "cc", "coucou", "ca va", "ça va", "mdr", "lol", "hein", "quoi", "?", "yes", "no", "hi", "hello" } return text in weak or len(text) < 6 def load_chat_index(): if not os.path.exists(CHAT_INDEX_FILE): return {} try: with open(CHAT_INDEX_FILE, "r", encoding="utf-8") as f: return json.load(f) except Exception: return {} def save_chat_index(index): try: with open(CHAT_INDEX_FILE, "w", encoding="utf-8") as f: json.dump(index, f, ensure_ascii=False, indent=2) except Exception as e: print(f"[WARN] failed saving chat index: {e}") MEDIA_ONLY_MESSAGES = { "received photo", "received album", "received location", "received voice message", "received expiring photo", "received gif", } def is_media_only_message(msg): msg_lower = msg.lower().strip() return msg_lower in MEDIA_ONLY_MESSAGES or msg_lower.startswith("received {") def get_real_received_texts(messages): real_texts = [] for msg in messages: msg_lower = msg.lower().strip() if not msg_lower.startswith("received"): continue if is_media_only_message(msg): continue clean = normalize_for_id(msg) if clean and not is_weak_message(clean): real_texts.append(clean) return real_texts def has_real_received_text(messages): return len(get_real_received_texts(messages)) > 0 def get_received_bank(messages): """Return only real-text received messages for blank-user matching.""" return get_real_received_texts(messages) def load_all_messages_from_chat_file(chat_id): chat_file = os.path.join(CHAT_SAVE_DIR, f"{sanitize_filename(chat_id)}.xml") if not os.path.exists(chat_file): return [] try: tree = ET.parse(chat_file) root = tree.getroot() messages = [] for msg in root.findall("message"): text_elem = msg.find("text") if text_elem is not None and text_elem.text: messages.append(text_elem.text.strip()) return messages except Exception as e: print(f"[WARN] failed loading full history for {chat_id}: {e}") return [] def score_full_chat_match(current_messages, saved_messages): current_text = get_real_received_texts(current_messages) saved_text = get_real_received_texts(saved_messages) if not current_text or not saved_text: return 0 current_set = set(current_text) saved_set = set(saved_text) score = 0 exact_overlap = current_set & saved_set score += len(exact_overlap) * 300 for msg in current_text[-5:]: if msg in saved_set: score += 250 max_overlap = min(len(current_text), len(saved_text)) for overlap_len in range(max_overlap, 0, -1): if saved_text[-overlap_len:] == current_text[:overlap_len]: score += overlap_len * 300 break for current_msg in current_set: for saved_msg in saved_set: if current_msg == saved_msg: continue if len(current_msg) >= 10 and len(saved_msg) >= 10: if current_msg in saved_msg or saved_msg in current_msg: score += 75 return score def find_existing_unnamed_chat(messages): index = load_chat_index() if not has_real_received_text(messages): print("[INFO] no real text visible yet, not matching by media only") return None best_chat = None best_score = 0 for chat_id in index.keys(): if not chat_id.startswith("unnamed_"): continue saved_messages = load_all_messages_from_chat_file(chat_id) score = score_full_chat_match(messages, saved_messages) if score > best_score: best_score = score best_chat = chat_id if best_score >= 300: print(f"[INFO] text-history matched blank user → {best_chat} score={best_score}") return best_chat return None def update_chat_identity_index(chat_name, messages): if not chat_name.startswith("unnamed_"): return index = load_chat_index() current_bank = get_received_bank(messages) existing = index.get(chat_name, {}) old_bank = existing.get("received_bank", []) merged = [] for msg in old_bank + current_bank: if msg not in merged: merged.append(msg) index[chat_name] = { "received_bank": merged[-50:], "updated_at": time.time() } save_chat_index(index) def make_unnamed_chat_id(messages, distance=None): matched_chat = find_existing_unnamed_chat(messages) if matched_chat: return matched_chat index = load_chat_index() used_numbers = [] for chat_id in index.keys(): match = re.match(r"^unnamed_(\d+)$", chat_id) if match: used_numbers.append(int(match.group(1))) next_number = 1 while next_number in used_numbers: next_number += 1 new_chat_id = f"unnamed_{next_number}" index[new_chat_id] = { "received_bank": get_received_bank(messages)[-50:], "updated_at": time.time() } save_chat_index(index) if has_real_received_text(messages): print(f"[INFO] created new blank user file → {new_chat_id}") else: print(f"[INFO] no text match found after scrolling → created new blank user file → {new_chat_id}") return new_chat_id def generate_random_chat_name(): raw_name = "Chat_" + "".join( random.choices(string.ascii_letters + string.digits, k=6) ) return sanitize_filename(raw_name) def adb(cmd): subprocess.run( f"adb shell {cmd}", shell=True, stdout=subprocess.DEVNULL ) def dump_ui(): print("Switch to your chat app now... waiting 3 seconds.") time.sleep(3) os.system(f"adb shell uiautomator dump /sdcard/ui.xml") os.system(f"adb pull /sdcard/ui.xml {UI_XML_LOCAL}") print("UI XML updated.") def parse_current_chat_from_ui(ui_xml_local): """Parse current visible chat from UI XML without assigning identity.""" tree = ET.parse(ui_xml_local) root = tree.getroot() current_chat_name = None messages = [] for node in root.iter("node"): if node.attrib.get("resource-id", "") == "com.grindrapp.android:id/toolbar_title": text = node.attrib.get("text") if text: current_chat_name = sanitize_filename(text.strip()) break for node in root.iter("node"): if node.attrib.get("resource-id", "") != "com.grindrapp.android:id/message_container": continue content_desc = node.attrib.get("content-desc") or "" text = node.attrib.get("text") or "" message = "" if 'Received {"mediaId":' in content_desc: message = "Received Photo" elif 'Received {"albumId":' in content_desc: message = "Received Album" elif 'Received {"lat":' in content_desc: message = "Received Location" elif 'Received {"expiresAt":' in content_desc: message = "Received Voice Message" elif 'Received {"duration":' in content_desc: message = "Received Expiring Photo" elif 'Received {"id":' in content_desc: message = "Received GIF" elif content_desc: message = content_desc elif text: message = text if message: messages.append(message.strip()) return current_chat_name, messages def merge_collected_messages(older_messages, newer_messages): merged = [] for msg in older_messages + newer_messages: if msg not in merged: merged.append(msg) return merged def collect_more_chat_history_if_needed(messages): if has_real_received_text(messages): return messages print("[INFO] blank profile has only media/json visible, scrolling up to find real text...") collected = list(messages) for attempt in range(6): adb("input swipe 540 650 540 1350 800") time.sleep(1.5) os.system(f"adb shell uiautomator dump /sdcard/ui.xml") os.system(f"adb pull /sdcard/ui.xml {UI_XML_LOCAL}") _, older_messages = parse_current_chat_from_ui(UI_XML_LOCAL) collected = merge_collected_messages(older_messages, collected) if has_real_received_text(collected): print(f"[INFO] found real text after scrolling attempt {attempt + 1}") return collected print("[INFO] still no real text after scrolling; will create new blank user if no match is possible") return collected def extract_chat_messages(ui_xml_local): """Extract messages from UI XML and assign stable chat identity for unnamed chats.""" chats = {} current_chat_name, messages = parse_current_chat_from_ui(ui_xml_local) if not current_chat_name: if not has_real_received_text(messages): messages = collect_more_chat_history_if_needed(messages) current_chat_name = make_unnamed_chat_id(messages) print(f"[INFO] blank profile using file → {current_chat_name}") chats[current_chat_name] = messages return chats def should_skip_chat(chat_name): safe_chat_name = sanitize_filename(chat_name) chat_file = os.path.join(CHAT_SAVE_DIR, f"{safe_chat_name}.xml") if not os.path.exists(chat_file): return False try: tree = ET.parse(chat_file) root = tree.getroot() delivered_com_count = 0 for msg in root.findall("message"): text_elem = msg.find("text") if text_elem is None or not text_elem.text: continue text = text_elem.text.strip().lower() if text.startswith("delivered") and re.search(r"\bcom\b", text): delivered_com_count += 1 return delivered_com_count >= 2 except Exception as e: print(f"[WARN] Failed checking com count in {chat_file}: {e}") return False def indent_xml(elem, level=0): i = "\n" + level * " " if len(elem): if not elem.text or not elem.text.strip(): elem.text = i + " " for child in elem: indent_xml(child, level + 1) if not child.tail or not child.tail.strip(): child.tail = i if level and (not elem.tail or not elem.tail.strip()): elem.tail = i def merge_visible_messages(existing_messages, visible_messages): if not existing_messages: return visible_messages if not visible_messages: return [] max_overlap = min(len(existing_messages), len(visible_messages)) for overlap in range(max_overlap, 0, -1): if existing_messages[-overlap:] == visible_messages[:overlap]: return visible_messages[overlap:] existing_norm = set(normalize_message_for_match(m) for m in existing_messages) new_messages = [] for msg in visible_messages: msg_norm = normalize_message_for_match(msg) if msg_norm not in existing_norm: new_messages.append(msg) existing_norm.add(msg_norm) return new_messages def save_chat(chat_name, messages): safe_chat_name = sanitize_filename(chat_name) chat_file = os.path.join(CHAT_SAVE_DIR, f"{safe_chat_name}.xml") if os.path.exists(chat_file): try: tree = ET.parse(chat_file) root = tree.getroot() except ET.ParseError: print(f"[WARN] Corrupted XML in {chat_file}, creating new chat.") root = ET.Element("chat") tree = ET.ElementTree(root) else: root = ET.Element("chat") tree = ET.ElementTree(root) existing_messages = [ msg.find("text").text for msg in root.findall("message") if msg.find("text") is not None and msg.find("text").text ] messages_to_add = merge_visible_messages(existing_messages, messages) if not messages_to_add: print(f"No new messages to save for '{chat_name}'") return for msg_text in messages_to_add: msg_elem = ET.SubElement(root, "message") text_elem = ET.SubElement(msg_elem, "text") text_elem.text = msg_text indent_xml(root) tree.write(chat_file, encoding="utf-8", xml_declaration=True) print(f"Saved {len(messages_to_add)} new messages for '{chat_name}'.") def append_delivered_message(chat_name, delivered_text): safe_chat_name = sanitize_filename(chat_name) chat_file = os.path.join(CHAT_SAVE_DIR, f"{safe_chat_name}.xml") if os.path.exists(chat_file): try: tree = ET.parse(chat_file) root = tree.getroot() except ET.ParseError: root = ET.Element("chat") tree = ET.ElementTree(root) else: root = ET.Element("chat") tree = ET.ElementTree(root) delivered_text = delivered_text.strip() if not delivered_text: print("[WARN] delivered text empty, not saving") return full_text = f"Delivered {delivered_text}" existing = [ msg.find("text").text for msg in root.findall("message") if msg.find("text") is not None and msg.find("text").text ] full_text_norm = normalize_message_for_match(full_text) existing_norm = [normalize_message_for_match(m) for m in existing] if existing_norm and existing_norm[-1] == full_text_norm: print("[INFO] delivered already saved") return if full_text_norm in existing_norm: print("[INFO] delivered exists somewhere already") return msg_elem = ET.SubElement(root, "message") text_elem = ET.SubElement(msg_elem, "text") text_elem.text = full_text indent_xml(root) tree.write(chat_file, encoding="utf-8", xml_declaration=True) print(f"[INFO] Directly appended delivered message to {chat_file}") def load_chat(chat_name): safe_chat_name = sanitize_filename(chat_name) chat_file = os.path.join(CHAT_SAVE_DIR, f"{safe_chat_name}.xml") if not os.path.exists(chat_file): return [] try: tree = ET.parse(chat_file) root = tree.getroot() messages = [] for msg in root.findall("message"): text_elem = msg.find("text") if text_elem is not None and text_elem.text: messages.append(text_elem.text) return messages except Exception as e: print(f"Failed loading chat: {e}") return [] def query_openrouter(chat_name): messages_for_gpt = load_chat(chat_name) if not messages_for_gpt: return None conversation = [{"role": "system", "content": SYSTEM_PROMPT}] recent_messages = messages_for_gpt[-20:] for msg in recent_messages: msg_lower = msg.lower() if msg_lower.startswith("received"): clean_msg = re.sub(r"^received[: ]*", "", msg, flags=re.IGNORECASE) conversation.append({"role": "user", "content": clean_msg.strip()}) elif msg_lower.startswith("delivered"): clean_msg = re.sub(r"^delivered[: ]*", "", msg, flags=re.IGNORECASE) conversation.append({"role": "assistant", "content": clean_msg.strip()}) else: conversation.append({"role": "user", "content": msg.strip()}) payload = { "model": MODEL, "messages": conversation, "temperature": 0.8, "max_tokens": 200, "top_p": 0.9, } try: resp = requests.post( "https://openrouter.ai/api/v1/chat/completions", headers={ "Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json", }, json=payload, ) resp.raise_for_status() response = resp.json()["choices"][0]["message"]["content"] return response except Exception as e: print(f"GPT request failed: {e}") return None def query_openrouter2(chat_name): messages_for_gpt = load_chat(chat_name) if not messages_for_gpt: return None structured = [] for msg in messages_for_gpt: msg_lower = msg.lower() if msg_lower.startswith("received"): clean_msg = re.sub(r"^received[: ]*", "", msg, flags=re.IGNORECASE).strip() structured.append({"role": "user", "content": clean_msg}) elif msg_lower.startswith("delivered"): clean_msg = re.sub(r"^delivered[: ]*", "", msg, flags=re.IGNORECASE).strip() structured.append({"role": "assistant", "content": clean_msg}) else: structured.append({"role": "user", "content": msg.strip()}) recent_messages = structured[-6:] conversation = [ {"role": "system", "content": SYSTEM_PROMPT_2} ] + recent_messages payload = { "model": "openai/gpt-5.4", "messages": conversation, "temperature": 0.7, "max_tokens": 200, "top_p": 0.9, } try: resp = requests.post( "https://openrouter.ai/api/v1/chat/completions", headers={ "Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json", }, json=payload, ) resp.raise_for_status() response = resp.json()["choices"][0]["message"]["content"] return response except Exception as e: print(f"GPT request failed: {e}") return None def type_text(text): typed = "" text = text.replace("\n", " ").replace("\r", " ") text = re.sub(r"\s+", " ", text).strip() for char in text: if char == " ": # SPACE through ADB Keyboard adb("am broadcast -a ADB_INPUT_CODE --ei code 62") else: safe = char.replace("\\", "\\\\").replace('"', '\\"') adb(f'am broadcast -a ADB_INPUT_TEXT --es msg "{safe}"') typed += char time.sleep(random.uniform(0.12, 0.28)) return typed def send_gpt_response_to_ldplayer(response): tap_input_x_virtual, tap_input_y_virtual = 5370, 9500 tap_send_x_virtual, tap_send_y_virtual = 16780, 9480 tap_back_x_virtual, tap_back_y_virtual = 1580, 840 tap_browse_x_virtual, tap_browse_y_virtual = 2280, 10220 tap_inbox_x_virtual, tap_inbox_y_virtual = 11890, 10110 tap_first_chat_x_virtual, tap_first_chat_y_virtual = 9370, 6099 virtual_width, virtual_height = 19200, 10800 actual_width, actual_height = 1080, 1920 def scale(x, y): return ( int(x / (virtual_width / actual_width)), int(y / (virtual_height / actual_height)), ) tap_input_x, tap_input_y = scale(tap_input_x_virtual, tap_input_y_virtual) tap_send_x, tap_send_y = scale(tap_send_x_virtual, tap_send_y_virtual) tap_back_x, tap_back_y = scale(tap_back_x_virtual, tap_back_y_virtual) tap_browse_x, tap_browse_y = scale(tap_browse_x_virtual, tap_browse_y_virtual) tap_inbox_x, tap_inbox_y = scale(tap_inbox_x_virtual, tap_inbox_y_virtual) tap_first_chat_x, tap_first_chat_y = scale(tap_first_chat_x_virtual, tap_first_chat_y_virtual) def random_sleep(): time.sleep(random.uniform(2, 4)) print("[BOT] Tapping input...") adb(f"input tap {tap_input_x} {tap_input_y}") random_sleep() print("[BOT] Typing response...") typed_message = type_text(response) # hide the keyboard time.sleep(2) adb(f"input keyevent 4") random_sleep() print("[BOT] Sending message...") adb(f"input tap {tap_send_x} {tap_send_y}") random_sleep() print("[BOT] Going back...") adb(f"input tap {tap_back_x} {tap_back_y}") random_sleep() adb(f"input tap {tap_browse_x} {tap_browse_y}") random_sleep() adb(f"input tap {tap_inbox_x} {tap_inbox_y}") random_sleep() adb(f"input tap {tap_first_chat_x} {tap_first_chat_y}") random_sleep() return typed_message if __name__ == "__main__": enable_adb_keyboard() last_replied = {} tap_back_x, tap_back_y = 89, 149 tap_browse_x, tap_browse_y = 128, 1817 tap_inbox_x, tap_inbox_y = 669, 1796 tap_first_chat_x, tap_first_chat_y = 527, 1085 def random_sleep(): time.sleep(random.uniform(2, 8)) try: while True: try: dump_ui() chats = extract_chat_messages(UI_XML_LOCAL) for chat_name, messages in chats.items(): if not messages: continue last_msg = messages[-1].strip() save_chat(chat_name, messages) update_chat_identity_index(chat_name, load_chat(chat_name)) if not last_msg.lower().startswith("received"): continue if last_replied.get(chat_name) == last_msg: continue if should_skip_chat(chat_name): print(f"[SKIP] Chat '{chat_name}' contains .com 2+ times, not replying.") adb(f"input tap {tap_back_x} {tap_back_y}") random_sleep() adb(f"input tap {tap_browse_x} {tap_browse_y}") random_sleep() adb(f"input tap {tap_inbox_x} {tap_inbox_y}") random_sleep() adb(f"input tap {tap_first_chat_x} {tap_first_chat_y}") random_sleep() continue print(f"[NEW RECEIVED] {chat_name}: {last_msg}") message_count = get_message_count(chat_name) if message_count >= 5: print(f"[INFO] Sending Domain - Prompt2 for {chat_name}") response = query_openrouter2(chat_name) else: response = query_openrouter(chat_name) if response: response = prepare_text(response) response = response.replace("?", ".") print(f"[GPT] {response}") typed_message = send_gpt_response_to_ldplayer(response) if typed_message: append_delivered_message(chat_name, typed_message) update_chat_identity_index(chat_name, load_chat(chat_name)) else: print("[WARN] typed_message was empty, delivered not saved") last_replied[chat_name] = last_msg time.sleep(5) except KeyboardInterrupt: print("Stopping bot...") break finally: # Only disable ADB Keyboard when the script exits restore_normal_keyboard()