mixin_bot 0.1.0 → 0.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 29930a091b973b490ea096e14f6344a0bc95f67b5023f081ac47fca36bc78adb
4
- data.tar.gz: eb58283464badcf4573f417ad8108e06216811bce36e0990bf10022d8e4802c8
3
+ metadata.gz: 7d2493d906df4d0e0619c860f2b8260cb7b461e0c0aff489706e38d2126cf52a
4
+ data.tar.gz: a8106a0086282b5f8eb09feab36f1885715ba51c09285413dabc66d8fcd6f00d
5
5
  SHA512:
6
- metadata.gz: 51e9e665540a634c383ee266d7b12ce67824c11a06a67fce43f66d3666a317d7c80e1ec974f7401ddb634230a73d0163702792876789c28210e91b8a43c68871
7
- data.tar.gz: 7e503ad0ca5116e99cf36423049b3017109e1d4c2ced08b1625630c55448cc5fbbe4047d0fb3c4ea4bf4abdfa7b7ffc89592671116e8d4d0739bfef9b05c1f52
6
+ metadata.gz: 30442db3e5c4ac450500fb6c0b09023ac36c249c4b12fa2226bbd438076d7e6f9708ac4a93665524cdc28890d0ff22886ef317f1cac1ff38a393e69ca8e127be
7
+ data.tar.gz: 827e08df53317fbfb3441ecb476b4d665013eaaff843e6a7ef6f2776da24979ddb72544976efbf28fad4035a34f8ad1d5968df15ecbf4b0f03ba934853db5174
@@ -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,15 +1,21 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'http'
3
+ require 'English'
4
4
  require 'base64'
5
- require 'openssl'
6
- require 'jwt'
5
+ require 'digest'
6
+ require 'faye/websocket'
7
+ require 'http'
7
8
  require 'jose'
9
+ require 'msgpack'
10
+ require 'open3'
11
+ require 'openssl'
8
12
  require_relative './mixin_bot/api'
13
+ require_relative './mixin_bot/cli'
14
+ require_relative './mixin_bot/version'
9
15
 
10
16
  module MixinBot
11
17
  class<< self
12
- attr_accessor :client_id, :client_secret, :session_id, :pin_token, :private_key, :scope
18
+ attr_accessor :client_id, :client_secret, :session_id, :pin_token, :private_key, :scope, :api_host, :blaze_host
13
19
  end
14
20
 
15
21
  def self.api
@@ -2,10 +2,14 @@
2
2
 
3
3
  require_relative './client'
4
4
  require_relative './errors'
5
+ require_relative './api/app'
6
+ require_relative './api/attachment'
5
7
  require_relative './api/auth'
8
+ require_relative './api/blaze'
6
9
  require_relative './api/conversation'
7
10
  require_relative './api/me'
8
11
  require_relative './api/message'
12
+ require_relative './api/multisig'
9
13
  require_relative './api/payment'
10
14
  require_relative './api/pin'
11
15
  require_relative './api/snapshot'
@@ -15,27 +19,61 @@ require_relative './api/withdraw'
15
19
 
16
20
  module MixinBot
17
21
  class API
18
- attr_reader :client_id, :client_secret, :session_id, :pin_token, :private_key
19
- attr_reader :client
22
+ attr_reader :client_id, :client_secret, :session_id, :pin_token, :private_key, :client, :blaze_host, :schmoozer
20
23
 
21
24
  def initialize(options = {})
22
25
  @client_id = options[:client_id] || MixinBot.client_id
23
26
  @client_secret = options[:client_secret] || MixinBot.client_secret
24
27
  @session_id = options[:session_id] || MixinBot.session_id
25
- @pin_token = Base64.decode64 options[:pin_token] || MixinBot.pin_token
26
- @private_key = OpenSSL::PKey::RSA.new options[:private_key] || MixinBot.private_key
27
- @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
28
38
  end
29
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
49
+ end
50
+
51
+ include MixinBot::API::App
52
+ include MixinBot::API::Attachment
30
53
  include MixinBot::API::Auth
54
+ include MixinBot::API::Blaze
31
55
  include MixinBot::API::Conversation
32
56
  include MixinBot::API::Me
33
57
  include MixinBot::API::Message
58
+ include MixinBot::API::Multisig
34
59
  include MixinBot::API::Payment
35
60
  include MixinBot::API::Pin
36
61
  include MixinBot::API::Snapshot
37
62
  include MixinBot::API::Transfer
38
63
  include MixinBot::API::User
39
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
40
78
  end
41
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
@@ -3,7 +3,7 @@
3
3
  module MixinBot
4
4
  class API
5
5
  module Auth
6
- def access_token(method, uri, body = '', exp_in = 600)
6
+ def access_token(method, uri, body = '', exp_in: 600, scp: 'FULL')
7
7
  sig = Digest::SHA256.hexdigest(method + uri + body)
8
8
  iat = Time.now.utc.to_i
9
9
  exp = (Time.now.utc + exp_in).to_i
@@ -14,9 +14,19 @@ module MixinBot
14
14
  iat: iat,
15
15
  exp: exp,
16
16
  jti: jti,
17
- sig: sig
17
+ sig: sig,
18
+ scp: scp
18
19
  }
19
- 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
20
30
  end
21
31
 
22
32
  def oauth_token(code)
@@ -34,7 +44,7 @@ module MixinBot
34
44
  end
35
45
 
36
46
  def request_oauth(scope = nil)
37
- scope ||= (MixinBot.scope || 'PROFILE:READ+PHONE:READ')
47
+ scope ||= (MixinBot.scope || 'PROFILE:READ')
38
48
  format(
39
49
  'https://mixin.one/oauth/authorize?client_id=%<client_id>s&scope=%<scope>s',
40
50
  client_id: client_id,
@@ -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
@@ -33,14 +33,15 @@ module MixinBot
33
33
  client.post(path, headers: { 'Authorization': authorization }, json: payload)
34
34
  end
35
35
 
36
- def unique_conversation_id(user_id)
36
+ def unique_conversation_id(user_id, opponent_id = nil)
37
+ opponent_id ||= client_id
37
38
  md5 = Digest::MD5.new
38
- md5 << [user_id, client_id].min
39
- md5 << [user_id, client_id].max
39
+ md5 << [user_id, opponent_id].min
40
+ md5 << [user_id, opponent_id].max
40
41
  digest = md5.digest
41
42
  digest6 = (digest[6].ord & 0x0f | 0x30).chr
42
43
  digest8 = (digest[8].ord & 0x3f | 0x80).chr
43
- cipher = digest[0...6] + digest6 + digest[7] + digest8 + digest[9..-1]
44
+ cipher = digest[0...6] + digest6 + digest[7] + digest8 + digest[9..]
44
45
  hex = cipher.unpack1('H*')
45
46
 
46
47
  format(
@@ -49,7 +50,7 @@ module MixinBot
49
50
  second: hex[8..11],
50
51
  third: hex[12..15],
51
52
  forth: hex[16..19],
52
- fifth: hex[20..-1]
53
+ fifth: hex[20..]
53
54
  )
54
55
  end
55
56
  end
@@ -4,9 +4,9 @@ module MixinBot
4
4
  class API
5
5
  module Me
6
6
  # https://developers.mixin.one/api/beta-mixin-message/read-profile/
7
- def read_me
7
+ def read_me(access_token: nil)
8
8
  path = '/me'
9
- access_token = access_token('GET', path, '')
9
+ access_token ||= access_token('GET', path, '')
10
10
  authorization = format('Bearer %<access_token>s', access_token: access_token)
11
11
  client.get(path, headers: { 'Authorization': authorization })
12
12
  end
@@ -14,37 +14,37 @@ module MixinBot
14
14
  # https://developers.mixin.one/api/beta-mixin-message/update-profile/
15
15
  # avatar_base64:
16
16
  # String: Base64 of image, supports format png, jpeg and gif, base64 image size > 1024.
17
- def update_me(full_name:, avatar_base64: nil)
17
+ def update_me(full_name:, avatar_base64: nil, access_token: nil)
18
18
  path = '/me'
19
19
  payload = {
20
20
  full_name: full_name,
21
21
  avatar_base64: avatar_base64
22
22
  }
23
- access_token = access_token('POST', path, payload.to_json)
23
+ access_token ||= access_token('POST', path, payload.to_json)
24
24
  authorization = format('Bearer %<access_token>s', access_token: access_token)
25
25
  client.post(path, headers: { 'Authorization': authorization }, json: payload)
26
26
  end
27
27
 
28
28
  # https://developers.mixin.one/api/alpha-mixin-network/read-assets/
29
- def read_assets
29
+ def read_assets(access_token: nil)
30
30
  path = '/assets'
31
- access_token = access_token('GET', path, '')
31
+ access_token ||= access_token('GET', path, '')
32
32
  authorization = format('Bearer %<access_token>s', access_token: access_token)
33
33
  client.get(path, headers: { 'Authorization': authorization })
34
34
  end
35
35
 
36
36
  # https://developers.mixin.one/api/alpha-mixin-network/read-asset/
37
- def read_asset(asset_id)
37
+ def read_asset(asset_id, access_token: nil)
38
38
  path = format('/assets/%<asset_id>s', asset_id: asset_id)
39
- access_token = access_token('GET', path, '')
39
+ access_token ||= access_token('GET', path, '')
40
40
  authorization = format('Bearer %<access_token>s', access_token: access_token)
41
41
  client.get(path, headers: { 'Authorization': authorization })
42
42
  end
43
43
 
44
44
  # https://developers.mixin.one/api/beta-mixin-message/friends/
45
- def read_friends
45
+ def read_friends(access_token: nil)
46
46
  path = '/friends'
47
- access_token = access_token('GET', path, '')
47
+ access_token ||= access_token('GET', path, '')
48
48
  authorization = format('Bearer %<access_token>s', access_token: access_token)
49
49
  client.get(path, headers: { 'Authorization': authorization })
50
50
  end
@@ -41,6 +41,22 @@ module MixinBot
41
41
  base_message_params(options)
42
42
  end
43
43
 
44
+ # {
45
+ # "id": "UUID // generated by client",
46
+ # "action": "CREATE_MESSAGE",
47
+ # "params": {
48
+ # "conversation_id": "UUID",
49
+ # "category": "PLAIN_POST",
50
+ # "status": "SENT",
51
+ # "message_id": "UUID // generated by client",
52
+ # "data": "Base64 encoded data content is markdown" ,
53
+ # }
54
+ # }
55
+ def plain_post(options)
56
+ options.merge!(category: 'PLAIN_POST')
57
+ base_message_params(options)
58
+ end
59
+
44
60
  # {
45
61
  # "id": "UUID",
46
62
  # "action": "CREATE_MESSAGE",
@@ -203,15 +219,29 @@ module MixinBot
203
219
  base_message_params(options)
204
220
  end
205
221
 
222
+ def recall_message_params(message_id, options)
223
+ raise 'recipient_id is required!' if options[:recipient_id].nil?
224
+
225
+ options.merge!(
226
+ category: 'MESSAGE_RECALL',
227
+ data: {
228
+ message_id: message_id
229
+ }
230
+ )
231
+ base_message_params(options)
232
+ end
233
+
206
234
  # base format of message params
207
- def base_message_params(conversation_id:, category:, data:, quote_message_id: nil, message_id: nil)
208
- data = data.is_a?(String) ? data : data.to_json
235
+ def base_message_params(options)
236
+ data = options[:data].is_a?(String) ? options[:data] : options[:data].to_json
209
237
  {
210
- conversation_id: conversation_id,
211
- category: category,
238
+ conversation_id: options[:conversation_id],
239
+ recipient_id: options[:recipient_id],
240
+ representative_id: options[:representative_id],
241
+ category: options[:category],
212
242
  status: 'SENT',
213
- quote_message_id: quote_message_id,
214
- message_id: message_id || SecureRandom.uuid,
243
+ quote_message_id: options[:quote_message_id],
244
+ message_id: options[:message_id] || SecureRandom.uuid,
215
245
  data: Base64.encode64(data)
216
246
  }
217
247
  end
@@ -227,7 +257,7 @@ module MixinBot
227
257
  end
228
258
 
229
259
  # gzip the message for websocket
230
- def write_ws_message(action: 'CREATE_MESSAGE', params:)
260
+ def write_ws_message(params:, action: 'CREATE_MESSAGE')
231
261
  msg = {
232
262
  id: SecureRandom.uuid,
233
263
  action: action,
@@ -246,6 +276,10 @@ module MixinBot
246
276
  send_message plain_text(options)
247
277
  end
248
278
 
279
+ def send_post_message(options)
280
+ send_message plain_post(options)
281
+ end
282
+
249
283
  def send_contact_message(options)
250
284
  send_message plain_contact(options)
251
285
  end
@@ -258,6 +292,10 @@ module MixinBot
258
292
  send_message app_button_group(options)
259
293
  end
260
294
 
295
+ def recall_message(message_id, options)
296
+ send_message [recall_message_params(message_id, options)]
297
+ end
298
+
261
299
  # {
262
300
  # "id": "UUID",
263
301
  # "action": "CREATE_PLAIN_MESSAGES",
@@ -276,9 +314,8 @@ module MixinBot
276
314
  # ]
277
315
  # }
278
316
  # }
279
- # not verified yet
280
317
  def send_plain_messages(messages)
281
- send_message(messages: messages)
318
+ send_message messages
282
319
  end
283
320
 
284
321
  # http post request