1#!/usr/bin/env python3
2
3import logging
4import os
5from signal import SIG_DFL, SIGINT, signal
6from threading import Thread
7
8from gi.repository import AppIndicator3 as appindicator
9from gi.repository import GObject, Gtk, Notify
10from pkg_resources import resource_filename
11
12from weboob.capabilities import UserError
13from weboob.capabilities.bank import Account, CapBank
14from weboob.core import CallErrors, Weboob
15from weboob.exceptions import BrowserForbidden, BrowserIncorrectPassword, BrowserSSLError, BrowserUnavailable
16from weboob.tools.application.base import MoreResultsAvailable
17from weboob.tools.compat import unicode
18
19PING_FREQUENCY = 3600  # seconds
20APPINDICATOR_ID = "boobank_indicator"
21PATH = os.path.realpath(__file__)
22
23
24def create_image_menu_item(label, image):
25    item = Gtk.ImageMenuItem()
26    img = Gtk.Image()
27    img.set_from_file(os.path.abspath(resource_filename('boobank_indicator.data', image)))
28    item.set_image(img)
29    item.set_label(label)
30    item.set_always_show_image(True)
31    return item
32
33
34class BoobankTransactionsChecker(Thread):
35    def __init__(self, weboob, menu, account):
36        Thread.__init__(self)
37        self.weboob = weboob
38        self.menu = menu
39        self.account = account
40
41    def run(self):
42        account_history_menu = Gtk.Menu()
43
44        for tr in self.weboob.do('iter_history', self.account, backends=self.account.backend):
45            label = u'%s - %s: %s%s' % (tr.date, tr.label, tr.amount, self.account.currency_text)
46            image = "green_light.png" if tr.amount > 0 else "red_light.png"
47            transaction_item = create_image_menu_item(label, image)
48            account_history_menu.append(transaction_item)
49            transaction_item.show()
50
51        self.menu.set_submenu(account_history_menu)
52
53
54class BoobankChecker():
55    def __init__(self):
56        self.ind = appindicator.Indicator.new(APPINDICATOR_ID,
57                                              os.path.abspath(resource_filename('boobank_indicator.data',
58                                                                                'indicator-boobank.png')),
59                                              appindicator.IndicatorCategory.APPLICATION_STATUS)
60
61        self.menu = Gtk.Menu()
62        self.ind.set_menu(self.menu)
63
64        logging.basicConfig()
65        if 'weboob_path' in os.environ:
66            self.weboob = Weboob(os.environ['weboob_path'])
67        else:
68            self.weboob = Weboob()
69
70        self.weboob.load_backends(CapBank)
71
72    def clean_menu(self, menu):
73        for i in menu.get_children():
74            submenu = i.get_submenu()
75            if submenu:
76                self.clean_menu(i)
77            menu.remove(i)
78
79    def check_boobank(self):
80        self.ind.set_status(appindicator.IndicatorStatus.ACTIVE)
81        self.clean_menu(self.menu)
82
83        total = 0
84        currency = ''
85        threads = []
86
87        try:
88            for account in self.weboob.do('iter_accounts'):
89
90                balance = account.balance
91                if account.coming:
92                    balance += account.coming
93
94                if account.type != Account.TYPE_LOAN:
95                    total += balance
96                    image = "green_light.png" if balance > 0 else "red_light.png"
97                else:
98                    image = "personal-loan.png"
99
100                currency = account.currency_text
101                label = "%s: %s%s" % (account.label, balance, account.currency_text)
102                account_item = create_image_menu_item(label, image)
103                thread = BoobankTransactionsChecker(self.weboob, account_item, account)
104                thread.start()
105                threads.append(thread)
106
107        except CallErrors as errors:
108            self.bcall_errors_handler(errors)
109
110        for thread in threads:
111            thread.join()
112
113        for thread in threads:
114            self.menu.append(thread.menu)
115            thread.menu.show()
116
117        if len(self.menu.get_children()) == 0:
118            Notify.Notification.new('<b>Boobank</b>',
119                                    'No Bank account found\n Please configure one by running boobank',
120                                    'notification-message-im').show()
121
122        sep = Gtk.SeparatorMenuItem()
123        self.menu.append(sep)
124        sep.show()
125
126        total_item = Gtk.MenuItem("%s: %s%s" % ("Total", total, currency))
127        self.menu.append(total_item)
128        total_item.show()
129
130        sep = Gtk.SeparatorMenuItem()
131        self.menu.append(sep)
132        sep.show()
133
134        btnQuit = Gtk.ImageMenuItem()
135        image = Gtk.Image()
136        image.set_from_stock(Gtk.STOCK_QUIT, Gtk.IconSize.BUTTON)
137        btnQuit.set_image(image)
138        btnQuit.set_label('Quit')
139        btnQuit.set_always_show_image(True)
140        btnQuit.connect("activate", self.quit)
141        self.menu.append(btnQuit)
142        btnQuit.show()
143
144    def quit(self, widget):
145        Gtk.main_quit()
146
147    def bcall_errors_handler(self, errors):
148        """
149        Handler for the CallErrors exception.
150        """
151        self.ind.set_status(appindicator.IndicatorStatus.ATTENTION)
152        for backend, error, backtrace in errors.errors:
153            notify = True
154            if isinstance(error, BrowserIncorrectPassword):
155                msg = 'invalid login/password.'
156            elif isinstance(error, BrowserSSLError):
157                msg = '/!\ SERVER CERTIFICATE IS INVALID /!\\'
158            elif isinstance(error, BrowserForbidden):
159                msg = unicode(error) or 'Forbidden'
160            elif isinstance(error, BrowserUnavailable):
161                msg = unicode(error)
162                if not msg:
163                    msg = 'website is unavailable.'
164            elif isinstance(error, NotImplementedError):
165                notify = False
166            elif isinstance(error, UserError):
167                msg = unicode(error)
168            elif isinstance(error, MoreResultsAvailable):
169                notify = False
170            else:
171                msg = unicode(error)
172
173            if notify:
174                Notify.Notification.new('<b>Error Boobank: %s</b>' % backend.name,
175                                        msg,
176                                        'notification-message-im').show()
177
178    def main(self):
179        self.check_boobank()
180        GObject.timeout_add(PING_FREQUENCY * 1000, self.check_boobank)
181        Gtk.main()
182
183
184def main():
185    signal(SIGINT, SIG_DFL)
186    GObject.threads_init()
187    Notify.init('boobank_indicator')
188    BoobankChecker().main()
189
190
191if __name__ == "__main__":
192    main()
193