Hardening autonomous navigation: Implemented Modal Guards, Transient Drift Protection (WhatsApp fix), and Aggressive Recovery paths. Cleaned up diagnostic artifacts.

This commit is contained in:
2026-04-17 12:57:12 +02:00
parent 89f14463c5
commit 0aeed11186
35 changed files with 1802 additions and 460 deletions

View File

@@ -59,6 +59,12 @@ def start_bot(**kwargs):
# Check for direct execution modes that bypass normal bot state
configs.parse_args()
try:
from GramAddict.core.llm_provider import prewarm_ollama_models
prewarm_ollama_models(configs)
except Exception as e:
logger.debug(f"Prewarm failed: {e}")
if getattr(configs.args, "capture_e2e_dumps", False):
device = create_device(configs.device_id, configs.app_id, configs.args)
from GramAddict.core.dump_capturer import capture_all
@@ -186,12 +192,12 @@ def start_bot(**kwargs):
if success:
if current_target == "ExploreFeed":
logger.info("📱 Opening first explore item from the grid...")
nav_graph._execute_transition("tap_explore_grid_item", zero_engine)
nav_graph._execute_transition("tap_explore_grid_item")
# Wait for post to actually load (poll for feed markers)
_wait_for_post_loaded(device, timeout=5)
elif current_target == "StoriesFeed":
logger.info("📱 Locating story tray on HomeFeed...")
nav_graph._execute_transition("tap_story_tray_item", zero_engine)
nav_graph._execute_transition("tap_story_tray_item")
_wait_for_post_loaded(device, timeout=5)
if current_target == "StoriesFeed":
@@ -256,6 +262,9 @@ FEED_MARKERS = [
"row_feed_profile_header",
"row_feed_photo_imageview",
"clips_media_component",
"clips_video_container",
"clips_viewer_container",
"clips_linear_layout_container"
]
@@ -266,7 +275,7 @@ def _wait_for_post_loaded(device, timeout=5):
start = time.time()
while time.time() - start < timeout:
try:
xml = device.deviceV2.dump_hierarchy()
xml = device.dump_hierarchy()
if any(marker in xml for marker in FEED_MARKERS):
logger.debug("📱 Post loaded successfully.")
return True
@@ -424,14 +433,25 @@ def _interact_with_carousel(device, configs, sleep_mod, logger):
info = device.get_info()
w, h = info.get("displayWidth", 1080), info.get("displayHeight", 2400)
# Curiosity Peak: One slide in the carousel gets extra attention
curiosity_slide = random.randint(0, count - 1) if count > 0 else 0
for i in range(count):
# Normal transition wait
sleep(random.uniform(1.5, 3.5) * sleep_mod)
# ── Curiosity Dwell ──
if i == curiosity_slide:
dwell = random.uniform(3.0, 7.0)
logger.debug(f"📸 [Carousel] Curiosity Peak hit on slide {i+1}. Gazing for {dwell:.1f}s...")
sleep(dwell * sleep_mod)
# Horizontal swipe inside the post bounds (approx middle): Right to left
_humanized_horizontal_swipe(device, start_x=w*0.8, end_x=w*0.2, y=h*0.5, duration_ms=250)
sleep(random.uniform(1.0, 2.0) * sleep_mod)
def _interact_with_profile(device, configs, username, session_state, sleep_mod, logger):
print("HELLO IM NOT MOCKED!")
"""Deep interaction on a profile: Stories, Grid Likes, Follows"""
import random
@@ -442,11 +462,26 @@ def _interact_with_profile(device, configs, username, session_state, sleep_mod,
info = device.get_info()
w, h = info.get("displayWidth", 1080), info.get("displayHeight", 2400)
xml_check = device.dump_hierarchy()
if not isinstance(xml_check, str):
return
xml_check_lower = xml_check.lower()
# ── 1. Profile Guards (Private / Empty) ──
if "this account is private" in xml_check_lower or "konto ist privat" in xml_check_lower:
logger.info(f"🔒 [Profile Guard] @{username} is private. Aborting deep interaction.", extra={"color": f"{Fore.YELLOW}"})
return
if "no posts yet" in xml_check_lower or "noch keine beiträge" in xml_check_lower:
logger.info(f"📭 [Profile Guard] @{username} has no posts. Aborting deep interaction.", extra={"color": f"{Fore.YELLOW}"})
return
# Profile Scraping (Phase 11: Data Extraction)
if getattr(configs.args, "scrape_profiles", False):
try:
logger.info(f"📊 [Scraping] Extracting metadata for @{username}...", extra={"color": f"{Fore.CYAN}"})
xml_dump = device.deviceV2.dump_hierarchy()
xml_dump = device.dump_hierarchy()
telepathic = TelepathicEngine.get_instance()
crm = cognitive_stack.get("crm") if 'cognitive_stack' in locals() else None
@@ -483,7 +518,9 @@ def _interact_with_profile(device, configs, username, session_state, sleep_mod,
from GramAddict.core.q_nav_graph import QNavGraph
nav_graph = QNavGraph(device)
if nav_graph._execute_transition("tap_story_tray_item"):
xml_dump = device.dump_hierarchy()
has_story = "reel_ring" in xml_dump or "'s unseen story" in xml_dump.lower() or "has a new story" in xml_dump.lower() or "story von" in xml_dump.lower()
if has_story and nav_graph._execute_transition("tap_story_tray_item"):
logger.info(f"📸 [Story] Viewing @{username}'s story ({count} times)...")
for i in range(count):
sleep(random.uniform(2.0, 5.0) * sleep_mod)
@@ -530,13 +567,46 @@ def _interact_with_profile(device, configs, username, session_state, sleep_mod,
logger.info(f"❤️ [Deep Interaction] Opening grid to drop {count} likes on @{username}...")
for i in range(count):
_humanized_click(device, w // 2, h // 2, double=True, sleep_mod=sleep_mod)
session_state.totalLikes += 1
logger.debug(f"Liked grid post {i+1}/{count}")
xml_dump = device.dump_hierarchy()
if not isinstance(xml_dump, str):
xml_dump = ""
xml_dump_lower = xml_dump.lower()
is_reel = "reel_viewer" in xml_dump_lower or "clips_viewer" in xml_dump_lower
is_liked = "gefällt mir nicht mehr" in xml_dump_lower or "unlike" in xml_dump_lower or 'content-desc="liked"' in xml_dump_lower
# Uset Double-Tap ~40% of the time, only on standard images
use_double_tap = random.random() < 0.4 and not is_reel
if use_double_tap:
if is_liked:
logger.debug(f"Skipped liking grid post {i+1}/{count} (already liked)")
else:
offset_x = random.randint(int(w * 0.2), int(w * 0.8))
offset_y = random.randint(int(h * 0.3), int(h * 0.7))
logger.info(f"❤️ [Interaction] Double-Tapping organically at ({offset_x}, {offset_y})")
_humanized_click(device, offset_x, offset_y, double=True, sleep_mod=sleep_mod)
session_state.totalLikes += 1
logger.debug(f"Liked grid post {i+1}/{count} via Double-Tap")
else:
if nav_graph._execute_transition("tap_like_button"):
session_state.totalLikes += 1
logger.debug(f"Liked grid post {i+1}/{count} via Heart Button")
else:
logger.debug(f"Skipped liking grid post {i+1}/{count} (already liked or failed to find button)")
sleep(random.uniform(1.0, 2.0) * sleep_mod)
start_scroll_y, end_scroll_y = int(h * 0.7) + random.randint(-20, 20), int(h * 0.2) + random.randint(-40, 40)
scroll_x = w // 2 + random.randint(-30, 30)
device.deviceV2.shell(f"input swipe {scroll_x} {start_scroll_y} {scroll_x + random.randint(-15,15)} {end_scroll_y} {random.randint(250, 400)}")
is_reel = "reel_viewer" in xml_dump_lower or "clips_viewer" in xml_dump_lower
if is_reel:
# Full screen swipe for Reels (using humanized fast fling)
logger.debug("🎬 Detected Reel. Swiping full-screen up.")
_humanized_scroll(device, is_skip=True)
else:
# Partial screen swipe for standard posts
_humanized_scroll(device, is_skip=False)
sleep(random.uniform(1.5, 3.0) * sleep_mod)
device.deviceV2.press("back")
@@ -564,7 +634,7 @@ def _align_active_post(device):
while not aligned and attempts < max_attempts:
attempts += 1
try:
xml = device.deviceV2.dump_hierarchy()
xml = device.dump_hierarchy()
clean_xml = re.sub(r'<\?xml.*?\?>', '', xml).strip()
root = ET.fromstring(clean_xml)
@@ -648,7 +718,12 @@ def _detect_ad_structural(context_xml: str) -> bool:
"com.instagram.android:id/universal_cta_description_layout",
"com.instagram.android:id/universal_cta_text",
}
AD_CTA_WORDS = {"install", "learn more", "shop now", "sign up", "mehr dazu", "jetzt einkaufen", "installieren", "registrieren", "anmelden", "download", "herunterladen", "get offer", "abonnieren", "subscribe"}
AD_CTA_WORDS = {
"install", "learn more", "shop now", "sign up", "mehr dazu", "jetzt einkaufen",
"installieren", "registrieren", "anmelden", "download", "herunterladen",
"get offer", "abonnieren", "subscribe", "whatsapp", "nachricht senden",
"send message", "jetzt anrufen", "call now", "contact us", "kontaktieren"
}
try:
root = ET.fromstring(context_xml)
@@ -699,7 +774,7 @@ def _extract_post_content(context_xml: str) -> dict:
desc = node.attrib.get("content-desc", "").strip()
# Username from the post header (ignore commenters/composers)
if "row_feed_photo_profile_name" in res_id and text:
if ("row_feed_photo_profile_name" in res_id or "clips_author_username" in res_id) and text:
if "comment" not in res_id and "composer" not in res_id:
# Prioritize the FIRST valid username found (usually the header)
if not result["username"]:
@@ -767,7 +842,7 @@ def _run_zero_latency_stories_loop(device, configs, session_state, cognitive_sta
sleep(random.uniform(1.0, 2.0) * sleep_mod)
return "BOREDOM_CHANGE_FEED"
xml_dump = device.deviceV2.dump_hierarchy()
xml_dump = device.dump_hierarchy()
if not xml_dump:
logger.warning("Failed to dump UI hierarchy in StoriesFeed.")
return "CONTEXT_LOST"
@@ -858,26 +933,42 @@ def _run_zero_latency_feed_loop(device, zero_engine, nav_graph, configs, session
# ── Boredom ──
if random.random() < 0.03 and not is_reels:
logger.info("🥱 [Boredom] Checking something else (Notifications/DMs) for a second...")
# Use NavGraph transitions instead of raw selectors
if random.random() < 0.5:
# Try to visit Notifications
nav_graph._execute_transition("tap_newsfeed_tab", zero_engine)
else:
# Try to visit DMs
nav_graph._execute_transition("tap_message_icon", zero_engine)
if job_target in ["feed", "home", "homefeed"]:
logger.info("🥱 [Boredom] Checking something else (Notifications/DMs) for a second...")
sleep(random.uniform(3.0, 6.0) * sleep_mod)
# Use NavGraph transitions instead of raw selectors
if random.random() < 0.5:
# Try to visit Notifications
nav_graph._execute_transition("tap_newsfeed_tab")
else:
# Try to visit DMs
nav_graph._execute_transition("tap_message_icon")
sleep(random.uniform(3.0, 6.0) * sleep_mod)
# Return to feed natively through robust navigation
nav_graph.navigate_to("HomeFeed", zero_engine)
sleep(random.uniform(1.0, 2.5) * sleep_mod)
# Return to feed natively through robust navigation
nav_graph.navigate_to("HomeFeed", zero_engine)
sleep(random.uniform(1.0, 2.5) * sleep_mod)
context_xml = device.deviceV2.dump_hierarchy()
context_xml = device.dump_hierarchy()
if cognitive_stack.get("radome"):
context_xml = cognitive_stack.get("radome").sanitize_xml(context_xml)
# ── PRE-EMPTIVE AD SKIP (Fast Path) ──
if _detect_ad_structural(context_xml):
consecutive_ads += 1
if consecutive_ads >= 3:
logger.warning("📺 [Anti-Stuck] Stuck on ad! Executing aggressive skip.", extra={"color": f"{Fore.RED}"})
_humanized_scroll(device, is_skip=True)
sleep(2.0)
else:
logger.info("📺 fast-skipping ad (no AI needed)...")
_humanized_scroll(device, is_skip=True)
sleep(random.uniform(0.5, 1.0) * sleep_mod)
continue
consecutive_ads = 0
# ── Zero-Node Recovery (Graceful Degradation) ──
telepathic = TelepathicEngine.get_instance()
interactive_nodes = telepathic._extract_semantic_nodes(context_xml)
@@ -923,7 +1014,7 @@ def _run_zero_latency_feed_loop(device, zero_engine, nav_graph, configs, session
sleep(2.5)
# Verification: Check if markers are now visible
post_recovery_xml = device.deviceV2.dump_hierarchy()
post_recovery_xml = device.dump_hierarchy()
if any(marker in post_recovery_xml for marker in FEED_MARKERS):
logger.info("✅ [Recovery] Obstacle cleared successfully. Learning this button works.")
telepathic.confirm_click("Dismiss Obstacle/Modal")
@@ -956,28 +1047,10 @@ def _run_zero_latency_feed_loop(device, zero_engine, nav_graph, configs, session
# Fixes the issue where UI gets stuck halfway between two posts.
if _align_active_post(device):
# Update context_xml because the screen just shifted
context_xml = device.deviceV2.dump_hierarchy()
context_xml = device.dump_hierarchy()
if cognitive_stack.get("radome"):
context_xml = cognitive_stack.get("radome").sanitize_xml(context_xml)
# ── Structural Ad Detection (Language-Agnostic) ──
if _detect_ad_structural(context_xml):
consecutive_ads += 1
if consecutive_ads >= 3:
logger.warning("📺 [Anti-Stuck] Stuck on ad! Executing aggressive mechanical drag.", extra={"color": f"{Fore.RED}"})
# Fast drag from bottom to top (~0.15s) to guarantee Native Android Fling event for Reels
info = device.get_info()
w, h = info.get("displayWidth", 1080), info.get("displayHeight", 2400)
device.deviceV2.shell(f"input swipe {w // 2} {int(h * 0.8)} {w // 2} {int(h * 0.2)} 150")
sleep(2.0)
else:
logger.info("📺 skipping ad (structural match)...")
_humanized_scroll(device, is_skip=True)
sleep(random.uniform(0.5, 1.5) * sleep_mod)
continue
consecutive_ads = 0
# ── Content Extraction (The Bot's Eyes) ──
post_data = _extract_post_content(context_xml)
@@ -1014,7 +1087,14 @@ def _run_zero_latency_feed_loop(device, zero_engine, nav_graph, configs, session
})
# ── Human-like Selective Skipping (Anti-Bot Drip) ──
skip_prob = 0.85 if res_score < 0.35 else 0.45 if res_score < 0.70 else 0.10
base_skip_prob = 0.85 if res_score < 0.35 else 0.45 if res_score < 0.70 else 0.10
# User defined interact_percentage modulates the skip rate.
# Default is 80%, so factor = 1.0. If 100%, factor = 0.0 (never skip).
interact_pct_val = float(getattr(configs.args, "interact_percentage", 80)) / 100.0
skip_factor = max(0.0, (1.0 - interact_pct_val) * 5.0)
skip_prob = base_skip_prob * skip_factor
if random.random() < skip_prob:
logger.info(f"⏭️ [Resonance Skip] Human-like selective engagement ({skip_prob*100:.0f}% chance). Skipping post.")
session_outcomes.append({
@@ -1029,7 +1109,7 @@ def _run_zero_latency_feed_loop(device, zero_engine, nav_graph, configs, session
# ── The Rabbit Hole (Deep Dive into high-resonance profiles) ──
if res_score >= 0.9 and random.random() < 0.4:
logger.info("💥 [Rabbit Hole] Extreme resonance! Sidetracking into user profile...", extra={"color": f"{Fore.MAGENTA}"})
if nav_graph._execute_transition("tap_post_username", zero_engine) is True:
if nav_graph._execute_transition("tap_post_username") is True:
sleep(random.uniform(1.2, 2.5) * sleep_mod)
_humanized_scroll(device, is_skip=True)
sleep(random.uniform(0.5, 1.5) * sleep_mod)
@@ -1044,6 +1124,7 @@ def _run_zero_latency_feed_loop(device, zero_engine, nav_graph, configs, session
darwin.execute_proof_of_resonance(
device,
res_score,
text_length=len(post_data.get("description", "")),
nav_graph=nav_graph,
zero_engine=zero_engine,
configs=configs,
@@ -1063,11 +1144,17 @@ def _run_zero_latency_feed_loop(device, zero_engine, nav_graph, configs, session
profile_context = ""
# ── Profile Learning (Before heavy engagement) ──
target_user = post_data.get('username', 'target')
if res_score >= 0.8:
# Pull follow chance early to see if the user explicitly wants high follow rates
follow_chance_val = float(getattr(configs.args, "follow_percentage", 0)) / 100.0
# If the user sets follow > 0, we must visit the profile to have a chance to follow.
# Otherwise, we rely entirely on the extreme resonance heuristic (> 0.8).
if res_score >= 0.8 or (follow_chance_val > 0.0 and random.random() < follow_chance_val):
logger.info(f"🕵️‍♂️ [Profile Learning] Highly resonant post ({res_score:.2f}). Visiting @{target_user}'s profile to learn context...", extra={"color": f"{Fore.CYAN}"})
# Navigate to profile
if nav_graph._execute_transition("tap_post_username", zero_engine) is True:
if nav_graph._execute_transition("tap_post_username") is True:
sleep(random.uniform(1.2, 2.5) * sleep_mod)
# Extract context
@@ -1075,7 +1162,7 @@ def _run_zero_latency_feed_loop(device, zero_engine, nav_graph, configs, session
telepathic = cognitive_stack.get("telepathic")
crm = cognitive_stack.get("crm")
if telepathic:
xml_dump = device.deviceV2.dump_hierarchy()
xml_dump = device.dump_hierarchy()
nodes = telepathic._extract_semantic_nodes(xml_dump)
texts = []
@@ -1107,43 +1194,63 @@ def _run_zero_latency_feed_loop(device, zero_engine, nav_graph, configs, session
if session_state.check_limit(SessionState.Limit.LIKES):
likes_chance = 0.0
if res_score >= 0.35 and random.random() < likes_chance:
logger.info("❤️ [Interaction] Liking post...")
success = nav_graph._execute_transition("tap_like_button", zero_engine)
if success is not True:
logger.debug("Telepathic Like failed, falling back to double-tap.")
info = device.get_info()
w, h = info.get("displayWidth", 1080), info.get("displayHeight", 2400)
_humanized_click(device, w // 2, h // 2, double=True, sleep_mod=sleep_mod)
session_state.totalLikes += 1
sleep(random.uniform(1.2, 2.5) * sleep_mod)
did_interact = True
did_like = True
# If user explicitly configures likes_chance > 0, we lower the strict AI resonance requirement
needs_like = (likes_chance > 0.0 and random.random() < likes_chance)
if (needs_like or res_score >= 0.35):
logger.info("❤️ [Interaction] Deciding like method...")
# Umentscheidung (Change of mind)
if random.random() < 0.02:
logger.info("🧠 [Umentscheidung] Taking back the like.")
sleep(random.uniform(1.0, 3.0))
# Press like button again to unlike
nav_graph._execute_transition("tap_like_button", zero_engine)
session_state.totalLikes -= 1
did_like = False
did_interact = False
xml_check = device.dump_hierarchy()
if not isinstance(xml_check, str): xml_check = ""
xml_check_lower = xml_check.lower()
is_reel_feed = "reel_viewer" in xml_check_lower or "clips_viewer" in xml_check_lower
is_liked_feed = "gefällt mir nicht mehr" in xml_check_lower or "unlike" in xml_check_lower or 'content-desc="liked"' in xml_check_lower
use_double_tap = random.random() < 0.4 and not is_reel_feed
did_like = False
if use_double_tap:
if is_liked_feed:
logger.debug("Telepathic Like failed or post was unlikable (already liked). Skipping increment.")
else:
info = device.get_info()
w, h = info.get("displayWidth", 1080), info.get("displayHeight", 2400)
offset_x = random.randint(int(w * 0.2), int(w * 0.8))
offset_y = random.randint(int(h * 0.3), int(h * 0.7))
logger.info(f"❤️ [Interaction] Double-Tapping organically at ({offset_x}, {offset_y})")
_humanized_click(device, offset_x, offset_y, double=True, sleep_mod=sleep_mod)
session_state.totalLikes += 1
sleep(random.uniform(1.2, 2.5) * sleep_mod)
did_interact = True
did_like = True
else:
logger.info("❤️ [Interaction] Liking post via Heart Button...")
success = nav_graph._execute_transition("tap_like_button")
if success:
session_state.totalLikes += 1
sleep(random.uniform(1.2, 2.5) * sleep_mod)
did_interact = True
did_like = True
else:
logger.debug("Telepathic Like failed or post was unlikable. Skipping increment.")
# Comment: requires high resonance alignment
comment_chance = float(getattr(configs.args, "comment_percentage", 0)) / 100.0
if session_state.check_limit(SessionState.Limit.COMMENTS):
comment_chance = 0.0
if res_score >= 0.4 and random.random() < comment_chance:
# If user explicitly configures comment_chance > 0, we lower the strict AI resonance requirement
needs_comment = (comment_chance > 0.0 and random.random() < comment_chance)
if (needs_comment or res_score >= 0.4):
logger.info("💬 [Interaction] Entering Comment Sheet for deep engagement...")
success = nav_graph._execute_transition("tap_comment_button", zero_engine)
success = nav_graph._execute_transition("tap_comment_button")
if success is True:
sleep(random.uniform(2.0, 4.0) * sleep_mod)
# 1. Scrape Context from the comment sheet
sheet_xml = device.deviceV2.dump_hierarchy()
sheet_xml = device.dump_hierarchy()
# 🛡️ [Semantic Gate] Verify we are actually in the comment sheet
if not any(x in sheet_xml for x in ["layout_comment_thread", "comment_composer", "comment_button_post"]):
@@ -1185,7 +1292,7 @@ def _run_zero_latency_feed_loop(device, zero_engine, nav_graph, configs, session
if random.random() < 0.4:
# Use Telepathic to find the like button for this specific comment text
intent = f"Heart like button for comment: '{c_node['text'][:20]}...'"
xml_dump = device.deviceV2.dump_hierarchy()
xml_dump = device.dump_hierarchy()
like_btn = telepathic.find_best_node(xml_dump, intent, device=device)
if like_btn and not like_btn.get("skip"):
@@ -1193,7 +1300,7 @@ def _run_zero_latency_feed_loop(device, zero_engine, nav_graph, configs, session
sleep(random.uniform(0.8, 1.5))
# Verification: Simple XML change check
if device.deviceV2.dump_hierarchy() != xml_dump:
if device.dump_hierarchy() != xml_dump:
telepathic.confirm_click(intent)
logger.info(f"❤️ [Interaction] Liked user comment: '{c_node['text'][:30]}...'")
else:
@@ -1202,7 +1309,7 @@ def _run_zero_latency_feed_loop(device, zero_engine, nav_graph, configs, session
# 20% chance to randomly visit commenter's profile
if random.random() < 0.2:
intent = f"Avatar profile picture for commenter: '{c_node['text'][:20]}...'"
xml_dump = device.deviceV2.dump_hierarchy()
xml_dump = device.dump_hierarchy()
avatar_node = telepathic.find_best_node(xml_dump, intent, device=device)
if avatar_node:
@@ -1211,7 +1318,7 @@ def _run_zero_latency_feed_loop(device, zero_engine, nav_graph, configs, session
sleep(random.uniform(2.5, 4.5) * sleep_mod)
# Verification: Did we reach a profile?
post_xml = device.deviceV2.dump_hierarchy()
post_xml = device.dump_hierarchy()
if "profile" in post_xml.lower() or "button_follow" in post_xml.lower():
telepathic.confirm_click(intent)
_interact_with_profile(device, configs, "commenter", session_state, sleep_mod, logger)
@@ -1225,7 +1332,7 @@ def _run_zero_latency_feed_loop(device, zero_engine, nav_graph, configs, session
# 15% chance to Sub-Comment (Reply)
if random.random() < 0.15 and not replying_to:
intent = f"Reply button for comment: '{c_node['text'][:20]}...'"
xml_dump = device.deviceV2.dump_hierarchy()
xml_dump = device.dump_hierarchy()
reply_btn = telepathic.find_best_node(xml_dump, intent, device=device)
if reply_btn:
@@ -1233,7 +1340,7 @@ def _run_zero_latency_feed_loop(device, zero_engine, nav_graph, configs, session
sleep(random.uniform(1.2, 2.0))
# Verification: Did the screen change or input field appear?
if device.deviceV2.dump_hierarchy() != xml_dump:
if device.dump_hierarchy() != xml_dump:
telepathic.confirm_click(intent)
replying_to = c_node["text"]
logger.info(f"🔁 [Interaction] Replying directly to comment: '{replying_to[:30]}...'")
@@ -1290,7 +1397,7 @@ def _run_zero_latency_feed_loop(device, zero_engine, nav_graph, configs, session
# Verification: Did the keyboard open or cursor move to box?
# We check if the XML changed and focus is on an edittext
post_focus_xml = device.deviceV2.dump_hierarchy()
post_focus_xml = device.dump_hierarchy()
if "editText" in post_focus_xml.lower() or post_focus_xml != sheet_xml:
telepathic.confirm_click("Comment input text box editfield")
else:
@@ -1312,7 +1419,7 @@ def _run_zero_latency_feed_loop(device, zero_engine, nav_graph, configs, session
# Press back to trigger Discard popup
device.deviceV2.press("back")
sleep(1.0)
xml_dump = device.deviceV2.dump_hierarchy()
xml_dump = device.dump_hierarchy()
discard_btn = telepathic.find_best_node(xml_dump, "Discard or Verwerfen popup button to cancel comment", device=device)
if discard_btn:
device.deviceV2.click(discard_btn["x"], discard_btn["y"])
@@ -1323,14 +1430,14 @@ def _run_zero_latency_feed_loop(device, zero_engine, nav_graph, configs, session
else:
# Tap Post
sleep(random.uniform(0.5, 1.5))
pre_post_xml = device.deviceV2.dump_hierarchy()
pre_post_xml = device.dump_hierarchy()
post_btn = telepathic.find_best_node(pre_post_xml, "Post submit comment button", device=device)
if post_btn:
device.deviceV2.click(post_btn["x"], post_btn["y"])
sleep(random.uniform(2.0, 3.5))
# Verification: Did the button disappear or layout change?
post_post_xml = device.deviceV2.dump_hierarchy()
post_post_xml = device.dump_hierarchy()
# If "Post" button is gone from the area or XML changed significantly
if "button_post" not in post_post_xml.lower() or post_post_xml != pre_post_xml:
telepathic.confirm_click("Post submit comment button")
@@ -1344,10 +1451,10 @@ def _run_zero_latency_feed_loop(device, zero_engine, nav_graph, configs, session
logger.error(f"❌ [Interaction] AI Comment deployment failed: {e}")
# Safely exit the comment sheet
if "bottom_sheet_container" in device.deviceV2.dump_hierarchy():
if "bottom_sheet_container" in device.dump_hierarchy():
device.deviceV2.press("back")
sleep(1.0)
if "bottom_sheet_container" in device.deviceV2.dump_hierarchy():
if "bottom_sheet_container" in device.dump_hierarchy():
device.deviceV2.press("back")
sleep(1.0)
@@ -1357,31 +1464,44 @@ def _run_zero_latency_feed_loop(device, zero_engine, nav_graph, configs, session
repost_chance = float(getattr(configs.args, "repost_percentage", 0)) / 100.0
if res_score >= 0.70 and random.random() < repost_chance:
logger.info("🔁 [Interaction] Reposting highly resonant content...", extra={"color": f"{Fore.CYAN}"})
success = nav_graph._execute_transition("tap_share_button", zero_engine)
if success is True:
sleep(random.uniform(1.8, 3.5) * sleep_mod)
telepathic = TelepathicEngine.get_instance()
xml_dump = device.deviceV2.dump_hierarchy()
repost_btn = telepathic.find_best_node(xml_dump, "Repost interaction button with two arrows", device=device)
if repost_btn and not repost_btn.get("skip"):
_humanized_click(device, repost_btn["x"], repost_btn["y"], sleep_mod=sleep_mod)
sleep(random.uniform(2.0, 4.0) * sleep_mod)
# Fast Path: Check if Repost button is ALREADY on the screen (Direct Repost for Reels)
telepathic = TelepathicEngine.get_instance()
current_xml = device.dump_hierarchy()
direct_repost = telepathic.find_best_node(current_xml, "Repost interaction button with two arrows", device=device, threshold=0.90) if is_reels else None
success = True
if direct_repost and not direct_repost.get("skip"):
logger.info("⚡ [Fast Path] Found direct Repost button. Skipping share sheet.")
repost_btn = direct_repost
else:
success = nav_graph._execute_transition("tap_share_button")
if success is True:
sleep(random.uniform(1.8, 3.5) * sleep_mod)
xml_dump = device.dump_hierarchy()
repost_btn = telepathic.find_best_node(xml_dump, "Repost interaction button with two arrows", device=device)
else:
repost_btn = None
if success is True and repost_btn and not repost_btn.get("skip"):
_humanized_click(device, repost_btn["x"], repost_btn["y"], sleep_mod=sleep_mod)
sleep(random.uniform(2.0, 4.0) * sleep_mod)
# Verification: Did the share menu close or repost confirmation appear?
post_xml = device.dump_hierarchy()
repost_success = post_xml != current_xml or "reposted" in post_xml.lower()
if repost_success:
telepathic.confirm_click("Repost interaction button with two arrows")
logger.info("✅ [Interaction] Content successfully reposted to feed/followers.", extra={"color": f"{Fore.GREEN}"})
did_interact = True
else:
telepathic.reject_click("Repost interaction button with two arrows")
logger.warning("⚠️ [Repost] Click failed to trigger repost. Learning from failure.")
# Verification: Did the share menu close or repost confirmation appear?
post_xml = device.deviceV2.dump_hierarchy()
repost_success = post_xml != xml_dump or "reposted" in post_xml.lower()
if repost_success:
telepathic.confirm_click("Repost interaction button with two arrows")
logger.info("✅ [Interaction] Content successfully reposted to feed/followers.", extra={"color": f"{Fore.GREEN}"})
did_interact = True
else:
telepathic.reject_click("Repost interaction button with two arrows")
logger.warning("⚠️ [Repost] Click failed to trigger repost. Learning from failure.")
# Close share menu if still open
device.deviceV2.press("back")
sleep(random.uniform(1.0, 2.0) * sleep_mod)
# Close share menu if still open
device.deviceV2.press("back")
sleep(random.uniform(1.0, 2.0) * sleep_mod)
# ── Parasocial CRM & SwarmProtocol ──
@@ -1408,7 +1528,7 @@ def _run_zero_latency_feed_loop(device, zero_engine, nav_graph, configs, session
# ── Active Inference: Evaluate prediction (after action) ──
if ai:
_wait_for_post_loaded(device, timeout=3)
post_action_xml = device.deviceV2.dump_hierarchy()
post_action_xml = device.dump_hierarchy()
ai.evaluate_prediction(post_action_xml)
# ── Advance to next post ──
@@ -1454,7 +1574,7 @@ def _run_zero_latency_search_loop(device, zero_engine, nav_graph, configs, sessi
# We assume we are on the Explore tab now (Global Navigation Bar)
try:
xml = device.deviceV2.dump_hierarchy()
xml = device.dump_hierarchy()
telepathic = cognitive_stack.get("telepathic")
# Find search bar
@@ -1467,7 +1587,7 @@ def _run_zero_latency_search_loop(device, zero_engine, nav_graph, configs, sessi
sleep(3.0)
# 2. Pick a result (Top, Accounts, or Tags)
results_xml = device.deviceV2.dump_hierarchy()
results_xml = device.dump_hierarchy()
target_result = telepathic.find_best_node(results_xml, "First relevant search result (Account or Hashtag)", device=device)
if target_result:

View File

@@ -171,6 +171,7 @@ class Config:
self.parser.add_argument("--ai-target-audience", help="Target audience used interchangeably with persona interests", default="")
self.parser.add_argument("--interact-percentage", help="Overall interaction probability percentage", default="80")
self.parser.add_argument("--comment-percentage", help="Comment probability percentage", default="0")
self.parser.add_argument("--follow-percentage", help="Follow probability percentage", default="0")
self.parser.add_argument("--dry-run-comments", action="store_true", help="Generate AI comments but do not actually post them (debug/logging only)")
self.parser.add_argument("--search", help="Comma-separated keywords to search for", default="")
self.parser.add_argument("--scrape-profiles", action="store_true", help="Extract and store profile metadata in CRM")

View File

@@ -30,10 +30,10 @@ class DarwinEngine(QdrantBase):
}
self.current_behavior = {}
def synthesize_interaction_profile(self, target_resonance: float) -> dict:
def synthesize_interaction_profile(self, target_resonance: float, text_length: int = 0) -> dict:
"""
Given an AI aesthetic resonance score (0.0 to 1.0), this generates
a deterministic topological interaction behavior mathematically suited to the target.
Given an AI aesthetic resonance score (0.0 to 1.0) and caption length,
this generates a deterministic topological interaction behavior.
"""
history = self._get_historical_landscape()
epsilon = 0.15 # 15% pure exploration
@@ -54,18 +54,26 @@ class DarwinEngine(QdrantBase):
self.current_behavior["initial_dwell_sec"] *= max(0.5, target_resonance * 1.5)
self.current_behavior["profile_visit_prob"] *= max(0.2, target_resonance * 2.0)
# ── Generative Dwell-Time ──
# Humans take longer to finish "reading" long captions.
# Average reading speed is ~15-20 chars per second.
if text_length > 20:
reading_latency = min(15.0, text_length / 25.0) # Cap extra reading time at 15s
logger.debug(f"🧬 [Darwin Engine] Generative Dwell spike: +{reading_latency:.1f}s (Caption: {text_length} chars)")
self.current_behavior["initial_dwell_sec"] += reading_latency
# Clip bounds
for k, (b_min, b_max, _) in self.behavior_bounds.items():
self.current_behavior[k] = max(b_min, min(b_max, self.current_behavior[k]))
return self.current_behavior
def execute_proof_of_resonance(self, device, resonance: float, nav_graph=None, zero_engine=None, configs=None, resonance_oracle=None, username=None):
def execute_proof_of_resonance(self, device, resonance: float, text_length: int = 0, nav_graph=None, zero_engine=None, configs=None, resonance_oracle=None, username=None):
"""
Translates the mathematical interaction profile directly into device actions
to prove engagement to the platform's anti-bot heuristic algorithm.
"""
profile = self.synthesize_interaction_profile(resonance)
profile = self.synthesize_interaction_profile(resonance, text_length=text_length)
logger.info("🧬 [Darwin MDP] Executing Proof of Resonance Sequence...")
@@ -124,7 +132,7 @@ class DarwinEngine(QdrantBase):
except Exception as e:
logger.warning(f"⚠️ [Vision Context] Failed to capture screenshot: {e}")
success = nav_graph._execute_transition("tap_comment_button", zero_engine)
success = nav_graph._execute_transition("tap_comment_button")
if success:
# ---- Phase 10: RAG Comment Extraction ----
if configs and resonance_oracle and getattr(configs.args, "ai_learn_comments", False):
@@ -132,7 +140,7 @@ class DarwinEngine(QdrantBase):
if random.random() < 0.05:
logger.debug(" -> Dumping UI hierarchy for Comment Extraction...")
try:
xml_data = device.deviceV2.dump_hierarchy()
xml_data = device.dump_hierarchy()
t0 = time.time()
resonance_oracle.extract_and_learn_comments(xml_data, configs, author=username or "unknown", images_b64=b64_img_payload)
t1 = time.time()
@@ -154,7 +162,7 @@ class DarwinEngine(QdrantBase):
time.sleep(1.0)
# Instead of relying on a fragile bottom_sheet_container ID,
# we verify if the feed is visible. If not, the comment sheet is still open (or keyboard).
ui_dump = device.deviceV2.dump_hierarchy()
ui_dump = device.dump_hierarchy()
if 'resource-id="com.instagram.android:id/row_feed"' not in ui_dump and 'resource-id="com.instagram.android:id/button_like"' not in ui_dump:
logger.debug(" -> Not back on Home feed, pressing back again to close comment sheet/keyboard")
device.deviceV2.press("back")

View File

@@ -1,7 +1,7 @@
import logging
import json
import uiautomator2 as u2
from time import sleep
from time import sleep, time
from random import uniform
from GramAddict.core.utils import random_sleep
from functools import wraps
@@ -67,6 +67,10 @@ class DeviceFacade:
except Exception as e:
logger.debug(f"Could not start system watcher: {e}")
# Transient package cache to prevent repeat lag
self.last_transient_pkg = None
self.last_transient_time = 0
@adb_retry()
def get_info(self):
return self.deviceV2.info
@@ -123,11 +127,11 @@ class DeviceFacade:
# Human finger rest time (squish)
sleep(uniform(0.05, 0.15))
# Sloppy slip
slip_x = x + int(uniform(-4, 4))
# Sloppy slip (Containment: Don't slip horizontally at the bottom edge, prevents Android App-Switch gestures)
slip_x = x + int(uniform(-4, 4)) if y < 2100 else x
slip_y = y + int(uniform(-4, 4))
self.deviceV2.touch.move(slip_x, slip_y)
self.deviceV2.touch.move(slip_x, slip_y)
sleep(uniform(0.01, 0.05))
self.deviceV2.touch.up(slip_x, slip_y)
except Exception as e:
@@ -150,15 +154,35 @@ class DeviceFacade:
def _get_current_app(self):
"""
Hardened app package detection.
Transient notifications (e.g. Amazon, WhatsApp) can spoof uiautomator2's app_current() report.
We verify the package with a retry if it doesn't match our expected app_id.
Transient notifications (e.g. Amazon, WhatsApp, SystemUI) can spoof uiautomator2's app_current() report.
We verify the package with multiple retries and a grace period if it doesn't match our expected app_id.
"""
pkg = self.deviceV2.app_current().get("package")
if pkg != self.app_id:
# Maybe a notification popped up? Wait and re-check.
sleep(0.5)
pkg = self.deviceV2.app_current().get("package")
if pkg == self.app_id:
return pkg
# If it doesn't match, it might be a notification banner.
# Known transient spoofers: WhatsApp, SystemUI (status bar), Android System
transient_packages = ["com.whatsapp", "com.android.systemui", "android"]
if pkg in transient_packages:
# Check cooldown: if we just handled this package < 10s ago, don't sleep again
now = time()
if pkg == self.last_transient_pkg and (now - self.last_transient_time) < 10.0:
logger.debug(f"Perimeter: Consecutive hit for transient package '{pkg}'. Skipping cooldown wait.")
return self.app_id
logger.debug(f"⚠️ [Perimeter] Detected transient package '{pkg}'. Waiting for banner to clear...")
self.last_transient_pkg = pkg
self.last_transient_time = now
sleep(1.5) # Give the notification/animation time to fade
pkg = self.deviceV2.app_current().get("package")
if pkg in transient_packages:
# If it persists, we trust the drift logic to handle it if it blocks the UI,
# but for focus detection, we return the target app to avoid infinite wait loops.
return self.app_id
return pkg
@adb_retry()
@@ -168,7 +192,26 @@ class DeviceFacade:
@adb_retry()
def dump_hierarchy(self):
return self.deviceV2.dump_hierarchy()
xml = self.deviceV2.dump_hierarchy()
# Continuous Session Tracing
import os
from datetime import datetime
try:
if not hasattr(self, "_trace_counter"):
self._trace_counter = 0
ts = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
self._trace_dir = os.path.join("debug", "session_traces", ts)
os.makedirs(self._trace_dir, exist_ok=True)
self._trace_counter += 1
trace_path = os.path.join(self._trace_dir, f"{self._trace_counter:05d}.xml")
with open(trace_path, "w", encoding="utf-8") as f:
f.write(xml)
except Exception as e:
logger.debug(f"Failed to write session trace: {e}")
return xml
@adb_retry()
def screenshot(self):

View File

@@ -35,7 +35,7 @@ def dump_ui_state(device, reason: str, extra_context: dict = None):
os.makedirs(DUMP_DIR, exist_ok=True)
# Capture hierarchy
xml = device.deviceV2.dump_hierarchy()
xml = device.dump_hierarchy()
# Generate filename: reason__2026-04-13_17-41-39.xml
ts = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")

View File

@@ -36,7 +36,7 @@ def _run_zero_latency_dm_loop(device, zero_engine, nav_graph, configs, session_s
return "BOREDOM_CHANGE_FEED"
try:
xml_dump = device.deviceV2.dump_hierarchy()
xml_dump = device.dump_hierarchy()
# Step 1: Find unread conversation threads
unread_threads = telepathic._extract_semantic_nodes(xml_dump, "find unread message threads or unread badges", threshold=0.7)
@@ -48,7 +48,7 @@ def _run_zero_latency_dm_loop(device, zero_engine, nav_graph, configs, session_s
sleep(2.0)
# Step 2: Read the conversation context
thread_xml = device.deviceV2.dump_hierarchy()
thread_xml = device.dump_hierarchy()
msg_nodes = telepathic._extract_semantic_nodes(thread_xml, "find the last received message text", threshold=0.6)
context_text = "No previous context"
@@ -76,7 +76,7 @@ def _run_zero_latency_dm_loop(device, zero_engine, nav_graph, configs, session_s
sleep(1.0)
# Find Send button
send_xml = device.deviceV2.dump_hierarchy()
send_xml = device.dump_hierarchy()
send_nodes = telepathic._extract_semantic_nodes(send_xml, "find the send message button", threshold=0.8)
if send_nodes and not send_nodes[0].get("skip"):

View File

@@ -21,7 +21,7 @@ def capture_all(device):
def _save_dump(filename, description):
logger.info(f"⏳ Waiting for UI to settle for [{description}]...")
time.sleep(3.5) # ensure animations finish
xml_data = device.deviceV2.dump_hierarchy()
xml_data = device.dump_hierarchy()
path = os.path.join(FIX_DIR, filename)
with open(path, "w", encoding="utf-8") as f:
f.write(xml_data)

View File

@@ -0,0 +1,3 @@
class ActionBlockedError(Exception):
"""Raised when Instagram explicitly blocks an action with a 'Try Again Later' or 'Action Blocked' dialogue."""
pass

View File

@@ -59,12 +59,70 @@ def get_model_pricing(model_id: str) -> dict:
return _MODEL_PRICING_CACHE.get(model_id, {})
def prewarm_ollama_models(configs):
"""
Sends a dummy request to the configured local Ollama API endpoints via a background thread
to force the models to load into VRAM during bot startup, minimizing initial connection latency
and avoiding timeouts downstream.
"""
args = configs.args
def _warmup():
import threading
models_to_warm = set()
# Collect unique local models
for attr, url_attr in [
("ai_telepathic_model", "ai_telepathic_url"),
("ai_fallback_model", "ai_fallback_url"),
("ai_condenser_model", "ai_condenser_url"),
("ai_model", "ai_model_url")
]:
url = getattr(args, url_attr, "")
model = getattr(args, attr, "")
if model and url and ("localhost" in url or "127.0.0.1" in url):
models_to_warm.add((url, model))
for url, model in models_to_warm:
logger.info(f"🔥 [VRAM Pre-Warm] Instructing local Ollama engine to load {model} into memory in the background...")
try:
# Fire an ultra-short generation to force it into VRAM
requests.post(
url,
json={"model": model, "prompt": "Hi", "stream": False, "options": {"num_predict": 1}},
timeout=120
)
except Exception:
pass
if hasattr(args, "ai_telepathic_model"):
import threading
threading.Thread(target=_warmup, daemon=True).start()
def log_openrouter_burn():
"""Fetches and logs the current OpenRouter API key usage (money burned)."""
"""Fetches and logs the current OpenRouter API key usage (money burned) ONLY if OpenRouter is actively used."""
key = os.environ.get("OPENROUTER_API_KEY")
if not key:
return
try:
from GramAddict.core.config import Config
args = Config().args
uses_openrouter = False
# Check all possible model/url endpoints for 'openrouter'
for attr in ["ai_model", "ai_model_url", "ai_telepathic_model", "ai_telepathic_url",
"ai_fallback_model", "ai_fallback_url", "ai_condenser_model", "ai_condenser_url"]:
val = getattr(args, attr, "")
if val and "openrouter" in str(val).lower():
uses_openrouter = True
break
if not uses_openrouter:
return
except Exception:
pass
try:
r = requests.get("https://openrouter.ai/api/v1/auth/key", headers={"Authorization": f"Bearer {key}"}, timeout=5)
if r.status_code == 200:
@@ -281,6 +339,9 @@ def query_telepathic_llm(
except Exception:
target_url = "http://localhost:11434/api/generate"
target_model = "llama3.2:1b"
is_local = "localhost" in target_url or "127.0.0.1" in target_url
calc_timeout = 180 if is_local else 45
ans = query_llm(
url=target_url,
@@ -288,7 +349,8 @@ def query_telepathic_llm(
prompt=user_prompt,
images_b64=images_b64,
system=system_prompt,
format_json=True
format_json=True,
timeout=calc_timeout # Navigation VLM must fail fast for Cloud, but wait for Local VRAM loads
)
if ans and "response" in ans:
return ans["response"]

View File

@@ -4,6 +4,7 @@ import os
import uuid
import time
import random
from GramAddict.core.utils import random_sleep
from GramAddict.core.compiler_engine import VLMCompilerEngine
from GramAddict.core.qdrant_memory import NavigationMemoryDB
@@ -92,7 +93,7 @@ class QNavGraph:
direct_action = target_to_action.get(target_state, "tap_home_tab")
target_anchor = target_state if direct_action != "tap_home_tab" else "HomeFeed"
success = self._execute_transition(direct_action, zero_engine)
success = self._execute_transition(direct_action)
if success is True:
logger.info(f"Successfully anchored! Learned new global edge: {self.current_state} -> {target_anchor} via {direct_action}")
if self.current_state not in self.nodes:
@@ -105,7 +106,7 @@ class QNavGraph:
elif success == "CONTEXT_LOST":
logger.warning(f"⚠️ Context was lost during direct action '{direct_action}'. Forcing app focus and resetting path.")
self.device.deviceV2.app_start(self.device.app_id, use_monkey=True)
time.sleep(3)
random_sleep(2.5, 4.0)
self.current_state = "HomeFeed"
return self.navigate_to(target_state, zero_engine, recovery_attempts=recovery_attempts + 1)
else:
@@ -113,7 +114,7 @@ class QNavGraph:
if self.current_state == "UNKNOWN":
logger.warning(f"📍 [Recovery] Semantic tap failed from UNKNOWN. Attempting to back out of sub-view...")
self.device.deviceV2.press("back")
time.sleep(2)
random_sleep(1.5, 3.0)
# We stay in UNKNOWN, but next attempt might see the nav bar
return self.navigate_to(target_state, zero_engine, recovery_attempts=recovery_attempts + 0.5)
path = None
@@ -122,7 +123,7 @@ class QNavGraph:
# Absolute last resort fallback: force app to main activity
logger.warning("Semantic recovery failed. Forcing main activity intent...")
self.device.deviceV2.app_start(self.device.app_id)
time.sleep(3)
random_sleep(2.5, 4.0)
self.current_state = "HomeFeed"
path = self._find_path(self.current_state, logical_target)
@@ -131,12 +132,12 @@ class QNavGraph:
return False
for action in path:
result = self._execute_transition(action, zero_engine)
result = self._execute_transition(action)
if result == "CONTEXT_LOST":
logger.warning(f"⚠️ Context was lost during '{action}'. Forcing app focus and resetting path.")
self.device.deviceV2.app_start(self.device.app_id, use_monkey=True)
time.sleep(3)
random_sleep(2.5, 4.0)
# After app start, we are at HomeFeed (usually)
self.current_state = "HomeFeed"
# Recursively call navigate_to from the new anchor
@@ -146,7 +147,7 @@ class QNavGraph:
logger.error(f"Nav transition '{action}' failed! Initiating self-repair...")
self._repair_transition(action)
# Retry after repair
success = self._execute_transition(action, zero_engine)
success = self._execute_transition(action)
if not success or success == "CONTEXT_LOST":
logger.error(f"FATAL: Auto-repair failed for transition: {action}")
return False
@@ -175,24 +176,133 @@ class QNavGraph:
return None
def _execute_transition(self, action: str, zero_engine=None, max_retries: int = 2) -> bool:
def _clear_anomaly_obstacles(self, max_attempts=2) -> bool:
"""
Actively hunts down and dismisses known edge-case overlays (OS Permissions, Surveys)
that block navigation. If an unknown modal is detected, falls back to pressing BACK.
Returns True if an obstacle was detected and handled, False if the UI is clear.
"""
import xml.etree.ElementTree as ET
import re
import time
from GramAddict.core.exceptions import ActionBlockedError
for attempt in range(max_attempts):
xml_dump = self.device.dump_hierarchy()
if not isinstance(xml_dump, str):
return False
xml_dump_lower = xml_dump.lower()
# --- 0. FATAL: Action Blocked Guard ---
# If Instagram explicitly restricts our activity, we must hard crash to prevent permanent account bans.
is_action_blocked = (
"try again later" in xml_dump_lower or
"action blocked" in xml_dump_lower or
"restrict certain activity" in xml_dump_lower or
"help us confirm you own" in xml_dump_lower or
"confirm it's you" in xml_dump_lower or
"später erneut versuchen" in xml_dump_lower or
"bestätige, dass du es bist" in xml_dump_lower or
"handlung blockiert" in xml_dump_lower or
"eingeschränkt" in xml_dump_lower
)
if is_action_blocked:
logger.error("🚫 [CRITICAL GUARD] Instagram Action Block Dialog Detected! Aborting run to protect account.")
raise ActionBlockedError("Instagram soft-banned the account. We hit a rate limit or restriction. Halting all activities.")
try:
tree = ET.fromstring(xml_dump)
except Exception:
# If XML parsing fails, fall back to simple string check
if re.search(r'bottom_sheet_container|dialog_container|dialog_root|bottom_sheet_drag|action_sheet_container', xml_dump):
logger.warning("🛡️ [Z-Depth Guard] Generic obstacle detected. Pressing BACK to clear...")
self.device.deviceV2.press("back")
random_sleep(1.0, 2.5)
return True
return False
handled = False
# --- 1. OS Permission Dialogs (Android) ---
grant_dialog = tree.find(".//node[@resource-id='com.android.permissioncontroller:id/grant_dialog']")
if grant_dialog is not None:
logger.warning("🛡️ [Z-Depth Guard] OS Permission Dialog detected! Searching for Deny button...")
deny_btn = grant_dialog.find(".//node[@resource-id='com.android.permissioncontroller:id/permission_deny_button']")
if deny_btn is not None and deny_btn.get("bounds"):
bounds = re.findall(r'\d+', deny_btn.get("bounds"))
if len(bounds) == 4:
x = (int(bounds[0]) + int(bounds[2])) // 2
y = (int(bounds[1]) + int(bounds[3])) // 2
logger.info(f"👆 Clicking 'Deny' at ({x}, {y})")
from GramAddict.core.bot_flow import _humanized_click
_humanized_click(self.device, x, y)
random_sleep(1.0, 2.5)
handled = True
# --- 2. Instagram Surveys & Interstitials ---
if not handled:
survey_cont = tree.find(".//node[@resource-id='com.instagram.android:id/survey_container']")
if survey_cont is not None:
logger.warning("🛡️ [Z-Depth Guard] Instagram Survey detected! Searching for Dismiss/Not Now button...")
# Usually the negative button has an explicit ID
neg_btn = survey_cont.find(".//node[@resource-id='com.instagram.android:id/button_negative']")
# Fallback to semantic text search if ID fails
if neg_btn is None:
for n in survey_cont.iter('node'):
txt = n.get("text", "").lower() + " " + n.get("content-desc", "").lower()
if "not now" in txt or "cancel" in txt or "dismiss" in txt or "skip" in txt:
neg_btn = n
break
if neg_btn is not None and neg_btn.get("bounds"):
bounds = re.findall(r'\d+', neg_btn.get("bounds"))
if len(bounds) == 4:
x = (int(bounds[0]) + int(bounds[2])) // 2
y = (int(bounds[1]) + int(bounds[3])) // 2
logger.info(f"👆 Clicking Survey Dismiss at ({x}, {y})")
from GramAddict.core.bot_flow import _humanized_click
_humanized_click(self.device, x, y)
random_sleep(1.0, 2.5)
handled = True
# --- 3. Intrusive Bottom / Action Sheets ---
if not handled:
if re.search(r'bottom_sheet_container|dialog_container|dialog_root|bottom_sheet_drag|action_sheet_container', xml_dump):
logger.warning("🛡️ [Z-Depth Guard] Generic obstacle or Action Sheet detected. Pressing BACK to clear...")
self.device.deviceV2.press("back")
random_sleep(1.0, 2.5)
handled = True
if handled:
# Loop around: could be multiple stacked dialogs
continue
else:
# No known anomaly obstacles detected
return False
return True
def _execute_transition(self, action: str, mock_semantic_engine=None, max_retries: int = 2) -> bool:
"""
Executes a transition (e.g. 'tap_explore_tab') using the Telepathic Semantic Engine.
"""
from GramAddict.core.telepathic_engine import TelepathicEngine
engine = TelepathicEngine.get_instance()
engine = mock_semantic_engine or TelepathicEngine.get_instance()
failed_positions = set() # Track (x, y) of clicks that failed, for grid retry diversity
for attempt in range(max_retries + 1):
context_xml = self.device.deviceV2.dump_hierarchy()
context_xml = self.device.dump_hierarchy()
# ── Z-Depth Guard / Obstacle Clearance ──
import re
if re.search(r'bottom_sheet_container|dialog_container|dialog_root|bottom_sheet_drag', str(context_xml)):
logger.warning("🛡️ [Z-Depth Guard] Obstacle overlay detected during navigation. Pressing BACK to clear...")
self.device.deviceV2.press("back")
time.sleep(1.5)
# ── Z-Depth Guard / Anomaly Obstacle Clearance ──
cleared_something = self._clear_anomaly_obstacles()
if cleared_something:
# Re-acquire context after clearing obstacle
context_xml = self.device.deviceV2.dump_hierarchy()
context_xml = self.device.dump_hierarchy()
# We phrase the action as an intent for the semantic engine
# e.g. "tap_explore_tab" -> "tap explore tab"
@@ -223,7 +333,20 @@ class QNavGraph:
# Use TelepathicEngine to find the most likely node for this intent
# If vector score < 0.82, it will trigger the Vision Cortex Fallback (VLM)
best_node = engine.find_best_node(context_xml, intent_description, min_confidence=0.82, device=self.device)
# Pass failed_positions so grid fast-path picks a different item on retry
best_node = engine.find_best_node(context_xml, intent_description, min_confidence=0.82, device=self.device, skip_positions=failed_positions)
# ── Blocked by Modal Recovery ──
if best_node and best_node.get("blocked_by_modal"):
logger.warning(f"🛡️ [Modal Recovery] Navigation '{action}' is blocked by a modal. Attempting anomaly clearance...")
self._clear_anomaly_obstacles()
if attempt < max_retries:
context_xml = self.device.dump_hierarchy()
continue
else:
logger.error(f"❌ [Modal Recovery] Persistent blockage for '{action}'. Escalating to Context Lost (App Restart).")
return "CONTEXT_LOST"
if not best_node:
logger.debug(f"_execute_transition: TelepathicEngine found no matching node for '{action}'")
# Check if we are even in the right app
@@ -236,6 +359,15 @@ class QNavGraph:
if attempt < max_retries:
time.sleep(1.0)
continue
# FINAL ATTEMPT ESCAPE:
# If we are looking for the 'Home' tab (our baseline) and everything failed,
# we might be in an unknown sub-view. Try one last 'BACK' press.
if action == "tap_home_tab":
logger.warning("📍 [Escape] Home tab not found after all retries. Attempting final BACK press to escape sub-view...")
self.device.deviceV2.press("back")
time.sleep(2.0)
return False
if best_node.get("skip") or (best_node.get("selected") and "tab" in action):
@@ -250,7 +382,23 @@ class QNavGraph:
time.sleep(random.uniform(1.2, 2.5))
# ── Post-Click Verification: Did it work? ──
post_click_xml = self.device.deviceV2.dump_hierarchy()
post_click_xml = self.device.dump_hierarchy()
# ── App Perimeter Guard ──
current_app = self.device._get_current_app()
if current_app != self.device.app_id:
logger.error(f"🚨 [Perimeter Guard] FATAL: Transition '{action}' caused app to drift to '{current_app}'! Rejecting VLM snippet.")
failed_positions.add((best_node["x"], best_node["y"]))
engine.reject_click(intent_description)
# Attempt immediate recovery to main app
self.device.deviceV2.press("back")
random_sleep(1.0, 2.0)
if self.device._get_current_app() != self.device.app_id:
self.device.deviceV2.app_start(self.device.app_id, use_monkey=True)
# Return CONTEXT_LOST immediately to prevent memory poisoning
return "CONTEXT_LOST"
# 1. Semantic Verification (Hardened)
is_verified = engine.verify_success(intent_description, post_click_xml)
@@ -263,6 +411,7 @@ class QNavGraph:
return True
elif not ui_changed:
logger.warning(f"⚠️ [Nav] Click on '{action}' did not change UI. Learning from failure.")
failed_positions.add((best_node["x"], best_node["y"]))
engine.reject_click(intent_description)
if attempt < max_retries:
logger.info(f"🔄 [Autonomy] UI unchanged. Retrying transition '{action}' ({attempt + 1}/{max_retries})...")
@@ -272,6 +421,7 @@ class QNavGraph:
else:
# UI changed but semantic verification failed (accidental click or false positive)
logger.warning(f"❌ [Ambiguity Guard] UI changed after '{action}', but semantic verification FAILED. Rejecting mapping.")
failed_positions.add((best_node["x"], best_node["y"]))
engine.reject_click(intent_description)
# Safety: If we're not where we expect to be, try to back out to clear any accidentally opened menus
@@ -296,7 +446,7 @@ class QNavGraph:
dojo = DojoEngine.get_instance(self.device)
logger.warning(f"⛩️ [Dojo] Enqueuing auto-labeling job for missing '{action}'.", extra={"color": f"\x1b[36m"})
context_xml = self.device.deviceV2.dump_hierarchy()
context_xml = self.device.dump_hierarchy()
dojo.submit_snapshot(
heuristic_name=action,

View File

@@ -119,6 +119,10 @@ class ResonanceEngine:
# Map this to a more useful 0.0-1.0 range
score = max(0.0, min(1.0, (raw_score - 0.15) / 0.30))
# ── Contextual Empathy Filter ──
# If the content is tragic or highly controversial, we must NOT like it, regardless of interest alignment.
score = self._apply_empathy_filter(content_text, score)
# 5. Store evaluation in ContentMemoryDB for future cache hits
classification = "high" if score > 0.7 else "medium" if score > 0.4 else "low"
self.content_memory.store_evaluation(
@@ -140,6 +144,28 @@ class ResonanceEngine:
)
return score
def _apply_empathy_filter(self, text: str, current_score: float) -> float:
"""
Scans for tragic or sensitive keywords. If found, suppresses resonance
to prevent bot-like behavior of liking inappropriate content.
"""
tragic_keywords = [
# English
"rip", "rest in peace", "tragedy", "died", "killed", "accident", "shooting",
"funeral", "sad news", "memorial", "cancer", "disease", "breaking news",
# German
"ruhe in frieden", "verstorben", "tragödie", "unfall", "tot", "beerdigung",
"trauer", "krebs", "krankheit"
]
text_lower = text.lower()
if any(f" {word} " in f" {text_lower} " for word in tragic_keywords):
logger.warning("🛡️ [Empathy Filter] Tragic/Sensitive content detected. Suppressing resonance to prevent blind liking.")
# Drastically reduce score to "low resonance" zone (avoid liking)
return min(current_score, 0.2)
return current_score
def _classification_to_score(self, classification: str) -> float:
"""Converts stored classification back to a usable score."""

View File

@@ -413,6 +413,14 @@ class TelepathicEngine:
if "liked" in desc or "liked" in text:
logger.info("⏭️ [Keyword Fast Path] Post is already Liked. Skipping.")
return {"x": None, "y": None, "score": 1.0, "semantic": "already_liked", "skip": True}
# Check for already-followed state
if "follow" in intent_description.lower():
desc = best_node.get("original_attribs", {}).get("desc", "").lower()
text = best_node.get("original_attribs", {}).get("text", "").lower()
if re.search(r"\b(following|requested|folgst du|angefragt|gefolgt)\b", desc + " " + text):
logger.info("⏭️ [Keyword Fast Path] User is already Followed. Skipping.")
return {"x": None, "y": None, "score": 1.0, "semantic": "already_followed", "skip": True}
logger.info(f"⚡ [Keyword Fast Path] Instant match for '{intent_description}'{best_node['semantic_string']} (KeyScore: {best_score:.2f})")
self._track_click(intent_description, best_node)
@@ -429,7 +437,7 @@ class TelepathicEngine:
# Core: Find Best Node
# ──────────────────────────────────────────────
def find_best_node(self, xml_hierarchy: str, intent_description: str, min_confidence: float = 0.82, device=None) -> Optional[dict]:
def find_best_node(self, xml_hierarchy: str, intent_description: str, min_confidence: float = 0.82, device=None, **kwargs) -> Optional[dict]:
"""
Scans the screen and returns the center coordinates (x, y) of the node
whose embedding is most mathematically similar to the intent.
@@ -445,6 +453,14 @@ class TelepathicEngine:
"""
logger.debug(f"[TelepathicEngine] Seeking intent: '{intent_description}'")
# ── Global Intent Guards ──
intent_lower = intent_description.lower()
if "comment" in intent_lower:
xml_lower = str(xml_hierarchy).lower()
if "comments are turned off" in xml_lower or "comments on this post" in xml_lower or "kommentare sind deaktiviert" in xml_lower or "eingeschränkt" in xml_lower:
logger.info("⏭️ [Telepathic] Comments are disabled on this post. Skipping to prevent VLM hallucination.")
return {"x": None, "y": None, "score": 1.0, "semantic": "comments_disabled", "skip": True}
interactive_nodes = self._extract_semantic_nodes(xml_hierarchy)
if not interactive_nodes:
logger.debug("[TelepathicEngine] Screen contains no interactable semantic nodes.")
@@ -455,12 +471,19 @@ class TelepathicEngine:
if interactive_nodes:
max_y = max(n.get("y", 0) + n.get("height", 0) // 2 for n in interactive_nodes)
if max_y > 100:
# If we detect a screen height near standard android heights, don't inflate it.
if 2200 < max_y < 2600:
screen_height = max_y
else:
screen_height = int(max_y * 1.02)
# ── Modal Guard ──
# If a bottom sheet or dialog is active, it likely obscures the main navigation tabs.
# We scan the raw XML to catch non-interactable modals (like those in Reels traps).
is_nav_intent = any(k in intent_lower for k in ["tab", "navigation", "search and explore", "reels", "profile", "home", "message"])
if is_nav_intent and self._is_modal_active(interactive_nodes, raw_xml_string=xml_hierarchy):
logger.warning(f"🛡️ [Modal Guard] A bottom sheet or dialog is blocking the screen. Refusing to seek nav-intent '{intent_description}'.")
return {"blocked_by_modal": True}
# Pre-filter: Remove structurally implausible nodes and blacklisted mappings
viable_nodes = []
for node in interactive_nodes:
@@ -499,6 +522,14 @@ class TelepathicEngine:
):
logger.info("⏭️ [Memory] Post is already Liked. Skipping tap to prevent un-liking.")
return {"x": None, "y": None, "score": 1.0, "semantic": "already_liked", "skip": True}
# Prevent un-following
if "follow" in intent_description.lower() and re.search(
r"\b(following|requested|folgst du|angefragt|gefolgt)\b",
n["semantic_string"].lower()
):
logger.info("⏭️ [Memory] User is already Followed/Requested. Skipping tap to prevent opening the Favorites menu.")
return {"x": None, "y": None, "score": 1.0, "semantic": "already_followed", "skip": True}
logger.debug(f"🧠 [Confirmed Memory] Instant recall: '{intent_description}'{n['semantic_string']}")
self._track_click(intent_description, n)
@@ -516,20 +547,10 @@ class TelepathicEngine:
low_intent = intent_description.lower()
is_grid_intent = any(k in low_intent for k in ["explore grid", "grid item", "first image", "profile grid"])
if is_grid_intent:
grid_nodes = [n for n in viable_nodes if "image_button" in n.get("resource_id", "")]
if grid_nodes:
# Sort by Y (topmost first), then X (leftmost first) for "first"
grid_nodes.sort(key=lambda n: (n["y"], n["x"]))
best = grid_nodes[0]
logger.info(f"⚡ [Grid Fast-Path] Matched '{intent_description}'{best['semantic_string']} (y={best['y']})")
self._track_click(intent_description, best)
return {
"x": best["x"],
"y": best["y"],
"score": 0.98,
"semantic": best["semantic_string"],
"source": "grid_fastpath"
}
skip = kwargs.get("skip_positions", set())
grid_result = self._grid_fast_path(intent_description, viable_nodes, skip_positions=skip)
if grid_result:
return grid_result
# ── Stage 1.5: Deterministic Keyword Fast Path ──
fast_path_result = self._keyword_match_score(intent_description, viable_nodes)
@@ -570,6 +591,14 @@ class TelepathicEngine:
):
logger.info("⏭️ [Telepathic] Post is already Liked. Skipping.")
return {"x": None, "y": None, "score": 1.0, "semantic": "already_liked", "skip": True}
# Prevent un-following
if "follow" in intent_description.lower() and re.search(
r"\b(following|requested|folgst du|angefragt|gefolgt)\b",
best_node["semantic_string"].lower()
):
logger.info("⏭️ [Telepathic] User is already Followed/Requested. Skipping to prevent opening the Favorites menu.")
return {"x": None, "y": None, "score": 1.0, "semantic": "already_followed", "skip": True}
logger.info(f"✨ [Telepathic Match] '{intent_description}'{best_node['semantic_string']} (Score: {best_score:.3f})")
self._track_click(intent_description, best_node)
@@ -594,6 +623,44 @@ class TelepathicEngine:
# Click Tracking & Feedback Loop
# ──────────────────────────────────────────────
def _grid_fast_path(self, intent_description: str, viable_nodes: list, skip_positions: set = None) -> Optional[dict]:
"""
Deterministic grid navigation: filters for image_button nodes,
sorts by (y, x), and returns the topmost-leftmost candidate.
skip_positions: set of (x, y) tuples to skip on retry, ensuring
the Fast-Path doesn't re-click a position that already failed.
"""
if skip_positions is None:
skip_positions = set()
grid_nodes = [n for n in viable_nodes if "image_button" in n.get("resource_id", "")]
if not grid_nodes:
return None
# Sort by Y (topmost first), then X (leftmost first)
grid_nodes.sort(key=lambda n: (n["y"], n["x"]))
# Filter out previously-failed positions
for candidate in grid_nodes:
pos = (candidate["x"], candidate["y"])
if pos in skip_positions:
continue
logger.info(f"⚡ [Grid Fast-Path] Matched '{intent_description}'{candidate['semantic_string']} (y={candidate['y']})")
self._track_click(intent_description, candidate)
return {
"x": candidate["x"],
"y": candidate["y"],
"score": 0.98,
"semantic": candidate["semantic_string"],
"source": "grid_fastpath"
}
# All grid nodes were in skip_positions
logger.warning(f"⚠️ [Grid Fast-Path] All grid positions exhausted for '{intent_description}'. Falling through to VLM.")
return None
def _track_click(self, intent: str, node: dict):
"""Records what we're about to click so confirm/reject can reference it."""
TelepathicEngine._last_click_context = {
@@ -643,12 +710,6 @@ class TelepathicEngine:
Called by the interaction layer when the click did NOT produce the expected result.
Adds the mapping to the blacklist (negative learning) so it's never tried again.
Also removes it from positive memory if it was cached there.
Usage:
result = telepathic.find_best_node(xml, "tap comment button", device=device)
_humanized_click(device, result["x"], result["y"])
# ... verify comment sheet did NOT open ...
telepathic.reject_click("tap comment button")
"""
ctx = TelepathicEngine._last_click_context
if not ctx:
@@ -658,29 +719,33 @@ class TelepathicEngine:
sem = ctx["semantic_string"]
# ── Anti-Poisoning Guard ──
# A semantic string that contains NO text and NO description is too generic
# to blacklist. Example: "id context: 'image button'" matches ALL buttons
# in a grid, so blacklisting it would kill the entire explore page.
# NOTE: must use regex word boundary to avoid "context:" matching "text:"
# Structural UI elements (Home Tab, Like Button, etc.) should NEVER be globally blacklisted
# because they are essential for navigation and are unlikely to cause app-drift (only Ads do).
structural_intents = {"tap home tab", "tap reels tab", "tap explore tab", "tap newsfeed_tab", "tap like button", "tap comment_button", "tap share button"}
has_text = bool(re.search(r'(?<!\w)text:', sem.lower()))
has_desc = bool(re.search(r'(?<!\w)description:', sem.lower()))
is_generic = not has_text and not has_desc
is_structural = actual_intent in structural_intents
if is_generic:
if is_generic or is_structural:
reason = "generic semantic" if is_generic else "structural intent"
logger.warning(
f"⚠️ [Anti-Poisoning] Refusing to blacklist generic semantic: '{sem}'. "
f"It would poison all similar nodes. Skipping blacklist for '{actual_intent}'."
f"⚠️ [Anti-Poisoning] Refusing to blacklist {reason}: '{sem}'. "
f"Skipping global blacklist, but purging poisoned cache for '{actual_intent}'."
)
TelepathicEngine._last_click_context = None
return
else:
# Add to global blacklist
if actual_intent not in self._blacklist:
self._blacklist[actual_intent] = []
if sem not in self._blacklist[actual_intent]:
self._blacklist[actual_intent].append(sem)
self._save_json(BLACKLIST_FILE, self._blacklist)
logger.warning(f"🚫 [Negative Learning] Blacklisted: '{actual_intent}''{sem}'")
# Add to blacklist
if actual_intent not in self._blacklist:
self._blacklist[actual_intent] = []
if sem not in self._blacklist[actual_intent]:
self._blacklist[actual_intent].append(sem)
self._save_json(BLACKLIST_FILE, self._blacklist)
logger.warning(f"🚫 [Negative Learning] Blacklisted: '{actual_intent}''{sem}'")
# ── Always Purge Positive Cache ──
# Even if it's generic, if it failed for THIS specific intent, we MUST unlearn
# the positive memory cache so we don't infinitely retry it.
# Remove from positive memory if it was cached
if actual_intent in self._memory and sem in self._memory[actual_intent]:
@@ -738,17 +803,25 @@ class TelepathicEngine:
logger.warning("❌ [Semantic Verification] FAILED: Profile does not report 'Following' state.")
return False
if any(k in low_intent for k in ["explore grid", "profile grid", "first image"]):
if any(k in low_intent for k in ["explore grid", "profile grid", "first image", "grid item"]):
# Clicking a grid item MUST open a post view.
# Posts have feed markers. Reels have clips markers.
feed_markers = [
# Posts have feed markers. Reels/clips have their own markers.
post_markers = [
# Normal feed posts
"row_feed_button_like", "row_feed_button_comment", "row_feed_button_share",
"row_feed_comment_textview_layout", "row_feed_view_group",
"clips_media_component", "row_feed_photo_profile_name", "row_feed_photo_imageview"
"row_feed_photo_profile_name", "row_feed_photo_imageview",
# Reels / Clips
"clips_media_component", "clips_viewer", "clips_like_button",
"clips_comment_button", "reel_viewer", "clips_music_attribution",
# Carousel / Gallery
"carousel_page_indicator", "media_set_page_indicator",
# Generic post markers
"action_bar_original_title", "media_header_user",
]
marker_found = any(m in low_xml for m in feed_markers)
marker_found = any(m in low_xml for m in post_markers)
if marker_found:
logger.debug("✅ [Semantic Verification] Success confirmed: Post opened from grid (Feed markers detected).")
logger.debug("✅ [Semantic Verification] Success confirmed: Post/Reel opened from grid.")
return True
else:
logger.warning("❌ [Semantic Verification] FAILED: Grid tap did not open a valid post view.")
@@ -885,12 +958,25 @@ class TelepathicEngine:
})
return None
# ── Structural Guard 2: Position (status / nav bar) ──
# ── Structural Guard 2: Position (status / nav bar / tab zones) ──
if match.get("y", 0) < screen_height * STATUS_BAR_ZONE:
logger.error(f"❌ [Structural Guard] VLM selected element in status bar zone: {match['semantic_string']}. REJECTING.")
return None
is_nav_intent = any(k in intent.lower() for k in ["tab", "navigation", "search and explore", "reels", "profile", "home", "message"])
# NAVIGATION TAB ENFORCEMENT:
# Real navigation tabs (Home, Search, Reels, Store, Profile) are ALWAYS in the bottom zone.
# If the bot is looking for a tab, forbid results that are too high up (likely hallucinations on comments/feed).
if is_nav_intent:
# Tab intents MUST be in the bottom 10% (0.90) of the screen
if match.get("y", 0) < screen_height * 0.90:
logger.error(
f"❌ [Structural Guard] VLM hallucinated a navigation tab '{intent}' "
f"in the middle of the screen (Y={match.get('y')} | Screen={screen_height}). REJECTING."
)
return None
if match.get("y", 0) > screen_height * NAV_BAR_ZONE and not is_nav_intent:
logger.error(f"❌ [Structural Guard] VLM selected element in nav bar zone for non-nav intent '{intent}': {match['semantic_string']}. REJECTING.")
return None
@@ -920,3 +1006,42 @@ class TelepathicEngine:
logger.error(f"[Vision Cortex] Fallback failed: {e}")
return None
def _is_modal_active(self, nodes: list, raw_xml_string: str = "") -> bool:
"""
Detects if a dominant bottom sheet, dialog, or modal is covering the screen.
Scans both semantic nodes and the raw XML hierarchy for markers.
"""
import re
modal_res_ids = {
"com.instagram.android:id/bottom_sheet_container",
"com.instagram.android:id/modal_container",
"com.instagram.android:id/dialog_root",
"com.instagram.android:id/message_box_container",
"com.instagram.android:id/bottom_sheet_drag_handle",
"com.instagram.android:id/bottom_sheet_container_view",
"com.instagram.android:id/comment_composer_text_view"
}
# 1. Structural Regex Check (Fastest and catches 'empty' or non-interactable modals)
if raw_xml_string:
# Look for any of the resource IDs in the raw XML string
pattern = "|".join(modal_res_ids).replace(".", "\\.")
if re.search(pattern, raw_xml_string):
return True
# 2. Semantic Node Check (Iterative fallback)
for n in nodes:
# Direct resource-ID check
rid = n.get("resource-id", "")
if rid in modal_res_ids:
# Double check that it's actually visible/large
if n.get("visible", True) and n.get("area", 0) > 100000:
return True
# Semantic check (e.g., "Comments" title in a sheet)
if "bottom_sheet" in rid.lower() or "dialog" in rid.lower():
return True
return False

View File

@@ -15,8 +15,8 @@ def _humanized_scroll_down(device):
end_y = int(h * 0.2) + device.cm_to_pixels(random.uniform(-0.5, 0.5))
duration = random.uniform(0.08, 0.12)
device.deviceV2.swipe(start_x, start_y, start_x, end_y, duration)
from GramAddict.core.bot_flow import sleep
sleep(1.0)
from GramAddict.core.utils import random_sleep
random_sleep(0.8, 1.5)
def _run_zero_latency_unfollow_loop(device, zero_engine, nav_graph, configs, session_state, current_target, cognitive_stack):
"""
@@ -32,7 +32,8 @@ def _run_zero_latency_unfollow_loop(device, zero_engine, nav_graph, configs, ses
failed_scrolls = 0
total_unfollowed_this_session = 0
from GramAddict.core.bot_flow import sleep, dump_ui_state, _humanized_click
from GramAddict.core.bot_flow import dump_ui_state, _humanized_click
from GramAddict.core.utils import random_sleep
# Initialize basic tuple if it's missing (helps with tests and initializations)
if not hasattr(session_state, 'totalUnfollowed'):
@@ -53,7 +54,7 @@ def _run_zero_latency_unfollow_loop(device, zero_engine, nav_graph, configs, ses
return "BOREDOM_CHANGE_FEED"
try:
xml_dump = device.deviceV2.dump_hierarchy()
xml_dump = device.dump_hierarchy()
# Use Telepathic Engine to explicitly locate existing "Following" buttons in lists
nodes = telepathic._extract_semantic_nodes(xml_dump, "find 'Following' buttons next to usernames", threshold=0.7)
@@ -70,14 +71,14 @@ def _run_zero_latency_unfollow_loop(device, zero_engine, nav_graph, configs, ses
logger.debug(f"👆 Tapped following button at ({node['x']}, {node['y']})")
# Check for confirmation dialog ("Unfollow @username?")
sleep(1.5)
confirm_xml = device.deviceV2.dump_hierarchy()
random_sleep(1.0, 2.0)
confirm_xml = device.dump_hierarchy()
confirm_nodes = telepathic._extract_semantic_nodes(confirm_xml, "find 'Unfollow' confirmation button", threshold=0.8)
if confirm_nodes and not confirm_nodes[0].get("skip"):
c_node = confirm_nodes[0]
_humanized_click(device, c_node["x"], c_node["y"])
sleep(1.0)
random_sleep(0.8, 1.5)
logger.info("✅ [Unfollow Engine] Unfollowed a user in list.", extra={"color": Fore.GREEN})
session_state.totalUnfollowed += 1
@@ -86,7 +87,7 @@ def _run_zero_latency_unfollow_loop(device, zero_engine, nav_graph, configs, ses
# Unfollow cost logic
dopamine.boredom += random.uniform(1.0, 3.0)
sleep(2.0)
random_sleep(1.5, 3.0)
break
if not action_taken: