TTW_Bot_GO/internal/webservices/alert.go

209 lines
6.4 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package webservices
import (
"encoding/json"
"fmt"
"html/template"
"net/http"
"stream-bot/internal/db"
"stream-bot/internal/logger"
"strings"
)
type AlertService struct {
*baseService
config *db.AlertWebConfig
server *http.Server
}
func NewAlertService(port int, config *db.AlertWebConfig) *AlertService {
return &AlertService{
baseService: newBaseService(port),
config: config,
}
}
func (s *AlertService) Start() error {
mux := http.NewServeMux()
mux.HandleFunc("/", s.handleIndex)
mux.HandleFunc("/events", s.handleEvents)
mux.HandleFunc("/notify", s.handleNotify) // новый эндпоинт
mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("data/media"))))
mux.Handle("/sounds/", http.StripPrefix("/sounds/", http.FileServer(http.Dir("data/sounds")))) // для звуков
s.server = &http.Server{Addr: fmt.Sprintf(":%d", s.port), Handler: mux}
s.running = true
go func() {
if err := s.server.ListenAndServe(); nil != err && err != http.ErrServerClosed {
logger.Error("Alert service error on port %d: %v", s.port, err)
}
}()
logger.Info("Alert service started on port %d", s.port)
return nil
}
// handleNotify принимает POST-запросы с уведомлениями
func (s *AlertService) handleNotify(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
var data map[string]interface{}
if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// Отправляем всем подключённым клиентам
s.broadcast(data)
w.WriteHeader(http.StatusOK)
}
func (s *AlertService) Stop() error {
s.running = false
if s.server != nil {
return s.server.Close()
}
return nil
}
func (s *AlertService) ReloadConfig(config interface{}) error {
newCfg, ok := config.(*db.AlertWebConfig)
if !ok {
return fmt.Errorf("invalid config type, expected *db.AlertWebConfig")
}
s.config = newCfg
return nil
}
func (s *AlertService) GetPort() int { return s.port }
func (s *AlertService) GetType() string { return "alert" }
func (s *AlertService) IsRunning() bool { return s.running }
// SendToClients обрабатывает событие и рассылает его клиентам
func (s *AlertService) SendToClients(event AlertEvent) {
s.mu.RLock()
cfg := s.config
s.mu.RUnlock()
if cfg == nil {
return
}
// Обработка команды со звуком (без визуального уведомления)
if event.Type == "command_sound" && event.Sound != "" {
out := map[string]interface{}{
"title": "",
"text": "",
"duration": 1,
"image": "",
"sound": event.Sound,
}
s.broadcast(out)
return
}
// Обычные события (follow, subscribe и т.д.)
eventCfg, ok := cfg.Events[event.Type]
if !ok || !eventCfg.Enabled {
return
}
title := replacePlaceholders(eventCfg.TitleTemplate, event.Data)
text := replacePlaceholders(eventCfg.TextTemplate, event.Data)
duration := eventCfg.DurationSec
if duration == 0 {
duration = cfg.DefaultDuration
}
image := eventCfg.ImageFile
if image == "" {
image = cfg.DefaultImage
}
sound := eventCfg.SoundFile
if sound == "" {
sound = cfg.DefaultSound
}
out := map[string]interface{}{
"title": title,
"text": text,
"duration": duration,
"image": image,
"sound": sound,
}
s.broadcast(out)
}
func (s *AlertService) handleIndex(w http.ResponseWriter, _ *http.Request) {
tmpl := `<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Alerts Overlay</title>
<style>
body { margin: 0; overflow: hidden; font-family: 'Arial', sans-serif; }
.alert-container { position: fixed; bottom: 20px; right: 20px; width: 300px; z-index: 1000; }
.alert { background: rgba(0,0,0,0.8); color: white; border-radius: 8px; padding: 10px; margin-bottom: 10px; animation: slideIn 0.5s ease; display: flex; align-items: center; gap: 10px; }
.alert img { max-width: 60px; max-height: 60px; border-radius: 50%; }
.alert .content { flex: 1; }
.alert .title { font-weight: bold; font-size: 1.2em; }
.alert .text { font-size: 0.9em; }
@keyframes slideIn { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } }
</style>
</head>
<body>
<div id="alerts" class="alert-container"></div>
<script>
const eventSource = new EventSource('/events');
eventSource.onmessage = function(e) { showAlert(JSON.parse(e.data)); };
function showAlert(ev) {
const container = document.getElementById('alerts');
const div = document.createElement('div');
div.className = 'alert';
let imgHtml = ev.image ? '<img src="' + ev.image + '">' : '';
div.innerHTML = imgHtml + '<div class="content"><div class="title">' + escapeHtml(ev.title) + '</div><div class="text">' + escapeHtml(ev.text) + '</div></div>';
container.appendChild(div);
setTimeout(() => div.remove(), ev.duration * 1000);
if (ev.sound) new Audio(ev.sound).play().catch(e => console.log(e));
}
function escapeHtml(str) { if (!str) return ''; return str.replace(/[&<>]/g, function(m) { if (m === '&') return '&amp;'; if (m === '<') return '&lt;'; if (m === '>') return '&gt;'; return m; }); }
</script>
</body>
</html>`
t := template.Must(template.New("alert").Parse(tmpl))
_ = t.Execute(w, nil)
}
func (s *AlertService) handleEvents(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Connection", "keep-alive")
flusher, ok := w.(http.Flusher)
if !ok {
http.Error(w, "Streaming unsupported", http.StatusInternalServerError)
return
}
clientChan := make(chan interface{}, 100)
s.clientsMu.Lock()
s.clients[clientChan] = true
s.clientsMu.Unlock()
defer func() {
s.clientsMu.Lock()
delete(s.clients, clientChan)
s.clientsMu.Unlock()
close(clientChan)
}()
for {
select {
case data := <-clientChan:
jsonData, _ := json.Marshal(data)
_, _ = fmt.Fprintf(w, "data: %s\n\n", jsonData)
flusher.Flush()
case <-r.Context().Done():
return
}
}
}
func replacePlaceholders(tmpl string, data map[string]interface{}) string {
res := tmpl
for k, v := range data {
res = strings.ReplaceAll(res, "{"+k+"}", fmt.Sprintf("%v", v))
}
return res
}