From a8539b871e6ff3963366902f7a1d8f0766d48be4 Mon Sep 17 00:00:00 2001 From: Sylvie Ye Date: Tue, 12 Mar 2019 15:58:27 +0100 Subject: [PATCH] [ing] add feature: transfer --- modules/ing/api/__init__.py | 4 +-- modules/ing/api/login.py | 29 +++++++++++---- modules/ing/api/transfer_page.py | 60 ++++++++++++++++++++++++++++++-- modules/ing/api_browser.py | 52 +++++++++++++++++++++++++-- modules/ing/module.py | 18 +++++++--- 5 files changed, 144 insertions(+), 19 deletions(-) diff --git a/modules/ing/api/__init__.py b/modules/ing/api/__init__.py index ff74abf0be..6673d4880c 100644 --- a/modules/ing/api/__init__.py +++ b/modules/ing/api/__init__.py @@ -19,11 +19,11 @@ from .login import LoginPage from .accounts_page import AccountsPage, HistoryPage, ComingPage -from .transfer_page import DebitAccountsPage, CreditAccountsPage +from .transfer_page import DebitAccountsPage, CreditAccountsPage, TransferPage from .profile_page import ProfilePage __all__ = ['LoginPage', 'AccountsPage', 'HistoryPage', 'ComingPage', - 'DebitAccountsPage', 'CreditAccountsPage', + 'DebitAccountsPage', 'CreditAccountsPage', 'TransferPage', 'ProfilePage'] diff --git a/modules/ing/api/login.py b/modules/ing/api/login.py index ac99f42a91..100a79328d 100644 --- a/modules/ing/api/login.py +++ b/modules/ing/api/login.py @@ -27,10 +27,21 @@ class INGVirtKeyboard(SimpleVirtualKeyboard): + # from parent tile_margin = 3 margin = (0, 4, 0, 0) convert = 'RGB' + # for children + safe_tile_margin = 10 + small_img_size = (100, 40) + alter_img_params = { + 'radius': 2, + 'percent': 135, + 'threshold': 3, + 'limit_pixel': 160 + } + symbols = { '0': ('117b18365105224c7207d3ec0ce7516f',), '1': ('112a72c31ebdf0cdafb84e67c6e1f8f2',), @@ -50,10 +61,14 @@ def alter_image(self): self.original_image = self.image # create miniature of image to get more reliable hash - self.image = self.image.resize((100, 40), resample=Image.BILINEAR) + self.image = self.image.resize(self.small_img_size, resample=Image.BILINEAR) # See ImageFilter.UnsharpMask from Pillow - self.image = self.image.filter(ImageFilter.UnsharpMask(radius=2, percent=135, threshold=3)) - self.image = Image.eval(self.image, lambda px: 0 if px <= 160 else 255) + self.image = self.image.filter(ImageFilter.UnsharpMask( + radius=self.alter_img_params['radius'], + percent=self.alter_img_params['percent'], + threshold=self.alter_img_params['threshold']) + ) + self.image = Image.eval(self.image, lambda px: 0 if px <= self.alter_img_params['limit_pixel'] else 255) def password_tiles_coord(self, password): # get image original size to get password coord @@ -73,7 +88,7 @@ def password_tiles_coord(self, password): % (digit, self.path)) formatted_password = [] - safe_margin = 10 + safe_margin = self.safe_tile_margin for tile in password_tiles: # default matching_symbol is str(range(cols*rows)) x0 = (int(tile.matching_symbol) % self.cols) * tile_width @@ -101,7 +116,7 @@ def get_password_coord(self, img, password): pin_position = Dict('pinPositions')(self.doc) image = BytesIO(img) - vk = INGVirtKeyboard(image, 5, 2, browser=self.browser) - password_radom_coords = vk.password_tiles_coord(password) + vk = INGVirtKeyboard(image, cols=5, rows=2, browser=self.browser) + password_random_coords = vk.password_tiles_coord(password) # pin positions (website side) start at 1, our positions start at 0 - return [password_radom_coords[index-1] for index in pin_position] + return [password_random_coords[index-1] for index in pin_position] diff --git a/modules/ing/api/transfer_page.py b/modules/ing/api/transfer_page.py index 47ee658daf..c9349bca10 100644 --- a/modules/ing/api/transfer_page.py +++ b/modules/ing/api/transfer_page.py @@ -20,15 +20,46 @@ from __future__ import unicode_literals from datetime import datetime +from io import BytesIO from weboob.browser.pages import LoggedPage, JsonPage from weboob.browser.elements import method, DictElement, ItemElement from weboob.browser.filters.json import Dict -from weboob.browser.filters.standard import ( - Env, Field, -) +from weboob.browser.filters.standard import Env, Field, Date from weboob.capabilities.bank import Recipient +from .login import INGVirtKeyboard + + +class TransferINGVirtKeyboard(INGVirtKeyboard): + # from grand parent + tile_margin = 5 + margin = None + convert = 'RGB' + + # from parent + safe_tile_margin = 50 + small_img_size = (125, 50) # original image size is (2420, 950) + alter_img_params = { + 'radius': 2, + 'percent': 150, + 'threshold': 3, + 'limit_pixel': 125 + } + + symbols = { + '0': 'e3e62175aa1a5ef8dc67639194caa880', + '1': '80245727e4e5f123fd64bbb1fa80dde0', + '2': '62cfc40429652190c996db741ac90830', + '3': 'bb2f87d32f688679745fe95ac31b80fd', + '4': 'a4b5e16c64817deb12ca6311cb98e59a', + '5': '56a8f3b4f068f9e2f93c4daa3a53dc17', + '6': 'b50f7e4a375153b9f6b029dc9b0a7e64', + '7': 'd52320c62c6157d0cadbb7a186153628', + '8': 'dd3fb25fc7f0765610b0ffe47da85330', + '9': 'ca55399a5b36da3fedcd1dbb73d72a2f' + } + class DebitAccountsPage(LoggedPage, JsonPage): def get_debit_accounts_uid(self): @@ -59,3 +90,26 @@ def obj_category(self): if Field('_is_internal_recipient')(self): return 'Interne' return 'Externe' + + +class TransferPage(LoggedPage, JsonPage): + @property + def suggested_date(self): + return Date(Dict('pinValidateResponse/executionSuggestedDate'), dayfirst=True)(self.doc) + + def get_password_coord(self, password): + assert Dict('pinValidateResponse', default=None)(self.doc), "Transfer virtualkeyboard position has failed" + + pin_position = Dict('pinValidateResponse/pinPositions')(self.doc) + + image_url = '/secure/api-v1%s' % Dict('pinValidateResponse/keyPadUrl')(self.doc) + image = BytesIO(self.browser.open(image_url, headers={'Referer': self.browser.absurl('/secure/transfers/new')}).content) + + vk = TransferINGVirtKeyboard(image, cols=5, rows=2, browser=self.browser) + password_random_coords = vk.password_tiles_coord(password) + # pin positions (website side) start at 1, our positions start at 0 + return [password_random_coords[index-1] for index in pin_position] + + @property + def transfer_is_validated(self): + return Dict('acknowledged')(self.doc) diff --git a/modules/ing/api_browser.py b/modules/ing/api_browser.py index b4ed362dca..2954794091 100644 --- a/modules/ing/api_browser.py +++ b/modules/ing/api_browser.py @@ -26,10 +26,11 @@ from weboob.browser import LoginBrowser, URL from weboob.exceptions import BrowserIncorrectPassword, BrowserUnavailable, ActionNeeded from weboob.browser.exceptions import ClientError +from weboob.capabilities.bank import TransferBankError, TransferInvalidAmount from .api import ( LoginPage, AccountsPage, HistoryPage, ComingPage, - DebitAccountsPage, CreditAccountsPage, + DebitAccountsPage, CreditAccountsPage, TransferPage, ProfilePage, ) from .web import StopPage, ActionNeededPage @@ -104,6 +105,8 @@ class IngAPIBrowser(LoginBrowser): # transfer credit_accounts = URL(r'/secure/api-v1/transfers/debitAccounts/(?P.*)/creditAccounts', CreditAccountsPage) debit_accounts = URL(r'/secure/api-v1/transfers/debitAccounts', DebitAccountsPage) + init_transfer_page = URL(r'/secure/api-v1/transfers/v2/new/validate', TransferPage) + exec_transfer_page = URL(r'/secure/api-v1/transfers/v2/new/execute/pin', TransferPage) # profile informations = URL(r'/secure/api-v1/customer/info', ProfilePage) @@ -113,6 +116,7 @@ def __init__(self, *args, **kwargs): super(IngAPIBrowser, self).__init__(*args, **kwargs) self.old_browser = IngBrowser(*args, **kwargs) + self.transfer_data = None def handle_login_error(self, r): error_page = r.response.json() @@ -314,13 +318,55 @@ def iter_recipients(self, account): for recipient in self.page.iter_recipients(acc_uid=account._uid): yield recipient + def handle_transfer_errors(self, r): + error_page = r.response.json() + assert 'error' in error_page, "Something went wrong, transfer is not created" + + error = error_page['error'] + error_msg = error['message'] + + if error['code'] == 'TRANSFER.INVALID_AMOUNT_MINIMUM': + raise TransferInvalidAmount(message=error_msg) + elif error['code'] == 'INPUT_INVALID' and len(error['values']): + for value in error['values']: + error_msg = '%s %s %s.' % (error_msg, value, error['values'][value]) + + raise TransferBankError(message=error_msg) + + @need_to_be_on_website('api') @need_login def init_transfer(self, account, recipient, transfer): - raise NotImplementedError() + data = { + 'amount': transfer.amount, + 'executionDate': transfer.exec_date.strftime('%Y-%m-%d'), + 'keyPadSize': {'width': 3800, 'height': 1520}, + 'label': transfer.label, + 'fromAccount': account._uid, + 'toAccount': recipient.id + } + try: + self.init_transfer_page.go(json=data, headers={'Referer': self.absurl('/secure/transfers/new')}) + except ClientError as e: + self.handle_transfer_errors(e) + + assert self.page.suggested_date == transfer.exec_date, "Transfer date is not valid" + self.transfer_data = data + self.transfer_data.pop('keyPadSize') + self.transfer_data['clickPositions'] = self.page.get_password_coord(self.password) + return transfer + + @need_to_be_on_website('api') @need_login def execute_transfer(self, transfer): - raise NotImplementedError() + headers = { + 'Referer': self.absurl('/secure/transfers/new'), + 'Accept': 'application/json, text/plain, */*' + } + self.exec_transfer_page.go(json=self.transfer_data, headers=headers) + + assert self.page.transfer_is_validated, "Transfer is not validated" + return transfer ############# CapDocument ############# @need_login diff --git a/modules/ing/module.py b/modules/ing/module.py index 7595d0075c..38a21e153c 100644 --- a/modules/ing/module.py +++ b/modules/ing/module.py @@ -19,13 +19,15 @@ from __future__ import unicode_literals -from weboob.capabilities.bank import CapBankWealth, CapBankTransfer, Account, AccountNotFound +from decimal import Decimal + +from weboob.capabilities.bank import CapBankWealth, CapBankTransfer, Account, AccountNotFound, RecipientNotFound from weboob.capabilities.bill import ( CapDocument, Bill, Subscription, SubscriptionNotFound, DocumentNotFound, DocumentTypes, ) from weboob.capabilities.profile import CapProfile -from weboob.capabilities.base import find_object +from weboob.capabilities.base import find_object, strict_find_object from weboob.tools.backend import Module, BackendConfig from weboob.tools.value import ValueBackendPassword, ValueDate @@ -99,10 +101,18 @@ def iter_transfer_recipients(self, account): return self.browser.iter_recipients(account) def init_transfer(self, transfer, **params): - raise NotImplementedError() + self.logger.info('Going to do a new transfer') + + account = strict_find_object(self.iter_accounts(), id=transfer.account_id, error=AccountNotFound) + + recipient = strict_find_object(self.iter_transfer_recipients(account), id=transfer.recipient_id, error=RecipientNotFound) + + transfer.amount = Decimal(transfer.amount).quantize(Decimal('.01')) + + return self.browser.init_transfer(account, recipient, transfer) def execute_transfer(self, transfer, **params): - raise NotImplementedError() + return self.browser.execute_transfer(transfer) ############# CapDocument ############# def iter_subscription(self): -- GitLab