From 170db43de1c639ef3647fff541bb86deee1b6427 Mon Sep 17 00:00:00 2001 From: Kitof Date: Thu, 26 Feb 2015 11:48:24 +0100 Subject: [PATCH] =?UTF-8?q?[s2e]=20New=20module=20for=20Employee=20Savings?= =?UTF-8?q?=20Plans.=20Support=20for=20Esalia,=20Capeasi,=20"BNP=20Paribas?= =?UTF-8?q?=20=C3=89pargne=20&=20Retraite=20Entreprises"=20and=20"HSBC=20E?= =?UTF-8?q?pargne=20et=20Retraite=20en=20Entreprise"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/s2e/__init__.py | 23 ++++++++ modules/s2e/browser.py | 127 ++++++++++++++++++++++++++++++++++++++++ modules/s2e/module.py | 73 +++++++++++++++++++++++ modules/s2e/pages.py | 97 ++++++++++++++++++++++++++++++ modules/s2e/test.py | 31 ++++++++++ 5 files changed, 351 insertions(+) create mode 100644 modules/s2e/__init__.py create mode 100644 modules/s2e/browser.py create mode 100644 modules/s2e/module.py create mode 100644 modules/s2e/pages.py create mode 100644 modules/s2e/test.py diff --git a/modules/s2e/__init__.py b/modules/s2e/__init__.py new file mode 100644 index 00000000..0be786f7 --- /dev/null +++ b/modules/s2e/__init__.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- + +# Copyright(C) 2015 Christophe Lampin +# +# 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 .module import S2eModule + +__all__ = ['S2eModule'] diff --git a/modules/s2e/browser.py b/modules/s2e/browser.py new file mode 100644 index 00000000..5675d5cc --- /dev/null +++ b/modules/s2e/browser.py @@ -0,0 +1,127 @@ +# -*- coding: utf-8 -*- + +# Copyright(C) 2015 Christophe Lampin +# +# 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 datetime import datetime +from decimal import Decimal + +from weboob.browser import LoginBrowser, URL, need_login +from weboob.browser.profiles import Android +from weboob.exceptions import BrowserIncorrectPassword +from weboob.capabilities.bank import Account, Transaction + +from .pages import LoginPage, CalcPage, ProfilPage, AccountsPage, HistoryPage, I18nPage + +__all__ = ['Esalia'] + + +class S2eBrowser(LoginBrowser): + + PROFILE = Android() + CTCC = "" + LANG = "FR" + + sessionId = None + + loginp = URL('/$', LoginPage) + calcp = URL('/s2e_services/restServices/calculetteService/grillemdp\?uuid=(?P)', CalcPage) + profilp = URL('/s2e_services/restServices/authentification/loginS', ProfilPage) + accountsp = URL('/s2e_services/restServices/situationCompte', AccountsPage) + historyp = URL('/s2e_services/restServices/listeOperation', HistoryPage) + i18np = URL('/(?P.*)/LANG/(?P.*).json', I18nPage) + + def __init__(self, url, username, password, *args, **kwargs): + super(S2eBrowser, self).__init__(username, password, *args, **kwargs) + self.BASEURL = "https://" + url + + def do_login(self): + self.logger.debug('call Browser.do_login') + self.loginp.stay_or_go() + self.page.login(self.username, self.password) + if self.sessionId is None: + raise BrowserIncorrectPassword() + + @need_login + def get_accounts_list(self): + data = {'clang': self.LANG, + 'ctcc': self.CTCC, + 'login': self.username, + 'session': self.sessionId} + + for k, fond in self.accountsp.open(data=data).get_list().items(): + a = Account() + a.id = k + a.type = Account.TYPE_LOAN + a.balance = Decimal(fond["montantValeurEuro"]).quantize(Decimal('.01')) + a.label = fond["libelleSupport"] + a.currency = u"EUR" # Don't find any possbility to get that from configuration. + yield a + + @need_login + def iter_history(self, account): + # Load i18n for type translation + self.i18np.open(lang1=self.LANG, lang2=self.LANG).load_i18n() + + # For now detail for each account is not available. History is global for all accounts and very simplist + data = {'clang': self.LANG, + 'ctcc': self.CTCC, + 'login': self.username, + 'session': self.sessionId} + + for trans in self.historyp.open(data=data).get_transactions(): + t = Transaction() + t.id = trans["referenceOperationIndividuelle"] + t.date = datetime.strptime(trans["dateHeureSaisie"], "%d/%m/%Y") + t.rdate = datetime.strptime(trans["dateHeureSaisie"], "%d/%m/%Y") + t.type = Transaction.TYPE_DEPOSIT if trans["montantNetEuro"] > 0 else Transaction.TYPE_PAYBACK + t.raw = trans["typeOperation"] + t.label = self.i18n["OPERATION_TYPE_" + trans["casDeGestion"]] + t.amount = Decimal(trans["montantNetEuro"]).quantize(Decimal('.01')) + yield t + + +class Esalia(S2eBrowser): + CTCC = "SG" + loginp = URL('/Esalia/$', LoginPage) + i18np = URL('/Esalia/SG/(?P.*)/LANG/(?P.*).json', I18nPage) + + +class Capeasi(S2eBrowser): + CTCC = "AXA" + loginp = URL('/AXA/$', LoginPage) + i18np = URL('/AXA/(?P.*)/LANG/(?P.*).json', I18nPage) + + +class EREHSBC(S2eBrowser): + CTCC = "HSBC" + loginp = URL('/ERE-HSBC/$', LoginPage) + i18np = URL('/ERE-HSBC/HSBC/(?P.*)/LANG/(?P.*).json', I18nPage) + + +class CreditNord(S2eBrowser): + CTCC = "" # FIXME : Not Available Yet + loginp = URL('//$', LoginPage) + # Hack : Lang.json of BNPERE is only available in app. Get it from Esalia + i18np = URL('https://m.esalia.com/Esalia/SG/(?P.*)/LANG/(?P.*).json', I18nPage) + + +class BNPPERE(S2eBrowser): + CTCC = "BNP" + loginp = URL('/$', LoginPage) + # Hack : Lang.json of BNPERE is only available in app. Get it from Esalia + i18np = URL('https://m.esalia.com/Esalia/SG/(?P.*)/LANG/(?P.*).json', I18nPage) diff --git a/modules/s2e/module.py b/modules/s2e/module.py new file mode 100644 index 00000000..0135add3 --- /dev/null +++ b/modules/s2e/module.py @@ -0,0 +1,73 @@ +# -*- coding: utf-8 -*- + +# Copyright(C) 2015 Christophe Lampin + +# 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 weboob.capabilities.base import find_object +from weboob.capabilities.bank import CapBank, AccountNotFound +from weboob.tools.backend import Module, BackendConfig +from weboob.tools.value import Value, ValueBackendPassword +from weboob.tools.ordereddict import OrderedDict + +from .browser import Esalia, Capeasi, EREHSBC, BNPPERE + + +__all__ = ['S2eModule'] + + +class S2eModule(Module, CapBank): + NAME = 's2e' + MAINTAINER = u'Christophe Lampin' + EMAIL = 'weboob@lampin.net' + VERSION = '1.1' + LICENSE = 'AGPLv3+' + DESCRIPTION = u'S2e module for Employee Savings Plans. Support for Esalia, Capeasi, "BNP Paribas Épargne & Retraite Entreprises" and "HSBC Epargne et Retraite en Entreprise"' + + website_choices = OrderedDict([(k, u'%s (%s)' % (v, k)) for k, v in sorted({ + 'm.esalia.com': u'Esalia', # Good Url. Tested + 'mobile.capeasi.com': u'Capeasi', # Good Url. Not fully tested + 'mobi.ere.hsbc.fr': u'ERE HSBC', # Good Url. Not fully tested + 'smartphone.s2e-net.com': u'BNPP ERE', # Url To Confirm. Not tested + # 'smartphone.s2e-net.com': u'Groupe Crédit du Nord', # Mobile version not available yet. + }.iteritems(), key=lambda k_v: (k_v[1], k_v[0]))]) + + BROWSERS = { + 'm.esalia.com': Esalia, + 'mobile.capeasi.com': Capeasi, + 'mobi.ere.hsbc.fr': EREHSBC, + 'smartphone.s2e-net.com': BNPPERE, + # 'smartphone.s2e-net.com': CreditNord, # Mobile version not available yet. + } + + CONFIG = BackendConfig(Value('website', label='Banque', choices=website_choices, default='smartphone.s2e-net.com'), + ValueBackendPassword('login', label='Identifiant', masked=False), + ValueBackendPassword('password', label='Code secret', regexp='^(\d{6}|)$')) + + def create_default_browser(self): + self.BROWSER = self.BROWSERS[self.config['website'].get()] + return self.create_browser(self.config['website'].get(), + self.config['login'].get(), + self.config['password'].get()) + + def iter_accounts(self): + return self.browser.get_accounts_list() + + def get_account(self, _id): + return find_object(self.browser.get_accounts_list(), id=_id, error=AccountNotFound) + + def iter_history(self, account): + return self.browser.iter_history(account) \ No newline at end of file diff --git a/modules/s2e/pages.py b/modules/s2e/pages.py new file mode 100644 index 00000000..72d6bc45 --- /dev/null +++ b/modules/s2e/pages.py @@ -0,0 +1,97 @@ +# -*- coding: utf-8 -*- + +# Copyright(C) 2015 Christophe Lampiné +# +# 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 random + +from weboob.browser.pages import HTMLPage, LoggedPage, JsonPage + + +class LoginPage(HTMLPage): + def generate_uuid(self): + chars = list('0123456789abcdef') + uuid = [None]*36 + rnd = random.random + for i in (8, 13, 18, 23): + uuid[i] = '-' + uuid[14] = '4' # version 4 + + for i in range(36): + if uuid[i] is None: + r = 0 | int(rnd()*16) + idx = (((r & 0x3) | 0x8) if (i == 19) else (r & 0xf)) + uuid[i] = chars[idx] + i += 1 + + return ''.join(uuid) + + def login(self, login, password): + uuid = self.generate_uuid() + data = self.browser.calcp.open(uuid=uuid).get_data(login, password) + self.browser.profilp.open(data=data).store_sessionId() + + +class CalcPage(JsonPage): + def get_data(self, login, password): + convert_data = {} + for num_data in self.doc['grilleMdp']: + convert_data[num_data["nom"]] = num_data["valeur"] + + encrypt_pass = "" + for char in password: + encrypt_pass += (convert_data[int(char)] + ":") + + data = {'clang': self.browser.LANG, + 'conversationId': self.doc["conversationId"], + 'ctcc': self.browser.CTCC, + 'login': login, + 'password': encrypt_pass} + + return data + + +class ProfilPage(JsonPage): + def store_sessionId(self): + self.browser.sessionId = self.doc['session'] + + +class AccountsPage(LoggedPage, JsonPage): + def get_list(self): + accounts = {} + for entreprise in self.doc["listeEntreprise"]: + for dispositif in entreprise["listeDispositf"]: # Ceci n'est pas une erreur de frappe ;) + for fonds in dispositif["listeFonds"]: + if fonds["montantValeurEuro"] == 0: + continue + fonds["codeLong"] = entreprise["codeEntreprise"] + dispositif["codeDispositif"] + fonds["codeSupport"] + if fonds["codeLong"] in accounts: + accounts[fonds["codeLong"]]["montantValeurEuro"] += fonds["montantValeurEuro"] + else: + accounts[fonds["codeLong"]] = fonds + return accounts + + +class HistoryPage(LoggedPage, JsonPage): + def get_transactions(self): + for operation in self.doc["listeOperations"]: + yield operation + + +class I18nPage(JsonPage): + def load_i18n(self): + self.browser.i18n = self.doc["i18n"] diff --git a/modules/s2e/test.py b/modules/s2e/test.py new file mode 100644 index 00000000..77ce451d --- /dev/null +++ b/modules/s2e/test.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- + +# Copyright(C) 2015 Christophe Lampin +# +# 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 weboob.tools.test import BackendTest + + +class S2eTest(BackendTest): + MODULE = 's2e' + + def test_bank(self): + l = list(self.backend.iter_accounts()) + if len(l) > 0: + a = l[0] + list(self.backend.iter_history(a))