discordrb 1.8.1 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of discordrb might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/.overcommit.yml +7 -0
- data/.rubocop.yml +5 -4
- data/CHANGELOG.md +77 -0
- data/README.md +25 -15
- data/discordrb.gemspec +2 -3
- data/examples/commands.rb +14 -2
- data/examples/ping.rb +1 -1
- data/examples/pm_send.rb +1 -1
- data/lib/discordrb.rb +9 -0
- data/lib/discordrb/api.rb +176 -50
- data/lib/discordrb/await.rb +3 -0
- data/lib/discordrb/bot.rb +607 -372
- data/lib/discordrb/cache.rb +208 -0
- data/lib/discordrb/commands/command_bot.rb +50 -18
- data/lib/discordrb/commands/container.rb +11 -2
- data/lib/discordrb/commands/events.rb +2 -0
- data/lib/discordrb/commands/parser.rb +10 -8
- data/lib/discordrb/commands/rate_limiter.rb +2 -0
- data/lib/discordrb/container.rb +24 -25
- data/lib/discordrb/data.rb +521 -219
- data/lib/discordrb/errors.rb +6 -7
- data/lib/discordrb/events/await.rb +2 -0
- data/lib/discordrb/events/bans.rb +3 -1
- data/lib/discordrb/events/channels.rb +124 -0
- data/lib/discordrb/events/generic.rb +2 -0
- data/lib/discordrb/events/guilds.rb +16 -13
- data/lib/discordrb/events/lifetime.rb +12 -2
- data/lib/discordrb/events/members.rb +26 -15
- data/lib/discordrb/events/message.rb +20 -7
- data/lib/discordrb/events/presence.rb +18 -2
- data/lib/discordrb/events/roles.rb +83 -0
- data/lib/discordrb/events/typing.rb +15 -2
- data/lib/discordrb/events/voice_state_update.rb +2 -0
- data/lib/discordrb/light.rb +8 -0
- data/lib/discordrb/light/data.rb +62 -0
- data/lib/discordrb/light/integrations.rb +73 -0
- data/lib/discordrb/light/light_bot.rb +56 -0
- data/lib/discordrb/logger.rb +4 -0
- data/lib/discordrb/permissions.rb +16 -12
- data/lib/discordrb/token_cache.rb +3 -0
- data/lib/discordrb/version.rb +3 -1
- data/lib/discordrb/voice/encoder.rb +2 -0
- data/lib/discordrb/voice/network.rb +21 -14
- data/lib/discordrb/voice/voice_bot.rb +26 -3
- data/lib/discordrb/websocket.rb +69 -0
- metadata +15 -26
- data/lib/discordrb/events/channel_create.rb +0 -44
- data/lib/discordrb/events/channel_delete.rb +0 -44
- data/lib/discordrb/events/channel_update.rb +0 -46
- data/lib/discordrb/events/guild_role_create.rb +0 -35
- data/lib/discordrb/events/guild_role_delete.rb +0 -36
- data/lib/discordrb/events/guild_role_update.rb +0 -35
@@ -0,0 +1,83 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'discordrb/events/generic'
|
4
|
+
require 'discordrb/data'
|
5
|
+
|
6
|
+
module Discordrb::Events
|
7
|
+
# Raised when a role is created on a server
|
8
|
+
class ServerRoleCreateEvent < Event
|
9
|
+
# @return [Role] the role that got created
|
10
|
+
attr_reader :role
|
11
|
+
|
12
|
+
# @return [Server] the server on which a role got created
|
13
|
+
attr_reader :server
|
14
|
+
|
15
|
+
def initialize(data, bot)
|
16
|
+
@server = bot.server(data['guild_id'].to_i)
|
17
|
+
return unless @server
|
18
|
+
|
19
|
+
role_id = data['role']['id'].to_i
|
20
|
+
@role = @server.roles.find { |r| r.id == role_id }
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# Event handler for ServerRoleCreateEvent
|
25
|
+
class ServerRoleCreateEventHandler < EventHandler
|
26
|
+
def matches?(event)
|
27
|
+
# Check for the proper event type
|
28
|
+
return false unless event.is_a? ServerRoleCreateEvent
|
29
|
+
|
30
|
+
[
|
31
|
+
matches_all(@attributes[:name], event.name) do |a, e|
|
32
|
+
a == if a.is_a? String
|
33
|
+
e.to_s
|
34
|
+
else
|
35
|
+
e
|
36
|
+
end
|
37
|
+
end
|
38
|
+
].reduce(true, &:&)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# Raised when a role is deleted from a server
|
43
|
+
class ServerRoleDeleteEvent < Event
|
44
|
+
# @return [Integer] the ID of the role that got deleted.
|
45
|
+
attr_reader :id
|
46
|
+
|
47
|
+
# @return [Server] the server on which a role got deleted.
|
48
|
+
attr_reader :server
|
49
|
+
|
50
|
+
def initialize(data, bot)
|
51
|
+
# The role should already be deleted from the server's list
|
52
|
+
# by the time we create this event, so we'll create a temporary
|
53
|
+
# role object for event consumers to use.
|
54
|
+
@id = data['role_id'].to_i
|
55
|
+
server_id = data['guild_id'].to_i
|
56
|
+
@server = bot.server(server_id)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# EventHandler for ServerRoleDeleteEvent
|
61
|
+
class ServerRoleDeleteEventHandler < EventHandler
|
62
|
+
def matches?(event)
|
63
|
+
# Check for the proper event type
|
64
|
+
return false unless event.is_a? ServerRoleDeleteEvent
|
65
|
+
|
66
|
+
[
|
67
|
+
matches_all(@attributes[:name], event.name) do |a, e|
|
68
|
+
a == if a.is_a? String
|
69
|
+
e.to_s
|
70
|
+
else
|
71
|
+
e
|
72
|
+
end
|
73
|
+
end
|
74
|
+
].reduce(true, &:&)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
# Event raised when a role updates on a server
|
79
|
+
class ServerRoleUpdateEvent < ServerRoleCreateEvent; end
|
80
|
+
|
81
|
+
# Event handler for ServerRoleUpdateEvent
|
82
|
+
class ServerRoleUpdateEventHandler < ServerRoleCreateEventHandler; end
|
83
|
+
end
|
@@ -1,15 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'discordrb/events/generic'
|
2
4
|
|
3
5
|
module Discordrb::Events
|
4
6
|
# Event raised when a user starts typing
|
5
7
|
class TypingEvent < Event
|
6
|
-
|
8
|
+
# @return [Channel] the channel on which a user started typing.
|
9
|
+
attr_reader :channel
|
10
|
+
|
11
|
+
# @return [Member] the user that started typing.
|
12
|
+
attr_reader :user
|
13
|
+
alias_method :member, :user
|
14
|
+
|
15
|
+
# @return [Time] when the typing happened.
|
16
|
+
attr_reader :timestamp
|
7
17
|
|
8
18
|
def initialize(data, bot)
|
9
19
|
@user_id = data['user_id'].to_i
|
10
|
-
|
20
|
+
|
11
21
|
@channel_id = data['channel_id'].to_i
|
12
22
|
@channel = bot.channel(@channel_id)
|
23
|
+
|
24
|
+
@user = channel.private? ? channel.recipient : bot.member(@channel.server.id, @user_id)
|
25
|
+
|
13
26
|
@timestamp = Time.at(data['timestamp'].to_i)
|
14
27
|
end
|
15
28
|
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'discordrb/data'
|
4
|
+
|
5
|
+
module Discordrb::Light
|
6
|
+
# Represents the bot account used for the light bot, but without any methods to change anything.
|
7
|
+
class LightProfile
|
8
|
+
include Discordrb::IDObject
|
9
|
+
include Discordrb::UserAttributes
|
10
|
+
|
11
|
+
# @!visibility private
|
12
|
+
def initialize(data, bot)
|
13
|
+
@bot = bot
|
14
|
+
|
15
|
+
@username = data['username']
|
16
|
+
@id = data['id'].to_i
|
17
|
+
@discriminator = data['discriminator']
|
18
|
+
@avatar_id = data['avatar']
|
19
|
+
|
20
|
+
@bot_account = false
|
21
|
+
@bot_account = true if data['bot']
|
22
|
+
|
23
|
+
@verified = data['verified']
|
24
|
+
|
25
|
+
@email = data['email']
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# A server that only has an icon, a name, and an ID associated with it, like for example an integration's server.
|
30
|
+
class UltraLightServer
|
31
|
+
include Discordrb::IDObject
|
32
|
+
include Discordrb::ServerAttributes
|
33
|
+
|
34
|
+
# @!visibility private
|
35
|
+
def initialize(data, bot)
|
36
|
+
@bot = bot
|
37
|
+
|
38
|
+
@id = data['id'].to_i
|
39
|
+
|
40
|
+
@name = data['name']
|
41
|
+
@icon_id = data['icon']
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# Represents a light server which only has a fraction of the properties of any other server.
|
46
|
+
class LightServer < UltraLightServer
|
47
|
+
# @return [true, false] whether or not the LightBot this server belongs to is the owner of the server.
|
48
|
+
attr_reader :bot_is_owner
|
49
|
+
alias_method :bot_is_owner?, :bot_is_owner
|
50
|
+
|
51
|
+
# @return [Discordrb::Permissions] the permissions the LightBot has on this server
|
52
|
+
attr_reader :bot_permissions
|
53
|
+
|
54
|
+
# @!visibility private
|
55
|
+
def initialize(data, bot)
|
56
|
+
super(data, bot)
|
57
|
+
|
58
|
+
@bot_is_owner = data['owner']
|
59
|
+
@bot_permissions = Discordrb::Permissions.new(data['permissions'])
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'discordrb/data'
|
4
|
+
require 'discordrb/light/data'
|
5
|
+
|
6
|
+
module Discordrb::Light
|
7
|
+
# A connection of your Discord account to a particular other service (currently, Twitch and YouTube)
|
8
|
+
class Connection
|
9
|
+
# @return [Symbol] what type of connection this is (either :twitch or :youtube currently)
|
10
|
+
attr_reader :type
|
11
|
+
|
12
|
+
# @return [true, false] whether this connection is revoked
|
13
|
+
attr_reader :revoked
|
14
|
+
alias_method :revoked?, :revoked
|
15
|
+
|
16
|
+
# @return [String] the name of the connected account
|
17
|
+
attr_reader :name
|
18
|
+
|
19
|
+
# @return [String] the ID of the connected account
|
20
|
+
attr_reader :id
|
21
|
+
|
22
|
+
# @return [Array<Integration>] the integrations associated with this connection
|
23
|
+
attr_reader :integrations
|
24
|
+
|
25
|
+
# @!visibility private
|
26
|
+
def initialize(data, bot)
|
27
|
+
@bot = bot
|
28
|
+
|
29
|
+
@revoked = data['revoked']
|
30
|
+
@type = data['type'].to_sym
|
31
|
+
@name = data['name']
|
32
|
+
@id = data['id']
|
33
|
+
|
34
|
+
@integrations = data['integrations'].map { |e| Integration.new(e, self, bot) }
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# An integration of a connection into a particular server, for example being a member of a subscriber-only Twitch
|
39
|
+
# server.
|
40
|
+
class Integration
|
41
|
+
include Discordrb::IDObject
|
42
|
+
|
43
|
+
# @return [UltraLightServer] the server associated with this integration
|
44
|
+
attr_reader :server
|
45
|
+
|
46
|
+
# @note The connection returned by this method will have no integrations itself, as Discord doesn't provide that
|
47
|
+
# data. Also, it will always be considered not revoked.
|
48
|
+
# @return [Connection] the server's underlying connection (for a Twitch subscriber-only server, it would be the
|
49
|
+
# Twitch account connection of the server owner).
|
50
|
+
attr_reader :server_connection
|
51
|
+
|
52
|
+
# @return [Connection] the connection integrated with the server (i. e. your connection)
|
53
|
+
attr_reader :integrated_connection
|
54
|
+
|
55
|
+
# @!visibility private
|
56
|
+
def initialize(data, integrated, bot)
|
57
|
+
@bot = bot
|
58
|
+
@integrated_connection = integrated
|
59
|
+
|
60
|
+
@server = UltraLightServer.new(data['guild'], bot)
|
61
|
+
|
62
|
+
# Restructure the given data so we can reuse the Connection initializer
|
63
|
+
restructured = {}
|
64
|
+
|
65
|
+
restructured['type'] = data['type']
|
66
|
+
restructured['id'] = data['account']['id']
|
67
|
+
restructured['name'] = data['account']['name']
|
68
|
+
restructured['integrations'] = []
|
69
|
+
|
70
|
+
@server_connection = Connection.new(restructured, bot)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'discordrb/api'
|
4
|
+
require 'discordrb/light/data'
|
5
|
+
require 'discordrb/light/integrations'
|
6
|
+
|
7
|
+
# This module contains classes to allow connections to bots without a connection to the gateway socket, i. e. bots
|
8
|
+
# that only use the REST part of the API.
|
9
|
+
module Discordrb::Light
|
10
|
+
# A bot that only uses the REST part of the API. Hierarchically unrelated to the regular {Discordrb::Bot}. Useful to
|
11
|
+
# make applications integrated to Discord over OAuth, for example.
|
12
|
+
class LightBot
|
13
|
+
# Create a new LightBot. This does no networking yet, all networking is done by the methods on this class.
|
14
|
+
# @param token [String] The token that should be used to authenticate to Discord. Can be an OAuth token or a regular
|
15
|
+
# user account token.
|
16
|
+
def initialize(token)
|
17
|
+
if token.respond_to? :token
|
18
|
+
# Parse AccessTokens from the OAuth2 gem
|
19
|
+
token = token.token
|
20
|
+
end
|
21
|
+
|
22
|
+
unless token.include? '.'
|
23
|
+
# Discord user/bot tokens always contain two dots, so if there's none we can assume it's an OAuth token.
|
24
|
+
token = "Bearer #{token}" # OAuth tokens have to be prefixed with 'Bearer' for Discord to be able to use them
|
25
|
+
end
|
26
|
+
|
27
|
+
@token = token
|
28
|
+
end
|
29
|
+
|
30
|
+
# @return [LightProfile] the details of the user this bot is connected to.
|
31
|
+
def profile
|
32
|
+
response = Discordrb::API.profile(@token)
|
33
|
+
LightProfile.new(JSON.parse(response), self)
|
34
|
+
end
|
35
|
+
|
36
|
+
# @return [Array<LightServer>] the servers this bot is connected to.
|
37
|
+
def servers
|
38
|
+
response = Discordrb::API.servers(@token)
|
39
|
+
JSON.parse(response).map { |e| LightServer.new(e, self) }
|
40
|
+
end
|
41
|
+
|
42
|
+
# Joins a server using an instant invite.
|
43
|
+
# @param code [String] The code part of the invite (for example 0cDvIgU2voWn4BaD if the invite URL is
|
44
|
+
# https://discord.gg/0cDvIgU2voWn4BaD)
|
45
|
+
def join(code)
|
46
|
+
Discordrb::API.join_server(@token, code)
|
47
|
+
end
|
48
|
+
|
49
|
+
# Gets the connections associated with this account.
|
50
|
+
# @return [Array<Connection>] this account's connections.
|
51
|
+
def connections
|
52
|
+
response = Discordrb::API.connections(@token)
|
53
|
+
JSON.parse(response).map { |e| Connection.new(e, self) }
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
data/lib/discordrb/logger.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Discordrb
|
2
4
|
# The format log timestamps should be in, in strftime format
|
3
5
|
LOG_TIMESTAMP_FORMAT = '%Y-%m-%d %H:%M:%S.%L'.freeze
|
@@ -7,6 +9,8 @@ module Discordrb
|
|
7
9
|
# @return [true, false] whether this logger is in extra-fancy mode!
|
8
10
|
attr_writer :fancy
|
9
11
|
|
12
|
+
# Creates a new logger.
|
13
|
+
# @param fancy [true, false] Whether this logger uses fancy mode (ANSI escape codes to make the output colourful)
|
10
14
|
def initialize(fancy = false)
|
11
15
|
@fancy = fancy
|
12
16
|
self.mode = :normal
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Discordrb
|
2
4
|
# List of permissions Discord uses
|
3
5
|
class Permissions
|
@@ -8,7 +10,7 @@ module Discordrb
|
|
8
10
|
0 => :create_instant_invite, # 1
|
9
11
|
1 => :kick_members, # 2
|
10
12
|
2 => :ban_members, # 4
|
11
|
-
3 => :manage_roles, # 8
|
13
|
+
3 => :manage_roles, # 8, also Manage Permissions
|
12
14
|
4 => :manage_channels, # 16
|
13
15
|
5 => :manage_server, # 32
|
14
16
|
# 6 # 64
|
@@ -36,17 +38,15 @@ module Discordrb
|
|
36
38
|
Flags.each do |position, flag|
|
37
39
|
attr_reader flag
|
38
40
|
define_method "can_#{flag}=" do |value|
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
new_bits &= ~(1 << position)
|
45
|
-
end
|
46
|
-
@writer.write(new_bits)
|
47
|
-
@bits = new_bits
|
48
|
-
init_vars
|
41
|
+
new_bits = @bits
|
42
|
+
if value
|
43
|
+
new_bits |= (1 << position)
|
44
|
+
else
|
45
|
+
new_bits &= ~(1 << position)
|
49
46
|
end
|
47
|
+
@writer.write(new_bits) if @writer
|
48
|
+
@bits = new_bits
|
49
|
+
init_vars
|
50
50
|
end
|
51
51
|
end
|
52
52
|
|
@@ -67,7 +67,11 @@ module Discordrb
|
|
67
67
|
end
|
68
68
|
end
|
69
69
|
|
70
|
-
|
70
|
+
# Create a new Permissions object either as a blank slate to add permissions to (for example for
|
71
|
+
# {Channel#define_overwrite}) or from existing bit data to read out.
|
72
|
+
# @param bits [Integer] The permission bits that should be set from the beginning.
|
73
|
+
# @param writer [RoleWriter] The writer that should be used to update data when a permission is set.
|
74
|
+
def initialize(bits = 0, writer = nil)
|
71
75
|
@writer = writer
|
72
76
|
@bits = bits
|
73
77
|
init_vars
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'base64'
|
2
4
|
require 'json'
|
3
5
|
require 'openssl'
|
@@ -10,6 +12,7 @@ module Discordrb
|
|
10
12
|
|
11
13
|
# Represents a cached token with encryption data
|
12
14
|
class CachedToken
|
15
|
+
# Parse the cached token from the JSON data read from the file.
|
13
16
|
def initialize(data = nil)
|
14
17
|
if data
|
15
18
|
@verify_salt = Base64.decode64(data['verify_salt'])
|
data/lib/discordrb/version.rb
CHANGED
@@ -1,15 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'websocket-client-simple'
|
2
4
|
require 'resolv'
|
3
5
|
require 'socket'
|
4
6
|
require 'json'
|
5
7
|
|
8
|
+
require 'discordrb/websocket'
|
9
|
+
|
6
10
|
begin
|
7
|
-
|
8
|
-
|
11
|
+
RBNACL_AVAILABLE = false
|
12
|
+
unless ENV.key?('DISCORDRB_NONACL')
|
13
|
+
require 'rbnacl'
|
14
|
+
RBNACL_AVAILABLE = true
|
15
|
+
end
|
9
16
|
rescue LoadError
|
10
17
|
puts "libsodium not available! You can continue to use discordrb as normal but voice support won't work.
|
11
18
|
Read https://github.com/meew0/discordrb/wiki/Installing-libsodium for more details."
|
12
|
-
RBNACL_AVAILABLE = false
|
13
19
|
end
|
14
20
|
|
15
21
|
module Discordrb::Voice
|
@@ -42,7 +48,7 @@ module Discordrb::Voice
|
|
42
48
|
def connect(endpoint, port, ssrc)
|
43
49
|
@endpoint = endpoint
|
44
50
|
@endpoint = @endpoint[6..-1] if @endpoint.start_with? 'wss://'
|
45
|
-
@endpoint.gsub
|
51
|
+
@endpoint = @endpoint.gsub(':80', '') # The endpoint may contain a port, we don't want that
|
46
52
|
@endpoint = Resolv.getaddress @endpoint
|
47
53
|
|
48
54
|
@port = port
|
@@ -126,8 +132,7 @@ module Discordrb::Voice
|
|
126
132
|
@token = token
|
127
133
|
@session = session
|
128
134
|
|
129
|
-
@endpoint = endpoint
|
130
|
-
@endpoint.gsub!(':80', '')
|
135
|
+
@endpoint = endpoint.gsub(':80', '')
|
131
136
|
|
132
137
|
@udp = VoiceUDP.new
|
133
138
|
end
|
@@ -198,7 +203,7 @@ module Discordrb::Voice
|
|
198
203
|
Thread.current[:discordrb_name] = 'vws-i'
|
199
204
|
|
200
205
|
# Send the init packet
|
201
|
-
send_init(@channel.server.id, @bot.
|
206
|
+
send_init(@channel.server.id, @bot.profile.id, @session, @token)
|
202
207
|
end
|
203
208
|
|
204
209
|
# @!visibility private
|
@@ -285,15 +290,17 @@ module Discordrb::Voice
|
|
285
290
|
def init_ws
|
286
291
|
host = "wss://#{@endpoint}:443"
|
287
292
|
@bot.debug("Connecting VWS to host: #{host}")
|
288
|
-
@client = WebSocket::Client::Simple.connect(host)
|
289
293
|
|
290
|
-
#
|
291
|
-
|
294
|
+
# Connect the WS
|
295
|
+
@client = Discordrb::WebSocket.new(
|
296
|
+
host,
|
297
|
+
method(:websocket_open),
|
298
|
+
method(:websocket_message),
|
299
|
+
proc { |e| puts "VWS error: #{e}" },
|
300
|
+
proc { |e| puts "VWS close: #{e}" }
|
301
|
+
)
|
292
302
|
|
293
|
-
@
|
294
|
-
@client.on(:message) { |msg| instance.websocket_message(msg.data) }
|
295
|
-
@client.on(:error) { |e| puts "VWS error: #{e}" }
|
296
|
-
@client.on(:close) { |e| puts "VWS close: #{e}" }
|
303
|
+
@bot.debug('VWS connected')
|
297
304
|
|
298
305
|
# Block any further execution
|
299
306
|
heartbeat_loop
|