Netdata.cloud bot for Zulip

support the challenges from zulip

Changed files
+43 -1
netdata_zulip_bot
+1
netdata_zulip_bot/config.py
···
server_config = ServerConfig(
host=os.getenv("SERVER_HOST", "0.0.0.0"),
port=int(os.getenv("SERVER_PORT", "8080")),
+
challenge_secret=os.getenv("SERVER_CHALLENGE_SECRET"),
)
logger.info(
+3
netdata_zulip_bot/main.py
···
SERVER_HOST=0.0.0.0
SERVER_PORT=8080
+
# Netdata webhook challenge secret (required for webhook verification)
+
SERVER_CHALLENGE_SECRET=your-challenge-secret-here
+
# Optional: Override Zulip stream (default: netdata-alerts)
# ZULIP_STREAM=custom-alerts-stream
"""
+1
netdata_zulip_bot/models.py
···
"""Server configuration."""
host: str = "0.0.0.0"
port: int = 8080 # Default HTTP port
+
challenge_secret: Optional[str] = None # Netdata webhook challenge secret
model_config = ConfigDict(env_prefix="SERVER_")
+38 -1
netdata_zulip_bot/server.py
···
"""FastAPI webhook server for receiving Netdata notifications."""
+
import base64
+
import hashlib
+
import hmac
from typing import Dict, Any
import structlog
import uvicorn
-
from fastapi import FastAPI, HTTPException, Request, status
+
from fastapi import FastAPI, HTTPException, Request, status, Query
from fastapi.responses import JSONResponse
from .formatter import ZulipMessageFormatter
···
async def health_check():
"""Health check endpoint."""
return {"status": "healthy", "service": "netdata-zulip-bot"}
+
+
@self.app.get("/webhook/netdata")
+
async def netdata_webhook_challenge(crc_token: str = Query(...)):
+
"""Handle Netdata Cloud webhook challenge for verification."""
+
if not self.server_config.challenge_secret:
+
logger.error("Challenge secret not configured")
+
raise HTTPException(
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
+
detail="Challenge secret not configured"
+
)
+
+
try:
+
# Create HMAC SHA-256 hash from crc_token and challenge secret
+
token_bytes = crc_token.encode('ascii')
+
secret_bytes = self.server_config.challenge_secret.encode('utf-8')
+
+
sha256_hash = hmac.new(
+
secret_bytes,
+
msg=token_bytes,
+
digestmod=hashlib.sha256
+
).digest()
+
+
# Create response with base64 encoded hash
+
response_token = 'sha256=' + base64.b64encode(sha256_hash).decode('ascii')
+
+
logger.info("Responding to Netdata challenge", crc_token=crc_token[:16] + "...")
+
return {"response_token": response_token}
+
+
except Exception as e:
+
logger.error("Failed to process challenge", error=str(e))
+
raise HTTPException(
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
+
detail="Failed to process challenge"
+
)
@self.app.post("/webhook/netdata")
async def netdata_webhook(request: Request):