Skip to content

Commit

Permalink
v1.5.1
Browse files Browse the repository at this point in the history
  • Loading branch information
davidwickerhf committed Oct 27, 2020
1 parent 7666513 commit 37df58e
Show file tree
Hide file tree
Showing 3 changed files with 115 additions and 89 deletions.
167 changes: 92 additions & 75 deletions instaclient/client/instaclient.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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):
"""
Expand All @@ -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)
Expand All @@ -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()
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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()
Expand All @@ -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'))
Expand All @@ -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
Expand All @@ -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]:
Expand All @@ -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()


Expand All @@ -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<str>: List of usernames (str)
Expand All @@ -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
Expand All @@ -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)
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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:
Expand All @@ -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:
Expand Down
Loading

0 comments on commit 37df58e

Please sign in to comment.