Skip to content

Commit

Permalink
Added forward_post() & set_session_cookies()
Browse files Browse the repository at this point in the history
  • Loading branch information
davidwickerhf committed Jan 21, 2021
1 parent 63639e1 commit b94278e
Show file tree
Hide file tree
Showing 6 changed files with 153 additions and 46 deletions.
29 changes: 23 additions & 6 deletions instaclient/client/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

class Auth(Checker):
@Component._driver_required
def login(self:'InstaClient', username:str, password:str, check_user:bool=True) -> bool:
def login(self:'InstaClient', username:str, password:str) -> bool:
"""
#to Instagram with credentials. Go through 2FA if necessary. Sets the InstaClient variable `InstaClient.logged_in` to True if login was successful.
Expand Down Expand Up @@ -74,9 +74,8 @@ def login(self:'InstaClient', username:str, password:str, check_user:bool=True)
if waitalert:
raise LoginFloodException()

if check_user:
usernamealert: WebElement = self._check_existence(EC.presence_of_element_located((By.XPATH, Paths.INCORRECT_USERNAME_ALERT)), wait_time=2)
if usernamealert:
usernamealert: WebElement = self._check_existence(EC.presence_of_element_located((By.XPATH, Paths.INCORRECT_USERNAME_ALERT)), wait_time=2)
if usernamealert:
# Username is invalid
self.driver.get(ClientUrls.LOGIN_URL)
self.username = None
Expand Down Expand Up @@ -118,9 +117,9 @@ def login(self:'InstaClient', username:str, password:str, check_user:bool=True)
LOGGER.debug('INSTACLIENT: Credentials are Correct')

# Discard Driver or complete login
not_now = self._check_existence(EC.presence_of_element_located((By.XPATH, Paths.NOT_NOW_INFO_BTN)))
not_now = self._check_existence(EC.presence_of_element_located((By.XPATH, Paths.SAVE_INFO_BTN)))
if not_now:
not_now = self._find_element(EC.presence_of_element_located((By.XPATH, Paths.NOT_NOW_INFO_BTN)))
not_now = self._find_element(EC.presence_of_element_located((By.XPATH, Paths.SAVE_INFO_BTN)))
self._press_button(not_now)
LOGGER.debug('Dismissed Dialogue')

Expand All @@ -131,6 +130,24 @@ def login(self:'InstaClient', username:str, password:str, check_user:bool=True)
LOGGER.debug('Dismissed Dialogue')
return self.logged_in

@Component._driver_required
def set_session_cookies(self:'InstaClient', cookies:list):
self._nav_home()
for cookie in cookies:
try:
if 'instagram.com' not in cookie['domain']:
continue
self.driver.add_cookie(cookie)
except Exception as error:
LOGGER.warning(f'Error setting cookie: {str(error)}: {cookie}')
self.driver.refresh()
if self.logged_in:
LOGGER.info('Logged in with cookies')
else:
raise NotLoggedInError()
self._nav_home()
return self


@Component._driver_required
def resend_security_code(self:'InstaClient'):
Expand Down
38 changes: 14 additions & 24 deletions instaclient/client/component.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ class Component:
def _login_required(func):
@wraps(func)
def wrapper(self: 'InstaClient', *args, **kwargs):
if self.username is None or self.password is None:
raise NotLoggedInError()

if not self.logged_in:
if not self.username or not self.password:
raise NotLoggedInError()
if not self.driver:
self.connect(True, func=func.__name__)
else:
Expand Down Expand Up @@ -122,7 +122,6 @@ def connect(self: 'InstaClient', login=False, retries=0, func=None):
if self.proxy:
chrome_options.add_argument('--proxy-server=%s' % self.proxy)
self.driver = webdriver.Chrome(executable_path=os.environ.get("CHROMEDRIVER_PATH"), chrome_options=chrome_options)
return self
elif self.host_type == self.LOCAHOST:
# Running locally
chrome_options = webdriver.ChromeOptions()
Expand All @@ -137,7 +136,6 @@ def connect(self: 'InstaClient', login=False, retries=0, func=None):
chrome_options.add_argument('--proxy-server=%s' % self.proxy)

self.driver = webdriver.Chrome(executable_path=self.driver_path, chrome_options=chrome_options)
return self
else:
raise InvaildHostError(self.host_type)
else:
Expand All @@ -149,13 +147,15 @@ def connect(self: 'InstaClient', login=False, retries=0, func=None):
else:
raise error

self.driver.get(ClientUrls.HOME_URL)
self._dismiss_cookies()
LOGGER.debug(f'Logging in: {login}')
if login:
try:
self.login(self.username, self.password)
return self
except:
raise InstaClientError(message='Tried logging in when initiating driver, but username and password are not defined.')
return self


# IG PRIVATE UTILITIES (The client is considered initiated)
Expand Down Expand Up @@ -252,28 +252,24 @@ def _dismiss_cookies(self):
LOGGER.debug('Dismissed Cookies')


def _dismiss_useapp_bar(self):
dismiss_bar = self._check_existence(EC.presence_of_element_located((By.XPATH, Paths.USE_APP_BAR)))
def _dismiss_useapp_bar(self, wait_time=1.5):
dismiss_bar = self._check_existence(EC.presence_of_element_located((By.XPATH, Paths.USE_APP_BAR)), wait_time=wait_time)
if dismiss_bar:
LOGGER.debug('Dismissed Use App Bar')
dismiss = self._find_element(EC.presence_of_element_located((By.XPATH, Paths.USE_APP_BAR)))
self._press_button(dismiss)


def _dismiss_dialogue(self, wait_time:float=2):
def _dismiss_dialogue(self:'InstaClient', wait_time:float=1.5):
"""
Dismiss an eventual Instagram dialogue with button text containing either 'Cancel' or 'Not Now'.
"""
try:
if self._check_existence(EC.presence_of_element_located((By.XPATH, Paths.NOT_NOW_BTN))):
if self._check_existence(EC.presence_of_element_located((By.XPATH, Paths.DIALOGUE)), wait_time=wait_time):
dialogue = self._find_element(EC.presence_of_element_located((By.XPATH, Paths.NOT_NOW_BTN)), wait_time=wait_time)
self._press_button(dialogue)
except:
try:
dialogue = self.__find_buttons(button_text='Cancel') # TODO add this to translation docs
self._press_button(dialogue)
except:
pass

pass


def _press_button(self, button):
Expand All @@ -282,17 +278,11 @@ def _press_button(self, button):
time.sleep(randrange(0,2))
self._detect_restriction()
return True
except:
x = self._find_element(EC.presence_of_element_located((By.XPATH, Paths.X)), wait_time=3)
x.click()
time.sleep(1)
button.click()
time.sleep(randrange(0,2))
self._detect_restriction()
return True
except Exception as error:
LOGGER.warning('Error pressing button', exc_info=error)
return False



def _detect_restriction(self):
"""
_detect_restriction detects wheter instagram has restricted the current account
Expand Down
16 changes: 11 additions & 5 deletions instaclient/client/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,12 @@ class Paths:

# ENGAGEMENT PROCEDURES
# Post Interactions
POST_DIV = '//a[@href="/p/{}/"]'
COMMENT_TEXT_AREA = '//textarea[@class="Ypffh"]'
SEND_COMMENT_BTN = '//button[@class="sqdOP yWX7d y3zKF "]'
LIKE_BTN = '//span[@class="fr66n"]'
LIKE_BTN = '//span[@class="fr66n"]//descendant::button'
COMMENT_BTN = '//span[@class="_15y0l"]'
SHARE_POST_BTN = '//span[@class="_8-yf5 "]'
SHARE_POST_BTN = '//*[@id="react-root"]/section/main/div/div/article/div[3]/section[1]/button' # TODO critical

# Follow User Procedure
FOLLOW_BTN = '//button[@class="sqdOP L3NKy _4pI4F y3zKF " or @class="_5f5mN jIbKX _6VtSN yZn4P "]'
Expand All @@ -60,6 +61,7 @@ class Paths:
DM_USERNAME_DIV = '//div[@class="_7UhW9 xLCgt MMzan KV-D4 fDxYl "]'

# GENERAL
DIALOGUE = '//button[contains(text(), "Cancel") or contains(text(), "Not Now")]'
USE_THE_APP = '//button[@class="sqdOP yWX7d y3zKF cB_4K "]' #TODO
X = '//div[@class="storiesSpriteX__outline__44 u-__7"]'
USE_APP_BAR = '//button[@class="dCJp8 "]'
Expand All @@ -68,11 +70,14 @@ class Paths:
SETTINGS_BTN = '//button[@class="Q46SR"]'
BUTTON = '//button[text()="{}"]'
NOT_NOW_BTN = '//button[@class="aOOlW HoLwm "]'
NOT_NOW_INFO_BTN = '//button[@class="sqdOP yWX7d y3zKF "]'
SAVE_INFO_BTN = '//button[@class="sqdOP L3NKy y3zKF "]'
RESTRICTION_DIALOG = '//div[@class="_7UhW9 xLCgt MMzan _0PwGv uL8Hv " and contains(text(), "restrict")]'
RESTRICTION_DIALOGUE_BTNS = '//div[@class="pbNvD fPMEg " and @role="dialog"]//descendant::button'
BLOCK_DIV = '//div[@class="_7UhW9 vy6Bb MMzan KV-D4 uL8Hv l4b0S " and contains(text(), "unusual activity")]'
QUERY_ELEMENT = '//body//descendant::pre'
# Navigation Bar
HOME_BTN = '//a[@href="/"]'
EXPLORE_BTN = '//a[@href="/explore/"]'

# SETTINGS OPTIONS
LOG_OUT_BTN = '//a[@class="_34G9B H0ovd"]'
Expand All @@ -91,7 +96,8 @@ class Paths:
SHORTCODE_DIV = '//div[@class="v1Nh3 kIKUG _bz0w"]//descendant::a'

# EXPLORE PAGE
EXPLORE_SEARCH_INPUT = '//input[@class="j_2Hd iwQA6 RO68f M5V28"]'
EXPLORE_SEARCH_INPUT = '//label[@class="NcCcD"]//descendant::input'
SEARCH_USER_DIV = '//div[@class="_7UhW9 xLCgt qyrsm KV-D4 uL8Hv " and contains(text(), {})]'



Expand All @@ -101,7 +107,7 @@ class ClientUrls:
NEW_DM = 'https://www.instagram.com/direct/new/'
SEARCH_TAGS='https://www.instagram.com/explore/tags/{}/'
FOLLOWERS_URL = 'https://www.instagram.com/{}/followers/'
HOME_URL = 'https://www.instagram.com/'
HOME_URL = 'https://www.instagram.com'
LOGIN_THEN_USER = 'https://www.instagram.com/accounts/#/?next=/{}/'
SECURITY_CODE_URL = 'https://www.instagram.com/challenge/'
DM_URL = 'https://www.instagram.com/direct/t/'
Expand Down
14 changes: 12 additions & 2 deletions instaclient/client/instaclient.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""InstaClient class"""

# IMPORT UTILITIES, DEPENDENCIES & MODELS
from selenium.webdriver.remote.webdriver import WebDriver
from instaclient.instagram.comment import Comment
from instaclient.client import *

Expand Down Expand Up @@ -46,7 +47,7 @@ def __init__(self, driver_type: int=CHROMEDRIVER, host_type:int=LOCAHOST, driver
self.error_callback = error_callback
self.localhost_headless = localhost_headless
self.proxy = proxy
self.driver = None
self.driver:WebDriver = None
self.username = None
self.password = None

Expand All @@ -71,8 +72,10 @@ def logged_in(self) -> bool:
if self.driver:
if self.username and self.password:
url = self.driver.current_url
if 'https://www.instagram.com/' in url and ClientUrls.LOGIN_URL not in url:
if 'https://www.instagram.com' in url and ClientUrls.LOGIN_URL not in url:
return True
elif self.session_cookies:
return True
return False

@property
Expand All @@ -92,6 +95,13 @@ def threads(self) -> Optional[list]:
else:
return running

@property
def session_cookies(self):
if not self.driver:
return None
# TODO Filter cookies
return self.driver.get_cookies()

@property
def logger(self) -> Optional[logging.Logger]:
return LOGGER
Expand Down
65 changes: 62 additions & 3 deletions instaclient/client/interactions.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,14 +139,73 @@ def send_dm(self:'InstaClient', user:str, message:str):
raise error


""" @Component._login_required
def forward_post(self:'InstaClient', shortcode:str, user:str, message:str):
@Component._login_required
def forward_post(self:'InstaClient', shortcode:str, user:str, message:str=None):
# Load Post Page
post = self.get_post(shortcode)
if not post:
raise InvalidShortCodeError(shortcode)

# Forward """
# Nav home page
self._nav_home(manual=True)
# Nav search page
self._nav_explore(manual=True)
# Insert username in field
input_bar:WebElement = self._find_element(EC.presence_of_element_located((By.XPATH, Paths.EXPLORE_SEARCH_INPUT)))
input_bar.send_keys(post.owner)
# Click user div
user_div:WebElement = self._find_element(EC.presence_of_element_located((By.XPATH, Paths.SEARCH_USER_DIV.format(post.owner))))
self._press_button(user_div)
self._dismiss_useapp_bar()
# Scroll through posts to find correct one
last = None
break_warning = False
while True:
self.scroll(interval=1)
posts:WebElement = self._find_element(EC.presence_of_all_elements_located((By.XPATH, Paths.SHORTCODE_DIV)))
if posts[-1] == last:
if break_warning:
raise InvalidShortCodeError(shortcode)
else:
break_warning = True
last = posts[-1]

for post in posts:
pshortcode = post.get_attribute('href')
pshortcode = pshortcode.replace('https://www.instagram.com/p/', '')
pshortcode = pshortcode.replace('/', '')
if shortcode == pshortcode:
# Open Post
post:WebElement = self._find_element(EC.presence_of_element_located((By.XPATH, Paths.POST_DIV.format(pshortcode))))
post.click()
break
else:
continue
break
# Forward
share_btn = self._find_element(EC.presence_of_element_located((By.XPATH, Paths.SHARE_POST_BTN)))
LOGGER.debug("Found share button")
self._press_button(share_btn)
LOGGER.debug("Pressed Share button")

user_input = self._find_element(EC.presence_of_element_located((By.XPATH, Paths.SEARCH_USER_INPUT)))
user_input.send_keys(user)
LOGGER.debug("Found user input div")

user_div = self._find_element(EC.presence_of_element_located((By.XPATH, Paths.SEARCH_USER_DIV.format(user))))
self._press_button(user_div)
LOGGER.debug("Selected target user")

send_btn = self._find_element(EC.presence_of_element_located((By.XPATH, Paths.NEXT_BUTTON)))
self._press_button(send_btn)

if message:
self.send_dm(user, message)
return True





# ENGAGEMENT PROCEDURES
@Component._login_required
Expand Down
37 changes: 31 additions & 6 deletions instaclient/client/navigator.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,26 @@
class Navigator(Checker):

# NAVIGATION PROCEDURES
def _show_nav_bar(self:'InstaClient'):
if self.driver.current_url != ClientUrls.HOME_URL:
self._nav_home()
self._dismiss_dialogue()
self._dismiss_useapp_bar()


def _nav_home(self:'InstaClient', manual=False):
"""Navigates to IG home page
"""
if not manual:
if self.driver.current_url != ClientUrls.HOME_URL:
self.driver.get(ClientUrls.HOME_URL)
self._dismiss_dialogue()
else:
self._show_nav_bar()
home_btn = self._find_element(EC.presence_of_element_located((By.XPATH, Paths.HOME_BTN)))
self._press_button(home_btn)


def _nav_user(self:'InstaClient', user:str, check_user:bool=True):
"""
Navigates to a users profile page
Expand All @@ -26,6 +46,7 @@ def _nav_user(self:'InstaClient', user:str, check_user:bool=True):
result = self.is_valid_user(user=user)
if self.driver.current_url != ClientUrls.NAV_USER.format(user):
self.driver.get(ClientUrls.NAV_USER.format(user))
self._dismiss_useapp_bar()


def _nav_user_dm(self:'InstaClient', user:str, check_user:bool=True):
Expand Down Expand Up @@ -146,11 +167,15 @@ def _nav_location(self:'InstaClient', id:str, slug:str):
raise InvaildLocationError(id, slug)



def _nav_explore(self:'InstaClient'):
def _nav_explore(self:'InstaClient', manual=False):
"""Navigates to the explore page
"""
self.driver.get(ClientUrls.EXPLORE_PAGE)
if self._is_valid_page(ClientUrls.EXPLORE_PAGE):
return True
return False
if not manual:
self.driver.get(ClientUrls.EXPLORE_PAGE)
if self._is_valid_page(ClientUrls.EXPLORE_PAGE):
return True
return False
else:
self._show_nav_bar()
explore_btn = self._find_element(EC.presence_of_element_located((By.XPATH, Paths.EXPLORE_BTN)))
self._press_button(explore_btn)

0 comments on commit b94278e

Please # to comment.