discordrb 3.3.0 → 3.5.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 (93) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +152 -0
  3. data/.github/ISSUE_TEMPLATE/bug_report.md +38 -0
  4. data/.github/ISSUE_TEMPLATE/feature_request.md +24 -0
  5. data/.github/pull_request_template.md +37 -0
  6. data/.github/workflows/codeql.yml +65 -0
  7. data/.markdownlint.json +4 -0
  8. data/.rubocop.yml +39 -36
  9. data/CHANGELOG.md +874 -552
  10. data/Gemfile +2 -0
  11. data/LICENSE.txt +1 -1
  12. data/README.md +80 -86
  13. data/Rakefile +2 -0
  14. data/bin/console +1 -0
  15. data/discordrb-webhooks.gemspec +9 -6
  16. data/discordrb.gemspec +21 -18
  17. data/lib/discordrb/allowed_mentions.rb +36 -0
  18. data/lib/discordrb/api/application.rb +202 -0
  19. data/lib/discordrb/api/channel.rb +236 -47
  20. data/lib/discordrb/api/interaction.rb +54 -0
  21. data/lib/discordrb/api/invite.rb +5 -5
  22. data/lib/discordrb/api/server.rb +94 -66
  23. data/lib/discordrb/api/user.rb +17 -11
  24. data/lib/discordrb/api/webhook.rb +63 -6
  25. data/lib/discordrb/api.rb +55 -16
  26. data/lib/discordrb/await.rb +0 -1
  27. data/lib/discordrb/bot.rb +480 -93
  28. data/lib/discordrb/cache.rb +31 -24
  29. data/lib/discordrb/colour_rgb.rb +43 -0
  30. data/lib/discordrb/commands/command_bot.rb +35 -12
  31. data/lib/discordrb/commands/container.rb +21 -24
  32. data/lib/discordrb/commands/parser.rb +20 -20
  33. data/lib/discordrb/commands/rate_limiter.rb +4 -3
  34. data/lib/discordrb/container.rb +209 -20
  35. data/lib/discordrb/data/activity.rb +271 -0
  36. data/lib/discordrb/data/application.rb +50 -0
  37. data/lib/discordrb/data/attachment.rb +71 -0
  38. data/lib/discordrb/data/audit_logs.rb +345 -0
  39. data/lib/discordrb/data/channel.rb +993 -0
  40. data/lib/discordrb/data/component.rb +229 -0
  41. data/lib/discordrb/data/embed.rb +251 -0
  42. data/lib/discordrb/data/emoji.rb +82 -0
  43. data/lib/discordrb/data/integration.rb +122 -0
  44. data/lib/discordrb/data/interaction.rb +800 -0
  45. data/lib/discordrb/data/invite.rb +137 -0
  46. data/lib/discordrb/data/member.rb +372 -0
  47. data/lib/discordrb/data/message.rb +414 -0
  48. data/lib/discordrb/data/overwrite.rb +108 -0
  49. data/lib/discordrb/data/profile.rb +91 -0
  50. data/lib/discordrb/data/reaction.rb +33 -0
  51. data/lib/discordrb/data/recipient.rb +34 -0
  52. data/lib/discordrb/data/role.rb +248 -0
  53. data/lib/discordrb/data/server.rb +1004 -0
  54. data/lib/discordrb/data/user.rb +264 -0
  55. data/lib/discordrb/data/voice_region.rb +45 -0
  56. data/lib/discordrb/data/voice_state.rb +41 -0
  57. data/lib/discordrb/data/webhook.rb +238 -0
  58. data/lib/discordrb/data.rb +28 -4180
  59. data/lib/discordrb/errors.rb +46 -4
  60. data/lib/discordrb/events/bans.rb +7 -5
  61. data/lib/discordrb/events/channels.rb +3 -1
  62. data/lib/discordrb/events/guilds.rb +16 -9
  63. data/lib/discordrb/events/interactions.rb +482 -0
  64. data/lib/discordrb/events/invites.rb +125 -0
  65. data/lib/discordrb/events/members.rb +6 -2
  66. data/lib/discordrb/events/message.rb +72 -27
  67. data/lib/discordrb/events/presence.rb +35 -18
  68. data/lib/discordrb/events/raw.rb +1 -3
  69. data/lib/discordrb/events/reactions.rb +49 -4
  70. data/lib/discordrb/events/threads.rb +96 -0
  71. data/lib/discordrb/events/typing.rb +6 -4
  72. data/lib/discordrb/events/voice_server_update.rb +47 -0
  73. data/lib/discordrb/events/voice_state_update.rb +15 -10
  74. data/lib/discordrb/events/webhooks.rb +9 -6
  75. data/lib/discordrb/gateway.rb +99 -71
  76. data/lib/discordrb/id_object.rb +39 -0
  77. data/lib/discordrb/light/integrations.rb +1 -1
  78. data/lib/discordrb/light/light_bot.rb +1 -1
  79. data/lib/discordrb/logger.rb +4 -4
  80. data/lib/discordrb/paginator.rb +57 -0
  81. data/lib/discordrb/permissions.rb +159 -39
  82. data/lib/discordrb/version.rb +1 -1
  83. data/lib/discordrb/voice/encoder.rb +16 -7
  84. data/lib/discordrb/voice/network.rb +99 -47
  85. data/lib/discordrb/voice/sodium.rb +98 -0
  86. data/lib/discordrb/voice/voice_bot.rb +33 -25
  87. data/lib/discordrb/webhooks.rb +2 -0
  88. data/lib/discordrb.rb +107 -1
  89. metadata +126 -54
  90. data/.codeclimate.yml +0 -16
  91. data/.travis.yml +0 -33
  92. data/bin/travis_build_docs.sh +0 -17
  93. /data/{CONTRIBUTING.md → .github/CONTRIBUTING.md} +0 -0
@@ -0,0 +1,1004 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Discordrb
4
+ # Basic attributes a server should have
5
+ module ServerAttributes
6
+ # @return [String] this server's name.
7
+ attr_reader :name
8
+
9
+ # @return [String] the hexadecimal ID used to identify this server's icon.
10
+ attr_reader :icon_id
11
+
12
+ # Utility function to get the URL for the icon image
13
+ # @return [String] the URL to the icon image
14
+ def icon_url
15
+ return nil unless @icon_id
16
+
17
+ API.icon_url(@id, @icon_id)
18
+ end
19
+ end
20
+
21
+ # A server on Discord
22
+ class Server
23
+ include IDObject
24
+ include ServerAttributes
25
+
26
+ # @return [String] the ID of the region the server is on (e.g. `amsterdam`).
27
+ attr_reader :region_id
28
+
29
+ # @return [Array<Channel>] an array of all the channels (text and voice) on this server.
30
+ attr_reader :channels
31
+
32
+ # @return [Array<Role>] an array of all the roles created on this server.
33
+ attr_reader :roles
34
+
35
+ # @return [Hash<Integer => Emoji>] a hash of all the emoji available on this server.
36
+ attr_reader :emoji
37
+ alias_method :emojis, :emoji
38
+
39
+ # @return [true, false] whether or not this server is large (members > 100). If it is,
40
+ # it means the members list may be inaccurate for a couple seconds after starting up the bot.
41
+ attr_reader :large
42
+ alias_method :large?, :large
43
+
44
+ # @return [Array<Symbol>] the features of the server (eg. "INVITE_SPLASH")
45
+ attr_reader :features
46
+
47
+ # @return [Integer] the absolute number of members on this server, offline or not.
48
+ attr_reader :member_count
49
+
50
+ # @return [Integer] the amount of time after which a voice user gets moved into the AFK channel, in seconds.
51
+ attr_reader :afk_timeout
52
+
53
+ # @return [Hash<Integer => VoiceState>] the hash (user ID => voice state) of voice states of members on this server
54
+ attr_reader :voice_states
55
+
56
+ # The server's amount of Nitro boosters.
57
+ # @return [Integer] the amount of boosters, 0 if no one has boosted.
58
+ attr_reader :booster_count
59
+
60
+ # The server's Nitro boost level.
61
+ # @return [Integer] the boost level, 0 if no level.
62
+ attr_reader :boost_level
63
+
64
+ # @!visibility private
65
+ def initialize(data, bot)
66
+ @bot = bot
67
+ @owner_id = data['owner_id'].to_i
68
+ @id = data['id'].to_i
69
+ @members = {}
70
+ @voice_states = {}
71
+ @emoji = {}
72
+
73
+ process_channels(data['channels'])
74
+ update_data(data)
75
+
76
+ # Whether this server's members have been chunked (resolved using op 8 and GUILD_MEMBERS_CHUNK) yet
77
+ @chunked = false
78
+
79
+ @booster_count = data['premium_subscription_count'] || 0
80
+ @boost_level = data['premium_tier']
81
+ end
82
+
83
+ # @return [Member] The server owner.
84
+ def owner
85
+ @owner ||= member(@owner_id)
86
+ end
87
+
88
+ # The default channel is the text channel on this server with the highest position
89
+ # that the bot has Read Messages permission on.
90
+ # @param send_messages [true, false] whether to additionally consider if the bot has Send Messages permission
91
+ # @return [Channel, nil] The default channel on this server, or `nil` if there are no channels that the bot can read.
92
+ def default_channel(send_messages = false)
93
+ bot_member = member(@bot.profile)
94
+ text_channels.sort_by { |e| [e.position, e.id] }.find do |e|
95
+ if send_messages
96
+ bot_member.can_read_messages?(e) && bot_member.can_send_messages?(e)
97
+ else
98
+ bot_member.can_read_messages?(e)
99
+ end
100
+ end
101
+ end
102
+
103
+ alias_method :general_channel, :default_channel
104
+
105
+ # @return [Role] The @everyone role on this server
106
+ def everyone_role
107
+ role(@id)
108
+ end
109
+
110
+ # Gets a role on this server based on its ID.
111
+ # @param id [String, Integer] The role ID to look for.
112
+ # @return [Role, nil] The role identified by the ID, or `nil` if it couldn't be found.
113
+ def role(id)
114
+ id = id.resolve_id
115
+ @roles.find { |e| e.id == id }
116
+ end
117
+
118
+ # Gets a member on this server based on user ID
119
+ # @param id [Integer] The user ID to look for
120
+ # @param request [true, false] Whether the member should be requested from Discord if it's not cached
121
+ def member(id, request = true)
122
+ id = id.resolve_id
123
+ return @members[id] if member_cached?(id)
124
+ return nil unless request
125
+
126
+ member = @bot.member(self, id)
127
+ @members[id] = member unless member.nil?
128
+ rescue StandardError
129
+ nil
130
+ end
131
+
132
+ # @return [Array<Member>] an array of all the members on this server.
133
+ # @raise [RuntimeError] if the bot was not started with the :server_member intent
134
+ def members
135
+ return @members.values if @chunked
136
+
137
+ @bot.debug("Members for server #{@id} not chunked yet - initiating")
138
+
139
+ # If the SERVER_MEMBERS intent flag isn't set, the gateway won't respond when we ask for members.
140
+ raise 'The :server_members intent is required to get server members' if (@bot.gateway.intents & INTENTS[:server_members]).zero?
141
+
142
+ @bot.request_chunks(@id)
143
+ sleep 0.05 until @chunked
144
+ @members.values
145
+ end
146
+
147
+ alias_method :users, :members
148
+
149
+ # @return [Array<Member>] an array of all the bot members on this server.
150
+ def bot_members
151
+ members.select(&:bot_account?)
152
+ end
153
+
154
+ # @return [Array<Member>] an array of all the non bot members on this server.
155
+ def non_bot_members
156
+ members.reject(&:bot_account?)
157
+ end
158
+
159
+ # @return [Member] the bot's own `Member` on this server
160
+ def bot
161
+ member(@bot.profile)
162
+ end
163
+
164
+ # @return [Array<Integration>] an array of all the integrations connected to this server.
165
+ def integrations
166
+ integration = JSON.parse(API::Server.integrations(@bot.token, @id))
167
+ integration.map { |element| Integration.new(element, @bot, self) }
168
+ end
169
+
170
+ # @param action [Symbol] The action to only include.
171
+ # @param user [User, String, Integer] The user, or their ID, to filter entries to.
172
+ # @param limit [Integer] The amount of entries to limit it to.
173
+ # @param before [Entry, String, Integer] The entry, or its ID, to use to not include all entries after it.
174
+ # @return [AuditLogs] The server's audit logs.
175
+ def audit_logs(action: nil, user: nil, limit: 50, before: nil)
176
+ raise 'Invalid audit log action!' if action && AuditLogs::ACTIONS.key(action).nil?
177
+
178
+ action = AuditLogs::ACTIONS.key(action)
179
+ user = user.resolve_id if user
180
+ before = before.resolve_id if before
181
+ AuditLogs.new(self, @bot, JSON.parse(API::Server.audit_logs(@bot.token, @id, limit, user, action, before)))
182
+ end
183
+
184
+ # Cache @widget
185
+ # @note For internal use only
186
+ # @!visibility private
187
+ def cache_widget_data
188
+ data = JSON.parse(API::Server.widget(@bot.token, @id))
189
+ @widget_enabled = data['enabled']
190
+ @widget_channel_id = data['channel_id']
191
+ end
192
+
193
+ # @return [true, false] whether or not the server has widget enabled
194
+ def widget_enabled?
195
+ cache_widget_data if @widget_enabled.nil?
196
+ @widget_enabled
197
+ end
198
+ alias_method :widget?, :widget_enabled?
199
+ alias_method :embed_enabled, :widget_enabled?
200
+ alias_method :embed?, :widget_enabled?
201
+
202
+ # @return [Channel, nil] the channel the server widget will make an invite for.
203
+ def widget_channel
204
+ cache_widget_data if @widget_enabled.nil?
205
+ @bot.channel(@widget_channel_id) if @widget_channel_id
206
+ end
207
+ alias_method :embed_channel, :widget_channel
208
+
209
+ # Sets whether this server's widget is enabled
210
+ # @param value [true, false]
211
+ def widget_enabled=(value)
212
+ modify_widget(value, widget_channel)
213
+ end
214
+ alias_method :embed_enabled=, :widget_enabled=
215
+
216
+ # Sets whether this server's widget is enabled
217
+ # @param value [true, false]
218
+ # @param reason [String, nil] the reason to be shown in the audit log for this action
219
+ def set_widget_enabled(value, reason = nil)
220
+ modify_widget(value, widget_channel, reason)
221
+ end
222
+ alias_method :set_embed_enabled, :set_widget_enabled
223
+
224
+ # Changes the channel on the server's widget
225
+ # @param channel [Channel, String, Integer] the channel, or its ID, to be referenced by the widget
226
+ def widget_channel=(channel)
227
+ modify_widget(widget?, channel)
228
+ end
229
+ alias_method :embed_channel=, :widget_channel=
230
+
231
+ # Changes the channel on the server's widget
232
+ # @param channel [Channel, String, Integer] the channel, or its ID, to be referenced by the widget
233
+ # @param reason [String, nil] the reason to be shown in the audit log for this action
234
+ def set_widget_channel(channel, reason = nil)
235
+ modify_widget(widget?, channel, reason)
236
+ end
237
+ alias_method :set_embed_channel, :set_widget_channel
238
+
239
+ # Changes the channel on the server's widget, and sets whether it is enabled.
240
+ # @param enabled [true, false] whether the widget is enabled
241
+ # @param channel [Channel, String, Integer] the channel, or its ID, to be referenced by the widget
242
+ # @param reason [String, nil] the reason to be shown in the audit log for this action
243
+ def modify_widget(enabled, channel, reason = nil)
244
+ cache_widget_data if @widget_enabled.nil?
245
+ channel_id = channel ? channel.resolve_id : @widget_channel_id
246
+ response = JSON.parse(API::Server.modify_widget(@bot.token, @id, enabled, channel_id, reason))
247
+ @widget_enabled = response['enabled']
248
+ @widget_channel_id = response['channel_id']
249
+ end
250
+ alias_method :modify_embed, :modify_widget
251
+
252
+ # @param include_idle [true, false] Whether to count idle members as online.
253
+ # @param include_bots [true, false] Whether to include bot accounts in the count.
254
+ # @return [Array<Member>] an array of online members on this server.
255
+ def online_members(include_idle: false, include_bots: true)
256
+ @members.values.select do |e|
257
+ ((include_idle ? e.idle? : false) || e.online?) && (include_bots ? true : !e.bot_account?)
258
+ end
259
+ end
260
+
261
+ alias_method :online_users, :online_members
262
+
263
+ # Adds a member to this guild that has granted this bot's application an OAuth2 access token
264
+ # with the `guilds.join` scope.
265
+ # For more information about Discord's OAuth2 implementation, see: https://discord.com/developers/docs/topics/oauth2
266
+ # @note Your bot must be present in this server, and have permission to create instant invites for this to work.
267
+ # @param user [User, String, Integer] the user, or ID of the user to add to this server
268
+ # @param access_token [String] the OAuth2 Bearer token that has been granted the `guilds.join` scope
269
+ # @param nick [String] the nickname to give this member upon joining
270
+ # @param roles [Role, Array<Role, String, Integer>] the role (or roles) to give this member upon joining
271
+ # @param deaf [true, false] whether this member will be server deafened upon joining
272
+ # @param mute [true, false] whether this member will be server muted upon joining
273
+ # @return [Member, nil] the created member, or `nil` if the user is already a member of this server.
274
+ def add_member_using_token(user, access_token, nick: nil, roles: [], deaf: false, mute: false)
275
+ user_id = user.resolve_id
276
+ roles = roles.is_a?(Array) ? roles.map(&:resolve_id) : [roles.resolve_id]
277
+ response = API::Server.add_member(@bot.token, @id, user_id, access_token, nick, roles, deaf, mute)
278
+ return nil if response.empty?
279
+
280
+ add_member Member.new(JSON.parse(response), self, @bot)
281
+ end
282
+
283
+ # Returns the amount of members that are candidates for pruning
284
+ # @param days [Integer] the number of days to consider for inactivity
285
+ # @return [Integer] number of members to be removed
286
+ # @raise [ArgumentError] if days is not between 1 and 30 (inclusive)
287
+ def prune_count(days)
288
+ raise ArgumentError, 'Days must be between 1 and 30' unless days.between?(1, 30)
289
+
290
+ response = JSON.parse API::Server.prune_count(@bot.token, @id, days)
291
+ response['pruned']
292
+ end
293
+
294
+ # Prunes (kicks) an amount of members for inactivity
295
+ # @param days [Integer] the number of days to consider for inactivity (between 1 and 30)
296
+ # @param reason [String] The reason the for the prune.
297
+ # @return [Integer] the number of members removed at the end of the operation
298
+ # @raise [ArgumentError] if days is not between 1 and 30 (inclusive)
299
+ def begin_prune(days, reason = nil)
300
+ raise ArgumentError, 'Days must be between 1 and 30' unless days.between?(1, 30)
301
+
302
+ response = JSON.parse API::Server.begin_prune(@bot.token, @id, days, reason)
303
+ response['pruned']
304
+ end
305
+
306
+ alias_method :prune, :begin_prune
307
+
308
+ # @return [Array<Channel>] an array of text channels on this server
309
+ def text_channels
310
+ @channels.select(&:text?)
311
+ end
312
+
313
+ # @return [Array<Channel>] an array of voice channels on this server
314
+ def voice_channels
315
+ @channels.select(&:voice?)
316
+ end
317
+
318
+ # @return [Array<Channel>] an array of category channels on this server
319
+ def categories
320
+ @channels.select(&:category?)
321
+ end
322
+
323
+ # @return [Array<Channel>] an array of channels on this server that are not in a category
324
+ def orphan_channels
325
+ @channels.reject { |c| c.parent || c.category? }
326
+ end
327
+
328
+ # @return [String, nil] the widget URL to the server that displays the amount of online members in a
329
+ # stylish way. `nil` if the widget is not enabled.
330
+ def widget_url
331
+ update_data if @embed_enabled.nil?
332
+ return unless @embed_enabled
333
+
334
+ API.widget_url(@id)
335
+ end
336
+
337
+ # @param style [Symbol] The style the picture should have. Possible styles are:
338
+ # * `:banner1` creates a rectangular image with the server name, member count and icon, a "Powered by Discord" message on the bottom and an arrow on the right.
339
+ # * `:banner2` creates a less tall rectangular image that has the same information as `banner1`, but the Discord logo on the right - together with the arrow and separated by a diagonal separator.
340
+ # * `:banner3` creates an image similar in size to `banner1`, but it has the arrow in the bottom part, next to the Discord logo and with a "Chat now" text.
341
+ # * `:banner4` creates a tall, almost square, image that prominently features the Discord logo at the top and has a "Join my server" in a pill-style button on the bottom. The information about the server is in the same format as the other three `banner` styles.
342
+ # * `:shield` creates a very small, long rectangle, of the style you'd find at the top of GitHub `README.md` files. It features a small version of the Discord logo at the left and the member count at the right.
343
+ # @return [String, nil] the widget banner URL to the server that displays the amount of online members,
344
+ # server icon and server name in a stylish way. `nil` if the widget is not enabled.
345
+ def widget_banner_url(style)
346
+ update_data if @embed_enabled.nil?
347
+ return unless @embed_enabled
348
+
349
+ API.widget_url(@id, style)
350
+ end
351
+
352
+ # @return [String] the hexadecimal ID used to identify this server's splash image for their VIP invite page.
353
+ def splash_id
354
+ @splash_id ||= JSON.parse(API::Server.resolve(@bot.token, @id))['splash']
355
+ end
356
+ alias splash_hash splash_id
357
+
358
+ # @return [String, nil] the splash image URL for the server's VIP invite page.
359
+ # `nil` if there is no splash image.
360
+ def splash_url
361
+ splash_id if @splash_id.nil?
362
+ return nil unless @splash_id
363
+
364
+ API.splash_url(@id, @splash_id)
365
+ end
366
+
367
+ # @return [String] the hexadecimal ID used to identify this server's banner image, shown by the server name.
368
+ def banner_id
369
+ @banner_id ||= JSON.parse(API::Server.resolve(@bot.token, @id))['banner']
370
+ end
371
+
372
+ # @return [String, nil] the banner image URL for the server's banner image, or
373
+ # `nil` if there is no banner image.
374
+ def banner_url
375
+ banner_id if @banner_id.nil?
376
+ return unless banner_id
377
+
378
+ API.banner_url(@id, @banner_id)
379
+ end
380
+
381
+ # @return [String] a URL that a user can use to navigate to this server in the client
382
+ def link
383
+ "https://discord.com/channels/#{@id}"
384
+ end
385
+
386
+ alias_method :jump_link, :link
387
+
388
+ # Adds a role to the role cache
389
+ # @note For internal use only
390
+ # @!visibility private
391
+ def add_role(role)
392
+ @roles << role
393
+ end
394
+
395
+ # Removes a role from the role cache
396
+ # @note For internal use only
397
+ # @!visibility private
398
+ def delete_role(role_id)
399
+ @roles.reject! { |r| r.id == role_id }
400
+ @members.each do |_, member|
401
+ new_roles = member.roles.reject { |r| r.id == role_id }
402
+ member.update_roles(new_roles)
403
+ end
404
+ @channels.each do |channel|
405
+ overwrites = channel.permission_overwrites.reject { |id, _| id == role_id }
406
+ channel.update_overwrites(overwrites)
407
+ end
408
+ end
409
+
410
+ # Updates the positions of all roles on the server
411
+ # @note For internal use only
412
+ # @!visibility private
413
+ def update_role_positions(role_positions)
414
+ response = JSON.parse(API::Server.update_role_positions(@bot.token, @id, role_positions))
415
+ response.each do |data|
416
+ updated_role = Role.new(data, @bot, self)
417
+ role(updated_role.id).update_from(updated_role)
418
+ end
419
+ end
420
+
421
+ # Adds a member to the member cache.
422
+ # @note For internal use only
423
+ # @!visibility private
424
+ def add_member(member)
425
+ @member_count += 1
426
+ @members[member.id] = member
427
+ end
428
+
429
+ # Removes a member from the member cache.
430
+ # @note For internal use only
431
+ # @!visibility private
432
+ def delete_member(user_id)
433
+ @members.delete(user_id)
434
+ @member_count -= 1 unless @member_count <= 0
435
+ end
436
+
437
+ # Checks whether a member is cached
438
+ # @note For internal use only
439
+ # @!visibility private
440
+ def member_cached?(user_id)
441
+ @members.include?(user_id)
442
+ end
443
+
444
+ # Adds a member to the cache
445
+ # @note For internal use only
446
+ # @!visibility private
447
+ def cache_member(member)
448
+ @members[member.id] = member
449
+ end
450
+
451
+ # Updates a member's voice state
452
+ # @note For internal use only
453
+ # @!visibility private
454
+ def update_voice_state(data)
455
+ user_id = data['user_id'].to_i
456
+
457
+ if data['channel_id']
458
+ unless @voice_states[user_id]
459
+ # Create a new voice state for the user
460
+ @voice_states[user_id] = VoiceState.new(user_id)
461
+ end
462
+
463
+ # Update the existing voice state (or the one we just created)
464
+ channel = @channels_by_id[data['channel_id'].to_i]
465
+ @voice_states[user_id].update(
466
+ channel,
467
+ data['mute'],
468
+ data['deaf'],
469
+ data['self_mute'],
470
+ data['self_deaf']
471
+ )
472
+ else
473
+ # The user is not in a voice channel anymore, so delete its voice state
474
+ @voice_states.delete(user_id)
475
+ end
476
+ end
477
+
478
+ # Creates a channel on this server with the given name.
479
+ # @note If parent is provided, permission overwrites have the follow behavior:
480
+ #
481
+ # 1. If overwrites is null, the new channel inherits the parent's permissions.
482
+ # 2. If overwrites is [], the new channel inherits the parent's permissions.
483
+ # 3. If you supply one or more overwrites, the channel will be created with those permissions and ignore the parents.
484
+ #
485
+ # @param name [String] Name of the channel to create
486
+ # @param type [Integer, Symbol] Type of channel to create (0: text, 2: voice, 4: category, 5: news, 6: store)
487
+ # @param topic [String] the topic of this channel, if it will be a text channel
488
+ # @param bitrate [Integer] the bitrate of this channel, if it will be a voice channel
489
+ # @param user_limit [Integer] the user limit of this channel, if it will be a voice channel
490
+ # @param permission_overwrites [Array<Hash>, Array<Overwrite>] permission overwrites for this channel
491
+ # @param parent [Channel, String, Integer] parent category, or its ID, for this channel to be created in.
492
+ # @param nsfw [true, false] whether this channel should be created as nsfw
493
+ # @param rate_limit_per_user [Integer] how many seconds users need to wait in between messages.
494
+ # @param reason [String] The reason the for the creation of this channel.
495
+ # @return [Channel] the created channel.
496
+ # @raise [ArgumentError] if type is not 0 (text), 2 (voice), 4 (category), 5 (news), or 6 (store)
497
+ def create_channel(name, type = 0, topic: nil, bitrate: nil, user_limit: nil, permission_overwrites: nil, parent: nil, nsfw: false, rate_limit_per_user: nil, position: nil, reason: nil)
498
+ type = Channel::TYPES[type] if type.is_a?(Symbol)
499
+ raise ArgumentError, 'Channel type must be either 0 (text), 2 (voice), 4 (category), news (5), or store (6)!' unless [0, 2, 4, 5, 6].include?(type)
500
+
501
+ permission_overwrites.map! { |e| e.is_a?(Overwrite) ? e.to_hash : e } if permission_overwrites.is_a?(Array)
502
+ parent_id = parent.respond_to?(:resolve_id) ? parent.resolve_id : nil
503
+ response = API::Server.create_channel(@bot.token, @id, name, type, topic, bitrate, user_limit, permission_overwrites, parent_id, nsfw, rate_limit_per_user, position, reason)
504
+ Channel.new(JSON.parse(response), @bot)
505
+ end
506
+
507
+ # Creates a role on this server which can then be modified. It will be initialized
508
+ # with the regular role defaults the client uses unless specified, i.e. name is "new role",
509
+ # permissions are the default, colour is the default etc.
510
+ # @param name [String] Name of the role to create
511
+ # @param colour [Integer, ColourRGB, #combined] The roles colour
512
+ # @param hoist [true, false]
513
+ # @param mentionable [true, false]
514
+ # @param permissions [Integer, Array<Symbol>, Permissions, #bits] The permissions to write to the new role.
515
+ # @param reason [String] The reason the for the creation of this role.
516
+ # @return [Role] the created role.
517
+ def create_role(name: 'new role', colour: 0, hoist: false, mentionable: false, permissions: 104_324_161, reason: nil)
518
+ colour = colour.respond_to?(:combined) ? colour.combined : colour
519
+
520
+ permissions = if permissions.is_a?(Array)
521
+ Permissions.bits(permissions)
522
+ elsif permissions.respond_to?(:bits)
523
+ permissions.bits
524
+ else
525
+ permissions
526
+ end
527
+
528
+ response = API::Server.create_role(@bot.token, @id, name, colour, hoist, mentionable, permissions, reason)
529
+
530
+ role = Role.new(JSON.parse(response), @bot, self)
531
+ @roles << role
532
+ role
533
+ end
534
+
535
+ # Adds a new custom emoji on this server.
536
+ # @param name [String] The name of emoji to create.
537
+ # @param image [String, #read] A base64 encoded string with the image data, or an object that responds to `#read`, such as `File`.
538
+ # @param roles [Array<Role, String, Integer>] An array of roles, or role IDs to be whitelisted for this emoji.
539
+ # @param reason [String] The reason the for the creation of this emoji.
540
+ # @return [Emoji] The emoji that has been added.
541
+ def add_emoji(name, image, roles = [], reason: nil)
542
+ image_string = image
543
+ if image.respond_to? :read
544
+ image_string = 'data:image/jpg;base64,'
545
+ image_string += Base64.strict_encode64(image.read)
546
+ end
547
+
548
+ data = JSON.parse(API::Server.add_emoji(@bot.token, @id, image_string, name, roles.map(&:resolve_id), reason))
549
+ new_emoji = Emoji.new(data, @bot, self)
550
+ @emoji[new_emoji.id] = new_emoji
551
+ end
552
+
553
+ # Delete a custom emoji on this server
554
+ # @param emoji [Emoji, String, Integer] The emoji or emoji ID to be deleted.
555
+ # @param reason [String] The reason the for the deletion of this emoji.
556
+ def delete_emoji(emoji, reason: nil)
557
+ API::Server.delete_emoji(@bot.token, @id, emoji.resolve_id, reason)
558
+ end
559
+
560
+ # Changes the name and/or role whitelist of an emoji on this server.
561
+ # @param emoji [Emoji, String, Integer] The emoji or emoji ID to edit.
562
+ # @param name [String] The new name for the emoji.
563
+ # @param roles [Array<Role, String, Integer>] A new array of roles, or role IDs, to whitelist.
564
+ # @param reason [String] The reason for the editing of this emoji.
565
+ # @return [Emoji] The edited emoji.
566
+ def edit_emoji(emoji, name: nil, roles: nil, reason: nil)
567
+ emoji = @emoji[emoji.resolve_id]
568
+ data = JSON.parse(API::Server.edit_emoji(@bot.token, @id, emoji.resolve_id, name || emoji.name, (roles || emoji.roles).map(&:resolve_id), reason))
569
+ new_emoji = Emoji.new(data, @bot, self)
570
+ @emoji[new_emoji.id] = new_emoji
571
+ end
572
+
573
+ # The amount of emoji the server can have, based on its current Nitro Boost Level.
574
+ # @return [Integer] the max amount of emoji
575
+ def max_emoji
576
+ case @boost_level
577
+ when 1
578
+ 100
579
+ when 2
580
+ 150
581
+ when 3
582
+ 250
583
+ else
584
+ 50
585
+ end
586
+ end
587
+
588
+ # Retrieve banned users from this server.
589
+ # @param limit [Integer] Number of users to return (up to maximum 1000, default 1000).
590
+ # @param before_id [Integer] Consider only users before given user id.
591
+ # @param after_id [Integer] Consider only users after given user id.
592
+ # @return [Array<ServerBan>] a list of banned users on this server and the reason they were banned.
593
+ def bans(limit: nil, before_id: nil, after_id: nil)
594
+ response = JSON.parse(API::Server.bans(@bot.token, @id, limit, before_id, after_id))
595
+ response.map do |e|
596
+ ServerBan.new(self, User.new(e['user'], @bot), e['reason'])
597
+ end
598
+ end
599
+
600
+ # Bans a user from this server.
601
+ # @param user [User, String, Integer] The user to ban.
602
+ # @param message_days [Integer] How many days worth of messages sent by the user should be deleted.
603
+ # @param reason [String] The reason the user is being banned.
604
+ def ban(user, message_days = 0, reason: nil)
605
+ API::Server.ban_user(@bot.token, @id, user.resolve_id, message_days, reason)
606
+ end
607
+
608
+ # Unbans a previously banned user from this server.
609
+ # @param user [User, String, Integer] The user to unban.
610
+ # @param reason [String] The reason the user is being unbanned.
611
+ def unban(user, reason = nil)
612
+ API::Server.unban_user(@bot.token, @id, user.resolve_id, reason)
613
+ end
614
+
615
+ # Kicks a user from this server.
616
+ # @param user [User, String, Integer] The user to kick.
617
+ # @param reason [String] The reason the user is being kicked.
618
+ def kick(user, reason = nil)
619
+ API::Server.remove_member(@bot.token, @id, user.resolve_id, reason)
620
+ end
621
+
622
+ # Forcibly moves a user into a different voice channel.
623
+ # Only works if the bot has the permission needed and if the user is already connected to some voice channel on this server.
624
+ # @param user [User, String, Integer] The user to move.
625
+ # @param channel [Channel, String, Integer, nil] The voice channel to move into. (If nil, the user is disconnected from the voice channel)
626
+ def move(user, channel)
627
+ API::Server.update_member(@bot.token, @id, user.resolve_id, channel_id: channel&.resolve_id)
628
+ end
629
+
630
+ # Deletes this server. Be aware that this is permanent and impossible to undo, so be careful!
631
+ def delete
632
+ API::Server.delete(@bot.token, @id)
633
+ end
634
+
635
+ # Leave the server.
636
+ def leave
637
+ API::User.leave_server(@bot.token, @id)
638
+ end
639
+
640
+ # Transfers server ownership to another user.
641
+ # @param user [User, String, Integer] The user who should become the new owner.
642
+ def owner=(user)
643
+ API::Server.transfer_ownership(@bot.token, @id, user.resolve_id)
644
+ end
645
+
646
+ # Sets the server's name.
647
+ # @param name [String] The new server name.
648
+ def name=(name)
649
+ update_server_data(name: name)
650
+ end
651
+
652
+ # @return [Array<VoiceRegion>] collection of available voice regions to this guild
653
+ def available_voice_regions
654
+ return @available_voice_regions if @available_voice_regions
655
+
656
+ @available_voice_regions = {}
657
+
658
+ data = JSON.parse API::Server.regions(@bot.token, @id)
659
+ @available_voice_regions = data.map { |e| VoiceRegion.new e }
660
+ end
661
+
662
+ # @return [VoiceRegion, nil] voice region data for this server's region
663
+ # @note This may return `nil` if this server's voice region is deprecated.
664
+ def region
665
+ available_voice_regions.find { |e| e.id == @region_id }
666
+ end
667
+
668
+ # Moves the server to another region. This will cause a voice interruption of at most a second.
669
+ # @param region [String] The new region the server should be in.
670
+ def region=(region)
671
+ update_server_data(region: region.to_s)
672
+ end
673
+
674
+ # Sets the server's icon.
675
+ # @param icon [String, #read] The new icon, in base64-encoded JPG format.
676
+ def icon=(icon)
677
+ if icon.respond_to? :read
678
+ icon_string = 'data:image/jpg;base64,'
679
+ icon_string += Base64.strict_encode64(icon.read)
680
+ update_server_data(icon_id: icon_string)
681
+ else
682
+ update_server_data(icon_id: icon)
683
+ end
684
+ end
685
+
686
+ # Sets the server's AFK channel.
687
+ # @param afk_channel [Channel, nil] The new AFK channel, or `nil` if there should be none set.
688
+ def afk_channel=(afk_channel)
689
+ update_server_data(afk_channel_id: afk_channel.resolve_id)
690
+ end
691
+
692
+ # Sets the server's system channel.
693
+ # @param system_channel [Channel, String, Integer, nil] The new system channel, or `nil` should it be disabled.
694
+ def system_channel=(system_channel)
695
+ update_server_data(system_channel_id: system_channel.resolve_id)
696
+ end
697
+
698
+ # Sets the amount of time after which a user gets moved into the AFK channel.
699
+ # @param afk_timeout [Integer] The AFK timeout, in seconds.
700
+ def afk_timeout=(afk_timeout)
701
+ update_server_data(afk_timeout: afk_timeout)
702
+ end
703
+
704
+ # A map of possible server verification levels to symbol names
705
+ VERIFICATION_LEVELS = {
706
+ none: 0,
707
+ low: 1,
708
+ medium: 2,
709
+ high: 3,
710
+ very_high: 4
711
+ }.freeze
712
+
713
+ # @return [Symbol] the verification level of the server (:none = none, :low = 'Must have a verified email on their Discord account', :medium = 'Has to be registered with Discord for at least 5 minutes', :high = 'Has to be a member of this server for at least 10 minutes', :very_high = 'Must have a verified phone on their Discord account').
714
+ def verification_level
715
+ VERIFICATION_LEVELS.key @verification_level
716
+ end
717
+
718
+ # Sets the verification level of the server
719
+ # @param level [Integer, Symbol] The verification level from 0-4 or Symbol (see {VERIFICATION_LEVELS})
720
+ def verification_level=(level)
721
+ level = VERIFICATION_LEVELS[level] if level.is_a?(Symbol)
722
+
723
+ update_server_data(verification_level: level)
724
+ end
725
+
726
+ # A map of possible message notification levels to symbol names
727
+ NOTIFICATION_LEVELS = {
728
+ all_messages: 0,
729
+ only_mentions: 1
730
+ }.freeze
731
+
732
+ # @return [Symbol] the default message notifications settings of the server (:all = 'All messages', :mentions = 'Only @mentions').
733
+ def default_message_notifications
734
+ NOTIFICATION_LEVELS.key @default_message_notifications
735
+ end
736
+
737
+ # Sets the default message notification level
738
+ # @param notification_level [Integer, Symbol] The default message notification 0-1 or Symbol (see {NOTIFICATION_LEVELS})
739
+ def default_message_notifications=(notification_level)
740
+ notification_level = NOTIFICATION_LEVELS[notification_level] if notification_level.is_a?(Symbol)
741
+
742
+ update_server_data(default_message_notifications: notification_level)
743
+ end
744
+
745
+ alias_method :notification_level=, :default_message_notifications=
746
+
747
+ # Sets the server splash
748
+ # @param splash_hash [String] The splash hash
749
+ def splash=(splash_hash)
750
+ update_server_data(splash: splash_hash)
751
+ end
752
+
753
+ # A map of possible content filter levels to symbol names
754
+ FILTER_LEVELS = {
755
+ disabled: 0,
756
+ members_without_roles: 1,
757
+ all_members: 2
758
+ }.freeze
759
+
760
+ # @return [Symbol] the explicit content filter level of the server (:none = 'Don't scan any messages.', :exclude_roles = 'Scan messages for members without a role.', :all = 'Scan messages sent by all members.').
761
+ def explicit_content_filter
762
+ FILTER_LEVELS.key @explicit_content_filter
763
+ end
764
+
765
+ alias_method :content_filter_level, :explicit_content_filter
766
+
767
+ # Sets the server content filter.
768
+ # @param filter_level [Integer, Symbol] The content filter from 0-2 or Symbol (see {FILTER_LEVELS})
769
+ def explicit_content_filter=(filter_level)
770
+ filter_level = FILTER_LEVELS[filter_level] if filter_level.is_a?(Symbol)
771
+
772
+ update_server_data(explicit_content_filter: filter_level)
773
+ end
774
+
775
+ # @return [true, false] whether this server has any emoji or not.
776
+ def any_emoji?
777
+ @emoji.any?
778
+ end
779
+
780
+ alias_method :has_emoji?, :any_emoji?
781
+ alias_method :emoji?, :any_emoji?
782
+
783
+ # Requests a list of Webhooks on the server.
784
+ # @return [Array<Webhook>] webhooks on the server.
785
+ def webhooks
786
+ webhooks = JSON.parse(API::Server.webhooks(@bot.token, @id))
787
+ webhooks.map { |webhook| Webhook.new(webhook, @bot) }
788
+ end
789
+
790
+ # Requests a list of Invites to the server.
791
+ # @return [Array<Invite>] invites to the server.
792
+ def invites
793
+ invites = JSON.parse(API::Server.invites(@bot.token, @id))
794
+ invites.map { |invite| Invite.new(invite, @bot) }
795
+ end
796
+
797
+ # Processes a GUILD_MEMBERS_CHUNK packet, specifically the members field
798
+ # @note For internal use only
799
+ # @!visibility private
800
+ def process_chunk(members, chunk_index, chunk_count)
801
+ process_members(members)
802
+ LOGGER.debug("Processed chunk #{chunk_index + 1}/#{chunk_count} server #{@id} - index #{chunk_index} - length #{members.length}")
803
+
804
+ return if chunk_index + 1 < chunk_count
805
+
806
+ LOGGER.debug("Finished chunking server #{@id}")
807
+
808
+ # Reset everything to normal
809
+ @chunked = true
810
+ end
811
+
812
+ # @return [Channel, nil] the AFK voice channel of this server, or `nil` if none is set.
813
+ def afk_channel
814
+ @bot.channel(@afk_channel_id) if @afk_channel_id
815
+ end
816
+
817
+ # @return [Channel, nil] the system channel (used for automatic welcome messages) of a server, or `nil` if none is set.
818
+ def system_channel
819
+ @bot.channel(@system_channel_id) if @system_channel_id
820
+ end
821
+
822
+ # Updates the cached data with new data
823
+ # @note For internal use only
824
+ # @!visibility private
825
+ def update_data(new_data = nil)
826
+ new_data ||= JSON.parse(API::Server.resolve(@bot.token, @id))
827
+ @name = new_data[:name] || new_data['name'] || @name
828
+ @region_id = new_data[:region] || new_data['region'] || @region_id
829
+ @icon_id = new_data[:icon] || new_data['icon'] || @icon_id
830
+ @afk_timeout = new_data[:afk_timeout] || new_data['afk_timeout'] || @afk_timeout
831
+
832
+ afk_channel_id = new_data[:afk_channel_id] || new_data['afk_channel_id'] || @afk_channel
833
+ @afk_channel_id = afk_channel_id.nil? ? nil : afk_channel_id.resolve_id
834
+ widget_channel_id = new_data[:widget_channel_id] || new_data['widget_channel_id'] || @widget_channel
835
+ @widget_channel_id = widget_channel_id.nil? ? nil : widget_channel_id.resolve_id
836
+ system_channel_id = new_data[:system_channel_id] || new_data['system_channel_id'] || @system_channel
837
+ @system_channel_id = system_channel_id.nil? ? nil : system_channel_id.resolve_id
838
+
839
+ @widget_enabled = new_data[:widget_enabled] || new_data['widget_enabled']
840
+ @splash = new_data[:splash_id] || new_data['splash_id'] || @splash_id
841
+
842
+ @verification_level = new_data[:verification_level] || new_data['verification_level'] || @verification_level
843
+ @explicit_content_filter = new_data[:explicit_content_filter] || new_data['explicit_content_filter'] || @explicit_content_filter
844
+ @default_message_notifications = new_data[:default_message_notifications] || new_data['default_message_notifications'] || @default_message_notifications
845
+
846
+ @large = new_data.key?('large') ? new_data['large'] : @large
847
+ @member_count = new_data['member_count'] || @member_count || 0
848
+ @splash_id = new_data['splash'] || @splash_id
849
+ @banner_id = new_data['banner'] || @banner_id
850
+ @features = new_data['features'] ? new_data['features'].map { |element| element.downcase.to_sym } : @features || []
851
+
852
+ process_channels(new_data['channels']) if new_data['channels']
853
+ process_roles(new_data['roles']) if new_data['roles']
854
+ process_emoji(new_data['emojis']) if new_data['emojis']
855
+ process_members(new_data['members']) if new_data['members']
856
+ process_presences(new_data['presences']) if new_data['presences']
857
+ process_voice_states(new_data['voice_states']) if new_data['voice_states']
858
+ end
859
+
860
+ # Adds a channel to this server's cache
861
+ # @note For internal use only
862
+ # @!visibility private
863
+ def add_channel(channel)
864
+ @channels << channel
865
+ @channels_by_id[channel.id] = channel
866
+ end
867
+
868
+ # Deletes a channel from this server's cache
869
+ # @note For internal use only
870
+ # @!visibility private
871
+ def delete_channel(id)
872
+ @channels.reject! { |e| e.id == id }
873
+ @channels_by_id.delete(id)
874
+ end
875
+
876
+ # Updates the cached emoji data with new data
877
+ # @note For internal use only
878
+ # @!visibility private
879
+ def update_emoji_data(new_data)
880
+ @emoji = {}
881
+ process_emoji(new_data['emojis'])
882
+ end
883
+
884
+ # The inspect method is overwritten to give more useful output
885
+ def inspect
886
+ "<Server name=#{@name} id=#{@id} large=#{@large} region=#{@region} owner=#{@owner} afk_channel_id=#{@afk_channel_id} system_channel_id=#{@system_channel_id} afk_timeout=#{@afk_timeout}>"
887
+ end
888
+
889
+ private
890
+
891
+ def update_server_data(new_data)
892
+ response = JSON.parse(API::Server.update(@bot.token, @id,
893
+ new_data[:name] || @name,
894
+ new_data[:region] || @region_id,
895
+ new_data[:icon_id] || @icon_id,
896
+ new_data[:afk_channel_id] || @afk_channel_id,
897
+ new_data[:afk_timeout] || @afk_timeout,
898
+ new_data[:splash] || @splash,
899
+ new_data[:default_message_notifications] || @default_message_notifications,
900
+ new_data[:verification_level] || @verification_level,
901
+ new_data[:explicit_content_filter] || @explicit_content_filter,
902
+ new_data[:system_channel_id] || @system_channel_id))
903
+ update_data(response)
904
+ end
905
+
906
+ def process_roles(roles)
907
+ # Create roles
908
+ @roles = []
909
+ @roles_by_id = {}
910
+
911
+ return unless roles
912
+
913
+ roles.each do |element|
914
+ role = Role.new(element, @bot, self)
915
+ @roles << role
916
+ @roles_by_id[role.id] = role
917
+ end
918
+ end
919
+
920
+ def process_emoji(emoji)
921
+ return if emoji.empty?
922
+
923
+ emoji.each do |element|
924
+ new_emoji = Emoji.new(element, @bot, self)
925
+ @emoji[new_emoji.id] = new_emoji
926
+ end
927
+ end
928
+
929
+ def process_members(members)
930
+ return unless members
931
+
932
+ members.each do |element|
933
+ member = Member.new(element, self, @bot)
934
+ @members[member.id] = member
935
+ end
936
+ end
937
+
938
+ def process_presences(presences)
939
+ # Update user statuses with presence info
940
+ return unless presences
941
+
942
+ presences.each do |element|
943
+ next unless element['user']
944
+
945
+ user_id = element['user']['id'].to_i
946
+ user = @members[user_id]
947
+ if user
948
+ user.update_presence(element)
949
+ else
950
+ LOGGER.warn "Rogue presence update! #{element['user']['id']} on #{@id}"
951
+ end
952
+ end
953
+ end
954
+
955
+ def process_channels(channels)
956
+ @channels = []
957
+ @channels_by_id = {}
958
+
959
+ return unless channels
960
+
961
+ channels.each do |element|
962
+ channel = @bot.ensure_channel(element, self)
963
+ @channels << channel
964
+ @channels_by_id[channel.id] = channel
965
+ end
966
+ end
967
+
968
+ def process_voice_states(voice_states)
969
+ return unless voice_states
970
+
971
+ voice_states.each do |element|
972
+ update_voice_state(element)
973
+ end
974
+ end
975
+ end
976
+
977
+ # A ban entry on a server
978
+ class ServerBan
979
+ # @return [String, nil] the reason the user was banned, if provided
980
+ attr_reader :reason
981
+
982
+ # @return [User] the user that was banned
983
+ attr_reader :user
984
+
985
+ # @return [Server] the server this ban belongs to
986
+ attr_reader :server
987
+
988
+ # @!visibility private
989
+ def initialize(server, user, reason)
990
+ @server = server
991
+ @user = user
992
+ @reason = reason
993
+ end
994
+
995
+ # Removes this ban on the associated user in the server
996
+ # @param reason [String] the reason for removing the ban
997
+ def remove(reason = nil)
998
+ @server.unban(user, reason)
999
+ end
1000
+
1001
+ alias_method :unban, :remove
1002
+ alias_method :lift, :remove
1003
+ end
1004
+ end