Skip to content

Commit 6eb01b8

Browse files
committed
Merge remote-tracking branch 'upstream/dev'
2 parents cdfa935 + 4f4f428 commit 6eb01b8

1 file changed

Lines changed: 50 additions & 39 deletions

File tree

instagram_monitor.py

Lines changed: 50 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,9 @@
223223
# Set to True to enable verbose output for HTTP jitter/back-off wrappers
224224
JITTER_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:
716719
BE_HUMAN_VERBOSE = False
717720
ENABLE_JITTER = False
718721
JITTER_VERBOSE = False
722+
SKIP_WRAP_MESSAGES = False
719723
FOLLOWERS_PER_BATCH = 0
720724
FOLLOWEES_PER_BATCH = 0
721725
FOLLOWER_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
789793
SECRET_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
795799
FUNCTION_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):
66196632
def 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

Comments
 (0)