Class: Discordrb::Bot

Inherits:
Object
  • Object
show all
Includes:
Events
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 Events

matches_all

Constructor Details

#initialize(email, password, debug = false) ⇒ Bot

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

Parameters:

  • email (String)

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

  • password (String)

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

  • debug (Boolean) (defaults to: false)

    Whether or not the bug should run in debug mode, which gives increased console output.



70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
# File 'lib/discordrb/bot.rb', line 70

def initialize(email, password, debug = false)
  # Make sure people replace the login details in the example files...
  if email.end_with? 'example.com'
    puts 'You have to replace the login details in the example files with your own!'
    exit
  end

  LOGGER.debug = debug
  @should_parse_self = false

  @email = email
  @password = password

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

  @event_handlers = {}

  @channels = {}
  @users = {}

  @awaits = {}

  @event_threads = []
  @current_thread = 0
end

Instance Attribute Details

#bot_userUser (readonly)

The user that represents the bot itself. This version will always be identical to the user determined by #user called with the bot's ID.

Returns:

  • (User)

    The bot user.



36
37
38
# File 'lib/discordrb/bot.rb', line 36

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

Returns:

  • (Array<Thread>)

    The threads.



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

def event_threads
  @event_threads
end

#profileProfile (readonly)

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.



61
62
63
# File 'lib/discordrb/bot.rb', line 61

def profile
  @profile
end

#serversArray<Server> (readonly)

The list of servers the bot is currently in.

Returns:

  • (Array<Server>)

    The servers.



49
50
51
# File 'lib/discordrb/bot.rb', line 49

def servers
  @servers
end

#should_parse_selfObject

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



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

def should_parse_self
  @should_parse_self
end

#tokenString (readonly)

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

Returns:

  • (String)

    The API token.



41
42
43
# File 'lib/discordrb/bot.rb', line 41

def token
  @token
end

#usersArray<User> (readonly)

The list of users the bot shares a server with.

Returns:

  • (Array<User>)

    The users.



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

def users
  @users
end

#voiceObject (readonly)

Returns the value of attribute voice.



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

def voice
  @voice
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 #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.



337
338
339
340
341
# File 'lib/discordrb/bot.rb', line 337

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

#add_handler(handler) ⇒ Object Also known as: <<



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

def add_handler(handler)
  clazz = event_class(handler.class)
  @event_handlers[clazz] << handler
end

#await(attributes = {}) {|event| ... } ⇒ AwaitEventHandler

This event is raised when an Await is triggered. It provides an easy way to execute code on an await without having to rely on the await's block.

Parameters:

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

    The event's attributes.

Options Hash (attributes):

  • :key (Symbol)

    Exactly matches the await's key.

  • :type (Class)

    Exactly matches the event's type.

Yields:

  • The block is executed when the event is raised.

Yield Parameters:

  • event (AwaitEvent)

    The event that was raised.

Returns:



518
519
520
# File 'lib/discordrb/bot.rb', line 518

def await(attributes = {}, &block)
  register_event(AwaitEvent, attributes, block)
end

#channel(id) ⇒ Channel

Gets a channel given its ID. This queries the internal channel cache, and if the channel doesn't exist in there, it will get the data from Discord.

Parameters:

  • id (Integer)

    The channel ID for which to search for.

Returns:

  • (Channel)

    The channel identified by the ID.



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

def channel(id)
  debug("Obtaining data for channel with id #{id}")
  return @channels[id] if @channels[id]

  response = API.channel(@token, id)
  channel = Channel.new(JSON.parse(response), self)
  @channels[id] = channel
end

#channel_create(attributes = {}, &block) ⇒ Object

Handle channel creation Attributes:

  • type: Channel type ('text' or 'voice')
  • name: Channel name


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

def channel_create(attributes = {}, &block)
  register_event(ChannelCreateEvent, attributes, block)
end

#channel_delete(attributes = {}, &block) ⇒ Object

Handle channel deletion Attributes:

  • type: Channel type ('text' or 'voice')
  • name: Channel name


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

def channel_delete(attributes = {}, &block)
  register_event(ChannelDeleteEvent, attributes, block)
end

#channel_update(attributes = {}, &block) ⇒ Object

Handle channel update Attributes:

  • type: Channel type ('text' or 'voice')
  • name: Channel name


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

def channel_update(attributes = {}, &block)
  register_event(ChannelUpdateEvent, attributes, block)
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
    • :singapore
    • :sydney

Returns:

  • (Server)

    The server that was created.



357
358
359
360
361
362
363
364
# File 'lib/discordrb/bot.rb', line 357

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, important = false) ⇒ Object



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

def debug(message, important = false)
  LOGGER.debug(message, important)
end

#debug=(new_debug) ⇒ Object

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



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

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:



237
238
239
240
# File 'lib/discordrb/bot.rb', line 237

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

#disconnected(attributes = {}, &block) ⇒ Object



429
430
431
# File 'lib/discordrb/bot.rb', line 429

def disconnected(attributes = {}, &block)
  register_event(DisconnectEvent, attributes, block)
end

#find(channel_name, server_name = nil, threshold = 0) ⇒ Array<Channel>

Finds a channel given its name and optionally the name of the server it is in. If the threshold is not 0, it will use a Levenshtein distance function to find the channel in a fuzzy way, which allows slight misspellings.

Parameters:

  • channel_name (String)

    The channel to search for.

  • server_name (String) (defaults to: nil)

    The server to search for, or nil if only the channel should be searched for.

  • threshold (Integer) (defaults to: 0)

    The threshold for the Levenshtein algorithm. The larger the threshold is, the more misspellings will be allowed.

Returns:

  • (Array<Channel>)

    The array of channels that were found. May be empty if none were found.



266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
# File 'lib/discordrb/bot.rb', line 266

def find(channel_name, server_name = nil, threshold = 0)
  require 'levenshtein'

  results = []
  @servers.values.each do |server|
    server.channels.each do |channel|
      distance = Levenshtein.distance(channel.name, channel_name)
      distance += Levenshtein.distance(server_name || server.name, server.name)
      next if distance > threshold

      # Make a singleton accessor "distance"
      channel.instance_variable_set(:@distance, distance)
      class << channel
        attr_reader :distance
      end
      results << channel
    end
  end
  results
end

#find_user(username, threshold = 0) ⇒ Array<User>

Finds a user given its username. This allows fuzzy finding using Levenshtein distances, see #find

Parameters:

  • username (String)

    The username to look for.

  • threshold (Integer) (defaults to: 0)

    The threshold for the Levenshtein algorithm. The larger the threshold is, the more misspellings will be allowed.

Returns:

  • (Array<User>)

    The array of users that were found. May be empty if none were found.



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

def find_user(username, threshold = 0)
  require 'levenshtein'

  results = []
  @users.values.each do |user|
    distance = Levenshtein.distance(user.username, username)
    next if distance > threshold

    # Make a singleton accessor "distance"
    user.instance_variable_set(:@distance, distance)
    class << user
      attr_reader :distance
    end
    results << user
  end
  results
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.



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

def game=(name)
  @game = name

  data = {
    'op' => 3,
    'd' => {
      'idle_since' => nil,
      'game' => name ? { 'name' => name } : nil
    }
  }

  @ws.send(data.to_json)
  name
end

#handler_class(event_class) ⇒ Object



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

def handler_class(event_class)
  class_from_string(event_class.to_s + 'Handler')
end

#join(invite) ⇒ Object

Makes the bot join an invite to a server.

Parameters:



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

def join(invite)
  invite = resolve_invite_code(invite)
  resolved = JSON.parse(API.resolve_invite(@token, invite))['code']
  API.join_server(@token, resolved)
end

#log_exception(e) ⇒ Object



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

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

#member_join(attributes = {}, &block) ⇒ Object



486
487
488
# File 'lib/discordrb/bot.rb', line 486

def member_join(attributes = {}, &block)
  register_event(GuildMemberAddEvent, attributes, block)
end

#member_leave(attributes = {}, &block) ⇒ Object



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

def member_leave(attributes = {}, &block)
  register_event(GuildMemberDeleteEvent, attributes, block)
end

#member_update(attributes = {}, &block) ⇒ Object



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

def member_update(attributes = {}, &block)
  register_event(GuildMemberUpdateEvent, attributes, block)
end

#mention(attributes = {}, &block) ⇒ Object



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

def mention(attributes = {}, &block)
  register_event(MentionEvent, attributes, block)
end

#message(attributes = {}) {|event| ... } ⇒ MessageEventHandler

This event is raised when a message is sent to a text channel the bot is currently in.

Parameters:

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

    The event's attributes.

Options Hash (attributes):

  • :start_with (String, Regexp)

    Matches the string the message starts with.

  • :end_with (String, Regexp)

    Matches the string the message ends with.

  • :contains (String, Regexp)

    Matches a string the message contains.

  • :in (String, Integer, Channel)

    Matches the channel the message was sent in.

  • :from (String, Integer, User)

    Matches the user that sent the message.

  • :content (String)

    Exactly matches the entire content of the message.

  • :content (String)

    Exactly matches the entire content of the message.

  • :after (Time)

    Matches a time after the time the message was sent at.

  • :before (Time)

    Matches a time before the time the message was sent at.

  • :private (Boolean)

    Matches whether or not the channel is private.

Yields:

  • The block is executed when the event is raised.

Yield Parameters:

Returns:



421
422
423
# File 'lib/discordrb/bot.rb', line 421

def message(attributes = {}, &block)
  register_event(MessageEvent, attributes, block)
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.



369
370
371
372
373
# File 'lib/discordrb/bot.rb', line 369

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

#playing(attributes = {}, &block) ⇒ Object



441
442
443
# File 'lib/discordrb/bot.rb', line 441

def playing(attributes = {}, &block)
  register_event(PlayingEvent, attributes, block)
end

#pm(attributes = {}, &block) ⇒ Object Also known as: private_message



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

def pm(attributes = {}, &block)
  register_event(PrivateMessageEvent, attributes, block)
end

#presence(attributes = {}, &block) ⇒ Object



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

def presence(attributes = {}, &block)
  register_event(PresenceEvent, attributes, block)
end

#private_channel(id) ⇒ Channel

Creates a private channel for the given user ID, or if one exists already, returns that one. It is recommended that you use User#pm instead, as this is mainly for internal use. However, usage of this method may be unavoidable if only the user ID is known.

Parameters:

  • id (Integer)

    The user ID to generate a private channel for.

Returns:

  • (Channel)

    A private channel for that user.



169
170
171
172
173
174
175
176
# File 'lib/discordrb/bot.rb', line 169

def private_channel(id)
  debug("Creating private channel with user id #{id}")
  return @private_channels[id] if @private_channels[id]

  response = API.create_private(@token, @bot_user.id, id)
  channel = Channel.new(JSON.parse(response), self)
  @private_channels[id] = channel
end

#ready(attributes = {}, &block) ⇒ Object



425
426
427
# File 'lib/discordrb/bot.rb', line 425

def ready(attributes = {}, &block)
  register_event(ReadyEvent, attributes, block)
end

#remove_handler(handler) ⇒ Object



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

def remove_handler(handler)
  clazz = event_class(handler.class)
  @event_handlers[clazz].delete(handler)
end

#resolve_invite_code(invite) ⇒ String

Gets the code for an invite.

Parameters:

  • invite (String, Invite)

    The invite to get the code for. Possible formats are:

    • An Invite object
    • The code for an invite
    • A fully qualified invite URL (e. g. https://discordapp.com/invite/0A37aN7fasF7n83q)
    • A short invite URL with protocol (e. g. https://discord.gg/0A37aN7fasF7n83q)
    • A short invite URL without protocol (e. g. discord.gg/0A37aN7fasF7n83q)

Returns:

  • (String)

    Only the code for the invite.



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

def resolve_invite_code(invite)
  invite = invite.code if invite.is_a? Discordrb::Invite
  invite = invite[invite.rindex('/') + 1..-1] if invite.start_with?('http') || invite.start_with?('discord.gg')
  invite
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.



105
106
107
108
109
110
111
# File 'lib/discordrb/bot.rb', line 105

def run(async = false)
  run_async
  return if async

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

#run_asyncObject



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
# File 'lib/discordrb/bot.rb', line 113

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'
    loop do
      websocket_connect
      debug('Disconnected! Attempting to reconnect in 5 seconds.')
      sleep 5
      @token = 
    end
  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.



326
327
328
# File 'lib/discordrb/bot.rb', line 326

def send_file(channel_id, file)
  API.send_file(@token, channel_id, file)
end

#send_message(channel_id, content) ⇒ 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).

Returns:

  • (Message)

    The message that was sent.



315
316
317
318
319
320
# File 'lib/discordrb/bot.rb', line 315

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

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

#server(id) ⇒ Server?

Note:

This can only resolve servers the bot is currently in.

Gets a server by its ID.

Parameters:

  • id (Integer)

    The server ID that should be resolved.

Returns:

  • (Server, nil)

    The server identified by the ID, or nil if it couldn't be found.



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

def server(id)
  @servers[id]
end

#server_create(attributes = {}, &block) ⇒ Object



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

def server_create(attributes = {}, &block)
  register_event(GuildCreateEvent, attributes, block)
end

#server_delete(attributes = {}, &block) ⇒ Object



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

def server_delete(attributes = {}, &block)
  register_event(GuildDeleteEvent, attributes, block)
end

#server_update(attributes = {}, &block) ⇒ Object



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

def server_update(attributes = {}, &block)
  register_event(GuildUpdateEvent, attributes, block)
end

#stopObject

Kills the websocket thread, stopping all connections to Discord.



147
148
149
# File 'lib/discordrb/bot.rb', line 147

def stop
  @ws_thread.kill
end

#syncObject

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



142
143
144
# File 'lib/discordrb/bot.rb', line 142

def sync
  @ws_thread.join
end

#typing(attributes = {}, &block) ⇒ Object



433
434
435
# File 'lib/discordrb/bot.rb', line 433

def typing(attributes = {}, &block)
  register_event(TypingEvent, attributes, block)
end

#user(id) ⇒ User?

Note:

This can only resolve users known by the bot (i.e. that share a server with the bot).

Gets a user by its ID.

Parameters:

  • id (Integer)

    The user ID that should be resolved.

Returns:

  • (User, nil)

    The user identified by the ID, or nil if it couldn't be found.



246
247
248
# File 'lib/discordrb/bot.rb', line 246

def user(id)
  @users[id]
end

#voice_connect(chan) ⇒ Object



203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
# File 'lib/discordrb/bot.rb', line 203

def voice_connect(chan)
  if @voice
    debug('Voice bot exists already! Destroying it')
    @voice.destroy
    @voice = nil
  end

  chan = channel(chan) if chan.is_a? Integer
  @voice_channel = chan
  debug("Got voice channel: #{@voice_channel}")

  data = {
    op: 4,
    d: {
      guild_id: @voice_channel.server.id.to_s,
      channel_id: @voice_channel.id.to_s,
      self_mute: false,
      self_deaf: false
    }
  }
  debug("Voice channel init packet is: #{data.to_json}")

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

  sleep(0.05) until @voice
  debug('Voice connect succeeded!')
  @voice
end

#voice_state_update(attributes = {}, &block) ⇒ Object

Handle a change to a voice state. This includes joining a voice channel or changing mute or deaf state. Attributes:

  • from: User whose voice state changed
  • mute: server mute status
  • deaf: server deaf status
  • self_mute: self mute status
  • self_deaf: self deaf status
  • channel: channel the user joined


482
483
484
# File 'lib/discordrb/bot.rb', line 482

def voice_state_update(attributes = {}, &block)
  register_event(VoiceStateUpdateEvent, attributes, block)
end