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{}}}}) }