16bed1b8c0
This PR introduces the beginnings of Sprint Padawan. Reviewed-on: #1
234 lines
4.6 KiB
Go
234 lines
4.6 KiB
Go
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
|
|
}
|