Repository: InstaPy/InstaPy

Issue:
Browser class abstraction
#### Chrome/Firefox should have a separate class from instapy, and should handle executions/errors and must have a recovery method that WILL NOT fail the instapy flow.

Advantages of this feature:
- Decoupled error handling of web driver problems
- Better recovery capabilities

Possible ways of achieving this while keeping control:
- Creating a abstraction with a Browser class the handles errors and cares about recovery 

> [Bountysource this Issue](https://www.bountysource.com/issues/67497396-browser-class-abstraction)

--- 

###### Feel free to comment with adjustments and improvements


Discussion:
I VOTE FOR IT :)

#3596 

Resolving patch:
diff --git a/instapy/browser.py b/instapy/browser.py
--- a/instapy/browser.py
+++ b/instapy/browser.py
@@ -6,6 +6,7 @@
 from selenium.webdriver.common.proxy import Proxy, ProxyType
 from selenium.webdriver.common.keys import Keys
 from selenium.webdriver.firefox.options import Options as Firefox_Options
+from selenium.webdriver import Remote
 
 
 # general libs
@@ -15,6 +16,9 @@
 # local project
 from .util import interruption_handler
 from .util import highlight_print
+from .util import emergency_exit
+from .util import get_current_url
+from .util import check_authorization
 from .settings import Settings
 from .file_manager import get_chromedriver_location
 
@@ -66,6 +70,9 @@ def set_selenium_local_session(proxy_address,
         browser = webdriver.Firefox(firefox_profile=firefox_profile,
                                     options=firefox_options)
 
+        # converts to custom browser
+        browser = convert_selenium_browser(browser)
+    
         # authenticate with popup alert window
         if (proxy_username and proxy_password):
             proxy_authentication(browser,
@@ -76,7 +83,7 @@ def set_selenium_local_session(proxy_address,
     else:
         chromedriver_location = get_chromedriver_location()
         chrome_options = Options()
-        chrome_options.add_argument("--mute-audio")
+        chrome_options.add_argument('--mute-audio')
         chrome_options.add_argument('--dns-prefetch-disable')
         chrome_options.add_argument('--lang=en-US')
         chrome_options.add_argument('--disable-setuid-sandbox')
@@ -104,7 +111,7 @@ def set_selenium_local_session(proxy_address,
             proxy = ":".join([proxy_address, str(proxy_port)])
             if headless_browser:
                 chrome_options.add_argument(
-                    "--proxy-server=http://{}".format(proxy))
+                    '--proxy-server=http://{}'.format(proxy))
             else:
                 prox.proxy_type = ProxyType.MANUAL
                 prox.http_proxy = proxy
@@ -134,6 +141,9 @@ def set_selenium_local_session(proxy_address,
                                        desired_capabilities=capabilities,
                                        chrome_options=chrome_options)
 
+            # gets custom instance
+            browser = convert_selenium_browser(browser)
+
         except WebDriverException as exc:
             logger.exception(exc)
             err_msg = 'ensure chromedriver is installed at {}'.format(
@@ -171,17 +181,15 @@ def set_selenium_remote_session(use_firefox,
     :param selenium_driver: selenium WebDriver
     :return: self
     """
+
     if selenium_driver:
-        browser = selenium_driver
+        browser = convert_selenium_browser(selenium_driver)
     else:
-        if use_firefox:
-            browser = webdriver.Remote(
-                command_executor=selenium_url,
-                desired_capabilities=DesiredCapabilities.FIREFOX)
-        else:
-            browser = webdriver.Remote(
-                command_executor=selenium_url,
-                desired_capabilities=DesiredCapabilities.CHROME)
+        desired_caps = DesiredCapabilities.FIREFOX if use_firefox else DesiredCapabilities.CHROME
+        browser = get_remote_browser(
+            command_executor = selenium_url,
+            desired_capabilities=desired_caps
+        )
 
     message = "Session started!"
     highlight_print('browser', message, "initialization", "info", logger)
@@ -232,3 +240,123 @@ def close_browser(browser,
                     "Error occurred while "
                     "closing web browser!\n\t{}"
                     .format(str(exc).encode("utf-8")))
+
+
+
+
+def retry(max_retry_count = 3, start_page = None):
+    """Decorator which refreshes the page and tries to execute the function again.
+    Use it like that: @retry() => the '()' are important because its a decorator with params."""
+
+    def real_decorator(org_func):
+        def wrapper(*args, **kwargs):
+            browser = None
+
+            # try to find instance of a browser in the arguments
+            # all webdriver classes (chrome, firefox, ...) inherit from Remote class
+            for arg in args:
+                if not isinstance(arg, Remote): continue
+
+                browser = arg
+                break
+
+            else:
+                for _, value in kwargs.items():
+                    if not isinstance(value, Remote): continue
+
+                    browser = value
+                    break
+
+            if not browser:
+                print('not able to find browser in parameters!')
+                return org_func(*args, **kwargs)
+
+            if max_retry_count == 0:
+                print('max retry count is set to 0, this function is useless right now')
+                return org_func(*args, **kwargs)
+
+            # get current page if none is given
+            if not start_page:
+                start_page = browser.current_url
+
+            rv = None
+            retry_count = 0
+            while True:
+                try:
+                    rv = org_func(*args, **kwargs)
+                    break
+                except Exception as e:
+                    # TODO: maybe handle only certain exceptions here
+                    retry_count += 1
+
+                    # if above max retries => throw original exception
+                    if retry_count > max_retry_count:
+                        raise e
+
+                    rv = None
+
+                    # refresh page
+                    browser.get(start_page)
+
+            return rv;
+        return wrapper
+    return real_decorator
+
+
+
+class custom_browser(Remote):
+    '''Custom browser instance for manupulation later on'''
+
+
+    def find_element_by_xpath(self, *args, **kwargs):
+        '''example usage of hooking into built in functions'''
+        rv = super(custom_browser, self).find_element_by_xpath(*args, **kwargs)
+        return rv
+
+
+    def wait_for_valid_connection(self, username, logger):
+        counter = 0
+        while True and counter < 10:
+            sirens_wailing, emergency_state = emergency_exit(self, username, logger)
+            if sirens_wailing and emergency_state == 'not connected':
+                logger.warning('there is no valid connection')
+                counter += 1
+                sleep(60)
+            else:
+                break
+
+
+    def wait_for_valid_authorization(self, username, logger):
+        # save current page
+        current_url = get_current_url(self)
+
+        # stuck on invalid auth
+        auth_method = 'activity counts'
+        counter = 0
+        while True and counter < 10:
+            login_state = check_authorization(self, username, auth_method, logger)
+            if login_state is False:
+                logger.warning('not logged in')
+                counter += 1
+                sleep(60)
+            else:
+                break
+
+        # return to original page
+        web_address_navigator(self, current_url) 
+
+
+
+def get_remote_browser(command_executor, desired_capabilities):
+    browser = webdriver.Remote(
+        command_executor=command_executor,
+        desired_capabilities=desired_capabilities)
+
+    return convert_selenium_browser(browser)
+
+
+
+def convert_selenium_browser(driver):
+    """Changed the class to our custom class"""
+    driver.__class__ = custom_browser
+    return driver
diff --git a/instapy/like_util.py b/instapy/like_util.py
--- a/instapy/like_util.py
+++ b/instapy/like_util.py
@@ -3,11 +3,13 @@
 import re
 from re import findall
 
+from .browser import retry
 from .time_util import sleep
 from .util import format_number
 from .util import add_user_to_blacklist
 from .util import click_element
 from .util import is_private_profile
+from .util import is_page_available
 from .util import update_activity
 from .util import web_address_navigator
 from .util import get_number_of_posts
@@ -404,7 +406,7 @@ def get_links_for_username(browser,
     # then do not navigate to it again
     web_address_navigator(browser, user_link)
 
-    if "Page Not Found" in browser.title:
+    if not is_page_available(browser, logger):
         logger.error(
             'Instagram error: The link you followed may be broken, or the '
             'page may have been removed...')
@@ -414,12 +416,18 @@ def get_links_for_username(browser,
     following_status, follow_button = get_following_status(
         browser, "profile", username, person, None, logger, logfolder)
 
-    if following_status != "OWNER":
-        is_private = is_private_profile(browser, logger, following_status)
-        if (is_private is None
-            or (is_private and not following_status)
-                or following_status == "Blocked"):
-            return False
+    if following_status is None:
+        browser.wait_for_valid_connection(browser, username, logger)
+
+    if following_status == 'Follow':
+        browser.wait_for_valid_authorization(browser, username, logger, notify=False)
+
+    is_private = is_private_profile(browser, logger, following_status == 'Following')
+    if (is_private is None
+        or (is_private is True and following_status not in ['Following', True])
+        or (following_status == 'Blocked')):
+        logger.info('This user is private and we are not following')
+        return False
 
     # Get links
     links = []
@@ -732,7 +740,7 @@ def get_tags(browser, url):
 
     return tags
 
-
+@retry()
 def get_links(browser, page, logger, media, element):
     # Get image links in scope from hashtag, location and other pages
     link_elems = element.find_elements_by_tag_name('a')


Test patch: