m1_api 0.0.4 → 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/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
|