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 (
|
import (
|
||||||
"io"
|
"io"
|
||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
"net/url"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
"github.com/diamondburned/arikawa/discord"
|
"github.com/diamondburned/arikawa/discord"
|
||||||
|
"github.com/diamondburned/arikawa/internal/mulipartutil"
|
||||||
"github.com/diamondburned/arikawa/utils/httputil"
|
"github.com/diamondburned/arikawa/utils/httputil"
|
||||||
"github.com/diamondburned/arikawa/utils/json"
|
"github.com/diamondburned/arikawa/utils/json"
|
||||||
"github.com/pkg/errors"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const AttachmentSpoilerPrefix = "SPOILER_"
|
const AttachmentSpoilerPrefix = "SPOILER_"
|
||||||
|
@ -115,7 +115,7 @@ type SendMessageData struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (data *SendMessageData) WriteMultipart(body *multipart.Writer) error {
|
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
|
// 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 {
|
func (data *ExecuteWebhookData) WriteMultipart(body *multipart.Writer) error {
|
||||||
return writeMultipart(body, data, data.Files)
|
return mulipartutil.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
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -54,15 +54,6 @@ func (c *Client) Webhook(webhookID discord.WebhookID) (*discord.Webhook, error)
|
||||||
return w, c.RequestJSON(&w, "GET", EndpointWebhooks+webhookID.String())
|
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
|
// https://discord.com/developers/docs/resources/webhook#modify-webhook-json-params
|
||||||
type ModifyWebhookData struct {
|
type ModifyWebhookData struct {
|
||||||
// Name is the default name of the webhook.
|
// 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.
|
// DeleteWebhook deletes a webhook permanently.
|
||||||
//
|
//
|
||||||
// Requires the MANAGE_WEBHOOKS permission.
|
// Requires the MANAGE_WEBHOOKS permission.
|
||||||
func (c *Client) DeleteWebhook(webhookID discord.WebhookID) error {
|
func (c *Client) DeleteWebhook(webhookID discord.WebhookID) error {
|
||||||
return c.FastRequest("DELETE", EndpointWebhooks+webhookID.String())
|
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