diff --git a/api/send.go b/api/send.go index 3455165..6e4b463 100644 --- a/api/send.go +++ b/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) } diff --git a/api/webhook.go b/api/webhook.go index 49a086f..9a3ee17 100644 --- a/api/webhook.go +++ b/api/webhook.go @@ -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) -} diff --git a/internal/mulipartutil/multipart.go b/internal/mulipartutil/multipart.go new file mode 100644 index 0000000..992589a --- /dev/null +++ b/internal/mulipartutil/multipart.go @@ -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 +} diff --git a/webhook/webhook.go b/webhook/webhook.go new file mode 100644 index 0000000..3e1fe1f --- /dev/null +++ b/webhook/webhook.go @@ -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) +}