Class: FirstDirect

Inherits:
Object
  • Object
show all
Includes:
Helpers
Defined in:
lib/firstdirect.rb

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)
 = fd.('1st account')

Each Account has a name, number, and balance.

puts 
#=> 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 .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:
.recent_statement_array

An Account can also download statements in CSV format:

.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:
.download(:from => '2008-01-01')

# ...or between two dates:
.download(:from => '2008-01-01', :to => '2008-01-07')

# ...or in any other format offered by First Direct:
.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

Instance Method Summary collapse

Methods included from Helpers

#colour, #strip_js, #trace

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'

  
  initialize_accounts
end

Instance Attribute Details

#accountsObject (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

#agentObject (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 (name)
  if a = @accounts.select{ |a| a.name == name }.first
    return a
  else
    raise Error, "no account found with name '#{a}'"
  end
end

#initialize_accountsObject

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..>&nbsp;Balance</th>
    <td..>&nbsp;</td>
  </tr>
  </thead>
  <tbody>
    <tr>
      <td> <a..>1st Account</a><br> <a..>11-11-11&nbsp;11111111</a> </td>
      <td> £100.00<br> <a..>my limit</a> </td>
      <td> <strong>&nbsp;£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('&nbsp;', ' ').strip.gsub('&pound;', '£')
    end
    b.link = Helpers.strip_js( row.at('td[1]/a[1]') )
  end
end

#log_inObject

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 
  # 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