From 67cded7eca6b280c43577f90f620301c6bbeec38 Mon Sep 17 00:00:00 2001 From: Philip Ulrich Date: Tue, 17 Nov 2020 16:15:07 -0600 Subject: [PATCH] Release v1.0.3 (#66) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Release v1.0.3 Bug Fixes πŸ› Fix bug when you have no followers/following πŸ› Fix bug with filtering πŸ› Fix bug when user has a shop button πŸ› Fix bug where you would follow someone back if they already followed you πŸ› Fix bug where hashtag could not be found on compact screens πŸ› Fix bug with unfollow confirmation screen in new UI and simplifies check πŸ› Fix bug when there is no like button on screen Improvements βœ… Combine black and pyflakes into one workflow :neckbeard: Unfollow supports range now :neckbeard: Improve clicking and double clicking function :neckbeard: Add ability to β€œsmall swipe” in case you only need to swipe a little 🐈 Add argument logging for better debugging 🐈 Add click location for better debugging 🐈 Clean up logging πŸ“ Clean remains of remove-mass-followers. This may be added back at some point New Features 🎁 Add update checking 🎁 Add example filter file 🎁 Add (disabled) debugging plugin Co-authored-by: Philip Ulrich Co-authored-by: narkopolo Co-authored-by: Arthur Silva Co-authored-by: Alessandro Maggio Co-authored-by: Dennis <52335835+mastrolube@users.noreply.github.com> --- .github/workflows/black.yml | 11 - .github/workflows/code-checker.yml | 45 ++++ .github/workflows/pyflakes.yml | 13 - .gitignore | 1 + GramAddict/__init__.py | 31 ++- GramAddict/core/decorators.py | 6 +- GramAddict/core/device_facade.py | 178 ++++++++----- GramAddict/core/filter.py | 80 +++--- GramAddict/core/interaction.py | 73 +++--- GramAddict/core/log.py | 4 +- GramAddict/core/report.py | 116 +++++---- GramAddict/core/session_state.py | 1 - GramAddict/core/utils.py | 23 +- GramAddict/core/views.py | 167 ++++++++++--- .../plugins/action_unfollow_followers.py | 56 ++--- GramAddict/plugins/data_analytics.py | 2 +- GramAddict/plugins/force_interact.dis | 233 ++++++++++++++++++ .../plugins/interact_blogger_followers.py | 4 +- GramAddict/plugins/interact_hashtag_likers.py | 35 ++- GramAddict/version.py | 2 +- GramAddict/version.txt | 1 - README.md | 5 +- filter.example | 12 + 23 files changed, 792 insertions(+), 307 deletions(-) delete mode 100644 .github/workflows/black.yml create mode 100644 .github/workflows/code-checker.yml delete mode 100644 .github/workflows/pyflakes.yml create mode 100644 GramAddict/plugins/force_interact.dis delete mode 100644 GramAddict/version.txt create mode 100644 filter.example diff --git a/.github/workflows/black.yml b/.github/workflows/black.yml deleted file mode 100644 index 9ddfcd8c..00000000 --- a/.github/workflows/black.yml +++ /dev/null @@ -1,11 +0,0 @@ -name: lint - -on: [pull_request] - -jobs: - black: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - uses: actions/setup-python@v2 - - uses: psf/black@stable diff --git a/.github/workflows/code-checker.yml b/.github/workflows/code-checker.yml new file mode 100644 index 00000000..4503550d --- /dev/null +++ b/.github/workflows/code-checker.yml @@ -0,0 +1,45 @@ +name: code-checker + +on: + push: + # only build each push to develop and master, other branches are built through pull requests + branches: [develop, master] + pull_request: + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - name: Clone Repository + uses: actions/checkout@v2 + + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: '>=3.6' + + - name: Run black + uses: psf/black@stable + + static-check: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: [3.6, 3.7, 3.8] + steps: + - name: Clone Repository + uses: actions/checkout@v2 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + + - name: Install Pyflakes + run: | + pip3 install --upgrade pip + pip3 install pyflakes + + - name: Detect errors with pyflakes + run: pyflakes . diff --git a/.github/workflows/pyflakes.yml b/.github/workflows/pyflakes.yml deleted file mode 100644 index be0495d6..00000000 --- a/.github/workflows/pyflakes.yml +++ /dev/null @@ -1,13 +0,0 @@ -name: reviewdog -on: [pull_request] -jobs: - pyflakes: - name: runner / pyflakes - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: pyflakes - uses: reviewdog/action-pyflakes@master - with: - github_token: ${{ secrets.github_token }} - reporter: github-pr-check diff --git a/.gitignore b/.gitignore index b0312209..d363ac8b 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ screenshots crashes filter.json whitelist.txt +Pipfile Pipfile.lock *.pdf .venv diff --git a/GramAddict/__init__.py b/GramAddict/__init__.py index b2597bc5..f8ddd8a3 100644 --- a/GramAddict/__init__.py +++ b/GramAddict/__init__.py @@ -19,20 +19,26 @@ close_instagram, get_instagram_version, get_value, - get_version, open_instagram, save_crash, screen_sleep, + update_available, ) from GramAddict.core.views import TabBarView +from GramAddict.version import __version__ # Logging initialization configure_logger() logger = logging.getLogger(__name__) +if update_available(): + logger.warn( + "NOTICE: There is an update available. Please update so that you can get all the latest features and bugfixes. https://github.com/GramAddict/bot" + ) logger.info( - "GramAddict " + get_version(), extra={"color": f"{Style.BRIGHT}{Fore.MAGENTA}"} + f"GramAddict {__version__}", extra={"color": f"{Style.BRIGHT}{Fore.MAGENTA}"} ) + # Global Variables device_id = None plugins = PluginLoader("GramAddict.plugins").plugins @@ -50,9 +56,7 @@ def load_plugins(): action = arg.get("action", None) if action: parser.add_argument( - arg["arg"], - help=arg["help"], - action=arg.get("action", None), + arg["arg"], help=arg["help"], action=arg.get("action", None) ) else: parser.add_argument( @@ -72,6 +76,7 @@ def load_plugins(): def get_args(): + logger.debug(f"Arguments used: {' '.join(sys.argv[1:])}") if not len(sys.argv) > 1: parser.print_help() return False @@ -131,8 +136,10 @@ def run(): return logger.info("Instagram version: " + get_instagram_version()) device = create_device(device_id) + if device is None: return + while True: logger.info( "-------- START: " + str(session_state.startTime) + " --------", @@ -163,11 +170,17 @@ def run(): ) = profileView.getProfileInfo() if ( - not session_state.my_username - or not session_state.my_followers_count - or not session_state.my_following_count + session_state.my_username == None + or session_state.my_followers_count == None + or session_state.my_following_count == None ): - logger.critical("Could not get profile info") + logger.critical( + "Could not get one of the following from your profile: username, # of followers, # of followings. This is typically due to a soft ban. Review the crash screenshot to see if this is the case." + ) + logger.critical( + f"Username: {getattr(session_state,'my_username')}, Followers: {getattr(session_state,'my_followers_count')}, Following: {getattr(session_state,'my_following_count')}" + ) + save_crash(device) exit(1) username = session_state.my_username diff --git a/GramAddict/core/decorators.py b/GramAddict/core/decorators.py index 7b4ef4d1..9026e9b5 100644 --- a/GramAddict/core/decorators.py +++ b/GramAddict/core/decorators.py @@ -1,6 +1,7 @@ import logging import sys import traceback +from colorama import Fore, Style from datetime import datetime from http.client import HTTPException from socket import timeout @@ -26,7 +27,10 @@ def wrapper(*args, **kwargs): func(*args, **kwargs) except KeyboardInterrupt: close_instagram(device_id) - logger.warn(f"-------- FINISH: {datetime.now().time()} --------") + logger.info( + f"-------- FINISH: {datetime.now().time()} --------", + extra={"color": f"{Style.BRIGHT}{Fore.YELLOW}"}, + ) print_full_report(sessions) sessions.persist(directory=session_state.my_username) sys.exit(0) diff --git a/GramAddict/core/device_facade.py b/GramAddict/core/device_facade.py index c2c2564c..df5bfa76 100644 --- a/GramAddict/core/device_facade.py +++ b/GramAddict/core/device_facade.py @@ -1,5 +1,5 @@ import logging -from enum import Enum, unique +from enum import Enum, auto from random import uniform import uiautomator2 @@ -55,6 +55,29 @@ def dump_hierarchy(self, path): with open(path, "w", encoding="utf-8") as outfile: outfile.write(xml_dump) + def swipe(self, direction: "DeviceFacade.Direction", scale=0.5): + """Swipe finger in the `direction`. + Scale is the sliding distance. Default to 50% of the screen width + """ + swipe_dir = "" + if direction == DeviceFacade.Direction.TOP: + swipe_dir = "up" + elif direction == DeviceFacade.Direction.BOTTOM: + swipe_dir = "up" + elif direction == DeviceFacade.Direction.LEFT: + swipe_dir = "left" + elif direction == DeviceFacade.Direction.BOTTOM: + swipe_dir = "down" + + logger.debug(f"Swipe {swipe_dir}, scale={scale}") + self.deviceV2.swipe_ext(swipe_dir, scale=scale) + + def get_info(self): + # {'currentPackageName': 'net.oneplus.launcher', 'displayHeight': 1920, 'displayRotation': 0, 'displaySizeDpX': 411, + # 'displaySizeDpY': 731, 'displayWidth': 1080, 'productName': 'OnePlus5', ' + # screenOn': True, 'sdkInt': 27, 'naturalOrientation': True} + return self.deviceV2.info + class View: deviceV2 = None # uiautomator2 viewV2 = None # uiautomator2 @@ -81,54 +104,108 @@ def child(self, *args, **kwargs): raise DeviceFacade.JsonRpcError(e) return DeviceFacade.View(view=view, device=self.deviceV2) + def left(self, *args, **kwargs): + + try: + view = self.viewV2.left(*args, **kwargs) + except uiautomator2.JSONRPCError as e: + raise DeviceFacade.JsonRpcError(e) + return DeviceFacade.View(view=view, device=self.deviceV2) + def right(self, *args, **kwargs): try: view = self.viewV2.right(*args, **kwargs) except uiautomator2.JSONRPCError as e: raise DeviceFacade.JsonRpcError(e) - return DeviceFacade.View(view=view, device=self.deviceV2) # is_old =false + return DeviceFacade.View(view=view, device=self.deviceV2) + + def up(self, *args, **kwargs): + + try: + view = self.viewV2.up(*args, **kwargs) + except uiautomator2.JSONRPCError as e: + raise DeviceFacade.JsonRpcError(e) + return DeviceFacade.View(view=view, device=self.deviceV2) + + def down(self, *args, **kwargs): + + try: + view = self.viewV2.down(*args, **kwargs) + except uiautomator2.JSONRPCError as e: + raise DeviceFacade.JsonRpcError(e) + return DeviceFacade.View(view=view, device=self.deviceV2) def click(self, mode="whole"): + x_abs = -1 + y_abs = -1 + if mode == "whole": + x_offset = uniform(0.15, 0.85) + y_offset = uniform(0.15, 0.85) + + elif mode == "left": + x_offset = uniform(0.15, 0.4) + y_offset = uniform(0.15, 0.85) + + elif mode == "center": + x_offset = uniform(0.4, 0.6) + y_offset = uniform(0.15, 0.85) + + elif mode == "right": + x_offset = uniform(0.6, 0.85) + y_offset = uniform(0.15, 0.85) + else: + x_offset = 0.5 + y_offset = 0.5 + try: + visible_bounds = self.get_bounds() + x_abs = int( + visible_bounds["left"] + + (visible_bounds["right"] - visible_bounds["left"]) * x_offset + ) + y_abs = int( + visible_bounds["top"] + + (visible_bounds["bottom"] - visible_bounds["top"]) * y_offset + ) + logger.debug(f"Single click ({x_abs}, {y_abs})") + self.viewV2.click(UI_TIMEOUT_LONG, offset=(x_offset, y_offset)) + + except uiautomator2.JSONRPCError as e: + raise DeviceFacade.JsonRpcError(e) + + def double_click(self, padding=0.3): + """Double click randomly in the selected view using padding + padding: % of how far from the borders we want the double + click to happen. + """ + visible_bounds = self.get_bounds() + horizontal_len = visible_bounds["right"] - visible_bounds["left"] + vertical_len = visible_bounds["bottom"] - visible_bounds["top"] + horizintal_padding = int(padding * horizontal_len) + vertical_padding = int(padding * vertical_len) + random_x = int( + uniform( + visible_bounds["left"] + horizintal_padding, + visible_bounds["right"] - horizintal_padding, + ) + ) + random_y = int( + uniform( + visible_bounds["top"] + vertical_padding, + visible_bounds["bottom"] - vertical_padding, + ) + ) + time_between_clicks = uniform(0.050, 0.200) try: - if mode == "whole": - self.viewV2.click( - UI_TIMEOUT_LONG, - offset=( - uniform(0.15, 0.85), - uniform(0.15, 0.85), - ), - ) - elif mode == "left": - self.viewV2.click( - UI_TIMEOUT_LONG, - offset=( - uniform(0.15, 0.4), - uniform(0.15, 0.85), - ), - ) - elif mode == "center": - self.viewV2.click( - UI_TIMEOUT_LONG, - offset=( - uniform(0.4, 0.6), - uniform(0.15, 0.85), - ), - ) - elif mode == "right": - self.viewV2.click( - UI_TIMEOUT_LONG, - offset=( - uniform(0.6, 0.85), - uniform(0.15, 0.85), - ), - ) - except uiautomator2.JSONRPCError as e: - raise DeviceFacade.JsonRpcError(e) - - def double_click(self): - self._double_click_v2() + logger.debug( + f"Double click in x={random_x}; y={random_y} with t={int(time_between_clicks*1000)}ms" + ) + self.deviceV2.double_click( + random_x, random_y, duration=time_between_clicks + ) + except uiautomator2.JSONRPCError as e: + raise DeviceFacade.JsonRpcError(e) def scroll(self, direction): @@ -140,7 +217,7 @@ def scroll(self, direction): except uiautomator2.JSONRPCError as e: raise DeviceFacade.JsonRpcError(e) - def swipe(self, direction): + def fling(self, direction): try: if direction == DeviceFacade.Direction.TOP: @@ -193,24 +270,11 @@ def set_text(self, text): except uiautomator2.JSONRPCError as e: raise DeviceFacade.JsonRpcError(e) - def _double_click_v2(self): - - visible_bounds = self.get_bounds() - horizontal_offset = visible_bounds["left"] - horizontal_diff = visible_bounds["right"] - visible_bounds["left"] - vertical_offset = visible_bounds["top"] - vertical_diff = visible_bounds["bottom"] - visible_bounds["top"] - center_x = horizontal_offset + ((horizontal_diff) / 2) - center_y = vertical_offset + ((vertical_diff) / 2) - try: - self.deviceV2.double_click(center_x, center_y, duration=0) - except uiautomator2.JSONRPCError as e: - raise DeviceFacade.JsonRpcError(e) - - @unique class Direction(Enum): - TOP = 0 - BOTTOM = 1 + TOP = auto() + BOTTOM = auto() + RIGHT = auto() + LEFT = auto() class JsonRpcError(Exception): pass diff --git a/GramAddict/core/filter.py b/GramAddict/core/filter.py index 9cc0f699..e14d9469 100644 --- a/GramAddict/core/filter.py +++ b/GramAddict/core/filter.py @@ -64,41 +64,51 @@ def check_profile(self, device, username): or field_min_potency_ratio is not None ): followers, followings = self._get_followers_and_followings(device) - if field_min_followers is not None and followers < int(field_min_followers): - logger.info( - f"@{username} has less than {field_min_followers} followers, skip.", - extra={"color": f"{Fore.GREEN}"}, - ) - return False - if field_max_followers is not None and followers > int(field_max_followers): - logger.info( - f"@{username} has has more than {field_max_followers} followers, skip.", - extra={"color": f"{Fore.GREEN}"}, - ) - return False - if field_min_followings is not None and followings < int( - field_min_followings - ): - logger.info( - f"@{username} has less than {field_min_followings} followers, skip.", - extra={"color": f"{Fore.GREEN}"}, - ) - return False - if field_max_followings is not None and followings > int( - field_max_followings - ): - logger.info( - f"@{username} has more than {field_max_followings} followings, skip.", - extra={"color": f"{Fore.GREEN}"}, - ) - return False - if field_min_potency_ratio is not None and ( - int(followings) == 0 - or followers / followings < float(field_min_potency_ratio) - ): - logger.info( - f"@{username}'s potency ratio is less than {field_min_potency_ratio}, skip.", - extra={"color": f"{Fore.GREEN}"}, + if followers != None and followings != None: + if field_min_followers is not None and followers < int( + field_min_followers + ): + logger.info( + f"@{username} has less than {field_min_followers} followers, skip.", + extra={"color": f"{Fore.GREEN}"}, + ) + return False + if field_max_followers is not None and followers > int( + field_max_followers + ): + logger.info( + f"@{username} has has more than {field_max_followers} followers, skip.", + extra={"color": f"{Fore.GREEN}"}, + ) + return False + if field_min_followings is not None and followings < int( + field_min_followings + ): + logger.info( + f"@{username} has less than {field_min_followings} followings, skip.", + extra={"color": f"{Fore.GREEN}"}, + ) + return False + if field_max_followings is not None and followings > int( + field_max_followings + ): + logger.info( + f"@{username} has more than {field_max_followings} followings, skip.", + extra={"color": f"{Fore.GREEN}"}, + ) + return False + if field_min_potency_ratio is not None and ( + int(followings) == 0 + or followers / followings < float(field_min_potency_ratio) + ): + logger.info( + f"@{username}'s potency ratio is less than {field_min_potency_ratio}, skip.", + extra={"color": f"{Fore.GREEN}"}, + ) + return False + else: + logger.critical( + "Either followers, followings, or possibly both are undefined. Cannot filter." ) return False return True diff --git a/GramAddict/core/interaction.py b/GramAddict/core/interaction.py index 505f2338..e13746d3 100644 --- a/GramAddict/core/interaction.py +++ b/GramAddict/core/interaction.py @@ -11,9 +11,10 @@ logger = logging.getLogger(__name__) -TEXTVIEW_OR_BUTTON_REGEX = "android.widget.TextView|android.widget.Button" -FOLLOW_REGEX = "Follow|Follow Back" -UNFOLLOW_REGEX = "Following|Requested" +BUTTON_REGEX = "android.widget.Button" +FOLLOW_REGEX = "^Follow$" +FOLLOWBACK_REGEX = "^Follow Back$" +UNFOLLOW_REGEX = "^Following|^Requested" def interact_with_user( @@ -50,18 +51,12 @@ def interact_with_user( if is_private or is_empty: private_empty = "Private" if is_private else "Empty" - logger.info( - f"{private_empty} account.", - extra={"color": f"{Fore.GREEN}"}, - ) + logger.info(f"{private_empty} account.", extra={"color": f"{Fore.GREEN}"}) if can_follow and profile_filter.can_follow_private_or_empty(): followed = _follow(device, username, follow_percentage) else: followed = False - logger.info( - "Skip user.", - extra={"color": f"{Fore.GREEN}"}, - ) + logger.info("Skip user.", extra={"color": f"{Fore.GREEN}"}) return False, followed posts_tab_view = profile_view.navigateToPostsTab() @@ -84,17 +79,18 @@ def interact_with_user( like_succeed = False if opened_post_view: logger.info("Double click post") - opened_post_view.likePost() - random_sleep() - if not opened_post_view.isPostLiked(): + + like_succeed = opened_post_view.likePost() + if not like_succeed: logger.debug("Double click failed. Try the like button.") - opened_post_view.likePost(click_btn_like=True) - random_sleep() + like_succeed = opened_post_view.likePost(click_btn_like=True) - like_succeed = opened_post_view.isPostLiked() if like_succeed: + logger.debug("Like succeed. Check for block.") detect_block(device) on_like() + else: + logger.warning("Fail to like post. Let's continue...") logger.info("Back to profile") device.back() @@ -109,14 +105,10 @@ def interact_with_user( followed = False if not followed: - logger.info( - "Skip user.", - extra={"color": f"{Fore.GREEN}"}, - ) + logger.info("Skip user.", extra={"color": f"{Fore.GREEN}"}) return False, followed random_sleep() - if can_follow: return True, _follow(device, username, follow_percentage) @@ -190,34 +182,36 @@ def _follow(device, username, follow_percentage): random_sleep() - profile_header_actions_layout = device.find( - resourceId="com.instagram.android:id/profile_header_actions_top_row", - className="android.widget.LinearLayout", - ) - if not profile_header_actions_layout.exists(): - logger.error("Cannot find profile actions.") - return False - - follow_button = profile_header_actions_layout.child( - classNameMatches=TEXTVIEW_OR_BUTTON_REGEX, + follow_button = device.find( + classNameMatches=BUTTON_REGEX, clickable=True, textMatches=FOLLOW_REGEX, ) + if not follow_button.exists(): - unfollow_button = profile_header_actions_layout.child( - classNameMatches=TEXTVIEW_OR_BUTTON_REGEX, + unfollow_button = device.find( + classNameMatches=BUTTON_REGEX, clickable=True, textMatches=UNFOLLOW_REGEX, ) + followback_button = device.find( + classNameMatches=BUTTON_REGEX, + clickable=True, + textMatches=FOLLOWBACK_REGEX, + ) if unfollow_button.exists(): logger.info( - f"You already follow @{username}.", - extra={"color": f"{Fore.GREEN}"}, + f"You already follow @{username}.", extra={"color": f"{Fore.GREEN}"} + ) + return False + elif followback_button.exists(): + logger.info( + f"@{username} already follows you.", extra={"color": f"{Fore.GREEN}"} ) return False else: logger.error( - "Cannot find neither Follow button, nor Unfollow button. Maybe not English language is set?" + "Cannot find neither Follow button, Follow Back button, nor Unfollow button. Maybe not English language is set?" ) save_crash(device) switch_to_english(device) @@ -225,9 +219,6 @@ def _follow(device, username, follow_percentage): follow_button.click() detect_block(device) - logger.info( - f"Followed @{username}", - extra={"color": f"{Fore.GREEN}"}, - ) + logger.info(f"Followed @{username}", extra={"color": f"{Fore.GREEN}"}) random_sleep() return True diff --git a/GramAddict/core/log.py b/GramAddict/core/log.py index 38addcfb..07c34da9 100644 --- a/GramAddict/core/log.py +++ b/GramAddict/core/log.py @@ -31,11 +31,11 @@ def format(self, record): def configure_logger(): init_colorama() logger = logging.getLogger() # root logger - logger.setLevel(logging.DEBUG) + logger.setLevel(logging.INFO) # Formatters datefmt = r"[%m/%d %H:%M:%S]" - console_fmt = "%(asctime)s %(levelname)8s | %(message)s (%(filename)s:%(lineno)d)" + console_fmt = "%(asctime)s %(levelname)8s | %(message)s" console_formatter = ColoredFormatter(fmt=console_fmt, datefmt=datefmt) crash_report_fmt = ( "%(asctime)s %(levelname)8s | %(message)s (%(filename)s:%(lineno)d)" diff --git a/GramAddict/core/report.py b/GramAddict/core/report.py index 7bae1e4f..01fff0a8 100644 --- a/GramAddict/core/report.py +++ b/GramAddict/core/report.py @@ -1,4 +1,5 @@ import logging +from colorama import Fore, Style from datetime import datetime, timedelta logger = logging.getLogger(__name__) @@ -8,42 +9,74 @@ def print_full_report(sessions): if len(sessions) > 1: for index, session in enumerate(sessions): finish_time = session.finishTime or datetime.now() - logger.warn("") - logger.warn(f"SESSION #{index + 1}") - logger.warn(f"Start time: {session.startTime}") - logger.warn(f"Finish time: {finish_time}") - logger.warn(f"Duration: {finish_time - session.startTime}") - logger.warn( - f"Total interactions: {_stringify_interactions(session.totalInteractions)}" + logger.info( + "", + extra={"color": f"{Style.BRIGHT}{Fore.YELLOW}"}, ) - logger.warn( - f"Successful interactions: {_stringify_interactions(session.successfulInteractions)}" + logger.info( + f"SESSION #{index + 1}", + extra={"color": f"{Style.BRIGHT}{Fore.YELLOW}"}, ) - logger.warn( - f"Total followed: {_stringify_interactions(session.totalFollowed)}" + logger.info( + f"Start time: {session.startTime}", + extra={"color": f"{Style.BRIGHT}{Fore.YELLOW}"}, ) - logger.warn(f"Total likes: {session.totalLikes}") - logger.warn(f"Total unfollowed: {session.totalUnfollowed}") - logger.warn( - f"Removed mass followers: {_stringify_removed_mass_followers(session.removedMassFollowers)}" + logger.info( + f"Finish time: {finish_time}", + extra={"color": f"{Style.BRIGHT}{Fore.YELLOW}"}, + ) + logger.info( + f"Duration: {finish_time - session.startTime}", + extra={"color": f"{Style.BRIGHT}{Fore.YELLOW}"}, + ) + logger.info( + f"Total interactions: {_stringify_interactions(session.totalInteractions)}", + extra={"color": f"{Style.BRIGHT}{Fore.YELLOW}"}, + ) + logger.info( + f"Successful interactions: {_stringify_interactions(session.successfulInteractions)}", + extra={"color": f"{Style.BRIGHT}{Fore.YELLOW}"}, + ) + logger.info( + f"Total followed: {_stringify_interactions(session.totalFollowed)}", + extra={"color": f"{Style.BRIGHT}{Fore.YELLOW}"}, + ) + logger.info( + f"Total likes: {session.totalLikes}", + extra={"color": f"{Style.BRIGHT}{Fore.YELLOW}"}, + ) + logger.info( + f"Total unfollowed: {session.totalUnfollowed}", + extra={"color": f"{Style.BRIGHT}{Fore.YELLOW}"}, ) - logger.warn("") - logger.warn("TOTAL") + logger.info( + "", + extra={"color": f"{Style.BRIGHT}{Fore.YELLOW}"}, + ) + logger.info( + "TOTAL", + extra={"color": f"{Style.BRIGHT}{Fore.YELLOW}"}, + ) completed_sessions = [session for session in sessions if session.is_finished()] - logger.warn(f"Completed sessions: {len(completed_sessions)}") + logger.info( + f"Completed sessions: {len(completed_sessions)}", + extra={"color": f"{Style.BRIGHT}{Fore.YELLOW}"}, + ) duration = timedelta(0) for session in sessions: finish_time = session.finishTime or datetime.now() duration += finish_time - session.startTime - logger.warn(f"Total duration: {duration}") + logger.info( + f"Total duration: {duration}", + extra={"color": f"{Style.BRIGHT}{Fore.YELLOW}"}, + ) total_interactions = {} successful_interactions = {} total_followed = {} - total_removed_mass_followers = [] for session in sessions: for source, count in session.totalInteractions.items(): if total_interactions.get(source) is None: @@ -63,23 +96,28 @@ def print_full_report(sessions): else: total_followed[source] += count - for username in session.removedMassFollowers: - total_removed_mass_followers.append(username) - - logger.warn(f"Total interactions: {_stringify_interactions(total_interactions)}") - logger.warn( - f"Successful interactions: {_stringify_interactions(successful_interactions)}" + logger.info( + f"Total interactions: {_stringify_interactions(total_interactions)}", + extra={"color": f"{Style.BRIGHT}{Fore.YELLOW}"}, + ) + logger.info( + f"Successful interactions: {_stringify_interactions(successful_interactions)}", + extra={"color": f"{Style.BRIGHT}{Fore.YELLOW}"}, + ) + logger.info( + f"Total followed : {_stringify_interactions(total_followed)}", + extra={"color": f"{Style.BRIGHT}{Fore.YELLOW}"}, ) - logger.warn(f"Total followed : {_stringify_interactions(total_followed)}") - total_likes = sum(session.totalLikes for session in sessions) - logger.warn(f"Total likes: {total_likes}") + logger.info( + f"Total likes: {total_likes}", + extra={"color": f"{Style.BRIGHT}{Fore.YELLOW}"}, + ) total_unfollowed = sum(session.totalUnfollowed for session in sessions) - logger.warn(f"Total unfollowed: {total_unfollowed} ") - - logger.warn( - f"Removed mass followers: {_stringify_removed_mass_followers(total_removed_mass_followers)}" + logger.info( + f"Total unfollowed: {total_unfollowed}", + extra={"color": f"{Style.BRIGHT}{Fore.YELLOW}"}, ) @@ -87,8 +125,9 @@ def print_short_report(source, session_state): total_likes = session_state.totalLikes total_followed = sum(session_state.totalFollowed.values()) interactions = session_state.successfulInteractions.get(source, 0) - logger.warn( - f"Session progress: {total_likes} likes, {total_followed} followed, {interactions} successful interaction(s) for {source}" + logger.info( + f"Session progress: {total_likes} likes, {total_followed} followed, {interactions} successful interaction(s) for {source}", + extra={"color": f"{Style.BRIGHT}{Fore.YELLOW}"}, ) @@ -101,10 +140,3 @@ def _stringify_interactions(interactions): result += str(count) + " for " + source + ", " result = result[:-2] return result - - -def _stringify_removed_mass_followers(removed_mass_followers): - if len(removed_mass_followers) == 0: - return "none" - else: - return "@" + ", @".join(removed_mass_followers) diff --git a/GramAddict/core/session_state.py b/GramAddict/core/session_state.py index 49b11f31..89ca5cd5 100644 --- a/GramAddict/core/session_state.py +++ b/GramAddict/core/session_state.py @@ -66,7 +66,6 @@ def default(self, session_state: SessionState): "total_followed": sum(session_state.totalFollowed.values()), "total_likes": session_state.totalLikes, "total_unfollowed": session_state.totalUnfollowed, - "removed_mass_followers": session_state.removedMassFollowers, "start_time": str(session_state.startTime), "finish_time": str(session_state.finishTime), "args": session_state.args, diff --git a/GramAddict/core/utils.py b/GramAddict/core/utils.py index 26fee056..0a81e9ab 100644 --- a/GramAddict/core/utils.py +++ b/GramAddict/core/utils.py @@ -4,21 +4,25 @@ import re import shutil import sys +import urllib3 from datetime import datetime from random import randint, uniform from time import sleep from colorama import Fore, Style from GramAddict.core.log import get_logs +from GramAddict.version import __version__ +http = urllib3.PoolManager() logger = logging.getLogger(__name__) -def get_version(): - fin = open("GramAddict/version.txt") - version = fin.readline().strip() - fin.close() - return version +def update_available(): + r = http.request( + "GET", + "https://raw.githubusercontent.com/GramAddict/bot/master/GramAddict/version.py", + ) + return r.data.decode("utf-8").split('"')[1] > __version__ def check_adb_connection(is_device_id_provided): @@ -116,7 +120,7 @@ def screen_unlock(device_id, MENU_BUTTON): os.popen( f"adb {''if device_id is None else ('-s '+ device_id)} shell input keyevent {MENU_BUTTON}" ) - sleep(2) + sleep(3) if check_screen_locked(device_id): sys.exit( "Can't unlock your screen.. Maybe you've set a passcode.. Disable it or don't use this function!" @@ -187,13 +191,10 @@ def save_crash(device): extra={"color": Fore.GREEN}, ) logger.info( - "Please attach this file if you gonna report the crash at", - extra={"color": Fore.GREEN}, - ) - logger.info( - "https://github.com/GramAddict/bot/issues\n", + "If you want to report this crash, please upload the dump file via a ticket in the #lobby channel on discord ", extra={"color": Fore.GREEN}, ) + logger.info("https://discord.gg/9MTjgs8g5R\n", extra={"color": Fore.GREEN}) def detect_block(device): diff --git a/GramAddict/core/views.py b/GramAddict/core/views.py index da679174..13983dfa 100644 --- a/GramAddict/core/views.py +++ b/GramAddict/core/views.py @@ -211,6 +211,40 @@ def _getTabTextView(self, tab: SearchTabs): ) return tab_text_view + def _searchTabWithTextPlaceholder(self, tab: SearchTabs): + tab_layout = self.device.find( + resourceIdMatches=case_insensitive_re( + "com.instagram.android:id/fixed_tabbar_tabs_container" + ), + className="android.widget.LinearLayout", + ) + search_edit_text = self._getSearchEditText() + + fixed_text = "Search {}".format(tab.name if tab.name != "TAGS" else "hashtags") + logger.debug( + "Going to check if the search bar have as placeholder: {}".format( + fixed_text + ) + ) + + for item in tab_layout.child( + resourceId="com.instagram.android:id/tab_button_fallback_icon", + className="android.widget.ImageView", + ): + item.click() + # random_sleep() + + # Little trick for force-update the ui and placeholder text + search_edit_text.click() + self.device.back() + + if self.device.find( + className="android.widget.TextView", + textMatches=case_insensitive_re(fixed_text), + ).exists(): + return item + return None + def navigateToUsername(self, username): logger.debug("Navigate to profile @" + username) search_edit_text = self._getSearchEditText() @@ -228,24 +262,29 @@ def navigateToUsername(self, username): return ProfileView(self.device, is_own_profile=False) def navigateToHashtag(self, hashtag): - logger.debug(f"Navigate to hashtag #{hashtag}") + logger.info(f"Navigate to hashtag {hashtag}") search_edit_text = self._getSearchEditText() search_edit_text.click() random_sleep() hashtag_tab = self._getTabTextView(SearchTabs.TAGS) if not hashtag_tab.exists(): - hashtag_tab = self._getTabTextView(SearchTabs.Tags) - if not hashtag_tab.exists(): - logger.error("Cannot find tab: TAGS.") - return None + logger.debug( + "Cannot find tab: Tags. Going to attempt to search for placeholder in all tabs" + ) + hashtag_tab = self._searchTabWithTextPlaceholder(SearchTabs.TAGS) + if hashtag_tab is None: + logger.error("Cannot find tab: Tags.") + save_crash(self.device) + return None hashtag_tab.click() search_edit_text.set_text(hashtag) - hashtag_view = self._getHashtagRow(hashtag) + hashtag_view = self._getHashtagRow(hashtag[1:]) if not hashtag_view.exists(): - logger.error(f"Cannot find hashtag #{hashtag} , abort.") + logger.error(f"Cannot find hashtag {hashtag}, abort.") + save_crash(self.device) return None hashtag_view.click() @@ -325,16 +364,88 @@ class OpenedPostView: def __init__(self, device: DeviceFacade): self.device = device - def isPostLiked(self): - like_btn_view = self.device.find( + def _getPostLikeButton(self, scroll_to_find=True): + """Find the like button right bellow a post. + Note: sometimes the like button from the post above or bellow are + dumped as well, so we need handle that situation. + + scroll_to_find: if the like button is not found, scroll a bit down + to try to find it. Default: True + """ + MEDIA_GROUP_RE = case_insensitive_re( + [ + "com.instagram.android:id/media_group", + "com.instagram.android:id/carousel_media_group", + ] + ) + post_view_area = self.device.find( + resourceIdMatches=case_insensitive_re("android:id/list") + ) + if not post_view_area.exists(): + logger.debug("Cannot find post recycler view area") + return None + + post_media_view = self.device.find( + resourceIdMatches=MEDIA_GROUP_RE, + className="android.widget.FrameLayout", + ) + + if not post_media_view.exists(): + logger.debug("Cannot find post media view area") + return None + + like_btn_view = post_media_view.down( resourceIdMatches=case_insensitive_re(OpenedPostView.BTN_LIKE_RES_ID) ) + if like_btn_view.exists(): - return like_btn_view.get_selected() + # threshold of 30% of the display height + threshold = int((0.3) * self.device.get_info()["displayHeight"]) + like_btn_top_bound = like_btn_view.get_bounds()["top"] + is_like_btn_in_the_bottom = like_btn_top_bound > threshold + + if not is_like_btn_in_the_bottom: + logger.debug( + f"Like button is to high ({like_btn_top_bound} px). Threshold is {threshold} px" + ) + + post_view_area_bottom_bound = post_view_area.get_bounds()["bottom"] + is_like_btn_visible = like_btn_top_bound <= post_view_area_bottom_bound + if not is_like_btn_visible: + logger.debug( + f"Like btn out of current clickable area. Like btn top ({like_btn_top_bound}) recycler_view bottom ({post_view_area_bottom_bound})" + ) + else: + logger.debug("Like button not found bellow the post.") + + if ( + not like_btn_view.exists() + or not is_like_btn_in_the_bottom + or not is_like_btn_visible + ): + if scroll_to_find: + logger.debug("Try to scroll tiny bit down...") + # Remember: to scroll down we need to swipe up :) + self.device.swipe(DeviceFacade.Direction.TOP, scale=0.1) + like_btn_view = post_media_view.down( + resourceIdMatches=case_insensitive_re( + OpenedPostView.BTN_LIKE_RES_ID + ) + ) - logger.error("Cannot find button like") + if not scroll_to_find or not like_btn_view.exists(): + logger.error("Could not find like button bellow the post") + return None - return False + return like_btn_view + + def _isPostLiked(self): + + like_btn_view = self._getPostLikeButton() + if not like_btn_view: + return False + + return like_btn_view.get_selected() def likePost(self, click_btn_like=False): MEDIA_GROUP_RE = case_insensitive_re( @@ -344,32 +455,25 @@ def likePost(self, click_btn_like=False): ] ) post_media_view = self.device.find( - resourceIdMatches=MEDIA_GROUP_RE, - className="android.widget.FrameLayout", + resourceIdMatches=MEDIA_GROUP_RE, className="android.widget.FrameLayout" ) if click_btn_like: - like_btn_view = self.device.find( - resourceIdMatches=case_insensitive_re(OpenedPostView.BTN_LIKE_RES_ID) - ) - if post_media_view.exists() and like_btn_view.exists(): - image_bottom_bound = post_media_view.get_bounds()["bottom"] - like_btn_top_bound = like_btn_view.get_bounds()["top"] - # to avoid clicking in a like button that is for another picture (previous one) - if like_btn_top_bound >= image_bottom_bound: - like_btn_view.click() - else: - logger.debug( - "Like btn out of current view. Don't click, just ignore." - ) - else: - logger.error("Cannot find button like to click") + like_btn_view = self._getPostLikeButton() + if not like_btn_view: + return False + like_btn_view.click() else: if post_media_view.exists(): post_media_view.double_click() else: logger.error("Could not find post area to double click") + return False + + random_sleep() + + return self._isPostLiked() class PostsGridView: @@ -428,8 +532,7 @@ def _getActionBarTitleBtn(self): ] ) return self.action_bar.child( - resourceIdMatches=re_case_insensitive, - className="android.widget.TextView", + resourceIdMatches=re_case_insensitive, className="android.widget.TextView" ) def getUsername(self): @@ -437,7 +540,7 @@ def getUsername(self): if title_view.exists(): return title_view.get_text() logger.error("Cannot get username") - return "" + return None def _parseCounter(self, text): multiplier = 1 diff --git a/GramAddict/plugins/action_unfollow_followers.py b/GramAddict/plugins/action_unfollow_followers.py index 718affdc..2999c1a1 100644 --- a/GramAddict/plugins/action_unfollow_followers.py +++ b/GramAddict/plugins/action_unfollow_followers.py @@ -1,6 +1,6 @@ import logging from enum import Enum, unique -from random import seed +from random import seed, randint from colorama import Fore from GramAddict.core.decorators import run_safely @@ -17,7 +17,10 @@ "com.instagram.android:id/row_profile_header_following_container" "|com.instagram.android:id/row_profile_header_container_following" ) -TEXTVIEW_OR_BUTTON_REGEX = "android.widget.TextView|android.widget.Button" +BUTTON_REGEX = "android.widget.Button" +BUTTON_OR_TEXTVIEW_REGEX = "android.widget.Button|android.widget.TextView" +FOLLOWING_REGEX = "^Following|^Requested" +UNFOLLOW_REGEX = "^Unfollow" # Script Initialization seed() @@ -78,8 +81,14 @@ def __init__(self): self.session_state = sessions[-1] self.sessions = sessions self.unfollow_type = enabled[0][2:] + range_arg = getattr(args, self.unfollow_type.replace("-", "_")).split("-") + if len(range_arg) > 1: + count_arg = randint(int(range_arg[0]), int(range_arg[1])) + else: + count_arg = int(range_arg[0]) + count = min( - int(getattr(args, self.unfollow_type.replace("-", "_"))), + count_arg, self.session_state.my_following_count - int(args.min_following), ) @@ -278,9 +287,9 @@ def do_unfollow(self, device, username, my_username, check_if_is_follower): while True: unfollow_button = device.find( - classNameMatches=TEXTVIEW_OR_BUTTON_REGEX, + classNameMatches=BUTTON_REGEX, clickable=True, - text="Following", + textMatches=FOLLOWING_REGEX, ) if not unfollow_button.exists() and attempts <= 1: scrollable = device.find( @@ -312,7 +321,16 @@ def do_unfollow(self, device, username, my_username, check_if_is_follower): confirm_unfollow_button.click() random_sleep() - self.close_confirm_dialog_if_shown(device) + + # Check if private account confirmation + private_unfollow_button = device.find( + classNameMatches=BUTTON_OR_TEXTVIEW_REGEX, + textMatches=UNFOLLOW_REGEX, + ) + + if private_unfollow_button.exists(): + private_unfollow_button.click() + detect_block(device) logger.info("Back to the followings list.") @@ -338,32 +356,6 @@ def check_is_follower(self, device, username, my_username): device.back() return result - def close_confirm_dialog_if_shown(self, device): - dialog_root_view = device.find( - resourceId="com.instagram.android:id/dialog_root_view", - className="android.widget.FrameLayout", - ) - if not dialog_root_view.exists(): - return - - # Avatar existence is the way to distinguish confirm dialog from block dialog - user_avatar_view = device.find( - resourceId="com.instagram.android:id/circular_image", - className="android.widget.ImageView", - ) - if not user_avatar_view.exists(): - return - - logger.info( - "Dialog shown, confirm unfollowing.", extra={"color": f"{Fore.GREEN}"} - ) - random_sleep() - unfollow_button = dialog_root_view.child( - resourceId="com.instagram.android:id/primary_button", - className="android.widget.TextView", - ) - unfollow_button.click() - @unique class UnfollowRestriction(Enum): diff --git a/GramAddict/plugins/data_analytics.py b/GramAddict/plugins/data_analytics.py index c0f29a41..e30c65f0 100644 --- a/GramAddict/plugins/data_analytics.py +++ b/GramAddict/plugins/data_analytics.py @@ -30,7 +30,7 @@ def __init__(self): "metavar": "username1", "default": None, "operation": True, - }, + } ] def run(self, device, device_id, args, enabled, storage, sessions): diff --git a/GramAddict/plugins/force_interact.dis b/GramAddict/plugins/force_interact.dis new file mode 100644 index 00000000..2ccf8135 --- /dev/null +++ b/GramAddict/plugins/force_interact.dis @@ -0,0 +1,233 @@ +""" +This is a very hacky test plugin for interacting with a single specific user + +DO NOT use this if you aren't testing something that requires a specific user test + +It does the following: +- Like photo(s) +- Follow based on percentage +- Unfollow + +NOTICE: The unfollow code is local to here because unfollow would need to be steeply refactored for it to work +""" + +import logging +from enum import Enum, unique +from functools import partial +from random import seed, shuffle + +from colorama import Fore +from GramAddict.core.decorators import run_safely +from GramAddict.core.device_facade import DeviceFacade +from GramAddict.core.filter import Filter +from GramAddict.core.interaction import ( + _on_interaction, + _on_like, + _on_likes_limit_reached, + interact_with_user, + is_follow_limit_reached_for_source, +) +from GramAddict.core.plugin_loader import Plugin +from GramAddict.core.scroll_end_detector import ScrollEndDetector +from GramAddict.core.storage import FollowingStatus +from GramAddict.core.utils import get_value, random_sleep, save_crash + +logger = logging.getLogger(__name__) + +from GramAddict.core.views import TabBarView + +BUTTON_REGEX = "android.widget.Button" +BUTTON_OR_TEXTVIEW_REGEX = "android.widget.Button|android.widget.TextView" +FOLLOWING_REGEX = "^Following|^Requested" +UNFOLLOW_REGEX = "^Unfollow" + + +# Script Initialization +seed() + + +class ForceIteract(Plugin): + """Force interact with user - mostly for testing""" + + def __init__(self): + super().__init__() + self.description = "Force interact with user - mostly for testing" + self.arguments = [ + { + "arg": "--force-interact", + "nargs": "+", + "help": "force interact with user - mostly for testing", + "metavar": ("username1", "username2"), + "default": None, + "operation": True, + } + ] + + def run(self, device, device_id, args, enabled, storage, sessions): + class State: + def __init__(self): + pass + + is_job_completed = False + is_likes_limit_reached = False + + self.device_id = device_id + self.state = None + self.sessions = sessions + self.session_state = sessions[-1] + profile_filter = Filter() + + # IMPORTANT: in each job we assume being on the top of the Profile tab already + sources = [source for source in args.force_interact] + shuffle(sources) + + for source in sources: + self.state = State() + is_myself = source[1:] == self.session_state.my_username + its_you = is_myself and " (it's you)" or "" + logger.info(f"Handle {source} {its_you}") + + on_likes_limit_reached = partial(_on_likes_limit_reached, state=self.state) + + on_interaction = partial( + _on_interaction, + on_likes_limit_reached=on_likes_limit_reached, + likes_limit=int(args.total_likes_limit), + source=source, + interactions_limit=get_value( + args.interactions_count, "Interactions count: {}", 70 + ), + sessions=self.sessions, + session_state=self.session_state, + ) + + on_like = partial( + _on_like, sessions=self.sessions, session_state=self.session_state + ) + + @run_safely( + device=device, + device_id=self.device_id, + sessions=self.sessions, + session_state=self.session_state, + ) + def job(): + self.handle_blogger( + device, + source[1:] if "@" in source else source, + args.likes_count, + int(args.follow_percentage), + int(args.follow_limit) if args.follow_limit else None, + storage, + profile_filter, + on_like, + on_interaction, + ) + self.state.is_job_completed = True + + while ( + not self.state.is_job_completed + and not self.state.is_likes_limit_reached + ): + job() + + if self.state.is_likes_limit_reached: + break + + def handle_blogger( + self, + device, + username, + likes_count, + follow_percentage, + follow_limit, + storage, + profile_filter, + on_like, + on_interaction, + ): + is_myself = username == self.session_state.my_username + interaction = partial( + interact_with_user, + my_username=self.session_state.my_username, + likes_count=likes_count, + follow_percentage=follow_percentage, + on_like=on_like, + profile_filter=profile_filter, + ) + is_follow_limit_reached = partial( + is_follow_limit_reached_for_source, + follow_limit=follow_limit, + source=username, + session_state=self.session_state, + ) + + if not self.open_user(device, username): + return + """ + can_follow = ( + not is_myself + and not is_follow_limit_reached() + and storage.get_following_status(username) == FollowingStatus.NONE + ) + + interaction_succeed, followed = interaction( + device, username=username, can_follow=can_follow + ) + storage.add_interacted_user(username, followed=followed) + can_continue = on_interaction(succeed=interaction_succeed, followed=followed) + """ + logger.info("Unfollow @" + username) + attempts = 0 + + while True: + unfollow_button = device.find( + classNameMatches=BUTTON_REGEX, + clickable=True, + textMatches=FOLLOWING_REGEX, + ) + if not unfollow_button.exists() and attempts <= 1: + scrollable = device.find( + classNameMatches="androidx.viewpager.widget.ViewPager" + ) + scrollable.scroll(DeviceFacade.Direction.TOP) + attempts += 1 + else: + break + + if not unfollow_button.exists(): + logger.error("Cannot find Following button.") + save_crash(device) + + unfollow_button.click() + + confirm_unfollow_button = device.find( + resourceId="com.instagram.android:id/follow_sheet_unfollow_row", + className="android.widget.TextView", + ) + if not confirm_unfollow_button.exists(): + logger.error("Cannot confirm unfollow.") + save_crash(device) + device.back() + return False + confirm_unfollow_button.click() + + # Check if private account confirmation + private_unfollow_button = device.find( + classNameMatches=BUTTON_OR_TEXTVIEW_REGEX, + textMatches=UNFOLLOW_REGEX, + ) + + if private_unfollow_button.exists(): + private_unfollow_button.click() + + return + + def open_user(self, device, username): + search_view = TabBarView(device).navigateToSearch() + profile_view = search_view.navigateToUsername(username) + random_sleep() + if not profile_view: + return False + + return True diff --git a/GramAddict/plugins/interact_blogger_followers.py b/GramAddict/plugins/interact_blogger_followers.py index ea6ec4e7..ca13905f 100644 --- a/GramAddict/plugins/interact_blogger_followers.py +++ b/GramAddict/plugins/interact_blogger_followers.py @@ -191,7 +191,7 @@ def is_end_reached(): resourceId="android:id/list", className="android.widget.ListView" ) while not is_end_reached(): - list_view.swipe(DeviceFacade.Direction.BOTTOM) + list_view.fling(DeviceFacade.Direction.BOTTOM) logger.info("Scroll back to the first follower") @@ -345,7 +345,7 @@ def scrolled_to_top(): "All followers skipped, let's do a swipe", extra={"color": f"{Fore.GREEN}"}, ) - list_view.swipe(DeviceFacade.Direction.BOTTOM) + list_view.fling(DeviceFacade.Direction.BOTTOM) else: logger.info( "Need to scroll now", extra={"color": f"{Fore.GREEN}"} diff --git a/GramAddict/plugins/interact_hashtag_likers.py b/GramAddict/plugins/interact_hashtag_likers.py index f6c35217..37c6ba4b 100644 --- a/GramAddict/plugins/interact_hashtag_likers.py +++ b/GramAddict/plugins/interact_hashtag_likers.py @@ -61,6 +61,8 @@ def __init__(self): for source in sources: self.state = State() + if source[0] != "#": + source = "#" + source logger.info(f"Handle {source}", extra={"color": f"{Style.BRIGHT}"}) on_likes_limit_reached = partial(_on_likes_limit_reached, state=self.state) @@ -90,7 +92,7 @@ def __init__(self): def job(): self.handle_hashtag( device, - source[1:] if "#" in source else source, + source, args.likes_count, int(args.follow_percentage), int(args.follow_limit) if args.follow_limit else None, @@ -143,6 +145,7 @@ def handle_hashtag( return logger.info("Opening the first result") + random_sleep() first_result_view = device.find( resourceId="com.instagram.android:id/recycler_view", @@ -253,14 +256,22 @@ def handle_hashtag( posts_list_view.scroll(DeviceFacade.Direction.BOTTOM) def open_likers(self, device): - likes_view = device.find( - resourceId="com.instagram.android:id/row_feed_textview_likes", - className="android.widget.TextView", - ) - if likes_view.exists(): - logger.info("Opening post likers") - random_sleep() - likes_view.click("right") - return True - else: - return False + attempts = 0 + while True: + likes_view = device.find( + resourceId="com.instagram.android:id/row_feed_textview_likes", + className="android.widget.TextView", + ) + if likes_view.exists(): + logger.info("Opening post likers") + random_sleep() + likes_view.click("right") + return True + else: + if attempts < 1: + attempts += 1 + logger.info("Can't find likers, trying small swipe") + device.swipe(DeviceFacade.Direction.TOP, scale=0.1) + continue + else: + return False diff --git a/GramAddict/version.py b/GramAddict/version.py index 7863915f..976498ab 100644 --- a/GramAddict/version.py +++ b/GramAddict/version.py @@ -1 +1 @@ -__version__ = "1.0.2" +__version__ = "1.0.3" diff --git a/GramAddict/version.txt b/GramAddict/version.txt deleted file mode 100644 index 6d7de6e6..00000000 --- a/GramAddict/version.txt +++ /dev/null @@ -1 +0,0 @@ -1.0.2 diff --git a/README.md b/README.md index 5419dca3..d0a5fe59 100644 --- a/README.md +++ b/README.md @@ -10,8 +10,7 @@ Liking and following automatically on your Android phone/tablet. No root require ## Requirements -- Python 3.8 -- pipenv [how to install](https://github.com/pypa/pipenv#installation) +- Python 3.6+ ### How to install 1. Clone project: `git clone https://github.com/GramAddict/bot.git gramaddict` @@ -28,7 +27,7 @@ mv /platform-tools/ ~/Library/Android/sdk ### How to install on Raspberry Pi OS 1. Update apt-get: `sudo apt-get update` -2. Install ADB and Fastboot: `sudo apt-get install -y android-tools-adb android-tools-fastboot pipenv` +2. Install ADB and Fastboot: `sudo apt-get install -y android-tools-adb android-tools-fastboot` 3. Clone project: `git clone https://github.com/GramAddict/bot.git gramaddict` 4. Go to GramAddict folder: `cd gramaddict` 5. (Optionally) Use virtualenv or similar to make a virtual environment `virtualenv -p python3 .venv` and enter the virtual environment `source .venv/bin/activate` diff --git a/filter.example b/filter.example new file mode 100644 index 00000000..eea301f6 --- /dev/null +++ b/filter.example @@ -0,0 +1,12 @@ +{ + "skip_business": true, + "skip_non_business": false, + "min_followers": 100, + "max_followers": 5000, + "min_followings": 10, + "max_followings": 1000, + "min_potency_ratio": 1, + "follow_private_or_empty": false, + "min_posts": 7, + "max_digits_in_profile_name": 4 +} \ No newline at end of file