burglar 0.0.1 → 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
[![Gem Version](https://img.shields.io/gem/v/burglar.svg)](https://rubygems.org/gems/burglar)
|
5
5
|
[![Dependency Status](https://img.shields.io/gemnasium/akerl/burglar.svg)](https://gemnasium.com/akerl/burglar)
|
6
|
-
[![Build Status](https://img.shields.io/circleci/project/akerl/burglar.svg)](https://circleci.com/gh/akerl/burglar)
|
6
|
+
[![Build Status](https://img.shields.io/circleci/project/akerl/burglar/master.svg)](https://circleci.com/gh/akerl/burglar)
|
7
7
|
[![Coverage Status](https://img.shields.io/codecov/c/github/akerl/burglar.svg)](https://codecov.io/github/akerl/burglar)
|
8
|
-
[![Code Quality](https://img.shields.io/codacy
|
8
|
+
[![Code Quality](https://img.shields.io/codacy/940f3f131d724124a68a0be7d21f951b.svg)](https://www.codacy.com/app/akerl/burglar)
|
9
9
|
[![MIT Licensed](https://img.shields.io/badge/license-MIT-green.svg)](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
|