onyxcord 3.2.7 → 4.0.0.beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/lib/onyxcord/application_commands.rb +2 -0
  3. data/lib/onyxcord/bot.rb +12 -6
  4. data/lib/onyxcord/cache/manager.rb +9 -9
  5. data/lib/onyxcord/cogs/bot.rb +28 -0
  6. data/lib/onyxcord/cogs/cog.rb +264 -0
  7. data/lib/onyxcord/cogs/cog_meta.rb +61 -0
  8. data/lib/onyxcord/cogs/extensions.rb +109 -0
  9. data/lib/onyxcord/commands/bot/channels.rb +1 -0
  10. data/lib/onyxcord/commands/bot/execution.rb +1 -0
  11. data/lib/onyxcord/commands/container.rb +51 -0
  12. data/lib/onyxcord/commands/parser.rb +61 -7
  13. data/lib/onyxcord/commands/rate_limiter.rb +2 -2
  14. data/lib/onyxcord/core/bot/application_commands.rb +20 -16
  15. data/lib/onyxcord/core/bot/messaging.rb +2 -2
  16. data/lib/onyxcord/core/bot/oauth.rb +1 -1
  17. data/lib/onyxcord/core/version.rb +1 -1
  18. data/lib/onyxcord/events/interactions/autocomplete.rb +21 -6
  19. data/lib/onyxcord/gateway/client.rb +29 -14
  20. data/lib/onyxcord/interactions/command.rb +27 -2
  21. data/lib/onyxcord/interactions/internal/application_command.rb +1 -1
  22. data/lib/onyxcord/interactions/registry.rb +36 -0
  23. data/lib/onyxcord/internal/event_bus.rb +5 -3
  24. data/lib/onyxcord/internal/event_executor.rb +7 -0
  25. data/lib/onyxcord/internal/http.rb +18 -2
  26. data/lib/onyxcord/internal/json.rb +26 -22
  27. data/lib/onyxcord/internal/rate_limiter/async_rest.rb +4 -3
  28. data/lib/onyxcord/internal/rate_limiter/rest.rb +5 -3
  29. data/lib/onyxcord/light/light_bot.rb +3 -3
  30. data/lib/onyxcord/models/application.rb +1 -1
  31. data/lib/onyxcord/models/channel.rb +19 -17
  32. data/lib/onyxcord/models/component.rb +3 -3
  33. data/lib/onyxcord/models/interaction.rb +5 -5
  34. data/lib/onyxcord/models/member.rb +8 -8
  35. data/lib/onyxcord/models/message.rb +5 -5
  36. data/lib/onyxcord/models/poll.rb +2 -2
  37. data/lib/onyxcord/models/profile.rb +4 -4
  38. data/lib/onyxcord/models/role.rb +1 -1
  39. data/lib/onyxcord/models/scheduled_event.rb +3 -3
  40. data/lib/onyxcord/models/server.rb +46 -34
  41. data/lib/onyxcord/models/user.rb +1 -1
  42. data/lib/onyxcord/models/webhook.rb +7 -7
  43. data/lib/onyxcord/rest/client.rb +43 -7
  44. data/lib/onyxcord/voice/client.rb +2 -0
  45. data/lib/onyxcord/voice/network/udp.rb +4 -1
  46. data/lib/onyxcord/voice/network/websocket.rb +8 -3
  47. data/lib/onyxcord/webhooks/view/container_builder.rb +6 -1
  48. data/lib/onyxcord.rb +5 -1
  49. metadata +5 -1
@@ -122,6 +122,8 @@ module OnyxCord
122
122
 
123
123
  # Whether this server's members have been chunked (resolved using op 8 and GUILD_MEMBERS_CHUNK) yet
124
124
  @chunked = false
125
+ @chunk_mutex = Mutex.new
126
+ @chunk_condition = ConditionVariable.new
125
127
  end
126
128
 
127
129
  # @return [Member] The server owner.
@@ -172,7 +174,7 @@ module OnyxCord
172
174
  # end
173
175
  # @return [Hash<Integer => Integer>] A hash mapping role IDs to their respective member counts.
174
176
  def role_member_counts
175
- response = JSON.parse(REST::Server.role_member_counts(@bot.token, @id))
177
+ response = OnyxCord::Internal::JSON.parse(REST::Server.role_member_counts(@bot.token, @id))
176
178
  response.transform_keys!(&:to_i)
177
179
  response.tap { |hash| hash[@id] = @member_count }
178
180
  end
@@ -202,7 +204,13 @@ module OnyxCord
202
204
  raise 'The :server_members intent is required to get server members' if @bot.gateway.intents.nobits?(INTENTS[:server_members])
203
205
 
204
206
  @bot.request_chunks(@id)
205
- sleep 0.05 until @chunked
207
+
208
+ @chunk_mutex.synchronize do
209
+ @chunk_condition.wait(@chunk_mutex, 30) unless @chunked
210
+ end
211
+
212
+ raise Timeout::Error, "Guild members not received for server #{@id} within 30s" unless @chunked
213
+
206
214
  @members.values
207
215
  end
208
216
 
@@ -226,7 +234,7 @@ module OnyxCord
226
234
  # @return [Array<Integration>] an array of the integrations in this server.
227
235
  # @note If the server has more than 50 integrations, they cannot be accessed.
228
236
  def integrations
229
- integration = JSON.parse(REST::Server.integrations(@bot.token, @id))
237
+ integration = OnyxCord::Internal::JSON.parse(REST::Server.integrations(@bot.token, @id))
230
238
  integration.map { |element| Integration.new(element, @bot, self) }
231
239
  end
232
240
 
@@ -241,14 +249,14 @@ module OnyxCord
241
249
  action = AuditLogs::ACTIONS.key(action)
242
250
  user = user.resolve_id if user
243
251
  before = before.resolve_id if before
244
- AuditLogs.new(self, @bot, JSON.parse(REST::Server.audit_logs(@bot.token, @id, limit, user, action, before)))
252
+ AuditLogs.new(self, @bot, OnyxCord::Internal::JSON.parse(REST::Server.audit_logs(@bot.token, @id, limit, user, action, before)))
245
253
  end
246
254
 
247
255
  # @note For internal use only
248
256
  # @!visibility private
249
257
  def cache_widget_data(data = nil)
250
258
  data ||= if bot.permission?(:manage_server)
251
- JSON.parse(REST::Server.widget(@bot.token, @id))
259
+ OnyxCord::Internal::JSON.parse(REST::Server.widget(@bot.token, @id))
252
260
  else
253
261
  return update_data(nil)
254
262
  end
@@ -315,7 +323,7 @@ module OnyxCord
315
323
  def modify_widget(enabled, channel, reason = nil)
316
324
  cache_widget_data if @widget_enabled.nil?
317
325
  channel_id = channel ? channel.resolve_id : @widget_channel_id
318
- cache_widget_data(JSON.parse(REST::Server.modify_widget(@bot.token, @id, enabled, channel_id, reason)))
326
+ cache_widget_data(OnyxCord::Internal::JSON.parse(REST::Server.modify_widget(@bot.token, @id, enabled, channel_id, reason)))
319
327
  end
320
328
  alias_method :modify_embed, :modify_widget
321
329
 
@@ -347,7 +355,7 @@ module OnyxCord
347
355
  response = REST::Server.add_member(@bot.token, @id, user_id, access_token, nick, roles, deaf, mute)
348
356
  return nil if response.empty?
349
357
 
350
- add_member Member.new(JSON.parse(response), self, @bot)
358
+ add_member Member.new(OnyxCord::Internal::JSON.parse(response), self, @bot)
351
359
  end
352
360
 
353
361
  # Returns the amount of members that are candidates for pruning
@@ -357,7 +365,7 @@ module OnyxCord
357
365
  def prune_count(days)
358
366
  raise ArgumentError, 'Days must be between 1 and 30' unless days.between?(1, 30)
359
367
 
360
- response = JSON.parse REST::Server.prune_count(@bot.token, @id, days)
368
+ response = OnyxCord::Internal::JSON.parse REST::Server.prune_count(@bot.token, @id, days)
361
369
  response['pruned']
362
370
  end
363
371
 
@@ -369,7 +377,7 @@ module OnyxCord
369
377
  def begin_prune(days, reason = nil)
370
378
  raise ArgumentError, 'Days must be between 1 and 30' unless days.between?(1, 30)
371
379
 
372
- response = JSON.parse REST::Server.begin_prune(@bot.token, @id, days, reason)
380
+ response = OnyxCord::Internal::JSON.parse REST::Server.begin_prune(@bot.token, @id, days, reason)
373
381
  response['pruned']
374
382
  end
375
383
 
@@ -581,7 +589,7 @@ module OnyxCord
581
589
  total = nil
582
590
 
583
591
  get_messages = lambda do |query|
584
- data = JSON.parse(REST::Server.search_messages(@bot.token, @id, **options, **query.compact))
592
+ data = OnyxCord::Internal::JSON.parse(REST::Server.search_messages(@bot.token, @id, **options, **query.compact))
585
593
  total ||= data['total_results']
586
594
 
587
595
  data['threads']&.each do |thread|
@@ -621,13 +629,14 @@ module OnyxCord
621
629
  # @note For internal use only
622
630
  # @!visibility private
623
631
  def delete_role(role_id)
624
- @roles.delete(role_id.resolve_id)
632
+ resolved_id = role_id.resolve_id
633
+ @roles.delete(resolved_id)
625
634
  @members.each_value do |member|
626
- new_roles = member.roles.reject { |r| r.id == role_id }
635
+ new_roles = member.roles.reject { |r| r.id == resolved_id }
627
636
  member.update_roles(new_roles)
628
637
  end
629
638
  @channels.each do |channel|
630
- overwrites = channel.permission_overwrites.reject { |id, _| id == role_id }
639
+ overwrites = channel.permission_overwrites.reject { |id, _| id == resolved_id }
631
640
  channel.update_overwrites(overwrites)
632
641
  end
633
642
  end
@@ -636,7 +645,7 @@ module OnyxCord
636
645
  # @note For internal use only
637
646
  # @!visibility private
638
647
  def update_role_positions(role_positions, reason: nil)
639
- response = JSON.parse(REST::Server.update_role_positions(@bot.token, @id, role_positions, reason))
648
+ response = OnyxCord::Internal::JSON.parse(REST::Server.update_role_positions(@bot.token, @id, role_positions, reason))
640
649
  response.each { |data| role(data['id'].to_i)&.update_data(data) }
641
650
  end
642
651
 
@@ -739,7 +748,7 @@ module OnyxCord
739
748
  permission_overwrites.map! { |e| e.is_a?(Overwrite) ? e.to_hash : e } if permission_overwrites.is_a?(Array)
740
749
  parent_id = parent.respond_to?(:resolve_id) ? parent.resolve_id : nil
741
750
  response = REST::Server.create_channel(@bot.token, @id, name, type, topic, bitrate, user_limit, permission_overwrites, parent_id, nsfw, rate_limit_per_user, position, reason)
742
- Channel.new(JSON.parse(response), @bot)
751
+ Channel.new(OnyxCord::Internal::JSON.parse(response), @bot)
743
752
  end
744
753
 
745
754
  # Creates a role on this server which can then be modified. It will be initialized
@@ -784,7 +793,7 @@ module OnyxCord
784
793
 
785
794
  response = REST::Server.create_role(@bot.token, @id, name, nil, hoist, mentionable, permissions&.to_s, reason, colours, icon, unicode_emoji)
786
795
 
787
- role = Role.new(JSON.parse(response), @bot, self)
796
+ role = Role.new(OnyxCord::Internal::JSON.parse(response), @bot, self)
788
797
  @roles[role.id] = role
789
798
  end
790
799
 
@@ -797,7 +806,7 @@ module OnyxCord
797
806
  def add_emoji(name, image, roles = [], reason: nil)
798
807
  image = image.respond_to?(:read) ? OnyxCord.encode64(image) : image
799
808
 
800
- data = JSON.parse(REST::Server.add_emoji(@bot.token, @id, image, name, roles.map(&:resolve_id), reason))
809
+ data = OnyxCord::Internal::JSON.parse(REST::Server.add_emoji(@bot.token, @id, image, name, roles.map(&:resolve_id), reason))
801
810
  new_emoji = Emoji.new(data, @bot, self)
802
811
  @emoji[new_emoji.id] = new_emoji
803
812
  end
@@ -817,7 +826,7 @@ module OnyxCord
817
826
  # @return [Emoji] The edited emoji.
818
827
  def edit_emoji(emoji, name: nil, roles: nil, reason: nil)
819
828
  emoji = @emoji[emoji.resolve_id]
820
- data = JSON.parse(REST::Server.edit_emoji(@bot.token, @id, emoji.resolve_id, name || emoji.name, (roles || emoji.roles).map(&:resolve_id), reason))
829
+ data = OnyxCord::Internal::JSON.parse(REST::Server.edit_emoji(@bot.token, @id, emoji.resolve_id, name || emoji.name, (roles || emoji.roles).map(&:resolve_id), reason))
821
830
  new_emoji = Emoji.new(data, @bot, self)
822
831
  @emoji[new_emoji.id] = new_emoji
823
832
  end
@@ -842,7 +851,7 @@ module OnyxCord
842
851
  # @param limit [Integer] The maximum number of members between 1-1000 to return. Returns 1 member by default.
843
852
  # @return [Array<Member>, nil] An array of member objects that match the given parameters, or nil for no members.
844
853
  def search_members(name:, limit: nil)
845
- response = JSON.parse(REST::Server.search_guild_members(@bot.token, @id, name, limit))
854
+ response = OnyxCord::Internal::JSON.parse(REST::Server.search_guild_members(@bot.token, @id, name, limit))
846
855
  return nil if response.empty?
847
856
 
848
857
  response.map { |mem| Member.new(mem, self, @bot) }
@@ -854,7 +863,7 @@ module OnyxCord
854
863
  # @param after_id [Integer] Consider only users after given user id.
855
864
  # @return [Array<ServerBan>] a list of banned users on this server and the reason they were banned.
856
865
  def bans(limit: nil, before_id: nil, after_id: nil)
857
- response = JSON.parse(REST::Server.bans(@bot.token, @id, limit, before_id, after_id))
866
+ response = OnyxCord::Internal::JSON.parse(REST::Server.bans(@bot.token, @id, limit, before_id, after_id))
858
867
  response.map do |e|
859
868
  ServerBan.new(self, @bot.ensure_user(e['user']), e['reason'])
860
869
  end
@@ -876,7 +885,7 @@ module OnyxCord
876
885
 
877
886
  get_bans = lambda do |before: nil, after: nil|
878
887
  data = REST::Server.bans(@bot.token, @id, f_limit, before&.id || f_before, after&.id || f_after)
879
- JSON.parse(data).map { |ban| ServerBan.new(self, @bot.ensure_user(ban['user']), ban['reason']) }
888
+ OnyxCord::Internal::JSON.parse(data).map { |ban| ServerBan.new(self, @bot.ensure_user(ban['user']), ban['reason']) }
880
889
  end
881
890
 
882
891
  paginator = Paginator.new(limit, before ? :up : :down) do |page|
@@ -923,7 +932,7 @@ module OnyxCord
923
932
  return ban(users.first, 0, message_seconds: message_seconds, reason: reason) if users.size == 1
924
933
 
925
934
  response = REST::Server.bulk_ban(@bot.token, @id, users.map(&:resolve_id), message_seconds, reason)
926
- BulkBan.new(JSON.parse(response), self, reason)
935
+ BulkBan.new(OnyxCord::Internal::JSON.parse(response), self, reason)
927
936
  end
928
937
 
929
938
  # Kicks a user from this server.
@@ -958,7 +967,7 @@ module OnyxCord
958
967
 
959
968
  @available_voice_regions = {}
960
969
 
961
- data = JSON.parse REST::Server.regions(@bot.token, @id)
970
+ data = OnyxCord::Internal::JSON.parse REST::Server.regions(@bot.token, @id)
962
971
  @available_voice_regions = data.map { |e| VoiceRegion.new e }
963
972
  end
964
973
 
@@ -971,7 +980,7 @@ module OnyxCord
971
980
  # Moves the server to another region. This will cause a voice interruption of at most a second.
972
981
  # @param region [String] The new region the server should be in.
973
982
  def region=(region)
974
- update_data(JSON.parse(REST::Server.update!(@bot.token, @id, region: region.to_s)))
983
+ update_data(OnyxCord::Internal::JSON.parse(REST::Server.update!(@bot.token, @id, region: region.to_s)))
975
984
  end
976
985
 
977
986
  # Sets the server's icon.
@@ -1127,14 +1136,14 @@ module OnyxCord
1127
1136
  # Requests a list of Webhooks on the server.
1128
1137
  # @return [Array<Webhook>] webhooks on the server.
1129
1138
  def webhooks
1130
- webhooks = JSON.parse(REST::Server.webhooks(@bot.token, @id))
1139
+ webhooks = OnyxCord::Internal::JSON.parse(REST::Server.webhooks(@bot.token, @id))
1131
1140
  webhooks.map { |webhook| Webhook.new(webhook, @bot) }
1132
1141
  end
1133
1142
 
1134
1143
  # Requests a list of Invites to the server.
1135
1144
  # @return [Array<Invite>] invites to the server.
1136
1145
  def invites
1137
- invites = JSON.parse(REST::Server.invites(@bot.token, @id))
1146
+ invites = OnyxCord::Internal::JSON.parse(REST::Server.invites(@bot.token, @id))
1138
1147
  invites.map { |invite| Invite.new(invite, @bot) }
1139
1148
  end
1140
1149
 
@@ -1143,7 +1152,7 @@ module OnyxCord
1143
1152
  # should be ignored and re-fetched via an HTTP request.
1144
1153
  # @return [Array<ScheduledEvent>] The scheduled events on the server.
1145
1154
  def scheduled_events(bypass_cache: false)
1146
- process_scheduled_events(JSON.parse(REST::Server.list_scheduled_events(@bot.token, @id, with_user_count: true))) if bypass_cache
1155
+ process_scheduled_events(OnyxCord::Internal::JSON.parse(REST::Server.list_scheduled_events(@bot.token, @id, with_user_count: true))) if bypass_cache
1147
1156
 
1148
1157
  @scheduled_events.values
1149
1158
  end
@@ -1157,7 +1166,7 @@ module OnyxCord
1157
1166
  return @scheduled_events[id] if @scheduled_events[id]
1158
1167
  return nil unless request
1159
1168
 
1160
- event = JSON.parse(REST::Server.get_scheduled_event(@bot.token, @id, id, with_user_count: true))
1169
+ event = OnyxCord::Internal::JSON.parse(REST::Server.get_scheduled_event(@bot.token, @id, id, with_user_count: true))
1161
1170
  scheduled_event = ScheduledEvent.new(event, self, @bot)
1162
1171
  @scheduled_events[scheduled_event.id] = scheduled_event
1163
1172
  rescue StandardError
@@ -1193,7 +1202,7 @@ module OnyxCord
1193
1202
  recurrence_rule: block_given? ? builder.to_h : recurrence_rule&.to_h
1194
1203
  }
1195
1204
 
1196
- event = JSON.parse(REST::Server.create_scheduled_event(@bot.token, @id, **options, reason: reason))
1205
+ event = OnyxCord::Internal::JSON.parse(REST::Server.create_scheduled_event(@bot.token, @id, **options, reason: reason))
1197
1206
  scheduled_event = ScheduledEvent.new(event, self, @bot)
1198
1207
  @scheduled_events[scheduled_event.id] = scheduled_event
1199
1208
  end
@@ -1210,7 +1219,10 @@ module OnyxCord
1210
1219
  LOGGER.debug("Finished chunking server #{@id}")
1211
1220
 
1212
1221
  # Reset everything to normal
1213
- @chunked = true
1222
+ @chunk_mutex.synchronize do
1223
+ @chunked = true
1224
+ end
1225
+ @chunk_condition.broadcast
1214
1226
  end
1215
1227
 
1216
1228
  # Get the AFK channel of the server.
@@ -1305,7 +1317,7 @@ module OnyxCord
1305
1317
  channel_id: widget_channel == :undef ? widget_channel : widget_channel&.resolve_id
1306
1318
  }
1307
1319
 
1308
- cache_widget_data(JSON.parse(REST::Server.update_widget(@bot.token, @id, **widget_data, reason: reason)))
1320
+ cache_widget_data(OnyxCord::Internal::JSON.parse(REST::Server.update_widget(@bot.token, @id, **widget_data, reason: reason)))
1309
1321
  end
1310
1322
 
1311
1323
  if invites_disabled_until != :undef || dms_disabled_until != :undef
@@ -1314,12 +1326,12 @@ module OnyxCord
1314
1326
  invites_disabled_until: invites_disabled_until == :undef ? @invites_disabled_until&.iso8601 : invites_disabled_until&.iso8601
1315
1327
  }
1316
1328
 
1317
- process_incident_actions(JSON.parse(REST::Server.update_incident_actions(@bot.token, @id, **incidents_data, reason: reason)))
1329
+ process_incident_actions(OnyxCord::Internal::JSON.parse(REST::Server.update_incident_actions(@bot.token, @id, **incidents_data, reason: reason)))
1318
1330
  end
1319
1331
 
1320
1332
  return unless data.any? { |_, value| value != :undef }
1321
1333
 
1322
- update_data(JSON.parse(REST::Server.update!(@bot.token, @id, **data, reason: reason)))
1334
+ update_data(OnyxCord::Internal::JSON.parse(REST::Server.update!(@bot.token, @id, **data, reason: reason)))
1323
1335
  nil
1324
1336
  end
1325
1337
 
@@ -1327,7 +1339,7 @@ module OnyxCord
1327
1339
  # @note For internal use only
1328
1340
  # @!visibility private
1329
1341
  def update_data(new_data = nil)
1330
- new_data ||= JSON.parse(REST::Server.resolve(@bot.token, @id))
1342
+ new_data ||= OnyxCord::Internal::JSON.parse(REST::Server.resolve(@bot.token, @id))
1331
1343
  @name = new_data['name']
1332
1344
  @icon_id = new_data['icon']
1333
1345
  @splash_id = new_data['splash']
@@ -193,7 +193,7 @@ module OnyxCord
193
193
  # @return [String, nil] the ID of this user's current banner, can be used to generate a banner URL.
194
194
  # @see #banner_url
195
195
  def banner_id
196
- @banner_id ||= JSON.parse(REST::User.resolve(@bot.token, @id))['banner']
196
+ @banner_id ||= OnyxCord::Internal::JSON.parse(REST::User.resolve(@bot.token, @id))['banner']
197
197
  end
198
198
 
199
199
  # Set the user's username
@@ -153,7 +153,7 @@ module OnyxCord
153
153
 
154
154
  resp = REST::Webhook.token_execute_webhook(@token, @id, wait, data[:content], data[:username], data[:avatar_url], data[:tts], data[:file], data[:embeds], data[:allowed_mentions], flags, components.to_a)
155
155
 
156
- Message.new(JSON.parse(resp), @bot) if wait
156
+ Message.new(OnyxCord::Internal::JSON.parse(resp), @bot) if wait
157
157
  end
158
158
 
159
159
  # Delete a message created by this webhook.
@@ -192,7 +192,7 @@ module OnyxCord
192
192
  edit_content = content == OnyxCord::MessagePayload::KEEP || !content.nil? ? content : data[:content]
193
193
  edit_embeds = embeds == OnyxCord::MessagePayload::KEEP || !embeds.nil? ? embeds : data[:embeds]
194
194
  resp = REST::Webhook.token_edit_message(@token, @id, message.resolve_id, edit_content, edit_embeds, data[:allowed_mentions], components.to_a, nil, flags)
195
- Message.new(JSON.parse(resp), @bot)
195
+ Message.new(OnyxCord::Internal::JSON.parse(resp), @bot)
196
196
  end
197
197
 
198
198
  # Utility function to get a webhook's avatar URL.
@@ -228,11 +228,11 @@ module OnyxCord
228
228
 
229
229
  def update_webhook(new_data)
230
230
  reason = new_data.delete(:reason)
231
- data = JSON.parse(if token?
232
- REST::Webhook.token_update_webhook(@token, @id, new_data, reason)
233
- else
234
- REST::Webhook.update_webhook(@bot.token, @id, new_data, reason)
235
- end)
231
+ data = OnyxCord::Internal::JSON.parse(if token?
232
+ REST::Webhook.token_update_webhook(@token, @id, new_data, reason)
233
+ else
234
+ REST::Webhook.update_webhook(@bot.token, @id, new_data, reason)
235
+ end)
236
236
  # Only update cache if API call worked
237
237
  update_internal(data) if data['name']
238
238
  end
@@ -12,11 +12,17 @@ require_relative '../internal/rate_limiter/async_rest'
12
12
  # List of methods representing endpoints in Discord's API
13
13
  module OnyxCord::REST
14
14
  # The base URL of the Discord REST API.
15
- APIBASE = 'https://discord.com/api/v9'
15
+ APIBASE = 'https://discord.com/api/v10'
16
16
 
17
17
  # The URL of Discord's CDN
18
18
  CDN_URL = 'https://cdn.discordapp.com'
19
19
 
20
+ # Maximum number of retries for 502 Bad Gateway errors
21
+ MAX_RAW_RETRIES = 3
22
+
23
+ # Maximum number of retries for 429 Rate Limit errors
24
+ MAX_429_RETRIES = 5
25
+
20
26
  module_function
21
27
 
22
28
  # @return [String] the currently used API base URL.
@@ -62,8 +68,11 @@ module OnyxCord::REST
62
68
 
63
69
  # Resets all rate limit mutexes
64
70
  def reset_mutexes
65
- @rate_limiter = ::OnyxCord::Internal::RateLimiter::Rest.new
66
- @async_rate_limiter = ::OnyxCord::Internal::RateLimiter::AsyncRest.new
71
+ @rest_mutex ||= Mutex.new
72
+ @rest_mutex.synchronize do
73
+ @rate_limiter = ::OnyxCord::Internal::RateLimiter::Rest.new
74
+ @async_rate_limiter = ::OnyxCord::Internal::RateLimiter::AsyncRest.new
75
+ end
67
76
  end
68
77
 
69
78
  def rate_limiter
@@ -118,9 +127,15 @@ module OnyxCord::REST
118
127
  raise noprm
119
128
  end
120
129
 
121
- # Retry on 502 Bad Gateway
130
+ # Retry on 502 Bad Gateway (with limit and backoff)
122
131
  if response.code == 502
132
+ retries = caller.count { |c| c.include?('raw_request') }
133
+ if retries >= MAX_RAW_RETRIES
134
+ OnyxCord::LOGGER.warn("Got #{MAX_RAW_RETRIES} consecutive 502s — giving up on #{url}")
135
+ return response
136
+ end
123
137
  OnyxCord::LOGGER.warn('Got a 502 while sending a request! Not a big deal, retrying the request')
138
+ sleep(2**retries)
124
139
  return raw_request(type, url, body, **headers)
125
140
  end
126
141
 
@@ -203,7 +218,7 @@ module OnyxCord::REST
203
218
 
204
219
  if response.code >= 400 && response.code != 429
205
220
  data = begin
206
- JSON.parse(response.body)
221
+ OnyxCord::Internal::JSON.parse(response.body)
207
222
  rescue StandardError
208
223
  nil
209
224
  end
@@ -222,7 +237,11 @@ module OnyxCord::REST
222
237
 
223
238
  err_klass = OnyxCord::Errors.error_class_for(data['code'] || 0)
224
239
  e = err_klass.new(data['message'], data['errors'], status: response.code, headers: response.headers, route: route, body: response.body, response: response)
225
- OnyxCord::LOGGER.error(e.full_message)
240
+ if e.is_a?(OnyxCord::Errors::UnknownMessage)
241
+ OnyxCord::LOGGER.warn('Ignoring stale Discord message reference.')
242
+ else
243
+ OnyxCord::LOGGER.error(e.full_message)
244
+ end
226
245
  raise e
227
246
  end
228
247
  rescue OnyxCord::Errors::NoPermission => e
@@ -233,13 +252,30 @@ module OnyxCord::REST
233
252
  end
234
253
 
235
254
  if response.code == 429
255
+ retries_for_429 = caller.count { |c| c.include?('request_async') && c.include?('429') }
236
256
  trace("429 #{key} #{major_parameter}")
237
257
  async_rate_limiter.handle_rate_limit(key, major_parameter, response)
258
+
259
+ if retries_for_429 >= MAX_429_RETRIES
260
+ raise OnyxCord::Errors::HTTPError.new(
261
+ "Rate limited after #{MAX_429_RETRIES} retries: #{key} #{major_parameter}",
262
+ status: 429,
263
+ headers: response.headers,
264
+ body: response.body,
265
+ response: response
266
+ )
267
+ end
268
+
269
+ retry_after = response.headers[:x_ratelimit_reset_after]&.to_f || response.headers[:retry_after]&.to_f
270
+ if retry_after && retry_after.positive?
271
+ OnyxCord::LOGGER.warn("Rate limited — waiting #{retry_after}s before retry ##{retries_for_429 + 1}")
272
+ OnyxCord::Internal::AsyncRuntime.sleep(retry_after)
273
+ end
238
274
  return request_async(key, major_parameter, type, url, body, headers)
239
275
  end
240
276
 
241
277
  if response.code == 202 && response.body
242
- body_data = JSON.parse(response.body)
278
+ body_data = OnyxCord::Internal::JSON.parse(response.body)
243
279
 
244
280
  if body_data['code'] == 110_000
245
281
  case body_data['retry_after']
@@ -230,6 +230,8 @@ module OnyxCord::Voice
230
230
  # Windows does not support TERM as a kill signal, so we use KILL. `Process.waitpid` verifies that our
231
231
  # child process has not already completed.
232
232
  Process.kill(Gem.win_platform? ? 'KILL' : 'TERM', pid) if Process.waitpid(pid, Process::WNOHANG).nil?
233
+ rescue Errno::ESRCH, Errno::ECHILD
234
+ # Process already exited or doesn't exist — no action needed
233
235
  rescue StandardError => e
234
236
  OnyxCord::LOGGER.warn('Failed to kill ffmpeg process! You *might* have a process leak now.')
235
237
  OnyxCord::LOGGER.warn("Reason: #{e}")
@@ -37,8 +37,11 @@ module OnyxCord::Voice
37
37
  # Waits for a UDP discovery reply, and returns the sent data.
38
38
  # @return [Array(String, Integer)] the IP and port received from the discovery reply.
39
39
  def receive_discovery_reply
40
- # Wait for a UDP message
40
+ # Wait for a UDP message with timeout
41
+ raise Timeout::Error, 'Timed out waiting for UDP discovery reply' unless @socket.wait_readable(5)
42
+
41
43
  message = @socket.recv(74)
44
+
42
45
  ip = message[8..-3].delete("\0")
43
46
  port = message[-2..].unpack1('n')
44
47
  [ip, port]
@@ -64,7 +64,7 @@ module OnyxCord::Voice
64
64
 
65
65
  # Send a heartbeat (op 3), has to be done every @heartbeat_interval seconds or the connection will terminate
66
66
  def send_heartbeat
67
- millis = Time.now.strftime('%s%L').to_i
67
+ millis = (Process.clock_gettime(Process::CLOCK_REALTIME, :millisecond)).to_i
68
68
  @bot.debug("Sending voice heartbeat at #{millis}")
69
69
 
70
70
  send_opcode(
@@ -110,7 +110,7 @@ module OnyxCord::Voice
110
110
  # @!visibility private
111
111
  def websocket_message(msg)
112
112
  @bot.debug("Received VWS message! #{msg}")
113
- packet = JSON.parse(msg)
113
+ packet = OnyxCord::Internal::JSON.parse(msg)
114
114
 
115
115
  @seq = packet['seq'] if packet['seq']
116
116
 
@@ -136,6 +136,7 @@ module OnyxCord::Voice
136
136
  @ready = true
137
137
  @udp.secret_key = @ws_data['secret_key'].pack('C*')
138
138
  @udp.mode = @ws_data['mode']
139
+ @ready_condition&.broadcast
139
140
  when OnyxCord::Voice::Opcodes::HELLO
140
141
  # Opcode 8 contains the heartbeat interval.
141
142
  @heartbeat_interval = packet['d']['heartbeat_interval']
@@ -176,7 +177,11 @@ module OnyxCord::Voice
176
177
  @bot.debug('Waiting for op 4 now')
177
178
 
178
179
  # Wait for op 4, then finish
179
- sleep 0.05 until @ready
180
+ @ready_mutex ||= Mutex.new
181
+ @ready_condition ||= ConditionVariable.new
182
+ @ready_mutex.synchronize do
183
+ @ready_condition.wait(@ready_mutex, 30) unless @ready
184
+ end
180
185
  end
181
186
 
182
187
  # Disconnects the websocket and kills the thread
@@ -60,7 +60,12 @@ class OnyxCord::Webhooks::View
60
60
  def colour=(colour)
61
61
  @colour = case colour
62
62
  when Array
63
- (colour[0] << 16) | (colour[1] << 8) | colour[2]
63
+ raise ArgumentError, 'Colour array must have exactly 3 elements (R, G, B)' unless colour.size == 3
64
+
65
+ r, g, b = colour
66
+ raise ArgumentError, 'RGB values must be between 0 and 255' unless [r, g, b].all? { |v| v.is_a?(Integer) && v.between?(0, 255) }
67
+
68
+ (r << 16) | (g << 8) | b
64
69
  when String
65
70
  colour.delete('#').to_i(16)
66
71
  else
data/lib/onyxcord.rb CHANGED
@@ -199,8 +199,10 @@ loader.ignore(
199
199
  "#{__dir__}/onyxcord/cache",
200
200
  "#{__dir__}/onyxcord/gateway",
201
201
  "#{__dir__}/onyxcord/container.rb",
202
+ "#{__dir__}/onyxcord/cogs",
202
203
  "#{__dir__}/onyxcord/bot.rb",
203
- "#{__dir__}/onyxcord/commands"
204
+ "#{__dir__}/onyxcord/commands",
205
+ "#{__dir__}/onyxcord/light"
204
206
  )
205
207
 
206
208
  loader.setup
@@ -238,6 +240,7 @@ loader.setup
238
240
  cache/manager
239
241
  await
240
242
  container
243
+ cogs/cog
241
244
  internal/event_bus
242
245
  gateway/client
243
246
  application_commands
@@ -246,6 +249,7 @@ loader.setup
246
249
  webhooks
247
250
  bot
248
251
  commands/bot
252
+ light
249
253
  ].each { |path| require_relative "onyxcord/#{path}" }
250
254
 
251
255
  loader.eager_load if ENV['ONYXCORD_EAGER_LOAD']
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: onyxcord
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.2.7
4
+ version: 4.0.0.beta.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Gustavo Silva
@@ -391,6 +391,10 @@ files:
391
391
  - lib/onyxcord/bot.rb
392
392
  - lib/onyxcord/cache/manager.rb
393
393
  - lib/onyxcord/cache/stores/gateway.rb
394
+ - lib/onyxcord/cogs/bot.rb
395
+ - lib/onyxcord/cogs/cog.rb
396
+ - lib/onyxcord/cogs/cog_meta.rb
397
+ - lib/onyxcord/cogs/extensions.rb
394
398
  - lib/onyxcord/commands/bot.rb
395
399
  - lib/onyxcord/commands/bot/channels.rb
396
400
  - lib/onyxcord/commands/bot/execution.rb