Browse Source

Add and fix support for multiple file uploads via ChannelMessageSendComplex via the new field MessageSend.Files. (#391)

For compatibility with existing library consumers, the File field is retained but will behave as if Files contained that single file. If both are specified, ChannelMessageSendComplex will return an error.

The message JSON payload is moved to a form-data field called `payload_json`, instead of set in multipart form data. This is supported and the recommended way, as per the API docs.

Apparently, you can attach multiple files if you just name the parts names differently in the multipart request. The parts are named here using the order the files were specified, as `file%d`. This is not documented in the API docs, but definitely works.

This also removes serialization of the File field via json.Marshal, as it will never be directly serialized in the JSON. The new field, Files, is similarly not marshaled.

This additionally adds a ContentType field in File, which can be used to specify the content type of the attached file. The ContentType field will default to setting the header to `application/octet-stream` if empty. Discord currently doesn't do much with the Content-Type header, but we should pass this information along anyway in accordance to the MIME standard.
rfw 7 years ago
parent
commit
874325a504
2 changed files with 50 additions and 29 deletions
  1. 7 3
      message.go
  2. 43 26
      restapi.go

+ 7 - 3
message.go

@@ -34,8 +34,9 @@ type Message struct {
 
 // File stores info about files you e.g. send in messages.
 type File struct {
-	Name   string
-	Reader io.Reader
+	Name        string
+	ContentType string
+	Reader      io.Reader
 }
 
 // MessageSend stores all parameters you can send with ChannelMessageSendComplex.
@@ -43,7 +44,10 @@ type MessageSend struct {
 	Content string        `json:"content,omitempty"`
 	Embed   *MessageEmbed `json:"embed,omitempty"`
 	Tts     bool          `json:"tts"`
-	File    *File         `json:"file"`
+	Files   []*File       `json:"-"`
+
+	// TODO: Remove this when compatibility is not required.
+	File *File `json:"-"`
 }
 
 // MessageEdit is used to chain parameters via ChannelMessageEditComplex, which

+ 43 - 26
restapi.go

@@ -23,6 +23,7 @@ import (
 	"log"
 	"mime/multipart"
 	"net/http"
+	"net/textproto"
 	"net/url"
 	"strconv"
 	"strings"
@@ -1316,6 +1317,8 @@ func (s *Session) ChannelMessageSend(channelID string, content string) (*Message
 	})
 }
 
+var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"")
+
 // ChannelMessageSendComplex sends a message to the given channel.
 // channelID : The ID of a Channel.
 // data      : The message struct to send.
@@ -1326,48 +1329,62 @@ func (s *Session) ChannelMessageSendComplex(channelID string, data *MessageSend)
 
 	endpoint := EndpointChannelMessages(channelID)
 
-	var response []byte
+	// TODO: Remove this when compatibility is not required.
+	files := data.Files
 	if data.File != nil {
+		if files == nil {
+			files = []*File{data.File}
+		} else {
+			err = fmt.Errorf("cannot specify both File and Files")
+			return
+		}
+	}
+
+	var response []byte
+	if len(files) > 0 {
 		body := &bytes.Buffer{}
 		bodywriter := multipart.NewWriter(body)
 
-		// What's a better way of doing this? Reflect? Generator? I'm open to suggestions
+		var payload []byte
+		payload, err = json.Marshal(data)
+		if err != nil {
+			return
+		}
 
-		if data.Content != "" {
-			if err = bodywriter.WriteField("content", data.Content); 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 data.Embed != nil {
-			var embed []byte
-			embed, err = json.Marshal(data.Embed)
-			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"
 			}
-			err = bodywriter.WriteField("embed", string(embed))
+			h.Set("Content-Type", contentType)
+
+			p, err = bodywriter.CreatePart(h)
 			if err != nil {
 				return
 			}
-		}
 
-		if data.Tts {
-			if err = bodywriter.WriteField("tts", "true"); err != nil {
+			if _, err = io.Copy(p, file.Reader); err != nil {
 				return
 			}
 		}
 
-		var writer io.Writer
-		writer, err = bodywriter.CreateFormFile("file", data.File.Name)
-		if err != nil {
-			return
-		}
-
-		_, err = io.Copy(writer, data.File.Reader)
-		if err != nil {
-			return
-		}
-
 		err = bodywriter.Close()
 		if err != nil {
 			return