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, #pm_channel, #request_chunks, #resolve_invite_code, #server, #user, #voice_regions

Methods included from EventContainer

#add_handler, #await, #channel_create, #channel_delete, #channel_recipient_add, #channel_recipient_remove, #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, #message_update, #playing, #pm, #presence, #raw, #reaction_add, #reaction_remove, #reaction_remove_all, #ready, #remove_handler, #server_create, #server_delete, #server_emoji, #server_emoji_create, #server_emoji_delete, #server_emoji_update, #server_role_create, #server_role_delete, #server_role_update, #server_update, #typing, #unknown, #user_ban, #user_unban, #voice_state_update, #webhook_update

Methods included from Events

matches_all

Constructor Details

#initialize(log_mode: :normal, token: nil, client_id: nil, type: nil, name: '', fancy_log: false, suppress_ready: false, parse_self: false, shard_id: nil, num_shards: nil, redact_token: true, ignore_bots: false, compress_mode: :large) ⇒ 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.

As support for logging in using username and password has been removed in version 3.0.0, only a token login is possible. Be sure to specify the type parameter as :user if you're logging in as a user.

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.


104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
# File 'lib/discordrb/bot.rb', line 104

def initialize(
  log_mode: :normal,
  token: nil, client_id: nil,
  type: nil, name: '', fancy_log: false, suppress_ready: false, parse_self: false,
  shard_id: nil, num_shards: nil, redact_token: true, ignore_bots: false,
  compress_mode: :large
)
  LOGGER.mode = log_mode
  LOGGER.token = token if redact_token

  @should_parse_self = parse_self

  @client_id = client_id

  @type = type || :bot
  @name = name

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

  LOGGER.fancy = fancy_log
  @prevent_ready = suppress_ready

  @compress_mode = compress_mode

  raise 'Token string is empty or nil' if token.nil? || token.empty?

  @token = process_token(@type, token)
  @gateway = Gateway.new(self, @token, @shard_key, @compress_mode)

  init_cache

  @voices = {}
  @should_connect_to_voice = {}

  @ignored_ids = Set.new
  @ignore_bots = ignore_bots

  @event_threads = []
  @current_thread = 0

  @status = :online
end

Instance Attribute Details

#awaitsHash<Symbol => Await> (readonly)


58
59
60
# File 'lib/discordrb/bot.rb', line 58

def awaits
  @awaits
end

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


44
45
46
# File 'lib/discordrb/bot.rb', line 44

def event_threads
  @event_threads
end

#gatewayGateway (readonly)

The gateway connection is an internal detail that is useless to most people. It is however essential while debugging or developing discordrb itself, or while writing very custom bots.


63
64
65
# File 'lib/discordrb/bot.rb', line 63

def gateway
  @gateway
end

#nameString

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.


52
53
54
# File 'lib/discordrb/bot.rb', line 52

def name
  @name
end

#shard_keyArray(Integer, Integer) (readonly)


55
56
57
# File 'lib/discordrb/bot.rb', line 55

def shard_key
  @shard_key
end

#should_parse_selftrue, false


47
48
49
# File 'lib/discordrb/bot.rb', line 47

def should_parse_self
  @should_parse_self
end

#voicesHash<Integer => VoiceBot> (readonly)


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

def voices
  @voices
end

Instance Method Details

#accept_invite(invite) ⇒ Object

Makes the bot join an invite to a server.


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

def accept_invite(invite)
  resolved = invite(invite).code
  API::Invite.accept(token, resolved)
end

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

Deprecated.

Will be changed to blocking behavior in v4.0. Use #add_await! instead.

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

Yields:

  • Is executed when the await is triggered.

Yield Parameters:

  • event (Event)

    The event object that was triggered.


606
607
608
609
610
611
612
# File 'lib/discordrb/bot.rb', line 606

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

#add_await!(type, attributes = {}) ⇒ Event?

Awaits an event, blocking the current thread until a response is received.

Options Hash (attributes):

  • :timeout (Numeric)

    the amount of time to wait for a response before returning nil. Waits forever if omitted.

Raises:

  • (ArgumentError)

    if timeout is given and is not a positive numeric value


619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
# File 'lib/discordrb/bot.rb', line 619

def add_await!(type, attributes = {})
  raise "You can't await an AwaitEvent!" if type == Discordrb::Events::AwaitEvent

  timeout = attributes[:timeout]
  raise ArgumentError, 'Timeout must be a number > 0' if timeout&.is_a?(Numeric) && !timeout&.positive?

  mutex = Mutex.new
  cv = ConditionVariable.new
  response = nil
  block = lambda do |event|
    mutex.synchronize do
      response = event
      cv.signal
    end
  end

  handler = register_event(type, attributes, block)

  if timeout
    Thread.new do
      sleep timeout
      mutex.synchronize { cv.signal }
    end
  end

  mutex.synchronize { cv.wait(mutex) }

  remove_handler(handler)
  raise 'ConditionVariable was signaled without returning an event!' if response.nil? && timeout.nil?

  response
end

#bot_applicationApplication? Also known as: bot_app

The bot's OAuth application.


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

def bot_application
  return unless @type == :bot

  response = API.oauth_application(token)
  Application.new(JSON.parse(response), self)
end

#connected?true, false


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

def connected?
  @gateway.open?
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


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 = :'eu-central') ⇒ 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.


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

def create_server(name, region = :'eu-central')
  response = API::Server.create(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

674
675
676
# File 'lib/discordrb/bot.rb', line 674

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.


583
584
585
# File 'lib/discordrb/bot.rb', line 583

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.


355
356
357
358
# File 'lib/discordrb/bot.rb', line 355

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

#dispatch(type, data) ⇒ Object

Dispatches an event to this bot. Called by the gateway connection handler used internally.


684
685
686
# File 'lib/discordrb/bot.rb', line 684

def dispatch(type, data)
  handle_dispatch(type, data)
end

#dndObject

Sets the bot's status to DnD (red icon).


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

def dnd
  gateway_check
  update_status(:dnd, @activity, nil)
end

#emoji(id) ⇒ Emoji? #emojiArray<Emoji> Also known as: emojis, all_emoji

Overloads:

  • #emoji(id) ⇒ Emoji?

    Return an emoji by its ID

  • #emojiArray<Emoji>

    The list of emoji the bot can use.


170
171
172
173
174
175
176
177
178
179
180
181
# File 'lib/discordrb/bot.rb', line 170

def emoji(id = nil)
  gateway_check
  unavailable_servers_check

  emoji_hash = @servers.values.map(&:emoji).reduce(&:merge)
  if id
    id = id.resolve_id
    emoji_hash[id]
  else
    emoji_hash.values
  end
end

#find_emoji(name) ⇒ GlobalEmoji?

Finds an emoji by its name.


189
190
191
192
# File 'lib/discordrb/bot.rb', line 189

def find_emoji(name)
  LOGGER.out("Resolving emoji #{name}")
  emoji.find { |element| element.name == name }
end

#game=(name) ⇒ String Also known as: playing=

Sets the currently playing game to the specified game.


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

def game=(name)
  gateway_check
  update_status(@status, name, nil)
end

#idleObject Also known as: away

Sets status to idle.


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

def idle
  gateway_check
  update_status(:idle, @activity, nil)
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.


656
657
658
# File 'lib/discordrb/bot.rb', line 656

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

#ignored?(user) ⇒ true, false

Checks whether a user is being ignored.


669
670
671
# File 'lib/discordrb/bot.rb', line 669

def ignored?(user)
  @ignored_ids.include?(user.resolve_id)
end

#invisibleObject

Sets the bot's status to invisible (appears offline).


577
578
579
580
# File 'lib/discordrb/bot.rb', line 577

def invisible
  gateway_check
  update_status(:invisible, @activity, nil)
end

#invite_url(server: nil, permission_bits: nil) ⇒ String

Creates an OAuth invite URL that can be used to invite this bot to a particular server.


281
282
283
284
285
286
287
# File 'lib/discordrb/bot.rb', line 281

def invite_url(server: nil, permission_bits: nil)
  @client_id ||= bot_application.id

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

#joinObject Also known as: sync

Joins the bot's connection thread with the current thread. This blocks execution until the websocket stops, which should only happen manually triggered. or due to an error. This is necessary to have a continuously running bot.


253
254
255
# File 'lib/discordrb/bot.rb', line 253

def join
  @gateway.sync
end

#listening=(name) ⇒ String

Sets the current listening status to the specified name.


531
532
533
534
# File 'lib/discordrb/bot.rb', line 531

def listening=(name)
  gateway_check
  update_status(@status, name, nil, nil, nil, 2)
end

#log_exception(e) ⇒ Object


679
680
681
# File 'lib/discordrb/bot.rb', line 679

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

#mode=(new_mode) ⇒ Object

Sets the logging mode

See Also:


589
590
591
# File 'lib/discordrb/bot.rb', line 589

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

#onlineObject Also known as: on

Sets status to online.


555
556
557
558
# File 'lib/discordrb/bot.rb', line 555

def online
  gateway_check
  update_status(:online, @activity, @streamurl)
end

#parse_mention(mention, server = nil) ⇒ User, ...

Gets the user, channel, role or emoji from a string.


491
492
493
# File 'lib/discordrb/bot.rb', line 491

def parse_mention(mention, server = nil)
  parse_mentions(mention, server).first
end

#parse_mentions(mentions, server = nil) ⇒ Array<User, Channel, Role, Emoji>

Gets the users, channels, roles and emoji from a string.


457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
# File 'lib/discordrb/bot.rb', line 457

def parse_mentions(mentions, server = nil)
  array_to_return = []
  # While possible mentions may be in message
  while mentions.include?('<') && mentions.include?('>')
    # Removing all content before the next possible mention
    mentions = mentions.split('<', 2)[1]
    # Locate the first valid mention enclosed in `<...>`, otherwise advance to the next open `<`
    next unless mentions.split('>', 2).first.length < mentions.split('<', 2).first.length

    # Store the possible mention value to be validated with RegEx
    mention = mentions.split('>', 2).first
    if /@!?(?<id>\d+)/ =~ mention
      array_to_return << user(id) unless user(id).nil?
    elsif /#(?<id>\d+)/ =~ mention
      array_to_return << channel(id, server) unless channel(id, server).nil?
    elsif /@&(?<id>\d+)/ =~ mention
      if server
        array_to_return << server.role(id) unless server.role(id).nil?
      else
        @servers.values.each do |element|
          array_to_return << element.role(id) unless element.role(id).nil?
        end
      end
    elsif /(?<animated>^[a]|^${0}):(?<name>\w+):(?<id>\d+)/ =~ mention
      array_to_return << (emoji(id) || Emoji.new({ 'animated' => !animated.nil?, 'name' => name, 'id' => id }, self, nil))
    end
  end
  array_to_return
end

#profileProfile 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=).


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

def profile
  gateway_check
  @profile
end

#prune_empty_groupsObject

Makes the bot leave any groups with no recipients remaining


694
695
696
697
698
# File 'lib/discordrb/bot.rb', line 694

def prune_empty_groups
  @channels.each_value do |channel|
    channel.leave_group if channel.group? && channel.recipients.empty?
  end
end

#raise_heartbeat_eventObject

Raises a heartbeat event. Called by the gateway connection handler used internally.


689
690
691
# File 'lib/discordrb/bot.rb', line 689

def raise_heartbeat_event
  raise_event(HeartbeatEvent.new(self))
end

#raw_tokenString

Returns the raw token, without any prefix

See Also:


225
226
227
# File 'lib/discordrb/bot.rb', line 225

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

#run(background = false) ⇒ Object

Note:

Running the bot in the background means that you can call some methods that require a gateway connection before that connection is established. In most cases an exception will be raised if you try to do this. If you need a way to safely run code after the bot is fully connected, use a EventContainer#ready event handler instead.

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


241
242
243
244
245
246
247
# File 'lib/discordrb/bot.rb', line 241

def run(background = false)
  @gateway.run_async
  return if background

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

#send_file(channel, file, caption: nil, tts: false, filename: nil, spoiler: nil) ⇒ 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.

Examples:

Send a file from disk

bot.send_file(83281822225530880, File.open('rubytaco.png', 'r'))

403
404
405
406
407
408
409
410
411
412
413
414
415
416
# File 'lib/discordrb/bot.rb', line 403

def send_file(channel, file, caption: nil, tts: false, filename: nil, spoiler: nil)
  if file.respond_to?(:read)
    if spoiler
      filename ||= File.basename(file.path)
      filename = 'SPOILER_' + filename unless filename.start_with? 'SPOILER_'
    end
    # https://github.com/rest-client/rest-client/blob/v2.0.2/lib/restclient/payload.rb#L160
    file.define_singleton_method(:original_filename) { filename } if filename
  end

  channel = channel.resolve_id
  response = API::Channel.upload_file(token, channel, file, caption: caption, tts: tts)
  Message.new(JSON.parse(response), self)
end

#send_message(channel, content, tts = false, embed = nil) ⇒ Message

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


366
367
368
369
370
371
372
# File 'lib/discordrb/bot.rb', line 366

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

  response = API::Channel.create_message(token, channel, content, tts, embed ? embed.to_hash : nil)
  Message.new(JSON.parse(response), self)
end

#send_temporary_message(channel, content, timeout, tts = false, embed = nil) ⇒ Object

Sends a text message to a channel given its ID and the message's content, then deletes it after the specified timeout in seconds.


381
382
383
384
385
386
387
388
389
390
391
# File 'lib/discordrb/bot.rb', line 381

def send_temporary_message(channel, content, timeout, tts = false, embed = nil)
  Thread.new do
    Thread.current[:discordrb_name] = "#{@current_thread}-temp-msg"

    message = send_message(channel, content, tts, embed)
    sleep(timeout)
    message.delete
  end

  nil
end

#serversHash<Integer => Server>

The list of servers the bot is currently in.


157
158
159
160
161
# File 'lib/discordrb/bot.rb', line 157

def servers
  gateway_check
  unavailable_servers_check
  @servers
end

#stop(no_sync = false) ⇒ Object

Stops the bot gracefully, disconnecting the websocket without immediately killing the thread. This means that Discord is immediately aware of the closed connection and makes the bot appear offline instantly.


261
262
263
# File 'lib/discordrb/bot.rb', line 261

def stop(no_sync = false)
  @gateway.stop(no_sync)
end

#stream(name, url) ⇒ String

Sets the currently online stream to the specified name and Twitch URL.


548
549
550
551
552
# File 'lib/discordrb/bot.rb', line 548

def stream(name, url)
  gateway_check
  update_status(@status, name, url)
  name
end

#suppress_ready_debugObject

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


594
595
596
# File 'lib/discordrb/bot.rb', line 594

def suppress_ready_debug
  @prevent_ready = true
end

#tokenString

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


218
219
220
221
# File 'lib/discordrb/bot.rb', line 218

def token
  API.bot_name = @name
  @token
end

#unignore_user(user) ⇒ Object

Remove a user from the ignore list.


662
663
664
# File 'lib/discordrb/bot.rb', line 662

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


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

#update_status(status, activity, url, since = 0, afk = false, activity_type = 0) ⇒ Object

Updates presence status.


503
504
505
506
507
508
509
510
511
512
513
514
515
516
# File 'lib/discordrb/bot.rb', line 503

def update_status(status, activity, url, since = 0, afk = false, activity_type = 0)
  gateway_check

  @activity = activity
  @status = status
  @streamurl = url
  type = url ? 1 : activity_type

  activity_obj = activity || url ? { 'name' => activity, 'url' => url, 'type' => type } : nil
  @gateway.send_status_update(status, since, activity_obj, afk)

  # Update the status in the cache
  profile.update_presence('status' => status.to_s, 'game' => activity_obj)
end

#usersHash<Integer => User>

The list of users the bot shares a server with.


149
150
151
152
153
# File 'lib/discordrb/bot.rb', line 149

def users
  gateway_check
  unavailable_servers_check
  @users
end

#voice(thing) ⇒ Voice::VoiceBot?

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


296
297
298
299
300
301
302
303
304
305
306
307
# File 'lib/discordrb/bot.rb', line 296

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 Voice::VoiceBot#destroy before calling this method.


317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
# File 'lib/discordrb/bot.rb', line 317

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}")

  @should_connect_to_voice[server_id] = chan
  @gateway.send_voice_state_update(server_id.to_s, chan.id.to_s, false, false)

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


345
346
347
348
349
350
# File 'lib/discordrb/bot.rb', line 345

def voice_destroy(server, destroy_vws = true)
  server = server.resolve_id
  @gateway.send_voice_state_update(server.to_s, nil, false, false)
  @voices[server].destroy if @voices[server] && destroy_vws
  @voices.delete(server)
end

#watching=(name) ⇒ String

Sets the current watching status to the specified name.


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

def watching=(name)
  gateway_check
  update_status(@status, name, nil, nil, nil, 3)
end