Files

206 lines
6.1 KiB
Go

package websocket
import (
"api-service/internal/config"
ws "api-service/internal/services/websocket"
"encoding/json"
"fmt"
"net/http"
"time"
"github.com/gin-gonic/gin"
"github.com/gorilla/websocket"
)
// WebSocketHandler menangani koneksi WebSocket
type WebSocketHandler struct {
hub *ws.Hub
broadcaster *ws.Broadcaster
config config.WebSocketConfig
upgrader websocket.Upgrader
monitoringManager *ws.MonitoringManager
}
// NewWebSocketHandler membuat handler WebSocket baru
func NewWebSocketHandler(hub *ws.Hub, broadcaster *ws.Broadcaster, config config.WebSocketConfig) *WebSocketHandler {
return &WebSocketHandler{
hub: hub,
broadcaster: broadcaster,
config: config,
monitoringManager: ws.NewMonitoringManager(hub),
upgrader: websocket.Upgrader{
ReadBufferSize: config.ReadBufferSize,
WriteBufferSize: config.WriteBufferSize,
CheckOrigin: func(r *http.Request) bool {
// Dalam production, implementasikan validasi origin yang proper
return true
},
},
}
}
// HandleWebSocket menangani upgrade koneksi ke WebSocket
func (h *WebSocketHandler) HandleWebSocket(c *gin.Context) {
// Ambil parameter dari query atau header
clientID := c.Query("client_id")
if clientID == "" {
clientID = fmt.Sprintf("client_%d", time.Now().UnixNano())
}
staticID := c.Query("static_id")
userID := c.Query("user_id")
room := c.Query("room")
ipAddress := c.ClientIP()
// Upgrade koneksi HTTP ke WebSocket
conn, err := h.upgrader.Upgrade(c.Writer, c.Request, nil)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Failed to upgrade connection",
"details": err.Error(),
})
return
}
// Buat klien WebSocket baru
client := ws.NewClient(clientID, staticID, userID, room, ipAddress, conn, h.hub, h.config)
// Daftarkan klien ke hub
h.hub.RegisterChannel() <- client
// Jalankan goroutine untuk membaca dan menulis
go client.ReadPump()
go client.WritePump()
}
// GetWebSocketStats mengembalikan statistik WebSocket
func (h *WebSocketHandler) GetWebSocketStats(c *gin.Context) {
stats := h.hub.GetStats()
c.JSON(http.StatusOK, stats)
}
// TestWebSocketConnection endpoint untuk test koneksi WebSocket
func (h *WebSocketHandler) TestWebSocketConnection(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"message": "WebSocket service is running",
"status": "active",
"endpoint": "/api/v1/ws",
"parameters": gin.H{
"client_id": "optional, auto-generated if not provided",
"static_id": "optional, for persistent client identification",
"user_id": "optional, for user identification",
"room": "optional, for room-based messaging",
},
"example": "ws://meninjar.dev.rssa.id:8070/api/v1/ws?client_id=test_client&room=test_room",
"timestamp": time.Now().Unix(),
})
}
// GetDetailedStats mengembalikan statistik detail WebSocket
func (h *WebSocketHandler) GetDetailedStats(c *gin.Context) {
detailedStats := h.monitoringManager.GetDetailedStats()
c.JSON(http.StatusOK, gin.H{
"admin_stats": detailedStats,
"timestamp": time.Now().Unix(),
})
}
// DisconnectClient memutuskan koneksi klien tertentu
func (h *WebSocketHandler) DisconnectClient(c *gin.Context) {
clientID := c.Param("clientId")
success := h.monitoringManager.DisconnectClient(clientID)
c.JSON(http.StatusOK, gin.H{
"status": "force disconnect attempted",
"client_id": clientID,
"success": success,
"timestamp": time.Now().Unix(),
})
}
// CleanupInactiveClients membersihkan klien tidak aktif
func (h *WebSocketHandler) CleanupInactiveClients(c *gin.Context) {
var req struct {
InactiveMinutes int `json:"inactive_minutes"`
Force bool `json:"force"`
}
if err := c.ShouldBindJSON(&req); err != nil {
req.InactiveMinutes = 10
req.Force = false
}
cleanedCount := h.monitoringManager.CleanupInactiveClients(time.Duration(req.InactiveMinutes) * time.Minute)
c.JSON(http.StatusOK, gin.H{
"status": "admin cleanup completed",
"cleaned_clients": cleanedCount,
"inactive_minutes": req.InactiveMinutes,
"force": req.Force,
"timestamp": time.Now().Unix(),
})
}
// SendMessageToClient forwards a message to a specific connected client via an HTTP POST request.
// This version normalizes the data and uses the custom broadcaster method for clean delivery.
func (h *WebSocketHandler) SendMessageToClient(c *gin.Context) {
// Define a flexible struct to parse the incoming HTTP request body
var req struct {
Data interface{} `json:"data"`
ToClient string `json:"to_client"`
Type string `json:"type"`
}
// Parse the JSON from the HTTP request body
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// Normalize data into map[string]interface{} for consistency
var dataMap map[string]interface{}
switch v := req.Data.(type) {
case map[string]interface{}:
dataMap = v
case string:
// If data is a JSON string, try to unmarshal it
if err := json.Unmarshal([]byte(v), &dataMap); err != nil {
// If unmarshaling fails, treat it as a raw string
dataMap = map[string]interface{}{"raw": v}
}
default:
// For any other type, wrap it in a map
dataMap = map[string]interface{}{"data": v}
}
// Get the target client ID, checking the body first, then the query parameter
to := req.ToClient
if to == "" {
to = c.Query("to")
}
if to == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "target client id required (field 'to' or query param 'to')"})
return
}
msgType := req.Type
if msgType == "" {
msgType = "direct_message"
}
// Use the existing broadcaster instance from the Hub.
// DO NOT create a new broadcaster on every request.
h.broadcaster.CustomBroadcastClientToClient(
"server", // fromClientID, indicating the message is from the server itself
to, // toClientID
msgType, // messageType, as specified in the request
dataMap, // data payload, already normalized
)
// Respond with a detailed confirmation
c.JSON(http.StatusOK, gin.H{
"status": "sent",
"to_client": to,
"type": msgType,
"data": dataMap,
"timestamp": time.Now().Unix(),
})
}