bank_scrap 0.0.7 → 0.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|