mixin_bot 0.0.1.4 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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