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
This commit is contained in:
parent
083bf5c1d9
commit
ab47f123ba
5 changed files with 110 additions and 100 deletions
|
@ -6,6 +6,7 @@ import (
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/bwmarrin/discordgo"
|
"github.com/bwmarrin/discordgo"
|
||||||
|
@ -39,6 +40,10 @@ var (
|
||||||
// of the command.
|
// of the command.
|
||||||
Description: "Basic command",
|
Description: "Basic command",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: "basic-command-with-files",
|
||||||
|
Description: "Basic command with files",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
Name: "options",
|
Name: "options",
|
||||||
Description: "Command for demonstrating 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) {
|
"options": func(s *discordgo.Session, i *discordgo.InteractionCreate) {
|
||||||
margs := []interface{}{
|
margs := []interface{}{
|
||||||
// Here we need to convert raw interface{} value to wanted type.
|
// Here we need to convert raw interface{} value to wanted type.
|
||||||
|
|
|
@ -381,6 +381,8 @@ type InteractionResponseData struct {
|
||||||
|
|
||||||
// NOTE: Undocumented feature, be careful with it.
|
// NOTE: Undocumented feature, be careful with it.
|
||||||
Flags uint64 `json:"flags,omitempty"`
|
Flags uint64 `json:"flags,omitempty"`
|
||||||
|
|
||||||
|
Files []*File `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// VerifyInteraction implements message verification of the discord interactions api
|
// VerifyInteraction implements message verification of the discord interactions api
|
||||||
|
|
127
restapi.go
127
restapi.go
|
@ -21,9 +21,7 @@ import (
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"log"
|
"log"
|
||||||
"mime/multipart"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/textproto"
|
|
||||||
"net/url"
|
"net/url"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -1573,55 +1571,12 @@ func (s *Session) ChannelMessageSendComplex(channelID string, data *MessageSend)
|
||||||
|
|
||||||
var response []byte
|
var response []byte
|
||||||
if len(files) > 0 {
|
if len(files) > 0 {
|
||||||
body := &bytes.Buffer{}
|
contentType, body, encodeErr := MultipartBodyWithJSON(data, files)
|
||||||
bodywriter := multipart.NewWriter(body)
|
if encodeErr != nil {
|
||||||
|
return st, encodeErr
|
||||||
var payload []byte
|
|
||||||
payload, err = json.Marshal(data)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var p io.Writer
|
response, err = s.request("POST", endpoint, contentType, body, endpoint, 0)
|
||||||
|
|
||||||
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)
|
|
||||||
} else {
|
} else {
|
||||||
response, err = s.RequestWithBucketID("POST", endpoint, data, endpoint)
|
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
|
var response []byte
|
||||||
if len(data.Files) > 0 {
|
if len(data.Files) > 0 {
|
||||||
body := &bytes.Buffer{}
|
contentType, body, encodeErr := MultipartBodyWithJSON(data, data.Files)
|
||||||
bodywriter := multipart.NewWriter(body)
|
if encodeErr != nil {
|
||||||
|
return st, encodeErr
|
||||||
var payload []byte
|
|
||||||
payload, err = json.Marshal(data)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var p io.Writer
|
response, err = s.request("POST", uri, contentType, body, uri, 0)
|
||||||
|
|
||||||
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)
|
|
||||||
} else {
|
} else {
|
||||||
response, err = s.RequestWithBucketID("POST", uri, data, uri)
|
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
|
// messageID : The ID of message to edit
|
||||||
func (s *Session) WebhookMessageEdit(webhookID, token, messageID string, data *WebhookEdit) (err error) {
|
func (s *Session) WebhookMessageEdit(webhookID, token, messageID string, data *WebhookEdit) (err error) {
|
||||||
uri := EndpointWebhookMessage(webhookID, token, messageID)
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2566,11 +2485,19 @@ func (s *Session) ApplicationCommands(appID, guildID string) (cmd []*Application
|
||||||
// appID : The application ID.
|
// appID : The application ID.
|
||||||
// interaction : Interaction instance.
|
// interaction : Interaction instance.
|
||||||
// resp : Response message data.
|
// 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)
|
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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
60
util.go
60
util.go
|
@ -1,6 +1,12 @@
|
||||||
package discordgo
|
package discordgo
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"mime/multipart"
|
||||||
|
"net/textproto"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
@ -15,3 +21,57 @@ func SnowflakeTimestamp(ID string) (t time.Time, err error) {
|
||||||
t = time.Unix(0, timestamp*1000000)
|
t = time.Unix(0, timestamp*1000000)
|
||||||
return
|
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
|
||||||
|
}
|
||||||
|
|
|
@ -42,5 +42,6 @@ type WebhookEdit struct {
|
||||||
Content string `json:"content,omitempty"`
|
Content string `json:"content,omitempty"`
|
||||||
Components []MessageComponent `json:"components"`
|
Components []MessageComponent `json:"components"`
|
||||||
Embeds []*MessageEmbed `json:"embeds,omitempty"`
|
Embeds []*MessageEmbed `json:"embeds,omitempty"`
|
||||||
|
Files []*File `json:"-"`
|
||||||
AllowedMentions *MessageAllowedMentions `json:"allowed_mentions,omitempty"`
|
AllowedMentions *MessageAllowedMentions `json:"allowed_mentions,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue