TTW_Bot_GO/internal/webservices/chat.go

153 lines
5.7 KiB
Go

package webservices
import (
"encoding/json"
"fmt"
"html/template"
"net/http"
"stream-bot/internal/db"
"stream-bot/internal/logger"
)
type ChatService struct {
*baseService
config *db.ChatWebConfig
server *http.Server
}
func NewChatService(port int, config *db.ChatWebConfig) *ChatService {
return &ChatService{
baseService: newBaseService(port),
config: config,
}
}
func (s *ChatService) Start() error {
mux := http.NewServeMux()
mux.HandleFunc("/", s.handleIndex)
mux.HandleFunc("/events", s.handleEvents)
s.server = &http.Server{Addr: fmt.Sprintf(":%d", s.port), Handler: mux}
s.running = true
go func() {
if err := s.server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
logger.Error("Chat service error on port %d: %v", s.port, err)
}
}()
logger.Info("Chat service started on port %d", s.port)
return nil
}
func (s *ChatService) Stop() error {
s.running = false
if s.server != nil {
return s.server.Close()
}
return nil
}
func (s *ChatService) ReloadConfig(config interface{}) error {
newCfg, ok := config.(*db.ChatWebConfig)
if !ok {
return fmt.Errorf("invalid config type, expected *db.ChatWebConfig")
}
s.config = newCfg
return nil
}
func (s *ChatService) GetPort() int { return s.port }
func (s *ChatService) GetType() string { return "chat" }
func (s *ChatService) IsRunning() bool { return s.running }
// SendToClients отправляет сообщение всем подключённым SSE-клиентам
func (s *ChatService) SendToClients(msg ChatMessage) {
s.broadcast(msg)
}
func (s *ChatService) handleIndex(w http.ResponseWriter, r *http.Request) {
tmpl := `<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Chat Overlay</title>
<style>
body { background-color: {{.BackgroundColor}}; color: {{.TextColor}}; font-family: {{.FontFamily}}; font-size: {{.FontSize}}px; opacity: {{.Opacity}}%; margin: 0; padding: 10px; overflow: hidden; }
.message { margin-bottom: 8px; padding: 4px; border-bottom: 1px solid rgba(255,255,255,0.2); animation: fadeIn 0.3s; }
.badge { display: inline-block; margin-right: 4px; }
.username { font-weight: bold; margin-right: 8px; }
.mod { color: #34eb5e; }
.vip { color: #e8b92e; }
.sub { color: #9147ff; }
.time { font-size: 0.8em; color: #aaa; margin-right: 8px; }
@keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
</style>
</head>
<body>
<div id="messages"></div>
<script>
if (window.eventSource) window.eventSource.close();
const eventSource = new EventSource('/events');
window.eventSource = eventSource;
let messageCounter = 0;
const maxMessages = {{.MaxMessages}};
const messageTimeout = {{.MessageTimeoutSec}} * 1000;
let messageQueue = [];
function addMessage(msg) {
messageCounter++;
console.log('#' + messageCounter, msg.username + ':', msg.message);
const container = document.getElementById('messages');
const div = document.createElement('div');
div.className = 'message';
let badgeHtml = '';
if (msg.is_mod) badgeHtml += '<span class="badge mod">🎭</span>';
if (msg.is_vip) badgeHtml += '<span class="badge vip">👑</span>';
if (msg.is_sub) badgeHtml += '<span class="badge sub">✔️</span>';
let timeHtml = msg.timestamp ? '<span class="time">' + new Date(msg.timestamp*1000).toLocaleTimeString() + '</span>' : '';
div.innerHTML = timeHtml + badgeHtml + '<span class="username">' + escapeHtml(msg.username) + ':</span> ' + escapeHtml(msg.message);
container.appendChild(div);
messageQueue.push(div);
if (messageQueue.length > maxMessages) { const oldest = messageQueue.shift(); oldest.remove(); }
if (messageTimeout > 0) { setTimeout(() => { const idx = messageQueue.indexOf(div); if (idx !== -1) { messageQueue.splice(idx, 1); div.remove(); } }, messageTimeout); }
container.scrollTop = container.scrollHeight;
}
eventSource.onmessage = function(e) { addMessage(JSON.parse(e.data)); };
eventSource.onerror = function(e) { console.error('SSE error', 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("chat").Parse(tmpl))
t.Execute(w, s.config)
}
func (s *ChatService) 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")
w.Header().Set("Access-Control-Allow-Origin", "*")
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
}
}
}