Guest User

Untitled

a guest
Jul 1st, 2024
119
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 23.92 KB | Source Code | 0 0
  1. import logging
  2. from enum import Enum, unique
  3.  
  4. from colorama import Fore
  5.  
  6. from GramAddict.core.decorators import run_safely
  7. from GramAddict.core.device_facade import DeviceFacade, Timeout
  8. from GramAddict.core.plugin_loader import Plugin
  9. from GramAddict.core.resources import ClassName
  10. from GramAddict.core.resources import ResourceID as resources
  11. from GramAddict.core.scroll_end_detector import ScrollEndDetector
  12. from GramAddict.core.storage import FollowingStatus
  13. from GramAddict.core.utils import (
  14.     get_value,
  15.     inspect_current_view,
  16.     random_sleep,
  17.     save_crash,
  18. )
  19. from GramAddict.core.views import (
  20.     Direction,
  21.     FollowingView,
  22.     ProfileView,
  23.     UniversalActions,
  24. )
  25.  
  26. logger = logging.getLogger(__name__)
  27.  
  28. FOLLOWING_REGEX = "^Following|^Requested"
  29. UNFOLLOW_REGEX = "^Unfollow"
  30.  
  31.  
  32. class ActionUnfollowFollowers(Plugin):
  33.     """Handles the functionality of unfollowing your followers"""
  34.  
  35.     def __init__(self):
  36.         super().__init__()
  37.         self.description = "Handles the functionality of unfollowing your followers"
  38.         self.arguments = [
  39.             {
  40.                 "arg": "--unfollow",
  41.                 "nargs": None,
  42.                 "help": "unfollow at most given number of users. Only users followed by this script will be unfollowed. The order is from oldest to newest followings. It can be a number (e.g. 10) or a range (e.g. 10-20)",
  43.                 "metavar": "10-20",
  44.                 "default": None,
  45.                 "operation": True,
  46.             },
  47.             {
  48.                 "arg": "--unfollow-non-followers",
  49.                 "nargs": None,
  50.                 "help": "unfollow at most given number of users, that don't follow you back. Only users followed by this script will be unfollowed. The order is from oldest to newest followings. It can be a number (e.g. 10) or a range (e.g. 10-20)",
  51.                 "metavar": "10-20",
  52.                 "default": None,
  53.                 "operation": True,
  54.             },
  55.             {
  56.                 "arg": "--unfollow-any-non-followers",
  57.                 "nargs": None,
  58.                 "help": "unfollow at most given number of users, that don't follow you back. The order is from oldest to newest followings. It can be a number (e.g. 10) or a range (e.g. 10-20)",
  59.                 "metavar": "10-20",
  60.                 "default": None,
  61.                 "operation": True,
  62.             },
  63.             {
  64.                 "arg": "--unfollow-any-followers",
  65.                 "nargs": None,
  66.                 "help": "unfollow at most given number of users, that follow you back. The order is from oldest to newest followings. It can be a number (e.g. 10) or a range (e.g. 10-20)",
  67.                 "metavar": "10-20",
  68.                 "default": None,
  69.                 "operation": True,
  70.             },
  71.             {
  72.                 "arg": "--unfollow-any",
  73.                 "nargs": None,
  74.                 "help": "unfollow at most given number of users. The order is from oldest to newest followings. It can be a number (e.g. 10) or a range (e.g. 10-20)",
  75.                 "metavar": "10-20",
  76.                 "default": None,
  77.                 "operation": True,
  78.             },
  79.             {
  80.                 "arg": "--min-following",
  81.                 "nargs": None,
  82.                 "help": "minimum amount of followings, after reaching this amount unfollow stops",
  83.                 "metavar": "100",
  84.                 "default": 0,
  85.             },
  86.             {
  87.                 "arg": "--sort-followers-newest-to-oldest",
  88.                 "help": "sort the followers from newest to oldest instead of vice-versa (default)",
  89.                 "action": "store_true",
  90.             },
  91.             {
  92.                 "arg": "--unfollow-delay",
  93.                 "nargs": None,
  94.                 "help": "unfollow users followed by the bot after x amount of days",
  95.                 "metavar": "3",
  96.                 "default": "0",
  97.             },
  98.         ]
  99.  
  100.     def run(self, device, configs, storage, sessions, profile_filter, plugin):
  101.         class State:
  102.             def __init__(self):
  103.                 pass
  104.  
  105.             unfollowed_count = 0
  106.             is_job_completed = False
  107.  
  108.         self.args = configs.args
  109.         self.device_id = configs.args.device
  110.         self.state = State()
  111.         self.session_state = sessions[-1]
  112.         self.sessions = sessions
  113.         self.unfollow_type = plugin
  114.         self.ResourceID = resources(self.args.app_id)
  115.  
  116.         count_arg = get_value(
  117.             getattr(self.args, self.unfollow_type.replace("-", "_")),
  118.             "Unfollow count: {}",
  119.             10,
  120.         )
  121.  
  122.         count = min(
  123.             count_arg,
  124.             self.session_state.my_following_count - int(self.args.min_following),
  125.         )
  126.         if count < 1:
  127.             logger.warning(
  128.                 f"Now you're following {self.session_state.my_following_count} accounts, {'less then' if count <0 else 'equal to'} min following allowed (you set min-following: {self.args.min_following}). No further unfollows are required. Finish."
  129.             )
  130.             return
  131.         elif self.session_state.my_following_count < count_arg:
  132.             logger.warning(
  133.                 f"You can't unfollow {count_arg} accounts, because you are following {self.session_state.my_following_count} accounts. For that reason only {count} unfollows can be performed."
  134.             )
  135.         elif count < count_arg:
  136.             logger.warning(
  137.                 f"You can't unfollow {count_arg} accounts, because you set min-following to {self.args.min_following} and you have {self.session_state.my_following_count} followers. For that reason only {count} unfollows can be performed."
  138.             )
  139.  
  140.         if self.unfollow_type == "unfollow":
  141.             self.unfollow_type = UnfollowRestriction.FOLLOWED_BY_SCRIPT
  142.         elif self.unfollow_type == "unfollow-non-followers":
  143.             self.unfollow_type = UnfollowRestriction.FOLLOWED_BY_SCRIPT_NON_FOLLOWERS
  144.         elif self.unfollow_type == "unfollow-any-non-followers":
  145.             self.unfollow_type = UnfollowRestriction.ANY_NON_FOLLOWERS
  146.         elif self.unfollow_type == "unfollow-any-followers":
  147.             self.unfollow_type = UnfollowRestriction.ANY_FOLLOWERS
  148.         else:
  149.             self.unfollow_type = UnfollowRestriction.ANY
  150.  
  151.         @run_safely(
  152.             device=device,
  153.             device_id=self.device_id,
  154.             sessions=self.sessions,
  155.             session_state=self.session_state,
  156.             screen_record=self.args.screen_record,
  157.             configs=configs,
  158.         )
  159.         def job():
  160.             self.unfollow(
  161.                 device,
  162.                 count - self.state.unfollowed_count,
  163.                 self.on_unfollow,
  164.                 storage,
  165.                 self.unfollow_type,
  166.                 self.session_state.my_username,
  167.                 plugin,
  168.             )
  169.             logger.info(
  170.                 f"Unfollowed {self.state.unfollowed_count}, finish.",
  171.                 extra={"color": f"{Fore.CYAN}"},
  172.             )
  173.             self.state.is_job_completed = True
  174.             device.back()
  175.  
  176.         while not self.state.is_job_completed and (self.state.unfollowed_count < count):
  177.             job()
  178.  
  179.     def unfollow(
  180.         self,
  181.         device,
  182.         count,
  183.         on_unfollow,
  184.         storage,
  185.         unfollow_restriction,
  186.         my_username,
  187.         job_name,
  188.     ):
  189.         skipped_list_limit = get_value(self.args.skipped_list_limit, None, 15)
  190.         skipped_fling_limit = get_value(self.args.fling_when_skipped, None, 0)
  191.         posts_end_detector = ScrollEndDetector(
  192.             repeats_to_end=2,
  193.             skipped_list_limit=skipped_list_limit,
  194.             skipped_fling_limit=skipped_fling_limit,
  195.         )
  196.         ProfileView(device).navigateToFollowing()
  197.         self.iterate_over_followings(
  198.             device,
  199.             count,
  200.             on_unfollow,
  201.             storage,
  202.             unfollow_restriction,
  203.             my_username,
  204.             posts_end_detector,
  205.             job_name,
  206.         )
  207.  
  208.     def on_unfollow(self):
  209.         self.state.unfollowed_count += 1
  210.         self.session_state.totalUnfollowed += 1
  211.  
  212.     def sort_followings_by_date(self, device, newest_to_oldest=False) -> bool:
  213.         sort_button = device.find(
  214.             resourceId=self.ResourceID.SORTING_ENTRY_ROW_OPTION,
  215.         )
  216.         if not sort_button.exists(Timeout.MEDIUM):
  217.             logger.error(
  218.                 "Cannot find button to sort followings. Continue without sorting."
  219.             )
  220.             return False
  221.         sort_button.click()
  222.  
  223.         sort_options_recycler_view = device.find(
  224.             resourceId=self.ResourceID.FOLLOW_LIST_SORTING_OPTIONS_RECYCLER_VIEW
  225.         )
  226.         if not sort_options_recycler_view.exists(Timeout.MEDIUM):
  227.             logger.error(
  228.                 "Cannot find options to sort followings. Continue without sorting."
  229.             )
  230.             return False
  231.         if newest_to_oldest:
  232.             logger.info("Sort followings by date: from newest to oldest.")
  233.             sort_options_recycler_view.child(textContains="Latest").click()
  234.         else:
  235.             logger.info("Sort followings by date: from oldest to newest.")
  236.             sort_options_recycler_view.child(textContains="Earliest").click()
  237.         return True
  238.  
  239.     def iterate_over_followings(
  240.         self,
  241.         device,
  242.         count,
  243.         on_unfollow,
  244.         storage,
  245.         unfollow_restriction,
  246.         my_username,
  247.         posts_end_detector,
  248.         job_name,
  249.     ):
  250.         # Wait until list is rendered
  251.         sorted = False
  252.         for _ in range(2):
  253.             user_lst = device.find(
  254.                 resourceId=self.ResourceID.FOLLOW_LIST_CONTAINER,
  255.                 className=ClassName.LINEAR_LAYOUT,
  256.             )
  257.             user_lst.wait(Timeout.LONG)
  258.  
  259.             sort_container_obj = device.find(
  260.                 resourceId=self.ResourceID.SORTING_ENTRY_ROW_OPTION
  261.             )
  262.             if sort_container_obj.exists() and not sorted:
  263.                 sorted = self.sort_followings_by_date(
  264.                     device, self.args.sort_followers_newest_to_oldest
  265.                 )
  266.                 continue
  267.  
  268.             top_tab_obj = device.find(
  269.                 resourceId=self.ResourceID.UNIFIED_FOLLOW_LIST_TAB_LAYOUT
  270.             )
  271.             if sort_container_obj.exists(Timeout.SHORT) and top_tab_obj.exists(
  272.                 Timeout.SHORT
  273.             ):
  274.                 sort_container_bounds = sort_container_obj.get_bounds()["top"]
  275.                 list_tab_bounds = top_tab_obj.get_bounds()["bottom"]
  276.                 delta = sort_container_bounds - list_tab_bounds
  277.                 UniversalActions(device)._swipe_points(
  278.                     direction=Direction.DOWN,
  279.                     start_point_y=sort_container_bounds,
  280.                     delta_y=delta - 50,
  281.                 )
  282.             else:
  283.                 UniversalActions(device)._swipe_points(
  284.                     direction=Direction.DOWN, delta_y=380
  285.                 )
  286.  
  287.             if sort_container_obj.exists() and not sorted:
  288.                 self.sort_followings_by_date(
  289.                     device, self.args.sort_followers_newest_to_oldest
  290.                 )
  291.                 sorted = True
  292.         checked = {}
  293.         unfollowed_count = 0
  294.         total_unfollows_limit_reached = False
  295.         posts_end_detector.notify_new_page()
  296.         prev_screen_iterated_followings = []
  297.         # variables to save appeared usernames
  298.         seen_users = set()
  299.         seen_user_threshold = 3 #how many people with the same usernames the bot should see again to stop
  300.         seen_user_count = 0
  301.         while True:
  302.             screen_iterated_followings = []
  303.             logger.info("Iterate over visible followings.")
  304.             user_list = device.find(
  305.                 resourceIdMatches=self.ResourceID.USER_LIST_CONTAINER,
  306.             )
  307.             row_height, n_users = inspect_current_view(user_list)
  308.             for item in user_list:
  309.                 # inner user_list counter
  310.                 cur_row_height = item.get_height()
  311.                 if cur_row_height < row_height:
  312.                     continue
  313.                 user_info_view = item.child(index=1)
  314.                 user_name_view = user_info_view.child(index=0).child()
  315.                 if not user_name_view.exists():
  316.                     logger.info(
  317.                         "Next item not found: probably reached end of the screen.",
  318.                         extra={"color": f"{Fore.GREEN}"},
  319.                     )
  320.                     break
  321.  
  322.                 username = user_name_view.get_text()
  323.                 screen_iterated_followings.append(username)
  324.                 # check if a username has seen previously
  325.                 if username in seen_users:
  326.                     seen_user_count += 1
  327.                 seen_users.add(username)
  328.                 if username not in checked:
  329.                     checked[username] = None
  330.  
  331.                     if storage.is_user_in_whitelist(username):
  332.                         logger.info(f"@{username} is in whitelist. Skip.")
  333.                         continue
  334.  
  335.                     if unfollow_restriction in [
  336.                         UnfollowRestriction.FOLLOWED_BY_SCRIPT,
  337.                         UnfollowRestriction.FOLLOWED_BY_SCRIPT_NON_FOLLOWERS,
  338.                     ]:
  339.                         following_status = storage.get_following_status(username)
  340.                         _, last_interaction = storage.check_user_was_interacted(
  341.                             username
  342.                         )
  343.                         if following_status == FollowingStatus.NOT_IN_LIST:
  344.                             logger.info(
  345.                                 f"@{username} has not been followed by this bot. Skip."
  346.                             )
  347.                             continue
  348.                         elif not storage.can_be_unfollowed(
  349.                             last_interaction,
  350.                             get_value(self.args.unfollow_delay, None, 0),
  351.                         ):
  352.                             logger.info(
  353.                                 f"@{username} has been followed less then {self.args.unfollow_delay} days ago. Skip."
  354.                             )
  355.                             continue
  356.                         elif following_status == FollowingStatus.UNFOLLOWED:
  357.                             logger.info(
  358.                                 f"You have already unfollowed @{username} on {last_interaction}. Probably you got a soft ban at some point. Try again... Following status: {following_status.name}."
  359.                             )
  360.                         elif following_status not in (
  361.                             FollowingStatus.FOLLOWED,
  362.                             FollowingStatus.REQUESTED,
  363.                         ):
  364.                             logger.info(
  365.                                 f"Skip @{username}. Following status: {following_status.name}."
  366.                             )
  367.                             continue
  368.  
  369.                     if unfollow_restriction in [
  370.                         UnfollowRestriction.ANY,
  371.                         UnfollowRestriction.ANY_NON_FOLLOWERS,
  372.                     ]:
  373.                         following_status = storage.get_following_status(username)
  374.                         if following_status == FollowingStatus.UNFOLLOWED:
  375.                             logger.info(
  376.                                 f"Skip @{username}. Following status: {following_status.name}."
  377.                             )
  378.                             continue
  379.                     if unfollow_restriction in [
  380.                         UnfollowRestriction.ANY,
  381.                         UnfollowRestriction.FOLLOWED_BY_SCRIPT,
  382.                     ]:
  383.                         unfollowed = FollowingView(device).do_unfollow_from_list(
  384.                             user_row=item, username=username
  385.                         )
  386.                     else:
  387.                         unfollowed = self.do_unfollow(
  388.                             device,
  389.                             username,
  390.                             my_username,
  391.                             unfollow_restriction
  392.                             in [
  393.                                 UnfollowRestriction.FOLLOWED_BY_SCRIPT_NON_FOLLOWERS,
  394.                                 UnfollowRestriction.ANY_NON_FOLLOWERS,
  395.                                 UnfollowRestriction.ANY_FOLLOWERS,
  396.                             ],
  397.                             job_name == "unfollow-any-followers",
  398.                         )
  399.  
  400.                     if unfollowed:
  401.                         storage.add_interacted_user(
  402.                             username,
  403.                             self.session_state.id,
  404.                             unfollowed=True,
  405.                             job_name=job_name,
  406.                             target=None,
  407.                         )
  408.                         on_unfollow()
  409.                         unfollowed_count += 1
  410.                         total_unfollows_limit_reached = self.session_state.check_limit(
  411.                             limit_type=self.session_state.Limit.UNFOLLOWS,
  412.                             output=True,
  413.                         )
  414.                     if unfollowed_count >= count or total_unfollows_limit_reached:
  415.                         return
  416.                 else:
  417.                     logger.debug(f"Already checked {username}.")
  418.  
  419.             if screen_iterated_followings != prev_screen_iterated_followings:
  420.                 prev_screen_iterated_followings = screen_iterated_followings
  421.                 # exit if reach seen threshold
  422.                 if seen_user_count > seen_user_threshold:
  423.                     logger.info(
  424.                         "Reached the following list end, finish.",
  425.                         extra={"color": f"{Fore.GREEN}"},
  426.                     )
  427.                     return
  428.                 logger.info("Need to scroll now.", extra={"color": f"{Fore.GREEN}"})
  429.                 list_view = device.find(
  430.                     resourceId=self.ResourceID.LIST,
  431.                 )
  432.                 list_view.scroll(Direction.DOWN)
  433.             else:
  434.                 load_more_button = device.find(
  435.                     resourceId=self.ResourceID.ROW_LOAD_MORE_BUTTON
  436.                 )
  437.                 if load_more_button.exists():
  438.                     load_more_button.click()
  439.                     random_sleep()
  440.                     if load_more_button.exists():
  441.                         logger.warning(
  442.                             "Can't iterate over the list anymore, you may be soft-banned and cannot perform this action (refreshing follower list)."
  443.                         )
  444.                         return
  445.                     list_view.scroll(Direction.DOWN)
  446.                 else:
  447.                     logger.info(
  448.                         "Reached the following list end, finish.",
  449.                         extra={"color": f"{Fore.GREEN}"},
  450.                     )
  451.                     return
  452.  
  453.     def do_unfollow(
  454.         self,
  455.         device: DeviceFacade,
  456.         username,
  457.         my_username,
  458.         check_if_is_follower,
  459.         unfollow_followers=False,
  460.     ):
  461.         """
  462.        :return: whether unfollow was successful
  463.        """
  464.         username_view = device.find(
  465.             resourceId=self.ResourceID.FOLLOW_LIST_USERNAME,
  466.             className=ClassName.TEXT_VIEW,
  467.             text=username,
  468.         )
  469.         if not username_view.exists():
  470.             logger.error(f"Cannot find @{username}, skip.")
  471.             return False
  472.         username_view.click_retry()
  473.  
  474.         is_following_you = self.check_is_follower(device, username, my_username)
  475.         if is_following_you is not None:
  476.             if check_if_is_follower and is_following_you:
  477.                 if not unfollow_followers:
  478.                     logger.info(f"Skip @{username}. This user is following you.")
  479.                     logger.info("Back to the followings list.")
  480.                     device.back()
  481.                     return False
  482.                 else:
  483.                     logger.info(f"@{username} is following you, unfollow. 😈")
  484.             unfollow_button = device.find(
  485.                 classNameMatches=ClassName.BUTTON_OR_TEXTVIEW_REGEX,
  486.                 clickable=True,
  487.                 textMatches=FOLLOWING_REGEX,
  488.             )
  489.             # I don't know/remember the origin of this, if someone does - let's document it
  490.             attempts = 2
  491.             for _ in range(attempts):
  492.                 if unfollow_button.exists():
  493.                     break
  494.  
  495.                 scrollable = device.find(classNameMatches=ClassName.VIEW_PAGER)
  496.                 if scrollable.exists():
  497.                     scrollable.scroll(Direction.UP)
  498.                 unfollow_button = device.find(
  499.                     classNameMatches=ClassName.BUTTON_OR_TEXTVIEW_REGEX,
  500.                     clickable=True,
  501.                     textMatches=FOLLOWING_REGEX,
  502.                 )
  503.  
  504.             if not unfollow_button.exists():
  505.                 logger.error("Cannot find Following button.")
  506.                 save_crash(device)
  507.             logger.debug("Unfollow button click.")
  508.             unfollow_button.click()
  509.             logger.info(f"Unfollow @{username}.", extra={"color": f"{Fore.YELLOW}"})
  510.  
  511.             # Weirdly enough, this is a fix for after you unfollow someone that follows
  512.             # you back - the next person you unfollow the button is missing on first find
  513.             # additional find - finds it. :shrug:
  514.             confirm_unfollow_button = None
  515.             attempts = 2
  516.             for _ in range(attempts):
  517.                 confirm_unfollow_button = device.find(
  518.                     resourceId=self.ResourceID.FOLLOW_SHEET_UNFOLLOW_ROW
  519.                 )
  520.                 if confirm_unfollow_button.exists(Timeout.SHORT):
  521.                     break
  522.  
  523.             if not confirm_unfollow_button or not confirm_unfollow_button.exists():
  524.                 logger.error("Cannot confirm unfollow.")
  525.                 save_crash(device)
  526.                 device.back()
  527.                 return False
  528.             logger.debug("Confirm unfollow.")
  529.             confirm_unfollow_button.click()
  530.  
  531.             random_sleep(0, 1, modulable=False)
  532.  
  533.             # Check if private account confirmation
  534.             private_unfollow_button = device.find(
  535.                 classNameMatches=ClassName.BUTTON_OR_TEXTVIEW_REGEX,
  536.                 textMatches=UNFOLLOW_REGEX,
  537.             )
  538.             if private_unfollow_button.exists(Timeout.SHORT):
  539.                 logger.debug("Confirm unfollow private account.")
  540.                 private_unfollow_button.click()
  541.  
  542.             UniversalActions.detect_block(device)
  543.         else:
  544.             logger.info("Back to the followings list.")
  545.             device.back()
  546.             return False
  547.         logger.info("Back to the followings list.")
  548.         device.back()
  549.         return True
  550.  
  551.     def check_is_follower(self, device, username, my_username):
  552.         logger.info(
  553.             f"Check if @{username} is following you.", extra={"color": f"{Fore.GREEN}"}
  554.         )
  555.  
  556.         if not ProfileView(device).navigateToFollowing():
  557.             logger.info("Can't load profile in time. Skip.")
  558.             return None
  559.  
  560.         rows = device.find(
  561.             resourceId=self.ResourceID.FOLLOW_LIST_USERNAME,
  562.             className=ClassName.TEXT_VIEW,
  563.         )
  564.         if rows.exists(Timeout.LONG):
  565.             my_username_view = device.find(
  566.                 resourceId=self.ResourceID.FOLLOW_LIST_USERNAME,
  567.                 className=ClassName.TEXT_VIEW,
  568.                 text=my_username,
  569.             )
  570.             result = my_username_view.exists()
  571.             logger.info("Back to the profile.")
  572.             device.back()
  573.             return result
  574.         else:
  575.             logger.info("Can't load profile followers in time. Skip.")
  576.             device.back()
  577.             return None
  578.  
  579.  
  580. @unique
  581. class UnfollowRestriction(Enum):
  582.     ANY = 0
  583.     FOLLOWED_BY_SCRIPT = 1
  584.     FOLLOWED_BY_SCRIPT_NON_FOLLOWERS = 2
  585.     ANY_NON_FOLLOWERS = 3
  586.     ANY_FOLLOWERS = 4
  587.  
Advertisement
Add Comment
Please, Sign In to add comment