m1_api 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/Readme.md +6 -0
- data/lib/m1_api/api_configs.yml +66 -0
- data/lib/m1_api/version.rb +3 -0
- data/lib/m1_api.rb +127 -0
- metadata +75 -0
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,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
|
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: []
|