discordrb 3.4.3 → 3.6.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.
Files changed (95) hide show
  1. checksums.yaml +4 -4
  2. data/.devcontainer/Dockerfile +13 -0
  3. data/.devcontainer/devcontainer.json +29 -0
  4. data/.devcontainer/postcreate.sh +4 -0
  5. data/.github/ISSUE_TEMPLATE/bug_report.md +0 -1
  6. data/.github/ISSUE_TEMPLATE/feature_request.md +0 -1
  7. data/.github/workflows/ci.yml +78 -0
  8. data/.github/workflows/codeql.yml +65 -0
  9. data/.github/workflows/deploy.yml +54 -0
  10. data/.github/workflows/release.yml +45 -0
  11. data/.markdownlint.json +4 -0
  12. data/.rubocop.yml +58 -2
  13. data/CHANGELOG.md +485 -225
  14. data/LICENSE.txt +1 -1
  15. data/README.md +38 -26
  16. data/discordrb-webhooks.gemspec +4 -1
  17. data/discordrb.gemspec +18 -10
  18. data/lib/discordrb/api/application.rb +278 -0
  19. data/lib/discordrb/api/channel.rb +222 -18
  20. data/lib/discordrb/api/interaction.rb +63 -0
  21. data/lib/discordrb/api/invite.rb +2 -2
  22. data/lib/discordrb/api/server.rb +123 -66
  23. data/lib/discordrb/api/user.rb +20 -5
  24. data/lib/discordrb/api/webhook.rb +72 -0
  25. data/lib/discordrb/api.rb +35 -25
  26. data/lib/discordrb/bot.rb +437 -66
  27. data/lib/discordrb/cache.rb +41 -22
  28. data/lib/discordrb/commands/command_bot.rb +13 -21
  29. data/lib/discordrb/commands/container.rb +1 -1
  30. data/lib/discordrb/commands/parser.rb +7 -7
  31. data/lib/discordrb/commands/rate_limiter.rb +1 -1
  32. data/lib/discordrb/container.rb +178 -3
  33. data/lib/discordrb/data/activity.rb +1 -1
  34. data/lib/discordrb/data/application.rb +1 -0
  35. data/lib/discordrb/data/attachment.rb +38 -3
  36. data/lib/discordrb/data/audit_logs.rb +3 -3
  37. data/lib/discordrb/data/avatar_decoration.rb +26 -0
  38. data/lib/discordrb/data/call.rb +22 -0
  39. data/lib/discordrb/data/channel.rb +299 -30
  40. data/lib/discordrb/data/collectibles.rb +45 -0
  41. data/lib/discordrb/data/component.rb +229 -0
  42. data/lib/discordrb/data/embed.rb +10 -3
  43. data/lib/discordrb/data/emoji.rb +20 -1
  44. data/lib/discordrb/data/integration.rb +45 -3
  45. data/lib/discordrb/data/interaction.rb +937 -0
  46. data/lib/discordrb/data/invite.rb +1 -1
  47. data/lib/discordrb/data/member.rb +236 -44
  48. data/lib/discordrb/data/message.rb +278 -51
  49. data/lib/discordrb/data/overwrite.rb +15 -7
  50. data/lib/discordrb/data/primary_server.rb +60 -0
  51. data/lib/discordrb/data/profile.rb +2 -7
  52. data/lib/discordrb/data/reaction.rb +2 -1
  53. data/lib/discordrb/data/recipient.rb +1 -1
  54. data/lib/discordrb/data/role.rb +204 -18
  55. data/lib/discordrb/data/server.rb +194 -118
  56. data/lib/discordrb/data/server_preview.rb +68 -0
  57. data/lib/discordrb/data/snapshot.rb +110 -0
  58. data/lib/discordrb/data/user.rb +132 -12
  59. data/lib/discordrb/data/voice_region.rb +1 -0
  60. data/lib/discordrb/data/webhook.rb +99 -9
  61. data/lib/discordrb/data.rb +9 -0
  62. data/lib/discordrb/errors.rb +47 -3
  63. data/lib/discordrb/events/await.rb +1 -1
  64. data/lib/discordrb/events/channels.rb +38 -1
  65. data/lib/discordrb/events/generic.rb +2 -0
  66. data/lib/discordrb/events/guilds.rb +6 -1
  67. data/lib/discordrb/events/interactions.rb +575 -0
  68. data/lib/discordrb/events/invites.rb +2 -0
  69. data/lib/discordrb/events/members.rb +19 -2
  70. data/lib/discordrb/events/message.rb +42 -8
  71. data/lib/discordrb/events/presence.rb +23 -14
  72. data/lib/discordrb/events/raw.rb +1 -0
  73. data/lib/discordrb/events/reactions.rb +2 -1
  74. data/lib/discordrb/events/roles.rb +2 -0
  75. data/lib/discordrb/events/threads.rb +100 -0
  76. data/lib/discordrb/events/typing.rb +1 -0
  77. data/lib/discordrb/events/voice_server_update.rb +1 -0
  78. data/lib/discordrb/events/voice_state_update.rb +1 -0
  79. data/lib/discordrb/events/webhooks.rb +1 -0
  80. data/lib/discordrb/gateway.rb +57 -28
  81. data/lib/discordrb/paginator.rb +3 -3
  82. data/lib/discordrb/permissions.rb +71 -35
  83. data/lib/discordrb/version.rb +1 -1
  84. data/lib/discordrb/voice/encoder.rb +2 -2
  85. data/lib/discordrb/voice/network.rb +18 -7
  86. data/lib/discordrb/voice/sodium.rb +3 -1
  87. data/lib/discordrb/voice/voice_bot.rb +3 -3
  88. data/lib/discordrb/webhooks.rb +2 -0
  89. data/lib/discordrb/websocket.rb +0 -10
  90. data/lib/discordrb.rb +54 -5
  91. metadata +87 -25
  92. data/.circleci/config.yml +0 -126
  93. data/.codeclimate.yml +0 -16
  94. data/.travis.yml +0 -32
  95. data/bin/travis_build_docs.sh +0 -17
@@ -17,9 +17,11 @@ module Discordrb::Events
17
17
  # @param attachments [Array<File>] Files that can be referenced in embeds via `attachment://file.png`
18
18
  # @param allowed_mentions [Hash, Discordrb::AllowedMentions, false, nil] Mentions that are allowed to ping on this message. `false` disables all pings
19
19
  # @param message_reference [Message, String, Integer, nil] The message, or message ID, to reply to if any.
20
+ # @param components [View, Array<Hash>, nil] A collection of components to attach to the message.
21
+ # @param flags [Integer] Flags for this message. Currently only SUPPRESS_EMBEDS (1 << 2) and SUPPRESS_NOTIFICATIONS (1 << 12) can be set.
20
22
  # @return [Discordrb::Message] the message that was sent
21
- def send_message(content, tts = false, embed = nil, attachments = nil, allowed_mentions = nil, message_reference = nil)
22
- channel.send_message(content, tts, embed, attachments, allowed_mentions, message_reference)
23
+ def send_message(content, tts = false, embed = nil, attachments = nil, allowed_mentions = nil, message_reference = nil, components = nil, flags = 0)
24
+ channel.send_message(content, tts, embed, attachments, allowed_mentions, message_reference, components, flags)
23
25
  end
24
26
 
25
27
  # The same as {#send_message}, but yields a {Webhooks::Embed} for easy building of embedded content inside a block.
@@ -30,11 +32,13 @@ module Discordrb::Events
30
32
  # @param tts [true, false] Whether or not this message should be sent using Discord text-to-speech.
31
33
  # @param allowed_mentions [Hash, Discordrb::AllowedMentions, false, nil] Mentions that are allowed to ping on this message. `false` disables all pings
32
34
  # @param message_reference [Message, String, Integer, nil] The message, or message ID, to reply to if any.
35
+ # @param components [View, Array<Hash>, nil] A collection of components to attach to the message.
36
+ # @param flags [Integer] Flags for this message. Currently only SUPPRESS_EMBEDS (1 << 2) and SUPPRESS_NOTIFICATIONS (1 << 12) can be set.
33
37
  # @yield [embed] Yields the embed to allow for easy building inside a block.
34
38
  # @yieldparam embed [Discordrb::Webhooks::Embed] The embed from the parameters, or a new one.
35
39
  # @return [Message] The resulting message.
36
- def send_embed(message = '', embed = nil, attachments = nil, tts = false, allowed_mentions = nil, message_reference = nil, &block)
37
- channel.send_embed(message, embed, attachments, tts, allowed_mentions, message_reference, &block)
40
+ def send_embed(message = '', embed = nil, attachments = nil, tts = false, allowed_mentions = nil, message_reference = nil, components = nil, flags = 0, &block)
41
+ channel.send_embed(message, embed, attachments, tts, allowed_mentions, message_reference, components, flags, &block)
38
42
  end
39
43
 
40
44
  # Sends a temporary message to the channel this message was sent in, right now.
@@ -44,8 +48,16 @@ module Discordrb::Events
44
48
  # @param embed [Hash, Discordrb::Webhooks::Embed, nil] The rich embed to append to this message.
45
49
  # @param attachments [Array<File>] Files that can be referenced in embeds via `attachment://file.png`
46
50
  # @param allowed_mentions [Hash, Discordrb::AllowedMentions, false, nil] Mentions that are allowed to ping on this message. `false` disables all pings
47
- def send_temporary_message(content, timeout, tts = false, embed = nil, attachments = nil, allowed_mentions = nil)
48
- channel.send_temporary_message(content, timeout, tts, embed, attachments, allowed_mentions)
51
+ # @param components [View, Array<Hash>, nil] A collection of components to attach to the message.
52
+ # @param flags [Integer] Flags for this message. Currently only SUPPRESS_EMBEDS (1 << 2) and SUPPRESS_NOTIFICATIONS (1 << 12) can be set.
53
+ def send_temporary_message(content, timeout, tts = false, embed = nil, attachments = nil, allowed_mentions = nil, components = nil, flags = 0)
54
+ channel.send_temporary_message(content, timeout, tts, embed, attachments, allowed_mentions, components, flags)
55
+ end
56
+
57
+ # Sends a message to the channel this message was sent in, right now.
58
+ # @see Channel#send_message!
59
+ def send_message!(...)
60
+ channel.send_message!(...)
49
61
  end
50
62
 
51
63
  # Adds a string to be sent after the event has finished execution. Avoids problems with rate limiting because only
@@ -81,6 +93,9 @@ module Discordrb::Events
81
93
  alias_method :send, :send_message
82
94
  alias_method :respond, :send_message
83
95
  alias_method :send_temp, :send_temporary_message
96
+
97
+ alias_method :send!, :send_message!
98
+ alias_method :respond!, :send_message!
84
99
  end
85
100
 
86
101
  # Event raised when a text message is sent to a channel
@@ -121,6 +136,7 @@ module Discordrb::Events
121
136
  # @see Channel#server
122
137
  delegate :server, to: :channel
123
138
 
139
+ # @!visibility private
124
140
  def initialize(message, bot)
125
141
  @bot = bot
126
142
  @message = message
@@ -223,7 +239,9 @@ module Discordrb::Events
223
239
  a == e
224
240
  end
225
241
  end,
226
- matches_all(@attributes[:from], event.author) do |a, e|
242
+ matches_all(@attributes[:from], event.message) do |a, e|
243
+ # Resolve the author in the block in order to prevent resolving the author even when the attribute is `nil`
244
+ e = e.author
227
245
  case a
228
246
  when String
229
247
  a == e.name
@@ -244,9 +262,18 @@ module Discordrb::Events
244
262
  match ? (e == match[0]) : false
245
263
  end
246
264
  end,
265
+ matches_all(@attributes[:type] || @attributes[:message_type], event.message.type) do |a, e|
266
+ case a
267
+ when String, Symbol
268
+ Discordrb::Message::TYPES[a.to_sym] == e
269
+ when Integer
270
+ a == e
271
+ end
272
+ end,
247
273
  matches_all(@attributes[:after], event.timestamp) { |a, e| a > e },
248
274
  matches_all(@attributes[:before], event.timestamp) { |a, e| a < e },
249
- matches_all(@attributes[:private], event.channel.private?) { |a, e| !e == !a }
275
+ matches_all(@attributes[:private], event.channel.private?) { |a, e| !e == !a },
276
+ matches_all(@attributes[:server], event.server) { |a, e| a&.resolve_id == e&.resolve_id }
250
277
  ].reduce(true, &:&)
251
278
  end
252
279
 
@@ -279,10 +306,14 @@ module Discordrb::Events
279
306
  # @return [Integer] the ID associated with this event
280
307
  attr_reader :id
281
308
 
309
+ # @return [Server, nil] the server associated with this event
310
+ attr_reader :server
311
+
282
312
  # @!visibility private
283
313
  def initialize(data, bot)
284
314
  @id = data['id'].to_i
285
315
  @channel = bot.channel(data['channel_id'].to_i)
316
+ @server = @channel.server
286
317
  @saved_message = ''
287
318
  @bot = bot
288
319
  end
@@ -308,6 +339,9 @@ module Discordrb::Events
308
339
  else
309
340
  a == e
310
341
  end
342
+ end,
343
+ matches_all(@attributes[:server], event.server) do |a, e|
344
+ a&.resolve_id == e&.resolve_id
311
345
  end
312
346
  ].reduce(true, &:&)
313
347
  end
@@ -19,6 +19,7 @@ module Discordrb::Events
19
19
  # on various device types (`:desktop`, `:mobile`, or `:web`). The value will be `nil` if the user is offline or invisible.
20
20
  attr_reader :client_status
21
21
 
22
+ # @!visibility private
22
23
  def initialize(data, bot)
23
24
  @bot = bot
24
25
 
@@ -65,28 +66,36 @@ module Discordrb::Events
65
66
  # @return [User] the user whose status got updated.
66
67
  attr_reader :user
67
68
 
68
- # @return [String] the new game the user is playing.
69
- attr_reader :game
69
+ # @return [Discordrb::Activity] The new activity
70
+ attr_reader :activity
70
71
 
71
- # @return [String] the URL to the stream
72
- attr_reader :url
72
+ # @!attribute [r] url
73
+ # @return [String] the URL to the stream
73
74
 
74
- # @return [String] what the player is currently doing (ex. game being streamed)
75
- attr_reader :details
75
+ # @!attribute [r] details
76
+ # @return [String] what the player is currently doing (ex. game being streamed)
76
77
 
77
- # @return [Integer] the type of play. 0 = game, 1 = Twitch
78
- attr_reader :type
78
+ # @!attribute [r] type
79
+ # @return [Integer] the type of play. See {Discordrb::Activity}
80
+ delegate :url, :details, :type, to: :activity
79
81
 
80
- def initialize(data, bot)
82
+ # @return [Hash<Symbol, Symbol>] the current online status (`:online`, `:idle` or `:dnd`) of the user
83
+ # on various device types (`:desktop`, `:mobile`, or `:web`). The value will be `nil` if the user is offline or invisible.
84
+ attr_reader :client_status
85
+
86
+ # @!visibility private
87
+ def initialize(data, activity, bot)
81
88
  @bot = bot
89
+ @activity = activity
82
90
 
83
91
  @server = bot.server(data['guild_id'].to_i)
84
92
  @user = bot.user(data['user']['id'].to_i)
85
- @game = data['game'] ? data['game']['name'] : nil
86
- @type = data['game'] ? data['game']['type'].to_i : nil
87
- # Handle optional 'game' fields safely
88
- @url = data['game'] && data['game']['url'] ? data['game']['url'] : nil
89
- @details = data['game'] && data['game']['details'] ? data['game']['details'] : nil
93
+ @client_status = @user.client_status
94
+ end
95
+
96
+ # @return [String] the name of the new game the user is playing.
97
+ def game
98
+ @activity.name
90
99
  end
91
100
  end
92
101
 
@@ -14,6 +14,7 @@ module Discordrb::Events
14
14
  attr_reader :data
15
15
  alias_method :d, :data
16
16
 
17
+ # @!visibility private
17
18
  def initialize(type, data, bot)
18
19
  @type = type
19
20
  @data = data
@@ -14,6 +14,7 @@ module Discordrb::Events
14
14
  # @!visibility private
15
15
  attr_reader :message_id
16
16
 
17
+ # @!visibility private
17
18
  def initialize(data, bot)
18
19
  @bot = bot
19
20
 
@@ -113,6 +114,7 @@ module Discordrb::Events
113
114
  # @!visibility private
114
115
  attr_reader :message_id
115
116
 
117
+ # @!visibility private
116
118
  def initialize(data, bot)
117
119
  @bot = bot
118
120
 
@@ -137,7 +139,6 @@ module Discordrb::Events
137
139
  # Check for the proper event type
138
140
  return false unless event.is_a? ReactionRemoveAllEvent
139
141
 
140
- # No attributes yet as there is no property available on the event that doesn't involve doing a resolution request
141
142
  [
142
143
  matches_all(@attributes[:message], event.message_id) do |a, e|
143
144
  a == e
@@ -17,6 +17,7 @@ module Discordrb::Events
17
17
  # @see Role#name
18
18
  delegate :name, to: :role
19
19
 
20
+ # @!visibility private
20
21
  def initialize(data, bot)
21
22
  @bot = bot
22
23
 
@@ -54,6 +55,7 @@ module Discordrb::Events
54
55
  # @return [Server] the server on which a role got deleted.
55
56
  attr_reader :server
56
57
 
58
+ # @!visibility private
57
59
  def initialize(data, bot)
58
60
  @bot = bot
59
61
 
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Generic subclass for threads
4
+ module Discordrb::Events
5
+ # Raised when a thread is created
6
+ class ThreadCreateEvent < Event
7
+ # @return [Channel] the thread in question.
8
+ attr_reader :thread
9
+
10
+ delegate :name, :server, :owner, :parent_channel, :thread_metadata, to: :thread
11
+
12
+ # @!visibility private
13
+ def initialize(data, bot)
14
+ @bot = bot
15
+ @thread = data.is_a?(Discordrb::Channel) ? data : bot.channel(data['id'].to_i)
16
+ end
17
+ end
18
+
19
+ # Event handler for ChannelCreateEvent
20
+ class ThreadCreateEventHandler < EventHandler
21
+ def matches?(event)
22
+ # Check for the proper event type
23
+ return false unless event.is_a? ThreadCreateEvent
24
+
25
+ [
26
+ matches_all(@attributes[:name], event.name) do |a, e|
27
+ a == if a.is_a? String
28
+ e.to_s
29
+ else
30
+ e
31
+ end
32
+ end,
33
+ matches_all(@attributes[:server], event.server) do |a, e|
34
+ a.resolve_id == e.resolve_id
35
+ end,
36
+ matches_all(@attributes[:invitable], event.thread.invitable) do |a, e|
37
+ a == e
38
+ end,
39
+ matches_all(@attributes[:owner], event.thread.owner) do |a, e|
40
+ a.resolve_id == e.resolve_id
41
+ end,
42
+ matches_all(@attributes[:channel], event.thread.parent) do |a, e|
43
+ a.resolve_id == e.resolve_id
44
+ end
45
+ ].reduce(true, &:&)
46
+ end
47
+ end
48
+
49
+ # Raised when a thread is updated (e.g. name changes)
50
+ class ThreadUpdateEvent < ThreadCreateEvent; end
51
+
52
+ # Event handler for ThreadUpdateEvent
53
+ class ThreadUpdateEventHandler < ThreadCreateEventHandler
54
+ def matches?(event)
55
+ # Check for the proper event type
56
+ return false unless event.is_a? ThreadUpdateEvent
57
+
58
+ super
59
+ end
60
+ end
61
+
62
+ # Raised when members are added or removed from a thread.
63
+ class ThreadMembersUpdateEvent < Event
64
+ # @return [Channel]
65
+ attr_reader :thread
66
+
67
+ # @return [Array<Integer>]
68
+ attr_reader :removed_member_ids
69
+
70
+ # @return [Integer]
71
+ attr_reader :member_count
72
+
73
+ delegate :name, :server, :owner, :parent_channel, :thread_metadata, to: :thread
74
+
75
+ # @!visibility private
76
+ def initialize(data, bot)
77
+ @bot = bot
78
+ @server = bot.server(data['guild_id'].to_i) if data['guild_id']
79
+ @thread = data.is_a?(Discordrb::Channel) ? data : bot.channel(data['id'].to_i)
80
+ @added_member_ids = data['added_members']&.map { |m| m['user_id']&.to_i } || []
81
+ @removed_member_ids = data['removed_member_ids']&.map(&:resolve_id) || []
82
+ @member_count = data['member_count']
83
+ end
84
+ end
85
+
86
+ # @return [Array<Member, User>] the members that were added to the thread
87
+ def added_members
88
+ @added_members ||= @added_member_ids&.map { |id| @server&.member(id) || @bot.user(id) }
89
+ end
90
+
91
+ # Event handler for ThreadMembersUpdateEvent
92
+ class ThreadMembersUpdateEventHandler < ThreadCreateEventHandler
93
+ def matches?(event)
94
+ # Check for the proper event type
95
+ return false unless event.is_a? ThreadMembersUpdateEvent
96
+
97
+ super
98
+ end
99
+ end
100
+ end
@@ -17,6 +17,7 @@ module Discordrb::Events
17
17
  # @return [Time] when the typing happened.
18
18
  attr_reader :timestamp
19
19
 
20
+ # @!visibility private
20
21
  def initialize(data, bot)
21
22
  @bot = bot
22
23
 
@@ -19,6 +19,7 @@ module Discordrb::Events
19
19
  # @return [String] The voice server host.
20
20
  attr_reader :endpoint
21
21
 
22
+ # @!visibility private
22
23
  def initialize(data, bot)
23
24
  @bot = bot
24
25
 
@@ -11,6 +11,7 @@ module Discordrb::Events
11
11
  # @return [Channel, nil] the old channel this user was on, or nil if the user is newly joining voice.
12
12
  attr_reader :old_channel
13
13
 
14
+ # @!visibility private
14
15
  def initialize(data, old_channel_id, bot)
15
16
  @bot = bot
16
17
 
@@ -12,6 +12,7 @@ module Discordrb::Events
12
12
  # @return [Channel] the channel the webhook is associated to
13
13
  attr_reader :channel
14
14
 
15
+ # @!visibility private
15
16
  def initialize(data, bot)
16
17
  @bot = bot
17
18
 
@@ -90,14 +90,22 @@ module Discordrb
90
90
  # This class stores the data of an active gateway session. Note that this is different from a websocket connection -
91
91
  # there may be multiple sessions per connection or one session may persist over multiple connections.
92
92
  class Session
93
+ # @return [String] Used to uniquely identify this session. Mostly used when resuming connections.
93
94
  attr_reader :session_id
95
+
96
+ # @return [Integer] Incrementing integer used to determine the most recent event reccived from Discord.
94
97
  attr_accessor :sequence
95
98
 
96
- def initialize(session_id)
99
+ # @return [String] Gateway URL used to reconnect to the gateway node that Discord wants this session to use.
100
+ attr_reader :resume_gateway_url
101
+
102
+ # @!visibility private
103
+ def initialize(session_id, resume_gateway_url)
97
104
  @session_id = session_id
98
105
  @sequence = 0
99
106
  @suspended = false
100
107
  @invalid = false
108
+ @resume_gateway_url = resume_gateway_url
101
109
  end
102
110
 
103
111
  # Flags this session as suspended, so we know not to try and send heartbeats, etc. to the gateway until we've reconnected
@@ -117,6 +125,7 @@ module Discordrb
117
125
  # Flags this session as being invalid
118
126
  def invalidate
119
127
  @invalid = true
128
+ @resume_gateway_url = nil
120
129
  end
121
130
 
122
131
  def invalid?
@@ -134,7 +143,14 @@ module Discordrb
134
143
  LARGE_THRESHOLD = 100
135
144
 
136
145
  # The version of the gateway that's supposed to be used.
137
- GATEWAY_VERSION = 6
146
+ GATEWAY_VERSION = 9
147
+
148
+ # Close codes that are unrecoverable, after which we should not try to reconnect.
149
+ # - 4003: Not authenticated. How did this happen?
150
+ # - 4004: Authentication failed. Token was wrong, nothing we can do.
151
+ # - 4011: Sharding required. Currently requires developer intervention.
152
+ # - 4014: Use of disabled privileged intents.
153
+ FATAL_CLOSE_CODES = [4003, 4004, 4011, 4014].freeze
138
154
 
139
155
  # Heartbeat ACKs are Discord's way of verifying on the client side whether the connection is still alive. If this is
140
156
  # set to true (default value) the gateway client will use that functionality to detect zombie connections and
@@ -143,7 +159,10 @@ module Discordrb
143
159
  # @return [true, false] whether or not this gateway should check for heartbeat ACKs.
144
160
  attr_accessor :check_heartbeat_acks
145
161
 
146
- def initialize(bot, token, shard_key = nil, compress_mode = :stream, intents = nil)
162
+ # @return [Integer] the intent parameter sent to the gateway server.
163
+ attr_reader :intents
164
+
165
+ def initialize(bot, token, shard_key = nil, compress_mode = :stream, intents = ALL_INTENTS)
147
166
  @token = token
148
167
  @bot = bot
149
168
 
@@ -277,12 +296,12 @@ module Discordrb
277
296
  def identify
278
297
  compress = @compress_mode == :large
279
298
  send_identify(@token, {
280
- '$os': RUBY_PLATFORM,
281
- '$browser': 'discordrb',
282
- '$device': 'discordrb',
283
- '$referrer': '',
284
- '$referring_domain': ''
285
- }, compress, 100, @shard_key)
299
+ os: RUBY_PLATFORM,
300
+ browser: 'discordrb',
301
+ device: 'discordrb',
302
+ referrer: '',
303
+ referring_domain: ''
304
+ }, compress, LARGE_THRESHOLD, @shard_key, @intents)
286
305
  end
287
306
 
288
307
  # Sends an identify packet (op 2). This starts a new session on the current connection and tells Discord who we are.
@@ -292,26 +311,26 @@ module Discordrb
292
311
  # @param properties [Hash<Symbol => String>] A list of properties for Discord to use in analytics. The following
293
312
  # keys are recognised:
294
313
  #
295
- # - "$os" (recommended value: the operating system the bot is running on)
296
- # - "$browser" (recommended value: library name)
297
- # - "$device" (recommended value: library name)
298
- # - "$referrer" (recommended value: empty)
299
- # - "$referring_domain" (recommended value: empty)
314
+ # - "os" (recommended value: the operating system the bot is running on)
315
+ # - "browser" (recommended value: library name)
316
+ # - "device" (recommended value: library name)
317
+ # - "referrer" (recommended value: empty)
318
+ # - "referring_domain" (recommended value: empty)
300
319
  #
301
320
  # @param compress [true, false] Whether certain large packets should be compressed using zlib.
302
321
  # @param large_threshold [Integer] The member threshold after which a server counts as large and will have to have
303
322
  # its member list chunked.
304
323
  # @param shard_key [Array(Integer, Integer), nil] The shard key to use for sharding, represented as
305
324
  # [shard_id, num_shards], or nil if the bot should not be sharded.
306
- def send_identify(token, properties, compress, large_threshold, shard_key = nil)
325
+ def send_identify(token, properties, compress, large_threshold, shard_key = nil, intents = ALL_INTENTS)
307
326
  data = {
308
327
  # Don't send a v anymore as it's entirely determined by the URL now
309
328
  token: token,
310
329
  properties: properties,
311
330
  compress: compress,
312
- large_threshold: large_threshold
331
+ large_threshold: large_threshold,
332
+ intents: intents
313
333
  }
314
- data[:intents] = @intents unless @intents.nil?
315
334
 
316
335
  # Don't include the shard key at all if it is nil as Discord checks for its mere existence
317
336
  data[:shard] = shard_key if shard_key
@@ -449,8 +468,13 @@ module Discordrb
449
468
  # suspended (e.g. after op7)
450
469
  if (@session && !@session.suspended?) || !@session
451
470
  sleep @heartbeat_interval
452
- @bot.raise_heartbeat_event
453
- heartbeat
471
+ # Check if we're connected here, since we could possibly be waiting for a reconnect to occur.
472
+ if @handshaked && !@closed
473
+ @bot.raise_heartbeat_event
474
+ heartbeat
475
+ else
476
+ LOGGER.debug('Tried to send a heartbeat without being connected! Ignoring, we should be fine.')
477
+ end
454
478
  else
455
479
  sleep 1
456
480
  end
@@ -535,7 +559,7 @@ module Discordrb
535
559
  end
536
560
 
537
561
  def process_gateway
538
- raw_url = find_gateway
562
+ raw_url = @session&.resume_gateway_url || find_gateway
539
563
 
540
564
  # Append a slash in case it's not there (I'm not sure how well WSCS handles it otherwise)
541
565
  raw_url += '/' unless raw_url.end_with? '/'
@@ -653,7 +677,9 @@ module Discordrb
653
677
  LOGGER.log_exception(e)
654
678
  end
655
679
 
680
+ # rubocop:disable Lint/UselessConstantScoping
656
681
  ZLIB_SUFFIX = "\x00\x00\xFF\xFF".b.freeze
682
+ # rubocop:enable Lint/UselessConstantScoping
657
683
 
658
684
  def handle_message(msg)
659
685
  case @compress_mode
@@ -713,9 +739,9 @@ module Discordrb
713
739
  when :READY
714
740
  LOGGER.info("Discord using gateway protocol version: #{data['v']}, requested: #{GATEWAY_VERSION}")
715
741
 
716
- @session = Session.new(data['session_id'])
742
+ @session = Session.new(data['session_id'], data['resume_gateway_url'])
717
743
  @session.sequence = 0
718
- @bot.__send__(:notify_ready) if @intents && (@intents & INTENTS[:servers]).zero?
744
+ @bot.__send__(:notify_ready) if @intents && @intents.nobits?(INTENTS[:servers])
719
745
  when :RESUMED
720
746
  # The RESUMED event is received after a successful op 6 (resume). It does nothing except tell the bot the
721
747
  # connection is initiated (like READY would). Starting with v5, it doesn't set a new heartbeat interval anymore
@@ -786,12 +812,6 @@ module Discordrb
786
812
  handle_close(e)
787
813
  end
788
814
 
789
- # Close codes that are unrecoverable, after which we should not try to reconnect.
790
- # - 4003: Not authenticated. How did this happen?
791
- # - 4004: Authentication failed. Token was wrong, nothing we can do.
792
- # - 4011: Sharding required. Currently requires developer intervention.
793
- FATAL_CLOSE_CODES = [4003, 4004, 4011].freeze
794
-
795
815
  def handle_close(e)
796
816
  @bot.__send__(:raise_event, Events::DisconnectEvent.new(@bot))
797
817
 
@@ -800,6 +820,15 @@ module Discordrb
800
820
  LOGGER.error('Websocket close frame received!')
801
821
  LOGGER.error("Code: #{e.code}")
802
822
  LOGGER.error("Message: #{e.data}")
823
+
824
+ if e.code == 4014
825
+ LOGGER.error(<<~ERROR)
826
+ You attempted to identify with privileged intents that your bot is not authorized to use
827
+ Please enable the privileged intents on the bot page of your application on the discord developer page.
828
+ Read more here https://discord.com/developers/docs/topics/gateway#privileged-intents
829
+ ERROR
830
+ end
831
+
803
832
  @should_reconnect = false if FATAL_CLOSE_CODES.include?(e.code)
804
833
  elsif e.is_a? Exception
805
834
  # Log the exception
@@ -24,7 +24,7 @@ module Discordrb
24
24
  # no more results or the configured `limit` is reached.
25
25
  def each
26
26
  last_page = nil
27
- until limit_check
27
+ until limit_exceeded?
28
28
  page = @block.call(last_page)
29
29
  return if page.empty?
30
30
 
@@ -38,7 +38,7 @@ module Discordrb
38
38
  enumerator.each do |item|
39
39
  yield item
40
40
  @count += 1
41
- break if limit_check
41
+ break if limit_exceeded?
42
42
  end
43
43
 
44
44
  last_page = page
@@ -48,7 +48,7 @@ module Discordrb
48
48
  private
49
49
 
50
50
  # Whether the paginator limit has been exceeded
51
- def limit_check
51
+ def limit_exceeded?
52
52
  return false if @limit.nil?
53
53
 
54
54
  @count >= @limit