Browse Source

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
plally 2 years ago
parent
commit
ab47f123ba
5 changed files with 110 additions and 100 deletions
  1. 20 0
      examples/slash_commands/main.go
  2. 2 0
      interactions.go
  3. 27 100
      restapi.go
  4. 60 0
      util.go
  5. 1 0
      webhook.go

+ 20 - 0
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.

+ 2 - 0
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

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

+ 60 - 0
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
+}

+ 1 - 0
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"`
 }