m1_api 0.0.1

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: '08d1d5911c6c9b467ec3b624636792f2e2f25e84ab21a71eb2b80e3de5ea507c'
4
+ data.tar.gz: 5c1352c418bbd48c61d44b523e7b7e09450b5c848838b8d748aa12d82f30979f
5
+ SHA512:
6
+ metadata.gz: 1544448680d618f9b45e33109abd9b783e41fed0a84d8531f12c6db9d9a4c3bc1f5701bfbf6e1cd6ff1b73998f171f37d239bcd0d6109ef8fa57beda696eed53
7
+ data.tar.gz: 57ea8a1e016e181d023b77cc3fbbdb300b4f5c6f0fde513eae16ae229d280009f74e6927891bb898584ee08930dfd7fbd657af524cfda232492eb3c40ec1720b
data/Readme.md ADDED
@@ -0,0 +1,6 @@
1
+ ## M1 API
2
+ Access M1 Finance functionalities via API
3
+
4
+ #todos
5
+ Everything
6
+ Automate api intercept and parse to keep endpoints up to date if they change
@@ -0,0 +1,66 @@
1
+ :base_url: &base_url
2
+ - https://lens.m1finance.com/graphql
3
+
4
+ :common_headers: &common_headers
5
+ :Origin: https://dashboard.m1finance.com
6
+ :User-Agent: Mozilla/5.0 (X11; CrOS x86_64 11021.81.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36
7
+ :Content-Type: application/json
8
+ :Accept-Encoding: gzip, deflate, br
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
11
+ :X-Client-Sentinel: <%= ((Time.new).to_i * 1000).to_s %>
12
+
13
+ :authenticate:
14
+ :method: post
15
+ :url: *base_url
16
+ :headers:
17
+ :Accept: application/json
18
+ :Referer: https://dashboard.m1finance.com/login
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>>>"}}}'
21
+
22
+ :list_account_ids:
23
+ :method: post
24
+ :url: *base_url
25
+ :headers:
26
+ :Authorization: Bearer <<<token>>>
27
+ :Accept: '*/*'
28
+ :Referer: https://dashboard.m1finance.com/login
29
+ <<: *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
+
32
+ :query_account:
33
+ :method: post
34
+ :url: *base_url
35
+ :headers:
36
+ :Authorization: Bearer <<<token>>>
37
+ :Accept: '*/*'
38
+ :Referer: https://dashboard.m1finance.com/d/c/deposit-funds
39
+ <<: *common_headers
40
+ :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
+ :deposit:
43
+ :method:
44
+ :url: *base_url
45
+ :headers:
46
+ :Accept: application/json
47
+ :Referer: https://dashboard.m1finance.com/d/c/deposit-funds
48
+ :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":"QUNDOjVRTTM1NTUz","achRelationshipId":"UkVMOjVRTTM1NTUzLDQ3ZDYzYzdhLTI1ZWItNDkzZC1hMzRhLWQzNTE4MGRjNzFkZg==","amount":"<<<deposit_amount>>>","clientMutationId":"1"},"first_1":1}}'
50
+
51
+ :cancel_deposit:
52
+ :method:
53
+ :url: *base_url
54
+ :headers:
55
+ :Accept: application/json
56
+ :Referer: https://dashboard.m1finance.com/d/c/deposit-funds
57
+ :Authorization: Bearer <<<token>>>
58
+ :body: ''
59
+
60
+ :check_status:
61
+ :method:
62
+ :url: *base_url
63
+
64
+ :test_get:
65
+ :method: get
66
+ :url: https://google.com
@@ -0,0 +1,3 @@
1
+ module M1API
2
+ VERSION = Version = '0.0.1'
3
+ end
data/lib/m1_api.rb ADDED
@@ -0,0 +1,127 @@
1
+ require 'rest-client'
2
+ require 'json'
3
+ require 'yaml'
4
+ require 'erb'
5
+
6
+ ###
7
+ ### change it to just authenticate once and use the same token
8
+
9
+
10
+ module M1API
11
+ # autoload :CLI, 'm1_api/cli'
12
+ autoload :VERSION, 'm1_api/version'
13
+
14
+ class << self
15
+
16
+ @@api_config_file = "#{__dir__}/m1_api/api_configs.yml"
17
+
18
+ def define_custom_api_file(path)
19
+ @@api_config_file = path
20
+ end
21
+
22
+ def read_credentials(credentials_file=nil)
23
+ if credentials_file
24
+ credentials = load_yaml(credentials_file)
25
+ { username: credentials[:M1_USERNAME], password: credentials[:M1_PASSWORD] }
26
+ else
27
+ {username: ENV['M1_USERNAME'], password: ENV['M1_PASSWORD']}
28
+ end
29
+ end
30
+
31
+ def load_yaml(file_path)
32
+ YAML.load(ERB.new(File.read(file_path)).result) || {}
33
+ rescue SystemCallError
34
+ raise "Could not load file: '#{file_path}"
35
+ end
36
+
37
+ def call_api(api_configs_file, api)
38
+ raise 'nyi'
39
+ api_config = load_yaml(api_configs_file)[api]
40
+ raise "No api '#{api}' defined in '#{api_configs_file}'" unless api_config
41
+ params = parse_api_config(api_config)
42
+ JSON.parse(RestClient.send(params['method'], params['url']), params['body'], params['headers'])
43
+ end
44
+
45
+ # might have to convert everything to sym first
46
+ def replace_dynamic_string(string, context)
47
+ raise 'input is not a string' unless string.is_a?(String)
48
+ replace_targets = string.split('>>>').map { |target| target.match(/<<<.*/).to_s }
49
+ replace_targets.each do |target|
50
+ key = target.match(/<<<(.*)/)
51
+ if key
52
+ temp_value = context
53
+ key[1].split(' ').each do |current_key|
54
+ raise "no value '#{current_key}' defined in context" unless temp_value.key?(current_key.to_sym)
55
+ temp_value = temp_value[current_key.to_sym]
56
+ end
57
+ string = string.gsub("#{target}>>>", temp_value)
58
+ end
59
+ end
60
+ string
61
+ end
62
+
63
+ # need something to deal with uri encode
64
+ def replace_dynamic_array(array, context)
65
+ raise 'input is not a array' unless array.is_a?(Array)
66
+ dup = array.clone
67
+ dup.each_with_index do |value, index|
68
+ dup[index] = replace_dynamic_string(value, context)
69
+ end
70
+ dup.join
71
+ end
72
+
73
+ def replace_dynamic_hash(hash, context = hash)
74
+ raise 'input is not a hash' unless hash.is_a?(Hash)
75
+ hash.each do |key, value|
76
+ if value.is_a?(String)
77
+ hash[key] = replace_dynamic_string(value, context)
78
+ elsif value.is_a?(Array)
79
+ hash[key] = replace_dynamic_array(value, context)
80
+ elsif value.is_a?(Hash)
81
+ hash[key] = replace_dynamic_hash(value, context)
82
+ end
83
+ end
84
+ hash
85
+ end
86
+
87
+ def call_api_from_yml(config_file, api, data = {})
88
+ config = load_yaml(config_file)[api.to_sym]
89
+ raise "no api defined for #{api}" unless config
90
+ context = config.merge data
91
+ parsed_config = replace_dynamic_hash(context)
92
+ params = [parsed_config[:method], parsed_config[:url], parsed_config[:body], parsed_config[:headers]]
93
+ params.delete(nil)
94
+ res = RestClient.send(*params)
95
+ { code: res.code, body: JSON.parse(res.body) }
96
+ rescue Exception => e
97
+ return { code: res.code, body: res.body } if res
98
+ puts "failed to call api for api #{api}: #{e}"
99
+ end
100
+
101
+ def authenticate(credentials_file = nil)
102
+ credentials = read_credentials(credentials_file)
103
+ res = call_api_from_yml(@@api_config_file, 'authenticate', credentials)
104
+ raise "failed to authenticate:\n\t#{res}" unless res[:code] == 200 && res[:body]['data']['authenticate']['result']['didSucceed']
105
+ res[:body]['data']['authenticate']['accessToken']
106
+ end
107
+
108
+ def check_status(token)
109
+ token = { token: token }
110
+ res = call_api_from_yml(@@api_config_file, 'check_status', token)
111
+ puts res.inspect
112
+ end
113
+
114
+ def query_accounts(token)
115
+ accounts = {}
116
+ data = { token: token }
117
+ id_res = call_api_from_yml(@@api_config_file, 'list_account_ids', data)
118
+ ids = id_res[:body]['data']['viewer']['_accounts1NFCow']['edges'].map { |account| account['node']['id'] }
119
+ ids.each do |id|
120
+ data[:account_id] = id
121
+ account_res = call_api_from_yml(@@api_config_file, 'query_account', data)
122
+ accounts[id] = account_res[:body]['data']['node']
123
+ end
124
+ accounts
125
+ end
126
+ end
127
+ end
metadata ADDED
@@ -0,0 +1,75 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: m1_api
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Yuan Feng
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2019-01-05 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rest-client
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: json
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ description:
42
+ email: thefunkyphresh@gmail.com
43
+ executables: []
44
+ extensions: []
45
+ extra_rdoc_files: []
46
+ files:
47
+ - Readme.md
48
+ - lib/m1_api.rb
49
+ - lib/m1_api/api_configs.yml
50
+ - lib/m1_api/version.rb
51
+ homepage: http://github.com/ynot_gnef/m1_api
52
+ licenses:
53
+ - MIT
54
+ metadata: {}
55
+ post_install_message:
56
+ rdoc_options: []
57
+ require_paths:
58
+ - lib
59
+ required_ruby_version: !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - ">="
62
+ - !ruby/object:Gem::Version
63
+ version: 2.2.0
64
+ required_rubygems_version: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ requirements: []
70
+ rubyforge_project:
71
+ rubygems_version: 2.7.8
72
+ signing_key:
73
+ specification_version: 4
74
+ summary: Access M1 Finance functionalities via API
75
+ test_files: []