discordrb 3.4.0 → 3.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (63) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +44 -18
  3. data/.github/ISSUE_TEMPLATE/bug_report.md +0 -1
  4. data/.github/ISSUE_TEMPLATE/feature_request.md +0 -1
  5. data/.github/workflows/codeql.yml +65 -0
  6. data/.markdownlint.json +4 -0
  7. data/.rubocop.yml +8 -2
  8. data/CHANGELOG.md +419 -222
  9. data/LICENSE.txt +1 -1
  10. data/README.md +37 -25
  11. data/discordrb-webhooks.gemspec +4 -1
  12. data/discordrb.gemspec +9 -6
  13. data/lib/discordrb/api/application.rb +202 -0
  14. data/lib/discordrb/api/channel.rb +182 -11
  15. data/lib/discordrb/api/interaction.rb +54 -0
  16. data/lib/discordrb/api/invite.rb +2 -2
  17. data/lib/discordrb/api/server.rb +42 -19
  18. data/lib/discordrb/api/user.rb +9 -3
  19. data/lib/discordrb/api/webhook.rb +57 -0
  20. data/lib/discordrb/api.rb +19 -5
  21. data/lib/discordrb/bot.rb +328 -33
  22. data/lib/discordrb/cache.rb +27 -22
  23. data/lib/discordrb/commands/command_bot.rb +14 -7
  24. data/lib/discordrb/commands/container.rb +1 -1
  25. data/lib/discordrb/commands/parser.rb +2 -2
  26. data/lib/discordrb/commands/rate_limiter.rb +1 -1
  27. data/lib/discordrb/container.rb +132 -3
  28. data/lib/discordrb/data/activity.rb +8 -1
  29. data/lib/discordrb/data/attachment.rb +15 -0
  30. data/lib/discordrb/data/audit_logs.rb +3 -3
  31. data/lib/discordrb/data/channel.rb +167 -23
  32. data/lib/discordrb/data/component.rb +229 -0
  33. data/lib/discordrb/data/integration.rb +42 -3
  34. data/lib/discordrb/data/interaction.rb +800 -0
  35. data/lib/discordrb/data/invite.rb +2 -2
  36. data/lib/discordrb/data/member.rb +108 -33
  37. data/lib/discordrb/data/message.rb +100 -20
  38. data/lib/discordrb/data/overwrite.rb +13 -7
  39. data/lib/discordrb/data/role.rb +58 -1
  40. data/lib/discordrb/data/server.rb +82 -80
  41. data/lib/discordrb/data/user.rb +69 -9
  42. data/lib/discordrb/data/webhook.rb +97 -4
  43. data/lib/discordrb/data.rb +3 -0
  44. data/lib/discordrb/errors.rb +44 -3
  45. data/lib/discordrb/events/channels.rb +1 -1
  46. data/lib/discordrb/events/interactions.rb +482 -0
  47. data/lib/discordrb/events/message.rb +9 -6
  48. data/lib/discordrb/events/presence.rb +21 -14
  49. data/lib/discordrb/events/reactions.rb +0 -1
  50. data/lib/discordrb/events/threads.rb +96 -0
  51. data/lib/discordrb/gateway.rb +30 -17
  52. data/lib/discordrb/permissions.rb +59 -34
  53. data/lib/discordrb/version.rb +1 -1
  54. data/lib/discordrb/voice/encoder.rb +13 -4
  55. data/lib/discordrb/voice/network.rb +18 -7
  56. data/lib/discordrb/voice/sodium.rb +3 -1
  57. data/lib/discordrb/voice/voice_bot.rb +3 -3
  58. data/lib/discordrb/webhooks.rb +2 -0
  59. data/lib/discordrb.rb +37 -4
  60. metadata +53 -19
  61. data/.codeclimate.yml +0 -16
  62. data/.travis.yml +0 -32
  63. data/bin/travis_build_docs.sh +0 -17
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Discordrb::Events
4
+ # Raised when a thread is created
5
+ class ThreadCreateEvent < Event
6
+ # @return [Channel] the thread in question.
7
+ attr_reader :thread
8
+
9
+ delegate :name, :server, :owner, :parent_channel, :thread_metadata, to: :thread
10
+
11
+ def initialize(data, bot)
12
+ @bot = bot
13
+ @thread = data.is_a?(Discordrb::Channel) ? data : bot.channel(data['id'].to_i)
14
+ end
15
+ end
16
+
17
+ # Event handler for ChannelCreateEvent
18
+ class ThreadCreateEventHandler < EventHandler
19
+ def matches?(event)
20
+ # Check for the proper event type
21
+ return false unless event.is_a? ThreadCreateEvent
22
+
23
+ [
24
+ matches_all(@attributes[:name], event.name) do |a, e|
25
+ a == if a.is_a? String
26
+ e.to_s
27
+ else
28
+ e
29
+ end
30
+ end,
31
+ matches_all(@attributes[:server], event.server) do |a, e|
32
+ a.resolve_id == e.resolve_id
33
+ end,
34
+ matches_all(@attributes[:invitable], event.thread.invitable) do |a, e|
35
+ a == e
36
+ end,
37
+ matches_all(@attributes[:owner], event.thread.owner) do |a, e|
38
+ a.resolve_id == e.resolve_id
39
+ end,
40
+ matches_all(@attributes[:channel], event.thread.parent) do |a, e|
41
+ a.resolve_id == e.resolve_id
42
+ end
43
+ ].reduce(true, &:&)
44
+ end
45
+ end
46
+
47
+ # Raised when a thread is updated (e.g. name changes)
48
+ class ThreadUpdateEvent < ThreadCreateEvent; end
49
+
50
+ # Event handler for ThreadUpdateEvent
51
+ class ThreadUpdateEventHandler < ThreadCreateEventHandler
52
+ def matches?(event)
53
+ # Check for the proper event type
54
+ return false unless event.is_a? ThreadUpdateEvent
55
+
56
+ super
57
+ end
58
+ end
59
+
60
+ # Raised when members are added or removed from a thread.
61
+ class ThreadMembersUpdateEvent < Event
62
+ # @return [Channel]
63
+ attr_reader :thread
64
+
65
+ # @return [Array<Member, User>]
66
+ attr_reader :added_members
67
+
68
+ # @return [Array<Integer>]
69
+ attr_reader :removed_member_ids
70
+
71
+ # @return [Integer]
72
+ attr_reader :member_count
73
+
74
+ delegate :name, :server, :owner, :parent_channel, :thread_metadata, to: :thread
75
+
76
+ def initialize(data, bot)
77
+ @bot = bot
78
+ @thread = data.is_a?(Discordrb::Channel) ? data : bot.channel(data['id'].to_i)
79
+ @added_members = data['added_members']&.map do |member|
80
+ data['guild_id'] ? bot.member(data['guild_id'], member['user_id']) : bot.user(member['user_id'])
81
+ end || []
82
+ @removed_member_ids = data['removed_member_ids']&.map(&:resolve_id) || []
83
+ @member_count = data['member_count']
84
+ end
85
+ end
86
+
87
+ # Event handler for ThreadMembersUpdateEvent
88
+ class ThreadMembersUpdateEventHandler < ThreadCreateEventHandler
89
+ def matches?(event)
90
+ # Check for the proper event type
91
+ return false unless event.is_a? ThreadMembersUpdateEvent
92
+
93
+ super
94
+ end
95
+ end
96
+ end
@@ -134,7 +134,7 @@ module Discordrb
134
134
  LARGE_THRESHOLD = 100
135
135
 
136
136
  # The version of the gateway that's supposed to be used.
137
- GATEWAY_VERSION = 6
137
+ GATEWAY_VERSION = 9
138
138
 
139
139
  # Heartbeat ACKs are Discord's way of verifying on the client side whether the connection is still alive. If this is
140
140
  # set to true (default value) the gateway client will use that functionality to detect zombie connections and
@@ -143,7 +143,10 @@ module Discordrb
143
143
  # @return [true, false] whether or not this gateway should check for heartbeat ACKs.
144
144
  attr_accessor :check_heartbeat_acks
145
145
 
146
- def initialize(bot, token, shard_key = nil, compress_mode = :stream, intents = nil)
146
+ # @return [Integer] the intent parameter sent to the gateway server.
147
+ attr_reader :intents
148
+
149
+ def initialize(bot, token, shard_key = nil, compress_mode = :stream, intents = ALL_INTENTS)
147
150
  @token = token
148
151
  @bot = bot
149
152
 
@@ -277,12 +280,12 @@ module Discordrb
277
280
  def identify
278
281
  compress = @compress_mode == :large
279
282
  send_identify(@token, {
280
- '$os': RUBY_PLATFORM,
281
- '$browser': 'discordrb',
282
- '$device': 'discordrb',
283
- '$referrer': '',
284
- '$referring_domain': ''
285
- }, compress, 100, @shard_key)
283
+ os: RUBY_PLATFORM,
284
+ browser: 'discordrb',
285
+ device: 'discordrb',
286
+ referrer: '',
287
+ referring_domain: ''
288
+ }, compress, LARGE_THRESHOLD, @shard_key, @intents)
286
289
  end
287
290
 
288
291
  # Sends an identify packet (op 2). This starts a new session on the current connection and tells Discord who we are.
@@ -292,26 +295,26 @@ module Discordrb
292
295
  # @param properties [Hash<Symbol => String>] A list of properties for Discord to use in analytics. The following
293
296
  # keys are recognised:
294
297
  #
295
- # - "$os" (recommended value: the operating system the bot is running on)
296
- # - "$browser" (recommended value: library name)
297
- # - "$device" (recommended value: library name)
298
- # - "$referrer" (recommended value: empty)
299
- # - "$referring_domain" (recommended value: empty)
298
+ # - "os" (recommended value: the operating system the bot is running on)
299
+ # - "browser" (recommended value: library name)
300
+ # - "device" (recommended value: library name)
301
+ # - "referrer" (recommended value: empty)
302
+ # - "referring_domain" (recommended value: empty)
300
303
  #
301
304
  # @param compress [true, false] Whether certain large packets should be compressed using zlib.
302
305
  # @param large_threshold [Integer] The member threshold after which a server counts as large and will have to have
303
306
  # its member list chunked.
304
307
  # @param shard_key [Array(Integer, Integer), nil] The shard key to use for sharding, represented as
305
308
  # [shard_id, num_shards], or nil if the bot should not be sharded.
306
- def send_identify(token, properties, compress, large_threshold, shard_key = nil)
309
+ def send_identify(token, properties, compress, large_threshold, shard_key = nil, intents = ALL_INTENTS)
307
310
  data = {
308
311
  # Don't send a v anymore as it's entirely determined by the URL now
309
312
  token: token,
310
313
  properties: properties,
311
314
  compress: compress,
312
- large_threshold: large_threshold
315
+ large_threshold: large_threshold,
316
+ intents: intents
313
317
  }
314
- data[:intents] = @intents unless @intents.nil?
315
318
 
316
319
  # Don't include the shard key at all if it is nil as Discord checks for its mere existence
317
320
  data[:shard] = shard_key if shard_key
@@ -790,7 +793,8 @@ module Discordrb
790
793
  # - 4003: Not authenticated. How did this happen?
791
794
  # - 4004: Authentication failed. Token was wrong, nothing we can do.
792
795
  # - 4011: Sharding required. Currently requires developer intervention.
793
- FATAL_CLOSE_CODES = [4003, 4004, 4011].freeze
796
+ # - 4014: Use of disabled privileged intents.
797
+ FATAL_CLOSE_CODES = [4003, 4004, 4011, 4014].freeze
794
798
 
795
799
  def handle_close(e)
796
800
  @bot.__send__(:raise_event, Events::DisconnectEvent.new(@bot))
@@ -800,6 +804,15 @@ module Discordrb
800
804
  LOGGER.error('Websocket close frame received!')
801
805
  LOGGER.error("Code: #{e.code}")
802
806
  LOGGER.error("Message: #{e.data}")
807
+
808
+ if e.code == 4014
809
+ LOGGER.error(<<~ERROR)
810
+ You attempted to identify with privileged intents that your bot is not authorized to use
811
+ Please enable the privileged intents on the bot page of your application on the discord developer page.
812
+ Read more here https://discord.com/developers/docs/topics/gateway#privileged-intents
813
+ ERROR
814
+ end
815
+
803
816
  @should_reconnect = false if FATAL_CLOSE_CODES.include?(e.code)
804
817
  elsif e.is_a? Exception
805
818
  # Log the exception
@@ -6,37 +6,47 @@ module Discordrb
6
6
  # This hash maps bit positions to logical permissions.
7
7
  FLAGS = {
8
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
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
40
50
  }.freeze
41
51
 
42
52
  FLAGS.each do |position, flag|
@@ -101,7 +111,7 @@ module Discordrb
101
111
  # permission.can_speak = true
102
112
  # @example Create a permissions object that could allow/deny read messages, connect, and speak by an array of symbols
103
113
  # Permissions.new [:read_messages, :connect, :speak]
104
- # @param bits [Integer, Array<Symbol>] The permission bits that should be set from the beginning, or an array of permission flag symbols
114
+ # @param bits [String, Integer, Array<Symbol>] The permission bits that should be set from the beginning, or an array of permission flag symbols
105
115
  # @param writer [RoleWriter] The writer that should be used to update data when a permission is set.
106
116
  def initialize(bits = 0, writer = nil)
107
117
  @writer = writer
@@ -109,12 +119,21 @@ module Discordrb
109
119
  @bits = if bits.is_a? Array
110
120
  self.class.bits(bits)
111
121
  else
112
- bits
122
+ bits.to_i
113
123
  end
114
124
 
115
125
  init_vars
116
126
  end
117
127
 
128
+ # Return an array of permission flag symbols for this class's permissions
129
+ # @example Get the permissions for the bits "9"
130
+ # permissions = Permissions.new(9)
131
+ # permissions.defined_permissions #=> [:create_instant_invite, :administrator]
132
+ # @return [Array<Symbol>] the permissions
133
+ def defined_permissions
134
+ FLAGS.filter_map { |value, name| (@bits & (1 << value)).positive? ? name : nil }
135
+ end
136
+
118
137
  # Comparison based on permission bits
119
138
  def ==(other)
120
139
  false unless other.is_a? Discordrb::Permissions
@@ -154,6 +173,12 @@ module Discordrb
154
173
  # has_manage_channels = member.defined_permission?(:manage_channels)
155
174
  # @return [true, false] whether or not this user has the permission defined.
156
175
  def defined_permission?(action, channel = nil)
176
+ # For slash commands we may not have access to the server or role
177
+ # permissions. In this case we use the permissions given to us by the
178
+ # interaction. If attempting to check against a specific channel the check
179
+ # is skipped.
180
+ return @permissions.__send__(action) if @permissions && channel.nil?
181
+
157
182
  # Get the permission the user's roles have
158
183
  role_permission = defined_role_permission?(action, channel)
159
184
 
@@ -179,7 +204,7 @@ module Discordrb
179
204
  private
180
205
 
181
206
  def defined_role_permission?(action, channel)
182
- roles_to_check = [@server.everyone_role] + @roles
207
+ roles_to_check = [@server.everyone_role] + roles
183
208
 
184
209
  # For each role, check if
185
210
  # (1) the channel explicitly allows or permits an action for the role and
@@ -3,5 +3,5 @@
3
3
  # Discordrb and all its functionality, in this case only the version.
4
4
  module Discordrb
5
5
  # The current version of discordrb.
6
- VERSION = '3.4.0'
6
+ VERSION = '3.5.0'
7
7
  end
@@ -77,7 +77,7 @@ module Discordrb::Voice
77
77
  # @param options [String] ffmpeg options to pass after the -i flag
78
78
  # @return [IO] the audio, encoded as s16le PCM
79
79
  def encode_file(file, options = '')
80
- command = "#{ffmpeg_command} -loglevel 0 -i \"#{file}\" #{options} -f s16le -ar 48000 -ac 2 #{filter_volume_argument} pipe:1"
80
+ command = ffmpeg_command(input: file, options: options)
81
81
  IO.popen(command)
82
82
  end
83
83
 
@@ -87,14 +87,23 @@ module Discordrb::Voice
87
87
  # @param options [String] ffmpeg options to pass after the -i flag
88
88
  # @return [IO] the audio, encoded as s16le PCM
89
89
  def encode_io(io, options = '')
90
- command = "#{ffmpeg_command} -loglevel 0 -i - #{options} -f s16le -ar 48000 -ac 2 #{filter_volume_argument} pipe:1"
90
+ command = ffmpeg_command(options: options)
91
91
  IO.popen(command, in: io)
92
92
  end
93
93
 
94
94
  private
95
95
 
96
- def ffmpeg_command
97
- @use_avconv ? 'avconv' : 'ffmpeg'
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 == '' }
98
107
  end
99
108
 
100
109
  def filter_volume_argument
@@ -42,10 +42,10 @@ module Discordrb::Voice
42
42
  attr_writer :secret_key
43
43
 
44
44
  # The UDP encryption mode
45
- attr_reader :mode # rubocop:disable Style/BisectedAttrAccessor
45
+ attr_reader :mode
46
46
 
47
47
  # @!visibility private
48
- attr_writer :mode # rubocop:disable Style/BisectedAttrAccessor
48
+ attr_writer :mode
49
49
 
50
50
  # Creates a new UDP connection. Only creates a socket as the discovery reply may come before the data is
51
51
  # initialized.
@@ -69,9 +69,9 @@ module Discordrb::Voice
69
69
  # @return [Array(String, Integer)] the IP and port received from the discovery reply.
70
70
  def receive_discovery_reply
71
71
  # Wait for a UDP message
72
- message = @socket.recv(70)
73
- ip = message[4..-3].delete("\0")
74
- port = message[-2..-1].unpack1('n')
72
+ message = @socket.recv(74)
73
+ ip = message[8..-3].delete("\0")
74
+ port = message[-2..].unpack1('n')
75
75
  [ip, port]
76
76
  end
77
77
 
@@ -98,10 +98,21 @@ module Discordrb::Voice
98
98
  # Sends the UDP discovery packet with the internally stored SSRC. Discord will send a reply afterwards which can
99
99
  # be received using {#receive_discovery_reply}
100
100
  def send_discovery
101
- discovery_packet = [@ssrc].pack('N')
101
+ # Create empty packet
102
+ discovery_packet = ''
102
103
 
103
- # Add 66 zeroes so the packet is 70 bytes long
104
+ # Add Type request (0x1 = request, 0x2 = response)
105
+ discovery_packet += [0x1].pack('n')
106
+
107
+ # Add Length (excluding Type and itself = 70)
108
+ discovery_packet += [70].pack('n')
109
+
110
+ # Add SSRC
111
+ discovery_packet += [@ssrc].pack('N')
112
+
113
+ # Add 66 zeroes so the packet is 74 bytes long
104
114
  discovery_packet += "\0" * 66
115
+
105
116
  send_packet(discovery_packet)
106
117
  end
107
118
 
@@ -1,9 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'ffi'
4
+
3
5
  module Discordrb::Voice
4
6
  # @!visibility private
5
7
  module Sodium
6
- extend FFI::Library
8
+ extend ::FFI::Library
7
9
 
8
10
  ffi_lib(['sodium', 'libsodium.so.18', 'libsodium.so.23'])
9
11
 
@@ -22,10 +22,10 @@ module Discordrb::Voice
22
22
  # {VoiceBot#adjust_offset}, and {VoiceBot#adjust_average}.
23
23
  class VoiceBot
24
24
  # @return [Channel] the current voice channel
25
- attr_reader :channel # rubocop:disable Style/BisectedAttrAccessor
25
+ attr_reader :channel
26
26
 
27
27
  # @!visibility private
28
- attr_writer :channel # rubocop:disable Style/BisectedAttrAccessor
28
+ attr_writer :channel
29
29
 
30
30
  # @return [Integer, nil] the amount of time the stream has been playing, or `nil` if nothing has been played yet.
31
31
  attr_reader :stream_time
@@ -149,7 +149,7 @@ module Discordrb::Voice
149
149
 
150
150
  # Sets whether or not the bot is speaking (green circle around user).
151
151
  # @param value [true, false, Integer] whether or not the bot should be speaking, or a bitmask denoting the audio type
152
- # @note https://discordapp.com/developers/docs/topics/voice-connections#speaking for information on the speaking bitmask
152
+ # @note https://discord.com/developers/docs/topics/voice-connections#speaking for information on the speaking bitmask
153
153
  def speaking=(value)
154
154
  @playing = value
155
155
  @ws.send_speaking(value)
@@ -4,6 +4,8 @@ require 'discordrb/webhooks/version'
4
4
  require 'discordrb/webhooks/embeds'
5
5
  require 'discordrb/webhooks/client'
6
6
  require 'discordrb/webhooks/builder'
7
+ require 'discordrb/webhooks/view'
8
+ require 'discordrb/webhooks/modal'
7
9
 
8
10
  module Discordrb
9
11
  # Webhook client
data/lib/discordrb.rb CHANGED
@@ -10,13 +10,13 @@ module Discordrb
10
10
  Thread.current[:discordrb_name] = 'main'
11
11
 
12
12
  # The default debug logger used by discordrb.
13
- LOGGER = Logger.new(ENV['DISCORDRB_FANCY_LOG'])
13
+ LOGGER = Logger.new(ENV.fetch('DISCORDRB_FANCY_LOG', false))
14
14
 
15
15
  # The Unix timestamp Discord IDs are based on
16
16
  DISCORD_EPOCH = 1_420_070_400_000
17
17
 
18
18
  # Used to declare what events you wish to recieve from Discord.
19
- # @see https://discordapp.com/developers/docs/topics/gateway#gateway-intents
19
+ # @see https://discord.com/developers/docs/topics/gateway#gateway-intents
20
20
  INTENTS = {
21
21
  servers: 1 << 0,
22
22
  server_members: 1 << 1,
@@ -35,9 +35,16 @@ module Discordrb
35
35
  direct_message_typing: 1 << 14
36
36
  }.freeze
37
37
 
38
- # @return [Integer] All available intents
38
+ # All available intents
39
39
  ALL_INTENTS = INTENTS.values.reduce(&:|)
40
40
 
41
+ # All unprivileged intents
42
+ # @see https://discord.com/developers/docs/topics/gateway#privileged-intents
43
+ UNPRIVILEGED_INTENTS = ALL_INTENTS & ~(INTENTS[:server_members] | INTENTS[:server_presences])
44
+
45
+ # No intents
46
+ NO_INTENTS = 0
47
+
41
48
  # Compares two objects based on IDs - either the objects' IDs are equal, or one object is equal to the other's ID.
42
49
  def self.id_compare(one_id, other)
43
50
  other.respond_to?(:resolve_id) ? (one_id.resolve_id == other.resolve_id) : (one_id == other)
@@ -46,6 +53,18 @@ module Discordrb
46
53
  # The maximum length a Discord message can have
47
54
  CHARACTER_LIMIT = 2000
48
55
 
56
+ # For creating timestamps with {timestamp}
57
+ # @see https://discord.com/developers/docs/reference#message-formatting-timestamp-styles
58
+ TIMESTAMP_STYLES = {
59
+ short_time: 't', # 16:20
60
+ long_time: 'T', # 16:20:30
61
+ short_date: 'd', # 20/04/2021
62
+ long_date: 'D', # 20 April 2021
63
+ short_datetime: 'f', # 20 April 2021 16:20
64
+ long_datetime: 'F', # Tuesday, 20 April 2021 16:20
65
+ relative: 'R' # 2 months ago
66
+ }.freeze
67
+
49
68
  # Splits a message into chunks of 2000 characters. Attempts to split by lines if possible.
50
69
  # @param msg [String] The message to split.
51
70
  # @return [Array<String>] the message split into chunks
@@ -76,7 +95,7 @@ module Discordrb
76
95
  ideal_ary = ideal.length > CHARACTER_LIMIT ? ideal.split(/(.{1,#{CHARACTER_LIMIT}}\b|.{1,#{CHARACTER_LIMIT}})/o).reject(&:empty?) : [ideal]
77
96
 
78
97
  # Slice off the ideal part and strip newlines
79
- rest = msg[ideal.length..-1].strip
98
+ rest = msg[ideal.length..].strip
80
99
 
81
100
  # If none remains, return an empty array -> we're done
82
101
  return [] unless rest
@@ -84,6 +103,20 @@ module Discordrb
84
103
  # Otherwise, call the method recursively to split the rest of the string and add it onto the ideal array
85
104
  ideal_ary + split_message(rest)
86
105
  end
106
+
107
+ # @param time [Time, Integer] The time to create the timestamp from, or a unix timestamp integer.
108
+ # @param style [Symbol, String] One of the keys from {TIMESTAMP_STYLES} or a string with the style.
109
+ # @return [String]
110
+ # @example
111
+ # Discordrb.timestamp(Time.now, :short_time)
112
+ # # => "<t:1632146954:t>"
113
+ def self.timestamp(time, style = nil)
114
+ if style.nil?
115
+ "<t:#{time.to_i}>"
116
+ else
117
+ "<t:#{time.to_i}:#{TIMESTAMP_STYLES[style] || style}>"
118
+ end
119
+ end
87
120
  end
88
121
 
89
122
  # In discordrb, Integer and {String} are monkey-patched to allow for easy resolution of IDs