1
0
Fork 0
mirror of https://github.com/diamondburned/arikawa.git synced 2024-08-19 23:04:51 +00:00
arikawa/api/message.go
diamondburned 331ec59dec discord: Refactor interactions and components
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.
2021-11-12 11:38:36 -08:00

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()),
)
}