package main import ( "database/sql" "encoding/json" "fmt" "log" "math/rand" "net/http" "strings" "sync" "time" _ "github.com/go-sql-driver/mysql" ) type Comment struct { ID int `json:"id,omitempty"` CommentID string `json:"comment_id"` TopicID string `json:"topic_id"` SiliconID string `json:"silicon_id"` PbID string `json:"pb_id"` UserID string `json:"user_id"` UserName string `json:"user_name"` CommentText string `json:"comment_text"` IsApproved bool `json:"is_approved"` ParentCommentID sql.NullString `json:"parent_comment_id,omitempty"` CreatedAt time.Time `json:"created_at,omitempty"` UpdatedAt time.Time `json:"updated_at,omitempty"` Replies []Comment `json:"replies,omitempty"` } var ( db *sql.DB idCounter uint64 mutex sync.Mutex ) const ( idPrefix = "CMT" idLength = 10 baseChars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ" ) func main() { // Database configuration dbUser := "sp" dbPass := "0000" dbHost := "65.108.85.191" dbPort := "3306" dbName := "sp_hostapi" dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?parseTime=true", dbUser, dbPass, dbHost, dbPort, dbName) var err error db, err = sql.Open("mysql", dsn) if err != nil { log.Fatal(err) } defer db.Close() db.SetMaxOpenConns(25) db.SetMaxIdleConns(25) db.SetConnMaxLifetime(5 * time.Minute) if err = db.Ping(); err != nil { log.Fatal(err) } if err = initIDCounter(); err != nil { log.Fatal(err) } // Set up HTTP server with CORS middleware mux := http.NewServeMux() mux.HandleFunc("/comments", corsMiddleware(handleComments)) mux.HandleFunc("/comments/reply", corsMiddleware(handleReply)) mux.HandleFunc("/comments/thread/", corsMiddleware(handleCommentThread)) mux.HandleFunc("/comments/approve", corsMiddleware(handleCommentApproval)) mux.HandleFunc("/health", corsMiddleware(healthCheck)) mux.HandleFunc("/comments/", corsMiddleware(handleSingleComment)) log.Println("Server started on :8080") log.Fatal(http.ListenAndServe(":8080", mux)) } func corsMiddleware(next http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization") if r.Method == "OPTIONS" { w.WriteHeader(http.StatusNoContent) return } next(w, r) } } func healthCheck(w http.ResponseWriter, r *http.Request) { if err := db.Ping(); err != nil { http.Error(w, "DB connection error", http.StatusServiceUnavailable) return } w.WriteHeader(http.StatusOK) w.Write([]byte("OK")) } func handleComments(w http.ResponseWriter, r *http.Request) { switch r.Method { case "POST": insertComment(w, r, "") case "GET": getComments(w, r) default: http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) } } func handleReply(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } var req struct { ParentCommentID string `json:"parent_comment_id"` TopicID string `json:"topic_id"` SiliconID string `json:"silicon_id"` PbID string `json:"pb_id"` UserID string `json:"user_id"` UserName string `json:"user_name"` CommentText string `json:"comment_text"` IsApproved bool `json:"is_approved"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "Invalid request payload", http.StatusBadRequest) return } // Create comment with proper NullString handling comment := Comment{ TopicID: req.TopicID, SiliconID: req.SiliconID, PbID: req.PbID, UserID: req.UserID, UserName: req.UserName, CommentText: req.CommentText, IsApproved: req.IsApproved, ParentCommentID: sql.NullString{ String: req.ParentCommentID, Valid: req.ParentCommentID != "", }, } // Convert to JSON and pass to insertComment jsonData, err := json.Marshal(comment) if err != nil { http.Error(w, "Error creating request", http.StatusInternalServerError) return } // Create a new request with the properly formatted JSON newReq, err := http.NewRequest("POST", "", strings.NewReader(string(jsonData))) if err != nil { http.Error(w, "Error creating request", http.StatusInternalServerError) return } // Call insertComment with the parent ID insertComment(w, newReq, req.ParentCommentID) } func insertComment(w http.ResponseWriter, r *http.Request, parentCommentID string) { var comment Comment if err := json.NewDecoder(r.Body).Decode(&comment); err != nil { http.Error(w, "Invalid request payload: "+err.Error(), http.StatusBadRequest) return } // Validate required fields if comment.TopicID == "" || comment.SiliconID == "" || comment.PbID == "" || comment.UserID == "" || comment.UserName == "" || comment.CommentText == "" { http.Error(w, "All fields except is_approved are required", http.StatusBadRequest) return } // Generate unique ID comment.CommentID = generateUniqueID() comment.CreatedAt = time.Now() comment.UpdatedAt = time.Now() // Handle parent comment reference if parentCommentID != "" { comment.ParentCommentID = sql.NullString{ String: parentCommentID, Valid: true, } // Verify parent exists var exists bool err := db.QueryRow("SELECT EXISTS(SELECT 1 FROM commentsdb WHERE comment_id = ?)", parentCommentID).Scan(&exists) if err != nil || !exists { http.Error(w, "Parent comment not found", http.StatusBadRequest) return } } else { comment.ParentCommentID = sql.NullString{Valid: false} } // Insert the comment _, err := db.Exec( `INSERT INTO commentsdb (comment_id, topic_id, silicon_id, pb_id, user_id, user_name, comment_text, is_approved, parent_comment_id, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, comment.CommentID, comment.TopicID, comment.SiliconID, comment.PbID, comment.UserID, comment.UserName, comment.CommentText, comment.IsApproved, comment.ParentCommentID, comment.CreatedAt, comment.UpdatedAt, ) if err != nil { http.Error(w, "Failed to insert comment: "+err.Error(), http.StatusInternalServerError) return } // Return the complete comment w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(comment) } func getComments(w http.ResponseWriter, r *http.Request) { query := r.URL.Query() topicID := query.Get("topic_id") // siliconID := query.Get("silicon_id") // pbID := query.Get("pb_id") if topicID == "" { http.Error(w, "topic_id is required", http.StatusBadRequest) return } baseQuery := `SELECT id, comment_id, topic_id, silicon_id, pb_id, user_id, user_name, comment_text, is_approved, parent_comment_id, created_at, updated_at FROM commentsdb WHERE topic_id = ? AND is_approved = 1` args := []interface{}{topicID} // Add optional filters if provided and not "null" // if siliconID != "" && siliconID != "null" { // baseQuery += " AND silicon_id = ?" // args = append(args, siliconID) // } // if pbID != "" && pbID != "null" { // baseQuery += " AND pb_id = ?" // args = append(args, pbID) // } baseQuery += " ORDER BY created_at DESC" log.Printf("Executing query: %s\nParameters: %v\n", baseQuery, args) rows, err := db.Query(baseQuery, args...) if err != nil { http.Error(w, "Database query failed: "+err.Error(), http.StatusInternalServerError) return } defer rows.Close() var comments []Comment for rows.Next() { var comment Comment var parentID sql.NullString err := rows.Scan( &comment.ID, &comment.CommentID, &comment.TopicID, &comment.SiliconID, &comment.PbID, &comment.UserID, &comment.UserName, &comment.CommentText, &comment.IsApproved, &parentID, &comment.CreatedAt, &comment.UpdatedAt, ) if err != nil { http.Error(w, "Failed to scan comment: "+err.Error(), http.StatusInternalServerError) return } comment.ParentCommentID = parentID comments = append(comments, comment) } if comments == nil { comments = []Comment{} } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(comments) } func handleCommentThread(w http.ResponseWriter, r *http.Request) { if r.Method != "GET" { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } pathParts := strings.Split(r.URL.Path, "/") if len(pathParts) < 4 { http.Error(w, "Invalid comment ID", http.StatusBadRequest) return } commentID := pathParts[3] var parentComment Comment var parentID sql.NullString err := db.QueryRow( `SELECT id, comment_id, topic_id, silicon_id, pb_id, user_id, user_name, comment_text, is_approved, parent_comment_id, created_at, updated_at FROM commentsdb WHERE comment_id = ?`, commentID, ).Scan( &parentComment.ID, &parentComment.CommentID, &parentComment.TopicID, &parentComment.SiliconID, &parentComment.PbID, &parentComment.UserID, &parentComment.UserName, &parentComment.CommentText, &parentComment.IsApproved, &parentID, &parentComment.CreatedAt, &parentComment.UpdatedAt, ) if err != nil { if err == sql.ErrNoRows { http.Error(w, "Parent comment not found", http.StatusNotFound) } else { http.Error(w, "Database error", http.StatusInternalServerError) } return } parentComment.ParentCommentID = parentID replies, err := getRepliesRecursive(commentID) if err != nil { http.Error(w, "Error fetching replies", http.StatusInternalServerError) return } parentComment.Replies = replies w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(parentComment) } func getRepliesRecursive(parentID string) ([]Comment, error) { rows, err := db.Query( `SELECT id, comment_id, topic_id, silicon_id, pb_id, user_id, user_name, comment_text, is_approved, parent_comment_id, created_at, updated_at FROM commentsdb WHERE parent_comment_id = ? ORDER BY created_at ASC`, parentID, ) if err != nil { return nil, fmt.Errorf("query failed: %v", err) } defer rows.Close() var replies []Comment for rows.Next() { var reply Comment var parentID sql.NullString err := rows.Scan( &reply.ID, &reply.CommentID, &reply.TopicID, &reply.SiliconID, &reply.PbID, &reply.UserID, &reply.UserName, &reply.CommentText, &reply.IsApproved, &parentID, &reply.CreatedAt, &reply.UpdatedAt, ) if err != nil { return nil, fmt.Errorf("scan failed: %v", err) } reply.ParentCommentID = parentID nestedReplies, err := getRepliesRecursive(reply.CommentID) if err != nil { return nil, fmt.Errorf("recursive query failed: %v", err) } reply.Replies = nestedReplies replies = append(replies, reply) } return replies, nil } func handleCommentApproval(w http.ResponseWriter, r *http.Request) { if r.Method != "PUT" { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } var req struct { CommentID string `json:"comment_id"` IsApproved bool `json:"is_approved"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "Invalid request payload", http.StatusBadRequest) return } _, err := db.Exec( "UPDATE commentsdb SET is_approved = ? WHERE comment_id = ?", req.IsApproved, req.CommentID, ) if err != nil { http.Error(w, "Failed to update comment", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]string{"status": "success"}) } func handleSingleComment(w http.ResponseWriter, r *http.Request) { if r.Method != "GET" { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } pathParts := strings.Split(r.URL.Path, "/") if len(pathParts) < 3 { http.Error(w, "Invalid comment ID", http.StatusBadRequest) return } commentID := pathParts[2] var comment Comment var parentID sql.NullString err := db.QueryRow( `SELECT id, comment_id, topic_id, silicon_id, pb_id, user_id, user_name, comment_text, is_approved, parent_comment_id, created_at, updated_at FROM commentsdb WHERE comment_id = ?`, commentID, ).Scan( &comment.ID, &comment.CommentID, &comment.TopicID, &comment.SiliconID, &comment.PbID, &comment.UserID, &comment.UserName, &comment.CommentText, &comment.IsApproved, &parentID, &comment.CreatedAt, &comment.UpdatedAt, ) if err != nil { if err == sql.ErrNoRows { http.Error(w, "Comment not found", http.StatusNotFound) } else { http.Error(w, "Database error", http.StatusInternalServerError) } return } comment.ParentCommentID = parentID w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(comment) } func generateUniqueID() string { mutex.Lock() defer mutex.Unlock() idCounter++ var id strings.Builder id.WriteString(idPrefix) n := idCounter for i := 0; i < idLength-len(idPrefix)-2; i++ { id.WriteByte(baseChars[n%36]) n /= 36 } rand.Seed(time.Now().UnixNano()) for i := 0; i < 2; i++ { id.WriteByte(baseChars[rand.Intn(36)]) } return id.String() } func initIDCounter() error { var maxID sql.NullString err := db.QueryRow("SELECT MAX(comment_id) FROM commentsdb").Scan(&maxID) if err != nil { return fmt.Errorf("error getting max ID: %v", err) } if !maxID.Valid { idCounter = 0 return nil } if !strings.HasPrefix(maxID.String, idPrefix) { return fmt.Errorf("invalid ID format in database") } trimmed := strings.TrimPrefix(maxID.String, idPrefix) if len(trimmed) < 2 { return fmt.Errorf("ID too short") } counterPart := trimmed[:len(trimmed)-2] var counter uint64 for _, c := range counterPart { idx := strings.IndexByte(baseChars, byte(c)) if idx == -1 { return fmt.Errorf("invalid character in ID") } counter = counter*36 + uint64(idx) } idCounter = counter return nil }