wow that took awhile
This commit is contained in:
174
bridge/jackbox/webhook.go
Normal file
174
bridge/jackbox/webhook.go
Normal file
@@ -0,0 +1,174 @@
|
||||
package jackbox
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"crypto/subtle"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// WebhookServer handles incoming webhooks from the Jackbox API
|
||||
type WebhookServer struct {
|
||||
port int
|
||||
secret string
|
||||
messageCallback func(string) // Callback to broadcast messages
|
||||
log *logrus.Entry
|
||||
server *http.Server
|
||||
}
|
||||
|
||||
// WebhookPayload represents the webhook event from the API
|
||||
type WebhookPayload struct {
|
||||
Event string `json:"event"`
|
||||
Timestamp string `json:"timestamp"`
|
||||
Data struct {
|
||||
Session struct {
|
||||
ID int `json:"id"`
|
||||
IsActive bool `json:"is_active"`
|
||||
GamesPlayed int `json:"games_played"`
|
||||
} `json:"session"`
|
||||
Game struct {
|
||||
ID int `json:"id"`
|
||||
Title string `json:"title"`
|
||||
PackName string `json:"pack_name"`
|
||||
MinPlayers int `json:"min_players"`
|
||||
MaxPlayers int `json:"max_players"`
|
||||
ManuallyAdded bool `json:"manually_added"`
|
||||
} `json:"game"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
// NewWebhookServer creates a new webhook server
|
||||
func NewWebhookServer(port int, secret string, messageCallback func(string), log *logrus.Entry) *WebhookServer {
|
||||
return &WebhookServer{
|
||||
port: port,
|
||||
secret: secret,
|
||||
messageCallback: messageCallback,
|
||||
log: log,
|
||||
}
|
||||
}
|
||||
|
||||
// Start starts the webhook HTTP server
|
||||
func (w *WebhookServer) Start() error {
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/webhook/jackbox", w.handleWebhook)
|
||||
|
||||
// Health check endpoint
|
||||
mux.HandleFunc("/health", func(rw http.ResponseWriter, r *http.Request) {
|
||||
rw.WriteHeader(http.StatusOK)
|
||||
rw.Write([]byte("OK"))
|
||||
})
|
||||
|
||||
w.server = &http.Server{
|
||||
Addr: fmt.Sprintf(":%d", w.port),
|
||||
Handler: mux,
|
||||
}
|
||||
|
||||
w.log.Infof("Starting Jackbox webhook server on port %d", w.port)
|
||||
|
||||
go func() {
|
||||
if err := w.server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||
w.log.Errorf("Webhook server error: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop stops the webhook server
|
||||
func (w *WebhookServer) Stop() error {
|
||||
if w.server != nil {
|
||||
w.log.Info("Stopping Jackbox webhook server")
|
||||
return w.server.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// handleWebhook processes incoming webhook requests
|
||||
func (w *WebhookServer) handleWebhook(rw http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(rw, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
// Read the raw body for signature verification
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
w.log.Errorf("Failed to read webhook body: %v", err)
|
||||
http.Error(rw, "Failed to read body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
defer r.Body.Close()
|
||||
|
||||
// Verify signature
|
||||
signature := r.Header.Get("X-Webhook-Signature")
|
||||
if !w.verifySignature(signature, body) {
|
||||
w.log.Warn("Webhook signature verification failed")
|
||||
http.Error(rw, "Invalid signature", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
// Parse the webhook payload
|
||||
var payload WebhookPayload
|
||||
if err := json.Unmarshal(body, &payload); err != nil {
|
||||
w.log.Errorf("Failed to parse webhook payload: %v", err)
|
||||
http.Error(rw, "Invalid payload", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Handle the event
|
||||
w.handleEvent(&payload)
|
||||
|
||||
// Always respond with 200 OK
|
||||
rw.WriteHeader(http.StatusOK)
|
||||
rw.Write([]byte("OK"))
|
||||
}
|
||||
|
||||
// verifySignature verifies the HMAC-SHA256 signature of the webhook
|
||||
func (w *WebhookServer) verifySignature(signature string, body []byte) bool {
|
||||
if signature == "" || len(signature) < 7 || signature[:7] != "sha256=" {
|
||||
return false
|
||||
}
|
||||
|
||||
// Extract the hex signature (after "sha256=")
|
||||
receivedSig := signature[7:]
|
||||
|
||||
// Compute expected signature
|
||||
mac := hmac.New(sha256.New, []byte(w.secret))
|
||||
mac.Write(body)
|
||||
expectedSig := hex.EncodeToString(mac.Sum(nil))
|
||||
|
||||
// Timing-safe comparison
|
||||
return subtle.ConstantTimeCompare([]byte(receivedSig), []byte(expectedSig)) == 1
|
||||
}
|
||||
|
||||
// handleEvent processes webhook events
|
||||
func (w *WebhookServer) handleEvent(payload *WebhookPayload) {
|
||||
switch payload.Event {
|
||||
case "game.added":
|
||||
w.handleGameAdded(payload)
|
||||
default:
|
||||
w.log.Debugf("Unhandled webhook event: %s", payload.Event)
|
||||
}
|
||||
}
|
||||
|
||||
// handleGameAdded handles the game.added event
|
||||
func (w *WebhookServer) handleGameAdded(payload *WebhookPayload) {
|
||||
game := payload.Data.Game
|
||||
|
||||
w.log.Infof("Game added: %s from %s", game.Title, game.PackName)
|
||||
|
||||
// Format the announcement message
|
||||
message := fmt.Sprintf("🎮 Coming up next: %s!", game.Title)
|
||||
|
||||
// Broadcast to both chats via callback
|
||||
if w.messageCallback != nil {
|
||||
w.messageCallback(message)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user