package api import ( "errors" "net/http" "strconv" "sync" "sprintpadawan/lib" ) type contextKey string const userKey contextKey = "user" type sseClient struct { ch chan string userID int } type MemberView struct { Username string HasVoted bool ID int } type RoomData struct { *lib.Room User *lib.User Members []MemberView Stories []lib.Story IsOwner bool UserVotes map[int]string StoryVotes map[int][]lib.VoteView } var ( roomClients = make(map[int][]*sseClient) clientsMu sync.RWMutex ) func addSSEClient(roomID, userID int, ch chan string) { clientsMu.Lock() defer clientsMu.Unlock() roomClients[roomID] = append(roomClients[roomID], &sseClient{ch: ch, userID: userID}) } func removeSSEClient(roomID int, ch chan string) { clientsMu.Lock() defer clientsMu.Unlock() clients := roomClients[roomID] for i, c := range clients { if c.ch == ch { roomClients[roomID] = append(clients[:i], clients[i+1:]...) break } } if len(roomClients[roomID]) == 0 { delete(roomClients, roomID) } } func broadcast(roomID int, event string) { clientsMu.RLock() snapshot := append([]*sseClient(nil), roomClients[roomID]...) clientsMu.RUnlock() if len(snapshot) == 0 { return } deadSet := make(map[*sseClient]struct{}) for _, c := range snapshot { select { case c.ch <- event: default: deadSet[c] = struct{}{} } } if len(deadSet) == 0 { return } clientsMu.Lock() defer clientsMu.Unlock() current := roomClients[roomID] alive := current[:0] for _, c := range current { if _, dead := deadSet[c]; dead { continue } alive = append(alive, c) } if len(alive) == 0 { delete(roomClients, roomID) return } roomClients[roomID] = alive } func GetConnectedUserIDs(roomID int) []int { clientsMu.RLock() defer clientsMu.RUnlock() seen := make(map[int]bool) var ids []int for _, c := range roomClients[roomID] { if !seen[c.userID] { seen[c.userID] = true ids = append(ids, c.userID) } } return ids } func scaleToOptions(scale string) []string { switch scale { case "fibonacci": return []string{"1", "2", "3", "5", "8", "13", "21", "?"} case "tshirt": return []string{"XS", "S", "M", "L", "XL", "XXL", "?"} case "linear": return []string{"1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "?"} default: return []string{"1", "2", "3", "5", "8", "13", "21", "?"} } } func getRoomID(r *http.Request) (int, error) { return getPathInt(r, "id") } func getPathInt(r *http.Request, key string) (int, error) { raw := r.PathValue(key) if raw == "" { return 0, errors.New("missing path parameter") } id, err := strconv.Atoi(raw) if err != nil || id <= 0 { return 0, errors.New("invalid path parameter") } return id, nil } func getFormInt(r *http.Request, key string) (int, error) { raw := r.FormValue(key) if raw == "" { return 0, errors.New("missing form value") } id, err := strconv.Atoi(raw) if err != nil || id <= 0 { return 0, errors.New("invalid form value") } return id, nil } func buildRoomData(room *lib.Room, user *lib.User) (RoomData, error) { members, err := lib.GetRoomMembers(room.ID) if err != nil { return RoomData{}, err } stories, err := lib.GetStoriesForRoom(room.ID) if err != nil { return RoomData{}, err } connectedIDs := GetConnectedUserIDs(room.ID) connectedMap := make(map[int]bool) for _, cid := range connectedIDs { connectedMap[cid] = true } connectedMap[user.ID] = true storyIDs := make([]int, 0, len(stories)) revealedStoryIDs := make([]int, 0, len(stories)) for _, s := range stories { storyIDs = append(storyIDs, s.ID) if s.Voted { revealedStoryIDs = append(revealedStoryIDs, s.ID) } } votesByStory, err := lib.GetVotesForStories(storyIDs) if err != nil { return RoomData{}, err } storyVotes, err := lib.GetVoteViewsForStories(revealedStoryIDs) if err != nil { return RoomData{}, err } activeVotesByUser := make(map[int]bool) if room.ActiveStoryID != nil { for _, v := range votesByStory[*room.ActiveStoryID] { activeVotesByUser[v.UserID] = true } } var memberViews []MemberView for _, m := range members { if !connectedMap[m.ID] { continue } hasVoted := activeVotesByUser[m.ID] memberViews = append(memberViews, MemberView{ Username: m.Username, HasVoted: hasVoted, ID: m.ID, }) } userVotes := make(map[int]string) for _, s := range stories { for _, v := range votesByStory[s.ID] { if v.UserID == user.ID { userVotes[s.ID] = v.Value } } } return RoomData{ Room: room, User: user, Members: memberViews, Stories: stories, IsOwner: room.OwnerID == user.ID, UserVotes: userVotes, StoryVotes: storyVotes, }, nil }