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