burglar 0.0.3 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +1 -1
- data/bin/burglar +1 -1
- data/burglar.gemspec +2 -2
- data/lib/burglar/modules/plaid.rb +109 -0
- data/lib/burglar/version.rb +1 -1
- metadata +7 -8
- data/lib/burglar/modules/ally.rb +0 -156
- data/lib/burglar/modules/american_express.rb +0 -79
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6cbc26fc77ea4427b7ae6cbfa3d53fb441e0897beafba7797b63a7cd853b4a45
|
4
|
+
data.tar.gz: 45e247c5fa8886e65d70ebc35e61227f9c64b5012247922b7373ed20aa76a8fd
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7d33eb6451240ee1300b6ecdb2a39792f859aec122084d31470b1ba2fbae0ed3ab50f84ae5d986b4584358c0b8d1d2090811da5de8dcfb749872796a64b42daa
|
7
|
+
data.tar.gz: 20af4b002fc0e12ae83d54a70a22db7b9580816920fdf6ea0eb02d9392e5c2c3104a289a6ede64423cea94cc12fc1d119d905d9dc0f9f8dc2c7f6bc38306168f
|
data/.travis.yml
CHANGED
data/bin/burglar
CHANGED
@@ -32,7 +32,7 @@ Mercenary.program(:burglar) do |p|
|
|
32
32
|
|
33
33
|
p.action do |_, options|
|
34
34
|
options[:end] = date_parse_or_default(options[:end], Date.today)
|
35
|
-
options[:begin] = date_parse_or_default(options[:begin], options[:end])
|
35
|
+
options[:begin] = date_parse_or_default(options[:begin], options[:end] - 30)
|
36
36
|
config = load_config(options[:config]).merge(options)
|
37
37
|
puts Burglar.new(config).transactions
|
38
38
|
end
|
data/burglar.gemspec
CHANGED
@@ -25,8 +25,8 @@ Gem::Specification.new do |s|
|
|
25
25
|
|
26
26
|
s.add_development_dependency 'codecov', '~> 0.1.1'
|
27
27
|
s.add_development_dependency 'fuubar', '~> 2.3.0'
|
28
|
-
s.add_development_dependency 'goodcop', '~> 0.
|
28
|
+
s.add_development_dependency 'goodcop', '~> 0.7.0'
|
29
29
|
s.add_development_dependency 'rake', '~> 12.3.0'
|
30
30
|
s.add_development_dependency 'rspec', '~> 3.8.0'
|
31
|
-
s.add_development_dependency 'rubocop', '~> 0.
|
31
|
+
s.add_development_dependency 'rubocop', '~> 0.67.2'
|
32
32
|
end
|
@@ -0,0 +1,109 @@
|
|
1
|
+
Burglar.extra_dep('plaid', 'plaid')
|
2
|
+
|
3
|
+
module LogCabin
|
4
|
+
module Modules
|
5
|
+
##
|
6
|
+
# Plaid
|
7
|
+
module Plaid
|
8
|
+
include Burglar.helpers.find(:creds)
|
9
|
+
include Burglar.helpers.find(:ledger)
|
10
|
+
|
11
|
+
PLAID_DOMAIN = 'https://plaid.com'.freeze
|
12
|
+
|
13
|
+
def raw_transactions # rubocop:disable Metrics/MethodLength
|
14
|
+
@raw_transactions ||= all_transactions.map do |row|
|
15
|
+
amount = "$#{row.amount}"
|
16
|
+
name = row.name.downcase
|
17
|
+
action = guess_action(name)
|
18
|
+
state = row.pending ? :pending : :cleared
|
19
|
+
|
20
|
+
::Ledger::Entry.new(
|
21
|
+
name: name,
|
22
|
+
state: state,
|
23
|
+
date: row.date,
|
24
|
+
actions: [
|
25
|
+
{ name: action, amount: amount },
|
26
|
+
{ name: account_name }
|
27
|
+
]
|
28
|
+
)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def api_client
|
35
|
+
@api_client ||= ::Plaid::Client.new(
|
36
|
+
env: 'development',
|
37
|
+
client_id: client_id,
|
38
|
+
secret: secret_key,
|
39
|
+
public_key: public_key
|
40
|
+
)
|
41
|
+
end
|
42
|
+
|
43
|
+
def get_transactions_page(offset)
|
44
|
+
resp = api_client.transactions.get(
|
45
|
+
access_token,
|
46
|
+
begin_date_str,
|
47
|
+
end_date_str,
|
48
|
+
account_ids: [account_id],
|
49
|
+
offset: offset
|
50
|
+
)
|
51
|
+
[resp.transactions, resp.total_transactions]
|
52
|
+
end
|
53
|
+
|
54
|
+
def all_transactions
|
55
|
+
return @all_transactions if @all_transactions
|
56
|
+
list, total = get_transactions_page(0)
|
57
|
+
while list.length < total
|
58
|
+
new, total = get_transactions_page(list.length)
|
59
|
+
list += new
|
60
|
+
end
|
61
|
+
@all_transactions = list
|
62
|
+
end
|
63
|
+
|
64
|
+
def begin_date_str
|
65
|
+
@begin_date_str ||= date_str(begin_date)
|
66
|
+
end
|
67
|
+
|
68
|
+
def end_date_str
|
69
|
+
@end_date_str ||= date_str(end_date)
|
70
|
+
end
|
71
|
+
|
72
|
+
def date_str(date)
|
73
|
+
date.strftime('%Y-%m-%d')
|
74
|
+
end
|
75
|
+
|
76
|
+
def accounts
|
77
|
+
@accounts ||= api_client.accounts.get(access_token)['accounts']
|
78
|
+
end
|
79
|
+
|
80
|
+
def account_id
|
81
|
+
@account_id ||= accounts.find do |x|
|
82
|
+
x['name'] == account_clean_name
|
83
|
+
end['account_id']
|
84
|
+
end
|
85
|
+
|
86
|
+
def account_clean_name
|
87
|
+
@account_clean_name ||= @options[:name] || raise(
|
88
|
+
'No account name provided'
|
89
|
+
)
|
90
|
+
end
|
91
|
+
|
92
|
+
def client_id
|
93
|
+
@client_id ||= creds(PLAID_DOMAIN, 'client_id')
|
94
|
+
end
|
95
|
+
|
96
|
+
def secret_key
|
97
|
+
@secret_key ||= creds(PLAID_DOMAIN, 'secret_key')
|
98
|
+
end
|
99
|
+
|
100
|
+
def public_key
|
101
|
+
@public_key ||= creds(PLAID_DOMAIN, 'public_key')
|
102
|
+
end
|
103
|
+
|
104
|
+
def access_token
|
105
|
+
@access_token ||= creds(PLAID_DOMAIN, account_name)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
data/lib/burglar/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: burglar
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Les Aker
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2019-
|
11
|
+
date: 2019-05-26 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: cymbal
|
@@ -100,14 +100,14 @@ dependencies:
|
|
100
100
|
requirements:
|
101
101
|
- - "~>"
|
102
102
|
- !ruby/object:Gem::Version
|
103
|
-
version: 0.
|
103
|
+
version: 0.7.0
|
104
104
|
type: :development
|
105
105
|
prerelease: false
|
106
106
|
version_requirements: !ruby/object:Gem::Requirement
|
107
107
|
requirements:
|
108
108
|
- - "~>"
|
109
109
|
- !ruby/object:Gem::Version
|
110
|
-
version: 0.
|
110
|
+
version: 0.7.0
|
111
111
|
- !ruby/object:Gem::Dependency
|
112
112
|
name: rake
|
113
113
|
requirement: !ruby/object:Gem::Requirement
|
@@ -142,14 +142,14 @@ dependencies:
|
|
142
142
|
requirements:
|
143
143
|
- - "~>"
|
144
144
|
- !ruby/object:Gem::Version
|
145
|
-
version: 0.
|
145
|
+
version: 0.67.2
|
146
146
|
type: :development
|
147
147
|
prerelease: false
|
148
148
|
version_requirements: !ruby/object:Gem::Requirement
|
149
149
|
requirements:
|
150
150
|
- - "~>"
|
151
151
|
- !ruby/object:Gem::Version
|
152
|
-
version: 0.
|
152
|
+
version: 0.67.2
|
153
153
|
description: Tool for parsing data from bank websites
|
154
154
|
email: me@lesaker.org
|
155
155
|
executables:
|
@@ -175,8 +175,7 @@ files:
|
|
175
175
|
- lib/burglar/helpers/creds.rb
|
176
176
|
- lib/burglar/helpers/ledger.rb
|
177
177
|
- lib/burglar/helpers/mechanize.rb
|
178
|
-
- lib/burglar/modules/
|
179
|
-
- lib/burglar/modules/american_express.rb
|
178
|
+
- lib/burglar/modules/plaid.rb
|
180
179
|
- lib/burglar/version.rb
|
181
180
|
- spec/burglar_spec.rb
|
182
181
|
- spec/spec_helper.rb
|
data/lib/burglar/modules/ally.rb
DELETED
@@ -1,156 +0,0 @@
|
|
1
|
-
require 'json'
|
2
|
-
require 'csv'
|
3
|
-
require 'date'
|
4
|
-
require 'digest/sha1'
|
5
|
-
|
6
|
-
module LogCabin
|
7
|
-
module Modules
|
8
|
-
##
|
9
|
-
# Ally
|
10
|
-
module Ally # rubocop:disable Metrics/ModuleLength
|
11
|
-
include Burglar.helpers.find(:creds)
|
12
|
-
include Burglar.helpers.find(:mechanize)
|
13
|
-
include Burglar.helpers.find(:ledger)
|
14
|
-
|
15
|
-
ALLY_DOMAIN = 'https://secure.ally.com'.freeze
|
16
|
-
ALLY_CSRF_URL = ALLY_DOMAIN + '/capi-gw/session/status/olbWeb'
|
17
|
-
ALLY_AUTH_URL = ALLY_DOMAIN + '/capi-gw/customer/authentication'
|
18
|
-
ALLY_AUTH_PATCH_URL = ALLY_AUTH_URL + '?_method=PATCH'
|
19
|
-
ALLY_MFA_URL = ALLY_DOMAIN + '/capi-gw/notification'
|
20
|
-
ALLY_DEVICE_URL = ALLY_DOMAIN + '/capi-gw/customer/device'
|
21
|
-
ALLY_DEVICE_PATCH_URL = ALLY_DEVICE_URL + '?_method=PATCH'
|
22
|
-
ALLY_ACCOUNT_URL = ALLY_DOMAIN + '/capi-gw/accounts'
|
23
|
-
|
24
|
-
def raw_transactions
|
25
|
-
csv.map do |x|
|
26
|
-
amount = format('$%.2f', x[:amount] * -1)
|
27
|
-
simple_ledger(x[:date], x[:description], amount)
|
28
|
-
end
|
29
|
-
end
|
30
|
-
|
31
|
-
private
|
32
|
-
|
33
|
-
def default_account_name
|
34
|
-
'Assets:' + @options[:name].gsub(/\s/, '')
|
35
|
-
end
|
36
|
-
|
37
|
-
def raw_csv_url
|
38
|
-
ALLY_DOMAIN + "/capi-gw/accounts/#{account_id}/transactions.csv"
|
39
|
-
end
|
40
|
-
|
41
|
-
def raw_csv
|
42
|
-
csv_headers = headers('text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8') # rubocop:disable Metrics/LineLength
|
43
|
-
@raw_csv ||= mech.get(raw_csv_url, csv_data, nil, csv_headers)
|
44
|
-
end
|
45
|
-
|
46
|
-
def csv
|
47
|
-
@csv ||= CSV.new(
|
48
|
-
raw_csv.body,
|
49
|
-
headers: true,
|
50
|
-
header_converters: :symbol,
|
51
|
-
converters: %i[date float]
|
52
|
-
).map(&:to_h)
|
53
|
-
end
|
54
|
-
|
55
|
-
def csv_data
|
56
|
-
@csv_data ||= {
|
57
|
-
'fromDate' => begin_date.strftime('%Y-%m-%d'),
|
58
|
-
'toDate' => end_date.strftime('%Y-%m-%d'),
|
59
|
-
'status' => 'Posted'
|
60
|
-
}
|
61
|
-
end
|
62
|
-
|
63
|
-
def user
|
64
|
-
@user ||= @options[:user]
|
65
|
-
end
|
66
|
-
|
67
|
-
def password
|
68
|
-
@password ||= creds(ALLY_DOMAIN, user)
|
69
|
-
end
|
70
|
-
|
71
|
-
def csrf_token
|
72
|
-
mech
|
73
|
-
@csrf_token ||= mech.get(ALLY_CSRF_URL).response['csrfchallengetoken']
|
74
|
-
end
|
75
|
-
|
76
|
-
def device_token
|
77
|
-
return @device_token if @device_token
|
78
|
-
res = `system_profiler SPHardwareDataType`
|
79
|
-
raw_token = res.lines.grep(/Serial Number/).first.split.last
|
80
|
-
@device_token = Digest::SHA1.hexdigest raw_token
|
81
|
-
end
|
82
|
-
|
83
|
-
def headers(accept, spname = nil)
|
84
|
-
{
|
85
|
-
'CSRFChallengeToken' => csrf_token,
|
86
|
-
'ApplicationName' => 'AOB',
|
87
|
-
'ApplicationId' => 'ALLYUSBOLB',
|
88
|
-
'ApplicationVersion' => '1.0',
|
89
|
-
'Accept' => accept,
|
90
|
-
'spname' => spname
|
91
|
-
}.reject { |_, v| v.nil? }
|
92
|
-
end
|
93
|
-
|
94
|
-
def auth_headers
|
95
|
-
@auth_headers ||= headers('application/v1+json', 'auth')
|
96
|
-
end
|
97
|
-
|
98
|
-
def common_data
|
99
|
-
@common_data = {
|
100
|
-
'channelType' => 'OLB',
|
101
|
-
'devicePrintRSA' => device_token
|
102
|
-
}
|
103
|
-
end
|
104
|
-
|
105
|
-
def auth_data
|
106
|
-
@auth_data = common_data.merge(
|
107
|
-
'userNamePvtEncrypt' => user,
|
108
|
-
'passwordPvtBlock' => password,
|
109
|
-
'rememberMeFlag' => 'false'
|
110
|
-
)
|
111
|
-
end
|
112
|
-
|
113
|
-
def setup_mech
|
114
|
-
auth = mech.post(ALLY_AUTH_URL, auth_data, auth_headers)
|
115
|
-
auth_res = JSON.parse(auth.body)
|
116
|
-
mfa(auth_res) if auth_res['authentication']['mfa']
|
117
|
-
end
|
118
|
-
|
119
|
-
def mfa_data(payload)
|
120
|
-
mfa_methods = payload['authentication']['mfa']['mfaDeliveryMethods']
|
121
|
-
mfa_id = mfa_methods.first['deliveryMethodId']
|
122
|
-
common_data.merge('deliveryMethodId' => mfa_id)
|
123
|
-
end
|
124
|
-
|
125
|
-
def mfa_token
|
126
|
-
UserInput.new(message: 'MFA token', validation: /^\d+$/).ask
|
127
|
-
end
|
128
|
-
|
129
|
-
def mfa(payload)
|
130
|
-
mech.post(ALLY_MFA_URL, mfa_data(payload), auth_headers)
|
131
|
-
data = common_data.merge('otpCodePvtBlock' => mfa_token)
|
132
|
-
mech.post(ALLY_AUTH_PATCH_URL, data, auth_headers)
|
133
|
-
mech.post(ALLY_DEVICE_PATCH_URL, common_data, auth_headers)
|
134
|
-
end
|
135
|
-
|
136
|
-
def accounts
|
137
|
-
account_headers = headers('application/vnd.api+json', 'common-api')
|
138
|
-
@accounts ||= JSON.parse(
|
139
|
-
mech.get(ALLY_ACCOUNT_URL, [], nil, account_headers).body
|
140
|
-
)
|
141
|
-
end
|
142
|
-
|
143
|
-
def account
|
144
|
-
return @account if @account
|
145
|
-
list = accounts['accounts']['deposit']['accountSummary']
|
146
|
-
match = list.find { |x| x['accountNickname'].match @options[:name] }
|
147
|
-
raise('No matching account found') unless match
|
148
|
-
@account = match
|
149
|
-
end
|
150
|
-
|
151
|
-
def account_id
|
152
|
-
@account_id ||= account['accountId']
|
153
|
-
end
|
154
|
-
end
|
155
|
-
end
|
156
|
-
end
|
@@ -1,79 +0,0 @@
|
|
1
|
-
require 'csv'
|
2
|
-
require 'date'
|
3
|
-
|
4
|
-
module LogCabin
|
5
|
-
module Modules
|
6
|
-
##
|
7
|
-
# American Express
|
8
|
-
module AmericanExpress
|
9
|
-
include Burglar.helpers.find(:creds)
|
10
|
-
include Burglar.helpers.find(:ledger)
|
11
|
-
include Burglar.helpers.find(:mechanize)
|
12
|
-
|
13
|
-
# rubocop:disable Metrics/LineLength
|
14
|
-
AMEX_DOMAIN = 'https://online.americanexpress.com'.freeze
|
15
|
-
AMEX_LOGIN_PATH = '/myca/logon/us/action/LogonHandler?request_type=LogonHandler&Face=en_US'.freeze
|
16
|
-
AMEX_LOGIN_FORM = 'lilo_formLogon'.freeze
|
17
|
-
AMEX_CSV_PATH = '/myca/estmt/us/downloadTxn.do'.freeze
|
18
|
-
# rubocop:enable Metrics/LineLength
|
19
|
-
|
20
|
-
def raw_transactions
|
21
|
-
@raw_transactions ||= csv.map do |row|
|
22
|
-
raw_date, raw_amount, raw_name = row.values_at(0, 7, 11)
|
23
|
-
date = Date.strptime(raw_date, '%m/%d/%Y %a')
|
24
|
-
amount = format('$%.2f', raw_amount)
|
25
|
-
name = raw_name.empty? ? 'Amex Payment' : raw_name.downcase
|
26
|
-
simple_ledger(date, name, amount)
|
27
|
-
end
|
28
|
-
end
|
29
|
-
|
30
|
-
private
|
31
|
-
|
32
|
-
def default_account_name
|
33
|
-
'Liabilities:Credit:american_express'.freeze
|
34
|
-
end
|
35
|
-
|
36
|
-
def user
|
37
|
-
@user ||= @options[:user]
|
38
|
-
end
|
39
|
-
|
40
|
-
def password
|
41
|
-
@password ||= creds(AMEX_DOMAIN, user)
|
42
|
-
end
|
43
|
-
|
44
|
-
def setup_mech
|
45
|
-
mech.user_agent = 'chrome'
|
46
|
-
page = mech.get(AMEX_DOMAIN + AMEX_LOGIN_PATH)
|
47
|
-
form = page.form_with(id: AMEX_LOGIN_FORM) do |f|
|
48
|
-
f.UserID = user
|
49
|
-
f.Password = password
|
50
|
-
end
|
51
|
-
form.submit
|
52
|
-
end
|
53
|
-
|
54
|
-
def csv
|
55
|
-
CSV.parse(csv_page.body)
|
56
|
-
end
|
57
|
-
|
58
|
-
def csv_page
|
59
|
-
params = static_fields.merge(
|
60
|
-
'startDate' => begin_date.strftime('%m%d%Y'),
|
61
|
-
'endDate' => end_date.strftime('%m%d%Y')
|
62
|
-
)
|
63
|
-
mech.post(AMEX_DOMAIN + AMEX_CSV_PATH, params)
|
64
|
-
end
|
65
|
-
|
66
|
-
def static_fields
|
67
|
-
@static_fields ||= {
|
68
|
-
'request_type' => 'authreg_Statement',
|
69
|
-
'downloadType' => 'C',
|
70
|
-
'downloadView' => 'C',
|
71
|
-
'downloadWithETDTool' => 'true',
|
72
|
-
'viewType' => 'L',
|
73
|
-
'reportType' => '1',
|
74
|
-
'BPIndex' => '-99'
|
75
|
-
}.freeze
|
76
|
-
end
|
77
|
-
end
|
78
|
-
end
|
79
|
-
end
|