diff --git a/modules/bforbank/__init__.py b/modules/bforbank/__init__.py new file mode 100644 index 00000000..1739544d --- /dev/null +++ b/modules/bforbank/__init__.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- + +# Copyright(C) 2015 Baptiste Delpey +# +# 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 BforbankModule + + +__all__ = ['BforbankModule'] diff --git a/modules/bforbank/browser.py b/modules/bforbank/browser.py new file mode 100644 index 00000000..d57c0188 --- /dev/null +++ b/modules/bforbank/browser.py @@ -0,0 +1,58 @@ +# -*- coding: utf-8 -*- + +# Copyright(C) 2015 Baptiste Delpey +# +# 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.exceptions import BrowserIncorrectPassword +from weboob.browser import LoginBrowser, URL, need_login + +from .pages import LoginPage, ErrorPage, AccountsPage, HistoryPage + + +class BforbankBrowser(LoginBrowser): + BASEURL = 'https://www.bforbank.com' + + login = URL('/connexion-client/service/login\?urlBack=%2Fespace-client', LoginPage) + error = URL('/connexion-client/service/auth', ErrorPage) + home = URL('/espace-client/$', AccountsPage) + history = URL('/espace-client/livret/consultation.*', HistoryPage) + + def __init__(self, birthdate, *args, **kwargs): + super(BforbankBrowser, self).__init__(*args, **kwargs) + self.birthdate = birthdate + + def do_login(self): + assert isinstance(self.username, basestring) + assert isinstance(self.password, basestring) + assert self.password.isdigit() + self.login.stay_or_go() + assert self.login.is_here() + self.page.login(self.birthdate, self.username, self.password) + if self.error.is_here(): + raise BrowserIncorrectPassword() + + @need_login + def iter_accounts(self): + self.home.stay_or_go() + return self.page.iter_accounts() + + @need_login + def get_history(self, account): + self.location(account._link) + for tr in self.page.get_operations(): + yield tr diff --git a/modules/bforbank/module.py b/modules/bforbank/module.py new file mode 100644 index 00000000..89cd7f50 --- /dev/null +++ b/modules/bforbank/module.py @@ -0,0 +1,63 @@ +# -*- coding: utf-8 -*- + +# Copyright(C) 2015 Baptiste Delpey +# +# 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.backend import Module, BackendConfig +from weboob.capabilities.bank import CapBank, AccountNotFound +from weboob.capabilities.base import find_object +from weboob.tools.value import ValueBackendPassword +from .browser import BforbankBrowser + + +__all__ = ['BforbankModule'] + + +class BforbankModule(Module, CapBank): + NAME = 'bforbank' + DESCRIPTION = u'BforBank' + MAINTAINER = u'Baptiste Delpey' + EMAIL = 'b.delpey@hotmail.fr' + LICENSE = 'AGPLv3+' + VERSION = '1.1' + CONFIG = BackendConfig(ValueBackendPassword('login', label='Identifiant', masked=False), + ValueBackendPassword('password', label='Code personnel'), + ValueBackendPassword('birthdate', label='Date de naissance'), + ) + + BROWSER = BforbankBrowser + + def create_default_browser(self): + return self.create_browser(self.config['birthdate'].get(), + self.config['login'].get(), + self.config['password'].get()) + + def get_account(self, _id): + return find_object(self.browser.iter_accounts(), id=_id, error=AccountNotFound) + + def iter_accounts(self): + return self.browser.iter_accounts() + + def iter_coming(self, account): + raise NotImplementedError() + + def iter_history(self, account): + return self.browser.get_history(account) + + def iter_investment(self, account): + raise NotImplementedError() diff --git a/modules/bforbank/pages.py b/modules/bforbank/pages.py new file mode 100644 index 00000000..2a34bd63 --- /dev/null +++ b/modules/bforbank/pages.py @@ -0,0 +1,137 @@ +# -*- coding: utf-8 -*- + +# Copyright(C) 2015 Baptiste Delpey +# +# 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 re +from decimal import Decimal +from StringIO import StringIO +from PIL import Image + +from weboob.browser.pages import LoggedPage, HTMLPage +from weboob.browser.elements import method, ListElement, ItemElement +from weboob.capabilities.bank import Account +from weboob.browser.filters.standard import CleanText, Regexp, Field, Map +from weboob.tools.capabilities.bank.transactions import FrenchTransaction + + +class BfBKeyboard(object): + symbols = {'0': '00111111001111111111111111111111000000111000000001111111111111111111110011111100', + '1': '00000000000011000000011100000001100000001111111111111111111100000000000000000000', + '2': '00100000111110000111111000011111000011111000011101111111100111111100010111000001', + '3': '00100001001110000111111000011111001000011101100001111111011111111111110000011110', + '4': '00000011000000111100000111010001111001001111111111111111111111111111110000000100', + '5': '00000001001111100111111110011110010000011001000001100110011110011111110000111110', + '6': '00011111000111111110111111111111001100011000100001110011001111001111110100011110', + '7': '10000000001000000000100000111110011111111011111100111110000011100000001100000000', + '8': '00000011001111111111111111111110001000011000100001111111111111111111110010011110', + '9': '00111000001111110011111111001110000100011000010011111111111111111111110011111100', + } + + def __init__(self, basepage): + self.basepage = basepage + self.fingerprints = [] + for htmlimg in self.basepage.doc.xpath('.//div[@class="m-btn-pin"]//img'): + url = htmlimg.attrib.get("src") + imgfile = StringIO(basepage.browser.open(url).content) + img = Image.open(imgfile) + matrix = img.load() + s = "" + # The digit is only displayed in the center of image + for x in range(19, 27): + for y in range(17, 27): + (r, g, b, o) = matrix[x, y] + # If the pixel is "red" enough + if g + b < 450: + s += "1" + else: + s += "0" + + self.fingerprints.append(s) + + def get_symbol_code(self, digit): + fingerprint = self.symbols[digit] + for i, string in enumerate(self.fingerprints): + if string == fingerprint: + return i + + def get_string_code(self, string): + code = '' + for c in string: + codesymbol = self.get_symbol_code(c) + code += str(codesymbol) + return code + +class LoginPage(HTMLPage): + def login(self, birthdate, username, password): + vk = BfBKeyboard(self) + code = vk.get_string_code(password) + form = self.get_form() + form['j_username'] = username + form['birthDate'] = birthdate + form['indexes'] = code + form.submit() + + +class ErrorPage(HTMLPage): + pass + +class AccountsPage(LoggedPage, HTMLPage): + @method + class iter_accounts(ListElement): + item_xpath = '//table/tbody/tr' + + class item(ItemElement): + klass = Account + + TYPE = {'Livret': Account.TYPE_SAVINGS, + } + + def obj_balance(self): + balance = CleanText(self.el.xpath('./td/div/div[1]/div/span'))(self) + balance = re.sub(r'[^\d\-\,]', '', balance) + return Decimal(re.sub(r',(?!(\d+$))', '', balance).replace(',', '.')) + + obj_id = CleanText('./td/div/div[3]/span') + obj_label = CleanText('./td/div/div[2]/span') + obj_currency = FrenchTransaction.Currency('./td/div/div[1]/div/span') + obj_type = Map(Regexp(Field('label'), r'^(\w+)'), TYPE, default=Account.TYPE_UNKNOWN) + obj__link = CleanText('./@data-href') + + +class Transaction(FrenchTransaction): + PATTERNS = [(re.compile('^(?PVIREMENT)'), FrenchTransaction.TYPE_TRANSFER), + (re.compile('^(?PINTERETS)'), FrenchTransaction.TYPE_BANK), + ] + + +class HistoryPage(LoggedPage, HTMLPage): + @method + class get_operations(ListElement): + item_xpath = '//table[contains(@class, "table")]/tbody/div/tr[contains(@class, "submit")]' + + class item(ItemElement): + klass = Transaction + + def obj_amount(self): + balance = CleanText(self.el.xpath('./td[4]'))(self) + balance = re.sub(r'[^\d\-\,]', '', balance) + return Decimal(re.sub(r',(?!(\d+$))', '', balance).replace(',', '.')) + + obj_date = Transaction.Date('./td[2]') + obj_vdate = Transaction.Date('./td[3]') + obj_raw = Transaction.Raw('./td[1]') diff --git a/modules/bforbank/test.py b/modules/bforbank/test.py new file mode 100644 index 00000000..48e2ee8d --- /dev/null +++ b/modules/bforbank/test.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- + +# Copyright(C) 2015 Baptiste Delpey +# +# 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 BforbankTest(BackendTest): + MODULE = 'bforbank' + + def test_bforbank(self): + raise NotImplementedError()