rubycord 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|