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.



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

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

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



34
35
36
# File 'lib/discordrb/bot.rb', line 34

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.



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

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.



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

def profile
  @profile
end

#serversArray<Server> (readonly)

The list of servers the bot is currently in.

Returns:

  • (Array<Server>)

    The servers.



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

def servers
  @servers
end

#should_parse_selfObject

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



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

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.



39
40
41
# File 'lib/discordrb/bot.rb', line 39

def token
  @token
end

#usersArray<User> (readonly)

The list of users the bot shares a server with.

Returns:

  • (Array<User>)

    The users.



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

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



311
312
313
314
315
# File 'lib/discordrb/bot.rb', line 311

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: <<



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

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:



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

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.



150
151
152
153
154
155
156
157
# File 'lib/discordrb/bot.rb', line 150

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


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

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


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

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


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

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.



331
332
333
334
335
336
337
338
# File 'lib/discordrb/bot.rb', line 331

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



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

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.



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

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:



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

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

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



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

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.



228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
# File 'lib/discordrb/bot.rb', line 228

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.



255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
# File 'lib/discordrb/bot.rb', line 255

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_or_id) ⇒ Game

Sets the currently playing game to the specified game.

Parameters:

  • name_or_id (String, Fixnum)

    The name or the ID of the game to be played.

Returns:

  • (Game)

    The game object that is being played now.



352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
# File 'lib/discordrb/bot.rb', line 352

def game=(name_or_id)
  game = Discordrb::Games.find_game(name_or_id)
  @game = game

  data = {
    'op' => 3,
    'd' => {
      'idle_since' => nil,
      'game_id' => game ? game.id : 60 # 60 blanks out the game playing
    }
  }

  @ws.send(data.to_json)
  game
end

#handler_class(event_class) ⇒ Object



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

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:



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

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



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

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

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



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

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

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



465
466
467
# File 'lib/discordrb/bot.rb', line 465

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

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



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

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

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



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

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:



396
397
398
# File 'lib/discordrb/bot.rb', line 396

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.



343
344
345
346
347
# File 'lib/discordrb/bot.rb', line 343

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

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



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

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

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



412
413
414
# File 'lib/discordrb/bot.rb', line 412

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.



164
165
166
167
168
169
170
171
# File 'lib/discordrb/bot.rb', line 164

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



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

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

#remove_handler(handler) ⇒ Object



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

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.



182
183
184
185
186
# File 'lib/discordrb/bot.rb', line 182

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.



100
101
102
103
104
105
106
# File 'lib/discordrb/bot.rb', line 100

def run(async = false)
  run_async
  return if async

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

#run_asyncObject



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

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.



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

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.



277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
# File 'lib/discordrb/bot.rb', line 277

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

  # Replace mentions
  mentions = []
  content.gsub!(/<@([0-9]+)>/) do
    id = Regexp.last_match(1).to_i
    if @users[id]
      mentions << id
      "@#{@users[id].name}"
    else
      "<@#{id}}>"
    end
  end

  response = API.send_message(@token, channel_id, content, mentions)
  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.



216
217
218
# File 'lib/discordrb/bot.rb', line 216

def server(id)
  @servers[id]
end

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



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

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

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



477
478
479
# File 'lib/discordrb/bot.rb', line 477

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

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



473
474
475
# File 'lib/discordrb/bot.rb', line 473

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

#stopObject

Kills the websocket thread, stopping all connections to Discord.



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

def stop
  @ws_thread.kill
end

#syncObject

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



137
138
139
# File 'lib/discordrb/bot.rb', line 137

def sync
  @ws_thread.join
end

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



408
409
410
# File 'lib/discordrb/bot.rb', line 408

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.



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

def user(id)
  @users[id]
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


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

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