Browse Source

Combine with slash

Signed-off-by: jolheiser <john.olheiser@gmail.com>
main v0.1.0
jolheiser 7 months ago
parent
commit
52aa378de8
Signed by: jolheiser GPG Key ID: B853ADA5DA7BBF7A
  1. 2
      Makefile
  2. 6
      README.md
  3. 2
      embed/embed.go
  4. 2
      embed/fields.go
  5. 2
      embed/mention.go
  6. 115
      slash/client.go
  7. 136
      slash/global.go
  8. 136
      slash/guild.go
  9. 91
      slash/handler.go
  10. 84
      slash/handler_test.go
  11. 105
      slash/interaction.go
  12. 19
      slash/oauth.go
  13. 63
      slash/oauth_test.go
  14. 235
      slash/structs.go
  15. 23
      webhook/webhook.go
  16. 24
      webhook/webhook_test.go

2
Makefile

@ -6,4 +6,4 @@ fmt:
.PHONY: test
test:
$(GO) test -race ./...
$(GO) test -v -race ./...

6
README.md

@ -1,10 +1,8 @@
# Disco
A Discord webhook library. That's it.
A Discord web library. No gateway, just embeds, webhooks, and slash commands.
Not a full REST API, not a bot maker. Just a webhook/embed library.
[example](webhook_test.go)
[example](webhook/webhook_test.go)
## License

2
embed.go → embed/embed.go

@ -1,4 +1,4 @@
package disco
package embed
import "time"

2
fields.go → embed/fields.go

@ -1,4 +1,4 @@
package disco
package embed
// Author is the author of an embed
type Author struct {

2
mention.go → embed/mention.go

@ -1,4 +1,4 @@
package disco
package embed
// Parse is the parse types of AllowedMentions
type Parse string

115
slash/client.go

@ -0,0 +1,115 @@
package slash
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"time"
)
var baseEndpoint = "https://discord.com/api/v8/"
// Client is a slash client
type Client struct {
clientID string
clientSecret string
http *http.Client
oauth oauth
}
// ClientOption is options for a Client
type ClientOption func(*Client)
// WithHTTP sets the http.Client for a Client
func WithHTTP(client *http.Client) ClientOption {
return func(c *Client) {
c.http = client
}
}
// NewClient creates a new Client for working with slash commands
func NewClient(clientID, clientSecret string, opts ...ClientOption) *Client {
c := &Client{
clientID: clientID,
clientSecret: clientSecret,
http: http.DefaultClient,
}
for _, opt := range opts {
opt(c)
}
return c
}
// OAuthURL returns a URL suitable for giving an app slash command scope
func (c *Client) OAuthURL() string {
return fmt.Sprintf("https://discord.com/api/oauth2/authorize?client_id=%s&scope=applications.commands", c.clientID)
}
func (c *Client) newRequest(ctx context.Context, method, url string, body io.Reader) (*http.Request, error) {
if err := c.checkToken(ctx); err != nil {
return nil, err
}
req, err := http.NewRequestWithContext(ctx, method, url, body)
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", c.oauth.header())
return req, nil
}
func (c *Client) checkToken(ctx context.Context) error {
if time.Now().Before(c.oauth.ExpiresAt) {
return nil
}
data := url.Values{
"grant_type": []string{"client_credentials"},
"scope": []string{"applications.commands.update"},
}
endpoint := baseEndpoint + "oauth2/token"
req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, strings.NewReader(data.Encode()))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.SetBasicAuth(c.clientID, c.clientSecret)
resp, err := c.http.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return errors.New("could not get client credentials token")
}
var oauth oauth
if err := json.NewDecoder(resp.Body).Decode(&oauth); err != nil {
return err
}
oauth.ExpiresAt = time.Now().Add(time.Second * time.Duration(oauth.ExpiresIn))
c.oauth = oauth
return nil
}
func newBuffer(val interface{}) (bytes.Buffer, error) {
var buf bytes.Buffer
return buf, json.NewEncoder(&buf).Encode(val)
}
func errMsg(r io.Reader) string {
msg, err := io.ReadAll(r)
if err != nil {
return err.Error()
}
return string(msg)
}

136
slash/global.go

@ -0,0 +1,136 @@
package slash
import (
"context"
"encoding/json"
"fmt"
"net/http"
)
// GetGlobalApplicationCommands gets all global slash commands
func (c *Client) GetGlobalApplicationCommands(ctx context.Context) ([]*ApplicationCommand, error) {
endpoint := baseEndpoint + fmt.Sprintf("applications/%s/commands", c.clientID)
req, err := c.newRequest(ctx, http.MethodGet, endpoint, nil)
if err != nil {
return nil, err
}
resp, err := c.http.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("GetGlobalApplicationCommands: returned non-200 status code: %s\n%s", resp.Status, errMsg(resp.Body))
}
var appCmds []*ApplicationCommand
return appCmds, json.NewDecoder(resp.Body).Decode(&appCmds)
}
// CreateGlobalApplicationCommand creates a global slash command
//
// Creating a global application command is an upsert, meaning creating a command with the same name will update it
// rather than return an error
func (c *Client) CreateGlobalApplicationCommand(ctx context.Context, cmd *CreateApplicationCommand) (*ApplicationCommand, error) {
endpoint := baseEndpoint + fmt.Sprintf("applications/%s/commands", c.clientID)
buf, err := newBuffer(cmd)
if err != nil {
return nil, err
}
req, err := c.newRequest(ctx, http.MethodPost, endpoint, &buf)
if err != nil {
return nil, err
}
resp, err := c.http.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("CreateGlobalApplicationCommand: returned non-20x status code: %s\n%s", resp.Status, errMsg(resp.Body))
}
var appCmd *ApplicationCommand
return appCmd, json.NewDecoder(resp.Body).Decode(&appCmd)
}
// BulkOverwriteGlobalApplicationCommands bulk overwrites global slash commands
func (c *Client) BulkOverwriteGlobalApplicationCommands(ctx context.Context, cmd []*CreateApplicationCommand) ([]*ApplicationCommand, error) {
endpoint := baseEndpoint + fmt.Sprintf("applications/%s/commands", c.clientID)
buf, err := newBuffer(cmd)
if err != nil {
return nil, err
}
req, err := c.newRequest(ctx, http.MethodPatch, endpoint, &buf)
if err != nil {
return nil, err
}
resp, err := c.http.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("CreateGlobalApplicationCommand: returned non-200 status code: %s\n%s", resp.Status, errMsg(resp.Body))
}
var appCmd []*ApplicationCommand
return appCmd, json.NewDecoder(resp.Body).Decode(&appCmd)
}
// GetGlobalApplicationCommand gets a global slash command by its ID
func (c *Client) GetGlobalApplicationCommand(ctx context.Context, cmdID string) (*ApplicationCommand, error) {
endpoint := baseEndpoint + fmt.Sprintf("applications/%s/commands/%s", c.clientID, cmdID)
req, err := c.newRequest(ctx, http.MethodGet, endpoint, nil)
if err != nil {
return nil, err
}
resp, err := c.http.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("GetGlobalApplicationCommand: returned non-200 status code: %s\n%s", resp.Status, errMsg(resp.Body))
}
var appCmd *ApplicationCommand
return appCmd, json.NewDecoder(resp.Body).Decode(&appCmd)
}
// UpdateGlobalApplicationCommand updates a global slash command
func (c *Client) UpdateGlobalApplicationCommand(ctx context.Context, cmdID string, cmd *CreateApplicationCommand) (*ApplicationCommand, error) {
endpoint := baseEndpoint + fmt.Sprintf("applications/%s/commands/%s", c.clientID, cmdID)
buf, err := newBuffer(cmd)
if err != nil {
return nil, err
}
req, err := c.newRequest(ctx, http.MethodPatch, endpoint, &buf)
if err != nil {
return nil, err
}
resp, err := c.http.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("UpdateGlobalApplicationCommand: returned non-200 status code: %s\n%s", resp.Status, errMsg(resp.Body))
}
var appCmd *ApplicationCommand
return appCmd, json.NewDecoder(resp.Body).Decode(&appCmd)
}
// DeleteGlobalApplicationCommand deletes a global slash command
func (c *Client) DeleteGlobalApplicationCommand(ctx context.Context, cmdID string) error {
endpoint := baseEndpoint + fmt.Sprintf("applications/%s/commands/%s", c.clientID, cmdID)
req, err := c.newRequest(ctx, http.MethodDelete, endpoint, nil)
if err != nil {
return err
}
resp, err := c.http.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusNoContent {
return fmt.Errorf("DeleteGlobalApplicationCommand: returned non-204 status code: %s\n%s", resp.Status, errMsg(resp.Body))
}
return nil
}

136
slash/guild.go

@ -0,0 +1,136 @@
package slash
import (
"context"
"encoding/json"
"fmt"
"net/http"
)
// GetGuildApplicationCommands gets all slash commands for a guild
func (c *Client) GetGuildApplicationCommands(ctx context.Context, guildID string) ([]*ApplicationCommand, error) {
endpoint := baseEndpoint + fmt.Sprintf("applications/%s/guilds/%s/commands", c.clientID, guildID)
req, err := c.newRequest(ctx, http.MethodGet, endpoint, nil)
if err != nil {
return nil, err
}
resp, err := c.http.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("GetGuildApplicationCommands: returned non-200 status code: %s\n%s", resp.Status, errMsg(resp.Body))
}
var appCmds []*ApplicationCommand
return appCmds, json.NewDecoder(resp.Body).Decode(&appCmds)
}
// CreateGuildApplicationCommand creates a slash command for a guild
//
// Creating a guild application command is an upsert, meaning creating a command with the same name will update it
// rather than return an error
func (c *Client) CreateGuildApplicationCommand(ctx context.Context, guildID string, cmd *CreateApplicationCommand) (*ApplicationCommand, error) {
endpoint := baseEndpoint + fmt.Sprintf("applications/%s/guilds/%s/commands", c.clientID, guildID)
buf, err := newBuffer(cmd)
if err != nil {
return nil, err
}
req, err := c.newRequest(ctx, http.MethodPost, endpoint, &buf)
if err != nil {
return nil, err
}
resp, err := c.http.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("CreateGuildApplicationCommand: returned non-20x status code: %s\n%s", resp.Status, errMsg(resp.Body))
}
var appCmd *ApplicationCommand
return appCmd, json.NewDecoder(resp.Body).Decode(&appCmd)
}
// BulkOverwriteGuildApplicationCommands bulk overwrites slash commands for a guild
func (c *Client) BulkOverwriteGuildApplicationCommands(ctx context.Context, guildID string, cmd []*CreateApplicationCommand) ([]*ApplicationCommand, error) {
endpoint := baseEndpoint + fmt.Sprintf("applications/%s/guilds/%s/commands", c.clientID, guildID)
buf, err := newBuffer(cmd)
if err != nil {
return nil, err
}
req, err := c.newRequest(ctx, http.MethodPatch, endpoint, &buf)
if err != nil {
return nil, err
}
resp, err := c.http.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("CreateGuildApplicationCommand: returned non-20x status code: %s\n%s", resp.Status, errMsg(resp.Body))
}
var appCmd []*ApplicationCommand
return appCmd, json.NewDecoder(resp.Body).Decode(&appCmd)
}
// GetGuildApplicationCommand gets a slash command for a guild by its ID
func (c *Client) GetGuildApplicationCommand(ctx context.Context, guildID, cmdID string) (*ApplicationCommand, error) {
endpoint := baseEndpoint + fmt.Sprintf("applications/%s/guilds/%s/commands/%s", c.clientID, guildID, cmdID)
req, err := c.newRequest(ctx, http.MethodGet, endpoint, nil)
if err != nil {
return nil, err
}
resp, err := c.http.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("GetGuildApplicationCommand: returned non-200 status code: %s\n%s", resp.Status, errMsg(resp.Body))
}
var appCmd *ApplicationCommand
return appCmd, json.NewDecoder(resp.Body).Decode(&appCmd)
}
// UpdateGuildApplicationCommand updates a slash command for a guild
func (c *Client) UpdateGuildApplicationCommand(ctx context.Context, guildID, cmdID string, cmd *CreateApplicationCommand) (*ApplicationCommand, error) {
endpoint := baseEndpoint + fmt.Sprintf("applications/%s/guilds/%s/commands/%s", c.clientID, guildID, cmdID)
buf, err := newBuffer(cmd)
if err != nil {
return nil, err
}
req, err := c.newRequest(ctx, http.MethodPatch, endpoint, &buf)
if err != nil {
return nil, err
}
resp, err := c.http.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("UpdateGuildApplicationCommand: returned non-200 status code: %s\n%s", resp.Status, errMsg(resp.Body))
}
var appCmd *ApplicationCommand
return appCmd, json.NewDecoder(resp.Body).Decode(&appCmd)
}
// DeleteGuildApplicationCommand deletes a slash command for a guild
func (c *Client) DeleteGuildApplicationCommand(ctx context.Context, guildID, cmdID string) error {
endpoint := baseEndpoint + fmt.Sprintf("applications/%s/guilds/%s/commands/%s", c.clientID, guildID, cmdID)
req, err := c.newRequest(ctx, http.MethodDelete, endpoint, nil)
if err != nil {
return err
}
resp, err := c.http.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusNoContent {
return fmt.Errorf("DeleteGuildApplicationCommand: returned non-204 status code: %s\n%s", resp.Status, errMsg(resp.Body))
}
return nil
}

91
slash/handler.go

@ -0,0 +1,91 @@
package slash
import (
"bytes"
"crypto/ed25519"
"encoding/hex"
"encoding/json"
"io"
"net/http"
)
// Handler returns an http.Handler capable of responding to slash commands
func Handler(key string, commands []*Command) (http.Handler, error) {
hexKey, err := hex.DecodeString(key)
if err != nil {
return nil, err
}
mux := http.NewServeMux()
mux.Handle("/", handler(hexKey, commands))
return mux, nil
}
func handler(hexKey []byte, commands []*Command) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// Verify Signature
if ok := verifyMiddleware(hexKey, w, r); !ok {
return
}
var interaction *Interaction
if err := json.NewDecoder(r.Body).Decode(&interaction); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// Pre-ACK
if interaction.Type == 0 {
http.Error(w, "invalid body", http.StatusBadRequest)
return
}
// ACK
if interaction.Type == 1 {
if err := json.NewEncoder(w).Encode(map[string]int{"type": 1}); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
return
}
// Interaction
for _, cmd := range commands {
if cmd.ID == interaction.Data.ID {
resp, err := cmd.Handle(interaction)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if err := json.NewEncoder(w).Encode(resp); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
break
}
}
}
}
func verifyMiddleware(key []byte, w http.ResponseWriter, r *http.Request) bool {
signature := r.Header.Get("X-Signature-Ed25519")
timestamp := r.Header.Get("X-Signature-Timestamp")
sig, err := hex.DecodeString(signature)
if err != nil {
http.Error(w, err.Error(), http.StatusUnauthorized)
return false
}
body := new(bytes.Buffer)
if _, err := io.Copy(body, r.Body); err != nil {
http.Error(w, err.Error(), http.StatusUnauthorized)
return false
}
buf := bytes.NewBufferString(timestamp + body.String())
if !ed25519.Verify(key, buf.Bytes(), sig) {
http.Error(w, "invalid signature", http.StatusUnauthorized)
return false
}
r.Body = io.NopCloser(body)
return true
}

84
slash/handler_test.go

@ -0,0 +1,84 @@
package slash
import (
"crypto/ed25519"
"encoding/hex"
"fmt"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
)
func TestACK(t *testing.T) {
pub, priv, err := ed25519.GenerateKey(nil)
if err != nil {
t.Log(err)
t.FailNow()
}
_, bad, err := ed25519.GenerateKey(nil)
if err != nil {
t.Log(err)
t.FailNow()
}
timestamp := fmt.Sprint(time.Now().Unix())
tt := []struct {
Name string
Body string
Key ed25519.PrivateKey
Status int
}{
{
Name: "Correct",
Body: `{"type": 1}`,
Key: priv,
Status: http.StatusOK,
},
{
Name: "Bad Key",
Body: `{"type": 1}`,
Key: bad,
Status: http.StatusUnauthorized,
},
{
Name: "Blank Body",
Body: "",
Key: priv,
Status: http.StatusBadRequest,
},
}
handler, err := Handler(hex.EncodeToString(pub), nil)
if err != nil {
t.Log(err)
t.FailNow()
}
s := httptest.NewServer(handler)
defer s.Close()
for _, tc := range tt {
t.Run(tc.Name, func(t *testing.T) {
req, err := http.NewRequest(http.MethodPost, s.URL, strings.NewReader(tc.Body))
if err != nil {
t.Log(err)
t.FailNow()
}
req.Header.Set("X-Signature-Timestamp", timestamp)
sig := ed25519.Sign(tc.Key, []byte(timestamp+tc.Body))
req.Header.Set("X-Signature-Ed25519", hex.EncodeToString(sig))
resp, err := http.DefaultClient.Do(req)
if err != nil {
t.Log(err)
t.FailNow()
}
if resp.StatusCode != tc.Status {
t.Logf("inccorrect response, expected status %d but got %d\n", tc.Status, resp.StatusCode)
t.FailNow()
}
})
}
}

105
slash/interaction.go

@ -0,0 +1,105 @@
package slash
import (
"context"
"fmt"
"net/http"
"go.jolheiser.com/disco/webhook"
)
// CreateInteractionResponse creates an interaction response
func (c *Client) CreateInteractionResponse(ctx context.Context, interactionID, interactionToken string, interaction *InteractionResponse) error {
endpoint := baseEndpoint + fmt.Sprintf("interactions/%s/%s/callback", interactionID, interactionToken)
buf, err := newBuffer(interaction)
if err != nil {
return err
}
req, err := c.newRequest(ctx, http.MethodPost, endpoint, &buf)
if err != nil {
return err
}
_, err = c.http.Do(req)
return err
}
// EditInteractionResponse edits an interaction response
func (c *Client) EditInteractionResponse(ctx context.Context, interactionToken string, interaction *WebhookEdit) error {
endpoint := baseEndpoint + fmt.Sprintf("webhooks/%s/%s/messages/@original", c.clientID, interactionToken)
buf, err := newBuffer(interaction)
if err != nil {
return err
}
req, err := c.newRequest(ctx, http.MethodPatch, endpoint, &buf)
if err != nil {
return err
}
_, err = c.http.Do(req)
return err
}
// DeleteInteractionResponse deletes an interaction response
func (c *Client) DeleteInteractionResponse(ctx context.Context, interactionToken string) error {
endpoint := baseEndpoint + fmt.Sprintf("webhooks/%s/%s/messages/@original", c.clientID, interactionToken)
req, err := c.newRequest(ctx, http.MethodDelete, endpoint, nil)
if err != nil {
return err
}
resp, err := c.http.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("returned non-200 status code: %s", resp.Status)
}
return nil
}
// CreateFollowupMessage creates a followup message
func (c *Client) CreateFollowupMessage(ctx context.Context, interactionToken string, interaction *webhook.Webhook) error {
endpoint := baseEndpoint + fmt.Sprintf("webhooks/%s/%s", c.clientID, interactionToken)
buf, err := newBuffer(interaction)
if err != nil {
return err
}
req, err := c.newRequest(ctx, http.MethodPost, endpoint, &buf)
if err != nil {
return err
}
_, err = c.http.Do(req)
return err
}
// EditFollowupMessage edits a followup message
func (c *Client) EditFollowupMessage(ctx context.Context, interactionToken, messageID string, interaction *WebhookEdit) error {
endpoint := baseEndpoint + fmt.Sprintf("webhooks/%s/%s/messages/%s", c.clientID, interactionToken, messageID)
buf, err := newBuffer(interaction)
if err != nil {
return err
}
req, err := c.newRequest(ctx, http.MethodPatch, endpoint, &buf)
if err != nil {
return err
}
_, err = c.http.Do(req)
return err
}
// DeleteFollowupMessage deletes a followup message
func (c *Client) DeleteFollowupMessage(ctx context.Context, interactionToken, messageID string) error {
endpoint := baseEndpoint + fmt.Sprintf("webhooks/%s/%s/messages/%s", c.clientID, interactionToken, messageID)
req, err := c.newRequest(ctx, http.MethodPatch, endpoint, nil)
if err != nil {
return err
}
resp, err := c.http.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("returned non-200 status code: %s", resp.Status)
}
return nil
}

19
slash/oauth.go

@ -0,0 +1,19 @@
package slash
import (
"fmt"
"time"
)
type oauth struct {
AccessToken string `json:"access_token"`
TokenType string `json:"token_type"`
ExpiresIn int `json:"expires_in"`
Scope string `json:"scope"`
ExpiresAt time.Time
}
func (o oauth) header() string {
return fmt.Sprintf("%s %s", o.TokenType, o.AccessToken)
}

63
slash/oauth_test.go

@ -0,0 +1,63 @@
package slash
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
)
const (
clientID = "client"
clientSecret = "secret"
token = "abcd"
)
func TestOAuth(t *testing.T) {
s := httptest.NewServer(oauthHandler)
defer s.Close()
client := NewClient(clientID, clientSecret)
baseEndpoint = s.URL + "/"
// OK
if err := client.checkToken(context.Background()); err != nil {
t.Log(err)
t.FailNow()
}
if client.oauth.AccessToken != token {
t.Logf("incorrect access token, got %s but expected %s\n", client.oauth.AccessToken, token)
}
// Rinse
client.oauth = oauth{}
// Not-OK
client.clientSecret = "public"
if err := client.checkToken(context.Background()); err == nil {
t.Log("bad client secret should fail oauth check")
t.FailNow()
}
}
var oauthHandler http.HandlerFunc = func(w http.ResponseWriter, r *http.Request) {
user, pass, ok := r.BasicAuth()
if !ok {
w.WriteHeader(http.StatusBadRequest)
return
}
if user != clientID || pass != clientSecret {
w.WriteHeader(http.StatusUnauthorized)
return
}
o := oauth{
AccessToken: token,
TokenType: "Bearer",
ExpiresIn: 60,
Scope: "360no",
}
_ = json.NewEncoder(w).Encode(o)
}

235
slash/structs.go

@ -0,0 +1,235 @@
package slash
import (
"go.jolheiser.com/disco/embed"
)
// CreateApplicationCommand https://discord.com/developers/docs/interactions/slash-commands#applicationcommand
type CreateApplicationCommand struct {
Name string `json:"name"`
Description string `json:"description"`
Options []*ApplicationCommandOption `json:"options,omitempty"`
}
// ApplicationCommand https://discord.com/developers/docs/interactions/slash-commands#applicationcommand
type ApplicationCommand struct {
ID string `json:"id"`
ApplicationID string `json:"application_id"`
CreateApplicationCommand
}
// ApplicationCommandOptionType (ACOT)
type ApplicationCommandOptionType int
// https://discord.com/developers/docs/interactions/slash-commands#applicationcommandoptiontype
const (
SubCommandACOT ApplicationCommandOptionType = iota + 1
SubCommandGroupACOT
StringACOT
IntegerACOT
BooleanACOT
UserACOT
ChannelACOT
RoleACOT
)
// ApplicationCommandOption https://discord.com/developers/docs/interactions/slash-commands#applicationcommandoption
type ApplicationCommandOption struct {
Type ApplicationCommandOptionType `json:"type"`
Name string `json:"name"`
Description string `json:"description"`
Required bool `json:"required,omitempty"`
Choices []*ApplicationCommandOptionChoice `json:"choices,omitempty"`
Options []*ApplicationCommandOption `json:"options,omitempty"`
}
// ApplicationCommandOptionChoice https://discord.com/developers/docs/interactions/slash-commands#applicationcommandoptionchoice
type ApplicationCommandOptionChoice struct {
Name string `json:"name"`
Value interface{} `json:"value"`
}
// InteractionType (IT)
type InteractionType int
// https://discord.com/developers/docs/interactions/slash-commands#interaction-interactiontype
const (
PingIT InteractionType = iota + 1
ApplicationCommandIT
)
// https://discord.com/developers/docs/interactions/slash-commands#interaction
type Interaction struct {
ID string `json:"id"`
Type InteractionType `json:"type"`
Data *ApplicationCommandInteractionData `json:"data"`
GuildID string `json:"guild_id"`
ChannelID string `json:"channel_id"`
Member *Member `json:"member"`
User *User `json:"user"`
Token string `json:"token"`
Version int `json:"version"`
}
// ApplicationCommandInteractionData https://discord.com/developers/docs/interactions/slash-commands#interaction-applicationcommandinteractiondata
type ApplicationCommandInteractionData struct {
ID string `json:"id"`
Name string `json:"name"`
Options []*ApplicationCommandInteractionDataOption `json:"options"`
}
// ApplicationCommandInteractionDataOption https://discord.com/developers/docs/interactions/slash-commands#interaction-applicationcommandinteractiondataoption
type ApplicationCommandInteractionDataOption struct {
Name string `json:"name"`
Value interface{} `json:"value"`
Options []*ApplicationCommandInteractionDataOption `json:"options"`
}
// ValueInt returns the int (forced from JSON float64) or 0
func (o ApplicationCommandInteractionDataOption) ValueInt() int {
if f, ok := o.Value.(float64); ok {
return int(f)
}
return 0
}
// ValueString returns the string or ""
func (o ApplicationCommandInteractionDataOption) ValueString() string {
if s, ok := o.Value.(string); ok {
return s
}
return ""
}
// ValueBool returns the boolean or false
func (o ApplicationCommandInteractionDataOption) ValueBool() bool {
if b, ok := o.Value.(bool); ok {
return b
}
return false
}
// Member https://discord.com/developers/docs/resources/guild#guild-member-object
type Member struct {
User *User `json:"user"`
Nick string `json:"nick"`
Roles []string `json:"roles"`
JoinedAt string `json:"joined_at"`
PremiumSince string `json:"premium_since"`
Deaf bool `json:"deaf"`
Mute bool `json:"mute"`
Pending bool `json:"pending"`
Permissions string `json:"permissions"`
}
// PremiumType (PT)
type PremiumType int
// https://discord.com/developers/docs/resources/user#user-object-premium-types
const (
NonePT PremiumType = iota
NitroClassic
Nitro
)
// UserFlags (UF)
type UserFlags int
// https://discord.com/developers/docs/resources/user#user-object-user-flags
const (
NoneUF UserFlags = 0
DiscordEmployeeUF UserFlags = 1 << iota
PartneredServerOwnerUF
HypeSquadEventsUF
BugHunterLevel1UF
_
_
HouseBraveryUF
HouseBrillianceUF
HouseBalanceUF
EarlySupporterUF
TeamUserUF
_
SystemUF
_
BugHunterLevel2UF
_
VerifiedBotUF
EarlyVerifiedBotDeveloperUF
)
// Has checks for a specific UserFlags
func (u UserFlags) Has(f UserFlags) bool {
return u&f != 0
}
// User https://discord.com/developers/docs/resources/user#user-object
type User struct {
ID string `json:"id"`
Username string `json:"username"`
Discriminator string `json:"discriminator"`
Avatar string `json:"avatar"`
Bot bool `json:"bot"`
System bool `json:"system"`
MFAEnabled bool `json:"mfa_enabled"`
Locale string `json:"locale"`
Verified bool `json:"verified"`
Email string `json:"email"`
Flags UserFlags `json:"flags"`
PremiumType PremiumType `json:"premium_type"`
PublicFlags UserFlags `json:"public_flags"`
}
// InteractionResponseType (IRT)
type InteractionResponseType int
// https://discord.com/developers/docs/interactions/slash-commands#interaction-response-interactionresponsetype
const (
PongIRT InteractionResponseType = iota + 1
_
_
ChannelMessageWithSourceIRT
DeferredChannelMessageWithSourceIRT
)
// InteractionResponse https://discord.com/developers/docs/interactions/slash-commands#interaction-response
type InteractionResponse struct {
Type InteractionResponseType `json:"type"`
Data *InteractionApplicationCommandCallbackData `json:"data"`
}
// CallbackFlags (CF)
type CallbackFlags int
const (
NoneCF CallbackFlags = 0
EphemeralCF CallbackFlags = 64
)
// InteractionApplicationCommandCallbackData https://discord.com/developers/docs/interactions/slash-commands#interaction-response-interactionapplicationcommandcallbackdata
type InteractionApplicationCommandCallbackData struct {
TTS bool `json:"tts"`
Content string `json:"content"`
Embeds []*embed.Embed `json:"embeds"`
AllowedMentions *embed.AllowedMentions `json:"allowed_mentions"`
Flags CallbackFlags `json:"flags"`
}
// WebhookEdit https://discord.com/developers/docs/resources/webhook#edit-webhook-message
type WebhookEdit struct {
Content string `json:"content"`
Embeds []*embed.Embed `json:"embeds"`
AllowedMentions *embed.AllowedMentions `json:"allowed_mentions"`
}
// Command is used when creating a Handler
//
// ID should map to a corresponding ApplicationCommand
// Handle is the func to be called whenever the Handler receives a request for the ApplicationCommand
type Command struct {
ID string
Handle CommandHandleFunc
}
// CommandHandleFunc is a func for Command.Handle
type CommandHandleFunc func(*Interaction) (*InteractionResponse, error)

23
webhook.go → webhook/webhook.go

@ -1,20 +1,29 @@
package disco
package webhook
import (
"bytes"
"context"
"encoding/json"
"net/http"
"go.jolheiser.com/disco/embed"
)
// Webhook is a Discord webhook
type Webhook struct {
Content string `json:"content"`
Username string `json:"username,omitempty"`
AvatarURL string `json:"avatar_url,omitempty"`
TTS bool `json:"tts,omitempty"`
Embeds []*Embed `json:"embeds,omitempty"`
AllowedMentions *AllowedMentions `json:"allowed_mentions,omitempty"`
Content string `json:"content"`
Username string `json:"username,omitempty"`
AvatarURL string `json:"avatar_url,omitempty"`
TTS bool `json:"tts,omitempty"`
Embeds []*embed.Embed `json:"embeds"`
AllowedMentions *embed.AllowedMentions `json:"allowed_mentions"`
}
// WebhookEdit https://discord.com/developers/docs/resources/webhook#edit-webhook-message
type WebhookEdit struct {
Content string `json:"content"`
Embeds []*embed.Embed `json:"embeds"`
AllowedMentions *embed.AllowedMentions `json:"allowed_mentions"`
}
// Request returns the Webhook as an *http.Request payload

24
webhook_test.go → webhook/webhook_test.go

@ -1,10 +1,14 @@
package disco
//+build e2e
package webhook
import (
"context"
"io"
"os"
"testing"
"go.jolheiser.com/disco/embed"
)
func TestMain(m *testing.M) {
@ -21,27 +25,27 @@ func TestWebhook(t *testing.T) {
Content: "@everyone Webhook Content! (No Ping)",
Username: "Disco",
AvatarURL: "https://gitea.com/user/avatar/jolheiser/-1",
Embeds: []*Embed{
Embeds: []*embed.Embed{
{
URL: "https://gitea.com/jolheiser/disco",
Type: TypeRich,
Type: embed.TypeRich,
Title: "Disco Webhook",
Description: "A webhook created with disco",
Timestamp: Now(),
Timestamp: embed.Now(),
Color: 0x007D96,
Author: &Author{
Author: &embed.Author{
Name: "jolheiser",
URL: "https://gitea.com/jolheiser",
IconURL: "https://gitea.com/user/avatar/jolheiser/-1",
},
Footer: &Footer{
Footer: &embed.Footer{
Text: "Webhook Footer",
IconURL: "https://gitea.com/user/avatar/jolheiser/-1",
},
Image: &Image{
Image: &embed.Image{
URL: "https://gitea.com/user/avatar/jolheiser/-1",
},
Fields: []*Field{
Fields: []*embed.Field{
{
Name: "Library",
Value: "Disco",
@ -55,8 +59,8 @@ func TestWebhook(t *testing.T) {
},
},
},
AllowedMentions: &AllowedMentions{
Parse: []Parse{Users},
AllowedMentions: &embed.AllowedMentions{
Parse: []embed.Parse{embed.Users},
},
}
Loading…
Cancel
Save