m1_api 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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: []