First pass at basic functionality.
This PR introduces the beginnings of Sprint Padawan. Reviewed-on: #1
This commit was merged in pull request #1.
This commit is contained in:
+233
@@ -0,0 +1,233 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user