m1_api 0.0.4 → 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/lib/m1_api.rb +63 -17
- data/lib/m1_api/api_configs.yml +49 -10
- data/lib/m1_api/maintenance_helpers.rb +46 -0
- data/lib/m1_api/version.rb +1 -1
- data/lib/m1_api/yaml_helpers.rb +14 -3
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 475e1c6fb04452314313521cd450258bdbe267261bf832473296682027a35c9b
|
4
|
+
data.tar.gz: db5ebe7aaf45bed0c93ebc132d19d47017d5faba256ff489b6145c085d019c6d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 25cf2ab1e505f41201feff61a3a02349531c8fd1bbc1a36259d80c4d1d9a4384e3759cf92832eaeb58bc7f6df306cdb2477f91cf895c0d801bf143265be4f885
|
7
|
+
data.tar.gz: 960d1e1f5650eead60a3a23396a37d8fd7e2fcaf7cf9324e38eef2b90ea24b8eb7c7addb533ee7e331a0f3ee4c3b6fb46d648ca01bf6c9899a97d78f238ec091
|
data/lib/m1_api.rb
CHANGED
@@ -1,21 +1,38 @@
|
|
1
1
|
require_relative './m1_api/yaml_helpers.rb'
|
2
2
|
|
3
|
+
# open browser
|
4
|
+
# open network logs
|
5
|
+
# save entire thing as har
|
6
|
+
# parse
|
7
|
+
# should match the logs against when the actions were performed
|
8
|
+
|
3
9
|
class M1API
|
4
10
|
# autoload :CLI, 'm1_api/cli'
|
5
11
|
autoload :VERSION, 'm1_api/version'
|
6
12
|
|
7
|
-
attr_accessor :api_config_file, :token
|
13
|
+
attr_accessor :api_config_file, :api_configs, :token, :accounts, :accounts_detail
|
14
|
+
|
8
15
|
|
9
|
-
|
16
|
+
def output(string)
|
17
|
+
puts string
|
18
|
+
end
|
10
19
|
|
11
|
-
def initialize(username, password, api_config_file =
|
12
|
-
@
|
20
|
+
def initialize(username, password, api_config_file = "#{__dir__}/m1_api/api_configs.yml")
|
21
|
+
@accounts = {}
|
22
|
+
@accounts_detail = {}
|
23
|
+
load_config_file(api_config_file)
|
13
24
|
credentials = { username: username, password: password }
|
14
|
-
res = YamlHelpers.
|
25
|
+
res = YamlHelpers.call_api_from_config(@api_configs, :authenticate, credentials)
|
15
26
|
raise "failed to authenticate:\n\t#{res}" unless res[:code] == 200 && res[:body]['data']['authenticate']['result']['didSucceed']
|
16
27
|
@token = res[:body]['data']['authenticate']['accessToken']
|
17
28
|
end
|
18
29
|
|
30
|
+
def load_config_file(api_config_file)
|
31
|
+
# change it to reject both if either fails
|
32
|
+
@api_config_file = api_config_file
|
33
|
+
@api_configs = YamlHelpers.load_yaml(@api_config_file)
|
34
|
+
end
|
35
|
+
|
19
36
|
def self.read_credentials(credentials_file=nil)
|
20
37
|
if credentials_file
|
21
38
|
credentials = YamlHelpers.load_yaml(credentials_file)
|
@@ -25,21 +42,50 @@ class M1API
|
|
25
42
|
end
|
26
43
|
end
|
27
44
|
|
28
|
-
def check_status
|
29
|
-
res = call_api_from_yml(@@api_config_file, 'check_status', token)
|
30
|
-
puts res.inspect
|
31
|
-
end
|
32
|
-
|
33
45
|
def query_accounts
|
34
46
|
accounts = {}
|
35
47
|
data = { token: @token }
|
36
|
-
id_res = YamlHelpers.
|
37
|
-
ids = id_res[:body]['data']['viewer']['
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
48
|
+
id_res = YamlHelpers.call_api_from_config(@api_configs, :list_account_ids, data)
|
49
|
+
ids = id_res[:body]['data']['viewer']['accounts']['edges'].each do |account|
|
50
|
+
accounts[account['node']['nickname']] = account['node']['id']
|
51
|
+
end
|
52
|
+
@accounts = accounts
|
53
|
+
end
|
54
|
+
|
55
|
+
def query_account_detail(account_id)
|
56
|
+
data = { token: @token, account_id: account_id }
|
57
|
+
detail = YamlHelpers.call_api_from_config(@api_configs, :query_account_detail, data)[:body]['data']['node']
|
58
|
+
@accounts_detail[account_id] = {
|
59
|
+
status: detail['status'],
|
60
|
+
bank: detail['lastAchRelationship'],
|
61
|
+
transfers: detail['_achTransfers']['edges']
|
62
|
+
}
|
63
|
+
@accounts_detail
|
64
|
+
end
|
65
|
+
|
66
|
+
def deposit(action, account_id, bank_id, transaction)
|
67
|
+
case action
|
68
|
+
when 'deposit'
|
69
|
+
data = { token: @token, account_id: account_id, bank_id: bank_id, amount: transaction }
|
70
|
+
YamlHelpers.call_api_from_config(@api_configs, :deposit, data)
|
71
|
+
when 'cancel'
|
72
|
+
data = { token: @token, account_id: account_id, bank_id: bank_id, transfer_id: transaction }
|
73
|
+
YamlHelpers.call_api_from_config(@api_configs, :cancel_deposit, data)
|
74
|
+
else
|
75
|
+
output "Invalid deposit action: '#{action}'"
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def withdraw(action, account_id, bank_id, transaction)
|
80
|
+
case action
|
81
|
+
when 'withdraw'
|
82
|
+
data = { token: @token, account_id: account_id, bank_id: bank_id, amount: transaction }
|
83
|
+
YamlHelpers.call_api_from_config(@api_configs, :withdraw, data)
|
84
|
+
when 'cancel'
|
85
|
+
data = { token: @token, account_id: account_id, bank_id: bank_id, transfer_id: transaction }
|
86
|
+
YamlHelpers.call_api_from_config(@api_configs, :cancel_withdraw, data)
|
87
|
+
else
|
88
|
+
output "Invalid withdraw action: '#{action}'"
|
42
89
|
end
|
43
|
-
accounts
|
44
90
|
end
|
45
91
|
end
|
data/lib/m1_api/api_configs.yml
CHANGED
@@ -7,7 +7,7 @@
|
|
7
7
|
:Content-Type: application/json
|
8
8
|
:Accept-Encoding: gzip, deflate, br
|
9
9
|
:Accept-Language: en-GB,en-US;q=0.9,en;q=0.8
|
10
|
-
:X-Client-Id: m1-web/3.20.
|
10
|
+
:X-Client-Id: m1-web/3.20.10 # changes regularly, hasn't broken anything yet
|
11
11
|
:X-Client-Sentinel: <%= ((Time.new).to_i * 1000).to_s %>
|
12
12
|
|
13
13
|
:authenticate:
|
@@ -17,8 +17,9 @@
|
|
17
17
|
:Accept: application/json
|
18
18
|
:Referer: https://dashboard.m1finance.com/login
|
19
19
|
<<: *common_headers
|
20
|
-
:body: '{"query":"
|
20
|
+
:body: '{"query":"mutation M($input: AuthenticateInput!) {authenticate(input: $input) {result {didSucceed inputError} accessToken refreshToken viewer {user {id correlationKey}}}} ","variables":{"input":{"clientMutationId":"authenticate19","username":"<<<username>>>","password":"<<<password>>>"}}}'
|
21
21
|
|
22
|
+
# lists first 1000 accounts - need to paginate if there are more
|
22
23
|
:list_account_ids:
|
23
24
|
:method: post
|
24
25
|
:url: *base_url
|
@@ -27,7 +28,7 @@
|
|
27
28
|
:Accept: '*/*'
|
28
29
|
:Referer: https://dashboard.m1finance.com/login
|
29
30
|
<<: *common_headers
|
30
|
-
:body: '{"query":"query
|
31
|
+
:body: '{"query":"query Routes($first_0:Int!) {viewer {id,...F1}} fragment F0 on Account {nickname,id} fragment F1 on Viewer {profile {primary {firstName,lastName}},accounts:accounts(first:$first_0) {edges {node {id,...F0},cursor},pageInfo {hasNextPage,hasPreviousPage}},id}","variables":{"first_0":1000}}'
|
31
32
|
|
32
33
|
:query_account:
|
33
34
|
:method: post
|
@@ -39,27 +40,65 @@
|
|
39
40
|
<<: *common_headers
|
40
41
|
:body: '{"query":"query Routes($id_0:ID!) {node(id:$id_0) {id,__typename,...F1}} fragment F0 on Account {isRetirement,achTransferLimit,lastAchRelationship {id,nickname,__typename},balance {totalValue {value}},id} fragment F1 on Account {registration,id,...F0}","variables":{"id_0":"<<<account_id>>>"}}'
|
41
42
|
|
43
|
+
:query_bank_id:
|
44
|
+
:method: post
|
45
|
+
:url: *base_url
|
46
|
+
:headers:
|
47
|
+
:Accept: '*/*'
|
48
|
+
:Referer: https://dashboard.m1finance.com/d/invest/portfolio
|
49
|
+
:Authorization: Bearer <<<token>>>
|
50
|
+
<<: *common_headers
|
51
|
+
:body: '{"query":"query Routes($id_0:ID!) {node(id:$id_0) {id,__typename,...F1}} fragment F0 on Account {isLiquidating,lastAchRelationship {__typename,status,isActive,id},id} fragment F1 on Account {id,...F0}","variables":{"id_0":"<<<account_id>>>"}}'
|
52
|
+
|
53
|
+
:query_account_detail:
|
54
|
+
:method: post
|
55
|
+
:url: *base_url
|
56
|
+
:headers:
|
57
|
+
:Accept: '*/*'
|
58
|
+
:Referer: https://dashboard.m1finance.com/d/funding
|
59
|
+
:Authorization: Bearer <<<token>>>
|
60
|
+
<<: *common_headers
|
61
|
+
:body: '{"query":"query Routes($id_0:ID!,$first_1:Int!,$number_2:Int!) {node(id:$id_0) {id,__typename,...Fc}} fragment F0 on AchRelationshipViaLink {toExternalAccount {id,name,accountNumber,institutionCode},id} fragment F1 on AchRelationshipViaDeposits {id,nickname,bankAccountNumber,bankAccountType} fragment F2 on AchRelationship {__typename,id,internalId,rejectionReason,nickname,...F0,...F1} fragment F3 on AchRelationship {__typename,id,isActive} fragment F4 on AchRelationship {id,isActive,__typename} fragment F5 on Account {id,balance {cash {available}},maxCashThreshold} fragment F6 on Account {id,status,isRetirement,isLiquidating,balance {totalValue {hasValue}}} fragment F7 on Account {id,fundingTotals {totalsByYear {year,totalDeposits,totalWithdrawals}}} fragment F8 on Account {iraContributionTotals {totalsByYear {year,totalContribution,maxContribution}},id} fragment F9 on Account {isRetirement,id,...F7,...F8} fragment Fa on Account {nickname,id} fragment Fb on Account {id,registration,isRetirement,balance {margin {availableForWithdrawal}},_achTransfers:achTransfers(first:$first_1) {pageInfo {hasNextPage,hasPreviousPage},edges {node {__typename,id,direction,status,isComplete,isCreatedBySchedule,amount,contributionYear,forLiquidation,lastUpdate,viaAchRelationship {id,nickname,__typename}},cursor}},_achTransferSchedules1pTWSD:achTransferSchedules(first:$first_1) {pageInfo {hasNextPage,hasPreviousPage},edges {node {__typename,id,isDeposit,amount,description {description,_nextInstances1zNmZ6:nextInstances(number:$number_2,ofTrading:true,inTradingTimezone:true)},viaAchRelationship {id,nickname,__typename}},cursor}},...Fa} fragment Fc on Account {id,status,balance {totalValue {value}},lastAchRelationship {__typename,id,...F2,...F3,...F4},...F5,...F6,...F9,...Fb}","variables":{"id_0":"<<<account_id>>>","first_1":8,"number_2":10}}'
|
62
|
+
|
42
63
|
:deposit:
|
43
|
-
:method:
|
64
|
+
:method: post
|
44
65
|
:url: *base_url
|
45
66
|
:headers:
|
46
67
|
:Accept: application/json
|
47
68
|
:Referer: https://dashboard.m1finance.com/d/c/deposit-funds
|
48
69
|
:Authorization: Bearer <<<token>>>
|
49
|
-
|
70
|
+
<<: *common_headers
|
71
|
+
:body: '{"query":"mutation CreateImmediateAchDeposit($input_0:CreateImmediateAchDepositInput!,$first_1:Int!) {createImmediateAchDeposit(input:$input_0) {clientMutationId,...F1,...F2}} fragment F0 on Account {lastAchRelationship {__typename,status,id,isActive,nickname},id,rootPortfolioSlice {id},estimatedTrading {id,hasTrades},_achTransferSchedules2RSrrY:achTransferSchedules(first:$first_1) {edges {node {id},cursor},pageInfo {hasNextPage,hasPreviousPage}}} fragment F1 on CreateImmediateAchDepositPayload {account {id,...F0}} fragment F2 on CreateImmediateAchDepositPayload {result {didSucceed,inputError}}","variables":{"input_0":{"accountId":"<<<account_id>>>","achRelationshipId":"<<<bank_id>>>","amount":"<<<amount>>>","clientMutationId":"1"},"first_1":1}}'
|
50
72
|
|
51
73
|
:cancel_deposit:
|
52
|
-
:method:
|
74
|
+
:method: post
|
53
75
|
:url: *base_url
|
54
76
|
:headers:
|
55
77
|
:Accept: application/json
|
56
|
-
:Referer: https://dashboard.m1finance.com/d/
|
78
|
+
:Referer: https://dashboard.m1finance.com/d/funding
|
57
79
|
:Authorization: Bearer <<<token>>>
|
58
|
-
|
80
|
+
<<: *common_headers
|
81
|
+
:body: '{"query":"mutation CancelAchTransfer($input_0:CancelAchTransferInput!,$first_1:Int!) {cancelAchTransfer(input:$input_0) {clientMutationId,...F1,...F2}} fragment F0 on Account {id,isLiquidating,_achTransfers1iwpJk:achTransfers(first:$first_1) {pageInfo {hasNextPage,hasPreviousPage},edges {node {__typename,id,direction,status,isComplete,isCreatedBySchedule,amount,contributionYear,forLiquidation,lastUpdate,viaAchRelationship {id,nickname,__typename}},cursor}}} fragment F1 on CancelAchTransferPayload {account {id,...F0}} fragment F2 on CancelAchTransferPayload {result {didSucceed,inputError}}","variables":{"input_0":{"accountId":"<<<account_id>>>","achRelationshipId":"<<<bank_id>>>","achTransferId":"<<<transfer_id>>>","clientMutationId":"2"},"first_1":8}}'
|
59
82
|
|
60
|
-
:
|
61
|
-
:method:
|
83
|
+
:withdraw:
|
84
|
+
:method: post
|
62
85
|
:url: *base_url
|
86
|
+
:headers:
|
87
|
+
:Accept: application/json
|
88
|
+
:Referer: https://dashboard.m1finance.com/d/c/withdraw-funds/confirm
|
89
|
+
:Authorization: Bearer <<<token>>>
|
90
|
+
<<: *common_headers
|
91
|
+
:body: '{"query":"mutation CreateImmediateAchWithdrawal($input_0:CreateImmediateAchWithdrawalInput!) {createImmediateAchWithdrawal(input:$input_0) {clientMutationId,...F1,...F2}} fragment F0 on Account {id} fragment F1 on CreateImmediateAchWithdrawalPayload {achTransferEdge {cursor,__typename,node {__typename,id,direction,status,isComplete,isCreatedBySchedule,amount,contributionYear,forLiquidation,lastUpdate,viaAchRelationship {id,nickname,__typename}}},account {id,...F0}} fragment F2 on CreateImmediateAchWithdrawalPayload {result {didSucceed,inputError}}","variables":{"input_0":{"accountId":"<<<account_id>>>","achRelationshipId":"<<<bank_id>>>","amount":"<<<amount>>>","clientMutationId":"3"}}}'
|
92
|
+
|
93
|
+
:cancel_withdraw:
|
94
|
+
:method: post
|
95
|
+
:url: *base_url
|
96
|
+
:headers:
|
97
|
+
:Accept: application/json
|
98
|
+
:Referer: https://dashboard.m1finance.com/d/funding
|
99
|
+
:Authorization: Bearer <<<token>>>
|
100
|
+
<<: *common_headers
|
101
|
+
:body: '{"query":"mutation CancelAchTransfer($input_0:CancelAchTransferInput!,$first_1:Int!) {cancelAchTransfer(input:$input_0) {clientMutationId,...F1,...F2}} fragment F0 on Account {id,isLiquidating,_achTransfers1iwpJk:achTransfers(first:$first_1) {pageInfo {hasNextPage,hasPreviousPage},edges {node {__typename,id,direction,status,isComplete,isCreatedBySchedule,amount,contributionYear,forLiquidation,lastUpdate,viaAchRelationship {id,nickname,__typename}},cursor}}} fragment F1 on CancelAchTransferPayload {account {id,...F0}} fragment F2 on CancelAchTransferPayload {result {didSucceed,inputError}}","variables":{"input_0":{"accountId":"<<<account_id>>>","achRelationshipId":"<<<bank_id>>>","achTransferId":"<<<transfer_id>>>","clientMutationId":"4"},"first_1":8}}'
|
63
102
|
|
64
103
|
:test_get:
|
65
104
|
:method: get
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
# need some semi automated way of maintaining apis as they change
|
4
|
+
# start selenium web session, authenticate manually, then run scripts to go through flows
|
5
|
+
# parse the generated .har file and compare against current api configs file
|
6
|
+
# might have to do fuzzy matching of the schema?
|
7
|
+
|
8
|
+
# create git conflict for each call list as an accept/reject
|
9
|
+
|
10
|
+
# to do the replace string, should convert this to a class
|
11
|
+
|
12
|
+
module MaintenanceHelpers
|
13
|
+
module_function
|
14
|
+
|
15
|
+
def load_har(path_to_file)
|
16
|
+
out = []
|
17
|
+
JSON.parse(File.read(path_to_file))['log']['entries'].each do |call|
|
18
|
+
out << {
|
19
|
+
start_time: call['startTime'],
|
20
|
+
request: {
|
21
|
+
method: call['request']['method'],
|
22
|
+
url: call['request']['url'],
|
23
|
+
headers: call['request']['headers'],
|
24
|
+
body: call['request']['postData']
|
25
|
+
},
|
26
|
+
response: {
|
27
|
+
status: call['response']['status'],
|
28
|
+
content: call['response']['content']
|
29
|
+
}
|
30
|
+
}
|
31
|
+
end
|
32
|
+
out
|
33
|
+
end
|
34
|
+
|
35
|
+
def filter_by_top_value(calls, key, value)
|
36
|
+
if value.is_a? Regexp
|
37
|
+
calls.select { |call| call if call[:request][key].match?(value) }
|
38
|
+
else
|
39
|
+
calls.select { |call| call if call[:request][key].include?(value) }
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def replace_value_with_key(data, matcher, key)
|
44
|
+
|
45
|
+
end
|
46
|
+
end
|
data/lib/m1_api/version.rb
CHANGED
data/lib/m1_api/yaml_helpers.rb
CHANGED
@@ -35,7 +35,13 @@ module YamlHelpers
|
|
35
35
|
raise 'input is not a array' unless array.is_a?(Array)
|
36
36
|
dup = array.clone
|
37
37
|
dup.each_with_index do |value, index|
|
38
|
-
|
38
|
+
if value.is_a?(String)
|
39
|
+
dup[index] = replace_dynamic_string(value, context)
|
40
|
+
elsif value.is_a?(Array)
|
41
|
+
dup[index] = replace_dynamic_array(value, context)
|
42
|
+
elsif value.is_a?(Hash)
|
43
|
+
dup[index] = replace_dynamic_hash(value, context)
|
44
|
+
end
|
39
45
|
end
|
40
46
|
dup.join
|
41
47
|
end
|
@@ -54,8 +60,8 @@ module YamlHelpers
|
|
54
60
|
hash
|
55
61
|
end
|
56
62
|
|
57
|
-
def
|
58
|
-
config =
|
63
|
+
def call_api_from_config(configs, api, data = {})
|
64
|
+
config = configs[api].dup
|
59
65
|
raise "no api defined for #{api}" unless config
|
60
66
|
context = config.merge data
|
61
67
|
parsed_config = replace_dynamic_hash(context)
|
@@ -67,4 +73,9 @@ module YamlHelpers
|
|
67
73
|
return { code: res.code, body: res.body } if res
|
68
74
|
puts "failed to call api for api #{api}: #{e}"
|
69
75
|
end
|
76
|
+
|
77
|
+
def call_api_from_yml(config_file, api, data = {})
|
78
|
+
configs = load_yaml(config_file)
|
79
|
+
call_api_from_config(configs, api, data)
|
80
|
+
end
|
70
81
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: m1_api
|
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
|
- Yuan Feng
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2019-01-
|
11
|
+
date: 2019-01-21 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rest-client
|
@@ -47,6 +47,7 @@ files:
|
|
47
47
|
- Readme.md
|
48
48
|
- lib/m1_api.rb
|
49
49
|
- lib/m1_api/api_configs.yml
|
50
|
+
- lib/m1_api/maintenance_helpers.rb
|
50
51
|
- lib/m1_api/version.rb
|
51
52
|
- lib/m1_api/yaml_helpers.rb
|
52
53
|
homepage: http://github.com/ynotgnef/m1_api
|