bank_scrap 0.0.7 → 0.0.8
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +52 -16
- data/bank_scrap.gemspec +1 -0
- data/lib/bank_scrap/account.rb +32 -0
- data/lib/bank_scrap/bank.rb +22 -3
- data/lib/bank_scrap/banks/bbva.rb +76 -5
- data/lib/bank_scrap/banks/ing.rb +91 -28
- data/lib/bank_scrap/cli.rb +13 -4
- data/lib/bank_scrap/config.rb +5 -0
- data/lib/bank_scrap/locale/en.yml +12 -0
- data/lib/bank_scrap/transaction.rb +28 -0
- data/lib/bank_scrap/utils/inspectable.rb +19 -0
- data/lib/bank_scrap/version.rb +1 -1
- data/lib/bank_scrap.rb +6 -0
- metadata +21 -3
- data/lib/bank_scrap/banks/ing_backup.rb +0 -237
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c9c667caab2fefb56490b67d4e33be414f969ea7
|
4
|
+
data.tar.gz: af3751b08dfd95655f0cd86f9eaf0f13e186a6cf
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 946cf536efb91eebb2b69d23b88d18b10d9a89cf73adc29fe6f23207388ed4078b36b829d45d049af76a6fff8d2ac8bf8532942dd95dfb6b531ac9e8f9a0f158
|
7
|
+
data.tar.gz: e63351124eaf1920397fcf116d0359d9a897948f61cab3ad84329ff7a4ea01336cf935e08a1d0a648e12d56bcf2504096f0f41d8a053df405ff90e08bdaf945e
|
data/README.md
CHANGED
@@ -1,15 +1,15 @@
|
|
1
1
|
# BankScrap
|
2
2
|
|
3
|
-
Ruby gem to extract balance and transactions from banks. You can use it either as command line tool or as a library.
|
3
|
+
Ruby gem to extract account balance and transactions from banks. You can use it either as command line tool or as a library.
|
4
4
|
|
5
5
|
Feel free to contribute and add your bank if it isn't supported.
|
6
6
|
|
7
7
|
## Supported banks
|
8
8
|
|
9
|
-
|
|
10
|
-
|
11
|
-
|
|
12
|
-
|
|
9
|
+
| | BBVA | ING Direct | Bankinter |
|
10
|
+
|-----------------|:------:|:----------:|:---------:|
|
11
|
+
| Account Balance | ✓ | ✓ | WIP |
|
12
|
+
| Transactions | ✓ | ✓ | WIP |
|
13
13
|
|
14
14
|
Interested in any other bank? Open a new Issue and we'll try to help.
|
15
15
|
|
@@ -50,24 +50,34 @@ Or, if you're using Bundler, just add the following to your Gemfile:
|
|
50
50
|
## Usage
|
51
51
|
|
52
52
|
### From terminal
|
53
|
-
|
53
|
+
#### Bank account balance
|
54
54
|
|
55
|
-
|
56
|
-
|
57
|
-
$ bank_scrap balance bankinter --user YOUR_BANKINTER_USER --password YOUR_BANKINTER_PASSWORD
|
58
|
-
|
59
|
-
##### BBVA
|
55
|
+
###### BBVA
|
60
56
|
|
61
57
|
$ bank_scrap balance bbva --user YOUR_BBVA_USER --password YOUR_BBVA_PASSWORD
|
62
58
|
|
63
|
-
|
59
|
+
###### ING Direct
|
64
60
|
ING needs one more argument: your bithday.
|
65
61
|
|
66
62
|
$ bank_scrap balance ing --user YOUR_DNI --password YOUR_PASSWORD --extra=birthday:01/01/1980
|
67
63
|
|
68
64
|
Replace 01/01/1980 with your actual birthday.
|
69
65
|
|
66
|
+
#### Transactions for last 30 days
|
67
|
+
###### BBVA
|
68
|
+
|
69
|
+
$ bank_scrap transactions bbva --user YOUR_BBVA_USER --password YOUR_BBVA_PASSWORD
|
70
|
+
|
71
|
+
###### ING Direct
|
72
|
+
|
73
|
+
$ bank_scrap transactions ing --user YOUR_DNI --password YOUR_PASSWORD --extra=birthday:01/01/1980
|
74
|
+
|
70
75
|
---
|
76
|
+
|
77
|
+
By default it will use your first bank account, if you want to fetch transactions for a different account you can use this syntax:
|
78
|
+
|
79
|
+
$ bank_scrap transactions your_bank your_iban --user YOUR_DNI --password YOUR_PASSWORD
|
80
|
+
|
71
81
|
If you don't want to pass your user and password everytime you can define them in your .bash_profile by adding:
|
72
82
|
|
73
83
|
export BANK_SCRAP_USER=YOUR_BANK_USER
|
@@ -77,19 +87,45 @@ If you don't want to pass your user and password everytime you can define them i
|
|
77
87
|
|
78
88
|
You can also use this gem from your own app as library. To do so first you must initialize a BankScrap::Bank object
|
79
89
|
|
90
|
+
|
80
91
|
```ruby
|
81
92
|
require 'bank_scrap'
|
82
|
-
|
93
|
+
# BBVA
|
94
|
+
bbva = BankScrap::Bbva.new(YOUR_BBVA_USER, YOUR_BBVA_PASSWORD)
|
95
|
+
# ING
|
96
|
+
ing = BankScrap::Ing.new(YOUR_DNI, YOUR_ING_PASSWORD, extra_args: {"birthday" => "dd/mm/yyyy"})
|
83
97
|
```
|
84
98
|
|
85
|
-
(Replace Bbva with your own bank)
|
86
99
|
|
87
|
-
|
100
|
+
The initialize method will automatically login and fetch your bank accounts
|
101
|
+
|
102
|
+
You can now explore your bank accounts accounts:
|
88
103
|
|
89
104
|
```ruby
|
90
|
-
|
105
|
+
bank.accounts
|
91
106
|
```
|
92
107
|
|
108
|
+
And get its balance:
|
109
|
+
```ruby
|
110
|
+
bank.accounts.first.balance
|
111
|
+
```
|
112
|
+
|
113
|
+
Get last month transactions for a particular account:
|
114
|
+
|
115
|
+
```ruby
|
116
|
+
account = bank.accounts.first
|
117
|
+
account.transactions
|
118
|
+
```
|
119
|
+
|
120
|
+
Get transactions for last year (from now):
|
121
|
+
|
122
|
+
```ruby
|
123
|
+
account = bank.accounts.first
|
124
|
+
account.transactions = account.fetch_transactions(start_date: Date.today - 1.year, end_date: Date.today)
|
125
|
+
account.transactions
|
126
|
+
```
|
127
|
+
|
128
|
+
|
93
129
|
|
94
130
|
## Contributing
|
95
131
|
|
data/bank_scrap.gemspec
CHANGED
@@ -0,0 +1,32 @@
|
|
1
|
+
module BankScrap
|
2
|
+
class Account
|
3
|
+
include Utils::Inspectable
|
4
|
+
|
5
|
+
attr_accessor :bank, :id, :name, :balance, :currency,
|
6
|
+
:available_balance, :description,
|
7
|
+
:transactions, :iban, :bic
|
8
|
+
|
9
|
+
def initialize(params = {})
|
10
|
+
params.each { |key, value| send "#{key}=", value }
|
11
|
+
end
|
12
|
+
|
13
|
+
def transactions
|
14
|
+
@transactions ||= bank.fetch_transactions_for(self)
|
15
|
+
end
|
16
|
+
|
17
|
+
def fetch_transactions(start_date: Date.today - 2.years, end_date: Date.today)
|
18
|
+
bank.fetch_transactions_for(self, start_date: start_date, end_date: end_date)
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def inspect_attributes
|
24
|
+
[
|
25
|
+
:id, :name, :balance, :currency,
|
26
|
+
:available_balance, :description,
|
27
|
+
:iban, :bic
|
28
|
+
]
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
data/lib/bank_scrap/bank.rb
CHANGED
@@ -5,12 +5,30 @@ module BankScrap
|
|
5
5
|
class Bank
|
6
6
|
|
7
7
|
WEB_USER_AGENT = 'Mozilla/5.0 (Linux; Android 4.2.1; en-us; Nexus 4 Build/JOP40D) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.166 Mobile Safari/535.19'
|
8
|
-
attr_accessor :headers
|
8
|
+
attr_accessor :headers, :accounts
|
9
|
+
|
10
|
+
def initialize(user, password, log: false, debug: false, extra_args: nil)
|
11
|
+
@accounts = fetch_accounts
|
12
|
+
end
|
13
|
+
|
14
|
+
# Interface method placeholders
|
15
|
+
|
16
|
+
def fetch_accounts
|
17
|
+
raise Exception.new "#{self.class} should implement a fetch_account method"
|
18
|
+
end
|
19
|
+
|
20
|
+
def fetch_transactions_for(account, start_date: Date.today - 1.month, end_date: Date.today)
|
21
|
+
raise Exception.new "#{self.class} should implement a fetch_transactions method"
|
22
|
+
end
|
23
|
+
|
24
|
+
def account_with_iban(iban)
|
25
|
+
accounts.find { |account| account.iban.gsub(' ','') == iban.gsub(' ','') }
|
26
|
+
end
|
9
27
|
|
10
28
|
private
|
11
29
|
|
12
|
-
def get(url)
|
13
|
-
@http.get(url).body
|
30
|
+
def get(url, params = {})
|
31
|
+
@http.get(url, params).body
|
14
32
|
end
|
15
33
|
|
16
34
|
def post(url, fields)
|
@@ -50,6 +68,7 @@ module BankScrap
|
|
50
68
|
mechanize.user_agent = WEB_USER_AGENT
|
51
69
|
mechanize.agent.http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
52
70
|
mechanize.log = Logger.new(STDOUT) if @debug
|
71
|
+
# mechanize.set_proxy 'localhost', 8888
|
53
72
|
end
|
54
73
|
|
55
74
|
@headers = {}
|
@@ -4,7 +4,8 @@ module BankScrap
|
|
4
4
|
class Bbva < Bank
|
5
5
|
BASE_ENDPOINT = 'https://bancamovil.grupobbva.com'
|
6
6
|
LOGIN_ENDPOINT = '/DFAUTH/slod/DFServletXML'
|
7
|
-
|
7
|
+
PRODUCTS_ENDPOINT = '/ENPP/enpp_mult_web_mobility_02/products/v1'
|
8
|
+
ACCOUNT_ENDPOINT = '/ENPP/enpp_mult_web_mobility_02/accounts/'
|
8
9
|
# BBVA expects an identifier before the actual User Agent, but 12345 works fine
|
9
10
|
USER_AGENT = '12345;Android;LGE;Nexus 5;1080x1776;Android;4.4.4;BMES;4.0.4'
|
10
11
|
|
@@ -29,22 +30,63 @@ module BankScrap
|
|
29
30
|
})
|
30
31
|
|
31
32
|
login
|
33
|
+
super
|
32
34
|
end
|
33
35
|
|
34
|
-
|
35
|
-
|
36
|
+
# Fetch all the accounts for the given user
|
37
|
+
# Returns an array of BankScrap::Account objects
|
38
|
+
def fetch_accounts
|
39
|
+
log 'fetch_accounts'
|
36
40
|
|
37
41
|
# Even if the required method is an HTTP POST
|
38
42
|
# the API requires a funny header that says is a GET
|
39
43
|
# otherwise the request doesn't work.
|
40
44
|
response = with_headers({'BBVA-Method' => 'GET'}) do
|
41
|
-
post(BASE_ENDPOINT +
|
45
|
+
post(BASE_ENDPOINT + PRODUCTS_ENDPOINT, {})
|
42
46
|
end
|
43
47
|
|
44
48
|
json = JSON.parse(response)
|
45
|
-
json["
|
49
|
+
json["accounts"].collect { |data| build_account(data) }
|
46
50
|
end
|
47
51
|
|
52
|
+
# Fetch transactions for the given account.
|
53
|
+
# By default it fetches transactions for the last month,
|
54
|
+
# The maximum allowed by the BBVA API is the last 3 years.
|
55
|
+
#
|
56
|
+
# Account should be a BankScrap::Account object
|
57
|
+
# Returns an array of BankScrap::Transaction objects
|
58
|
+
def fetch_transactions_for(account, start_date: Date.today - 1.month, end_date: Date.today)
|
59
|
+
fromDate = start_date.strftime("%Y-%m-%d")
|
60
|
+
toDate = end_date.strftime("%Y-%m-%d")
|
61
|
+
|
62
|
+
# Misteriously we need a specific content-type here
|
63
|
+
funny_headers = {
|
64
|
+
'Content-Type' => 'application/json; charset=UTF-8',
|
65
|
+
'BBVA-Method' => 'GET'
|
66
|
+
}
|
67
|
+
|
68
|
+
url = BASE_ENDPOINT + ACCOUNT_ENDPOINT + account.id+ "/movements/v1?fromDate=#{fromDate}&toDate=#{toDate}"
|
69
|
+
offset = nil
|
70
|
+
transactions = []
|
71
|
+
|
72
|
+
with_headers(funny_headers) do
|
73
|
+
# Loop over pagination
|
74
|
+
loop do
|
75
|
+
new_url = offset ? (url + "&offset=#{offset}") : url
|
76
|
+
json = JSON.parse(post(new_url, {}))
|
77
|
+
|
78
|
+
unless json["movements"].blank?
|
79
|
+
transactions += json["movements"].collect { |data| build_transaction(data, account) }
|
80
|
+
offset = json["offset"]
|
81
|
+
end
|
82
|
+
|
83
|
+
break unless json["thereAreMoreMovements"] == true
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
transactions
|
88
|
+
end
|
89
|
+
|
48
90
|
private
|
49
91
|
|
50
92
|
# As far as we know there are two types of identifiers BBVA uses
|
@@ -72,5 +114,34 @@ module BankScrap
|
|
72
114
|
'eai_URLDestino' => '/ENPP/enpp_mult_web_mobility_02/sessions/v1'
|
73
115
|
})
|
74
116
|
end
|
117
|
+
|
118
|
+
# Build an Account object from API data
|
119
|
+
def build_account(data)
|
120
|
+
Account.new(
|
121
|
+
bank: self,
|
122
|
+
id: data['id'],
|
123
|
+
name: data['name'],
|
124
|
+
available_balance: data['availableBalance'],
|
125
|
+
balance: data['availableBalance'],
|
126
|
+
currency: data['currency'],
|
127
|
+
iban: data['iban'],
|
128
|
+
description: "#{data['typeDescription']} #{data['familyCode']}"
|
129
|
+
)
|
130
|
+
end
|
131
|
+
|
132
|
+
# Build a transaction object from API data
|
133
|
+
def build_transaction(data, account)
|
134
|
+
amount = Money.new(data['amount'] * 100, data['currency'])
|
135
|
+
balance = data['accountBalanceAfterMovement'] ? Money.new(data['accountBalanceAfterMovement'] * 100, data['currency']) : nil
|
136
|
+
Transaction.new(
|
137
|
+
account: account,
|
138
|
+
id: data['id'],
|
139
|
+
amount: amount,
|
140
|
+
description: data['conceptDescription'] || data['description'],
|
141
|
+
effective_date: Date.strptime(data['operationDate'], "%Y-%m-%d"),
|
142
|
+
currency: data['currency'],
|
143
|
+
balance: balance
|
144
|
+
)
|
145
|
+
end
|
75
146
|
end
|
76
147
|
end
|
data/lib/bank_scrap/banks/ing.rb
CHANGED
@@ -18,27 +18,70 @@ module BankScrap
|
|
18
18
|
def initialize(user, password, log: false, debug: false, extra_args:)
|
19
19
|
@dni = user
|
20
20
|
@password = password.to_s
|
21
|
-
@birthday = extra_args['birthday']
|
21
|
+
@birthday = extra_args.with_indifferent_access['birthday']
|
22
22
|
@log = log
|
23
23
|
@debug = debug
|
24
24
|
|
25
25
|
initialize_connection
|
26
26
|
bundled_login
|
27
|
-
|
27
|
+
|
28
|
+
super
|
28
29
|
end
|
29
30
|
|
30
31
|
def get_balance
|
31
32
|
log 'get_balance'
|
32
|
-
|
33
|
-
|
34
|
-
|
33
|
+
balances = {}
|
34
|
+
total_balance = 0
|
35
|
+
@accounts.each do |account|
|
36
|
+
balances[account.description] = account.balance
|
37
|
+
total_balance += account.balance
|
35
38
|
end
|
36
39
|
|
37
|
-
|
40
|
+
balances['TOTAL'] = total_balance
|
41
|
+
balances
|
42
|
+
end
|
43
|
+
|
44
|
+
def raw_accounts_data
|
45
|
+
@raw_accounts_data
|
38
46
|
end
|
39
47
|
|
40
|
-
def
|
41
|
-
|
48
|
+
def fetch_accounts
|
49
|
+
log 'fetch_accounts'
|
50
|
+
set_headers({
|
51
|
+
"Accept" => '*/*',
|
52
|
+
'Content-Type' => 'application/json; charset=utf-8'
|
53
|
+
})
|
54
|
+
|
55
|
+
@raw_accounts_data = JSON.parse(get(PRODUCTS_ENDPOINT))
|
56
|
+
|
57
|
+
@raw_accounts_data.collect do |account|
|
58
|
+
if account['iban']
|
59
|
+
build_account(account)
|
60
|
+
end
|
61
|
+
end.compact
|
62
|
+
end
|
63
|
+
|
64
|
+
def fetch_transactions_for(account, start_date: Date.today - 1.month, end_date: Date.today)
|
65
|
+
log "fetch_transactions for #{account.id}"
|
66
|
+
|
67
|
+
# The API allows any limit to be passed, but we better keep
|
68
|
+
# being good API citizens and make a loop with a short limit
|
69
|
+
params = {
|
70
|
+
fromDate: start_date.strftime("%d/%m/%Y"),
|
71
|
+
toDate: end_date.strftime("%d/%m/%Y"),
|
72
|
+
limit: 25,
|
73
|
+
offset: 0
|
74
|
+
}
|
75
|
+
|
76
|
+
transactions = []
|
77
|
+
loop do
|
78
|
+
request = get("#{PRODUCTS_ENDPOINT}/#{account.id}/movements", params)
|
79
|
+
json = JSON.parse(request)
|
80
|
+
transactions += json['elements'].collect { |transaction| build_transaction(transaction, account) }
|
81
|
+
params[:offset] += 25
|
82
|
+
break if (params[:offset] > json['total']) || json['elements'].blank?
|
83
|
+
end
|
84
|
+
transactions
|
42
85
|
end
|
43
86
|
|
44
87
|
private
|
@@ -55,16 +98,17 @@ module BankScrap
|
|
55
98
|
'Content-Type' => 'application/json; charset=utf-8'
|
56
99
|
})
|
57
100
|
|
58
|
-
param =
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
101
|
+
param = {
|
102
|
+
loginDocument: {
|
103
|
+
documentType: 0,
|
104
|
+
document: @dni.to_s
|
105
|
+
},
|
106
|
+
birthday: @birthday.to_s,
|
107
|
+
companyDocument: nil,
|
108
|
+
device: 'desktop'
|
109
|
+
}
|
110
|
+
|
111
|
+
response = JSON.parse(post(LOGIN_ENDPOINT, param.to_json))
|
68
112
|
positions = response['pinPositions']
|
69
113
|
pinpad = response['pinpad']
|
70
114
|
|
@@ -95,19 +139,10 @@ module BankScrap
|
|
95
139
|
post(POST_AUTH_ENDPOINT, param)
|
96
140
|
end
|
97
141
|
|
98
|
-
def get_products
|
99
|
-
set_headers({
|
100
|
-
"Accept" => '*/*',
|
101
|
-
'Content-Type' => 'application/json; charset=utf-8'
|
102
|
-
})
|
103
|
-
|
104
|
-
@data = JSON.parse(get(PRODUCTS_ENDPOINT))
|
105
|
-
end
|
106
|
-
|
107
142
|
def save_pinpad_numbers(pinpad)
|
108
143
|
pinpad_numbers_paths = []
|
109
144
|
pinpad.each_with_index do |digit,index|
|
110
|
-
tmp = Tempfile.new(["pinpad_number_#{index}
|
145
|
+
tmp = Tempfile.new(["pinpad_number_#{index}", '.png'])
|
111
146
|
File.open(tmp.path, 'wb'){ |f| f.write(Base64.decode64(digit)) }
|
112
147
|
pinpad_numbers_paths << tmp.path
|
113
148
|
end
|
@@ -154,5 +189,33 @@ module BankScrap
|
|
154
189
|
pinpad_numbers.index(third_digit.to_i)
|
155
190
|
]
|
156
191
|
end
|
192
|
+
|
193
|
+
# Build an Account object from API data
|
194
|
+
def build_account(data)
|
195
|
+
Account.new(
|
196
|
+
bank: self,
|
197
|
+
id: data['uuid'],
|
198
|
+
name: data['name'],
|
199
|
+
balance: data['balance'],
|
200
|
+
currency: 'EUR',
|
201
|
+
available_balance: data['availableBalance'],
|
202
|
+
description: (data['alias'] || data['name']),
|
203
|
+
iban: data['iban'],
|
204
|
+
bic: data['bic']
|
205
|
+
)
|
206
|
+
end
|
207
|
+
|
208
|
+
# Build a transaction object from API data
|
209
|
+
def build_transaction(data, account)
|
210
|
+
Transaction.new(
|
211
|
+
account: account,
|
212
|
+
id: data['uuid'],
|
213
|
+
amount: data['amount'],
|
214
|
+
currency: data['EUR'],
|
215
|
+
effective_date: data['effectiveDate'],
|
216
|
+
description: data['description'],
|
217
|
+
balance: data['balance']
|
218
|
+
)
|
219
|
+
end
|
157
220
|
end
|
158
221
|
end
|
data/lib/bank_scrap/cli.rb
CHANGED
@@ -15,21 +15,30 @@ module BankScrap
|
|
15
15
|
option :extra, type: :hash
|
16
16
|
end
|
17
17
|
|
18
|
-
desc "balance BANK", "get
|
18
|
+
desc "balance BANK", "get accounts' balance"
|
19
19
|
shared_options
|
20
20
|
def balance(bank)
|
21
21
|
assign_shared_options
|
22
22
|
initialize_client_for(bank)
|
23
23
|
|
24
|
-
|
24
|
+
@client.accounts.each do |account|
|
25
|
+
say "Account: #{account.description} (#{account.iban})", :cyan
|
26
|
+
say "Balance: #{account.balance}", :green
|
27
|
+
end
|
25
28
|
end
|
26
29
|
|
27
30
|
desc "transactions BANK", "get account's transactions"
|
28
31
|
shared_options
|
29
|
-
def transactions(bank)
|
32
|
+
def transactions(bank, iban = nil)
|
30
33
|
assign_shared_options
|
31
34
|
initialize_client_for(bank)
|
32
|
-
|
35
|
+
account = iban ? @client.account_with_iban(iban) : @client.accounts.first
|
36
|
+
transactions = account.transactions
|
37
|
+
say "Transactions for: #{account.description} (#{account.iban})", :cyan
|
38
|
+
|
39
|
+
transactions.each do |transaction|
|
40
|
+
say transaction.to_s, (transaction.amount > Money.new(0) ? :green : :red)
|
41
|
+
end
|
33
42
|
end
|
34
43
|
|
35
44
|
private
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module BankScrap
|
2
|
+
class Transaction
|
3
|
+
include Utils::Inspectable
|
4
|
+
|
5
|
+
attr_accessor :id, :amount, :currency,
|
6
|
+
:effective_date, :description,
|
7
|
+
:balance, :account
|
8
|
+
|
9
|
+
def initialize(params = {})
|
10
|
+
params.each{ |key, value| send "#{key}=", value }
|
11
|
+
end
|
12
|
+
|
13
|
+
def to_s
|
14
|
+
"#{effective_date.strftime("%d/%m/%Y")} #{description.ljust(45)} #{amount.format.rjust(20)}"
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def inspect_attributes
|
20
|
+
[
|
21
|
+
:id, :amount, :currency,
|
22
|
+
:effective_date, :description,
|
23
|
+
:balance
|
24
|
+
]
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module BankScrap
|
2
|
+
module Utils
|
3
|
+
module Inspectable
|
4
|
+
def inspect
|
5
|
+
attributes = inspect_attributes.reject { |x|
|
6
|
+
begin
|
7
|
+
attribute = send x
|
8
|
+
!attribute || (attribute.respond_to?(:empty?) && attribute.empty?)
|
9
|
+
rescue NoMethodError
|
10
|
+
true
|
11
|
+
end
|
12
|
+
}.map { |attribute|
|
13
|
+
"#{attribute.to_s}: #{send(attribute).inspect}"
|
14
|
+
}.join ' '
|
15
|
+
"#<#{self.class.name}:#{sprintf("0x%x", object_id)} #{attributes}>"
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
data/lib/bank_scrap/version.rb
CHANGED
data/lib/bank_scrap.rb
CHANGED
@@ -1,6 +1,12 @@
|
|
1
|
+
require 'active_support/all'
|
2
|
+
require 'money'
|
3
|
+
require 'bank_scrap/utils/inspectable'
|
1
4
|
require 'bank_scrap/version'
|
5
|
+
require 'bank_scrap/config'
|
2
6
|
require 'bank_scrap/cli'
|
3
7
|
require 'bank_scrap/bank'
|
8
|
+
require 'bank_scrap/account'
|
9
|
+
require 'bank_scrap/transaction'
|
4
10
|
|
5
11
|
module BankScrap
|
6
12
|
# autoload only requires the file when the specified
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: bank_scrap
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.8
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ismael Sánchez
|
@@ -11,7 +11,7 @@ authors:
|
|
11
11
|
autorequire:
|
12
12
|
bindir: bin
|
13
13
|
cert_chain: []
|
14
|
-
date: 2015-01-
|
14
|
+
date: 2015-01-29 00:00:00.000000000 Z
|
15
15
|
dependencies:
|
16
16
|
- !ruby/object:Gem::Dependency
|
17
17
|
name: bundler
|
@@ -151,6 +151,20 @@ dependencies:
|
|
151
151
|
- - ">="
|
152
152
|
- !ruby/object:Gem::Version
|
153
153
|
version: 2.2.2
|
154
|
+
- !ruby/object:Gem::Dependency
|
155
|
+
name: money
|
156
|
+
requirement: !ruby/object:Gem::Requirement
|
157
|
+
requirements:
|
158
|
+
- - "~>"
|
159
|
+
- !ruby/object:Gem::Version
|
160
|
+
version: 6.5.0
|
161
|
+
type: :runtime
|
162
|
+
prerelease: false
|
163
|
+
version_requirements: !ruby/object:Gem::Requirement
|
164
|
+
requirements:
|
165
|
+
- - "~>"
|
166
|
+
- !ruby/object:Gem::Version
|
167
|
+
version: 6.5.0
|
154
168
|
description: Command line tools to get bank account details from some banks.
|
155
169
|
email:
|
156
170
|
- root@ismagnu.com
|
@@ -167,6 +181,7 @@ files:
|
|
167
181
|
- bank_scrap.gemspec
|
168
182
|
- bin/bank_scrap
|
169
183
|
- lib/bank_scrap.rb
|
184
|
+
- lib/bank_scrap/account.rb
|
170
185
|
- lib/bank_scrap/bank.rb
|
171
186
|
- lib/bank_scrap/banks/bankinter.rb
|
172
187
|
- lib/bank_scrap/banks/bbva.rb
|
@@ -181,8 +196,11 @@ files:
|
|
181
196
|
- lib/bank_scrap/banks/ing/numbers/pinpad7.png
|
182
197
|
- lib/bank_scrap/banks/ing/numbers/pinpad8.png
|
183
198
|
- lib/bank_scrap/banks/ing/numbers/pinpad9.png
|
184
|
-
- lib/bank_scrap/banks/ing_backup.rb
|
185
199
|
- lib/bank_scrap/cli.rb
|
200
|
+
- lib/bank_scrap/config.rb
|
201
|
+
- lib/bank_scrap/locale/en.yml
|
202
|
+
- lib/bank_scrap/transaction.rb
|
203
|
+
- lib/bank_scrap/utils/inspectable.rb
|
186
204
|
- lib/bank_scrap/version.rb
|
187
205
|
homepage: https://github.com/ismaGNU/bank_scrap
|
188
206
|
licenses:
|
@@ -1,237 +0,0 @@
|
|
1
|
-
require 'execjs'
|
2
|
-
require 'pp'
|
3
|
-
require 'json'
|
4
|
-
require 'base64'
|
5
|
-
require 'RMagick'
|
6
|
-
require 'active_support'
|
7
|
-
require 'byebug'
|
8
|
-
require 'open-uri'
|
9
|
-
|
10
|
-
module BankScrap
|
11
|
-
class Ing < Bank
|
12
|
-
|
13
|
-
DESKTOP_USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/38.0.2125.111 Safari/537.36'
|
14
|
-
|
15
|
-
BASE_ENDPOINT = 'https://ing.ingdirect.es/'
|
16
|
-
FALSE_LOGIN_ENDPOINT = BASE_ENDPOINT + 'login/'
|
17
|
-
CACHE_ENDPOINT = BASE_ENDPOINT + 'login/cache.manifest'
|
18
|
-
DELETE_SESSION_ENDPOINT = BASE_ENDPOINT + 'genoma_api/rest/session'
|
19
|
-
LOGIN_ENDPOINT = BASE_ENDPOINT + 'genoma_login/rest/session'
|
20
|
-
POST_AUTH_ENDPOINT = BASE_ENDPOINT + 'genoma_api/login/auth/response'
|
21
|
-
CLIENT_ENDPOINT = BASE_ENDPOINT + 'genoma_api/rest/client'
|
22
|
-
PRODUCTS_ENDPOINT = BASE_ENDPOINT + 'genoma_api/rest/products'
|
23
|
-
|
24
|
-
SAMPLE_WIDTH = 30
|
25
|
-
SAMPLE_HEIGHT = 30
|
26
|
-
|
27
|
-
def initialize(dni, birthday, password, log: false, debug: false)
|
28
|
-
@dni = dni
|
29
|
-
@birthday = birthday
|
30
|
-
@password = password.to_s
|
31
|
-
@log = log
|
32
|
-
@debug = debug
|
33
|
-
|
34
|
-
initialize_connection
|
35
|
-
|
36
|
-
@curl.proxy_port = 8888
|
37
|
-
@curl.proxy_url = '192.168.1.21'
|
38
|
-
|
39
|
-
false_login
|
40
|
-
cache
|
41
|
-
delete_session
|
42
|
-
selected_positions = login
|
43
|
-
|
44
|
-
ticket = pass_pinpad(selected_positions)
|
45
|
-
|
46
|
-
post_auth(ticket)
|
47
|
-
call_client
|
48
|
-
|
49
|
-
get_products
|
50
|
-
end
|
51
|
-
|
52
|
-
private
|
53
|
-
|
54
|
-
def false_login
|
55
|
-
@curl.url = FALSE_LOGIN_ENDPOINT
|
56
|
-
@curl.headers['Host'] = 'ing.ingdirect.es'
|
57
|
-
@curl.headers['Connection'] = 'keep-alive'
|
58
|
-
@curl.headers['Accept'] = 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8'
|
59
|
-
@curl.headers['Accept-Encoding'] = 'gzip,deflate,sdch'
|
60
|
-
@curl.headers['Accept-Language'] = 'en,es;q=0.8'
|
61
|
-
|
62
|
-
response = @curl.get
|
63
|
-
end
|
64
|
-
|
65
|
-
def cache
|
66
|
-
@curl.url = CACHE_ENDPOINT
|
67
|
-
@curl.headers['Host'] = 'ing.ingdirect.es'
|
68
|
-
@curl.headers['Connection'] = 'keep-alive'
|
69
|
-
@curl.headers['Accept-Encoding'] = 'gzip,deflate,sdch'
|
70
|
-
@curl.headers['Accept-Language'] = 'en,es;q=0.8'
|
71
|
-
end
|
72
|
-
|
73
|
-
def delete_session
|
74
|
-
@curl.headers['Host'] = 'ing.ingdirect.es'
|
75
|
-
@curl.headers['Connection'] = 'keep-alive'
|
76
|
-
@curl.headers['Pragma'] = 'no-cache'
|
77
|
-
@curl.headers['Accept'] = 'application/json, text/javascript, */*; q=0.01'
|
78
|
-
@curl.headers['Origin'] = 'https://ing.ingdirect.es'
|
79
|
-
@curl.headers['X-Requested-With'] = 'XMLHttpRequest'
|
80
|
-
@curl.headers['Content-Type'] = 'application/json; charset=utf-8'
|
81
|
-
@curl.headers['Referer'] = 'https://ing.ingdirect.es/login/'
|
82
|
-
@curl.headers['Accept-Encoding'] = 'zip,deflate,sdch'
|
83
|
-
@curl.headers['Accept-Language'] = 'n,es;q=0.8'
|
84
|
-
@curl.headers['Cookie'] = 's_cc=true; s_mca=Direct; s_gts=1; s_nr=1414955726141; s_sq=%5B%5BB%5D%5D'
|
85
|
-
|
86
|
-
response = @curl.delete
|
87
|
-
pp response
|
88
|
-
end
|
89
|
-
|
90
|
-
def login
|
91
|
-
param = '{"loginDocument":{"documentType":0,"document":"' + @dni.to_s +
|
92
|
-
'"},"birthday":"' + @birthday.to_s + '","companyDocument":null,"device":"desktop"}'
|
93
|
-
puts param
|
94
|
-
@curl.url = LOGIN_ENDPOINT
|
95
|
-
@curl.headers['Host'] = 'ing.ingdirect.es'
|
96
|
-
@curl.headers['Connection'] = 'keep-alive'
|
97
|
-
@curl.headers['Pragma'] = 'no-cache'
|
98
|
-
|
99
|
-
@curl.headers['Accept'] = 'application/json, text/javascript, */*; q=0.01'
|
100
|
-
@curl.headers['Origin'] = 'https://ing.ingdirect.es'
|
101
|
-
@curl.headers['X-Requested-With'] = 'XMLHttpRequest'
|
102
|
-
@curl.headers['Content-Type'] = 'application/json; charset=utf-8'
|
103
|
-
@curl.headers['Referer'] = 'https://ing.ingdirect.es/login/'
|
104
|
-
@curl.headers['Accept-Encoding'] = 'zip,deflate,sdch'
|
105
|
-
@curl.headers['Accept-Language'] = 'n,es;q=0.8'
|
106
|
-
@curl.headers['Cookie'] = 's_cc=true; s_mca=Direct; s_gts=1; s_nr=1414955726141; s_sq=%5B%5BB%5D%5D'
|
107
|
-
|
108
|
-
response = post(LOGIN_ENDPOINT, param)
|
109
|
-
# response = @curl.body_str
|
110
|
-
response = JSON.parse(response)
|
111
|
-
positions = response['pinPositions']
|
112
|
-
pinpad = response['pinpad']
|
113
|
-
|
114
|
-
save_pinpad_numbers(pinpad)
|
115
|
-
pinpad_numbers = recognize_pinpad_numbers
|
116
|
-
|
117
|
-
get_correct_positions(pinpad_numbers, positions)
|
118
|
-
end
|
119
|
-
|
120
|
-
def pass_pinpad(positions)
|
121
|
-
param = "{\"pinPositions\": #{positions}}"
|
122
|
-
@curl.url = LOGIN_ENDPOINT
|
123
|
-
@curl.headers['Host'] = 'ing.ingdirect.es'
|
124
|
-
@curl.headers['Connection'] = 'keep-alive'
|
125
|
-
@curl.headers['Accept'] = 'application/json, text/javascript, */*; q=0.01'
|
126
|
-
@curl.headers['Origin'] = 'https://ing.ingdirect.es'
|
127
|
-
@curl.headers['X-Requested-With'] = 'XMLHttpRequest'
|
128
|
-
@curl.headers['Content-Type'] = 'application/json; charset=utf-8'
|
129
|
-
@curl.headers['Referer'] = 'https://ing.ingdirect.es/login/?clientId=281afde24c938607e5edeac6239e8a38&continue=%2Fpfm%2F'
|
130
|
-
@curl.headers['Accept-Encoding'] = 'gzip,deflate,sdch'
|
131
|
-
|
132
|
-
response = put(LOGIN_ENDPOINT, param)
|
133
|
-
response = ActiveSupport::Gzip.decompress(response)
|
134
|
-
response = JSON.parse(response)
|
135
|
-
|
136
|
-
response['ticket']
|
137
|
-
end
|
138
|
-
|
139
|
-
def post_auth(ticket)
|
140
|
-
@curl.headers['Host'] = 'ing.ingdirect.es'
|
141
|
-
@curl.headers['Connection'] = 'keep-alive'
|
142
|
-
@curl.headers['Accept'] = 'application/json, text/javascript, */*; q=0.01'
|
143
|
-
@curl.headers['Origin'] = 'https://ing.ingdirect.es'
|
144
|
-
@curl.headers['X-Requested-With'] = 'XMLHttpRequest'
|
145
|
-
@curl.headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8'
|
146
|
-
@curl.headers['Referer'] = 'https://ing.ingdirect.es/login'
|
147
|
-
@curl.headers['Accept-Encoding'] = 'gzip,deflate'
|
148
|
-
|
149
|
-
@curl.url = POST_AUTH_ENDPOINT
|
150
|
-
param = "ticket=#{ticket}&device=desktop"
|
151
|
-
@curl.post(param)
|
152
|
-
response = @curl.body_str
|
153
|
-
end
|
154
|
-
|
155
|
-
def call_client
|
156
|
-
@curl.headers['Host'] = 'ing.ingdirect.es'
|
157
|
-
@curl.headers['Connection'] = 'keep-alive'
|
158
|
-
@curl.headers['Accept'] = 'application/json, text/javascript, */*; q=0.01'
|
159
|
-
@curl.headers['X-Requested-With'] = 'XMLHttpRequest'
|
160
|
-
@curl.headers['Content-Type'] = 'application/json; charset=utf-8'
|
161
|
-
@curl.headers['Referer'] = 'https://ing.ingdirect.es/pfm'
|
162
|
-
@curl.headers['Accept-Encoding'] = 'gzip,deflate,sdch'
|
163
|
-
|
164
|
-
response = get(CLIENT_ENDPOINT)
|
165
|
-
|
166
|
-
response = ActiveSupport::Gzip.decompress(response)
|
167
|
-
response = JSON.parse(response)
|
168
|
-
end
|
169
|
-
|
170
|
-
def get_products
|
171
|
-
@curl.headers['Host'] = 'ing.ingdirect.es'
|
172
|
-
@curl.headers['Connection'] = 'keep-alive'
|
173
|
-
@curl.headers['Accept'] = '*/*'
|
174
|
-
@curl.headers['X-Requested-With'] = 'XMLHttpRequest'
|
175
|
-
@curl.headers['Content-Type'] = 'application/json; charset=utf-8'
|
176
|
-
@curl.headers['Referer'] = 'https://ing.ingdirect.es/pfm'
|
177
|
-
@curl.headers['Accept-Encoding'] = 'gzip,deflate,sdch'
|
178
|
-
|
179
|
-
response = get(PRODUCTS_ENDPOINT)
|
180
|
-
|
181
|
-
File.open('response_raw.txt', 'w') { |file| file.write(response) }
|
182
|
-
response = ActiveSupport::Gzip.decompress(response)
|
183
|
-
File.open('response_decompressed.txt', 'w') { |file| file.write(response) }
|
184
|
-
File.open('response_parsed.txt', 'w') { |file| file.write(JSON.parse(response)) }
|
185
|
-
end
|
186
|
-
|
187
|
-
def save_pinpad_numbers(pinpad)
|
188
|
-
pinpad.each_with_index do |p,index|
|
189
|
-
File.open(build_tmp_path(index), 'wb'){ |f| f.write(Base64.decode64(p)) }
|
190
|
-
end
|
191
|
-
end
|
192
|
-
|
193
|
-
def build_tmp_path(number)
|
194
|
-
"tmp/original_pinpad_#{number}.png"
|
195
|
-
end
|
196
|
-
|
197
|
-
def recognize_pinpad_numbers
|
198
|
-
pinpad_numbers = []
|
199
|
-
0.upto(9) do |i|
|
200
|
-
pinpad = Magick::ImageList.new(build_tmp_path(i)).first
|
201
|
-
|
202
|
-
differences = []
|
203
|
-
0.upto(9) do |j|
|
204
|
-
pinpad_pixels_sample = pinpad.get_pixels(0,0, SAMPLE_WIDTH, SAMPLE_HEIGHT)
|
205
|
-
|
206
|
-
img = Magick::ImageList.new("numbers/pinpad#{j}.png").first
|
207
|
-
number_pixels_sample = img.get_pixels(0, 0, SAMPLE_WIDTH, SAMPLE_HEIGHT)
|
208
|
-
diff = 0
|
209
|
-
pinpad_pixels_sample.each_with_index do |pixel, index|
|
210
|
-
sample_pixel = number_pixels_sample[index]
|
211
|
-
diff += (pixel.red - sample_pixel.red).abs +
|
212
|
-
(pixel.green - sample_pixel.green).abs +
|
213
|
-
(pixel.blue - sample_pixel.blue).abs
|
214
|
-
end
|
215
|
-
differences << diff
|
216
|
-
end
|
217
|
-
|
218
|
-
real_number = differences.each_with_index.min.last
|
219
|
-
pinpad_numbers << real_number
|
220
|
-
end
|
221
|
-
|
222
|
-
pinpad_numbers
|
223
|
-
end
|
224
|
-
|
225
|
-
def get_correct_positions(pinpad_numbers, positions)
|
226
|
-
first_digit = @password[positions[0] - 1]
|
227
|
-
second_digit = @password[positions[1] - 1]
|
228
|
-
third_digit = @password[positions[2] - 1]
|
229
|
-
|
230
|
-
[
|
231
|
-
pinpad_numbers.index(first_digit.to_i),
|
232
|
-
pinpad_numbers.index(second_digit.to_i),
|
233
|
-
pinpad_numbers.index(third_digit.to_i)
|
234
|
-
]
|
235
|
-
end
|
236
|
-
end
|
237
|
-
end
|