burglar 0.0.1 → 0.0.2
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/.circle-ruby +3 -0
- data/.prospectus +21 -7
- data/.rubocop.yml +5 -2
- data/Gemfile +0 -1
- data/README.md +64 -3
- data/Rakefile +1 -2
- data/bin/burglar +33 -0
- data/burglar.gemspec +17 -6
- data/circle.yml +3 -8
- data/lib/burglar.rb +43 -0
- data/lib/burglar/bank.rb +40 -0
- data/lib/burglar/heist.rb +21 -0
- data/lib/burglar/helpers/creds.rb +14 -0
- data/lib/burglar/helpers/ledger.rb +24 -0
- data/lib/burglar/helpers/mechanize.rb +16 -0
- data/lib/burglar/modules/ally.rb +156 -0
- data/lib/burglar/modules/american_express.rb +79 -0
- data/lib/burglar/version.rb +7 -0
- data/spec/burglar_spec.rb +0 -1
- metadata +98 -17
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 81787646ec2d922aea291dfaf4de5fcb7d08eb01
|
4
|
+
data.tar.gz: a329034499aa42ad0ef1a8ec79459d1cf89fa50e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4d9e2cc34ec13edf2daa8e1ea132061ef0bd660b43991ddac1cbdd175d9d3fca126510c79542ed9bb92545b1c9867828d1fb9bb3f66074e31a661ddaa7aead81
|
7
|
+
data.tar.gz: 6b236a999b709735b7bfc130dd57ad321c391d478cd4004beebc14585b5056c8b18a6dca41fae9e74fcb701e9cf201595b190c92742d773f48cf3eaf57175172
|
data/.circle-ruby
ADDED
data/.prospectus
CHANGED
@@ -1,11 +1,25 @@
|
|
1
|
+
Prospectus.extra_dep('file', 'prospectus_circleci')
|
2
|
+
|
3
|
+
my_slug = 'akerl/burglar'
|
4
|
+
|
1
5
|
item do
|
2
|
-
|
3
|
-
static
|
4
|
-
set 'green'
|
5
|
-
end
|
6
|
+
noop
|
6
7
|
|
7
|
-
|
8
|
-
|
9
|
-
|
8
|
+
deps do
|
9
|
+
item do
|
10
|
+
name 'gems'
|
11
|
+
|
12
|
+
expected do
|
13
|
+
static
|
14
|
+
set 'green'
|
15
|
+
end
|
16
|
+
|
17
|
+
actual do
|
18
|
+
gemnasium
|
19
|
+
slug my_slug
|
20
|
+
end
|
21
|
+
end
|
10
22
|
end
|
23
|
+
|
24
|
+
extend ProspectusCircleci::Build.new(my_slug)
|
11
25
|
end
|
data/.rubocop.yml
CHANGED
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -3,15 +3,76 @@ burglar
|
|
3
3
|
|
4
4
|
[](https://rubygems.org/gems/burglar)
|
5
5
|
[](https://gemnasium.com/akerl/burglar)
|
6
|
-
[](https://circleci.com/gh/akerl/burglar)
|
6
|
+
[](https://circleci.com/gh/akerl/burglar)
|
7
7
|
[](https://codecov.io/github/akerl/burglar)
|
8
|
-
[](https://www.codacy.com/app/akerl/burglar)
|
9
9
|
[](https://tldrlegal.com/license/mit-license)
|
10
10
|
|
11
|
-
Tool for parsing data from bank websites
|
11
|
+
Tool for parsing data from bank websites into the format of [ledger](http://ledger-cli.org/)
|
12
12
|
|
13
13
|
## Usage
|
14
14
|
|
15
|
+
Burglar operates on the concept of `modules` for each bank's site. The module does the heavy lifting to hit the API or scrape the site for data.
|
16
|
+
|
17
|
+
### Running burglar
|
18
|
+
|
19
|
+
Burglar supports a couple of command line flags, each with a sane default:
|
20
|
+
|
21
|
+
```
|
22
|
+
-c FILE, --config FILE Config file to load
|
23
|
+
-b DATE, --begin DATE Beginning of date range
|
24
|
+
-e DATE, --end DATE End of date range
|
25
|
+
```
|
26
|
+
|
27
|
+
The config file will default to `~/.burglar.yml`, begin defaults to 30 days ago, and end defaults to today.
|
28
|
+
|
29
|
+
### Configuration file
|
30
|
+
|
31
|
+
Here's an initial example, which should be mostly self explanatory.
|
32
|
+
|
33
|
+
```
|
34
|
+
banks:
|
35
|
+
amex:
|
36
|
+
type: american_express
|
37
|
+
user: akerl
|
38
|
+
account: Liabilities:Credit:amex
|
39
|
+
```
|
40
|
+
|
41
|
+
The banks object is a hash of named hashes, each one is an account that will be polled. Each account *must* have a `type`, which is the name of the module to use. You can also pass an `account`, which sets the account name for the ledger output (if not set, modules can attempt to provide a default). Other configuration depends on the module.
|
42
|
+
|
43
|
+
### Modules
|
44
|
+
|
45
|
+
#### American Express
|
46
|
+
|
47
|
+
This pulls from the American Express site by scraping a CSV.
|
48
|
+
|
49
|
+
Configuration:
|
50
|
+
|
51
|
+
* [required] user: your American Express username
|
52
|
+
|
53
|
+
#### Ally
|
54
|
+
|
55
|
+
This pulls from the Ally site by scraping a CSV.
|
56
|
+
|
57
|
+
* [required] user: your Ally username
|
58
|
+
* [required] name: the nickname of the specific account
|
59
|
+
|
60
|
+
### Helpers
|
61
|
+
|
62
|
+
Helpers exist to centralize common activity that modules can rely on.
|
63
|
+
|
64
|
+
#### Creds
|
65
|
+
|
66
|
+
This uses [keylime](https://github.com/akerl/keylime) to pull creds from an OSX keychain
|
67
|
+
|
68
|
+
#### Ledger
|
69
|
+
|
70
|
+
This helps convert transactions into ledger entries using [libledger](https://github.com/akerl/libledger)
|
71
|
+
|
72
|
+
#### Mechanize
|
73
|
+
|
74
|
+
This provides a [Mechanize](https://github.com/sparklemotion/mechanize) client for modules that want to scrape the bank website
|
75
|
+
|
15
76
|
## Installation
|
16
77
|
|
17
78
|
gem install burglar
|
data/Rakefile
CHANGED
@@ -7,8 +7,7 @@ RSpec::Core::RakeTask.new(:spec)
|
|
7
7
|
|
8
8
|
desc 'Run Rubocop on the gem'
|
9
9
|
RuboCop::RakeTask.new(:rubocop) do |task|
|
10
|
-
task.patterns = ['lib/**/*.rb', 'spec/**/*.rb']
|
11
10
|
task.fail_on_error = true
|
12
11
|
end
|
13
12
|
|
14
|
-
task default: [
|
13
|
+
task default: %i[spec rubocop build install]
|
data/bin/burglar
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'burglar'
|
4
|
+
require 'mercenary'
|
5
|
+
require 'cymbal'
|
6
|
+
require 'yaml'
|
7
|
+
|
8
|
+
def add_common_opts(c)
|
9
|
+
c.option :config, '-c FILE', '--config FILE', 'Config file to load'
|
10
|
+
c.option :begin, '-b DATE', '--begin DATE', 'Beginning of date range'
|
11
|
+
c.option :end, '-e DATE', '--end DATE', 'End of date range'
|
12
|
+
end
|
13
|
+
|
14
|
+
def load_config(file)
|
15
|
+
file ||= '~/.burglar.yml'
|
16
|
+
file = File.expand_path file
|
17
|
+
Cymbal.symbolize YAML.safe_load(File.read(file))
|
18
|
+
end
|
19
|
+
|
20
|
+
Mercenary.program(:burglar) do |p|
|
21
|
+
p.version Burglar::VERSION
|
22
|
+
p.description 'Load data from banks'
|
23
|
+
p.syntax 'burglar [options]'
|
24
|
+
|
25
|
+
add_common_opts(p)
|
26
|
+
|
27
|
+
p.action do |_, options|
|
28
|
+
options[:end] ||= Date.today
|
29
|
+
options[:begin] ||= options[:end] - 30
|
30
|
+
config = load_config(options[:config]).merge(options)
|
31
|
+
puts Burglar.new(config).transactions
|
32
|
+
end
|
33
|
+
end
|
data/burglar.gemspec
CHANGED
@@ -1,10 +1,14 @@
|
|
1
|
+
require 'English'
|
2
|
+
$LOAD_PATH.unshift File.expand_path('../lib/', __FILE__)
|
3
|
+
require 'burglar/version'
|
4
|
+
|
1
5
|
Gem::Specification.new do |s|
|
2
6
|
s.name = 'burglar'
|
3
|
-
s.version =
|
7
|
+
s.version = Burglar::VERSION
|
4
8
|
s.date = Time.now.strftime('%Y-%m-%d')
|
5
9
|
|
6
10
|
s.summary = 'Tool for parsing data from bank websites'
|
7
|
-
s.description =
|
11
|
+
s.description = 'Tool for parsing data from bank websites'
|
8
12
|
s.authors = ['Les Aker']
|
9
13
|
s.email = 'me@lesaker.org'
|
10
14
|
s.homepage = 'https://github.com/akerl/burglar'
|
@@ -12,10 +16,17 @@ Gem::Specification.new do |s|
|
|
12
16
|
|
13
17
|
s.files = `git ls-files`.split
|
14
18
|
s.test_files = `git ls-files spec/*`.split
|
19
|
+
s.executables = ['burglar']
|
20
|
+
|
21
|
+
s.add_dependency 'cymbal', '~> 1.0.0'
|
22
|
+
s.add_dependency 'libledger', '~> 0.0.3'
|
23
|
+
s.add_dependency 'logcabin', '~> 0.1.3'
|
24
|
+
s.add_dependency 'mercenary', '~> 0.3.4'
|
15
25
|
|
16
|
-
s.add_development_dependency 'rubocop', '~> 0.35.0'
|
17
|
-
s.add_development_dependency 'rake', '~> 10.4.0'
|
18
26
|
s.add_development_dependency 'codecov', '~> 0.1.1'
|
19
|
-
s.add_development_dependency '
|
20
|
-
s.add_development_dependency '
|
27
|
+
s.add_development_dependency 'fuubar', '~> 2.2.0'
|
28
|
+
s.add_development_dependency 'goodcop', '~> 0.1.0'
|
29
|
+
s.add_development_dependency 'rake', '~> 12.3.0'
|
30
|
+
s.add_development_dependency 'rspec', '~> 3.7.0'
|
31
|
+
s.add_development_dependency 'rubocop', '~> 0.51.0'
|
21
32
|
end
|
data/circle.yml
CHANGED
@@ -1,12 +1,7 @@
|
|
1
1
|
dependencies:
|
2
2
|
override:
|
3
|
-
|
4
|
-
|
5
|
-
- 'rvm-exec 2.2.2 bundle install'
|
6
|
-
- 'rvm-exec 2.3.1 bundle install'
|
3
|
+
- 'for i in $(cat .circle-ruby) ; do rvm-exec $i gem update --system --no-doc || exit 1 ; done'
|
4
|
+
- 'for i in $(cat .circle-ruby) ; do rvm-exec $i bundle install || exit 1 ; done'
|
7
5
|
test:
|
8
6
|
override:
|
9
|
-
|
10
|
-
- 'rvm-exec 2.1.6 bundle exec rake'
|
11
|
-
- 'rvm-exec 2.2.2 bundle exec rake'
|
12
|
-
- 'rvm-exec 2.3.1 bundle exec rake'
|
7
|
+
- 'for i in $(cat .circle-ruby) ; do rvm-exec $i bundle exec rake || exit 1 ; done'
|
data/lib/burglar.rb
CHANGED
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'logcabin'
|
2
|
+
require 'libledger'
|
3
|
+
|
4
|
+
##
|
5
|
+
# This module provides a unified interface for pulling accountig transactions
|
6
|
+
module Burglar
|
7
|
+
class << self
|
8
|
+
##
|
9
|
+
# Insert a helper .new() method for creating a new Heist object
|
10
|
+
|
11
|
+
def new(*args)
|
12
|
+
self::Heist.new(*args)
|
13
|
+
end
|
14
|
+
|
15
|
+
def modules
|
16
|
+
@modules ||= LogCabin.new(load_path: load_path(:modules))
|
17
|
+
end
|
18
|
+
|
19
|
+
def helpers
|
20
|
+
@helpers ||= LogCabin.new(load_path: load_path(:helpers))
|
21
|
+
end
|
22
|
+
|
23
|
+
def extra_dep(name, dep)
|
24
|
+
require dep
|
25
|
+
rescue LoadError
|
26
|
+
raise("The #{name} module requires the #{dep} gem")
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def gem_dir
|
32
|
+
Gem::Specification.find_by_name('burglar').gem_dir
|
33
|
+
end
|
34
|
+
|
35
|
+
def load_path(type)
|
36
|
+
File.join(gem_dir, 'lib', 'burglar', type.to_s)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
require 'burglar/version'
|
42
|
+
require 'burglar/heist'
|
43
|
+
require 'burglar/bank'
|
data/lib/burglar/bank.rb
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
module Burglar
|
2
|
+
##
|
3
|
+
# Single bank's information
|
4
|
+
class Bank
|
5
|
+
def initialize(params = {})
|
6
|
+
@options = params
|
7
|
+
extend module_obj
|
8
|
+
end
|
9
|
+
|
10
|
+
def transactions
|
11
|
+
Ledger.new(entries: raw_transactions)
|
12
|
+
end
|
13
|
+
|
14
|
+
def begin_date
|
15
|
+
@begin_date ||= @options[:begin]
|
16
|
+
end
|
17
|
+
|
18
|
+
def end_date
|
19
|
+
@end_date ||= @options[:end]
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def account_name
|
25
|
+
@account_name ||= @options[:account] || default_account_name
|
26
|
+
end
|
27
|
+
|
28
|
+
def default_account_name
|
29
|
+
raise('Module failed to override default_account_name')
|
30
|
+
end
|
31
|
+
|
32
|
+
def type
|
33
|
+
@type ||= @options[:type] || raise('Must supply an account type')
|
34
|
+
end
|
35
|
+
|
36
|
+
def module_obj
|
37
|
+
@module_obj ||= Burglar.modules.find(type) || raise("No module: #{type}")
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Burglar
|
2
|
+
##
|
3
|
+
# Collection of banks
|
4
|
+
class Heist
|
5
|
+
def initialize(params = {})
|
6
|
+
@options = params
|
7
|
+
end
|
8
|
+
|
9
|
+
def banks
|
10
|
+
@banks ||= @options[:banks].map do |k, v|
|
11
|
+
[k, Burglar::Bank.new(@options.merge(v))]
|
12
|
+
end.to_h
|
13
|
+
end
|
14
|
+
|
15
|
+
def transactions
|
16
|
+
@transactions ||= Ledger.new(
|
17
|
+
entries: banks.map { |_, v| v.transactions.entries }.flatten.sort
|
18
|
+
)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
Burglar.extra_dep('creds', 'keylime')
|
2
|
+
|
3
|
+
module LogCabin
|
4
|
+
module Modules
|
5
|
+
##
|
6
|
+
# Provide a helper to access OSX keychain credentials
|
7
|
+
module Creds
|
8
|
+
def creds(server, account)
|
9
|
+
credential = Keylime.new(server: server, account: account)
|
10
|
+
credential.get!("Enter password for #{server} (#{account})").password
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module LogCabin
|
2
|
+
module Modules
|
3
|
+
##
|
4
|
+
# Provide a helper to create simple Ledger objects
|
5
|
+
module Ledger
|
6
|
+
def simple_ledger(date, name, amount)
|
7
|
+
::Ledger::Entry.new(
|
8
|
+
name: name,
|
9
|
+
state: date > Date.today ? :pending : :cleared,
|
10
|
+
date: date.strftime('%Y/%m/%d'),
|
11
|
+
actions: [
|
12
|
+
{ name: guess_action(name), amount: amount },
|
13
|
+
{ name: account_name }
|
14
|
+
]
|
15
|
+
)
|
16
|
+
end
|
17
|
+
|
18
|
+
def guess_action(name)
|
19
|
+
guess = `ledger xact '#{name.delete("'")}' 2>/dev/null`.split("\n")[1]
|
20
|
+
guess ? guess.split.first : 'Expenses:generic'
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
Burglar.extra_dep('mechanize', 'mechanize')
|
2
|
+
|
3
|
+
module LogCabin
|
4
|
+
module Modules
|
5
|
+
##
|
6
|
+
# Provide a helper to scrape websites
|
7
|
+
module Mechanize
|
8
|
+
def mech
|
9
|
+
return @mech if @mech
|
10
|
+
@mech = ::Mechanize.new
|
11
|
+
setup_mech if respond_to? :setup_mech, true
|
12
|
+
@mech
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,156 @@
|
|
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
|
@@ -0,0 +1,79 @@
|
|
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
|
data/spec/burglar_spec.rb
CHANGED
metadata
CHANGED
@@ -1,43 +1,71 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: burglar
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Les Aker
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2017-12-07 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
|
-
name:
|
14
|
+
name: cymbal
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
17
|
- - "~>"
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: 0.
|
20
|
-
type: :
|
19
|
+
version: 1.0.0
|
20
|
+
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
24
|
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version: 0.
|
26
|
+
version: 1.0.0
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
|
-
name:
|
28
|
+
name: libledger
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
31
|
- - "~>"
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version:
|
34
|
-
type: :
|
33
|
+
version: 0.0.3
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 0.0.3
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: logcabin
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 0.1.3
|
48
|
+
type: :runtime
|
35
49
|
prerelease: false
|
36
50
|
version_requirements: !ruby/object:Gem::Requirement
|
37
51
|
requirements:
|
38
52
|
- - "~>"
|
39
53
|
- !ruby/object:Gem::Version
|
40
|
-
version:
|
54
|
+
version: 0.1.3
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: mercenary
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 0.3.4
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: 0.3.4
|
41
69
|
- !ruby/object:Gem::Dependency
|
42
70
|
name: codecov
|
43
71
|
requirement: !ruby/object:Gem::Requirement
|
@@ -52,40 +80,84 @@ dependencies:
|
|
52
80
|
- - "~>"
|
53
81
|
- !ruby/object:Gem::Version
|
54
82
|
version: 0.1.1
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: fuubar
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: 2.2.0
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: 2.2.0
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: goodcop
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: 0.1.0
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - "~>"
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: 0.1.0
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: rake
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - "~>"
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: 12.3.0
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - "~>"
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: 12.3.0
|
55
125
|
- !ruby/object:Gem::Dependency
|
56
126
|
name: rspec
|
57
127
|
requirement: !ruby/object:Gem::Requirement
|
58
128
|
requirements:
|
59
129
|
- - "~>"
|
60
130
|
- !ruby/object:Gem::Version
|
61
|
-
version: 3.
|
131
|
+
version: 3.7.0
|
62
132
|
type: :development
|
63
133
|
prerelease: false
|
64
134
|
version_requirements: !ruby/object:Gem::Requirement
|
65
135
|
requirements:
|
66
136
|
- - "~>"
|
67
137
|
- !ruby/object:Gem::Version
|
68
|
-
version: 3.
|
138
|
+
version: 3.7.0
|
69
139
|
- !ruby/object:Gem::Dependency
|
70
|
-
name:
|
140
|
+
name: rubocop
|
71
141
|
requirement: !ruby/object:Gem::Requirement
|
72
142
|
requirements:
|
73
143
|
- - "~>"
|
74
144
|
- !ruby/object:Gem::Version
|
75
|
-
version:
|
145
|
+
version: 0.51.0
|
76
146
|
type: :development
|
77
147
|
prerelease: false
|
78
148
|
version_requirements: !ruby/object:Gem::Requirement
|
79
149
|
requirements:
|
80
150
|
- - "~>"
|
81
151
|
- !ruby/object:Gem::Version
|
82
|
-
version:
|
152
|
+
version: 0.51.0
|
83
153
|
description: Tool for parsing data from bank websites
|
84
154
|
email: me@lesaker.org
|
85
|
-
executables:
|
155
|
+
executables:
|
156
|
+
- burglar
|
86
157
|
extensions: []
|
87
158
|
extra_rdoc_files: []
|
88
159
|
files:
|
160
|
+
- ".circle-ruby"
|
89
161
|
- ".gitignore"
|
90
162
|
- ".prospectus"
|
91
163
|
- ".rspec"
|
@@ -94,9 +166,18 @@ files:
|
|
94
166
|
- LICENSE
|
95
167
|
- README.md
|
96
168
|
- Rakefile
|
169
|
+
- bin/burglar
|
97
170
|
- burglar.gemspec
|
98
171
|
- circle.yml
|
99
172
|
- lib/burglar.rb
|
173
|
+
- lib/burglar/bank.rb
|
174
|
+
- lib/burglar/heist.rb
|
175
|
+
- lib/burglar/helpers/creds.rb
|
176
|
+
- lib/burglar/helpers/ledger.rb
|
177
|
+
- lib/burglar/helpers/mechanize.rb
|
178
|
+
- lib/burglar/modules/ally.rb
|
179
|
+
- lib/burglar/modules/american_express.rb
|
180
|
+
- lib/burglar/version.rb
|
100
181
|
- spec/burglar_spec.rb
|
101
182
|
- spec/spec_helper.rb
|
102
183
|
homepage: https://github.com/akerl/burglar
|
@@ -119,7 +200,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
119
200
|
version: '0'
|
120
201
|
requirements: []
|
121
202
|
rubyforge_project:
|
122
|
-
rubygems_version: 2.
|
203
|
+
rubygems_version: 2.6.14
|
123
204
|
signing_key:
|
124
205
|
specification_version: 4
|
125
206
|
summary: Tool for parsing data from bank websites
|