mixin_bot 0.9.0 → 0.11.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: a1bfa2114ffe972196a7d934cc499f08cc43b108a274ebca7740cf7ce150b58c
4
- data.tar.gz: 7fd0378e46a47bdb0ce558496dff98327f66f0b9cfef16e5cb042cfd3c511f32
3
+ metadata.gz: c7444d18a70abce961709af67cf6ad5a60b75c4ca42eefa00893bff00941c3b3
4
+ data.tar.gz: 8bc1bbc95ee57b65ed32e1ce88f6118eecc2651cbd6c6bea7a34f5365c0304bd
5
5
  SHA512:
6
- metadata.gz: 46d7366d604e930323471ab60cbec49fe8e881dc5a3d58b5a9302712ea17c611d8cd0c7a99b6afde424ab028d28a810a52cb7a790bf945aa8327765e4fb653e5
7
- data.tar.gz: fef02f6c7722bb033317be3ddc9878b4a0739f88b676192fbc85b956a0cb51aaf477a25737e925b1388da3402a48989d691a75fe4dc16877f8b9c6416c8be4c3
6
+ metadata.gz: 2b036b4f994283e0332f02c74155c51f7acf97c4fe69ba7df895d213945785dbb29af6f9784d383ce2c5c302b0592955ec233f8712ab0c6555188522bf416222
7
+ data.tar.gz: 1e5800db35abccd37683b427d6a607a71f73e294c6903da4ee929037c50673705acd6bdca86a451d6609d30c5aed9956b41a9fee72947d887497a2c9d3706295
@@ -0,0 +1,204 @@
1
+ # frozen_string_literal: false
2
+
3
+ module MixinBot
4
+ class API
5
+ module EncryptedMessage
6
+ def encrypted_text(options)
7
+ options.merge!(category: 'ENCRYPTED_TEXT')
8
+ base_encrypted_message_params(options)
9
+ end
10
+
11
+ def encrypted_post(options)
12
+ options.merge!(category: 'ENCRYPTED_POST')
13
+ base_encrypted_message_params(options)
14
+ end
15
+
16
+ def encrypted_image(options)
17
+ options.merge!(category: 'ENCRYPTED_IMAGE')
18
+ base_encrypted_message_params(options)
19
+ end
20
+
21
+ def encrypted_data(options)
22
+ options.merge!(category: 'ENCRYPTED_DATA')
23
+ base_encrypted_message_params(options)
24
+ end
25
+
26
+ def encrypted_sticker(options)
27
+ options.merge!(category: 'ENCRYPTED_STICKER')
28
+ base_encrypted_message_params(options)
29
+ end
30
+
31
+ def encrypted_contact(options)
32
+ options.merge!(category: 'ENCRYPTED_CONTACT')
33
+ base_encrypted_message_params(options)
34
+ end
35
+
36
+ def encrypted_audio(options)
37
+ options.merge!(category: 'ENCRYPTED_AUDIO')
38
+ base_encrypted_message_params(options)
39
+ end
40
+
41
+ def encrypted_video(options)
42
+ options.merge!(category: 'ENCRYPTED_VIDEO')
43
+ base_encrypted_message_params(options)
44
+ end
45
+
46
+ # use HTTP to send message
47
+ def send_encrypted_text_message(options)
48
+ send_encrypted_message encrypted_text(options)
49
+ end
50
+
51
+ def send_encrypted_post_message(options)
52
+ send_encrypted_message encrypted_post(options)
53
+ end
54
+
55
+ def send_encrypted_image_message(options)
56
+ send_encrypted_message encrypted_image(options)
57
+ end
58
+
59
+ def send_encrypted_data_message(options)
60
+ send_encrypted_message encrypted_data(options)
61
+ end
62
+
63
+ def send_encrypted_sticker_message(options)
64
+ send_encrypted_message encrypted_sticker(options)
65
+ end
66
+
67
+ def send_encrypted_contact_message(options)
68
+ send_encrypted_message encrypted_contact(options)
69
+ end
70
+
71
+ def send_encrypted_audio_message(options)
72
+ send_encrypted_message encrypted_audio(options)
73
+ end
74
+
75
+ def send_encrypted_video_message(options)
76
+ send_encrypted_message encrypted_video(options)
77
+ end
78
+
79
+ # base format of message params
80
+ def base_encrypted_message_params(options)
81
+ data = options[:data].is_a?(String) ? options[:data] : options[:data].to_json
82
+ data_base64 = encrypt_message Base64.urlsafe_encode64(data, padding: false), options[:sessions]
83
+ session_ids = options[:sessions].map(&->(s) { s['session_id'] }).sort
84
+ checksum = Digest::MD5.hexdigest session_ids.join
85
+
86
+ {
87
+ conversation_id: options[:conversation_id],
88
+ recipient_id: options[:recipient_id],
89
+ representative_id: options[:representative_id],
90
+ category: options[:category],
91
+ quote_message_id: options[:quote_message_id],
92
+ message_id: options[:message_id] || SecureRandom.uuid,
93
+ data_base64: data_base64,
94
+ checksum: checksum,
95
+ recipient_sessions: session_ids.map(&->(s) { { session_id: s } }),
96
+ silent: false
97
+ }
98
+ end
99
+
100
+ def send_encrypted_messages(messages)
101
+ send_encrypted_message messages
102
+ end
103
+
104
+ # http post request
105
+ def send_encrypted_message(payload)
106
+ path = '/encrypted_messages'
107
+ payload = [payload] unless payload.is_a?(Array)
108
+ access_token ||= access_token('POST', path, payload.to_json)
109
+ authorization = format('Bearer %<access_token>s', access_token: access_token)
110
+ client.post(path, headers: { 'Authorization': authorization }, json: payload)
111
+ end
112
+
113
+ def encrypt_message(data, sessions = [], sk: nil, pk: nil)
114
+ raise ArgumentError, 'Wrong sessions format!' unless sessions.all?(&->(s) { s.key?('session_id') && s.key?('public_key') })
115
+
116
+ sk = private_key[0...32]
117
+ pk ||= private_key[32...]
118
+
119
+ checksum = Digest::MD5.hexdigest sessions.map(&->(s) { s['session_id'] }).sort.join
120
+ encrypter = OpenSSL::Cipher.new('AES-128-GCM').encrypt
121
+ key = encrypter.random_key
122
+ nounce = encrypter.random_iv
123
+ encrypter.key = key
124
+ encrypter.iv = nounce
125
+ encrypter.auth_data = ''
126
+ ciphertext = encrypter.update(Base64.urlsafe_decode64(data)) + encrypter.final + encrypter.auth_tag
127
+
128
+ bytes = [1]
129
+ bytes += [sessions.size].pack('v*').bytes
130
+ bytes += JOSE::JWA::Ed25519.pk_to_curve25519(pk).bytes
131
+
132
+ sessions.each do |session|
133
+ aes_key = JOSE::JWA::X25519.shared_secret(
134
+ Base64.urlsafe_decode64(session['public_key']),
135
+ JOSE::JWA::Ed25519.secret_to_curve25519(sk)
136
+ )
137
+
138
+ padding = 16 - key.size % 16
139
+ padtext = ([padding] * padding).pack('C*')
140
+
141
+ encrypter = OpenSSL::Cipher.new('AES-256-CBC').encrypt
142
+ encrypter.key = aes_key
143
+ iv = encrypter.random_iv
144
+ encrypter.iv = iv
145
+
146
+ bytes += (MixinBot::Utils::UUID.new(hex: session['session_id']).packed + iv).bytes
147
+ bytes += encrypter.update(key + padtext).bytes
148
+ end
149
+
150
+ bytes += nounce.bytes
151
+ bytes += ciphertext.bytes
152
+
153
+ Base64.urlsafe_encode64 bytes.pack('C*'), padding: false
154
+ end
155
+
156
+ def decrypt_message(data, sk: nil, si: nil)
157
+ bytes = Base64.urlsafe_decode64(data).bytes
158
+
159
+ si ||= session_id
160
+ sk ||= private_key[0...32]
161
+
162
+ size = 16 + 48
163
+ return '' if bytes.size < 1 + 2 + 32 + size + 12
164
+
165
+ session_length = bytes[1...3].pack('v*').unpack1('C*')
166
+ prefix_size = 35 + session_length * size
167
+
168
+ i = 35
169
+ key = ''
170
+ while i < prefix_size
171
+ uuid = MixinBot::Utils::UUID.new(raw: bytes[i...(i + 16)].pack('C*')).unpacked
172
+ if uuid == si
173
+ pub = bytes[3...35]
174
+ aes_key = JOSE::JWA::X25519.shared_secret(
175
+ pub.pack('C*'),
176
+ JOSE::JWA::Ed25519.secret_to_curve25519(sk)
177
+ )
178
+ iv = bytes[(i + 16)...(i + 16 + 16)].pack('C*')
179
+ encrypted_key = bytes[(i + 16 + 16)...(i + size)].pack('C*')
180
+
181
+ decrypter = OpenSSL::Cipher.new('AES-256-CBC').decrypt
182
+ decrypter.iv = iv
183
+ decrypter.key = aes_key
184
+ cipher = decrypter.update(encrypted_key)
185
+ key = cipher[...16]
186
+ break
187
+ end
188
+ i += size
189
+ end
190
+
191
+ return '' unless key.size == 16
192
+
193
+ decrypter = OpenSSL::Cipher.new('AES-128-GCM').decrypt
194
+ decrypter.key = key
195
+ decrypter.iv = bytes[prefix_size...(prefix_size + 12)].pack('C*')
196
+ decrypter.auth_tag = bytes.last(16).pack('C*')
197
+ decrypted = decrypter.update(bytes[(prefix_size + 12)...(bytes.size - 16)].pack('C*'))
198
+ decrypter.final
199
+
200
+ Base64.urlsafe_encode64 decrypted
201
+ end
202
+ end
203
+ end
204
+ end
@@ -8,15 +8,6 @@ module MixinBot
8
8
  write_ws_message(action: 'LIST_PENDING_MESSAGES', params: {})
9
9
  end
10
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
- # }
20
11
  def acknowledge_message_receipt(message_id)
21
12
  params = {
22
13
  message_id: message_id,
@@ -25,200 +16,56 @@ module MixinBot
25
16
  write_ws_message(action: 'ACKNOWLEDGE_MESSAGE_RECEIPT', params: params)
26
17
  end
27
18
 
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
19
  def plain_text(options)
40
20
  options.merge!(category: 'PLAIN_TEXT')
41
21
  base_message_params(options)
42
22
  end
43
23
 
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
24
  def plain_post(options)
56
25
  options.merge!(category: 'PLAIN_POST')
57
26
  base_message_params(options)
58
27
  end
59
28
 
60
- # {
61
- # "id": "UUID",
62
- # "action": "CREATE_MESSAGE",
63
- # "params": {
64
- # "conversation_id": "UUID"
65
- # "category": "PLAIN_IMAGE"
66
- # "status": "SENT",
67
- # "message_id": "UUID",
68
- # "data": "Base64 encoded data"
69
- # }
70
- # }
71
- # data format:
72
- # {
73
- # "attachment_id":
74
- # "Read From POST /attachments",
75
- # "mime_type": "",
76
- # "width": 1024,
77
- # "height": 1024,
78
- # "size": 1024,
79
- # "thumbnail": "base64 encoded"
80
- # }
81
29
  def plain_image(options)
82
30
  options.merge!(category: 'PLAIN_IMAGE')
83
31
  base_message_params(options)
84
32
  end
85
33
 
86
- # {
87
- # "id": "UUID",
88
- # "action": "CREATE_MESSAGE",
89
- # "params": {
90
- # "conversation_id": "UUID",
91
- # "category": "PLAIN_DATA",
92
- # "status": "SENT",
93
- # "message_id": "UUID",
94
- # "data": "Base64 encoded data",
95
- # }
96
- # }
97
- # data format:
98
- # {
99
- # "attachment_id": "Read From POST /attachments",
100
- # "mime_type": "",
101
- # "size": 1024,
102
- # "name": "Share"
103
- # }
104
34
  def plain_data(options)
105
35
  options.merge!(category: 'PLAIN_DATA')
106
36
  base_message_params(options)
107
37
  end
108
38
 
109
- # {
110
- # "id": "UUID",
111
- # "action": "CREATE_MESSAGE",
112
- # "params": {
113
- # "conversation_id": "UUID",
114
- # "category": "PLAIN_STICKER",
115
- # "status": "SENT",
116
- # "message_id": "UUID",
117
- # "data": "Base64 encoded data"
118
- # }
119
- # }
120
- # data format:
121
- # {
122
- # "name": "hello",
123
- # "album_id": "UUID"
124
- # }
125
39
  def plain_sticker(options)
126
40
  options.merge!(category: 'PLAIN_STICKER')
127
41
  base_message_params(options)
128
42
  end
129
43
 
130
- # {
131
- # "id": "UUID",
132
- # "action": "CREATE_MESSAGE",
133
- # "params": {
134
- # "conversation_id": "UUID",
135
- # "category": "PLAIN_CONTACT"
136
- # "status": "SENT",
137
- # "message_id": "UUID",
138
- # "data": "Base64 encoded data"
139
- # }
140
- # }
141
- # data format:
142
- # { "user_id": "UUID"}
143
44
  def plain_contact(options)
144
45
  options.merge!(category: 'PLAIN_CONTACT')
145
46
  base_message_params(options)
146
47
  end
147
48
 
148
- # {
149
- # "id": "UUID",
150
- # "action": "CREATE_MESSAGE",
151
- # "params": {
152
- # "conversation_id": "UUID",
153
- # "category": "APP_CARD",
154
- # "status": "SENT",
155
- # "message_id": "UUID",
156
- # "data": "Base64 encoded data"
157
- # }
158
- # }
159
- # data format:
160
- # {
161
- # "icon_url": "https://mixin.one/assets/98b586edb270556d1972112bd7985e9e.png",
162
- # "title": "Mixin",
163
- # "description": "A free and lightning fast peer-to-peer transactional network for digital assets.",
164
- # "action": "https://mixin.one"
165
- # }
49
+ def plain_audio(options)
50
+ options.merge!(category: 'PLAIN_AUDIO')
51
+ base_message_params(options)
52
+ end
53
+
54
+ def plain_video(options)
55
+ options.merge!(category: 'PLAIN_VIDEO')
56
+ base_message_params(options)
57
+ end
58
+
166
59
  def app_card(options)
167
60
  options.merge!(category: 'APP_CARD')
168
61
  base_message_params(options)
169
62
  end
170
63
 
171
- # {
172
- # "id": "UUID",
173
- # "action": "CREATE_MESSAGE",
174
- # "params": {
175
- # "conversation_id": "UUID",
176
- # "category": "APP_BUTTON_GROUP",
177
- # "status": "SENT",
178
- # "message_id": "UUID",
179
- # "data": "Base64 encoded data"
180
- # }
181
- # }
182
- # data format:
183
- # [
184
- # {
185
- # "label": "Mixin Website",
186
- # "color": "#ABABAB",
187
- # "action": "https://mixin.one"
188
- # },
189
- # ...
190
- # ]
191
64
  def app_button_group(options)
192
65
  options.merge!(category: 'APP_BUTTON_GROUP')
193
66
  base_message_params(options)
194
67
  end
195
68
 
196
- # {
197
- # "id": "UUID",
198
- # "action": "CREATE_MESSAGE",
199
- # "params": {
200
- # "conversation_id": "UUID",
201
- # "category": "PLAIN_VIDEO",
202
- # "status": "SENT",
203
- # "message_id": "UUID",
204
- # "data": "Base64 encoded data"
205
- # }
206
- # }
207
- # data format:
208
- # {
209
- # "attachment_id": "Read From POST /attachments",
210
- # "mime_type": "",
211
- # "width": 1024,
212
- # "height": 1024,
213
- # "size": 1024,
214
- # "duration": 1024,
215
- # "thumbnail": "base64 encoded"
216
- # }
217
- def plain_video(options)
218
- options.merge!(category: 'PLAIN_VIDEO')
219
- base_message_params(options)
220
- end
221
-
222
69
  def recall_message_params(message_id, options)
223
70
  raise 'recipient_id is required!' if options[:recipient_id].nil?
224
71
 
@@ -304,24 +151,6 @@ module MixinBot
304
151
  send_message [recall_message_params(message_id, options)]
305
152
  end
306
153
 
307
- # {
308
- # "id": "UUID",
309
- # "action": "CREATE_PLAIN_MESSAGES",
310
- # "params": {
311
- # "messages": [
312
- # {
313
- # "conversation_id": "UUID",
314
- # "recipient_id": "UUID",
315
- # "message_id": "UUID",
316
- # "representative_id": "UUID (optional, only supported in peer to peer conversation)",
317
- # "quote_message_id": "UUID (optional, only supported text, e.g. PLAIN_TEXT)",
318
- # "category": "Only support plain category e.g.: PLAIN_TEXT, PLAIN_STICKER etc",
319
- # "data": "Correspond to category."
320
- # },
321
- # ...
322
- # ]
323
- # }
324
- # }
325
154
  def send_plain_messages(messages)
326
155
  send_message messages
327
156
  end
@@ -25,7 +25,7 @@ module MixinBot
25
25
  offset: options[:offset],
26
26
  asset: options[:asset],
27
27
  opponent: options[:opponent],
28
- order: options[:order],
28
+ order: options[:order]
29
29
  )
30
30
 
31
31
  access_token = options[:access_token] || access_token('GET', path)
data/lib/mixin_bot/api.rb CHANGED
@@ -8,6 +8,7 @@ require_relative './api/auth'
8
8
  require_relative './api/blaze'
9
9
  require_relative './api/collectible'
10
10
  require_relative './api/conversation'
11
+ require_relative './api/encrypted_message'
11
12
  require_relative './api/me'
12
13
  require_relative './api/message'
13
14
  require_relative './api/multisig'
@@ -83,6 +84,7 @@ module MixinBot
83
84
  include MixinBot::API::Blaze
84
85
  include MixinBot::API::Collectible
85
86
  include MixinBot::API::Conversation
87
+ include MixinBot::API::EncryptedMessage
86
88
  include MixinBot::API::Me
87
89
  include MixinBot::API::Message
88
90
  include MixinBot::API::Multisig
@@ -6,12 +6,12 @@ module MixinBot
6
6
  option :keystore, type: :string, aliases: '-k', required: true, desc: 'keystore or keystore.json file path'
7
7
  option :iterator, type: :string, aliases: '-i', desc: 'Iterator'
8
8
  def encrypt(pin)
9
- log api_instance.encrypt_pin options[:pin].to_s, iterator: options[:iterator]
9
+ log api_instance.encrypt_pin pin.to_s, iterator: options[:iterator]
10
10
  end
11
11
 
12
12
  desc 'unique UUIDS', 'generate unique UUID for two or more UUIDs'
13
13
  def unique(*uuids)
14
- log MixinBot::Utils.unique_uuid *uuids
14
+ log MixinBot::Utils.unique_uuid(*uuids)
15
15
  end
16
16
 
17
17
  desc 'generatetrace HASH', 'generate trace ID from Tx hash'
@@ -31,7 +31,7 @@ module MixinBot
31
31
 
32
32
  @collection = NULL_UUID if collection.blank?
33
33
  @chain = NFT_MEMO_DEFAULT_CHAIN
34
- @nm_class= NFT_MEMO_DEFAULT_CLASS
34
+ @nm_class = NFT_MEMO_DEFAULT_CLASS
35
35
  mark 0
36
36
  encode
37
37
 
@@ -59,9 +59,8 @@ module MixinBot
59
59
 
60
60
  def mark(*indexes)
61
61
  indexes.map do |index|
62
- if index >= 64 || index < 0
63
- raise ArgumentError, "invalid NFO memo index #{index}"
64
- end
62
+ raise ArgumentError, "invalid NFO memo index #{index}" if index >= 64 || index.negative?
63
+
65
64
  @mask = mask ^ (1 << index)
66
65
  end
67
66
  end
@@ -69,7 +68,7 @@ module MixinBot
69
68
  def encode
70
69
  bytes = []
71
70
 
72
- bytes += prefix.bytes
71
+ bytes += prefix.bytes
73
72
  bytes += [version]
74
73
 
75
74
  if mask != 0
@@ -7,13 +7,13 @@ require_relative './utils/transaction'
7
7
  module MixinBot
8
8
  module Utils
9
9
  class << self
10
- MAGIC = [0x77, 0x77]
10
+ MAGIC = [0x77, 0x77].freeze
11
11
  TX_VERSION = 2
12
12
  MAX_ENCODE_INT = 0xFFFF
13
- NULL_BYTES = [0x00, 0x00]
13
+ NULL_BYTES = [0x00, 0x00].freeze
14
14
  AGGREGATED_SIGNATURE_PREFIX = 0xFF01
15
- AGGREGATED_SIGNATURE_ORDINAY_MASK = [0x00]
16
- AGGREGATED_SIGNATURE_SPARSE_MASK = [0x01]
15
+ AGGREGATED_SIGNATURE_ORDINAY_MASK = [0x00].freeze
16
+ AGGREGATED_SIGNATURE_SPARSE_MASK = [0x01].freeze
17
17
 
18
18
  def generate_unique_uuid(uuid_1, uuid_2)
19
19
  md5 = Digest::MD5.new
@@ -53,9 +53,7 @@ module MixinBot
53
53
  end
54
54
 
55
55
  def sign_raw_transaction(tx)
56
- if tx.is_a? String
57
- tx = JSON.parse tx
58
- end
56
+ tx = JSON.parse tx if tx.is_a? String
59
57
  raise ArgumentError, "#{tx} is not a valid json" unless tx.is_a? Hash
60
58
 
61
59
  tx = tx.with_indifferent_access
@@ -64,7 +62,7 @@ module MixinBot
64
62
  asset: tx[:asset],
65
63
  inputs: tx[:inputs],
66
64
  outputs: tx[:outputs],
67
- extra: tx[:extra],
65
+ extra: tx[:extra]
68
66
  ).encode.hex
69
67
  end
70
68
 
@@ -74,15 +72,15 @@ module MixinBot
74
72
 
75
73
  def nft_memo(collection, token, extra)
76
74
  MixinBot::Utils::Nfo.new(
77
- collection: collection,
78
- token: token,
75
+ collection: collection,
76
+ token: token,
79
77
  extra: extra
80
78
  ).mint_memo
81
79
  end
82
80
 
83
81
  def encode_int(int)
84
82
  raise ArgumentError, "only support int #{int}" unless int.is_a?(Integer)
85
- raise ArgumentError,"int #{int} is larger than MAX_ENCODE_INT #{MAX_ENCODE_INT}" if int > MAX_ENCODE_INT
83
+ raise ArgumentError, "int #{int} is larger than MAX_ENCODE_INT #{MAX_ENCODE_INT}" if int > MAX_ENCODE_INT
86
84
 
87
85
  [int].pack('S*').bytes.reverse
88
86
  end
@@ -97,8 +95,9 @@ module MixinBot
97
95
  bytes = []
98
96
  loop do
99
97
  break if int === 0
98
+
100
99
  bytes.push int & 255
101
- int = int / (2 ** 8) | 0
100
+ int = int / (2**8) | 0
102
101
  end
103
102
 
104
103
  bytes.reverse
@@ -107,7 +106,7 @@ module MixinBot
107
106
  def bytes_to_int(bytes)
108
107
  int = 0
109
108
  bytes.each do |byte|
110
- int = int * (2 ** 8) + byte
109
+ int = int * (2**8) + byte
111
110
  end
112
111
 
113
112
  int
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MixinBot
4
- VERSION = '0.9.0'
4
+ VERSION = '0.11.0'
5
5
  end