diff --git a/modules/freemobile/__init__.py b/modules/freemobile/__init__.py new file mode 100644 index 00000000..e31da4a3 --- /dev/null +++ b/modules/freemobile/__init__.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- + +# Copyright(C) 2012 Florent Fourcot +# +# 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 .backend import FreeMobileBackend + +__all__ = ['FreeMobileBackend'] diff --git a/modules/freemobile/backend.py b/modules/freemobile/backend.py new file mode 100644 index 00000000..c6804515 --- /dev/null +++ b/modules/freemobile/backend.py @@ -0,0 +1,73 @@ +# -*- coding: utf-8 -*- + +# Copyright(C) 2012 Florent Fourcot +# +# 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.bill import ICapBill, SubscriptionNotFound +from weboob.tools.backend import BaseBackend, BackendConfig +from weboob.tools.value import ValueBackendPassword + +from .browser import Freemobile + + +__all__ = ['FreeMobileBackend'] + + +class FreeMobileBackend(BaseBackend, ICapBill): + NAME = 'freemobile' + MAINTAINER = 'Florent Fourcot' + EMAIL = 'weboob@flo.fourcot.fr' + VERSION = '0.b' + LICENSE = 'AGPLv3+' + DESCRIPTION = 'Free Mobile website' + CONFIG = BackendConfig(ValueBackendPassword('login', label='Account ID', masked=False, regexp='^(\d{8}|)$'), + ValueBackendPassword('password', label='Password') + ) + BROWSER = Freemobile + + def create_default_browser(self): + return self.create_browser(self.config['login'].get(), + self.config['password'].get()) + + def iter_subscription(self): + for subscription in self.browser.get_subscription_list(): + yield subscription + + def get_subscription(self, _id): + if not _id.isdigit(): + raise SubscriptionNotFound() + with self.browser: + subscription = self.browser.get_subscription(_id) + if subscription: + return subscription + else: + raise SubscriptionNotFound() + + + def iter_history(self, subscription): + raise NotImplementedError() + + def get_pdf(self, account): + raise NotImplementedError() + + # The subscription is actually useless, but maybe for the futur... + def get_details(self, subscription): + with self.browser: + for detail in self.browser.get_details(): + yield detail + diff --git a/modules/freemobile/browser.py b/modules/freemobile/browser.py new file mode 100644 index 00000000..ea5fe4c1 --- /dev/null +++ b/modules/freemobile/browser.py @@ -0,0 +1,87 @@ +# -*- coding: utf-8 -*- + +# Copyright(C) 2012 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 weboob.tools.browser import BaseBrowser, BrowserIncorrectPassword +from .pages import HomePage, LoginPage, HistoryPage + +__all__ = ['Freemobile'] + + +class Freemobile(BaseBrowser): + DOMAIN = 'mobile.free.fr' + PROTOCOL = 'https' + ENCODING = None # refer to the HTML encoding + PAGES = {'.*moncompte/index.php': LoginPage, + '.*page=home': HomePage, + '.*page=suiviconso': HistoryPage + } + + def __init__(self, *args, **kwargs): + BaseBrowser.__init__(self, *args, **kwargs) + + def home(self): + self.location('https://mobile.free.fr/moncompte/index.php') + + def is_logged(self): + return not self.is_on_page(LoginPage) + + def login(self): + assert isinstance(self.username, basestring) + assert isinstance(self.password, basestring) + assert self.username.isdigit() + + if not self.is_on_page(LoginPage): + self.location('https://mobile.free.fr/moncompte/index.php') + + self.page.login(self.username, self.password) + + if self.is_on_page(LoginPage): + raise BrowserIncorrectPassword() + + def get_subscription_list(self): + if not self.is_on_page(HomePage): + self.location('/moncompte/index.php?page=home') + + return self.page.get_list() + + def get_subscription(self, id): + assert isinstance(id, basestring) + + if not self.is_on_page(HomePage): + self.location('/moncompte/index.php?page=home') + + l = self.page.get_list() + for a in l: + if a.id == id: + return a + + return None + + # XXX : not implemented + def get_history(self): + if not self.is_on_page(HistoryPage): + self.location('/moncompte/index.php?page=suiviconso') + return self.page.get_calls() + + def get_details(self): + if not self.is_on_page(HistoryPage): + self.location('/moncompte/index.php?page=suiviconso') + test = self.page.get_details() + return test diff --git a/modules/freemobile/pages/__init__.py b/modules/freemobile/pages/__init__.py new file mode 100644 index 00000000..bd7ad218 --- /dev/null +++ b/modules/freemobile/pages/__init__.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- + +# Copyright(C) 2012 Florent Fourcot +# +# 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 .homepage import HomePage +from .history import HistoryPage +from .login import LoginPage + +__all__ = ['LoginPage', 'HomePage', 'HistoryPage'] diff --git a/modules/freemobile/pages/history.py b/modules/freemobile/pages/history.py new file mode 100644 index 00000000..1996af74 --- /dev/null +++ b/modules/freemobile/pages/history.py @@ -0,0 +1,77 @@ +# -*- coding: utf-8 -*- + +# Copyright(C) 2012 Florent Fourcot +# +# 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.browser import BasePage +from weboob.capabilities.bill import Detail + +__all__ = ['HistoryPage'] + +def convert_price(div): + try: + price = div.find('div[@class="horsForfait"]/p/span').text + price = price.encode('utf-8', 'replace').replace('€', '').replace(',', '.') + return float(price) + except: + return 0. + + +class HistoryPage(BasePage): + calls = [] + details = [] + + def on_loaded(self): + + divnat = self.document.xpath('//div[@class="national"]')[0] + divs = divnat.xpath('div[@class="detail"]') + divvoice = divs.pop(0) + + # Two informations in one div... + voice = Detail() + voice.label = divvoice.find('div[@class="titreDetail"]/p').text_content() + voice.price = convert_price(divvoice) + voicenat = divvoice.xpath('div[@class="consoDetail"]/p/span')[0].text + voiceint = divvoice.xpath('div[@class="consoDetail"]/p/span')[1].text + voice.infos = "Consommation : " + voicenat + " International : " + voiceint + self.details.append(voice) + + self.iter_divs(divs) + divint = self.document.xpath('//div[@class="international hide"]')[0] + self.iter_divs(divint.xpath('div[@class="detail"]'), True) + + + def iter_divs(self, divs, inter=False): + for div in divs: + detail = Detail() + + detail.label = div.find('div[@class="titreDetail"]/p').text_content() + if inter: + detail.label = detail.label + " (international)" + detail.infos = div.find('div[@class="consoDetail"]/p').text_content().lstrip() + detail.price = convert_price(div) + + self.details.append(detail) + + + + def get_calls(self): + return self.calls + + def get_details(self): + return self.details diff --git a/modules/freemobile/pages/homepage.py b/modules/freemobile/pages/homepage.py new file mode 100644 index 00000000..745411b3 --- /dev/null +++ b/modules/freemobile/pages/homepage.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- + +# Copyright(C) 2012 Florent Fourcot +# +# 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.bill import Subscription +from weboob.tools.browser import BasePage + + +__all__ = ['HomePage'] + + +class HomePage(BasePage): + def on_loaded(self): + pass + + def get_list(self): + l = [] + divabo = self.document.xpath('//div[@class="idAbonne"]')[0] + owner = divabo.xpath('p')[0].text.replace(' - ', '') + phone = divabo.xpath('p/span')[0].text + self.browser.logger.debug('Found ' + owner + ' has subscriber') + self.browser.logger.debug('Found ' + phone + ' has phone number') + phoneplan = self.document.xpath('//div[@class="forfaitChoisi"]')[0].text + self.browser.logger.debug('Found ' + phoneplan + ' has subscription type') + + subscription = Subscription(phone) + subscription.label = phone + ' - ' + phoneplan + subscription.owner = owner + + l.append(subscription) + + return l diff --git a/modules/freemobile/pages/login.py b/modules/freemobile/pages/login.py new file mode 100644 index 00000000..aa5d9a8c --- /dev/null +++ b/modules/freemobile/pages/login.py @@ -0,0 +1,107 @@ +# -*- coding: utf-8 -*- + +# Copyright(C) 2012 Florent Fourcot +# +# 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 Image + +from weboob.tools.browser import BasePage + +__all__ = ['LoginPage'] + +class FreeKeyboard(object): + symbols={'0':'001111111111110011111111111111111111111111111110000000000011110000000000011111111111111111011111111111111001111111111110', + '1':'001110000000000001110000000000001110000000000011111111111111111111111111111111111111111111000000000000000000000000000000', + '2':'011110000001111011110000111111111000001111111110000011110011110000111100011111111111000011011111110000011001111000000011', + '3':'011100000011110111100000011111111000110000111110000110000011110001110000011111111111111111011111111111110001110001111100', + '4':'000000011111000000001111111000000111110011000011110000011000111111111111111111111111111111111111111111111000000000011000', + '5':'111111110011110111111110011111111001110000111111001100000011111001100000011111001111111111111001111111111010000111111110', + '6':'001111111111110011111111111111111111111111111110001100000011110001100000011111001111111111111101111111111011100111111110', + '7':'111000000000000111000000000000111000000011111111000011111111111011111111111111111111000000111111000000000111100000000000', + '8':'001110001111110011111111111111111111111111111110000110000011110000110000011111111111111111011111111111111001111001111110', + '9':'001111111000110011111111100111111111111100111110000001100011110000001100011111111111111111011111111111111001111111111110' + } + fingerprints = [] + + def __init__(self,basepage): + for htmlimg in basepage.document.xpath('//img[@class="ident_chiffre_img pointer"]'): + url = htmlimg.attrib.get("src") + fichier = basepage.browser.openurl(url) + image = Image.open(fichier) + matrix = image.load() + s = "" + # The digit is only displayed in the center of image + for x in range(15, 23): + for y in range(12, 27): + (r, g, b) = matrix[x,y] + # If the pixel is "red" enough + if (g*g + b*b) < r*r: + s += "1" + else: + s += "0" + + self.fingerprints.append(s) + + def get_symbol_code(self,digit): + fingerprint = self.symbols[digit] + i = 0 + for string in self.fingerprints: + if string.__eq__(fingerprint): + return i + i += 1 + # Image contains some noise, and the match is not alaways perfect + # (this is why we can't use md5 hashs) + # But if we can't find the perfect one, we can take one with smalls errors + i = 0 + for string in self.fingerprints: + j = 0 + match = 0 + for bit in string: + if bit == fingerprint[j]: + match += 1 + j += 1 + if match > 115: + return i + i += 1 + + # TODO : exception + + def get_string_code(self,string): + code='' + for c in string: + codesymbol = self.get_symbol_code(c) + code+=str(codesymbol) + return code + + + +class LoginPage(BasePage): + def on_loaded(self): + pass + + def login(self, login, password): + vk = FreeKeyboard(self) + + # Fucking form without name... + self.browser.select_form(nr=0) + self.browser.set_all_readonly(False) + self.browser['login_abo'] = vk.get_string_code(login) + self.browser['pwd_abo'] = password + self.browser.submit(nologin=True) + +