rubycord 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/lib/rubycord/allowed_mentions.rb +34 -0
- data/lib/rubycord/api/application.rb +200 -0
- data/lib/rubycord/api/channel.rb +597 -0
- data/lib/rubycord/api/interaction.rb +52 -0
- data/lib/rubycord/api/invite.rb +42 -0
- data/lib/rubycord/api/server.rb +557 -0
- data/lib/rubycord/api/user.rb +153 -0
- data/lib/rubycord/api/webhook.rb +138 -0
- data/lib/rubycord/api.rb +356 -0
- data/lib/rubycord/await.rb +49 -0
- data/lib/rubycord/bot.rb +1757 -0
- data/lib/rubycord/cache.rb +259 -0
- data/lib/rubycord/colour_rgb.rb +41 -0
- data/lib/rubycord/commands/command_bot.rb +519 -0
- data/lib/rubycord/commands/container.rb +110 -0
- data/lib/rubycord/commands/events.rb +9 -0
- data/lib/rubycord/commands/parser.rb +325 -0
- data/lib/rubycord/commands/rate_limiter.rb +142 -0
- data/lib/rubycord/container.rb +753 -0
- data/lib/rubycord/data/activity.rb +269 -0
- data/lib/rubycord/data/application.rb +48 -0
- data/lib/rubycord/data/attachment.rb +109 -0
- data/lib/rubycord/data/audit_logs.rb +343 -0
- data/lib/rubycord/data/channel.rb +996 -0
- data/lib/rubycord/data/component.rb +227 -0
- data/lib/rubycord/data/embed.rb +249 -0
- data/lib/rubycord/data/emoji.rb +80 -0
- data/lib/rubycord/data/integration.rb +120 -0
- data/lib/rubycord/data/interaction.rb +798 -0
- data/lib/rubycord/data/invite.rb +135 -0
- data/lib/rubycord/data/member.rb +370 -0
- data/lib/rubycord/data/message.rb +412 -0
- data/lib/rubycord/data/overwrite.rb +106 -0
- data/lib/rubycord/data/profile.rb +89 -0
- data/lib/rubycord/data/reaction.rb +31 -0
- data/lib/rubycord/data/recipient.rb +32 -0
- data/lib/rubycord/data/role.rb +246 -0
- data/lib/rubycord/data/server.rb +1002 -0
- data/lib/rubycord/data/user.rb +261 -0
- data/lib/rubycord/data/voice_region.rb +43 -0
- data/lib/rubycord/data/voice_state.rb +39 -0
- data/lib/rubycord/data/webhook.rb +232 -0
- data/lib/rubycord/data.rb +40 -0
- data/lib/rubycord/errors.rb +737 -0
- data/lib/rubycord/events/await.rb +46 -0
- data/lib/rubycord/events/bans.rb +58 -0
- data/lib/rubycord/events/channels.rb +186 -0
- data/lib/rubycord/events/generic.rb +126 -0
- data/lib/rubycord/events/guilds.rb +191 -0
- data/lib/rubycord/events/interactions.rb +480 -0
- data/lib/rubycord/events/invites.rb +123 -0
- data/lib/rubycord/events/lifetime.rb +29 -0
- data/lib/rubycord/events/members.rb +91 -0
- data/lib/rubycord/events/message.rb +337 -0
- data/lib/rubycord/events/presence.rb +127 -0
- data/lib/rubycord/events/raw.rb +45 -0
- data/lib/rubycord/events/reactions.rb +156 -0
- data/lib/rubycord/events/roles.rb +86 -0
- data/lib/rubycord/events/threads.rb +94 -0
- data/lib/rubycord/events/typing.rb +70 -0
- data/lib/rubycord/events/voice_server_update.rb +45 -0
- data/lib/rubycord/events/voice_state_update.rb +103 -0
- data/lib/rubycord/events/webhooks.rb +62 -0
- data/lib/rubycord/gateway.rb +867 -0
- data/lib/rubycord/id_object.rb +37 -0
- data/lib/rubycord/light/data.rb +60 -0
- data/lib/rubycord/light/integrations.rb +71 -0
- data/lib/rubycord/light/light_bot.rb +56 -0
- data/lib/rubycord/light.rb +6 -0
- data/lib/rubycord/logger.rb +118 -0
- data/lib/rubycord/paginator.rb +55 -0
- data/lib/rubycord/permissions.rb +251 -0
- data/lib/rubycord/version.rb +5 -0
- data/lib/rubycord/voice/encoder.rb +113 -0
- data/lib/rubycord/voice/network.rb +366 -0
- data/lib/rubycord/voice/sodium.rb +96 -0
- data/lib/rubycord/voice/voice_bot.rb +408 -0
- data/lib/rubycord/webhooks/builder.rb +100 -0
- data/lib/rubycord/webhooks/client.rb +132 -0
- data/lib/rubycord/webhooks/embeds.rb +248 -0
- data/lib/rubycord/webhooks/modal.rb +78 -0
- data/lib/rubycord/webhooks/version.rb +7 -0
- data/lib/rubycord/webhooks/view.rb +192 -0
- data/lib/rubycord/webhooks.rb +12 -0
- data/lib/rubycord/websocket.rb +70 -0
- data/lib/rubycord.rb +140 -0
- metadata +231 -0
@@ -0,0 +1,37 @@
|
|
1
|
+
module Rubycord
|
2
|
+
# Mixin for objects that have IDs
|
3
|
+
module IDObject
|
4
|
+
# @return [Integer] the ID which uniquely identifies this object across Discord.
|
5
|
+
attr_reader :id
|
6
|
+
alias_method :resolve_id, :id
|
7
|
+
alias_method :hash, :id
|
8
|
+
|
9
|
+
# ID based comparison
|
10
|
+
def ==(other)
|
11
|
+
Rubycord.id_compare(@id, other)
|
12
|
+
end
|
13
|
+
|
14
|
+
alias_method :eql?, :==
|
15
|
+
|
16
|
+
# Estimates the time this object was generated on based on the beginning of the ID. This is fairly accurate but
|
17
|
+
# shouldn't be relied on as Discord might change its algorithm at any time
|
18
|
+
# @return [Time] when this object was created at
|
19
|
+
def creation_time
|
20
|
+
# Milliseconds
|
21
|
+
ms = (@id >> 22) + DISCORD_EPOCH
|
22
|
+
Time.at(ms / 1000.0)
|
23
|
+
end
|
24
|
+
|
25
|
+
# Creates an artificial snowflake at the given point in time. Useful for comparing against.
|
26
|
+
# @param time [Time] The time the snowflake should represent.
|
27
|
+
# @return [Integer] a snowflake with the timestamp data as the given time
|
28
|
+
def self.synthesise(time)
|
29
|
+
ms = (time.to_f * 1000).to_i
|
30
|
+
(ms - DISCORD_EPOCH) << 22
|
31
|
+
end
|
32
|
+
|
33
|
+
class << self
|
34
|
+
alias_method :synthesize, :synthesise
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require "rubycord/data"
|
2
|
+
|
3
|
+
module Rubycord::Light
|
4
|
+
# Represents the bot account used for the light bot, but without any methods to change anything.
|
5
|
+
class LightProfile
|
6
|
+
include Rubycord::IDObject
|
7
|
+
include Rubycord::UserAttributes
|
8
|
+
|
9
|
+
# @!visibility private
|
10
|
+
def initialize(data, bot)
|
11
|
+
@bot = bot
|
12
|
+
|
13
|
+
@username = data["username"]
|
14
|
+
@id = data["id"].to_i
|
15
|
+
@discriminator = data["discriminator"]
|
16
|
+
@avatar_id = data["avatar"]
|
17
|
+
|
18
|
+
@bot_account = false
|
19
|
+
@bot_account = true if data["bot"]
|
20
|
+
|
21
|
+
@verified = data["verified"]
|
22
|
+
|
23
|
+
@email = data["email"]
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# A server that only has an icon, a name, and an ID associated with it, like for example an integration's server.
|
28
|
+
class UltraLightServer
|
29
|
+
include Rubycord::IDObject
|
30
|
+
include Rubycord::ServerAttributes
|
31
|
+
|
32
|
+
# @!visibility private
|
33
|
+
def initialize(data, bot)
|
34
|
+
@bot = bot
|
35
|
+
|
36
|
+
@id = data["id"].to_i
|
37
|
+
|
38
|
+
@name = data["name"]
|
39
|
+
@icon_id = data["icon"]
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# Represents a light server which only has a fraction of the properties of any other server.
|
44
|
+
class LightServer < UltraLightServer
|
45
|
+
# @return [true, false] whether or not the LightBot this server belongs to is the owner of the server.
|
46
|
+
attr_reader :bot_is_owner
|
47
|
+
alias_method :bot_is_owner?, :bot_is_owner
|
48
|
+
|
49
|
+
# @return [Rubycord::Permissions] the permissions the LightBot has on this server
|
50
|
+
attr_reader :bot_permissions
|
51
|
+
|
52
|
+
# @!visibility private
|
53
|
+
def initialize(data, bot)
|
54
|
+
super
|
55
|
+
|
56
|
+
@bot_is_owner = data["owner"]
|
57
|
+
@bot_permissions = Rubycord::Permissions.new(data["permissions"])
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
require "rubycord/data"
|
2
|
+
require "rubycord/light/data"
|
3
|
+
|
4
|
+
module Rubycord::Light
|
5
|
+
# A connection of your Discord account to a particular other service (currently, Twitch and YouTube)
|
6
|
+
class Connection
|
7
|
+
# @return [Symbol] what type of connection this is (either :twitch or :youtube currently)
|
8
|
+
attr_reader :type
|
9
|
+
|
10
|
+
# @return [true, false] whether this connection is revoked
|
11
|
+
attr_reader :revoked
|
12
|
+
alias_method :revoked?, :revoked
|
13
|
+
|
14
|
+
# @return [String] the name of the connected account
|
15
|
+
attr_reader :name
|
16
|
+
|
17
|
+
# @return [String] the ID of the connected account
|
18
|
+
attr_reader :id
|
19
|
+
|
20
|
+
# @return [Array<Integration>] the integrations associated with this connection
|
21
|
+
attr_reader :integrations
|
22
|
+
|
23
|
+
# @!visibility private
|
24
|
+
def initialize(data, bot)
|
25
|
+
@bot = bot
|
26
|
+
|
27
|
+
@revoked = data["revoked"]
|
28
|
+
@type = data["type"].to_sym
|
29
|
+
@name = data["name"]
|
30
|
+
@id = data["id"]
|
31
|
+
|
32
|
+
@integrations = data["integrations"].map { |e| Integration.new(e, self, bot) }
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# An integration of a connection into a particular server, for example being a member of a subscriber-only Twitch
|
37
|
+
# server.
|
38
|
+
class Integration
|
39
|
+
include Rubycord::IDObject
|
40
|
+
|
41
|
+
# @return [UltraLightServer] the server associated with this integration
|
42
|
+
attr_reader :server
|
43
|
+
|
44
|
+
# @note The connection returned by this method will have no integrations itself, as Discord doesn't provide that
|
45
|
+
# data. Also, it will always be considered not revoked.
|
46
|
+
# @return [Connection] the server's underlying connection (for a Twitch subscriber-only server, it would be the
|
47
|
+
# Twitch account connection of the server owner).
|
48
|
+
attr_reader :server_connection
|
49
|
+
|
50
|
+
# @return [Connection] the connection integrated with the server (i.e. your connection)
|
51
|
+
attr_reader :integrated_connection
|
52
|
+
|
53
|
+
# @!visibility private
|
54
|
+
def initialize(data, integrated, bot)
|
55
|
+
@bot = bot
|
56
|
+
@integrated_connection = integrated
|
57
|
+
|
58
|
+
@server = UltraLightServer.new(data["guild"], bot)
|
59
|
+
|
60
|
+
# Restructure the given data so we can reuse the Connection initializer
|
61
|
+
restructured = {}
|
62
|
+
|
63
|
+
restructured["type"] = data["type"]
|
64
|
+
restructured["id"] = data["account"]["id"]
|
65
|
+
restructured["name"] = data["account"]["name"]
|
66
|
+
restructured["integrations"] = []
|
67
|
+
|
68
|
+
@server_connection = Connection.new(restructured, bot)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
require "rubycord/api"
|
2
|
+
require "rubycord/api/invite"
|
3
|
+
require "rubycord/api/user"
|
4
|
+
require "rubycord/light/data"
|
5
|
+
require "rubycord/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 Rubycord::Light
|
10
|
+
# A bot that only uses the REST part of the API. Hierarchically unrelated to the regular {Rubycord::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 = Rubycord::API::User.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 = Rubycord::API::User.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
|
+
Rubycord::API::Invite.accept(@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 = Rubycord::API::User.connections(@token)
|
53
|
+
JSON.parse(response).map { |e| Connection.new(e, self) }
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,118 @@
|
|
1
|
+
module Rubycord
|
2
|
+
# The format log timestamps should be in, in strftime format
|
3
|
+
LOG_TIMESTAMP_FORMAT = "%Y-%m-%d %H:%M:%S.%L"
|
4
|
+
|
5
|
+
# Logs debug messages
|
6
|
+
class Logger
|
7
|
+
# @return [true, false] whether this logger is in extra-fancy mode!
|
8
|
+
attr_writer :fancy
|
9
|
+
|
10
|
+
# @return [String, nil] The bot token to be redacted or nil if it shouldn't.
|
11
|
+
attr_writer :token
|
12
|
+
|
13
|
+
# @return [Array<IO>, Array<#puts & #flush>] the streams the logger should write to.
|
14
|
+
attr_accessor :streams
|
15
|
+
|
16
|
+
# Creates a new logger.
|
17
|
+
# @param fancy [true, false] Whether this logger uses fancy mode (ANSI escape codes to make the output colourful)
|
18
|
+
# @param streams [Array<IO>, Array<#puts & #flush>] the streams the logger should write to.
|
19
|
+
def initialize(fancy = false, streams = [$stdout])
|
20
|
+
@fancy = fancy
|
21
|
+
self.mode = :normal
|
22
|
+
|
23
|
+
@streams = streams
|
24
|
+
end
|
25
|
+
|
26
|
+
# The modes this logger can have. This is probably useless unless you want to write your own Logger
|
27
|
+
MODES = {
|
28
|
+
debug: {long: "DEBUG", short: "D", format_code: ""},
|
29
|
+
good: {long: "GOOD", short: "✓", format_code: "\u001B[32m"}, # green
|
30
|
+
info: {long: "INFO", short: "i", format_code: ""},
|
31
|
+
warn: {long: "WARN", short: "!", format_code: "\u001B[33m"}, # yellow
|
32
|
+
error: {long: "ERROR", short: "✗", format_code: "\u001B[31m"}, # red
|
33
|
+
out: {long: "OUT", short: "→", format_code: "\u001B[36m"}, # cyan
|
34
|
+
in: {long: "IN", short: "←", format_code: "\u001B[35m"}, # purple
|
35
|
+
ratelimit: {long: "RATELIMIT", short: "R", format_code: "\u001B[41m"} # red background
|
36
|
+
}.freeze
|
37
|
+
|
38
|
+
# The ANSI format code that resets formatting
|
39
|
+
FORMAT_RESET = "\u001B[0m"
|
40
|
+
|
41
|
+
# The ANSI format code that makes something bold
|
42
|
+
FORMAT_BOLD = "\u001B[1m"
|
43
|
+
|
44
|
+
MODES.each do |mode, hash|
|
45
|
+
define_method(mode) do |message|
|
46
|
+
write(message.to_s, hash) if @enabled_modes.include? mode
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# Sets the logging mode to :debug
|
51
|
+
# @param value [true, false] Whether debug mode should be on. If it is off the mode will be set to :normal.
|
52
|
+
def debug=(value)
|
53
|
+
self.mode = value ? :debug : :normal
|
54
|
+
end
|
55
|
+
|
56
|
+
# Sets the logging mode
|
57
|
+
# Possible modes are:
|
58
|
+
# * :debug logs everything
|
59
|
+
# * :verbose logs everything except for debug messages
|
60
|
+
# * :normal logs useful information, warnings and errors
|
61
|
+
# * :quiet only logs warnings and errors
|
62
|
+
# * :silent logs nothing
|
63
|
+
# @param value [Symbol] What logging mode to use
|
64
|
+
def mode=(value)
|
65
|
+
case value
|
66
|
+
when :debug
|
67
|
+
@enabled_modes = %i[debug good info warn error out in ratelimit]
|
68
|
+
when :verbose
|
69
|
+
@enabled_modes = %i[good info warn error out in ratelimit]
|
70
|
+
when :normal
|
71
|
+
@enabled_modes = %i[info warn error ratelimit]
|
72
|
+
when :quiet
|
73
|
+
@enabled_modes = %i[warn error]
|
74
|
+
when :silent
|
75
|
+
@enabled_modes = %i[]
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
# Logs an exception to the console.
|
80
|
+
# @param e [Exception] The exception to log.
|
81
|
+
def log_exception(e)
|
82
|
+
error("Exception: #{e.inspect}")
|
83
|
+
e.backtrace.each { |line| error(line) }
|
84
|
+
end
|
85
|
+
|
86
|
+
private
|
87
|
+
|
88
|
+
def write(message, mode)
|
89
|
+
thread_name = Thread.current[:rubycord_name]
|
90
|
+
timestamp = Time.now.strftime(LOG_TIMESTAMP_FORMAT)
|
91
|
+
|
92
|
+
# Redact token if set
|
93
|
+
log = if @token && @token != ""
|
94
|
+
message.to_s.gsub(@token, "REDACTED_TOKEN")
|
95
|
+
else
|
96
|
+
message.to_s
|
97
|
+
end
|
98
|
+
|
99
|
+
@streams.each do |stream|
|
100
|
+
if @fancy && !stream.is_a?(File)
|
101
|
+
fancy_write(stream, log, mode, thread_name, timestamp)
|
102
|
+
else
|
103
|
+
simple_write(stream, log, mode, thread_name, timestamp)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def fancy_write(stream, message, mode, thread_name, timestamp)
|
109
|
+
stream.puts "#{timestamp} #{FORMAT_BOLD}#{thread_name.ljust(16)}#{FORMAT_RESET} #{mode[:format_code]}#{mode[:short]}#{FORMAT_RESET} #{message}"
|
110
|
+
stream.flush
|
111
|
+
end
|
112
|
+
|
113
|
+
def simple_write(stream, message, mode, thread_name, timestamp)
|
114
|
+
stream.puts "[#{mode[:long]} : #{thread_name} @ #{timestamp}] #{message}"
|
115
|
+
stream.flush
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module Rubycord
|
2
|
+
# Utility class for wrapping paginated endpoints. It is [Enumerable](https://ruby-doc.org/core-2.5.1/Enumerable.html),
|
3
|
+
# similar to an `Array`, so most of the same methods can be used to filter the results of the request
|
4
|
+
# that it wraps. If you simply want an array of all of the results, `#to_a` can be called.
|
5
|
+
class Paginator
|
6
|
+
include Enumerable
|
7
|
+
|
8
|
+
# Creates a new {Paginator}
|
9
|
+
# @param limit [Integer] the maximum number of items to request before stopping
|
10
|
+
# @param direction [:up, :down] the order in which results are returned in
|
11
|
+
# @yield [Array, nil] the last page of results, or nil if this is the first iteration.
|
12
|
+
# This should be used to request the next page of results.
|
13
|
+
# @yieldreturn [Array] the next page of results
|
14
|
+
def initialize(limit, direction, &block)
|
15
|
+
@count = 0
|
16
|
+
@limit = limit
|
17
|
+
@direction = direction
|
18
|
+
@block = block
|
19
|
+
end
|
20
|
+
|
21
|
+
# Yields every item produced by the wrapped request, until it returns
|
22
|
+
# no more results or the configured `limit` is reached.
|
23
|
+
def each
|
24
|
+
last_page = nil
|
25
|
+
until limit_check
|
26
|
+
page = @block.call(last_page)
|
27
|
+
return if page.empty?
|
28
|
+
|
29
|
+
enumerator = case @direction
|
30
|
+
when :down
|
31
|
+
page.each
|
32
|
+
when :up
|
33
|
+
page.reverse_each
|
34
|
+
end
|
35
|
+
|
36
|
+
enumerator.each do |item|
|
37
|
+
yield item
|
38
|
+
@count += 1
|
39
|
+
break if limit_check
|
40
|
+
end
|
41
|
+
|
42
|
+
last_page = page
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
# Whether the paginator limit has been exceeded
|
49
|
+
def limit_check
|
50
|
+
return false if @limit.nil?
|
51
|
+
|
52
|
+
@count >= @limit
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,251 @@
|
|
1
|
+
module Rubycord
|
2
|
+
# List of permissions Discord uses
|
3
|
+
class Permissions
|
4
|
+
# This hash maps bit positions to logical permissions.
|
5
|
+
# see https://discord.com/developers/docs/topics/permissions#permissions-bitwise-permission-flags
|
6
|
+
FLAGS = {
|
7
|
+
# Bit => Permission # Value
|
8
|
+
0 => :create_instant_invite, # 1
|
9
|
+
1 => :kick_members, # 2
|
10
|
+
2 => :ban_members, # 4
|
11
|
+
3 => :administrator, # 8
|
12
|
+
4 => :manage_channels, # 16
|
13
|
+
5 => :manage_server, # 32
|
14
|
+
6 => :add_reactions, # 64
|
15
|
+
7 => :view_audit_log, # 128
|
16
|
+
8 => :priority_speaker, # 256
|
17
|
+
9 => :stream, # 512
|
18
|
+
10 => :view_channel, # 1024 (which includes reading messages in text channels and joining voice channels)
|
19
|
+
11 => :send_messages, # 2048
|
20
|
+
12 => :send_tts_messages, # 4096
|
21
|
+
13 => :manage_messages, # 8192
|
22
|
+
14 => :embed_links, # 16384
|
23
|
+
15 => :attach_files, # 32768
|
24
|
+
16 => :read_message_history, # 65536
|
25
|
+
17 => :mention_everyone, # 131072
|
26
|
+
18 => :use_external_emojis, # 262144
|
27
|
+
19 => :view_server_insights, # 524288
|
28
|
+
20 => :connect, # 1048576
|
29
|
+
21 => :speak, # 2097152
|
30
|
+
22 => :mute_members, # 4194304
|
31
|
+
23 => :deafen_members, # 8388608
|
32
|
+
24 => :move_members, # 16777216
|
33
|
+
25 => :use_vad, # 33554432 (voice-activity-detection in a voice channel)
|
34
|
+
26 => :change_nickname, # 67108864
|
35
|
+
27 => :manage_nicknames, # 134217728
|
36
|
+
28 => :manage_roles, # 268435456 (roles & roles permissions)
|
37
|
+
29 => :manage_webhooks, # 536870912
|
38
|
+
30 => :manage_server_expressions, # 1073741824 (emojis, stickers, and soundboard)
|
39
|
+
31 => :use_application_commands, # 2147483648 (to use application commands, including slash commands and context menu commands)
|
40
|
+
32 => :request_to_speak, # 4294967296
|
41
|
+
33 => :manage_events, # 8589934592
|
42
|
+
34 => :manage_threads, # 17179869184
|
43
|
+
35 => :create_public_threads, # 34359738368 (for creating public and announcement threads)
|
44
|
+
36 => :create_private_threads, # 68719476736
|
45
|
+
37 => :use_external_stickers, # 137438953472
|
46
|
+
38 => :send_messages_in_threads, # 274877906944
|
47
|
+
39 => :use_embedded_activities, # 549755813888
|
48
|
+
40 => :moderate_members, # 1099511627776
|
49
|
+
41 => :view_creator_monetization_analytics, # 2199023255552
|
50
|
+
42 => :use_soundboard, # 4398046511104
|
51
|
+
43 => :create_server_expressions, # 8796093022208 (for creating emojis, stickers, and soundboard sounds, and editing and deleting)
|
52
|
+
44 => :create_events, # 17592186044416 (for creating scheduled events, and editing and deleting those created by the current user)
|
53
|
+
45 => :use_external_sounds, # 35184372088832
|
54
|
+
46 => :send_voice_messages, # 70368744177664
|
55
|
+
49 => :send_polls, # 562949953421312
|
56
|
+
50 => :use_external_apps # 1125899906842624
|
57
|
+
}.freeze
|
58
|
+
|
59
|
+
FLAGS.each do |position, flag|
|
60
|
+
attr_reader flag
|
61
|
+
|
62
|
+
define_method :"can_#{flag}=" do |value|
|
63
|
+
new_bits = @bits
|
64
|
+
if value
|
65
|
+
new_bits |= (1 << position)
|
66
|
+
else
|
67
|
+
new_bits &= ~(1 << position)
|
68
|
+
end
|
69
|
+
@writer&.write(new_bits)
|
70
|
+
@bits = new_bits
|
71
|
+
init_vars
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
alias_method :can_administrate=, :can_administrator=
|
76
|
+
alias_method :administrate, :administrator
|
77
|
+
|
78
|
+
attr_reader :bits
|
79
|
+
|
80
|
+
# Set the raw bitset of this permission object
|
81
|
+
# @param bits [Integer] A number whose binary representation is the desired bitset.
|
82
|
+
def bits=(bits)
|
83
|
+
@bits = bits
|
84
|
+
init_vars
|
85
|
+
end
|
86
|
+
|
87
|
+
# Initialize the instance variables based on the bitset.
|
88
|
+
def init_vars
|
89
|
+
FLAGS.each do |position, flag|
|
90
|
+
flag_set = ((@bits >> position) & 0x1) == 1
|
91
|
+
instance_variable_set :"@#{flag}", flag_set
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
# Return the corresponding bits for an array of permission flag symbols.
|
96
|
+
# This is a class method that can be used to calculate bits instead
|
97
|
+
# of instancing a new Permissions object.
|
98
|
+
# @example Get the bits for permissions that could allow/deny read messages, connect, and speak
|
99
|
+
# Permissions.bits [:view_channel, :connect, :speak] #=> 3146752
|
100
|
+
# @param list [Array<Symbol>]
|
101
|
+
# @return [Integer] the computed permissions integer
|
102
|
+
def self.bits(list)
|
103
|
+
value = 0
|
104
|
+
|
105
|
+
FLAGS.each do |position, flag|
|
106
|
+
value += 2**position if list.include? flag
|
107
|
+
end
|
108
|
+
|
109
|
+
value
|
110
|
+
end
|
111
|
+
|
112
|
+
# Create a new Permissions object either as a blank slate to add permissions to (for example for
|
113
|
+
# {Channel#define_overwrite}) or from existing bit data to read out.
|
114
|
+
# @example Create a permissions object that could allow/deny read messages, connect, and speak by setting flags
|
115
|
+
# permission = Permissions.new
|
116
|
+
# permission.can_view_channel = true
|
117
|
+
# permission.can_connect = true
|
118
|
+
# permission.can_speak = true
|
119
|
+
# @example Create a permissions object that could allow/deny read messages, connect, and speak by an array of symbols
|
120
|
+
# Permissions.new [:view_channel, :connect, :speak]
|
121
|
+
# @param bits [String, Integer, Array<Symbol>] The permission bits that should be set from the beginning, or an array of permission flag symbols
|
122
|
+
# @param writer [RoleWriter] The writer that should be used to update data when a permission is set.
|
123
|
+
def initialize(bits = 0, writer = nil)
|
124
|
+
@writer = writer
|
125
|
+
|
126
|
+
@bits = if bits.is_a? Array
|
127
|
+
self.class.bits(bits)
|
128
|
+
else
|
129
|
+
bits.to_i
|
130
|
+
end
|
131
|
+
|
132
|
+
init_vars
|
133
|
+
end
|
134
|
+
|
135
|
+
# Return an array of permission flag symbols for this class's permissions
|
136
|
+
# @example Get the permissions for the bits "9"
|
137
|
+
# permissions = Permissions.new(9)
|
138
|
+
# permissions.defined_permissions #=> [:create_instant_invite, :administrator]
|
139
|
+
# @return [Array<Symbol>] the permissions
|
140
|
+
def defined_permissions
|
141
|
+
FLAGS.filter_map { |value, name| (@bits & (1 << value)).positive? ? name : nil }
|
142
|
+
end
|
143
|
+
|
144
|
+
# Comparison based on permission bits
|
145
|
+
def ==(other)
|
146
|
+
false unless other.is_a? Rubycord::Permissions
|
147
|
+
bits == other.bits
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
# Mixin to calculate resulting permissions from overrides etc.
|
152
|
+
module PermissionCalculator
|
153
|
+
# Checks whether this user can do the particular action, regardless of whether it has the permission defined,
|
154
|
+
# through for example being the server owner or having the Manage Roles permission
|
155
|
+
# @param action [Symbol] The permission that should be checked. See also {Permissions::FLAGS} for a list.
|
156
|
+
# @param channel [Channel, nil] If channel overrides should be checked too, this channel specifies where the overrides should be checked.
|
157
|
+
# @example Check if the bot can send messages to a specific channel in a server.
|
158
|
+
# bot_profile = bot.profile.on(event.server)
|
159
|
+
# can_send_messages = bot_profile.permission?(:send_messages, channel)
|
160
|
+
# @return [true, false] whether or not this user has the permission.
|
161
|
+
def permission?(action, channel = nil)
|
162
|
+
# If the member is the server owner, it irrevocably has all permissions.
|
163
|
+
return true if owner?
|
164
|
+
|
165
|
+
# First, check whether the user has Manage Roles defined.
|
166
|
+
# (Coincidentally, Manage Permissions is the same permission as Manage Roles, and a
|
167
|
+
# Manage Permissions deny overwrite will override Manage Roles, so we can just check for
|
168
|
+
# Manage Roles once and call it a day.)
|
169
|
+
return true if defined_permission?(:administrator, channel)
|
170
|
+
|
171
|
+
# Otherwise, defer to defined_permission
|
172
|
+
defined_permission?(action, channel)
|
173
|
+
end
|
174
|
+
|
175
|
+
# Checks whether this user has a particular permission defined (i.e. not implicit, through for example
|
176
|
+
# Manage Roles)
|
177
|
+
# @param action [Symbol] The permission that should be checked. See also {Permissions::FLAGS} for a list.
|
178
|
+
# @param channel [Channel, nil] If channel overrides should be checked too, this channel specifies where the overrides should be checked.
|
179
|
+
# @example Check if a member has the Manage Channels permission defined in the server.
|
180
|
+
# has_manage_channels = member.defined_permission?(:manage_channels)
|
181
|
+
# @return [true, false] whether or not this user has the permission defined.
|
182
|
+
def defined_permission?(action, channel = nil)
|
183
|
+
# For slash commands we may not have access to the server or role
|
184
|
+
# permissions. In this case we use the permissions given to us by the
|
185
|
+
# interaction. If attempting to check against a specific channel the check
|
186
|
+
# is skipped.
|
187
|
+
return @permissions.__send__(action) if @permissions && channel.nil?
|
188
|
+
|
189
|
+
# Get the permission the user's roles have
|
190
|
+
role_permission = defined_role_permission?(action, channel)
|
191
|
+
|
192
|
+
# Once we have checked the role permission, we have to check the channel overrides for the
|
193
|
+
# specific user
|
194
|
+
user_specific_override = permission_overwrite(action, channel, id) # Use the ID reader as members have no ID instance variable
|
195
|
+
|
196
|
+
# Merge the two permissions - if an override is defined, it has to be allow, otherwise we only care about the role
|
197
|
+
return role_permission unless user_specific_override
|
198
|
+
|
199
|
+
user_specific_override == :allow
|
200
|
+
end
|
201
|
+
|
202
|
+
# Define methods for querying permissions
|
203
|
+
Rubycord::Permissions::FLAGS.each_value do |flag|
|
204
|
+
define_method :"can_#{flag}?" do |channel = nil|
|
205
|
+
permission? flag, channel
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
alias_method :can_administrate?, :can_administrator?
|
210
|
+
|
211
|
+
private
|
212
|
+
|
213
|
+
def defined_role_permission?(action, channel)
|
214
|
+
roles_to_check = [@server.everyone_role] + roles
|
215
|
+
|
216
|
+
# For each role, check if
|
217
|
+
# (1) the channel explicitly allows or permits an action for the role and
|
218
|
+
# (2) if the user is allowed to do the action if the channel doesn't specify
|
219
|
+
roles_to_check.sort_by(&:position).reduce(false) do |can_act, role|
|
220
|
+
# Get the override defined for the role on the channel
|
221
|
+
channel_allow = permission_overwrite(action, channel, role.id)
|
222
|
+
if channel_allow
|
223
|
+
# If the channel has an override, check whether it is an allow - if yes,
|
224
|
+
# the user can act, if not, it can't
|
225
|
+
break true if channel_allow == :allow
|
226
|
+
|
227
|
+
false
|
228
|
+
else
|
229
|
+
# Otherwise defer to the role
|
230
|
+
role.permissions.instance_variable_get(:"@#{action}") || can_act
|
231
|
+
end
|
232
|
+
end
|
233
|
+
end
|
234
|
+
|
235
|
+
def permission_overwrite(action, channel, id)
|
236
|
+
# If no overwrites are defined, or no channel is set, no overwrite will be present
|
237
|
+
return nil unless channel && channel.permission_overwrites[id]
|
238
|
+
|
239
|
+
# Otherwise, check the allow and deny objects
|
240
|
+
allow = channel.permission_overwrites[id].allow
|
241
|
+
deny = channel.permission_overwrites[id].deny
|
242
|
+
if allow.instance_variable_get(:"@#{action}")
|
243
|
+
:allow
|
244
|
+
elsif deny.instance_variable_get(:"@#{action}")
|
245
|
+
:deny
|
246
|
+
end
|
247
|
+
|
248
|
+
# If there's no variable defined, nil will implicitly be returned
|
249
|
+
end
|
250
|
+
end
|
251
|
+
end
|