feat(cache): 🎉 add message caching functionality
Some checks failed
CI / Test (1.23) (push) Failing after -27m1s
CI / Lint (push) Successful in -26m31s
CI / Build (push) Successful in -27m3s
CI / Test (1.22) (push) Failing after -24m58s

* Implement MessageCache to store events when no webhooks are available.
* Add configuration options for enabling cache, setting data path, max age, and max events.
* Create API endpoints for managing cached events, including listing, replaying, and deleting.
* Integrate caching into the hooks manager to store events when no active webhooks are found.
* Enhance logging for better traceability of cached events and operations.
This commit is contained in:
Hein
2026-01-30 16:00:34 +02:00
parent 3901bbb668
commit c4d974d6ce
9 changed files with 1535 additions and 30 deletions

254
pkg/handlers/cache.go Normal file
View File

@@ -0,0 +1,254 @@
package handlers
import (
"encoding/json"
"net/http"
"strconv"
"git.warky.dev/wdevs/whatshooked/pkg/events"
"git.warky.dev/wdevs/whatshooked/pkg/logging"
)
// GetCachedEvents returns all cached events
// GET /api/cache
func (h *Handlers) GetCachedEvents(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
cache := h.hookMgr.GetCache()
if cache == nil || !cache.IsEnabled() {
http.Error(w, "Message cache is not enabled", http.StatusServiceUnavailable)
return
}
// Optional event_type filter
eventType := r.URL.Query().Get("event_type")
var cachedEvents interface{}
if eventType != "" {
cachedEvents = cache.ListByEventType(events.EventType(eventType))
} else {
cachedEvents = cache.List()
}
writeJSON(w, map[string]interface{}{
"cached_events": cachedEvents,
"count": cache.Count(),
})
}
// GetCachedEvent returns a specific cached event by ID
// GET /api/cache/{id}
func (h *Handlers) GetCachedEvent(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
cache := h.hookMgr.GetCache()
if cache == nil || !cache.IsEnabled() {
http.Error(w, "Message cache is not enabled", http.StatusServiceUnavailable)
return
}
// Extract ID from path
id := r.URL.Query().Get("id")
if id == "" {
http.Error(w, "Event ID required", http.StatusBadRequest)
return
}
cached, exists := cache.Get(id)
if !exists {
http.Error(w, "Cached event not found", http.StatusNotFound)
return
}
writeJSON(w, cached)
}
// ReplayCachedEvents replays all cached events
// POST /api/cache/replay
func (h *Handlers) ReplayCachedEvents(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
cache := h.hookMgr.GetCache()
if cache == nil || !cache.IsEnabled() {
http.Error(w, "Message cache is not enabled", http.StatusServiceUnavailable)
return
}
logging.Info("Replaying all cached events via API")
successCount, failCount, err := h.hookMgr.ReplayCachedEvents()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
writeJSON(w, map[string]interface{}{
"success": true,
"replayed": successCount + failCount,
"delivered": successCount,
"failed": failCount,
"remaining_cached": cache.Count(),
})
}
// ReplayCachedEvent replays a specific cached event
// POST /api/cache/replay/{id}
func (h *Handlers) ReplayCachedEvent(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
cache := h.hookMgr.GetCache()
if cache == nil || !cache.IsEnabled() {
http.Error(w, "Message cache is not enabled", http.StatusServiceUnavailable)
return
}
// Extract ID from request body or query param
var req struct {
ID string `json:"id"`
}
// Try query param first
id := r.URL.Query().Get("id")
if id == "" {
// Try JSON body
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid request body", http.StatusBadRequest)
return
}
id = req.ID
}
if id == "" {
http.Error(w, "Event ID required", http.StatusBadRequest)
return
}
logging.Info("Replaying cached event via API", "event_id", id)
if err := h.hookMgr.ReplayCachedEvent(id); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
writeJSON(w, map[string]interface{}{
"success": true,
"event_id": id,
"message": "Event replayed successfully",
})
}
// DeleteCachedEvent removes a specific cached event
// DELETE /api/cache/{id}
func (h *Handlers) DeleteCachedEvent(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodDelete {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
cache := h.hookMgr.GetCache()
if cache == nil || !cache.IsEnabled() {
http.Error(w, "Message cache is not enabled", http.StatusServiceUnavailable)
return
}
id := r.URL.Query().Get("id")
if id == "" {
http.Error(w, "Event ID required", http.StatusBadRequest)
return
}
logging.Info("Deleting cached event via API", "event_id", id)
if err := cache.Remove(id); err != nil {
http.Error(w, err.Error(), http.StatusNotFound)
return
}
writeJSON(w, map[string]interface{}{
"success": true,
"event_id": id,
"message": "Cached event deleted successfully",
})
}
// ClearCache removes all cached events
// DELETE /api/cache
func (h *Handlers) ClearCache(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodDelete {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
cache := h.hookMgr.GetCache()
if cache == nil || !cache.IsEnabled() {
http.Error(w, "Message cache is not enabled", http.StatusServiceUnavailable)
return
}
// Optional confirmation parameter
confirm := r.URL.Query().Get("confirm")
confirmInt, _ := strconv.ParseBool(confirm)
if !confirmInt {
http.Error(w, "Add ?confirm=true to confirm cache clearing", http.StatusBadRequest)
return
}
count := cache.Count()
logging.Warn("Clearing all cached events via API", "count", count)
if err := cache.Clear(); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
writeJSON(w, map[string]interface{}{
"success": true,
"cleared": count,
"message": "Cache cleared successfully",
})
}
// GetCacheStats returns cache statistics
// GET /api/cache/stats
func (h *Handlers) GetCacheStats(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
cache := h.hookMgr.GetCache()
if cache == nil || !cache.IsEnabled() {
writeJSON(w, map[string]interface{}{
"enabled": false,
"count": 0,
})
return
}
// Group by event type
cachedEvents := cache.List()
eventTypeCounts := make(map[string]int)
for _, cached := range cachedEvents {
eventTypeCounts[string(cached.Event.Type)]++
}
writeJSON(w, map[string]interface{}{
"enabled": true,
"total_count": cache.Count(),
"by_event_type": eventTypeCounts,
})
}