Class: FirstDirect
Overview
firstdirect.rb – internet banking with First Direct using Ruby
A FirstDirect object will log into First Direct’s website and create some Account objects for each account listed on the first page after login.
require 'firstdirect'
require 'yaml'
secrets = YAML.load <<'...'
userid: [email protected]
password: notaplonker
memorable_answer:
"Favourite lager?": 'Castle lager'
"Uncle's first name?": 'Albert'
...
fd = FirstDirect.new(secrets)
account = fd.account('1st account')
Each Account has a name, number, and balance.
puts account
#=> 1st Account (10-20-30 10000000): £100.00
You can fetch an Account’s recent activity (as parsed from the HTML of the account page on the First Direct website) as a CSV string or an Array:
puts account.recent_statement_csv
#=> date,details,paid out,paid in,balance,?
# 08/07/2008,TROTTER R *FRS REGULAR SAVER,100.00,"",100.00,""
# Get an array instead:
account.recent_statement_array
An Account can also download statements in CSV format:
account.download
#=> Date,Description,Amount,Balance
# 03/07/2008,"FOOBAR LTD",-600.00,350.21
# 02/07/2008,"CASH MACHINE @12:34",-50.00,950.21
# 01/07/2008,"WHERE I WORK LTD",1000.21,1000.21
# ...or from a particular date, up to yesterday:
account.download(:from => '2008-01-01')
# ...or between two dates:
account.download(:from => '2008-01-01', :to => '2008-01-07')
# ...or in any other format offered by First Direct:
account.download(:to => '2008-01-07', :format => 'Quicken 97')
# => !Type:Bank
# D06-09-08
# PREGULAR SAVER TROTTER R
# T100.00
Copyright © Edward Speyer, 2008
Defined Under Namespace
Modules: Helpers Classes: Account, Error
Constant Summary collapse
- VERSION =
'0.2'- @@start_url =
The Ruby WWW::Mechanize browser doesn’t have javascript, but we have to go through lots of Javascript loops to get to the login page :(
The javascript on firstdirect.com/ takes us (somewhere like) here:
http://firstdirect.com/certificate.html?/trends/intbanking_refer.html?12...........But then even more javascript causes us to end up at the following URL, which eventually 301 and 302 redirects us to a page with a username_form, so it actually appears to be OK to start at this URL instead:
'https://www.banking.first-direct.com/1/2/idv.Logoff?nextPage=fsdtBalances'
Instance Attribute Summary collapse
-
#accounts ⇒ Object
readonly
The Account objects we found after log_in (see initialize_accounts).
-
#agent ⇒ Object
readonly
The WWW::Mechanize object we are using.
Instance Method Summary collapse
-
#account(name) ⇒ Object
Fetch the first account with the given name.
-
#initialize(secrets) ⇒ FirstDirect
constructor
secretsshould be a Hash of the form:. -
#initialize_accounts ⇒ Object
Fetch all the accounts from the main page.
-
#log_in ⇒ Object
Log in as far as the main accounts page.
Methods included from Helpers
Constructor Details
#initialize(secrets) ⇒ FirstDirect
secrets should be a Hash of the form:
userid: [email protected]
password: notaplonker
memorable_answer:
"Favourite lager?": 'Castle lager'
"Uncle's first name?": 'Albert'
The keys to memorable_answer should be exactly in the form that they appear on the login form.
I keep mine in a file called ‘secrets’ and load it with YAML.load_file.
158 159 160 161 162 163 164 165 |
# File 'lib/firstdirect.rb', line 158 def initialize(secrets) @secrets = secrets @agent = WWW::Mechanize.new @agent.user_agent_alias = 'Mac Safari' log_in initialize_accounts end |
Instance Attribute Details
#accounts ⇒ Object (readonly)
The Account objects we found after log_in (see initialize_accounts).
171 172 173 |
# File 'lib/firstdirect.rb', line 171 def accounts @accounts end |
#agent ⇒ Object (readonly)
The WWW::Mechanize object we are using.
168 169 170 |
# File 'lib/firstdirect.rb', line 168 def agent @agent end |
Instance Method Details
#account(name) ⇒ Object
Fetch the first account with the given name.
426 427 428 429 430 431 432 |
# File 'lib/firstdirect.rb', line 426 def account(name) if a = @accounts.select{ |a| a.name == name }.first return a else raise Error, "no account found with name '#{a}'" end end |
#initialize_accounts ⇒ Object
Fetch all the accounts from the main page. We assume that we are on the main page already (so this method should only be called from FirstDirect.initialize).
– Balances look a bit like this:
<table class="fdBalancesTable"..>
<colgroup>
<col..
</colgroup>
<thead>
<tr>
<th..>Account details</th>
<th..>Limit</th>
<th..> Balance</th>
<td..> </td>
</tr>
</thead>
<tbody>
<tr>
<td> <a..>1st Account</a><br> <a..>11-11-11 11111111</a> </td>
<td> £100.00<br> <a..>my limit</a> </td>
<td> <strong> £100.00</strong> </td>
<td> <a..>make payment</a><br> <a title="upgrade your account..> </td>
</tr>
407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 |
# File 'lib/firstdirect.rb', line 407 def initialize_accounts @accounts = [] @agent.current_page.search('//table[@class="fdBalancesTable"]/tbody/tr').each do |row| @accounts << b = Account.new b.parent = self b.name, b.number, b.balance = [ row.at('td[1]/a[1]'), row.at('td[1]/a[2]'), row.at('td[3]/strong') ].map do |v| v.inner_html.gsub(' ', ' ').strip.gsub('£', '£') end b.link = Helpers.strip_js( row.at('td[1]/a[1]') ) end end |
#log_in ⇒ Object
Log in as far as the main accounts page.
176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 |
# File 'lib/firstdirect.rb', line 176 def log_in # Going to the start URL gets a page where we can tell FirstDirect who we # are: username_page = trace('getting start_url'){ @agent.get @@start_url } username_form = username_page.forms.first username_form['userid'] = @secrets['userid'] password_page = trace('submitting username form'){ @agent.submit(username_form) } # Now we'll be asked a question about our password letters. # # <label for="password" class="light">Please enter the # <strong>1st</strong>, <strong>4th</strong> and <strong>5th</strong> # characters from your <strong>electronic password</strong>, and the # answer to your question.</label> # secrets_form = password_page.forms.first begin letter_requests = password_page.search("//label[@for='password']/strong") letters = letter_requests[0..2].map do |elem| # Only the first three <strong>s refer to letters. as_word = elem.inner_html index = case as_word when /^(\d+)(st|nd|rd|th)/ $1.to_i - 1 when 'last' -1 when 'penultimate' -2 else raise Error, "unknown letter index: #{as_word}" end @secrets['password'].split(//)[index] end.join secrets_form['password'] = letters end # Also, we need to answer the 'memorableAnswer' thingy: # # <label for="memorableAnswer"><strong>Favourite colour?</strong></label> # begin question = password_page.at("//label[@for='memorableAnswer']/strong").inner_html answer = @secrets['memorable_answer'][question] raise Error, "no known answer to question: #{question}" if answer.nil? or answer.empty? secrets_form['memorableAnswer'] = answer end trace('logging in') { @agent.submit(secrets_form) } end |