From a3920b6509fd5e2fcbc5f00a9723a05ea6bd27e2 Mon Sep 17 00:00:00 2001 From: Steve Lacey Date: Wed, 14 Nov 2012 23:59:44 +0000 Subject: [PATCH 1/7] Require YAML. --- bin/nw | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/bin/nw b/bin/nw index f45a2c3..e42c602 100755 --- a/bin/nw +++ b/bin/nw @@ -2,10 +2,11 @@ $LOAD_PATH.unshift File.join(File.dirname(__FILE__), '..', 'lib') require 'natwest' require 'highline/import' +require 'yaml' CONFIG = File.expand_path("~/.natwest.yaml") -if File.exists?(CONFIG) +if File.exists?(CONFIG) if File.world_readable?(CONFIG) or not File.owned?(CONFIG) mode = File.stat(CONFIG).mode.to_s(8) $stderr.puts "#{CONFIG}: Insecure permissions: #{mode}" @@ -21,7 +22,7 @@ credentials = YAML.load(File.read(CONFIG)) rescue {} $stderr.puts "Can't prompt for credentials; STDIN or STDOUT is not a TTY" exit(1) end - credentials[key] = ask("Please enter your #{credential}:") do |q| + credentials[key] = ask("Please enter your #{credential}:") do |q| q.echo = false end end From ab17fc3fde11f804fcf032d5dc42174f3d10e32b Mon Sep 17 00:00:00 2001 From: Steve Lacey Date: Thu, 15 Nov 2012 00:00:43 +0000 Subject: [PATCH 2/7] Fixed field pin + password field names and don't verify SSL. --- lib/natwest.rb | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/lib/natwest.rb b/lib/natwest.rb index 601fbea..8a2ae89 100644 --- a/lib/natwest.rb +++ b/lib/natwest.rb @@ -9,7 +9,7 @@ def assert(condition, message) module Natwest URL = 'https://nwolb.com/' - + module Login attr_reader :ua, :pin attr_accessor :password, :pin, :customer_number @@ -31,24 +31,24 @@ def enter_customer_number login_form = ua.get(URL).frames.first.click.forms.first login_form['ctl00$mainContent$LI5TABA$DBID_edit'] = customer_number self.page = login_form.submit - assert(page.title.include?('PIN and Password details'), + assert(page.title.include?('PIN and Password details'), "Got '#{page.title}' instead of PIN/Password prompt") end def enter_pin_and_password expected = expected('PIN','number') + expected('Password','character') self.page = page.forms.first.tap do |form| - ('A'..'F').map do |letter| - "ctl00$mainContent$LI6PPE#{letter}_edit" + ('A'..'F').map do |letter| + "ctl00$mainContent$Tab1$LI6PPE#{letter}_edit" end.zip(expected).each {|field, value| form[field] = value} end.submit - assert(page.title.include?('Last log in confirmation'), + assert(page.title.include?('Last log in confirmation'), "Got '#{page.title}' instead of last login confirmation") end def confirm_last_login self.page = page.forms.first.submit - assert(page.title.include?('Accounts summary'), + assert(page.title.include?('Accounts summary'), "Got '#{page.title}' instead of accounts summary") end @@ -56,7 +56,7 @@ def expected(credential, type) page.body. scan(/Enter the (\d+)[a-z]{2} #{type}/). flatten.map{|i| i.to_i - 1}.tap do |indices| - assert(indices.uniq.size == 3, + assert(indices.uniq.size == 3, "Unexpected #{credential} characters requested") characters = [*send(credential.downcase.to_sym).to_s.chars] indices.map! {|i| characters[i]} @@ -70,7 +70,10 @@ class Customer attr_accessor :page def initialize - @ua = Mechanize.new {|ua| ua.user_agent_alias = 'Windows IE 7'} + @ua = Mechanize.new + + ua.user_agent_alias = 'Windows IE 7' + ua.verify_mode = 0 end def accounts @@ -81,7 +84,7 @@ def accounts acc.sort_code = meta.at('span.SortCode').inner_text.gsub(/[^\d-]/,'') acc.balance = meta.css('td')[-2].inner_text acc.available = meta.css('td')[-1].inner_text - acc.transactions = + acc.transactions = statement.css('table.InnerAccountTable > tbody > tr').map do |tr| transaction = Hash[[:date, :details, :credit, :debit]. zip((cells = tr.css('td')).map(&:inner_text))] @@ -95,7 +98,7 @@ def accounts end end - class Account + class Account attr_accessor :name, :number, :sort_code, :balance, :available, :transactions end end From 6df916372133d1c071087fcf70c247164a76bff9 Mon Sep 17 00:00:00 2001 From: Andrew Tumelty Date: Mon, 20 Oct 2014 20:39:35 +0100 Subject: [PATCH 3/7] Tweaks to fix with new NatWest flow: page renames and removal of last login confirmation --- bin/nw | 2 +- lib/natwest.rb | 12 +++--------- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/bin/nw b/bin/nw index e42c602..1c10ce1 100755 --- a/bin/nw +++ b/bin/nw @@ -23,7 +23,7 @@ credentials = YAML.load(File.read(CONFIG)) rescue {} exit(1) end credentials[key] = ask("Please enter your #{credential}:") do |q| - q.echo = false + q.echo = "*" end end diff --git a/lib/natwest.rb b/lib/natwest.rb index 8a2ae89..643c305 100644 --- a/lib/natwest.rb +++ b/lib/natwest.rb @@ -22,7 +22,7 @@ def login(credentials) credentials.each_pair{|name, value| send("#{name}=".to_sym, value)} enter_customer_number enter_pin_and_password - confirm_last_login + #confirm_last_login @logged_in = true end @@ -31,7 +31,7 @@ def enter_customer_number login_form = ua.get(URL).frames.first.click.forms.first login_form['ctl00$mainContent$LI5TABA$DBID_edit'] = customer_number self.page = login_form.submit - assert(page.title.include?('PIN and Password details'), + assert(page.title.include?('PIN and password details'), "Got '#{page.title}' instead of PIN/Password prompt") end @@ -42,13 +42,7 @@ def enter_pin_and_password "ctl00$mainContent$Tab1$LI6PPE#{letter}_edit" end.zip(expected).each {|field, value| form[field] = value} end.submit - assert(page.title.include?('Last log in confirmation'), - "Got '#{page.title}' instead of last login confirmation") - end - - def confirm_last_login - self.page = page.forms.first.submit - assert(page.title.include?('Accounts summary'), + assert(page.title.include?('Account summary'), "Got '#{page.title}' instead of accounts summary") end From e95ce732c51b0a0a31f82a631969040efe08661e Mon Sep 17 00:00:00 2001 From: Andrew Tumelty Date: Mon, 20 Oct 2014 20:43:24 +0100 Subject: [PATCH 4/7] Remove obsolete confirm_last_login call --- lib/natwest.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/natwest.rb b/lib/natwest.rb index 643c305..23c78e0 100644 --- a/lib/natwest.rb +++ b/lib/natwest.rb @@ -22,7 +22,6 @@ def login(credentials) credentials.each_pair{|name, value| send("#{name}=".to_sym, value)} enter_customer_number enter_pin_and_password - #confirm_last_login @logged_in = true end From c967e8aefff89ed8fcaf65a7f43f5a8604f93ded Mon Sep 17 00:00:00 2001 From: Andrew Tumelty Date: Sun, 26 Oct 2014 14:10:52 +0000 Subject: [PATCH 5/7] New transactions method to get transactions from an account between 2 dates --- bin/nw | 90 ++++++++++++++++++++++++++++++++++---------------- lib/natwest.rb | 75 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 136 insertions(+), 29 deletions(-) diff --git a/bin/nw b/bin/nw index 1c10ce1..9fe9840 100755 --- a/bin/nw +++ b/bin/nw @@ -4,41 +4,73 @@ require 'natwest' require 'highline/import' require 'yaml' -CONFIG = File.expand_path("~/.natwest.yaml") +def credential_load + config = File.expand_path("~/.natwest.yaml") -if File.exists?(CONFIG) - if File.world_readable?(CONFIG) or not File.owned?(CONFIG) - mode = File.stat(CONFIG).mode.to_s(8) - $stderr.puts "#{CONFIG}: Insecure permissions: #{mode}" + if File.exists?(config) + if File.world_readable?(config) or not File.owned?(config) + mode = File.stat(config).mode.to_s(8) + $stderr.puts "#{config}: Insecure permissions: #{mode}" + end end -end -credentials = YAML.load(File.read(CONFIG)) rescue {} + credentials = YAML.load(File.read(config)) rescue {} -['customer number', 'PIN', 'password'].each do |credential| - key = credential.tr(' ','_').downcase.to_sym - next if credentials.key?(key) - unless $stdin.tty? and $stdout.tty? - $stderr.puts "Can't prompt for credentials; STDIN or STDOUT is not a TTY" - exit(1) - end - credentials[key] = ask("Please enter your #{credential}:") do |q| - q.echo = "*" + ['customer number', 'PIN', 'password'].each do |credential| + key = credential.tr(' ','_').downcase.to_sym + next if credentials.key?(key) + unless $stdin.tty? and $stdout.tty? + $stderr.puts "Can't prompt for credentials; STDIN or STDOUT is not a TTY" + exit(1) + end + credentials[key] = ask("Please enter your #{credential}:") do |q| + q.echo = "*" + end end + + return credentials end -Natwest::Customer.new.tap do |nw| - nw.login credentials - nw.accounts.each do |acc| - puts '###' - puts "#{acc.name} [#{acc.number}; #{acc.sort_code}] " + - "balance: #{acc.balance}; available: #{acc.available}" - puts "\nRecent Transactions:" - acc.transactions.each do |trans| - amount = trans[:credit] ? "+#{trans[:credit]}" : "-#{trans[:debit]}" - puts "#{trans[:date]}: #{amount}" - puts "\t" + trans[:details] +action = ARGV[0] + +if action == "summary" + credentials = credential_load + Natwest::Customer.new.tap do |nw| + nw.login credentials + nw.accounts.each do |acc| + puts '###' + puts "#{acc.name} [#{acc.number}; #{acc.sort_code}] " + + "balance: #{acc.balance}; available: #{acc.available}" + puts "\nRecent Transactions:" + acc.transactions.each do |trans| + amount = trans[:credit] ? "+#{trans[:credit]}" : "-#{trans[:debit]}" + puts "#{trans[:date]}: #{amount}" + puts "\t" + trans[:details] + end + puts end - puts end -end +elsif action == "transactions" + credentials = credential_load + Natwest::Customer.new.tap do |nw| + nw.login credentials + nw.transactions(ARGV[1], ARGV[2], ARGV[3]) + end +else + puts "This is a rudimentary API for Natwest Online banking. + https://github.com/laycat/natwest + + Usage: + nw + + Commands: + summary + Displays an account summary and recent transactions + transactions + Gets transactions between two dates for an account. Dates are parsed by ruby + (so can be any format parsable by Date.parse), account is the 3 last digits + of the account or credit card number. + + For more information see the README: + https://github.com/laycat/natwest/blob/master/README.markdown" +end \ No newline at end of file diff --git a/lib/natwest.rb b/lib/natwest.rb index 23c78e0..46b31b7 100644 --- a/lib/natwest.rb +++ b/lib/natwest.rb @@ -1,5 +1,8 @@ # coding: utf-8 require 'mechanize' +require 'time' + +require 'awesome_print' module Kernel def assert(condition, message) @@ -67,6 +70,7 @@ def initialize ua.user_agent_alias = 'Windows IE 7' ua.verify_mode = 0 + ua.pluggable_parser.default = Mechanize::Download end def accounts @@ -89,6 +93,77 @@ def accounts end end end + + def transactions(start_date, end_date, account) + # TODO check end_date >= start_date? + start_date = Date.parse(start_date) + end_date = Date.parse(end_date) + + transactions = [] + + this_end_date = end_date + this_start_date = [end_date - 364, start_date].max + + while this_start_date <= this_end_date + self.page = page.link_with(text: 'Statements').click + assert(page.title.include?('Statements'), + "Got '#{page.title}' instead of Statements") + + form = page.form_with(action: 'StatementsLandingPageA.aspx') + button = form.button_with(value: 'Search transactions') + self.page = form.submit(button) + assert(page.title.include?('Transaction search - Select account and period'), + "Got '#{page.title}' instead of Transaction search") + + self.page = page.link_with(text: 'view transactions between two dates.').click + assert(page.title.include?('Transaction search - Select account and dates'), + "Got '#{page.title}' instead of Transaction search - Select account and dates") + + form = page.form_with(action: 'TransactionSearchSpecificDates.aspx') + form.field_with(name: 'ctl00$mainContent$TS2DEA_day').value = this_start_date.day + form.field_with(name: 'ctl00$mainContent$TS2DEA_month').value = this_start_date.month + form.field_with(name: 'ctl00$mainContent$TS2DEA_year').value = this_start_date.year + form.field_with(name: 'ctl00$mainContent$TS2DEB_day').value = this_end_date.day + form.field_with(name: 'ctl00$mainContent$TS2DEB_month').value = this_end_date.month + form.field_with(name: 'ctl00$mainContent$TS2DEB_year').value = this_end_date.year + form.field_with(name: 'ctl00$mainContent$TS2ACCDDA').option_with(text: /(.*?)#{account}/).select + self.page = form.click_button + assert(page.title.include?('Transaction search details'), + "Got '#{page.title}' instead of Transaction search details") + + search_form = page.form_with(action: 'TransactionSearchSpecificDates.aspx') + search_button = search_form.button_with(value: 'Search') + self.page = search_form.submit(search_button) + assert(page.title.include?('Transaction search results'), + "Got '#{page.title}' instead of Transaction search results") + + if !page.link_with(text: 'All').nil? + self.page = page.link_with(text: 'All').click + end + + transaction_table = page.search('table.ItemTable') + + transaction_header = transaction_table.search('th > a').map { |th| th.inner_text } + + transaction_table.search('tbody > tr').each do |tr| + values = tr.search('td').map{ |td| td.inner_text } + tr = Hash[transaction_header.zip values.map{|v| v == ' - ' ? '0' : v}] + transaction = {} + transaction[:date] = Date.parse(tr['Posting date'] || tr['Date']) + transaction[:description] = tr['Description'] + transaction[:amount] = tr['Paid in'].gsub(/,/,'').to_f - tr['Paid out'].gsub(/,/,'').to_f + + transactions << transaction + end + this_end_date = this_start_date - 1 + this_start_date = [this_end_date - 364, start_date].max + end + + #transactions.reverse! + return transactions + + end + end class Account From cc36a93c3437591dbd50242ff26af523130559ac Mon Sep 17 00:00:00 2001 From: Andrew Tumelty Date: Sun, 26 Oct 2014 14:41:20 +0000 Subject: [PATCH 6/7] Pretty transaction output and README update --- README.markdown | 45 ++++++++++++++++++++++++++++++++++----------- bin/nw | 7 ++++++- 2 files changed, 40 insertions(+), 12 deletions(-) diff --git a/README.markdown b/README.markdown index f586701..0e4b2ae 100644 --- a/README.markdown +++ b/README.markdown @@ -9,7 +9,8 @@ information, balance, and recent transactions. ## Usage - $ nw +Get a quick summary: + $ nw summary > Please enter your customer number: > Please enter your PIN: > Please enter your password: @@ -21,14 +22,35 @@ information, balance, and recent transactions. Cash Withdrawal (LLOYDS BANK 15JAN) 18 Jan 2010: -£20.00 Cash Withdrawal (LLOYDS BANK 17JAN) - 19 Jan 2010: -£45.00 - OnLine Transaction (CALL REF.NO. 1234 LOANSHARK FP 19/01/10 10) - 19 Jan 2010: -£49.99 - Debit Card Transaction (1234 18JAN10 EXAMPLE.COM 0800 123 4567 GB) - 20 Jan 2010: +£2,000.00 - Automated Credit - 20 Jan 2010: +£38.83 - Automated Credit + ... + +Get transactions for one account between 2 dates: + $ nw transactions 2013-08-01 2014-10-26 123 + Transactions for account ending 123, between 2013-08-01 and 2014-10-26 + Date Description Amount + 2014-10-21 4371 20OCT14 , NATIONAL LOTTERY , INTE , WATFORD GB -10.00 + 2014-10-20 4371 17OCT14 , KATZENJAMMERS , LONDON GB -10.30 + 2014-10-20 MOBILE PAYMENT , FROM 07123456789 50.00 + 2014-10-17 HSBC 17OCT -30.00 + ... + +Hooking into the ./lib/natwest.rb methods is very easy: the transactions method +for example returns an array of transactions, each one a hash of the date, description +and amount: + + [ + [ 0] { + :date => #, + :description => "4371 20OCT14 , NATIONAL LOTTERY , INTE , WATFORD GB", + :amount => -10.0 + }, + [ 1] { + :date => #, + :description => "4371 17OCT14 , KATZENJAMMERS , LONDON GB", + :amount => -10.3 + } + ] + ## Purpose @@ -77,5 +99,6 @@ bad idea. ## Bugs This utility relies on screen-scraping multiple pages of horrendous HTML. -Further, it has only been tested with one account. Feel free to report errors, -preferably with the HTML, appropriately sanitised, on which it fails. +Further, it has only been tested with one online account (with one current account +and one credit card). Feel free to report errors, preferably with the HTML, +appropriately sanitised, on which it fails. \ No newline at end of file diff --git a/bin/nw b/bin/nw index 9fe9840..31ce15d 100755 --- a/bin/nw +++ b/bin/nw @@ -54,7 +54,12 @@ elsif action == "transactions" credentials = credential_load Natwest::Customer.new.tap do |nw| nw.login credentials - nw.transactions(ARGV[1], ARGV[2], ARGV[3]) + transactions = nw.transactions(ARGV[1], ARGV[2], ARGV[3]) + puts "Transactions for account ending #{ARGV[3]}, between #{ARGV[1]} and #{ARGV[2]}" + puts "Date Description Amount" + transactions.each do |t| + puts "#{t[:date]} #{sprintf('%-60.60s',t[:description])} #{sprintf('%9.2f', t[:amount])}" + end end else puts "This is a rudimentary API for Natwest Online banking. From 3961938800eb42f370bd4961cf657354c3e47787 Mon Sep 17 00:00:00 2001 From: Andrew Tumelty Date: Sun, 26 Oct 2014 14:43:46 +0000 Subject: [PATCH 7/7] Fix README styling --- README.markdown | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.markdown b/README.markdown index 0e4b2ae..3ce9aa0 100644 --- a/README.markdown +++ b/README.markdown @@ -10,6 +10,7 @@ information, balance, and recent transactions. ## Usage Get a quick summary: + $ nw summary > Please enter your customer number: > Please enter your PIN: @@ -25,6 +26,7 @@ Get a quick summary: ... Get transactions for one account between 2 dates: + $ nw transactions 2013-08-01 2014-10-26 123 Transactions for account ending 123, between 2013-08-01 and 2014-10-26 Date Description Amount