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 }