feat: add alert-bridge service for external cronjob -> Discord alerts
All checks were successful
CI / changes (push) Successful in 19s
CI / test (push) Successful in 31s
CI / build-ai-gateway (push) Successful in 1m8s
CI / build-ha-gateway (push) Successful in 1m19s
CI / build-discord-bot (push) Successful in 59s
CI / build-alexa-bridge (push) Successful in 1m8s
CI / build-alert-bridge (push) Successful in 1m17s
CI / build-tts-gateway (push) Successful in 1m10s
CI / build-tts-sidecar (push) Has been skipped
CI / build-tts-model (push) Has been skipped
All checks were successful
CI / changes (push) Successful in 19s
CI / test (push) Successful in 31s
CI / build-ai-gateway (push) Successful in 1m8s
CI / build-ha-gateway (push) Successful in 1m19s
CI / build-discord-bot (push) Successful in 59s
CI / build-alexa-bridge (push) Successful in 1m8s
CI / build-alert-bridge (push) Successful in 1m17s
CI / build-tts-gateway (push) Successful in 1m10s
CI / build-tts-sidecar (push) Has been skipped
CI / build-tts-model (push) Has been skipped
New standalone service that receives authenticated HTTP POSTs (e.g. from a cronjob elsewhere on the home network) and posts them to a Discord channel via an Incoming Webhook. Fire-and-forget, no dependency on discord-bot's bot session at all - mirrors alexa-bridge's precedent of a small bridge service with its own HTTP listener and auth rather than adding an inbound edge to an existing internal service. Wires alert-bridge into go.work, CI (changes filter, test job, new build-alert-bridge job), and the other five Dockerfiles' COPY lines. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
8f7024edfa
commit
39b7d6d4b4
@ -29,6 +29,7 @@ jobs:
|
|||||||
ha-gateway: ${{ steps.filter.outputs.ha-gateway }}
|
ha-gateway: ${{ steps.filter.outputs.ha-gateway }}
|
||||||
discord-bot: ${{ steps.filter.outputs.discord-bot }}
|
discord-bot: ${{ steps.filter.outputs.discord-bot }}
|
||||||
alexa-bridge: ${{ steps.filter.outputs.alexa-bridge }}
|
alexa-bridge: ${{ steps.filter.outputs.alexa-bridge }}
|
||||||
|
alert-bridge: ${{ steps.filter.outputs.alert-bridge }}
|
||||||
tts-gateway: ${{ steps.filter.outputs.tts-gateway }}
|
tts-gateway: ${{ steps.filter.outputs.tts-gateway }}
|
||||||
tts-sidecar: ${{ steps.filter.outputs.tts-sidecar }}
|
tts-sidecar: ${{ steps.filter.outputs.tts-sidecar }}
|
||||||
tts-model: ${{ steps.filter.outputs.tts-model }}
|
tts-model: ${{ steps.filter.outputs.tts-model }}
|
||||||
@ -65,6 +66,11 @@ jobs:
|
|||||||
- 'go.work'
|
- 'go.work'
|
||||||
- 'go.work.sum'
|
- 'go.work.sum'
|
||||||
- 'alexa-bridge/**'
|
- 'alexa-bridge/**'
|
||||||
|
alert-bridge:
|
||||||
|
- 'gen/**'
|
||||||
|
- 'go.work'
|
||||||
|
- 'go.work.sum'
|
||||||
|
- 'alert-bridge/**'
|
||||||
tts-gateway:
|
tts-gateway:
|
||||||
- 'gen/**'
|
- 'gen/**'
|
||||||
- 'go.work'
|
- 'go.work'
|
||||||
@ -96,6 +102,7 @@ jobs:
|
|||||||
ha-gateway/go.sum
|
ha-gateway/go.sum
|
||||||
discord-bot/go.sum
|
discord-bot/go.sum
|
||||||
alexa-bridge/go.sum
|
alexa-bridge/go.sum
|
||||||
|
alert-bridge/go.sum
|
||||||
tts-gateway/go.sum
|
tts-gateway/go.sum
|
||||||
gen/go.sum
|
gen/go.sum
|
||||||
|
|
||||||
@ -106,6 +113,7 @@ jobs:
|
|||||||
cd ../ha-gateway && go vet ./...
|
cd ../ha-gateway && go vet ./...
|
||||||
cd ../discord-bot && go vet ./...
|
cd ../discord-bot && go vet ./...
|
||||||
cd ../alexa-bridge && go vet ./...
|
cd ../alexa-bridge && go vet ./...
|
||||||
|
cd ../alert-bridge && go vet ./...
|
||||||
cd ../tts-gateway && go vet ./...
|
cd ../tts-gateway && go vet ./...
|
||||||
|
|
||||||
- name: go test
|
- name: go test
|
||||||
@ -115,6 +123,7 @@ jobs:
|
|||||||
cd ../ha-gateway && go test ./...
|
cd ../ha-gateway && go test ./...
|
||||||
cd ../discord-bot && go test ./...
|
cd ../discord-bot && go test ./...
|
||||||
cd ../alexa-bridge && go test ./...
|
cd ../alexa-bridge && go test ./...
|
||||||
|
cd ../alert-bridge && go test ./...
|
||||||
cd ../tts-gateway && go test ./...
|
cd ../tts-gateway && go test ./...
|
||||||
|
|
||||||
build-ai-gateway:
|
build-ai-gateway:
|
||||||
@ -225,6 +234,33 @@ jobs:
|
|||||||
${{ env.IMAGE_PREFIX }}/alexa-bridge:${{ github.sha }}
|
${{ env.IMAGE_PREFIX }}/alexa-bridge:${{ github.sha }}
|
||||||
${{ env.IMAGE_PREFIX }}/alexa-bridge:latest
|
${{ env.IMAGE_PREFIX }}/alexa-bridge:latest
|
||||||
|
|
||||||
|
build-alert-bridge:
|
||||||
|
needs: [test, changes]
|
||||||
|
if: github.ref == 'refs/heads/main' && needs.changes.outputs.alert-bridge == 'true'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- uses: docker/setup-buildx-action@v4
|
||||||
|
|
||||||
|
- uses: docker/login-action@v4
|
||||||
|
with:
|
||||||
|
registry: ${{ env.REGISTRY }}
|
||||||
|
username: ${{ secrets.REGISTRY_USER }}
|
||||||
|
password: ${{ secrets.REGISTRY_PASSWORD }}
|
||||||
|
|
||||||
|
- uses: docker/build-push-action@v7
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: alert-bridge/Dockerfile
|
||||||
|
push: true
|
||||||
|
platforms: linux/amd64
|
||||||
|
cache-from: type=registry,ref=${{ env.IMAGE_PREFIX }}/alert-bridge:buildcache
|
||||||
|
cache-to: type=registry,ref=${{ env.IMAGE_PREFIX }}/alert-bridge:buildcache,mode=max
|
||||||
|
tags: |
|
||||||
|
${{ env.IMAGE_PREFIX }}/alert-bridge:${{ github.sha }}
|
||||||
|
${{ env.IMAGE_PREFIX }}/alert-bridge:latest
|
||||||
|
|
||||||
build-tts-gateway:
|
build-tts-gateway:
|
||||||
needs: [test, changes]
|
needs: [test, changes]
|
||||||
if: github.ref == 'refs/heads/main' && needs.changes.outputs.tts-gateway == 'true'
|
if: github.ref == 'refs/heads/main' && needs.changes.outputs.tts-gateway == 'true'
|
||||||
|
|||||||
@ -10,6 +10,7 @@ COPY ha-gateway/go.mod ha-gateway/go.sum ./ha-gateway/
|
|||||||
COPY discord-bot/go.mod discord-bot/go.sum ./discord-bot/
|
COPY discord-bot/go.mod discord-bot/go.sum ./discord-bot/
|
||||||
COPY tts-gateway/go.mod tts-gateway/go.sum ./tts-gateway/
|
COPY tts-gateway/go.mod tts-gateway/go.sum ./tts-gateway/
|
||||||
COPY alexa-bridge/go.mod alexa-bridge/go.sum ./alexa-bridge/
|
COPY alexa-bridge/go.mod alexa-bridge/go.sum ./alexa-bridge/
|
||||||
|
COPY alert-bridge/go.mod alert-bridge/go.sum ./alert-bridge/
|
||||||
COPY ai-gateway/go.mod ai-gateway/go.sum ./ai-gateway/
|
COPY ai-gateway/go.mod ai-gateway/go.sum ./ai-gateway/
|
||||||
|
|
||||||
WORKDIR /workspace/ai-gateway
|
WORKDIR /workspace/ai-gateway
|
||||||
@ -21,6 +22,7 @@ COPY ha-gateway/ ./ha-gateway/
|
|||||||
COPY discord-bot/ ./discord-bot/
|
COPY discord-bot/ ./discord-bot/
|
||||||
COPY tts-gateway/ ./tts-gateway/
|
COPY tts-gateway/ ./tts-gateway/
|
||||||
COPY alexa-bridge/ ./alexa-bridge/
|
COPY alexa-bridge/ ./alexa-bridge/
|
||||||
|
COPY alert-bridge/ ./alert-bridge/
|
||||||
COPY ai-gateway/ ./ai-gateway/
|
COPY ai-gateway/ ./ai-gateway/
|
||||||
|
|
||||||
WORKDIR /workspace/ai-gateway
|
WORKDIR /workspace/ai-gateway
|
||||||
|
|||||||
7
alert-bridge/.env.example
Normal file
7
alert-bridge/.env.example
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
HTTP_PORT=8080
|
||||||
|
API_KEY=change-me-to-a-random-shared-secret
|
||||||
|
DISCORD_WEBHOOK_URL=https://discord.com/api/webhooks/your-webhook-id/your-webhook-token
|
||||||
|
MENTION_USER_ID=
|
||||||
|
OTEL_ENDPOINT=
|
||||||
|
LOG_LEVEL=info
|
||||||
|
LOG_FORMAT=text
|
||||||
37
alert-bridge/Dockerfile
Normal file
37
alert-bridge/Dockerfile
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
FROM golang:1.26-alpine AS builder
|
||||||
|
WORKDIR /workspace
|
||||||
|
|
||||||
|
COPY go.work go.work.sum ./
|
||||||
|
|
||||||
|
# Manifests only, copied first, so `go mod download` gets its own cache layer
|
||||||
|
# invalidated only by dependency changes - not by every source edit below.
|
||||||
|
COPY gen/go.mod gen/go.sum ./gen/
|
||||||
|
COPY ha-gateway/go.mod ha-gateway/go.sum ./ha-gateway/
|
||||||
|
COPY ai-gateway/go.mod ai-gateway/go.sum ./ai-gateway/
|
||||||
|
COPY discord-bot/go.mod discord-bot/go.sum ./discord-bot/
|
||||||
|
COPY tts-gateway/go.mod tts-gateway/go.sum ./tts-gateway/
|
||||||
|
COPY alexa-bridge/go.mod alexa-bridge/go.sum ./alexa-bridge/
|
||||||
|
COPY alert-bridge/go.mod alert-bridge/go.sum ./alert-bridge/
|
||||||
|
|
||||||
|
WORKDIR /workspace/alert-bridge
|
||||||
|
RUN go mod download
|
||||||
|
|
||||||
|
WORKDIR /workspace
|
||||||
|
COPY gen/ ./gen/
|
||||||
|
COPY ha-gateway/ ./ha-gateway/
|
||||||
|
COPY ai-gateway/ ./ai-gateway/
|
||||||
|
COPY discord-bot/ ./discord-bot/
|
||||||
|
COPY tts-gateway/ ./tts-gateway/
|
||||||
|
COPY alexa-bridge/ ./alexa-bridge/
|
||||||
|
COPY alert-bridge/ ./alert-bridge/
|
||||||
|
|
||||||
|
WORKDIR /workspace/alert-bridge
|
||||||
|
ARG VERSION=dev
|
||||||
|
RUN CGO_ENABLED=0 GOOS=linux go build \
|
||||||
|
-ldflags="-s -w -X main.version=${VERSION}" \
|
||||||
|
-o /alert-bridge ./cmd/bridge
|
||||||
|
|
||||||
|
FROM gcr.io/distroless/static:nonroot
|
||||||
|
COPY --from=builder /alert-bridge /alert-bridge
|
||||||
|
EXPOSE 8080
|
||||||
|
ENTRYPOINT ["/alert-bridge"]
|
||||||
114
alert-bridge/README.md
Normal file
114
alert-bridge/README.md
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
# alert-bridge
|
||||||
|
|
||||||
|
`alert-bridge` turns an authenticated HTTP POST from an external caller (e.g.
|
||||||
|
a cronjob running elsewhere on the home network) into a message posted to a
|
||||||
|
Discord channel. It is fire-and-forget: each request becomes one new Discord
|
||||||
|
message, there is no editing or reacting to prior messages.
|
||||||
|
|
||||||
|
Unlike `ha-gateway`/`ai-gateway`/`discord-bot`/`tts-gateway`, it has no
|
||||||
|
dependency on any other service in this repo — it posts directly to a Discord
|
||||||
|
[Incoming Webhook](https://discord.com/developers/docs/resources/webhook), not
|
||||||
|
through `discord-bot`'s bot session. That keeps the whole flow to one hop and
|
||||||
|
means `discord-bot` needed zero changes to support this feature.
|
||||||
|
|
||||||
|
## Runtime Flow
|
||||||
|
|
||||||
|
1. The process loads `.env`, configures logging/telemetry, and starts serving
|
||||||
|
HTTP on `HTTP_PORT`.
|
||||||
|
2. A caller `POST`s to `/alerts` with a bearer token and a JSON body.
|
||||||
|
3. The handler checks the bearer token (constant-time compare against
|
||||||
|
`API_KEY`), validates the body, and defaults `level` to `info` if omitted.
|
||||||
|
4. The alert is formatted as a Discord embed (color-coded by level) and POSTed
|
||||||
|
to `DISCORD_WEBHOOK_URL`. `level=error` alerts additionally prefix the
|
||||||
|
message with a mention of `MENTION_USER_ID`, since embeds alone never
|
||||||
|
trigger a Discord ping.
|
||||||
|
|
||||||
|
## HTTP API
|
||||||
|
|
||||||
|
- `POST /alerts` — requires `Authorization: Bearer <API_KEY>`.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"source": "ba-cronjob", "message": "Finished with 0 errors", "level": "info"}
|
||||||
|
```
|
||||||
|
|
||||||
|
- `source`, `message`: required.
|
||||||
|
- `level`: optional, one of `info` | `warn` | `error`, defaults to `info`.
|
||||||
|
- Responses: `202` delivered, `400` bad request, `401` bad/missing token,
|
||||||
|
`502` the Discord webhook call itself failed.
|
||||||
|
|
||||||
|
- `GET /healthz`: unauthenticated liveness/readiness check, always `200 OK`.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Environment variables:
|
||||||
|
|
||||||
|
| Variable | Default | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `HTTP_PORT` | `8080` | HTTP listen port |
|
||||||
|
| `API_KEY` | required | Shared secret callers must present as `Authorization: Bearer <API_KEY>` |
|
||||||
|
| `DISCORD_WEBHOOK_URL` | required | Target channel's Discord Incoming Webhook URL |
|
||||||
|
| `MENTION_USER_ID` | empty | Discord user ID mentioned on `level=error` alerts; leave empty to never mention |
|
||||||
|
| `OTEL_ENDPOINT` | empty | OTLP gRPC collector endpoint; empty disables telemetry |
|
||||||
|
| `LOG_LEVEL` | `info` | `debug`, `info`, `warn`, or `error` |
|
||||||
|
| `LOG_FORMAT` | `json` | `json` or `text` |
|
||||||
|
|
||||||
|
Example env file: [.env.example](.env.example)
|
||||||
|
|
||||||
|
## Local Run
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd alert-bridge
|
||||||
|
cp .env.example .env
|
||||||
|
# edit .env: set API_KEY and DISCORD_WEBHOOK_URL
|
||||||
|
go run ./cmd/bridge
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:8080/alerts \
|
||||||
|
-H "Authorization: Bearer $API_KEY" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"source":"ba-cronjob","message":"Started","level":"info"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Test And Build
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go test ./...
|
||||||
|
go build ./...
|
||||||
|
```
|
||||||
|
|
||||||
|
Build the container image from the workspace root:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker build -f alert-bridge/Dockerfile -t alert-bridge:dev .
|
||||||
|
```
|
||||||
|
|
||||||
|
## Package Map
|
||||||
|
|
||||||
|
```text
|
||||||
|
cmd/bridge/ # process entrypoint and wiring
|
||||||
|
internal/adapters/primary/http/ # POST /alerts handler: auth, decode, validate
|
||||||
|
internal/adapters/secondary/discordwebhook/ # builds the embed payload, posts to the Discord webhook
|
||||||
|
internal/app/ # thin orchestration between the HTTP handler and the Notifier
|
||||||
|
internal/core/domain/ # Alert{Source, Message, Level}
|
||||||
|
internal/core/ports/driven/ # Notifier interface
|
||||||
|
internal/config/ # environment loading
|
||||||
|
internal/logger/ # slog setup
|
||||||
|
internal/telemetry/ # OpenTelemetry setup
|
||||||
|
```
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
Kubernetes manifests (Deployment/Service, and an internal-CA `IngressRoute` at
|
||||||
|
a `*.home.arpa` hostname so LAN callers outside the cluster can reach it) live
|
||||||
|
in the separate `homelab` repo, not here — not yet added as of this writing.
|
||||||
|
|
||||||
|
## Limitations
|
||||||
|
|
||||||
|
- Single channel/webhook only — no per-source routing to multiple channels.
|
||||||
|
- No rate limiting of its own; relies on Discord's own webhook rate limits
|
||||||
|
(5 requests/2s per webhook), which is more than enough for cronjob-scale
|
||||||
|
notification volume.
|
||||||
|
- Fire-and-forget only: no message editing, threading, or reaction handling.
|
||||||
|
That would require routing through `discord-bot`'s live bot session instead
|
||||||
|
of a plain webhook — not needed for the current use case.
|
||||||
93
alert-bridge/cmd/bridge/main.go
Normal file
93
alert-bridge/cmd/bridge/main.go
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log/slog"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/joho/godotenv"
|
||||||
|
|
||||||
|
httpadapter "gitea.nik4nao.com/nik/home-services/alert-bridge/internal/adapters/primary/http"
|
||||||
|
"gitea.nik4nao.com/nik/home-services/alert-bridge/internal/adapters/secondary/discordwebhook"
|
||||||
|
"gitea.nik4nao.com/nik/home-services/alert-bridge/internal/app"
|
||||||
|
"gitea.nik4nao.com/nik/home-services/alert-bridge/internal/config"
|
||||||
|
"gitea.nik4nao.com/nik/home-services/alert-bridge/internal/logger"
|
||||||
|
"gitea.nik4nao.com/nik/home-services/alert-bridge/internal/telemetry"
|
||||||
|
)
|
||||||
|
|
||||||
|
var version = "dev"
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
_ = godotenv.Load()
|
||||||
|
|
||||||
|
cfg, err := config.Load()
|
||||||
|
if err != nil {
|
||||||
|
os.Stderr.WriteString("config error: " + err.Error() + "\n")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
log := logger.New(cfg.LogFormat, cfg.LogLevel)
|
||||||
|
slog.SetDefault(log)
|
||||||
|
log.Info("starting alert-bridge",
|
||||||
|
"version", version,
|
||||||
|
"http_port", cfg.HTTPPort,
|
||||||
|
"log_level", cfg.LogLevel,
|
||||||
|
"log_format", cfg.LogFormat,
|
||||||
|
)
|
||||||
|
|
||||||
|
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
defer stop()
|
||||||
|
ctx = logger.WithLogger(ctx, log)
|
||||||
|
|
||||||
|
shutdown, err := telemetry.Setup(ctx, "alert-bridge", version, cfg)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("telemetry setup failed", "err", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
if cfg.OTELEndpoint != "" {
|
||||||
|
log.Info("telemetry enabled", "endpoint", cfg.OTELEndpoint)
|
||||||
|
} else {
|
||||||
|
log.Debug("telemetry disabled")
|
||||||
|
}
|
||||||
|
|
||||||
|
notifier := discordwebhook.New(cfg.DiscordWebhookURL, cfg.MentionUserID)
|
||||||
|
alertApp := app.NewAlertApp(notifier)
|
||||||
|
alertHandler := httpadapter.NewHandler(alertApp, cfg.APIKey)
|
||||||
|
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
mux.Handle("POST /alerts", alertHandler)
|
||||||
|
mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
})
|
||||||
|
|
||||||
|
srv := &http.Server{
|
||||||
|
Addr: ":" + cfg.HTTPPort,
|
||||||
|
Handler: mux,
|
||||||
|
ReadHeaderTimeout: 5 * time.Second,
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
log.Info("alert-bridge listening", "addr", srv.Addr)
|
||||||
|
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||||
|
log.Error("serve failed", "err", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
<-ctx.Done()
|
||||||
|
log.Info("shutdown signal received, draining")
|
||||||
|
|
||||||
|
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
if err := srv.Shutdown(shutdownCtx); err != nil {
|
||||||
|
log.Error("http server shutdown failed", "err", err)
|
||||||
|
}
|
||||||
|
log.Info("shutdown complete")
|
||||||
|
|
||||||
|
if err := shutdown(context.Background()); err != nil {
|
||||||
|
log.Error("telemetry shutdown error", "err", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
33
alert-bridge/go.mod
Normal file
33
alert-bridge/go.mod
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
module gitea.nik4nao.com/nik/home-services/alert-bridge
|
||||||
|
|
||||||
|
go 1.26
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/joho/godotenv v1.5.1
|
||||||
|
go.opentelemetry.io/otel v1.39.0
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.35.0
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0
|
||||||
|
go.opentelemetry.io/otel/metric v1.39.0
|
||||||
|
go.opentelemetry.io/otel/sdk v1.39.0
|
||||||
|
go.opentelemetry.io/otel/sdk/metric v1.39.0
|
||||||
|
go.opentelemetry.io/otel/trace v1.39.0
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
|
github.com/go-logr/logr v1.4.3 // indirect
|
||||||
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 // indirect
|
||||||
|
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 // indirect
|
||||||
|
go.opentelemetry.io/proto/otlp v1.5.0 // indirect
|
||||||
|
golang.org/x/net v0.35.0 // indirect
|
||||||
|
golang.org/x/sys v0.39.0 // indirect
|
||||||
|
golang.org/x/text v0.22.0 // indirect
|
||||||
|
google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a // indirect
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a // indirect
|
||||||
|
google.golang.org/grpc v1.71.0 // indirect
|
||||||
|
google.golang.org/protobuf v1.36.5 // indirect
|
||||||
|
)
|
||||||
63
alert-bridge/go.sum
Normal file
63
alert-bridge/go.sum
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
||||||
|
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||||
|
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||||
|
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||||
|
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||||
|
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||||
|
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||||
|
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||||
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 h1:e9Rjr40Z98/clHv5Yg79Is0NtosR5LXRvdr7o/6NwbA=
|
||||||
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1/go.mod h1:tIxuGz/9mpox++sgp9fJjHO0+q1X9/UOWd798aAm22M=
|
||||||
|
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||||
|
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
|
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||||
|
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||||
|
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
|
||||||
|
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.35.0 h1:QcFwRrZLc82r8wODjvyCbP7Ifp3UANaBSmhDSFjnqSc=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.35.0/go.mod h1:CXIWhUomyWBG/oY2/r/kLp6K/cmx9e/7DLpBuuGdLCA=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 h1:1fTNlAIJZGWLP5FVu0fikVry1IsiUnXjf7QFvoNN3Xw=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0/go.mod h1:zjPK58DtkqQFn+YUMbx0M2XV3QgKU0gS9LeGohREyK4=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 h1:m639+BofXTvcY1q8CGs4ItwQarYtJPOWmVobfM1HpVI=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0/go.mod h1:LjReUci/F4BUyv+y4dwnq3h/26iNOeC3wAIqgvTIZVo=
|
||||||
|
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
|
||||||
|
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
|
||||||
|
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
|
||||||
|
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
|
||||||
|
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
|
||||||
|
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
|
||||||
|
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
|
||||||
|
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
|
||||||
|
go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4=
|
||||||
|
go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4=
|
||||||
|
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||||
|
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||||
|
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
|
||||||
|
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
|
||||||
|
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
|
||||||
|
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
|
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||||
|
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||||
|
google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a h1:nwKuGPlUAt+aR+pcrkfFRrTU1BVrSmYyYMxYbUIVHr0=
|
||||||
|
google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a/go.mod h1:3kWAYMk1I75K4vykHtKt2ycnOgpA6974V7bREqbsenU=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a h1:51aaUVRocpvUOSQKM6Q7VuoaktNIaMCLuhZB6DKksq4=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a/go.mod h1:uRxBH1mhmO8PGhU89cMcHaXKZqO+OfakD8QQO0oYwlQ=
|
||||||
|
google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg=
|
||||||
|
google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec=
|
||||||
|
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
|
||||||
|
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
91
alert-bridge/internal/adapters/primary/http/handler.go
Normal file
91
alert-bridge/internal/adapters/primary/http/handler.go
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/subtle"
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"gitea.nik4nao.com/nik/home-services/alert-bridge/internal/core/domain"
|
||||||
|
"gitea.nik4nao.com/nik/home-services/alert-bridge/internal/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AlertHandler processes one decoded, validated alert. internal/app.AlertApp
|
||||||
|
// implements this.
|
||||||
|
type AlertHandler interface {
|
||||||
|
Handle(ctx context.Context, alert domain.Alert) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// alertRequest is the wire shape callers POST to /alerts.
|
||||||
|
type alertRequest struct {
|
||||||
|
Source string `json:"source"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Level string `json:"level"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handler is the HTTP entrypoint external callers (e.g. cronjobs) POST alerts to.
|
||||||
|
type Handler struct {
|
||||||
|
app AlertHandler
|
||||||
|
apiKey string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewHandler constructs the HTTP handler for the /alerts endpoint.
|
||||||
|
func NewHandler(app AlertHandler, apiKey string) *Handler {
|
||||||
|
return &Handler{app: app, apiKey: apiKey}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := r.Context()
|
||||||
|
log := logger.FromContext(ctx)
|
||||||
|
|
||||||
|
if !h.authorized(r) {
|
||||||
|
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var body alertRequest
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||||
|
log.Warn("decode request body failed", "err", err)
|
||||||
|
http.Error(w, "bad request", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if body.Source == "" || body.Message == "" {
|
||||||
|
http.Error(w, "source and message are required", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
level := domain.Level(body.Level)
|
||||||
|
if level == "" {
|
||||||
|
level = domain.LevelInfo
|
||||||
|
}
|
||||||
|
switch level {
|
||||||
|
case domain.LevelInfo, domain.LevelWarn, domain.LevelError:
|
||||||
|
default:
|
||||||
|
http.Error(w, "level must be one of: info, warn, error", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
alert := domain.Alert{Source: body.Source, Message: body.Message, Level: level}
|
||||||
|
if err := h.app.Handle(ctx, alert); err != nil {
|
||||||
|
log.Error("handle alert failed", "err", err, "source", body.Source)
|
||||||
|
http.Error(w, "failed to deliver alert", http.StatusBadGateway)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusAccepted)
|
||||||
|
}
|
||||||
|
|
||||||
|
// authorized checks the Authorization: Bearer <token> header against the
|
||||||
|
// configured API key using a constant-time comparison so a mistyped key
|
||||||
|
// doesn't leak how many leading bytes matched via response timing.
|
||||||
|
func (h *Handler) authorized(r *http.Request) bool {
|
||||||
|
const prefix = "Bearer "
|
||||||
|
auth := r.Header.Get("Authorization")
|
||||||
|
if !strings.HasPrefix(auth, prefix) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
token := strings.TrimPrefix(auth, prefix)
|
||||||
|
return subtle.ConstantTimeCompare([]byte(token), []byte(h.apiKey)) == 1
|
||||||
|
}
|
||||||
120
alert-bridge/internal/adapters/primary/http/handler_test.go
Normal file
120
alert-bridge/internal/adapters/primary/http/handler_test.go
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"gitea.nik4nao.com/nik/home-services/alert-bridge/internal/core/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
type fakeAlertHandler struct {
|
||||||
|
handleFn func(ctx context.Context, alert domain.Alert) error
|
||||||
|
got domain.Alert
|
||||||
|
called bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakeAlertHandler) Handle(ctx context.Context, alert domain.Alert) error {
|
||||||
|
f.called = true
|
||||||
|
f.got = alert
|
||||||
|
if f.handleFn != nil {
|
||||||
|
return f.handleFn(ctx, alert)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func doRequest(h *Handler, apiKey, body string) *httptest.ResponseRecorder {
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/alerts", strings.NewReader(body))
|
||||||
|
if apiKey != "" {
|
||||||
|
req.Header.Set("Authorization", "Bearer "+apiKey)
|
||||||
|
}
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
h.ServeHTTP(rec, req)
|
||||||
|
return rec
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandler_Unauthorized(t *testing.T) {
|
||||||
|
fake := &fakeAlertHandler{}
|
||||||
|
h := NewHandler(fake, "secret")
|
||||||
|
|
||||||
|
rec := doRequest(h, "wrong-key", `{"source":"x","message":"y"}`)
|
||||||
|
if rec.Code != http.StatusUnauthorized {
|
||||||
|
t.Fatalf("expected 401, got %d", rec.Code)
|
||||||
|
}
|
||||||
|
if fake.called {
|
||||||
|
t.Fatal("app should not be called when unauthorized")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandler_MissingAuthHeader(t *testing.T) {
|
||||||
|
fake := &fakeAlertHandler{}
|
||||||
|
h := NewHandler(fake, "secret")
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/alerts", strings.NewReader(`{"source":"x","message":"y"}`))
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
h.ServeHTTP(rec, req)
|
||||||
|
if rec.Code != http.StatusUnauthorized {
|
||||||
|
t.Fatalf("expected 401, got %d", rec.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandler_BadJSON(t *testing.T) {
|
||||||
|
fake := &fakeAlertHandler{}
|
||||||
|
h := NewHandler(fake, "secret")
|
||||||
|
|
||||||
|
rec := doRequest(h, "secret", `not json`)
|
||||||
|
if rec.Code != http.StatusBadRequest {
|
||||||
|
t.Fatalf("expected 400, got %d", rec.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandler_MissingFields(t *testing.T) {
|
||||||
|
fake := &fakeAlertHandler{}
|
||||||
|
h := NewHandler(fake, "secret")
|
||||||
|
|
||||||
|
rec := doRequest(h, "secret", `{"source":"","message":""}`)
|
||||||
|
if rec.Code != http.StatusBadRequest {
|
||||||
|
t.Fatalf("expected 400, got %d", rec.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandler_InvalidLevel(t *testing.T) {
|
||||||
|
fake := &fakeAlertHandler{}
|
||||||
|
h := NewHandler(fake, "secret")
|
||||||
|
|
||||||
|
rec := doRequest(h, "secret", `{"source":"x","message":"y","level":"critical"}`)
|
||||||
|
if rec.Code != http.StatusBadRequest {
|
||||||
|
t.Fatalf("expected 400, got %d", rec.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandler_DefaultsLevelToInfo(t *testing.T) {
|
||||||
|
fake := &fakeAlertHandler{}
|
||||||
|
h := NewHandler(fake, "secret")
|
||||||
|
|
||||||
|
rec := doRequest(h, "secret", `{"source":"ba-cronjob","message":"Started"}`)
|
||||||
|
if rec.Code != http.StatusAccepted {
|
||||||
|
t.Fatalf("expected 202, got %d", rec.Code)
|
||||||
|
}
|
||||||
|
if fake.got.Level != domain.LevelInfo {
|
||||||
|
t.Fatalf("expected default level info, got %q", fake.got.Level)
|
||||||
|
}
|
||||||
|
if fake.got.Source != "ba-cronjob" || fake.got.Message != "Started" {
|
||||||
|
t.Fatalf("unexpected alert passed through: %+v", fake.got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandler_NotifierFailure(t *testing.T) {
|
||||||
|
fake := &fakeAlertHandler{handleFn: func(ctx context.Context, alert domain.Alert) error {
|
||||||
|
return errors.New("discord unreachable")
|
||||||
|
}}
|
||||||
|
h := NewHandler(fake, "secret")
|
||||||
|
|
||||||
|
rec := doRequest(h, "secret", `{"source":"x","message":"y","level":"error"}`)
|
||||||
|
if rec.Code != http.StatusBadGateway {
|
||||||
|
t.Fatalf("expected 502, got %d", rec.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,108 @@
|
|||||||
|
package discordwebhook
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gitea.nik4nao.com/nik/home-services/alert-bridge/internal/core/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Discord embed colors (decimal RGB), matching Alert severity.
|
||||||
|
const (
|
||||||
|
colorInfo = 3447003 // blue
|
||||||
|
colorWarn = 15844367 // yellow
|
||||||
|
colorError = 15158332 // red
|
||||||
|
)
|
||||||
|
|
||||||
|
// Client posts Alerts to a single Discord Incoming Webhook.
|
||||||
|
type Client struct {
|
||||||
|
webhookURL string
|
||||||
|
mentionUserID string
|
||||||
|
httpClient *http.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// New constructs a Discord webhook Notifier. mentionUserID may be empty, in
|
||||||
|
// which case level=error alerts are posted without a mention.
|
||||||
|
func New(webhookURL, mentionUserID string) *Client {
|
||||||
|
return &Client{
|
||||||
|
webhookURL: webhookURL,
|
||||||
|
mentionUserID: mentionUserID,
|
||||||
|
httpClient: &http.Client{Timeout: 10 * time.Second},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type embed struct {
|
||||||
|
Title string `json:"title"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
Color int `json:"color"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// allowedMentions is spelled out explicitly (rather than relying on Discord's
|
||||||
|
// default) so a mention only ever pings the specific configured user, never
|
||||||
|
// @everyone/@here/roles even if alert.Message text happens to contain them.
|
||||||
|
type allowedMentions struct {
|
||||||
|
Parse []string `json:"parse"`
|
||||||
|
Users []string `json:"users"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type webhookPayload struct {
|
||||||
|
Content string `json:"content,omitempty"`
|
||||||
|
Embeds []embed `json:"embeds"`
|
||||||
|
AllowedMentions allowedMentions `json:"allowed_mentions"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notify posts alert to the configured Discord webhook. level=error alerts
|
||||||
|
// mention mentionUserID (if configured) in the message content — Discord
|
||||||
|
// embeds alone never trigger a ping, only plain message content can.
|
||||||
|
func (c *Client) Notify(ctx context.Context, alert domain.Alert) error {
|
||||||
|
payload := webhookPayload{
|
||||||
|
Embeds: []embed{{
|
||||||
|
Title: fmt.Sprintf("[%s]", alert.Source),
|
||||||
|
Description: alert.Message,
|
||||||
|
Color: colorFor(alert.Level),
|
||||||
|
}},
|
||||||
|
AllowedMentions: allowedMentions{Parse: []string{}, Users: []string{}},
|
||||||
|
}
|
||||||
|
|
||||||
|
if alert.Level == domain.LevelError && c.mentionUserID != "" {
|
||||||
|
payload.Content = fmt.Sprintf("<@%s>", c.mentionUserID)
|
||||||
|
payload.AllowedMentions.Users = []string{c.mentionUserID}
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := json.Marshal(payload)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("marshal webhook payload: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.webhookURL, bytes.NewReader(body))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("build webhook request: %w", err)
|
||||||
|
}
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
resp, err := c.httpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("send webhook request: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode >= 300 {
|
||||||
|
return fmt.Errorf("discord webhook returned status %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func colorFor(level domain.Level) int {
|
||||||
|
switch level {
|
||||||
|
case domain.LevelWarn:
|
||||||
|
return colorWarn
|
||||||
|
case domain.LevelError:
|
||||||
|
return colorError
|
||||||
|
default:
|
||||||
|
return colorInfo
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,97 @@
|
|||||||
|
package discordwebhook
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"gitea.nik4nao.com/nik/home-services/alert-bridge/internal/core/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestClient_Notify_InfoNoMention(t *testing.T) {
|
||||||
|
var got webhookPayload
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&got); err != nil {
|
||||||
|
t.Fatalf("decode payload: %v", err)
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusNoContent)
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
c := New(server.URL, "182801655711006721")
|
||||||
|
err := c.Notify(t.Context(), domain.Alert{Source: "ba-cronjob", Message: "Started", Level: domain.LevelInfo})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Notify failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if got.Content != "" {
|
||||||
|
t.Fatalf("expected no mention content for info level, got %q", got.Content)
|
||||||
|
}
|
||||||
|
if len(got.Embeds) != 1 || got.Embeds[0].Title != "[ba-cronjob]" || got.Embeds[0].Description != "Started" {
|
||||||
|
t.Fatalf("unexpected embed: %+v", got.Embeds)
|
||||||
|
}
|
||||||
|
if got.Embeds[0].Color != colorInfo {
|
||||||
|
t.Fatalf("expected info color, got %d", got.Embeds[0].Color)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClient_Notify_ErrorMentionsConfiguredUser(t *testing.T) {
|
||||||
|
var got webhookPayload
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&got); err != nil {
|
||||||
|
t.Fatalf("decode payload: %v", err)
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusNoContent)
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
c := New(server.URL, "182801655711006721")
|
||||||
|
err := c.Notify(t.Context(), domain.Alert{Source: "ba-cronjob", Message: "Finished with 3 errors", Level: domain.LevelError})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Notify failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if got.Content != "<@182801655711006721>" {
|
||||||
|
t.Fatalf("expected mention content, got %q", got.Content)
|
||||||
|
}
|
||||||
|
if len(got.AllowedMentions.Users) != 1 || got.AllowedMentions.Users[0] != "182801655711006721" {
|
||||||
|
t.Fatalf("expected allowed_mentions scoped to configured user, got %+v", got.AllowedMentions)
|
||||||
|
}
|
||||||
|
if got.Embeds[0].Color != colorError {
|
||||||
|
t.Fatalf("expected error color, got %d", got.Embeds[0].Color)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClient_Notify_NoMentionUserConfigured(t *testing.T) {
|
||||||
|
var got webhookPayload
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&got); err != nil {
|
||||||
|
t.Fatalf("decode payload: %v", err)
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusNoContent)
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
c := New(server.URL, "")
|
||||||
|
err := c.Notify(t.Context(), domain.Alert{Source: "x", Message: "y", Level: domain.LevelError})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Notify failed: %v", err)
|
||||||
|
}
|
||||||
|
if got.Content != "" {
|
||||||
|
t.Fatalf("expected no mention when no user configured, got %q", got.Content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClient_Notify_NonSuccessStatus(t *testing.T) {
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusTooManyRequests)
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
c := New(server.URL, "")
|
||||||
|
err := c.Notify(t.Context(), domain.Alert{Source: "x", Message: "y", Level: domain.LevelInfo})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error on non-2xx response, got nil")
|
||||||
|
}
|
||||||
|
}
|
||||||
27
alert-bridge/internal/app/alert.go
Normal file
27
alert-bridge/internal/app/alert.go
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"gitea.nik4nao.com/nik/home-services/alert-bridge/internal/core/domain"
|
||||||
|
"gitea.nik4nao.com/nik/home-services/alert-bridge/internal/core/ports/driven"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AlertApp orchestrates delivering an incoming alert to its Notifier.
|
||||||
|
type AlertApp struct {
|
||||||
|
notifier driven.Notifier
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewAlertApp constructs the alert application service.
|
||||||
|
func NewAlertApp(notifier driven.Notifier) *AlertApp {
|
||||||
|
return &AlertApp{notifier: notifier}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle delivers one alert, wrapping any delivery failure for the caller.
|
||||||
|
func (a *AlertApp) Handle(ctx context.Context, alert domain.Alert) error {
|
||||||
|
if err := a.notifier.Notify(ctx, alert); err != nil {
|
||||||
|
return fmt.Errorf("notify: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
48
alert-bridge/internal/config/config.go
Normal file
48
alert-bridge/internal/config/config.go
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Config holds runtime configuration for the alert-bridge process.
|
||||||
|
type Config struct {
|
||||||
|
HTTPPort string // HTTP_PORT, default "8080"
|
||||||
|
APIKey string // API_KEY — required bearer token callers must present in Authorization: Bearer <key>
|
||||||
|
DiscordWebhookURL string // DISCORD_WEBHOOK_URL — required, the target channel's Discord Incoming Webhook URL
|
||||||
|
MentionUserID string // MENTION_USER_ID — optional Discord user ID; when set, level=error alerts mention this user
|
||||||
|
OTELEndpoint string
|
||||||
|
LogLevel string
|
||||||
|
LogFormat string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load reads alert-bridge configuration from environment variables.
|
||||||
|
func Load() (*Config, error) {
|
||||||
|
apiKey := os.Getenv("API_KEY")
|
||||||
|
if apiKey == "" {
|
||||||
|
return nil, errors.New("API_KEY is required but not set")
|
||||||
|
}
|
||||||
|
|
||||||
|
webhookURL := os.Getenv("DISCORD_WEBHOOK_URL")
|
||||||
|
if webhookURL == "" {
|
||||||
|
return nil, errors.New("DISCORD_WEBHOOK_URL is required but not set")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Config{
|
||||||
|
HTTPPort: getenvDefault("HTTP_PORT", "8080"),
|
||||||
|
APIKey: apiKey,
|
||||||
|
DiscordWebhookURL: webhookURL,
|
||||||
|
MentionUserID: os.Getenv("MENTION_USER_ID"),
|
||||||
|
OTELEndpoint: os.Getenv("OTEL_ENDPOINT"),
|
||||||
|
LogLevel: getenvDefault("LOG_LEVEL", "info"),
|
||||||
|
LogFormat: getenvDefault("LOG_FORMAT", "json"),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getenvDefault keeps config loading concise for optional variables with defaults.
|
||||||
|
func getenvDefault(key, fallback string) string {
|
||||||
|
if v := os.Getenv(key); v != "" {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
18
alert-bridge/internal/core/domain/alert.go
Normal file
18
alert-bridge/internal/core/domain/alert.go
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
package domain
|
||||||
|
|
||||||
|
// Level is the severity of an Alert. It controls the posted Discord embed's
|
||||||
|
// color and whether the message mentions a user to raise attention.
|
||||||
|
type Level string
|
||||||
|
|
||||||
|
const (
|
||||||
|
LevelInfo Level = "info"
|
||||||
|
LevelWarn Level = "warn"
|
||||||
|
LevelError Level = "error"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Alert is one notification submitted by an external caller (e.g. a cronjob).
|
||||||
|
type Alert struct {
|
||||||
|
Source string
|
||||||
|
Message string
|
||||||
|
Level Level
|
||||||
|
}
|
||||||
13
alert-bridge/internal/core/ports/driven/notifier.go
Normal file
13
alert-bridge/internal/core/ports/driven/notifier.go
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
package driven
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"gitea.nik4nao.com/nik/home-services/alert-bridge/internal/core/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Notifier delivers an Alert to its destination. internal/adapters/secondary/discordwebhook
|
||||||
|
// is the only current implementation.
|
||||||
|
type Notifier interface {
|
||||||
|
Notify(ctx context.Context, alert domain.Alert) error
|
||||||
|
}
|
||||||
38
alert-bridge/internal/logger/logger.go
Normal file
38
alert-bridge/internal/logger/logger.go
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
package logger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
type contextKey struct{}
|
||||||
|
|
||||||
|
// New constructs a root logger for the configured format and level.
|
||||||
|
func New(format, level string) *slog.Logger {
|
||||||
|
var parsed slog.Level
|
||||||
|
if err := parsed.UnmarshalText([]byte(level)); err != nil {
|
||||||
|
_, _ = fmt.Fprintf(os.Stderr, "invalid log level %q, falling back to info\n", level)
|
||||||
|
parsed = slog.LevelInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
opts := &slog.HandlerOptions{Level: parsed}
|
||||||
|
if format == "json" {
|
||||||
|
return slog.New(slog.NewJSONHandler(os.Stdout, opts))
|
||||||
|
}
|
||||||
|
return slog.New(slog.NewTextHandler(os.Stdout, opts))
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithLogger attaches a logger to the provided context.
|
||||||
|
func WithLogger(ctx context.Context, l *slog.Logger) context.Context {
|
||||||
|
return context.WithValue(ctx, contextKey{}, l)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FromContext retrieves a logger from context and falls back to slog.Default().
|
||||||
|
func FromContext(ctx context.Context) *slog.Logger {
|
||||||
|
if l, ok := ctx.Value(contextKey{}).(*slog.Logger); ok && l != nil {
|
||||||
|
return l
|
||||||
|
}
|
||||||
|
return slog.Default()
|
||||||
|
}
|
||||||
82
alert-bridge/internal/telemetry/telemetry.go
Normal file
82
alert-bridge/internal/telemetry/telemetry.go
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
package telemetry
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gitea.nik4nao.com/nik/home-services/alert-bridge/internal/config"
|
||||||
|
"gitea.nik4nao.com/nik/home-services/alert-bridge/internal/logger"
|
||||||
|
"go.opentelemetry.io/otel"
|
||||||
|
"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc"
|
||||||
|
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
|
||||||
|
metricnoop "go.opentelemetry.io/otel/metric/noop"
|
||||||
|
"go.opentelemetry.io/otel/propagation"
|
||||||
|
sdkmetric "go.opentelemetry.io/otel/sdk/metric"
|
||||||
|
"go.opentelemetry.io/otel/sdk/resource"
|
||||||
|
sdktrace "go.opentelemetry.io/otel/sdk/trace"
|
||||||
|
semconv "go.opentelemetry.io/otel/semconv/v1.26.0"
|
||||||
|
tracenoop "go.opentelemetry.io/otel/trace/noop"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Setup initialises OTel trace and metric providers for one service.
|
||||||
|
func Setup(ctx context.Context, serviceName, version string, cfg *config.Config) (shutdown func(context.Context) error, err error) {
|
||||||
|
if cfg.OTELEndpoint == "" {
|
||||||
|
otel.SetTracerProvider(tracenoop.NewTracerProvider())
|
||||||
|
otel.SetMeterProvider(metricnoop.NewMeterProvider())
|
||||||
|
logger.FromContext(ctx).Debug("otel disabled — OTEL_ENDPOINT not set")
|
||||||
|
return func(context.Context) error { return nil }, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
res := resource.NewWithAttributes(
|
||||||
|
semconv.SchemaURL,
|
||||||
|
semconv.ServiceNameKey.String(serviceName),
|
||||||
|
semconv.ServiceVersionKey.String(version),
|
||||||
|
)
|
||||||
|
|
||||||
|
traceExp, err := otlptracegrpc.New(ctx,
|
||||||
|
otlptracegrpc.WithEndpoint(cfg.OTELEndpoint),
|
||||||
|
otlptracegrpc.WithInsecure(),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
tp := sdktrace.NewTracerProvider(
|
||||||
|
sdktrace.WithBatcher(traceExp),
|
||||||
|
sdktrace.WithResource(res),
|
||||||
|
sdktrace.WithSampler(sdktrace.AlwaysSample()),
|
||||||
|
)
|
||||||
|
otel.SetTracerProvider(tp)
|
||||||
|
otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(
|
||||||
|
propagation.TraceContext{},
|
||||||
|
propagation.Baggage{},
|
||||||
|
))
|
||||||
|
|
||||||
|
metricExp, err := otlpmetricgrpc.New(ctx,
|
||||||
|
otlpmetricgrpc.WithEndpoint(cfg.OTELEndpoint),
|
||||||
|
otlpmetricgrpc.WithInsecure(),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
_ = tp.Shutdown(ctx)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
mp := sdkmetric.NewMeterProvider(
|
||||||
|
sdkmetric.WithReader(sdkmetric.NewPeriodicReader(metricExp,
|
||||||
|
sdkmetric.WithInterval(30*time.Second))),
|
||||||
|
sdkmetric.WithResource(res),
|
||||||
|
)
|
||||||
|
otel.SetMeterProvider(mp)
|
||||||
|
|
||||||
|
return func(ctx context.Context) error {
|
||||||
|
shutdownCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
var shutdownErr error
|
||||||
|
if err := tp.Shutdown(shutdownCtx); err != nil {
|
||||||
|
shutdownErr = errors.Join(shutdownErr, err)
|
||||||
|
}
|
||||||
|
if err := mp.Shutdown(shutdownCtx); err != nil {
|
||||||
|
shutdownErr = errors.Join(shutdownErr, err)
|
||||||
|
}
|
||||||
|
return shutdownErr
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
@ -11,6 +11,7 @@ COPY ai-gateway/go.mod ai-gateway/go.sum ./ai-gateway/
|
|||||||
COPY discord-bot/go.mod discord-bot/go.sum ./discord-bot/
|
COPY discord-bot/go.mod discord-bot/go.sum ./discord-bot/
|
||||||
COPY tts-gateway/go.mod tts-gateway/go.sum ./tts-gateway/
|
COPY tts-gateway/go.mod tts-gateway/go.sum ./tts-gateway/
|
||||||
COPY alexa-bridge/go.mod alexa-bridge/go.sum ./alexa-bridge/
|
COPY alexa-bridge/go.mod alexa-bridge/go.sum ./alexa-bridge/
|
||||||
|
COPY alert-bridge/go.mod alert-bridge/go.sum ./alert-bridge/
|
||||||
|
|
||||||
WORKDIR /workspace/alexa-bridge
|
WORKDIR /workspace/alexa-bridge
|
||||||
RUN go mod download
|
RUN go mod download
|
||||||
@ -22,6 +23,7 @@ COPY ai-gateway/ ./ai-gateway/
|
|||||||
COPY discord-bot/ ./discord-bot/
|
COPY discord-bot/ ./discord-bot/
|
||||||
COPY tts-gateway/ ./tts-gateway/
|
COPY tts-gateway/ ./tts-gateway/
|
||||||
COPY alexa-bridge/ ./alexa-bridge/
|
COPY alexa-bridge/ ./alexa-bridge/
|
||||||
|
COPY alert-bridge/ ./alert-bridge/
|
||||||
|
|
||||||
WORKDIR /workspace/alexa-bridge
|
WORKDIR /workspace/alexa-bridge
|
||||||
ARG VERSION=dev
|
ARG VERSION=dev
|
||||||
|
|||||||
@ -10,6 +10,7 @@ COPY ai-gateway/go.mod ai-gateway/go.sum ./ai-gateway/
|
|||||||
COPY ha-gateway/go.mod ha-gateway/go.sum ./ha-gateway/
|
COPY ha-gateway/go.mod ha-gateway/go.sum ./ha-gateway/
|
||||||
COPY tts-gateway/go.mod tts-gateway/go.sum ./tts-gateway/
|
COPY tts-gateway/go.mod tts-gateway/go.sum ./tts-gateway/
|
||||||
COPY alexa-bridge/go.mod alexa-bridge/go.sum ./alexa-bridge/
|
COPY alexa-bridge/go.mod alexa-bridge/go.sum ./alexa-bridge/
|
||||||
|
COPY alert-bridge/go.mod alert-bridge/go.sum ./alert-bridge/
|
||||||
COPY discord-bot/go.mod discord-bot/go.sum ./discord-bot/
|
COPY discord-bot/go.mod discord-bot/go.sum ./discord-bot/
|
||||||
|
|
||||||
WORKDIR /workspace/discord-bot
|
WORKDIR /workspace/discord-bot
|
||||||
@ -21,6 +22,7 @@ COPY ai-gateway/ ./ai-gateway/
|
|||||||
COPY ha-gateway/ ./ha-gateway/
|
COPY ha-gateway/ ./ha-gateway/
|
||||||
COPY tts-gateway/ ./tts-gateway/
|
COPY tts-gateway/ ./tts-gateway/
|
||||||
COPY alexa-bridge/ ./alexa-bridge/
|
COPY alexa-bridge/ ./alexa-bridge/
|
||||||
|
COPY alert-bridge/ ./alert-bridge/
|
||||||
COPY discord-bot/ ./discord-bot/
|
COPY discord-bot/ ./discord-bot/
|
||||||
|
|
||||||
WORKDIR /workspace/discord-bot
|
WORKDIR /workspace/discord-bot
|
||||||
|
|||||||
1
go.work
1
go.work
@ -2,6 +2,7 @@ go 1.26
|
|||||||
|
|
||||||
use (
|
use (
|
||||||
./ai-gateway
|
./ai-gateway
|
||||||
|
./alert-bridge
|
||||||
./alexa-bridge
|
./alexa-bridge
|
||||||
./discord-bot
|
./discord-bot
|
||||||
./gen
|
./gen
|
||||||
|
|||||||
32
go.work.sum
32
go.work.sum
@ -1,6 +1,7 @@
|
|||||||
cel.dev/expr v0.19.1/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw=
|
cel.dev/expr v0.19.1/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw=
|
||||||
cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4=
|
cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4=
|
||||||
cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4=
|
cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4=
|
||||||
|
cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=
|
||||||
cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg=
|
cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg=
|
||||||
cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
|
cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
|
||||||
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
|
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
|
||||||
@ -39,6 +40,8 @@ github.com/golang/glog v1.2.5 h1:DrW6hGnjIhtvhOIiAKT6Psh/Kd/ldepEa81DKeiRJ5I=
|
|||||||
github.com/golang/glog v1.2.5/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w=
|
github.com/golang/glog v1.2.5/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w=
|
||||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 h1:X5VWvz21y3gzm9Nw/kaUeku/1+uBhcekkmy4IkffJww=
|
||||||
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1/go.mod h1:Zanoh4+gvIgluNqcfMVTJueD4wSS5hT7zTt4Mrutd90=
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c=
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c=
|
||||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
@ -88,12 +91,13 @@ golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc
|
|||||||
golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
|
golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
|
||||||
golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
|
golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
|
||||||
golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU=
|
golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU=
|
||||||
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
|
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
|
||||||
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
|
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
|
||||||
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
|
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
|
||||||
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
|
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
|
||||||
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
|
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
|
||||||
golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||||
|
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
|
||||||
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
|
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
|
||||||
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||||
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
|
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
|
||||||
@ -103,6 +107,9 @@ golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
|||||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
|
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
|
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
@ -110,32 +117,33 @@ golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
|||||||
golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q=
|
golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q=
|
||||||
golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg=
|
golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg=
|
||||||
golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk=
|
golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk=
|
||||||
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
|
||||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
|
||||||
|
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
|
||||||
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
||||||
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
|
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
|
||||||
golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=
|
golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=
|
||||||
golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
|
golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
|
||||||
golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI=
|
golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a h1:nwKuGPlUAt+aR+pcrkfFRrTU1BVrSmYyYMxYbUIVHr0=
|
google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822/go.mod h1:h3c4v36UTKzUiuaOKQ6gr3S+0hovBtUrXzTG/i3+XEc=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a/go.mod h1:3kWAYMk1I75K4vykHtKt2ycnOgpA6974V7bREqbsenU=
|
google.golang.org/genproto/googleapis/api v0.0.0-20250728155136-f173205681a0/go.mod h1:8ytArBbtOy2xfht+y2fqKd5DRDJRUQhqbyEnQ4bDChs=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:kSJwQxqmFXeo79zOmbrALdflXQeAYcUbgS7PbpMknCY=
|
google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:kSJwQxqmFXeo79zOmbrALdflXQeAYcUbgS7PbpMknCY=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478/go.mod h1:C6ADNqOxbgdUUeRTU+LCHDPB9ttAMCTff6auwCVa4uc=
|
google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478/go.mod h1:C6ADNqOxbgdUUeRTU+LCHDPB9ttAMCTff6auwCVa4uc=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a h1:51aaUVRocpvUOSQKM6Q7VuoaktNIaMCLuhZB6DKksq4=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a/go.mod h1:uRxBH1mhmO8PGhU89cMcHaXKZqO+OfakD8QQO0oYwlQ=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20250728155136-f173205681a0/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20251124214823-79d6a2a48846/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260523011958-0a33c5d7ca68/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260523011958-0a33c5d7ca68/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||||
google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg=
|
google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc=
|
||||||
google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec=
|
google.golang.org/grpc v1.74.2/go.mod h1:CtQ+BGjaAIXHs/5YS3i473GqwBBa1zGQNevxdeBEXrM=
|
||||||
google.golang.org/grpc v1.79.2/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
|
google.golang.org/grpc v1.79.2/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
|
||||||
google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4=
|
google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4=
|
||||||
google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I=
|
google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I=
|
||||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||||
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||||
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
|
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
||||||
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
|
||||||
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
|
|||||||
@ -10,6 +10,7 @@ COPY ai-gateway/go.mod ai-gateway/go.sum ./ai-gateway/
|
|||||||
COPY discord-bot/go.mod discord-bot/go.sum ./discord-bot/
|
COPY discord-bot/go.mod discord-bot/go.sum ./discord-bot/
|
||||||
COPY tts-gateway/go.mod tts-gateway/go.sum ./tts-gateway/
|
COPY tts-gateway/go.mod tts-gateway/go.sum ./tts-gateway/
|
||||||
COPY alexa-bridge/go.mod alexa-bridge/go.sum ./alexa-bridge/
|
COPY alexa-bridge/go.mod alexa-bridge/go.sum ./alexa-bridge/
|
||||||
|
COPY alert-bridge/go.mod alert-bridge/go.sum ./alert-bridge/
|
||||||
COPY ha-gateway/go.mod ha-gateway/go.sum ./ha-gateway/
|
COPY ha-gateway/go.mod ha-gateway/go.sum ./ha-gateway/
|
||||||
|
|
||||||
WORKDIR /workspace/ha-gateway
|
WORKDIR /workspace/ha-gateway
|
||||||
@ -21,6 +22,7 @@ COPY ai-gateway/ ./ai-gateway/
|
|||||||
COPY discord-bot/ ./discord-bot/
|
COPY discord-bot/ ./discord-bot/
|
||||||
COPY tts-gateway/ ./tts-gateway/
|
COPY tts-gateway/ ./tts-gateway/
|
||||||
COPY alexa-bridge/ ./alexa-bridge/
|
COPY alexa-bridge/ ./alexa-bridge/
|
||||||
|
COPY alert-bridge/ ./alert-bridge/
|
||||||
COPY ha-gateway/ ./ha-gateway/
|
COPY ha-gateway/ ./ha-gateway/
|
||||||
|
|
||||||
WORKDIR /workspace/ha-gateway
|
WORKDIR /workspace/ha-gateway
|
||||||
|
|||||||
@ -16,6 +16,7 @@ COPY ai-gateway/go.mod ai-gateway/go.sum ./ai-gateway/
|
|||||||
COPY ha-gateway/go.mod ha-gateway/go.sum ./ha-gateway/
|
COPY ha-gateway/go.mod ha-gateway/go.sum ./ha-gateway/
|
||||||
COPY discord-bot/go.mod discord-bot/go.sum ./discord-bot/
|
COPY discord-bot/go.mod discord-bot/go.sum ./discord-bot/
|
||||||
COPY alexa-bridge/go.mod alexa-bridge/go.sum ./alexa-bridge/
|
COPY alexa-bridge/go.mod alexa-bridge/go.sum ./alexa-bridge/
|
||||||
|
COPY alert-bridge/go.mod alert-bridge/go.sum ./alert-bridge/
|
||||||
COPY tts-gateway/go.mod tts-gateway/go.sum ./tts-gateway/
|
COPY tts-gateway/go.mod tts-gateway/go.sum ./tts-gateway/
|
||||||
|
|
||||||
WORKDIR /workspace/tts-gateway
|
WORKDIR /workspace/tts-gateway
|
||||||
@ -27,6 +28,7 @@ COPY ai-gateway/ ./ai-gateway/
|
|||||||
COPY ha-gateway/ ./ha-gateway/
|
COPY ha-gateway/ ./ha-gateway/
|
||||||
COPY discord-bot/ ./discord-bot/
|
COPY discord-bot/ ./discord-bot/
|
||||||
COPY alexa-bridge/ ./alexa-bridge/
|
COPY alexa-bridge/ ./alexa-bridge/
|
||||||
|
COPY alert-bridge/ ./alert-bridge/
|
||||||
COPY tts-gateway/ ./tts-gateway/
|
COPY tts-gateway/ ./tts-gateway/
|
||||||
|
|
||||||
WORKDIR /workspace/tts-gateway
|
WORKDIR /workspace/tts-gateway
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user