discordrb 3.2.1 → 3.3.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 (47) hide show
  1. checksums.yaml +5 -5
  2. data/.gitignore +4 -0
  3. data/.rubocop.yml +3 -3
  4. data/.travis.yml +28 -3
  5. data/.yardopts +1 -1
  6. data/CHANGELOG.md +555 -144
  7. data/CONTRIBUTING.md +1 -1
  8. data/Gemfile +0 -4
  9. data/README.md +86 -15
  10. data/Rakefile +2 -2
  11. data/bin/travis_build_docs.sh +17 -0
  12. data/discordrb-webhooks.gemspec +2 -1
  13. data/discordrb.gemspec +12 -5
  14. data/lib/discordrb.rb +2 -2
  15. data/lib/discordrb/api.rb +94 -25
  16. data/lib/discordrb/api/channel.rb +53 -17
  17. data/lib/discordrb/api/invite.rb +7 -4
  18. data/lib/discordrb/api/server.rb +173 -36
  19. data/lib/discordrb/api/user.rb +18 -4
  20. data/lib/discordrb/api/webhook.rb +83 -0
  21. data/lib/discordrb/await.rb +1 -1
  22. data/lib/discordrb/bot.rb +191 -102
  23. data/lib/discordrb/cache.rb +39 -9
  24. data/lib/discordrb/commands/command_bot.rb +79 -24
  25. data/lib/discordrb/commands/container.rb +16 -2
  26. data/lib/discordrb/commands/parser.rb +46 -7
  27. data/lib/discordrb/commands/rate_limiter.rb +8 -6
  28. data/lib/discordrb/container.rb +51 -7
  29. data/lib/discordrb/data.rb +1729 -286
  30. data/lib/discordrb/errors.rb +34 -1
  31. data/lib/discordrb/events/generic.rb +1 -1
  32. data/lib/discordrb/events/guilds.rb +1 -0
  33. data/lib/discordrb/events/message.rb +18 -12
  34. data/lib/discordrb/events/presence.rb +7 -2
  35. data/lib/discordrb/events/reactions.rb +13 -4
  36. data/lib/discordrb/events/roles.rb +7 -6
  37. data/lib/discordrb/events/typing.rb +1 -1
  38. data/lib/discordrb/events/webhooks.rb +61 -0
  39. data/lib/discordrb/gateway.rb +85 -32
  40. data/lib/discordrb/light.rb +1 -1
  41. data/lib/discordrb/logger.rb +8 -7
  42. data/lib/discordrb/permissions.rb +41 -4
  43. data/lib/discordrb/version.rb +1 -1
  44. data/lib/discordrb/voice/encoder.rb +10 -8
  45. data/lib/discordrb/voice/voice_bot.rb +4 -4
  46. data/lib/discordrb/websocket.rb +2 -2
  47. metadata +59 -14
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # API calls for User object
2
4
  module Discordrb::API::User
3
5
  module_function
@@ -27,7 +29,7 @@ module Discordrb::API::User
27
29
  end
28
30
 
29
31
  # Change the current bot's nickname on a server
30
- def change_own_nickname(token, server_id, nick)
32
+ def change_own_nickname(token, server_id, nick, reason = nil)
31
33
  Discordrb::API.request(
32
34
  :guilds_sid_members_me_nick,
33
35
  server_id, # This is technically a guild endpoint
@@ -35,7 +37,8 @@ module Discordrb::API::User
35
37
  "#{Discordrb::API.api_base}/guilds/#{server_id}/members/@me/nick",
36
38
  { nick: nick }.to_json,
37
39
  Authorization: token,
38
- content_type: :json
40
+ content_type: :json,
41
+ 'X-Audit-Log-Reason': reason
39
42
  )
40
43
  end
41
44
 
@@ -128,8 +131,19 @@ module Discordrb::API::User
128
131
  )
129
132
  end
130
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
+
131
140
  # Make an avatar URL from the user and avatar IDs
132
- def avatar_url(user_id, avatar_id)
133
- "#{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}"
134
148
  end
135
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://discordapp.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://discordapp.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://discordapp.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://discordapp.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://discordapp.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://discordapp.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,7 +41,7 @@ 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
46
  should_delete = nil
47
47
  should_delete = true if (@block && @block.call(event) != false) || !@block
@@ -17,6 +17,7 @@ require 'discordrb/events/await'
17
17
  require 'discordrb/events/bans'
18
18
  require 'discordrb/events/raw'
19
19
  require 'discordrb/events/reactions'
20
+ require 'discordrb/events/webhooks'
20
21
 
21
22
  require 'discordrb/api'
22
23
  require 'discordrb/api/channel'
@@ -42,11 +43,12 @@ module Discordrb
42
43
  # @return [Array<Thread>] The threads.
43
44
  attr_reader :event_threads
44
45
 
45
- # Whether or not the bot should parse its own messages. Off by default.
46
+ # @return [true, false] whether or not the bot should parse its own messages. Off by default.
46
47
  attr_accessor :should_parse_self
47
48
 
48
49
  # The bot's name which discordrb sends to Discord when making any request, so Discord can identify bots with the
49
50
  # same codebase. Not required but I recommend setting it anyway.
51
+ # @return [String] The bot's name.
50
52
  attr_accessor :name
51
53
 
52
54
  # @return [Array(Integer, Integer)] the current shard key
@@ -76,7 +78,8 @@ module Discordrb
76
78
  # @param token [String] The token that should be used to log in. If your bot is a bot account, you have to specify
77
79
  # this. If you're logging in as a user, make sure to also set the account type to :user so discordrb doesn't think
78
80
  # you're trying to log in as a bot.
79
- # @param client_id [Integer] If you're logging in as a bot, the bot's client ID.
81
+ # @param client_id [Integer] If you're logging in as a bot, the bot's client ID. This is optional, and may be fetched
82
+ # from the API by calling {Bot#bot_application} (see {Application}).
80
83
  # @param type [Symbol] This parameter lets you manually overwrite the account type. This needs to be set when
81
84
  # logging in as a user, otherwise discordrb will treat you as a bot account. Valid values are `:user` and `:bot`.
82
85
  # @param name [String] Your bot's name. This will be sent to Discord with any API requests, who will use this to
@@ -89,24 +92,23 @@ module Discordrb
89
92
  # @param parse_self [true, false] Whether the bot should react on its own messages. It's best to turn this off
90
93
  # unless you really need this so you don't inadvertently create infinite loops.
91
94
  # @param shard_id [Integer] The number of the shard this bot should handle. See
92
- # https://github.com/hammerandchisel/discord-api-docs/issues/17 for how to do sharding.
95
+ # https://github.com/discordapp/discord-api-docs/issues/17 for how to do sharding.
93
96
  # @param num_shards [Integer] The total number of shards that should be running. See
94
- # https://github.com/hammerandchisel/discord-api-docs/issues/17 for how to do sharding.
97
+ # https://github.com/discordapp/discord-api-docs/issues/17 for how to do sharding.
95
98
  # @param redact_token [true, false] Whether the bot should redact the token in logs. Default is true.
96
99
  # @param ignore_bots [true, false] Whether the bot should ignore bot accounts or not. Default is false.
100
+ # @param compress_mode [:none, :large, :stream] Sets which compression mode should be used when connecting
101
+ # to Discord's gateway. `:none` will request that no payloads are received compressed (not recommended for
102
+ # production bots). `:large` will request that large payloads are received compressed. `:stream` will request
103
+ # that all data be received in a continuous compressed stream.
97
104
  def initialize(
98
105
  log_mode: :normal,
99
106
  token: nil, client_id: nil,
100
107
  type: nil, name: '', fancy_log: false, suppress_ready: false, parse_self: false,
101
- shard_id: nil, num_shards: nil, redact_token: true, ignore_bots: false
108
+ shard_id: nil, num_shards: nil, redact_token: true, ignore_bots: false,
109
+ compress_mode: :stream
102
110
  )
103
-
104
- LOGGER.mode = if log_mode.is_a? TrueClass # Specifically check for `true` because people might not have updated yet
105
- :debug
106
- else
107
- log_mode
108
- end
109
-
111
+ LOGGER.mode = log_mode
110
112
  LOGGER.token = token if redact_token
111
113
 
112
114
  @should_parse_self = parse_self
@@ -121,8 +123,10 @@ module Discordrb
121
123
  LOGGER.fancy = fancy_log
122
124
  @prevent_ready = suppress_ready
123
125
 
126
+ @compress_mode = compress_mode
127
+
124
128
  @token = process_token(@type, token)
125
- @gateway = Gateway.new(self, @token, @shard_key)
129
+ @gateway = Gateway.new(self, @token, @shard_key, @compress_mode)
126
130
 
127
131
  init_cache
128
132
 
@@ -142,6 +146,7 @@ module Discordrb
142
146
  # @return [Hash<Integer => User>] The users by ID.
143
147
  def users
144
148
  gateway_check
149
+ unavailable_servers_check
145
150
  @users
146
151
  end
147
152
 
@@ -149,29 +154,27 @@ module Discordrb
149
154
  # @return [Hash<Integer => Server>] The servers by ID.
150
155
  def servers
151
156
  gateway_check
157
+ unavailable_servers_check
152
158
  @servers
153
159
  end
154
160
 
155
161
  # @overload emoji(id)
156
162
  # 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.
163
+ # @param id [Integer, #resolve_id] The emoji's ID.
164
+ # @return [Emoji, nil] the emoji object. `nil` if the emoji was not found.
159
165
  # @overload emoji
160
166
  # The list of emoji the bot can use.
161
- # @return [Array<GlobalEmoji>] the emoji available.
167
+ # @return [Array<Emoji>] the emoji available.
162
168
  def emoji(id = nil)
163
169
  gateway_check
170
+ unavailable_servers_check
171
+
172
+ emoji_hash = @servers.values.map(&:emoji).reduce(&:merge)
164
173
  if id
165
- emoji
166
- @emoji.find { |sth| sth.id == id }
174
+ id = id.resolve_id
175
+ emoji_hash[id]
167
176
  else
168
- emoji = {}
169
- @servers.each do |_, server|
170
- server.emoji.values.each do |element|
171
- emoji[element.name] = GlobalEmoji.new(element, self)
172
- end
173
- end
174
- @emoji = emoji.values
177
+ emoji_hash.values
175
178
  end
176
179
  end
177
180
 
@@ -199,8 +202,7 @@ module Discordrb
199
202
  # The bot's OAuth application.
200
203
  # @return [Application, nil] The bot's application info. Returns `nil` if bot is not a bot account.
201
204
  def bot_application
202
- gateway_check
203
- return nil unless @type == :bot
205
+ return unless @type == :bot
204
206
  response = API.oauth_application(token)
205
207
  Application.new(JSON.parse(response), self)
206
208
  end
@@ -215,31 +217,40 @@ module Discordrb
215
217
  @token
216
218
  end
217
219
 
218
- # @return the raw token, without any prefix
220
+ # @return [String] the raw token, without any prefix
219
221
  # @see #token
220
222
  def raw_token
221
223
  @token.split(' ').last
222
224
  end
223
225
 
224
- # Runs the bot, which logs into Discord and connects the WebSocket. This prevents all further execution unless it is executed with `async` = `:async`.
225
- # @param async [Symbol] If it is `:async`, then the bot will allow further execution.
226
- # It doesn't necessarily have to be that, anything truthy will work,
227
- # however it is recommended to use `:async` for code readability reasons.
228
- # If the bot is run in async mode, make sure to eventually run {#sync} so
229
- # the script doesn't stop prematurely.
230
- def run(async = false)
226
+ # Runs the bot, which logs into Discord and connects the WebSocket. This
227
+ # prevents all further execution unless it is executed with
228
+ # `backround` = `true`.
229
+ # @param background [true, false] If it is `true`, then the bot will run in
230
+ # another thread to allow further execution. If it is `false`, this method
231
+ # will block until {#stop} is called. If the bot is run with `true`, make
232
+ # sure to eventually call {#join} so the script doesn't stop prematurely.
233
+ # @note Running the bot in the background means that you can call some
234
+ # methods that require a gateway connection *before* that connection is
235
+ # established. In most cases an exception will be raised if you try to do
236
+ # this. If you need a way to safely run code after the bot is fully
237
+ # connected, use a {#ready} event handler instead.
238
+ def run(background = false)
231
239
  @gateway.run_async
232
- return if async
240
+ return if background
233
241
 
234
242
  debug('Oh wait! Not exiting yet as run was run synchronously.')
235
243
  @gateway.sync
236
244
  end
237
245
 
238
- # Blocks execution until the websocket stops, which should only happen manually triggered
239
- # or due to an error. This is necessary to have a continuously running bot.
240
- def sync
246
+ # Joins the bot's connection thread with the current thread.
247
+ # This blocks execution until the websocket stops, which should only happen
248
+ # manually triggered. or due to an error. This is necessary to have a
249
+ # continuously running bot.
250
+ def join
241
251
  @gateway.sync
242
252
  end
253
+ alias_method :sync, :join
243
254
 
244
255
  # Stops the bot gracefully, disconnecting the websocket without immediately killing the thread. This means that
245
256
  # Discord is immediately aware of the closed connection and makes the bot appear offline instantly.
@@ -255,18 +266,17 @@ module Discordrb
255
266
 
256
267
  # Makes the bot join an invite to a server.
257
268
  # @param invite [String, Invite] The invite to join. For possible formats see {#resolve_invite_code}.
258
- def join(invite)
269
+ def accept_invite(invite)
259
270
  resolved = invite(invite).code
260
271
  API::Invite.accept(token, resolved)
261
272
  end
262
273
 
263
274
  # Creates an OAuth invite URL that can be used to invite this bot to a particular server.
264
- # Requires the application ID to have been set during initialization.
265
275
  # @param server [Server, nil] The server the bot should be invited to, or nil if a general invite should be created.
266
276
  # @param permission_bits [Integer, String] Permission bits that should be appended to invite url.
267
277
  # @return [String] the OAuth invite URL.
268
278
  def invite_url(server: nil, permission_bits: nil)
269
- raise 'No application ID has been set during initialization! Add one as the `client_id` named parameter while creating your bot.' unless @client_id
279
+ @client_id ||= bot_application.id
270
280
 
271
281
  server_id_str = server ? "&guild_id=#{server.id}" : ''
272
282
  permission_bits_str = permission_bits ? "&permissions=#{permission_bits}" : ''
@@ -279,7 +289,7 @@ module Discordrb
279
289
  # Gets the voice bot for a particular server or channel. You can connect to a new channel using the {#voice_connect}
280
290
  # method.
281
291
  # @param thing [Channel, Server, Integer] the server or channel you want to get the voice bot for, or its ID.
282
- # @return [VoiceBot, nil] the VoiceBot for the thing you specified, or nil if there is no connection yet
292
+ # @return [Voice::VoiceBot, nil] the VoiceBot for the thing you specified, or nil if there is no connection yet
283
293
  def voice(thing)
284
294
  id = thing.resolve_id
285
295
  return @voices[id] if @voices[id]
@@ -297,7 +307,7 @@ module Discordrb
297
307
  # data can then be sent. After connecting, the bot can also be accessed using {#voice}. If the bot is already
298
308
  # connected to voice, the existing connection will be terminated - you don't have to call
299
309
  # {Discordrb::Voice::VoiceBot#destroy} before calling this method.
300
- # @param chan [Channel] The voice channel to connect to.
310
+ # @param chan [Channel, Integer, #resolve_id] The voice channel to connect to.
301
311
  # @param encrypted [true, false] Whether voice communication should be encrypted using RbNaCl's SecretBox
302
312
  # (uses an XSalsa20 stream cipher for encryption and Poly1305 for authentication)
303
313
  # @return [Voice::VoiceBot] the initialized bot over which audio data can then be sent.
@@ -326,13 +336,14 @@ module Discordrb
326
336
 
327
337
  # Disconnects the client from a specific voice connection given the server ID. Usually it's more convenient to use
328
338
  # {Discordrb::Voice::VoiceBot#destroy} rather than this.
329
- # @param server_id [Integer] The ID of the server the voice connection is on.
339
+ # @param server [Server, Integer, #resolve_id] The server the voice connection is on.
330
340
  # @param destroy_vws [true, false] Whether or not the VWS should also be destroyed. If you're calling this method
331
341
  # directly, you should leave it as true.
332
- def voice_destroy(server_id, destroy_vws = true)
333
- @gateway.send_voice_state_update(server_id.to_s, nil, false, false)
334
- @voices[server_id].destroy if @voices[server_id] && destroy_vws
335
- @voices.delete(server_id)
342
+ def voice_destroy(server, destroy_vws = true)
343
+ server = server.resolve_id
344
+ @gateway.send_voice_state_update(server.to_s, nil, false, false)
345
+ @voices[server].destroy if @voices[server] && destroy_vws
346
+ @voices.delete(server)
336
347
  end
337
348
 
338
349
  # Revokes an invite to a server. Will fail unless you have the *Manage Server* permission.
@@ -344,32 +355,32 @@ module Discordrb
344
355
  end
345
356
 
346
357
  # Sends a text message to a channel given its ID and the message's content.
347
- # @param channel_id [Integer] The ID that identifies the channel to send something to.
358
+ # @param channel [Channel, Integer, #resolve_id] The channel to send something to.
348
359
  # @param content [String] The text that should be sent as a message. It is limited to 2000 characters (Discord imposed).
349
360
  # @param tts [true, false] Whether or not this message should be sent using Discord text-to-speech.
350
361
  # @param embed [Hash, Discordrb::Webhooks::Embed, nil] The rich embed to append to this message.
351
362
  # @return [Message] The message that was sent.
352
- def send_message(channel_id, content, tts = false, embed = nil)
353
- channel_id = channel_id.resolve_id
354
- debug("Sending message to #{channel_id} with content '#{content}'")
363
+ def send_message(channel, content, tts = false, embed = nil)
364
+ channel = channel.resolve_id
365
+ debug("Sending message to #{channel} with content '#{content}'")
355
366
 
356
- response = API::Channel.create_message(token, channel_id, content, [], tts, embed ? embed.to_hash : nil)
367
+ response = API::Channel.create_message(token, channel, content, tts, embed ? embed.to_hash : nil)
357
368
  Message.new(JSON.parse(response), self)
358
369
  end
359
370
 
360
371
  # Sends a text message to a channel given its ID and the message's content,
361
372
  # then deletes it after the specified timeout in seconds.
362
- # @param channel_id [Integer] The ID that identifies the channel to send something to.
373
+ # @param channel [Channel, Integer, #resolve_id] The channel to send something to.
363
374
  # @param content [String] The text that should be sent as a message. It is limited to 2000 characters (Discord imposed).
364
375
  # @param timeout [Float] The amount of time in seconds after which the message sent will be deleted.
365
376
  # @param tts [true, false] Whether or not this message should be sent using Discord text-to-speech.
366
377
  # @param embed [Hash, Discordrb::Webhooks::Embed, nil] The rich embed to append to this message.
367
- def send_temporary_message(channel_id, content, timeout, tts = false, embed = nil)
378
+ def send_temporary_message(channel, content, timeout, tts = false, embed = nil)
368
379
  Thread.new do
369
- message = send_message(channel_id, content, tts, embed)
380
+ Thread.current[:discordrb_name] = "#{@current_thread}-temp-msg"
370
381
 
382
+ message = send_message(channel, content, tts, embed)
371
383
  sleep(timeout)
372
-
373
384
  message.delete
374
385
  end
375
386
 
@@ -378,12 +389,15 @@ module Discordrb
378
389
 
379
390
  # Sends a file to a channel. If it is an image, it will automatically be embedded.
380
391
  # @note This executes in a blocking way, so if you're sending long files, be wary of delays.
381
- # @param channel_id [Integer] The ID that identifies the channel to send something to.
392
+ # @param channel [Channel, Integer, #resolve_id] The channel to send something to.
382
393
  # @param file [File] The file that should be sent.
383
394
  # @param caption [string] The caption for the file.
384
395
  # @param tts [true, false] Whether or not this file's caption should be sent using Discord text-to-speech.
385
- def send_file(channel_id, file, caption: nil, tts: false)
386
- response = API::Channel.upload_file(token, channel_id, file, caption: caption, tts: tts)
396
+ # @example Send a file from disk
397
+ # bot.send_file(83281822225530880, File.open('rubytaco.png', 'r'))
398
+ def send_file(channel, file, caption: nil, tts: false)
399
+ channel = channel.resolve_id
400
+ response = API::Channel.upload_file(token, channel, file, caption: caption, tts: tts)
387
401
  Message.new(JSON.parse(response), self)
388
402
  end
389
403
 
@@ -391,19 +405,9 @@ module Discordrb
391
405
  # @note Discord's API doesn't directly return the server when creating it, so this method
392
406
  # waits until the data has been received via the websocket. This may make the execution take a while.
393
407
  # @param name [String] The name the new server should have. Doesn't have to be alphanumeric.
394
- # @param region [Symbol] The region where the server should be created. Possible regions are:
395
- #
396
- # * `:london`
397
- # * `:amsterdam`
398
- # * `:frankfurt`
399
- # * `:us-east`
400
- # * `:us-west`
401
- # * `:us-south`
402
- # * `:us-central`
403
- # * `:singapore`
404
- # * `:sydney`
408
+ # @param region [Symbol] The region where the server should be created, for example 'eu-central' or 'hongkong'.
405
409
  # @return [Server] The server that was created.
406
- def create_server(name, region = :london)
410
+ def create_server(name, region = :'eu-central')
407
411
  response = API::Server.create(token, name, region)
408
412
  id = JSON.parse(response)['id'].to_i
409
413
  sleep 0.1 until @servers[id]
@@ -432,45 +436,51 @@ module Discordrb
432
436
  API.update_oauth_application(@token, name, redirect_uris, description, icon)
433
437
  end
434
438
 
435
- # Gets the user, role or emoji from a mention of the user, role or emoji.
436
- # @param mention [String] The mention, which should look like `<@12314873129>`, `<@&123456789>` or `<:Name:126328:>`.
439
+ # Gets the user, channel, role or emoji from a mention of the user, channel, role or emoji.
440
+ # @param mention [String] The mention, which should look like `<@12314873129>`, `<#123456789>`, `<@&123456789>` or `<:name:126328:>`.
437
441
  # @param server [Server, nil] The server of the associated mention. (recommended for role parsing, to speed things up)
438
- # @return [User, Role, Emoji] The user, role or emoji identified by the mention, or `nil` if none exists.
442
+ # @return [User, Channel, Role, Emoji] The user, channel, role or emoji identified by the mention, or `nil` if none exists.
439
443
  def parse_mention(mention, server = nil)
440
444
  # Mention format: <@id>
441
- if /<@!?(?<id>\d+)>?/ =~ mention
442
- user(id.to_i)
443
- elsif /<@&(?<id>\d+)>?/ =~ mention
444
- return server.role(id.to_i) if server
445
+ if /<@!?(?<id>\d+)>/ =~ mention
446
+ user(id)
447
+ elsif /<#(?<id>\d+)>/ =~ mention
448
+ channel(id, server)
449
+ elsif /<@&(?<id>\d+)>/ =~ mention
450
+ return server.role(id) if server
445
451
  @servers.values.each do |element|
446
- role = element.role(id.to_i)
452
+ role = element.role(id)
447
453
  return role unless role.nil?
448
454
  end
449
455
 
450
456
  # Return nil if no role is found
451
457
  nil
452
- elsif /<:(\w+):(?<id>\d+)>?/ =~ mention
453
- emoji.find { |element| element.id.to_i == id.to_i }
458
+ elsif /<(?<animated>a)?:(?<name>\w+):(?<id>\d+)>/ =~ mention
459
+ emoji(id) || Emoji.new({ 'animated' => !animated.nil?, 'name' => name, 'id' => id }, self, nil)
454
460
  end
455
461
  end
456
462
 
457
463
  # Updates presence status.
458
- # @param status [String] The status the bot should show up as.
459
- # @param game [String, nil] The name of the game to be played/stream name on the stream.
464
+ # @param status [String] The status the bot should show up as. Can be `online`, `dnd`, `idle`, or `invisible`
465
+ # @param activity [String, nil] The name of the activity to be played/watched/listened to/stream name on the stream.
460
466
  # @param url [String, nil] The Twitch URL to display as a stream. nil for no stream.
461
467
  # @param since [Integer] When this status was set.
462
468
  # @param afk [true, false] Whether the bot is AFK.
469
+ # @param activity_type [Integer] The type of activity status to display. Can be 0 (Playing), 1 (Streaming), 2 (Listening), 3 (Watching)
463
470
  # @see Gateway#send_status_update
464
- def update_status(status, game, url, since = 0, afk = false)
471
+ def update_status(status, activity, url, since = 0, afk = false, activity_type = 0)
465
472
  gateway_check
466
473
 
467
- @game = game
474
+ @activity = activity
468
475
  @status = status
469
476
  @streamurl = url
470
- type = url ? 1 : 0
477
+ type = url ? 1 : activity_type
478
+
479
+ activity_obj = activity || url ? { 'name' => activity, 'url' => url, 'type' => type } : nil
480
+ @gateway.send_status_update(status, since, activity_obj, afk)
471
481
 
472
- game_obj = game || url ? { name: game, url: url, type: type } : nil
473
- @gateway.send_status_update(status, since, game_obj, afk)
482
+ # Update the status in the cache
483
+ profile.update_presence('status' => status.to_s, 'game' => activity_obj)
474
484
  end
475
485
 
476
486
  # Sets the currently playing game to the specified game.
@@ -482,6 +492,26 @@ module Discordrb
482
492
  name
483
493
  end
484
494
 
495
+ alias_method :playing=, :game=
496
+
497
+ # Sets the current listening status to the specified name.
498
+ # @param name [String] The thing to be listened to.
499
+ # @return [String] The thing that is now being listened to.
500
+ def listening=(name)
501
+ gateway_check
502
+ update_status(@status, name, nil, nil, nil, 2)
503
+ name
504
+ end
505
+
506
+ # Sets the current watching status to the specified name.
507
+ # @param name [String] The thing to be watched.
508
+ # @return [String] The thing that is now being watched.
509
+ def watching=(name)
510
+ gateway_check
511
+ update_status(@status, name, nil, nil, nil, 3)
512
+ name
513
+ end
514
+
485
515
  # Sets the currently online stream to the specified name and Twitch URL.
486
516
  # @param name [String] The name of the stream to display.
487
517
  # @param url [String] The url of the current Twitch stream.
@@ -495,7 +525,7 @@ module Discordrb
495
525
  # Sets status to online.
496
526
  def online
497
527
  gateway_check
498
- update_status(:online, @game, @streamurl)
528
+ update_status(:online, @activity, @streamurl)
499
529
  end
500
530
 
501
531
  alias_method :on, :online
@@ -503,7 +533,7 @@ module Discordrb
503
533
  # Sets status to idle.
504
534
  def idle
505
535
  gateway_check
506
- update_status(:idle, @game, nil)
536
+ update_status(:idle, @activity, nil)
507
537
  end
508
538
 
509
539
  alias_method :away, :idle
@@ -511,13 +541,13 @@ module Discordrb
511
541
  # Sets the bot's status to DnD (red icon).
512
542
  def dnd
513
543
  gateway_check
514
- update_status(:dnd, @game, nil)
544
+ update_status(:dnd, @activity, nil)
515
545
  end
516
546
 
517
547
  # Sets the bot's status to invisible (appears offline).
518
548
  def invisible
519
549
  gateway_check
520
- update_status(:invisible, @game, nil)
550
+ update_status(:invisible, @activity, nil)
521
551
  end
522
552
 
523
553
  # Sets debug mode. If debug mode is on, many things will be outputted to STDOUT.
@@ -543,6 +573,7 @@ module Discordrb
543
573
  # @yield Is executed when the await is triggered.
544
574
  # @yieldparam event [Event] The event object that was triggered.
545
575
  # @return [Await] The await that was created.
576
+ # @deprecated Will be changed to blocking behavior in v4.0. Use {#add_await!} instead.
546
577
  def add_await(key, type, attributes = {}, &block)
547
578
  raise "You can't await an AwaitEvent!" if type == Discordrb::Events::AwaitEvent
548
579
  await = Await.new(self, key, type, attributes, block)
@@ -550,6 +581,43 @@ module Discordrb
550
581
  @awaits[key] = await
551
582
  end
552
583
 
584
+ # Awaits an event, blocking the current thread until a response is received.
585
+ # @param type [Class] The event class that should be listened for.
586
+ # @option attributes [Numeric] :timeout the amount of time to wait for a response before returning `nil`. Waits forever if omitted.
587
+ # @return [Event, nil] The event object that was triggered, or `nil` if a `timeout` was set and no event was raised in time.
588
+ # @raise [ArgumentError] if `timeout` is given and is not a positive numeric value
589
+ def add_await!(type, attributes = {})
590
+ raise "You can't await an AwaitEvent!" if type == Discordrb::Events::AwaitEvent
591
+
592
+ timeout = attributes[:timeout]
593
+ raise ArgumentError, 'Timeout must be a number > 0' if timeout && timeout.is_a?(Numeric) && timeout <= 0
594
+
595
+ mutex = Mutex.new
596
+ cv = ConditionVariable.new
597
+ response = nil
598
+ block = lambda do |event|
599
+ mutex.synchronize do
600
+ response = event
601
+ cv.signal
602
+ end
603
+ end
604
+
605
+ handler = register_event(type, attributes, block)
606
+
607
+ if timeout
608
+ Thread.new do
609
+ sleep timeout
610
+ mutex.synchronize { cv.signal }
611
+ end
612
+ end
613
+
614
+ mutex.synchronize { cv.wait(mutex) }
615
+
616
+ remove_handler(handler)
617
+ raise 'ConditionVariable was signaled without returning an event!' if response.nil? && timeout.nil?
618
+ response
619
+ end
620
+
553
621
  # Add a user to the list of ignored users. Those users will be ignored in message events at event processing level.
554
622
  # @note Ignoring a user only prevents any message events (including mentions, commands etc.) from them! Typing and
555
623
  # presence and any other events will still be received.
@@ -591,6 +659,7 @@ module Discordrb
591
659
  raise_event(HeartbeatEvent.new(self))
592
660
  end
593
661
 
662
+ # Makes the bot leave any groups with no recipients remaining
594
663
  def prune_empty_groups
595
664
  @channels.each_value do |channel|
596
665
  channel.leave_group if channel.group? && channel.recipients.empty?
@@ -599,11 +668,18 @@ module Discordrb
599
668
 
600
669
  private
601
670
 
602
- # Throws a useful exception if there's currently no gateway connection
671
+ # Throws a useful exception if there's currently no gateway connection.
603
672
  def gateway_check
604
- return if connected?
673
+ 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?
674
+ end
605
675
 
606
- 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`."
676
+ # Logs a warning if there are servers which are still unavailable.
677
+ # e.g. due to a Discord outage or because the servers are large and taking a while to load.
678
+ def unavailable_servers_check
679
+ # Return unless there are servers that are unavailable.
680
+ return unless @unavailable_servers && @unavailable_servers > 0
681
+ LOGGER.warn("#{@unavailable_servers} servers haven't been cached yet.")
682
+ 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.')
607
683
  end
608
684
 
609
685
  ### ## ## ######## ######## ######## ## ## ### ## ######
@@ -813,7 +889,12 @@ module Discordrb
813
889
  server_id = data['guild_id'].to_i
814
890
  server = @servers[server_id]
815
891
  new_role = Role.new(role_data, self, server)
816
- server.add_role(new_role)
892
+ existing_role = server.role(new_role.id)
893
+ if existing_role
894
+ existing_role.update_from(new_role)
895
+ else
896
+ server.add_role(new_role)
897
+ end
817
898
  end
818
899
 
819
900
  # Internal handler for GUILD_ROLE_DELETE
@@ -878,8 +959,8 @@ module Discordrb
878
959
  # Check whether there are still unavailable servers and there have been more than 10 seconds since READY
879
960
  if @unavailable_servers && @unavailable_servers > 0 && (Time.now - @unavailable_timeout_time) > 10
880
961
  # The server streaming timed out!
881
- LOGGER.warn("Server streaming timed out with #{@unavailable_servers} servers remaining")
882
- LOGGER.warn("This means some servers are unavailable due to an outage. Notifying ready now, we'll have to live without these servers")
962
+ LOGGER.debug("Server streaming timed out with #{@unavailable_servers} servers remaining")
963
+ 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.')
883
964
 
884
965
  # Unset the unavailable server count so this doesn't get triggered again
885
966
  @unavailable_servers = 0
@@ -1014,11 +1095,15 @@ module Discordrb
1014
1095
  when :MESSAGE_REACTION_ADD
1015
1096
  add_message_reaction(data)
1016
1097
 
1098
+ return if profile.id == data['user_id'].to_i && !should_parse_self
1099
+
1017
1100
  event = ReactionAddEvent.new(data, self)
1018
1101
  raise_event(event)
1019
1102
  when :MESSAGE_REACTION_REMOVE
1020
1103
  remove_message_reaction(data)
1021
1104
 
1105
+ return if profile.id == data['user_id'].to_i && !should_parse_self
1106
+
1022
1107
  event = ReactionRemoveEvent.new(data, self)
1023
1108
  raise_event(event)
1024
1109
  when :MESSAGE_REACTION_REMOVE_ALL
@@ -1030,7 +1115,7 @@ module Discordrb
1030
1115
  # Ignore friends list presences
1031
1116
  return unless data['guild_id']
1032
1117
 
1033
- now_playing = data['game']
1118
+ now_playing = data['game'].nil? ? nil : data['game']['name']
1034
1119
  presence_user = @users[data['user']['id'].to_i]
1035
1120
  played_before = presence_user.nil? ? nil : presence_user.game
1036
1121
  update_presence(data)
@@ -1177,6 +1262,9 @@ module Discordrb
1177
1262
  event = ServerEmojiUpdateEvent.new(server, old_emoji_data[e], new_emoji_data[e], self)
1178
1263
  raise_event(event)
1179
1264
  end
1265
+ when :WEBHOOKS_UPDATE
1266
+ event = WebhookUpdateEvent.new(data, self)
1267
+ raise_event(event)
1180
1268
  else
1181
1269
  # another event that we don't support yet
1182
1270
  debug "Event #{type} has been received but is unsupported. Raising UnknownEvent"
@@ -1211,7 +1299,8 @@ module Discordrb
1211
1299
 
1212
1300
  @event_handlers ||= {}
1213
1301
  handlers = @event_handlers[event.class]
1214
- (handlers || []).each do |handler|
1302
+ return unless handlers
1303
+ handlers.dup.each do |handler|
1215
1304
  call_event(handler, event) if handler.matches?(event)
1216
1305
  end
1217
1306
  end