diff --git a/instaclient/client/instaclient.py b/instaclient/client/instaclient.py index 1f4a7ad..06c13a3 100644 --- a/instaclient/client/instaclient.py +++ b/instaclient/client/instaclient.py @@ -17,7 +17,7 @@ class InstaClient: CHROMEDRIVER=1 LOCAHOST=1 WEB_SERVER=2 - def __init__(self, driver_type: int=CHROMEDRIVER, host=None): + def __init__(self, driver_type: int=CHROMEDRIVER, host=LOCAHOST): """ Creates an instance of instaclient class. @@ -38,23 +38,44 @@ def __init__(self, driver_type: int=CHROMEDRIVER, host=None): # Running on web server chrome_options = webdriver.ChromeOptions() chrome_options.binary_location = os.environ.get("GOOGLE_CHROME_BIN") + chrome_options.add_argument('--user-agent=Mozilla/5.0 (iPhone; CPU iPhone OS 10_3 like Mac OS X) AppleWebKit/602.1.50 (KHTML, like Gecko) CriOS/56.0.2924.75 Mobile/14E5239e Safari/602.1') + chrome_options.add_argument("window-size=525,950") chrome_options.add_argument("--headless") chrome_options.add_argument("--disable-dev-shm-usage") chrome_options.add_argument("--no-sandbox") self.driver = webdriver.Chrome(executable_path=os.environ.get("CHROMEDRIVER_PATH"), chrome_options=chrome_options) elif host == self.LOCAHOST: # Running locally - self.driver = webdriver.Chrome('instaclient/drivers/chromedriver.exe') + chrome_options = webdriver.ChromeOptions() + chrome_options.add_argument('--user-agent=Mozilla/5.0 (iPhone; CPU iPhone OS 10_3 like Mac OS X) AppleWebKit/602.1.50 (KHTML, like Gecko) CriOS/56.0.2924.75 Mobile/14E5239e Safari/602.1') + chrome_options.add_argument("window-size=525,950") + self.driver = webdriver.Chrome(executable_path='instaclient/drivers/chromedriver.exe', chrome_options=chrome_options) else: raise InvaildHostError(host) else: raise InvaildDriverError(driver_type) - self.driver.maximize_window() except Exception as error: raise error self.logged_in = False + @insta_method + def check_status(self): + """ + Check if account is currently logged in. Returns True if account is logged in. Sets the instaclient.logged_in variable accordingly. + + :return: True if account is logged in, False if account is NOT logged in. + :rtype: boolean + """ + icon = self.__check_existence(EC.presence_of_element_located((By.XPATH, Paths.NAV_BAR)), wait_time=4) + if icon: + self.logged_in = True + return True + else: + self.logged_in = False + return False + + @insta_method def login(self, username:str, password:str): """ @@ -68,15 +89,15 @@ def login(self, username:str, password:str): self.driver.get(ClientUrls.LOGIN_URL) # Detect Cookies Dialogue try: - alert = self._find_element(EC.element_to_be_clickable((By.XPATH, Paths.ACCEPT_COOKIES)), wait_time=3) + alert = self.__find_element(EC.element_to_be_clickable((By.XPATH, Paths.ACCEPT_COOKIES)), wait_time=3) alert.click() except: print('No alert') pass # Get Form elements - username_input = self._find_element(EC.presence_of_element_located((By.XPATH,Paths.USERNAME_INPUT))) - password_input = self._find_element(EC.presence_of_element_located((By.XPATH,Paths.PASSWORD_INPUT))) - login_btn = self._find_element(EC.presence_of_element_located((By.XPATH,Paths.LOGIN_BTN)))# login button xpath changes after text is entered, find first + username_input = self.__find_element(EC.presence_of_element_located((By.XPATH,Paths.USERNAME_INPUT))) + password_input = self.__find_element(EC.presence_of_element_located((By.XPATH,Paths.PASSWORD_INPUT))) + login_btn = self.__find_element(EC.presence_of_element_located((By.XPATH,Paths.LOGIN_BTN)))# login button xpath changes after text is entered, find first # Fill out form username_input.send_keys(username) time.sleep(1) @@ -87,21 +108,24 @@ def login(self, username:str, password:str): # User already logged in ? print('User already logged in?') return self.logged_in + # Detect correct Login - try: - # Credentials Incorrect - alert: WebElement = self._find_element(EC.presence_of_element_located((By.XPATH,Paths.ALERT)), wait_time=3) - if 'username' in alert.text: #TODO insert in translation - self.driver.get(ClientUrls.LOGIN_URL) - raise InvalidUserError(self.username) - elif 'password' in alert.text: #TODO insert in translation - self.driver.get(ClientUrls.LOGIN_URL) - raise InvaildPasswordError(self.password) - except (TimeoutException, NoSuchElementException): - pass + usernamealert: WebElement = self.__check_existence(EC.presence_of_element_located((By.XPATH, Paths.INCORRECT_USERNAME_ALERT)), wait_time=3) + if usernamealert: + # Username is invalid + self.driver.get(ClientUrls.LOGIN_URL) + self.username = None + raise InvalidUserError(username) + + passwordalert: WebElement = self.__check_existence(EC.presence_of_element_located((By.XPATH,Paths.INCORRECT_PASSWORD_ALERT)), wait_time=3) + if passwordalert: + # Password is incorrect + self.driver.get(ClientUrls.LOGIN_URL) + self.password = None + raise InvaildPasswordError(password) # Detect 2FS - scode_input = self._check_existence(EC.presence_of_element_located((By.XPATH, Paths.SECURITY_CODE)), wait_time=4) + scode_input = self.__check_existence(EC.presence_of_element_located((By.XPATH, Paths.SECURITY_CODE)), wait_time=3) if scode_input: # 2F Auth is enabled, request security code raise SecurityCodeNecessary() @@ -113,7 +137,7 @@ def login(self, username:str, password:str): # Detect 'Turn On Notifications' Box try: - no_notifications_btn = self._find_element(EC.presence_of_element_located((By.XPATH, Paths.NO_NOTIFICATIONS_BTN)), wait_time=4) + no_notifications_btn = self.__find_element(EC.presence_of_element_located((By.XPATH, Paths.NO_NOTIFICATIONS_BTN)), wait_time=3) no_notifications_btn.click() except: pass @@ -135,13 +159,13 @@ def input_security_code(self, code): Raises: InvalidSecurityCodeError() if the security code is incorrect """ - scode_input: WebElement = self._find_element(EC.presence_of_element_located((By.XPATH, Paths.SECURITY_CODE)), wait_time=4) + scode_input: WebElement = self.__find_element(EC.presence_of_element_located((By.XPATH, Paths.SECURITY_CODE)), wait_time=4) scode_input.send_keys(code) - scode_btn: WebElement = self._find_element(EC.element_to_be_clickable((By.XPATH, Paths.SECURITY_CODE_BTN)), wait_time=5) + scode_btn: WebElement = self.__find_element(EC.element_to_be_clickable((By.XPATH, Paths.SECURITY_CODE_BTN)), wait_time=5) time.sleep(1) scode_btn.click() - alert = self._check_existence(EC.presence_of_element_located((By.XPATH, Paths.ALERT))) + alert = self.__check_existence(EC.presence_of_element_located((By.XPATH, Paths.ALERT))) if alert: # Code is Wrong # Clear input field @@ -165,7 +189,7 @@ def follow_user(self, user:str): self.nav_user(user) - follow_buttons = self._find_buttons('Follow') + follow_buttons = self.__find_buttons('Follow') for btn in follow_buttons: btn.click() @@ -182,12 +206,12 @@ def unfollow_user(self, user:str): self.nav_user(user) - unfollow_btns = self._find_buttons('Following') + unfollow_btns = self.__find_buttons('Following') if unfollow_btns: for btn in unfollow_btns: btn.click() - unfollow_confirmation = self._find_buttons('Unfollow')[0] + unfollow_confirmation = self.__find_buttons('Unfollow')[0] unfollow_confirmation.click() else: print('No {} buttons were found.'.format('Following')) @@ -214,7 +238,7 @@ def get_user_images(self, user:str): finished = self._infinite_scroll() # scroll down - elements = self._find_element((EC.presence_of_element_located(By.CLASS_NAME, 'FFVAD'))) + elements = self.__find_element((EC.presence_of_element_located(By.CLASS_NAME, 'FFVAD'))) img_srcs.extend([img.get_attribute('src') for img in elements]) # scrape srcs img_srcs = list(set(img_srcs)) # clean up duplicates @@ -240,7 +264,7 @@ def like_latest_posts(self, user:str, n_posts:int, like:bool=True): self.nav_user(user) imgs = [] - elements = self._find_element(EC.presence_of_all_elements_located((By.CLASS_NAME, '_9AhH0'))) + elements = self.__find_element(EC.presence_of_all_elements_located((By.CLASS_NAME, '_9AhH0'))) imgs.extend(elements) for img in imgs[:n_posts]: @@ -265,10 +289,10 @@ def send_dm(self, user:str, message:str, check_user=True): """ # Navigate to User's dm page self.nav_user_dm(user, check_user=check_user) - text_area = self._find_element(EC.presence_of_element_located((By.XPATH, Paths.DM_TEXT_AREA))) + text_area = self.__find_element(EC.presence_of_element_located((By.XPATH, Paths.DM_TEXT_AREA))) print(text_area) text_area.send_keys(message) - send_btn = self._find_element(EC.presence_of_element_located((By.XPATH, Paths.SEND_DM_BTN))) + send_btn = self.__find_element(EC.presence_of_element_located((By.XPATH, Paths.SEND_DM_BTN))) send_btn.click() @@ -287,16 +311,14 @@ def send_dm(self, user:str, message:str, check_user=True): @insta_method - def scrape_followers(self, user:str, count:int=100, callback_frequency:int=10, callback=None, check_user=True, *args, **kwargs): + def scrape_followers(self, user:str, check_user=True, *args, **kwargs): """ Gets all followers of a certain user Args: user:str: Username of the user for followers look-up count:int: Number of followers to get. Note that high follower counts will take longer and longer exponentially (even hours) - callback_frequency:int: Number of followers to get before sending an update - - + Returns: followers:list: List of usernames (str) @@ -307,48 +329,43 @@ def scrape_followers(self, user:str, count:int=100, callback_frequency:int=10, c # Nav to user page self.nav_user(user, check_user=check_user) # Find Followers button/link - followers_btn:WebElement = self._find_element(EC.presence_of_element_located((By.XPATH, Paths.FOLLOWERS_BTN)), wait_time=4) - # Get the number of followers and set the count - follower_count_div:WebElement = followers_btn.find_element_by_class_name(Paths.FOLLOWER_COUNT) - follower_count = int(follower_count_div.get_attribute('title')) - if count == -1: - # Scrape all followers: - count = follower_count - elif count > follower_count: - count = follower_count - + followers_btn:WebElement = self.__find_element(EC.presence_of_element_located((By.XPATH, Paths.FOLLOWERS_BTN)), wait_time=4) # Start scraping followers = [] # Click followers btn followers_btn.click() time.sleep(2) - for i in range(1,count+1): - try: - div:WebElement = self.driver.find_element_by_xpath(Paths.FOLLOWER_USER_DIV % i) - time.sleep(1) - username = div.text.split('\n')[0] - if username not in followers: - followers.append(username) - if i%callback_frequency==0: - if callback is None: - print('Got another {} followers...'.format(callback_frequency)) - else: - callback(*args, **kwargs) - self.driver.execute_script("arguments[0].scrollIntoView();", div) - # TODO OPTIMIZE ALGORITHM (scroll by more than one account only) - except Exception as error: - raise error - - # and you're back + # Load all followers + followers = [] + main:WebElement = self.__find_element(EC.presence_of_element_located((By.XPATH, Paths.FOLLOWERS_LIST_MAIN))) + size = main.size.get('height') + time.sleep(15) + while True: + main:WebElement = self.__find_element(EC.presence_of_element_located((By.XPATH, Paths.FOLLOWERS_LIST_MAIN)), wait_time=3) + new_size = main.size.get('height') + if new_size > 60000: + break + if new_size > size: + size = new_size + time.sleep(15) + continue + else: + break + followers_list:WebElement = self.__find_element(EC.presence_of_element_located((By.XPATH, Paths.FOLLOWERS_LIST)), wait_time=3) + divs = followers_list.find_elements_by_xpath(Paths.FOLLOWER_USER_DIV) + for div in divs: + username = div.text.split('\n')[0] + if username not in followers and username not in ('Follow',): + followers.append(username) return followers - + # IG UTILITY METHODS @insta_method def dismiss_dialogue(self): try: - dialogue = self._find_buttons(button_text='Not Now') # add this to 'Translation' doc + dialogue = self.__find_buttons(button_text='Not Now') # add this to 'Translation' doc dialogue.click() except: pass @@ -364,7 +381,7 @@ def search_tag(self, tag:str): """ self.driver.get(ClientUrls.SEARCH_TAGS.format(tag)) - alert: WebElement = self._check_existence(EC.presence_of_element_located((By.XPATH, Paths.PAGE_NOT_FOUND))) + alert: WebElement = self.__check_existence(EC.presence_of_element_located((By.XPATH, Paths.PAGE_NOT_FOUND))) if alert: # Tag does not exist raise InvaildTagError(tag=tag) @@ -374,7 +391,7 @@ def search_tag(self, tag:str): @insta_method - def nav_user(self, user:str, check_user=True): + def nav_user(self, user:str, check_user:bool=True): """ Navigates to a users profile page @@ -395,7 +412,7 @@ def nav_user(self, user:str, check_user=True): @insta_method - def nav_user_dm(self, user:str): + def nav_user_dm(self, user:str, check_user:bool=True): """ Open DM page with a specific user @@ -408,15 +425,15 @@ def nav_user_dm(self, user:str): Returns: True if operation was successful """ - self.nav_user(user) - message_btn = self._find_buttons('Message') + self.nav_user(user, check_user=check_user) + message_btn = self.__find_buttons('Message') # Open User DM Page message_btn.click() return True def is_valid_user(self, user): - element = self._check_existence(EC.presence_of_element_located((By.XPATH, Paths.PAGE_NOT_FOUND)), wait_time=3) + element = self.__check_existence(EC.presence_of_element_located((By.XPATH, Paths.PAGE_NOT_FOUND)), wait_time=3) if element: # User does not exist self.driver.get(ClientUrls.HOME_URL) @@ -425,7 +442,7 @@ def is_valid_user(self, user): else: # Operation Successful print('Sucessfully Navigated to user') - paccount_alert = self._check_existence(EC.presence_of_element_located((By.XPATH, Paths.PRIVATE_ACCOUNT_ALERT)), wait_time=3) + paccount_alert = self.__check_existence(EC.presence_of_element_located((By.XPATH, Paths.PRIVATE_ACCOUNT_ALERT)), wait_time=3) if paccount_alert: # navigate back to home page raise PrivateAccountError(user) @@ -461,18 +478,18 @@ def _infinite_scroll(self): return False - def _find_buttons(self, button_text:str): + def __find_buttons(self, button_text:str): """ Finds buttons for following and unfollowing users by filtering follow elements for buttons. Defaults to finding follow buttons. Args: button_text: Text that the desired button(s) has """ - buttons = self._find_element(EC.presence_of_element_located((By.XPATH, Paths.BUTTON.format(button_text))), wait_time=4) + buttons = self.__find_element(EC.presence_of_element_located((By.XPATH, Paths.BUTTON.format(button_text))), wait_time=4) return buttons - def _find_element(self, expectation, wait_time:int=10): + def __find_element(self, expectation, wait_time:int=10): """ Finds widget (element) based on the field's value Args: @@ -484,7 +501,7 @@ def _find_element(self, expectation, wait_time:int=10): return widgets - def _check_existence(self, expectation, wait_time:int=10): + def __check_existence(self, expectation, wait_time:int=10): """ Checks if an element exists. Args: diff --git a/instaclient/client/paths.py b/instaclient/client/paths.py index 6dfc3e1..ed9f019 100644 --- a/instaclient/client/paths.py +++ b/instaclient/client/paths.py @@ -1,21 +1,30 @@ class Paths: #TODO Traslate texts in German + # Login Procedue ACCEPT_COOKIES = "/html/body/div[2]/div/div/div/div[2]/button[1]" - LOGIN_BTN = '//*[@id="loginForm"]/div/div[3]/button' - USERNAME_INPUT = '//*[@id="loginForm"]/div/div[1]/div/label/input' - PASSWORD_INPUT = '//*[@id="loginForm"]/div/div[2]/div/label/input' + LOGIN_BTN = '//button[@class="sqdOP L3NKy y3zKF "]/div' + USERNAME_INPUT = '//input[@name="username"]' + PASSWORD_INPUT = '//input[@name="password"]' SECURITY_CODE = '//input[@name="verificationCode"]' SECURITY_CODE_BTN = '//button[@class="sqdOP L3NKy y3zKF "]' - BUTTON = '//button[text()="{}"]' NO_NOTIFICATIONS_BTN = '/html/body/div[4]/div/div/div/div[3]/button[2]' - + # Nav to User Procedure + INCORRECT_USERNAME_ALERT = '//p[@role="alert" and @id="slfErrorAlert"]' + INCORRECT_PASSWORD_ALERT = '//div[@class="piCib"]' + INCORRECT_PASSWORD_ALERT_BTNS = '{}//button[contains(@class, "aOOlW")]' ALERT = '//*[@id="slfErrorAlert" or @id="twoFactorErrorAlert"]' PAGE_NOT_FOUND = '//*[@class="Cv-5h"]' PRIVATE_ACCOUNT_ALERT = '//*[@class="rkEop"]' - FOLLOWER_COUNT = 'g47SY ' - FOLLOWERS_BTN = '//*[@id="react-root"]/section/main/div/header/section/ul/li[2]/a' - FOLLOWERS_DIV = '//div[@class="PZuss"]//a' - FOLLOWER_USER_DIV = '/html/body/div[4]/div/div/div[2]/ul/div/li[%s]' - - DM_TEXT_AREA = '//*[@id="react-root"]/section/div/div[2]/div/div/div[2]/div[2]/div/div[2]/div/div/div[2]/textarea' - SEND_DM_BTN = '//*[@id="react-root"]/section/div/div[2]/div/div/div[2]/div[2]/div/div[2]/div/div/div[3]/button' \ No newline at end of file + # Followers Procedure + FOLLOWERS_BTN = '//li[@class=" LH36I"]//descendant::a' + FOLLOWERS_LIST_MAIN = '//main[@role="main"]' + FOLLOWERS_LIST = '//ul[@class=" jjbaz _6xe7A"]' + FOLLOWER_USER_DIV = '{}//li'.format(FOLLOWERS_LIST) + FOLLOWER_COUNT = '{}//span[@class="g47SY lOXF2"]'.format(FOLLOWERS_BTN) + # Send DM Procedure + DM_TEXT_AREA = '//div[@class="X3a-9"]//descendant::textarea' + SEND_DM_BTN = '//div[@class="X3a-9"]//descendant::button' + # Check Login Status Procedure + NAV_BAR = '//div[@data-testid="mobile-nav-logged-in" and @class="BvyAW"]' + # GENERAL + BUTTON = '//button[text()="{}"]' \ No newline at end of file diff --git a/setup.py b/setup.py index 0355ccf..8901eba 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ setup( name = 'instaclient', # How you named your package folder (MyLib) packages = find_packages(exclude=['tests']), # Chose the same as "name" - version = '1.4.3', # Start with a small number and increase it with every change you make + version = '1.5.1', # Start with a small number and increase it with every change you make license='MIT', # Chose a license from here: https://help.github.com/articles/licensing-a-repository description = 'Instagram client built with Python 3.8 and the Selenium package.', long_description=README, @@ -19,7 +19,7 @@ author = 'David Wicker', # Type in your name author_email = 'davidwickerhf@gmail.com', # Type in your E-Mail url = 'https://github.com/wickerdevs/py-instaclient', # Provide either the link to your github or to your website - download_url = 'https://github.com/wickerdevs/py-instaclient/archive/v1.4.3.tar.gz', # I explain this later on + download_url = 'https://github.com/wickerdevs/py-instaclient/archive/v1.5.1.tar.gz', # I explain this later on keywords = ['INSTAGRAM', 'BOT', 'INSTAGRAM BOT', 'INSTAGRAM CLIENT'], # Keywords that define your package best install_requires=[ # I get to this in a second 'selenium',