All checks were successful
CI / changes (push) Successful in 19s
CI / test (push) Successful in 24s
CI / build-ai-gateway (push) Successful in 1m6s
CI / build-ha-gateway (push) Successful in 1m3s
CI / build-discord-bot (push) Successful in 1m4s
CI / build-alexa-bridge (push) Successful in 1m15s
CI / build-tts-gateway (push) Successful in 1m5s
CI / build-tts-sidecar (push) Has been skipped
CI / build-tts-model (push) Has been skipped
- Implemented SetTemperature in ClimateService for setting an absolute target temperature. - Updated ClimateServiceClient and ClimateServiceServer interfaces to include SetTemperature. - Added corresponding handler and tests for SetTemperature in ClimateGRPC. - Modified ClimateApp to handle SetTemperature requests without clamping. - Updated climate.proto to define SetTemperatureRequest message. - Adjusted Dockerfiles to include alexa-bridge dependencies.
81 lines
2.3 KiB
Go
81 lines
2.3 KiB
Go
package alexa
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"io"
|
|
"net/http"
|
|
|
|
"gitea.nik4nao.com/nik/home-services/alexa-bridge/internal/logger"
|
|
)
|
|
|
|
// Dispatcher handles one already-validated Alexa request and produces a
|
|
// response. internal/directive.Router implements this.
|
|
type Dispatcher interface {
|
|
Dispatch(ctx context.Context, req Request) (Response, error)
|
|
}
|
|
|
|
// Handler is the HTTP entrypoint Alexa's servers call into: it verifies the
|
|
// request signature, timestamp, and skill ID (in that order — cheapest and
|
|
// most identity-establishing checks first), then hands the decoded envelope
|
|
// to a Dispatcher.
|
|
type Handler struct {
|
|
validator Validator
|
|
skillID string
|
|
dispatcher Dispatcher
|
|
}
|
|
|
|
// NewHandler constructs the HTTP handler for the Alexa endpoint.
|
|
func NewHandler(validator Validator, skillID string, dispatcher Dispatcher) *Handler {
|
|
return &Handler{validator: validator, skillID: skillID, dispatcher: dispatcher}
|
|
}
|
|
|
|
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
log := logger.FromContext(ctx)
|
|
|
|
body, err := io.ReadAll(r.Body)
|
|
if err != nil {
|
|
log.Warn("read request body failed", "err", err)
|
|
http.Error(w, "bad request", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
if err := h.validator.Validate(ctx, r, body); err != nil {
|
|
log.Warn("signature validation failed", "err", err)
|
|
http.Error(w, "signature verification failed", http.StatusUnauthorized)
|
|
return
|
|
}
|
|
|
|
var req Request
|
|
if err := json.Unmarshal(body, &req); err != nil {
|
|
log.Warn("decode request body failed", "err", err)
|
|
http.Error(w, "bad request", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
if err := ValidateTimestamp(req.Request.Timestamp); err != nil {
|
|
log.Warn("timestamp validation failed", "err", err)
|
|
http.Error(w, "request timestamp outside tolerance", http.StatusUnauthorized)
|
|
return
|
|
}
|
|
|
|
if err := ValidateApplicationID(&req, h.skillID); err != nil {
|
|
log.Warn("application id validation failed", "err", err)
|
|
http.Error(w, "application id mismatch", http.StatusUnauthorized)
|
|
return
|
|
}
|
|
|
|
resp, err := h.dispatcher.Dispatch(ctx, req)
|
|
if err != nil {
|
|
log.Error("dispatch failed", "err", err)
|
|
http.Error(w, "internal error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
|
log.Error("encode response failed", "err", err)
|
|
}
|
|
}
|