mixin_bot 0.0.1.4 → 0.3.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cc2618a1deab474b441b98f81845094b3178d3f5123d73a33aaefd0cc82f6e0d
4
- data.tar.gz: 0da61f7a15af136541318f8af9fa107a864f5b4cdbc6c45ce17c8bea0ae8ac74
3
+ metadata.gz: 490beec97c1c3ba36f339f4a947f7717ee7eafa0aad01ba7d36c0ad8962af0e2
4
+ data.tar.gz: e00974b0b39f0002d60ced9f3ad416ce4b0addc63ba2d6e14584fc5b3f64a4c3
5
5
  SHA512:
6
- metadata.gz: a474915b84c4fea0086aba993b5b4e0f43744aaa64808cd0728c966ad5f5d7c1375d6525fcedc203ec6943b1d163a8a56a0dc32e34cff15cdf3f55e0c15b7eaa
7
- data.tar.gz: fcf9a02b7eb85c63e959bc47ea9054b7901d05bc68950ab8a90c99d976b424178f31db2fef693eb94c191480ebe77e27a5a21d6a8d326cc5a7c12d70f7f3d921
6
+ metadata.gz: ccf975f89a77c9421151ec6235939a9b1e78359253597daa81e70e7e4badd1362e9c617425e547681ac25be72bdad25c0d56334dc9cc9a56b97abe0b08913dd7
7
+ data.tar.gz: c6d2dabfd14fa960f32549e2ba082d275a618f058b25ab54217e900197d42d10659c48a36e484aff55ed58c92e3355c35a121e599e6b110286bbd9ac3fc06b22
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require_relative '../lib/mixin_bot'
5
+
6
+ MixinBot::CLI.start(ARGV)
@@ -1,17 +1,25 @@
1
- require 'active_support/all'
2
- require 'http'
1
+ # frozen_string_literal: true
2
+
3
+ require 'English'
3
4
  require 'base64'
4
- require 'openssl'
5
- require 'jwt'
5
+ require 'digest'
6
+ require 'faye/websocket'
7
+ require 'http'
6
8
  require 'jose'
9
+ require 'jwt'
10
+ require 'msgpack'
11
+ require 'open3'
12
+ require 'openssl'
7
13
  require_relative './mixin_bot/api'
14
+ require_relative './mixin_bot/cli'
15
+ require_relative './mixin_bot/version'
8
16
 
9
17
  module MixinBot
10
18
  class<< self
11
- attr_accessor :client_id, :client_secret, :session_id, :pin_token, :private_key, :scope
19
+ attr_accessor :client_id, :client_secret, :session_id, :pin_token, :private_key, :scope, :api_host, :blaze_host
12
20
  end
13
21
 
14
22
  def self.api
15
- @api ||= MixinBot::API.new(options={})
23
+ @api ||= MixinBot::API.new
16
24
  end
17
25
  end
@@ -1,37 +1,79 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative './client'
2
4
  require_relative './errors'
5
+ require_relative './api/app'
6
+ require_relative './api/attachment'
3
7
  require_relative './api/auth'
8
+ require_relative './api/blaze'
4
9
  require_relative './api/conversation'
5
10
  require_relative './api/me'
6
11
  require_relative './api/message'
12
+ require_relative './api/multisig'
7
13
  require_relative './api/payment'
8
14
  require_relative './api/pin'
9
15
  require_relative './api/snapshot'
10
16
  require_relative './api/transfer'
11
17
  require_relative './api/user'
18
+ require_relative './api/withdraw'
12
19
 
13
20
  module MixinBot
14
21
  class API
15
- attr_reader :client_id, :client_secret, :session_id, :pin_token, :private_key
16
- attr_reader :client
22
+ attr_reader :client_id, :client_secret, :session_id, :pin_token, :private_key, :client, :blaze_host, :schmoozer
17
23
 
18
- def initialize(options={})
19
- @client_id = options[:client_id] || MixinBot.client_id
24
+ def initialize(options = {})
25
+ @client_id = options[:client_id] || MixinBot.client_id
20
26
  @client_secret = options[:client_secret] || MixinBot.client_secret
21
27
  @session_id = options[:session_id] || MixinBot.session_id
22
- @pin_token = Base64.decode64 options[:pin_token] || MixinBot.pin_token
23
- @private_key = OpenSSL::PKey::RSA.new options[:private_key] || MixinBot.private_key
24
- @client = Client.new
28
+ @pin_token = Base64.urlsafe_decode64 options[:pin_token] || MixinBot.pin_token
29
+ @client = Client.new(MixinBot.api_host || 'api.mixin.one')
30
+ @blaze_host = MixinBot.blaze_host || 'blaze.mixin.one'
31
+ _private_key = options[:private_key] || MixinBot.private_key
32
+ @private_key =
33
+ if /^-----BEGIN RSA PRIVATE KEY-----/.match? _private_key
34
+ _private_key.gsub('\\r\\n', "\n").gsub("\r\n", "\n")
35
+ else
36
+ Base64.urlsafe_decode64 _private_key
37
+ end
38
+ end
39
+
40
+ # Use a mixin software to implement transaction build
41
+ def build_transaction(json)
42
+ ensure_mixin_command_exist
43
+ command = format("mixin signrawtransaction --raw '%<arg>s'", arg: json)
44
+
45
+ output, error = Open3.capture3(command)
46
+ raise error unless error.empty?
47
+
48
+ output.chomp
25
49
  end
26
50
 
51
+ include MixinBot::API::App
52
+ include MixinBot::API::Attachment
27
53
  include MixinBot::API::Auth
54
+ include MixinBot::API::Blaze
28
55
  include MixinBot::API::Conversation
29
56
  include MixinBot::API::Me
30
57
  include MixinBot::API::Message
58
+ include MixinBot::API::Multisig
31
59
  include MixinBot::API::Payment
32
60
  include MixinBot::API::Pin
33
61
  include MixinBot::API::Snapshot
34
62
  include MixinBot::API::Transfer
35
63
  include MixinBot::API::User
64
+ include MixinBot::API::Withdraw
65
+
66
+ private
67
+
68
+ def ensure_mixin_command_exist
69
+ return if command?('mixin')
70
+
71
+ raise '`mixin` command is not valid!'
72
+ end
73
+
74
+ def command?(name)
75
+ `which #{name}`
76
+ $CHILD_STATUS.success?
77
+ end
36
78
  end
37
79
  end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MixinBot
4
+ class API
5
+ module App
6
+ def add_favorite_app(app_id, access_token: nil)
7
+ path = format('/apps/%<id>s/favorite', id: app_id)
8
+
9
+ access_token ||= access_token('POST', path, {})
10
+ authorization = format('Bearer %<access_token>s', access_token: access_token)
11
+ client.post(path, headers: { 'Authorization': authorization })
12
+ end
13
+
14
+ def remove_favorite_app(app_id, access_token: nil)
15
+ path = format('/apps/%<id>s/unfavorite', id: app_id)
16
+
17
+ access_token ||= access_token('POST', path, '')
18
+ authorization = format('Bearer %<access_token>s', access_token: access_token)
19
+ client.post(path, headers: { 'Authorization': authorization })
20
+ end
21
+
22
+ def favorite_apps(user_id, access_token: nil)
23
+ path = format('/users/%<id>s/apps/favorite', id: user_id)
24
+
25
+ access_token ||= access_token('GET', path, '')
26
+ authorization = format('Bearer %<access_token>s', access_token: access_token)
27
+ client.get(path, headers: { 'Authorization': authorization })
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MixinBot
4
+ class API
5
+ module Attachment
6
+ # https://developers.mixin.one/api/beta-mixin-message/create-attachment/
7
+ # Sample Response
8
+ # {
9
+ # "data":{
10
+ # "type":"attachment",
11
+ # "attachment_id":"7a54e394-1626-4cd4-b967-543932c2a032",
12
+ # "upload_url":"https://moments-shou-tv.s3.amazonaws.com/mixin/attachments/xxx",
13
+ # "view_url":"https://moments.shou.tv/mixin/attachments/1526305123xxxx"
14
+ # }
15
+ # }
16
+ # Once get the upload_url, use it to upload the your file via PUT request
17
+ def create_attachment
18
+ path = '/attachments'
19
+ access_token ||= access_token('POST', path, {}.to_json)
20
+ authorization = format('Bearer %<access_token>s', access_token: access_token)
21
+ client.post(path, headers: { 'Authorization': authorization }, json: {})
22
+ end
23
+
24
+ def read_attachment(attachment_id)
25
+ path = format('/attachments/%<id>s', id: attachment_id)
26
+ access_token ||= access_token('GET', path, '')
27
+ authorization = format('Bearer %<access_token>s', access_token: access_token)
28
+ client.get(path, headers: { 'Authorization': authorization })
29
+ end
30
+ end
31
+ end
32
+ end
@@ -1,8 +1,10 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module MixinBot
2
4
  class API
3
5
  module Auth
4
- def access_token(method, uri, body, exp_in=10.minutes)
5
- sig = Digest::SHA256.hexdigest (method + uri + body)
6
+ def access_token(method, uri, body = '', exp_in: 600, scp: 'FULL')
7
+ sig = Digest::SHA256.hexdigest(method + uri + body)
6
8
  iat = Time.now.utc.to_i
7
9
  exp = (Time.now.utc + exp_in).to_i
8
10
  jti = SecureRandom.uuid
@@ -12,9 +14,19 @@ module MixinBot
12
14
  iat: iat,
13
15
  exp: exp,
14
16
  jti: jti,
15
- sig: sig
17
+ sig: sig,
18
+ scp: scp
16
19
  }
17
- JWT.encode payload, private_key, 'RS512'
20
+ if pin_token.size == 32
21
+ jwk = JOSE::JWK.from_okp [:Ed25519, private_key]
22
+ jws = JOSE::JWS.from({ 'alg' => 'EdDSA' })
23
+ else
24
+ jwk = JOSE::JWK.from_pem private_key
25
+ jws = JOSE::JWS.from({ 'alg' => 'RS512' })
26
+ end
27
+
28
+ jwt = JOSE::JWT.from payload
29
+ JOSE::JWT.sign(jwk, jws, jwt).compact
18
30
  end
19
31
 
20
32
  def oauth_token(code)
@@ -28,12 +40,16 @@ module MixinBot
28
40
 
29
41
  raise r.inspect if r['error'].present?
30
42
 
31
- return r['data']['access_token']
43
+ r['data']&.[]('access_token')
32
44
  end
33
45
 
34
- def request_oauth(scope=nil)
35
- scope ||= (MixinBot.scope || 'PROFILE:READ+PHONE:READ')
36
- format('https://mixin.one/oauth/authorize?client_id=%s&scope=%s', client_id, scope)
46
+ def request_oauth(scope = nil)
47
+ scope ||= (MixinBot.scope || 'PROFILE:READ')
48
+ format(
49
+ 'https://mixin.one/oauth/authorize?client_id=%<client_id>s&scope=%<scope>s',
50
+ client_id: client_id,
51
+ scope: scope
52
+ )
37
53
  end
38
54
  end
39
55
  end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MixinBot
4
+ class API
5
+ module Blaze
6
+ def blaze
7
+ access_token = access_token('GET', '/', '')
8
+ authorization = format('Bearer %<access_token>s', access_token: access_token)
9
+ Faye::WebSocket::Client.new(
10
+ format('wss://%<host>s/', host: blaze_host),
11
+ ['Mixin-Blaze-1'],
12
+ headers: { 'Authorization' => authorization },
13
+ ping: 60
14
+ )
15
+ end
16
+
17
+ def start_blaze_connect(reconnect: true, &_block)
18
+ ws ||= blaze
19
+ yield if block_given?
20
+
21
+ ws.on :open do |event|
22
+ if defined? on_open
23
+ on_open ws, event
24
+ else
25
+ p [Time.now.to_s, :open]
26
+ ws.send list_pending_message
27
+ end
28
+ end
29
+
30
+ ws.on :message do |event|
31
+ if defined? on_message
32
+ on_message ws, event
33
+ else
34
+ raw = JSON.parse read_ws_message(event.data)
35
+ p [Time.now.to_s, :message, raw&.[]('action')]
36
+
37
+ ws.send acknowledge_message_receipt(raw['data']['message_id']) unless raw&.[]('data')&.[]('message_id').nil?
38
+ end
39
+ end
40
+
41
+ ws.on :error do |event|
42
+ if defined? on_error
43
+ on_error ws, event
44
+ else
45
+ p [Time.now.to_s, :error]
46
+ end
47
+ end
48
+
49
+ ws.on :close do |event|
50
+ if defined? on_close
51
+ on_close ws, event
52
+ else
53
+ p [Time.now.to_s, :close, event.code, event.reason]
54
+ end
55
+
56
+ ws = nil
57
+ start_blaze_connect(&block) if reconnect
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -1,16 +1,18 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module MixinBot
2
4
  class API
3
5
  module Conversation
4
6
  def read_conversation(conversation_id)
5
- path = format('/conversations/%s', conversation_id)
6
- _access_token ||= self.access_token('GET', path, '')
7
- authorization = format('Bearer %s', _access_token)
7
+ path = format('/conversations/%<conversation_id>s', conversation_id: conversation_id)
8
+ access_token ||= access_token('GET', path, '')
9
+ authorization = format('Bearer %<access_token>s', access_token: access_token)
8
10
  client.get(path, headers: { 'Authorization': authorization })
9
11
  end
10
12
 
11
13
  def read_conversation_by_user_id(user_id)
12
14
  conversation_id = unique_conversation_id(user_id)
13
- return self.read_conversation(conversation_id)
15
+ read_conversation(conversation_id)
14
16
  end
15
17
 
16
18
  def create_contact_conversation(user_id)
@@ -26,21 +28,30 @@ module MixinBot
26
28
  }
27
29
  ]
28
30
  }
29
- _access_token ||= self.access_token('POST', path, payload.to_json)
30
- authorization = format('Bearer %s', _access_token)
31
+ access_token ||= access_token('POST', path, payload.to_json)
32
+ authorization = format('Bearer %<access_token>s', access_token: access_token)
31
33
  client.post(path, headers: { 'Authorization': authorization }, json: payload)
32
34
  end
33
35
 
34
- def unique_conversation_id(user_id)
36
+ def unique_conversation_id(user_id, opponent_id = nil)
37
+ opponent_id ||= client_id
35
38
  md5 = Digest::MD5.new
36
- md5 << [user_id, client_id].min
37
- md5 << [user_id, client_id].max
39
+ md5 << [user_id, opponent_id].min
40
+ md5 << [user_id, opponent_id].max
38
41
  digest = md5.digest
39
- digest_6 = (digest[6].ord & 0x0f | 0x30).chr
40
- digest_8 = (digest[8].ord & 0x3f | 0x80).chr
41
- cipher = digest[0...6] + digest_6 + digest[7] + digest_8 + digest[9..-1]
42
- hex = cipher.unpack('H*').first
43
- conversation_id = format('%s-%s-%s-%s-%s', hex[0..7], hex[8..11], hex[12..15], hex[16..19], hex[20..-1])
42
+ digest6 = (digest[6].ord & 0x0f | 0x30).chr
43
+ digest8 = (digest[8].ord & 0x3f | 0x80).chr
44
+ cipher = digest[0...6] + digest6 + digest[7] + digest8 + digest[9..]
45
+ hex = cipher.unpack1('H*')
46
+
47
+ format(
48
+ '%<first>s-%<second>s-%<third>s-%<forth>s-%<fifth>s',
49
+ first: hex[0..7],
50
+ second: hex[8..11],
51
+ third: hex[12..15],
52
+ forth: hex[16..19],
53
+ fifth: hex[20..]
54
+ )
44
55
  end
45
56
  end
46
57
  end
@@ -1,42 +1,51 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module MixinBot
2
4
  class API
3
5
  module Me
4
- def read_me(access_token=nil)
6
+ # https://developers.mixin.one/api/beta-mixin-message/read-profile/
7
+ def read_me(access_token: nil)
5
8
  path = '/me'
6
- access_token ||= self.access_token('GET', path, '')
7
- authorization = format('Bearer %s', access_token)
9
+ access_token ||= access_token('GET', path, '')
10
+ authorization = format('Bearer %<access_token>s', access_token: access_token)
8
11
  client.get(path, headers: { 'Authorization': authorization })
9
12
  end
10
13
 
11
- def update_me(full_name, avatar_base64, access_token=nil)
14
+ # https://developers.mixin.one/api/beta-mixin-message/update-profile/
15
+ # avatar_base64:
16
+ # String: Base64 of image, supports format png, jpeg and gif, base64 image size > 1024.
17
+ def update_me(full_name:, avatar_base64: nil, access_token: nil)
12
18
  path = '/me'
13
19
  payload = {
14
20
  full_name: full_name,
15
21
  avatar_base64: avatar_base64
16
22
  }
17
- access_token ||= self.access_token('POST', path, payload.to_json)
18
- authorization = format('Bearer %s', access_token)
23
+ access_token ||= access_token('POST', path, payload.to_json)
24
+ authorization = format('Bearer %<access_token>s', access_token: access_token)
19
25
  client.post(path, headers: { 'Authorization': authorization }, json: payload)
20
26
  end
21
27
 
22
- def read_assets(access_token=nil)
28
+ # https://developers.mixin.one/api/alpha-mixin-network/read-assets/
29
+ def read_assets(access_token: nil)
23
30
  path = '/assets'
24
- access_token ||= self.access_token('GET', path, '')
25
- authorization = format('Bearer %s', access_token)
31
+ access_token ||= access_token('GET', path, '')
32
+ authorization = format('Bearer %<access_token>s', access_token: access_token)
26
33
  client.get(path, headers: { 'Authorization': authorization })
27
34
  end
28
35
 
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)
36
+ # https://developers.mixin.one/api/alpha-mixin-network/read-asset/
37
+ def read_asset(asset_id, access_token: nil)
38
+ path = format('/assets/%<asset_id>s', asset_id: asset_id)
39
+ access_token ||= access_token('GET', path, '')
40
+ authorization = format('Bearer %<access_token>s', access_token: access_token)
33
41
  client.get(path, headers: { 'Authorization': authorization })
34
42
  end
35
43
 
36
- def read_friends(access_token=nil)
44
+ # https://developers.mixin.one/api/beta-mixin-message/friends/
45
+ def read_friends(access_token: nil)
37
46
  path = '/friends'
38
- access_token ||= self.access_token('GET', path, '')
39
- authorization = format('Bearer %s', access_token)
47
+ access_token ||= access_token('GET', path, '')
48
+ authorization = format('Bearer %<access_token>s', access_token: access_token)
40
49
  client.get(path, headers: { 'Authorization': authorization })
41
50
  end
42
51
  end