diff --git a/blacklist.txt b/blacklist.txt index ae4e6b2..b0ba43b 100644 --- a/blacklist.txt +++ b/blacklist.txt @@ -1,2 +1,5 @@ +# Important: This file is shared as blacklist file across all of the accounts you run insomniac with. +# If you wish to create a blacklist file thats dedicated to a specific accout, copy this file and name it -blacklist.txt (replace with your profile-name) + username1 username2 diff --git a/docs/README.md b/docs/README.md index ca739ce..0eef1c8 100644 --- a/docs/README.md +++ b/docs/README.md @@ -262,6 +262,10 @@ number (e.g. 4) or a range (e.g. 3-8) ### Special features Other features that are unblocked by [joining Patreon $10 tier](https://www.patreon.com/join/insomniac_bot): +#### --warmup-time-before-session 2-6 +Set warmup length in minutes, disabled by default. +It can be a number (e.g. 2) or a range (e.g. 1-3). + #### --remove-mass-followers 10-20 Remove given number of mass followers from the list of your followers. "Mass followers" are those who has diff --git a/insomniac/__version__.py b/insomniac/__version__.py index 864e9df..3b88bb1 100644 --- a/insomniac/__version__.py +++ b/insomniac/__version__.py @@ -13,7 +13,7 @@ __title__ = 'insomniac' __description__ = 'Simple Instagram bot for automated Instagram interaction using Android.' __url__ = 'https://github.com/alexal1/Insomniac/' -__version__ = '3.7.10' +__version__ = '3.7.11' __debug_mode__ = False __author__ = 'Insomniac Team' __author_email__ = 'info@insomniac-bot.com' diff --git a/insomniac/action_get_my_profile_info.py b/insomniac/action_get_my_profile_info.py index 039e522..29cc150 100644 --- a/insomniac/action_get_my_profile_info.py +++ b/insomniac/action_get_my_profile_info.py @@ -49,6 +49,7 @@ def get_my_profile_info(device, username): report_string = "" if username: + username = username.strip() report_string += "Hello, @" + username + "! " if followers is not None: report_string += "You have " + str(followers) + " followers" diff --git a/insomniac/action_runners/interact/action_handle_place.py b/insomniac/action_runners/interact/action_handle_place.py index 60f793a..f8b5edf 100644 --- a/insomniac/action_runners/interact/action_handle_place.py +++ b/insomniac/action_runners/interact/action_handle_place.py @@ -54,6 +54,23 @@ def handle_place(device, is_limit_reached, is_passed_filters, action_status): + if not search_for(device, place=place, on_action=on_action): + return None + + # Switch to Recent tab if needed + if instructions == PlaceInteractionType.RECENT_LIKERS or instructions == PlaceInteractionType.RECENT_POSTS: + sleeper.random_sleep() + print("Switching to Recent tab") + tab_layout = device.find(resourceId=f'{device.app_id}:id/tab_layout', + className='android.widget.LinearLayout') + if tab_layout.exists(): + tab_layout.child(index=1).click() + else: + print("Can't Find recent tab. Interacting with Popular.") + + # Sleep longer because posts loading takes time + sleeper.random_sleep(multiplier=2.0) + source_type = f'{SourceType.PLACE.value}-{instructions.value}' interaction = partial(interact_with_user, device=device, @@ -215,22 +232,6 @@ def interact_with_profile(liker_username, liker_username_view): return can_continue def navigate_to_feed(): - if not search_for(device, place=place, on_action=on_action): - return None - - # Switch to Recent tab - if instructions == PlaceInteractionType.RECENT_LIKERS or instructions == PlaceInteractionType.RECENT_POSTS: - print("Switching to Recent tab") - tab_layout = device.find(resourceId=f'{device.app_id}:id/tab_layout', - className='android.widget.LinearLayout') - if tab_layout.exists(): - tab_layout.child(index=1).click() - else: - print("Can't Find recent tab. Interacting with Popular.") - - # Sleep longer because posts loading takes time - sleeper.random_sleep(multiplier=2.0) - # Open post posts_view_list = PostsGridView(device).open_random_post() if posts_view_list is None: @@ -296,22 +297,6 @@ def extract_place_likers_and_interact(device, on_action): print("Interacting with place-{0}-{1}".format(place, instructions.value)) - if not search_for(device, place=place, on_action=on_action): - return - - # Switch to Recent tab - if instructions == PlaceInteractionType.RECENT_LIKERS or instructions == PlaceInteractionType.RECENT_POSTS: - print("Switching to Recent tab") - tab_layout = device.find(resourceId=f'{device.app_id}:id/tab_layout', - className='android.widget.LinearLayout') - if tab_layout.exists(): - tab_layout.child(index=1).click() - else: - print("Can't Find recent tab. Interacting with Popular.") - - # Sleep longer because posts loading takes time - sleeper.random_sleep(multiplier=2.0) - # Open post posts_view_list = navigate_to_feed() if posts_view_list is None: diff --git a/insomniac/navigation.py b/insomniac/navigation.py index 45560ac..8128cce 100644 --- a/insomniac/navigation.py +++ b/insomniac/navigation.py @@ -1,3 +1,4 @@ +from insomniac.sleeper import sleeper from insomniac.utils import * from insomniac.views import TabBarView, ProfileView, TabBarTabs, LanguageNotEnglishException @@ -16,7 +17,18 @@ def navigate(device, tab, switch_to_english_on_exception=True): def search_for(device, username=None, hashtag=None, place=None, on_action=None): - search_view = TabBarView(device).navigate_to_search() + tab_bar_view = TabBarView(device) + + # There may be no TabBarView if Instagram was opened via a deeplink. Then we have to clear the backstack. + is_message_printed = False + while not tab_bar_view.is_visible(): + if not is_message_printed: + print(COLOR_OKGREEN + "Clearing the back stack..." + COLOR_ENDC) + is_message_printed = True + tab_bar_view.press_back_arrow() + sleeper.random_sleep() + + search_view = tab_bar_view.navigate_to_search() target_view = None if username is not None: diff --git a/insomniac/storage.py b/insomniac/storage.py index a1ec345..bd2de9b 100644 --- a/insomniac/storage.py +++ b/insomniac/storage.py @@ -58,6 +58,7 @@ def wrap(*args, **kwargs): class Storage: + my_username = None profile = None scrape_for_account_list = [] recheck_follow_status_after = None @@ -71,6 +72,7 @@ class Storage: def _reset_state(self): global IS_USING_DATABASE IS_USING_DATABASE = False + self.my_username = None self.profile = None self.scrape_for_account_list = [] self.recheck_follow_status_after = None @@ -93,6 +95,7 @@ def __init__(self, my_username, args): global IS_USING_DATABASE IS_USING_DATABASE = True + self.my_username = my_username self.profile = get_ig_profile_by_profile_name(my_username) scrape_for_account = args.__dict__.get('scrape_for_account', []) self.scrape_for_account_list = scrape_for_account if isinstance(scrape_for_account, list) else [scrape_for_account] @@ -108,17 +111,30 @@ def __init__(self, my_username, args): blacklist_from_parameters = args.__dict__.get('blacklist_profiles', None) # Whitelist and Blacklist - try: - with open(FILENAME_WHITELIST, encoding="utf-8") as file: - self.whitelist = [line.rstrip() for line in file] - except FileNotFoundError: - print_debug("No whitelist provided") - try: - with open(FILENAME_BLACKLIST, encoding="utf-8") as file: - self.blacklist = [line.rstrip() for line in file] - except FileNotFoundError: - print_debug("No blacklist provided") + whitelist_files = { + FILENAME_WHITELIST: "global-whitelist", + f"{self.my_username}-{FILENAME_WHITELIST}": "profile-whitelist" + } + + blacklist_files = { + FILENAME_BLACKLIST: "global-blacklist", + f"{self.my_username}-{FILENAME_BLACKLIST}": "profile-blacklist" + } + + for file_path, file_desc in whitelist_files.items(): + try: + with open(file_path, encoding="utf-8") as file: + self.whitelist.extend([line.rstrip() for line in file if not line.startswith("#")]) + except FileNotFoundError: + print_debug(f"No {file_desc} file provided") + + for file_path, file_desc in blacklist_files.items(): + try: + with open(file_path, encoding="utf-8") as file: + self.blacklist.extend([line.rstrip() for line in file if not line.startswith("#")]) + except FileNotFoundError: + print_debug(f"No {file_desc} file provided") if whitelist_from_parameters is not None: if isinstance(whitelist_from_parameters, list) and len(whitelist_from_parameters) > 0: @@ -289,32 +305,37 @@ def _get_target(self): except IndexError: pass - # From file - try: - with open(FILENAME_TARGETS, "r+", encoding="utf-8") as file: - lines = [line.rstrip() for line in file] - - for i, line in enumerate(lines): - # Skip comments - if line.startswith("#"): - continue - - # Skip already interacted - if "DONE" in line: - continue - - data = line.strip() - if data.startswith("https://"): - target_type = TargetType.URL - else: - target_type = TargetType.USERNAME - lines[i] += " - DONE" - file.truncate(0) - file.seek(0) - file.write("\n".join(lines)) - return data, target_type - except FileNotFoundError: - pass + targets_files = { + FILENAME_TARGETS: "global-targets", + f"{self.my_username}-{FILENAME_TARGETS}": "profile-targets" + } + + for file_path, file_desc in targets_files.items(): + try: + with open(file_path, "r+", encoding="utf-8") as file: + lines = [line.rstrip() for line in file] + + for i, line in enumerate(lines): + # Skip comments + if line.startswith("#"): + continue + + # Skip already interacted + if "DONE" in line: + continue + + data = line.strip() + if data.startswith("https://"): + target_type = TargetType.URL + else: + target_type = TargetType.USERNAME + lines[i] += " - DONE" + file.truncate(0) + file.seek(0) + file.write("\n".join(lines)) + return data, target_type + except FileNotFoundError: + pass # From scrapping scrapped_profile = self.profile.get_scrapped_profile_for_interaction() @@ -330,22 +351,30 @@ def is_user_in_blacklist(self, username): def _count_targets_from_file(self): count = 0 - try: - with open(FILENAME_TARGETS, encoding="utf-8") as file: - lines = [line.rstrip() for line in file] - - for i, line in enumerate(lines): - # Skip comments - if line.startswith("#"): - continue - - # Skip already interacted - if "DONE" in line: - continue - count += 1 - except FileNotFoundError: - pass + # From file + targets_files = { + FILENAME_TARGETS: "global-targets", + f"{self.my_username}-{FILENAME_TARGETS}": "profile-targets" + } + + for file_path, file_desc in targets_files.items(): + try: + with open(file_path, encoding="utf-8") as file: + lines = [line.rstrip() for line in file] + + for i, line in enumerate(lines): + # Skip comments + if line.startswith("#"): + continue + + # Skip already interacted + if "DONE" in line: + continue + + count += 1 + except FileNotFoundError: + pass return count diff --git a/insomniac/utils.py b/insomniac/utils.py index 4144d04..3bf8d2e 100644 --- a/insomniac/utils.py +++ b/insomniac/utils.py @@ -35,6 +35,8 @@ ENGINE_LOGS_DIR_NAME = 'logs' UI_LOGS_DIR_NAME = 'ui-logs' +APP_REOPEN_WARNING = "Warning: Activity not started, intent has been delivered to currently running top-most instance." + def get_instagram_version(device_id, app_id): stream = os.popen("adb" + ("" if device_id is None else " -s " + device_id) + @@ -121,7 +123,7 @@ def open_instagram(device_id, app_id): cmd_res = subprocess.run(cmd, stdout=PIPE, stderr=PIPE, shell=True, encoding="utf8") err = cmd_res.stderr.strip() - if err: + if err and err != APP_REOPEN_WARNING: print(COLOR_FAIL + err + COLOR_ENDC) @@ -132,7 +134,7 @@ def open_instagram_with_url(device_id, app_id, url): cmd_res = subprocess.run(cmd, stdout=PIPE, stderr=PIPE, shell=True, encoding="utf8") err = cmd_res.stderr.strip() - if err: + if err and err != APP_REOPEN_WARNING: print(COLOR_FAIL + err + COLOR_ENDC) return False diff --git a/insomniac/views.py b/insomniac/views.py index 7611de3..ea86ed2 100644 --- a/insomniac/views.py +++ b/insomniac/views.py @@ -95,6 +95,9 @@ def __init__(self, device: DeviceFacade): super().__init__(device) self.top = None + def is_visible(self) -> bool: + return self._get_tab_bar().exists() + def _get_tab_bar(self): self.device.close_keyboard() @@ -331,13 +334,21 @@ def _get_hashtag_row(self, hashtag): ) def _get_place_row(self, place): - return self.device.find( - resourceIdMatches=case_insensitive_re( - f"{self.device.app_id}:id/row_place_title" - ), - className="android.widget.TextView", - textMatches=case_insensitive_re(place) - ) + if place is None: + return self.device.find( + resourceIdMatches=case_insensitive_re( + f"{self.device.app_id}:id/row_place_title" + ), + className="android.widget.TextView" + ) + else: + return self.device.find( + resourceIdMatches=case_insensitive_re( + f"{self.device.app_id}:id/row_place_title" + ), + className="android.widget.TextView", + textMatches = case_insensitive_re(place) + ) def _get_tab_text_view(self, tab: SearchTabs): tab_layout = self.device.find( @@ -500,7 +511,7 @@ def navigate_to_place(self, place): return None places_tab.click() - place_view = self._get_place_row(place) + place_view = self._get_place_row(None) # just open first place we see if not place_view.exists(): print(COLOR_FAIL + f"Cannot find place {place}, abort." + COLOR_ENDC) save_crash(self.device) diff --git a/registration/README.md b/registration/README.md index f539b86..70c93fd 100644 --- a/registration/README.md +++ b/registration/README.md @@ -1,6 +1,4 @@ -## Registration Flow (experimental feature) - -_IMPORTANT! This is an experimental feature. It means that the code is working, but we don't have an exact recipe how to avoid a CAPTCHA. Instagram is very strict when it comes to creating new accounts._ +## Registration Flow Insomniac gives the possibility to create Instagram accounts automatically. This can be done via `--register registration/users.txt` argument. Insomniac will do the following: 1. Open the app and **create new identity** if you're using a clone via [appcloner.app](https://appcloner.app), or just clear app data if you're not. diff --git a/targets.txt b/targets.txt index 40ddf52..b25927b 100644 --- a/targets.txt +++ b/targets.txt @@ -1,4 +1,6 @@ # Targets to interact with. Provide either usernames or posts urls. +# Important: This file is shared as targets file across all of the accounts you run insomniac with. +# If you wish to create a targets file thats dedicated to a specific accout, copy this file and name it -targets.txt (replace with your profile-name) # # Example: # natgeo diff --git a/whitelist.txt b/whitelist.txt index ae4e6b2..0fd86d3 100644 --- a/whitelist.txt +++ b/whitelist.txt @@ -1,2 +1,5 @@ +# Important: This file is shared as whitelist file across all of the accounts you run insomniac with. +# If you wish to create a whitelist file thats dedicated to a specific accout, copy this file and name it -whitelist.txt (replace with your profile-name) + username1 username2