Commits

Manuel Barkhau committed 37681e1

Initial Commit

  • Participants

Comments (0)

Files changed (12)

+#!/usr/bin/env python
+#1822direkt Monitor - 1822direkt Transaction Monitor for GNOME
+#Copyright (C) 2010 Manuel Barkhau <mbarkhau@gmail.com>
+#
+#This program is free software: you can redistribute it and/or modify
+#it under the terms of the GNU General Public License as published by
+#the Free Software Foundation, either version 3 of the License, or
+#(at your option) any later version.
+#
+#This program 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 General Public License for more details.
+#
+#You should have received a copy of the GNU General Public License
+#along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+import gtk
+
+from lib1822direkt.ui import Gui
+
+if __name__ == "__main__":
+    gtk.gdk.threads_init()
+    app = Gui()
+    try:
+        gtk.main()
+    except KeyboardInterrupt:
+        app.exit()
+
+[Desktop Entry]
+Type=Application
+Terminal=false
+Name=1822direkt Monitor
+Comment=1822 Transaction Monitor for GNOME
+Icon=1822direkt
+Exec=1822direkt-monitor.py
+Categories=GNOME;GTK;Network;Application;Network;
+

1822direkt.png

Added
New image

icons/hicolor/16x16/apps/1822direkt.png

Added
New image

icons/hicolor/22x22/apps/1822direkt.png

Added
New image

icons/hicolor/32x32/1822direkt.png

Added
New image

lib1822direkt/__init__.py

+APP_NAME = "1822direkt Monitor"
+APP_DESCRIPTION = "1822direkt Transaction Monitor for GNOME"
+APP_VERSION = "0.1"
+APP_AUTHORS = ("Manuel Barkhau",)
+APP_HOMEPAGE = "http://bitbucket.org/mbarkhau/1822direkt"
+APP_LICENSE = "GPL v3"

lib1822direkt/__init__.pyc

Binary file added.

lib1822direkt/comm.py

+#1822direkt Monitor - 1822direkt Transaction Monitor for GNOME
+#Copyright (C) 2010 Manuel Barkhau <mbarkhau@gmail.com>
+#
+#This program is free software: you can redistribute it and/or modify
+#it under the terms of the GNU General Public License as published by
+#the Free Software Foundation, either version 3 of the License, or
+#(at your option) any later version.
+#
+#This program 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 General Public License for more details.
+#
+#You should have received a copy of the GNU General Public License
+#along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+import os
+import re
+import time
+import tempfile
+import threading
+
+import urllib
+import urllib2
+import cookielib 
+from BeautifulSoup import BeautifulSoup
+
+BASE_URL = "https://banking.1822direkt.com"
+LOGIN_PAGE = "/JOBa1822Web/login.do"
+ACCOUNTS_PAGE = "/JOBa1822Web/getAccountsOverviewList.do"
+SELECT_TRANSACTIONS_PAGE = "/JOBa1822Web/getAccountTurnovers.do?accNdx=%i"
+LIST_TRANSACTIONS_PAGE = "/JOBa1822Web/getAccountTurnoversList.do#"
+
+SESSION_TIMEOUT = 5 * 60
+
+COOKIE_FILE = tempfile.mktemp()
+
+class BankInterface:
+
+  # available callbacks:
+  #   connection_ready
+  #   connection_fail
+  #   got_accounts
+  #   got_transactions
+
+  def __init__(self, ui_callbacks):
+
+    self._ui_callbacks = ui_callbacks
+    self._cookiejar = cookielib.LWPCookieJar()
+    processor = urllib2.HTTPCookieProcessor(self._cookiejar)
+    self._opener = urllib2.build_opener( processor )
+    urllib2.install_opener( self._opener )
+
+    self._credentials_available = False
+    self._session_start = 0
+
+  def _assert_login(self):
+    if not self._credentials_available:
+      self._ui_callbacks.connection_fail()
+      return
+
+    session_age = time.time() - self._session_start
+    if session_age < SESSION_TIMEOUT:
+      # TODO make a call to check if the session is still valid
+      return
+
+    try:
+      self._cookiejar.clear()
+      res = self._get_login_page()
+      self._cookiejar.save(COOKIE_FILE)
+      login_str = self._parse_login_page(res)
+      post_data = urllib.urlencode({
+          "login" : self._account,
+          "pin"   : self._pin 
+        })
+      url = BASE_URL + "/" + login_str
+      res = self._opener.open(url, post_data)
+      res.close()
+      self._cookiejar.save(COOKIE_FILE)
+      self._session_start = time.time()
+
+    except Exception as e:
+      self._ui_callbacks.connection_fail()
+
+    self._ui_callbacks.connection_ready()
+
+  def _get_login_page(self):
+      page = self._opener.open(BASE_URL + LOGIN_PAGE)
+      data = page.read()
+      page.close()
+      return data
+
+  def _parse_login_page(self, page):
+    if re.search("login.do;jsessionid\=", page):
+      return re.search("action\=\"\/(.{61})\"", page).group(1)
+    else:
+      raise Exception("Could not find session id")
+
+  def login(self, account, pin):
+    self._account = account
+    self._pin = pin
+    self._credentials_available = True
+    self._assert_login()
+
+  def get_accounts(self):
+    self._assert_login()
+    url = BASE_URL + ACCOUNTS_PAGE
+    res = self._opener.open(url)
+    accounts_page = res.read()
+    res.close()
+    self._cookiejar.save(COOKIE_FILE)
+    accounts = self._parse_accounts(accounts_page)
+    self._ui_callbacks.got_accounts(accounts)
+
+  def get_transactions(self, accounts):
+    self._assert_login()
+    select_url = BASE_URL + SELECT_TRANSACTIONS_PAGE
+    transactions_url = BASE_URL + LIST_TRANSACTIONS_PAGE
+    for i, account in enumerate(accounts):
+      self._opener.open(select_url % i).close()
+      res = self._opener.open(transactions_url)
+      transactions_page = res.read()
+      res.close()
+      transactions = self._parse_transactions(transactions_page)
+      self._ui_callbacks.got_transactions( account, transactions )
+
+  def _clean_cell(self, cell):
+    raw = "".join(cell.contents).strip()
+    return " ".join(raw.split("\n"))
+
+  def _parse_accounts(self, accounts_page):
+    soup = BeautifulSoup(accounts_page)
+    soup = soup.findAll("table")[0]
+    rows = soup.findAll("tr")
+    cc = self._clean_cell
+
+    accounts = []
+    for tr in rows[0:2]:
+      cells = tr.findAll("td")[0:3]
+      accounts.append(
+          {
+            "number"   : cc(cells[0]),
+            "name"     : cc(cells[1]),
+            "balance"  : cc(cells[2].div)
+          })
+
+    return accounts
+
+  def _parse_transactions(self, transactions_page):
+    soup = BeautifulSoup(transactions_page)
+    soup = soup.find("table", "listTable")
+    rows = soup.findAll("tr")
+    cc = self._clean_cell
+
+    transactions = []
+    for row in rows:
+      cells = row.findAll("td")
+      try:
+        ammount = cc(cells[5].div)
+      except Exception:
+        ammount = cc(cells[5])
+      transactions.append( {
+            "date"     : cc(cells[2]),
+            "person"   : cc(cells[4]),
+            "ammount"  : ammount
+          })
+
+    return transactions
+    

lib1822direkt/comm.pyc

Binary file added.

lib1822direkt/ui.py

+#1822direkt Monitor - 1822direkt Transaction Monitor for GNOME
+#Copyright (C) 2010 Manuel Barkhau <mbarkhau@gmail.com>
+#
+#This program is free software: you can redistribute it and/or modify
+#it under the terms of the GNU General Public License as published by
+#the Free Software Foundation, either version 3 of the License, or
+#(at your option) any later version.
+#
+#This program 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 General Public License for more details.
+#
+#You should have received a copy of the GNU General Public License
+#along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+import time
+import threading
+import pickle
+import os.path
+
+import gobject
+
+import pygtk
+pygtk.require('2.0')
+
+import gtk
+import pango
+
+import pynotify
+import gnomekeyring as gk
+
+import lib1822direkt.comm as comm
+from lib1822direkt import APP_NAME, APP_DESCRIPTION, APP_VERSION
+from lib1822direkt import APP_AUTHORS, APP_HOMEPAGE, APP_LICENSE
+
+import glib
+glib.set_application_name(APP_NAME)
+
+ICONS_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)),"..", "icons")
+
+SECONDS_1_MIN = 60
+SECONDS_1_HOUR = 60*60
+SECONDS_1_DAY = 60*60*24
+
+SECONDS_UPDATE_FREQ = 1 * SECONDS_1_HOUR
+
+COLOR_BLACK = "#000000"
+COLOR_RED = "#CC0000"
+
+APP_DIR = os.path.join(os.path.expanduser("~"), ".1822direkt")
+if not os.path.isdir(APP_DIR):
+  os.mkdir(APP_DIR)
+
+PICKLEPATH = os.path.join(APP_DIR, "transactions.pkl")
+
+class Gui:
+
+  def __init__(self):
+    pynotify.init(APP_NAME)
+    
+    self._create_gui()
+
+    self._uid = None
+    self._keyring = KeyringInterface()
+    self._comm = comm.BankInterface(self)
+    self._refresher = RefresherThread(self._comm)
+
+    self._init_stored()
+
+    if self._keyring.has_credentials():
+      account, pin = self._keyring.get_credentials()
+      self._comm.login(account, pin)
+
+  def _init_stored(self):
+    if os.path.isfile(PICKLEPATH):
+      store = open(PICKLEPATH, 'rb')
+      self._accounts, self._transactions = pickle.load(store)
+      store.close()
+    else:
+      self._accounts = []
+      self._transactions = {}
+
+  def connection_ready(self):
+    if not self._refresher.isAlive():
+      self._refresher.start()
+
+  def connection_fail(self):
+    pass
+
+  def got_accounts(self, accounts):
+    balance_changed = False
+    for aa in accounts:
+      for ab in self._accounts:
+        if aa["number"] == ab["number"] and \
+            aa["balance"] != ab["balance"]:
+              balance_changed = True
+              self.tray.set_blinking(True)
+
+    self._accounts = accounts
+    if (not self._transactions) or balance_changed:
+       self._comm.get_transactions(accounts)
+
+    self._refresh_table()
+
+
+  def got_transactions(self, account, transactions):
+    self._transactions[account["number"]] = transactions
+    self._refresh_table()
+
+    if os.path.isfile(PICKLEPATH):
+      os.remove(PICKLEPATH)
+    store = open(PICKLEPATH, 'wb')
+    pickle.dump((self._accounts, self._transactions), store, -1)
+    store.close()
+
+  def _create_right_menu(self):
+    self._rmenu = gtk.Menu()
+    about = gtk.ImageMenuItem(stock_id=gtk.STOCK_ABOUT)
+    about.connect("activate", self._on_about_clicked)
+    quit = gtk.ImageMenuItem(stock_id=gtk.STOCK_QUIT)
+    quit.connect("activate", self.exit)
+
+    self._accountbtn = gtk.ImageMenuItem(stock_id=gtk.STOCK_DIALOG_AUTHENTICATION)
+    self._accountbtn.get_children()[0].set_text("Enter Account Details")
+    self._accountbtn.connect("activate", self._on_edit_account_clicked)
+    self._accountbtn.set_sensitive(True)
+
+    self._rmenu.add(self._accountbtn)
+    self._rmenu.add(about)
+    self._rmenu.add(quit)
+    self._rmenu.show_all()
+
+  def _refresh_table(self):
+    def get_color(num):
+      return COLOR_RED if num.startswith("-") else COLOR_BLACK
+
+    self._liststore.clear()
+    for i, a in enumerate(self._accounts):
+      if i is not 0:
+        self._liststore.append(["", "", "", COLOR_BLACK])
+
+      row = [a["number"], a["name"], a["balance"]]
+      row.append(get_color(a["balance"]))
+      self._liststore.append(row)
+
+      if self._transactions.has_key(a["number"]):
+        for j, t in enumerate(self._transactions[a["number"]]):
+          if j > 5:
+            break
+          row = [t["date"], t["person"], t["ammount"]]
+          row.append(get_color(t["ammount"]))
+          self._liststore.append(row)
+
+  def _create_accounts_table(self):
+    self._main_window = gtk.Window()
+    self._main_window.set_decorated(False)
+    self._main_window.connect("focus-out-event", self._on_deactivate)
+
+    self._liststore = gtk.ListStore(str, str, str, str)
+
+    treeview = gtk.TreeView(self._liststore)
+    treeview.set_headers_visible(False)
+
+    acc_number_cell = gtk.CellRendererText()
+    acc_number_col = gtk.TreeViewColumn()
+    acc_number_col.pack_start(acc_number_cell, True)
+    acc_number_col.set_attributes(acc_number_cell, text=0)
+    treeview.append_column(acc_number_col)
+
+    acc_name_cell = gtk.CellRendererText()
+    acc_name_col = gtk.TreeViewColumn()
+    acc_name_col.pack_start(acc_name_cell, True)
+    acc_name_col.set_attributes(acc_name_cell, text=1)
+    treeview.append_column(acc_name_col)
+
+    acc_balance_cell = gtk.CellRendererText()
+    acc_balance_cell.set_property('xalign', 1.0)
+    acc_balance_cell.set_property('foreground-set' , True)
+    acc_balance_col = gtk.TreeViewColumn()
+    acc_balance_col.pack_start(acc_balance_cell, True)
+    acc_balance_col.set_attributes(acc_balance_cell, text=2, foreground=3)
+    treeview.append_column(acc_balance_col)
+ 
+    self._main_window.add(treeview)
+
+  def _create_gui(self):
+    icon_name = "1822direkt"
+
+    #load themed or fallback app icon
+    theme = gtk.icon_theme_get_default()
+    if not theme.has_icon(icon_name):
+      theme.prepend_search_path(ICONS_PATH)
+    if theme.has_icon(icon_name):
+      self._icon_name = icon_name
+    else:
+      self._icon_name = gtk.STOCK_NETWORK
+
+    #build tray icon
+    self.tray = gtk.StatusIcon()
+    self.tray.set_from_icon_name(self._icon_name)
+    self.tray.connect('popup-menu', self._on_popup_menu)
+    self.tray.connect('activate', self._on_activate)
+    self.tray.set_visible(True)
+    
+    #create popup menus
+    self._create_right_menu()
+    self._create_accounts_table()
+
+  def _set_tooltip(self, msg):
+    gobject.idle_add(self.tray.set_tooltip, msg)
+
+  def _on_popup_menu(self, status, button, time):
+    self._rmenu.popup(None, None, gtk.status_icon_position_menu, 
+        button, time, self.tray)
+
+  def _on_activate(self, *args):
+    self.tray.set_blinking(False)
+    if self._main_window.get_property("visible"):
+      self._main_window.hide()
+    else:
+      w, r, o = self.tray.get_geometry()
+      self._main_window.move(r.x, r.y)
+      self._main_window.grab_focus()
+      self._main_window.show_all()
+
+  def _on_deactivate(self, window, event):
+    self._main_window.hide()
+
+  def _on_about_clicked(self, widget):
+    #should probbably only do this once.
+    gtk.about_dialog_set_url_hook(
+            lambda dlg, url: self._sb.open_url(url)
+    )
+
+    dlg = gtk.AboutDialog()
+    dlg.set_name(APP_NAME)
+    dlg.set_comments(APP_DESCRIPTION)
+    dlg.set_copyright("License: %s" % APP_LICENSE)
+    dlg.set_version(APP_VERSION)
+    dlg.set_website(APP_HOMEPAGE)
+    dlg.set_authors(APP_AUTHORS)
+    dlg.set_logo_icon_name(self._icon_name)
+    dlg.run()
+    dlg.destroy()
+
+  def _on_edit_account_clicked(self, widget):
+    dlg = gtk.MessageDialog(
+        None,
+        gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
+        gtk.MESSAGE_QUESTION,
+        gtk.BUTTONS_OK,
+        None)
+    dlg.set_markup('Please enter your <b>account details</b>:')
+    
+    #create the text inputs
+    self._account_nr_entry = gtk.Entry()
+
+    accountbox = gtk.HBox()
+    accountbox.pack_start(gtk.Label("Number:"), False, 5, 5)
+    accountbox.pack_end(self._account_nr_entry)
+
+    dlg.vbox.pack_start(accountbox, True, True, 0)
+
+    self._pin_entry = gtk.Entry()
+    self._pin_entry.set_visibility(False)
+
+    pinbox = gtk.HBox()
+    pinbox.pack_start(gtk.Label("Pin:"), False, 5, 5)
+    pinbox.pack_end(self._pin_entry)
+
+    dlg.vbox.pack_start(pinbox, True, True, 0)
+
+    dlg.show_all()
+    dlg.run()
+    
+    account = self._account_nr_entry.get_text()
+    pin = self._pin_entry.get_text()
+
+    self._keyring.save_login(account, pin)
+    self._comm.login(account, pin)
+    dlg.destroy()
+
+  def exit(self, *args):
+    self._refresher.stop()
+    gtk.main_quit()
+
+
+class KeyringInterface:
+
+  KEYRING = gk.get_default_keyring_sync()
+
+  def has_credentials(self):
+    items_ids = gk.list_item_ids_sync(self.KEYRING)
+    for item_id in items_ids:
+      item_info = gk.item_get_info_sync(self.KEYRING, item_id)
+      name = item_info.get_display_name() 
+      if name.startswith("1822direkt"):
+        return True
+
+    return False
+
+  def get_credentials(self):
+    items_ids = gk.list_item_ids_sync(self.KEYRING)
+    for item_id in items_ids:
+      item_info = gk.item_get_info_sync(self.KEYRING, item_id)
+      name = item_info.get_display_name() 
+      if name.startswith("1822direkt"):
+        account = name.split(";")[-1]
+        pin = item_info.get_secret()
+        return (account, pin)
+
+    raise Exception("No credentials available")
+
+  def save_login(self, account, pin):
+    #clear entry if exists
+    items_ids = gk.list_item_ids_sync(self.KEYRING)
+    for item_id in items_ids:
+      item_info = gk.item_get_info_sync(self.KEYRING, item_id)
+      name = item_info.get_display_name() 
+      if name.startswith("1822direkt"):
+        gk.item_delete_sync(self.KEYRING, item_id)
+        break
+
+    name = "1822direkt;" + account
+    attrs = {"app": "1822direkt", "account" : account}
+    num_id = gk.item_create_sync(self.KEYRING, \
+        gk.ITEM_GENERIC_SECRET, name, attrs, pin, True)
+
+
+class RefresherThread(threading.Thread):
+
+  def __init__(self, comm):
+    super(RefresherThread, self).__init__()
+    self._stop = threading.Event()
+    self._comm = comm
+
+  def run(self):
+    last_refresh = 0
+    while True:
+      if self.stopped():
+        return
+
+      if (time.time() - last_refresh) > SECONDS_UPDATE_FREQ:
+        self._comm.get_accounts()
+        last_refresh = time.time()
+
+      time.sleep(0.5)
+
+  def stop(self):
+    self._stop.set()
+
+  def stopped(self):
+    return self._stop.isSet()
+
+
+if __name__ == "__main__":
+    gtk.gdk.threads_init()
+    app = Gui()
+    gtk.main()
+

lib1822direkt/ui.pyc

Binary file added.