discordrb 3.1.1 → 3.4.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of discordrb might be problematic. Click here for more details.

Files changed (91) hide show
  1. checksums.yaml +5 -5
  2. data/.circleci/config.yml +126 -0
  3. data/.codeclimate.yml +16 -0
  4. data/.github/CONTRIBUTING.md +13 -0
  5. data/.github/ISSUE_TEMPLATE/bug_report.md +39 -0
  6. data/.github/ISSUE_TEMPLATE/feature_request.md +25 -0
  7. data/.github/pull_request_template.md +37 -0
  8. data/.gitignore +5 -0
  9. data/.rubocop.yml +39 -33
  10. data/.travis.yml +27 -2
  11. data/.yardopts +1 -1
  12. data/CHANGELOG.md +808 -208
  13. data/Gemfile +4 -1
  14. data/LICENSE.txt +1 -1
  15. data/README.md +108 -53
  16. data/Rakefile +14 -1
  17. data/bin/console +1 -0
  18. data/bin/travis_build_docs.sh +17 -0
  19. data/discordrb-webhooks.gemspec +26 -0
  20. data/discordrb.gemspec +24 -15
  21. data/lib/discordrb.rb +75 -2
  22. data/lib/discordrb/allowed_mentions.rb +36 -0
  23. data/lib/discordrb/api.rb +126 -27
  24. data/lib/discordrb/api/channel.rb +165 -43
  25. data/lib/discordrb/api/invite.rb +10 -7
  26. data/lib/discordrb/api/server.rb +240 -61
  27. data/lib/discordrb/api/user.rb +26 -24
  28. data/lib/discordrb/api/webhook.rb +83 -0
  29. data/lib/discordrb/await.rb +1 -2
  30. data/lib/discordrb/bot.rb +417 -149
  31. data/lib/discordrb/cache.rb +42 -10
  32. data/lib/discordrb/colour_rgb.rb +43 -0
  33. data/lib/discordrb/commands/command_bot.rb +186 -31
  34. data/lib/discordrb/commands/container.rb +30 -16
  35. data/lib/discordrb/commands/parser.rb +102 -47
  36. data/lib/discordrb/commands/rate_limiter.rb +18 -17
  37. data/lib/discordrb/container.rb +245 -41
  38. data/lib/discordrb/data.rb +27 -2511
  39. data/lib/discordrb/data/activity.rb +264 -0
  40. data/lib/discordrb/data/application.rb +50 -0
  41. data/lib/discordrb/data/attachment.rb +56 -0
  42. data/lib/discordrb/data/audit_logs.rb +345 -0
  43. data/lib/discordrb/data/channel.rb +849 -0
  44. data/lib/discordrb/data/embed.rb +251 -0
  45. data/lib/discordrb/data/emoji.rb +82 -0
  46. data/lib/discordrb/data/integration.rb +83 -0
  47. data/lib/discordrb/data/invite.rb +137 -0
  48. data/lib/discordrb/data/member.rb +297 -0
  49. data/lib/discordrb/data/message.rb +334 -0
  50. data/lib/discordrb/data/overwrite.rb +102 -0
  51. data/lib/discordrb/data/profile.rb +91 -0
  52. data/lib/discordrb/data/reaction.rb +33 -0
  53. data/lib/discordrb/data/recipient.rb +34 -0
  54. data/lib/discordrb/data/role.rb +191 -0
  55. data/lib/discordrb/data/server.rb +1002 -0
  56. data/lib/discordrb/data/user.rb +204 -0
  57. data/lib/discordrb/data/voice_region.rb +45 -0
  58. data/lib/discordrb/data/voice_state.rb +41 -0
  59. data/lib/discordrb/data/webhook.rb +145 -0
  60. data/lib/discordrb/errors.rb +36 -2
  61. data/lib/discordrb/events/bans.rb +7 -5
  62. data/lib/discordrb/events/channels.rb +2 -0
  63. data/lib/discordrb/events/generic.rb +19 -3
  64. data/lib/discordrb/events/guilds.rb +129 -6
  65. data/lib/discordrb/events/invites.rb +125 -0
  66. data/lib/discordrb/events/members.rb +6 -2
  67. data/lib/discordrb/events/message.rb +86 -36
  68. data/lib/discordrb/events/presence.rb +23 -16
  69. data/lib/discordrb/events/raw.rb +47 -0
  70. data/lib/discordrb/events/reactions.rb +159 -0
  71. data/lib/discordrb/events/roles.rb +7 -6
  72. data/lib/discordrb/events/typing.rb +9 -5
  73. data/lib/discordrb/events/voice_server_update.rb +47 -0
  74. data/lib/discordrb/events/voice_state_update.rb +29 -9
  75. data/lib/discordrb/events/webhooks.rb +64 -0
  76. data/lib/discordrb/gateway.rb +219 -88
  77. data/lib/discordrb/id_object.rb +39 -0
  78. data/lib/discordrb/light.rb +1 -1
  79. data/lib/discordrb/light/integrations.rb +1 -1
  80. data/lib/discordrb/light/light_bot.rb +1 -1
  81. data/lib/discordrb/logger.rb +12 -11
  82. data/lib/discordrb/paginator.rb +57 -0
  83. data/lib/discordrb/permissions.rb +148 -14
  84. data/lib/discordrb/version.rb +1 -1
  85. data/lib/discordrb/voice/encoder.rb +14 -15
  86. data/lib/discordrb/voice/network.rb +86 -45
  87. data/lib/discordrb/voice/sodium.rb +96 -0
  88. data/lib/discordrb/voice/voice_bot.rb +52 -40
  89. data/lib/discordrb/webhooks.rb +12 -0
  90. data/lib/discordrb/websocket.rb +2 -2
  91. metadata +137 -34
@@ -1,21 +1,11 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # API calls for User object
2
4
  module Discordrb::API::User
3
5
  module_function
4
6
 
5
- # Returns users based on a query
6
- # https://discordapp.com/developers/docs/resources/user#query-users
7
- def query(token, query, limit = nil)
8
- Discordrb::API.request(
9
- :users,
10
- nil,
11
- :get,
12
- "#{Discordrb::API.api_base}/users?q=#{query}#{"&limit=#{limit}" if limit}",
13
- Authorization: token
14
- )
15
- end
16
-
17
7
  # Get user data
18
- # https://discordapp.com/developers/docs/resources/user#get-user
8
+ # https://discord.com/developers/docs/resources/user#get-user
19
9
  def resolve(token, user_id)
20
10
  Discordrb::API.request(
21
11
  :users_uid,
@@ -27,7 +17,7 @@ module Discordrb::API::User
27
17
  end
28
18
 
29
19
  # Get profile data
30
- # https://discordapp.com/developers/docs/resources/user#get-current-user
20
+ # https://discord.com/developers/docs/resources/user#get-current-user
31
21
  def profile(token)
32
22
  Discordrb::API.request(
33
23
  :users_me,
@@ -39,7 +29,7 @@ module Discordrb::API::User
39
29
  end
40
30
 
41
31
  # Change the current bot's nickname on a server
42
- def change_own_nickname(token, server_id, nick)
32
+ def change_own_nickname(token, server_id, nick, reason = nil)
43
33
  Discordrb::API.request(
44
34
  :guilds_sid_members_me_nick,
45
35
  server_id, # This is technically a guild endpoint
@@ -47,12 +37,13 @@ module Discordrb::API::User
47
37
  "#{Discordrb::API.api_base}/guilds/#{server_id}/members/@me/nick",
48
38
  { nick: nick }.to_json,
49
39
  Authorization: token,
50
- content_type: :json
40
+ content_type: :json,
41
+ 'X-Audit-Log-Reason': reason
51
42
  )
52
43
  end
53
44
 
54
45
  # Update user data
55
- # https://discordapp.com/developers/docs/resources/user#modify-current-user
46
+ # https://discord.com/developers/docs/resources/user#modify-current-user
56
47
  def update_profile(token, email, password, new_username, avatar, new_password = nil)
57
48
  Discordrb::API.request(
58
49
  :users_me,
@@ -66,7 +57,7 @@ module Discordrb::API::User
66
57
  end
67
58
 
68
59
  # Get the servers a user is connected to
69
- # https://discordapp.com/developers/docs/resources/user#get-current-user-guilds
60
+ # https://discord.com/developers/docs/resources/user#get-current-user-guilds
70
61
  def servers(token)
71
62
  Discordrb::API.request(
72
63
  :users_me_guilds,
@@ -78,7 +69,7 @@ module Discordrb::API::User
78
69
  end
79
70
 
80
71
  # Leave a server
81
- # https://discordapp.com/developers/docs/resources/user#leave-guild
72
+ # https://discord.com/developers/docs/resources/user#leave-guild
82
73
  def leave_server(token, server_id)
83
74
  Discordrb::API.request(
84
75
  :users_me_guilds_sid,
@@ -90,7 +81,7 @@ module Discordrb::API::User
90
81
  end
91
82
 
92
83
  # Get the DMs for the current user
93
- # https://discordapp.com/developers/docs/resources/user#get-user-dms
84
+ # https://discord.com/developers/docs/resources/user#get-user-dms
94
85
  def user_dms(token)
95
86
  Discordrb::API.request(
96
87
  :users_me_channels,
@@ -102,7 +93,7 @@ module Discordrb::API::User
102
93
  end
103
94
 
104
95
  # Create a DM to another user
105
- # https://discordapp.com/developers/docs/resources/user#create-dm
96
+ # https://discord.com/developers/docs/resources/user#create-dm
106
97
  def create_pm(token, recipient_id)
107
98
  Discordrb::API.request(
108
99
  :users_me_channels,
@@ -116,7 +107,7 @@ module Discordrb::API::User
116
107
  end
117
108
 
118
109
  # Get information about a user's connections
119
- # https://discordapp.com/developers/docs/resources/user#get-users-connections
110
+ # https://discord.com/developers/docs/resources/user#get-users-connections
120
111
  def connections(token)
121
112
  Discordrb::API.request(
122
113
  :users_me_connections,
@@ -140,8 +131,19 @@ module Discordrb::API::User
140
131
  )
141
132
  end
142
133
 
134
+ # Returns one of the "default" discord avatars from the CDN given a discriminator
135
+ def default_avatar(discrim = 0)
136
+ index = discrim.to_i % 5
137
+ "#{Discordrb::API.cdn_url}/embed/avatars/#{index}.png"
138
+ end
139
+
143
140
  # Make an avatar URL from the user and avatar IDs
144
- def avatar_url(user_id, avatar_id)
145
- "#{Discordrb::API.api_base}/users/#{user_id}/avatars/#{avatar_id}.jpg"
141
+ def avatar_url(user_id, avatar_id, format = nil)
142
+ format ||= if avatar_id.start_with?('a_')
143
+ 'gif'
144
+ else
145
+ 'webp'
146
+ end
147
+ "#{Discordrb::API.cdn_url}/avatars/#{user_id}/#{avatar_id}.#{format}"
146
148
  end
147
149
  end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ # API calls for Webhook object
4
+ module Discordrb::API::Webhook
5
+ module_function
6
+
7
+ # Get a webhook
8
+ # https://discord.com/developers/docs/resources/webhook#get-webhook
9
+ def webhook(token, webhook_id)
10
+ Discordrb::API.request(
11
+ :webhooks_wid,
12
+ nil,
13
+ :get,
14
+ "#{Discordrb::API.api_base}/webhooks/#{webhook_id}",
15
+ Authorization: token
16
+ )
17
+ end
18
+
19
+ # Get a webhook via webhook token
20
+ # https://discord.com/developers/docs/resources/webhook#get-webhook-with-token
21
+ def token_webhook(webhook_token, webhook_id)
22
+ Discordrb::API.request(
23
+ :webhooks_wid,
24
+ nil,
25
+ :get,
26
+ "#{Discordrb::API.api_base}/webhooks/#{webhook_id}/#{webhook_token}"
27
+ )
28
+ end
29
+
30
+ # Update a webhook
31
+ # https://discord.com/developers/docs/resources/webhook#modify-webhook
32
+ def update_webhook(token, webhook_id, data, reason = nil)
33
+ Discordrb::API.request(
34
+ :webhooks_wid,
35
+ webhook_id,
36
+ :patch,
37
+ "#{Discordrb::API.api_base}/webhooks/#{webhook_id}",
38
+ data.to_json,
39
+ Authorization: token,
40
+ content_type: :json,
41
+ 'X-Audit-Log-Reason': reason
42
+ )
43
+ end
44
+
45
+ # Update a webhook via webhook token
46
+ # https://discord.com/developers/docs/resources/webhook#modify-webhook-with-token
47
+ def token_update_webhook(webhook_token, webhook_id, data, reason = nil)
48
+ Discordrb::API.request(
49
+ :webhooks_wid,
50
+ webhook_id,
51
+ :patch,
52
+ "#{Discordrb::API.api_base}/webhooks/#{webhook_id}/#{webhook_token}",
53
+ data.to_json,
54
+ content_type: :json,
55
+ 'X-Audit-Log-Reason': reason
56
+ )
57
+ end
58
+
59
+ # Deletes a webhook
60
+ # https://discord.com/developers/docs/resources/webhook#delete-webhook
61
+ def delete_webhook(token, webhook_id, reason = nil)
62
+ Discordrb::API.request(
63
+ :webhooks_wid,
64
+ webhook_id,
65
+ :delete,
66
+ "#{Discordrb::API.api_base}/webhooks/#{webhook_id}",
67
+ Authorization: token,
68
+ 'X-Audit-Log-Reason': reason
69
+ )
70
+ end
71
+
72
+ # Deletes a webhook via webhook token
73
+ # https://discord.com/developers/docs/resources/webhook#delete-webhook-with-token
74
+ def token_delete_webhook(webhook_token, webhook_id, reason = nil)
75
+ Discordrb::API.request(
76
+ :webhooks_wid,
77
+ webhook_id,
78
+ :delete,
79
+ "#{Discordrb::API.api_base}/webhooks/#{webhook_id}/#{webhook_token}",
80
+ 'X-Audit-Log-Reason': reason
81
+ )
82
+ end
83
+ end
@@ -41,9 +41,8 @@ module Discordrb
41
41
  # @return [Array] This await's key and whether or not it should be deleted. If there was no match, both are nil.
42
42
  def match(event)
43
43
  dummy_handler = EventContainer.handler_class(@type).new(@attributes, @bot)
44
- return [nil, nil] unless dummy_handler.matches?(event)
44
+ return [nil, nil] unless event.instance_of?(@type) && dummy_handler.matches?(event)
45
45
 
46
- should_delete = nil
47
46
  should_delete = true if (@block && @block.call(event) != false) || !@block
48
47
 
49
48
  [@key, should_delete]
@@ -15,6 +15,10 @@ require 'discordrb/events/roles'
15
15
  require 'discordrb/events/guilds'
16
16
  require 'discordrb/events/await'
17
17
  require 'discordrb/events/bans'
18
+ require 'discordrb/events/raw'
19
+ require 'discordrb/events/reactions'
20
+ require 'discordrb/events/webhooks'
21
+ require 'discordrb/events/invites'
18
22
 
19
23
  require 'discordrb/api'
20
24
  require 'discordrb/api/channel'
@@ -40,11 +44,12 @@ module Discordrb
40
44
  # @return [Array<Thread>] The threads.
41
45
  attr_reader :event_threads
42
46
 
43
- # Whether or not the bot should parse its own messages. Off by default.
47
+ # @return [true, false] whether or not the bot should parse its own messages. Off by default.
44
48
  attr_accessor :should_parse_self
45
49
 
46
50
  # The bot's name which discordrb sends to Discord when making any request, so Discord can identify bots with the
47
51
  # same codebase. Not required but I recommend setting it anyway.
52
+ # @return [String] The bot's name.
48
53
  attr_accessor :name
49
54
 
50
55
  # @return [Array(Integer, Integer)] the current shard key
@@ -74,7 +79,8 @@ module Discordrb
74
79
  # @param token [String] The token that should be used to log in. If your bot is a bot account, you have to specify
75
80
  # this. If you're logging in as a user, make sure to also set the account type to :user so discordrb doesn't think
76
81
  # you're trying to log in as a bot.
77
- # @param client_id [Integer] If you're logging in as a bot, the bot's client ID.
82
+ # @param client_id [Integer] If you're logging in as a bot, the bot's client ID. This is optional, and may be fetched
83
+ # from the API by calling {Bot#bot_application} (see {Application}).
78
84
  # @param type [Symbol] This parameter lets you manually overwrite the account type. This needs to be set when
79
85
  # logging in as a user, otherwise discordrb will treat you as a bot account. Valid values are `:user` and `:bot`.
80
86
  # @param name [String] Your bot's name. This will be sent to Discord with any API requests, who will use this to
@@ -87,31 +93,29 @@ module Discordrb
87
93
  # @param parse_self [true, false] Whether the bot should react on its own messages. It's best to turn this off
88
94
  # unless you really need this so you don't inadvertently create infinite loops.
89
95
  # @param shard_id [Integer] The number of the shard this bot should handle. See
90
- # https://github.com/hammerandchisel/discord-api-docs/issues/17 for how to do sharding.
96
+ # https://github.com/discordapp/discord-api-docs/issues/17 for how to do sharding.
91
97
  # @param num_shards [Integer] The total number of shards that should be running. See
92
- # https://github.com/hammerandchisel/discord-api-docs/issues/17 for how to do sharding.
98
+ # https://github.com/discordapp/discord-api-docs/issues/17 for how to do sharding.
93
99
  # @param redact_token [true, false] Whether the bot should redact the token in logs. Default is true.
100
+ # @param ignore_bots [true, false] Whether the bot should ignore bot accounts or not. Default is false.
101
+ # @param compress_mode [:none, :large, :stream] Sets which compression mode should be used when connecting
102
+ # to Discord's gateway. `:none` will request that no payloads are received compressed (not recommended for
103
+ # production bots). `:large` will request that large payloads are received compressed. `:stream` will request
104
+ # that all data be received in a continuous compressed stream.
105
+ # @param intents [:all, Array<Symbol>, nil] Intents that this bot requires. See {Discordrb::INTENTS}. If `nil`, no intents
106
+ # field will be passed.
94
107
  def initialize(
95
- log_mode: :normal,
96
- token: nil, client_id: nil, application_id: nil,
97
- type: nil, name: '', fancy_log: false, suppress_ready: false, parse_self: false,
98
- shard_id: nil, num_shards: nil, redact_token: true
108
+ log_mode: :normal,
109
+ token: nil, client_id: nil,
110
+ type: nil, name: '', fancy_log: false, suppress_ready: false, parse_self: false,
111
+ shard_id: nil, num_shards: nil, redact_token: true, ignore_bots: false,
112
+ compress_mode: :large, intents: nil
99
113
  )
100
-
101
- LOGGER.mode = if log_mode.is_a? TrueClass # Specifically check for `true` because people might not have updated yet
102
- :debug
103
- else
104
- log_mode
105
- end
106
-
114
+ LOGGER.mode = log_mode
107
115
  LOGGER.token = token if redact_token
108
116
 
109
117
  @should_parse_self = parse_self
110
118
 
111
- if application_id
112
- raise ArgumentError, 'Starting with discordrb 3.0.0, the application_id parameter has been renamed to client_id! Make sure to change this in your bot. This check will be removed in 3.1.0.'
113
- end
114
-
115
119
  @client_id = client_id
116
120
 
117
121
  @type = type || :bot
@@ -122,8 +126,14 @@ module Discordrb
122
126
  LOGGER.fancy = fancy_log
123
127
  @prevent_ready = suppress_ready
124
128
 
129
+ @compress_mode = compress_mode
130
+
131
+ raise 'Token string is empty or nil' if token.nil? || token.empty?
132
+
133
+ @intents = intents == :all ? INTENTS.values.reduce(&:|) : calculate_intents(intents) if intents
134
+
125
135
  @token = process_token(@type, token)
126
- @gateway = Gateway.new(self, @token)
136
+ @gateway = Gateway.new(self, @token, @shard_key, @compress_mode, @intents)
127
137
 
128
138
  init_cache
129
139
 
@@ -131,6 +141,7 @@ module Discordrb
131
141
  @should_connect_to_voice = {}
132
142
 
133
143
  @ignored_ids = Set.new
144
+ @ignore_bots = ignore_bots
134
145
 
135
146
  @event_threads = []
136
147
  @current_thread = 0
@@ -142,6 +153,7 @@ module Discordrb
142
153
  # @return [Hash<Integer => User>] The users by ID.
143
154
  def users
144
155
  gateway_check
156
+ unavailable_servers_check
145
157
  @users
146
158
  end
147
159
 
@@ -149,28 +161,24 @@ module Discordrb
149
161
  # @return [Hash<Integer => Server>] The servers by ID.
150
162
  def servers
151
163
  gateway_check
164
+ unavailable_servers_check
152
165
  @servers
153
166
  end
154
167
 
155
168
  # @overload emoji(id)
156
169
  # Return an emoji by its ID
157
- # @param id [Integer] The emoji's ID.
158
- # @return emoji [GlobalEmoji, nil] the emoji object. `nil` if the emoji was not found.
170
+ # @param id [String, Integer] The emoji's ID.
171
+ # @return [Emoji, nil] the emoji object. `nil` if the emoji was not found.
159
172
  # @overload emoji
160
173
  # The list of emoji the bot can use.
161
- # @return [Array<GlobalEmoji>] the emoji available.
174
+ # @return [Array<Emoji>] the emoji available.
162
175
  def emoji(id = nil)
163
- gateway_check
176
+ emoji_hash = servers.values.map(&:emoji).reduce(&:merge)
164
177
  if id
165
- emoji = @emoji.find { |sth| sth.id == id }
178
+ id = id.resolve_id
179
+ emoji_hash[id]
166
180
  else
167
- emoji = {}
168
- @servers.each do |_, server|
169
- server.emoji.values.each do |element|
170
- emoji[element.name] = GlobalEmoji.new(element, self)
171
- end
172
- end
173
- emoji.values
181
+ emoji_hash.values
174
182
  end
175
183
  end
176
184
 
@@ -198,8 +206,8 @@ module Discordrb
198
206
  # The bot's OAuth application.
199
207
  # @return [Application, nil] The bot's application info. Returns `nil` if bot is not a bot account.
200
208
  def bot_application
201
- gateway_check
202
- return nil unless @type == :bot
209
+ return unless @type == :bot
210
+
203
211
  response = API.oauth_application(token)
204
212
  Application.new(JSON.parse(response), self)
205
213
  end
@@ -214,37 +222,46 @@ module Discordrb
214
222
  @token
215
223
  end
216
224
 
217
- # @return the raw token, without any prefix
225
+ # @return [String] the raw token, without any prefix
218
226
  # @see #token
219
227
  def raw_token
220
228
  @token.split(' ').last
221
229
  end
222
230
 
223
- # Runs the bot, which logs into Discord and connects the WebSocket. This prevents all further execution unless it is executed with `async` = `:async`.
224
- # @param async [Symbol] If it is `:async`, then the bot will allow further execution.
225
- # It doesn't necessarily have to be that, anything truthy will work,
226
- # however it is recommended to use `:async` for code readability reasons.
227
- # If the bot is run in async mode, make sure to eventually run {#sync} so
228
- # the script doesn't stop prematurely.
229
- def run(async = false)
231
+ # Runs the bot, which logs into Discord and connects the WebSocket. This
232
+ # prevents all further execution unless it is executed with
233
+ # `background` = `true`.
234
+ # @param background [true, false] If it is `true`, then the bot will run in
235
+ # another thread to allow further execution. If it is `false`, this method
236
+ # will block until {#stop} is called. If the bot is run with `true`, make
237
+ # sure to eventually call {#join} so the script doesn't stop prematurely.
238
+ # @note Running the bot in the background means that you can call some
239
+ # methods that require a gateway connection *before* that connection is
240
+ # established. In most cases an exception will be raised if you try to do
241
+ # this. If you need a way to safely run code after the bot is fully
242
+ # connected, use a {#ready} event handler instead.
243
+ def run(background = false)
230
244
  @gateway.run_async
231
- return if async
245
+ return if background
232
246
 
233
247
  debug('Oh wait! Not exiting yet as run was run synchronously.')
234
248
  @gateway.sync
235
249
  end
236
250
 
237
- # Blocks execution until the websocket stops, which should only happen manually triggered
238
- # or due to an error. This is necessary to have a continuously running bot.
239
- def sync
251
+ # Joins the bot's connection thread with the current thread.
252
+ # This blocks execution until the websocket stops, which should only happen
253
+ # manually triggered. or due to an error. This is necessary to have a
254
+ # continuously running bot.
255
+ def join
240
256
  @gateway.sync
241
257
  end
258
+ alias_method :sync, :join
242
259
 
243
260
  # Stops the bot gracefully, disconnecting the websocket without immediately killing the thread. This means that
244
261
  # Discord is immediately aware of the closed connection and makes the bot appear offline instantly.
245
- # @param no_sync [true, false] Whether or not to disable use of synchronize in the close method. This should be true if called from a trap context.
246
- def stop(no_sync = false)
247
- @gateway.stop(no_sync)
262
+ # @note This method no longer takes an argument as of 3.4.0
263
+ def stop(_no_sync = nil)
264
+ @gateway.stop
248
265
  end
249
266
 
250
267
  # @return [true, false] whether or not the bot is currently connected to Discord.
@@ -254,22 +271,21 @@ module Discordrb
254
271
 
255
272
  # Makes the bot join an invite to a server.
256
273
  # @param invite [String, Invite] The invite to join. For possible formats see {#resolve_invite_code}.
257
- def join(invite)
274
+ def accept_invite(invite)
258
275
  resolved = invite(invite).code
259
276
  API::Invite.accept(token, resolved)
260
277
  end
261
278
 
262
279
  # Creates an OAuth invite URL that can be used to invite this bot to a particular server.
263
- # Requires the application ID to have been set during initialization.
264
280
  # @param server [Server, nil] The server the bot should be invited to, or nil if a general invite should be created.
265
- # @param permission_bits [Integer, String] Permission bits that should be appended to invite url.
281
+ # @param permission_bits [String, Integer] Permission bits that should be appended to invite url.
266
282
  # @return [String] the OAuth invite URL.
267
283
  def invite_url(server: nil, permission_bits: nil)
268
- raise 'No application ID has been set during initialization! Add one as the `application_id` named parameter while creating your bot.' unless @client_id
284
+ @client_id ||= bot_application.id
269
285
 
270
286
  server_id_str = server ? "&guild_id=#{server.id}" : ''
271
287
  permission_bits_str = permission_bits ? "&permissions=#{permission_bits}" : ''
272
- "https://discordapp.com/oauth2/authorize?&client_id=#{@client_id}#{server_id_str}#{permission_bits_str}&scope=bot"
288
+ "https://discord.com/oauth2/authorize?&client_id=#{@client_id}#{server_id_str}#{permission_bits_str}&scope=bot"
273
289
  end
274
290
 
275
291
  # @return [Hash<Integer => VoiceBot>] the voice connections this bot currently has, by the server ID to which they are connected.
@@ -278,7 +294,7 @@ module Discordrb
278
294
  # Gets the voice bot for a particular server or channel. You can connect to a new channel using the {#voice_connect}
279
295
  # method.
280
296
  # @param thing [Channel, Server, Integer] the server or channel you want to get the voice bot for, or its ID.
281
- # @return [VoiceBot, nil] the VoiceBot for the thing you specified, or nil if there is no connection yet
297
+ # @return [Voice::VoiceBot, nil] the VoiceBot for the thing you specified, or nil if there is no connection yet
282
298
  def voice(thing)
283
299
  id = thing.resolve_id
284
300
  return @voices[id] if @voices[id]
@@ -288,22 +304,21 @@ module Discordrb
288
304
 
289
305
  server_id = channel.server.id
290
306
  return @voices[server_id] if @voices[server_id]
291
-
292
- nil
293
307
  end
294
308
 
295
309
  # Connects to a voice channel, initializes network connections and returns the {Voice::VoiceBot} over which audio
296
310
  # data can then be sent. After connecting, the bot can also be accessed using {#voice}. If the bot is already
297
311
  # connected to voice, the existing connection will be terminated - you don't have to call
298
312
  # {Discordrb::Voice::VoiceBot#destroy} before calling this method.
299
- # @param chan [Channel] The voice channel to connect to.
300
- # @param encrypted [true, false] Whether voice communication should be encrypted using RbNaCl's SecretBox
313
+ # @param chan [Channel, String, Integer] The voice channel, or its ID, to connect to.
314
+ # @param encrypted [true, false] Whether voice communication should be encrypted using
301
315
  # (uses an XSalsa20 stream cipher for encryption and Poly1305 for authentication)
302
316
  # @return [Voice::VoiceBot] the initialized bot over which audio data can then be sent.
303
317
  def voice_connect(chan, encrypted = true)
318
+ raise ArgumentError, 'Unencrypted voice connections are no longer supported.' unless encrypted
319
+
304
320
  chan = channel(chan.resolve_id)
305
321
  server_id = chan.server.id
306
- @should_encrypt_voice = encrypted
307
322
 
308
323
  if @voices[chan.id]
309
324
  debug('Voice bot exists already! Destroying it')
@@ -325,13 +340,14 @@ module Discordrb
325
340
 
326
341
  # Disconnects the client from a specific voice connection given the server ID. Usually it's more convenient to use
327
342
  # {Discordrb::Voice::VoiceBot#destroy} rather than this.
328
- # @param server_id [Integer] The ID of the server the voice connection is on.
343
+ # @param server [Server, String, Integer] The server, or server ID, the voice connection is on.
329
344
  # @param destroy_vws [true, false] Whether or not the VWS should also be destroyed. If you're calling this method
330
345
  # directly, you should leave it as true.
331
- def voice_destroy(server_id, destroy_vws = true)
332
- @gateway.send_voice_state_update(server_id.to_s, nil, false, false)
333
- @voices[server_id].destroy if @voices[server_id] && destroy_vws
334
- @voices.delete(server_id)
346
+ def voice_destroy(server, destroy_vws = true)
347
+ server = server.resolve_id
348
+ @gateway.send_voice_state_update(server.to_s, nil, false, false)
349
+ @voices[server].destroy if @voices[server] && destroy_vws
350
+ @voices.delete(server)
335
351
  end
336
352
 
337
353
  # Revokes an invite to a server. Will fail unless you have the *Manage Server* permission.
@@ -343,32 +359,38 @@ module Discordrb
343
359
  end
344
360
 
345
361
  # Sends a text message to a channel given its ID and the message's content.
346
- # @param channel_id [Integer] The ID that identifies the channel to send something to.
362
+ # @param channel [Channel, String, Integer] The channel, or its ID, to send something to.
347
363
  # @param content [String] The text that should be sent as a message. It is limited to 2000 characters (Discord imposed).
348
364
  # @param tts [true, false] Whether or not this message should be sent using Discord text-to-speech.
349
- # @param server_id [Integer] The ID that identifies the server to send something to.
365
+ # @param embed [Hash, Discordrb::Webhooks::Embed, nil] The rich embed to append to this message.
366
+ # @param allowed_mentions [Hash, Discordrb::AllowedMentions, false, nil] Mentions that are allowed to ping on this message. `false` disables all pings
367
+ # @param message_reference [Message, String, Integer, nil] The message, or message ID, to reply to if any.
350
368
  # @return [Message] The message that was sent.
351
- def send_message(channel_id, content, tts = false, server_id = nil)
352
- channel_id = channel_id.resolve_id
353
- debug("Sending message to #{channel_id} with content '#{content}'")
369
+ def send_message(channel, content, tts = false, embed = nil, attachments = nil, allowed_mentions = nil, message_reference = nil)
370
+ channel = channel.resolve_id
371
+ debug("Sending message to #{channel} with content '#{content}'")
372
+ allowed_mentions = { parse: [] } if allowed_mentions == false
373
+ message_reference = { message_id: message_reference.id } if message_reference
354
374
 
355
- response = API::Channel.create_message(token, channel_id, content, [], tts, server_id)
375
+ response = API::Channel.create_message(token, channel, content, tts, embed&.to_hash, nil, attachments, allowed_mentions&.to_hash, message_reference)
356
376
  Message.new(JSON.parse(response), self)
357
377
  end
358
378
 
359
379
  # Sends a text message to a channel given its ID and the message's content,
360
380
  # then deletes it after the specified timeout in seconds.
361
- # @param channel_id [Integer] The ID that identifies the channel to send something to.
381
+ # @param channel [Channel, String, Integer] The channel, or its ID, to send something to.
362
382
  # @param content [String] The text that should be sent as a message. It is limited to 2000 characters (Discord imposed).
363
383
  # @param timeout [Float] The amount of time in seconds after which the message sent will be deleted.
364
384
  # @param tts [true, false] Whether or not this message should be sent using Discord text-to-speech.
365
- # @param server_id [Integer] The ID that identifies the server to send something to.
366
- def send_temporary_message(channel_id, content, timeout, tts = false, server_id = nil)
385
+ # @param embed [Hash, Discordrb::Webhooks::Embed, nil] The rich embed to append to this message.
386
+ # @param attachments [Array<File>] Files that can be referenced in embeds via `attachment://file.png`
387
+ # @param allowed_mentions [Hash, Discordrb::AllowedMentions, false, nil] Mentions that are allowed to ping on this message. `false` disables all pings
388
+ def send_temporary_message(channel, content, timeout, tts = false, embed = nil, attachments = nil, allowed_mentions = nil)
367
389
  Thread.new do
368
- message = send_message(channel_id, content, tts, server_id)
390
+ Thread.current[:discordrb_name] = "#{@current_thread}-temp-msg"
369
391
 
392
+ message = send_message(channel, content, tts, embed, attachments, allowed_mentions)
370
393
  sleep(timeout)
371
-
372
394
  message.delete
373
395
  end
374
396
 
@@ -377,12 +399,26 @@ module Discordrb
377
399
 
378
400
  # Sends a file to a channel. If it is an image, it will automatically be embedded.
379
401
  # @note This executes in a blocking way, so if you're sending long files, be wary of delays.
380
- # @param channel_id [Integer] The ID that identifies the channel to send something to.
402
+ # @param channel [Channel, String, Integer] The channel, or its ID, to send something to.
381
403
  # @param file [File] The file that should be sent.
382
404
  # @param caption [string] The caption for the file.
383
405
  # @param tts [true, false] Whether or not this file's caption should be sent using Discord text-to-speech.
384
- def send_file(channel_id, file, caption: nil, tts: false)
385
- response = API::Channel.upload_file(token, channel_id, file, caption: caption, tts: tts)
406
+ # @param filename [String] Overrides the filename of the uploaded file
407
+ # @param spoiler [true, false] Whether or not this file should appear as a spoiler.
408
+ # @example Send a file from disk
409
+ # bot.send_file(83281822225530880, File.open('rubytaco.png', 'r'))
410
+ def send_file(channel, file, caption: nil, tts: false, filename: nil, spoiler: nil)
411
+ if file.respond_to?(:read)
412
+ if spoiler
413
+ filename ||= File.basename(file.path)
414
+ filename = "SPOILER_#{filename}" unless filename.start_with? 'SPOILER_'
415
+ end
416
+ # https://github.com/rest-client/rest-client/blob/v2.0.2/lib/restclient/payload.rb#L160
417
+ file.define_singleton_method(:original_filename) { filename } if filename
418
+ end
419
+
420
+ channel = channel.resolve_id
421
+ response = API::Channel.upload_file(token, channel, file, caption: caption, tts: tts)
386
422
  Message.new(JSON.parse(response), self)
387
423
  end
388
424
 
@@ -390,29 +426,18 @@ module Discordrb
390
426
  # @note Discord's API doesn't directly return the server when creating it, so this method
391
427
  # waits until the data has been received via the websocket. This may make the execution take a while.
392
428
  # @param name [String] The name the new server should have. Doesn't have to be alphanumeric.
393
- # @param region [Symbol] The region where the server should be created. Possible regions are:
394
- #
395
- # * `:london`
396
- # * `:amsterdam`
397
- # * `:frankfurt`
398
- # * `:us-east`
399
- # * `:us-west`
400
- # * `:us-south`
401
- # * `:us-central`
402
- # * `:singapore`
403
- # * `:sydney`
429
+ # @param region [Symbol] The region where the server should be created, for example 'eu-central' or 'hongkong'.
404
430
  # @return [Server] The server that was created.
405
- def create_server(name, region = :london)
431
+ def create_server(name, region = :'eu-central')
406
432
  response = API::Server.create(token, name, region)
407
433
  id = JSON.parse(response)['id'].to_i
408
- sleep 0.1 until @servers[id]
409
- server = @servers[id]
434
+ sleep 0.1 until (server = @servers[id])
410
435
  debug "Successfully created server #{server.id} with name #{server.name}"
411
436
  server
412
437
  end
413
438
 
414
439
  # Creates a new application to do OAuth authorization with. This allows you to use OAuth to authorize users using
415
- # Discord. For information how to use this, see the docs: https://discordapp.com/developers/docs/topics/oauth2
440
+ # Discord. For information how to use this, see the docs: https://discord.com/developers/docs/topics/oauth2
416
441
  # @param name [String] What your application should be called.
417
442
  # @param redirect_uris [Array<String>] URIs that Discord should redirect your users to after authorizing.
418
443
  # @return [Array(String, String)] your applications' client ID and client secret to be used in OAuth authorization.
@@ -431,42 +456,69 @@ module Discordrb
431
456
  API.update_oauth_application(@token, name, redirect_uris, description, icon)
432
457
  end
433
458
 
434
- # Gets the user, role or emoji from a mention of the user, role or emoji.
435
- # @param mention [String] The mention, which should look like `<@12314873129>`, `<@&123456789>` or `<:Name:126328:>`.
436
- # @param server [Server, nil] The server of the associated mention. (recommended for role parsing, to speed things up)
437
- # @return [User, Role, Emoji] The user, role or emoji identified by the mention, or `nil` if none exists.
438
- def parse_mention(mention, server = nil)
439
- # Mention format: <@id>
440
- if /<@!?(?<id>\d+)>?/ =~ mention
441
- user(id.to_i)
442
- elsif /<@&(?<id>\d+)>?/ =~ mention
443
- return server.role(id) if server
444
- servers.each do |element|
445
- role = element.role(id)
446
- return role unless role.nil?
459
+ # Gets the users, channels, roles and emoji from a string.
460
+ # @param mentions [String] The mentions, which should look like `<@12314873129>`, `<#123456789>`, `<@&123456789>` or `<:name:126328:>`.
461
+ # @param server [Server, nil] The server of the associated mentions. (recommended for role parsing, to speed things up)
462
+ # @return [Array<User, Channel, Role, Emoji>] The array of users, channels, roles and emoji identified by the mentions, or `nil` if none exists.
463
+ def parse_mentions(mentions, server = nil)
464
+ array_to_return = []
465
+ # While possible mentions may be in message
466
+ while mentions.include?('<') && mentions.include?('>')
467
+ # Removing all content before the next possible mention
468
+ mentions = mentions.split('<', 2)[1]
469
+ # Locate the first valid mention enclosed in `<...>`, otherwise advance to the next open `<`
470
+ next unless mentions.split('>', 2).first.length < mentions.split('<', 2).first.length
471
+
472
+ # Store the possible mention value to be validated with RegEx
473
+ mention = mentions.split('>', 2).first
474
+ if /@!?(?<id>\d+)/ =~ mention
475
+ array_to_return << user(id) unless user(id).nil?
476
+ elsif /#(?<id>\d+)/ =~ mention
477
+ array_to_return << channel(id, server) unless channel(id, server).nil?
478
+ elsif /@&(?<id>\d+)/ =~ mention
479
+ if server
480
+ array_to_return << server.role(id) unless server.role(id).nil?
481
+ else
482
+ @servers.each_value do |element|
483
+ array_to_return << element.role(id) unless element.role(id).nil?
484
+ end
485
+ end
486
+ elsif /(?<animated>^a|^${0}):(?<name>\w+):(?<id>\d+)/ =~ mention
487
+ array_to_return << (emoji(id) || Emoji.new({ 'animated' => !animated.nil?, 'name' => name, 'id' => id }, self, nil))
447
488
  end
448
- elsif /<:(\w+):(?<id>\d+)>?/ =~ mention
449
- emoji.find { |element| element.id.to_i == id.to_i }
450
489
  end
490
+ array_to_return
491
+ end
492
+
493
+ # Gets the user, channel, role or emoji from a string.
494
+ # @param mention [String] The mention, which should look like `<@12314873129>`, `<#123456789>`, `<@&123456789>` or `<:name:126328:>`.
495
+ # @param server [Server, nil] The server of the associated mention. (recommended for role parsing, to speed things up)
496
+ # @return [User, Channel, Role, Emoji] The user, channel, role or emoji identified by the mention, or `nil` if none exists.
497
+ def parse_mention(mention, server = nil)
498
+ parse_mentions(mention, server).first
451
499
  end
452
500
 
453
501
  # Updates presence status.
454
- # @param status [String] The status the bot should show up as.
455
- # @param game [String, nil] The name of the game to be played/stream name on the stream.
502
+ # @param status [String] The status the bot should show up as. Can be `online`, `dnd`, `idle`, or `invisible`
503
+ # @param activity [String, nil] The name of the activity to be played/watched/listened to/stream name on the stream.
456
504
  # @param url [String, nil] The Twitch URL to display as a stream. nil for no stream.
457
505
  # @param since [Integer] When this status was set.
458
506
  # @param afk [true, false] Whether the bot is AFK.
507
+ # @param activity_type [Integer] The type of activity status to display. Can be 0 (Playing), 1 (Streaming), 2 (Listening), 3 (Watching)
459
508
  # @see Gateway#send_status_update
460
- def update_status(status, game, url, since = 0, afk = false)
509
+ def update_status(status, activity, url, since = 0, afk = false, activity_type = 0)
461
510
  gateway_check
462
511
 
463
- @game = game
512
+ @activity = activity
464
513
  @status = status
465
514
  @streamurl = url
466
- type = url ? 1 : nil
515
+ type = url ? 1 : activity_type
516
+
517
+ activity_obj = activity || url ? { 'name' => activity, 'url' => url, 'type' => type } : nil
518
+ @gateway.send_status_update(status, since, activity_obj, afk)
467
519
 
468
- game_obj = game || url ? { name: game, url: url, type: type } : nil
469
- @gateway.send_status_update(status, since, game_obj, afk)
520
+ # Update the status in the cache
521
+ profile.update_presence('status' => status.to_s, 'activities' => [activity_obj].compact)
470
522
  end
471
523
 
472
524
  # Sets the currently playing game to the specified game.
@@ -475,7 +527,24 @@ module Discordrb
475
527
  def game=(name)
476
528
  gateway_check
477
529
  update_status(@status, name, nil)
478
- name
530
+ end
531
+
532
+ alias_method :playing=, :game=
533
+
534
+ # Sets the current listening status to the specified name.
535
+ # @param name [String] The thing to be listened to.
536
+ # @return [String] The thing that is now being listened to.
537
+ def listening=(name)
538
+ gateway_check
539
+ update_status(@status, name, nil, nil, nil, 2)
540
+ end
541
+
542
+ # Sets the current watching status to the specified name.
543
+ # @param name [String] The thing to be watched.
544
+ # @return [String] The thing that is now being watched.
545
+ def watching=(name)
546
+ gateway_check
547
+ update_status(@status, name, nil, nil, nil, 3)
479
548
  end
480
549
 
481
550
  # Sets the currently online stream to the specified name and Twitch URL.
@@ -491,7 +560,7 @@ module Discordrb
491
560
  # Sets status to online.
492
561
  def online
493
562
  gateway_check
494
- update_status(:online, @game, @streamurl)
563
+ update_status(:online, @activity, @streamurl)
495
564
  end
496
565
 
497
566
  alias_method :on, :online
@@ -499,7 +568,7 @@ module Discordrb
499
568
  # Sets status to idle.
500
569
  def idle
501
570
  gateway_check
502
- update_status(:idle, @game, nil)
571
+ update_status(:idle, @activity, nil)
503
572
  end
504
573
 
505
574
  alias_method :away, :idle
@@ -507,13 +576,13 @@ module Discordrb
507
576
  # Sets the bot's status to DnD (red icon).
508
577
  def dnd
509
578
  gateway_check
510
- update_status(:dnd, @game, nil)
579
+ update_status(:dnd, @activity, nil)
511
580
  end
512
581
 
513
582
  # Sets the bot's status to invisible (appears offline).
514
583
  def invisible
515
584
  gateway_check
516
- update_status(:invisible, @game, nil)
585
+ update_status(:invisible, @activity, nil)
517
586
  end
518
587
 
519
588
  # Sets debug mode. If debug mode is on, many things will be outputted to STDOUT.
@@ -539,29 +608,77 @@ module Discordrb
539
608
  # @yield Is executed when the await is triggered.
540
609
  # @yieldparam event [Event] The event object that was triggered.
541
610
  # @return [Await] The await that was created.
611
+ # @deprecated Will be changed to blocking behavior in v4.0. Use {#add_await!} instead.
542
612
  def add_await(key, type, attributes = {}, &block)
543
613
  raise "You can't await an AwaitEvent!" if type == Discordrb::Events::AwaitEvent
614
+
544
615
  await = Await.new(self, key, type, attributes, block)
545
616
  @awaits ||= {}
546
617
  @awaits[key] = await
547
618
  end
548
619
 
620
+ # Awaits an event, blocking the current thread until a response is received.
621
+ # @param type [Class] The event class that should be listened for.
622
+ # @option attributes [Numeric] :timeout the amount of time (in seconds) to wait for a response before returning `nil`. Waits forever if omitted.
623
+ # @yield Executed when a matching event is received.
624
+ # @yieldparam event [Event] The event object that was triggered.
625
+ # @yieldreturn [true, false] Whether the event matches extra await criteria described by the block
626
+ # @return [Event, nil] The event object that was triggered, or `nil` if a `timeout` was set and no event was raised in time.
627
+ # @raise [ArgumentError] if `timeout` is given and is not a positive numeric value
628
+ def add_await!(type, attributes = {})
629
+ raise "You can't await an AwaitEvent!" if type == Discordrb::Events::AwaitEvent
630
+
631
+ timeout = attributes[:timeout]
632
+ raise ArgumentError, 'Timeout must be a number > 0' if timeout.is_a?(Numeric) && !timeout.positive?
633
+
634
+ mutex = Mutex.new
635
+ cv = ConditionVariable.new
636
+ response = nil
637
+ block = lambda do |event|
638
+ mutex.synchronize do
639
+ response = event
640
+ if block_given?
641
+ result = yield(event)
642
+ cv.signal if result.is_a?(TrueClass)
643
+ else
644
+ cv.signal
645
+ end
646
+ end
647
+ end
648
+
649
+ handler = register_event(type, attributes, block)
650
+
651
+ if timeout
652
+ Thread.new do
653
+ sleep timeout
654
+ mutex.synchronize { cv.signal }
655
+ end
656
+ end
657
+
658
+ mutex.synchronize { cv.wait(mutex) }
659
+
660
+ remove_handler(handler)
661
+ raise 'ConditionVariable was signaled without returning an event!' if response.nil? && timeout.nil?
662
+
663
+ response
664
+ end
665
+
549
666
  # Add a user to the list of ignored users. Those users will be ignored in message events at event processing level.
550
667
  # @note Ignoring a user only prevents any message events (including mentions, commands etc.) from them! Typing and
551
668
  # presence and any other events will still be received.
552
- # @param user [User, Integer, #resolve_id] The user, or its ID, to be ignored.
669
+ # @param user [User, String, Integer] The user, or its ID, to be ignored.
553
670
  def ignore_user(user)
554
671
  @ignored_ids << user.resolve_id
555
672
  end
556
673
 
557
674
  # Remove a user from the ignore list.
558
- # @param user [User, Integer, #resolve_id] The user, or its ID, to be unignored.
675
+ # @param user [User, String, Integer] The user, or its ID, to be unignored.
559
676
  def unignore_user(user)
560
677
  @ignored_ids.delete(user.resolve_id)
561
678
  end
562
679
 
563
680
  # Checks whether a user is being ignored.
564
- # @param user [User, Integer, #resolve_id] The user, or its ID, to check.
681
+ # @param user [User, String, Integer] The user, or its ID, to check.
565
682
  # @return [true, false] whether or not the user is ignored.
566
683
  def ignored?(user)
567
684
  @ignored_ids.include?(user.resolve_id)
@@ -587,6 +704,7 @@ module Discordrb
587
704
  raise_event(HeartbeatEvent.new(self))
588
705
  end
589
706
 
707
+ # Makes the bot leave any groups with no recipients remaining
590
708
  def prune_empty_groups
591
709
  @channels.each_value do |channel|
592
710
  channel.leave_group if channel.group? && channel.recipients.empty?
@@ -595,11 +713,19 @@ module Discordrb
595
713
 
596
714
  private
597
715
 
598
- # Throws a useful exception if there's currently no gateway connection
716
+ # Throws a useful exception if there's currently no gateway connection.
599
717
  def gateway_check
600
- return if connected?
718
+ raise "A gateway connection is necessary to call this method! You'll have to do it inside any event (e.g. `ready`) or after `bot.run :async`." unless connected?
719
+ end
720
+
721
+ # Logs a warning if there are servers which are still unavailable.
722
+ # e.g. due to a Discord outage or because the servers are large and taking a while to load.
723
+ def unavailable_servers_check
724
+ # Return unless there are servers that are unavailable.
725
+ return unless @unavailable_servers&.positive?
601
726
 
602
- raise "A gateway connection is necessary to call this method! You'll have to do it inside any event (e.g. `ready`) or after `bot.run :async`."
727
+ LOGGER.warn("#{@unavailable_servers} servers haven't been cached yet.")
728
+ LOGGER.warn('Servers may be unavailable due to an outage, or your bot is on very large servers that are taking a while to load.')
603
729
  end
604
730
 
605
731
  ### ## ## ######## ######## ######## ## ## ### ## ######
@@ -640,21 +766,39 @@ module Discordrb
640
766
  member.update_username(username)
641
767
  end
642
768
 
643
- member.status = data['status'].to_sym
644
- member.game = data['game'] ? data['game']['name'] : nil
769
+ member.update_presence(data)
770
+
771
+ member.avatar_id = data['user']['avatar'] if data['user']['avatar']
645
772
 
646
773
  server.cache_member(member)
647
774
  end
648
775
 
649
- # Internal handler for VOICE_STATUS_UPDATE
776
+ # Internal handler for VOICE_STATE_UPDATE
650
777
  def update_voice_state(data)
778
+ @session_id = data['session_id']
779
+
651
780
  server_id = data['guild_id'].to_i
652
781
  server = server(server_id)
653
782
  return unless server
654
783
 
784
+ user_id = data['user_id'].to_i
785
+ old_voice_state = server.voice_states[user_id]
786
+ old_channel_id = old_voice_state.voice_channel&.id if old_voice_state
787
+
655
788
  server.update_voice_state(data)
656
789
 
657
- @session_id = data['session_id']
790
+ existing_voice = @voices[server_id]
791
+ if user_id == @profile.id && existing_voice
792
+ new_channel_id = data['channel_id']
793
+ if new_channel_id
794
+ new_channel = channel(new_channel_id)
795
+ existing_voice.channel = new_channel
796
+ else
797
+ voice_destroy(server_id)
798
+ end
799
+ end
800
+
801
+ old_channel_id
658
802
  end
659
803
 
660
804
  # Internal handler for VOICE_SERVER_UPDATE
@@ -664,6 +808,7 @@ module Discordrb
664
808
 
665
809
  debug("Voice server update received! chan: #{channel.inspect}")
666
810
  return unless channel
811
+
667
812
  @should_connect_to_voice.delete(server_id)
668
813
  debug('Updating voice server!')
669
814
 
@@ -676,7 +821,7 @@ module Discordrb
676
821
  end
677
822
 
678
823
  debug('Got data, now creating the bot.')
679
- @voices[server_id] = Discordrb::Voice::VoiceBot.new(channel, self, token, @session_id, endpoint, @should_encrypt_voice)
824
+ @voices[server_id] = Discordrb::Voice::VoiceBot.new(channel, self, token, @session_id, endpoint)
680
825
  end
681
826
 
682
827
  # Internal handler for CHANNEL_CREATE
@@ -686,7 +831,7 @@ module Discordrb
686
831
 
687
832
  # Handle normal and private channels separately
688
833
  if server
689
- server.channels << channel
834
+ server.add_channel(channel)
690
835
  @channels[channel.id] = channel
691
836
  elsif channel.pm?
692
837
  @pm_channels[channel.recipient.id] = channel
@@ -700,6 +845,7 @@ module Discordrb
700
845
  channel = Channel.new(data, self)
701
846
  old_channel = @channels[channel.id]
702
847
  return unless old_channel
848
+
703
849
  old_channel.update_from(channel)
704
850
  end
705
851
 
@@ -711,7 +857,7 @@ module Discordrb
711
857
  # Handle normal and private channels separately
712
858
  if server
713
859
  @channels.delete(channel.id)
714
- server.channels.reject! { |c| c.id == channel.id }
860
+ server.delete_channel(channel.id)
715
861
  elsif channel.pm?
716
862
  @pm_channels.delete(channel.recipient.id)
717
863
  elsif channel.group?
@@ -756,12 +902,14 @@ module Discordrb
756
902
  member = server.member(data['user']['id'].to_i)
757
903
  member.update_roles(data['roles'])
758
904
  member.update_nick(data['nick'])
905
+ member.update_boosting_since(data['premium_since'])
759
906
  end
760
907
 
761
908
  # Internal handler for GUILD_MEMBER_DELETE
762
909
  def delete_guild_member(data)
763
910
  server_id = data['guild_id'].to_i
764
911
  server = self.server(server_id)
912
+ return unless server
765
913
 
766
914
  user_id = data['user']['id'].to_i
767
915
  server.delete_member(user_id)
@@ -802,7 +950,12 @@ module Discordrb
802
950
  server_id = data['guild_id'].to_i
803
951
  server = @servers[server_id]
804
952
  new_role = Role.new(role_data, self, server)
805
- server.add_role(new_role)
953
+ existing_role = server.role(new_role.id)
954
+ if existing_role
955
+ existing_role.update_from(new_role)
956
+ else
957
+ server.add_role(new_role)
958
+ end
806
959
  end
807
960
 
808
961
  # Internal handler for GUILD_ROLE_DELETE
@@ -813,6 +966,13 @@ module Discordrb
813
966
  server.delete_role(role_id)
814
967
  end
815
968
 
969
+ # Internal handler for GUILD_EMOJIS_UPDATE
970
+ def update_guild_emoji(data)
971
+ server_id = data['guild_id'].to_i
972
+ server = @servers[server_id]
973
+ server.update_emoji_data(data)
974
+ end
975
+
816
976
  # Internal handler for MESSAGE_CREATE
817
977
  def create_message(data); end
818
978
 
@@ -825,6 +985,15 @@ module Discordrb
825
985
  # Internal handler for MESSAGE_DELETE
826
986
  def delete_message(data); end
827
987
 
988
+ # Internal handler for MESSAGE_REACTION_ADD
989
+ def add_message_reaction(data); end
990
+
991
+ # Internal handler for MESSAGE_REACTION_REMOVE
992
+ def remove_message_reaction(data); end
993
+
994
+ # Internal handler for MESSAGE_REACTION_REMOVE_ALL
995
+ def remove_all_message_reactions(data); end
996
+
828
997
  # Internal handler for GUILD_BAN_ADD
829
998
  def add_user_ban(data); end
830
999
 
@@ -843,16 +1012,16 @@ module Discordrb
843
1012
  # Remove the "Bot " prefix if it exists
844
1013
  token = token[4..-1] if token.start_with? 'Bot '
845
1014
 
846
- token = 'Bot ' + token unless type == :user
1015
+ token = "Bot #{token}" unless type == :user
847
1016
  token
848
1017
  end
849
1018
 
850
1019
  def handle_dispatch(type, data)
851
1020
  # Check whether there are still unavailable servers and there have been more than 10 seconds since READY
852
- if @unavailable_servers && @unavailable_servers > 0 && (Time.now - @unavailable_timeout_time) > 10
1021
+ if @unavailable_servers&.positive? && (Time.now - @unavailable_timeout_time) > 10 && !((@intents || 0) & INTENTS[:servers]).zero?
853
1022
  # The server streaming timed out!
854
- LOGGER.warn("Server streaming timed out with #{@unavailable_servers} servers remaining")
855
- LOGGER.warn("This means some servers are unavailable due to an outage. Notifying ready now, we'll have to live without these servers")
1023
+ LOGGER.debug("Server streaming timed out with #{@unavailable_servers} servers remaining")
1024
+ LOGGER.debug('Calling ready now because server loading is taking a long time. Servers may be unavailable due to an outage, or your bot is on very large servers.')
856
1025
 
857
1026
  # Unset the unavailable server count so this doesn't get triggered again
858
1027
  @unavailable_servers = 0
@@ -911,12 +1080,22 @@ module Discordrb
911
1080
  id = data['guild_id'].to_i
912
1081
  server = server(id)
913
1082
  server.process_chunk(data['members'])
1083
+ when :INVITE_CREATE
1084
+ invite = Invite.new(data, self)
1085
+ raise_event(InviteCreateEvent.new(data, invite, self))
1086
+ when :INVITE_DELETE
1087
+ raise_event(InviteDeleteEvent.new(data, self))
914
1088
  when :MESSAGE_CREATE
915
1089
  if ignored?(data['author']['id'].to_i)
916
1090
  debug("Ignored author with ID #{data['author']['id']}")
917
1091
  return
918
1092
  end
919
1093
 
1094
+ if @ignore_bots && data['author']['bot']
1095
+ debug("Ignored Bot account with ID #{data['author']['id']}")
1096
+ return
1097
+ end
1098
+
920
1099
  # If create_message is overwritten with a method that returns the parsed message, use that instead, so we don't
921
1100
  # parse the message twice (which is just thrown away performance)
922
1101
  message = create_message(data)
@@ -940,6 +1119,10 @@ module Discordrb
940
1119
  update_message(data)
941
1120
 
942
1121
  message = Message.new(data, self)
1122
+
1123
+ event = MessageUpdateEvent.new(message, self)
1124
+ raise_event(event)
1125
+
943
1126
  return if message.from_bot? && !should_parse_self
944
1127
 
945
1128
  unless message.author
@@ -979,31 +1162,51 @@ module Discordrb
979
1162
  rescue Discordrb::Errors::NoPermission
980
1163
  debug 'Typing started in channel the bot has no access to, ignoring'
981
1164
  end
1165
+ when :MESSAGE_REACTION_ADD
1166
+ add_message_reaction(data)
1167
+
1168
+ return if profile.id == data['user_id'].to_i && !should_parse_self
1169
+
1170
+ event = ReactionAddEvent.new(data, self)
1171
+ raise_event(event)
1172
+ when :MESSAGE_REACTION_REMOVE
1173
+ remove_message_reaction(data)
1174
+
1175
+ return if profile.id == data['user_id'].to_i && !should_parse_self
1176
+
1177
+ event = ReactionRemoveEvent.new(data, self)
1178
+ raise_event(event)
1179
+ when :MESSAGE_REACTION_REMOVE_ALL
1180
+ remove_all_message_reactions(data)
1181
+
1182
+ event = ReactionRemoveAllEvent.new(data, self)
1183
+ raise_event(event)
982
1184
  when :PRESENCE_UPDATE
983
1185
  # Ignore friends list presences
984
1186
  return unless data['guild_id']
985
1187
 
986
- now_playing = data['game']
1188
+ now_playing = data['game'].nil? ? nil : data['game']['name']
987
1189
  presence_user = @users[data['user']['id'].to_i]
988
1190
  played_before = presence_user.nil? ? nil : presence_user.game
989
1191
  update_presence(data)
990
1192
 
991
- event = if now_playing != played_before
992
- PlayingEvent.new(data, self)
993
- else
1193
+ event = if now_playing == played_before
994
1194
  PresenceEvent.new(data, self)
1195
+ else
1196
+ PlayingEvent.new(data, self)
995
1197
  end
996
1198
 
997
1199
  raise_event(event)
998
1200
  when :VOICE_STATE_UPDATE
999
- update_voice_state(data)
1201
+ old_channel_id = update_voice_state(data)
1000
1202
 
1001
- event = VoiceStateUpdateEvent.new(data, self)
1203
+ event = VoiceStateUpdateEvent.new(data, old_channel_id, self)
1002
1204
  raise_event(event)
1003
1205
  when :VOICE_SERVER_UPDATE
1004
1206
  update_voice_server(data)
1005
1207
 
1006
- # no event as this is irrelevant to users
1208
+ event = VoiceServerUpdateEvent.new(data, self)
1209
+ raise_event(event)
1007
1210
  when :CHANNEL_CREATE
1008
1211
  create_channel(data)
1009
1212
 
@@ -1100,9 +1303,52 @@ module Discordrb
1100
1303
 
1101
1304
  event = ServerDeleteEvent.new(data, self)
1102
1305
  raise_event(event)
1306
+ when :GUILD_EMOJIS_UPDATE
1307
+ server_id = data['guild_id'].to_i
1308
+ server = @servers[server_id]
1309
+ old_emoji_data = server.emoji.clone
1310
+ update_guild_emoji(data)
1311
+ new_emoji_data = server.emoji
1312
+
1313
+ created_ids = new_emoji_data.keys - old_emoji_data.keys
1314
+ deleted_ids = old_emoji_data.keys - new_emoji_data.keys
1315
+ updated_ids = old_emoji_data.select do |k, v|
1316
+ new_emoji_data[k] && (v.name != new_emoji_data[k].name || v.roles != new_emoji_data[k].roles)
1317
+ end.keys
1318
+
1319
+ event = ServerEmojiChangeEvent.new(server, data, self)
1320
+ raise_event(event)
1321
+
1322
+ created_ids.each do |e|
1323
+ event = ServerEmojiCreateEvent.new(server, new_emoji_data[e], self)
1324
+ raise_event(event)
1325
+ end
1326
+
1327
+ deleted_ids.each do |e|
1328
+ event = ServerEmojiDeleteEvent.new(server, old_emoji_data[e], self)
1329
+ raise_event(event)
1330
+ end
1331
+
1332
+ updated_ids.each do |e|
1333
+ event = ServerEmojiUpdateEvent.new(server, old_emoji_data[e], new_emoji_data[e], self)
1334
+ raise_event(event)
1335
+ end
1336
+ when :WEBHOOKS_UPDATE
1337
+ event = WebhookUpdateEvent.new(data, self)
1338
+ raise_event(event)
1103
1339
  else
1104
1340
  # another event that we don't support yet
1105
- debug "Event #{type} has been received but is unsupported, ignoring"
1341
+ debug "Event #{type} has been received but is unsupported. Raising UnknownEvent"
1342
+
1343
+ event = UnknownEvent.new(type, data, self)
1344
+ raise_event(event)
1345
+ end
1346
+
1347
+ # The existence of this array is checked before for performance reasons, since this has to be done for *every*
1348
+ # dispatch.
1349
+ if @event_handlers && @event_handlers[RawEvent]
1350
+ event = RawEvent.new(type, data, self)
1351
+ raise_event(event)
1106
1352
  end
1107
1353
  rescue Exception => e
1108
1354
  LOGGER.error('Gateway message error!')
@@ -1124,7 +1370,9 @@ module Discordrb
1124
1370
 
1125
1371
  @event_handlers ||= {}
1126
1372
  handlers = @event_handlers[event.class]
1127
- (handlers || []).each do |handler|
1373
+ return unless handlers
1374
+
1375
+ handlers.dup.each do |handler|
1128
1376
  call_event(handler, event) if handler.matches?(event)
1129
1377
  end
1130
1378
  end
@@ -1139,7 +1387,7 @@ module Discordrb
1139
1387
  begin
1140
1388
  handler.call(event)
1141
1389
  handler.after_call(event)
1142
- rescue => e
1390
+ rescue StandardError => e
1143
1391
  log_exception(e)
1144
1392
  ensure
1145
1393
  @event_threads.delete(t)
@@ -1152,6 +1400,7 @@ module Discordrb
1152
1400
  @awaits.each do |_, await|
1153
1401
  key, should_delete = await.match(event)
1154
1402
  next unless key
1403
+
1155
1404
  debug("should_delete: #{should_delete}")
1156
1405
  @awaits.delete(await.key) if should_delete
1157
1406
 
@@ -1159,5 +1408,24 @@ module Discordrb
1159
1408
  raise_event(await_event)
1160
1409
  end
1161
1410
  end
1411
+
1412
+ def calculate_intents(intents)
1413
+ intents.reduce(0) do |sum, intent|
1414
+ case intent
1415
+ when Symbol
1416
+ if INTENTS[intent]
1417
+ sum | INTENTS[intent]
1418
+ else
1419
+ LOGGER.warn("Unknown intent: #{intent}")
1420
+ sum
1421
+ end
1422
+ when Integer
1423
+ sum | intent
1424
+ else
1425
+ LOGGER.warn("Invalid intent: #{intent}")
1426
+ sum
1427
+ end
1428
+ end
1429
+ end
1162
1430
  end
1163
1431
  end