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.
Files changed (88) hide show
  1. checksums.yaml +7 -0
  2. data/lib/rubycord/allowed_mentions.rb +34 -0
  3. data/lib/rubycord/api/application.rb +200 -0
  4. data/lib/rubycord/api/channel.rb +597 -0
  5. data/lib/rubycord/api/interaction.rb +52 -0
  6. data/lib/rubycord/api/invite.rb +42 -0
  7. data/lib/rubycord/api/server.rb +557 -0
  8. data/lib/rubycord/api/user.rb +153 -0
  9. data/lib/rubycord/api/webhook.rb +138 -0
  10. data/lib/rubycord/api.rb +356 -0
  11. data/lib/rubycord/await.rb +49 -0
  12. data/lib/rubycord/bot.rb +1757 -0
  13. data/lib/rubycord/cache.rb +259 -0
  14. data/lib/rubycord/colour_rgb.rb +41 -0
  15. data/lib/rubycord/commands/command_bot.rb +519 -0
  16. data/lib/rubycord/commands/container.rb +110 -0
  17. data/lib/rubycord/commands/events.rb +9 -0
  18. data/lib/rubycord/commands/parser.rb +325 -0
  19. data/lib/rubycord/commands/rate_limiter.rb +142 -0
  20. data/lib/rubycord/container.rb +753 -0
  21. data/lib/rubycord/data/activity.rb +269 -0
  22. data/lib/rubycord/data/application.rb +48 -0
  23. data/lib/rubycord/data/attachment.rb +109 -0
  24. data/lib/rubycord/data/audit_logs.rb +343 -0
  25. data/lib/rubycord/data/channel.rb +996 -0
  26. data/lib/rubycord/data/component.rb +227 -0
  27. data/lib/rubycord/data/embed.rb +249 -0
  28. data/lib/rubycord/data/emoji.rb +80 -0
  29. data/lib/rubycord/data/integration.rb +120 -0
  30. data/lib/rubycord/data/interaction.rb +798 -0
  31. data/lib/rubycord/data/invite.rb +135 -0
  32. data/lib/rubycord/data/member.rb +370 -0
  33. data/lib/rubycord/data/message.rb +412 -0
  34. data/lib/rubycord/data/overwrite.rb +106 -0
  35. data/lib/rubycord/data/profile.rb +89 -0
  36. data/lib/rubycord/data/reaction.rb +31 -0
  37. data/lib/rubycord/data/recipient.rb +32 -0
  38. data/lib/rubycord/data/role.rb +246 -0
  39. data/lib/rubycord/data/server.rb +1002 -0
  40. data/lib/rubycord/data/user.rb +261 -0
  41. data/lib/rubycord/data/voice_region.rb +43 -0
  42. data/lib/rubycord/data/voice_state.rb +39 -0
  43. data/lib/rubycord/data/webhook.rb +232 -0
  44. data/lib/rubycord/data.rb +40 -0
  45. data/lib/rubycord/errors.rb +737 -0
  46. data/lib/rubycord/events/await.rb +46 -0
  47. data/lib/rubycord/events/bans.rb +58 -0
  48. data/lib/rubycord/events/channels.rb +186 -0
  49. data/lib/rubycord/events/generic.rb +126 -0
  50. data/lib/rubycord/events/guilds.rb +191 -0
  51. data/lib/rubycord/events/interactions.rb +480 -0
  52. data/lib/rubycord/events/invites.rb +123 -0
  53. data/lib/rubycord/events/lifetime.rb +29 -0
  54. data/lib/rubycord/events/members.rb +91 -0
  55. data/lib/rubycord/events/message.rb +337 -0
  56. data/lib/rubycord/events/presence.rb +127 -0
  57. data/lib/rubycord/events/raw.rb +45 -0
  58. data/lib/rubycord/events/reactions.rb +156 -0
  59. data/lib/rubycord/events/roles.rb +86 -0
  60. data/lib/rubycord/events/threads.rb +94 -0
  61. data/lib/rubycord/events/typing.rb +70 -0
  62. data/lib/rubycord/events/voice_server_update.rb +45 -0
  63. data/lib/rubycord/events/voice_state_update.rb +103 -0
  64. data/lib/rubycord/events/webhooks.rb +62 -0
  65. data/lib/rubycord/gateway.rb +867 -0
  66. data/lib/rubycord/id_object.rb +37 -0
  67. data/lib/rubycord/light/data.rb +60 -0
  68. data/lib/rubycord/light/integrations.rb +71 -0
  69. data/lib/rubycord/light/light_bot.rb +56 -0
  70. data/lib/rubycord/light.rb +6 -0
  71. data/lib/rubycord/logger.rb +118 -0
  72. data/lib/rubycord/paginator.rb +55 -0
  73. data/lib/rubycord/permissions.rb +251 -0
  74. data/lib/rubycord/version.rb +5 -0
  75. data/lib/rubycord/voice/encoder.rb +113 -0
  76. data/lib/rubycord/voice/network.rb +366 -0
  77. data/lib/rubycord/voice/sodium.rb +96 -0
  78. data/lib/rubycord/voice/voice_bot.rb +408 -0
  79. data/lib/rubycord/webhooks/builder.rb +100 -0
  80. data/lib/rubycord/webhooks/client.rb +132 -0
  81. data/lib/rubycord/webhooks/embeds.rb +248 -0
  82. data/lib/rubycord/webhooks/modal.rb +78 -0
  83. data/lib/rubycord/webhooks/version.rb +7 -0
  84. data/lib/rubycord/webhooks/view.rb +192 -0
  85. data/lib/rubycord/webhooks.rb +12 -0
  86. data/lib/rubycord/websocket.rb +70 -0
  87. data/lib/rubycord.rb +140 -0
  88. 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,6 @@
1
+ require "rubycord/light/light_bot"
2
+
3
+ # This module contains classes to allow connections to bots without a connection to the gateway socket, i.e. bots
4
+ # that only use the REST part of the API.
5
+ module Rubycord::Light
6
+ 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
@@ -0,0 +1,5 @@
1
+ # Rubycord and all its functionality, in this case only the version.
2
+ module Rubycord
3
+ # The current version of rubycord.
4
+ VERSION = "1.0.0" unless const_defined?(:VERSION)
5
+ end