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