discordrb 1.8.1 → 2.0.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.
- checksums.yaml +4 -4
- data/.overcommit.yml +7 -0
- data/.rubocop.yml +5 -4
- data/CHANGELOG.md +77 -0
- data/README.md +25 -15
- data/discordrb.gemspec +2 -3
- data/examples/commands.rb +14 -2
- data/examples/ping.rb +1 -1
- data/examples/pm_send.rb +1 -1
- data/lib/discordrb.rb +9 -0
- data/lib/discordrb/api.rb +176 -50
- data/lib/discordrb/await.rb +3 -0
- data/lib/discordrb/bot.rb +607 -372
- data/lib/discordrb/cache.rb +208 -0
- data/lib/discordrb/commands/command_bot.rb +50 -18
- data/lib/discordrb/commands/container.rb +11 -2
- data/lib/discordrb/commands/events.rb +2 -0
- data/lib/discordrb/commands/parser.rb +10 -8
- data/lib/discordrb/commands/rate_limiter.rb +2 -0
- data/lib/discordrb/container.rb +24 -25
- data/lib/discordrb/data.rb +521 -219
- data/lib/discordrb/errors.rb +6 -7
- data/lib/discordrb/events/await.rb +2 -0
- data/lib/discordrb/events/bans.rb +3 -1
- data/lib/discordrb/events/channels.rb +124 -0
- data/lib/discordrb/events/generic.rb +2 -0
- data/lib/discordrb/events/guilds.rb +16 -13
- data/lib/discordrb/events/lifetime.rb +12 -2
- data/lib/discordrb/events/members.rb +26 -15
- data/lib/discordrb/events/message.rb +20 -7
- data/lib/discordrb/events/presence.rb +18 -2
- data/lib/discordrb/events/roles.rb +83 -0
- data/lib/discordrb/events/typing.rb +15 -2
- data/lib/discordrb/events/voice_state_update.rb +2 -0
- data/lib/discordrb/light.rb +8 -0
- data/lib/discordrb/light/data.rb +62 -0
- data/lib/discordrb/light/integrations.rb +73 -0
- data/lib/discordrb/light/light_bot.rb +56 -0
- data/lib/discordrb/logger.rb +4 -0
- data/lib/discordrb/permissions.rb +16 -12
- data/lib/discordrb/token_cache.rb +3 -0
- data/lib/discordrb/version.rb +3 -1
- data/lib/discordrb/voice/encoder.rb +2 -0
- data/lib/discordrb/voice/network.rb +21 -14
- data/lib/discordrb/voice/voice_bot.rb +26 -3
- data/lib/discordrb/websocket.rb +69 -0
- metadata +15 -26
- data/lib/discordrb/events/channel_create.rb +0 -44
- data/lib/discordrb/events/channel_delete.rb +0 -44
- data/lib/discordrb/events/channel_update.rb +0 -46
- data/lib/discordrb/events/guild_role_create.rb +0 -35
- data/lib/discordrb/events/guild_role_delete.rb +0 -36
- data/lib/discordrb/events/guild_role_update.rb +0 -35
data/lib/discordrb/await.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'discordrb/container'
|
2
4
|
|
3
5
|
module Discordrb
|
@@ -26,6 +28,7 @@ module Discordrb
|
|
26
28
|
attr_reader :attributes
|
27
29
|
|
28
30
|
# Makes a new await. For internal use only.
|
31
|
+
# @!visibility private
|
29
32
|
def initialize(bot, key, type, attributes, block = nil)
|
30
33
|
@bot = bot
|
31
34
|
@key = key
|
data/lib/discordrb/bot.rb
CHANGED
@@ -1,19 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'rest-client'
|
2
|
-
require '
|
3
|
-
require 'eventmachine'
|
4
|
+
require 'zlib'
|
4
5
|
|
5
6
|
require 'discordrb/events/message'
|
6
7
|
require 'discordrb/events/typing'
|
7
8
|
require 'discordrb/events/lifetime'
|
8
9
|
require 'discordrb/events/presence'
|
9
10
|
require 'discordrb/events/voice_state_update'
|
10
|
-
require 'discordrb/events/
|
11
|
-
require 'discordrb/events/channel_update'
|
12
|
-
require 'discordrb/events/channel_delete'
|
11
|
+
require 'discordrb/events/channels'
|
13
12
|
require 'discordrb/events/members'
|
14
|
-
require 'discordrb/events/
|
15
|
-
require 'discordrb/events/guild_role_delete'
|
16
|
-
require 'discordrb/events/guild_role_update'
|
13
|
+
require 'discordrb/events/roles'
|
17
14
|
require 'discordrb/events/guilds'
|
18
15
|
require 'discordrb/events/await'
|
19
16
|
require 'discordrb/events/bans'
|
@@ -24,17 +21,65 @@ require 'discordrb/data'
|
|
24
21
|
require 'discordrb/await'
|
25
22
|
require 'discordrb/token_cache'
|
26
23
|
require 'discordrb/container'
|
24
|
+
require 'discordrb/websocket'
|
25
|
+
require 'discordrb/cache'
|
27
26
|
|
28
27
|
require 'discordrb/voice/voice_bot'
|
29
28
|
|
30
29
|
module Discordrb
|
30
|
+
# Gateway packet opcodes
|
31
|
+
module Opcodes
|
32
|
+
# **Received** when Discord dispatches an event to the gateway (like MESSAGE_CREATE, PRESENCE_UPDATE or whatever).
|
33
|
+
# The vast majority of received packets will have this opcode.
|
34
|
+
DISPATCH = 0
|
35
|
+
|
36
|
+
# **Two-way**: The client has to send a packet with this opcode every ~40 seconds (actual interval specified in
|
37
|
+
# READY or RESUMED) and the current sequence number, otherwise it will be disconnected from the gateway. In certain
|
38
|
+
# cases Discord may also send one, specifically if two clients are connected at once.
|
39
|
+
HEARTBEAT = 1
|
40
|
+
|
41
|
+
# **Sent**: This is one of the two possible ways to initiate a session after connecting to the gateway. It
|
42
|
+
# should contain the authentication token along with other stuff the server has to know right from the start, such
|
43
|
+
# as large_threshold and, for older gateway versions, the desired version.
|
44
|
+
IDENTIFY = 2
|
45
|
+
|
46
|
+
# **Sent**: Packets with this opcode are used to change the user's status and played game. (Sending this is never
|
47
|
+
# necessary for a gateway client to behave correctly)
|
48
|
+
PRESENCE = 3
|
49
|
+
|
50
|
+
# **Sent**: Packets with this opcode are used to change a user's voice state (mute/deaf/unmute/undeaf/etc.). It is
|
51
|
+
# also used to connect to a voice server in the first place. (Sending this is never necessary for a gateway client
|
52
|
+
# to behave correctly)
|
53
|
+
VOICE_STATE = 4
|
54
|
+
|
55
|
+
# **Sent**: This opcode is used to ping a voice server, whatever that means. The functionality of this opcode isn't
|
56
|
+
# known well but non-user clients should never send it.
|
57
|
+
VOICE_PING = 5
|
58
|
+
|
59
|
+
# **Sent**: This is the other of two possible ways to initiate a gateway session (other than {IDENTIFY}). Rather
|
60
|
+
# than starting an entirely new session, it resumes an existing session by replaying all events from a given
|
61
|
+
# sequence number. It should be used to recover from a connection error or anything like that when the session is
|
62
|
+
# still valid - sending this with an invalid session will cause an error to occur.
|
63
|
+
RESUME = 6
|
64
|
+
|
65
|
+
# **Received**: Discord sends this opcode to indicate that the client should reconnect to a different gateway
|
66
|
+
# server because the old one is currently being decommissioned. Counterintuitively, this opcode also invalidates the
|
67
|
+
# session - the client has to create an entirely new session with the new gateway instead of resuming the old one.
|
68
|
+
RECONNECT = 7
|
69
|
+
|
70
|
+
# **Sent**: This opcode identifies packets used to retrieve a list of members from a particular server. There is
|
71
|
+
# also a REST endpoint available for this, but it is inconvenient to use because the client has to implement
|
72
|
+
# pagination itself, whereas sending this opcode lets Discord handle the pagination and the client can just add
|
73
|
+
# members when it receives them. (Sending this is never necessary for a gateway client to behave correctly)
|
74
|
+
REQUEST_MEMBERS = 8
|
75
|
+
|
76
|
+
# **Received**: The functionality of this opcode is less known than the others but it appears to specifically
|
77
|
+
# tell the client to invalidate its local session and continue by {IDENTIFY}ing.
|
78
|
+
INVALIDATE_SESSION = 9
|
79
|
+
end
|
80
|
+
|
31
81
|
# Represents a Discord bot, including servers, users, etc.
|
32
82
|
class Bot
|
33
|
-
# The user that represents the bot itself. This version will always be identical to
|
34
|
-
# the user determined by {#user} called with the bot's ID.
|
35
|
-
# @return [User] The bot user.
|
36
|
-
attr_reader :bot_user
|
37
|
-
|
38
83
|
# The list of users the bot shares a server with.
|
39
84
|
# @return [Array<User>] The users.
|
40
85
|
attr_reader :users
|
@@ -54,6 +99,7 @@ module Discordrb
|
|
54
99
|
# to edit user data like the current username (see {Profile#username=}).
|
55
100
|
# @return [Profile] The bot's profile that can be used to edit data.
|
56
101
|
attr_reader :profile
|
102
|
+
alias_method :bot_user, :profile
|
57
103
|
|
58
104
|
# Whether or not the bot should parse its own messages. Off by default.
|
59
105
|
attr_accessor :should_parse_self
|
@@ -63,36 +109,80 @@ module Discordrb
|
|
63
109
|
attr_accessor :name
|
64
110
|
|
65
111
|
include EventContainer
|
112
|
+
include Cache
|
66
113
|
|
67
|
-
# Makes a new bot with the given
|
114
|
+
# Makes a new bot with the given authentication data. It will be ready to be added event handlers to and can
|
115
|
+
# eventually be run with {#run}.
|
116
|
+
#
|
117
|
+
# Depending on the authentication information present, discordrb will deduce whether you're running on a user or a
|
118
|
+
# bot account. (Discord recommends using bot accounts whenever possible.) The following sets of authentication
|
119
|
+
# information are valid:
|
120
|
+
# * token + application_id (bot account)
|
121
|
+
# * email + password (user account)
|
122
|
+
# * email + password + token (user account; the given token will be used for authentication instead of email
|
123
|
+
# and password)
|
124
|
+
#
|
125
|
+
# Simply creating a bot won't be enough to start sending messages etc. with, only a limited set of methods can
|
126
|
+
# be used after logging in. If you want to do something when the bot has connected successfully, either do it in the
|
127
|
+
# {#ready} event, or use the {#run} method with the :async parameter and do the processing after that.
|
68
128
|
# @param email [String] The email for your (or the bot's) Discord account.
|
69
129
|
# @param password [String] The valid password that should be used to log in to the account.
|
70
|
-
# @param
|
71
|
-
|
130
|
+
# @param log_mode [Symbol] The mode this bot should use for logging. See {Logger#mode=} for a list of modes.
|
131
|
+
# @param token [String] The token that should be used to log in. If your bot is a bot account, you have to specify
|
132
|
+
# this. If you're logging in as a user, make sure to also set the account type to :user so discordrb doesn't think
|
133
|
+
# you're trying to log in as a bot.
|
134
|
+
# @param application_id [Integer] If you're logging in as a bot, the bot's application ID.
|
135
|
+
# @param type [Symbol] This parameter lets you manually overwrite the account type. If this isn't specified, it will
|
136
|
+
# be determined by checking what other attributes are there. The only use case for this is if you want to log in
|
137
|
+
# as a user but only with a token. Valid values are :user and :bot.
|
138
|
+
# @param name [String] Your bot's name. This will be sent to Discord with any API requests, who will use this to
|
139
|
+
# trace the source of excessive API requests; it's recommended to set this to something if you make bots that many
|
140
|
+
# people will host on their servers separately.
|
141
|
+
# @param fancy_log [true, false] Whether the output log should be made extra fancy using ANSI escape codes. (Your
|
142
|
+
# terminal may not support this.)
|
143
|
+
# @param suppress_ready [true, false] Whether the READY packet should be exempt from being printed to console.
|
144
|
+
# Useful for very large bots running in debug or verbose log_mode.
|
145
|
+
# @param parse_self [true, false] Whether the bot should react on its own messages. It's best to turn this off
|
146
|
+
# unless you really need this so you don't inadvertently create infinite loops.
|
147
|
+
def initialize(
|
148
|
+
email: nil, password: nil, log_mode: :normal,
|
149
|
+
token: nil, application_id: nil,
|
150
|
+
type: nil, name: '', fancy_log: false, suppress_ready: false, parse_self: false)
|
72
151
|
# Make sure people replace the login details in the example files...
|
73
152
|
if email.is_a?(String) && email.end_with?('example.com')
|
74
153
|
puts 'You have to replace the login details in the example files with your own!'
|
75
154
|
exit
|
76
155
|
end
|
77
156
|
|
78
|
-
LOGGER.
|
79
|
-
|
157
|
+
LOGGER.mode = if log_mode.is_a? TrueClass # Specifically check for `true` because people might not have updated yet
|
158
|
+
:debug
|
159
|
+
else
|
160
|
+
log_mode
|
161
|
+
end
|
162
|
+
|
163
|
+
@should_parse_self = parse_self
|
80
164
|
|
81
165
|
@email = email
|
82
166
|
@password = password
|
83
167
|
|
84
|
-
@
|
168
|
+
@application_id = application_id
|
169
|
+
|
170
|
+
@type = determine_account_type(type, email, password, token, application_id)
|
171
|
+
|
172
|
+
@name = name
|
173
|
+
|
174
|
+
LOGGER.fancy = fancy_log
|
175
|
+
@prevent_ready = suppress_ready
|
85
176
|
|
86
177
|
debug('Creating token cache')
|
87
|
-
|
178
|
+
token_cache = Discordrb::TokenCache.new
|
88
179
|
debug('Token cache created successfully')
|
89
|
-
@token = login
|
180
|
+
@token = login(type, email, password, token, token_cache)
|
90
181
|
|
91
|
-
|
92
|
-
@users = {}
|
182
|
+
init_cache
|
93
183
|
|
94
|
-
|
95
|
-
@
|
184
|
+
@voices = {}
|
185
|
+
@should_connect_to_voice = {}
|
96
186
|
|
97
187
|
@event_threads = []
|
98
188
|
@current_thread = 0
|
@@ -106,6 +196,12 @@ module Discordrb
|
|
106
196
|
@token
|
107
197
|
end
|
108
198
|
|
199
|
+
# @return the raw token, without any prefix
|
200
|
+
# @see #token
|
201
|
+
def raw_token
|
202
|
+
@token.split(' ').last
|
203
|
+
end
|
204
|
+
|
109
205
|
# Runs the bot, which logs into Discord and connects the WebSocket. This prevents all further execution unless it is executed with `async` = `:async`.
|
110
206
|
# @param async [Symbol] If it is `:async`, then the bot will allow further execution.
|
111
207
|
# It doesn't necessarily have to be that, anything truthy will work,
|
@@ -142,14 +238,21 @@ module Discordrb
|
|
142
238
|
|
143
239
|
loop do
|
144
240
|
websocket_connect
|
145
|
-
debug("Disconnected! Attempting to reconnect in #{@falloff} seconds.")
|
146
|
-
sleep @falloff
|
147
|
-
@token = login
|
148
241
|
|
149
|
-
#
|
150
|
-
|
151
|
-
|
242
|
+
# websocket_connect is blocking so being in here means we're disconnected
|
243
|
+
LOGGER.warn('Oh dear, we got disconnected!')
|
244
|
+
|
245
|
+
if @reconnect_url
|
246
|
+
# We got an op 7! Don't wait before reconnecting
|
247
|
+
debug('Got an op 7, reconnecting right away')
|
248
|
+
else
|
249
|
+
wait_for_reconnect
|
250
|
+
end
|
251
|
+
|
252
|
+
# Restart the loop, i. e. reconnect
|
152
253
|
end
|
254
|
+
|
255
|
+
LOGGER.warn('The WS loop exited! Not sure if this is a good thing')
|
153
256
|
end
|
154
257
|
|
155
258
|
debug('WS thread created! Now waiting for confirmation that everything worked')
|
@@ -168,123 +271,95 @@ module Discordrb
|
|
168
271
|
@ws_thread.kill
|
169
272
|
end
|
170
273
|
|
171
|
-
#
|
172
|
-
#
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
raise Discordrb::Errors::NoPermission if @restricted_channels.include? id
|
274
|
+
# Makes the bot join an invite to a server.
|
275
|
+
# @param invite [String, Invite] The invite to join. For possible formats see {#resolve_invite_code}.
|
276
|
+
def join(invite)
|
277
|
+
resolved = invite(invite).code
|
278
|
+
API.join_server(token, resolved)
|
279
|
+
end
|
179
280
|
|
180
|
-
|
181
|
-
|
281
|
+
# Creates an OAuth invite URL that can be used to invite this bot to a particular server.
|
282
|
+
# Requires the application ID to have been set during initialization.
|
283
|
+
# @param server [Server, nil] The server the bot should be invited to, or nil if a general invite should be created.
|
284
|
+
# @return [String] the OAuth invite URL.
|
285
|
+
def invite_url(server = nil)
|
286
|
+
raise 'No application ID has been set during initialization! Add one as the `application_id` named parameter while creating your bot.' unless @application_id
|
182
287
|
|
183
|
-
|
184
|
-
|
185
|
-
channel = Channel.new(JSON.parse(response), self)
|
186
|
-
@channels[id] = channel
|
187
|
-
rescue Discordrb::Errors::NoPermission
|
188
|
-
debug "Tried to get access to restricted channel #{id}, blacklisting it"
|
189
|
-
@restricted_channels << id
|
190
|
-
raise
|
191
|
-
end
|
288
|
+
guild_id_str = server ? "&guild_id=#{server.id}" : ''
|
289
|
+
"https://discordapp.com/oauth2/authorize?&client_id=#{@application_id}#{guild_id_str}&scope=bot"
|
192
290
|
end
|
193
291
|
|
194
|
-
#
|
195
|
-
|
196
|
-
# usage of this method may be unavoidable if only the user ID is known.
|
197
|
-
# @param id [Integer] The user ID to generate a private channel for.
|
198
|
-
# @return [Channel] A private channel for that user.
|
199
|
-
def private_channel(id)
|
200
|
-
id = id.resolve_id
|
201
|
-
debug("Creating private channel with user id #{id}")
|
202
|
-
return @private_channels[id] if @private_channels[id]
|
292
|
+
# @return [Hash<Integer => VoiceBot>] the voice connections this bot currently has, by the server ID to which they are connected.
|
293
|
+
attr_reader :voices
|
203
294
|
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
295
|
+
# Gets the voice bot for a particular server or channel. You can connect to a new channel using the {#voice_connect}
|
296
|
+
# method.
|
297
|
+
# @param thing [Channel, Server, Integer] the server or channel you want to get the voice bot for, or its ID.
|
298
|
+
# @return [VoiceBot, nil] the VoiceBot for the thing you specified, or nil if there is no connection yet
|
299
|
+
def voice(thing)
|
300
|
+
id = thing.resolve_id
|
301
|
+
return @voices[id] if @voices[id]
|
208
302
|
|
209
|
-
|
210
|
-
|
211
|
-
#
|
212
|
-
# * An {Invite} object
|
213
|
-
# * The code for an invite
|
214
|
-
# * A fully qualified invite URL (e. g. `https://discordapp.com/invite/0A37aN7fasF7n83q`)
|
215
|
-
# * A short invite URL with protocol (e. g. `https://discord.gg/0A37aN7fasF7n83q`)
|
216
|
-
# * A short invite URL without protocol (e. g. `discord.gg/0A37aN7fasF7n83q`)
|
217
|
-
# @return [String] Only the code for the invite.
|
218
|
-
def resolve_invite_code(invite)
|
219
|
-
invite = invite.code if invite.is_a? Discordrb::Invite
|
220
|
-
invite = invite[invite.rindex('/') + 1..-1] if invite.start_with?('http', 'discord.gg')
|
221
|
-
invite
|
222
|
-
end
|
223
|
-
|
224
|
-
# Gets information about an invite.
|
225
|
-
# @param invite [String, Invite] The invite to join. For possible formats see {#resolve_invite_code}.
|
226
|
-
# @return [Invite] The invite with information about the given invite URL.
|
227
|
-
def invite(invite)
|
228
|
-
code = resolve_invite_code(invite)
|
229
|
-
Invite.new(JSON.parse(API.resolve_invite(token, code)), self)
|
230
|
-
end
|
303
|
+
channel = channel(id)
|
304
|
+
return nil unless channel
|
231
305
|
|
232
|
-
|
233
|
-
|
234
|
-
def join(invite)
|
235
|
-
resolved = invite(invite).code
|
236
|
-
API.join_server(token, resolved)
|
237
|
-
end
|
306
|
+
server_id = channel.server.id
|
307
|
+
return @voices[server_id] if @voices[server_id]
|
238
308
|
|
239
|
-
|
309
|
+
nil
|
310
|
+
end
|
240
311
|
|
241
312
|
# Connects to a voice channel, initializes network connections and returns the {Voice::VoiceBot} over which audio
|
242
|
-
# data can then be sent. After connecting, the bot can also be accessed using {#voice}.
|
313
|
+
# data can then be sent. After connecting, the bot can also be accessed using {#voice}. If the bot is already
|
314
|
+
# connected to voice, the existing connection will be terminated - you don't have to call {VoiceBot#destroy}
|
315
|
+
# before calling this method.
|
243
316
|
# @param chan [Channel] The voice channel to connect to.
|
244
317
|
# @param encrypted [true, false] Whether voice communication should be encrypted using RbNaCl's SecretBox
|
245
318
|
# (uses an XSalsa20 stream cipher for encryption and Poly1305 for authentication)
|
246
319
|
# @return [Voice::VoiceBot] the initialized bot over which audio data can then be sent.
|
247
320
|
def voice_connect(chan, encrypted = true)
|
248
|
-
if @voice
|
249
|
-
debug('Voice bot exists already! Destroying it')
|
250
|
-
@voice.destroy
|
251
|
-
@voice = nil
|
252
|
-
end
|
253
|
-
|
254
321
|
chan = channel(chan.resolve_id)
|
255
|
-
|
322
|
+
server_id = chan.server.id
|
256
323
|
@should_encrypt_voice = encrypted
|
257
324
|
|
258
|
-
|
325
|
+
if @voices[chan.id]
|
326
|
+
debug('Voice bot exists already! Destroying it')
|
327
|
+
@voices[chan.id].destroy
|
328
|
+
@voices.delete(chan.id)
|
329
|
+
end
|
330
|
+
|
331
|
+
debug("Got voice channel: #{chan}")
|
259
332
|
|
260
333
|
data = {
|
261
|
-
op:
|
334
|
+
op: Opcodes::VOICE_STATE,
|
262
335
|
d: {
|
263
|
-
guild_id:
|
264
|
-
channel_id:
|
336
|
+
guild_id: server_id.to_s,
|
337
|
+
channel_id: chan.id.to_s,
|
265
338
|
self_mute: false,
|
266
339
|
self_deaf: false
|
267
340
|
}
|
268
341
|
}
|
269
342
|
debug("Voice channel init packet is: #{data.to_json}")
|
270
343
|
|
271
|
-
@should_connect_to_voice =
|
344
|
+
@should_connect_to_voice[server_id] = chan
|
272
345
|
@ws.send(data.to_json)
|
273
346
|
debug('Voice channel init packet sent! Now waiting.')
|
274
347
|
|
275
|
-
sleep(0.05) until @
|
348
|
+
sleep(0.05) until @voices[server_id]
|
276
349
|
debug('Voice connect succeeded!')
|
277
|
-
@
|
350
|
+
@voices[server_id]
|
278
351
|
end
|
279
352
|
|
280
|
-
# Disconnects the client from
|
353
|
+
# Disconnects the client from a specific voice connection given the server ID. Usually it's more convenient to use
|
354
|
+
# {Discordrb::Voice::VoiceBot#destroy} rather than this.
|
355
|
+
# @param server_id [Integer] The ID of the server the voice connection is on.
|
281
356
|
# @param destroy_vws [true, false] Whether or not the VWS should also be destroyed. If you're calling this method
|
282
357
|
# directly, you should leave it as true.
|
283
|
-
def voice_destroy(destroy_vws = true)
|
358
|
+
def voice_destroy(server_id, destroy_vws = true)
|
284
359
|
data = {
|
285
|
-
op:
|
360
|
+
op: Opcodes::VOICE_STATE,
|
286
361
|
d: {
|
287
|
-
guild_id:
|
362
|
+
guild_id: server_id.to_s,
|
288
363
|
channel_id: nil,
|
289
364
|
self_mute: false,
|
290
365
|
self_deaf: false
|
@@ -294,8 +369,8 @@ module Discordrb
|
|
294
369
|
debug("Voice channel destroy packet is: #{data.to_json}")
|
295
370
|
@ws.send(data.to_json)
|
296
371
|
|
297
|
-
@
|
298
|
-
@
|
372
|
+
@voices[server_id].destroy if @voices[server_id] && destroy_vws
|
373
|
+
@voices.delete(server_id)
|
299
374
|
end
|
300
375
|
|
301
376
|
# Revokes an invite to a server. Will fail unless you have the *Manage Server* permission.
|
@@ -306,53 +381,6 @@ module Discordrb
|
|
306
381
|
API.delete_invite(token, invite)
|
307
382
|
end
|
308
383
|
|
309
|
-
# Gets a user by its ID.
|
310
|
-
# @note This can only resolve users known by the bot (i.e. that share a server with the bot).
|
311
|
-
# @param id [Integer] The user ID that should be resolved.
|
312
|
-
# @return [User, nil] The user identified by the ID, or `nil` if it couldn't be found.
|
313
|
-
def user(id)
|
314
|
-
id = id.resolve_id
|
315
|
-
@users[id]
|
316
|
-
end
|
317
|
-
|
318
|
-
# Gets a server by its ID.
|
319
|
-
# @note This can only resolve servers the bot is currently in.
|
320
|
-
# @param id [Integer] The server ID that should be resolved.
|
321
|
-
# @return [Server, nil] The server identified by the ID, or `nil` if it couldn't be found.
|
322
|
-
def server(id)
|
323
|
-
id = id.resolve_id
|
324
|
-
@servers[id]
|
325
|
-
end
|
326
|
-
|
327
|
-
# Finds a channel given its name and optionally the name of the server it is in.
|
328
|
-
# @param channel_name [String] The channel to search for.
|
329
|
-
# @param server_name [String] The server to search for, or `nil` if only the channel should be searched for.
|
330
|
-
# @return [Array<Channel>] The array of channels that were found. May be empty if none were found.
|
331
|
-
def find_channel(channel_name, server_name = nil)
|
332
|
-
results = []
|
333
|
-
|
334
|
-
@servers.values.each do |server|
|
335
|
-
server.channels.each do |channel|
|
336
|
-
results << channel if channel.name == channel_name && (server_name || server.name) == server.name
|
337
|
-
end
|
338
|
-
end
|
339
|
-
|
340
|
-
results
|
341
|
-
end
|
342
|
-
|
343
|
-
# Finds a user given its username.
|
344
|
-
# @param username [String] The username to look for.
|
345
|
-
# @return [Array<User>] The array of users that were found. May be empty if none were found.
|
346
|
-
def find_user(username)
|
347
|
-
@users.values.find_all { |e| e.username == username }
|
348
|
-
end
|
349
|
-
|
350
|
-
# @deprecated Use {#find_channel} instead
|
351
|
-
def find(channel_name, server_name = nil)
|
352
|
-
debug('Attempted to use bot.find - this method is deprecated! Use find_channel for the same functionality')
|
353
|
-
find_channel(channel_name, server_name)
|
354
|
-
end
|
355
|
-
|
356
384
|
# Sends a text message to a channel given its ID and the message's content.
|
357
385
|
# @param channel_id [Integer] The ID that identifies the channel to send something to.
|
358
386
|
# @param content [String] The text that should be sent as a message. It is limited to 2000 characters (Discord imposed).
|
@@ -435,7 +463,7 @@ module Discordrb
|
|
435
463
|
@game = name
|
436
464
|
|
437
465
|
data = {
|
438
|
-
op:
|
466
|
+
op: Opcodes::PRESENCE,
|
439
467
|
d: {
|
440
468
|
idle_since: nil,
|
441
469
|
game: name ? { name: name } : nil
|
@@ -446,6 +474,27 @@ module Discordrb
|
|
446
474
|
name
|
447
475
|
end
|
448
476
|
|
477
|
+
# Injects a reconnect event (op 7) into the event processor, causing Discord to reconnect to the given gateway URL.
|
478
|
+
# If the URL is set to nil, it will reconnect and get an entirely new gateway URL. This method has not much use
|
479
|
+
# outside of testing and implementing highly custom reconnect logic.
|
480
|
+
# @param url [String, nil] the URL to connect to or nil if one should be obtained from Discord.
|
481
|
+
def inject_reconnect(url)
|
482
|
+
websocket_message({
|
483
|
+
op: Opcodes::RECONNECT,
|
484
|
+
d: {
|
485
|
+
url: url
|
486
|
+
}
|
487
|
+
}.to_json)
|
488
|
+
end
|
489
|
+
|
490
|
+
# Injects a resume packet (op 6) into the gateway. If this is done with a running connection, it will cause an
|
491
|
+
# error. It has no use outside of testing stuff that I know of, but if you want to use it anyway for some reason,
|
492
|
+
# here it is.
|
493
|
+
# @param seq [Integer, nil] The sequence ID to inject, or nil if the currently tracked one should be used.
|
494
|
+
def inject_resume(seq)
|
495
|
+
resume(seq || @sequence, raw_token, @session_id)
|
496
|
+
end
|
497
|
+
|
449
498
|
# Sets debug mode. If debug mode is on, many things will be outputted to STDOUT.
|
450
499
|
def debug=(new_debug)
|
451
500
|
LOGGER.debug = new_debug
|
@@ -488,29 +537,24 @@ module Discordrb
|
|
488
537
|
|
489
538
|
private
|
490
539
|
|
491
|
-
|
492
|
-
|
493
|
-
|
494
|
-
|
495
|
-
## ######### ## ## ## ##
|
496
|
-
## ## ## ## ## ## ## ## ##
|
497
|
-
####### ## ## ###### ## ## ########
|
498
|
-
|
499
|
-
def add_server(data)
|
500
|
-
server = Server.new(data, self)
|
501
|
-
@servers[server.id] = server
|
502
|
-
|
503
|
-
# Initialize users
|
504
|
-
server.members.each do |member|
|
505
|
-
if @users[member.id]
|
506
|
-
# If the user is already cached, just add the new roles
|
507
|
-
@users[member.id].merge_roles(server, member.roles[server.id])
|
508
|
-
else
|
509
|
-
@users[member.id] = member
|
510
|
-
end
|
511
|
-
end
|
540
|
+
# Determines the type of an account by checking which parameters are given
|
541
|
+
def determine_account_type(type, email, password, token, application_id)
|
542
|
+
# Case 1: if a type is already given, return it
|
543
|
+
return type if type
|
512
544
|
|
513
|
-
|
545
|
+
# Case 2: user accounts can't have application IDs so if one is given, return bot type
|
546
|
+
return :bot if application_id
|
547
|
+
|
548
|
+
# Case 3: bot accounts can't have emails and passwords so if either is given, assume user
|
549
|
+
return :user if email || password
|
550
|
+
|
551
|
+
# Case 4: If we're here and no token is given, throw an exception because that's impossible
|
552
|
+
raise ArgumentError, "Can't login because no authentication data was given! Specify at least a token" unless token
|
553
|
+
|
554
|
+
# Case 5: Only a token has been specified, we can assume it's a bot but we should tell the user
|
555
|
+
# to specify the application ID:
|
556
|
+
LOGGER.warn('The application ID is missing! Logging in as a bot will work but some OAuth-related functionality will be unavailable!')
|
557
|
+
:bot
|
514
558
|
end
|
515
559
|
|
516
560
|
### ## ## ######## ######## ######## ## ## ### ## ######
|
@@ -523,77 +567,97 @@ module Discordrb
|
|
523
567
|
|
524
568
|
# Internal handler for PRESENCE_UPDATE
|
525
569
|
def update_presence(data)
|
570
|
+
# Friends list presences have no guild ID so ignore these to not cause an error
|
571
|
+
return unless data['guild_id']
|
572
|
+
|
526
573
|
user_id = data['user']['id'].to_i
|
527
574
|
server_id = data['guild_id'].to_i
|
528
|
-
server =
|
575
|
+
server = server(server_id)
|
529
576
|
return unless server
|
530
577
|
|
531
|
-
|
532
|
-
unless user
|
533
|
-
user = User.new(data['user'], self)
|
534
|
-
@users[user_id] = user
|
535
|
-
end
|
578
|
+
member_is_new = false
|
536
579
|
|
537
|
-
|
538
|
-
|
539
|
-
|
540
|
-
|
541
|
-
|
580
|
+
if server.member_cached?(user_id)
|
581
|
+
member = server.member(user_id)
|
582
|
+
else
|
583
|
+
# If the member is not cached yet, it means that it just came online from not being cached at all
|
584
|
+
# due to large_threshold. Fortunately, Discord sends the entire member object in this case, and
|
585
|
+
# not just a part of it - we can just cache this member directly
|
586
|
+
member = Member.new(data, server, self)
|
587
|
+
debug("Implicitly adding presence-obtained member #{user_id} to #{server_id} cache")
|
588
|
+
|
589
|
+
member_is_new = true
|
542
590
|
end
|
543
591
|
|
544
592
|
username = data['user']['username']
|
545
|
-
if username
|
546
|
-
debug "
|
547
|
-
|
593
|
+
if username && !member_is_new # Don't set the username for newly-cached members
|
594
|
+
debug "Implicitly updating presence-obtained information for member #{user_id}"
|
595
|
+
member.update_username(username)
|
548
596
|
end
|
549
597
|
|
550
|
-
|
551
|
-
|
552
|
-
|
598
|
+
member.status = data['status'].to_sym
|
599
|
+
member.game = data['game'] ? data['game']['name'] : nil
|
600
|
+
|
601
|
+
server.cache_member(member)
|
553
602
|
end
|
554
603
|
|
555
604
|
# Internal handler for VOICE_STATUS_UPDATE
|
556
605
|
def update_voice_state(data)
|
557
606
|
user_id = data['user_id'].to_i
|
558
607
|
server_id = data['guild_id'].to_i
|
559
|
-
server =
|
608
|
+
server = server(server_id)
|
560
609
|
return unless server
|
561
610
|
|
562
|
-
user =
|
563
|
-
user.server_mute = data['mute']
|
564
|
-
user.server_deaf = data['deaf']
|
565
|
-
user.self_mute = data['self_mute']
|
566
|
-
user.self_deaf = data['self_deaf']
|
611
|
+
user = server.member(user_id)
|
567
612
|
|
568
613
|
channel_id = data['channel_id']
|
569
614
|
channel = nil
|
570
615
|
channel = self.channel(channel_id.to_i) if channel_id
|
571
|
-
|
616
|
+
|
617
|
+
user.update_voice_state(
|
618
|
+
channel,
|
619
|
+
data['mute'],
|
620
|
+
data['deaf'],
|
621
|
+
data['self_mute'],
|
622
|
+
data['self_deaf'])
|
572
623
|
|
573
624
|
@session_id = data['session_id']
|
574
625
|
end
|
575
626
|
|
576
627
|
# Internal handler for VOICE_SERVER_UPDATE
|
577
628
|
def update_voice_server(data)
|
578
|
-
|
579
|
-
|
580
|
-
|
629
|
+
server_id = data['guild_id'].to_i
|
630
|
+
channel = @should_connect_to_voice[server_id]
|
631
|
+
|
632
|
+
debug("Voice server update received! chan: #{channel.inspect}")
|
633
|
+
return unless channel
|
634
|
+
@should_connect_to_voice.delete(server_id)
|
581
635
|
debug('Updating voice server!')
|
582
636
|
|
583
637
|
token = data['token']
|
584
638
|
endpoint = data['endpoint']
|
585
|
-
|
639
|
+
|
640
|
+
unless endpoint
|
641
|
+
debug('VOICE_SERVER_UPDATE sent with nil endpoint! Ignoring')
|
642
|
+
return
|
643
|
+
end
|
586
644
|
|
587
645
|
debug('Got data, now creating the bot.')
|
588
|
-
@
|
646
|
+
@voices[server_id] = Discordrb::Voice::VoiceBot.new(channel, self, token, @session_id, endpoint, @should_encrypt_voice)
|
589
647
|
end
|
590
648
|
|
591
649
|
# Internal handler for CHANNEL_CREATE
|
592
650
|
def create_channel(data)
|
593
651
|
channel = Channel.new(data, self)
|
594
652
|
server = channel.server
|
595
|
-
|
596
|
-
|
653
|
+
|
654
|
+
# Handle normal and private channels separately
|
655
|
+
if server
|
656
|
+
server.channels << channel
|
657
|
+
@channels[channel.id] = channel
|
658
|
+
else
|
659
|
+
@private_channels[channel.id] = channel
|
660
|
+
end
|
597
661
|
end
|
598
662
|
|
599
663
|
# Internal handler for CHANNEL_UPDATE
|
@@ -608,64 +672,46 @@ module Discordrb
|
|
608
672
|
def delete_channel(data)
|
609
673
|
channel = Channel.new(data, self)
|
610
674
|
server = channel.server
|
611
|
-
|
612
|
-
|
675
|
+
|
676
|
+
# Handle normal and private channels separately
|
677
|
+
if server
|
678
|
+
@channels.delete(channel.id)
|
679
|
+
server.channels.reject! { |c| c.id == channel.id }
|
680
|
+
else
|
681
|
+
@private_channels.delete(channel.id)
|
682
|
+
end
|
613
683
|
end
|
614
684
|
|
615
685
|
# Internal handler for GUILD_MEMBER_ADD
|
616
686
|
def add_guild_member(data)
|
617
|
-
user = User.new(data['user'], self)
|
618
687
|
server_id = data['guild_id'].to_i
|
619
|
-
server =
|
620
|
-
|
621
|
-
roles = []
|
622
|
-
data['roles'].each do |element|
|
623
|
-
role_id = element.to_i
|
624
|
-
roles << server.roles.find { |r| r.id == role_id }
|
625
|
-
end
|
626
|
-
user.update_roles(server, roles)
|
627
|
-
|
628
|
-
if @users[user.id]
|
629
|
-
# If the user is already cached, just add the new roles
|
630
|
-
@users[user.id].merge_roles(server, user.roles[server.id])
|
631
|
-
else
|
632
|
-
@users[user.id] = user
|
633
|
-
end
|
688
|
+
server = self.server(server_id)
|
634
689
|
|
635
|
-
|
690
|
+
member = Member.new(data, server, self)
|
691
|
+
server.add_member(member)
|
636
692
|
end
|
637
693
|
|
638
694
|
# Internal handler for GUILD_MEMBER_UPDATE
|
639
695
|
def update_guild_member(data)
|
640
|
-
user_id = data['user']['id'].to_i
|
641
|
-
user = @users[user_id]
|
642
|
-
|
643
696
|
server_id = data['guild_id'].to_i
|
644
|
-
server =
|
697
|
+
server = self.server(server_id)
|
645
698
|
|
646
|
-
|
647
|
-
data['roles']
|
648
|
-
role_id = element.to_i
|
649
|
-
roles << server.roles.find { |r| r.id == role_id }
|
650
|
-
end
|
651
|
-
user.update_roles(server, roles)
|
699
|
+
member = server.member(data['user']['id'].to_i)
|
700
|
+
member.update_roles(data['roles'])
|
652
701
|
end
|
653
702
|
|
654
703
|
# Internal handler for GUILD_MEMBER_DELETE
|
655
704
|
def delete_guild_member(data)
|
656
|
-
user_id = data['user']['id'].to_i
|
657
|
-
user = @users[user_id]
|
658
|
-
|
659
705
|
server_id = data['guild_id'].to_i
|
660
|
-
server =
|
706
|
+
server = self.server(server_id)
|
661
707
|
|
662
|
-
user.
|
663
|
-
server.
|
708
|
+
user_id = data['user']['id'].to_i
|
709
|
+
server.delete_member(user_id)
|
664
710
|
end
|
665
711
|
|
666
712
|
# Internal handler for GUILD_CREATE
|
667
713
|
def create_guild(data)
|
668
|
-
|
714
|
+
ensure_server(data)
|
669
715
|
end
|
670
716
|
|
671
717
|
# Internal handler for GUILD_UPDATE
|
@@ -738,60 +784,88 @@ module Discordrb
|
|
738
784
|
## ## ## ## ## ## ## ###
|
739
785
|
######## ####### ###### #### ## ##
|
740
786
|
|
741
|
-
def login
|
742
|
-
if
|
743
|
-
|
787
|
+
def login(type, email, password, token, token_cache)
|
788
|
+
# Don't bother with any login code if a token is already specified
|
789
|
+
return process_token(type, token) if token
|
744
790
|
|
745
|
-
|
746
|
-
|
747
|
-
end
|
791
|
+
# If a bot account attempts logging in without a token, throw an error
|
792
|
+
raise ArgumentError, 'Bot account detected (type == :bot) but no token was found! Please specify a token in the Bot initializer, or use a user account.' if type == :bot
|
748
793
|
|
749
|
-
|
750
|
-
|
794
|
+
# If the type is not a user account at this point, it must be invalid
|
795
|
+
raise ArgumentError, 'Invalid type specified! Use either :bot or :user' if type == :user
|
751
796
|
|
752
|
-
|
753
|
-
|
754
|
-
if token
|
755
|
-
debug('Token successfully obtained from cache!')
|
756
|
-
return token
|
757
|
-
end
|
797
|
+
user_login(email, password, token_cache)
|
798
|
+
end
|
758
799
|
|
759
|
-
|
760
|
-
|
761
|
-
|
800
|
+
def process_token(type, token)
|
801
|
+
# Remove the "Bot " prefix if it exists
|
802
|
+
token = token[4..-1] if token.start_with? 'Bot '
|
762
803
|
|
763
|
-
|
764
|
-
|
765
|
-
|
804
|
+
token = 'Bot ' + token unless type == :user
|
805
|
+
token
|
806
|
+
end
|
807
|
+
|
808
|
+
def user_login(email, password, token_cache)
|
809
|
+
debug('Logging in')
|
766
810
|
|
811
|
+
# Attempt to retrieve the token from the cache
|
812
|
+
retrieved_token = retrieve_token(email, password, token_cache)
|
813
|
+
return retrieved_token if retrieved_token
|
814
|
+
|
815
|
+
login_attempts ||= 0
|
816
|
+
|
817
|
+
# Login
|
818
|
+
login_response = JSON.parse(API.login(email, password))
|
819
|
+
token = login_response['token']
|
820
|
+
raise Discordrb::Errors::InvalidAuthenticationError unless token
|
767
821
|
debug('Received token from Discord!')
|
768
822
|
|
769
823
|
# Cache the token
|
770
|
-
|
771
|
-
|
772
|
-
|
773
|
-
rescue
|
774
|
-
|
775
|
-
|
776
|
-
|
777
|
-
|
778
|
-
|
779
|
-
login_attempts += 1
|
780
|
-
retry
|
824
|
+
token_cache.store_token(email, password, token)
|
825
|
+
|
826
|
+
token
|
827
|
+
rescue RestClient::BadRequest
|
828
|
+
raise Discordrb::Errors::InvalidAuthenticationError
|
829
|
+
rescue SocketError, RestClient::RequestFailed => e # RequestFailed handles the 52x error codes Cloudflare sometimes sends that aren't covered by specific RestClient classes
|
830
|
+
if login_attempts && login_attempts > 100
|
831
|
+
LOGGER.error("User login failed permanently after #{login_attempts} attempts")
|
832
|
+
raise
|
781
833
|
else
|
782
|
-
|
783
|
-
|
784
|
-
|
785
|
-
debug("Are you sure you're using the correct username and password?") if e.class == RestClient::BadRequest
|
786
|
-
log_exception(e)
|
787
|
-
raise $ERROR_INFO
|
834
|
+
LOGGER.error("User login failed! Trying again in 5 seconds, #{100 - login_attempts} remaining")
|
835
|
+
LOGGER.log_exception(e)
|
836
|
+
retry
|
788
837
|
end
|
789
838
|
end
|
790
839
|
|
840
|
+
def retrieve_token(email, password, token_cache)
|
841
|
+
# First, attempt to get the token from the cache
|
842
|
+
token = token_cache.token(email, password)
|
843
|
+
debug('Token successfully obtained from cache!') if token
|
844
|
+
token
|
845
|
+
end
|
846
|
+
|
791
847
|
def find_gateway
|
792
|
-
#
|
793
|
-
|
794
|
-
|
848
|
+
# If the reconnect URL is set, it means we got an op 7 earlier and should reconnect to the new URL
|
849
|
+
if @reconnect_url
|
850
|
+
debug("Reconnecting to URL #{@reconnect_url}")
|
851
|
+
url = @reconnect_url
|
852
|
+
@reconnect_url = nil # Unset the URL so we don't connect to the same URL again if the connection fails
|
853
|
+
url
|
854
|
+
else
|
855
|
+
# Get the correct gateway URL from Discord
|
856
|
+
response = API.gateway(token)
|
857
|
+
JSON.parse(response)['url']
|
858
|
+
end
|
859
|
+
end
|
860
|
+
|
861
|
+
def process_gateway
|
862
|
+
raw_url = find_gateway
|
863
|
+
|
864
|
+
# Append a slash in case it's not there (I'm not sure how well WSCS handles it otherwise)
|
865
|
+
raw_url += '/' unless raw_url.end_with? '/'
|
866
|
+
|
867
|
+
# Add the parameters we want
|
868
|
+
raw_url + "?encoding=json&v=#{GATEWAY_VERSION}"
|
795
869
|
end
|
796
870
|
|
797
871
|
## ## ###### ######## ## ## ######## ## ## ######## ######
|
@@ -802,106 +876,168 @@ module Discordrb
|
|
802
876
|
## ## ## ## ## ## ## ## ## ## ### ## ## ##
|
803
877
|
#### ### ###### ######## ### ######## ## ## ## ######
|
804
878
|
|
879
|
+
# Desired gateway version
|
880
|
+
GATEWAY_VERSION = 4
|
881
|
+
|
805
882
|
def websocket_connect
|
806
883
|
debug('Attempting to get gateway URL...')
|
807
|
-
|
808
|
-
debug("Success! Gateway URL is #{
|
884
|
+
gateway_url = process_gateway
|
885
|
+
debug("Success! Gateway URL is #{gateway_url}.")
|
809
886
|
debug('Now running bot')
|
810
887
|
|
811
|
-
|
812
|
-
|
888
|
+
@ws = Discordrb::WebSocket.new(
|
889
|
+
gateway_url,
|
890
|
+
method(:websocket_open),
|
891
|
+
method(:websocket_message),
|
892
|
+
method(:websocket_close),
|
893
|
+
proc { |e| LOGGER.error "Gateway error: #{e}" }
|
894
|
+
)
|
895
|
+
|
896
|
+
@ws.thread[:discordrb_name] = 'gateway'
|
897
|
+
@ws.thread.join
|
898
|
+
rescue => e
|
899
|
+
LOGGER.error 'Error while connecting to the gateway!'
|
900
|
+
LOGGER.log_exception e
|
901
|
+
raise
|
902
|
+
end
|
813
903
|
|
814
|
-
|
815
|
-
|
816
|
-
|
817
|
-
|
818
|
-
|
819
|
-
|
820
|
-
|
821
|
-
|
904
|
+
def websocket_reconnect(url)
|
905
|
+
# In here, we do nothing except set the reconnect URL and close the current connection.
|
906
|
+
@reconnect_url = url
|
907
|
+
@ws.close
|
908
|
+
|
909
|
+
# Reset the packet sequence number so we don't try to resume the connection afterwards
|
910
|
+
@sequence = 0
|
911
|
+
|
912
|
+
# Let's hope the reconnect handler reconnects us correctly...
|
822
913
|
end
|
823
914
|
|
824
915
|
def websocket_message(event)
|
916
|
+
if event.byteslice(0) == 'x'
|
917
|
+
# The message is encrypted
|
918
|
+
event = Zlib::Inflate.inflate(event)
|
919
|
+
end
|
920
|
+
|
825
921
|
# Parse packet
|
826
|
-
packet = JSON.parse(event
|
922
|
+
packet = JSON.parse(event)
|
827
923
|
|
828
924
|
if @prevent_ready && packet['t'] == 'READY'
|
829
925
|
debug('READY packet was received and suppressed')
|
830
926
|
elsif @prevent_ready && packet['t'] == 'GUILD_MEMBERS_CHUNK'
|
831
927
|
# Ignore chunks as they will be handled later anyway
|
832
928
|
else
|
833
|
-
LOGGER.in(event.
|
929
|
+
LOGGER.in(event.to_s)
|
930
|
+
end
|
931
|
+
|
932
|
+
opcode = packet['op'].to_i
|
933
|
+
|
934
|
+
if opcode == Opcodes::HEARTBEAT
|
935
|
+
# If Discord sends us a heartbeat, simply reply with a heartbeat with the packet's sequence number
|
936
|
+
@sequence = packet['s'].to_i
|
937
|
+
|
938
|
+
LOGGER.info("Received an op1 (seq: #{@sequence})! This means another client connected while this one is already running. Replying with the same seq")
|
939
|
+
send_heartbeat
|
940
|
+
|
941
|
+
return
|
834
942
|
end
|
835
943
|
|
836
|
-
|
944
|
+
if opcode == Opcodes::RECONNECT
|
945
|
+
websocket_reconnect(packet['d'] ? packet['d']['url'] : nil)
|
946
|
+
return
|
947
|
+
end
|
948
|
+
|
949
|
+
if opcode == Opcodes::INVALIDATE_SESSION
|
950
|
+
LOGGER.info "We got an opcode 9 from Discord! Invalidating the session. You probably don't have to worry about this."
|
951
|
+
invalidate_session
|
952
|
+
LOGGER.debug 'Session invalidated!'
|
953
|
+
|
954
|
+
LOGGER.debug 'Reconnecting with IDENTIFY'
|
955
|
+
websocket_open # Since we just invalidated the session, pretending we just opened the WS again will re-identify
|
956
|
+
LOGGER.debug "Re-identified! Let's hope everything works fine."
|
957
|
+
return
|
958
|
+
end
|
959
|
+
|
960
|
+
raise "Got an unexpected opcode (#{opcode}) in a gateway event!
|
961
|
+
Please report this issue along with the following information:
|
962
|
+
v#{GATEWAY_VERSION} #{packet}" unless opcode == Opcodes::DISPATCH
|
963
|
+
|
964
|
+
# Check whether there are still unavailable servers and there have been more than 10 seconds since READY
|
965
|
+
if @unavailable_servers && @unavailable_servers > 0 && (Time.now - @ready_time) > 10
|
966
|
+
# The server streaming timed out!
|
967
|
+
LOGGER.warn("Server streaming timed out with #{@unavailable_servers} servers remaining")
|
968
|
+
LOGGER.warn("This means some servers are unavailable due to an outage. Notifying ready now, we'll have to live without these servers")
|
969
|
+
notify_ready
|
970
|
+
end
|
971
|
+
|
972
|
+
# Keep track of the packet sequence (continually incrementing number for every packet) so we can resume a
|
973
|
+
# connection if we disconnect
|
974
|
+
@sequence = packet['s'].to_i
|
837
975
|
|
838
976
|
data = packet['d']
|
839
977
|
type = packet['t'].intern
|
840
978
|
case type
|
841
979
|
when :READY
|
980
|
+
LOGGER.info("Discord using gateway protocol version: #{data['v']}, requested: #{GATEWAY_VERSION}")
|
981
|
+
|
982
|
+
# Set the session ID in case we get disconnected and have to resume
|
983
|
+
@session_id = data['session_id']
|
984
|
+
|
842
985
|
# Activate the heartbeats
|
843
986
|
@heartbeat_interval = data['heartbeat_interval'].to_f / 1000.0
|
844
987
|
@heartbeat_active = true
|
845
988
|
debug("Desired heartbeat_interval: #{@heartbeat_interval}")
|
846
989
|
|
847
|
-
bot_user_id = data['user']['id'].to_i
|
848
990
|
@profile = Profile.new(data['user'], self, @email, @password)
|
849
991
|
|
850
992
|
# Initialize servers
|
851
993
|
@servers = {}
|
994
|
+
|
995
|
+
# Count unavailable servers
|
996
|
+
@unavailable_servers = 0
|
997
|
+
|
852
998
|
data['guilds'].each do |element|
|
853
|
-
|
999
|
+
# Check for true specifically because unavailable=false indicates that a previously unavailable server has
|
1000
|
+
# come online
|
1001
|
+
if element['unavailable'].is_a? TrueClass
|
1002
|
+
@unavailable_servers += 1
|
1003
|
+
|
1004
|
+
# Ignore any unavailable servers
|
1005
|
+
next
|
1006
|
+
end
|
854
1007
|
|
855
|
-
|
856
|
-
@bot_user = @users[bot_user_id]
|
1008
|
+
ensure_server(element)
|
857
1009
|
end
|
858
1010
|
|
859
1011
|
# Add private channels
|
860
|
-
@private_channels = {}
|
861
1012
|
data['private_channels'].each do |element|
|
862
|
-
channel =
|
863
|
-
@channels[channel.id] = channel
|
1013
|
+
channel = ensure_channel(element)
|
864
1014
|
@private_channels[channel.recipient.id] = channel
|
865
1015
|
end
|
866
1016
|
|
867
|
-
#
|
868
|
-
|
1017
|
+
# Don't notify yet if there are unavailable servers because they need to get available before the bot truly has
|
1018
|
+
# all the data
|
1019
|
+
if @unavailable_servers == 0
|
1020
|
+
# No unavailable servers - we're ready!
|
1021
|
+
notify_ready
|
1022
|
+
end
|
1023
|
+
|
1024
|
+
@ready_time = Time.now
|
1025
|
+
when :RESUMED
|
1026
|
+
# The RESUMED event is received after a successful op 6 (resume). It does nothing except tell the bot the
|
1027
|
+
# connection is initiated (like READY would) and set a new heartbeat interval.
|
1028
|
+
debug('Connection resumed')
|
869
1029
|
|
870
|
-
|
871
|
-
chunk_packet = {
|
872
|
-
op: 8,
|
873
|
-
d: {
|
874
|
-
guild_id: @servers.keys,
|
875
|
-
query: '',
|
876
|
-
limit: 0
|
877
|
-
}
|
878
|
-
}.to_json
|
879
|
-
@ws.send(chunk_packet)
|
1030
|
+
@heartbeat_interval = data['heartbeat_interval'].to_f / 1000.0
|
880
1031
|
|
881
|
-
|
1032
|
+
# Since we disabled it earlier so we don't send any heartbeats in between close and resume,, make sure to
|
1033
|
+
# re-enable heartbeating
|
1034
|
+
@heartbeat_active = true
|
882
1035
|
|
883
|
-
|
884
|
-
@ws_success = true
|
1036
|
+
debug("Desired heartbeat_interval: #{@heartbeat_interval}")
|
885
1037
|
when :GUILD_MEMBERS_CHUNK
|
886
1038
|
id = data['guild_id'].to_i
|
887
|
-
|
888
|
-
|
889
|
-
start_time = Time.now
|
890
|
-
|
891
|
-
members.each do |member|
|
892
|
-
# Add the guild_id to the member so we can reuse add_guild_member
|
893
|
-
member['guild_id'] = id
|
894
|
-
|
895
|
-
add_guild_member(member)
|
896
|
-
end
|
897
|
-
|
898
|
-
duration = Time.now - start_time
|
899
|
-
|
900
|
-
if members.length < 1000
|
901
|
-
debug "Got final chunk for server #{id}, parsing took #{duration} seconds"
|
902
|
-
else
|
903
|
-
debug "Got one chunk for server #{id}, parsing took #{duration} seconds"
|
904
|
-
end
|
1039
|
+
server = server(id)
|
1040
|
+
server.process_chunk(data['members'])
|
905
1041
|
when :MESSAGE_CREATE
|
906
1042
|
create_message(data)
|
907
1043
|
|
@@ -912,7 +1048,7 @@ module Discordrb
|
|
912
1048
|
event = MessageEvent.new(message, self)
|
913
1049
|
raise_event(event)
|
914
1050
|
|
915
|
-
if message.mentions.any? { |user| user.id == @
|
1051
|
+
if message.mentions.any? { |user| user.id == @profile.id }
|
916
1052
|
event = MentionEvent.new(message, self)
|
917
1053
|
raise_event(event)
|
918
1054
|
end
|
@@ -924,7 +1060,10 @@ module Discordrb
|
|
924
1060
|
when :MESSAGE_UPDATE
|
925
1061
|
update_message(data)
|
926
1062
|
|
927
|
-
|
1063
|
+
message = Message.new(data, self)
|
1064
|
+
return if message.from_bot? && !should_parse_self
|
1065
|
+
|
1066
|
+
event = MessageEditEvent.new(message, self)
|
928
1067
|
raise_event(event)
|
929
1068
|
when :MESSAGE_DELETE
|
930
1069
|
delete_message(data)
|
@@ -941,8 +1080,11 @@ module Discordrb
|
|
941
1080
|
debug 'Typing started in channel the bot has no access to, ignoring'
|
942
1081
|
end
|
943
1082
|
when :PRESENCE_UPDATE
|
1083
|
+
# Ignore friends list presences
|
1084
|
+
return unless data['guild_id']
|
1085
|
+
|
944
1086
|
now_playing = data['game']
|
945
|
-
presence_user =
|
1087
|
+
presence_user = @users[data['user']['id'].to_i]
|
946
1088
|
played_before = presence_user.nil? ? nil : presence_user.game
|
947
1089
|
update_presence(data)
|
948
1090
|
|
@@ -980,17 +1122,17 @@ module Discordrb
|
|
980
1122
|
when :GUILD_MEMBER_ADD
|
981
1123
|
add_guild_member(data)
|
982
1124
|
|
983
|
-
event =
|
1125
|
+
event = ServerMemberAddEvent.new(data, self)
|
984
1126
|
raise_event(event)
|
985
1127
|
when :GUILD_MEMBER_UPDATE
|
986
1128
|
update_guild_member(data)
|
987
1129
|
|
988
|
-
event =
|
1130
|
+
event = ServerMemberUpdateEvent.new(data, self)
|
989
1131
|
raise_event(event)
|
990
1132
|
when :GUILD_MEMBER_REMOVE
|
991
1133
|
delete_guild_member(data)
|
992
1134
|
|
993
|
-
event =
|
1135
|
+
event = ServerMemberDeleteEvent.new(data, self)
|
994
1136
|
raise_event(event)
|
995
1137
|
when :GUILD_BAN_ADD
|
996
1138
|
add_user_ban(data)
|
@@ -1005,56 +1147,106 @@ module Discordrb
|
|
1005
1147
|
when :GUILD_ROLE_UPDATE
|
1006
1148
|
update_guild_role(data)
|
1007
1149
|
|
1008
|
-
event =
|
1150
|
+
event = ServerRoleUpdateEvent.new(data, self)
|
1009
1151
|
raise_event(event)
|
1010
1152
|
when :GUILD_ROLE_CREATE
|
1011
1153
|
create_guild_role(data)
|
1012
1154
|
|
1013
|
-
event =
|
1155
|
+
event = ServerRoleCreateEvent.new(data, self)
|
1014
1156
|
raise_event(event)
|
1015
1157
|
when :GUILD_ROLE_DELETE
|
1016
1158
|
delete_guild_role(data)
|
1017
1159
|
|
1018
|
-
event =
|
1160
|
+
event = ServerRoleDeleteEvent.new(data, self)
|
1019
1161
|
raise_event(event)
|
1020
1162
|
when :GUILD_CREATE
|
1021
1163
|
create_guild(data)
|
1022
1164
|
|
1023
|
-
|
1165
|
+
# Check for false specifically (no data means the server has never been unavailable)
|
1166
|
+
if data['unavailable'].is_a? FalseClass
|
1167
|
+
@unavailable_servers -= 1
|
1168
|
+
|
1169
|
+
notify_ready if @unavailable_servers == 0
|
1170
|
+
end
|
1171
|
+
|
1172
|
+
event = ServerCreateEvent.new(data, self)
|
1024
1173
|
raise_event(event)
|
1025
1174
|
when :GUILD_UPDATE
|
1026
1175
|
update_guild(data)
|
1027
1176
|
|
1028
|
-
event =
|
1177
|
+
event = ServerUpdateEvent.new(data, self)
|
1029
1178
|
raise_event(event)
|
1030
1179
|
when :GUILD_DELETE
|
1031
1180
|
delete_guild(data)
|
1032
1181
|
|
1033
|
-
event =
|
1182
|
+
event = ServerDeleteEvent.new(data, self)
|
1034
1183
|
raise_event(event)
|
1035
1184
|
else
|
1036
1185
|
# another event that we don't support yet
|
1037
1186
|
debug "Event #{packet['t']} has been received but is unsupported, ignoring"
|
1038
1187
|
end
|
1039
1188
|
rescue Exception => e
|
1189
|
+
LOGGER.error('Gateway message error!')
|
1040
1190
|
log_exception(e)
|
1041
1191
|
end
|
1042
1192
|
|
1043
1193
|
def websocket_close(event)
|
1044
1194
|
LOGGER.error('Disconnected from WebSocket!')
|
1045
|
-
|
1046
|
-
|
1047
|
-
|
1048
|
-
|
1195
|
+
|
1196
|
+
# Don't handle nil events (for example if the disconnect came from our side)
|
1197
|
+
return unless event
|
1198
|
+
|
1199
|
+
# Handle actual close frames and errors separately
|
1200
|
+
if event.respond_to? :code
|
1201
|
+
LOGGER.error(" (Reason: #{event.data})")
|
1202
|
+
LOGGER.error(" (Code: #{event.code})")
|
1203
|
+
else
|
1204
|
+
LOGGER.log_exception event
|
1205
|
+
end
|
1206
|
+
|
1207
|
+
if event.code.to_i == 4006
|
1208
|
+
# If we got disconnected with a 4006, it means we sent a resume when Discord wanted an identify. To battle this,
|
1209
|
+
# we invalidate the local session so we'll just send an identify next time
|
1210
|
+
debug('Apparently we just sent the wrong type of initiation packet (resume rather than identify) to Discord. (Sorry!)
|
1211
|
+
Invalidating session so this is fixed next time')
|
1212
|
+
invalidate_session
|
1213
|
+
end
|
1214
|
+
|
1215
|
+
raise_event(DisconnectEvent.new(self))
|
1216
|
+
|
1217
|
+
# Stop sending heartbeats
|
1218
|
+
@heartbeat_active = false
|
1219
|
+
|
1220
|
+
# Safely close the WS connection and handle any errors that occur there
|
1221
|
+
begin
|
1222
|
+
@ws.close
|
1223
|
+
rescue => e
|
1224
|
+
LOGGER.warn 'Got the following exception while closing the WS after being disconnected:'
|
1225
|
+
LOGGER.log_exception e
|
1226
|
+
end
|
1227
|
+
rescue => e
|
1228
|
+
LOGGER.log_exception e
|
1229
|
+
raise
|
1049
1230
|
end
|
1050
1231
|
|
1051
|
-
def websocket_open
|
1232
|
+
def websocket_open
|
1233
|
+
# If we've already received packets (packet sequence > 0) resume an existing connection instead of identifying anew
|
1234
|
+
if @sequence && @sequence > 0
|
1235
|
+
resume(@sequence, raw_token, @session_id)
|
1236
|
+
return
|
1237
|
+
end
|
1238
|
+
|
1239
|
+
identify(raw_token, 100, GATEWAY_VERSION)
|
1240
|
+
end
|
1241
|
+
|
1242
|
+
# Identify the client to the gateway
|
1243
|
+
def identify(token, large_threshold, version)
|
1052
1244
|
# Send the initial packet
|
1053
1245
|
packet = {
|
1054
|
-
op:
|
1055
|
-
d: {
|
1056
|
-
v:
|
1057
|
-
token:
|
1246
|
+
op: Opcodes::IDENTIFY, # Opcode
|
1247
|
+
d: { # Packet data
|
1248
|
+
v: version, # WebSocket protocol version
|
1249
|
+
token: token,
|
1058
1250
|
properties: { # I'm unsure what these values are for exactly, but they don't appear to impact bot functionality in any way.
|
1059
1251
|
:'$os' => RUBY_PLATFORM.to_s,
|
1060
1252
|
:'$browser' => 'discordrb',
|
@@ -1062,19 +1254,62 @@ module Discordrb
|
|
1062
1254
|
:'$referrer' => '',
|
1063
1255
|
:'$referring_domain' => ''
|
1064
1256
|
},
|
1065
|
-
large_threshold:
|
1257
|
+
large_threshold: large_threshold,
|
1258
|
+
compress: true
|
1066
1259
|
}
|
1067
1260
|
}
|
1068
1261
|
|
1069
1262
|
@ws.send(packet.to_json)
|
1070
1263
|
end
|
1071
1264
|
|
1072
|
-
|
1073
|
-
|
1074
|
-
|
1265
|
+
# Resume a previous gateway connection when reconnecting to a different server
|
1266
|
+
def resume(seq, token, session_id)
|
1267
|
+
data = {
|
1268
|
+
op: Opcodes::RESUME,
|
1269
|
+
d: {
|
1270
|
+
seq: seq,
|
1271
|
+
token: token,
|
1272
|
+
session_id: session_id
|
1273
|
+
}
|
1274
|
+
}
|
1275
|
+
|
1276
|
+
@ws.send(data.to_json)
|
1277
|
+
end
|
1278
|
+
|
1279
|
+
# Invalidate the current session (whatever this means)
|
1280
|
+
def invalidate_session
|
1281
|
+
@sequence = 0
|
1282
|
+
@session_id = nil
|
1283
|
+
end
|
1284
|
+
|
1285
|
+
# Notifies everything there is to be notified that the connection is now ready
|
1286
|
+
def notify_ready
|
1287
|
+
# Make sure to raise the event
|
1288
|
+
raise_event(ReadyEvent.new(self))
|
1289
|
+
LOGGER.good 'Ready'
|
1290
|
+
|
1291
|
+
# Tell the run method that everything was successful
|
1292
|
+
@ws_success = true
|
1293
|
+
end
|
1294
|
+
|
1295
|
+
# Separate method to wait an ever-increasing amount of time before reconnecting after being disconnected in an
|
1296
|
+
# unexpected way
|
1297
|
+
def wait_for_reconnect
|
1298
|
+
# We disconnected in an unexpected way! Wait before reconnecting so we don't spam Discord's servers.
|
1299
|
+
debug("Disconnected! Attempting to reconnect in #{@falloff} seconds.")
|
1300
|
+
sleep @falloff
|
1301
|
+
|
1302
|
+
# Calculate new falloff
|
1303
|
+
@falloff *= 1.5
|
1304
|
+
@falloff = 115 + (rand * 10) if @falloff > 120 # Cap the falloff at 120 seconds and then add some random jitter
|
1305
|
+
end
|
1306
|
+
|
1307
|
+
def send_heartbeat(sequence = nil)
|
1308
|
+
sequence ||= @sequence
|
1309
|
+
LOGGER.out("Sending heartbeat with sequence #{sequence}")
|
1075
1310
|
data = {
|
1076
|
-
op:
|
1077
|
-
d:
|
1311
|
+
op: Opcodes::HEARTBEAT,
|
1312
|
+
d: sequence
|
1078
1313
|
}
|
1079
1314
|
|
1080
1315
|
@ws.send(data.to_json)
|