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.



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
190
191
192
# File 'lib/discordrb/bot.rb', line 148

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

  @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

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



296
297
298
# File 'lib/discordrb/bot.rb', line 296

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.



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

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.



438
439
440
441
# File 'lib/discordrb/bot.rb', line 438

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.



424
425
426
427
428
429
430
431
# File 'lib/discordrb/bot.rb', line 424

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


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

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.



502
503
504
# File 'lib/discordrb/bot.rb', line 502

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:



382
383
384
385
# File 'lib/discordrb/bot.rb', line 382

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.



465
466
467
468
469
470
471
472
473
474
475
476
477
478
# File 'lib/discordrb/bot.rb', line 465

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.



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

def ignore_user(user)
  @ignored_ids << 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.



484
485
486
487
488
489
490
491
# File 'lib/discordrb/bot.rb', line 484

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.



497
498
499
# File 'lib/discordrb/bot.rb', line 497

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.



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

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:



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

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

#log_exception(e) ⇒ Object



551
552
553
# File 'lib/discordrb/bot.rb', line 551

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

#mode=(new_mode) ⇒ Object

Sets the logging mode

See Also:



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

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.



456
457
458
459
460
# File 'lib/discordrb/bot.rb', line 456

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:



204
205
206
# File 'lib/discordrb/bot.rb', line 204

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.



214
215
216
217
218
219
220
# File 'lib/discordrb/bot.rb', line 214

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:



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
263
264
265
# File 'lib/discordrb/bot.rb', line 224

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.



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

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.



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

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.



273
274
275
# File 'lib/discordrb/bot.rb', line 273

def stop
  @ws_thread.kill
end

#suppress_ready_debugObject

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



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

def suppress_ready_debug
  @prevent_ready = true
end

#syncObject

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



268
269
270
# File 'lib/discordrb/bot.rb', line 268

def sync
  @ws_thread.join
end

#tokenString

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

Returns:



197
198
199
200
# File 'lib/discordrb/bot.rb', line 197

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.



541
542
543
# File 'lib/discordrb/bot.rb', line 541

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.



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

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



302
303
304
305
306
307
308
309
310
311
312
313
# File 'lib/discordrb/bot.rb', line 302

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.



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
352
353
354
# File 'lib/discordrb/bot.rb', line 323

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.



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

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