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 +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: []
|