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,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OnyxCord
4
+ # A permissions overwrite, when applied to channels describes additional
5
+ # permissions a member needs to perform certain actions in context.
6
+ class Overwrite
7
+ # Types of overwrites mapped to their API value.
8
+ TYPES = {
9
+ role: 0,
10
+ member: 1
11
+ }.freeze
12
+
13
+ # @return [Integer] ID of the thing associated with this overwrite type
14
+ attr_accessor :id
15
+
16
+ # @return [Symbol] either :role or :member
17
+ attr_accessor :type
18
+
19
+ # @return [Permissions] allowed permissions for this overwrite type
20
+ attr_accessor :allow
21
+
22
+ # @return [Permissions] denied permissions for this overwrite type
23
+ attr_accessor :deny
24
+
25
+ # Creates a new Overwrite object
26
+ # @example Create an overwrite for a role that can mention everyone, send TTS messages, but can't create instant invites
27
+ # allow = OnyxCord::Permissions.new
28
+ # allow.can_mention_everyone = true
29
+ # allow.can_send_tts_messages = true
30
+ #
31
+ # deny = OnyxCord::Permissions.new
32
+ # deny.can_create_instant_invite = true
33
+ #
34
+ # # Find some role by name
35
+ # role = server.roles.find { |r| r.name == 'some role' }
36
+ #
37
+ # Overwrite.new(role, allow: allow, deny: deny)
38
+ # @example Create an overwrite by ID and permissions bits
39
+ # Overwrite.new(120571255635181568, type: 'member', allow: 1024, deny: 0)
40
+ # @param object [Integer, #id] the ID or object this overwrite is for
41
+ # @param type [String, Symbol, Integer] the type of object this overwrite is for (only required if object is an Integer)
42
+ # @param allow [String, Integer, Permissions] allowed permissions for this overwrite, by bits or a Permissions object
43
+ # @param deny [String, Integer, Permissions] denied permissions for this overwrite, by bits or a Permissions object
44
+ # @raise [ArgumentError] if type is not :member or :role
45
+ def initialize(object = nil, type: nil, allow: 0, deny: 0)
46
+ if type
47
+ type = TYPES.value?(type) ? TYPES.key(type) : type.to_sym
48
+ raise ArgumentError, 'Overwrite type must be :member or :role' unless type
49
+ end
50
+
51
+ @id = object.respond_to?(:id) ? object.id : object
52
+
53
+ @type = case object
54
+ when User, Member, Recipient, Profile
55
+ :member
56
+ when Role
57
+ :role
58
+ else
59
+ type
60
+ end
61
+
62
+ @allow = allow.is_a?(Permissions) ? allow : Permissions.new(allow)
63
+ @deny = deny.is_a?(Permissions) ? deny : Permissions.new(deny)
64
+ end
65
+
66
+ # Comparison by attributes [:id, :type, :allow, :deny]
67
+ def ==(other)
68
+ return false unless other.is_a?(OnyxCord::Overwrite)
69
+
70
+ id == other.id &&
71
+ type == other.type &&
72
+ allow == other.allow &&
73
+ deny == other.deny
74
+ end
75
+
76
+ # @return [Overwrite] create an overwrite from a hash payload
77
+ # @!visibility private
78
+ def self.from_hash(data)
79
+ new(
80
+ data['id'].to_i,
81
+ type: TYPES.key(data['type']),
82
+ allow: Permissions.new(data['allow']),
83
+ deny: Permissions.new(data['deny'])
84
+ )
85
+ end
86
+
87
+ # @return [Overwrite] copies an overwrite from another Overwrite
88
+ # @!visibility private
89
+ def self.from_other(other)
90
+ new(
91
+ other.id,
92
+ type: other.type,
93
+ allow: Permissions.new(other.allow.bits),
94
+ deny: Permissions.new(other.deny.bits)
95
+ )
96
+ end
97
+
98
+ # @return [Hash] hash representation of an overwrite
99
+ # @!visibility private
100
+ def to_hash
101
+ {
102
+ id: id,
103
+ type: TYPES[type],
104
+ allow: allow.bits,
105
+ deny: deny.bits
106
+ }
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,365 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OnyxCord
4
+ # A surface with selectable answers.
5
+ class Poll
6
+ # Mapping of layout types for polls.
7
+ LAYOUTS = {
8
+ default: 1
9
+ }.freeze
10
+
11
+ # @return [Integer] the layout type of the poll.
12
+ attr_reader :layout
13
+
14
+ # @return [Array<Answer>] the answers of the poll.
15
+ attr_reader :answers
16
+
17
+ # @return [Message] the message linked to the poll.
18
+ attr_reader :message
19
+
20
+ # @return [Media] the display-data for the poll question.
21
+ attr_reader :question
22
+
23
+ # @return [Time, nil] the time at when the poll will expire.
24
+ attr_reader :closes_at
25
+
26
+ # @return [true, false] whether or not Discord has precisely counted the votes yet.
27
+ attr_reader :finalised
28
+ alias finalised? finalised
29
+ alias finalized? finalised
30
+
31
+ # @return [true, false] whether or not users are allowed to vote for multiple answers.
32
+ attr_reader :multiselect
33
+ alias multiselect? multiselect
34
+
35
+ # @return [true, false] whether or not Discord did not fetch the results for the poll.
36
+ # When this is `true`, the {#finalised?} method will always return a value of `false`,
37
+ # and {Answer#votes} will always return a value of `0`.
38
+ attr_reader :unknown_results
39
+ alias unknown_results? unknown_results
40
+
41
+ # @!visibility private
42
+ def initialize(data, message, bot)
43
+ @bot = bot
44
+ @message = message
45
+ @layout = data['layout_type']
46
+ @question = Media.new(data['question'], @bot)
47
+ @multiselect = data['allow_multiselect']
48
+ @closes_at = Time.parse(data['expiry']) if data['expiry']
49
+ results = data['results']
50
+ @unknown_results = results.nil?
51
+ @finalised = results&.[]('is_finalized') || false
52
+ process_answers(data['answers'], results&.[]('answer_counts'))
53
+ end
54
+
55
+ # Get the total amount of votes cast on the poll.
56
+ # @return [Integer] The total amount of votes that the poll received.
57
+ def total_votes
58
+ @answers.sum(&:votes)
59
+ end
60
+
61
+ # Get a single answer for the poll by its ID.
62
+ # @param answer_id [Integer, String, Answer] The ID of the poll answer.
63
+ # @return [Answer, nil] The poll answer, or `nil` if it couldn't be found.
64
+ def answer(answer_id)
65
+ answer_id = if answer_id.is_a?(Answer)
66
+ answer_id.id
67
+ else
68
+ answer_id.resolve_id
69
+ end
70
+
71
+ @answers.find { |answer| answer.id == answer_id }
72
+ end
73
+
74
+ # Check if the poll has closed.
75
+ # @return [true, false] Whether or not the poll has closed.
76
+ def closed?
77
+ @finalised || (!@closes_at.nil? && Time.now > @closes_at)
78
+ end
79
+
80
+ # Get the poll answer that has the most amount of votes.
81
+ # @return [Answer, nil] The winning poll answer, or `nil` for no winner.
82
+ def winner
83
+ return unless (max = @answers.max_by(&:votes))&.votes&.nonzero?
84
+
85
+ @answers.one? { |answer| answer.votes == max&.votes } ? max : nil
86
+ end
87
+
88
+ # Prematurely end the poll, only functional for polls created by the current bot.
89
+ # @return [Message] The resulting message. Will fail if the poll was not sent by the current bot.
90
+ # @raise [OnyxCord::Errors::NoPermission] If the poll was not created by the current bot account.
91
+ def close
92
+ raise OnyxCord::Errors::NoPermission, 'Cannot close the poll' if !@message.from_bot? || closed?
93
+
94
+ Message.new(JSON.parse(API::Channel.end_poll(@bot.token, @message.channel.id, @message.id)), @bot)
95
+ end
96
+
97
+ # Check if two poll objects are equivalent.
98
+ # @param other [Object] The object to compare the poll object against.
99
+ # @return [true, false] Whether or not the poll objects are equivalent.
100
+ def ==(other)
101
+ other.is_a?(Poll) ? @message == other.message : false
102
+ end
103
+
104
+ alias_method :eql?, :==
105
+
106
+ # @!method default_layout?
107
+ # @return [true, false] whether or not the poll is using the default layout.
108
+ LAYOUTS.each do |name, value|
109
+ define_method("#{name}_layout?") { @layout == value }
110
+ end
111
+
112
+ # @!visibility private
113
+ def inspect
114
+ "<Poll question=\"#{@question.text}\" unknown_results=#{@unknown_results}>"
115
+ end
116
+
117
+ # @!visibility private
118
+ def to_h
119
+ {
120
+ layout_type: @layout,
121
+ question: @question.to_h,
122
+ answers: @answers.map(&:to_h),
123
+ allow_multiselect: @multiselect,
124
+ duration: @closes_at ? ((@closes_at - @message.creation_time) / 3600).round : nil
125
+ }
126
+ end
127
+
128
+ private
129
+
130
+ # @!visibility private
131
+ def process_answers(answers, counts)
132
+ @answers = answers.map do |answer|
133
+ count_data = counts&.find { |count| count['id'] == answer['answer_id'] }
134
+
135
+ Answer.new(answer.tap { answer['_votes'] = count_data&.[]('count') }, self, @bot)
136
+ end
137
+ end
138
+
139
+ # A selectable poll answer.
140
+ class Answer
141
+ # @return [Integer] the ID of the poll answer.
142
+ attr_reader :id
143
+
144
+ # @return [Integer] the number of votes the answer has.
145
+ attr_reader :votes
146
+ alias vote_count votes
147
+
148
+ # @return [Integer] the ID of the message the answer is from.
149
+ attr_reader :message_id
150
+
151
+ # @!visibility private
152
+ def initialize(data, poll, bot)
153
+ @bot = bot
154
+ @poll = poll
155
+ @id = data['answer_id']
156
+ @votes = data['_votes'] || 0
157
+ @media = Media.new(data['poll_media'], @bot)
158
+ @channel_id = data['_channel_id'] unless @poll
159
+ @message_id = @poll&.message&.id || data['_message_id']&.to_i
160
+ end
161
+
162
+ # Get the text of the poll answer.
163
+ # @return [String] The text of the poll answer.
164
+ def text
165
+ @media.text
166
+ end
167
+
168
+ # Get the emoji of the poll answer.
169
+ # @return [Emoji, nil] The emoji of the poll answer.
170
+ def emoji
171
+ @media.emoji
172
+ end
173
+
174
+ # Check if the poll answer won the poll.
175
+ # @return [true, false] Whether or not the poll answer is the winner.
176
+ def winner?
177
+ @poll.nil? || @poll.winner&.id == @id
178
+ end
179
+
180
+ # Get the users who voted for the poll answer.
181
+ # @param after [Time, #resolve_id, nil] The ID or timestamp to start fetching voters from.
182
+ # @param limit [Integer, nil] The maximum number of voters to fetch, or `nil` to fetch all the voters.
183
+ # @return [Array<User>] The users who voted for the poll answer; ordered by user ID in ascending order.
184
+ def voters(after: nil, limit: 100)
185
+ stable = @poll.nil? || @poll.finalised?
186
+ channel_id = @channel_id || @poll.message.channel.id
187
+ after_time = after.is_a?(Time) ? IDObject.synthesise(after) : after&.resolve_id
188
+
189
+ get_users = lambda do |limit, after|
190
+ data = API::Channel.get_poll_voters(@bot.token, channel_id, @message_id, @id, after:, limit:)
191
+ JSON.parse(data)['users'].collect { |poll_voter_data| @bot.ensure_user(poll_voter_data) }
192
+ end
193
+
194
+ return get_users.call(limit, after_time) if limit && limit <= 100
195
+
196
+ paginator = Paginator.new(limit, :down) do |last_page|
197
+ if stable && last_page && last_page.count < 100
198
+ []
199
+ else
200
+ get_users.call(100, last_page&.last&.id || after_time)
201
+ end
202
+ end
203
+
204
+ paginator.to_a
205
+ end
206
+
207
+ # Check if two poll answers are equivalent.
208
+ # @param other [Object] The object to compare the poll answer against.
209
+ # @return [true, false] Whether or not the poll answers are equivalent.
210
+ def ==(other)
211
+ return false unless other.is_a?(Answer)
212
+
213
+ @message_id == other.message_id && @id == other.id
214
+ end
215
+
216
+ alias_method :eql?, :==
217
+
218
+ # @!visibility private
219
+ def to_h
220
+ { poll_media: @media.to_h }
221
+ end
222
+
223
+ # @!visibility private
224
+ def inspect
225
+ "<Answer id=#{@id} text=\"#{text}\" votes=#{@votes}>"
226
+ end
227
+ end
228
+
229
+ # Represents display-data for a poll.
230
+ class Media
231
+ # @return [String, nil] the text of the poll media.
232
+ attr_reader :text
233
+
234
+ # @return [Emoji, nil] the emoji of the poll media.
235
+ attr_reader :emoji
236
+
237
+ # @!visibility private
238
+ def initialize(data, bot)
239
+ @bot = bot
240
+ @text = data['text']
241
+ @emoji = Emoji.new(data['emoji'], @bot) if data['emoji']
242
+ end
243
+
244
+ # Check if two poll media objects are equivalent.
245
+ # @param other [Object] The object to compare the poll media against.
246
+ # @return [true, false] Whether or not the poll media objects are equivalent.
247
+ def ==(other)
248
+ return false unless other.is_a?(Media)
249
+
250
+ @text == other.text && @emoji == other.emoji
251
+ end
252
+
253
+ alias_method :eql?, :==
254
+
255
+ # @!visibility private
256
+ def to_h
257
+ { text: @text, emoji: @emoji&.to_h }.compact
258
+ end
259
+
260
+ # @!visibility private
261
+ def inspect
262
+ "<Media text=\"#{@text}\" emoji=#{@emoji.inspect}>"
263
+ end
264
+ end
265
+
266
+ # Finalised results for a poll.
267
+ class Result
268
+ # @return [Answer, nil] the answer that won the poll, if any.
269
+ attr_reader :winner
270
+ alias answer winner
271
+
272
+ # @return [Media] the display-data for the question of the poll.
273
+ attr_reader :question
274
+
275
+ # @return [Integer] the ID of the message the poll result is for.
276
+ attr_reader :message_id
277
+
278
+ # @return [Integer] the total amount of votes that the poll received.
279
+ attr_reader :total_votes
280
+
281
+ # @!visibility private
282
+ def initialize(embed, reference, bot)
283
+ @bot = bot
284
+ @message_id = reference['message_id'].to_i
285
+ embed = embed['fields'].to_h { |field| [field['name'], field['value']] }
286
+ @question = Media.new({ 'text' => embed['poll_question_text'] }, @bot)
287
+ @total_votes = embed['total_votes'].to_i
288
+ return unless (id = embed['victor_answer_id']&.to_i)
289
+
290
+ data = {
291
+ 'poll_media' => {
292
+ 'text' => embed['victor_answer_text']
293
+ },
294
+ 'answer_id' => id,
295
+ '_message_id' => @message_id,
296
+ '_channel_id' => reference['channel_id'],
297
+ '_votes' => embed['victor_answer_votes'].to_i
298
+ }
299
+
300
+ if embed['victor_answer_emoji_id'] || embed['victor_answer_emoji_name']
301
+ data['poll_media']['emoji'] = {
302
+ 'name' => embed['victor_answer_emoji_name'],
303
+ 'id' => embed['victor_answer_emoji_id']&.to_i,
304
+ 'animated' => embed['victor_answer_emoji_animated'] == 'true'
305
+ }
306
+ end
307
+
308
+ @winner = Answer.new(data, nil, @bot)
309
+ end
310
+
311
+ # Check if two result objects are equivalent.
312
+ # @param other [Object] The object to compare the result object against.
313
+ # @return [true, false] Whether or not the result objects are equivalent.
314
+ def ==(other)
315
+ other.is_a?(Result) ? @message_id == other.message_id : false
316
+ end
317
+
318
+ alias_method :eql?, :==
319
+
320
+ # @!visibility private
321
+ def inspect
322
+ "<Result winner=#{@winner.inspect} total_votes=#{@total_votes}>"
323
+ end
324
+ end
325
+
326
+ # Builder for polls.
327
+ class Builder
328
+ # Create a poll request object.
329
+ # @param question [String] The question of the poll; between 1-55 characters.
330
+ # @param layout [Integer, Symbol, nil] The layout type of the poll; see {LAYOUTS}.
331
+ # @param duration [Integer, Time, nil] The number of hours before the poll expires.
332
+ # @param multiselect [true, false, nil] Whether or not users can pick multiple answers.
333
+ def initialize(question:, layout: :default, duration: 24, multiselect: false)
334
+ @layout = layout
335
+ @answers = []
336
+ @question = question
337
+ @duration = duration || 24
338
+ @multiselect = multiselect
339
+ end
340
+
341
+ # Add an answer to the poll builder.
342
+ # @param text [String] The text of the poll answer.
343
+ # @param emoji [Emoji, Reaction, Integer, String, nil] The emoji of the poll answer.
344
+ # @return [void]
345
+ def answer(text:, emoji: nil)
346
+ emoji = Emoji.build_emoji_hash(emoji, prefix: false) if emoji
347
+
348
+ @answers << { poll_media: { text: text, emoji: emoji }.compact }
349
+ end
350
+
351
+ alias_method :add_answer, :answer
352
+
353
+ # @!visibility private
354
+ def to_h
355
+ {
356
+ question: { text: @question },
357
+ answers: @answers,
358
+ layout_type: LAYOUTS[@layout] || @layout,
359
+ allow_multiselect: @multiselect,
360
+ duration: @duration.is_a?(Time) ? ((@duration - Time.now) / 3600).round : @duration
361
+ }
362
+ end
363
+ end
364
+ end
365
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OnyxCord
4
+ # A server tag that a user has chosen to display on their profile.
5
+ class PrimaryServer
6
+ # @return [Integer] the ID of the server this primary server is for.
7
+ attr_reader :server_id
8
+
9
+ # @return [true, false] if the user is displaying the primary server's tag.
10
+ attr_reader :enabled
11
+ alias_method :enabled?, :enabled
12
+
13
+ # @return [String] the text of the primary server's tag. Limited to four characters.
14
+ attr_reader :name
15
+ alias_method :text, :name
16
+
17
+ # @return [String] the ID of the server tag's badge. Can be used to generate a badge URL.
18
+ # @see #badge_url
19
+ attr_reader :badge_id
20
+
21
+ # @!visibility private
22
+ def initialize(data, bot)
23
+ @bot = bot
24
+ @server_id = data['identity_guild_id']&.to_i
25
+ @enabled = data['identity_enabled']
26
+ @name = data['tag']
27
+ @badge_id = data['badge']
28
+ end
29
+
30
+ # Get the server associated with this primary server.
31
+ # @return [Server] the server associated with this primary server.
32
+ # @raise [OnyxCord::Errors::NoPermission] this can happen when the bot is not in the associated server.
33
+ def server
34
+ @bot.server(@server_id)
35
+ end
36
+
37
+ # Get the server preview associated with this primary server.
38
+ # @return [ServerPreview, nil] the server preview associated with this primary server, or `nil` if it can't be accessed.
39
+ def server_preview
40
+ @bot.server_preview(@server_id)
41
+ end
42
+
43
+ # Utility method to get a server tag's badge URL.
44
+ # @param format [String] the URL will default to `webp`. You can otherwise specify one of `jpg` or `png` to override this.
45
+ # @return [String] the URL to the server tag's badge image.
46
+ def badge_url(format = 'webp')
47
+ API.server_tag_badge_url(@server_id, @badge_id, format)
48
+ end
49
+
50
+ # Comparison based off of server ID.
51
+ # @return [true, false] if the other object is equal to this primary server.
52
+ def ==(other)
53
+ return false unless other.is_a?(PrimaryServer)
54
+
55
+ OnyxCord.id_compare?(other.server_id, @server_id)
56
+ end
57
+
58
+ alias_method :eql?, :==
59
+ end
60
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OnyxCord
4
+ # This class is a special variant of User that represents the bot's user profile (things like own username and the avatar).
5
+ # It can be accessed using {Bot#profile}.
6
+ class Profile < User
7
+ # Whether or not the user is the bot. The Profile can only ever be the bot user, so this always returns true.
8
+ # @return [true]
9
+ def current_bot?
10
+ true
11
+ end
12
+
13
+ # Sets the bot's username.
14
+ # @param username [String] The new username.
15
+ def username=(username)
16
+ update_profile_data(username: username)
17
+ end
18
+
19
+ alias_method :name=, :username=
20
+
21
+ # Changes the bot's avatar.
22
+ # @param avatar [String, File, #read, nil] A file to be used as the avatar, either
23
+ # something readable (e.g. File Object) or a data URI.
24
+ def avatar=(avatar)
25
+ if avatar.respond_to?(:read)
26
+ update_profile_data(avatar: OnyxCord.encode64(avatar))
27
+ else
28
+ update_profile_data(avatar: avatar)
29
+ end
30
+ end
31
+
32
+ # Changes the bot's banner.
33
+ # @param banner [String, File, #read, nil] A file to be used as the banner, either
34
+ # something readable (e.g. File Object) or a data URI.
35
+ def banner=(banner)
36
+ if banner.respond_to?(:read)
37
+ update_profile_data(banner: OnyxCord.encode64(banner))
38
+ else
39
+ update_profile_data(banner: banner)
40
+ end
41
+ end
42
+
43
+ # Get the bot's global bio.
44
+ # @return [String] The bot's global bio, or an empty string if it doesn't have one set.
45
+ def bio
46
+ @bot.application.description
47
+ end
48
+
49
+ # Set the bot's global bio.
50
+ # @param bio [String, nil] The bot's new global bio, or `nil` to remove the current bio.
51
+ def bio=(bio)
52
+ @bot.application.modify(description: bio)
53
+ end
54
+
55
+ # Updates the cached profile data with the new one.
56
+ # @note For internal use only.
57
+ # @!visibility private
58
+ def update_data(new_data)
59
+ @username = new_data['username']
60
+ @avatar_id = new_data['avatar']
61
+ @banner_id = new_data['banner']
62
+ end
63
+
64
+ # The inspect method is overwritten to give more useful output
65
+ def inspect
66
+ "<Profile user=#{super}>"
67
+ end
68
+
69
+ private
70
+
71
+ # @!visibility private
72
+ def update_profile_data(new_data)
73
+ update_data(JSON.parse(API::User.update_current_user(@bot.token,
74
+ new_data[:username] || :undef,
75
+ new_data.key?(:avatar) ? new_data[:avatar] : :undef,
76
+ new_data.key?(:banner) ? new_data[:banner] : :undef)))
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OnyxCord
4
+ # A reaction to a message.
5
+ class Reaction
6
+ # Map of reaction types.
7
+ TYPES = {
8
+ normal: 0,
9
+ burst: 1
10
+ }.freeze
11
+
12
+ # @return [Integer] the total amount of users who have reacted with this reaction (including burst reactions)
13
+ attr_reader :count
14
+
15
+ # @return [true, false] whether the current bot or user used this reaction
16
+ attr_reader :me
17
+ alias_method :me?, :me
18
+
19
+ # @return [Integer] the ID of the emoji, if it was custom
20
+ attr_reader :id
21
+
22
+ # @return [String] the name or unicode representation of the emoji
23
+ attr_reader :name
24
+
25
+ # @return [true, false] whether the current bot or user used this reaction as a burst reaction
26
+ attr_reader :me_burst
27
+ alias_method :me_burst?, :me_burst
28
+
29
+ # @return [Array<ColourRGB>] an array of colors used for animations in burst reactions
30
+ attr_reader :burst_colours
31
+ alias_method :burst_colors, :burst_colours
32
+
33
+ # @return [Integer] the total amount of users who have reacted with this reaction as a burst reaction
34
+ attr_reader :burst_count
35
+
36
+ # @return [Integer] the total amount of users who have reacted with this reaction as a normal reaction
37
+ attr_reader :normal_count
38
+
39
+ # @!visibility private
40
+ def initialize(data)
41
+ @count = data['count']
42
+ @me = data['me']
43
+ @id = data['emoji']['id']&.to_i
44
+ @name = data['emoji']['name']
45
+ @me_burst = data['me_burst']
46
+ @burst_colours = data['burst_colors'].map { |b| ColourRGB.new(b.delete('#')) }
47
+ @burst_count = data['count_details']['burst']
48
+ @normal_count = data['count_details']['normal']
49
+ end
50
+
51
+ # Converts this Reaction into a string that can be sent back to Discord in other reaction endpoints.
52
+ # If ID is present, it will be rendered into the form of `name:id`.
53
+ # @return [String] the name of this reaction, including the ID if it is a custom emoji
54
+ def to_s
55
+ id.nil? ? name : "#{name}:#{id}"
56
+ end
57
+
58
+ # Converts this Reaction into a hash that can be sent back to Discord in other endpoints.
59
+ # @return [Hash] A hash representation of this reaction's emoji.
60
+ def to_h
61
+ id.nil? ? { name: name } : { id: id }
62
+ end
63
+ end
64
+ end