onyxcord-webhooks 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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 48bbef48168955335002ea4aa8a7f61a6881f7d511290e27461b1f2fec078cbd
4
+ data.tar.gz: 78511aff20a88484953ca58e471a36572c08c41757853feb83f305c0bf1b87bc
5
+ SHA512:
6
+ metadata.gz: 3c12912a5a5b79122bf0edfa5c79aa82012461c1bbe8fda4af540564f0823af6e840d00b9ade45c1434b35e5dec93db6524d80b0419a47cd11adf94b56f580ce
7
+ data.tar.gz: b7181c12ce901f98e925493a9f196c1b2f94676a4b77241780572f4853939ba389b4dfb260c84220c387f9e19290080e8c607ccfff7e47712e1e6f0b45f82664
@@ -0,0 +1,150 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'onyxcord/webhooks/embeds'
4
+ require 'onyxcord/message_components'
5
+
6
+ module OnyxCord::Webhooks
7
+ # A class that acts as a builder for a webhook message object.
8
+ class Builder
9
+ def initialize(content: '', username: nil, avatar_url: nil, tts: false, file: nil, embeds: [], allowed_mentions: nil, poll: nil, flags: 0)
10
+ @content = content
11
+ @username = username
12
+ @avatar_url = avatar_url
13
+ @tts = tts
14
+ @file = file
15
+ @embeds = embeds
16
+ @allowed_mentions = allowed_mentions
17
+ @poll = poll
18
+ @flags = flags
19
+ end
20
+
21
+ # The content of the message. May be 2000 characters long at most.
22
+ # @return [String] the content of the message.
23
+ attr_accessor :content
24
+
25
+ # The username the webhook will display as. If this is not set, the default username set in the webhook's settings
26
+ # will be used instead.
27
+ # @return [String] the username.
28
+ attr_accessor :username
29
+
30
+ # The URL of an image file to be used as an avatar. If this is not set, the default avatar from the webhook's
31
+ # settings will be used instead.
32
+ # @return [String] the avatar URL.
33
+ attr_accessor :avatar_url
34
+
35
+ # Whether this message should use TTS or not. By default, it doesn't.
36
+ # @return [true, false] the TTS status.
37
+ attr_accessor :tts
38
+
39
+ # Message flags to send with this webhook payload.
40
+ # @return [Integer] the message flags.
41
+ attr_accessor :flags
42
+
43
+ # Enable Discord's Components V2 message flag.
44
+ # @return [Builder] this builder.
45
+ def components_v2!
46
+ @flags = @flags.to_i | OnyxCord::MessageComponents::IS_COMPONENTS_V2
47
+ self
48
+ end
49
+
50
+ alias has_components! components_v2!
51
+
52
+ # @return [true, false] whether Components V2 is enabled on this payload.
53
+ def components_v2?
54
+ (@flags.to_i & OnyxCord::MessageComponents::IS_COMPONENTS_V2).positive?
55
+ end
56
+
57
+ # Sets a file to be sent together with the message. Mutually exclusive with embeds; a webhook message can contain
58
+ # either a file to be sent or an embed.
59
+ # @param file [File] A file to be sent.
60
+ def file=(file)
61
+ raise ArgumentError, 'Embeds and files are mutually exclusive!' unless @embeds.empty?
62
+
63
+ @file = file
64
+ end
65
+
66
+ # Adds an embed to this message.
67
+ # @param embed [Embed] The embed to add.
68
+ def <<(embed)
69
+ raise ArgumentError, 'Embeds and files are mutually exclusive!' if @file
70
+
71
+ @embeds << embed
72
+ end
73
+
74
+ # Convenience method to add an embed using a block-style builder pattern
75
+ # @example Add an embed to a message
76
+ # builder.add_embed do |embed|
77
+ # embed.title = 'Testing'
78
+ # embed.image = OnyxCord::Webhooks::EmbedImage.new(url: 'https://i.imgur.com/PcMltU7.jpg')
79
+ # end
80
+ # @param embed [Embed, nil] The embed to start the building process with, or nil if one should be created anew.
81
+ # @return [Embed] The created embed.
82
+ def add_embed(embed = nil)
83
+ embed ||= Embed.new
84
+ yield(embed)
85
+ self << embed
86
+ embed
87
+ end
88
+
89
+ # Convenience method to add a poll using a builder pattern
90
+ # @example Add a poll to a message
91
+ # builder.poll(question: "Best Fruit?", duration: 48) do |poll|
92
+ # poll.answer(text: "Apple", emoji: "🍎")
93
+ # poll.answer(text: "Orange", emoji: "🍊")
94
+ # poll.answer(text: "Pomelo", emoji: "🍈")
95
+ # end
96
+ # @param poll [Poll::Builder, Poll, Hash, nil] The poll to start the building process with, or nil if one should be created anew.
97
+ # @return [Poll::Builder, Poll] The created poll.
98
+ def add_poll(poll = nil, **kwargs)
99
+ poll ||= OnyxCord::Poll::Builder.new(**kwargs)
100
+ yield(poll) if block_given?
101
+ @poll = poll
102
+ poll
103
+ end
104
+
105
+ alias_method :poll, :add_poll
106
+
107
+ # @return [File, nil] the file attached to this message.
108
+ attr_reader :file
109
+
110
+ # @return [Array<Embed>] the embeds attached to this message.
111
+ attr_reader :embeds
112
+
113
+ # @return [OnyxCord::AllowedMentions, Hash] Mentions that are allowed to ping in this message.
114
+ # @see https://discord.com/developers/docs/resources/channel#allowed-mentions-object
115
+ attr_accessor :allowed_mentions
116
+
117
+ # @return [Poll, Poll::Builder, Hash, nil] The poll attached to this message.
118
+ # @see https://discord.com/developers/docs/resources/poll#poll-create-request-object
119
+ attr_writer :poll
120
+
121
+ # @return [Hash] a hash representation of the created message, for JSON format.
122
+ def to_json_hash
123
+ data = {
124
+ content: @content,
125
+ username: @username,
126
+ avatar_url: @avatar_url,
127
+ tts: @tts,
128
+ embeds: @embeds.map(&:to_hash),
129
+ allowed_mentions: @allowed_mentions&.to_hash,
130
+ poll: @poll&.to_h
131
+ }
132
+ data[:flags] = @flags if @flags.to_i.positive?
133
+ data
134
+ end
135
+
136
+ # @return [Hash] a hash representation of the created message, for multipart format.
137
+ def to_multipart_hash
138
+ data = {
139
+ content: @content,
140
+ username: @username,
141
+ avatar_url: @avatar_url,
142
+ tts: @tts,
143
+ file: @file,
144
+ allowed_mentions: @allowed_mentions&.to_hash
145
+ }
146
+ data[:flags] = @flags if @flags.to_i.positive?
147
+ data
148
+ end
149
+ end
150
+ end
@@ -0,0 +1,172 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rest-client'
4
+ require 'json'
5
+
6
+ require 'onyxcord/webhooks/builder'
7
+
8
+ module OnyxCord::Webhooks
9
+ # A client for a particular webhook added to a Discord channel.
10
+ class Client
11
+ # Create a new webhook
12
+ # @param url [String] The URL to post messages to.
13
+ # @param id [Integer] The webhook's ID. Will only be used if `url` is not
14
+ # set.
15
+ # @param token [String] The webhook's authorisation token. Will only be used
16
+ # if `url` is not set.
17
+ def initialize(url: nil, id: nil, token: nil)
18
+ @url = url || generate_url(id, token)
19
+ end
20
+
21
+ # Executes the webhook this client points to with the given data.
22
+ # @param builder [Builder, nil] The builder to start out with, or nil if one should be created anew.
23
+ # @param wait [true, false] Whether Discord should wait for the message to be successfully received by clients, or
24
+ # whether it should return immediately after sending the message.
25
+ # @param thread_id [String, Integer, nil] The thread_id of the thread if a thread should be targeted for the webhook execution
26
+ # @yield [builder] Gives the builder to the block to add additional steps, or to do the entire building process.
27
+ # @yieldparam builder [Builder] The builder given as a parameter which is used as the initial step to start from.
28
+ # @example Execute the webhook with an already existing builder
29
+ # builder = OnyxCord::Webhooks::Builder.new # ...
30
+ # client.execute(builder)
31
+ # @example Execute the webhook by building a new message
32
+ # client.execute do |builder|
33
+ # builder.content = 'Testing'
34
+ # builder.username = 'onyxcord'
35
+ # builder.add_embed do |embed|
36
+ # embed.timestamp = Time.now
37
+ # embed.title = 'Testing'
38
+ # embed.image = OnyxCord::Webhooks::EmbedImage.new(url: 'https://i.imgur.com/PcMltU7.jpg')
39
+ # end
40
+ # end
41
+ # @return [RestClient::Response] the response returned by Discord.
42
+ def execute(builder = nil, wait = false, components = nil, thread_id: nil, flags: nil, has_components: false, components_v2: false)
43
+ raise TypeError, 'builder needs to be nil or like a OnyxCord::Webhooks::Builder!' unless
44
+ (builder.respond_to?(:file) && builder.respond_to?(:to_multipart_hash)) || builder.respond_to?(:to_json_hash) || builder.nil?
45
+
46
+ builder ||= Builder.new
47
+ view = View.new
48
+
49
+ yield(builder, view) if block_given?
50
+
51
+ components ||= view
52
+
53
+ if builder.file
54
+ post_multipart(builder, components, wait, thread_id, flags: flags, has_components: has_components || components_v2)
55
+ else
56
+ post_json(builder, components, wait, thread_id, flags: flags, has_components: has_components || components_v2)
57
+ end
58
+ end
59
+
60
+ # Modify this webhook's properties.
61
+ # @param name [String, nil] The default name.
62
+ # @param avatar [String, #read, nil] The new avatar, in base64-encoded JPG format.
63
+ # @param channel_id [String, Integer, nil] The channel to move the webhook to.
64
+ # @return [RestClient::Response] the response returned by Discord.
65
+ def modify(name: nil, avatar: nil, channel_id: nil)
66
+ RestClient.patch(@url, { name: name, avatar: avatarise(avatar), channel_id: channel_id }.compact.to_json, content_type: :json)
67
+ end
68
+
69
+ # Delete this webhook.
70
+ # @param reason [String, nil] The reason this webhook was deleted.
71
+ # @return [RestClient::Response] the response returned by Discord.
72
+ # @note This is permanent and cannot be undone.
73
+ def delete(reason: nil)
74
+ RestClient.delete(@url, 'X-Audit-Log-Reason': reason)
75
+ end
76
+
77
+ # Edit a message from this webhook.
78
+ # @param message_id [String, Integer] The ID of the message to edit.
79
+ # @param builder [Builder, nil] The builder to start out with, or nil if one should be created anew.
80
+ # @param content [String] The message content.
81
+ # @param embeds [Array<Embed, Hash>]
82
+ # @param allowed_mentions [Hash]
83
+ # @param thread_id [String, Integer, nil] The id of the thread in which the message resides
84
+ # @return [RestClient::Response] the response returned by Discord.
85
+ # @example Edit message content
86
+ # client.edit_message(message_id, content: 'goodbye world!')
87
+ # @example Edit a message via builder
88
+ # client.edit_message(message_id) do |builder|
89
+ # builder.add_embed do |e|
90
+ # e.description = 'Hello World!'
91
+ # end
92
+ # end
93
+ # @note Not all builder options are available when editing.
94
+ def edit_message(message_id, builder: nil, content: nil, embeds: nil, allowed_mentions: nil, components: nil, flags: nil, thread_id: nil, has_components: false, components_v2: false)
95
+ builder ||= Builder.new
96
+
97
+ yield builder if block_given?
98
+
99
+ query = URI.encode_www_form({ thread_id: }.compact)
100
+ components = View.component_payload(components) unless components.nil?
101
+ data = builder.to_json_hash
102
+ builder_flags = data[:flags] if data.is_a?(Hash)
103
+ flags = View.apply_v2_flag(flags || builder_flags, components, force: has_components || components_v2)
104
+ data = data.merge({ content: content, embeds: embeds, allowed_mentions: allowed_mentions, components: components, flags: flags }.compact)
105
+ RestClient.patch(
106
+ "#{@url}/messages/#{message_id}#{(query.empty? ? '' : "?#{query}")}",
107
+ data.compact.to_json, content_type: :json
108
+ )
109
+ end
110
+
111
+ # Delete a message created by this webhook.
112
+ # @param message_id [String, Integer] The ID of the message to delete.
113
+ # @return [RestClient::Response] the response returned by Discord.
114
+ def delete_message(message_id)
115
+ RestClient.delete("#{@url}/messages/#{message_id}")
116
+ end
117
+
118
+ private
119
+
120
+ # Convert an avatar to API ready data.
121
+ # @param avatar [String, #read] Avatar data.
122
+ def avatarise(avatar)
123
+ if avatar.respond_to? :read
124
+ "data:image/jpg;base64,#{Base64.strict_encode64(avatar.read)}"
125
+ else
126
+ avatar
127
+ end
128
+ end
129
+
130
+ def post_json(builder, components, wait, thread_id, flags: nil, has_components: false)
131
+ components = View.component_payload(components)
132
+ data = builder.to_json_hash
133
+ builder_flags = data[:flags] if data.is_a?(Hash)
134
+ flags = View.apply_v2_flag(flags || builder_flags, components, force: has_components)
135
+ data = data.merge({ components: components })
136
+ data[:flags] = flags unless flags.nil?
137
+ RestClient.post(encode_url(wait, thread_id, with_components: components.any?), data.to_json, content_type: :json)
138
+ end
139
+
140
+ def post_multipart(builder, components, wait, thread_id, flags: nil, has_components: false)
141
+ components = View.component_payload(components)
142
+ data = builder.to_multipart_hash
143
+ builder_flags = data[:flags] if data.is_a?(Hash)
144
+ flags = View.apply_v2_flag(flags || builder_flags, components, force: has_components)
145
+ data[:components] = components if components.any?
146
+ data[:flags] = flags unless flags.nil?
147
+ RestClient.post(encode_url(wait, thread_id, with_components: components.any?), data.compact)
148
+ end
149
+
150
+ def generate_url(id, token)
151
+ "https://discord.com/api/v9/webhooks/#{id}/#{token}"
152
+ end
153
+
154
+ def encode_url(wait, thread_id, with_components: false)
155
+ uri = URI.parse(@url)
156
+
157
+ # NOTE: We have to use string keys here, since URI#decode_www_form returns
158
+ # string keys, and keys with different types are treated as array query params
159
+ # and appended to the query params twice.
160
+ query = {
161
+ 'wait' => wait,
162
+ 'thread_id' => thread_id,
163
+ 'with_components' => (with_components ? true : nil),
164
+ **URI.decode_www_form(uri.query || '').to_h
165
+ }
166
+
167
+ query = URI.encode_www_form(query.compact)
168
+ uri.query = query unless query.empty?
169
+ uri.to_s
170
+ end
171
+ end
172
+ end
@@ -0,0 +1,250 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OnyxCord::Webhooks
4
+ # An embed is a multipart-style attachment to a webhook message that can have a variety of different purposes and
5
+ # appearances.
6
+ class Embed
7
+ def initialize(title: nil, description: nil, url: nil, timestamp: nil, colour: nil, color: nil, footer: nil,
8
+ image: nil, thumbnail: nil, video: nil, provider: nil, author: nil, fields: [])
9
+ @title = title
10
+ @description = description
11
+ @url = url
12
+ @timestamp = timestamp
13
+ self.colour = colour || color
14
+ @footer = footer
15
+ @image = image
16
+ @thumbnail = thumbnail
17
+ @video = video
18
+ @provider = provider
19
+ @author = author
20
+ @fields = fields
21
+ end
22
+
23
+ # @return [String, nil] title of the embed that will be displayed above everything else.
24
+ attr_accessor :title
25
+
26
+ # @return [String, nil] description for this embed
27
+ attr_accessor :description
28
+
29
+ # @return [String, nil] URL the title should point to
30
+ attr_accessor :url
31
+
32
+ # @return [Time, nil] timestamp for this embed. Will be displayed just below the title.
33
+ attr_accessor :timestamp
34
+
35
+ # @return [Integer, nil] the colour of the bar to the side, in decimal form
36
+ attr_reader :colour
37
+ alias_method :color, :colour
38
+
39
+ # Sets the colour of the bar to the side of the embed to something new.
40
+ # @param value [String, Integer, {Integer, Integer, Integer}, #to_i, nil] The colour in decimal, hexadecimal, R/G/B decimal, or nil to clear the embeds colour
41
+ # form.
42
+ def colour=(value)
43
+ if value.nil?
44
+ @colour = nil
45
+ elsif value.is_a? Integer
46
+ raise ArgumentError, 'Embed colour must be 24-bit!' if value >= 16_777_216
47
+
48
+ @colour = value
49
+ elsif value.is_a? String
50
+ self.colour = value.delete('#').to_i(16)
51
+ elsif value.is_a? Array
52
+ raise ArgumentError, 'Colour tuple must have three values!' if value.length != 3
53
+
54
+ self.colour = (value[0] << 16) | (value[1] << 8) | value[2]
55
+ else
56
+ self.colour = value.to_i
57
+ end
58
+ end
59
+
60
+ alias_method :color=, :colour=
61
+
62
+ # @example Add a footer to an embed
63
+ # embed.footer = OnyxCord::Webhooks::EmbedFooter.new(text: 'Hello', icon_url: 'https://i.imgur.com/j69wMDu.jpg')
64
+ # @return [EmbedFooter, nil] footer for this embed
65
+ attr_accessor :footer
66
+
67
+ # @see EmbedImage
68
+ # @example Add a image to an embed
69
+ # embed.image = OnyxCord::Webhooks::EmbedImage.new(url: 'https://i.imgur.com/PcMltU7.jpg')
70
+ # @return [EmbedImage, nil] image for this embed
71
+ attr_accessor :image
72
+
73
+ # @see EmbedThumbnail
74
+ # @example Add a thumbnail to an embed
75
+ # embed.thumbnail = OnyxCord::Webhooks::EmbedThumbnail.new(url: 'https://i.imgur.com/xTG3a1I.jpg')
76
+ # @return [EmbedThumbnail, nil] thumbnail for this embed
77
+ attr_accessor :thumbnail
78
+
79
+ # @see EmbedAuthor
80
+ # @example Add a author to an embed
81
+ # embed.author = OnyxCord::Webhooks::EmbedAuthor.new(name: 'Gustavo S.', url: 'https://github.com/kruldevb')
82
+ # @return [EmbedAuthor, nil] author for this embed
83
+ attr_accessor :author
84
+
85
+ # Add a field object to this embed.
86
+ # @param field [EmbedField] The field to add.
87
+ def <<(field)
88
+ @fields << field
89
+ end
90
+
91
+ # Convenience method to add a field to the embed without having to create one manually.
92
+ # @see EmbedField
93
+ # @example Add a field to an embed, conveniently
94
+ # embed.add_field(name: 'A field', value: "The field's content")
95
+ # @param name [String] The field's name
96
+ # @param value [String] The field's value
97
+ # @param inline [true, false] Whether the field should be inline
98
+ def add_field(name: nil, value: nil, inline: nil)
99
+ self << EmbedField.new(name: name, value: value, inline: inline)
100
+ end
101
+
102
+ # @return [Array<EmbedField>] the fields attached to this embed.
103
+ attr_accessor :fields
104
+
105
+ # @return [Hash] a hash representation of this embed, to be converted to JSON.
106
+ def to_hash
107
+ {
108
+ title: @title,
109
+ description: @description,
110
+ url: @url,
111
+ timestamp: @timestamp&.utc&.iso8601,
112
+ color: @colour,
113
+ footer: @footer&.to_hash,
114
+ image: @image&.to_hash,
115
+ thumbnail: @thumbnail&.to_hash,
116
+ video: @video&.to_hash,
117
+ provider: @provider&.to_hash,
118
+ author: @author&.to_hash,
119
+ fields: @fields.map(&:to_hash)
120
+ }
121
+ end
122
+ end
123
+
124
+ # An embed's footer will be displayed at the very bottom of an embed, together with the timestamp. An icon URL can be
125
+ # set together with some text to be displayed.
126
+ class EmbedFooter
127
+ # @return [String, nil] text to be displayed in the footer
128
+ attr_accessor :text
129
+
130
+ # @return [String, nil] URL to an icon to be showed alongside the text
131
+ attr_accessor :icon_url
132
+
133
+ # Creates a new footer object.
134
+ # @param text [String, nil] The text to be displayed in the footer.
135
+ # @param icon_url [String, nil] The URL to an icon to be showed alongside the text.
136
+ def initialize(text: nil, icon_url: nil)
137
+ @text = text
138
+ @icon_url = icon_url
139
+ end
140
+
141
+ # @return [Hash] a hash representation of this embed footer, to be converted to JSON.
142
+ def to_hash
143
+ {
144
+ text: @text,
145
+ icon_url: @icon_url
146
+ }
147
+ end
148
+ end
149
+
150
+ # An embed's image will be displayed at the bottom, in large format. It will replace a footer icon URL if one is set.
151
+ class EmbedImage
152
+ # @return [String, nil] URL of the image
153
+ attr_accessor :url
154
+
155
+ # Creates a new image object.
156
+ # @param url [String, nil] The URL of the image.
157
+ def initialize(url: nil)
158
+ @url = url
159
+ end
160
+
161
+ # @return [Hash] a hash representation of this embed image, to be converted to JSON.
162
+ def to_hash
163
+ {
164
+ url: @url
165
+ }
166
+ end
167
+ end
168
+
169
+ # An embed's thumbnail will be displayed at the right of the message, next to the description and fields. When clicked
170
+ # it will point to the embed URL.
171
+ class EmbedThumbnail
172
+ # @return [String, nil] URL of the thumbnail
173
+ attr_accessor :url
174
+
175
+ # Creates a new thumbnail object.
176
+ # @param url [String, nil] The URL of the thumbnail.
177
+ def initialize(url: nil)
178
+ @url = url
179
+ end
180
+
181
+ # @return [Hash] a hash representation of this embed thumbnail, to be converted to JSON.
182
+ def to_hash
183
+ {
184
+ url: @url
185
+ }
186
+ end
187
+ end
188
+
189
+ # An embed's author will be shown at the top to indicate who "authored" the particular event the webhook was sent for.
190
+ class EmbedAuthor
191
+ # @return [String, nil] name of the author
192
+ attr_accessor :name
193
+
194
+ # @return [String, nil] URL the name should link to
195
+ attr_accessor :url
196
+
197
+ # @return [String, nil] URL of the icon to be displayed next to the author
198
+ attr_accessor :icon_url
199
+
200
+ # Creates a new author object.
201
+ # @param name [String, nil] The name of the author.
202
+ # @param url [String, nil] The URL the name should link to.
203
+ # @param icon_url [String, nil] The URL of the icon to be displayed next to the author.
204
+ def initialize(name: nil, url: nil, icon_url: nil)
205
+ @name = name
206
+ @url = url
207
+ @icon_url = icon_url
208
+ end
209
+
210
+ # @return [Hash] a hash representation of this embed author, to be converted to JSON.
211
+ def to_hash
212
+ {
213
+ name: @name,
214
+ url: @url,
215
+ icon_url: @icon_url
216
+ }
217
+ end
218
+ end
219
+
220
+ # A field is a small block of text with a header that can be relatively freely layouted with other fields.
221
+ class EmbedField
222
+ # @return [String, nil] name of the field, displayed in bold at the top of the field.
223
+ attr_accessor :name
224
+
225
+ # @return [String, nil] value of the field, displayed in normal text below the name.
226
+ attr_accessor :value
227
+
228
+ # @return [true, false] whether the field should be displayed inline with other fields.
229
+ attr_accessor :inline
230
+
231
+ # Creates a new field object.
232
+ # @param name [String, nil] The name of the field, displayed in bold at the top of the field.
233
+ # @param value [String, nil] The value of the field, displayed in normal text below the name.
234
+ # @param inline [true, false] Whether the field should be displayed inline with other fields.
235
+ def initialize(name: nil, value: nil, inline: false)
236
+ @name = name
237
+ @value = value
238
+ @inline = inline
239
+ end
240
+
241
+ # @return [Hash] a hash representation of this embed field, to be converted to JSON.
242
+ def to_hash
243
+ {
244
+ name: @name,
245
+ value: @value,
246
+ inline: @inline
247
+ }
248
+ end
249
+ end
250
+ end