From ab47f123ba40675ab411005781b16b34fd8623fd Mon Sep 17 00:00:00 2001 From: plally Date: Fri, 30 Jul 2021 11:22:08 -0400 Subject: [PATCH] support allowing webhook edits with files, and responding to interactions with files (#931) * allow files in webhook message edits * add Files to WebhookEdit struct * move the construction of the multipart body for files into a shared function * allow interaction responses to have files * go fmt * fix err shadowing * document MakeFilesBody * rename MakeFilesBody -> EncodeWithFiles. fix InteractionRespond responding twice * use resp in InteractionRespond files, add basic-command-with-files example command * import strings and go fmt * EncodeWithFiles -> MultiPartBodyWithJSON * go fmt * fix example for slash_commands * move files to responsedata --- examples/slash_commands/main.go | 20 +++++ interactions.go | 2 + restapi.go | 127 +++++++------------------------- util.go | 60 +++++++++++++++ webhook.go | 1 + 5 files changed, 110 insertions(+), 100 deletions(-) diff --git a/examples/slash_commands/main.go b/examples/slash_commands/main.go index 57d27ca..f28bece 100644 --- a/examples/slash_commands/main.go +++ b/examples/slash_commands/main.go @@ -6,6 +6,7 @@ import ( "log" "os" "os/signal" + "strings" "time" "github.com/bwmarrin/discordgo" @@ -39,6 +40,10 @@ var ( // of the command. Description: "Basic command", }, + { + Name: "basic-command-with-files", + Description: "Basic command with files", + }, { Name: "options", Description: "Command for demonstrating options", @@ -160,6 +165,21 @@ var ( }, }) }, + "basic-command-with-files": func(s *discordgo.Session, i *discordgo.InteractionCreate) { + s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Content: "Hey there! Congratulations, you just executed your first slash command with a file in the response", + Files: []*discordgo.File{ + { + ContentType: "text/plain", + Name: "test.txt", + Reader: strings.NewReader("Hello Discord!!"), + }, + }, + }, + }) + }, "options": func(s *discordgo.Session, i *discordgo.InteractionCreate) { margs := []interface{}{ // Here we need to convert raw interface{} value to wanted type. diff --git a/interactions.go b/interactions.go index 9a86878..c45a635 100644 --- a/interactions.go +++ b/interactions.go @@ -381,6 +381,8 @@ type InteractionResponseData struct { // NOTE: Undocumented feature, be careful with it. Flags uint64 `json:"flags,omitempty"` + + Files []*File `json:"-"` } // VerifyInteraction implements message verification of the discord interactions api diff --git a/restapi.go b/restapi.go index cf2c330..9b48e2c 100644 --- a/restapi.go +++ b/restapi.go @@ -21,9 +21,7 @@ import ( "io" "io/ioutil" "log" - "mime/multipart" "net/http" - "net/textproto" "net/url" "strconv" "strings" @@ -1573,55 +1571,12 @@ func (s *Session) ChannelMessageSendComplex(channelID string, data *MessageSend) var response []byte if len(files) > 0 { - body := &bytes.Buffer{} - bodywriter := multipart.NewWriter(body) - - var payload []byte - payload, err = json.Marshal(data) - if err != nil { - return + contentType, body, encodeErr := MultipartBodyWithJSON(data, files) + if encodeErr != nil { + return st, encodeErr } - var p io.Writer - - h := make(textproto.MIMEHeader) - h.Set("Content-Disposition", `form-data; name="payload_json"`) - h.Set("Content-Type", "application/json") - - p, err = bodywriter.CreatePart(h) - if err != nil { - return - } - - if _, err = p.Write(payload); err != nil { - return - } - - for i, file := range files { - h := make(textproto.MIMEHeader) - h.Set("Content-Disposition", fmt.Sprintf(`form-data; name="file%d"; filename="%s"`, i, quoteEscaper.Replace(file.Name))) - contentType := file.ContentType - if contentType == "" { - contentType = "application/octet-stream" - } - h.Set("Content-Type", contentType) - - p, err = bodywriter.CreatePart(h) - if err != nil { - return - } - - if _, err = io.Copy(p, file.Reader); err != nil { - return - } - } - - err = bodywriter.Close() - if err != nil { - return - } - - response, err = s.request("POST", endpoint, bodywriter.FormDataContentType(), body.Bytes(), endpoint, 0) + response, err = s.request("POST", endpoint, contentType, body, endpoint, 0) } else { response, err = s.RequestWithBucketID("POST", endpoint, data, endpoint) } @@ -2176,55 +2131,12 @@ func (s *Session) WebhookExecute(webhookID, token string, wait bool, data *Webho var response []byte if len(data.Files) > 0 { - body := &bytes.Buffer{} - bodywriter := multipart.NewWriter(body) - - var payload []byte - payload, err = json.Marshal(data) - if err != nil { - return + contentType, body, encodeErr := MultipartBodyWithJSON(data, data.Files) + if encodeErr != nil { + return st, encodeErr } - var p io.Writer - - h := make(textproto.MIMEHeader) - h.Set("Content-Disposition", `form-data; name="payload_json"`) - h.Set("Content-Type", "application/json") - - p, err = bodywriter.CreatePart(h) - if err != nil { - return - } - - if _, err = p.Write(payload); err != nil { - return - } - - for i, file := range data.Files { - h := make(textproto.MIMEHeader) - h.Set("Content-Disposition", fmt.Sprintf(`form-data; name="file%d"; filename="%s"`, i, quoteEscaper.Replace(file.Name))) - contentType := file.ContentType - if contentType == "" { - contentType = "application/octet-stream" - } - h.Set("Content-Type", contentType) - - p, err = bodywriter.CreatePart(h) - if err != nil { - return - } - - if _, err = io.Copy(p, file.Reader); err != nil { - return - } - } - - err = bodywriter.Close() - if err != nil { - return - } - - response, err = s.request("POST", uri, bodywriter.FormDataContentType(), body.Bytes(), uri, 0) + response, err = s.request("POST", uri, contentType, body, uri, 0) } else { response, err = s.RequestWithBucketID("POST", uri, data, uri) } @@ -2259,9 +2171,16 @@ func (s *Session) WebhookMessage(webhookID, token, messageID string) (message *M // messageID : The ID of message to edit func (s *Session) WebhookMessageEdit(webhookID, token, messageID string, data *WebhookEdit) (err error) { uri := EndpointWebhookMessage(webhookID, token, messageID) + if len(data.Files) > 0 { + contentType, body, err := MultipartBodyWithJSON(data, data.Files) + if err != nil { + return err + } - _, err = s.RequestWithBucketID("PATCH", uri, data, EndpointWebhookToken("", "")) - + _, err = s.request("PATCH", uri, contentType, body, uri, 0) + } else { + _, err = s.RequestWithBucketID("PATCH", uri, data, EndpointWebhookToken("", "")) + } return } @@ -2566,11 +2485,19 @@ func (s *Session) ApplicationCommands(appID, guildID string) (cmd []*Application // appID : The application ID. // interaction : Interaction instance. // resp : Response message data. -func (s *Session) InteractionRespond(interaction *Interaction, resp *InteractionResponse) error { +func (s *Session) InteractionRespond(interaction *Interaction, resp *InteractionResponse) (err error) { endpoint := EndpointInteractionResponse(interaction.ID, interaction.Token) - _, err := s.RequestWithBucketID("POST", endpoint, *resp, endpoint) + if resp.Data != nil && len(resp.Data.Files) > 0 { + contentType, body, err := MultipartBodyWithJSON(resp, resp.Data.Files) + if err != nil { + return err + } + _, err = s.request("POST", endpoint, contentType, body, endpoint, 0) + } else { + _, err = s.RequestWithBucketID("POST", endpoint, *resp, endpoint) + } return err } diff --git a/util.go b/util.go index 8a2b2e0..fae34f7 100644 --- a/util.go +++ b/util.go @@ -1,6 +1,12 @@ package discordgo import ( + "bytes" + "encoding/json" + "fmt" + "io" + "mime/multipart" + "net/textproto" "strconv" "time" ) @@ -15,3 +21,57 @@ func SnowflakeTimestamp(ID string) (t time.Time, err error) { t = time.Unix(0, timestamp*1000000) return } + +// MultipartBodyWithJSON returns the contentType and body for a discord request +// data : The object to encode for payload_json in the multipart request +// files : Files to include in the request +func MultipartBodyWithJSON(data interface{}, files []*File) (requestContentType string, requestBody []byte, err error) { + body := &bytes.Buffer{} + bodywriter := multipart.NewWriter(body) + + payload, err := json.Marshal(data) + if err != nil { + return + } + + var p io.Writer + + h := make(textproto.MIMEHeader) + h.Set("Content-Disposition", `form-data; name="payload_json"`) + h.Set("Content-Type", "application/json") + + p, err = bodywriter.CreatePart(h) + if err != nil { + return + } + + if _, err = p.Write(payload); err != nil { + return + } + + for i, file := range files { + h := make(textproto.MIMEHeader) + h.Set("Content-Disposition", fmt.Sprintf(`form-data; name="file%d"; filename="%s"`, i, quoteEscaper.Replace(file.Name))) + contentType := file.ContentType + if contentType == "" { + contentType = "application/octet-stream" + } + h.Set("Content-Type", contentType) + + p, err = bodywriter.CreatePart(h) + if err != nil { + return + } + + if _, err = io.Copy(p, file.Reader); err != nil { + return + } + } + + err = bodywriter.Close() + if err != nil { + return + } + + return bodywriter.FormDataContentType(), body.Bytes(), nil +} diff --git a/webhook.go b/webhook.go index d61b281..438a6df 100644 --- a/webhook.go +++ b/webhook.go @@ -42,5 +42,6 @@ type WebhookEdit struct { Content string `json:"content,omitempty"` Components []MessageComponent `json:"components"` Embeds []*MessageEmbed `json:"embeds,omitempty"` + Files []*File `json:"-"` AllowedMentions *MessageAllowedMentions `json:"allowed_mentions,omitempty"` }