onyxcord 1.1.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 (133) hide show
  1. checksums.yaml +7 -0
  2. data/.devcontainer/Dockerfile +13 -0
  3. data/.devcontainer/devcontainer.json +29 -0
  4. data/.devcontainer/postcreate.sh +4 -0
  5. data/.github/CONTRIBUTING.md +13 -0
  6. data/.github/ISSUE_TEMPLATE/bug_report.md +38 -0
  7. data/.github/ISSUE_TEMPLATE/feature_request.md +24 -0
  8. data/.github/pull_request_template.md +37 -0
  9. data/.github/workflows/ci.yml +78 -0
  10. data/.github/workflows/codeql.yml +65 -0
  11. data/.github/workflows/deploy.yml +54 -0
  12. data/.github/workflows/release.yml +51 -0
  13. data/.gitignore +16 -0
  14. data/.markdownlint.json +4 -0
  15. data/.overcommit.yml +7 -0
  16. data/.rspec +2 -0
  17. data/.rubocop.yml +129 -0
  18. data/.yardopts +1 -0
  19. data/CHANGELOG.md +0 -0
  20. data/Gemfile +7 -0
  21. data/LICENSE.txt +21 -0
  22. data/README.md +305 -0
  23. data/Rakefile +17 -0
  24. data/bin/console +15 -0
  25. data/bin/setup +7 -0
  26. data/lib/onyxcord/allowed_mentions.rb +43 -0
  27. data/lib/onyxcord/api/application.rb +316 -0
  28. data/lib/onyxcord/api/channel.rb +700 -0
  29. data/lib/onyxcord/api/interaction.rb +67 -0
  30. data/lib/onyxcord/api/invite.rb +44 -0
  31. data/lib/onyxcord/api/server.rb +775 -0
  32. data/lib/onyxcord/api/user.rb +158 -0
  33. data/lib/onyxcord/api/webhook.rb +163 -0
  34. data/lib/onyxcord/api.rb +335 -0
  35. data/lib/onyxcord/await.rb +51 -0
  36. data/lib/onyxcord/bot.rb +1971 -0
  37. data/lib/onyxcord/cache.rb +326 -0
  38. data/lib/onyxcord/colour_rgb.rb +43 -0
  39. data/lib/onyxcord/commands/command_bot.rb +511 -0
  40. data/lib/onyxcord/commands/container.rb +112 -0
  41. data/lib/onyxcord/commands/events.rb +11 -0
  42. data/lib/onyxcord/commands/parser.rb +327 -0
  43. data/lib/onyxcord/commands/rate_limiter.rb +144 -0
  44. data/lib/onyxcord/configuration.rb +125 -0
  45. data/lib/onyxcord/container.rb +988 -0
  46. data/lib/onyxcord/data/activity.rb +271 -0
  47. data/lib/onyxcord/data/application.rb +341 -0
  48. data/lib/onyxcord/data/attachment.rb +91 -0
  49. data/lib/onyxcord/data/audit_logs.rb +438 -0
  50. data/lib/onyxcord/data/avatar_decoration.rb +26 -0
  51. data/lib/onyxcord/data/call.rb +22 -0
  52. data/lib/onyxcord/data/channel.rb +1355 -0
  53. data/lib/onyxcord/data/channel_tag.rb +69 -0
  54. data/lib/onyxcord/data/collectibles.rb +47 -0
  55. data/lib/onyxcord/data/component.rb +583 -0
  56. data/lib/onyxcord/data/embed.rb +258 -0
  57. data/lib/onyxcord/data/emoji.rb +123 -0
  58. data/lib/onyxcord/data/install_params.rb +24 -0
  59. data/lib/onyxcord/data/integration.rb +144 -0
  60. data/lib/onyxcord/data/interaction.rb +1141 -0
  61. data/lib/onyxcord/data/invite.rb +137 -0
  62. data/lib/onyxcord/data/member.rb +528 -0
  63. data/lib/onyxcord/data/message.rb +612 -0
  64. data/lib/onyxcord/data/message_activity.rb +41 -0
  65. data/lib/onyxcord/data/overwrite.rb +109 -0
  66. data/lib/onyxcord/data/poll.rb +365 -0
  67. data/lib/onyxcord/data/primary_server.rb +60 -0
  68. data/lib/onyxcord/data/profile.rb +79 -0
  69. data/lib/onyxcord/data/reaction.rb +64 -0
  70. data/lib/onyxcord/data/recipient.rb +34 -0
  71. data/lib/onyxcord/data/role.rb +449 -0
  72. data/lib/onyxcord/data/role_connection_data.rb +69 -0
  73. data/lib/onyxcord/data/role_subscription.rb +41 -0
  74. data/lib/onyxcord/data/scheduled_event.rb +513 -0
  75. data/lib/onyxcord/data/server.rb +1614 -0
  76. data/lib/onyxcord/data/server_preview.rb +68 -0
  77. data/lib/onyxcord/data/snapshot.rb +112 -0
  78. data/lib/onyxcord/data/team.rb +98 -0
  79. data/lib/onyxcord/data/timestamp.rb +69 -0
  80. data/lib/onyxcord/data/user.rb +324 -0
  81. data/lib/onyxcord/data/voice_region.rb +46 -0
  82. data/lib/onyxcord/data/voice_state.rb +41 -0
  83. data/lib/onyxcord/data/webhook.rb +238 -0
  84. data/lib/onyxcord/data.rb +57 -0
  85. data/lib/onyxcord/errors.rb +246 -0
  86. data/lib/onyxcord/event_executor.rb +80 -0
  87. data/lib/onyxcord/events/await.rb +48 -0
  88. data/lib/onyxcord/events/bans.rb +60 -0
  89. data/lib/onyxcord/events/channels.rb +225 -0
  90. data/lib/onyxcord/events/generic.rb +129 -0
  91. data/lib/onyxcord/events/guilds.rb +269 -0
  92. data/lib/onyxcord/events/integrations.rb +100 -0
  93. data/lib/onyxcord/events/interactions.rb +624 -0
  94. data/lib/onyxcord/events/invites.rb +127 -0
  95. data/lib/onyxcord/events/lifetime.rb +31 -0
  96. data/lib/onyxcord/events/members.rb +110 -0
  97. data/lib/onyxcord/events/message.rb +399 -0
  98. data/lib/onyxcord/events/polls.rb +118 -0
  99. data/lib/onyxcord/events/presence.rb +131 -0
  100. data/lib/onyxcord/events/raw.rb +74 -0
  101. data/lib/onyxcord/events/reactions.rb +218 -0
  102. data/lib/onyxcord/events/roles.rb +87 -0
  103. data/lib/onyxcord/events/scheduled_events.rb +171 -0
  104. data/lib/onyxcord/events/threads.rb +100 -0
  105. data/lib/onyxcord/events/typing.rb +73 -0
  106. data/lib/onyxcord/events/voice_server_update.rb +48 -0
  107. data/lib/onyxcord/events/voice_state_update.rb +106 -0
  108. data/lib/onyxcord/events/webhooks.rb +65 -0
  109. data/lib/onyxcord/gateway.rb +890 -0
  110. data/lib/onyxcord/id_object.rb +39 -0
  111. data/lib/onyxcord/light/data.rb +62 -0
  112. data/lib/onyxcord/light/integrations.rb +73 -0
  113. data/lib/onyxcord/light/light_bot.rb +58 -0
  114. data/lib/onyxcord/light.rb +8 -0
  115. data/lib/onyxcord/logger.rb +120 -0
  116. data/lib/onyxcord/message_components.rb +70 -0
  117. data/lib/onyxcord/paginator.rb +60 -0
  118. data/lib/onyxcord/permissions.rb +255 -0
  119. data/lib/onyxcord/rate_limiter/gateway.rb +42 -0
  120. data/lib/onyxcord/rate_limiter/rest.rb +89 -0
  121. data/lib/onyxcord/version.rb +7 -0
  122. data/lib/onyxcord/voice/encoder.rb +115 -0
  123. data/lib/onyxcord/voice/network.rb +380 -0
  124. data/lib/onyxcord/voice/opcodes.rb +29 -0
  125. data/lib/onyxcord/voice/sodium.rb +157 -0
  126. data/lib/onyxcord/voice/timer.rb +19 -0
  127. data/lib/onyxcord/voice/voice_bot.rb +386 -0
  128. data/lib/onyxcord/webhooks.rb +14 -0
  129. data/lib/onyxcord/websocket.rb +62 -0
  130. data/lib/onyxcord.rb +180 -0
  131. data/onyxcord-webhooks.gemspec +30 -0
  132. data/onyxcord.gemspec +50 -0
  133. metadata +421 -0
@@ -0,0 +1,255 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OnyxCord
4
+ # List of permissions Discord uses
5
+ class Permissions
6
+ # This hash maps bit positions to logical permissions.
7
+ FLAGS = {
8
+ # Bit => Permission # Value
9
+ 0 => :create_instant_invite, # 1
10
+ 1 => :kick_members, # 2
11
+ 2 => :ban_members, # 4
12
+ 3 => :administrator, # 8
13
+ 4 => :manage_channels, # 16
14
+ 5 => :manage_server, # 32
15
+ 6 => :add_reactions, # 64
16
+ 7 => :view_audit_log, # 128
17
+ 8 => :priority_speaker, # 256
18
+ 9 => :stream, # 512
19
+ 10 => :read_messages, # 1024
20
+ 11 => :send_messages, # 2048
21
+ 12 => :send_tts_messages, # 4096
22
+ 13 => :manage_messages, # 8192
23
+ 14 => :embed_links, # 16384
24
+ 15 => :attach_files, # 32768
25
+ 16 => :read_message_history, # 65536
26
+ 17 => :mention_everyone, # 131072
27
+ 18 => :use_external_emoji, # 262144
28
+ 19 => :view_server_insights, # 524288
29
+ 20 => :connect, # 1048576
30
+ 21 => :speak, # 2097152
31
+ 22 => :mute_members, # 4194304
32
+ 23 => :deafen_members, # 8388608
33
+ 24 => :move_members, # 16777216
34
+ 25 => :use_voice_activity, # 33554432
35
+ 26 => :change_nickname, # 67108864
36
+ 27 => :manage_nicknames, # 134217728
37
+ 28 => :manage_roles, # 268435456, also Manage Permissions
38
+ 29 => :manage_webhooks, # 536870912
39
+ 30 => :manage_emojis, # 1073741824, also Manage Stickers
40
+ 31 => :use_slash_commands, # 2147483648
41
+ 32 => :request_to_speak, # 4294967296
42
+ 33 => :manage_events, # 8589934592
43
+ 34 => :manage_threads, # 17179869184
44
+ 35 => :use_public_threads, # 34359738368
45
+ 36 => :use_private_threads, # 68719476736
46
+ 37 => :use_external_stickers, # 137438953472
47
+ 38 => :send_messages_in_threads, # 274877906944
48
+ 39 => :use_embedded_activities, # 549755813888
49
+ 40 => :moderate_members, # 1099511627776
50
+ 41 => :view_monetization_analytics, # 2199023255552
51
+ 42 => :use_soundboard, # 4398046511104
52
+ 43 => :create_server_expressions, # 8796093022208
53
+ 44 => :create_scheduled_events, # 17592186044416
54
+ 45 => :use_external_sounds, # 35184372088832
55
+ 46 => :send_voice_messages, # 70368744177664
56
+ 49 => :send_polls, # 562949953421312
57
+ 50 => :use_external_apps, # 1125899906842624
58
+ 51 => :pin_messages, # 2251799813685248
59
+ 52 => :bypass_slowmode # 4503599627370496
60
+ }.freeze
61
+
62
+ FLAGS.each do |position, flag|
63
+ attr_reader flag
64
+
65
+ define_method "can_#{flag}=" do |value|
66
+ new_bits = @bits
67
+ if value
68
+ new_bits |= (1 << position)
69
+ else
70
+ new_bits &= ~(1 << position)
71
+ end
72
+ @writer&.write(new_bits)
73
+ @bits = new_bits
74
+ init_vars
75
+ end
76
+ end
77
+
78
+ alias_method :can_administrate=, :can_administrator=
79
+ alias_method :administrate, :administrator
80
+
81
+ attr_reader :bits
82
+
83
+ # Set the raw bitset of this permission object
84
+ # @param bits [Integer] A number whose binary representation is the desired bitset.
85
+ def bits=(bits)
86
+ @bits = bits
87
+ init_vars
88
+ end
89
+
90
+ # Initialize the instance variables based on the bitset.
91
+ def init_vars
92
+ FLAGS.each do |position, flag|
93
+ flag_set = (@bits >> position).allbits?(0x1)
94
+ instance_variable_set "@#{flag}", flag_set
95
+ end
96
+ end
97
+
98
+ # Return the corresponding bits for an array of permission flag symbols.
99
+ # This is a class method that can be used to calculate bits instead
100
+ # of instancing a new Permissions object.
101
+ # @example Get the bits for permissions that could allow/deny read messages, connect, and speak
102
+ # Permissions.bits [:read_messages, :connect, :speak] #=> 3146752
103
+ # @param list [Array<Symbol>]
104
+ # @return [Integer] the computed permissions integer
105
+ def self.bits(list)
106
+ value = 0
107
+
108
+ FLAGS.each do |position, flag|
109
+ value += 2**position if list.include? flag
110
+ end
111
+
112
+ value
113
+ end
114
+
115
+ # Create a new Permissions object either as a blank slate to add permissions to (for example for
116
+ # {Channel#define_overwrite}) or from existing bit data to read out.
117
+ # @example Create a permissions object that could allow/deny read messages, connect, and speak by setting flags
118
+ # permission = Permissions.new
119
+ # permission.can_read_messages = true
120
+ # permission.can_connect = true
121
+ # permission.can_speak = true
122
+ # @example Create a permissions object that could allow/deny read messages, connect, and speak by an array of symbols
123
+ # Permissions.new [:read_messages, :connect, :speak]
124
+ # @param bits [String, Integer, Array<Symbol>] The permission bits that should be set from the beginning, or an array of permission flag symbols
125
+ # @param writer [RoleWriter] The writer that should be used to update data when a permission is set.
126
+ def initialize(bits = 0, writer = nil)
127
+ @writer = writer
128
+
129
+ @bits = if bits.is_a? Array
130
+ self.class.bits(bits)
131
+ else
132
+ bits.to_i
133
+ end
134
+
135
+ init_vars
136
+ end
137
+
138
+ # Return an array of permission flag symbols for this class's permissions
139
+ # @example Get the permissions for the bits "9"
140
+ # permissions = Permissions.new(9)
141
+ # permissions.defined_permissions #=> [:create_instant_invite, :administrator]
142
+ # @return [Array<Symbol>] the permissions
143
+ def defined_permissions
144
+ FLAGS.filter_map { |value, name| @bits.anybits?((1 << value)) ? name : nil }
145
+ end
146
+
147
+ # Comparison based on permission bits
148
+ def ==(other)
149
+ return false unless other.is_a?(OnyxCord::Permissions)
150
+
151
+ bits == other.bits
152
+ end
153
+ end
154
+
155
+ # Mixin to calculate resulting permissions from overrides etc.
156
+ module PermissionCalculator
157
+ # Checks whether this user can do the particular action, regardless of whether it has the permission defined,
158
+ # through for example being the server owner or having the Manage Roles permission
159
+ # @param action [Symbol] The permission that should be checked. See also {Permissions::FLAGS} for a list.
160
+ # @param channel [Channel, nil] If channel overrides should be checked too, this channel specifies where the overrides should be checked.
161
+ # @example Check if the bot can send messages to a specific channel in a server.
162
+ # bot_profile = bot.profile.on(event.server)
163
+ # can_send_messages = bot_profile.permission?(:send_messages, channel)
164
+ # @return [true, false] whether or not this user has the permission.
165
+ def permission?(action, channel = nil)
166
+ # If the member is the server owner, it irrevocably has all permissions.
167
+ return true if owner?
168
+
169
+ # First, check whether the user has Manage Roles defined.
170
+ # (Coincidentally, Manage Permissions is the same permission as Manage Roles, and a
171
+ # Manage Permissions deny overwrite will override Manage Roles, so we can just check for
172
+ # Manage Roles once and call it a day.)
173
+ return true if defined_permission?(:administrator, channel)
174
+
175
+ # Otherwise, defer to defined_permission
176
+ defined_permission?(action, channel)
177
+ end
178
+
179
+ # Checks whether this user has a particular permission defined (i.e. not implicit, through for example
180
+ # Manage Roles)
181
+ # @param action [Symbol] The permission that should be checked. See also {Permissions::FLAGS} for a list.
182
+ # @param channel [Channel, nil] If channel overrides should be checked too, this channel specifies where the overrides should be checked.
183
+ # @example Check if a member has the Manage Channels permission defined in the server.
184
+ # has_manage_channels = member.defined_permission?(:manage_channels)
185
+ # @return [true, false] whether or not this user has the permission defined.
186
+ def defined_permission?(action, channel = nil)
187
+ # For slash commands we may not have access to the server or role
188
+ # permissions. In this case we use the permissions given to us by the
189
+ # interaction. If attempting to check against a specific channel the check
190
+ # is skipped.
191
+ return @permissions.__send__(action) if @permissions && channel.nil?
192
+
193
+ # Get the permission the user's roles have
194
+ role_permission = defined_role_permission?(action, channel)
195
+
196
+ # Once we have checked the role permission, we have to check the channel overrides for the
197
+ # specific user
198
+ user_specific_override = permission_overwrite(action, channel, id) # Use the ID reader as members have no ID instance variable
199
+
200
+ # Merge the two permissions - if an override is defined, it has to be allow, otherwise we only care about the role
201
+ return role_permission unless user_specific_override
202
+
203
+ user_specific_override == :allow
204
+ end
205
+
206
+ # Define methods for querying permissions
207
+ OnyxCord::Permissions::FLAGS.each_value do |flag|
208
+ define_method "can_#{flag}?" do |channel = nil|
209
+ permission? flag, channel
210
+ end
211
+ end
212
+
213
+ alias_method :can_administrate?, :can_administrator?
214
+
215
+ private
216
+
217
+ def defined_role_permission?(action, channel)
218
+ roles_to_check = [@server.everyone_role] + roles
219
+
220
+ # For each role, check if
221
+ # (1) the channel explicitly allows or permits an action for the role and
222
+ # (2) if the user is allowed to do the action if the channel doesn't specify
223
+ roles_to_check.sort_by(&:position).reduce(false) do |can_act, role|
224
+ # Get the override defined for the role on the channel
225
+ channel_allow = permission_overwrite(action, channel, role.id)
226
+ if channel_allow
227
+ # If the channel has an override, check whether it is an allow - if yes,
228
+ # the user can act, if not, it can't
229
+ break true if channel_allow == :allow
230
+
231
+ false
232
+ else
233
+ # Otherwise defer to the role
234
+ role.permissions.instance_variable_get("@#{action}") || can_act
235
+ end
236
+ end
237
+ end
238
+
239
+ def permission_overwrite(action, channel, id)
240
+ # If no overwrites are defined, or no channel is set, no overwrite will be present
241
+ return nil unless channel && channel.permission_overwrites[id]
242
+
243
+ # Otherwise, check the allow and deny objects
244
+ allow = channel.permission_overwrites[id].allow
245
+ deny = channel.permission_overwrites[id].deny
246
+ if allow.instance_variable_get("@#{action}")
247
+ :allow
248
+ elsif deny.instance_variable_get("@#{action}")
249
+ :deny
250
+ end
251
+
252
+ # If there's no variable defined, nil will implicitly be returned
253
+ end
254
+ end
255
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OnyxCord
4
+ module RateLimiter
5
+ # Sliding-window limiter for Gateway sends.
6
+ class Gateway
7
+ DEFAULT_LIMIT = 120
8
+ DEFAULT_INTERVAL = 60
9
+
10
+ def initialize(limit: DEFAULT_LIMIT, interval: DEFAULT_INTERVAL, clock: -> { Time.now }, sleeper: ->(duration) { sleep(duration) })
11
+ @limit = limit
12
+ @interval = interval
13
+ @clock = clock
14
+ @sleeper = sleeper
15
+ @sent_at = []
16
+ @mutex = Mutex.new
17
+ end
18
+
19
+ def wait
20
+ @mutex.synchronize do
21
+ now = @clock.call
22
+ prune(now)
23
+
24
+ if @sent_at.length >= @limit
25
+ sleep_for = @interval - (now - @sent_at.first)
26
+ @sleeper.call(sleep_for) if sleep_for.positive?
27
+ now = @clock.call
28
+ prune(now)
29
+ end
30
+
31
+ @sent_at << now
32
+ end
33
+ end
34
+
35
+ private
36
+
37
+ def prune(now)
38
+ @sent_at.shift while @sent_at.any? && (now - @sent_at.first) >= @interval
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module OnyxCord
6
+ module RateLimiter
7
+ # Discord REST rate limiter keyed by route/major parameter and remapped to
8
+ # X-RateLimit-Bucket whenever Discord returns a concrete bucket id.
9
+ class Rest
10
+ def initialize
11
+ @route_buckets = {}
12
+ @bucket_mutexes = {}
13
+ @global_mutex = Mutex.new
14
+ end
15
+
16
+ def before_request(route, major_parameter)
17
+ mutex_wait(mutex_for(route, major_parameter))
18
+ mutex_wait(@global_mutex) if @global_mutex.locked?
19
+ end
20
+
21
+ def record_response(route, major_parameter, headers)
22
+ headers = normalize_headers(headers)
23
+ bucket = headers[:x_ratelimit_bucket]
24
+
25
+ @route_buckets[route_key(route, major_parameter)] = bucket_key(bucket, major_parameter) if bucket
26
+
27
+ return unless headers[:x_ratelimit_remaining] == '0'
28
+
29
+ wait_seconds = headers[:x_ratelimit_reset_after].to_f
30
+ return unless wait_seconds.positive?
31
+
32
+ sync_wait(wait_seconds, mutex_for(route, major_parameter))
33
+ end
34
+
35
+ def handle_rate_limit(route, major_parameter, response)
36
+ headers = normalize_headers(response.headers)
37
+ mutex = headers[:x_ratelimit_global] == 'true' || headers[:x_ratelimit_scope] == 'global' ? @global_mutex : mutex_for(route, major_parameter)
38
+ wait_seconds = retry_after(response, headers)
39
+
40
+ sync_wait(wait_seconds, mutex) if wait_seconds.positive?
41
+ end
42
+
43
+ private
44
+
45
+ def mutex_for(route, major_parameter)
46
+ @bucket_mutexes[resolved_key(route, major_parameter)] ||= Mutex.new
47
+ end
48
+
49
+ def resolved_key(route, major_parameter)
50
+ @route_buckets[route_key(route, major_parameter)] || route_key(route, major_parameter)
51
+ end
52
+
53
+ def route_key(route, major_parameter)
54
+ [route, major_parameter].freeze
55
+ end
56
+
57
+ def bucket_key(bucket, major_parameter)
58
+ [:bucket, bucket, major_parameter].freeze
59
+ end
60
+
61
+ def retry_after(response, headers)
62
+ body = response.respond_to?(:body) ? response.body : response.to_s
63
+ if body && !body.empty?
64
+ data = JSON.parse(body)
65
+ return data['retry_after'].to_f if data['retry_after']
66
+ end
67
+
68
+ (headers[:retry_after] || 0).to_f
69
+ rescue JSON::ParserError
70
+ (headers[:retry_after] || 0).to_f
71
+ end
72
+
73
+ def normalize_headers(headers)
74
+ headers.each_with_object({}) do |(key, value), memo|
75
+ memo[key.to_s.tr('-', '_').downcase.to_sym] = value.to_s
76
+ end
77
+ end
78
+
79
+ def sync_wait(time, mutex)
80
+ mutex.synchronize { sleep time }
81
+ end
82
+
83
+ def mutex_wait(mutex)
84
+ mutex.lock
85
+ mutex.unlock
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ # OnyxCord and all its functionality, in this case only the version.
4
+ module OnyxCord
5
+ # The current version of onyxcord.
6
+ VERSION = '1.1.0'
7
+ end
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This makes opus an optional dependency
4
+ begin
5
+ require 'opus-ruby'
6
+ OPUS_AVAILABLE = true
7
+ rescue LoadError
8
+ OPUS_AVAILABLE = false
9
+ end
10
+
11
+ # Discord voice chat support
12
+ module OnyxCord::Voice
13
+ # This class conveniently abstracts opus and ffmpeg/avconv, for easy implementation of voice sending. It's not very
14
+ # useful for most users, but I guess it can be useful sometimes.
15
+ class Encoder
16
+ # Whether or not avconv should be used instead of ffmpeg. If possible, it is recommended to use ffmpeg instead,
17
+ # as it is better supported.
18
+ # @return [true, false] whether avconv should be used instead of ffmpeg.
19
+ attr_accessor :use_avconv
20
+
21
+ # @see VoiceBot#filter_volume=
22
+ # @return [Integer] the volume used as a filter to ffmpeg/avconv.
23
+ attr_accessor :filter_volume
24
+
25
+ # Create a new encoder
26
+ def initialize
27
+ sample_rate = 48_000
28
+ frame_size = 960
29
+ channels = 2
30
+ @filter_volume = 1
31
+
32
+ raise LoadError, 'Opus unavailable - voice not supported! Please install opus for voice support to work.' unless OPUS_AVAILABLE
33
+
34
+ @opus = Opus::Encoder.new(sample_rate, frame_size, channels)
35
+ end
36
+
37
+ # Set the opus encoding bitrate
38
+ # @param value [Integer] The new bitrate to use, in bits per second (so 64000 if you want 64 kbps)
39
+ def bitrate=(value)
40
+ @opus.bitrate = value
41
+ end
42
+
43
+ # Encodes the given buffer using opus.
44
+ # @param buffer [String] An unencoded PCM (s16le) buffer.
45
+ # @return [String] A buffer encoded using opus.
46
+ def encode(buffer)
47
+ @opus.encode(buffer, 1920)
48
+ end
49
+
50
+ # One frame of complete silence Opus encoded
51
+ OPUS_SILENCE = [0xF8, 0xFF, 0xFE].pack('C*').freeze
52
+
53
+ # Adjusts the volume of a given buffer of s16le PCM data.
54
+ # @param buf [String] An unencoded PCM (s16le) buffer.
55
+ # @param mult [Float] The volume multiplier, 1 for same volume.
56
+ # @return [String] The buffer with adjusted volume, s16le again
57
+ def adjust_volume(buf, mult)
58
+ # We don't need to adjust anything if the buf is nil so just return in that case
59
+ return unless buf
60
+
61
+ # buf is s16le so use 's<' for signed, 16 bit, LE
62
+ result = buf.unpack('s<*').map do |sample|
63
+ sample *= mult
64
+
65
+ # clamp to s16 range
66
+ [32_767, [-32_768, sample].max].min
67
+ end
68
+
69
+ # After modification, make it s16le again
70
+ result.pack('s<*')
71
+ end
72
+
73
+ # Encodes a given file (or rather, decodes it) using ffmpeg. This accepts pretty much any format, even videos with
74
+ # an audio track. For a list of supported formats, see https://ffmpeg.org/general.html#Audio-Codecs. It even accepts
75
+ # URLs, though encoding them is pretty slow - I recommend to make a stream of it and then use {#encode_io} instead.
76
+ # @param file [String] The path or URL to encode.
77
+ # @param options [String] ffmpeg options to pass after the -i flag
78
+ # @return [IO] the audio, encoded as s16le PCM
79
+ def encode_file(file, options = '')
80
+ command = ffmpeg_command(input: file, options: options)
81
+ IO.popen(command)
82
+ end
83
+
84
+ # Encodes an arbitrary IO audio stream using ffmpeg. Accepts pretty much any media format, even videos with audio
85
+ # tracks. For a list of supported audio formats, see https://ffmpeg.org/general.html#Audio-Codecs.
86
+ # @param io [IO] The stream to encode.
87
+ # @param options [String] ffmpeg options to pass after the -i flag
88
+ # @return [IO] the audio, encoded as s16le PCM
89
+ def encode_io(io, options = '')
90
+ command = ffmpeg_command(options: options)
91
+ IO.popen(command, in: io)
92
+ end
93
+
94
+ private
95
+
96
+ def ffmpeg_command(input: '-', options: null)
97
+ [
98
+ @use_avconv ? 'avconv' : 'ffmpeg',
99
+ '-loglevel', '0',
100
+ '-i', input,
101
+ '-f', 's16le',
102
+ '-ar', '48000',
103
+ '-ac', '2',
104
+ 'pipe:1',
105
+ filter_volume_argument
106
+ ].concat(options.split).reject { |segment| segment.nil? || segment == '' }
107
+ end
108
+
109
+ def filter_volume_argument
110
+ return '' if @filter_volume == 1
111
+
112
+ @use_avconv ? "-vol #{(@filter_volume * 256).ceil}" : "-af volume=#{@filter_volume}"
113
+ end
114
+ end
115
+ end