mirror of
https://github.com/diamondburned/arikawa.git
synced 2024-08-20 15:24:37 +00:00
This commit gets rid of contain-it-all structs and instead opt for interface union types containing underlying concrete types with no overloading. The code is much more verbose by doing this, but the API is much nicer to use. The only disadvantage in that regard is the interface assertion being too verbose and risky for users at times.
476 lines
15 KiB
Go
476 lines
15 KiB
Go
package api
|
|
|
|
import (
|
|
"mime/multipart"
|
|
"strconv"
|
|
|
|
"github.com/pkg/errors"
|
|
|
|
"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, errors.Wrap(err, "allowedMentions error")
|
|
}
|
|
}
|
|
|
|
if data.Embeds != nil {
|
|
sum := 0
|
|
for i, embed := range *data.Embeds {
|
|
if err := embed.Validate(); err != nil {
|
|
return nil, errors.Wrap(err, "embed error at "+strconv.Itoa(i))
|
|
}
|
|
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()),
|
|
)
|
|
}
|