Files
notify-gateway/internal/httpserver/discord.go
T
2026-09-16 06:26:16 +02:00

99 lines
2.9 KiB
Go

package httpserver
import (
"context"
"crypto/ed25519"
"encoding/hex"
"encoding/json"
"io"
"net/http"
"slices"
"strconv"
"time"
"github.com/example/notify-gateway/internal/model"
)
func (s *Server) discord(w http.ResponseWriter, r *http.Request) {
c := s.store.Get().Ingress.Discord
if !c.Enabled {
http.NotFound(w, r)
return
}
body, err := io.ReadAll(io.LimitReader(r.Body, (1<<20)+1))
if err != nil || len(body) > 1<<20 {
http.Error(w, "invalid request", 400)
return
}
stamp := r.Header.Get("X-Signature-Timestamp")
ts, err := strconv.ParseInt(stamp, 10, 64)
key, e1 := hex.DecodeString(c.PublicKey)
sig, e2 := hex.DecodeString(r.Header.Get("X-Signature-Ed25519"))
if err != nil || e1 != nil || e2 != nil || len(key) != ed25519.PublicKeySize || ts < time.Now().Add(-5*time.Minute).Unix() || ts > time.Now().Add(5*time.Minute).Unix() || !ed25519.Verify(key, append([]byte(stamp), body...), sig) {
http.Error(w, "invalid signature", 401)
return
}
var in struct {
ID string `json:"id"`
ApplicationID string `json:"application_id"`
Type int `json:"type"`
GuildID string `json:"guild_id"`
ChannelID string `json:"channel_id"`
Data struct {
Name string `json:"name"`
Type int `json:"type"`
Options []struct {
Name string `json:"name"`
Type int `json:"type"`
Value json.RawMessage `json:"value"`
} `json:"options"`
} `json:"data"`
}
if json.Unmarshal(body, &in) != nil || in.ApplicationID != c.ApplicationID {
http.Error(w, "invalid interaction", 400)
return
}
if in.Type == 1 {
writeJSON(w, 200, map[string]int{"type": 1})
return
}
if in.Type != 2 || in.Data.Type != 1 || in.Data.Name != c.Command || in.ID == "" || !slices.Contains(c.GuildIDs, in.GuildID) || !slices.Contains(c.ChannelIDs, in.ChannelID) {
http.Error(w, "interaction not allowed", 403)
return
}
msg := model.InboundMessage{Source: "discord", Channel: in.ChannelID, ReceivedAt: time.Now().UTC()}
for _, o := range in.Data.Options {
switch o.Name {
case "title":
err = json.Unmarshal(o.Value, &msg.Title)
case "message":
err = json.Unmarshal(o.Value, &msg.Message)
case "priority":
err = json.Unmarshal(o.Value, &msg.Priority)
default:
http.Error(w, "unsupported option", 400)
return
}
if err != nil {
http.Error(w, "invalid option", 400)
return
}
}
if msg.Message == "" {
http.Error(w, "message required", 400)
return
}
if s.queue == nil {
http.Error(w, "outbox unavailable", 503)
return
}
ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second)
defer cancel()
receipt, err := s.queue.Accept(ctx, msg, "discord:"+in.ID)
if err != nil {
http.Error(w, "interaction could not be queued; check routing", 503)
return
}
writeJSON(w, 200, map[string]any{"type": 4, "data": map[string]any{"content": "Meldung angenommen. Referenz: " + receipt.ID, "flags": 64, "allowed_mentions": map[string]any{"parse": []string{}}}})
}