From 6ce0a845b3ecd5f3339a06355cbe53ecca1a228e Mon Sep 17 00:00:00 2001 From: Florian Duguet Date: Fri, 26 Apr 2019 13:52:34 +0200 Subject: [PATCH] [bouygues] new module --- modules/bouygues/__init__.py | 11 ++++ modules/bouygues/browser.py | 102 +++++++++++++++++++++++++++++ modules/bouygues/module.py | 68 +++++++++++++++++++ modules/bouygues/pages.py | 123 +++++++++++++++++++++++++++++++++++ 4 files changed, 304 insertions(+) create mode 100644 modules/bouygues/__init__.py create mode 100644 modules/bouygues/browser.py create mode 100644 modules/bouygues/module.py create mode 100644 modules/bouygues/pages.py diff --git a/modules/bouygues/__init__.py b/modules/bouygues/__init__.py new file mode 100644 index 0000000000..f6b92a3988 --- /dev/null +++ b/modules/bouygues/__init__.py @@ -0,0 +1,11 @@ +# -*- coding: utf-8 -*- + +# Copyright(C) 2019 Budget Insight + +from __future__ import unicode_literals + + +from .module import BouyguesModule + + +__all__ = ['BouyguesModule'] diff --git a/modules/bouygues/browser.py b/modules/bouygues/browser.py new file mode 100644 index 0000000000..f1d6448109 --- /dev/null +++ b/modules/bouygues/browser.py @@ -0,0 +1,102 @@ +# -*- coding: utf-8 -*- + +# Copyright(C) 2019 Budget Insight +# +# This file is part of a weboob module. +# +# This weboob module is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This weboob module 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 Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this weboob module. If not, see . + +from __future__ import unicode_literals + +from time import time +from jose import jwt + +from weboob.browser import LoginBrowser, URL, need_login +from weboob.browser.exceptions import HTTPNotFound +from weboob.tools.compat import urlparse, parse_qsl + +from .pages import ( + LoginPage, AppConfigPage, SubscriberPage, SubscriptionPage, SubscriptionDetail, DocumentPage, DocumentDownloadPage, + DocumentFilePage, +) + + +class MyURL(URL): + def go(self, *args, **kwargs): + kwargs['id_personne'] = self.browser.id_personne + kwargs['headers'] = self.browser.headers + return super(MyURL, self).go(*args, **kwargs) + + +class BouyguesBrowser(LoginBrowser): + BASEURL = 'https://api.bouyguestelecom.fr' + + login_page = URL(r'https://www.mon-compte.bouyguestelecom.fr/cas/login', LoginPage) + app_config = URL(r'https://www.bouyguestelecom.fr/mon-compte/data/app-config.json', AppConfigPage) + subscriber_page = MyURL(r'/personnes/(?P\d+)$', SubscriberPage) + subscriptions_page = MyURL(r'/personnes/(?P\d+)/comptes-facturation', SubscriptionPage) + subscription_detail_page = URL(r'/comptes-facturation/(?P\d+)/contrats-payes', SubscriptionDetail) + document_file_page = URL(r'/comptes-facturation/(?P\d+)/factures/.*/documents/.*', DocumentFilePage) + documents_page = URL(r'/comptes-facturation/(?P\d+)/factures(\?|$)', DocumentPage) + document_download_page = URL(r'/comptes-facturation/(?P\d+)/factures/.*(\?|$)', DocumentDownloadPage) + + def __init__(self, username, password, lastname, *args, **kwargs): + super(BouyguesBrowser, self).__init__(username, password, *args, **kwargs) + self.lastname = lastname + self.id_personne = None + self.headers = None + + def do_login(self): + self.login_page.go() + self.page.login(self.username, self.password) + + # q is timestamp millisecond + self.app_config.go(params={'q': int(time()*1000)}) + client_id = self.page.get_client_id() + + params = { + 'client_id': client_id, + 'response_type': 'id_token token', + 'redirect_uri': 'https://www.bouyguestelecom.fr/mon-compte/' + } + self.location('https://oauth2.bouyguestelecom.fr/authorize', params=params) + fragments = dict(parse_qsl(urlparse(self.url).fragment)) + + self.id_personne = jwt.get_unverified_claims(fragments['id_token'])['id_personne'] + authorization = 'Bearer ' + fragments['access_token'] + self.headers = {'Authorization': authorization} + + @need_login + def iter_subscriptions(self): + subscriber = self.subscriber_page.go().get_subscriber() + self.subscriptions_page.go() + for sub in self.page.iter_subscriptions(): + sub.subscriber = subscriber + sub.label = self.subscription_detail_page.go(id_account=sub.id, headers=self.headers).get_label() + yield sub + + @need_login + def iter_documents(self, subscription): + try: + self.location(subscription.url, headers=self.headers) + except HTTPNotFound as error: + json_response = error.response.json() + if json_response['error'] in ('facture_introuvable', 'compte_jamais_facture'): + return [] + raise + return self.page.iter_documents(subid=subscription.id) + + @need_login + def download_document(self, document): + return self.location(document.url, headers=self.headers).content diff --git a/modules/bouygues/module.py b/modules/bouygues/module.py new file mode 100644 index 0000000000..2f1a311ff3 --- /dev/null +++ b/modules/bouygues/module.py @@ -0,0 +1,68 @@ +# -*- coding: utf-8 -*- + +# Copyright(C) 2019 Budget Insight +# +# This file is part of a weboob module. +# +# This weboob module is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This weboob module 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 Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this weboob module. If not, see . + +from __future__ import unicode_literals + + +from weboob.tools.backend import Module, BackendConfig +from weboob.capabilities.base import find_object +from weboob.capabilities.bill import CapDocument, Document, SubscriptionNotFound, Subscription, DocumentNotFound +from weboob.tools.value import Value, ValueBackendPassword + +from .browser import BouyguesBrowser + + +__all__ = ['BouyguesModule'] + + +class BouyguesModule(Module, CapDocument): + NAME = 'bouygues' + DESCRIPTION = 'Bouygues Télécom' + MAINTAINER = 'Florian Duguet' + EMAIL = 'florian.duguet@budget-insight.com' + LICENSE = 'LGPLv3+' + VERSION = '1.6' + CONFIG = BackendConfig(Value('login', label='Numéro de mobile, de clé/tablette ou e-mail en @bbox.fr'), + ValueBackendPassword('password', label='Mot de passe'), + ValueBackendPassword('lastname', label='Nom de famille', default='')) + BROWSER = BouyguesBrowser + + def create_default_browser(self): + return self.create_browser(self.config['login'].get(), self.config['password'].get(), self.config['lastname'].get()) + + def iter_subscription(self): + return self.browser.iter_subscriptions() + + def get_subscription(self, _id): + return find_object(self.iter_subscription(), id=_id, error=SubscriptionNotFound) + + def iter_documents(self, subscription): + if not isinstance(subscription, Subscription): + subscription = self.get_subscription(subscription) + return self.browser.iter_documents(subscription) + + def get_document(self, _id): + subid = _id.rsplit('_', 1)[0] + subscription = self.get_subscription(subid) + return find_object(self.iter_documents(subscription), id=_id, error=DocumentNotFound) + + def download_document(self, document): + if not isinstance(document, Document): + document = self.get_document(document) + return self.browser.download_document(document) diff --git a/modules/bouygues/pages.py b/modules/bouygues/pages.py new file mode 100644 index 0000000000..5bd2013f09 --- /dev/null +++ b/modules/bouygues/pages.py @@ -0,0 +1,123 @@ +# -*- coding: utf-8 -*- + +# Copyright(C) 2019 Budget Insight +# +# This file is part of a weboob module. +# +# This weboob module is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This weboob module 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 Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this weboob module. If not, see . + +from __future__ import unicode_literals + +import re +from datetime import timedelta + +from weboob.browser.elements import DictElement, ItemElement, method +from weboob.browser.filters.json import Dict +from weboob.browser.pages import HTMLPage, JsonPage, LoggedPage, RawPage +from weboob.capabilities import NotAvailable +from weboob.capabilities.bill import Subscription, Bill +from weboob.browser.filters.standard import Date, CleanDecimal, Env, Format + + +class LoginPage(HTMLPage): + def login(self, username, password): + form = self.get_form() + form['username'] = username + form['password'] = password + form.submit() + + +class AppConfigPage(JsonPage): + def get_client_id(self): + return self.doc['config']['oauth']['clientId'] + + +class SubscriberPage(LoggedPage, JsonPage): + def get_subscriber(self): + assert self.doc['type'] in ('INDIVIDU', 'ENTREPRISE'), "%s is unknown" % self.doc['type'] + + if self.doc['type'] == 'INDIVIDU': + subscriber_dict = self.doc + elif self.doc['type'] == 'ENTREPRISE': + subscriber_dict = self.doc['representantLegal'] + + return '%s %s %s' % (subscriber_dict['civilite'], subscriber_dict['prenom'], subscriber_dict['nom']) + + +class SubscriptionDetail(LoggedPage, JsonPage): + def get_label(self): + label_list = [] + for s in self.doc['items']: + if 'numeroTel' in s: + phone = re.sub(r'^\+\d{2}', '0', s['numeroTel']) + label_list.append(' '.join([phone[i:i + 2] for i in range(0, len(phone), 2)])) + else: + continue + + return ' - '.join(label_list) + + +class SubscriptionPage(LoggedPage, JsonPage): + @method + class iter_subscriptions(DictElement): + item_xpath = 'items' + + class item(ItemElement): + klass = Subscription + + obj_id = Dict('id') + obj_url = Dict('_links/factures/href') + + +class MyDate(Date): + """ + some date are datetime and contains date at GMT, and always at 22H or 23H + but date inside PDF file is at GMT +1H or +2H (depends of summer or winter hour) + so we add one day and skip time to get good date + """ + def filter(self, txt): + date = super(MyDate, self).filter(txt) + if date: + date += timedelta(days=1) + return date + + +class DocumentPage(LoggedPage, JsonPage): + @method + class iter_documents(DictElement): + item_xpath = 'items' + + class item(ItemElement): + klass = Bill + + obj_id = Format('%s_%s', Env('subid'), Dict('idFacture')) + obj_price = CleanDecimal(Dict('mntTotFacture')) + obj_url = Dict('_links/facturePDF/href') + obj_date = MyDate(Dict('dateFacturation')) + obj_duedate = MyDate(Dict('dateLimitePaieFacture', default=NotAvailable), default=NotAvailable) + obj_label = Format('Facture %s', Dict('idFacture')) + obj_format = 'pdf' + obj_currency = 'EUR' + + +class DocumentDownloadPage(LoggedPage, JsonPage): + def on_load(self): + # this url change each time we want to download document, (the same one or another) + self.browser.location(self.doc['_actions']['telecharger']['action']) + + +class DocumentFilePage(LoggedPage, RawPage): + # since url of this file is almost the same than url of DocumentDownloadPage (which is a JsonPage) + # we have to define it to avoid mismatching + pass -- GitLab