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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 880305cc13e0cde33cdabd9c30e1d4b8dff25a337cb6363bbb574deff09fd656
4
- data.tar.gz: 03c84cca83c77623a41fe1715bcad2b5396fc440a3e0fccb0d2494017e81fa7f
3
+ metadata.gz: 475e1c6fb04452314313521cd450258bdbe267261bf832473296682027a35c9b
4
+ data.tar.gz: db5ebe7aaf45bed0c93ebc132d19d47017d5faba256ff489b6145c085d019c6d
5
5
  SHA512:
6
- metadata.gz: 518a8c44b1dedba1c6790d4383bf22102588c1c45208c733c38c5520cb5da97b7e89a715df7346ee1c1a5b115da48a5f2ac241b22c7e73936de703479eaad47e
7
- data.tar.gz: 61ad53f7ddac12bf53d60fe9eb4c6f78158f366ea45fd9ba8525746106bdb7f1f1e45ac81d32b00b27bf438f80329065818f5bcede0608cdf22a9507cb0b9fb7
6
+ metadata.gz: 25cf2ab1e505f41201feff61a3a02349531c8fd1bbc1a36259d80c4d1d9a4384e3759cf92832eaeb58bc7f6df306cdb2477f91cf895c0d801bf143265be4f885
7
+ data.tar.gz: 960d1e1f5650eead60a3a23396a37d8fd7e2fcaf7cf9324e38eef2b90ea24b8eb7c7addb533ee7e331a0f3ee4c3b6fb46d648ca01bf6c9899a97d78f238ec091
@@ -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
- api_config_file = "#{__dir__}/m1_api/api_configs.yml"
16
+ def output(string)
17
+ puts string
18
+ end
10
19
 
11
- def initialize(username, password, api_config_file = nil)
12
- @api_config_file = api_config_file || "#{__dir__}/m1_api/api_configs.yml"
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.call_api_from_yml(@api_config_file, 'authenticate', credentials)
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.call_api_from_yml(@api_config_file, 'list_account_ids', data)
37
- ids = id_res[:body]['data']['viewer']['_accounts1NFCow']['edges'].map { |account| account['node']['id'] }
38
- ids.each do |id|
39
- data[:account_id] = id
40
- account_res = YamlHelpers.call_api_from_yml(@api_config_file, 'query_account', data)
41
- accounts[id] = account_res[:body]['data']['node']
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
@@ -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.1 #might have to update in real time
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":"\n mutation M($input: AuthenticateInput!) {\n authenticate(input: $input) {\n result {\n didSucceed\n inputError\n }\n accessToken\n refreshToken\n viewer {\n user {\n id\n correlationKey\n }\n }\n }\n }\n ","variables":{"input":{"clientMutationId":"authenticate19","username":"<<<username>>>","password":"<<<password>>>"}}}'
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 Active($first_0:Int!,$filterStatus_1:[AccountStatusEnum!]!) {viewer {_accounts1NFCow:accounts(first:$first_0,filterStatus:$filterStatus_1) {edges {node {id},cursor},pageInfo {hasNextPage,hasPreviousPage}},id}}","variables":{"first_0":1,"filterStatus_1":["NEW","OPENED","REJECTED"]}}'
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
- :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":"<<<deposit_amount>>>","clientMutationId":"1"},"first_1":1}}'
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/c/deposit-funds
78
+ :Referer: https://dashboard.m1finance.com/d/funding
57
79
  :Authorization: Bearer <<<token>>>
58
- :body: ''
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
- :check_status:
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
@@ -1,3 +1,3 @@
1
1
  class M1API
2
- VERSION = Version = '0.0.4'
2
+ VERSION = Version = '0.1.0'
3
3
  end
@@ -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
- dup[index] = replace_dynamic_string(value, context)
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 call_api_from_yml(config_file, api, data = {})
58
- config = load_yaml(config_file)[api.to_sym]
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
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-06 00:00:00.000000000 Z
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