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, #heartbeat, #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, shard_id: nil, num_shards: nil) ⇒ 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.

  • shard_id (Integer) (defaults to: nil)

    The number of the shard this bot should handle. See https://github.com/hammerandchisel/discord-api-docs/issues/17 for how to do sharding.

  • num_shards (Integer) (defaults to: nil)

    The total number of shards that should be running. See https://github.com/hammerandchisel/discord-api-docs/issues/17 for how to do sharding.



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
190
191
192
193
194
195
196
197
198
199
200
201
202
# File 'lib/discordrb/bot.rb', line 155

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,
    shard_id: nil, num_shards: nil)
  # 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

  @shard_key = num_shards ? [shard_id, num_shards] : nil

  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 = {}

  @ignored_ids = Set.new

  @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.



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

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.



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

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.



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

def profile
  @profile
end

#serversHash<Integer => Server> (readonly)

The list of servers the bot is currently in.

Returns:



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

def servers
  @servers
end

#shard_keyArray(Integer, Integer) (readonly)

Returns the current shard key.

Returns:



113
114
115
# File 'lib/discordrb/bot.rb', line 113

def shard_key
  @shard_key
end

#should_parse_selfObject

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



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

def should_parse_self
  @should_parse_self
end

#usersHash<Integer => User> (readonly)

The list of users the bot shares a server with.

Returns:



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

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.



307
308
309
# File 'lib/discordrb/bot.rb', line 307

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.



536
537
538
539
540
541
# File 'lib/discordrb/bot.rb', line 536

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 the docs: https://discordapp.com/developers/docs/topics/oauth2

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.



450
451
452
453
# File 'lib/discordrb/bot.rb', line 450

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.



436
437
438
439
440
441
442
443
# File 'lib/discordrb/bot.rb', line 436

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


565
566
567
# File 'lib/discordrb/bot.rb', line 565

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.



514
515
516
# File 'lib/discordrb/bot.rb', line 514

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:



393
394
395
396
# File 'lib/discordrb/bot.rb', line 393

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.



477
478
479
480
481
482
483
484
485
486
487
488
489
490
# File 'lib/discordrb/bot.rb', line 477

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

#ignore_user(user) ⇒ Object

Note:

Ignoring a user only prevents any message events (including mentions, commands etc.) from them! Typing and presence and any other events will still be received.

Add a user to the list of ignored users. Those users will be ignored in message events at event processing level.

Parameters:

  • user (User, Integer, #resolve_id)

    The user, or its ID, to be ignored.



547
548
549
# File 'lib/discordrb/bot.rb', line 547

def ignore_user(user)
  @ignored_ids << user.resolve_id
end

#ignored?(user) ⇒ true, false

Checks whether a user is being ignored.

Parameters:

  • user (User, Integer, #resolve_id)

    The user, or its ID, to check.

Returns:

  • (true, false)

    whether or not the user is ignored.



560
561
562
# File 'lib/discordrb/bot.rb', line 560

def ignored?(user)
  @ignored_ids.include?(user.resolve_id)
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.



496
497
498
499
500
501
502
503
# File 'lib/discordrb/bot.rb', line 496

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.



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

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.



299
300
301
302
303
304
# File 'lib/discordrb/bot.rb', line 299

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:



290
291
292
293
# File 'lib/discordrb/bot.rb', line 290

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

#log_exception(e) ⇒ Object



570
571
572
# File 'lib/discordrb/bot.rb', line 570

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

#mode=(new_mode) ⇒ Object

Sets the logging mode

See Also:



520
521
522
# File 'lib/discordrb/bot.rb', line 520

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.



468
469
470
471
472
# File 'lib/discordrb/bot.rb', line 468

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:



214
215
216
# File 'lib/discordrb/bot.rb', line 214

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.



224
225
226
227
228
229
230
# File 'lib/discordrb/bot.rb', line 224

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:



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
263
264
265
266
267
268
269
270
271
272
273
274
275
276
# File 'lib/discordrb/bot.rb', line 234

def run_async
  # Handle heartbeats
  @heartbeat_interval = 1
  @heartbeat_active = false
  @heartbeat_thread = Thread.new do
    Thread.current[:discordrb_name] = 'heartbeat'
    loop do
      if @heartbeat_active
        send_heartbeat
        sleep @heartbeat_interval
      else
        sleep 1
      end
    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

      if @reconnect_url
        # We got an op 7! Don't wait before reconnecting
        LOGGER.info('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.



415
416
417
418
# File 'lib/discordrb/bot.rb', line 415

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, server_id = nil) ⇒ 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.



403
404
405
406
407
408
409
# File 'lib/discordrb/bot.rb', line 403

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

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

#stopObject

Kills the websocket thread, stopping all connections to Discord.



284
285
286
# File 'lib/discordrb/bot.rb', line 284

def stop
  @ws_thread.kill
end

#suppress_ready_debugObject

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



525
526
527
# File 'lib/discordrb/bot.rb', line 525

def suppress_ready_debug
  @prevent_ready = true
end

#syncObject

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



279
280
281
# File 'lib/discordrb/bot.rb', line 279

def sync
  @ws_thread.join
end

#tokenString

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

Returns:



207
208
209
210
# File 'lib/discordrb/bot.rb', line 207

def token
  API.bot_name = @name
  @token
end

#unignore_user(user) ⇒ Object

Remove a user from the ignore list.

Parameters:

  • user (User, Integer, #resolve_id)

    The user, or its ID, to be unignored.



553
554
555
# File 'lib/discordrb/bot.rb', line 553

def unignore_user(user)
  @ignored_ids.delete(user.resolve_id)
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.



461
462
463
# File 'lib/discordrb/bot.rb', line 461

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



313
314
315
316
317
318
319
320
321
322
323
324
# File 'lib/discordrb/bot.rb', line 313

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.



334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
# File 'lib/discordrb/bot.rb', line 334

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.



372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
# File 'lib/discordrb/bot.rb', line 372

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