github.com/diamondburned/arikawa/v2@v2.1.0/api/webhook/webhook.go (about)

     1  // Package webhook provides means to interact with webhooks directly and not
     2  // through the bot API.
     3  package webhook
     4  
     5  import (
     6  	"context"
     7  	"mime/multipart"
     8  	"net/url"
     9  	"strconv"
    10  
    11  	"github.com/pkg/errors"
    12  
    13  	"github.com/diamondburned/arikawa/v2/api"
    14  	"github.com/diamondburned/arikawa/v2/api/rate"
    15  	"github.com/diamondburned/arikawa/v2/discord"
    16  	"github.com/diamondburned/arikawa/v2/utils/httputil"
    17  	"github.com/diamondburned/arikawa/v2/utils/httputil/httpdriver"
    18  	"github.com/diamondburned/arikawa/v2/utils/json/option"
    19  	"github.com/diamondburned/arikawa/v2/utils/sendpart"
    20  )
    21  
    22  // TODO: if there's ever an Arikawa v3, then a new Client abstraction could be
    23  // made that wraps around Session being an interface. Just a food for thought.
    24  
    25  // Session keeps a single webhook session. It is referenced by other webhook
    26  // clients using the same session.
    27  type Session struct {
    28  	// Limiter is the rate limiter used for the client. This field should not be
    29  	// changed, as doing so is potentially racy.
    30  	Limiter *rate.Limiter
    31  
    32  	// ID is the ID of the webhook.
    33  	ID discord.WebhookID
    34  	// Token is the token of the webhook.
    35  	Token string
    36  }
    37  
    38  // OnRequest should be called on each client request to inject itself.
    39  func (s *Session) OnRequest(r httpdriver.Request) error {
    40  	return s.Limiter.Acquire(r.GetContext(), r.GetPath())
    41  }
    42  
    43  // OnResponse should be called after each client request to clean itself up.
    44  func (s *Session) OnResponse(r httpdriver.Request, resp httpdriver.Response) error {
    45  	return s.Limiter.Release(r.GetPath(), httpdriver.OptHeader(resp))
    46  }
    47  
    48  // Client is the client used to interact with a webhook.
    49  type Client struct {
    50  	// Client is the httputil.Client used to call Discord's API.
    51  	*httputil.Client
    52  	*Session
    53  }
    54  
    55  // New creates a new Client using the passed webhook token and ID. It uses its
    56  // own rate limiter.
    57  func New(id discord.WebhookID, token string) *Client {
    58  	return NewCustom(id, token, httputil.NewClient())
    59  }
    60  
    61  // NewCustom creates a new webhook client using the passed webhook token, ID and
    62  // a copy of the given httputil.Client. The copy will have a new rate limiter
    63  // added in.
    64  func NewCustom(id discord.WebhookID, token string, hcl *httputil.Client) *Client {
    65  	ses := Session{
    66  		Limiter: rate.NewLimiter(api.Path),
    67  		ID:      id,
    68  		Token:   token,
    69  	}
    70  
    71  	hcl = hcl.Copy()
    72  	hcl.OnRequest = append(hcl.OnRequest, ses.OnRequest)
    73  	hcl.OnResponse = append(hcl.OnResponse, ses.OnResponse)
    74  
    75  	return &Client{
    76  		Client:  hcl,
    77  		Session: &ses,
    78  	}
    79  }
    80  
    81  // FromAPI creates a new client that shares the same internal HTTP client with
    82  // the one in the API's. This is often useful for bots that need webhook
    83  // interaction, since the rate limiter is shared.
    84  func FromAPI(id discord.WebhookID, token string, c *api.Client) *Client {
    85  	return &Client{
    86  		Client: c.Client,
    87  		Session: &Session{
    88  			Limiter: c.Limiter,
    89  			ID:      id,
    90  			Token:   token,
    91  		},
    92  	}
    93  }
    94  
    95  // WithContext returns a shallow copy of Client with the given context. It's
    96  // used for method timeouts and such. This method is thread-safe.
    97  func (c *Client) WithContext(ctx context.Context) *Client {
    98  	return &Client{
    99  		Client:  c.Client.WithContext(ctx),
   100  		Session: c.Session,
   101  	}
   102  }
   103  
   104  // Get gets the webhook.
   105  func (c *Client) Get() (*discord.Webhook, error) {
   106  	var w *discord.Webhook
   107  	return w, c.RequestJSON(&w, "GET", api.EndpointWebhooks+c.ID.String()+"/"+c.Token)
   108  }
   109  
   110  // Modify modifies the webhook.
   111  func (c *Client) Modify(data api.ModifyWebhookData) (*discord.Webhook, error) {
   112  	var w *discord.Webhook
   113  	return w, c.RequestJSON(
   114  		&w, "PATCH",
   115  		api.EndpointWebhooks+c.ID.String()+"/"+c.Token,
   116  		httputil.WithJSONBody(data),
   117  	)
   118  }
   119  
   120  // Delete deletes a webhook permanently.
   121  func (c *Client) Delete() error {
   122  	return c.FastRequest("DELETE", api.EndpointWebhooks+c.ID.String()+"/"+c.Token)
   123  }
   124  
   125  // https://discord.com/developers/docs/resources/webhook#execute-webhook-jsonform-params
   126  type ExecuteData struct {
   127  	// Content are the message contents (up to 2000 characters).
   128  	//
   129  	// Required: one of content, file, embeds
   130  	Content string `json:"content,omitempty"`
   131  
   132  	// Username overrides the default username of the webhook
   133  	Username string `json:"username,omitempty"`
   134  	// AvatarURL overrides the default avatar of the webhook.
   135  	AvatarURL discord.URL `json:"avatar_url,omitempty"`
   136  
   137  	// TTS is true if this is a TTS message.
   138  	TTS bool `json:"tts,omitempty"`
   139  	// Embeds contains embedded rich content.
   140  	//
   141  	// Required: one of content, file, embeds
   142  	Embeds []discord.Embed `json:"embeds,omitempty"`
   143  
   144  	// Files represents a list of files to upload. This will not be JSON-encoded
   145  	// and will only be available through WriteMultipart.
   146  	Files []sendpart.File `json:"-"`
   147  
   148  	// AllowedMentions are the allowed mentions for the message.
   149  	AllowedMentions *api.AllowedMentions `json:"allowed_mentions,omitempty"`
   150  }
   151  
   152  // NeedsMultipart returns true if the ExecuteWebhookData has files.
   153  func (data ExecuteData) NeedsMultipart() bool {
   154  	return len(data.Files) > 0
   155  }
   156  
   157  // WriteMultipart writes the webhook data into the given multipart body. It does
   158  // not close body.
   159  func (data ExecuteData) WriteMultipart(body *multipart.Writer) error {
   160  	return sendpart.Write(body, data, data.Files)
   161  }
   162  
   163  // Execute sends a message to the webhook, but doesn't wait for the message to
   164  // get created. This is generally faster, but only applicable if no further
   165  // interaction is required.
   166  func (c *Client) Execute(data ExecuteData) (err error) {
   167  	_, err = c.execute(data, false)
   168  	return
   169  }
   170  
   171  // ExecuteAndWait executes the webhook, and waits for the generated
   172  // discord.Message to be returned.
   173  func (c *Client) ExecuteAndWait(data ExecuteData) (*discord.Message, error) {
   174  	return c.execute(data, true)
   175  }
   176  
   177  func (c *Client) execute(data ExecuteData, wait bool) (*discord.Message, error) {
   178  	if data.Content == "" && len(data.Embeds) == 0 && len(data.Files) == 0 {
   179  		return nil, api.ErrEmptyMessage
   180  	}
   181  
   182  	if data.AllowedMentions != nil {
   183  		if err := data.AllowedMentions.Verify(); err != nil {
   184  			return nil, errors.Wrap(err, "allowedMentions error")
   185  		}
   186  	}
   187  
   188  	for i, embed := range data.Embeds {
   189  		if err := embed.Validate(); err != nil {
   190  			return nil, errors.Wrap(err, "embed error at "+strconv.Itoa(i))
   191  		}
   192  	}
   193  
   194  	var param url.Values
   195  	if wait {
   196  		param = url.Values{"wait": {"true"}}
   197  	}
   198  
   199  	var URL = api.EndpointWebhooks + c.ID.String() + "/" + c.Token + "?" + param.Encode()
   200  
   201  	var msg *discord.Message
   202  	var ptr interface{}
   203  	if wait {
   204  		ptr = &msg
   205  	}
   206  
   207  	return msg, sendpart.POST(c.Client, data, ptr, URL)
   208  }
   209  
   210  // https://discord.com/developers/docs/resources/webhook#edit-webhook-message-jsonform-params
   211  type EditMessageData struct {
   212  	// Content are the message contents. They may be up to 2000 characters
   213  	// characters long.
   214  	Content option.NullableString `json:"content,omitempty"`
   215  	// Embeds is an array of up to 10 discord.Embeds.
   216  	Embeds *[]discord.Embed `json:"embeds,omitempty"`
   217  	// AllowedMentions are the AllowedMentions for the message.
   218  	AllowedMentions *api.AllowedMentions `json:"allowed_mentions,omitempty"`
   219  }
   220  
   221  // EditMessage edits a previously-sent webhook message from the same webhook.
   222  func (c *Client) EditMessage(messageID discord.MessageID, data EditMessageData) error {
   223  	return c.FastRequest("PATCH",
   224  		api.EndpointWebhooks+c.ID.String()+"/"+c.Token+"/messages/"+messageID.String(),
   225  		httputil.WithJSONBody(data))
   226  }
   227  
   228  // DeleteMessage deletes a message that was previously created by the same
   229  // webhook.
   230  func (c *Client) DeleteMessage(messageID discord.MessageID) error {
   231  	return c.FastRequest("DELETE",
   232  		api.EndpointWebhooks+c.ID.String()+"/"+c.Token+"/messages/"+messageID.String())
   233  }