99 lines
2.9 KiB
Go
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{}}}})
|
|
}
|