madipo2611 8c84a5d7d5
All checks were successful
continuous-integration/drone/push Build is passing
v0.0.17.4
2025-08-11 19:04:08 +03:00

313 lines
8.0 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 handlers
import (
"context"
"encoding/json"
"fmt"
"log"
"net/http"
"tailly_back_v2/internal/domain"
"tailly_back_v2/internal/service"
"tailly_back_v2/internal/ws"
"tailly_back_v2/pkg/auth"
"time"
"github.com/gorilla/websocket"
)
var upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
CheckOrigin: func(r *http.Request) bool {
return true
},
Subprotocols: []string{"graphql-transport-ws"},
}
type ChatHandler struct {
chatService service.ChatService
hub *ws.Hub
tokenAuth *auth.TokenAuth
}
func NewChatHandler(chatService service.ChatService, hub *ws.Hub, tokenAuth *auth.TokenAuth) *ChatHandler {
return &ChatHandler{
chatService: chatService,
hub: hub,
tokenAuth: tokenAuth,
}
}
func (h *ChatHandler) HandleWebSocket(w http.ResponseWriter, r *http.Request) {
log.Printf("Incoming WebSocket headers: %+v", r.Header)
log.Printf("Cookies: %+v", r.Cookies())
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Printf("WebSocket upgrade error: %v", err)
return
}
// Создаем клиента без userID (он будет установлен при аутентификации)
client := &ws.Client{
Send: make(chan *domain.Message, 256),
LastSeen: time.Now(),
CloseChan: make(chan bool, 1),
}
h.hub.RegisterClient(client)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// Горутина для чтения (теперь включает аутентификацию)
go h.readPump(ctx, conn, client)
// Горутина для записи с обработкой закрытия
go func() {
defer conn.Close()
defer h.hub.UnregisterClient(client)
for {
select {
case <-ctx.Done():
return
case <-client.CloseChan:
return
case message, ok := <-client.Send:
if !ok {
return
}
// Добавляем таймаут на запись
conn.SetWriteDeadline(time.Now().Add(10 * time.Second))
if err := conn.WriteJSON(message); err != nil {
log.Printf("WebSocket write error: %v", err)
return
}
conn.SetWriteDeadline(time.Time{}) // Сбрасываем таймаут
}
}
}()
}
func (h *ChatHandler) readPump(ctx context.Context, conn *websocket.Conn, client *ws.Client) {
ticker := time.NewTicker(30 * time.Second)
defer ticker.Stop()
defer conn.Close()
// 1. Аутентификация
if err := h.authenticateConnection(conn, client); err != nil {
log.Printf("Authentication failed: %v", err)
return
}
// 2. Основной цикл обработки сообщений
for {
select {
case <-ticker.C:
if err := h.sendPing(conn); err != nil {
return
}
case <-ctx.Done():
return
default:
if err := h.handleMessage(ctx, conn, client); err != nil {
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
log.Printf("WebSocket error: %v", err)
}
return
}
}
}
}
func (h *ChatHandler) authenticateConnection(conn *websocket.Conn, client *ws.Client) error {
var authMsg struct {
Type string `json:"type"`
Token string `json:"token"`
}
// Устанавливаем таймаут для аутентификации
conn.SetReadDeadline(time.Now().Add(10 * time.Second))
defer conn.SetReadDeadline(time.Time{})
if err := conn.ReadJSON(&authMsg); err != nil {
conn.WriteJSON(map[string]string{"type": "error", "message": "Auth required"})
return fmt.Errorf("failed to read auth message: %v", err)
}
if authMsg.Type != "auth" || authMsg.Token == "" {
conn.WriteJSON(map[string]string{"type": "error", "message": "Invalid auth message"})
return fmt.Errorf("first message must be auth with token")
}
userID, err := h.tokenAuth.ValidateAccessToken(authMsg.Token)
if err != nil {
conn.WriteJSON(map[string]string{"type": "error", "message": "Invalid token"})
return fmt.Errorf("token validation error: %v", err)
}
client.UserID = userID
log.Printf("WebSocket authenticated, userID=%d", userID)
// Отправляем подтверждение аутентификации
if err := conn.WriteJSON(map[string]interface{}{
"type": "auth_success",
"user": map[string]interface{}{
"id": userID,
},
}); err != nil {
return fmt.Errorf("failed to send auth confirmation: %v", err)
}
return nil
}
func (h *ChatHandler) sendPing(conn *websocket.Conn) error {
if err := conn.WriteJSON(map[string]string{"type": "ping"}); err != nil {
log.Printf("Ping error: %v", err)
return err
}
return nil
}
func (h *ChatHandler) handleMessage(ctx context.Context, conn *websocket.Conn, client *ws.Client) error {
var msg struct {
Type string `json:"type"`
Payload json.RawMessage `json:"payload"` // Используем RawMessage для гибкости
}
if err := conn.ReadJSON(&msg); err != nil {
return err
}
switch msg.Type {
case "pong":
return nil
case "ping":
return conn.WriteJSON(map[string]string{"type": "pong"})
case "message":
return h.handleChatMessage(ctx, conn, client, msg.Payload)
default:
log.Printf("Unknown message type: %s", msg.Type)
return conn.WriteJSON(map[string]string{
"type": "error",
"message": "Unknown message type: " + msg.Type,
})
}
}
func (h *ChatHandler) handleChatMessage(ctx context.Context, conn *websocket.Conn, client *ws.Client, payload json.RawMessage) error {
if client.UserID == 0 {
return conn.WriteJSON(map[string]string{
"type": "error",
"message": "Not authenticated",
})
}
var messageData struct {
ReceiverID int `json:"receiverId"`
ChatID int `json:"chatId"`
Content string `json:"content"`
}
if err := json.Unmarshal(payload, &messageData); err != nil {
return conn.WriteJSON(map[string]interface{}{
"type": "error",
"message": "Invalid message format",
"details": err.Error(),
})
}
// Валидация данных сообщения
if messageData.ReceiverID == 0 && messageData.ChatID == 0 {
return conn.WriteJSON(map[string]interface{}{
"type": "error",
"message": "Either receiverId or chatId must be provided",
})
}
if messageData.Content == "" {
return conn.WriteJSON(map[string]interface{}{
"type": "error",
"message": "Message content cannot be empty",
})
}
// Определяем chatId если не указан
chatID := messageData.ChatID
if chatID == 0 {
chat, err := h.chatService.GetOrCreateChat(ctx, client.UserID, messageData.ReceiverID)
if err != nil {
log.Printf("Chat error: %v", err)
return conn.WriteJSON(map[string]interface{}{
"type": "error",
"message": "Failed to get or create chat",
"details": err.Error(),
})
}
chatID = chat.ID
}
// Отправляем сообщение
message, err := h.chatService.SendMessage(
ctx,
client.UserID,
chatID,
messageData.Content,
)
if err != nil {
log.Printf("Message send error: %v", err)
return conn.WriteJSON(map[string]interface{}{
"type": "error",
"message": "Failed to send message",
"details": err.Error(),
})
}
// Убедимся, что в сообщении есть получатель
if message.ReceiverID == 0 {
if messageData.ReceiverID != 0 {
message.ReceiverID = messageData.ReceiverID
} else {
// Если receiver не указан, определяем его через чат
chat, err := h.chatService.GetChatByID(ctx, chatID)
if err == nil {
if chat.User1ID == client.UserID {
message.ReceiverID = chat.User2ID
} else {
message.ReceiverID = chat.User1ID
}
}
}
}
// Рассылаем сообщение всем подписчикам
h.hub.Broadcast(message)
return nil
}
func (h *ChatHandler) writePump(ctx context.Context, conn *websocket.Conn, client *ws.Client) {
defer conn.Close()
for {
select {
case <-ctx.Done():
return
case message, ok := <-client.Send:
if !ok {
// Канал закрыт
conn.WriteMessage(websocket.CloseMessage, []byte{})
return
}
if err := conn.WriteJSON(message); err != nil {
log.Printf("WebSocket write error: %v", err)
return
}
}
}
}