223223# Set to True to enable verbose output for HTTP jitter/back-off wrappers
224224JITTER_VERBOSE = False
225225
226+ # Set to True to skip the per-request WRAP-REQ/WRAP-SEND log lines from the HTTP jitter/back-off wrappers
227+ # These can be overwhelming in debug or jitter-verbose mode and are not needed in most cases
228+ SKIP_WRAP_MESSAGES = False
226229# Optional Follower and Followee Adjustments
227230#
228231# This allows control of the fetching of followers and followees, which may be beneficial in avoiding account flagging by Instagram.
@@ -716,6 +719,7 @@ def generate_config_with_current_values() -> str:
716719BE_HUMAN_VERBOSE = False
717720ENABLE_JITTER = False
718721JITTER_VERBOSE = False
722+ SKIP_WRAP_MESSAGES = False
719723FOLLOWERS_PER_BATCH = 0
720724FOLLOWEES_PER_BATCH = 0
721725FOLLOWER_LIMIT_TO_FETCH = 0
@@ -788,8 +792,8 @@ def generate_config_with_current_values() -> str:
788792# List of secret keys to load from env/config
789793SECRET_KEYS = ("SESSION_PASSWORD", "SMTP_PASSWORD", "WEBHOOK_URL", "PROXY_URL")
790794
791- # List of error messages indicating an Instagram has been flagged for 'automation' or 'botting'
792- FLAGGED_TRIGGERS = ("detected automated checks", "ProfileNotExistsException", "cannot access local variable", " checkpoint_required")
795+ # List of error substrings that unambiguously indicate the session account or IP has been flagged (challenge/checkpoint/shadowban)
796+ FLAGGED_TRIGGERS = ("detected automated checks", "checkpoint_required")
793797
794798# Default value for network-related timeouts in functions
795799FUNCTION_TIMEOUT = 15
@@ -3755,11 +3759,21 @@ def send_webhook(title, description, color=0x7289DA, fields=None, image_url=None
37553759 return 1
37563760
37573761
3762+ # Sleeps for the given seconds but returns True early if stop_event becomes set
3763+ def interruptible_sleep(seconds, stop_event=None):
3764+ if stop_event is None:
3765+ time.sleep(seconds)
3766+ return False
3767+ return stop_event.wait(seconds)
3768+
3769+
37583770# Fetches the current outbound IP via IP_ADDRESS_URL, tolerating JSON and plain-text responses
3759- def get_ip_address(max_retries=5, timeout=10, retry_delay=5, long_retry=120, long_retry_attempts=3):
3771+ def get_ip_address(max_retries=5, timeout=10, retry_delay=5, long_retry=120, long_retry_attempts=3, stop_event=None ):
37603772 last_err = None
37613773 for long_attempt in range(1, long_retry_attempts + 1):
37623774 for attempt in range(1, max_retries + 1):
3775+ if stop_event is not None and stop_event.is_set():
3776+ return f"(unavailable: {format_error_message(last_err) if last_err else 'stopped'})"
37633777 try:
37643778 ip_response = req.get(IP_ADDRESS_URL, timeout=timeout, verify=get_proxies_ssl(), proxies=get_proxies())
37653779 ip_response.raise_for_status()
@@ -3778,15 +3792,14 @@ def get_ip_address(max_retries=5, timeout=10, retry_delay=5, long_retry=120, lon
37783792 raise ValueError(f"empty response body from {IP_ADDRESS_URL}")
37793793 except Exception as e:
37803794 last_err = e
3781- if attempt < max_retries:
3782- time.sleep(retry_delay)
3783- else:
3784- debug_print(f"get_ip_address failed after {max_retries} attempts: {e}")
3795+ if attempt < max_retries and interruptible_sleep(retry_delay, stop_event):
3796+ return f"(unavailable: {format_error_message(last_err)})"
37853797 if long_attempt < long_retry_attempts:
3786- debug_print(f"get_ip_address retrying in {long_retry} seconds")
3787- time.sleep(long_retry)
3798+ debug_print(f"get_ip_address: all {max_retries} attempts failed in loop {long_attempt}/{long_retry_attempts}, retrying in {long_retry} seconds: {last_err}")
3799+ if interruptible_sleep(long_retry, stop_event):
3800+ return f"(unavailable: {format_error_message(last_err) if last_err else 'stopped'})"
37883801 else:
3789- debug_print(f"get_ip_address failed after {long_retry_attempts} loops")
3802+ debug_print(f"get_ip_address failed after {long_retry_attempts} loops of {max_retries} attempts: {last_err} ")
37903803 return f"(unavailable: {format_error_message(last_err) if last_err else 'unknown error'})"
37913804
37923805
@@ -4782,7 +4795,6 @@ def check_posts_counts(user, posts_count, posts_count_old, r_sleep_time):
47824795 send_email(m_subject, m_body, m_body_html, SMTP_SSL)
47834796
47844797 # Send webhook notification for posts count change
4785-
47864798 if posts_count is not None and posts_count_old is not None:
47874799 diff = posts_count - posts_count_old
47884800 diff_str = f" ({'+' if diff > 0 else ''}{diff})"
@@ -4791,7 +4803,7 @@ def check_posts_counts(user, posts_count, posts_count_old, r_sleep_time):
47914803
47924804 send_webhook(
47934805 f"📮 {user} Posts Count Changed",
4794- f"User **{user}** posts count changed from **{posts_count_old}** to **{posts_count}** {diff_str}",
4806+ f"User **{user}** posts count changed from **{posts_count_old}** to **{posts_count}**{diff_str}",
47954807 color=0x34495e, # Dark Blue
47964808 notification_type="status"
47974809 )
@@ -4959,14 +4971,15 @@ def import_session(cookiefile, sessionfile):
49594971 else:
49604972 instaloader.save_session_to_file()
49614973
4962- # The sequence is: \033[ + {code} + m
4963- RED = f"\033[{_STYLE_CODES['red']}m"
4964- RESET = "\033[0m "
4974+ # Emit the warning in red only when colour output is enabled and supported, otherwise plain text
4975+ RED = f"\033[{_STYLE_CODES['red']}m" if COLOR_ENABLED else ""
4976+ RESET = ANSI_RESET if COLOR_ENABLED else " "
49654977
49664978 print("")
49674979 print(f"{RED}*********************************************************************{RESET}")
4968- print(f"{RED} Clear Instagram cookies in Firefox now to avoid duplicate activity. {RESET}")
4969- print(f"{RED} Otherwise, the session account may get flagged by Instagram. {RESET}")
4980+ print(f"{RED} Do not use Instagram in Firefox while the script is running. {RESET}")
4981+ print(f"{RED} Simultaneous browser and tool activity can get the account flagged. {RESET}")
4982+ print(f"{RED} Tip: you might want to clear Instagram cookies in Firefox now. {RESET}")
49704983 print(f"{RED}*********************************************************************{RESET}")
49714984
49724985
@@ -6619,7 +6632,6 @@ def sleep_message(sleeptime, user=None):
66196632def format_error_message(e: Exception) -> str:
66206633 error_str = str(e)
66216634 error_type = type(e).__name__
6622- # debug_print(f"Formatting error message for {error_type}: {error_str}")
66236635
66246636 # Check for KeyError related to 'data' key - indicates Instagram challenge/shadow ban
66256637 if error_type == "KeyError" and ("'data'" in error_str or '"data"' in error_str or error_str == "data"):
@@ -7244,7 +7256,6 @@ def _get_iphone_json(path, params, **kwargs):
72447256 err_str = f"Session account '{SESSION_USERNAME or '<anonymous>'}' has been flagged. Log into Instagram and clear warnings."
72457257 update_ui_data(targets={user: {'status': f'Paused: {err_str}'}})
72467258 print(f"* Error: {err_str}")
7247-
72487259 # Pause all other threads once the session account is flagged.
72497260 if WEB_DASHBOARD_ENABLED or DASHBOARD_ENABLED:
72507261 for other_user in list(WEB_DASHBOARD_STOP_EVENTS.keys()):
@@ -7254,17 +7265,17 @@ def _get_iphone_json(path, params, **kwargs):
72547265 stop_monitoring_for_target(other_user)
72557266 update_ui_data(targets={other_user: {'status': f'Paused: {err_str}'}})
72567267 # Update next_check status for this thread
7257- NEXT_CHECK_TIME = None
7268+ NEXT_CHECK_TIME = None
72587269 NEXT_CHECK_DISPLAY = "Paused"
72597270 update_check_times(next_time="Paused", user=user, increment_count=False)
7260- # Pause this thread also
72617271 log_activity("Stopping monitoring", user=user)
7262- print_cur_ts("\nTimestamp:\t\t\t\t")
7263- else:
7264- print_cur_ts("\nTimestamp:\t\t\t\t")
7272+ print_cur_ts("\nTimestamp:\t\t\t\t")
7273+
7274+ # Without the Web Dashboard there is no in-place session recovery so exit since the flagged session is dead for every target
7275+ if not WEB_DASHBOARD_ENABLED:
72657276 signal_handler(signal.SIGINT, None, message='')
72667277
7267- # Wait for session refresh or stop event
7278+ # Web Dashboard can re-import a session and resume, so wait for that or a stop event
72687279 if WEB_DASHBOARD_ENABLED:
72697280 while not (stop_event and stop_event.is_set()):
72707281 if SESSION_REFRESHED_EVENT.wait(timeout=1.0):
@@ -8094,7 +8105,7 @@ def _get_iphone_json(path, params, **kwargs):
80948105
80958106 # Show proxy IP at per-user startup only in verbose/debug (the run_main banner already shows it once)
80968107 if PROXY_ENABLED and (VERBOSE_MODE or DEBUG_MODE):
8097- ipaddr = get_ip_address()
8108+ ipaddr = get_ip_address(stop_event=stop_event )
80988109 print(f"* Proxy IP address is {ipaddr}")
80998110
81008111 # Monitoring active message
@@ -8184,7 +8195,7 @@ def _get_iphone_json(path, params, **kwargs):
81848195 # Debug/Verbose: show check start
81858196 ip_str = ""
81868197 if PROXY_ENABLED and (VERBOSE_MODE or DEBUG_MODE):
8187- ipaddr = get_ip_address()
8198+ ipaddr = get_ip_address(stop_event=stop_event )
81888199 ip_str = f" with proxy IP address of {ipaddr}"
81898200 if VERBOSE_MODE:
81908201 print(f"* Starting check #{CHECK_COUNT} for {user} ...{ip_str}")
@@ -8330,11 +8341,9 @@ def _get_iphone_json(path, params, **kwargs):
83308341 # Handle session recovery for automated checks/challenge errors
83318342 if any(t in error_msg for t in FLAGGED_TRIGGERS):
83328343 err_str = f"Session account '{SESSION_USERNAME or '<anonymous>'}' has been flagged. Log into Instagram and clear warnings."
8333- for i in range(50):
8334- log_activity(f"({i}) - {err_str}", user=user)
83358344 update_ui_data(targets={user: {'status': f'Paused: {err_str}'}})
83368345 print(f"* Error: {err_str}")
8337-
8346+
83388347 # Pause all other threads once the session account is flagged.
83398348 if WEB_DASHBOARD_ENABLED or DASHBOARD_ENABLED:
83408349 for other_user in list(WEB_DASHBOARD_STOP_EVENTS.keys()):
@@ -8344,17 +8353,17 @@ def _get_iphone_json(path, params, **kwargs):
83448353 stop_monitoring_for_target(other_user)
83458354 update_ui_data(targets={other_user: {'status': f'Paused: {err_str}'}})
83468355 # Update next_check status for this thread
8347- NEXT_CHECK_TIME = None
8356+ NEXT_CHECK_TIME = None
83488357 NEXT_CHECK_DISPLAY = "Paused"
83498358 update_check_times(next_time="Paused", user=user, increment_count=False)
8350- # Pause this thread also
83518359 log_activity("Stopping monitoring", user=user)
8352- print_cur_ts("\nTimestamp:\t\t\t\t")
8353- else:
8354- print_cur_ts("\nTimestamp:\t\t\t\t")
8360+ print_cur_ts("\nTimestamp:\t\t\t\t")
8361+
8362+ # Without the Web Dashboard there is no in-place session recovery so exit since the flagged session is dead for every target
8363+ if not WEB_DASHBOARD_ENABLED:
83558364 signal_handler(signal.SIGINT, None, message='')
83568365
8357- # Wait for session refresh or stop event
8366+ # Web Dashboard can re-import a session and resume, so wait for that or a stop event
83588367 while not (stop_event and stop_event.is_set()):
83598368 if SESSION_REFRESHED_EVENT.wait(timeout=1.0):
83608369 # Session refreshed!
@@ -8590,7 +8599,8 @@ def _get_iphone_json(path, params, **kwargs):
85908599 m_body_html = f"Followings number changed by user <b>{user}</b> from <b>{followings_old_count}</b> to <b>{followings_count}</b> ({followings_diff_str})<br><br>Check interval: <b>{display_time(r_sleep_time)}</b> ({get_range_of_dates_from_tss(int(time.time()) - r_sleep_time, int(time.time()), short=True)}){get_cur_ts('<br>Timestamp: ')}"
85918600 send_email(m_subject, m_body, m_body_html, SMTP_SSL)
85928601
8593- # Send webhook notification for followings change
8602+ # Send webhook notification for followings change (independent of email notifications) only if something changed
8603+ if followings_count != followings_old_count or added_followings_list or removed_followings_list:
85948604 webhook_result = send_follower_change_webhook(
85958605 user, "followings", followings_old_count, followings_count,
85968606 added_followings_list_webhook, removed_followings_list_webhook
@@ -8746,7 +8756,8 @@ def _get_iphone_json(path, params, **kwargs):
87468756 m_body_html = f"Followers number changed for user <b>{user}</b> from <b>{followers_old_count}</b> to <b>{followers_count}</b> ({followers_diff_str})<br><br>Check interval: <b>{display_time(r_sleep_time)}</b> ({get_range_of_dates_from_tss(int(time.time()) - r_sleep_time, int(time.time()), short=True)}){get_cur_ts('<br>Timestamp: ')}"
87478757 send_email(m_subject, m_body, m_body_html, SMTP_SSL)
87488758
8749- # Send webhook notification for followers change
8759+ # Send webhook notification for followers change (independent of email notifications) only if something changed
8760+ if followers_count != followers_old_count or added_followers_list or removed_followers_list:
87508761 webhook_result = send_follower_change_webhook(
87518762 user, "followers", followers_old_count, followers_count,
87528763 added_followers_list_webhook, removed_followers_list_webhook
@@ -10965,7 +10976,7 @@ def _runner(u: str, delay_s: int, idx: int, stop_event: Optional[threading.Event
1096510976 finally:
1096610977 # next code line added per Claude to fix 'deadlock' in multi-threaded mode when an account gets flagged and goes idle
1096710978 # reason: loading_events never signaled on early return (note: event.set() is idempotent so calling it twice is harmless)
10968- loading_events[idx + 1].set()
10979+ loading_events[idx + 1].set()
1096910980 with WEB_DASHBOARD_DATA_LOCK: # type: ignore
1097010981 if u in WEB_DASHBOARD_RECHECK_EVENTS:
1097110982 del WEB_DASHBOARD_RECHECK_EVENTS[u]
0 commit comments