Class: Discordrb::Bot

Inherits:
Object
  • Object
show all
Includes:
Cache, EventContainer
Defined in:
lib/discordrb/bot.rb

Overview

Represents a Discord bot, including servers, users, etc.

Direct Known Subclasses

Commands::CommandBot

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from Cache

#channel, #ensure_channel, #ensure_server, #ensure_user, #find_channel, #find_user, #init_cache, #invite, #member, #private_channel, #request_chunks, #resolve_invite_code, #server, #user

Methods included from EventContainer

#add_handler, #await, #channel_create, #channel_delete, #channel_update, class_from_string, #clear!, #disconnected, event_class, handler_class, #include_events, #member_join, #member_leave, #member_update, #mention, #message, #message_delete, #message_edit, #playing, #pm, #presence, #ready, #remove_handler, #server_create, #server_delete, #server_update, #typing, #user_ban, #user_unban, #voice_state_update

Methods included from Events

matches_all

Constructor Details

#initialize(email: nil, password: nil, log_mode: :normal, token: nil, application_id: nil, type: nil, name: '', fancy_log: false, suppress_ready: false, parse_self: false) ⇒ Bot

Makes a new bot with the given authentication data. It will be ready to be added event handlers to and can eventually be run with #run.

Depending on the authentication information present, discordrb will deduce whether you're running on a user or a bot account. (Discord recommends using bot accounts whenever possible.) The following sets of authentication information are valid:

  • token + application_id (bot account)
  • email + password (user account)
  • email + password + token (user account; the given token will be used for authentication instead of email and password)

Simply creating a bot won't be enough to start sending messages etc. with, only a limited set of methods can be used after logging in. If you want to do something when the bot has connected successfully, either do it in the EventContainer#ready event, or use the #run method with the :async parameter and do the processing after that.

Parameters:

  • email (String) (defaults to: nil)

    The email for your (or the bot's) Discord account.

  • password (String) (defaults to: nil)

    The valid password that should be used to log in to the account.

  • log_mode (Symbol) (defaults to: :normal)

    The mode this bot should use for logging. See Logger#mode= for a list of modes.

  • token (String) (defaults to: nil)

    The token that should be used to log in. If your bot is a bot account, you have to specify this. If you're logging in as a user, make sure to also set the account type to :user so discordrb doesn't think you're trying to log in as a bot.

  • application_id (Integer) (defaults to: nil)

    If you're logging in as a bot, the bot's application ID.

  • type (Symbol) (defaults to: nil)

    This parameter lets you manually overwrite the account type. If this isn't specified, it will be determined by checking what other attributes are there. The only use case for this is if you want to log in as a user but only with a token. Valid values are :user and :bot.

  • name (String) (defaults to: '')

    Your bot's name. This will be sent to Discord with any API requests, who will use this to trace the source of excessive API requests; it's recommended to set this to something if you make bots that many people will host on their servers separately.

  • fancy_log (true, false) (defaults to: false)

    Whether the output log should be made extra fancy using ANSI escape codes. (Your terminal may not support this.)

  • suppress_ready (true, false) (defaults to: false)

    Whether the READY packet should be exempt from being printed to console. Useful for very large bots running in debug or verbose log_mode.

  • parse_self (true, false) (defaults to: false)

    Whether the bot should react on its own messages. It's best to turn this off unless you really need this so you don't inadvertently create infinite loops.



147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
# File 'lib/discordrb/bot.rb', line 147

def initialize(
    email: nil, password: nil, log_mode: :normal,
    token: nil, application_id: nil,
    type: nil, name: '', fancy_log: false, suppress_ready: false, parse_self: false)
  # Make sure people replace the login details in the example files...
  if email.is_a?(String) && email.end_with?('example.com')
    puts 'You have to replace the login details in the example files with your own!'
    exit
  end

  LOGGER.mode = if log_mode.is_a? TrueClass # Specifically check for `true` because people might not have updated yet
                  :debug
                else
                  log_mode
                end

  @should_parse_self = parse_self

  @email = email
  @password = password

  @application_id = application_id

  @type = (type, email, password, token, application_id)

  @name = name

  LOGGER.fancy = fancy_log
  @prevent_ready = suppress_ready

  debug('Creating token cache')
  token_cache = Discordrb::TokenCache.new
  debug('Token cache created successfully')
  @token = (type, email, password, token, token_cache)

  init_cache

  @voices = {}
  @should_connect_to_voice = {}

  @event_threads = []
  @current_thread = 0
end

Instance Attribute Details

#event_threadsArray<Thread> (readonly)

The list of currently running threads used to parse and call events. The threads will have a local variable :discordrb_name in the format of et-1234, where "et" stands for "event thread" and the number is a continually incrementing number representing how many events were executed before.

Returns:

  • (Array<Thread>)

    The threads.



96
97
98
# File 'lib/discordrb/bot.rb', line 96

def event_threads
  @event_threads
end

#nameObject

The bot's name which discordrb sends to Discord when making any request, so Discord can identify bots with the same codebase. Not required but I recommend setting it anyway.



109
110
111
# File 'lib/discordrb/bot.rb', line 109

def name
  @name
end

#profileProfile (readonly) Also known as: bot_user

The bot's user profile. This special user object can be used to edit user data like the current username (see Profile#username=).

Returns:

  • (Profile)

    The bot's profile that can be used to edit data.



101
102
103
# File 'lib/discordrb/bot.rb', line 101

def profile
  @profile
end

#serversArray<Server> (readonly)

The list of servers the bot is currently in.

Returns:

  • (Array<Server>)

    The servers.



89
90
91
# File 'lib/discordrb/bot.rb', line 89

def servers
  @servers
end

#should_parse_selfObject

Whether or not the bot should parse its own messages. Off by default.



105
106
107
# File 'lib/discordrb/bot.rb', line 105

def should_parse_self
  @should_parse_self
end

#usersArray<User> (readonly)

The list of users the bot shares a server with.

Returns:

  • (Array<User>)

    The users.



85
86
87
# File 'lib/discordrb/bot.rb', line 85

def users
  @users
end

#voicesHash<Integer => VoiceBot> (readonly)

Returns the voice connections this bot currently has, by the server ID to which they are connected.

Returns:

  • (Hash<Integer => VoiceBot>)

    the voice connections this bot currently has, by the server ID to which they are connected.



293
294
295
# File 'lib/discordrb/bot.rb', line 293

def voices
  @voices
end

Instance Method Details

#add_await(key, type, attributes = {}) {|event| ... } ⇒ Await

Add an await the bot should listen to. For information on awaits, see Await.

Parameters:

  • key (Symbol)

    The key that uniquely identifies the await for Events::AwaitEvents to listen to (see EventContainer#await).

  • type (Class)

    The event class that should be listened for.

  • attributes (Hash) (defaults to: {})

    The attributes the event should check for. The block will only be executed if all attributes match.

Yields:

  • Is executed when the await is triggered.

Yield Parameters:

  • event (Event)

    The event object that was triggered.

Returns:

  • (Await)

    The await that was created.



521
522
523
524
525
526
# File 'lib/discordrb/bot.rb', line 521

def add_await(key, type, attributes = {}, &block)
  raise "You can't await an AwaitEvent!" if type == Discordrb::Events::AwaitEvent
  await = Await.new(self, key, type, attributes, block)
  @awaits ||= {}
  @awaits[key] = await
end

#create_oauth_application(name, redirect_uris) ⇒ Array(String, String)

Creates a new application to do OAuth authorization with. This allows you to use OAuth to authorize users using Discord. For information how to use this, see this example: https://github.com/vishnevskiy/discord-oauth2-example

Parameters:

  • name (String)

    What your application should be called.

  • redirect_uris (Array<String>)

    URIs that Discord should redirect your users to after authorizing.

Returns:

  • (Array(String, String))

    your applications' client ID and client secret to be used in OAuth authorization.



435
436
437
438
# File 'lib/discordrb/bot.rb', line 435

def create_oauth_application(name, redirect_uris)
  response = JSON.parse(API.create_oauth_application(@token, name, redirect_uris))
  [response['id'], response['secret']]
end

#create_server(name, region = :london) ⇒ Server

Note:

Discord's API doesn't directly return the server when creating it, so this method waits until the data has been received via the websocket. This may make the execution take a while.

Creates a server on Discord with a specified name and a region.

Parameters:

  • name (String)

    The name the new server should have. Doesn't have to be alphanumeric.

  • region (Symbol) (defaults to: :london)

    The region where the server should be created. Possible regions are:

    • :london
    • :amsterdam
    • :frankfurt
    • :us-east
    • :us-west
    • :us-south
    • :us-central
    • :singapore
    • :sydney

Returns:

  • (Server)

    The server that was created.



421
422
423
424
425
426
427
428
# File 'lib/discordrb/bot.rb', line 421

def create_server(name, region = :london)
  response = API.create_server(token, name, region)
  id = JSON.parse(response)['id'].to_i
  sleep 0.1 until @servers[id]
  server = @servers[id]
  debug "Successfully created server #{server.id} with name #{server.name}"
  server
end

#debug(message) ⇒ Object

See Also:

  • Logger#debug


529
530
531
# File 'lib/discordrb/bot.rb', line 529

def debug(message)
  LOGGER.debug(message)
end

#debug=(new_debug) ⇒ Object

Sets debug mode. If debug mode is on, many things will be outputted to STDOUT.



499
500
501
# File 'lib/discordrb/bot.rb', line 499

def debug=(new_debug)
  LOGGER.debug = new_debug
end

#delete_invite(code) ⇒ Object

Revokes an invite to a server. Will fail unless you have the Manage Server permission. It is recommended that you use Invite#delete instead.

Parameters:



379
380
381
382
# File 'lib/discordrb/bot.rb', line 379

def delete_invite(code)
  invite = resolve_invite_code(code)
  API.delete_invite(token, invite)
end

#game=(name) ⇒ String

Sets the currently playing game to the specified game.

Parameters:

  • name (String)

    The name of the game to be played.

Returns:

  • (String)

    The game that is being played now.



462
463
464
465
466
467
468
469
470
471
472
473
474
475
# File 'lib/discordrb/bot.rb', line 462

def game=(name)
  @game = name

  data = {
    op: Opcodes::PRESENCE,
    d: {
      idle_since: nil,
      game: name ? { name: name } : nil
    }
  }

  @ws.send(data.to_json)
  name
end

#inject_reconnect(url) ⇒ Object

Injects a reconnect event (op 7) into the event processor, causing Discord to reconnect to the given gateway URL. If the URL is set to nil, it will reconnect and get an entirely new gateway URL. This method has not much use outside of testing and implementing highly custom reconnect logic.

Parameters:

  • url (String, nil)

    the URL to connect to or nil if one should be obtained from Discord.



481
482
483
484
485
486
487
488
# File 'lib/discordrb/bot.rb', line 481

def inject_reconnect(url)
  websocket_message({
    op: Opcodes::RECONNECT,
    d: {
      url: url
    }
  }.to_json)
end

#inject_resume(seq) ⇒ Object

Injects a resume packet (op 6) into the gateway. If this is done with a running connection, it will cause an error. It has no use outside of testing stuff that I know of, but if you want to use it anyway for some reason, here it is.

Parameters:

  • seq (Integer, nil)

    The sequence ID to inject, or nil if the currently tracked one should be used.



494
495
496
# File 'lib/discordrb/bot.rb', line 494

def inject_resume(seq)
  resume(seq || @sequence, raw_token, @session_id)
end

#invite_url(server = nil) ⇒ String

Creates an OAuth invite URL that can be used to invite this bot to a particular server. Requires the application ID to have been set during initialization.

Parameters:

  • server (Server, nil) (defaults to: nil)

    The server the bot should be invited to, or nil if a general invite should be created.

Returns:

  • (String)

    the OAuth invite URL.



285
286
287
288
289
290
# File 'lib/discordrb/bot.rb', line 285

def invite_url(server = nil)
  raise 'No application ID has been set during initialization! Add one as the `application_id` named parameter while creating your bot.' unless @application_id

  guild_id_str = server ? "&guild_id=#{server.id}" : ''
  "https://discordapp.com/oauth2/authorize?&client_id=#{@application_id}#{guild_id_str}&scope=bot"
end

#join(invite) ⇒ Object

Makes the bot join an invite to a server.

Parameters:



276
277
278
279
# File 'lib/discordrb/bot.rb', line 276

def join(invite)
  resolved = invite(invite).code
  API.join_server(token, resolved)
end

#log_exception(e) ⇒ Object



534
535
536
# File 'lib/discordrb/bot.rb', line 534

def log_exception(e)
  LOGGER.log_exception(e)
end

#mode=(new_mode) ⇒ Object

Sets the logging mode

See Also:



505
506
507
# File 'lib/discordrb/bot.rb', line 505

def mode=(new_mode)
  LOGGER.mode = new_mode
end

#parse_mention(mention) ⇒ User

Gets the user from a mention of the user.

Parameters:

  • mention (String)

    The mention, which should look like <@12314873129>.

Returns:

  • (User)

    The user identified by the mention, or nil if none exists.



453
454
455
456
457
# File 'lib/discordrb/bot.rb', line 453

def parse_mention(mention)
  # Mention format: <@id>
  return nil unless /<@(?<id>\d+)>?/ =~ mention
  user(id.to_i)
end

#raw_tokenObject

Returns the raw token, without any prefix.

Returns:

  • the raw token, without any prefix

See Also:



201
202
203
# File 'lib/discordrb/bot.rb', line 201

def raw_token
  @token.split(' ').last
end

#run(async = false) ⇒ Object

Runs the bot, which logs into Discord and connects the WebSocket. This prevents all further execution unless it is executed with async = :async.

Parameters:

  • async (Symbol) (defaults to: false)

    If it is :async, then the bot will allow further execution. It doesn't necessarily have to be that, anything truthy will work, however it is recommended to use :async for code readability reasons. If the bot is run in async mode, make sure to eventually run #sync so the script doesn't stop prematurely.



211
212
213
214
215
216
217
# File 'lib/discordrb/bot.rb', line 211

def run(async = false)
  run_async
  return if async

  debug('Oh wait! Not exiting yet as run was run synchronously.')
  sync
end

#run_asyncObject

Runs the bot asynchronously. Equivalent to #run with the :async parameter.

See Also:



221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
# File 'lib/discordrb/bot.rb', line 221

def run_async
  # Handle heartbeats
  @heartbeat_interval = 1
  @heartbeat_active = false
  @heartbeat_thread = Thread.new do
    Thread.current[:discordrb_name] = 'heartbeat'
    loop do
      sleep @heartbeat_interval
      send_heartbeat if @heartbeat_active
    end
  end

  @ws_thread = Thread.new do
    Thread.current[:discordrb_name] = 'websocket'

    # Initialize falloff so we wait for more time before reconnecting each time
    @falloff = 1.0

    loop do
      websocket_connect

      # websocket_connect is blocking so being in here means we're disconnected
      LOGGER.warn('Oh dear, we got disconnected!')

      if @reconnect_url
        # We got an op 7! Don't wait before reconnecting
        debug('Got an op 7, reconnecting right away')
      else
        wait_for_reconnect
      end

      # Restart the loop, i. e. reconnect
    end

    LOGGER.warn('The WS loop exited! Not sure if this is a good thing')
  end

  debug('WS thread created! Now waiting for confirmation that everything worked')
  @ws_success = false
  sleep(0.5) until @ws_success
  debug('Confirmation received! Exiting run.')
end

#send_file(channel_id, file) ⇒ Object

Note:

This executes in a blocking way, so if you're sending long files, be wary of delays.

Sends a file to a channel. If it is an image, it will automatically be embedded.

Parameters:

  • channel_id (Integer)

    The ID that identifies the channel to send something to.

  • file (File)

    The file that should be sent.



400
401
402
403
# File 'lib/discordrb/bot.rb', line 400

def send_file(channel_id, file)
  response = API.send_file(token, channel_id, file)
  Message.new(JSON.parse(response), self)
end

#send_message(channel_id, content, tts = false) ⇒ Message

Sends a text message to a channel given its ID and the message's content.

Parameters:

  • channel_id (Integer)

    The ID that identifies the channel to send something to.

  • content (String)

    The text that should be sent as a message. It is limited to 2000 characters (Discord imposed).

  • tts (true, false) (defaults to: false)

    Whether or not this message should be sent using Discord text-to-speech.

Returns:

  • (Message)

    The message that was sent.



389
390
391
392
393
394
# File 'lib/discordrb/bot.rb', line 389

def send_message(channel_id, content, tts = false)
  debug("Sending message to #{channel_id} with content '#{content}'")

  response = API.send_message(token, channel_id, content, [], tts)
  Message.new(JSON.parse(response), self)
end

#stopObject

Kills the websocket thread, stopping all connections to Discord.



270
271
272
# File 'lib/discordrb/bot.rb', line 270

def stop
  @ws_thread.kill
end

#suppress_ready_debugObject

Prevents the READY packet from being printed regardless of debug mode.



510
511
512
# File 'lib/discordrb/bot.rb', line 510

def suppress_ready_debug
  @prevent_ready = true
end

#syncObject

Prevents all further execution until the websocket thread stops (e. g. through a closed connection).



265
266
267
# File 'lib/discordrb/bot.rb', line 265

def sync
  @ws_thread.join
end

#tokenString

The Discord API token received when logging in. Useful to explicitly call API methods.

Returns:



194
195
196
197
# File 'lib/discordrb/bot.rb', line 194

def token
  API.bot_name = @name
  @token
end

#update_oauth_application(name, redirect_uris, description = '', icon = nil) ⇒ Object

Changes information about your OAuth application

Parameters:

  • name (String)

    What your application should be called.

  • redirect_uris (Array<String>)

    URIs that Discord should redirect your users to after authorizing.

  • description (String) (defaults to: '')

    A string that describes what your application does.

  • icon (String, nil) (defaults to: nil)

    A data URI for your icon image (for example a base 64 encoded image), or nil if no icon should be set or changed.



446
447
448
# File 'lib/discordrb/bot.rb', line 446

def update_oauth_application(name, redirect_uris, description = '', icon = nil)
  API.update_oauth_application(@token, name, redirect_uris, description, icon)
end

#voice(thing) ⇒ VoiceBot?

Gets the voice bot for a particular server or channel. You can connect to a new channel using the #voice_connect method.

Parameters:

  • thing (Channel, Server, Integer)

    the server or channel you want to get the voice bot for, or its ID.

Returns:

  • (VoiceBot, nil)

    the VoiceBot for the thing you specified, or nil if there is no connection yet



299
300
301
302
303
304
305
306
307
308
309
310
# File 'lib/discordrb/bot.rb', line 299

def voice(thing)
  id = thing.resolve_id
  return @voices[id] if @voices[id]

  channel = channel(id)
  return nil unless channel

  server_id = channel.server.id
  return @voices[server_id] if @voices[server_id]

  nil
end

#voice_connect(chan, encrypted = true) ⇒ Voice::VoiceBot

Connects to a voice channel, initializes network connections and returns the Voice::VoiceBot over which audio data can then be sent. After connecting, the bot can also be accessed using #voice. If the bot is already connected to voice, the existing connection will be terminated - you don't have to call VoiceBot#destroy before calling this method.

Parameters:

  • chan (Channel)

    The voice channel to connect to.

  • encrypted (true, false) (defaults to: true)

    Whether voice communication should be encrypted using RbNaCl's SecretBox (uses an XSalsa20 stream cipher for encryption and Poly1305 for authentication)

Returns:

  • (Voice::VoiceBot)

    the initialized bot over which audio data can then be sent.



320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
# File 'lib/discordrb/bot.rb', line 320

def voice_connect(chan, encrypted = true)
  chan = channel(chan.resolve_id)
  server_id = chan.server.id
  @should_encrypt_voice = encrypted

  if @voices[chan.id]
    debug('Voice bot exists already! Destroying it')
    @voices[chan.id].destroy
    @voices.delete(chan.id)
  end

  debug("Got voice channel: #{chan}")

  data = {
    op: Opcodes::VOICE_STATE,
    d: {
      guild_id: server_id.to_s,
      channel_id: chan.id.to_s,
      self_mute: false,
      self_deaf: false
    }
  }
  debug("Voice channel init packet is: #{data.to_json}")

  @should_connect_to_voice[server_id] = chan
  @ws.send(data.to_json)
  debug('Voice channel init packet sent! Now waiting.')

  sleep(0.05) until @voices[server_id]
  debug('Voice connect succeeded!')
  @voices[server_id]
end

#voice_destroy(server_id, destroy_vws = true) ⇒ Object

Disconnects the client from a specific voice connection given the server ID. Usually it's more convenient to use Voice::VoiceBot#destroy rather than this.

Parameters:

  • server_id (Integer)

    The ID of the server the voice connection is on.

  • destroy_vws (true, false) (defaults to: true)

    Whether or not the VWS should also be destroyed. If you're calling this method directly, you should leave it as true.



358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
# File 'lib/discordrb/bot.rb', line 358

def voice_destroy(server_id, destroy_vws = true)
  data = {
    op: Opcodes::VOICE_STATE,
    d: {
      guild_id: server_id.to_s,
      channel_id: nil,
      self_mute: false,
      self_deaf: false
    }
  }

  debug("Voice channel destroy packet is: #{data.to_json}")
  @ws.send(data.to_json)

  @voices[server_id].destroy if @voices[server_id] && destroy_vws
  @voices.delete(server_id)
end