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:
Maximilian von Lindern 2020-07-29 21:29:30 +02:00 committed by diamondburned
parent ba4b224168
commit e1d9685cdb
4 changed files with 209 additions and 122 deletions

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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
}

163
webhook/webhook.go Normal file
View File

@ -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)
}