Skip to content

Commit

Permalink
first commit
Browse files Browse the repository at this point in the history
  • Loading branch information
pauldowman committed Jul 27, 2008
0 parents commit c64dc41
Show file tree
Hide file tree
Showing 11 changed files with 377 additions and 0 deletions.
17 changes: 17 additions & 0 deletions 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 changes: 32 additions & 0 deletions 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 changes: 59 additions & 0 deletions 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 changes: 32 additions & 0 deletions 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 changes: 9 additions & 0 deletions 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 changes: 41 additions & 0 deletions 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 changes: 75 additions & 0 deletions 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 changes: 5 additions & 0 deletions lib/bank_account.rb
@@ -0,0 +1,5 @@
class BankAccount < AccountBase
def ofx_account(ofx)
ofx.bank_account
end
end
5 changes: 5 additions & 0 deletions lib/credit_card_account.rb
@@ -0,0 +1,5 @@
class CreditCardAccount < AccountBase
def ofx_account(ofx)
ofx.credit_card
end
end
94 changes: 94 additions & 0 deletions 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 changes: 8 additions & 0 deletions 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.