Permalink
Browse files

first commit

  • Loading branch information...
0 parents commit c64dc41690969f268667887fff58ea4f0a58ed5c @pauldowman committed Jul 27, 2008
Showing with 377 additions and 0 deletions.
  1. +17 −0 README.textile
  2. +32 −0 accounts/cibc_visa.rb
  3. +59 −0 accounts/ing_canada.rb
  4. +32 −0 accounts/pc_financial.rb
  5. +9 −0 config.rb.example
  6. +41 −0 gmoney.rb
  7. +75 −0 lib/account_base.rb
  8. +5 −0 lib/bank_account.rb
  9. +5 −0 lib/credit_card_account.rb
  10. +94 −0 lib/spreadsheet.rb
  11. +8 −0 todo.txt
17 README.textile
@@ -0,0 +1,17 @@
+h2. GMoney
+
+h3. Automatically download your bank transactions into a Google Spreadsheet
+
+http://github.com/pauldowman/gmoney
+
+GMoney automatically downloads your bank account transactions and inserts them into a Google spreadsheet.
+Transactions from each account go into a different sheet, and you can write additional sheets to generate summaries,
+spending reports and charts.
+
+GMoney requires the "mechanize":http://mechanize.rubyforge.org/mechanize/ and "ofx-parser":http://ofx-parser.rubyforge.org/ gems.
+
+You'll need to roll up your sleeves and write a bit of code though, you'll probably need to write a few lines of code to
+script loggin in to your bank and clicking the "download transactions" link, but it's not that hard, see accounts/pc_financial.rb
+for an example. If you do that please send me a patch or pull request and I'll add your bank.
+
+Your account details (login, account id, password, etc) go into $HOME/.gmoney/config.rb
32 accounts/cibc_visa.rb
@@ -0,0 +1,32 @@
+class CibcVisa < CreditCardAccount
+ def name
+ "CIBC VISA"
+ end
+
+ def download_data
+ # return File.read("#{ENV['HOME']}/Desktop/cibcvisa.qfx")
+
+ agent.keep_alive = false
+
+ page = agent.get "https://www.cibconline.cibc.com/olbtxn/authentication/PreSignOn.cibc?locale=en_CA"
+ form = page.form("signonForm")
+ form.newCardNumber = config[:userid]
+ form.pswPassword = config[:password]
+ form.securityUID = get_cookie("securityUID")
+ form.isPersistentCookieDisabled = "1"
+
+ page = form.submit
+
+ page = agent.get "https://www.cibconline.cibc.com/olbtxn/accounts/TransactionDownload1.cibc"
+ form = page.form("transactionDownloadForm")
+ page = form.submit
+
+ data = page.body
+
+ if data =~ /There are no transactions available to download for the account and the date range you have selected/
+ return ""
+ else
+ return data
+ end
+ end
+end
59 accounts/ing_canada.rb
@@ -0,0 +1,59 @@
+class IngCanada < BankAccount
+ def name
+ "ING account #{config[:account_id]}"
+ end
+
+ def download_data
+ # return File.read("#{ENV['HOME']}/Desktop/ing.ofx")
+
+ page = agent.get("https://secure.ingdirect.ca/InitialINGDirect.html?command=displayLogin&device=web&locale=en_CA")
+ form = page.form("Signin")
+ form.ACN = config[:userid]
+
+ # c = WEBrick::Cookie.new("Name", "ING%20DIRECT")
+ # c.domain = "secure.ingdirect.ca"
+ # c.path = "/"
+ # agent.cookie_jar.add(URI.parse("https://secure.ingdirect.ca/"), c)
+
+ form.submit
+
+ page = agent.get("https://secure.ingdirect.ca/INGDirect.html?command=displayChallengeQuestion")
+
+ form = page.form("ChallengeQuestion")
+ if page.body =~ /On what street did you grow up?/
+ form.Answer = config[:street]
+ elsif page.body =~ /What colour was your first car?/
+ form.Answer = config[:car]
+ elsif page.body =~ /What is your favourite colour?/
+ form.Answer = config[:colour]
+ end
+
+ form.submit
+ page = agent.get("https://secure.ingdirect.ca/INGDirect.html?command=displayPINPad")
+
+ form = page.form("Signin")
+ form.PIN = config[:password]
+
+ form.submit
+ page = agent.get("https://secure.ingdirect.ca/INGDirect.html?command=displayAccountSummary")
+
+ page = agent.get("https://secure.ingdirect.ca/INGDirect.html?command=displayInitialDownLoadTransactionsCommand")
+ form = page.form("MainForm")
+ form.ACCT = config[:account_id]
+ form.DOWNLOADTYPE = "OFX"
+ submit_button = nil
+ form.buttons.each {|b| submit_button = b if b.name = "YES, I WANT TO CONTINUE." }
+
+ agent.submit(form, submit_button)
+ page = agent.get("https://secure.ingdirect.ca/INGDirect.html?command=displayDownLoadTransactionsCommand&fill=1")
+ page = agent.click(page.links.text("DownLoad"))
+
+ data = page.body
+
+ if data =~ /<STMTTRN>/
+ return data
+ else
+ return ""
+ end
+ end
+end
32 accounts/pc_financial.rb
@@ -0,0 +1,32 @@
+class PcFinancial < BankAccount
+ def name
+ "PC account #{config[:account_id]}"
+ end
+
+ def download_data
+ # return File.read("#{ENV['HOME']}/Desktop/PCF.qfx")
+
+ page = agent.get("https://www.txn.banking.pcfinancial.ca/a/authentication/preSignOn.ams")
+ form = page.form("SignOnForm")
+ form.cardNumber = config[:userid]
+ form.password = config[:password]
+
+ page = agent.submit(form, form.buttons.first)
+
+ page = agent.get("https://www.txn.banking.pcfinancial.ca/a/banking/accounts/downloadTransactions1.ams")
+ form = page.form("DownloadTransactionsForm")
+ form.fields.name('fromAccount').value = config[:account_id]
+ submit_button = nil
+ form.buttons.each {|b| submit_button = b if b.value = "Download transactions" }
+
+ page = agent.submit(form, submit_button)
+
+ data = page.body
+
+ if data =~ /There are no transactions found that met your request/
+ return ""
+ else
+ return data
+ end
+ end
+end
9 config.rb.example
@@ -0,0 +1,9 @@
+@gdata_user = 'you@gmail.com'
+@gdata_pass = 'secret'
+@gs_key = 'abc123'
+
+@accounts = [
+ PcFinancial.new(:worksheet => "1", :userid => "userid", :password => "password", :account_id => "account_id"),
+ CibcVisa.new(:worksheet => "2", :userid => "userid", :password => "password"),
+ IngCanada.new(:worksheet => "3", :userid => "userid", :password => "password", :street => "mystreet", :colour => "purple", :car => "red", :account_id => "account_id")
+]
41 gmoney.rb
@@ -0,0 +1,41 @@
+#!/usr/bin/env ruby
+
+require "#{File.dirname(__FILE__)}/lib/account_base"
+require "#{File.dirname(__FILE__)}/lib/bank_account"
+require "#{File.dirname(__FILE__)}/lib/credit_card_account"
+require "#{File.dirname(__FILE__)}/lib/spreadsheet"
+
+Dir["#{File.dirname(__FILE__)}/accounts/*"].each do |file|
+ require file
+end
+
+require "#{ENV['HOME']}/.gmoney/config.rb"
+
+spreadsheet = Spreadsheet.new(@gs_key, @gdata_user, @gdata_pass)
+
+@accounts.each do |account|
+ begin
+ worksheet = account.config[:worksheet]
+ puts "Getting data from #{account.name}..."
+ txns = account.txns
+ puts "Inserting into spreadsheet..." if account.txns.any?
+ txns.each do |txn|
+ retried = false
+ begin
+ spreadsheet.add_row(worksheet, txn)
+ rescue Exception => e
+ if retried
+ puts "error: #{e.inspect}"
+ else
+ retried = true
+ puts "error: #{e.inspect}, retrying..."
+ retry
+ end
+ end
+ print "."
+ end
+ puts ""
+ rescue Exception => e
+ puts "ERROR: \n#{e.inspect}\n#{e.backtrace.join("\n")}"
+ end
+end
75 lib/account_base.rb
@@ -0,0 +1,75 @@
+require "rubygems"
+require "mechanize"
+require "ofx-parser"
+
+class AccountBase
+ attr :config, true
+
+ def initialize(config)
+ self.config = config
+ end
+
+ def get_data
+ return if @data
+
+ @data = download_data
+
+ if @data == ""
+ puts "No new transactions."
+ else
+ puts "downloaded data:"
+ puts "#{@data}"
+ end
+
+ return if @data == ""
+
+ @data.gsub!(/\r\n/, "\n")
+ @data.gsub!(/:$/, ": ")
+ @data.gsub!(/&amp;/, "")
+
+ @ofx = OfxParser::OfxParser.parse(@data)
+ account = ofx_account(@ofx)
+
+ if account && account.statement
+ @ofx_txns = account.statement.transactions
+ @balance = account.balance
+ end
+ end
+
+ def txns
+ get_data
+ return [] unless @ofx_txns
+
+ txns = @ofx_txns.collect do |t|
+ {
+ (t.amount.to_f >= 0 ? "deposit" : "withdrawal") => t.amount,
+ "chequenumber" => t.check_number,
+ "date" => t.date.strftime("%Y-%m-%d"),
+ "transactionid" => t.fit_id,
+ "memo" => t.memo,
+ "payee" => t.payee,
+ "siccode" => t.sic,
+ "type" => t.type
+ }
+ end
+ if txns.size > 0
+ txns.last["balance"] = @balance
+ end
+ txns
+ end
+
+ def get_cookie(name)
+ agent.cookies.each do |c|
+ if c.to_s =~ /#{name}=(.*)/
+ return $1
+ end
+ end
+ return nil
+ end
+
+ def agent
+ @agent ||= WWW::Mechanize.new
+ @agent.user_agent_alias = 'Mac FireFox'
+ @agent
+ end
+end
5 lib/bank_account.rb
@@ -0,0 +1,5 @@
+class BankAccount < AccountBase
+ def ofx_account(ofx)
+ ofx.bank_account
+ end
+end
5 lib/credit_card_account.rb
@@ -0,0 +1,5 @@
+class CreditCardAccount < AccountBase
+ def ofx_account(ofx)
+ ofx.credit_card
+ end
+end
94 lib/spreadsheet.rb
@@ -0,0 +1,94 @@
+# Adapted from http://rubyforge.org/projects/gdata-ruby
+
+require "rubygems"
+require "net/http"
+
+module Net
+ class HTTPS < HTTP
+ def initialize(address, port = nil)
+ super(address, port)
+ self.use_ssl = true
+ end
+ end
+end
+
+class Spreadsheet
+ GOOGLE_LOGIN_URL = URI.parse('https://www.google.com/accounts/ClientLogin')
+
+ def initialize(spreadsheet_id, email, password)
+ $VERBOSE = nil
+
+ @spreadsheet_id = spreadsheet_id
+ service = 'wise'
+ source = 'gdata-ruby'
+ @url = 'spreadsheets.google.com'
+
+ response = Net::HTTPS.post_form(GOOGLE_LOGIN_URL,
+ {'Email' => email,
+ 'Passwd' => password,
+ 'source' => source,
+ 'service' => service })
+
+ response.error! unless response.kind_of? Net::HTTPSuccess
+
+ @headers = {
+ 'Authorization' => "GoogleLogin auth=#{response.body.split(/=/).last}",
+ 'Content-Type' => 'application/atom+xml'
+ }
+ end
+
+ def visibility
+ @headers ? 'private' : 'public'
+ end
+
+ # def set_worksheet(name)
+ # path = "/feeds/worksheets/#{@spreadsheet_id}/#{visibility}/full"
+ # doc = Hpricot(get(path))
+ # result = (doc/"entry/link[@rel='self'][href]")
+ # pp result
+ # end
+
+ def request(path)
+ response, data = get(path)
+ data
+ end
+
+ def get(path)
+ response, data = http.get(path, @headers)
+ raise "error: #{response.inspect}, #{response.body}" unless response.kind_of? Net::HTTPSuccess
+ data
+ end
+
+ def post(path, entry)
+ response = http.post(path, entry, @headers)
+ raise "error: #{response.inspect}, #{response.body}" unless response.kind_of? Net::HTTPSuccess
+ response
+ end
+
+ def put(path, entry)
+ h = @headers
+ h['X-HTTP-Method-Override'] = 'PUT' # just to be nice, add the method override
+ response = http.put(path, entry, h)
+ raise "error: #{response.inspect}, #{response.body}" unless response.kind_of? Net::HTTPSuccess
+ response
+ end
+
+ def http
+ conn = Net::HTTP.new(@url, 80)
+ #conn.set_debug_output $stderr
+ conn
+ end
+
+ def add_row(worksheet, hash)
+ path = "/feeds/list/#{@spreadsheet_id}/#{worksheet}/#{visibility}/full"
+
+ entry = "<?xml version='1.0' ?><entry xmlns='http://www.w3.org/2005/Atom' xmlns:gsx='http://schemas.google.com/spreadsheets/2006/extended'>"
+ hash.each_pair do |k,v|
+ entry += "<gsx:#{k}>#{v}</gsx:#{k}>"
+ end
+ entry += "</entry>"
+
+ post(path, entry)
+ end
+
+end
8 todo.txt
@@ -0,0 +1,8 @@
+Provide a template or automatic way to set up the Google spreadsheet with spending reports and calculations.
+
+use gdata token?
+
+store downloaded data in case of exception
+
+easily allow input from file instead of download
+- maybe if a specially-named file exists use it?

0 comments on commit c64dc41

Please sign in to comment.