Skip to content

Commit

Permalink
support allowing webhook edits with files, and responding to interact…
Browse files Browse the repository at this point in the history
…ions 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
  • Loading branch information
plally authored and FedorLap2006 committed Jul 30, 2021
1 parent 6450f39 commit c6f37ad
Show file tree
Hide file tree
Showing 5 changed files with 111 additions and 144 deletions.
20 changes: 20 additions & 0 deletions examples/slash_commands/main.go
Expand Up @@ -6,6 +6,7 @@ import (
"log"
"os"
"os/signal"
"strings"
"time"

"github.com/bwmarrin/discordgo"
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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.
Expand Down
5 changes: 4 additions & 1 deletion interactions.go
Expand Up @@ -369,7 +369,10 @@ type InteractionResponseData struct {
Components []MessageComponent `json:"components"`
Embeds []*MessageEmbed `json:"embeds,omitempty"`
AllowedMentions *MessageAllowedMentions `json:"allowed_mentions,omitempty"`
Flags uint64 `json:"flags,omitempty"`

Flags uint64 `json:"flags,omitempty"`

Files []*File `json:"-"`
}

// VerifyInteraction implements message verification of the discord interactions api
Expand Down
169 changes: 26 additions & 143 deletions restapi.go
Expand Up @@ -21,9 +21,7 @@ import (
"io"
"io/ioutil"
"log"
"mime/multipart"
"net/http"
"net/textproto"
"net/url"
"strconv"
"strings"
Expand Down Expand Up @@ -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
}

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
contentType, body, encodeErr := MultipartBodyWithJSON(data, files)
if encodeErr != nil {
return st, encodeErr
}

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)
}
Expand Down Expand Up @@ -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
}

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
contentType, body, encodeErr := MultipartBodyWithJSON(data, data.Files)
if encodeErr != nil {
return st, encodeErr
}

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)
}
Expand All @@ -2242,62 +2154,25 @@ func (s *Session) WebhookExecute(webhookID, token string, wait bool, data *Webho
// messageID : The ID of message to edit
func (s *Session) WebhookMessageEdit(webhookID, token, messageID string, data *WebhookEdit) (st *Message, err error) {
uri := EndpointWebhookMessage(webhookID, token, messageID)

var response []byte
if len(data.Files) > 0 {
body := &bytes.Buffer{}
bodywriter := multipart.NewWriter(body)

var payload []byte
payload, err = json.Marshal(data)
contentType, body, err := MultipartBodyWithJSON(data, data.Files)
if err != nil {
return
return nil, err
}

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)
response, err = s.request("PATCH", uri, contentType, body, uri, 0)
if err != nil {
return
}

if _, err = p.Write(payload); err != nil {
return
return nil, err
}

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
}
}
} else {
response, err = s.RequestWithBucketID("PATCH", uri, data, EndpointWebhookToken("", ""))

err = bodywriter.Close()
if err != nil {
return
return nil, err
}

response, err = s.request("PATCH", uri, bodywriter.FormDataContentType(), body.Bytes(), uri, 0)
} else {
response, err = s.RequestWithBucketID("PATCH", uri, data, uri)
}
if err != nil {
return
}

err = unmarshal(response, &st)
Expand Down Expand Up @@ -2605,11 +2480,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
}

Expand Down
60 changes: 60 additions & 0 deletions util.go
@@ -1,6 +1,12 @@
package discordgo

import (
"bytes"
"encoding/json"
"fmt"
"io"
"mime/multipart"
"net/textproto"
"strconv"
"time"
)
Expand All @@ -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
}
1 change: 1 addition & 0 deletions webhook.go
Expand Up @@ -45,5 +45,6 @@ type WebhookEdit struct {
Components []MessageComponent `json:"components"`
Files []*File `json:"-"`
Embeds []*MessageEmbed `json:"embeds,omitempty"`
Files []*File `json:"-"`
AllowedMentions *MessageAllowedMentions `json:"allowed_mentions,omitempty"`
}

0 comments on commit c6f37ad

Please sign in to comment.