/
message.go
473 lines (396 loc) · 15.2 KB
/
message.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
package api
import (
"fmt"
"mime/multipart"
"github.com/diamondburned/arikawa/v3/discord"
"github.com/diamondburned/arikawa/v3/internal/intmath"
"github.com/diamondburned/arikawa/v3/utils/httputil"
"github.com/diamondburned/arikawa/v3/utils/json/option"
"github.com/diamondburned/arikawa/v3/utils/sendpart"
)
const (
// the limit of max messages per request, as imposed by Discord
maxMessageFetchLimit = 100
// maxMessageDeleteLimit is the limit of max message that can be deleted
// per bulk delete request, as imposed by Discord.
maxMessageDeleteLimit = 100
)
// Messages returns a slice filled with the most recent messages sent in the
// channel with the passed ID. The method automatically paginates until it
// reaches the passed limit, or, if the limit is set to 0, has fetched all
// messages in the channel.
//
// As the underlying endpoint is capped at a maximum of 100 messages per
// request, at maximum a total of limit/100 rounded up requests will be made,
// although they may be less, if no more messages are available.
//
// When fetching the messages, those with the highest ID, will be fetched
// first.
// The returned slice will be sorted from latest to oldest.
func (c *Client) Messages(channelID discord.ChannelID, limit uint) ([]discord.Message, error) {
// Since before is 0 it will be omitted by the http lib, which in turn
// will lead discord to send us the most recent messages without having to
// specify a Snowflake.
return c.MessagesBefore(channelID, 0, limit)
}
// MessagesAround returns messages around the ID, with a limit of 100.
func (c *Client) MessagesAround(
channelID discord.ChannelID, around discord.MessageID, limit uint) ([]discord.Message, error) {
return c.messagesRange(channelID, 0, 0, around, limit)
}
// MessagesBefore returns a slice filled with the messages sent in the channel
// with the passed id. The method automatically paginates until it reaches the
// passed limit, or, if the limit is set to 0, has fetched all messages in the
// channel with an id smaller than before.
//
// As the underlying endpoint has a maximum of 100 messages per request, at
// maximum a total of limit/100 rounded up requests will be made, although they
// may be less, if no more messages are available.
//
// The returned slice will be sorted from latest to oldest.
func (c *Client) MessagesBefore(
channelID discord.ChannelID, before discord.MessageID, limit uint) ([]discord.Message, error) {
msgs := make([]discord.Message, 0, limit)
fetch := uint(maxMessageFetchLimit)
// Check if we are truly fetching unlimited messages to avoid confusion
// later on, if the limit reaches 0.
unlimited := limit == 0
for limit > 0 || unlimited {
if limit > 0 {
// Only fetch as much as we need. Since limit gradually decreases,
// we only need to fetch intmath.Min(fetch, limit).
fetch = uint(intmath.Min(maxMessageFetchLimit, int(limit)))
limit -= maxMessageFetchLimit
}
m, err := c.messagesRange(channelID, before, 0, 0, fetch)
if err != nil {
return msgs, err
}
// Append the older messages into the list of newer messages.
msgs = append(msgs, m...)
if len(m) < maxMessageFetchLimit {
break
}
before = m[len(m)-1].ID
}
if len(msgs) == 0 {
return nil, nil
}
return msgs, nil
}
// MessagesAfter returns a slice filled with the messages sent in the channel
// with the passed ID. The method automatically paginates until it reaches the
// passed limit, or, if the limit is set to 0, has fetched all messages in the
// channel with an id higher than after.
//
// As the underlying endpoint has a maximum of 100 messages per request, at
// maximum a total of limit/100 rounded up requests will be made, although they
// may be less, if no more messages are available.
//
// The returned slice will be sorted from latest to oldest.
func (c *Client) MessagesAfter(
channelID discord.ChannelID, after discord.MessageID, limit uint) ([]discord.Message, error) {
// 0 is uint's zero value and will lead to the after param getting omitted,
// which in turn will lead to the most recent messages being returned.
// Setting this to 1 will prevent that.
if after == 0 {
after = 1
}
var msgs []discord.Message
fetch := uint(maxMessageFetchLimit)
// Check if we are truly fetching unlimited messages to avoid confusion
// later on, if the limit reaches 0.
unlimited := limit == 0
for limit > 0 || unlimited {
if limit > 0 {
// Only fetch as much as we need. Since limit gradually decreases,
// we only need to fetch intmath.Min(fetch, limit).
fetch = uint(intmath.Min(maxMessageFetchLimit, int(limit)))
limit -= maxMessageFetchLimit
}
m, err := c.messagesRange(channelID, 0, after, 0, fetch)
if err != nil {
return msgs, err
}
// Prepend the older messages into the newly-fetched messages list.
msgs = append(m, msgs...)
if len(m) < maxMessageFetchLimit {
break
}
after = m[0].ID
}
if len(msgs) == 0 {
return nil, nil
}
return msgs, nil
}
func (c *Client) messagesRange(
channelID discord.ChannelID,
before, after, around discord.MessageID, limit uint) ([]discord.Message, error) {
switch {
case limit == 0:
limit = 50
case limit > 100:
limit = 100
}
var param struct {
Before discord.MessageID `schema:"before,omitempty"`
After discord.MessageID `schema:"after,omitempty"`
Around discord.MessageID `schema:"around,omitempty"`
Limit uint `schema:"limit"`
}
param.Before = before
param.After = after
param.Around = around
param.Limit = limit
var msgs []discord.Message
return msgs, c.RequestJSON(
&msgs, "GET",
EndpointChannels+channelID.String()+"/messages",
httputil.WithSchema(c, param),
)
}
// Message returns a specific message in the channel.
//
// If operating on a guild channel, this endpoint requires the
// READ_MESSAGE_HISTORY permission to be present on the current user.
func (c *Client) Message(
channelID discord.ChannelID, messageID discord.MessageID) (*discord.Message, error) {
var msg *discord.Message
return msg, c.RequestJSON(&msg, "GET",
EndpointChannels+channelID.String()+"/messages/"+messageID.String())
}
// SendTextReply posts a text-only reply to a message ID in a guild text or DM channel
//
// If operating on a guild channel, this endpoint requires the SEND_MESSAGES
// permission to be present on the current user.
//
// Fires a Message Create Gateway event.
func (c *Client) SendTextReply(
channelID discord.ChannelID,
content string, referenceID discord.MessageID) (*discord.Message, error) {
return c.SendMessageComplex(channelID, SendMessageData{
Content: content,
Reference: &discord.MessageReference{MessageID: referenceID},
})
}
// SendEmbeds sends embeds to a guild text or DM channel.
//
// If operating on a guild channel, this endpoint requires the SEND_MESSAGES
// permission to be present on the current user.
//
// Fires a Message Create Gateway event.
func (c *Client) SendEmbeds(
channelID discord.ChannelID, e ...discord.Embed) (*discord.Message, error) {
return c.SendMessageComplex(channelID, SendMessageData{
Embeds: e,
})
}
// SendEmbedReply posts an Embed reply to a message ID in a guild text or DM channel.
//
// If operating on a guild channel, this endpoint requires the SEND_MESSAGES
// permission to be present on the current user.
//
// Fires a Message Create Gateway event.
func (c *Client) SendEmbedReply(
channelID discord.ChannelID,
referenceID discord.MessageID, embeds ...discord.Embed) (*discord.Message, error) {
return c.SendMessageComplex(channelID, SendMessageData{
Embeds: embeds,
Reference: &discord.MessageReference{MessageID: referenceID},
})
}
// SendMessage posts a message to a guild text or DM channel.
//
// If operating on a guild channel, this endpoint requires the SEND_MESSAGES
// permission to be present on the current user.
//
// Fires a Message Create Gateway event.
func (c *Client) SendMessage(
channelID discord.ChannelID,
content string, embeds ...discord.Embed) (*discord.Message, error) {
data := SendMessageData{
Content: content,
Embeds: embeds,
}
return c.SendMessageComplex(channelID, data)
}
// SendMessageReply posts a reply to a message ID in a guild text or DM channel.
//
// If operating on a guild channel, this endpoint requires the SEND_MESSAGES
// permission to be present on the current user.
//
// Fires a Message Create Gateway event.
func (c *Client) SendMessageReply(
channelID discord.ChannelID, content string,
referenceID discord.MessageID, embeds ...discord.Embed) (*discord.Message, error) {
data := SendMessageData{
Content: content,
Reference: &discord.MessageReference{MessageID: referenceID},
Embeds: embeds,
}
return c.SendMessageComplex(channelID, data)
}
// https://discord.com/developers/docs/resources/channel#edit-message
type EditMessageData struct {
// Content is the new message contents (up to 2000 characters).
Content option.NullableString `json:"content,omitempty"`
// Embeds contains embedded rich content.
Embeds *[]discord.Embed `json:"embeds,omitempty"`
// Components contains the new components to attach.
Components *discord.ContainerComponents `json:"components,omitempty"`
// AllowedMentions are the allowed mentions for a message.
AllowedMentions *AllowedMentions `json:"allowed_mentions,omitempty"`
// Attachments are the attached files to keep
Attachments *[]discord.Attachment `json:"attachments,omitempty"`
// Flags edits the flags of a message (only SUPPRESS_EMBEDS can currently
// be set/unset)
//
// This field is nullable.
Flags *discord.MessageFlags `json:"flags,omitempty"`
Files []sendpart.File `json:"-"`
}
// NeedsMultipart returns true if the SendMessageData has files.
func (data EditMessageData) NeedsMultipart() bool {
return len(data.Files) > 0
}
func (data EditMessageData) WriteMultipart(body *multipart.Writer) error {
return sendpart.Write(body, data, data.Files)
}
// EditText edits the contents of a previously sent message. For more
// documentation, refer to EditMessageComplex.
func (c *Client) EditText(
channelID discord.ChannelID,
messageID discord.MessageID, content string) (*discord.Message, error) {
return c.EditMessageComplex(channelID, messageID, EditMessageData{
Content: option.NewNullableString(content),
})
}
// EditEmbeds edits the embed of a previously sent message. For more
// documentation, refer to EditMessageComplex.
func (c *Client) EditEmbeds(
channelID discord.ChannelID,
messageID discord.MessageID, embeds ...discord.Embed) (*discord.Message, error) {
return c.EditMessageComplex(channelID, messageID, EditMessageData{
Embeds: &embeds,
})
}
// EditMessage edits a previously sent message. If content or embeds are empty
// the original content or embed will remain untouched. This means EditMessage
// will only update, but not remove parts of the message.
//
// For more documentation, refer to EditMessageComplex.
func (c *Client) EditMessage(
channelID discord.ChannelID, messageID discord.MessageID,
content string, embeds ...discord.Embed) (*discord.Message, error) {
var data EditMessageData
if len(content) > 0 {
data.Content = option.NewNullableString(content)
}
if len(embeds) > 0 {
data.Embeds = &embeds
}
return c.EditMessageComplex(channelID, messageID, data)
}
// EditMessageComplex edits a previously sent message. The fields Content,
// Embed, AllowedMentions and Flags can be edited by the original message
// author. Other users can only edit flags and only if they have the
// MANAGE_MESSAGES permission in the corresponding channel. When specifying
// flags, ensure to include all previously set flags/bits in addition to ones
// that you are modifying. Only flags documented in EditMessageData may be
// modified by users (unsupported flag changes are currently ignored without
// error).
//
// Fires a Message Update Gateway event.
func (c *Client) EditMessageComplex(
channelID discord.ChannelID,
messageID discord.MessageID, data EditMessageData) (*discord.Message, error) {
if data.AllowedMentions != nil {
if err := data.AllowedMentions.Verify(); err != nil {
return nil, fmt.Errorf("allowedMentions error: %w", err)
}
}
if data.Embeds != nil {
sum := 0
for i, embed := range *data.Embeds {
if err := embed.Validate(); err != nil {
return nil, fmt.Errorf("embed error at %d: %w", i, err)
}
sum += embed.Length()
if sum > 6000 {
return nil, &discord.OverboundError{Count: sum, Max: 6000, Thing: "sum of all text in embeds"}
}
(*data.Embeds)[i] = embed // embed.Validate changes fields
}
}
var msg *discord.Message
return msg, sendpart.PATCH(c.Client, data, &msg,
EndpointChannels+channelID.String()+"/messages/"+messageID.String())
}
// CrosspostMessage crossposts a message in a news channel to following channels.
// This endpoint requires the SEND_MESSAGES permission if the current user sent the message,
// or additionally the MANAGE_MESSAGES permission for all other messages.
func (c *Client) CrosspostMessage(
channelID discord.ChannelID, messageID discord.MessageID) (*discord.Message, error) {
var msg *discord.Message
return msg, c.RequestJSON(
&msg,
"POST",
EndpointChannels+channelID.String()+"/messages/"+messageID.String()+"/crosspost",
)
}
// DeleteMessage delete a message. If operating on a guild channel and trying
// to delete a message that was not sent by the current user, this endpoint
// requires the MANAGE_MESSAGES permission.
func (c *Client) DeleteMessage(
channelID discord.ChannelID, messageID discord.MessageID, reason AuditLogReason) error {
return c.FastRequest(
"DELETE", EndpointChannels+channelID.String()+"/messages/"+messageID.String(),
httputil.WithHeaders(reason.Header()))
}
// DeleteMessages deletes multiple messages in a single request. This endpoint
// can only be used on guild channels and requires the MANAGE_MESSAGES
// permission. This endpoint only works for bots.
//
// This endpoint will not delete messages older than 2 weeks, and will fail if
// any message provided is older than that or if any duplicate message IDs are
// provided.
//
// Because the underlying endpoint only supports a maximum of 100 message IDs
// per request, DeleteMessages will make a total of messageIDs/100 rounded up
// requests.
//
// Fires a Message Delete Bulk Gateway event.
func (c *Client) DeleteMessages(
channelID discord.ChannelID, messageIDs []discord.MessageID, reason AuditLogReason) error {
switch {
case len(messageIDs) == 0:
return nil
case len(messageIDs) == 1:
return c.DeleteMessage(channelID, messageIDs[0], reason)
case len(messageIDs) <= maxMessageDeleteLimit: // Fast path
return c.deleteMessages(channelID, messageIDs, reason)
}
// If the number of messages to be deleted exceeds the amount discord is willing
// to accept at one time then batches of messages will be deleted
for start := 0; start < len(messageIDs); start += maxMessageDeleteLimit {
end := intmath.Min(len(messageIDs), start+maxMessageDeleteLimit)
err := c.deleteMessages(channelID, messageIDs[start:end], reason)
if err != nil {
return err
}
}
return nil
}
func (c *Client) deleteMessages(
channelID discord.ChannelID, messageIDs []discord.MessageID, reason AuditLogReason) error {
var param struct {
Messages []discord.MessageID `json:"messages"`
}
param.Messages = messageIDs
return c.FastRequest(
"POST",
EndpointChannels+channelID.String()+"/messages/bulk-delete",
httputil.WithJSONBody(param), httputil.WithHeaders(reason.Header()),
)
}