729 lines
18 KiB
Go
729 lines
18 KiB
Go
package websocket
|
|
|
|
import (
|
|
"api-service/internal/config"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/gorilla/websocket"
|
|
)
|
|
|
|
// Client mewakili koneksi klien WebSocket
|
|
type Client struct {
|
|
// Identifikasi
|
|
ID string
|
|
StaticID string
|
|
UserID string
|
|
Room string
|
|
|
|
// Koneksi
|
|
IPAddress string
|
|
Conn *websocket.Conn
|
|
Send chan WebSocketMessage
|
|
|
|
// Hub
|
|
Hub *Hub
|
|
|
|
// Context
|
|
ctx context.Context
|
|
cancel context.CancelFunc
|
|
|
|
// Status
|
|
lastPing time.Time
|
|
lastPong time.Time
|
|
connectedAt time.Time
|
|
isActive bool
|
|
|
|
// Sinkronisasi
|
|
mu sync.RWMutex
|
|
}
|
|
|
|
// NewClient membuat klien baru
|
|
func NewClient(
|
|
id, staticID, userID, room, ipAddress string,
|
|
conn *websocket.Conn,
|
|
hub *Hub,
|
|
config config.WebSocketConfig,
|
|
) *Client {
|
|
ctx, cancel := context.WithCancel(hub.ctx)
|
|
|
|
return &Client{
|
|
ID: id,
|
|
StaticID: staticID,
|
|
UserID: userID,
|
|
Room: room,
|
|
IPAddress: ipAddress,
|
|
Conn: conn,
|
|
Send: make(chan WebSocketMessage, config.ChannelBufferSize),
|
|
Hub: hub,
|
|
ctx: ctx,
|
|
cancel: cancel,
|
|
lastPing: time.Now(),
|
|
connectedAt: time.Now(),
|
|
isActive: true,
|
|
}
|
|
}
|
|
|
|
// readPup menangani pembacaan pesan dari klien
|
|
func (c *Client) readPump() {
|
|
defer func() {
|
|
c.Hub.unregister <- c
|
|
c.Conn.Close()
|
|
}()
|
|
|
|
// Konfigurasi koneksi
|
|
c.Conn.SetReadLimit(int64(c.Hub.config.MaxMessageSize))
|
|
c.resetReadDeadline()
|
|
|
|
// Setup ping/pong handlers
|
|
c.Conn.SetPingHandler(func(message string) error {
|
|
c.resetReadDeadline()
|
|
return c.Conn.WriteControl(websocket.PongMessage, []byte(message), time.Now().Add(c.Hub.config.WriteTimeout))
|
|
})
|
|
|
|
c.Conn.SetPongHandler(func(message string) error {
|
|
c.mu.Lock()
|
|
c.lastPong = time.Now()
|
|
c.isActive = true
|
|
c.mu.Unlock()
|
|
c.resetReadDeadline()
|
|
return nil
|
|
})
|
|
|
|
for {
|
|
select {
|
|
case <-c.ctx.Done():
|
|
return
|
|
default:
|
|
_, message, err := c.Conn.ReadMessage()
|
|
if err != nil {
|
|
if websocket.IsUnexpectedCloseError(err,
|
|
websocket.CloseGoingAway,
|
|
websocket.CloseAbnormalClosure,
|
|
websocket.CloseNormalClosure) {
|
|
c.Hub.errorCount++
|
|
}
|
|
return
|
|
}
|
|
|
|
// Reset deadline setiap kali ada pesan masuk
|
|
c.resetReadDeadline()
|
|
c.updateLastActivity()
|
|
|
|
// Parse pesan
|
|
var msg WebSocketMessage
|
|
if err := json.Unmarshal(message, &msg); err != nil {
|
|
c.sendErrorResponse("Invalid message format", err.Error())
|
|
continue
|
|
}
|
|
|
|
// Set metadata pesan
|
|
msg.Timestamp = time.Now()
|
|
msg.ClientID = c.ID
|
|
if msg.MessageID == "" {
|
|
msg.MessageID = uuid.New().String()
|
|
}
|
|
|
|
// Proses pesan menggunakan registry
|
|
c.handleMessage(msg)
|
|
}
|
|
}
|
|
}
|
|
|
|
// writePump menangani pengiriman pesan ke klien
|
|
func (c *Client) writePump() {
|
|
ticker := time.NewTicker(c.Hub.config.PingInterval)
|
|
defer func() {
|
|
ticker.Stop()
|
|
c.Conn.Close()
|
|
}()
|
|
|
|
for {
|
|
select {
|
|
case message, ok := <-c.Send:
|
|
c.Conn.SetWriteDeadline(time.Now().Add(c.Hub.config.WriteTimeout))
|
|
if !ok {
|
|
c.Conn.WriteMessage(websocket.CloseMessage, []byte{})
|
|
return
|
|
}
|
|
|
|
if err := c.Conn.WriteJSON(message); err != nil {
|
|
c.Hub.errorCount++
|
|
return
|
|
}
|
|
|
|
case <-ticker.C:
|
|
// Kirim ping dan periksa respons
|
|
if err := c.sendPing(); err != nil {
|
|
c.Hub.errorCount++
|
|
return
|
|
}
|
|
|
|
// Periksa timeout pong
|
|
if c.isPongTimeout() {
|
|
return
|
|
}
|
|
|
|
case <-c.ctx.Done():
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// handleMessage memproses pesan masuk
|
|
func (c *Client) handleMessage(msg WebSocketMessage) {
|
|
// Dapatkan handler dari registry
|
|
handler, exists := c.Hub.messageRegistry.GetHandler(msg.Type)
|
|
if !exists {
|
|
// Handler tidak ditemukan, gunakan handler default
|
|
c.handleDefaultMessage(msg)
|
|
return
|
|
}
|
|
|
|
// Jalankan handler
|
|
if err := handler.HandleMessage(c, msg); err != nil {
|
|
c.sendErrorResponse("Error handling message", err.Error())
|
|
c.Hub.errorCount++
|
|
}
|
|
}
|
|
|
|
// handleDefaultMessage menangani pesan tanpa handler khusus
|
|
func (c *Client) handleDefaultMessage(msg WebSocketMessage) {
|
|
switch msg.Type {
|
|
case "broadcast":
|
|
c.Hub.broadcast <- msg
|
|
c.sendDirectResponse("broadcast_sent", "Message broadcasted to all clients")
|
|
|
|
case "direct_message":
|
|
c.handleDirectMessage(msg)
|
|
|
|
case "ping":
|
|
c.handlePing(msg)
|
|
|
|
case "pong":
|
|
// Pong sudah ditangani di level koneksi
|
|
break
|
|
|
|
case "heartbeat":
|
|
c.handleHeartbeat(msg)
|
|
|
|
case "connection_test":
|
|
c.handleConnectionTest(msg)
|
|
|
|
case "get_online_users":
|
|
c.sendOnlineUsers()
|
|
|
|
case "join_room":
|
|
c.handleJoinRoom(msg)
|
|
|
|
case "leave_room":
|
|
c.handleLeaveRoom(msg)
|
|
|
|
case "get_room_info":
|
|
c.handleGetRoomInfo(msg)
|
|
|
|
case "database_query", "db_query":
|
|
c.handleDatabaseQuery(msg)
|
|
|
|
case "db_insert":
|
|
c.handleDatabaseInsert(msg)
|
|
|
|
case "db_custom_query":
|
|
c.handleDatabaseCustomQuery(msg)
|
|
|
|
case "get_stats":
|
|
c.handleGetStats(msg)
|
|
|
|
case "get_server_stats":
|
|
c.handleGetServerStats(msg)
|
|
|
|
case "get_system_health":
|
|
c.handleGetSystemHealth(msg)
|
|
|
|
case "admin_kick_client":
|
|
c.handleAdminKickClient(msg)
|
|
|
|
case "admin_kill_server":
|
|
c.handleAdminKillServer(msg)
|
|
|
|
case "admin_clear_logs":
|
|
c.handleAdminClearLogs(msg)
|
|
|
|
default:
|
|
c.sendDirectResponse("message_received", fmt.Sprintf("Message received: %v", msg.Data))
|
|
c.Hub.broadcast <- msg
|
|
}
|
|
}
|
|
|
|
// Metode helper lainnya...
|
|
func (c *Client) resetReadDeadline() {
|
|
c.Conn.SetReadDeadline(time.Now().Add(c.Hub.config.ReadTimeout))
|
|
}
|
|
|
|
func (c *Client) updateLastActivity() {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
c.lastPing = time.Now()
|
|
c.isActive = true
|
|
}
|
|
|
|
func (c *Client) sendPing() error {
|
|
c.Conn.SetWriteDeadline(time.Now().Add(c.Hub.config.WriteTimeout))
|
|
if err := c.Conn.WriteMessage(websocket.PingMessage, nil); err != nil {
|
|
return err
|
|
}
|
|
|
|
c.mu.Lock()
|
|
c.lastPing = time.Now()
|
|
c.mu.Unlock()
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *Client) isPongTimeout() bool {
|
|
c.mu.RLock()
|
|
defer c.mu.RUnlock()
|
|
|
|
lastActivity := c.lastPong
|
|
if lastActivity.IsZero() {
|
|
lastActivity = c.lastPing
|
|
}
|
|
|
|
return time.Since(lastActivity) > c.Hub.config.PongTimeout
|
|
}
|
|
|
|
func (c *Client) isClientActive() bool {
|
|
c.mu.RLock()
|
|
defer c.mu.RUnlock()
|
|
return c.isActive && time.Since(c.lastPing) < c.Hub.config.PongTimeout
|
|
}
|
|
|
|
func (c *Client) sendDirectResponse(messageType string, data interface{}) {
|
|
response := NewWebSocketMessage(MessageType(messageType), data, c.ID, c.Room)
|
|
|
|
select {
|
|
case c.Send <- response:
|
|
default:
|
|
// Channel penuh, abaikan pesan
|
|
}
|
|
}
|
|
|
|
func (c *Client) sendErrorResponse(error, details string) {
|
|
c.sendDirectResponse("error", map[string]interface{}{
|
|
"error": error,
|
|
"details": details,
|
|
})
|
|
}
|
|
|
|
func (c *Client) gracefulClose() {
|
|
c.mu.Lock()
|
|
c.isActive = false
|
|
c.mu.Unlock()
|
|
|
|
// Kirim pesan close
|
|
c.Conn.SetWriteDeadline(time.Now().Add(c.Hub.config.WriteTimeout))
|
|
c.Conn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""))
|
|
|
|
// Batalkan context
|
|
c.cancel()
|
|
}
|
|
|
|
// ReadPump mengekspos metode readPump untuk handler
|
|
func (c *Client) ReadPump() {
|
|
c.readPump()
|
|
}
|
|
|
|
// WritePump mengekspos metode writePump untuk handler
|
|
func (c *Client) WritePump() {
|
|
c.writePump()
|
|
}
|
|
|
|
// handleDirectMessage menangani pesan direct message
|
|
func (c *Client) handleDirectMessage(msg WebSocketMessage) {
|
|
data, ok := msg.Data.(map[string]interface{})
|
|
if !ok {
|
|
c.sendErrorResponse("Invalid direct message format", "Data must be an object")
|
|
return
|
|
}
|
|
|
|
targetClientID, ok := data["target_client_id"].(string)
|
|
if !ok || targetClientID == "" {
|
|
c.sendErrorResponse("Invalid target client ID", "target_client_id is required")
|
|
return
|
|
}
|
|
|
|
message, ok := data["message"].(string)
|
|
if !ok || message == "" {
|
|
c.sendErrorResponse("Invalid message content", "message is required")
|
|
return
|
|
}
|
|
|
|
// Buat pesan direct message
|
|
directMsg := NewWebSocketMessage(DirectMessage, map[string]interface{}{
|
|
"from_client_id": c.ID,
|
|
"from_user_id": c.UserID,
|
|
"message": message,
|
|
"timestamp": msg.Timestamp,
|
|
}, targetClientID, "")
|
|
|
|
// Kirim ke target client
|
|
select {
|
|
case c.Hub.broadcast <- directMsg:
|
|
c.sendDirectResponse("direct_message_sent", map[string]interface{}{
|
|
"target_client_id": targetClientID,
|
|
"message": message,
|
|
})
|
|
default:
|
|
c.sendErrorResponse("Failed to send direct message", "Message queue is full")
|
|
}
|
|
}
|
|
|
|
// handlePing menangani ping message
|
|
func (c *Client) handlePing(msg WebSocketMessage) {
|
|
// Kirim pong response
|
|
pongMsg := NewWebSocketMessage(PongMessage, map[string]interface{}{
|
|
"timestamp": msg.Timestamp.Unix(),
|
|
"client_id": c.ID,
|
|
}, c.ID, "")
|
|
|
|
select {
|
|
case c.Send <- pongMsg:
|
|
default:
|
|
// Channel penuh, abaikan
|
|
}
|
|
}
|
|
|
|
// handleHeartbeat menangani heartbeat message
|
|
func (c *Client) handleHeartbeat(msg WebSocketMessage) {
|
|
// Kirim heartbeat acknowledgment
|
|
heartbeatAck := NewWebSocketMessage(MessageType("heartbeat_ack"), map[string]interface{}{
|
|
"timestamp": time.Now().Unix(),
|
|
"client_uptime": time.Since(c.connectedAt).Seconds(),
|
|
"server_uptime": time.Since(c.Hub.startTime).Seconds(),
|
|
"client_id": c.ID,
|
|
}, c.ID, "")
|
|
|
|
select {
|
|
case c.Send <- heartbeatAck:
|
|
default:
|
|
// Channel penuh, abaikan
|
|
}
|
|
}
|
|
|
|
// handleConnectionTest menangani connection test
|
|
func (c *Client) handleConnectionTest(msg WebSocketMessage) {
|
|
// Kirim connection test result
|
|
testResult := NewWebSocketMessage(MessageType("connection_test_result"), map[string]interface{}{
|
|
"timestamp": time.Now().Unix(),
|
|
"client_id": c.ID,
|
|
"connection_status": "healthy",
|
|
"latency_ms": 0, // Could be calculated if ping timestamp is provided
|
|
"uptime_seconds": time.Since(c.connectedAt).Seconds(),
|
|
}, c.ID, "")
|
|
|
|
select {
|
|
case c.Send <- testResult:
|
|
default:
|
|
// Channel penuh, abaikan
|
|
}
|
|
}
|
|
|
|
// handleJoinRoom menangani join room
|
|
func (c *Client) handleJoinRoom(msg WebSocketMessage) {
|
|
data, ok := msg.Data.(map[string]interface{})
|
|
if !ok {
|
|
c.sendErrorResponse("Invalid join room format", "Data must be an object")
|
|
return
|
|
}
|
|
|
|
roomName, ok := data["room"].(string)
|
|
if !ok || roomName == "" {
|
|
c.sendErrorResponse("Invalid room name", "room is required")
|
|
return
|
|
}
|
|
|
|
// Update client room
|
|
oldRoom := c.Room
|
|
c.Room = roomName
|
|
|
|
// Update hub room mapping
|
|
c.Hub.mu.Lock()
|
|
if oldRoom != "" {
|
|
if roomClients, exists := c.Hub.rooms[oldRoom]; exists {
|
|
delete(roomClients, c)
|
|
if len(roomClients) == 0 {
|
|
delete(c.Hub.rooms, oldRoom)
|
|
}
|
|
}
|
|
}
|
|
|
|
if c.Hub.rooms[roomName] == nil {
|
|
c.Hub.rooms[roomName] = make(map[*Client]bool)
|
|
}
|
|
c.Hub.rooms[roomName][c] = true
|
|
c.Hub.mu.Unlock()
|
|
|
|
// Log activity
|
|
c.Hub.logActivity("client_join_room", c.ID, fmt.Sprintf("Room: %s", roomName))
|
|
|
|
// Kirim response
|
|
c.sendDirectResponse("room_joined", map[string]interface{}{
|
|
"room": roomName,
|
|
"previous_room": oldRoom,
|
|
})
|
|
}
|
|
|
|
// handleLeaveRoom menangani leave room
|
|
func (c *Client) handleLeaveRoom(msg WebSocketMessage) {
|
|
oldRoom := c.Room
|
|
|
|
if oldRoom == "" {
|
|
c.sendErrorResponse("Not in any room", "Client is not currently in a room")
|
|
return
|
|
}
|
|
|
|
// Update hub room mapping
|
|
c.Hub.mu.Lock()
|
|
if roomClients, exists := c.Hub.rooms[oldRoom]; exists {
|
|
delete(roomClients, c)
|
|
if len(roomClients) == 0 {
|
|
delete(c.Hub.rooms, oldRoom)
|
|
}
|
|
}
|
|
c.Hub.mu.Unlock()
|
|
|
|
// Clear client room
|
|
c.Room = ""
|
|
|
|
// Log activity
|
|
c.Hub.logActivity("client_leave_room", c.ID, fmt.Sprintf("Room: %s", oldRoom))
|
|
|
|
// Kirim response
|
|
c.sendDirectResponse("room_left", map[string]interface{}{
|
|
"room": oldRoom,
|
|
})
|
|
}
|
|
|
|
// handleGetRoomInfo menangani get room info
|
|
func (c *Client) handleGetRoomInfo(msg WebSocketMessage) {
|
|
c.Hub.mu.RLock()
|
|
defer c.Hub.mu.RUnlock()
|
|
|
|
roomInfo := make(map[string]interface{})
|
|
|
|
// Info ruangan saat ini
|
|
if c.Room != "" {
|
|
if roomClients, exists := c.Hub.rooms[c.Room]; exists {
|
|
clientIDs := make([]string, 0, len(roomClients))
|
|
for client := range roomClients {
|
|
clientIDs = append(clientIDs, client.ID)
|
|
}
|
|
roomInfo["current_room"] = map[string]interface{}{
|
|
"name": c.Room,
|
|
"client_count": len(roomClients),
|
|
"clients": clientIDs,
|
|
}
|
|
}
|
|
}
|
|
|
|
// Info semua ruangan
|
|
allRooms := make(map[string]int)
|
|
for roomName, clients := range c.Hub.rooms {
|
|
allRooms[roomName] = len(clients)
|
|
}
|
|
roomInfo["all_rooms"] = allRooms
|
|
roomInfo["total_rooms"] = len(c.Hub.rooms)
|
|
|
|
c.sendDirectResponse("room_info", roomInfo)
|
|
}
|
|
|
|
// handleDatabaseInsert menangani database insert
|
|
func (c *Client) handleDatabaseInsert(msg WebSocketMessage) {
|
|
data, ok := msg.Data.(map[string]interface{})
|
|
if !ok {
|
|
c.sendErrorResponse("Invalid database insert format", "Data must be an object")
|
|
return
|
|
}
|
|
|
|
table, ok := data["table"].(string)
|
|
if !ok || table == "" {
|
|
c.sendErrorResponse("Invalid table name", "table is required")
|
|
return
|
|
}
|
|
|
|
insertData, ok := data["data"].(map[string]interface{})
|
|
if !ok {
|
|
c.sendErrorResponse("Invalid insert data", "data must be an object")
|
|
return
|
|
}
|
|
|
|
// For now, just acknowledge the insert request
|
|
// In a real implementation, you would perform the actual database insert
|
|
c.sendDirectResponse("db_insert_result", map[string]interface{}{
|
|
"table": table,
|
|
"status": "acknowledged",
|
|
"message": "Insert request received (not implemented)",
|
|
"data": insertData, // Use the variable to avoid unused error
|
|
})
|
|
}
|
|
|
|
// handleDatabaseCustomQuery menangani custom database query
|
|
func (c *Client) handleDatabaseCustomQuery(msg WebSocketMessage) {
|
|
data, ok := msg.Data.(map[string]interface{})
|
|
if !ok {
|
|
c.sendErrorResponse("Invalid database query format", "Data must be an object")
|
|
return
|
|
}
|
|
|
|
database, ok := data["database"].(string)
|
|
if !ok || database == "" {
|
|
database = "default"
|
|
}
|
|
|
|
query, ok := data["query"].(string)
|
|
if !ok || query == "" {
|
|
c.sendErrorResponse("Invalid query", "query is required")
|
|
return
|
|
}
|
|
|
|
// For now, just acknowledge the query request
|
|
// In a real implementation, you would execute the query
|
|
c.sendDirectResponse("db_query_result", map[string]interface{}{
|
|
"database": database,
|
|
"query": query,
|
|
"status": "acknowledged",
|
|
"message": "Query request received (not implemented)",
|
|
})
|
|
}
|
|
|
|
// handleGetStats menangani get stats
|
|
func (c *Client) handleGetStats(msg WebSocketMessage) {
|
|
stats := c.Hub.GetStats()
|
|
c.sendDirectResponse("stats", stats)
|
|
}
|
|
|
|
// handleGetServerStats menangani get server stats
|
|
func (c *Client) handleGetServerStats(msg WebSocketMessage) {
|
|
// Create monitoring manager instance
|
|
monitoringManager := NewMonitoringManager(c.Hub)
|
|
detailedStats := monitoringManager.GetDetailedStats()
|
|
c.sendDirectResponse("server_stats", detailedStats)
|
|
}
|
|
|
|
// handleGetSystemHealth menangani get system health
|
|
func (c *Client) handleGetSystemHealth(msg WebSocketMessage) {
|
|
systemHealth := make(map[string]interface{})
|
|
if c.Hub.dbService != nil {
|
|
systemHealth["databases"] = c.Hub.dbService.Health()
|
|
systemHealth["available_dbs"] = c.Hub.dbService.ListDBs()
|
|
}
|
|
systemHealth["websocket_status"] = "healthy"
|
|
systemHealth["uptime_seconds"] = time.Since(c.Hub.startTime).Seconds()
|
|
|
|
c.sendDirectResponse("system_health", systemHealth)
|
|
}
|
|
|
|
// handleAdminKickClient menangani admin kick client
|
|
func (c *Client) handleAdminKickClient(msg WebSocketMessage) {
|
|
// This should be protected by authentication, but for now just acknowledge
|
|
data, ok := msg.Data.(map[string]interface{})
|
|
if !ok {
|
|
c.sendErrorResponse("Invalid admin command format", "Data must be an object")
|
|
return
|
|
}
|
|
|
|
targetClientID, ok := data["client_id"].(string)
|
|
if !ok || targetClientID == "" {
|
|
c.sendErrorResponse("Invalid target client ID", "client_id is required")
|
|
return
|
|
}
|
|
|
|
// For now, just acknowledge the kick request
|
|
// In a real implementation, you would check admin permissions and kick the client
|
|
c.sendDirectResponse("admin_command_result", map[string]interface{}{
|
|
"command": "kick_client",
|
|
"target_client_id": targetClientID,
|
|
"status": "acknowledged",
|
|
"message": "Kick request received (not implemented)",
|
|
})
|
|
}
|
|
|
|
// handleAdminKillServer menangani admin kill server
|
|
func (c *Client) handleAdminKillServer(msg WebSocketMessage) {
|
|
// This should be protected by authentication, but for now just acknowledge
|
|
c.sendDirectResponse("admin_command_result", map[string]interface{}{
|
|
"command": "kill_server",
|
|
"status": "acknowledged",
|
|
"message": "Kill server request received (not implemented - would require admin auth)",
|
|
})
|
|
}
|
|
|
|
// handleAdminClearLogs menangani admin clear logs
|
|
func (c *Client) handleAdminClearLogs(msg WebSocketMessage) {
|
|
// This should be protected by authentication, but for now just acknowledge
|
|
c.sendDirectResponse("admin_command_result", map[string]interface{}{
|
|
"command": "clear_logs",
|
|
"status": "acknowledged",
|
|
"message": "Clear logs request received (not implemented - would require admin auth)",
|
|
})
|
|
}
|
|
|
|
// Implementasi metode lainnya...
|
|
func (c *Client) handleDatabaseQuery(msg WebSocketMessage) {
|
|
// Implementasi yang sama seperti sebelumnya
|
|
data, ok := msg.Data.(map[string]interface{})
|
|
if !ok {
|
|
c.sendErrorResponse("Invalid database query format", "Data must be an object")
|
|
return
|
|
}
|
|
|
|
table, ok := data["table"].(string)
|
|
if !ok || table == "" {
|
|
c.sendErrorResponse("Invalid table name", "table is required")
|
|
return
|
|
}
|
|
|
|
// For now, just acknowledge the query request
|
|
c.sendDirectResponse("db_query_result", map[string]interface{}{
|
|
"table": table,
|
|
"status": "acknowledged",
|
|
"message": "Query request received (not implemented)",
|
|
})
|
|
}
|
|
|
|
func (c *Client) sendOnlineUsers() {
|
|
c.Hub.mu.RLock()
|
|
defer c.Hub.mu.RUnlock()
|
|
|
|
users := make([]map[string]interface{}, 0, len(c.Hub.clients))
|
|
for client := range c.Hub.clients {
|
|
user := map[string]interface{}{
|
|
"id": client.ID,
|
|
"static_id": client.StaticID,
|
|
"user_id": client.UserID,
|
|
"room": client.Room,
|
|
"ip_address": client.IPAddress,
|
|
"connected_at": client.connectedAt,
|
|
"last_ping": client.lastPing,
|
|
"last_pong": client.lastPong,
|
|
"is_active": client.isClientActive(),
|
|
}
|
|
users = append(users, user)
|
|
}
|
|
|
|
response := NewWebSocketMessage(OnlineUsersMessage, map[string]interface{}{
|
|
"users": users,
|
|
"count": len(users),
|
|
}, c.ID, "")
|
|
|
|
select {
|
|
case c.Send <- response:
|
|
default:
|
|
// Channel penuh, abaikan
|
|
}
|
|
}
|