mixin_bot 0.0.1.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
+ SHA1:
3
+ metadata.gz: b67635be46f18811338468b260f58143536dfba2
4
+ data.tar.gz: e5695a7e88236e880bf997d1d0880f0dca8ac08c
5
+ SHA512:
6
+ metadata.gz: 4a80cf2b75cf00e1ce0726e504f70ae83a97683421b8c19dc846060cd6cfccc224898ee896c3f60e5fed29945afb8964a7c28d59e54606f67490a661256e33d5
7
+ data.tar.gz: e67b7bdc244c2995bd1ae33c044d4a7d7872c1c2bf5a1e70cd8a211f713ce4a1b2a3ded2a011e1904080d84c2e92a74f5704b02e3462796c6d169d1a08ffd282
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2018 an-lee
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/lib/mixin_bot.rb ADDED
@@ -0,0 +1,17 @@
1
+ require 'active_support/all'
2
+ require 'http'
3
+ require 'base64'
4
+ require 'openssl'
5
+ require 'jwt'
6
+ require 'jose'
7
+ require_relative './mixin_bot/api'
8
+
9
+ module MixinBot
10
+ class<< self
11
+ attr_accessor :client_id, :client_secret, :session_id, :pin_token, :private_key, :scope
12
+ end
13
+
14
+ def self.api
15
+ @api ||= MixinBot::API.new(options={})
16
+ end
17
+ end
@@ -0,0 +1,32 @@
1
+ require_relative './client'
2
+ require_relative './errors'
3
+ require_relative './api/auth'
4
+ require_relative './api/me'
5
+ require_relative './api/payment'
6
+ require_relative './api/pin'
7
+ require_relative './api/transfer'
8
+ require_relative './api/user'
9
+
10
+ module MixinBot
11
+ class API
12
+ attr_reader :client_id, :client_secret, :session_id, :pin_token, :private_key, :scope
13
+ attr_reader :client
14
+
15
+ def initialize(options={})
16
+ @client_id = options[:client_id] || MixinBot.client_id
17
+ @client_secret = options[:client_secret] || MixinBot.client_secret
18
+ @session_id = options[:session_id] || MixinBot.session_id
19
+ @pin_token = Base64.decode64 options[:pin_token] || MixinBot.pin_token
20
+ @private_key = OpenSSL::PKey::RSA.new options[:private_key] || MixinBot.private_key
21
+ @scope = options[:scope] || MixinBot.scope || 'PROFILE:READ+PHONE:READ+ASSETS:READ'
22
+ @client = Client.new
23
+ end
24
+
25
+ include MixinBot::API::Auth
26
+ include MixinBot::API::Me
27
+ include MixinBot::API::Payment
28
+ include MixinBot::API::Pin
29
+ include MixinBot::API::Transfer
30
+ include MixinBot::API::User
31
+ end
32
+ end
@@ -0,0 +1,39 @@
1
+ module MixinBot
2
+ class API
3
+ module Auth
4
+ def access_token(method, uri, body)
5
+ sig = Digest::SHA256.hexdigest (method + uri + body)
6
+ iat = Time.now.utc.to_i
7
+ exp = (Time.now.utc + 1.day).to_i
8
+ jti = SecureRandom.uuid
9
+ payload = {
10
+ 'uid': client_id,
11
+ 'sid': session_id,
12
+ 'iat': iat,
13
+ 'exp': exp,
14
+ 'jti': jti,
15
+ 'sig': sig
16
+ }
17
+ JWT.encode payload, private_key, 'RS512'
18
+ end
19
+
20
+ def oauth_token(code)
21
+ path = 'oauth/token'
22
+ payload = {
23
+ client_id: client_id,
24
+ client_secret: client_secret,
25
+ code: code
26
+ }
27
+ r = client.post(path, json: payload)
28
+
29
+ raise r.inspect if r['error'].present?
30
+
31
+ return r['data']['access_token']
32
+ end
33
+
34
+ def request_oauth
35
+ format('https://mixin.one/oauth/authorize?client_id=%s&scope=%s', client_id, scope)
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,33 @@
1
+ module MixinBot
2
+ class API
3
+ module Conversation
4
+ def create_conversation
5
+ path = '/conversations'
6
+ access_token ||= self.access_token('GET', path, '')
7
+ params = {
8
+ category: category,
9
+ conversation_id: conversation_id,
10
+ participants: [
11
+ {
12
+ action: 'ADD',
13
+ role: '',
14
+ user_id: user_id
15
+ }
16
+ ]
17
+ }
18
+ end
19
+
20
+ def unique_conversation_id(user_id)
21
+ md5 = Digest::MD5.new
22
+ md5 << user_id
23
+ md5 << client_id
24
+ digest = md5.digest
25
+ digest_6 = (digest[6].ord & 0x0f | 0x30).chr
26
+ digest_8 = (digest[8].ord & 0x3f | 0x80).chr
27
+ cipher = digest[0...6] + digest_6 + digest[7] + digest_8 + digest[9..-1]
28
+ hex = cipher.unpack('H*').first
29
+ conversation_id = format('%s-%s-%s-%s-%s', hex[0..7], hex[8..11], hex[12..15], hex[16..19], hex[20..-1])
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,44 @@
1
+ module MixinBot
2
+ class API
3
+ module Me
4
+ def read_me(access_token=nil)
5
+ path = '/me'
6
+ access_token ||= self.access_token('GET', path, '')
7
+ authorization = format('Bearer %s', access_token)
8
+ client.get(path, headers: { 'Authorization': authorization })
9
+ end
10
+
11
+ def update_me(full_name, avatar_base64, access_token=nil)
12
+ path = '/me'
13
+ payload = {
14
+ "full_name": full_name,
15
+ "avatar_base64": avatar_base64
16
+ }
17
+ access_token ||= self.access_token('POST', path, payload.to_json)
18
+ authorization = format('Bearer %s', access_token)
19
+ client.post(path, headers: { 'Authorization': authorization }, json: payload)
20
+ end
21
+
22
+ def read_assets(access_token=nil)
23
+ path = '/assets'
24
+ access_token ||= self.access_token('GET', path, '')
25
+ authorization = format('Bearer %s', access_token)
26
+ client.get(path, headers: { 'Authorization': authorization })
27
+ end
28
+
29
+ def read_asset(asset_id, access_token=nil)
30
+ path = format('/assets/%s', asset_id)
31
+ access_token ||= self.access_token('GET', path, '')
32
+ authorization = format('Bearer %s', access_token)
33
+ client.get(path, headers: { 'Authorization': authorization })
34
+ end
35
+
36
+ def read_friends(access_token=nil)
37
+ path = '/friends'
38
+ access_token ||= self.access_token('GET', path, '')
39
+ authorization = format('Bearer %s', access_token)
40
+ client.get(path, headers: { 'Authorization': authorization })
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,27 @@
1
+ module MixinBot
2
+ class API
3
+ module Message
4
+ def read_message(data)
5
+ io = StringIO.new(data.pack('c*'), 'rb')
6
+ gzip = Zlib::GzipReader.new io
7
+ msg = gzip.read
8
+ gzip.close
9
+ return msg
10
+ end
11
+
12
+ def write_message(action, params)
13
+ msg = {
14
+ "id": SecureRandom.uuid,
15
+ "action": action,
16
+ "params": params
17
+ }.to_json
18
+
19
+ io = StringIO.new 'wb'
20
+ gzip = Zlib::GzipWriter.new io
21
+ gzip.write msg
22
+ gzip.close
23
+ data = io.string.unpack('c*')
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,31 @@
1
+ module MixinBot
2
+ class API
3
+ module Payment
4
+ def pay_url(options)
5
+ options = options.with_indifferent_access
6
+ recipient_id = options.fetch('recipient_id')
7
+ asset_id = options.fetch('asset_id')
8
+ amount = options.fetch('amount')
9
+ memo = options.fetch('memo')
10
+ trace = options.fetch('trace')
11
+ url = format('https://mixin.one/pay?recipient=%s&asset=%s&amount=%s&trace=%s&memo=%s', recipient_id, asset_id, amount, trace, memo)
12
+ end
13
+
14
+ def verify_payment(options)
15
+ options = options.with_indifferent_access
16
+ recipient_id = options.fetch('recipient_id')
17
+ asset_id = options.fetch('asset_id')
18
+ amount = options.fetch('amount')
19
+ trace = options.fetch('trace')
20
+ path = 'payments'
21
+ payload = {
22
+ asset_id: asset_id,
23
+ opponent_id: recipient_id,
24
+ amount: amount,
25
+ trace_id: trace,
26
+ }
27
+ client.post(path, json: payload)
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,57 @@
1
+ module MixinBot
2
+ class API
3
+ module Pin
4
+ def verify_pin(pin_code, access_token=nil)
5
+ path = '/pin/verify'
6
+ payload = {
7
+ pin: encrypt_pin(pin_code)
8
+ }
9
+
10
+ access_token ||= self.access_token('POST', path, payload.to_json)
11
+ authorization = format('Bearer %s', access_token)
12
+ client.post(path, headers: { 'Authorization': authorization }, json: payload)
13
+ end
14
+
15
+ def decrypt_pin(msg)
16
+ msg = Base64.strict_decode64 msg
17
+ iv = msg[0..15]
18
+ cipher = msg[16..47]
19
+ aes_key = JOSE::JWA::PKCS1::rsaes_oaep_decrypt('SHA256', pin_token, private_key, session_id)
20
+ alg = "AES-256-CBC"
21
+ decode_cipher = OpenSSL::Cipher.new(alg)
22
+ decode_cipher.decrypt
23
+ decode_cipher.iv = iv
24
+ decode_cipher.key = aes_key
25
+ plain = decode_cipher.update(cipher)
26
+ return plain
27
+ end
28
+
29
+ def encrypt_pin(pin_code)
30
+ aes_key = JOSE::JWA::PKCS1::rsaes_oaep_decrypt('SHA256', pin_token, private_key, session_id)
31
+ ts = Time.now.utc.to_i
32
+ tszero = ts % 0x100
33
+ tsone = (ts % 0x10000) >> 8
34
+ tstwo = (ts % 0x1000000) >> 16
35
+ tsthree = (ts % 0x100000000) >> 24
36
+ tsstring = tszero.chr + tsone.chr + tstwo.chr + tsthree.chr + "\0\0\0\0"
37
+ encrypt_content = pin_code + tsstring + tsstring
38
+ pad_count = 16 - encrypt_content.length % 16
39
+ if pad_count > 0
40
+ padded_content = encrypt_content + pad_count.chr * pad_count
41
+ else
42
+ padded_content = encrypt_content
43
+ end
44
+
45
+ alg = "AES-256-CBC"
46
+ aes = OpenSSL::Cipher.new(alg)
47
+ iv = OpenSSL::Cipher.new(alg).random_iv
48
+ aes.encrypt
49
+ aes.key = aes_key
50
+ aes.iv = iv
51
+ cipher = aes.update(padded_content)
52
+ msg = iv + cipher
53
+ return Base64.strict_encode64 msg
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,44 @@
1
+ module MixinBot
2
+ class API
3
+ module Transfer
4
+ def create_transfer(pin, options)
5
+ # data for test:
6
+ # asset_id = '965e5c6e-434c-3fa9-b780-c50f43cd955c'
7
+ # opponent_id = '7ed9292d-7c95-4333-aa48-a8c640064186'
8
+ # amount = '1'
9
+ # encrypted_pin = MixinBot.api_pin.encrypted_pin
10
+ # memo = 'test'
11
+
12
+ options = options.with_indifferent_access
13
+
14
+ asset_id = options.fetch('asset_id')
15
+ opponent_id = options.fetch('opponent_id')
16
+ amount = options.fetch('amount')
17
+ memo = options.fetch('memo')
18
+ trace_id = options.fetch('trace_id')
19
+ trace_id ||= SecureRandom.uuid
20
+
21
+ path = '/transfers'
22
+ payload = {
23
+ asset_id: asset_id,
24
+ opponent_id: opponent_id,
25
+ pin: pin,
26
+ amount: amount,
27
+ trace_id: trace_id,
28
+ memo: memo
29
+ }
30
+
31
+ access_token ||= self.access_token('POST', path, payload.to_json)
32
+ authorization = format('Bearer %s', access_token)
33
+ client.post(path, headers: { 'Authorization': authorization }, json: payload)
34
+ end
35
+
36
+ def read_transfer(trace_id)
37
+ path = format('/transfers/trace/%s', trace_id)
38
+ access_token ||= self.access_token('GET', path, '')
39
+ authorization = format('Bearer %s', access_token)
40
+ client.get(path, headers: { 'Authorization': authorization })
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,31 @@
1
+ module MixinBot
2
+ class API
3
+ module User
4
+ def read_user(user_id, access_token=nil)
5
+ # user_id: Mixin User Id
6
+ path = format('/users/%s', user_id)
7
+ access_token ||= self.access_token('GET', path, '')
8
+ authorization = format('Bearer %s', access_token)
9
+ client.get(path, headers: { 'Authorization': authorization })
10
+ end
11
+
12
+ def search_user(q, access_token=nil)
13
+ # q: Mixin Id or Phone Number
14
+ path = format('/search/%s', q)
15
+ access_token ||= self.access_token('GET', path, '')
16
+ authorization = format('Bearer %s', access_token)
17
+ client.get(path, headers: { 'Authorization': authorization })
18
+ end
19
+
20
+ def fetch_users(user_ids, access_token=nil)
21
+ # user_ids: a array of user_ids
22
+ path = '/users/fetch'
23
+ user_ids = [user_ids] if user_ids.is_a? String
24
+ payload = user_ids
25
+ access_token ||= self.access_token('POST', path, payload.to_json)
26
+ authorization = format('Bearer %s', access_token)
27
+ client.post(path, headers: { 'Authorization': authorization }, json: payload)
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,108 @@
1
+ module MixinBot
2
+ class Client
3
+ SERVER_SCHEME = 'https'.freeze
4
+ SERVER_HOST = 'api.mixin.one'.freeze
5
+
6
+ def get(path, options = {})
7
+ request(:get, path, options)
8
+ end
9
+
10
+ def post(path, options = {})
11
+ request(:post, path, options)
12
+ end
13
+
14
+ private
15
+
16
+ def request(verb, path, options = {})
17
+ uri = uri_for(path)
18
+ options = options.with_indifferent_access
19
+
20
+ options['headers'] ||= {}
21
+ if options['headers']['Content-Type'].blank?
22
+ options['headers']['Content-Type'] = 'application/json'
23
+ end
24
+
25
+ begin
26
+ response = HTTP.timeout(:global, connect: 5, write: 5, read: 5).request(verb, uri, options)
27
+ rescue HTTP::Error => ex
28
+ Rails.logger.error format('%s (%s):', ex.class.name, ex.message)
29
+ Rails.logger.error ex.backtrace.join("\n")
30
+ raise Errors::HttpError, ex.message
31
+ end
32
+
33
+ unless response.status.success?
34
+ raise Errors::APIError.new(nil, response.to_s)
35
+ end
36
+
37
+ parse_response(response) do |parse_as, result|
38
+ case parse_as
39
+ when :json
40
+ break result if result[:errcode].blank? || result[:errcode].zero?
41
+ raise Errors::APIError.new(result[:errcode], result[:errmsg])
42
+ else
43
+ result
44
+ end
45
+ end
46
+ end
47
+
48
+ def uri_for(path)
49
+ uri_options = {
50
+ scheme: SERVER_SCHEME,
51
+ host: SERVER_HOST,
52
+ path: path
53
+ }
54
+ Addressable::URI.new(uri_options)
55
+ end
56
+
57
+ def parse_response(response)
58
+ content_type = response.headers[:content_type]
59
+ parse_as = {
60
+ %r{^application\/json} => :json,
61
+ %r{^image\/.*} => :file,
62
+ %r{^text\/html} => :xml,
63
+ %r{^text\/plain} => :plain
64
+ }.each_with_object([]) { |match, memo| memo << match[1] if content_type =~ match[0] }.first || :plain
65
+
66
+ if parse_as == :plain
67
+ result = ActiveSupport::JSON.decode(response.body.to_s).with_indifferent_access rescue nil
68
+ if result
69
+ return yield(:json, result)
70
+ else
71
+ return yield(:plain, response.body)
72
+ end
73
+ end
74
+
75
+ case parse_as
76
+ when :json
77
+ result = ActiveSupport::JSON.decode(response.body.to_s).with_indifferent_access
78
+ when :file
79
+ if response.headers[:content_type] =~ %r{^image\/.*}
80
+ extension =
81
+ case response.headers['content-type']
82
+ when 'image/gif' then '.gif'
83
+ when 'image/jpeg' then '.jpg'
84
+ when 'image/png' then '.png'
85
+ end
86
+ else
87
+ extension = ''
88
+ end
89
+
90
+ begin
91
+ file = Tempfile.new(['mixin-file-', extension])
92
+ file.binmode
93
+ file.write(response.body)
94
+ ensure
95
+ file&.close
96
+ end
97
+
98
+ result = file
99
+ when :xml
100
+ result = Hash.from_xml(response.body.to_s)
101
+ else
102
+ result = response.body
103
+ end
104
+
105
+ yield(parse_as, result)
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,21 @@
1
+ module MixinBot
2
+ module Errors
3
+ # 通用异常
4
+ Error = Class.new(StandardError)
5
+
6
+ # HTTP 异常,比如请求超时等
7
+ HttpError = Class.new(Error)
8
+
9
+ # API 异常,比如返回失败状态码
10
+ class APIError < Error
11
+ attr_reader :errcode, :errmsg
12
+
13
+ def initialize(errcode, errmsg)
14
+ @errcode = errcode
15
+ @errmsg = errmsg
16
+
17
+ super(format('[%s]: %s', @errcode, @errmsg))
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,3 @@
1
+ module MixinBot
2
+ VERSION = '0.0.1.1'.freeze
3
+ end
metadata ADDED
@@ -0,0 +1,58 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: mixin_bot
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - an-lee
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2018-09-13 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: An API wrapper for Mixin Nexwork
14
+ email:
15
+ - an.lee.work@gmail.com
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - MIT-LICENSE
21
+ - lib/mixin_bot.rb
22
+ - lib/mixin_bot/api.rb
23
+ - lib/mixin_bot/api/auth.rb
24
+ - lib/mixin_bot/api/conversation.rb
25
+ - lib/mixin_bot/api/me.rb
26
+ - lib/mixin_bot/api/message.rb
27
+ - lib/mixin_bot/api/payment.rb
28
+ - lib/mixin_bot/api/pin.rb
29
+ - lib/mixin_bot/api/transfer.rb
30
+ - lib/mixin_bot/api/user.rb
31
+ - lib/mixin_bot/client.rb
32
+ - lib/mixin_bot/errors.rb
33
+ - lib/mixin_bot/version.rb
34
+ homepage: https://github.com/an-lee/mixin_bot
35
+ licenses:
36
+ - MIT
37
+ metadata: {}
38
+ post_install_message:
39
+ rdoc_options: []
40
+ require_paths:
41
+ - lib
42
+ required_ruby_version: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ required_rubygems_version: !ruby/object:Gem::Requirement
48
+ requirements:
49
+ - - ">="
50
+ - !ruby/object:Gem::Version
51
+ version: '0'
52
+ requirements: []
53
+ rubyforge_project:
54
+ rubygems_version: 2.6.14
55
+ signing_key:
56
+ specification_version: 4
57
+ summary: An API wrapper for Mixin Nexwork
58
+ test_files: []