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]\n password: notaplonker\n memorable_answer:\n   \"Favourite lager?\": 'Castle lager'\n   \"Uncle's first name?\": 'Albert'\n"

fd = FirstDirect.new(secrets)
 = fd.('1st account')

Each Account has a name, number, and balance.

puts 
#=> 1st Account (10-20-30 10000000): £100.00

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

Defined Under Namespace

Modules: Helpers Classes: Account, Error

Constant Summary collapse

VERSION =
'0.1'
@@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: rodney@nelsonmandelahouse.org.uk
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.



145
146
147
148
149
150
151
152
# File 'lib/firstdirect.rb', line 145

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).



158
159
160
# File 'lib/firstdirect.rb', line 158

def accounts
  @accounts
end

#agentObject (readonly)

The WWW::Mechanize object we are using.



155
156
157
# File 'lib/firstdirect.rb', line 155

def agent
  @agent
end

Instance Method Details

#account(name) ⇒ Object

Fetch the first account with the given name.



367
368
369
370
371
372
373
# File 'lib/firstdirect.rb', line 367

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> 


347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
# File 'lib/firstdirect.rb', line 347

def initialize_accounts
  @accounts = []
  @agent.current_page.search('//table[@class="fdBalancesTable"]/tbody/tr').each do |row|
    @accounts << b = .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|
      require 'cgi'
      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.



163
164
165
166
167
168
169
170
171
172
173
174
175
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
# File 'lib/firstdirect.rb', line 163

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