diff --git a/venmo_api/apis/auth_api.py b/venmo_api/apis/auth_api.py index 22fa22b..48d4547 100644 --- a/venmo_api/apis/auth_api.py +++ b/venmo_api/apis/auth_api.py @@ -1,4 +1,5 @@ from venmo_api import random_device_id, warn, confirm, AuthenticationFailedError, ApiClient +from collections.abc import Callable, Awaitable class AuthenticationApi(object): @@ -39,6 +40,37 @@ def login_with_credentials_cli(self, username: str, password: str) -> str: f"device-id: {self.__device_id}") return access_token + + def login_with_credentials_func(self, username: str, password: str, otp_func: Callable[[], str]) -> str: + """ + Pass your username and password to get an access_token for using the API. + :param username: Phone, email or username + :param password: Your account password to login + :param otp_func: Function that returns the OTP provided to the user + :return: + """ + + # Give warnings to the user about device-id and token expiration + warn("IMPORTANT: Take a note of your device-id to avoid 2-factor-authentication for your next login.") + print(f"device-id: {self.__device_id}") + warn("IMPORTANT: Your Access Token will NEVER expire, unless you logout manually (client.log_out(token)).\n" + "Take a note of your token, so you don't have to login every time.\n") + + response = self.authenticate_using_username_password(username, password) + + # if two-factor error + if response.get('body').get('error'): + access_token = self.__two_factor_process_func(response=response, otp_func=otp_func) + + self.trust_this_device() + else: + access_token = response['body']['access_token'] + + confirm("Successfully logged in. Note your token and device-id") + print(f"access_token: {access_token}\n" + f"device-id: {self.__device_id}") + + return access_token @staticmethod def log_out(access_token: str) -> bool: @@ -77,6 +109,29 @@ def __two_factor_process_cli(self, response: dict) -> str: return access_token + def __two_factor_process_func(self, response: dict, otp_func: Callable[[], str] = None) -> str: + """ + Get response from authenticate_with_username_password for a CLI two-factor process + :param response: + :param otp_func: + :return: access_token + """ + + otp_secret = response['headers'].get('venmo-otp-secret') + if not otp_secret: + raise AuthenticationFailedError("Failed to get the otp-secret for the 2-factor authentication process. " + "(check your password)") + + self.send_text_otp(otp_secret=otp_secret) + user_otp = otp_func() + while not self.__check_otp_validity(user_otp): + user_otp = otp_func() + + access_token = self.authenticate_using_otp(user_otp, otp_secret) + self.__api_client.update_access_token(access_token=access_token) + + return access_token + def authenticate_using_username_password(self, username: str, password: str) -> dict: """ Authenticate with username and password. Raises exception if either be incorrect. @@ -178,3 +233,11 @@ def __ask_user_for_otp_password(): otp = input("Enter OTP that you received on your phone from Venmo: (It must be 6 digits)\n") return otp + + @staticmethod + def __check_otp_validity(otp: str): + + if len(otp) >= 6 and otp.isdigit(): + return True + else: + return False \ No newline at end of file diff --git a/venmo_api/venmo.py b/venmo_api/venmo.py index a8ea1fb..dac6e45 100644 --- a/venmo_api/venmo.py +++ b/venmo_api/venmo.py @@ -1,4 +1,5 @@ from venmo_api import ApiClient, UserApi, PaymentApi, AuthenticationApi, validate_access_token +from collections.abc import Callable, Awaitable class Client(object): @@ -27,17 +28,23 @@ def my_profile(self, force_update=False): return self.__profile @staticmethod - def get_access_token(username: str, password: str, device_id: str = None) -> str: + def get_access_token(username: str, password: str, device_id: str = None, otp_func: Callable[[], str] = None) -> str: """ Log in using your credentials and get an access_token to use in the API :param username: Can be username, phone number (without +1) or email address. :param password: Account's password :param device_id: [optional] A valid device-id. + :param otp_func: [optional] Function to be called when API asks for OTP. :return: access_token """ authn_api = AuthenticationApi(api_client=ApiClient(), device_id=device_id) - return authn_api.login_with_credentials_cli(username=username, password=password) + if otp_func == None: + return authn_api.login_with_credentials_cli(username=username, password=password) + + else: + return authn_api.login_with_credentials_func(username=username, password=password, otp_func=otp_func) + @staticmethod def log_out(access_token) -> bool: