diff --git a/.gitea/workflows/docker-build.yml b/.gitea/workflows/docker-build.yml new file mode 100644 index 0000000..ed3c3a4 --- /dev/null +++ b/.gitea/workflows/docker-build.yml @@ -0,0 +1,28 @@ +name: Deploy and Push Docker Image +run-name: ${{ gitea.actor }} deploying to production 🚀 +on: [push] + +jobs: + BuildAndPush: + runs-on: ubuntu + steps: + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - name: Check out repository code + uses: actions/checkout@v3 + - name: Login to Docker Registry + uses: docker/login-action@v3 + with: + registry: gitea.henriburau.de + username: ${{ gitea.actor }} + password: ${{ secrets.REGISTRY_TOKEN}} + - name: Build and push + uses: docker/build-push-action@v5 + with: + context: . + file: build/Dockerfile + push: true + platforms: linux/arm64,linux/amd64 + tags: | + gitea.henriburau.de/ace966/WeeWooWebhook:latest + gitea.henriburau.de/ace966/WeeWooWebhook:${{ gitea.run_id }} diff --git a/build/Dockerfile b/build/Dockerfile new file mode 100644 index 0000000..f5aab77 --- /dev/null +++ b/build/Dockerfile @@ -0,0 +1,18 @@ +FROM golang:1.24-alpine AS builder + +# Set working directory +WORKDIR /app + +# Copy source code +COPY . . + +# Build the Go binary statically +RUN CGO_ENABLED=0 GOOS=linux go build -o weewoowebhook /app/cmd/wee_woo_webhook/main.go + +FROM scratch + +# Copy statically built binary from the builder +COPY --from=builder /app/weewoowebhook /weewoowebhook + +# Set the binary as the container entrypoint +ENTRYPOINT ["/weewoowebhook"] diff --git a/cmd/wee_woo_webhook/main.go b/cmd/wee_woo_webhook/main.go new file mode 100644 index 0000000..408e945 --- /dev/null +++ b/cmd/wee_woo_webhook/main.go @@ -0,0 +1,19 @@ +package main + +import ( + "log" + + "gitea.henriburau.de/ace966/WeeWooWebhook/internal/olc" + "gitea.henriburau.de/ace966/WeeWooWebhook/internal/server" +) + +const ( + WeeWooAddr = "http://192.168.1.167" +) + +func main() { + officeLightClient := olc.NewClient(WeeWooAddr) + server := server.NewServer(officeLightClient) + + log.Fatal(server.ListenAndServe()) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..90dadfb --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module gitea.henriburau.de/ace966/WeeWooWebhook + +go 1.23.5 diff --git a/internal/model/uptime_kuma_webhook_request.go b/internal/model/uptime_kuma_webhook_request.go new file mode 100644 index 0000000..e329793 --- /dev/null +++ b/internal/model/uptime_kuma_webhook_request.go @@ -0,0 +1,84 @@ +package model + +const ( + MonitorStatusDown = 0 + MonitorStatusUp = 1 +) + +type UptimeKumaWebhookRequest struct { + Heartbeat struct { + MonitorID int `json:"monitorID"` + Status int `json:"status"` + Time string `json:"time"` + Msg string `json:"msg"` + Important bool `json:"important"` + Duration int `json:"duration"` + Timezone string `json:"timezone"` + TimezoneOffset string `json:"timezoneOffset"` + LocalDateTime string `json:"localDateTime"` + } `json:"heartbeat"` + Monitor struct { + ID int `json:"id"` + Name string `json:"name"` + Description any `json:"description"` + PathName string `json:"pathName"` + Parent any `json:"parent"` + ChildrenIDs []any `json:"childrenIDs"` + URL string `json:"url"` + Method string `json:"method"` + Hostname any `json:"hostname"` + Port any `json:"port"` + Maxretries int `json:"maxretries"` + Weight int `json:"weight"` + Active bool `json:"active"` + ForceInactive bool `json:"forceInactive"` + Type string `json:"type"` + Timeout int `json:"timeout"` + Interval int `json:"interval"` + RetryInterval int `json:"retryInterval"` + ResendInterval int `json:"resendInterval"` + Keyword any `json:"keyword"` + InvertKeyword bool `json:"invertKeyword"` + ExpiryNotification bool `json:"expiryNotification"` + IgnoreTLS bool `json:"ignoreTls"` + UpsideDown bool `json:"upsideDown"` + PacketSize int `json:"packetSize"` + Maxredirects int `json:"maxredirects"` + AcceptedStatuscodes []string `json:"accepted_statuscodes"` + DNSResolveType string `json:"dns_resolve_type"` + DNSResolveServer string `json:"dns_resolve_server"` + DNSLastResult any `json:"dns_last_result"` + DockerContainer string `json:"docker_container"` + DockerHost any `json:"docker_host"` + ProxyID any `json:"proxyId"` + NotificationIDList struct { + Num5 bool `json:"5"` + } `json:"notificationIDList"` + Tags []any `json:"tags"` + Maintenance bool `json:"maintenance"` + MqttTopic string `json:"mqttTopic"` + MqttSuccessMessage string `json:"mqttSuccessMessage"` + DatabaseQuery any `json:"databaseQuery"` + AuthMethod any `json:"authMethod"` + GrpcURL any `json:"grpcUrl"` + GrpcProtobuf any `json:"grpcProtobuf"` + GrpcMethod any `json:"grpcMethod"` + GrpcServiceName any `json:"grpcServiceName"` + GrpcEnableTLS bool `json:"grpcEnableTls"` + RadiusCalledStationID any `json:"radiusCalledStationId"` + RadiusCallingStationID any `json:"radiusCallingStationId"` + Game any `json:"game"` + GamedigGivenPortOnly bool `json:"gamedigGivenPortOnly"` + HTTPBodyEncoding string `json:"httpBodyEncoding"` + JSONPath any `json:"jsonPath"` + ExpectedValue any `json:"expectedValue"` + KafkaProducerTopic any `json:"kafkaProducerTopic"` + KafkaProducerBrokers []any `json:"kafkaProducerBrokers"` + KafkaProducerSsl bool `json:"kafkaProducerSsl"` + KafkaProducerAllowAutoTopicCreation bool `json:"kafkaProducerAllowAutoTopicCreation"` + KafkaProducerMessage any `json:"kafkaProducerMessage"` + Screenshot any `json:"screenshot"` + IncludeSensitiveData bool `json:"includeSensitiveData"` + } `json:"monitor"` + Msg string `json:"msg"` +} diff --git a/internal/olc/office_light_client.go b/internal/olc/office_light_client.go new file mode 100644 index 0000000..cc6b91d --- /dev/null +++ b/internal/olc/office_light_client.go @@ -0,0 +1,57 @@ +package olc + +import ( + "fmt" + "net/http" + "net/url" + "time" +) + +type OfficeLightClient struct { + BaseURL string + Client *http.Client +} + +func NewClient(baseURL string) *OfficeLightClient { + return &OfficeLightClient{ + BaseURL: baseURL, + Client: &http.Client{ + Timeout: 10 * time.Second, + }, + } +} + +func (olc *OfficeLightClient) SetLightState(state bool) error { + endpoint := fmt.Sprintf("%s/state", olc.BaseURL) + stateString := "off" + if state { + stateString = "on" + } + + params := url.Values{} + params.Set("state", stateString) + + reqURL := fmt.Sprintf("%s?%s", endpoint, params.Encode()) + resp, err := olc.Client.Post(reqURL, "application/x-www-form-urlencoded", nil) + if err != nil { + return err + } + defer resp.Body.Close() + return nil +} + +func (olc *OfficeLightClient) SetColor(r, g, b uint8) error { + endpoint := fmt.Sprintf("%s/color", olc.BaseURL) + params := url.Values{} + params.Set("r", fmt.Sprintf("%d", r)) + params.Set("g", fmt.Sprintf("%d", g)) + params.Set("b", fmt.Sprintf("%d", b)) + + reqURL := fmt.Sprintf("%s?%s", endpoint, params.Encode()) + resp, err := olc.Client.Post(reqURL, "application/x-www-form-urlencoded", nil) + if err != nil { + return err + } + defer resp.Body.Close() + return nil +} diff --git a/internal/server/server.go b/internal/server/server.go new file mode 100644 index 0000000..5cb4632 --- /dev/null +++ b/internal/server/server.go @@ -0,0 +1,28 @@ +package server + +import ( + "fmt" + "net/http" + "sync" + + "gitea.henriburau.de/ace966/WeeWooWebhook/internal/olc" +) + +type Server struct { + OfficeLightClient *olc.OfficeLightClient + downServices map[int]bool + mu sync.Mutex +} + +func NewServer(olc *olc.OfficeLightClient) *Server { + return &Server{ + OfficeLightClient: olc, + downServices: make(map[int]bool), + } +} + +func (s *Server) ListenAndServe() error { + fmt.Println("Server listening on :8085/wehook...") + http.HandleFunc("/webhook", s.webhookHandler()) + return http.ListenAndServe(":8085", nil) +} diff --git a/internal/server/webhook.go b/internal/server/webhook.go new file mode 100644 index 0000000..58e7b3c --- /dev/null +++ b/internal/server/webhook.go @@ -0,0 +1,56 @@ +package server + +import ( + "encoding/json" + "fmt" + "log" + "net/http" + + "gitea.henriburau.de/ace966/WeeWooWebhook/internal/model" +) + +func (s *Server) webhookHandler() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var data model.UptimeKumaWebhookRequest + if err := json.NewDecoder(r.Body).Decode(&data); err != nil { + http.Error(w, "Invalid JSON", http.StatusBadRequest) + return + } + + log.Printf("Received webhook msg: %s\n", data.Msg) + + s.mu.Lock() + defer s.mu.Unlock() + + wasDown := s.downServices[data.Monitor.ID] + + switch data.Heartbeat.Status { + case model.MonitorStatusDown: + if !wasDown { + s.downServices[data.Heartbeat.MonitorID] = true + log.Printf("Service DOWN: %s", data.Monitor.Name) + go s.activateEmergencyLight() + } + case model.MonitorStatusUp: + if wasDown { + delete(s.downServices, data.Heartbeat.MonitorID) + log.Printf("Service UP: %s", data.Monitor.Name) + if len(s.downServices) == 0 { + go s.deactivateEmergencyLight() + } + } + } + + w.WriteHeader(http.StatusOK) + fmt.Fprintln(w, "Received") + } +} + +func (s *Server) activateEmergencyLight() { + s.OfficeLightClient.SetColor(255, 0, 0) // Set to red + s.OfficeLightClient.SetLightState(true) // Turn on +} + +func (s *Server) deactivateEmergencyLight() { + s.OfficeLightClient.SetLightState(false) // Turn off +}