From e979e323a35cbc2bb2195334ef1619df95608c59 Mon Sep 17 00:00:00 2001 From: mHaisham <47662901+mHaisham@users.noreply.github.com> Date: Thu, 25 Feb 2021 15:38:29 +0500 Subject: [PATCH] Added Guard page handler --- README.md | 4 ++ webnovel/decorators/__init__.py | 1 + webnovel/decorators/chainable.py | 25 ++++++++++++ webnovel/exceptions.py | 4 ++ webnovel/handlers/README.md | 68 ++++++++++++++++++++++++++++++++ webnovel/handlers/__init__.py | 1 + webnovel/handlers/guard.py | 61 ++++++++++++++++++++++++++++ webnovel/handlers/handler.py | 32 +++++++++++++++ 8 files changed, 196 insertions(+) create mode 100644 webnovel/decorators/chainable.py create mode 100644 webnovel/handlers/README.md create mode 100644 webnovel/handlers/__init__.py create mode 100644 webnovel/handlers/guard.py create mode 100644 webnovel/handlers/handler.py diff --git a/README.md b/README.md index 90bbc8e..ff2c675 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,10 @@ except GuardException: pass ``` +[Read more] on handling Guard + +[Read more]: https://github.com/mHaisham/webnovelbot/tree/master/webnovel/handlers + ### `manual=True` When manual is true the process would be expecting user input during the above mentioned situations. diff --git a/webnovel/decorators/__init__.py b/webnovel/decorators/__init__.py index 2b8c5f8..60fd948 100644 --- a/webnovel/decorators/__init__.py +++ b/webnovel/decorators/__init__.py @@ -1,3 +1,4 @@ +from .chainable import chainable from .deprecated import deprecated from .redirect import redirect from .signin import require_signin diff --git a/webnovel/decorators/chainable.py b/webnovel/decorators/chainable.py new file mode 100644 index 0000000..c6176b9 --- /dev/null +++ b/webnovel/decorators/chainable.py @@ -0,0 +1,25 @@ +from functools import wraps + + +def chainable(func): + """ + enables chaining of methods + + this makes the return of the function to be the class or `self` + + example usage: + + @chainable + def a_class_method(self): + ... + + """ + + @wraps(func) + def wrapper(*args, **kwargs): + func(*args, **kwargs) + + # when used with a method, the first arg is always the class + return args[0] + + return wrapper diff --git a/webnovel/exceptions.py b/webnovel/exceptions.py index 5be009e..4ac2c0a 100644 --- a/webnovel/exceptions.py +++ b/webnovel/exceptions.py @@ -22,3 +22,7 @@ class GuardException(Exception): class CaptchaException(Exception): pass + + +class ValidationError(Exception): + pass diff --git a/webnovel/handlers/README.md b/webnovel/handlers/README.md new file mode 100644 index 0000000..26d1049 --- /dev/null +++ b/webnovel/handlers/README.md @@ -0,0 +1,68 @@ +# Handlers + +They provide a convenient way to interact with some of the more obscure pages + +## GuardHandler + +As is named this handler handles most/all the interactions that are possible with the guard page + +```python +from webnovel.handlers import GuardHandler + +try: + webnovel.signin(USER_EMAIL, USER_PASS) +except CaptchaException: + pass +except GuardException: + handler = GuardHandler(webnovel) + success = handler.input('code').confirm().wait_until_confirmed() +``` + +Note that `input` and `confirm` are chainable while `wait_until_confirmed` returns a boolean. + +### `wait_until_confirmed()` + +checks for redirect and error and informs which is triggered first. + +- `True` when redirected +- `False` when error is encountered + +**Below is a full list of available methods** + +- `@chainable input(code)` it takes the authentication code and writes it in authentication field + +- `@chainable confirm()` press the confirmation button + +- `@chainable resend()` press the resend button + +- `back()` press the back button + +- `wait_until_confirmed()` described [above](#wait_until_confirmed) + +### ActionChains + +you may also create you own chain of events using action chains and the elements exposed + +- `input_element` +- `confirm_buttom` +- `resend_button` +- `back_button` + +The example below has the same functionality as that above, but it uses `ActionChains` + +```python +from selenium.webdriver.common.action_chains import ActionChains + +from webnovel.handlers import GuardHandler + + ... +except GuardException: + handler = GuardHandler(webnovel) + + ActionChains(handler.driver)\ + .send_keys_to_element(handler.input_element, 'code')\ + .click(handler.confirm_buttom)\ + .perform() + + success = handler.wait_until_confirmed() +``` \ No newline at end of file diff --git a/webnovel/handlers/__init__.py b/webnovel/handlers/__init__.py new file mode 100644 index 0000000..5ad5633 --- /dev/null +++ b/webnovel/handlers/__init__.py @@ -0,0 +1 @@ +from .guard import GuardHandler diff --git a/webnovel/handlers/guard.py b/webnovel/handlers/guard.py new file mode 100644 index 0000000..f0c83d4 --- /dev/null +++ b/webnovel/handlers/guard.py @@ -0,0 +1,61 @@ +from selenium.common.exceptions import NoSuchElementException + +from .handler import IHandler +from ..bot import GUARD_URL +from ..decorators import chainable + + +class GuardHandler(IHandler): + def __init__(self, bot): + """ + :param bot: webnovel bot + :raises ValueError: if current url is not a guard url + """ + super(GuardHandler, self).__init__(bot) + + if not self.driver.current_url.startswith(GUARD_URL): + raise ValueError('current page is not a guard') + + # make sure its fully loaded + self.wait_for('.codeInfo > input') + + self.input_element = self.get('.codeInfo > input') + self.confirm_buttom = self.get('#checkTrust') + self.resend_button = self.get('#resTrustEmail') + self.back_button = self.get('.m-main-hd a') + + @chainable + def input(self, code: str): + """ + inputs code into authentication input field + + :param code: authentication code + """ + self.input_element.send_keys(code) + + @chainable + def confirm(self): + """ press confirm button """ + self.confirm_buttom.click() + + @chainable + def resend(self): + """ press resend email button """ + self.resend_button.click() + + def back(self): + """ press back button """ + self.back_button.click() + + def wait_until_confirmed(self): + self.wait.until(lambda driver: ( + not self.driver.current_url.startswith(GUARD_URL) + or self.driver.find_elements_by_css_selector('.error_tip._on') + )) + + try: + self.get('.error_tip._on') + except NoSuchElementException: + return True + else: + return False diff --git a/webnovel/handlers/handler.py b/webnovel/handlers/handler.py new file mode 100644 index 0000000..eebddf6 --- /dev/null +++ b/webnovel/handlers/handler.py @@ -0,0 +1,32 @@ +from selenium.webdriver.common.by import By +from selenium.webdriver.remote.webdriver import WebDriver +from selenium.webdriver.support import expected_conditions as EC +from selenium.webdriver.support.ui import WebDriverWait + +from ..bot import WebnovelBot + + +class IHandler: + bot: WebnovelBot + driver: WebDriver + + def __init__(self, bot): + self.bot = bot + self.driver = bot.driver + + self.wait = WebDriverWait(self.driver, self.bot.timeout) + + def get(self, selector): + return self.driver.find_element_by_css_selector(selector) + + def wait_for(self, selector): + self.wait.until( + EC.presence_of_element_located((By.CSS_SELECTOR, selector)) + ) + + def wait_and_get(self, selector): + self.wait.until( + EC.presence_of_element_located((By.CSS_SELECTOR, selector)) + ) + + return self.driver.find_element_by_css_selector(selector)