mixin_bot 0.0.1.4 → 0.1.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: 29930a091b973b490ea096e14f6344a0bc95f67b5023f081ac47fca36bc78adb
4
+ data.tar.gz: eb58283464badcf4573f417ad8108e06216811bce36e0990bf10022d8e4802c8
5
5
  SHA512:
6
- metadata.gz: a474915b84c4fea0086aba993b5b4e0f43744aaa64808cd0728c966ad5f5d7c1375d6525fcedc203ec6943b1d163a8a56a0dc32e34cff15cdf3f55e0c15b7eaa
7
- data.tar.gz: fcf9a02b7eb85c63e959bc47ea9054b7901d05bc68950ab8a90c99d976b424178f31db2fef693eb94c191480ebe77e27a5a21d6a8d326cc5a7c12d70f7f3d921
6
+ metadata.gz: 51e9e665540a634c383ee266d7b12ce67824c11a06a67fce43f66d3666a317d7c80e1ec974f7401ddb634230a73d0163702792876789c28210e91b8a43c68871
7
+ data.tar.gz: 7e503ad0ca5116e99cf36423049b3017109e1d4c2ced08b1625630c55448cc5fbbe4047d0fb3c4ea4bf4abdfa7b7ffc89592671116e8d4d0739bfef9b05c1f52
@@ -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)
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
@@ -28,12 +30,16 @@ module MixinBot
28
30
 
29
31
  raise r.inspect if r['error'].present?
30
32
 
31
- return r['data']['access_token']
33
+ r['data']&.[]('access_token')
32
34
  end
33
35
 
34
- def request_oauth(scope=nil)
36
+ def request_oauth(scope = nil)
35
37
  scope ||= (MixinBot.scope || 'PROFILE:READ+PHONE:READ')
36
- format('https://mixin.one/oauth/authorize?client_id=%s&scope=%s', client_id, scope)
38
+ format(
39
+ 'https://mixin.one/oauth/authorize?client_id=%<client_id>s&scope=%<scope>s',
40
+ client_id: client_id,
41
+ scope: scope
42
+ )
37
43
  end
38
44
  end
39
45
  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,8 +28,8 @@ 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
 
@@ -36,11 +38,19 @@ module MixinBot
36
38
  md5 << [user_id, client_id].min
37
39
  md5 << [user_id, client_id].max
38
40
  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])
41
+ digest6 = (digest[6].ord & 0x0f | 0x30).chr
42
+ digest8 = (digest[8].ord & 0x3f | 0x80).chr
43
+ cipher = digest[0...6] + digest6 + digest[7] + digest8 + digest[9..-1]
44
+ hex = cipher.unpack1('H*')
45
+
46
+ format(
47
+ '%<first>s-%<second>s-%<third>s-%<forth>s-%<fifth>s',
48
+ first: hex[0..7],
49
+ second: hex[8..11],
50
+ third: hex[12..15],
51
+ forth: hex[16..19],
52
+ fifth: hex[20..-1]
53
+ )
44
54
  end
45
55
  end
46
56
  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
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)
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
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)
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
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
@@ -1,69 +1,236 @@
1
+ # frozen_string_literal: false
2
+
1
3
  module MixinBot
2
4
  class API
5
+ # https://developers.mixin.one/api/beta-mixin-message/websocket-messages/
3
6
  module Message
4
7
  def list_pending_message
5
- write_message('LIST_PENDING_MESSAGES', {})
8
+ write_ws_message(action: 'LIST_PENDING_MESSAGES', params: {})
6
9
  end
7
10
 
11
+ # ACKNOWLEDGE_MESSAGE_RECEIPT ack server received message
12
+ # {
13
+ # "id": "UUID",
14
+ # "action": "ACKNOWLEDGE_MESSAGE_RECEIPT",
15
+ # "params": {
16
+ # "message_id": "UUID // message_id is you received message's message_id",
17
+ # "status": "READ"
18
+ # }
19
+ # }
8
20
  def acknowledge_message_receipt(message_id)
9
21
  params = {
10
22
  message_id: message_id,
11
23
  status: 'READ'
12
24
  }
13
- write_message('ACKNOWLEDGE_MESSAGE_RECEIPT', params)
25
+ write_ws_message(action: 'ACKNOWLEDGE_MESSAGE_RECEIPT', params: params)
14
26
  end
15
27
 
16
- def plain_text_message(conversation_id, text)
17
- encoded_text = Base64.encode64 text
28
+ # {
29
+ # "id": "UUID // generated by client",
30
+ # "action": "CREATE_MESSAGE",
31
+ # "params": {
32
+ # "conversation_id": "UUID",
33
+ # "category": "PLAIN_TEXT",
34
+ # "status": "SENT",
35
+ # "message_id": "UUID // generated by client",
36
+ # "data": "Base64 encoded data" ,
37
+ # }
38
+ # }
39
+ def plain_text(options)
40
+ options.merge!(category: 'PLAIN_TEXT')
41
+ base_message_params(options)
42
+ end
18
43
 
19
- params = {
20
- conversation_id: conversation_id,
21
- category: 'PLAIN_TEXT',
22
- status: 'SENT',
23
- message_id: SecureRandom.uuid,
24
- data: encoded_text
25
- }
44
+ # {
45
+ # "id": "UUID",
46
+ # "action": "CREATE_MESSAGE",
47
+ # "params": {
48
+ # "conversation_id": "UUID"
49
+ # "category": "PLAIN_IMAGE"
50
+ # "status": "SENT",
51
+ # "message_id": "UUID",
52
+ # "data": "Base64 encoded data"
53
+ # }
54
+ # }
55
+ # data format:
56
+ # {
57
+ # "attachment_id":
58
+ # "Read From POST /attachments",
59
+ # "mime_type": "",
60
+ # "width": 1024,
61
+ # "height": 1024,
62
+ # "size": 1024,
63
+ # "thumbnail": "base64 encoded"
64
+ # }
65
+ def plain_image(options)
66
+ options.merge!(category: 'PLAIN_IMAGE')
67
+ base_message_params(options)
68
+ end
26
69
 
27
- write_message('CREATE_MESSAGE', params)
70
+ # {
71
+ # "id": "UUID",
72
+ # "action": "CREATE_MESSAGE",
73
+ # "params": {
74
+ # "conversation_id": "UUID",
75
+ # "category": "PLAIN_DATA",
76
+ # "status": "SENT",
77
+ # "message_id": "UUID",
78
+ # "data": "Base64 encoded data",
79
+ # }
80
+ # }
81
+ # data format:
82
+ # {
83
+ # "attachment_id": "Read From POST /attachments",
84
+ # "mime_type": "",
85
+ # "size": 1024,
86
+ # "name": "Share"
87
+ # }
88
+ def plain_data(options)
89
+ options.merge!(category: 'PLAIN_DATA')
90
+ base_message_params(options)
28
91
  end
29
92
 
30
- def app_card_message
31
- # TODO:
93
+ # {
94
+ # "id": "UUID",
95
+ # "action": "CREATE_MESSAGE",
96
+ # "params": {
97
+ # "conversation_id": "UUID",
98
+ # "category": "PLAIN_STICKER",
99
+ # "status": "SENT",
100
+ # "message_id": "UUID",
101
+ # "data": "Base64 encoded data"
102
+ # }
103
+ # }
104
+ # data format:
105
+ # {
106
+ # "name": "hello",
107
+ # "album_id": "UUID"
108
+ # }
109
+ def plain_sticker(options)
110
+ options.merge!(category: 'PLAIN_STICKER')
111
+ base_message_params(options)
32
112
  end
33
113
 
34
- def app_button_group_message(conversation_id, recipient_id, options={})
35
- options = options.with_indifferent_access
36
- label = options[:label] || ''
37
- color = options[:color] || '#467fcf'
38
- action = options[:action] || ''
114
+ # {
115
+ # "id": "UUID",
116
+ # "action": "CREATE_MESSAGE",
117
+ # "params": {
118
+ # "conversation_id": "UUID",
119
+ # "category": "PLAIN_CONTACT"
120
+ # "status": "SENT",
121
+ # "message_id": "UUID",
122
+ # "data": "Base64 encoded data"
123
+ # }
124
+ # }
125
+ # data format:
126
+ # { "user_id": "UUID"}
127
+ def plain_contact(options)
128
+ options.merge!(category: 'PLAIN_CONTACT')
129
+ base_message_params(options)
130
+ end
39
131
 
40
- data = [{ label: label, color: color, action: action }]
41
- encoded_data = Base64.encode64 data.to_json
132
+ # {
133
+ # "id": "UUID",
134
+ # "action": "CREATE_MESSAGE",
135
+ # "params": {
136
+ # "conversation_id": "UUID",
137
+ # "category": "APP_CARD",
138
+ # "status": "SENT",
139
+ # "message_id": "UUID",
140
+ # "data": "Base64 encoded data"
141
+ # }
142
+ # }
143
+ # data format:
144
+ # {
145
+ # "icon_url": "https://mixin.one/assets/98b586edb270556d1972112bd7985e9e.png",
146
+ # "title": "Mixin",
147
+ # "description": "A free and lightning fast peer-to-peer transactional network for digital assets.",
148
+ # "action": "https://mixin.one"
149
+ # }
150
+ def app_card(options)
151
+ options.merge!(category: 'APP_CARD')
152
+ base_message_params(options)
153
+ end
42
154
 
43
- params = {
155
+ # {
156
+ # "id": "UUID",
157
+ # "action": "CREATE_MESSAGE",
158
+ # "params": {
159
+ # "conversation_id": "UUID",
160
+ # "category": "APP_BUTTON_GROUP",
161
+ # "status": "SENT",
162
+ # "message_id": "UUID",
163
+ # "data": "Base64 encoded data"
164
+ # }
165
+ # }
166
+ # data format:
167
+ # [
168
+ # {
169
+ # "label": "Mixin Website",
170
+ # "color": "#ABABAB",
171
+ # "action": "https://mixin.one"
172
+ # },
173
+ # ...
174
+ # ]
175
+ def app_button_group(options)
176
+ options.merge!(category: 'APP_BUTTON_GROUP')
177
+ base_message_params(options)
178
+ end
179
+
180
+ # {
181
+ # "id": "UUID",
182
+ # "action": "CREATE_MESSAGE",
183
+ # "params": {
184
+ # "conversation_id": "UUID",
185
+ # "category": "PLAIN_VIDEO",
186
+ # "status": "SENT",
187
+ # "message_id": "UUID",
188
+ # "data": "Base64 encoded data"
189
+ # }
190
+ # }
191
+ # data format:
192
+ # {
193
+ # "attachment_id": "Read From POST /attachments",
194
+ # "mime_type": "",
195
+ # "width": 1024,
196
+ # "height": 1024,
197
+ # "size": 1024,
198
+ # "duration": 1024,
199
+ # "thumbnail": "base64 encoded"
200
+ # }
201
+ def plain_video(options)
202
+ options.merge!(category: 'PLAIN_VIDEO')
203
+ base_message_params(options)
204
+ end
205
+
206
+ # 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
209
+ {
44
210
  conversation_id: conversation_id,
45
- recipient_id: recipient_id,
46
- category: 'APP_BUTTON_GROUP',
211
+ category: category,
47
212
  status: 'SENT',
48
- message_id: SecureRandom.uuid,
49
- data: encoded_data
213
+ quote_message_id: quote_message_id,
214
+ message_id: message_id || SecureRandom.uuid,
215
+ data: Base64.encode64(data)
50
216
  }
51
-
52
- write_message('CREATE_MESSAGE', params)
53
217
  end
54
218
 
55
- def read_message(data)
219
+ # read the gzipped message form websocket
220
+ def read_ws_message(data)
56
221
  io = StringIO.new(data.pack('c*'), 'rb')
57
222
  gzip = Zlib::GzipReader.new io
58
223
  msg = gzip.read
59
224
  gzip.close
60
- return msg
225
+
226
+ msg
61
227
  end
62
228
 
63
- def write_message(action, params)
229
+ # gzip the message for websocket
230
+ def write_ws_message(action: 'CREATE_MESSAGE', params:)
64
231
  msg = {
65
232
  id: SecureRandom.uuid,
66
- action: action,
233
+ action: action,
67
234
  params: params
68
235
  }.to_json
69
236
 
@@ -71,7 +238,55 @@ module MixinBot
71
238
  gzip = Zlib::GzipWriter.new io
72
239
  gzip.write msg
73
240
  gzip.close
74
- data = io.string.unpack('c*')
241
+ io.string.unpack('c*')
242
+ end
243
+
244
+ # use HTTP to send message
245
+ def send_text_message(options)
246
+ send_message plain_text(options)
247
+ end
248
+
249
+ def send_contact_message(options)
250
+ send_message plain_contact(options)
251
+ end
252
+
253
+ def send_app_card_message(options)
254
+ send_message app_card(options)
255
+ end
256
+
257
+ def send_app_button_group_message(options)
258
+ send_message app_button_group(options)
259
+ end
260
+
261
+ # {
262
+ # "id": "UUID",
263
+ # "action": "CREATE_PLAIN_MESSAGES",
264
+ # "params": {
265
+ # "messages": [
266
+ # {
267
+ # "conversation_id": "UUID",
268
+ # "recipient_id": "UUID",
269
+ # "message_id": "UUID",
270
+ # "representative_id": "UUID (optional, only supported in peer to peer conversation)",
271
+ # "quote_message_id": "UUID (optional, only supported text, e.g. PLAIN_TEXT)",
272
+ # "category": "Only support plain category e.g.: PLAIN_TEXT, PLAIN_STICKER etc",
273
+ # "data": "Correspond to category."
274
+ # },
275
+ # ...
276
+ # ]
277
+ # }
278
+ # }
279
+ # not verified yet
280
+ def send_plain_messages(messages)
281
+ send_message(messages: messages)
282
+ end
283
+
284
+ # http post request
285
+ def send_message(payload)
286
+ path = '/messages'
287
+ access_token ||= access_token('POST', path, payload.to_json)
288
+ authorization = format('Bearer %<access_token>s', access_token: access_token)
289
+ client.post(path, headers: { 'Authorization': authorization }, json: payload)
75
290
  end
76
291
  end
77
292
  end
@@ -1,29 +1,29 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module MixinBot
2
4
  class API
3
5
  module Payment
4
6
  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)
7
+ format(
8
+ 'https://mixin.one/pay?recipient=%<recipient_id>s&asset=%<asset>s&amount=%<amount>s&trace=%<trace>s&memo=%<memo>s',
9
+ recipient_id: options[:recipient_id],
10
+ asset: options[:asset_id],
11
+ amount: options[:amount].to_s,
12
+ trace: options[:trace],
13
+ memo: options[:memo]
14
+ )
12
15
  end
13
16
 
17
+ # https://developers.mixin.one/api/alpha-mixin-network/verify-payment/
14
18
  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
19
  path = 'payments'
21
20
  payload = {
22
- asset_id: asset_id,
23
- opponent_id: recipient_id,
24
- amount: amount,
25
- trace_id: trace,
21
+ asset_id: options[:asset_id],
22
+ opponent_id: options[:opponent_id],
23
+ amount: options[:amount].to_s,
24
+ trace_id: options[:trace]
26
25
  }
26
+
27
27
  client.post(path, json: payload)
28
28
  end
29
29
  end
@@ -1,46 +1,68 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module MixinBot
2
4
  class API
3
5
  module Pin
4
- def verify_pin(pin_code, access_token=nil)
6
+ # https://developers.mixin.one/api/alpha-mixin-network/verify-pin/
7
+ def verify_pin(pin_code)
5
8
  path = '/pin/verify'
6
9
  payload = {
7
10
  pin: encrypt_pin(pin_code)
8
11
  }
9
12
 
10
- access_token ||= self.access_token('POST', path, payload.to_json)
11
- authorization = format('Bearer %s', access_token)
13
+ access_token = access_token('POST', path, payload.to_json)
14
+ authorization = format('Bearer %<access_token>s', access_token: access_token)
15
+ client.post(path, headers: { 'Authorization': authorization }, json: payload)
16
+ end
17
+
18
+ # not verified yet
19
+ # https://developers.mixin.one/api/alpha-mixin-network/create-pin/
20
+ def update_pin(old_pin:, new_pin:)
21
+ path = '/pin/update'
22
+ timestamp = Time.now.utc.to_i
23
+ payload = {
24
+ old_pin: old_pin.nil? ? '' : encrypt_pin(old_pin, timestamp: timestamp),
25
+ pin: encrypt_pin(new_pin, timestamp: timestamp)
26
+ }
27
+
28
+ access_token = access_token('POST', path, payload.to_json)
29
+ authorization = format('Bearer %<access_token>s', access_token: access_token)
12
30
  client.post(path, headers: { 'Authorization': authorization }, json: payload)
13
31
  end
14
32
 
33
+ # decrypt the encrpted pin, just for test
15
34
  def decrypt_pin(msg)
16
35
  msg = Base64.strict_decode64 msg
17
36
  iv = msg[0..15]
18
37
  cipher = msg[16..47]
19
- aes_key = JOSE::JWA::PKCS1::rsaes_oaep_decrypt('SHA256', pin_token, private_key, session_id)
38
+ aes_key = JOSE::JWA::PKCS1.rsaes_oaep_decrypt('SHA256', pin_token, private_key, session_id)
20
39
  alg = 'AES-256-CBC'
21
40
  decode_cipher = OpenSSL::Cipher.new(alg)
22
41
  decode_cipher.decrypt
23
42
  decode_cipher.iv = iv
24
43
  decode_cipher.key = aes_key
25
- plain = decode_cipher.update(cipher)
26
- return plain
44
+ decoded = decode_cipher.update(cipher)
45
+ decoded[0..5]
27
46
  end
28
47
 
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
48
+ # https://developers.mixin.one/api/alpha-mixin-network/encrypted-pin/
49
+ # use timestamp(timestamp) for iterator as default: must be bigger than the previous, the first time must be greater than 0. After a new session created, it will be reset to 0.
50
+ def encrypt_pin(pin_code, timestamp: nil)
51
+ aes_key = JOSE::JWA::PKCS1.rsaes_oaep_decrypt('SHA256', pin_token, private_key, session_id)
52
+ timestamp ||= Time.now.utc.to_i
53
+ tszero = timestamp % 0x100
54
+ tsone = (timestamp % 0x10000) >> 8
55
+ tstwo = (timestamp % 0x1000000) >> 16
56
+ tsthree = (timestamp % 0x100000000) >> 24
36
57
  tsstring = tszero.chr + tsone.chr + tstwo.chr + tsthree.chr + "\0\0\0\0"
37
58
  encrypt_content = pin_code + tsstring + tsstring
38
59
  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
60
+ padded_content =
61
+ if pad_count.positive?
62
+ encrypt_content + pad_count.chr * pad_count
63
+ else
64
+ encrypt_content
65
+ end
44
66
 
45
67
  alg = 'AES-256-CBC'
46
68
  aes = OpenSSL::Cipher.new(alg)
@@ -50,7 +72,7 @@ module MixinBot
50
72
  aes.iv = iv
51
73
  cipher = aes.update(padded_content)
52
74
  msg = iv + cipher
53
- return Base64.strict_encode64 msg
75
+ Base64.strict_encode64 msg
54
76
  end
55
77
  end
56
78
  end
@@ -1,25 +1,31 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module MixinBot
2
4
  class API
3
5
  module Snapshot
4
- def read_snapshots(options)
5
- options = options.with_indifferent_access
6
- limit = options['limit']
7
- offset = options['offset']
8
- asset = options['asset']
9
- order = options['order']
6
+ def read_snapshots(options = {})
7
+ path = format(
8
+ '/network/snapshots?limit=%<limit>s&offset=%<offset>s&asset=%<asset>s&order=%<order>s',
9
+ limit: options[:limit],
10
+ offset: options[:offset],
11
+ asset: options[:asset],
12
+ order: options[:order]
13
+ )
10
14
 
11
- path = 'network/snapshots'
12
- payload = {
13
- limit: limit,
14
- offset: offset,
15
- asset: asset,
16
- order: order
17
- }
18
- client.get(path, params: payload)
15
+ if options[:private]
16
+ # TODO:
17
+ # read private snapshots
18
+ access_token = access_token('GET', path)
19
+ authorization = format('Bearer %<access_token>s', access_token: access_token)
20
+ client.get(path, headers: { 'Authorization': authorization, 'Content-length': 0 })
21
+ else
22
+ # read public snapshots as default
23
+ client.get(path)
24
+ end
19
25
  end
20
26
 
21
27
  def read_snapshot(snapshot_id)
22
- path = format('network/snapshots/%s', snapshot_id)
28
+ path = format('network/snapshots/%<snapshot_id>s', snapshot_id: snapshot_id)
23
29
  client.get(path)
24
30
  end
25
31
  end
@@ -1,35 +1,34 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module MixinBot
2
4
  class API
3
5
  module Transfer
4
6
  def create_transfer(pin, options)
5
- options = options.with_indifferent_access
6
-
7
- asset_id = options.fetch('asset_id')
8
- opponent_id = options.fetch('opponent_id')
9
- amount = options.fetch('amount')
10
- memo = options.fetch('memo')
11
- trace_id = options.fetch('trace_id')
12
- trace_id ||= SecureRandom.uuid
7
+ asset_id = options[:asset_id]
8
+ opponent_id = options[:opponent_id]
9
+ amount = options[:amount]
10
+ memo = options[:memo]
11
+ trace_id = options[:trace_id] || SecureRandom.uuid
13
12
 
14
13
  path = '/transfers'
15
14
  payload = {
16
15
  asset_id: asset_id,
17
16
  opponent_id: opponent_id,
18
17
  pin: pin,
19
- amount: amount,
18
+ amount: amount.to_s,
20
19
  trace_id: trace_id,
21
20
  memo: memo
22
21
  }
23
22
 
24
- access_token ||= self.access_token('POST', path, payload.to_json)
25
- 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)
26
25
  client.post(path, headers: { 'Authorization': authorization }, json: payload)
27
26
  end
28
27
 
29
28
  def read_transfer(trace_id)
30
- path = format('/transfers/trace/%s', trace_id)
31
- access_token ||= self.access_token('GET', path, '')
32
- authorization = format('Bearer %s', access_token)
29
+ path = format('/transfers/trace/%<trace_id>s', trace_id: trace_id)
30
+ access_token ||= access_token('GET', path, '')
31
+ authorization = format('Bearer %<access_token>s', access_token: access_token)
33
32
  client.get(path, headers: { 'Authorization': authorization })
34
33
  end
35
34
  end
@@ -1,29 +1,62 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module MixinBot
2
4
  class API
3
5
  module User
4
- def read_user(user_id, access_token=nil)
6
+ def read_user(user_id, access_token = nil)
5
7
  # 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)
8
+ path = format('/users/%<user_id>s', user_id: user_id)
9
+ access_token ||= access_token('GET', path, '')
10
+ authorization = format('Bearer %<access_token>s', access_token: access_token)
9
11
  client.get(path, headers: { 'Authorization': authorization })
10
12
  end
11
13
 
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)
14
+ # https://developers.mixin.one/api/alpha-mixin-network/app-user/
15
+ # Create a new Mixin Network user (like a normal Mixin Messenger user). You should keep PrivateKey which is used to sign an AuthenticationToken and encrypted PIN for the user.
16
+ def create_user(full_name, rsa_key = nil)
17
+ rsa_key ||= generate_rsa_key
18
+ session_secret = rsa_key[:public_key]
19
+ session_secret.gsub!(/^-----.*PUBLIC KEY-----$/, '').strip!
20
+
21
+ payload = {
22
+ full_name: full_name,
23
+ session_secret: session_secret
24
+ }
25
+
26
+ access_token = access_token('POST', '/users', payload.to_json)
27
+ authorization = format('Bearer %<access_token>s', access_token: access_token)
28
+ res = client.post('/users', headers: { 'Authorization': authorization }, json: payload)
29
+
30
+ res.merge(rsa_key: rsa_key)
31
+ end
32
+
33
+ def generate_rsa_key
34
+ rsa_key = OpenSSL::PKey::RSA.new 1024
35
+ {
36
+ private_key: rsa_key.to_pem,
37
+ public_key: rsa_key.public_key.to_pem
38
+ }
39
+ end
40
+
41
+ # https://developers.mixin.one/api/beta-mixin-message/search-user/
42
+ # search by Mixin Id or Phone Number
43
+ def search_user(query, access_token = nil)
44
+ path = format('/search/%<query>s', query: query)
45
+
46
+ access_token ||= access_token('GET', path, '')
47
+ authorization = format('Bearer %<access_token>s', access_token: access_token)
17
48
  client.get(path, headers: { 'Authorization': authorization })
18
49
  end
19
50
 
20
- def fetch_users(user_ids, access_token=nil)
51
+ # https://developers.mixin.one/api/beta-mixin-message/read-users/
52
+ def fetch_users(user_ids, access_token = nil)
21
53
  # user_ids: a array of user_ids
22
54
  path = '/users/fetch'
23
55
  user_ids = [user_ids] if user_ids.is_a? String
24
56
  payload = user_ids
25
- access_token ||= self.access_token('POST', path, payload.to_json)
26
- authorization = format('Bearer %s', access_token)
57
+
58
+ access_token ||= access_token('POST', path, payload.to_json)
59
+ authorization = format('Bearer %<access_token>s', access_token: access_token)
27
60
  client.post(path, headers: { 'Authorization': authorization }, json: payload)
28
61
  end
29
62
  end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MixinBot
4
+ class API
5
+ module Withdraw
6
+ # https://developers.mixin.one/api/alpha-mixin-network/create-address/
7
+ def create_withdraw_address(options)
8
+ path = '/addresses'
9
+ encrypted_pin = encrypt_pin(options[:pin])
10
+ payload =
11
+ # for EOS withdraw, account_name & account_tag must be valid
12
+ if options[:public_key].nil?
13
+ {
14
+ asset_id: options[:asset_id],
15
+ account_name: options[:account_name],
16
+ account_tag: options[:account_tag],
17
+ label: options[:label],
18
+ pin: encrypted_pin
19
+ }
20
+ # for other withdraw
21
+ else
22
+ {
23
+ asset_id: options[:asset_id],
24
+ public_key: options[:public_key],
25
+ label: options[:label],
26
+ pin: encrypted_pin
27
+ }
28
+ end
29
+
30
+ access_token = access_token('POST', path, payload.to_json)
31
+ authorization = format('Bearer %<access_token>s', access_token: access_token)
32
+ client.post(path, headers: { 'Authorization': authorization }, json: payload)
33
+ end
34
+
35
+ # https://developers.mixin.one/api/alpha-mixin-network/read-address/
36
+ def get_withdraw_address(address)
37
+ path = format('/addresses/%<address>s', address: address)
38
+ access_token = access_token('GET', path, '')
39
+ authorization = format('Bearer %<access_token>s', access_token: access_token)
40
+ client.get(path, headers: { 'Authorization': authorization })
41
+ end
42
+
43
+ # https://developers.mixin.one/api/alpha-mixin-network/delete-address/
44
+ def delete_withdraw_address(address, pin)
45
+ path = format('/addresses/%<address>s/delete', address: address)
46
+ payload = {
47
+ pin: encrypt_pin(pin)
48
+ }
49
+
50
+ access_token = access_token('POST', path, payload.to_json)
51
+ authorization = format('Bearer %<access_token>s', access_token: access_token)
52
+ client.post(path, headers: { 'Authorization': authorization }, json: payload)
53
+ end
54
+
55
+ # https://developers.mixin.one/api/alpha-mixin-network/withdrawal-addresses/
56
+ def withdrawals(options)
57
+ address_id = options[:address_id]
58
+ pin = options[:pin]
59
+ amount = options[:amount]
60
+ trace_id = options[:trace_id]
61
+ memo = options[:memo]
62
+
63
+ path = '/withdrawals'
64
+ payload = {
65
+ address_id: address_id,
66
+ amount: amount,
67
+ trace_id: trace_id,
68
+ memo: memo,
69
+ pin: encrypt_pin(pin)
70
+ }
71
+
72
+ access_token = access_token('POST', path, payload.to_json)
73
+ authorization = format('Bearer %<access_token>s', access_token: access_token)
74
+ client.post(path, headers: { 'Authorization': authorization }, json: payload)
75
+ end
76
+ end
77
+ end
78
+ end
data/lib/mixin_bot/api.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative './client'
2
4
  require_relative './errors'
3
5
  require_relative './api/auth'
@@ -9,14 +11,15 @@ require_relative './api/pin'
9
11
  require_relative './api/snapshot'
10
12
  require_relative './api/transfer'
11
13
  require_relative './api/user'
14
+ require_relative './api/withdraw'
12
15
 
13
16
  module MixinBot
14
17
  class API
15
18
  attr_reader :client_id, :client_secret, :session_id, :pin_token, :private_key
16
19
  attr_reader :client
17
20
 
18
- def initialize(options={})
19
- @client_id = options[:client_id] || MixinBot.client_id
21
+ def initialize(options = {})
22
+ @client_id = options[:client_id] || MixinBot.client_id
20
23
  @client_secret = options[:client_secret] || MixinBot.client_secret
21
24
  @session_id = options[:session_id] || MixinBot.session_id
22
25
  @pin_token = Base64.decode64 options[:pin_token] || MixinBot.pin_token
@@ -33,5 +36,6 @@ module MixinBot
33
36
  include MixinBot::API::Snapshot
34
37
  include MixinBot::API::Transfer
35
38
  include MixinBot::API::User
39
+ include MixinBot::API::Withdraw
36
40
  end
37
41
  end
@@ -1,7 +1,9 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module MixinBot
2
4
  class Client
3
- SERVER_SCHEME = 'https'.freeze
4
- SERVER_HOST = 'api.mixin.one'.freeze
5
+ SERVER_SCHEME = 'https'
6
+ SERVER_HOST = 'api.mixin.one'
5
7
 
6
8
  def get(path, options = {})
7
9
  request(:get, path, options)
@@ -14,30 +16,24 @@ module MixinBot
14
16
  private
15
17
 
16
18
  def request(verb, path, options = {})
17
- uri = uri_for(path)
18
- options = options.with_indifferent_access
19
+ uri = uri_for path
19
20
 
20
- options['headers'] ||= {}
21
- if options['headers']['Content-Type'].blank?
22
- options['headers']['Content-Type'] = 'application/json'
23
- end
21
+ options[:headers] ||= {}
22
+ options[:headers]['Content-Type'] ||= 'application/json'
24
23
 
25
24
  begin
26
25
  response = HTTP.timeout(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
26
+ rescue HTTP::Error => e
27
+ raise Errors::HttpError, e.message
31
28
  end
32
29
 
33
- unless response.status.success?
34
- raise Errors::APIError.new(nil, response.to_s)
35
- end
30
+ raise Errors::APIError.new(nil, response.to_s) unless response.status.success?
36
31
 
37
32
  parse_response(response) do |parse_as, result|
38
33
  case parse_as
39
34
  when :json
40
- break result if result[:errcode].blank? || result[:errcode].zero?
35
+ break result if result[:errcode].nil? || result[:errcode].zero?
36
+
41
37
  raise Errors::APIError.new(result[:errcode], result[:errmsg])
42
38
  else
43
39
  result
@@ -58,34 +54,32 @@ module MixinBot
58
54
  content_type = response.headers[:content_type]
59
55
  parse_as = {
60
56
  %r{^application\/json} => :json,
61
- %r{^image\/.*} => :file,
62
- %r{^text\/html} => :xml,
63
- %r{^text\/plain} => :plain
57
+ %r{^image\/.*} => :file,
58
+ %r{^text\/html} => :xml,
59
+ %r{^text\/plain} => :plain
64
60
  }.each_with_object([]) { |match, memo| memo << match[1] if content_type =~ match[0] }.first || :plain
65
61
 
66
62
  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
63
+ result = JSON.parse(response&.body&.to_s)
64
+ result && yield(:json, result)
65
+
66
+ yield(:plain, response.body)
73
67
  end
74
68
 
75
69
  case parse_as
76
70
  when :json
77
- result = ActiveSupport::JSON.decode(response.body.to_s).with_indifferent_access
71
+ result = JSON.parse(response.body.to_s)
78
72
  when :file
79
- if response.headers[:content_type] =~ %r{^image\/.*}
80
- extension =
73
+ extension =
74
+ if response.headers[:content_type] =~ %r{^image\/.*}
81
75
  case response.headers['content-type']
82
76
  when 'image/gif' then '.gif'
83
77
  when 'image/jpeg' then '.jpg'
84
78
  when 'image/png' then '.png'
85
79
  end
86
- else
87
- extension = ''
88
- end
80
+ else
81
+ ''
82
+ end
89
83
 
90
84
  begin
91
85
  file = Tempfile.new(['mixin-file-', extension])
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module MixinBot
2
4
  module Errors
3
5
  # 通用异常
@@ -14,7 +16,7 @@ module MixinBot
14
16
  @errcode = errcode
15
17
  @errmsg = errmsg
16
18
 
17
- super(format('[%s]: %s', @errcode, @errmsg))
19
+ super(format('[%<errcode>s]: %<errmsg>s', errcode: @errcode, errmsg: @errmsg))
18
20
  end
19
21
  end
20
22
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module MixinBot
2
- VERSION = '0.0.1.4'.freeze
4
+ VERSION = '0.1.0'
3
5
  end
data/lib/mixin_bot.rb CHANGED
@@ -1,4 +1,5 @@
1
- require 'active_support/all'
1
+ # frozen_string_literal: true
2
+
2
3
  require 'http'
3
4
  require 'base64'
4
5
  require 'openssl'
@@ -12,6 +13,6 @@ module MixinBot
12
13
  end
13
14
 
14
15
  def self.api
15
- @api ||= MixinBot::API.new(options={})
16
+ @api ||= MixinBot::API.new
16
17
  end
17
18
  end
metadata CHANGED
@@ -1,17 +1,17 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mixin_bot
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1.4
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - an-lee
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-05-16 00:00:00.000000000 Z
11
+ date: 2019-07-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: http
14
+ name: bcrypt
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
17
  - - ">="
@@ -25,7 +25,7 @@ dependencies:
25
25
  - !ruby/object:Gem::Version
26
26
  version: '0'
27
27
  - !ruby/object:Gem::Dependency
28
- name: jwt
28
+ name: http
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
31
  - - ">="
@@ -53,7 +53,7 @@ dependencies:
53
53
  - !ruby/object:Gem::Version
54
54
  version: '0'
55
55
  - !ruby/object:Gem::Dependency
56
- name: bcrypt
56
+ name: jwt
57
57
  requirement: !ruby/object:Gem::Requirement
58
58
  requirements:
59
59
  - - ">="
@@ -67,13 +67,55 @@ dependencies:
67
67
  - !ruby/object:Gem::Version
68
68
  version: '0'
69
69
  - !ruby/object:Gem::Dependency
70
- name: activesupport
70
+ name: rake
71
71
  requirement: !ruby/object:Gem::Requirement
72
72
  requirements:
73
73
  - - ">="
74
74
  - !ruby/object:Gem::Version
75
75
  version: '0'
76
- type: :runtime
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rspec
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rubocop
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: rubocop-rspec
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
77
119
  prerelease: false
78
120
  version_requirements: !ruby/object:Gem::Requirement
79
121
  requirements:
@@ -99,6 +141,7 @@ files:
99
141
  - lib/mixin_bot/api/snapshot.rb
100
142
  - lib/mixin_bot/api/transfer.rb
101
143
  - lib/mixin_bot/api/user.rb
144
+ - lib/mixin_bot/api/withdraw.rb
102
145
  - lib/mixin_bot/client.rb
103
146
  - lib/mixin_bot/errors.rb
104
147
  - lib/mixin_bot/version.rb