API: separate token-based and bot-based interactions with webhooks (#130)
* API: separate token-based and bot-based interactions with webhooks * API: move writeMultipart to internal/multipartutil * Multipartutil: fix double filetype-suffix
This commit is contained in:
parent
ba4b224168
commit
e1d9685cdb
98
api/send.go
98
api/send.go
|
@ -3,14 +3,14 @@ package api
|
|||
import (
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/diamondburned/arikawa/discord"
|
||||
"github.com/diamondburned/arikawa/internal/mulipartutil"
|
||||
"github.com/diamondburned/arikawa/utils/httputil"
|
||||
"github.com/diamondburned/arikawa/utils/json"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const AttachmentSpoilerPrefix = "SPOILER_"
|
||||
|
@ -115,7 +115,7 @@ type SendMessageData struct {
|
|||
}
|
||||
|
||||
func (data *SendMessageData) WriteMultipart(body *multipart.Writer) error {
|
||||
return writeMultipart(body, data, data.Files)
|
||||
return mulipartutil.WriteMultipart(body, data, data.Files)
|
||||
}
|
||||
|
||||
// SendMessageComplex posts a message to a guild text or DM channel. If
|
||||
|
@ -204,93 +204,5 @@ type ExecuteWebhookData struct {
|
|||
}
|
||||
|
||||
func (data *ExecuteWebhookData) WriteMultipart(body *multipart.Writer) error {
|
||||
return writeMultipart(body, data, data.Files)
|
||||
}
|
||||
|
||||
// ExecuteWebhook sends a message to the webhook. If wait is bool, Discord will
|
||||
// wait for the message to be delivered and will return the message body. This
|
||||
// also means the returned message will only be there if wait is true.
|
||||
func (c *Client) ExecuteWebhook(
|
||||
webhookID discord.WebhookID,
|
||||
token string,
|
||||
wait bool, // if false, then nil returned for *Message.
|
||||
data ExecuteWebhookData) (*discord.Message, error) {
|
||||
|
||||
if data.Content == "" && len(data.Embeds) == 0 && len(data.Files) == 0 {
|
||||
return nil, ErrEmptyMessage
|
||||
}
|
||||
|
||||
if data.AllowedMentions != nil {
|
||||
if err := data.AllowedMentions.Verify(); err != nil {
|
||||
return nil, errors.Wrap(err, "allowedMentions error")
|
||||
}
|
||||
}
|
||||
|
||||
for i, embed := range data.Embeds {
|
||||
if err := embed.Validate(); err != nil {
|
||||
return nil, errors.Wrap(err, "embed error at "+strconv.Itoa(i))
|
||||
}
|
||||
}
|
||||
|
||||
var param = url.Values{}
|
||||
if wait {
|
||||
param.Set("wait", "true")
|
||||
}
|
||||
|
||||
var URL = EndpointWebhooks + webhookID.String() + "/" + token + "?" + param.Encode()
|
||||
var msg *discord.Message
|
||||
|
||||
if len(data.Files) == 0 {
|
||||
// No files, so no need for streaming.
|
||||
return msg, c.RequestJSON(&msg, "POST", URL,
|
||||
httputil.WithJSONBody(data))
|
||||
}
|
||||
|
||||
writer := func(mw *multipart.Writer) error {
|
||||
return data.WriteMultipart(mw)
|
||||
}
|
||||
|
||||
resp, err := c.MeanwhileMultipart(writer, "POST", URL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var body = resp.GetBody()
|
||||
defer body.Close()
|
||||
|
||||
if !wait {
|
||||
// Since we didn't tell Discord to wait, we have nothing to parse.
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return msg, json.DecodeStream(body, &msg)
|
||||
}
|
||||
|
||||
func writeMultipart(body *multipart.Writer, item interface{}, files []SendMessageFile) error {
|
||||
defer body.Close()
|
||||
|
||||
// Encode the JSON body first
|
||||
w, err := body.CreateFormField("payload_json")
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to create bodypart for JSON")
|
||||
}
|
||||
|
||||
if err := json.EncodeStream(w, item); err != nil {
|
||||
return errors.Wrap(err, "failed to encode JSON")
|
||||
}
|
||||
|
||||
for i, file := range files {
|
||||
num := strconv.Itoa(i)
|
||||
|
||||
w, err := body.CreateFormFile("file"+num, file.Name)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to create bodypart for "+num)
|
||||
}
|
||||
|
||||
if _, err := io.Copy(w, file.Reader); err != nil {
|
||||
return errors.Wrap(err, "failed to write for file "+num)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
return mulipartutil.WriteMultipart(body, data, data.Files)
|
||||
}
|
||||
|
|
|
@ -54,15 +54,6 @@ func (c *Client) Webhook(webhookID discord.WebhookID) (*discord.Webhook, error)
|
|||
return w, c.RequestJSON(&w, "GET", EndpointWebhooks+webhookID.String())
|
||||
}
|
||||
|
||||
// WebhookWithToken is the same as above, except this call does not require
|
||||
// authentication and returns no user in the webhook object.
|
||||
func (c *Client) WebhookWithToken(
|
||||
webhookID discord.WebhookID, token string) (*discord.Webhook, error) {
|
||||
|
||||
var w *discord.Webhook
|
||||
return w, c.RequestJSON(&w, "GET", EndpointWebhooks+webhookID.String()+"/"+token)
|
||||
}
|
||||
|
||||
// https://discord.com/developers/docs/resources/webhook#modify-webhook-json-params
|
||||
type ModifyWebhookData struct {
|
||||
// Name is the default name of the webhook.
|
||||
|
@ -87,29 +78,9 @@ func (c *Client) ModifyWebhook(
|
|||
)
|
||||
}
|
||||
|
||||
// ModifyWebhookWithToken is the same as above, except this call does not
|
||||
// require authentication, does not accept a channel_id parameter in the body,
|
||||
// and does not return a user in the webhook object.
|
||||
func (c *Client) ModifyWebhookWithToken(
|
||||
webhookID discord.WebhookID, token string, data ModifyWebhookData) (*discord.Webhook, error) {
|
||||
|
||||
var w *discord.Webhook
|
||||
return w, c.RequestJSON(
|
||||
&w, "PATCH",
|
||||
EndpointWebhooks+webhookID.String()+"/"+token,
|
||||
httputil.WithJSONBody(data),
|
||||
)
|
||||
}
|
||||
|
||||
// DeleteWebhook deletes a webhook permanently.
|
||||
//
|
||||
// Requires the MANAGE_WEBHOOKS permission.
|
||||
func (c *Client) DeleteWebhook(webhookID discord.WebhookID) error {
|
||||
return c.FastRequest("DELETE", EndpointWebhooks+webhookID.String())
|
||||
}
|
||||
|
||||
// DeleteWebhookWithToken is the same as above, except this call does not
|
||||
// require authentication.
|
||||
func (c *Client) DeleteWebhookWithToken(webhookID discord.WebhookID, token string) error {
|
||||
return c.FastRequest("DELETE", EndpointWebhooks+webhookID.String()+"/"+token)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
package mulipartutil
|
||||
|
||||
import (
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"strconv"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/diamondburned/arikawa/api"
|
||||
"github.com/diamondburned/arikawa/utils/json"
|
||||
)
|
||||
|
||||
func WriteMultipart(body *multipart.Writer, item interface{}, files []api.SendMessageFile) error {
|
||||
defer body.Close()
|
||||
|
||||
// Encode the JSON body first
|
||||
w, err := body.CreateFormField("payload_json")
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to create bodypart for JSON")
|
||||
}
|
||||
|
||||
if err := json.EncodeStream(w, item); err != nil {
|
||||
return errors.Wrap(err, "failed to encode JSON")
|
||||
}
|
||||
|
||||
for i, file := range files {
|
||||
num := strconv.Itoa(i)
|
||||
|
||||
w, err := body.CreateFormFile("file"+num, file.Name)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to create bodypart for "+num)
|
||||
}
|
||||
|
||||
if _, err := io.Copy(w, file.Reader); err != nil {
|
||||
return errors.Wrap(err, "failed to write for file "+num)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,163 @@
|
|||
// Package webhook provides means to interact with webhooks directly and not
|
||||
// through the bot API.
|
||||
package webhook
|
||||
|
||||
import (
|
||||
"mime/multipart"
|
||||
"net/url"
|
||||
"strconv"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/diamondburned/arikawa/api"
|
||||
"github.com/diamondburned/arikawa/discord"
|
||||
"github.com/diamondburned/arikawa/utils/httputil"
|
||||
"github.com/diamondburned/arikawa/utils/json"
|
||||
)
|
||||
|
||||
// DefaultHTTPClient is the httputil.Client used in the helper methods.
|
||||
var DefaultHTTPClient = httputil.NewClient()
|
||||
|
||||
// Client is the client used to interact with a webhook.
|
||||
type Client struct {
|
||||
// Client is the httputil.Client used to call Discord's API.
|
||||
*httputil.Client
|
||||
// Token is the token of the webhook.
|
||||
Token string
|
||||
// ID is the id of the webhook.
|
||||
ID discord.WebhookID
|
||||
}
|
||||
|
||||
// NewClient creates a new Client using the passed token and id.
|
||||
func NewClient(token string, id discord.WebhookID) *Client {
|
||||
return NewCustomClient(token, id, httputil.NewClient())
|
||||
}
|
||||
|
||||
// NewCustomClient creates a new Client creates a new Client using the passed
|
||||
// token and id and makes API calls using the passed httputil.Client
|
||||
func NewCustomClient(token string, id discord.WebhookID, c *httputil.Client) *Client {
|
||||
return &Client{
|
||||
Client: c,
|
||||
Token: token,
|
||||
ID: id,
|
||||
}
|
||||
}
|
||||
|
||||
// Get gets the webhook.
|
||||
func (c *Client) Get() (*discord.Webhook, error) {
|
||||
var w *discord.Webhook
|
||||
return w, c.RequestJSON(&w, "GET", api.EndpointWebhooks+c.ID.String()+"/"+c.Token)
|
||||
}
|
||||
|
||||
// Modify modifies the webhook.
|
||||
func (c *Client) Modify(data api.ModifyWebhookData) (*discord.Webhook, error) {
|
||||
var w *discord.Webhook
|
||||
return w, c.RequestJSON(
|
||||
&w, "PATCH",
|
||||
api.EndpointWebhooks+c.ID.String()+"/"+c.Token,
|
||||
httputil.WithJSONBody(data),
|
||||
)
|
||||
}
|
||||
|
||||
// Delete deletes a webhook permanently.
|
||||
func (c *Client) Delete() error {
|
||||
return c.FastRequest("DELETE", api.EndpointWebhooks+c.ID.String()+"/"+c.Token)
|
||||
}
|
||||
|
||||
// Execute sends a message to the webhook, but doesn't wait for the message to
|
||||
// get created. This is generally faster, but only applicable if no further
|
||||
// interaction is required.
|
||||
func (c *Client) Execute(data api.ExecuteWebhookData) (err error) {
|
||||
_, err = c.execute(data, false)
|
||||
return
|
||||
}
|
||||
|
||||
// ExecuteAndWait executes the webhook, and waits for the generated
|
||||
// discord.Message to be returned.
|
||||
func (c *Client) ExecuteAndWait(data api.ExecuteWebhookData) (*discord.Message, error) {
|
||||
return c.execute(data, true)
|
||||
}
|
||||
|
||||
func (c *Client) execute(data api.ExecuteWebhookData, wait bool) (*discord.Message, error) {
|
||||
if data.Content == "" && len(data.Embeds) == 0 && len(data.Files) == 0 {
|
||||
return nil, api.ErrEmptyMessage
|
||||
}
|
||||
|
||||
if data.AllowedMentions != nil {
|
||||
if err := data.AllowedMentions.Verify(); err != nil {
|
||||
return nil, errors.Wrap(err, "allowedMentions error")
|
||||
}
|
||||
}
|
||||
|
||||
for i, embed := range data.Embeds {
|
||||
if err := embed.Validate(); err != nil {
|
||||
return nil, errors.Wrap(err, "embed error at "+strconv.Itoa(i))
|
||||
}
|
||||
}
|
||||
|
||||
var param = url.Values{}
|
||||
if wait {
|
||||
param.Set("wait", "true")
|
||||
}
|
||||
|
||||
var URL = api.EndpointWebhooks + c.ID.String() + "/" + c.Token + "?" + param.Encode()
|
||||
var msg *discord.Message
|
||||
|
||||
if len(data.Files) == 0 {
|
||||
// No files, so no need for streaming.
|
||||
return msg, c.RequestJSON(&msg, "POST", URL,
|
||||
httputil.WithJSONBody(data))
|
||||
}
|
||||
|
||||
writer := func(mw *multipart.Writer) error {
|
||||
return data.WriteMultipart(mw)
|
||||
}
|
||||
|
||||
resp, err := c.MeanwhileMultipart(writer, "POST", URL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var body = resp.GetBody()
|
||||
defer body.Close()
|
||||
|
||||
if !wait {
|
||||
// Since we didn't tell Discord to wait, we have nothing to parse.
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return msg, json.DecodeStream(body, &msg)
|
||||
}
|
||||
|
||||
// Get is a shortcut for NewCustomClient(token, id, DefaultHTTPClient).Get().
|
||||
func Get(token string, id discord.WebhookID) (*discord.Webhook, error) {
|
||||
return NewCustomClient(token, id, DefaultHTTPClient).Get()
|
||||
}
|
||||
|
||||
// Modify is a shortcut for
|
||||
// NewCustomClient(token, id, DefaultHTTPClient).Modify(data).
|
||||
func Modify(
|
||||
token string, id discord.WebhookID, data api.ModifyWebhookData) (*discord.Webhook, error) {
|
||||
|
||||
return NewCustomClient(token, id, DefaultHTTPClient).Modify(data)
|
||||
}
|
||||
|
||||
// Delete is a shortcut for
|
||||
// NewCustomClient(token, id, DefaultHTTPClient).Delete().
|
||||
func Delete(token string, id discord.WebhookID) error {
|
||||
return NewCustomClient(token, id, DefaultHTTPClient).Delete()
|
||||
}
|
||||
|
||||
// Execute is a shortcut for
|
||||
// NewCustomClient(token, id, DefaultHTTPClient).Execute(data).
|
||||
func Execute(token string, id discord.WebhookID, data api.ExecuteWebhookData) error {
|
||||
return NewCustomClient(token, id, DefaultHTTPClient).Execute(data)
|
||||
}
|
||||
|
||||
// ExecuteAndWait is a shortcut for
|
||||
// NewCustomClient(token, id, DefaultHTTPClient).ExecuteAndWait(data).
|
||||
func ExecuteAndWait(
|
||||
token string, id discord.WebhookID, data api.ExecuteWebhookData) (*discord.Message, error) {
|
||||
|
||||
return NewCustomClient(token, id, DefaultHTTPClient).ExecuteAndWait(data)
|
||||
}
|
Loading…
Reference in New Issue