153 lines
5.7 KiB
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 '&'; if (m === '<') return '<'; if (m === '>') return '>'; 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
|
|
}
|
|
}
|
|
} |