Spaces:
Build error
Build error
| package main | |
| import ( | |
| "bytes" | |
| "context" | |
| "crypto/ecdsa" | |
| "crypto/hmac" | |
| "crypto/sha256" | |
| "encoding/base64" | |
| "encoding/json" | |
| "fmt" | |
| "io" | |
| "log" | |
| "math/big" | |
| "net/http" | |
| "os" | |
| "strconv" | |
| "strings" | |
| "sync" | |
| "time" | |
| "github.com/ethereum/go-ethereum" | |
| "github.com/ethereum/go-ethereum/accounts/abi" | |
| "github.com/ethereum/go-ethereum/common" | |
| "github.com/ethereum/go-ethereum/core/types" | |
| "github.com/ethereum/go-ethereum/crypto" | |
| "github.com/ethereum/go-ethereum/ethclient" | |
| "github.com/gin-gonic/gin" | |
| "github.com/golang-jwt/jwt/v5" | |
| "github.com/google/uuid" | |
| "github.com/gorilla/websocket" | |
| "github.com/joho/godotenv" | |
| // "golang.org/x/net/websocket" | |
| ) | |
| // --- Constants and Configuration --- | |
| var ( | |
| authRequired bool | |
| port string | |
| cloudflareAppID string | |
| cloudflareAppSecret string | |
| secretKey string | |
| cloudflareCallsBaseURL string | |
| cloudflareBasePath string | |
| debug bool | |
| RPC_URL string | |
| CONTRACT_ADDRESS string | |
| ABI_JSON string | |
| PRIVATE_KEY string | |
| ) | |
| func initConfig() { | |
| err := godotenv.Load() // Load .env file | |
| if err != nil { | |
| log.Println("Warning: Could not load .env file:", err) // Don't fatal, as we have defaults | |
| } | |
| // authRequired = getEnvBool("AUTH_REQUIRED", true) | |
| authRequired = false | |
| port = getEnv("PORT", "5000") | |
| cloudflareAppID = getEnv("CLOUDFLARE_APP_ID", "") | |
| cloudflareAppSecret = getEnv("CLOUDFLARE_APP_SECRET", "") | |
| secretKey = getEnv("JWT_SECRET", "thisisjustademokey") | |
| cloudflareCallsBaseURL = getEnv("CLOUDFLARE_APPS_URL", "https://rtc.live.cloudflare.com/v1/apps") | |
| cloudflareBasePath = fmt.Sprintf("%s/%s", cloudflareCallsBaseURL, cloudflareAppID) | |
| debug = getEnvBool("DEBUG", false) | |
| RPC_URL = "wss://bsc-testnet.publicnode.com" | |
| CONTRACT_ADDRESS = getEnv("CONTRACT_ADDRESS", "") | |
| ABI_JSON = getEnv("ABI_JSON", "") | |
| PRIVATE_KEY = getEnv("PRIVATE_KEY", "") | |
| if cloudflareAppID == "" || cloudflareAppSecret == "" { | |
| log.Fatal("CLOUDFLARE_APP_ID and CLOUDFLARE_APP_SECRET must be set") | |
| } | |
| // Thêm log để kiểm tra biến môi trường | |
| log.Println("Biến môi trường đã tải:") | |
| log.Println("CLOUDFLARE_APP_ID:", cloudflareAppID) | |
| log.Println("CLOUDFLARE_APP_SECRET:", cloudflareAppSecret) | |
| log.Println("JWT_SECRET:", secretKey) | |
| log.Println("CLOUDFLARE_APPS_URL:", cloudflareCallsBaseURL) | |
| log.Println("DEBUG:", debug) | |
| log.Println("RPC_URL:", RPC_URL) | |
| log.Println("CONTRACT_ADDRESS:", CONTRACT_ADDRESS) | |
| log.Println("ABI_JSON:", ABI_JSON) | |
| log.Println("PRIVATE_KEY:", PRIVATE_KEY) | |
| } | |
| // Helper function to get environment variables with default values | |
| func getEnv(key, defaultValue string) string { | |
| value := os.Getenv(key) | |
| if value == "" { | |
| return defaultValue | |
| } | |
| return value | |
| } | |
| func init() { | |
| // Initialize maps | |
| rooms.m = make(map[string]*Room) | |
| users.m = make(map[string]*User) | |
| wsConnections.m = make(map[string]map[string]*websocket.Conn) | |
| } | |
| // Helper function to get boolean environment variables | |
| func getEnvBool(key string, defaultValue bool) bool { | |
| value := os.Getenv(key) | |
| if value == "" { | |
| return defaultValue | |
| } | |
| b, err := strconv.ParseBool(value) | |
| if err != nil { | |
| return defaultValue // Return default if parsing fails | |
| } | |
| return b | |
| } | |
| // --- Data Structures --- | |
| type Room struct { | |
| RoomId string `json:"roomId"` // Thêm trường này | |
| Name string `json:"name"` | |
| Metadata map[string]interface{} `json:"metadata"` | |
| Participants []*Participant `json:"participants"` | |
| CreatedAt *big.Int `json:"createdAt"` | |
| sync.RWMutex // Protects concurrent access to the room | |
| } | |
| type Participant struct { | |
| UserID string `json:"userId"` | |
| SessionID string `json:"sessionId"` | |
| CreatedAt *big.Int `json:"createdAt"` | |
| PublishedTracks []string `json:"publishedTracks"` | |
| } | |
| type User struct { | |
| UserID string `json:"userId"` | |
| Username string `json:"username"` | |
| IsModerator bool `json:"isModerator"` | |
| Role string `json:"role"` | |
| } | |
| type SessionResponse struct { | |
| SessionId string `json:"sessionId"` | |
| OtherSessions []SessionInfo `json:"otherSessions"` | |
| } | |
| type SessionInfo struct { | |
| UserId string `json:"userId"` | |
| SessionId string `json:"sessionId"` | |
| PublishedTracks []string `json:"publishedTracks"` | |
| } | |
| // Use a concurrent-safe map for rooms. | |
| var rooms = struct { | |
| sync.RWMutex | |
| m map[string]*Room | |
| }{m: make(map[string]*Room)} | |
| var users = struct { | |
| sync.RWMutex | |
| m map[string]*User | |
| }{m: make(map[string]*User)} | |
| var wsConnections = struct { | |
| sync.RWMutex | |
| m map[string]map[string]*websocket.Conn | |
| }{m: make(map[string]map[string]*websocket.Conn)} | |
| // --- JWT Verification Middleware --- | |
| func verifyToken(c *gin.Context) { | |
| if !authRequired { | |
| // Even when auth is disabled, set a default user | |
| defaultUser := &User{ | |
| UserID: uuid.NewString(), | |
| Username: "Anonymous", | |
| Role: "demo", | |
| IsModerator: true, | |
| } | |
| // Store user in users map | |
| users.Lock() | |
| users.m[defaultUser.UserID] = defaultUser | |
| users.Unlock() | |
| c.Set("user", defaultUser) | |
| c.Next() | |
| return | |
| } | |
| authHeader := c.GetHeader("Authorization") | |
| if authHeader == "*" { | |
| defaultUser := &User{ | |
| UserID: uuid.NewString(), | |
| Username: "Anonymous", | |
| Role: "demo", | |
| IsModerator: true, | |
| } | |
| // Store user in users map | |
| users.Lock() | |
| users.m[defaultUser.UserID] = defaultUser | |
| users.Unlock() | |
| c.Set("user", defaultUser) | |
| c.Next() | |
| return | |
| } | |
| if authHeader == "" || len(authHeader) < 8 || authHeader[:7] != "Bearer " { | |
| c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized: No token provided"}) | |
| return | |
| } | |
| tokenString := authHeader[7:] | |
| token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { | |
| if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { | |
| return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) | |
| } | |
| return []byte(secretKey), nil | |
| }) | |
| if err != nil { | |
| c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "Forbidden: Invalid token"}) | |
| return | |
| } | |
| if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid { | |
| //convert claims to user | |
| userId, ok1 := claims["userId"].(string) | |
| username, ok2 := claims["username"].(string) | |
| role, ok3 := claims["role"].(string) | |
| isModerator, ok4 := claims["isModerator"].(bool) | |
| if ok1 && ok2 && ok3 && ok4 { | |
| c.Set("user", &User{ | |
| UserID: userId, | |
| Username: username, | |
| Role: role, | |
| IsModerator: isModerator, | |
| }) | |
| c.Next() | |
| } else { | |
| c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "Forbidden: Invalid token claims"}) | |
| } | |
| } else { | |
| c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "Forbidden: Invalid token"}) | |
| } | |
| } | |
| // --- Cloudflare API Interaction Functions --- | |
| func createCloudflareSession() (string, error) { | |
| url := fmt.Sprintf("%s/sessions/new", cloudflareBasePath) | |
| log.Printf("[Cloudflare API] Creating new session: %s", url) | |
| req, err := http.NewRequest("POST", url, nil) | |
| if err != nil { | |
| log.Printf("[Cloudflare API Error] Failed to create request: %v", err) | |
| return "", err | |
| } | |
| req.Header.Set("Authorization", "Bearer "+cloudflareAppSecret) | |
| client := &http.Client{} | |
| resp, err := client.Do(req) | |
| if err != nil { | |
| log.Printf("[Cloudflare API Error] Failed to execute request: %v", err) | |
| return "", err | |
| } | |
| defer resp.Body.Close() | |
| body, err := io.ReadAll(resp.Body) | |
| if err != nil { | |
| log.Printf("[Cloudflare API Error] Failed to read response body: %v", err) | |
| return "", err | |
| } | |
| log.Printf("[Cloudflare API Response] Status: %d, Body: %s", resp.StatusCode, string(body)) | |
| var responseData map[string]interface{} | |
| if err := json.Unmarshal(body, &responseData); err != nil { | |
| log.Printf("[Cloudflare API Error] Failed to parse response: %v", err) | |
| return "", err | |
| } | |
| sessionID, ok := responseData["sessionId"].(string) | |
| if !ok { | |
| log.Printf("[Cloudflare API Error] Session ID not found in response") | |
| return "", fmt.Errorf("sessionId not found in response: %s", string(body)) | |
| } | |
| log.Printf("[Cloudflare API Success] Created session: %s", sessionID) | |
| return sessionID, nil | |
| } | |
| func makeCloudflareRequest(method string, url string, requestBody map[string]interface{}) (map[string]interface{}, error, int) { | |
| var reqBodyBytes []byte = nil | |
| if requestBody != nil { | |
| var errMarshal error | |
| reqBodyBytes, errMarshal = json.Marshal(requestBody) | |
| if errMarshal != nil { | |
| log.Printf("Lỗi Marshal body yêu cầu JSON: %v", errMarshal) | |
| return nil, errMarshal, http.StatusInternalServerError | |
| } | |
| } | |
| req, errNewRequest := http.NewRequest(method, url, bytes.NewBuffer(reqBodyBytes)) | |
| if errNewRequest != nil { | |
| log.Printf("Lỗi tạo yêu cầu HTTP mới: %v", errNewRequest) | |
| return nil, errNewRequest, http.StatusInternalServerError | |
| } | |
| // Thiết lập các header quan trọng và phổ biến | |
| req.Header.Set("Authorization", "Bearer "+cloudflareAppSecret) // Đảm bảo cloudflareAppSecret có giá trị | |
| if requestBody != nil { | |
| req.Header.Set("Content-Type", "application/json") // Cho các yêu cầu có body JSON | |
| } | |
| req.Header.Set("Accept", "application/json") // Yêu cầu phản hồi JSON | |
| req.Header.Set("User-Agent", "CloudflareCalls-Backend-Go") // Thêm User-Agent để nhận diện backend (tùy chọn) | |
| // Bạn có thể thêm các header khác nếu cần, ví dụ: | |
| // req.Header.Set("Cache-Control", "no-cache") | |
| // req.Header.Set("Connection", "keep-alive") | |
| log.Println("Yêu cầu HTTP:") | |
| log.Println(" Method:", method) | |
| log.Println(" URL:", url) | |
| log.Println(" Headers:", req.Header) | |
| if requestBody != nil { | |
| log.Println(" Body yêu cầu:", string(reqBodyBytes)) | |
| } | |
| client := &http.Client{} | |
| resp, errClientDo := client.Do(req) | |
| if errClientDo != nil { | |
| log.Printf("Lỗi khi thực hiện yêu cầu HTTP: %v", errClientDo) | |
| return nil, errClientDo, http.StatusInternalServerError | |
| } | |
| defer resp.Body.Close() | |
| respBodyBytes, errReadAll := io.ReadAll(resp.Body) | |
| if errReadAll != nil { | |
| log.Printf("Lỗi đọc body phản hồi: %v", errReadAll) | |
| return nil, errReadAll, http.StatusInternalServerError | |
| } | |
| log.Println("Phản hồi HTTP:") | |
| log.Println(" Mã trạng thái:", resp.StatusCode) | |
| log.Println(" Body phản hồi:", string(respBodyBytes)) | |
| var responseData map[string]interface{} | |
| errUnmarshal := json.Unmarshal(respBodyBytes, &responseData) | |
| if errUnmarshal != nil { | |
| log.Printf("Lỗi Unmarshal body phản hồi JSON: %v", errUnmarshal) | |
| return nil, errUnmarshal, http.StatusInternalServerError | |
| } | |
| return responseData, nil, resp.StatusCode | |
| } | |
| func publishToCloudflare(sessionId string, offer map[string]interface{}, tracks []struct { | |
| TrackName string `json:"trackName"` | |
| Mid string `json:"mid"` | |
| Location string `json:"location"` | |
| }) (map[string]interface{}, error) { | |
| url := fmt.Sprintf("%s/sessions/%s/tracks/new", cloudflareBasePath, sessionId) | |
| log.Printf("[Cloudflare API] Publishing tracks to session %s", sessionId) | |
| trackData := make([]map[string]interface{}, len(tracks)) | |
| for i, t := range tracks { | |
| location := t.Location | |
| if location == "" { | |
| location = "local" | |
| } | |
| trackData[i] = map[string]interface{}{ | |
| "trackName": t.TrackName, | |
| "mid": t.Mid, | |
| "location": location, | |
| } | |
| } | |
| requestBody := map[string]interface{}{ | |
| "sessionDescription": offer, | |
| "tracks": trackData, | |
| } | |
| jsonData, err := json.Marshal(requestBody) | |
| if err != nil { | |
| log.Printf("[Cloudflare API Error] Failed to marshal request body: %v", err) | |
| return nil, err | |
| } | |
| log.Printf("[Cloudflare API Request] URL: %s, Body: %s", url, string(jsonData)) | |
| req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) | |
| if err != nil { | |
| log.Printf("[Cloudflare API Error] Failed to create request: %v", err) | |
| return nil, err | |
| } | |
| req.Header.Set("Authorization", "Bearer "+cloudflareAppSecret) | |
| req.Header.Set("Content-Type", "application/json") | |
| client := &http.Client{} | |
| resp, err := client.Do(req) | |
| if err != nil { | |
| log.Printf("[Cloudflare API Error] Failed to execute request: %v", err) | |
| return nil, err | |
| } | |
| defer resp.Body.Close() | |
| body, err := io.ReadAll(resp.Body) | |
| if err != nil { | |
| log.Printf("[Cloudflare API Error] Failed to read response body: %v", err) | |
| return nil, err | |
| } | |
| log.Printf("[Cloudflare API Response] Status: %d, Body: %s", resp.StatusCode, string(body)) | |
| var responseData map[string]interface{} | |
| if err := json.Unmarshal(body, &responseData); err != nil { | |
| log.Printf("[Cloudflare API Error] Failed to parse response: %v", err) | |
| return nil, err | |
| } | |
| return responseData, nil | |
| } | |
| func unpublishToCloudflare(cfUrl string, requestBody map[string]interface{}) (map[string]interface{}, error) { | |
| jsonData, err := json.Marshal(requestBody) | |
| if err != nil { | |
| return nil, err | |
| } | |
| req, err := http.NewRequest("PUT", cfUrl, bytes.NewBuffer(jsonData)) | |
| if err != nil { | |
| return nil, err | |
| } | |
| req.Header.Set("Authorization", "Bearer "+cloudflareAppSecret) | |
| req.Header.Set("Content-Type", "application/json") | |
| client := &http.Client{} | |
| resp, err := client.Do(req) | |
| if err != nil { | |
| return nil, err | |
| } | |
| defer resp.Body.Close() | |
| body, err := io.ReadAll(resp.Body) | |
| if err != nil { | |
| return nil, err | |
| } | |
| var responseData map[string]interface{} | |
| if err := json.Unmarshal(body, &responseData); err != nil { | |
| return nil, err | |
| } | |
| return responseData, nil | |
| } | |
| func pullFromCloudflare(sessionId string, tracksToPull []map[string]interface{}) (map[string]interface{}, error) { | |
| url := fmt.Sprintf("%s/sessions/%s/tracks/new", cloudflareBasePath, sessionId) | |
| requestBody := map[string]interface{}{ | |
| "tracks": tracksToPull, | |
| } | |
| jsonData, err := json.Marshal(requestBody) | |
| if err != nil { | |
| return nil, err | |
| } | |
| req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) | |
| if err != nil { | |
| return nil, err | |
| } | |
| req.Header.Set("Authorization", "Bearer "+cloudflareAppSecret) | |
| req.Header.Set("Content-Type", "application/json") | |
| client := &http.Client{} | |
| resp, err := client.Do(req) | |
| if err != nil { | |
| return nil, err | |
| } | |
| defer resp.Body.Close() | |
| body, err := io.ReadAll(resp.Body) | |
| if err != nil { | |
| return nil, err | |
| } | |
| var responseData map[string]interface{} | |
| if err := json.Unmarshal(body, &responseData); err != nil { | |
| return nil, err | |
| } | |
| return responseData, nil | |
| } | |
| func renegotiateWithCloudflare(sessionId string, body map[string]interface{}) (map[string]interface{}, error) { | |
| url := fmt.Sprintf("%s/sessions/%s/renegotiate", cloudflareBasePath, sessionId) | |
| jsonData, err := json.Marshal(body) | |
| if err != nil { | |
| return nil, err | |
| } | |
| req, err := http.NewRequest("PUT", url, bytes.NewBuffer(jsonData)) | |
| if err != nil { | |
| return nil, err | |
| } | |
| req.Header.Set("Authorization", "Bearer "+cloudflareAppSecret) | |
| req.Header.Set("Content-Type", "application/json") | |
| client := &http.Client{} | |
| resp, err := client.Do(req) | |
| if err != nil { | |
| return nil, err | |
| } | |
| defer resp.Body.Close() | |
| responseBody, err := io.ReadAll(resp.Body) | |
| if err != nil { | |
| return nil, err | |
| } | |
| var responseData map[string]interface{} | |
| if err := json.Unmarshal(responseBody, &responseData); err != nil { | |
| return nil, err | |
| } | |
| return responseData, nil | |
| } | |
| func manageDataChannelsWithCloudflare(cfUrl string, dataChannels []map[string]interface{}) (map[string]interface{}, error) { | |
| jsonData, err := json.Marshal(gin.H{"dataChannels": dataChannels}) // Correct structure | |
| if err != nil { | |
| return nil, err | |
| } | |
| req, err := http.NewRequest("POST", cfUrl, bytes.NewBuffer(jsonData)) | |
| if err != nil { | |
| return nil, err | |
| } | |
| req.Header.Set("Authorization", "Bearer "+cloudflareAppSecret) | |
| req.Header.Set("Content-Type", "application/json") | |
| client := &http.Client{} | |
| resp, err := client.Do(req) | |
| if err != nil { | |
| return nil, err | |
| } | |
| defer resp.Body.Close() | |
| body, err := io.ReadAll(resp.Body) | |
| if err != nil { | |
| return nil, err | |
| } | |
| var responseData map[string]interface{} | |
| if err := json.Unmarshal(body, &responseData); err != nil { | |
| return nil, err | |
| } | |
| return responseData, nil | |
| } | |
| func getSessionStateFromCloudflare(sessionId string) (map[string]interface{}, error) { | |
| url := fmt.Sprintf("%s/sessions/%s", cloudflareBasePath, sessionId) | |
| log.Printf("[Cloudflare API] Getting session state: %s", url) | |
| req, err := http.NewRequest("GET", url, nil) | |
| if err != nil { | |
| log.Printf("[Cloudflare API Error] Failed to create request: %v", err) | |
| return nil, err | |
| } | |
| req.Header.Set("Authorization", "Bearer "+cloudflareAppSecret) | |
| client := &http.Client{} | |
| resp, err := client.Do(req) | |
| if err != nil { | |
| log.Printf("[Cloudflare API Error] Failed to execute request: %v", err) | |
| return nil, err | |
| } | |
| defer resp.Body.Close() | |
| body, err := io.ReadAll(resp.Body) | |
| if err != nil { | |
| log.Printf("[Cloudflare API Error] Failed to read response body: %v", err) | |
| return nil, err | |
| } | |
| log.Printf("[Cloudflare API Response] Status: %d, Body: %s", resp.StatusCode, string(body)) | |
| var responseData map[string]interface{} | |
| if err := json.Unmarshal(body, &responseData); err != nil { | |
| log.Printf("[Cloudflare API Error] Failed to parse response: %v", err) | |
| return nil, err | |
| } | |
| return responseData, nil | |
| } | |
| // --- Helper Functions --- | |
| func serializeRoom(roomId string, room *Room) gin.H { | |
| return gin.H{ | |
| "roomId": roomId, | |
| "name": room.Name, | |
| "metadata": room.Metadata, | |
| "participantCount": len(room.Participants), | |
| "createdAt": room.CreatedAt, | |
| } | |
| } | |
| // --- Route Handlers --- | |
| func createRoom(c *gin.Context, roomId string, name string, metadata map[string]interface{}) { | |
| room := &Room{ | |
| RoomId: roomId, // Thêm trường này | |
| Name: name, | |
| Metadata: metadata, | |
| Participants: []*Participant{}, | |
| CreatedAt: big.NewInt(time.Now().Unix()), | |
| } | |
| rooms.Lock() | |
| rooms.m[roomId] = room | |
| rooms.Unlock() | |
| log.Printf("Room metadata: %s", room.Metadata) | |
| // Trả về format giống Express | |
| c.JSON(http.StatusOK, gin.H{ | |
| "roomId": room.RoomId, | |
| "name": room.Name, | |
| "metadata": room.Metadata, | |
| "participantCount": len(room.Participants), | |
| "createdAt": room.CreatedAt, | |
| }) | |
| } | |
| func inspectRooms(c *gin.Context) { | |
| if os.Getenv("NODE_ENV") != "development" { | |
| c.JSON(http.StatusForbidden, gin.H{"error": "This endpoint is only available in development mode."}) | |
| return | |
| } | |
| rooms.RLock() | |
| defer rooms.RUnlock() | |
| users.RLock() | |
| defer users.RUnlock() | |
| wsConnections.RLock() | |
| defer wsConnections.RUnlock() | |
| debugInfo := gin.H{ | |
| "rooms": rooms.m, | |
| "roomCount": len(rooms.m), | |
| "users": users.m, // Convert to a slice or similar for JSON | |
| "wsConnections": wsConnections.m, // You might want to just show counts | |
| "raw": rooms.m, | |
| } | |
| c.JSON(http.StatusOK, debugInfo) | |
| } | |
| func joinRoom(c *gin.Context) { | |
| roomId := c.Param("roomId") | |
| // Get user info | |
| user, exists := c.Get("user") | |
| if !exists { | |
| c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "Forbidden: User not authenticated"}) | |
| return | |
| } | |
| currentUser, ok := user.(*User) | |
| if !ok { | |
| c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "Forbidden: Invalid user"}) | |
| return | |
| } | |
| // Return info about the room and joining process | |
| c.JSON(http.StatusOK, gin.H{ | |
| "message": "Join request received. Please use a web3 wallet to join this room by calling the joinRoom function on the smart contract.", | |
| "roomId": roomId, | |
| "userId": currentUser.UserID, | |
| "instructions": "Call joinRoom('" + roomId + "') on the smart contract at " + CONTRACT_ADDRESS, | |
| }) | |
| } | |
| // Sửa lại struct request cho publishTracks để match với Express | |
| func publishTracks(c *gin.Context) { | |
| roomId := c.Param("roomId") | |
| sessionId := c.Param("sessionId") | |
| rooms.RLock() | |
| room, ok := rooms.m[roomId] | |
| rooms.RUnlock() | |
| if !ok { | |
| c.JSON(http.StatusNotFound, gin.H{"error": "Room not found"}) | |
| return | |
| } | |
| var participant *Participant | |
| room.RLock() | |
| for _, p := range room.Participants { | |
| if p.SessionID == sessionId { | |
| participant = p | |
| break | |
| } | |
| } | |
| room.RUnlock() | |
| if participant == nil { | |
| c.JSON(http.StatusNotFound, gin.H{"error": "Session not found in this room"}) | |
| return | |
| } | |
| var req struct { | |
| Offer map[string]interface{} `json:"offer"` | |
| Tracks []struct { | |
| TrackName string `json:"trackName"` | |
| Mid string `json:"mid"` // Thêm mid | |
| Location string `json:"location"` | |
| } `json:"tracks"` | |
| } | |
| if err := c.ShouldBindJSON(&req); err != nil { | |
| c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) | |
| return | |
| } | |
| if debug { | |
| log.Printf("Publishing tracks for session %s: %+v\n", sessionId, req.Tracks) | |
| } | |
| // Prepare track data for Cloudflare API | |
| tracks := make([]map[string]interface{}, len(req.Tracks)) | |
| trackNames := make([]string, len(req.Tracks)) | |
| for i, t := range req.Tracks { | |
| location := t.Location | |
| if location == "" { | |
| location = "local" | |
| } | |
| tracks[i] = map[string]interface{}{ | |
| "trackName": t.TrackName, | |
| "location": location, | |
| "mid": t.Mid, | |
| } | |
| trackNames[i] = t.TrackName | |
| } | |
| // Call Cloudflare API | |
| requestBody := map[string]interface{}{ | |
| "sessionDescription": req.Offer, | |
| "tracks": tracks, | |
| } | |
| url := fmt.Sprintf("%s/sessions/%s/tracks/new", cloudflareBasePath, sessionId) | |
| jsonData, err := json.Marshal(requestBody) | |
| if err != nil { | |
| c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) | |
| return | |
| } | |
| cfReq, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) | |
| if err != nil { | |
| c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) | |
| return | |
| } | |
| cfReq.Header.Set("Authorization", "Bearer "+cloudflareAppSecret) | |
| cfReq.Header.Set("Content-Type", "application/json") | |
| client := &http.Client{} | |
| resp, err := client.Do(cfReq) | |
| if err != nil { | |
| c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) | |
| return | |
| } | |
| defer resp.Body.Close() | |
| var data map[string]interface{} | |
| if err := json.NewDecoder(resp.Body).Decode(&data); err != nil { | |
| c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) | |
| return | |
| } | |
| // Update participant's published tracks | |
| room.Lock() | |
| for _, t := range req.Tracks { | |
| if !contains(participant.PublishedTracks, t.TrackName) { | |
| participant.PublishedTracks = append(participant.PublishedTracks, t.TrackName) | |
| } | |
| } | |
| room.Unlock() | |
| // Broadcast track-published event with complete information | |
| if data["sessionDescription"] != nil { | |
| broadcastToRoom(roomId, gin.H{ | |
| "type": "track-published", | |
| "payload": gin.H{ | |
| "userId": participant.UserID, | |
| "sessionId": sessionId, | |
| "tracks": trackNames, | |
| }, | |
| }, participant.UserID) | |
| if debug { | |
| log.Printf("Track published event broadcasted for session %s with tracks: %v\n", sessionId, trackNames) | |
| } | |
| } | |
| c.JSON(http.StatusOK, data) | |
| } | |
| // Helper function to check if slice contains string | |
| func contains(slice []string, str string) bool { | |
| for _, v := range slice { | |
| if v == str { | |
| return true | |
| } | |
| } | |
| return false | |
| } | |
| func unpublishTrack(c *gin.Context) { | |
| roomId := c.Param("roomId") | |
| sessionId := c.Param("sessionId") | |
| user, _ := c.Get("user") | |
| currentUser, ok := user.(*User) | |
| if !ok { | |
| c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "Forbidden: Invalid user"}) | |
| return | |
| } | |
| var req struct { | |
| TrackName string `json:"trackName"` | |
| Mid string `json:"mid"` | |
| Force bool `json:"force"` | |
| SessionDescription map[string]interface{} `json:"sessionDescription"` | |
| } | |
| if err := c.ShouldBindJSON(&req); err != nil { | |
| c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) | |
| return | |
| } | |
| //If trying to force unpublish someone else's track | |
| if req.Force && sessionId != currentUser.UserID { | |
| //Check if user is moderator | |
| if !currentUser.IsModerator { | |
| c.JSON(http.StatusForbidden, gin.H{ | |
| "errorCode": "NOT_AUTHORIZED", | |
| "errorDescription": "Only moderators can force unpublish other participants' tracks", | |
| }) | |
| return | |
| } | |
| } | |
| if debug { | |
| log.Println("Unpublishing track:", map[string]interface{}{"roomId": roomId, "sessionId": sessionId, "trackName": req.TrackName, "mid": req.Mid}) | |
| } | |
| if req.Mid == "" { | |
| c.JSON(http.StatusBadRequest, gin.H{ | |
| "errorCode": "INVALID_REQUEST", | |
| "errorDescription": "mid is required to unpublish a track.", | |
| }) | |
| return | |
| } | |
| if req.SessionDescription == nil { | |
| c.JSON(http.StatusBadRequest, gin.H{ | |
| "errorCode": "INVALID_REQUEST", | |
| "errorDescription": "sessionDescription is required to unpublish a track.", | |
| }) | |
| return | |
| } | |
| cfUrl := fmt.Sprintf("%s/sessions/%s/tracks/close", cloudflareBasePath, sessionId) | |
| if debug { | |
| log.Println("Calling Cloudflare API:", cfUrl) | |
| } | |
| requestBody := map[string]interface{}{ | |
| "tracks": []map[string]string{ | |
| {"mid": req.Mid}, | |
| }, | |
| "force": req.Force, | |
| "sessionDescription": req.SessionDescription, | |
| } | |
| if debug { | |
| log.Printf("Request body: %+v\n", requestBody) // Use %+v for detailed printing | |
| } | |
| data, err := unpublishToCloudflare(cfUrl, requestBody) | |
| if err != nil { | |
| c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) | |
| return | |
| } | |
| if debug { | |
| log.Println("Cloudflare API response:", data) | |
| } | |
| broadcastToRoom(roomId, gin.H{ | |
| "type": "track-unpublished", | |
| "payload": gin.H{"sessionId": sessionId, "trackName": req.TrackName}, | |
| }, sessionId) | |
| c.JSON(http.StatusOK, data) | |
| } | |
| func pullTracks(c *gin.Context) { | |
| roomId := c.Param("roomId") | |
| sessionId := c.Param("sessionId") | |
| // Struct to hold request body, khớp với frontend JavaScript (_pullTracks) | |
| var req struct { | |
| RemoteSessionId string `json:"remoteSessionId"` // camelCase, khớp với frontend | |
| TrackName string `json:"trackName"` // Kéo từng track một | |
| } | |
| // Bind JSON request body từ request Gin | |
| if err := c.ShouldBindJSON(&req); err != nil { | |
| c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) | |
| return | |
| } | |
| // Lấy thông tin room từ bộ nhớ | |
| rooms.RLock() | |
| room, ok := rooms.m[roomId] | |
| rooms.RUnlock() | |
| if !ok { | |
| c.JSON(http.StatusNotFound, gin.H{"error": "Room not found"}) | |
| return | |
| } | |
| participant := findParticipantBySessionId(room, sessionId) | |
| if participant == nil { | |
| c.JSON(http.StatusNotFound, gin.H{"error": "Session not found in this room"}) | |
| return | |
| } | |
| // Tạo cấu trúc tracksToPull cho Cloudflare API (kéo 1 track) | |
| tracksToPull := []map[string]interface{}{ | |
| { | |
| "location": "remote", | |
| "sessionId": req.RemoteSessionId, // Dùng RemoteSessionId từ request struct | |
| "trackName": req.TrackName, // Dùng TrackName từ request struct | |
| }, | |
| } | |
| // Gọi Cloudflare API để pull track (dùng hàm makeCloudflareRequest helper) | |
| url := fmt.Sprintf("%s/sessions/%s/tracks/new", cloudflareBasePath, sessionId) | |
| requestBody := map[string]interface{}{ | |
| "tracks": tracksToPull, | |
| } | |
| // Gọi makeCloudflareRequest để thực hiện request đến Cloudflare API | |
| data, err, statusCode := makeCloudflareRequest("POST", url, requestBody) | |
| if err != nil { | |
| c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to pull track", "detail": err.Error()}) | |
| return | |
| } | |
| // Kiểm tra mã trạng thái lỗi từ Cloudflare API | |
| if statusCode >= 400 { | |
| c.JSON(statusCode, gin.H{"error": "Cloudflare API error during pull track", "detail": data}) | |
| return | |
| } | |
| // Trả về phản hồi từ Cloudflare API cho frontend | |
| c.JSON(http.StatusOK, data) | |
| } | |
| // Helper function để tìm participant theo sessionId (tái sử dụng từ code của bạn) | |
| func findParticipantBySessionId(room *Room, sessionId string) *Participant { | |
| room.RLock() | |
| defer room.RUnlock() | |
| for _, p := range room.Participants { | |
| if p.SessionID == sessionId { | |
| return p | |
| } | |
| } | |
| return nil | |
| } | |
| func renegotiateSession(c *gin.Context) { | |
| sessionId := c.Param("sessionId") | |
| var req struct { | |
| SDP string `json:"sdp"` | |
| SDPType string `json:"type"` | |
| } | |
| if err := c.ShouldBindJSON(&req); err != nil { | |
| c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) | |
| return | |
| } | |
| sdp := req.SDP | |
| sdpType := req.SDPType | |
| // var req struct { | |
| // SessionDescription struct { | |
| // SDP string `json:"sdp"` | |
| // Type string `json:"type"` | |
| // } `json:"sessionDescription"` | |
| // } | |
| // if err := c.ShouldBindJSON(&req); err != nil { | |
| // c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) | |
| // return | |
| // } | |
| body := map[string]interface{}{ | |
| "sessionDescription": map[string]string{ | |
| "sdp": sdp, | |
| "type": sdpType, | |
| }, | |
| } | |
| data, err := renegotiateWithCloudflare(sessionId, body) | |
| if err != nil { | |
| if data != nil && data["errorCode"] != nil { // Check for Cloudflare error | |
| c.JSON(http.StatusBadRequest, data) | |
| return | |
| } | |
| c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) | |
| return | |
| } | |
| c.JSON(http.StatusOK, data) | |
| } | |
| func manageDataChannels(c *gin.Context) { | |
| roomId := c.Param("roomId") | |
| sessionId := c.Param("sessionId") | |
| rooms.RLock() | |
| room, ok := rooms.m[roomId] | |
| print(room) | |
| rooms.RUnlock() | |
| if !ok { | |
| c.JSON(http.StatusNotFound, gin.H{"error": "Room not found"}) | |
| return | |
| } | |
| var req struct { | |
| DataChannels []struct { | |
| Location string `json:"location"` | |
| DataChannelName string `json:"dataChannelName"` | |
| } `json:"dataChannels"` | |
| } | |
| if err := c.ShouldBindJSON(&req); err != nil { | |
| c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) | |
| return | |
| } | |
| cfUrl := fmt.Sprintf("%s/sessions/%s/datachannels/new", cloudflareBasePath, sessionId) | |
| dataChannels := make([]map[string]interface{}, len(req.DataChannels)) | |
| for i, dc := range req.DataChannels { | |
| dataChannels[i] = map[string]interface{}{ | |
| "location": dc.Location, | |
| "dataChannelName": dc.DataChannelName, | |
| } | |
| } | |
| data, err := manageDataChannelsWithCloudflare(cfUrl, dataChannels) | |
| if err != nil { | |
| if data != nil && data["errorCode"] != nil { // Check for Cloudflare error | |
| c.JSON(http.StatusBadRequest, data) | |
| return | |
| } | |
| c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) | |
| return | |
| } | |
| //Optionally, if the user is publishing a channel, you could record that in `participant.publishedDataChannels` in memory | |
| for _, dc := range req.DataChannels { | |
| if dc.Location == "local" { | |
| // E.g. store in participant.publishedDataChannels = append(participant.publishedDataChannels, dc.DataChannelName) | |
| } | |
| } | |
| c.JSON(http.StatusOK, data) | |
| } | |
| func getParticipants(c *gin.Context) { | |
| roomId := c.Param("roomId") | |
| rooms.RLock() | |
| room, ok := rooms.m[roomId] | |
| rooms.RUnlock() | |
| if !ok { | |
| c.JSON(http.StatusNotFound, gin.H{"error": "Room not found"}) | |
| return | |
| } | |
| c.JSON(http.StatusOK, gin.H{"participants": room.Participants}) | |
| } | |
| func getParticipantTracks(c *gin.Context) { | |
| sessionId := c.Param("sessionId") | |
| roomId := c.Param("roomId") | |
| rooms.RLock() | |
| room, ok := rooms.m[roomId] | |
| rooms.RUnlock() | |
| if !ok { | |
| c.JSON(http.StatusNotFound, gin.H{"error": "Room not found"}) | |
| return | |
| } | |
| var participant *Participant | |
| room.RLock() | |
| for _, p := range room.Participants { | |
| if p.SessionID == sessionId { | |
| participant = p | |
| break | |
| } | |
| } | |
| room.RUnlock() | |
| if participant == nil { | |
| c.JSON(http.StatusNotFound, gin.H{"error": "Participant not found"}) | |
| return | |
| } | |
| c.JSON(http.StatusOK, participant.PublishedTracks) | |
| } | |
| func getICEServers(c *gin.Context) { | |
| cloudflareTurnID := os.Getenv("CLOUDFLARE_TURN_ID") | |
| cloudflareTurnToken := os.Getenv("CLOUDFLARE_TURN_TOKEN") | |
| if cloudflareTurnID == "" || cloudflareTurnToken == "" { | |
| c.JSON(http.StatusOK, gin.H{ | |
| "iceServers": []gin.H{ | |
| {"urls": "stun:stun.cloudflare.com:3478"}, | |
| }, | |
| }) | |
| return | |
| } | |
| lifetime := 600 // Credentials valid for 10 minutes (600 seconds) | |
| timestamp := time.Now().Unix() + int64(lifetime) | |
| username := fmt.Sprintf("%d:%s", timestamp, cloudflareTurnID) | |
| // Create HMAC-SHA256 hash using CLOUDFLARE_TURN_TOKEN as the key | |
| h := hmac.New(sha256.New, []byte(cloudflareTurnToken)) | |
| h.Write([]byte(username)) | |
| credential := base64.StdEncoding.EncodeToString(h.Sum(nil)) | |
| iceServers := gin.H{ | |
| "iceServers": []gin.H{ | |
| {"urls": "stun:stun.cloudflare.com:3478"}, | |
| { | |
| "urls": "turn:turn.cloudflare.com:3478?transport=udp", | |
| "username": username, | |
| "credential": credential, | |
| }, | |
| { | |
| "urls": "turn:turn.cloudflare.com:3478?transport=tcp", | |
| "username": username, | |
| "credential": credential, | |
| }, | |
| { | |
| "urls": "turns:turn.cloudflare.com:5349?transport=tcp", | |
| "username": username, | |
| "credential": credential, | |
| }, | |
| }, | |
| } | |
| c.JSON(http.StatusOK, iceServers) | |
| } | |
| func getToken(c *gin.Context) { | |
| var req struct { | |
| Username string `json:"username"` | |
| } | |
| if err := c.ShouldBindJSON(&req); err != nil { | |
| c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) | |
| return | |
| } | |
| userId := uuid.NewString() | |
| token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ | |
| "userId": userId, | |
| "username": req.Username, | |
| "role": "demo", | |
| "isModerator": true, // This should come from a database in production | |
| "exp": time.Now().Add(time.Hour * 8).Unix(), | |
| }) | |
| tokenString, err := token.SignedString([]byte(secretKey)) | |
| if err != nil { | |
| c.JSON(http.StatusInternalServerError, gin.H{"error": "Could not generate token"}) | |
| return | |
| } | |
| // Store initial user info | |
| users.Lock() | |
| users.m[userId] = &User{ | |
| UserID: userId, | |
| Username: req.Username, | |
| IsModerator: true, | |
| Role: "demo", | |
| } | |
| users.Unlock() | |
| c.JSON(http.StatusOK, gin.H{"token": tokenString}) | |
| } | |
| func getSessionState(c *gin.Context) { | |
| roomId := c.Param("roomId") | |
| sessionId := c.Param("sessionId") | |
| _ = roomId | |
| data, err := getSessionStateFromCloudflare(sessionId) | |
| if err != nil { | |
| c.JSON(http.StatusInternalServerError, gin.H{ | |
| "errorCode": "SESSION_STATE_ERROR", | |
| "errorDescription": err.Error(), | |
| }) | |
| return | |
| } | |
| c.JSON(http.StatusOK, data) | |
| } | |
| func getUserInfo(c *gin.Context) { | |
| userIdParam := c.Param("userId") | |
| user, _ := c.Get("user") | |
| currentUser, ok := user.(*User) | |
| if !ok { | |
| c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "Forbidden: Invalid user"}) | |
| return | |
| } | |
| if userIdParam == "me" { | |
| users.RLock() | |
| userInfo, ok := users.m[currentUser.UserID] | |
| users.RUnlock() | |
| if !ok { | |
| c.JSON(http.StatusNotFound, gin.H{ | |
| "errorCode": "USER_NOT_FOUND", | |
| "errorDescription": "Current user not found", | |
| }) | |
| return | |
| } | |
| c.JSON(http.StatusOK, userInfo) | |
| return | |
| } | |
| users.RLock() | |
| requestedUser, ok := users.m[userIdParam] | |
| users.RUnlock() | |
| if !ok { | |
| c.JSON(http.StatusNotFound, gin.H{ | |
| "errorCode": "USER_NOT_FOUND", | |
| "errorDescription": "User not found", | |
| }) | |
| return | |
| } | |
| // Return limited info for other users | |
| c.JSON(http.StatusOK, gin.H{ | |
| "userId": requestedUser.UserID, | |
| "username": requestedUser.Username, | |
| }) | |
| } | |
| func leaveRoom(c *gin.Context) { | |
| roomId := c.Param("roomId") | |
| var req struct { | |
| SessionId string `json:"sessionId"` | |
| } | |
| if err := c.ShouldBindJSON(&req); err != nil { | |
| c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) | |
| return | |
| } | |
| rooms.RLock() | |
| room, ok := rooms.m[roomId] | |
| rooms.RUnlock() | |
| if !ok { | |
| c.JSON(http.StatusNotFound, gin.H{"error": "Room not found"}) | |
| return | |
| } | |
| participantIndex := -1 | |
| var participant *Participant | |
| room.Lock() | |
| for i, p := range room.Participants { | |
| if p.SessionID == req.SessionId { | |
| participantIndex = i | |
| participant = p | |
| break | |
| } | |
| } | |
| if participantIndex != -1 { | |
| room.Participants = append(room.Participants[:participantIndex], room.Participants[participantIndex+1:]...) | |
| // Notify other participants about the leave | |
| broadcastToRoom(roomId, gin.H{ | |
| "type": "participant-left", | |
| "payload": gin.H{ | |
| "sessionId": req.SessionId, | |
| "userId": participant.UserID, | |
| }, | |
| }, req.SessionId) | |
| // If room is empty, delete it | |
| if len(room.Participants) == 0 { | |
| rooms.Lock() | |
| delete(rooms.m, roomId) | |
| rooms.Unlock() | |
| } | |
| } | |
| room.Unlock() | |
| c.JSON(http.StatusOK, gin.H{"success": true}) | |
| } | |
| func updateTrackStatus(c *gin.Context) { | |
| roomId := c.Param("roomId") | |
| sessionId := c.Param("sessionId") | |
| user, _ := c.Get("user") | |
| currentUser, ok := user.(*User) | |
| if !ok { | |
| c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "Forbidden: Invalid user"}) | |
| return | |
| } | |
| var req struct { | |
| TrackId string `json:"trackId"` | |
| Kind string `json:"kind"` | |
| Enabled bool `json:"enabled"` | |
| Force bool `json:"force"` | |
| } | |
| if err := c.ShouldBindJSON(&req); err != nil { | |
| c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) | |
| return | |
| } | |
| // If trying to force change someone else's track | |
| if req.Force && sessionId != currentUser.UserID { | |
| if !currentUser.IsModerator { | |
| c.JSON(http.StatusForbidden, gin.H{ | |
| "errorCode": "NOT_AUTHORIZED", | |
| "errorDescription": "Only moderators can force change other participants' tracks", | |
| }) | |
| return | |
| } | |
| } | |
| // Notify other participants about the track status change | |
| broadcastToRoom(roomId, gin.H{ | |
| "type": "track-status-changed", | |
| "payload": gin.H{ | |
| "sessionId": sessionId, | |
| "trackId": req.TrackId, | |
| "kind": req.Kind, | |
| "enabled": req.Enabled, | |
| }, | |
| }, sessionId) | |
| c.JSON(http.StatusOK, gin.H{"success": true}) | |
| } | |
| func getRooms(c *gin.Context) { | |
| rooms.RLock() | |
| roomList := make([]gin.H, 0, len(rooms.m)) | |
| for roomId, room := range rooms.m { | |
| roomList = append(roomList, serializeRoom(roomId, room)) | |
| } | |
| rooms.RUnlock() | |
| c.JSON(http.StatusOK, gin.H{"rooms": roomList}) | |
| } | |
| func updateRoomMetadata(c *gin.Context) { | |
| roomId := c.Param("roomId") | |
| var req struct { | |
| Name string `json:"name"` | |
| Metadata map[string]interface{} `json:"metadata"` | |
| } | |
| if err := c.ShouldBindJSON(&req); err != nil { | |
| c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) | |
| return | |
| } | |
| rooms.RLock() | |
| room, ok := rooms.m[roomId] | |
| rooms.RUnlock() | |
| if !ok { | |
| c.JSON(http.StatusNotFound, gin.H{"error": "Room not found"}) | |
| return | |
| } | |
| room.Lock() | |
| if req.Name != "" { | |
| room.Name = req.Name | |
| } | |
| if req.Metadata != nil { | |
| if room.Metadata == nil { | |
| room.Metadata = make(map[string]interface{}) | |
| } | |
| for k, v := range req.Metadata { | |
| room.Metadata[k] = v | |
| } | |
| } | |
| room.Unlock() | |
| // Notify room participants about the update | |
| broadcastToRoom(roomId, gin.H{ | |
| "type": "room-metadata-updated", | |
| "payload": gin.H{ | |
| "roomId": roomId, | |
| "name": room.Name, | |
| "metadata": room.Metadata, | |
| }, | |
| "from": "server", | |
| }, "") // Empty string means no user is excluded from the broadcast | |
| c.JSON(http.StatusOK, serializeRoom(roomId, room)) | |
| } | |
| // --- WebSocket Handling --- | |
| var upgrader = websocket.Upgrader{ | |
| CheckOrigin: func(r *http.Request) bool { | |
| return true // Cẩn thận với setting này trong production | |
| }, | |
| ReadBufferSize: 1024, | |
| WriteBufferSize: 1024, | |
| } | |
| func websocketHandler(c *gin.Context) { | |
| if debug { | |
| log.Printf("Incoming WebSocket request from: %s\n", c.Request.RemoteAddr) | |
| } | |
| ws, err := upgrader.Upgrade(c.Writer, c.Request, nil) | |
| if err != nil { | |
| log.Printf("Failed to upgrade connection: %v\n", err) | |
| return | |
| } | |
| defer ws.Close() | |
| handleWebSocket(ws) | |
| } | |
| func handleWebSocket(ws *websocket.Conn) { | |
| if debug { | |
| log.Printf("New WebSocket connection established from: %s\n", ws.RemoteAddr().String()) | |
| } | |
| defer ws.Close() | |
| var userId string | |
| var roomId string | |
| for { | |
| // Đọc message | |
| messageType, message, err := ws.ReadMessage() | |
| log.Println("messageType", messageType) | |
| if err != nil { | |
| if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) { | |
| log.Printf("WebSocket error: %v", err) | |
| } | |
| handleWSDisconnect(ws, roomId, userId) | |
| break | |
| } | |
| // Parse message | |
| var data map[string]interface{} | |
| if err := json.Unmarshal(message, &data); err != nil { | |
| log.Println("Error parsing message:", err) | |
| continue | |
| } | |
| messageTypeStr, ok := data["type"].(string) | |
| if !ok { | |
| log.Println("Invalid message format: missing or invalid 'type'") | |
| continue | |
| } | |
| switch messageTypeStr { | |
| case "join-websocket": | |
| // Format payload giống Express | |
| payload := map[string]interface{}{ | |
| "roomId": data["payload"].(map[string]interface{})["roomId"], | |
| "userId": data["payload"].(map[string]interface{})["userId"], | |
| "token": data["payload"].(map[string]interface{})["token"], | |
| } | |
| handleWSJoin(ws, payload) | |
| case "data-message": | |
| // Format payload giống Express | |
| payload := map[string]interface{}{ | |
| "from": data["payload"].(map[string]interface{})["from"], | |
| "data": data["payload"].(map[string]interface{})["message"], | |
| } | |
| handleDataMessage(ws, payload) | |
| default: | |
| log.Println("Unknown message type:", messageTypeStr) | |
| } | |
| } | |
| } | |
| // --- WebSocket Helper Functions --- | |
| func getRoomIdByUserId(userId string) string { | |
| rooms.RLock() | |
| defer rooms.RUnlock() | |
| for roomId, room := range rooms.m { | |
| for _, p := range room.Participants { | |
| if p.UserID == userId { | |
| return roomId | |
| } | |
| } | |
| } | |
| return "" | |
| } | |
| func getRoomIdBySessionId(sessionId string) string { | |
| rooms.RLock() | |
| defer rooms.RUnlock() | |
| for roomId, room := range rooms.m { | |
| for _, p := range room.Participants { | |
| if p.SessionID == sessionId { | |
| return roomId | |
| } | |
| } | |
| } | |
| return "" | |
| } | |
| func getWebSocketByUserId(userId string) *websocket.Conn { | |
| wsConnections.RLock() | |
| defer wsConnections.RUnlock() | |
| for _, userMap := range wsConnections.m { | |
| if conn, ok := userMap[userId]; ok { | |
| return conn | |
| } | |
| } | |
| return nil | |
| } | |
| func handleWSJoin(ws *websocket.Conn, payload map[string]interface{}) { | |
| roomId, ok1 := payload["roomId"].(string) | |
| userId, ok2 := payload["userId"].(string) | |
| token, ok3 := payload["token"].(string) | |
| //check roomid, userid and token | |
| if !ok1 || !ok2 || (authRequired && !ok3) { | |
| log.Println("Missing roomId, userId, or token in WS join") | |
| _ = ws.WriteJSON(map[string]string{"error": "Missing roomId, userId, or token"}) | |
| return | |
| } | |
| if authRequired { | |
| _, err := jwt.Parse(token, func(token *jwt.Token) (interface{}, error) { | |
| if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { | |
| return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) | |
| } | |
| return []byte(secretKey), nil | |
| }) | |
| if err != nil { | |
| log.Println("Invalid token in WS join:", err) | |
| _ = ws.WriteJSON(map[string]string{"error": "Invalid or expired token"}) | |
| return | |
| } | |
| } | |
| // Add user to the room's WebSocket connections | |
| wsConnections.Lock() | |
| if _, ok := wsConnections.m[roomId]; !ok { | |
| wsConnections.m[roomId] = make(map[string]*websocket.Conn) | |
| } | |
| wsConnections.m[roomId][userId] = ws | |
| wsConnections.Unlock() | |
| if debug { | |
| log.Printf("User %s joined room %s via WS\n", userId, roomId) | |
| } | |
| response := map[string]string{"message": "Joined room successfully"} | |
| if err := ws.WriteJSON(response); err != nil { | |
| log.Println("Error sending join response:", err) | |
| } | |
| } | |
| func handleDataMessage(ws *websocket.Conn, payload map[string]interface{}) { | |
| from, ok1 := payload["from"].(string) | |
| data, ok2 := payload["data"] // "data", not "message" | |
| if !ok1 || !ok2 { | |
| log.Println("Invalid data-message payload:", payload) | |
| return | |
| } | |
| // Get room ID from the session ID. Crucially, this now uses *session* ID. | |
| roomId := getRoomIdBySessionId(from) | |
| if roomId == "" { | |
| log.Printf("Room not found for session: %s\n", from) | |
| return | |
| } | |
| // Broadcast to all participants in the room except the sender | |
| broadcastToRoom(roomId, gin.H{ | |
| "type": "data-message", | |
| "payload": gin.H{ | |
| "from": from, | |
| "data": data, // "data", not "message" | |
| }, | |
| }, from) // Exclude the sender | |
| } | |
| func handleWSDisconnect(ws *websocket.Conn, roomId string, userId string) { | |
| wsConnections.Lock() | |
| defer wsConnections.Unlock() | |
| //remove user from room | |
| if _, ok := wsConnections.m[roomId]; ok { | |
| delete(wsConnections.m[roomId], userId) | |
| if debug { | |
| log.Printf("User %s disconnected from room %s\n", userId, roomId) | |
| } | |
| } | |
| } | |
| func broadcastToRoom(roomId string, message gin.H, excludeUserId string) { | |
| if debug { | |
| log.Printf("Broadcasting to room %s: %+v (excluding: %s)\n", roomId, message, excludeUserId) | |
| } | |
| rooms.RLock() | |
| _, ok := rooms.m[roomId] | |
| rooms.RUnlock() | |
| if !ok { | |
| log.Printf("Room %s not found for broadcast\n", roomId) | |
| return | |
| } | |
| wsConnections.RLock() | |
| connections, ok := wsConnections.m[roomId] | |
| wsConnections.RUnlock() | |
| if !ok { | |
| log.Printf("No WebSocket connections found for room %s\n", roomId) | |
| return | |
| } | |
| // Serialize message once | |
| msgBytes, err := json.Marshal(message) | |
| if err != nil { | |
| log.Printf("Error serializing broadcast message: %v\n", err) | |
| return | |
| } | |
| if debug { | |
| log.Printf("Serialized message for broadcast: %s\n", string(msgBytes)) | |
| } | |
| wsConnections.RLock() | |
| defer wsConnections.RUnlock() | |
| for userId, conn := range connections { | |
| if userId == excludeUserId { | |
| continue | |
| } | |
| if conn != nil { | |
| err := conn.WriteMessage(websocket.TextMessage, msgBytes) | |
| if err != nil { | |
| log.Printf("Error broadcasting to user %s: %v\n", userId, err) | |
| // Clean up failed connection | |
| wsConnections.Lock() | |
| delete(wsConnections.m[roomId], userId) | |
| wsConnections.Unlock() | |
| } else if debug { | |
| log.Printf("Successfully sent broadcast message to user: %s\n", userId) | |
| } | |
| } | |
| } | |
| } | |
| func processJoinRoomRequest(roomId string, userAddress common.Address) { | |
| log.Printf("[Join Room] Processing join request for room %s from user %s", roomId, userAddress.Hex()) | |
| // Check if room exists in our local cache | |
| rooms.RLock() | |
| room, exists := rooms.m[roomId] | |
| rooms.RUnlock() | |
| if !exists { | |
| log.Printf("[Join Room] Room %s not found, creating new room", roomId) | |
| room = &Room{ | |
| RoomId: roomId, | |
| Name: "New Room", | |
| Metadata: make(map[string]interface{}), | |
| Participants: make([]*Participant, 0), | |
| CreatedAt: big.NewInt(time.Now().Unix()), | |
| } | |
| rooms.Lock() | |
| rooms.m[roomId] = room | |
| rooms.Unlock() | |
| } | |
| // Create a Cloudflare session | |
| sessionID, err := createCloudflareSession() | |
| if err != nil { | |
| log.Printf("[Join Room] Failed to create Cloudflare session: %v", err) | |
| return | |
| } | |
| log.Printf("[Join Room] Created Cloudflare session: %s", sessionID) | |
| // Add participant to the room locally | |
| participant := &Participant{ | |
| UserID: userAddress.Hex(), | |
| SessionID: sessionID, | |
| CreatedAt: big.NewInt(time.Now().Unix()), | |
| PublishedTracks: make([]string, 0), | |
| } | |
| room.Lock() | |
| room.Participants = append(room.Participants, participant) | |
| room.Unlock() | |
| log.Printf("[Join Room] Added participant to room %s: %s", roomId, userAddress.Hex()) | |
| // Update the session ID on the smart contract | |
| updateSessionIDOnContract(roomId, userAddress, sessionID) | |
| } | |
| // Function to update session ID on the smart contract | |
| func updateSessionIDOnContract(roomId string, userAddress common.Address, sessionID string) { | |
| log.Printf("[Smart Contract] Updating session ID on contract for room %s, user %s, sessionID %s", | |
| roomId, userAddress.Hex(), sessionID) | |
| // Connect to the blockchain if not already connected | |
| client, err := ethclient.Dial(RPC_URL) | |
| if err != nil { | |
| log.Printf("[Smart Contract] Failed to connect to blockchain: %v", err) | |
| return | |
| } | |
| // Convert contract address | |
| contractAddress := common.HexToAddress(CONTRACT_ADDRESS) | |
| // Parse ABI | |
| parsedABI, err := abi.JSON(strings.NewReader(ABI_JSON)) | |
| if err != nil { | |
| log.Printf("[Smart Contract] Failed to parse ABI: %v", err) | |
| return | |
| } | |
| // Load private key | |
| privateKey, err := crypto.HexToECDSA(PRIVATE_KEY) | |
| if err != nil { | |
| log.Printf("[Smart Contract] Failed to parse private key: %v", err) | |
| return | |
| } | |
| // Get public key and address | |
| publicKey := privateKey.Public() | |
| publicKeyECDSA, ok := publicKey.(*ecdsa.PublicKey) | |
| if !ok { | |
| log.Printf("[Smart Contract] Error converting public key") | |
| return | |
| } | |
| fromAddress := crypto.PubkeyToAddress(*publicKeyECDSA) | |
| // Get nonce for transaction | |
| nonce, err := client.PendingNonceAt(context.Background(), fromAddress) | |
| if err != nil { | |
| log.Printf("[Smart Contract] Failed to get nonce: %v", err) | |
| return | |
| } | |
| // Get gas price | |
| gasPrice, err := client.SuggestGasPrice(context.Background()) | |
| if err != nil { | |
| log.Printf("[Smart Contract] Failed to get gas price: %v", err) | |
| return | |
| } | |
| // Pack the data for the function call | |
| data, err := parsedABI.Pack("setSessionIDByBackend", roomId, userAddress, sessionID) | |
| if err != nil { | |
| log.Printf("[Smart Contract] Failed to pack transaction data: %v", err) | |
| return | |
| } | |
| // Set gas limit | |
| gasLimit := uint64(300000) | |
| // Create the transaction | |
| tx := types.NewTransaction(nonce, contractAddress, big.NewInt(0), gasLimit, gasPrice, data) | |
| // BSC Testnet chain ID is 97 | |
| chainID := big.NewInt(97) | |
| // Sign the transaction | |
| signedTx, err := types.SignTx(tx, types.NewLondonSigner(chainID), privateKey) | |
| if err != nil { | |
| log.Printf("[Smart Contract] Failed to sign transaction: %v", err) | |
| return | |
| } | |
| // Send the transaction | |
| err = client.SendTransaction(context.Background(), signedTx) | |
| if err != nil { | |
| log.Printf("[Smart Contract] Failed to send transaction: %v", err) | |
| return | |
| } | |
| log.Printf("[Smart Contract] Session ID update transaction sent. Hash: %s", signedTx.Hash().Hex()) | |
| } | |
| func main() { | |
| initConfig() // Initialize configuration | |
| r := gin.Default() | |
| // Tạo Phòng và lắng nghe smrtract | |
| client, err := ethclient.Dial(RPC_URL) | |
| if err != nil { | |
| log.Fatalf("Không thể kết nối đến BSC Testnet: %v", err) | |
| } | |
| fmt.Println("Đã kết nối đến BSC Testnet!") | |
| contractAddress := common.HexToAddress(CONTRACT_ADDRESS) | |
| query := ethereum.FilterQuery{ | |
| Addresses: []common.Address{contractAddress}, | |
| } | |
| contractAbi, err := abi.JSON(strings.NewReader(ABI_JSON)) | |
| if err != nil { | |
| log.Fatalf("Không thể phân tích ABI: %v", err) | |
| } | |
| logs := make(chan types.Log) | |
| sub, err := client.SubscribeFilterLogs(context.Background(), query, logs) | |
| fmt.Println("Đã kết nối đến BSC Testnet!", sub) | |
| if err != nil { | |
| log.Fatalf("Không thể đăng ký lắng nghe sự kiện: %v", err) | |
| } | |
| fmt.Println("Đang lắng nghe sự kiện RoomCreated...") | |
| type RoomCreatedEvent struct { | |
| RoomId string `abi:"roomId"` | |
| Name string `abi:"name"` | |
| Metadata string `abi:"metadata"` | |
| Owner common.Address `abi:"owner"` | |
| Timestamp *big.Int `abi:"timestamp"` | |
| IsActive bool `abi:"isActive"` | |
| } | |
| // Define JoinRoomRequest event struct | |
| type JoinRoomRequestEvent struct { | |
| RoomId string `abi:"roomId"` | |
| User common.Address `abi:"user"` | |
| Timestamp *big.Int `abi:"timestamp"` | |
| } | |
| go func() { | |
| for eventLog := range logs { | |
| // Check event signature to determine which event it is | |
| if len(eventLog.Topics) > 0 { | |
| eventSignature := eventLog.Topics[0].Hex() | |
| switch eventSignature { | |
| case contractAbi.Events["RoomCreated"].ID.Hex(): | |
| event := new(RoomCreatedEvent) | |
| if err := contractAbi.UnpackIntoInterface(event, "RoomCreated", eventLog.Data); err != nil { | |
| log.Printf("Lỗi giải mã sự kiện RoomCreated: %v", err) | |
| continue | |
| } | |
| // Process RoomCreated event | |
| if eventLog.TxHash != (common.Hash{}) { | |
| var metadata map[string]interface{} | |
| if err := json.Unmarshal([]byte(event.Metadata), &metadata); err != nil { | |
| log.Printf("Lỗi parse metadata: %v", err) | |
| metadata = make(map[string]interface{}) | |
| } | |
| createTime := time.Unix(event.Timestamp.Int64(), 0) | |
| fmt.Printf("Sự kiện RoomCreated:\nRoom ID: %s\nTên: %s\nMetadata: %s\nOwner: %s\nThời gian: %v\nActive: %v\n", | |
| event.RoomId, event.Name, event.Metadata, event.Owner.Hex(), createTime, event.IsActive) | |
| room := &Room{ | |
| RoomId: event.RoomId, | |
| Name: event.Name, | |
| Metadata: metadata, | |
| CreatedAt: event.Timestamp, | |
| } | |
| rooms.Lock() | |
| rooms.m[event.RoomId] = room | |
| rooms.Unlock() | |
| } | |
| case contractAbi.Events["JoinRoomRequest"].ID.Hex(): | |
| event := new(JoinRoomRequestEvent) | |
| if err := contractAbi.UnpackIntoInterface(event, "JoinRoomRequest", eventLog.Data); err != nil { | |
| log.Printf("Lỗi giải mã sự kiện JoinRoomRequest: %v", err) | |
| continue | |
| } | |
| // Process JoinRoomRequest event with our new function | |
| if eventLog.TxHash != (common.Hash{}) { | |
| fmt.Printf("Sự kiện JoinRoomRequest:\nRoom ID: %s\nUser: %s\nThời gian: %v\n", | |
| event.RoomId, event.User.Hex(), time.Unix(event.Timestamp.Int64(), 0)) | |
| // Process join room request | |
| processJoinRoomRequest(event.RoomId, event.User) | |
| } | |
| } | |
| } | |
| } | |
| }() | |
| // CORS middleware (configure as needed) | |
| r.Use(func(c *gin.Context) { | |
| c.Writer.Header().Set("Access-Control-Allow-Origin", "*") // For development only! | |
| c.Writer.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") | |
| c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Requested-With, Upgrade, Connection") | |
| c.Writer.Header().Set("Access-Control-Max-Age", "3600") | |
| // Handle WebSocket pre-flight | |
| if c.Request.Method == "OPTIONS" { | |
| if c.Request.Header.Get("Upgrade") == "websocket" { | |
| c.Writer.Header().Set("Connection", "Upgrade") | |
| c.Writer.Header().Set("Upgrade", "websocket") | |
| } | |
| c.AbortWithStatus(204) | |
| return | |
| } | |
| c.Next() | |
| }) | |
| r.POST("/auth/token", getToken) | |
| // Protected routes (require JWT) | |
| api := r.Group("/api", verifyToken) | |
| { | |
| // Match Express routes | |
| // api.POST("/rooms", createRoom) | |
| api.GET("/rooms", getRooms) | |
| api.GET("/inspect-rooms", inspectRooms) | |
| api.POST("/rooms/:roomId/join", joinRoom) | |
| api.POST("/rooms/:roomId/sessions/:sessionId/publish", publishTracks) | |
| api.POST("/rooms/:roomId/sessions/:sessionId/unpublish", unpublishTrack) | |
| api.POST("/rooms/:roomId/sessions/:sessionId/pull", pullTracks) | |
| api.PUT("/rooms/:roomId/sessions/:sessionId/renegotiate", renegotiateSession) | |
| api.POST("/rooms/:roomId/sessions/:sessionId/datachannels/new", manageDataChannels) | |
| api.GET("/rooms/:roomId/participants", getParticipants) | |
| api.GET("/rooms/:roomId/participant/:sessionId/tracks", getParticipantTracks) | |
| api.GET("/ice-servers", getICEServers) | |
| api.GET("/rooms/:roomId/sessions/:sessionId/state", getSessionState) | |
| api.GET("/users/:userId", getUserInfo) | |
| api.POST("/rooms/:roomId/leave", leaveRoom) | |
| api.POST("/rooms/:roomId/sessions/:sessionId/track-status", updateTrackStatus) | |
| api.PUT("/rooms/:roomId/metadata", updateRoomMetadata) | |
| } | |
| r.GET("/ws", websocketHandler) | |
| // Start the server | |
| log.Printf("Server listening on http://localhost:%s\n", port) | |
| //listen and serve for http | |
| if err := r.Run(":" + port); err != nil { | |
| log.Fatal("Failed to start server:", err) | |
| } | |
| } | |