diff --git a/modules/bnporc/browser.py b/modules/bnporc/browser.py
new file mode 100644
index 00000000..a6206f48
--- /dev/null
+++ b/modules/bnporc/browser.py
@@ -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 .
+
+
+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)',
+ '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()
diff --git a/modules/bnporc/module.py b/modules/bnporc/module.py
index 654ab848..a6fad89b 100644
--- a/modules/bnporc/module.py
+++ b/modules/bnporc/module.py
@@ -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()
diff --git a/modules/bnporc/pages.py b/modules/bnporc/pages.py
new file mode 100644
index 00000000..6a683c8e
--- /dev/null
+++ b/modules/bnporc/pages.py
@@ -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 .
+
+
+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'),
+ })