burglar 0.0.3 → 0.1.0
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/.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
|