add support for new bnp site
This commit is contained in:
parent
511daa73a1
commit
322cb35e3d
3 changed files with 348 additions and 2 deletions
128
modules/bnporc/browser.py
Normal file
128
modules/bnporc/browser.py
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright(C) 2009-2013 Romain Bignon
|
||||
#
|
||||
# This file is part of weboob.
|
||||
#
|
||||
# weboob is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# weboob is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with weboob. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
import time
|
||||
|
||||
from weboob.browser import LoginBrowser, URL, need_login
|
||||
from weboob.capabilities.base import find_object
|
||||
from weboob.capabilities.bank import AccountNotFound
|
||||
from weboob.tools.json import json
|
||||
|
||||
from .pages import LoginPage, AccountsPage, AccountsIBANPage, HistoryPage
|
||||
|
||||
|
||||
__all__ = ['BNPParibasBrowser']
|
||||
|
||||
|
||||
class CompatMixin(object):
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, type, value, tb):
|
||||
pass
|
||||
|
||||
|
||||
def JSON(data):
|
||||
return ('json', data)
|
||||
|
||||
|
||||
def isJSON(obj):
|
||||
return type(obj) is tuple and obj and obj[0] == 'json'
|
||||
|
||||
|
||||
class JsonBrowserMixin(object):
|
||||
def open(self, *args, **kwargs):
|
||||
if isJSON(kwargs.get('data')):
|
||||
kwargs['data'] = json.dumps(kwargs['data'][1])
|
||||
if not 'headers' in kwargs:
|
||||
kwargs['headers'] = {}
|
||||
kwargs['headers']['Content-Type'] = 'application/json'
|
||||
|
||||
return super(JsonBrowserMixin, self).open(*args, **kwargs)
|
||||
|
||||
|
||||
class BNPParibasBrowser(CompatMixin, JsonBrowserMixin, LoginBrowser):
|
||||
BASEURL_TEMPLATE = r'https://%s.bnpparibas.net/'
|
||||
BASEURL = BASEURL_TEMPLATE % 'mabanque'
|
||||
TIMEOUT = 30.0
|
||||
|
||||
login = URL(r'identification-wspl-pres/identification\?acceptRedirection=true×tamp=(?P<timestamp>)',
|
||||
'SEEA-pa01/devServer/seeaserver',
|
||||
'https://mabanqueprivee.bnpparibas.net/fr/espace-prive/comptes-et-contrats\?u=%2FSEEA-pa01%2FdevServer%2Fseeaserver',
|
||||
LoginPage)
|
||||
accounts = URL('udc-wspl/rest/getlstcpt', AccountsPage)
|
||||
ibans = URL('rib-wspl/rpc/comptes', AccountsIBANPage)
|
||||
history = URL('rop-wspl/rest/releveOp', HistoryPage)
|
||||
|
||||
def switch(self, subdomain):
|
||||
self.BASEURL = self.BASEURL_TEMPLATE % subdomain
|
||||
|
||||
def do_login(self):
|
||||
self.switch('mabanque')
|
||||
timestamp = lambda: int(time.time() * 1e3)
|
||||
self.login.go(timestamp=timestamp())
|
||||
if self.login.is_here():
|
||||
self.page.login(self.username, self.password)
|
||||
|
||||
@need_login
|
||||
def get_accounts_list(self):
|
||||
ibans = self.ibans.go()
|
||||
self.accounts.go()
|
||||
assert self.accounts.is_here()
|
||||
return self.page.iter_accounts(ibans.get_ibans_dict())
|
||||
|
||||
@need_login
|
||||
def get_account(self, _id):
|
||||
return find_object(self.get_accounts_list(), id=_id, error=AccountNotFound)
|
||||
|
||||
@need_login
|
||||
def iter_history(self, account, coming=False):
|
||||
self.page = self.history.go(data=JSON({
|
||||
"ibanCrypte": account.id,
|
||||
"pastOrPending": 1,
|
||||
"triAV": 0,
|
||||
"startDate": None,
|
||||
"endDate": None
|
||||
}))
|
||||
return self.page.iter_coming() if coming else self.page.iter_history()
|
||||
|
||||
@need_login
|
||||
def iter_coming_operations(self, account):
|
||||
return self.iter_history(account, coming=True)
|
||||
|
||||
@need_login
|
||||
def iter_investment(self, account):
|
||||
raise NotImplementedError()
|
||||
|
||||
@need_login
|
||||
def get_transfer_accounts(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
@need_login
|
||||
def transfer(self, account, to, amount, reason):
|
||||
raise NotImplementedError()
|
||||
|
||||
@need_login
|
||||
def iter_threads(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
@need_login
|
||||
def get_thread(self, thread):
|
||||
raise NotImplementedError()
|
||||
|
|
@ -28,6 +28,7 @@ from weboob.tools.value import ValueBackendPassword, Value
|
|||
|
||||
from .deprecated.browser import BNPorc
|
||||
from .enterprise.browser import BNPEnterprise
|
||||
from .browser import BNPParibasBrowser
|
||||
|
||||
|
||||
__all__ = ['BNPorcModule']
|
||||
|
|
@ -47,7 +48,9 @@ class BNPorcModule(Module, CapBank, CapMessages):
|
|||
# label='Password to set when the allowed uses are exhausted (6 digits)',
|
||||
# regexp='^(\d{6}|)$'),
|
||||
Value('website', label='Type de compte', default='pp',
|
||||
choices={'pp': 'Particuliers/Professionnels', 'ent': 'Entreprises'}))
|
||||
choices={'pp': 'Particuliers/Professionnels',
|
||||
'ent': 'Entreprises',
|
||||
'pp2': 'Particuliers/Professionnels (nouveau site)'}))
|
||||
STORAGE = {'seen': []}
|
||||
|
||||
# Store the messages *list* for this duration
|
||||
|
|
@ -59,7 +62,7 @@ class BNPorcModule(Module, CapBank, CapMessages):
|
|||
self._threads_age = datetime.utcnow()
|
||||
|
||||
def create_default_browser(self):
|
||||
b = {'pp': BNPorc, 'ent': BNPEnterprise}
|
||||
b = {'pp': BNPorc, 'ent': BNPEnterprise, 'pp2': BNPParibasBrowser}
|
||||
self.BROWSER = b[self.config['website'].get()]
|
||||
#if self.config['rotating_password'].get().isdigit() and len(self.config['rotating_password'].get()) == 6:
|
||||
# rotating_password = self.config['rotating_password'].get()
|
||||
|
|
|
|||
215
modules/bnporc/pages.py
Normal file
215
modules/bnporc/pages.py
Normal file
|
|
@ -0,0 +1,215 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright(C) 2009-2013 Romain Bignon
|
||||
#
|
||||
# This file is part of weboob.
|
||||
#
|
||||
# weboob is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# weboob is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with weboob. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
from cStringIO import StringIO
|
||||
from random import randint
|
||||
from decimal import Decimal
|
||||
|
||||
from weboob.browser.pages import JsonPage, LoggedPage
|
||||
from weboob.tools.captcha.virtkeyboard import GridVirtKeyboard
|
||||
from weboob.capabilities.bank import Account
|
||||
from weboob.tools.capabilities.bank.transactions import FrenchTransaction as Transaction
|
||||
from weboob.exceptions import BrowserIncorrectPassword
|
||||
from weboob.tools.json import json
|
||||
from weboob.tools.date import parse_french_date as Date
|
||||
|
||||
|
||||
__all__ = ['LoginPage']
|
||||
|
||||
|
||||
def cast(x, typ, default=None):
|
||||
try:
|
||||
return typ(x or default)
|
||||
except ValueError:
|
||||
return default
|
||||
|
||||
|
||||
class BNPKeyboard(GridVirtKeyboard):
|
||||
color = (0x1f, 0x27, 0x28)
|
||||
margin = 3, 3
|
||||
symbols = {'0': '43b2227b92e0546d742a1f087015e487',
|
||||
'1': '2914e8cc694de26756096d0d0d4c6e0f',
|
||||
'2': 'aac54304a7bb850805d29f54557be366',
|
||||
'3': '0376d9f8419efee42e253d195a152547',
|
||||
'4': '3719595f15b1ac1c5a73d84aa290b5f6',
|
||||
'5': '617597f07a6530479927536671485439',
|
||||
'6': '4f5dce7bd0d9213fdae54b79bb8dd33a',
|
||||
'7': '49e07fa52b9bcee798f3a663f86e6cc1',
|
||||
'8': 'c60b723b3d95a46416b34c2cbefba3ed',
|
||||
'9': 'a13b8c3617a7bf854590833ddfb97f1f'}
|
||||
|
||||
def __init__(self, page, image):
|
||||
symbols = list('%02d' % x for x in range(1, 11))
|
||||
|
||||
super(BNPKeyboard, self).__init__(symbols, 5, 2, StringIO(image.content), self.color, convert='RGB')
|
||||
self.check_symbols(self.symbols, page.browser.responses_dirname)
|
||||
|
||||
|
||||
class LoginPage(JsonPage):
|
||||
@staticmethod
|
||||
def render_template(tmpl, **values):
|
||||
for k, v in values.iteritems():
|
||||
tmpl = tmpl.replace('{{ ' + k + ' }}', v)
|
||||
return tmpl
|
||||
|
||||
@staticmethod
|
||||
def generate_token(length=11):
|
||||
chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXTZabcdefghiklmnopqrstuvwxyz'
|
||||
return ''.join((chars[randint(0, len(chars)-1)] for _ in xrange(length)))
|
||||
|
||||
def build_doc(self, text):
|
||||
try:
|
||||
return super(LoginPage, self).build_doc(text)
|
||||
except json.JSONDecodeError:
|
||||
# XXX When login is successful, server sends HTML instead of JSON,
|
||||
# we can ignore it.
|
||||
return {}
|
||||
|
||||
def on_load(self):
|
||||
if self.url.startswith('https://mabanqueprivee.'):
|
||||
self.browser.switch('mabanqueprivee')
|
||||
|
||||
error = cast(self.get('errorCode'), int, 0)
|
||||
|
||||
if error:
|
||||
msg = self.get('message')
|
||||
if error == 201:
|
||||
raise BrowserIncorrectPassword(msg)
|
||||
self.logger.debug('Unexpected error at login: "%s" (code=%s)' % (msg, error))
|
||||
|
||||
def login(self, username, password):
|
||||
url = '/identification-wspl-pres/grille/%s' % self.get('data.grille.idGrille')
|
||||
keyboard = self.browser.open(url)
|
||||
vk = BNPKeyboard(self, keyboard)
|
||||
|
||||
target = self.browser.BASEURL + 'SEEA-pa01/devServer/seeaserver'
|
||||
user_agent = self.browser.session.headers.get('User-Agent') or ''
|
||||
auth = self.render_template(self.get('data.authTemplate'),
|
||||
idTelematique=username,
|
||||
password=vk.get_string_code(password),
|
||||
clientele=user_agent)
|
||||
# XXX useless ?
|
||||
csrf = self.generate_token()
|
||||
|
||||
self.browser.location(target, data={'AUTH': auth, 'CSRF': csrf})
|
||||
|
||||
|
||||
class BNPPage(LoggedPage, JsonPage):
|
||||
def build_doc(self, text):
|
||||
return json.loads(text, parse_float=Decimal)
|
||||
|
||||
def on_load(self):
|
||||
code = cast(self.get('codeRetour'), int, 0)
|
||||
|
||||
if code == -30:
|
||||
self.logger.debug('End of session detected, try to relog...')
|
||||
self.browser.do_login()
|
||||
elif code:
|
||||
self.logger.debug('Unexpected error: "%s" (code=%s)' % (self.get('message'), code))
|
||||
|
||||
|
||||
class AccountsPage(BNPPage):
|
||||
FAMILY_TO_TYPE = {
|
||||
1: Account.TYPE_CHECKING,
|
||||
2: Account.TYPE_SAVINGS,
|
||||
3: Account.TYPE_DEPOSIT,
|
||||
4: Account.TYPE_MARKET,
|
||||
5: Account.TYPE_LIFE_INSURANCE,
|
||||
}
|
||||
|
||||
def iter_accounts(self, ibans):
|
||||
for f in self.path('data.infoUdc.familleCompte.*'):
|
||||
for a in f.get('compte'):
|
||||
yield Account.from_dict({
|
||||
'id': a.get('key'),
|
||||
'label': a.get('libellePersoProduit') or a.get('libelleProduit'),
|
||||
'currency': a.get('devise'),
|
||||
'type': self.FAMILY_TO_TYPE.get(f.get('idFamilleCompte')) or Account.TYPE_UNKNOWN,
|
||||
'balance': a.get('soldeDispo'),
|
||||
'coming': a.get('soldeDispo') + a.get('soldeAVenir'),
|
||||
'iban': ibans.get(a.get('key')) or a.get('value')
|
||||
})
|
||||
|
||||
|
||||
class AccountsIBANPage(BNPPage):
|
||||
def get_ibans_dict(self):
|
||||
return dict((a.get('ibanCrypte'), a.get('iban')) for a in self.path('listeRib.*.infoCompte'))
|
||||
|
||||
|
||||
class HistoryPage(BNPPage):
|
||||
CODE_TO_TYPE = {
|
||||
1: Transaction.TYPE_CHECK, # Chèque émis
|
||||
2: Transaction.TYPE_CHECK, # Chèque reçu
|
||||
3: Transaction.TYPE_CASH_DEPOSIT, # Versement espèces
|
||||
4: Transaction.TYPE_ORDER, # Virements reçus
|
||||
5: Transaction.TYPE_ORDER, # Virements émis
|
||||
6: Transaction.TYPE_LOAN_PAYMENT, # Prélèvements / amortissements prêts
|
||||
7: Transaction.TYPE_CARD, # Paiements carte,
|
||||
8: Transaction.TYPE_CARD, # Carte / Formule BNP Net,
|
||||
9: Transaction.TYPE_UNKNOWN, # Opérations Titres
|
||||
10: Transaction.TYPE_UNKNOWN, # Effets de Commerce
|
||||
11: Transaction.TYPE_WITHDRAWAL, # Retraits d'espèces carte
|
||||
12: Transaction.TYPE_UNKNOWN, # Opérations avec l'étranger
|
||||
13: Transaction.TYPE_CARD, # Remises Carte
|
||||
14: Transaction.TYPE_WITHDRAWAL, # Retraits guichets
|
||||
15: Transaction.TYPE_BANK, # Intérêts/frais et commissions
|
||||
16: Transaction.TYPE_UNKNOWN, # Tercéo
|
||||
30: Transaction.TYPE_UNKNOWN, # Divers
|
||||
}
|
||||
|
||||
COMING_TYPE_TO_TYPE = {
|
||||
2: Transaction.TYPE_ORDER, # Prélèvement
|
||||
3: Transaction.TYPE_CHECK, # Chèque
|
||||
4: Transaction.TYPE_CARD, # Opération carte
|
||||
}
|
||||
|
||||
def one(self, path, context=None):
|
||||
try:
|
||||
return list(self.path(path, context))[0]
|
||||
except IndexError:
|
||||
return None
|
||||
|
||||
def iter_history(self):
|
||||
for op in self.get('data.listerOperations.compte.operationPassee') or []:
|
||||
codeFamille = cast(self.one('operationType.codeFamille', op), int)
|
||||
yield Transaction.from_dict({
|
||||
'id': op.get('idOperation'),
|
||||
'date': Date(op.get('dateOperation')),
|
||||
'rdate': Date(self.one('montant.executionDate', op)),
|
||||
'vdate': Date(self.one('montant.valueDate', op)),
|
||||
'type': self.CODE_TO_TYPE.get(codeFamille) or Transaction.TYPE_UNKNOWN,
|
||||
'raw': op.get('libelleOperation'),
|
||||
'category': op.get('categorie'),
|
||||
'amount': self.one('montant.montant', op),
|
||||
})
|
||||
|
||||
def iter_coming(self):
|
||||
for op in self.path('data.listerOperations.compte.operationAvenir.*.operation.*'):
|
||||
codeOperation = cast(op.get('codeOperation'), int, 0)
|
||||
yield Transaction.from_dict({
|
||||
'id': op.get('idOperation'),
|
||||
'date': Date(op.get('dateOperation')),
|
||||
'rdate': Date(op.get('executionDate')),
|
||||
'vdate': Date(op.get('valueDate')),
|
||||
'type': self.COMING_TYPE_TO_TYPE.get(codeOperation) or Transaction.TYPE_UNKNOWN,
|
||||
'raw': op.get('libelle'),
|
||||
'amount': op.get('montant'),
|
||||
'card': op.get('numeroPorteurCarte'),
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue