@@ -3618,32 +3618,32 @@ def send_webhook(title, description, color=0x7289DA, fields=None, image_url=None
36183618 "inline": field.get("inline", False)
36193619 })
36203620
3621- sanitized_image_url = apply_privacy_substitutions( str(image_url) ) if image_url else ""
3621+ webhook_image_url = str(image_url) if image_url else ""
36223622
36233623 # Load all possible items into payload for use in formatting the WEBHOOK_TEMPLATE and WEBHOOK_HEADERS
36243624 payload = {
36253625 "title": title[:WEBHOOK_EMBED_TITLE_LIMIT] if title else "Instagram Monitor", # type: ignore
36263626 "description": description[:WEBHOOK_EMBED_DESCRIPTION_LIMIT] if description else "", # type: ignore
36273627 "version": VERSION,
3628- "image_url": sanitized_image_url ,
3628+ "image_url": webhook_image_url ,
36293629 "fields": sanitized_fields,
36303630 "fields_str": "\n".join([f"{f['name']}: {f['value']}" for f in sanitized_fields]) if sanitized_fields else "",
36313631 "color": color,
36323632 "timestamp": datetime.now(timezone.utc).isoformat()
36333633 }
36343634
3635- if sanitized_image_url :
3636- payload["image"] = {"url": sanitized_image_url }
3635+ if webhook_image_url :
3636+ payload["image"] = {"url": webhook_image_url }
36373637 elif local_image_file and os.path.isfile(local_image_file):
36383638 # If using local file, use attachment:// syntax
36393639 filename = os.path.basename(local_image_file)
36403640 payload["image"] = {"url": f"attachment://{filename}"}
36413641
36423642 if WEBHOOK_USERNAME:
3643- payload["username"] = apply_privacy_substitutions(str( WEBHOOK_USERNAME))
3643+ payload["username"] = WEBHOOK_USERNAME
36443644
36453645 if WEBHOOK_AVATAR_URL:
3646- payload["avatar_url"] = apply_privacy_substitutions(str( WEBHOOK_AVATAR_URL))
3646+ payload["avatar_url"] = WEBHOOK_AVATAR_URL
36473647
36483648 # Apply optional transformations to payload, primarily the title and description
36493649 for transform in WEBHOOK_TRANSFORMS: # type: ignore
@@ -6318,19 +6318,19 @@ def _update_progress_bar(resp):
63186318 thread_wrapper_count = getattr(_thread_local, 'WRAPPER_COUNT', 0)
63196319
63206320 # Determine which config vars to use based on fetch type and advanced fetch flags
6321- fetch_type = getattr(_thread_local, 'FETCH_TYPE', 'none')
6321+ # If no FETCH_TYPE is set (e.g. a future setup_pbar caller that does not flag a fetch kind),
6322+ # fall back to non-batched defaults so the wrapper still updates the bar via tqdm's rate
6323+ fetch_type = getattr(_thread_local, 'FETCH_TYPE', None)
63226324 if fetch_type == 'followee':
6323- per_batch = FOLLOWEES_PER_BATCH if ADVANCED_FOLLOWEE_FETCH else 0
6324- batch_delay = FOLLOWEE_DELAY_PER_BATCH if ADVANCED_FOLLOWEE_FETCH else 0
6325- limit = FOLLOWEE_LIMIT_TO_FETCH if ADVANCED_FOLLOWEE_FETCH else 0
6325+ per_batch = FOLLOWEES_PER_BATCH if ADVANCED_FOLLOWEE_FETCH else 0
6326+ batch_delay = FOLLOWEE_DELAY_PER_BATCH if ADVANCED_FOLLOWEE_FETCH else 0
63266327 advanced_fetch = ADVANCED_FOLLOWEE_FETCH
63276328 elif fetch_type == 'follower':
6328- per_batch = FOLLOWERS_PER_BATCH if ADVANCED_FOLLOWER_FETCH else 0
6329- batch_delay = FOLLOWER_DELAY_PER_BATCH if ADVANCED_FOLLOWER_FETCH else 0
6330- limit = FOLLOWER_LIMIT_TO_FETCH if ADVANCED_FOLLOWER_FETCH else 0
6329+ per_batch = FOLLOWERS_PER_BATCH if ADVANCED_FOLLOWER_FETCH else 0
6330+ batch_delay = FOLLOWER_DELAY_PER_BATCH if ADVANCED_FOLLOWER_FETCH else 0
63316331 advanced_fetch = ADVANCED_FOLLOWER_FETCH
63326332 else:
6333- return
6333+ per_batch, batch_delay, advanced_fetch = 0, 0, False
63346334 # Only process and count requests that are likely follower/following related
63356335 # Check if response is successful and JSON before processing
63366336 user_list = []
@@ -6408,10 +6408,7 @@ def _update_progress_bar(resp):
64086408 else:
64096409 elapsed_str = f"{elapsed_m:.1f}"
64106410
6411- # batch_info = f", fetch={limit}/{per_batch}/{batch_delay}s" if advanced_fetch else ""
6412- # stats_string = f"{names_per_req:.1f} names/req, reqs={thread_wrapper_count:d}, mins={elapsed_m:.1f}, remain={rem_m:.1f}{batch_info}"
64136411 stats_string = f"{names_per_req:.1f} names/req, reqs={thread_wrapper_count:d}, mins={elapsed_str}, remain={remain_str}"
6414- # debug_print(f"{fetched_so_far}/{d['total']} [{stats_string}]")
64156412 thread_pbar.unit = stats_string
64166413 thread_pbar.update(increment)
64176414
@@ -6818,6 +6815,7 @@ def simulate_human_actions(bot: instaloader.Instaloader, sleep_seconds: int) ->
68186815 print("* BeHuman: simulation stop")
68196816
68206817
6818+ # Formats an advanced follower/followee fetch config as a human-readable description
68216819def build_follow_string(enabled, limit, batch, delay, alt_format=False):
68226820 if enabled:
68236821 if limit and not batch and not delay:
@@ -6831,7 +6829,7 @@ def build_follow_string(enabled, limit, batch, delay, alt_format=False):
68316829 return follow_str
68326830
68336831
6834- def fetch_usernames_paginated(bot, get_generator_fn, max_per_batch, total_limit, fetch_delay, advanced_fetch, estimated_limit, user):
6832+ def fetch_usernames_paginated(bot, get_generator_fn, max_per_batch, total_limit, fetch_delay, advanced_fetch, estimated_limit, user, stop_event=None ):
68356833 """Fetch usernames in batches using a fresh generator per call.
68366834
68376835 Args:
@@ -6840,11 +6838,13 @@ def fetch_usernames_paginated(bot, get_generator_fn, max_per_batch, total_limit,
68406838 total_limit: Stop after this many total accounts (FOLLOWER_LIMIT_TO_FETCH / FOLLOWEE_LIMIT_TO_FETCH). 0 = no limit.
68416839 fetch_delay: Seconds to sleep between batches.
68426840 advanced_fetch: Indicates if advanced_fetch is enabled (valid configuration of above 3 items)
6843- estimated_limit: Estimated number of items to fetch. Used for messaging.
6841+ estimated_limit: Estimated number of itesm to fetch. Used for messaging.
68446842 user: Instagram username, forwarded to log_activity.
6843+ stop_event: Optional threading.Event from the caller. If set, the inter-batch wait is
6844+ aborted and the function returns whatever has been fetched so far.
68456845
68466846 Returns:
6847- List of username strings.
6847+ List of username strings (may be partial if stop_event fired) .
68486848 """
68496849 results = []
68506850 gen = get_generator_fn() # single generator — keeps cursor position across batches
@@ -6854,15 +6854,24 @@ def fetch_usernames_paginated(bot, get_generator_fn, max_per_batch, total_limit,
68546854 msg = f"Fetching {build_follow_string(advanced_fetch, estimated_limit, max_per_batch, fetch_delay, alt_format=True)}"
68556855 if thread_pbar:
68566856 thread_pbar.write(f"* {msg}", file=thread_pbar.fp)
6857- print(f"* {msg}") # if pbar, this will go to log, while the thread_pbar.write only goes to screen
6857+ print(f"* {msg}") # if pbar, this will go to log, while the thread_pbar.write only goes to screen
68586858 log_activity(msg, user=user)
68596859
68606860 while True:
6861+ # Abort between batches if the caller (e.g. Ctrl-C, web-dashboard stop) requested shutdown
6862+ # Returns whatever has been fetched so far rather than None so the caller's len()/iteration is safe
6863+ if stop_event is not None and stop_event.is_set():
6864+ return results
6865+
68616866 batch = []
68626867 for f in gen:
68636868 batch.append(f.username)
68646869 if advanced_fetch and max_per_batch and (len(batch) >= max_per_batch):
68656870 break # pause; generator retains its position
6871+ # Stop mid-batch if the overall total_limit would be exceeded, otherwise a per_batch
6872+ # larger than total_limit (e.g. limit=30, per_batch=50) over-fetches by a full batch
6873+ if advanced_fetch and total_limit and (len(results) + len(batch) >= total_limit):
6874+ break
68666875
68676876 if not batch:
68686877 break # generator fully exhausted
@@ -6874,16 +6883,13 @@ def fetch_usernames_paginated(bot, get_generator_fn, max_per_batch, total_limit,
68746883
68756884 # advanced fetching feature disabled or generator ran out mid-batch
68766885 if not advanced_fetch or (max_per_batch and len(batch) < max_per_batch):
6877- break
6886+ break
68786887
68796888 # advanced fetching feature enabled if here
68806889 if fetch_delay:
6881- # Use thread-local storage for multi-target safety
6882- stop_event = threading.Event()
68836890 # Interruptible wait (stop/recheck aware) similar to the main sleep loop
68846891 sleep_remaining = fetch_delay
6885- if thread_pbar:
6886- batch_info_orig = thread_pbar.unit
6892+ batch_info_orig = thread_pbar.unit if thread_pbar else ""
68876893 while sleep_remaining > 0:
68886894 if thread_pbar:
68896895 # need to remove part of string to make room, since entire PBAR needs to fit within HORIZONTAL_LINE width (safe_ncols)
@@ -6892,24 +6898,23 @@ def fetch_usernames_paginated(bot, get_generator_fn, max_per_batch, total_limit,
68926898 batch_info = re.sub(r" - PAUSED.*$", "", batch_info) + f" - PAUSED for {sleep_remaining}s"
68936899 thread_pbar.unit = batch_info
68946900 thread_pbar.refresh()
6895- if stop_event and stop_event.is_set():
6896- return
6897- # Allow Web Dashboard "recheck" to break the wait early (still hour-gated later)
6901+ if stop_event is not None and stop_event.is_set():
6902+ return results
6903+ # If a Web Dashboard "recheck" is pending, shorten the current inter-batch wait so the
6904+ # in-progress fetch completes sooner. Do not consume the event here: the recheck applies
6905+ # to the next check cycle and is handled by the main loop after this fetch returns
6906+ recheck_pending = False
68986907 if WEB_DASHBOARD_ENABLED:
68996908 with WEB_DASHBOARD_DATA_LOCK: # type: ignore
69006909 if user in WEB_DASHBOARD_RECHECK_EVENTS and WEB_DASHBOARD_RECHECK_EVENTS[user].is_set():
6901- WEB_DASHBOARD_RECHECK_EVENTS[user].clear()
6902- manual_recheck_active = True
6903- manual_override_active = True
6904- log_activity("Manual recheck requested", user=user, level='system')
6905- update_ui_data(targets={user: {'status': 'Recheck requested'}})
6906- break
6910+ recheck_pending = True
69076911 # Check for proxy changes from web dashboard
69086912 refresh_proxy_if_needed(bot, user)
6913+ if recheck_pending:
6914+ break
69096915
69106916 wait_chunk = min(1, sleep_remaining)
6911- # debug_print(wait_chunk)
6912- if stop_event:
6917+ if stop_event is not None:
69136918 stop_event.wait(wait_chunk)
69146919 else:
69156920 time.sleep(wait_chunk)
@@ -7411,6 +7416,7 @@ def _get_iphone_json(path, params, **kwargs):
74117416 advanced_fetch=ADVANCED_FOLLOWER_FETCH,
74127417 estimated_limit=follower_limit,
74137418 user=user,
7419+ stop_event=stop_event,
74147420 )
74157421 _thread_local.FETCH_TYPE = None
74167422 end_time_dl = time.time()
@@ -7558,6 +7564,7 @@ def _get_iphone_json(path, params, **kwargs):
75587564 advanced_fetch=ADVANCED_FOLLOWEE_FETCH,
75597565 estimated_limit=followee_limit,
75607566 user=user,
7567+ stop_event=stop_event,
75617568 )
75627569 _thread_local.FETCH_TYPE = None
75637570 end_time_dl = time.time()
@@ -8448,6 +8455,7 @@ def _get_iphone_json(path, params, **kwargs):
84488455 advanced_fetch=ADVANCED_FOLLOWEE_FETCH,
84498456 estimated_limit=followee_limit,
84508457 user=user,
8458+ stop_event=stop_event,
84518459 )
84528460 _thread_local.FETCH_TYPE = None
84538461 followings_to_save = []
@@ -8607,6 +8615,7 @@ def _get_iphone_json(path, params, **kwargs):
86078615 advanced_fetch=ADVANCED_FOLLOWER_FETCH,
86088616 estimated_limit=follower_limit,
86098617 user=user,
8618+ stop_event=stop_event,
86108619 )
86118620 _thread_local.FETCH_TYPE = None
86128621 followers_to_save = []
@@ -10199,17 +10208,17 @@ def run_main():
1019910208
1020010209 # Advanced Follower/Followee Fetching Settings
1020110210 if any([FOLLOWERS_PER_BATCH, FOLLOWER_LIMIT_TO_FETCH, FOLLOWER_DELAY_PER_BATCH]):
10202- ADVANCED_FOLLOWER_FETCH = ( FOLLOWERS_PER_BATCH and FOLLOWER_DELAY_PER_BATCH) or (FOLLOWER_LIMIT_TO_FETCH and not FOLLOWERS_PER_BATCH and not FOLLOWER_DELAY_PER_BATCH)
10211+ ADVANCED_FOLLOWER_FETCH = bool(( FOLLOWERS_PER_BATCH and FOLLOWER_DELAY_PER_BATCH) or (FOLLOWER_LIMIT_TO_FETCH and not FOLLOWERS_PER_BATCH and not FOLLOWER_DELAY_PER_BATCH) )
1020310212 if not ADVANCED_FOLLOWER_FETCH:
1020410213 print(f"* Error: Invalid configuration for advanced follower fetching: FOLLOWER_LIMIT_TO_FETCH: {FOLLOWER_LIMIT_TO_FETCH}, FOLLOWERS_PER_BATCH: {FOLLOWERS_PER_BATCH}, FOLLOWER_DELAY_PER_BATCH: {FOLLOWER_DELAY_PER_BATCH}")
1020510214 sys.exit(1)
1020610215
1020710216 if any([FOLLOWEES_PER_BATCH, FOLLOWEE_LIMIT_TO_FETCH, FOLLOWEE_DELAY_PER_BATCH]):
10208- ADVANCED_FOLLOWEE_FETCH = ( FOLLOWEES_PER_BATCH and FOLLOWEE_DELAY_PER_BATCH) or (FOLLOWEE_LIMIT_TO_FETCH and not FOLLOWEES_PER_BATCH and not FOLLOWEE_DELAY_PER_BATCH)
10217+ ADVANCED_FOLLOWEE_FETCH = bool(( FOLLOWEES_PER_BATCH and FOLLOWEE_DELAY_PER_BATCH) or (FOLLOWEE_LIMIT_TO_FETCH and not FOLLOWEES_PER_BATCH and not FOLLOWEE_DELAY_PER_BATCH) )
1020910218 if not ADVANCED_FOLLOWEE_FETCH:
1021010219 print(f"* Error: Invalid configuration for advanced followee fetching: FOLLOWEE_LIMIT_TO_FETCH: {FOLLOWEE_LIMIT_TO_FETCH}, FOLLOWEES_PER_BATCH: {FOLLOWEES_PER_BATCH}, FOLLOWEE_DELAY_PER_BATCH: {FOLLOWEE_DELAY_PER_BATCH}")
1021110220 sys.exit(1)
10212-
10221+
1021310222 # Handle new debug, dashboard, and webhook arguments
1021410223 if args.debug_mode is True:
1021510224 DEBUG_MODE = True
0 commit comments