discordrb 2.1.3 → 3.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/.gitignore +1 -0
- data/CHANGELOG.md +76 -0
- data/README.md +4 -2
- data/discordrb.gemspec +1 -1
- data/examples/commands.rb +2 -0
- data/examples/data/music.dca +0 -0
- data/examples/data/music.mp3 +0 -0
- data/examples/eval.rb +6 -3
- data/examples/ping.rb +14 -1
- data/examples/ping_with_respond_time.rb +6 -4
- data/examples/pm_send.rb +3 -0
- data/examples/shutdown.rb +7 -2
- data/examples/voice_send.rb +51 -0
- data/lib/discordrb/api.rb +66 -460
- data/lib/discordrb/api/channel.rb +306 -0
- data/lib/discordrb/api/invite.rb +41 -0
- data/lib/discordrb/api/server.rb +357 -0
- data/lib/discordrb/api/user.rb +134 -0
- data/lib/discordrb/bot.rb +266 -576
- data/lib/discordrb/cache.rb +27 -28
- data/lib/discordrb/commands/command_bot.rb +44 -15
- data/lib/discordrb/commands/container.rb +3 -2
- data/lib/discordrb/commands/parser.rb +14 -6
- data/lib/discordrb/container.rb +30 -3
- data/lib/discordrb/data.rb +823 -189
- data/lib/discordrb/errors.rb +145 -0
- data/lib/discordrb/events/channels.rb +63 -3
- data/lib/discordrb/events/members.rb +1 -2
- data/lib/discordrb/events/message.rb +96 -17
- data/lib/discordrb/events/presence.rb +15 -0
- data/lib/discordrb/events/typing.rb +7 -1
- data/lib/discordrb/gateway.rb +724 -0
- data/lib/discordrb/light/light_bot.rb +6 -4
- data/lib/discordrb/logger.rb +26 -9
- data/lib/discordrb/permissions.rb +6 -3
- data/lib/discordrb/version.rb +1 -1
- data/lib/discordrb/voice/voice_bot.rb +29 -6
- metadata +12 -5
- data/lib/discordrb/token_cache.rb +0 -181
@@ -0,0 +1,134 @@
|
|
1
|
+
# API calls for User object
|
2
|
+
module Discordrb::API::User
|
3
|
+
module_function
|
4
|
+
|
5
|
+
# Returns users based on a query
|
6
|
+
# https://discordapp.com/developers/docs/resources/user#query-users
|
7
|
+
def query(token, query, limit = nil)
|
8
|
+
Discordrb::API.request(
|
9
|
+
:users,
|
10
|
+
nil,
|
11
|
+
:get,
|
12
|
+
"#{Discordrb::API.api_base}/users?q=#{query}#{"&limit=#{limit}" if limit}",
|
13
|
+
Authorization: token
|
14
|
+
)
|
15
|
+
end
|
16
|
+
|
17
|
+
# Get user data
|
18
|
+
# https://discordapp.com/developers/docs/resources/user#get-user
|
19
|
+
def resolve(token, user_id)
|
20
|
+
Discordrb::API.request(
|
21
|
+
:users_uid,
|
22
|
+
nil,
|
23
|
+
:get,
|
24
|
+
"#{Discordrb::API.api_base}/users/#{user_id}",
|
25
|
+
Authorization: token
|
26
|
+
)
|
27
|
+
end
|
28
|
+
|
29
|
+
# Get profile data
|
30
|
+
# https://discordapp.com/developers/docs/resources/user#get-current-user
|
31
|
+
def profile(token)
|
32
|
+
Discordrb::API.request(
|
33
|
+
:users_me,
|
34
|
+
nil,
|
35
|
+
:get,
|
36
|
+
"#{Discordrb::API.api_base}/users/@me",
|
37
|
+
Authorization: token
|
38
|
+
)
|
39
|
+
end
|
40
|
+
|
41
|
+
# Change the current bot's nickname on a server
|
42
|
+
def change_own_nickname(token, server_id, nick)
|
43
|
+
Discordrb::API.request(
|
44
|
+
:guilds_sid_members_me_nick,
|
45
|
+
server_id, # This is technically a guild endpoint
|
46
|
+
:patch,
|
47
|
+
"#{Discordrb::API.api_base}/guilds/#{server_id}/members/@me/nick",
|
48
|
+
{ nick: nick }.to_json,
|
49
|
+
Authorization: token,
|
50
|
+
content_type: :json
|
51
|
+
)
|
52
|
+
end
|
53
|
+
|
54
|
+
# Update user data
|
55
|
+
# https://discordapp.com/developers/docs/resources/user#modify-current-user
|
56
|
+
def update_profile(token, email, password, new_username, avatar, new_password = nil)
|
57
|
+
Discordrb::API.request(
|
58
|
+
:users_me,
|
59
|
+
nil,
|
60
|
+
:patch,
|
61
|
+
"#{Discordrb::API.api_base}/users/@me",
|
62
|
+
{ avatar: avatar, email: email, new_password: new_password, password: password, username: new_username }.to_json,
|
63
|
+
Authorization: token,
|
64
|
+
content_type: :json
|
65
|
+
)
|
66
|
+
end
|
67
|
+
|
68
|
+
# Get the servers a user is connected to
|
69
|
+
# https://discordapp.com/developers/docs/resources/user#get-current-user-guilds
|
70
|
+
def servers(token)
|
71
|
+
Discordrb::API.request(
|
72
|
+
:users_me_guilds,
|
73
|
+
nil,
|
74
|
+
:get,
|
75
|
+
"#{Discordrb::API.api_base}/users/@me/guilds",
|
76
|
+
Authorization: token
|
77
|
+
)
|
78
|
+
end
|
79
|
+
|
80
|
+
# Leave a server
|
81
|
+
# https://discordapp.com/developers/docs/resources/user#leave-guild
|
82
|
+
def leave_server(token, server_id)
|
83
|
+
Discordrb::API.request(
|
84
|
+
:users_me_guilds_sid,
|
85
|
+
nil,
|
86
|
+
:delete,
|
87
|
+
"#{Discordrb::API.api_base}/users/@me/guilds/#{server_id}",
|
88
|
+
Authorization: token
|
89
|
+
)
|
90
|
+
end
|
91
|
+
|
92
|
+
# Get the DMs for the current user
|
93
|
+
# https://discordapp.com/developers/docs/resources/user#get-user-dms
|
94
|
+
def user_dms(token)
|
95
|
+
Discordrb::API.request(
|
96
|
+
:users_me_channels,
|
97
|
+
nil,
|
98
|
+
:get,
|
99
|
+
"#{Discordrb::API.api_base}/users/@me/channels",
|
100
|
+
Authorization: token
|
101
|
+
)
|
102
|
+
end
|
103
|
+
|
104
|
+
# Create a DM to another user
|
105
|
+
# https://discordapp.com/developers/docs/resources/user#create-dm
|
106
|
+
def create_pm(token, recipient_id)
|
107
|
+
Discordrb::API.request(
|
108
|
+
:users_me_channels,
|
109
|
+
nil,
|
110
|
+
:post,
|
111
|
+
"#{Discordrb::API.api_base}/users/@me/channels",
|
112
|
+
{ recipient_id: recipient_id }.to_json,
|
113
|
+
Authorization: token,
|
114
|
+
content_type: :json
|
115
|
+
)
|
116
|
+
end
|
117
|
+
|
118
|
+
# Get information about a user's connections
|
119
|
+
# https://discordapp.com/developers/docs/resources/user#get-users-connections
|
120
|
+
def connections(token)
|
121
|
+
Discordrb::API.request(
|
122
|
+
:users_me_connections,
|
123
|
+
nil,
|
124
|
+
:get,
|
125
|
+
"#{Discordrb::API.api_base}/users/@me/connections",
|
126
|
+
Authorization: token
|
127
|
+
)
|
128
|
+
end
|
129
|
+
|
130
|
+
# Make an avatar URL from the user and avatar IDs
|
131
|
+
def avatar_url(user_id, avatar_id)
|
132
|
+
"#{Discordrb::API.api_base}/users/#{user_id}/avatars/#{avatar_id}.jpg"
|
133
|
+
end
|
134
|
+
end
|
data/lib/discordrb/bot.rb
CHANGED
@@ -17,78 +17,22 @@ require 'discordrb/events/await'
|
|
17
17
|
require 'discordrb/events/bans'
|
18
18
|
|
19
19
|
require 'discordrb/api'
|
20
|
+
require 'discordrb/api/channel'
|
21
|
+
require 'discordrb/api/server'
|
22
|
+
require 'discordrb/api/invite'
|
20
23
|
require 'discordrb/errors'
|
21
24
|
require 'discordrb/data'
|
22
25
|
require 'discordrb/await'
|
23
|
-
require 'discordrb/token_cache'
|
24
26
|
require 'discordrb/container'
|
25
27
|
require 'discordrb/websocket'
|
26
28
|
require 'discordrb/cache'
|
29
|
+
require 'discordrb/gateway'
|
27
30
|
|
28
31
|
require 'discordrb/voice/voice_bot'
|
29
32
|
|
30
33
|
module Discordrb
|
31
|
-
# Gateway packet opcodes
|
32
|
-
module Opcodes
|
33
|
-
# **Received** when Discord dispatches an event to the gateway (like MESSAGE_CREATE, PRESENCE_UPDATE or whatever).
|
34
|
-
# The vast majority of received packets will have this opcode.
|
35
|
-
DISPATCH = 0
|
36
|
-
|
37
|
-
# **Two-way**: The client has to send a packet with this opcode every ~40 seconds (actual interval specified in
|
38
|
-
# READY or RESUMED) and the current sequence number, otherwise it will be disconnected from the gateway. In certain
|
39
|
-
# cases Discord may also send one, specifically if two clients are connected at once.
|
40
|
-
HEARTBEAT = 1
|
41
|
-
|
42
|
-
# **Sent**: This is one of the two possible ways to initiate a session after connecting to the gateway. It
|
43
|
-
# should contain the authentication token along with other stuff the server has to know right from the start, such
|
44
|
-
# as large_threshold and, for older gateway versions, the desired version.
|
45
|
-
IDENTIFY = 2
|
46
|
-
|
47
|
-
# **Sent**: Packets with this opcode are used to change the user's status and played game. (Sending this is never
|
48
|
-
# necessary for a gateway client to behave correctly)
|
49
|
-
PRESENCE = 3
|
50
|
-
|
51
|
-
# **Sent**: Packets with this opcode are used to change a user's voice state (mute/deaf/unmute/undeaf/etc.). It is
|
52
|
-
# also used to connect to a voice server in the first place. (Sending this is never necessary for a gateway client
|
53
|
-
# to behave correctly)
|
54
|
-
VOICE_STATE = 4
|
55
|
-
|
56
|
-
# **Sent**: This opcode is used to ping a voice server, whatever that means. The functionality of this opcode isn't
|
57
|
-
# known well but non-user clients should never send it.
|
58
|
-
VOICE_PING = 5
|
59
|
-
|
60
|
-
# **Sent**: This is the other of two possible ways to initiate a gateway session (other than {IDENTIFY}). Rather
|
61
|
-
# than starting an entirely new session, it resumes an existing session by replaying all events from a given
|
62
|
-
# sequence number. It should be used to recover from a connection error or anything like that when the session is
|
63
|
-
# still valid - sending this with an invalid session will cause an error to occur.
|
64
|
-
RESUME = 6
|
65
|
-
|
66
|
-
# **Received**: Discord sends this opcode to indicate that the client should reconnect to a different gateway
|
67
|
-
# server because the old one is currently being decommissioned. Counterintuitively, this opcode also invalidates the
|
68
|
-
# session - the client has to create an entirely new session with the new gateway instead of resuming the old one.
|
69
|
-
RECONNECT = 7
|
70
|
-
|
71
|
-
# **Sent**: This opcode identifies packets used to retrieve a list of members from a particular server. There is
|
72
|
-
# also a REST endpoint available for this, but it is inconvenient to use because the client has to implement
|
73
|
-
# pagination itself, whereas sending this opcode lets Discord handle the pagination and the client can just add
|
74
|
-
# members when it receives them. (Sending this is never necessary for a gateway client to behave correctly)
|
75
|
-
REQUEST_MEMBERS = 8
|
76
|
-
|
77
|
-
# **Received**: The functionality of this opcode is less known than the others but it appears to specifically
|
78
|
-
# tell the client to invalidate its local session and continue by {IDENTIFY}ing.
|
79
|
-
INVALIDATE_SESSION = 9
|
80
|
-
end
|
81
|
-
|
82
34
|
# Represents a Discord bot, including servers, users, etc.
|
83
35
|
class Bot
|
84
|
-
# The list of users the bot shares a server with.
|
85
|
-
# @return [Hash<Integer => User>] The users by ID.
|
86
|
-
attr_reader :users
|
87
|
-
|
88
|
-
# The list of servers the bot is currently in.
|
89
|
-
# @return [Hash<Integer => Server>] The servers by ID.
|
90
|
-
attr_reader :servers
|
91
|
-
|
92
36
|
# The list of currently running threads used to parse and call events.
|
93
37
|
# The threads will have a local variable `:discordrb_name` in the format of `et-1234`, where
|
94
38
|
# "et" stands for "event thread" and the number is a continually incrementing number representing
|
@@ -96,12 +40,6 @@ module Discordrb
|
|
96
40
|
# @return [Array<Thread>] The threads.
|
97
41
|
attr_reader :event_threads
|
98
42
|
|
99
|
-
# The bot's user profile. This special user object can be used
|
100
|
-
# to edit user data like the current username (see {Profile#username=}).
|
101
|
-
# @return [Profile] The bot's profile that can be used to edit data.
|
102
|
-
attr_reader :profile
|
103
|
-
alias_method :bot_user, :profile
|
104
|
-
|
105
43
|
# Whether or not the bot should parse its own messages. Off by default.
|
106
44
|
attr_accessor :should_parse_self
|
107
45
|
|
@@ -115,33 +53,30 @@ module Discordrb
|
|
115
53
|
# @return [Hash<Symbol => Await>] the list of registered {Await}s.
|
116
54
|
attr_reader :awaits
|
117
55
|
|
56
|
+
# The gateway connection is an internal detail that is useless to most people. It is however essential while
|
57
|
+
# debugging or developing discordrb itself, or while writing very custom bots.
|
58
|
+
# @return [Gateway] the underlying {Gateway} object.
|
59
|
+
attr_reader :gateway
|
60
|
+
|
118
61
|
include EventContainer
|
119
62
|
include Cache
|
120
63
|
|
121
64
|
# Makes a new bot with the given authentication data. It will be ready to be added event handlers to and can
|
122
65
|
# eventually be run with {#run}.
|
123
66
|
#
|
124
|
-
#
|
125
|
-
#
|
126
|
-
# information are valid:
|
127
|
-
# * token + application_id (bot account)
|
128
|
-
# * email + password (user account)
|
129
|
-
# * email + password + token (user account; the given token will be used for authentication instead of email
|
130
|
-
# and password)
|
67
|
+
# As support for logging in using username and password has been removed in version 3.0.0, only a token login is
|
68
|
+
# possible. Be sure to specify the `type` parameter as `:user` if you're logging in as a user.
|
131
69
|
#
|
132
70
|
# Simply creating a bot won't be enough to start sending messages etc. with, only a limited set of methods can
|
133
71
|
# be used after logging in. If you want to do something when the bot has connected successfully, either do it in the
|
134
72
|
# {#ready} event, or use the {#run} method with the :async parameter and do the processing after that.
|
135
|
-
# @param email [String] The email for your (or the bot's) Discord account.
|
136
|
-
# @param password [String] The valid password that should be used to log in to the account.
|
137
73
|
# @param log_mode [Symbol] The mode this bot should use for logging. See {Logger#mode=} for a list of modes.
|
138
74
|
# @param token [String] The token that should be used to log in. If your bot is a bot account, you have to specify
|
139
75
|
# this. If you're logging in as a user, make sure to also set the account type to :user so discordrb doesn't think
|
140
76
|
# you're trying to log in as a bot.
|
141
|
-
# @param
|
142
|
-
# @param type [Symbol] This parameter lets you manually overwrite the account type.
|
143
|
-
#
|
144
|
-
# as a user but only with a token. Valid values are :user and :bot.
|
77
|
+
# @param client_id [Integer] If you're logging in as a bot, the bot's client ID.
|
78
|
+
# @param type [Symbol] This parameter lets you manually overwrite the account type. This needs to be set when
|
79
|
+
# logging in as a user, otherwise discordrb will treat you as a bot account. Valid values are `:user` and `:bot`.
|
145
80
|
# @param name [String] Your bot's name. This will be sent to Discord with any API requests, who will use this to
|
146
81
|
# trace the source of excessive API requests; it's recommended to set this to something if you make bots that many
|
147
82
|
# people will host on their servers separately.
|
@@ -155,16 +90,13 @@ module Discordrb
|
|
155
90
|
# https://github.com/hammerandchisel/discord-api-docs/issues/17 for how to do sharding.
|
156
91
|
# @param num_shards [Integer] The total number of shards that should be running. See
|
157
92
|
# https://github.com/hammerandchisel/discord-api-docs/issues/17 for how to do sharding.
|
93
|
+
# @param redact_token [true, false] Whether the bot should redact the token in logs. Default is true.
|
158
94
|
def initialize(
|
159
|
-
|
160
|
-
token: nil, application_id: nil,
|
95
|
+
log_mode: :normal,
|
96
|
+
token: nil, client_id: nil, application_id: nil,
|
161
97
|
type: nil, name: '', fancy_log: false, suppress_ready: false, parse_self: false,
|
162
|
-
shard_id: nil, num_shards: nil
|
163
|
-
|
164
|
-
if email.is_a?(String) && email.end_with?('example.com')
|
165
|
-
puts 'You have to replace the login details in the example files with your own!'
|
166
|
-
exit
|
167
|
-
end
|
98
|
+
shard_id: nil, num_shards: nil, redact_token: true
|
99
|
+
)
|
168
100
|
|
169
101
|
LOGGER.mode = if log_mode.is_a? TrueClass # Specifically check for `true` because people might not have updated yet
|
170
102
|
:debug
|
@@ -172,15 +104,17 @@ module Discordrb
|
|
172
104
|
log_mode
|
173
105
|
end
|
174
106
|
|
175
|
-
|
107
|
+
LOGGER.token = token if redact_token
|
176
108
|
|
177
|
-
@
|
178
|
-
@password = password
|
109
|
+
@should_parse_self = parse_self
|
179
110
|
|
180
|
-
|
111
|
+
if application_id
|
112
|
+
raise ArgumentError, 'Starting with discordrb 3.0.0, the application_id parameter has been renamed to client_id! Make sure to change this in your bot. This check will be removed in 3.1.0.'
|
113
|
+
end
|
181
114
|
|
182
|
-
@
|
115
|
+
@client_id = client_id
|
183
116
|
|
117
|
+
@type = type || :bot
|
184
118
|
@name = name
|
185
119
|
|
186
120
|
@shard_key = num_shards ? [shard_id, num_shards] : nil
|
@@ -188,10 +122,8 @@ module Discordrb
|
|
188
122
|
LOGGER.fancy = fancy_log
|
189
123
|
@prevent_ready = suppress_ready
|
190
124
|
|
191
|
-
|
192
|
-
|
193
|
-
debug('Token cache created successfully')
|
194
|
-
@token = login(type, email, password, token, token_cache)
|
125
|
+
@token = process_token(@type, token)
|
126
|
+
@gateway = Gateway.new(self, @token)
|
195
127
|
|
196
128
|
init_cache
|
197
129
|
|
@@ -202,8 +134,45 @@ module Discordrb
|
|
202
134
|
|
203
135
|
@event_threads = []
|
204
136
|
@current_thread = 0
|
137
|
+
|
138
|
+
@status = :online
|
139
|
+
end
|
140
|
+
|
141
|
+
# The list of users the bot shares a server with.
|
142
|
+
# @return [Hash<Integer => User>] The users by ID.
|
143
|
+
def users
|
144
|
+
gateway_check
|
145
|
+
@users
|
146
|
+
end
|
147
|
+
|
148
|
+
# The list of servers the bot is currently in.
|
149
|
+
# @return [Hash<Integer => Server>] The servers by ID.
|
150
|
+
def servers
|
151
|
+
gateway_check
|
152
|
+
@servers
|
205
153
|
end
|
206
154
|
|
155
|
+
# The bot's user profile. This special user object can be used
|
156
|
+
# to edit user data like the current username (see {Profile#username=}).
|
157
|
+
# @return [Profile] The bot's profile that can be used to edit data.
|
158
|
+
def profile
|
159
|
+
gateway_check
|
160
|
+
@profile
|
161
|
+
end
|
162
|
+
|
163
|
+
alias_method :bot_user, :profile
|
164
|
+
|
165
|
+
# The bot's OAuth application.
|
166
|
+
# @return [Application, nil] The bot's application info. Returns `nil` if bot is not a bot account.
|
167
|
+
def bot_application
|
168
|
+
gateway_check
|
169
|
+
return nil unless @type == :bot
|
170
|
+
response = API.oauth_application(token)
|
171
|
+
Application.new(JSON.parse(response), self)
|
172
|
+
end
|
173
|
+
|
174
|
+
alias_method :bot_app, :bot_application
|
175
|
+
|
207
176
|
# The Discord API token received when logging in. Useful to explicitly call
|
208
177
|
# {API} methods.
|
209
178
|
# @return [String] The API token.
|
@@ -225,85 +194,49 @@ module Discordrb
|
|
225
194
|
# If the bot is run in async mode, make sure to eventually run {#sync} so
|
226
195
|
# the script doesn't stop prematurely.
|
227
196
|
def run(async = false)
|
228
|
-
run_async
|
197
|
+
@gateway.run_async
|
229
198
|
return if async
|
230
199
|
|
231
200
|
debug('Oh wait! Not exiting yet as run was run synchronously.')
|
232
|
-
sync
|
233
|
-
end
|
234
|
-
|
235
|
-
# Runs the bot asynchronously. Equivalent to #run with the :async parameter.
|
236
|
-
# @see #run
|
237
|
-
def run_async
|
238
|
-
# Handle heartbeats
|
239
|
-
@heartbeat_interval = 1
|
240
|
-
@heartbeat_active = false
|
241
|
-
@heartbeat_thread = Thread.new do
|
242
|
-
Thread.current[:discordrb_name] = 'heartbeat'
|
243
|
-
loop do
|
244
|
-
if @heartbeat_active
|
245
|
-
send_heartbeat
|
246
|
-
sleep @heartbeat_interval
|
247
|
-
else
|
248
|
-
sleep 1
|
249
|
-
end
|
250
|
-
end
|
251
|
-
end
|
252
|
-
|
253
|
-
@ws_thread = Thread.new do
|
254
|
-
Thread.current[:discordrb_name] = 'websocket'
|
255
|
-
|
256
|
-
# Initialize falloff so we wait for more time before reconnecting each time
|
257
|
-
@falloff = 1.0
|
258
|
-
|
259
|
-
loop do
|
260
|
-
websocket_connect
|
261
|
-
|
262
|
-
if @reconnect_url
|
263
|
-
# We got an op 7! Don't wait before reconnecting
|
264
|
-
LOGGER.info('Got an op 7, reconnecting right away')
|
265
|
-
else
|
266
|
-
wait_for_reconnect
|
267
|
-
end
|
268
|
-
|
269
|
-
# Restart the loop, i. e. reconnect
|
270
|
-
end
|
271
|
-
|
272
|
-
LOGGER.warn('The WS loop exited! Not sure if this is a good thing')
|
273
|
-
end
|
274
|
-
|
275
|
-
debug('WS thread created! Now waiting for confirmation that everything worked')
|
276
|
-
@ws_success = false
|
277
|
-
sleep(0.5) until @ws_success
|
278
|
-
debug('Confirmation received! Exiting run.')
|
201
|
+
@gateway.sync
|
279
202
|
end
|
280
203
|
|
281
|
-
#
|
204
|
+
# Blocks execution until the websocket stops, which should only happen manually triggered
|
205
|
+
# or due to an error. This is necessary to have a continuously running bot.
|
282
206
|
def sync
|
283
|
-
@
|
207
|
+
@gateway.sync
|
208
|
+
end
|
209
|
+
|
210
|
+
# Stops the bot gracefully, disconnecting the websocket without immediately killing the thread. This means that
|
211
|
+
# Discord is immediately aware of the closed connection and makes the bot appear offline instantly.
|
212
|
+
# @param no_sync [true, false] Whether or not to disable use of synchronize in the close method. This should be true if called from a trap context.
|
213
|
+
def stop(no_sync = false)
|
214
|
+
@gateway.stop(no_sync)
|
284
215
|
end
|
285
216
|
|
286
|
-
#
|
287
|
-
def
|
288
|
-
@
|
217
|
+
# @return [true, false] whether or not the bot is currently connected to Discord.
|
218
|
+
def connected?
|
219
|
+
@gateway.open?
|
289
220
|
end
|
290
221
|
|
291
222
|
# Makes the bot join an invite to a server.
|
292
223
|
# @param invite [String, Invite] The invite to join. For possible formats see {#resolve_invite_code}.
|
293
224
|
def join(invite)
|
294
225
|
resolved = invite(invite).code
|
295
|
-
API.
|
226
|
+
API::Invite.accept(token, resolved)
|
296
227
|
end
|
297
228
|
|
298
229
|
# Creates an OAuth invite URL that can be used to invite this bot to a particular server.
|
299
230
|
# Requires the application ID to have been set during initialization.
|
300
231
|
# @param server [Server, nil] The server the bot should be invited to, or nil if a general invite should be created.
|
232
|
+
# @param permission_bits [Integer, String] Permission bits that should be appended to invite url.
|
301
233
|
# @return [String] the OAuth invite URL.
|
302
|
-
def invite_url(server
|
303
|
-
raise 'No application ID has been set during initialization! Add one as the `application_id` named parameter while creating your bot.' unless @
|
234
|
+
def invite_url(server: nil, permission_bits: nil)
|
235
|
+
raise 'No application ID has been set during initialization! Add one as the `application_id` named parameter while creating your bot.' unless @client_id
|
304
236
|
|
305
|
-
|
306
|
-
"
|
237
|
+
server_id_str = server ? "&guild_id=#{server.id}" : ''
|
238
|
+
permission_bits_str = permission_bits ? "&permissions=#{permission_bits}" : ''
|
239
|
+
"https://discordapp.com/oauth2/authorize?&client_id=#{@client_id}#{server_id_str}#{permission_bits_str}&scope=bot"
|
307
240
|
end
|
308
241
|
|
309
242
|
# @return [Hash<Integer => VoiceBot>] the voice connections this bot currently has, by the server ID to which they are connected.
|
@@ -347,19 +280,9 @@ module Discordrb
|
|
347
280
|
|
348
281
|
debug("Got voice channel: #{chan}")
|
349
282
|
|
350
|
-
data = {
|
351
|
-
op: Opcodes::VOICE_STATE,
|
352
|
-
d: {
|
353
|
-
guild_id: server_id.to_s,
|
354
|
-
channel_id: chan.id.to_s,
|
355
|
-
self_mute: false,
|
356
|
-
self_deaf: false
|
357
|
-
}
|
358
|
-
}
|
359
|
-
debug("Voice channel init packet is: #{data.to_json}")
|
360
|
-
|
361
283
|
@should_connect_to_voice[server_id] = chan
|
362
|
-
@
|
284
|
+
@gateway.send_voice_state_update(server_id.to_s, chan.id.to_s, false, false)
|
285
|
+
|
363
286
|
debug('Voice channel init packet sent! Now waiting.')
|
364
287
|
|
365
288
|
sleep(0.05) until @voices[server_id]
|
@@ -373,19 +296,7 @@ module Discordrb
|
|
373
296
|
# @param destroy_vws [true, false] Whether or not the VWS should also be destroyed. If you're calling this method
|
374
297
|
# directly, you should leave it as true.
|
375
298
|
def voice_destroy(server_id, destroy_vws = true)
|
376
|
-
|
377
|
-
op: Opcodes::VOICE_STATE,
|
378
|
-
d: {
|
379
|
-
guild_id: server_id.to_s,
|
380
|
-
channel_id: nil,
|
381
|
-
self_mute: false,
|
382
|
-
self_deaf: false
|
383
|
-
}
|
384
|
-
}
|
385
|
-
|
386
|
-
debug("Voice channel destroy packet is: #{data.to_json}")
|
387
|
-
@ws.send(data.to_json)
|
388
|
-
|
299
|
+
@gateway.send_voice_state_update(server_id.to_s, nil, false, false)
|
389
300
|
@voices[server_id].destroy if @voices[server_id] && destroy_vws
|
390
301
|
@voices.delete(server_id)
|
391
302
|
end
|
@@ -395,28 +306,50 @@ module Discordrb
|
|
395
306
|
# @param code [String, Invite] The invite to revoke. For possible formats see {#resolve_invite_code}.
|
396
307
|
def delete_invite(code)
|
397
308
|
invite = resolve_invite_code(code)
|
398
|
-
API.
|
309
|
+
API::Invite.delete(token, invite)
|
399
310
|
end
|
400
311
|
|
401
312
|
# Sends a text message to a channel given its ID and the message's content.
|
402
313
|
# @param channel_id [Integer] The ID that identifies the channel to send something to.
|
403
314
|
# @param content [String] The text that should be sent as a message. It is limited to 2000 characters (Discord imposed).
|
404
315
|
# @param tts [true, false] Whether or not this message should be sent using Discord text-to-speech.
|
316
|
+
# @param server_id [Integer] The ID that identifies the server to send something to.
|
405
317
|
# @return [Message] The message that was sent.
|
406
318
|
def send_message(channel_id, content, tts = false, server_id = nil)
|
407
319
|
channel_id = channel_id.resolve_id
|
408
320
|
debug("Sending message to #{channel_id} with content '#{content}'")
|
409
321
|
|
410
|
-
response = API.
|
322
|
+
response = API::Channel.create_message(token, channel_id, content, [], tts, server_id)
|
411
323
|
Message.new(JSON.parse(response), self)
|
412
324
|
end
|
413
325
|
|
326
|
+
# Sends a text message to a channel given its ID and the message's content,
|
327
|
+
# then deletes it after the specified timeout in seconds.
|
328
|
+
# @param channel_id [Integer] The ID that identifies the channel to send something to.
|
329
|
+
# @param content [String] The text that should be sent as a message. It is limited to 2000 characters (Discord imposed).
|
330
|
+
# @param timeout [Float] The amount of time in seconds after which the message sent will be deleted.
|
331
|
+
# @param tts [true, false] Whether or not this message should be sent using Discord text-to-speech.
|
332
|
+
# @param server_id [Integer] The ID that identifies the server to send something to.
|
333
|
+
def send_temporary_message(channel_id, content, timeout, tts = false, server_id = nil)
|
334
|
+
Thread.new do
|
335
|
+
message = send_message(channel_id, content, tts, server_id)
|
336
|
+
|
337
|
+
sleep(timeout)
|
338
|
+
|
339
|
+
message.delete
|
340
|
+
end
|
341
|
+
|
342
|
+
nil
|
343
|
+
end
|
344
|
+
|
414
345
|
# Sends a file to a channel. If it is an image, it will automatically be embedded.
|
415
346
|
# @note This executes in a blocking way, so if you're sending long files, be wary of delays.
|
416
347
|
# @param channel_id [Integer] The ID that identifies the channel to send something to.
|
417
348
|
# @param file [File] The file that should be sent.
|
418
|
-
|
419
|
-
|
349
|
+
# @param caption [string] The caption for the file.
|
350
|
+
# @param tts [true, false] Whether or not this file's caption should be sent using Discord text-to-speech.
|
351
|
+
def send_file(channel_id, file, caption: nil, tts: false)
|
352
|
+
response = API::Channel.upload_file(token, channel_id, file, caption: caption, tts: tts)
|
420
353
|
Message.new(JSON.parse(response), self)
|
421
354
|
end
|
422
355
|
|
@@ -437,7 +370,7 @@ module Discordrb
|
|
437
370
|
# * `:sydney`
|
438
371
|
# @return [Server] The server that was created.
|
439
372
|
def create_server(name, region = :london)
|
440
|
-
response = API.
|
373
|
+
response = API::Server.create(token, name, region)
|
441
374
|
id = JSON.parse(response)['id'].to_i
|
442
375
|
sleep 0.1 until @servers[id]
|
443
376
|
server = @servers[id]
|
@@ -474,43 +407,70 @@ module Discordrb
|
|
474
407
|
user(id.to_i)
|
475
408
|
end
|
476
409
|
|
410
|
+
# Updates presence status.
|
411
|
+
# @param status [String] The status the bot should show up as.
|
412
|
+
# @param game [String, nil] The name of the game to be played/stream name on the stream.
|
413
|
+
# @param url [String, nil] The Twitch URL to display as a stream. nil for no stream.
|
414
|
+
# @param since [Integer] When this status was set.
|
415
|
+
# @param afk [true, false] Whether the bot is AFK.
|
416
|
+
# @see Gateway#send_status_update
|
417
|
+
def update_status(status, game, url, since = 0, afk = false)
|
418
|
+
gateway_check
|
419
|
+
|
420
|
+
@game = game
|
421
|
+
@status = status
|
422
|
+
@streamurl = url
|
423
|
+
type = url ? 1 : nil
|
424
|
+
|
425
|
+
game_obj = game || url ? { name: game, url: url, type: type } : nil
|
426
|
+
@gateway.send_status_update(status, since, game_obj, afk)
|
427
|
+
end
|
428
|
+
|
477
429
|
# Sets the currently playing game to the specified game.
|
478
430
|
# @param name [String] The name of the game to be played.
|
479
431
|
# @return [String] The game that is being played now.
|
480
432
|
def game=(name)
|
481
|
-
|
482
|
-
|
483
|
-
|
484
|
-
|
485
|
-
d: {
|
486
|
-
idle_since: nil,
|
487
|
-
game: name ? { name: name } : nil
|
488
|
-
}
|
489
|
-
}
|
433
|
+
gateway_check
|
434
|
+
update_status(@status, name, nil)
|
435
|
+
name
|
436
|
+
end
|
490
437
|
|
491
|
-
|
438
|
+
# Sets the currently online stream to the specified name and Twitch URL.
|
439
|
+
# @param name [String] The name of the stream to display.
|
440
|
+
# @param url [String] The url of the current Twitch stream.
|
441
|
+
# @return [String] The stream name that is being displayed now.
|
442
|
+
def stream(name, url)
|
443
|
+
gateway_check
|
444
|
+
update_status(@status, name, url)
|
492
445
|
name
|
493
446
|
end
|
494
447
|
|
495
|
-
#
|
496
|
-
|
497
|
-
|
498
|
-
|
499
|
-
|
500
|
-
|
501
|
-
|
502
|
-
|
503
|
-
|
504
|
-
|
505
|
-
|
448
|
+
# Sets status to online.
|
449
|
+
def online
|
450
|
+
gateway_check
|
451
|
+
update_status(:online, @game, @streamurl)
|
452
|
+
end
|
453
|
+
|
454
|
+
alias_method :on, :online
|
455
|
+
|
456
|
+
# Sets status to idle.
|
457
|
+
def idle
|
458
|
+
gateway_check
|
459
|
+
update_status(:idle, @game, nil)
|
460
|
+
end
|
461
|
+
|
462
|
+
alias_method :away, :idle
|
463
|
+
|
464
|
+
# Sets the bot's status to DnD (red icon).
|
465
|
+
def dnd
|
466
|
+
gateway_check
|
467
|
+
update_status(:dnd, @game, nil)
|
506
468
|
end
|
507
469
|
|
508
|
-
#
|
509
|
-
|
510
|
-
|
511
|
-
|
512
|
-
def inject_resume(seq)
|
513
|
-
resume(seq || @sequence, raw_token, @session_id)
|
470
|
+
# Sets the bot's status to invisible (appears offline).
|
471
|
+
def invisible
|
472
|
+
gateway_check
|
473
|
+
update_status(:invisible, @game, nil)
|
514
474
|
end
|
515
475
|
|
516
476
|
# Sets debug mode. If debug mode is on, many things will be outputted to STDOUT.
|
@@ -574,26 +534,29 @@ module Discordrb
|
|
574
534
|
LOGGER.log_exception(e)
|
575
535
|
end
|
576
536
|
|
577
|
-
|
537
|
+
# Dispatches an event to this bot. Called by the gateway connection handler used internally.
|
538
|
+
def dispatch(type, data)
|
539
|
+
handle_dispatch(type, data)
|
540
|
+
end
|
578
541
|
|
579
|
-
#
|
580
|
-
def
|
581
|
-
|
582
|
-
|
542
|
+
# Raises a heartbeat event. Called by the gateway connection handler used internally.
|
543
|
+
def raise_heartbeat_event
|
544
|
+
raise_event(HeartbeatEvent.new(self))
|
545
|
+
end
|
583
546
|
|
584
|
-
|
585
|
-
|
547
|
+
def prune_empty_groups
|
548
|
+
@channels.each_value do |channel|
|
549
|
+
channel.leave_group if channel.group? && channel.recipients.empty?
|
550
|
+
end
|
551
|
+
end
|
586
552
|
|
587
|
-
|
588
|
-
return :user if email || password
|
553
|
+
private
|
589
554
|
|
590
|
-
|
591
|
-
|
555
|
+
# Throws a useful exception if there's currently no gateway connection
|
556
|
+
def gateway_check
|
557
|
+
return if connected?
|
592
558
|
|
593
|
-
|
594
|
-
# to specify the application ID:
|
595
|
-
LOGGER.warn('The application ID is missing! Logging in as a bot will work but some OAuth-related functionality will be unavailable!')
|
596
|
-
:bot
|
559
|
+
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`."
|
597
560
|
end
|
598
561
|
|
599
562
|
### ## ## ######## ######## ######## ## ## ### ## ######
|
@@ -606,7 +569,7 @@ module Discordrb
|
|
606
569
|
|
607
570
|
# Internal handler for PRESENCE_UPDATE
|
608
571
|
def update_presence(data)
|
609
|
-
# Friends list presences have no
|
572
|
+
# Friends list presences have no server ID so ignore these to not cause an error
|
610
573
|
return unless data['guild_id']
|
611
574
|
|
612
575
|
user_id = data['user']['id'].to_i
|
@@ -642,28 +605,11 @@ module Discordrb
|
|
642
605
|
|
643
606
|
# Internal handler for VOICE_STATUS_UPDATE
|
644
607
|
def update_voice_state(data)
|
645
|
-
user_id = data['user_id'].to_i
|
646
608
|
server_id = data['guild_id'].to_i
|
647
609
|
server = server(server_id)
|
648
610
|
return unless server
|
649
611
|
|
650
|
-
|
651
|
-
|
652
|
-
unless user
|
653
|
-
warn "Invalid user for voice state update: #{user_id} on #{server_id}, ignoring"
|
654
|
-
return
|
655
|
-
end
|
656
|
-
|
657
|
-
channel_id = data['channel_id']
|
658
|
-
channel = nil
|
659
|
-
channel = self.channel(channel_id.to_i) if channel_id
|
660
|
-
|
661
|
-
user.update_voice_state(
|
662
|
-
channel,
|
663
|
-
data['mute'],
|
664
|
-
data['deaf'],
|
665
|
-
data['self_mute'],
|
666
|
-
data['self_deaf'])
|
612
|
+
server.update_voice_state(data)
|
667
613
|
|
668
614
|
@session_id = data['session_id']
|
669
615
|
end
|
@@ -699,8 +645,10 @@ module Discordrb
|
|
699
645
|
if server
|
700
646
|
server.channels << channel
|
701
647
|
@channels[channel.id] = channel
|
702
|
-
|
703
|
-
@
|
648
|
+
elsif channel.pm?
|
649
|
+
@pm_channels[channel.recipient.id] = channel
|
650
|
+
elsif channel.group?
|
651
|
+
@channels[channel.id] = channel
|
704
652
|
end
|
705
653
|
end
|
706
654
|
|
@@ -721,11 +669,33 @@ module Discordrb
|
|
721
669
|
if server
|
722
670
|
@channels.delete(channel.id)
|
723
671
|
server.channels.reject! { |c| c.id == channel.id }
|
724
|
-
|
725
|
-
@
|
672
|
+
elsif channel.pm?
|
673
|
+
@pm_channels.delete(channel.recipient.id)
|
674
|
+
elsif channel.group?
|
675
|
+
@channels.delete(channel.id)
|
726
676
|
end
|
727
677
|
end
|
728
678
|
|
679
|
+
# Internal handler for CHANNEL_RECIPIENT_ADD
|
680
|
+
def add_recipient(data)
|
681
|
+
channel_id = data['channel_id'].to_i
|
682
|
+
channel = self.channel(channel_id)
|
683
|
+
|
684
|
+
recipient_user = ensure_user(data['user'])
|
685
|
+
recipient = Recipient.new(recipient_user, channel, self)
|
686
|
+
channel.add_recipient(recipient)
|
687
|
+
end
|
688
|
+
|
689
|
+
# Internal handler for CHANNEL_RECIPIENT_REMOVE
|
690
|
+
def remove_recipient(data)
|
691
|
+
channel_id = data['channel_id'].to_i
|
692
|
+
channel = self.channel(channel_id)
|
693
|
+
|
694
|
+
recipient_user = ensure_user(data['user'])
|
695
|
+
recipient = Recipient.new(recipient_user, channel, self)
|
696
|
+
channel.remove_recipient(recipient)
|
697
|
+
end
|
698
|
+
|
729
699
|
# Internal handler for GUILD_MEMBER_ADD
|
730
700
|
def add_guild_member(data)
|
731
701
|
server_id = data['guild_id'].to_i
|
@@ -826,19 +796,6 @@ module Discordrb
|
|
826
796
|
## ## ## ## ## ## ## ###
|
827
797
|
######## ####### ###### #### ## ##
|
828
798
|
|
829
|
-
def login(type, email, password, token, token_cache)
|
830
|
-
# Don't bother with any login code if a token is already specified
|
831
|
-
return process_token(type, token) if token
|
832
|
-
|
833
|
-
# If a bot account attempts logging in without a token, throw an error
|
834
|
-
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
|
835
|
-
|
836
|
-
# If the type is not a user account at this point, it must be invalid
|
837
|
-
raise ArgumentError, 'Invalid type specified! Use either :bot or :user' if type == :user
|
838
|
-
|
839
|
-
user_login(email, password, token_cache)
|
840
|
-
end
|
841
|
-
|
842
799
|
def process_token(type, token)
|
843
800
|
# Remove the "Bot " prefix if it exists
|
844
801
|
token = token[4..-1] if token.start_with? 'Bot '
|
@@ -847,162 +804,7 @@ module Discordrb
|
|
847
804
|
token
|
848
805
|
end
|
849
806
|
|
850
|
-
def
|
851
|
-
debug('Logging in')
|
852
|
-
|
853
|
-
# Attempt to retrieve the token from the cache
|
854
|
-
retrieved_token = retrieve_token(email, password, token_cache)
|
855
|
-
return retrieved_token if retrieved_token
|
856
|
-
|
857
|
-
login_attempts ||= 0
|
858
|
-
|
859
|
-
# Login
|
860
|
-
login_response = JSON.parse(API.login(email, password))
|
861
|
-
token = login_response['token']
|
862
|
-
raise Discordrb::Errors::InvalidAuthenticationError unless token
|
863
|
-
debug('Received token from Discord!')
|
864
|
-
|
865
|
-
# Cache the token
|
866
|
-
token_cache.store_token(email, password, token)
|
867
|
-
|
868
|
-
token
|
869
|
-
rescue RestClient::BadRequest
|
870
|
-
raise Discordrb::Errors::InvalidAuthenticationError
|
871
|
-
rescue SocketError, RestClient::RequestFailed => e # RequestFailed handles the 52x error codes Cloudflare sometimes sends that aren't covered by specific RestClient classes
|
872
|
-
if login_attempts && login_attempts > 100
|
873
|
-
LOGGER.error("User login failed permanently after #{login_attempts} attempts")
|
874
|
-
raise
|
875
|
-
else
|
876
|
-
LOGGER.error("User login failed! Trying again in 5 seconds, #{100 - login_attempts} remaining")
|
877
|
-
LOGGER.log_exception(e)
|
878
|
-
retry
|
879
|
-
end
|
880
|
-
end
|
881
|
-
|
882
|
-
def retrieve_token(email, password, token_cache)
|
883
|
-
# First, attempt to get the token from the cache
|
884
|
-
token = token_cache.token(email, password)
|
885
|
-
debug('Token successfully obtained from cache!') if token
|
886
|
-
token
|
887
|
-
end
|
888
|
-
|
889
|
-
def find_gateway
|
890
|
-
# If the reconnect URL is set, it means we got an op 7 earlier and should reconnect to the new URL
|
891
|
-
if @reconnect_url
|
892
|
-
debug("Reconnecting to URL #{@reconnect_url}")
|
893
|
-
url = @reconnect_url
|
894
|
-
@reconnect_url = nil # Unset the URL so we don't connect to the same URL again if the connection fails
|
895
|
-
url
|
896
|
-
else
|
897
|
-
# Get the correct gateway URL from Discord
|
898
|
-
response = API.gateway(token)
|
899
|
-
JSON.parse(response)['url']
|
900
|
-
end
|
901
|
-
end
|
902
|
-
|
903
|
-
def process_gateway
|
904
|
-
raw_url = find_gateway
|
905
|
-
|
906
|
-
# Append a slash in case it's not there (I'm not sure how well WSCS handles it otherwise)
|
907
|
-
raw_url += '/' unless raw_url.end_with? '/'
|
908
|
-
|
909
|
-
# Add the parameters we want
|
910
|
-
raw_url + "?encoding=json&v=#{GATEWAY_VERSION}"
|
911
|
-
end
|
912
|
-
|
913
|
-
## ## ###### ######## ## ## ######## ## ## ######## ######
|
914
|
-
## ## ## ## ## ## ## ## ## ### ## ## ## ##
|
915
|
-
## ## ## ## ## ## ## ## #### ## ## ##
|
916
|
-
## ## ## ###### ###### ## ## ###### ## ## ## ## ######
|
917
|
-
## ## ## ## ## ## ## ## ## #### ## ##
|
918
|
-
## ## ## ## ## ## ## ## ## ## ### ## ## ##
|
919
|
-
#### ### ###### ######## ### ######## ## ## ## ######
|
920
|
-
|
921
|
-
# Desired gateway version
|
922
|
-
GATEWAY_VERSION = 4
|
923
|
-
|
924
|
-
def websocket_connect
|
925
|
-
debug('Attempting to get gateway URL...')
|
926
|
-
gateway_url = process_gateway
|
927
|
-
debug("Success! Gateway URL is #{gateway_url}.")
|
928
|
-
debug('Now running bot')
|
929
|
-
|
930
|
-
@ws = Discordrb::WebSocket.new(
|
931
|
-
gateway_url,
|
932
|
-
method(:websocket_open),
|
933
|
-
method(:websocket_message),
|
934
|
-
method(:websocket_close),
|
935
|
-
proc { |e| LOGGER.error "Gateway error: #{e}" }
|
936
|
-
)
|
937
|
-
|
938
|
-
@ws.thread[:discordrb_name] = 'gateway'
|
939
|
-
@ws.thread.join
|
940
|
-
rescue => e
|
941
|
-
LOGGER.error 'Error while connecting to the gateway!'
|
942
|
-
LOGGER.log_exception e
|
943
|
-
raise
|
944
|
-
end
|
945
|
-
|
946
|
-
def websocket_reconnect(url)
|
947
|
-
# In here, we do nothing except set the reconnect URL and close the current connection.
|
948
|
-
@reconnect_url = url
|
949
|
-
@ws.close
|
950
|
-
|
951
|
-
# Reset the packet sequence number so we don't try to resume the connection afterwards
|
952
|
-
@sequence = 0
|
953
|
-
|
954
|
-
# Let's hope the reconnect handler reconnects us correctly...
|
955
|
-
end
|
956
|
-
|
957
|
-
def websocket_message(event)
|
958
|
-
if event.byteslice(0) == 'x'
|
959
|
-
# The message is encrypted
|
960
|
-
event = Zlib::Inflate.inflate(event)
|
961
|
-
end
|
962
|
-
|
963
|
-
# Parse packet
|
964
|
-
packet = JSON.parse(event)
|
965
|
-
|
966
|
-
if @prevent_ready && packet['t'] == 'READY'
|
967
|
-
debug('READY packet was received and suppressed')
|
968
|
-
elsif @prevent_ready && packet['t'] == 'GUILD_MEMBERS_CHUNK'
|
969
|
-
# Ignore chunks as they will be handled later anyway
|
970
|
-
else
|
971
|
-
LOGGER.in(event.to_s)
|
972
|
-
end
|
973
|
-
|
974
|
-
opcode = packet['op'].to_i
|
975
|
-
|
976
|
-
if opcode == Opcodes::HEARTBEAT
|
977
|
-
# If Discord sends us a heartbeat, simply reply with a heartbeat with the packet's sequence number
|
978
|
-
@sequence = packet['s'].to_i
|
979
|
-
|
980
|
-
LOGGER.info("Received an op1 (seq: #{@sequence})! This means another client connected while this one is already running. Replying with the same seq")
|
981
|
-
send_heartbeat
|
982
|
-
|
983
|
-
return
|
984
|
-
end
|
985
|
-
|
986
|
-
if opcode == Opcodes::RECONNECT
|
987
|
-
websocket_reconnect(packet['d'] ? packet['d']['url'] : nil)
|
988
|
-
return
|
989
|
-
end
|
990
|
-
|
991
|
-
if opcode == Opcodes::INVALIDATE_SESSION
|
992
|
-
LOGGER.info "We got an opcode 9 from Discord! Invalidating the session. You probably don't have to worry about this."
|
993
|
-
invalidate_session
|
994
|
-
LOGGER.debug 'Session invalidated!'
|
995
|
-
|
996
|
-
LOGGER.debug 'Reconnecting with IDENTIFY'
|
997
|
-
websocket_open # Since we just invalidated the session, pretending we just opened the WS again will re-identify
|
998
|
-
LOGGER.debug "Re-identified! Let's hope everything works fine."
|
999
|
-
return
|
1000
|
-
end
|
1001
|
-
|
1002
|
-
raise "Got an unexpected opcode (#{opcode}) in a gateway event!
|
1003
|
-
Please report this issue along with the following information:
|
1004
|
-
v#{GATEWAY_VERSION} #{packet}" unless opcode == Opcodes::DISPATCH
|
1005
|
-
|
807
|
+
def handle_dispatch(type, data)
|
1006
808
|
# Check whether there are still unavailable servers and there have been more than 10 seconds since READY
|
1007
809
|
if @unavailable_servers && @unavailable_servers > 0 && (Time.now - @unavailable_timeout_time) > 10
|
1008
810
|
# The server streaming timed out!
|
@@ -1015,26 +817,14 @@ module Discordrb
|
|
1015
817
|
notify_ready
|
1016
818
|
end
|
1017
819
|
|
1018
|
-
# Keep track of the packet sequence (continually incrementing number for every packet) so we can resume a
|
1019
|
-
# connection if we disconnect
|
1020
|
-
@sequence = packet['s'].to_i
|
1021
|
-
|
1022
|
-
data = packet['d']
|
1023
|
-
type = packet['t'].intern
|
1024
820
|
case type
|
1025
821
|
when :READY
|
1026
|
-
|
822
|
+
# As READY may be called multiple times over a single process lifetime, we here need to reset the cache entirely
|
823
|
+
# to prevent possible inconsistencies, like objects referencing old versions of other objects which have been
|
824
|
+
# replaced.
|
825
|
+
init_cache
|
1027
826
|
|
1028
|
-
|
1029
|
-
@session_id = data['session_id']
|
1030
|
-
|
1031
|
-
# Activate the heartbeats
|
1032
|
-
@heartbeat_interval = data['heartbeat_interval'].to_f / 1000.0
|
1033
|
-
@heartbeat_active = true
|
1034
|
-
debug("Desired heartbeat_interval: #{@heartbeat_interval}")
|
1035
|
-
send_heartbeat
|
1036
|
-
|
1037
|
-
@profile = Profile.new(data['user'], self, @email, @password)
|
827
|
+
@profile = Profile.new(data['user'], self)
|
1038
828
|
|
1039
829
|
# Initialize servers
|
1040
830
|
@servers = {}
|
@@ -1055,33 +845,25 @@ module Discordrb
|
|
1055
845
|
ensure_server(element)
|
1056
846
|
end
|
1057
847
|
|
1058
|
-
# Add
|
848
|
+
# Add PM and group channels
|
1059
849
|
data['private_channels'].each do |element|
|
1060
850
|
channel = ensure_channel(element)
|
1061
|
-
|
851
|
+
if channel.pm?
|
852
|
+
@pm_channels[channel.recipient.id] = channel
|
853
|
+
else
|
854
|
+
@channels[channel.id] = channel
|
855
|
+
end
|
1062
856
|
end
|
1063
857
|
|
1064
858
|
# Don't notify yet if there are unavailable servers because they need to get available before the bot truly has
|
1065
859
|
# all the data
|
1066
|
-
if @unavailable_servers
|
860
|
+
if @unavailable_servers.zero?
|
1067
861
|
# No unavailable servers - we're ready!
|
1068
862
|
notify_ready
|
1069
863
|
end
|
1070
864
|
|
1071
865
|
@ready_time = Time.now
|
1072
866
|
@unavailable_timeout_time = Time.now
|
1073
|
-
when :RESUMED
|
1074
|
-
# The RESUMED event is received after a successful op 6 (resume). It does nothing except tell the bot the
|
1075
|
-
# connection is initiated (like READY would) and set a new heartbeat interval.
|
1076
|
-
debug('Connection resumed')
|
1077
|
-
|
1078
|
-
@heartbeat_interval = data['heartbeat_interval'].to_f / 1000.0
|
1079
|
-
|
1080
|
-
# Since we disabled it earlier so we don't send any heartbeats in between close and resume,, make sure to
|
1081
|
-
# re-enable heartbeating
|
1082
|
-
@heartbeat_active = true
|
1083
|
-
|
1084
|
-
debug("Desired heartbeat_interval: #{@heartbeat_interval}")
|
1085
867
|
when :GUILD_MEMBERS_CHUNK
|
1086
868
|
id = data['guild_id'].to_i
|
1087
869
|
server = server(id)
|
@@ -1128,6 +910,22 @@ module Discordrb
|
|
1128
910
|
|
1129
911
|
event = MessageDeleteEvent.new(data, self)
|
1130
912
|
raise_event(event)
|
913
|
+
when :MESSAGE_DELETE_BULK
|
914
|
+
debug("MESSAGE_DELETE_BULK will raise #{data['ids'].length} events")
|
915
|
+
|
916
|
+
data['ids'].each do |single_id|
|
917
|
+
# Form a data hash for a single ID so the methods get what they want
|
918
|
+
single_data = {
|
919
|
+
'id' => single_id,
|
920
|
+
'channel_id' => data['channel_id']
|
921
|
+
}
|
922
|
+
|
923
|
+
# Raise as normal
|
924
|
+
delete_message(single_data)
|
925
|
+
|
926
|
+
event = MessageDeleteEvent.new(single_data, self)
|
927
|
+
raise_event(event)
|
928
|
+
end
|
1131
929
|
when :TYPING_START
|
1132
930
|
start_typing(data)
|
1133
931
|
|
@@ -1177,6 +975,16 @@ module Discordrb
|
|
1177
975
|
|
1178
976
|
event = ChannelDeleteEvent.new(data, self)
|
1179
977
|
raise_event(event)
|
978
|
+
when :CHANNEL_RECIPIENT_ADD
|
979
|
+
add_recipient(data)
|
980
|
+
|
981
|
+
event = ChannelRecipientAddEvent.new(data, self)
|
982
|
+
raise_event(event)
|
983
|
+
when :CHANNEL_RECIPIENT_REMOVE
|
984
|
+
remove_recipient(data)
|
985
|
+
|
986
|
+
event = ChannelRecipientRemoveEvent.new(data, self)
|
987
|
+
raise_event(event)
|
1180
988
|
when :GUILD_MEMBER_ADD
|
1181
989
|
add_guild_member(data)
|
1182
990
|
|
@@ -1225,7 +1033,7 @@ module Discordrb
|
|
1225
1033
|
@unavailable_servers -= 1 if @unavailable_servers
|
1226
1034
|
@unavailable_timeout_time = Time.now
|
1227
1035
|
|
1228
|
-
notify_ready if @unavailable_servers
|
1036
|
+
notify_ready if @unavailable_servers.zero?
|
1229
1037
|
|
1230
1038
|
# Return here so the event doesn't get triggered
|
1231
1039
|
return
|
@@ -1241,147 +1049,29 @@ module Discordrb
|
|
1241
1049
|
when :GUILD_DELETE
|
1242
1050
|
delete_guild(data)
|
1243
1051
|
|
1052
|
+
if d['unavailable'].is_a? TrueClass
|
1053
|
+
LOGGER.warn("Server #{d['id']} is unavailable due to an outage!")
|
1054
|
+
return # Don't raise an event
|
1055
|
+
end
|
1056
|
+
|
1244
1057
|
event = ServerDeleteEvent.new(data, self)
|
1245
1058
|
raise_event(event)
|
1246
1059
|
else
|
1247
1060
|
# another event that we don't support yet
|
1248
|
-
debug "Event #{
|
1061
|
+
debug "Event #{type} has been received but is unsupported, ignoring"
|
1249
1062
|
end
|
1250
1063
|
rescue Exception => e
|
1251
1064
|
LOGGER.error('Gateway message error!')
|
1252
1065
|
log_exception(e)
|
1253
1066
|
end
|
1254
1067
|
|
1255
|
-
def websocket_close(event)
|
1256
|
-
# Don't handle nil events (for example if the disconnect came from our side)
|
1257
|
-
return unless event
|
1258
|
-
|
1259
|
-
# Handle actual close frames and errors separately
|
1260
|
-
if event.respond_to? :code
|
1261
|
-
LOGGER.error(%(Disconnected from WebSocket - code #{event.code} with reason: "#{event.data}"))
|
1262
|
-
|
1263
|
-
if event.code.to_i == 4006
|
1264
|
-
# If we got disconnected with a 4006, it means we sent a resume when Discord wanted an identify. To battle this,
|
1265
|
-
# we invalidate the local session so we'll just send an identify next time
|
1266
|
-
debug('Apparently we just sent the wrong type of initiation packet (resume rather than identify) to Discord. (Sorry!)
|
1267
|
-
Invalidating session so this is fixed next time')
|
1268
|
-
invalidate_session
|
1269
|
-
end
|
1270
|
-
else
|
1271
|
-
LOGGER.error('Disconnected from WebSocket due to an exception!')
|
1272
|
-
LOGGER.log_exception event
|
1273
|
-
end
|
1274
|
-
|
1275
|
-
raise_event(DisconnectEvent.new(self))
|
1276
|
-
|
1277
|
-
# Stop sending heartbeats
|
1278
|
-
@heartbeat_active = false
|
1279
|
-
|
1280
|
-
# Safely close the WS connection and handle any errors that occur there
|
1281
|
-
begin
|
1282
|
-
@ws.close
|
1283
|
-
rescue => e
|
1284
|
-
LOGGER.warn 'Got the following exception while closing the WS after being disconnected:'
|
1285
|
-
LOGGER.log_exception e
|
1286
|
-
end
|
1287
|
-
rescue => e
|
1288
|
-
LOGGER.log_exception e
|
1289
|
-
raise
|
1290
|
-
end
|
1291
|
-
|
1292
|
-
def websocket_open
|
1293
|
-
# If we've already received packets (packet sequence > 0) resume an existing connection instead of identifying anew
|
1294
|
-
if @sequence && @sequence > 0
|
1295
|
-
resume(@sequence, raw_token, @session_id)
|
1296
|
-
return
|
1297
|
-
end
|
1298
|
-
|
1299
|
-
identify(raw_token, 100, GATEWAY_VERSION)
|
1300
|
-
end
|
1301
|
-
|
1302
|
-
# Identify the client to the gateway
|
1303
|
-
def identify(token, large_threshold, version)
|
1304
|
-
# Send the initial packet
|
1305
|
-
packet = {
|
1306
|
-
op: Opcodes::IDENTIFY, # Opcode
|
1307
|
-
d: { # Packet data
|
1308
|
-
v: version, # WebSocket protocol version
|
1309
|
-
token: token,
|
1310
|
-
properties: { # I'm unsure what these values are for exactly, but they don't appear to impact bot functionality in any way.
|
1311
|
-
:'$os' => RUBY_PLATFORM.to_s,
|
1312
|
-
:'$browser' => 'discordrb',
|
1313
|
-
:'$device' => 'discordrb',
|
1314
|
-
:'$referrer' => '',
|
1315
|
-
:'$referring_domain' => ''
|
1316
|
-
},
|
1317
|
-
large_threshold: large_threshold,
|
1318
|
-
compress: true
|
1319
|
-
}
|
1320
|
-
}
|
1321
|
-
|
1322
|
-
# Discord is very strict about the existence of the shard parameter, so only add it if it actually exists
|
1323
|
-
packet[:d][:shard] = @shard_key if @shard_key
|
1324
|
-
|
1325
|
-
@ws.send(packet.to_json)
|
1326
|
-
end
|
1327
|
-
|
1328
|
-
# Resume a previous gateway connection when reconnecting to a different server
|
1329
|
-
def resume(seq, token, session_id)
|
1330
|
-
data = {
|
1331
|
-
op: Opcodes::RESUME,
|
1332
|
-
d: {
|
1333
|
-
seq: seq,
|
1334
|
-
token: token,
|
1335
|
-
session_id: session_id
|
1336
|
-
}
|
1337
|
-
}
|
1338
|
-
|
1339
|
-
@ws.send(data.to_json)
|
1340
|
-
end
|
1341
|
-
|
1342
|
-
# Invalidate the current session (whatever this means)
|
1343
|
-
def invalidate_session
|
1344
|
-
@sequence = 0
|
1345
|
-
@session_id = nil
|
1346
|
-
end
|
1347
|
-
|
1348
1068
|
# Notifies everything there is to be notified that the connection is now ready
|
1349
1069
|
def notify_ready
|
1350
1070
|
# Make sure to raise the event
|
1351
1071
|
raise_event(ReadyEvent.new(self))
|
1352
1072
|
LOGGER.good 'Ready'
|
1353
1073
|
|
1354
|
-
|
1355
|
-
@ws_success = true
|
1356
|
-
end
|
1357
|
-
|
1358
|
-
# Separate method to wait an ever-increasing amount of time before reconnecting after being disconnected in an
|
1359
|
-
# unexpected way
|
1360
|
-
def wait_for_reconnect
|
1361
|
-
# We disconnected in an unexpected way! Wait before reconnecting so we don't spam Discord's servers.
|
1362
|
-
debug("Attempting to reconnect in #{@falloff} seconds.")
|
1363
|
-
sleep @falloff
|
1364
|
-
|
1365
|
-
# Calculate new falloff
|
1366
|
-
@falloff *= 1.5
|
1367
|
-
@falloff = 115 + (rand * 10) if @falloff > 120 # Cap the falloff at 120 seconds and then add some random jitter
|
1368
|
-
end
|
1369
|
-
|
1370
|
-
def send_heartbeat(sequence = nil)
|
1371
|
-
sequence ||= @sequence
|
1372
|
-
|
1373
|
-
raise_event(HeartbeatEvent.new(self))
|
1374
|
-
|
1375
|
-
LOGGER.out("Sending heartbeat with sequence #{sequence}")
|
1376
|
-
data = {
|
1377
|
-
op: Opcodes::HEARTBEAT,
|
1378
|
-
d: sequence
|
1379
|
-
}
|
1380
|
-
|
1381
|
-
@ws.send(data.to_json)
|
1382
|
-
rescue => e
|
1383
|
-
LOGGER.error('Got an error while sending a heartbeat! Carrying on anyway because heartbeats are vital for the connection to stay alive')
|
1384
|
-
LOGGER.log_exception(e)
|
1074
|
+
@gateway.notify_ready
|
1385
1075
|
end
|
1386
1076
|
|
1387
1077
|
def raise_event(event)
|