Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Integrate handling of interactions via outgoing webhooks #1263

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
implement http interactions
  • Loading branch information
topi314 committed Oct 23, 2022
commit 1b5a1ccd06e890765048882b6bd1338803298c42
125 changes: 125 additions & 0 deletions http.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
package discordgo

import (
"encoding/json"
"errors"
"net/http"
"sync"
"time"
)

var (
// ErrInteractionExpired is returned when you try to reply to an interaction after 3s
ErrInteractionExpired = errors.New("interaction expired")

// ErrInteractionAlreadyRepliedTo is returned when you try to reply to an interaction multiple times
ErrInteractionAlreadyRepliedTo = errors.New("interaction was already replied to")
)

type replyStatus int

const (
replyStatusReplied replyStatus = iota + 1
replyStatusTimedOut
)

// ServeHTTP handles the heavy lifting of parsing the interaction request and sending the response
func (s *Session) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if !VerifyInteraction(r, s.PublicKey) {
http.Error(w, "Forbidden", http.StatusForbidden)
return
}

var i *InteractionCreate

if err := json.NewDecoder(r.Body).Decode(&i); err != nil {
http.Error(w, "Bad Request", http.StatusBadRequest)
}

// we can always respond to ping with pong
if i.Type == InteractionPing {
s.log(LogDebug, "received http ping")
if err := json.NewEncoder(w).Encode(InteractionResponse{
Type: InteractionResponsePong,
}); err != nil {
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
}
return
}

responseChannel := make(chan *InteractionResponse)
defer close(responseChannel)
errorChannel := make(chan error)
defer close(errorChannel)

var (
status replyStatus
mu sync.Mutex
)

s.httpInteractionsMu.Lock()
s.httpInteractions[i.Token] = func(response *InteractionResponse) error {
mu.Lock()
defer mu.Unlock()

if status == replyStatusTimedOut {
return ErrInteractionExpired
}

if status == replyStatusReplied {
return ErrInteractionAlreadyRepliedTo
}

status = replyStatusReplied
responseChannel <- response
s.httpInteractionsMu.Lock()
defer s.httpInteractionsMu.Unlock()
delete(s.httpInteractions, i.Token)
return <-errorChannel
}
s.httpInteractionsMu.Unlock()

go s.handleEvent(interactionCreateEventType, i)

var (
body []byte
contentType string
err error
)

// interactions can be replied to within 3 seconds, wait 4 to be safe
timer := time.NewTimer(time.Second * 4)
defer timer.Stop()
select {
case resp := <-responseChannel:
if resp.Data != nil && len(resp.Data.Files) > 0 {
contentType, body, err = MultipartBodyWithJSON(resp, resp.Data.Files)
} else {
contentType = "application/json"
body, err = json.Marshal(*resp)
}
if err != nil {
http.Error(w, "internal server error", http.StatusInternalServerError)
errorChannel <- err
return
}

case <-timer.C:
mu.Lock()
defer mu.Unlock()
status = replyStatusTimedOut
s.httpInteractionsMu.Lock()
defer s.httpInteractionsMu.Unlock()
delete(s.httpInteractions, i.Token)

s.log(LogWarning, "interaction timed out")

http.Error(w, "interaction timed out", http.StatusRequestTimeout)
return
}

w.Header().Set("Content-Type", contentType)
if _, err = w.Write(body); err != nil {
errorChannel <- err
}
}
6 changes: 6 additions & 0 deletions restapi.go
Original file line number Diff line number Diff line change
Expand Up @@ -2945,6 +2945,12 @@ func (s *Session) ApplicationCommandPermissionsBatchEdit(appID, guildID string,
// interaction : Interaction instance.
// resp : Response message data.
func (s *Session) InteractionRespond(interaction *Interaction, resp *InteractionResponse) error {
s.httpInteractionsMu.Lock()
defer s.httpInteractionsMu.Unlock()
if respondFunc, ok := s.httpInteractions[interaction.ID]; ok {
return respondFunc(resp)
}

endpoint := EndpointInteractionResponse(interaction.ID, interaction.Token)

if resp.Data != nil && len(resp.Data.Files) > 0 {
Expand Down
6 changes: 6 additions & 0 deletions structs.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
package discordgo

import (
"crypto/ed25519"
"encoding/json"
"fmt"
"math"
Expand Down Expand Up @@ -115,6 +116,9 @@ type Session struct {
handlers map[string][]*eventHandlerInstance
onceHandlers map[string][]*eventHandlerInstance

httpInteractions map[string]func(response *InteractionResponse) error
httpInteractionsMu sync.Mutex
FedorLap2006 marked this conversation as resolved.
Show resolved Hide resolved

// The websocket connection.
wsConn *websocket.Conn

Expand All @@ -132,6 +136,8 @@ type Session struct {

// used to make sure gateway websocket writes do not happen concurrently
wsMutex sync.Mutex

PublicKey ed25519.PublicKey
}

// Application stores values for a Discord Application
Expand Down