diff --git a/modules/bouygues/__init__.py b/modules/bouygues/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..f6b92a3988db968ea15e0fffaa4e2728107f9aa9
--- /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 0000000000000000000000000000000000000000..f1d644810937c0f0bfe861100b9e43a2eede4859
--- /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 0000000000000000000000000000000000000000..2f1a311ff3add09b0b6e1fa10605bb0b3a47bbde
--- /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 0000000000000000000000000000000000000000..5bd2013f0911b79331643100d036bd300461d01b
--- /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