From 39e90b618522a9f28ffd57b7a423058b16f756aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20St=C4=99pie=C5=84?= Date: Wed, 8 Jan 2025 22:35:22 +0100 Subject: [PATCH 1/2] Prototype full messaging system --- cmd/feelbeatserver/main.go | 14 +- config_example.toml | 4 +- internal/infra/api/roomapi/fetchgames.go | 2 +- internal/infra/ws/client.go | 121 -------------- internal/infra/ws/message.go | 6 - internal/infra/ws/servewebsockets.go | 40 +++-- internal/infra/ws/socketclient.go | 137 ++++++++++++++++ internal/infra/ws/types.go | 15 -- internal/infra/ws/wshandler.go | 13 ++ internal/infra/ws/wshub.go | 153 +++++++++++++----- internal/lib/component/main.go | 3 +- internal/lib/feelbeaterror/errorcode.go | 6 + internal/lib/messages/client.go | 20 +++ internal/lib/messages/hub.go | 11 ++ internal/lib/messages/server.go | 10 ++ internal/lib/messages/usersocket.go | 17 ++ internal/lib/room/room.go | 54 +++++-- .../roomrepository/inmemoryroomrepository.go | 26 +-- internal/lib/roomrepository/roomrepository.go | 3 +- 19 files changed, 427 insertions(+), 228 deletions(-) delete mode 100644 internal/infra/ws/client.go delete mode 100644 internal/infra/ws/message.go create mode 100644 internal/infra/ws/socketclient.go delete mode 100644 internal/infra/ws/types.go create mode 100644 internal/infra/ws/wshandler.go create mode 100644 internal/lib/messages/client.go create mode 100644 internal/lib/messages/hub.go create mode 100644 internal/lib/messages/server.go create mode 100644 internal/lib/messages/usersocket.go diff --git a/cmd/feelbeatserver/main.go b/cmd/feelbeatserver/main.go index 45c3d19..11fc2e7 100644 --- a/cmd/feelbeatserver/main.go +++ b/cmd/feelbeatserver/main.go @@ -3,6 +3,7 @@ package main import ( "fmt" "log" + "log/slog" "net/http" "os" @@ -21,6 +22,7 @@ const ( ) func main() { + slog.SetLogLoggerLevel(slog.LevelDebug) fblog.ColorizeLogger() config := koanf.New(".") @@ -32,17 +34,15 @@ func main() { fblog.Info(component.FeelBeatServer, "config loaded", "config", config.All()) - port := config.MustInt("websocket.port") + port := config.MustInt("server.port") path := config.MustString("websocket.path") - hub := ws.NewHub() - go hub.Run() + roomRepo := roomrepository.NewInMemoryRoomRepository(spotify.SpotifyApi{}, ws.NewWSHub) - http.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) { - ws.ServeWebsockets(hub, w, r) - }) + ws := ws.NewWSHandler(path, roomRepo) + ws.ServeWebsockets(path, auth.AuthorizeThroughSpotify) - setupAPI(auth.AuthorizeThroughSpotify, roomrepository.NewInMemoryRoomRepository(spotify.SpotifyApi{})) + setupAPI(auth.AuthorizeThroughSpotify, roomRepo) fblog.Info(component.FeelBeatServer, "Server started", "port", port) log.Fatal(http.ListenAndServe(fmt.Sprintf("localhost:%d", port), nil)) diff --git a/config_example.toml b/config_example.toml index 21951d3..dd165f6 100644 --- a/config_example.toml +++ b/config_example.toml @@ -1,3 +1,5 @@ [websocket] -port = 3000 path = "/ws" + +[server] +port = 3000 diff --git a/internal/infra/api/roomapi/fetchgames.go b/internal/infra/api/roomapi/fetchgames.go index 88920ae..bd67f4d 100644 --- a/internal/infra/api/roomapi/fetchgames.go +++ b/internal/infra/api/roomapi/fetchgames.go @@ -31,7 +31,7 @@ func (r RoomApi) fetchRoomsHandler(user auth.User, res http.ResponseWriter, req formatted.Rooms = append(formatted.Rooms, responseRoom{ Id: room.Id(), Name: room.Name(), - Players: len(room.Players()), + Players: len(room.PlayerProfiles()), MaxPlayers: room.Settings().MaxPlayers, ImageUrl: room.ImageUrl(), }) diff --git a/internal/infra/ws/client.go b/internal/infra/ws/client.go deleted file mode 100644 index ac4a7bb..0000000 --- a/internal/infra/ws/client.go +++ /dev/null @@ -1,121 +0,0 @@ -package ws - -import ( - "time" - - "github.com/feelbeatapp/feelbeatserver/internal/lib/component" - "github.com/feelbeatapp/feelbeatserver/internal/infra/fblog" - "github.com/gorilla/websocket" -) - -const ( - defaultOutBufferSize = 256 - - // Time allowed to read the next pong mesage from the peer - pongWait = 60 * time.Second - - // Send ping frame with this period - pingPeriod = (pongWait * 9) / 10 -) - -type Client struct { - hub Hub - conn *websocket.Conn - send chan []byte -} - -func newClient(conn *websocket.Conn, hub Hub) *Client { - return &Client{ - hub: hub, - conn: conn, - send: make(chan []byte, defaultOutBufferSize), - } -} - -func (c *Client) setPongDeadline() { - err := c.conn.SetReadDeadline(time.Now().Add(pongWait)) - if err != nil { - fblog.Error(component.Client, "Unexpected error when setting up connection", "err", err) - } -} - -func (c *Client) readLoop() { - defer func() { - c.hub.UnregisterClient(c) - c.conn.Close() - }() - - c.setPongDeadline() - c.conn.SetPongHandler(func(string) error { - c.setPongDeadline() - return nil - }) - - for { - msgType, message, err := c.conn.ReadMessage() - if err != nil { - if websocket.IsCloseError(err, websocket.CloseNormalClosure) { - fblog.Info(component.Client, "client closed connection", "ip", c.conn.RemoteAddr()) - } else { - fblog.Error(component.Client, "Received unexpected error from client", "err", err) - } - break - } - - switch msgType { - case websocket.TextMessage: - c.hub.Broadcast(ClientMessage{ - From: c, - Payload: message, - }) - default: - fblog.Warn(component.Client, "ignoring message", "type", msgType, "msg", message) - } - } -} - -func (c *Client) sendLoop() { - ticker := time.NewTicker(pingPeriod) - defer func() { - ticker.Stop() - c.conn.Close() - }() - - for { - select { - case message, ok := <-c.send: - if !ok { - fblog.Info(component.Client, "server is closing connection", "with", c.conn.RemoteAddr()) - return - } - - err := c.conn.WriteMessage(websocket.TextMessage, message) - - if err != nil { - fblog.Error(component.Client, "error when sending message", "msg", message) - } - case <-ticker.C: - err := c.conn.WriteMessage(websocket.PingMessage, nil) - if err != nil { - fblog.Error(component.Client, "couldn't ping client", "ip", c.conn.RemoteAddr()) - } - } - - } -} - -func (c *Client) Send(payload []byte) { - c.send <- payload -} - -func (c *Client) Close() { - err := c.conn.WriteMessage(websocket.CloseMessage, nil) - if err != nil { - fblog.Error(component.Client, "couldn't close connection gracefully", "err", err) - c.conn.Close() - } -} - -func (c *Client) CloseNow() { - close(c.send) -} diff --git a/internal/infra/ws/message.go b/internal/infra/ws/message.go deleted file mode 100644 index d350137..0000000 --- a/internal/infra/ws/message.go +++ /dev/null @@ -1,6 +0,0 @@ -package ws - -type ClientMessage struct { - From HubClient - Payload []byte -} diff --git a/internal/infra/ws/servewebsockets.go b/internal/infra/ws/servewebsockets.go index ad8b7bd..ce81fa3 100644 --- a/internal/infra/ws/servewebsockets.go +++ b/internal/infra/ws/servewebsockets.go @@ -1,11 +1,13 @@ package ws import ( + "fmt" "net/http" - "os" - "github.com/feelbeatapp/feelbeatserver/internal/lib/component" - "github.com/feelbeatapp/feelbeatserver/internal/infra/fblog" + "github.com/feelbeatapp/feelbeatserver/internal/infra/api" + "github.com/feelbeatapp/feelbeatserver/internal/infra/auth" + "github.com/feelbeatapp/feelbeatserver/internal/lib/feelbeaterror" + "github.com/feelbeatapp/feelbeatserver/internal/lib/messages" "github.com/gorilla/websocket" ) @@ -15,20 +17,30 @@ var upgrader = websocket.Upgrader{ }, } -func ServeWebsockets(hub *WSHub, w http.ResponseWriter, r *http.Request) { - conn, err := upgrader.Upgrade(w, r, nil) - - fblog.Info(component.WebSocket, "received new connection", "ip", r.RemoteAddr) - +func (w WSHandler) websocketHandler(user auth.User, res http.ResponseWriter, req *http.Request) { + roomId := req.PathValue("id") + room := w.roomRepo.GetRoom(roomId) + if room == nil { + http.Error(res, feelbeaterror.RoomNotFound, feelbeaterror.StatusCode(feelbeaterror.RoomNotFound)) + api.LogApiError("User tried to connect to non existing room", nil, user.Profile.Id, req) + return + } + conn, err := upgrader.Upgrade(res, req, nil) if err != nil { - fblog.Error(component.WebSocket, "failed to upgrade connection", "ip", r.RemoteAddr) - os.Exit(1) + http.Error(res, feelbeaterror.Default, feelbeaterror.StatusCode(feelbeaterror.Default)) + api.LogApiError("Upgrading websocket connection failed", err, user.Profile.Id, req) return } - client := newClient(conn, hub) - hub.RegisterClient(client) + userSocket := messages.NewUserSocket(newSocketClient(conn), user) + err = room.Hub().Register(userSocket) + if err != nil { + http.Error(res, feelbeaterror.RoomNotFound, feelbeaterror.StatusCode(feelbeaterror.RoomNotFound)) + api.LogApiError("Registering socket failed, hub is closed", err, user.Profile.Id, req) + return + } +} - go client.readLoop() - go client.sendLoop() +func (w WSHandler) ServeWebsockets(basePath string, authWrapper auth.AuthWrapper) { + http.HandleFunc(fmt.Sprintf("%s/{id}", basePath), authWrapper(w.websocketHandler)) } diff --git a/internal/infra/ws/socketclient.go b/internal/infra/ws/socketclient.go new file mode 100644 index 0000000..e0b017d --- /dev/null +++ b/internal/infra/ws/socketclient.go @@ -0,0 +1,137 @@ +package ws + +import ( + "context" + "fmt" + "time" + + "github.com/feelbeatapp/feelbeatserver/internal/infra/fblog" + "github.com/feelbeatapp/feelbeatserver/internal/lib/component" + "github.com/gorilla/websocket" +) + +const ( + defaultOutBufferSize = 256 + + // Time allowed to read the next pong mesage from the peer + pongWait = 60 * time.Second + + // Send ping frame with this period + pingPeriod = (pongWait * 9) / 10 +) + +type SocketClient struct { + conn *websocket.Conn + rcv chan []byte + snd <-chan []byte + ctx context.Context + cancel context.CancelFunc +} + +func newSocketClient(conn *websocket.Conn) *SocketClient { + return &SocketClient{ + conn: conn, + rcv: make(chan []byte), + } +} + +func (s *SocketClient) Run(snd <-chan []byte) { + s.ctx, s.cancel = context.WithCancel(context.Background()) + s.snd = snd + + go s.receivingLoop() + go s.sendingLoop() +} + +func (s *SocketClient) ReceiveChannel() <-chan []byte { + return s.rcv +} + +func (s *SocketClient) setPongDeadline() error { + err := s.conn.SetReadDeadline(time.Now().Add(pongWait)) + if err != nil { + fblog.Error(component.Socket, "Pong dead line error: ", "err", err, "conn", s.conn) + return err + } + + return nil +} + +func (s *SocketClient) receivingLoop() { + defer func() { + close(s.rcv) + s.cancel() + }() + + err := s.setPongDeadline() + if err != nil { + return + } + s.conn.SetPongHandler(func(string) error { + return s.setPongDeadline() + }) + + for { + msgType, message, err := s.conn.ReadMessage() + if err != nil { + if websocket.IsCloseError(err, websocket.CloseNormalClosure) { + fblog.Info(component.Socket, "client closed connection", "ip", s.conn.RemoteAddr()) + } else { + fblog.Error(component.Socket, "client connection closing due to unexpected error", "err", err, "ip", s.conn.RemoteAddr()) + } + return + } + + switch msgType { + case websocket.TextMessage: + select { + case <-s.ctx.Done(): + return + case s.rcv <- message: + } + default: + fblog.Warn(component.Socket, "ignoring unexpected message", "type", msgType, "msg", string(message)) + } + } +} + +func (s *SocketClient) sendingLoop() { + ticker := time.NewTicker(pingPeriod) + defer func() { + ticker.Stop() + + for msg := range s.snd { + err := s.conn.WriteMessage(websocket.TextMessage, msg) + if err != nil { + fblog.Error(component.Socket, "error during draning send channel after server closing", "with", s.conn.RemoteAddr()) + } + } + + s.conn.Close() + s.cancel() + }() + + for { + select { + case message, ok := <-s.snd: + if !ok { + fblog.Info(component.Socket, "server is closing connection", "with", s.conn.RemoteAddr()) + return + } + + fmt.Println("Sending") + fmt.Println(string(message)) + err := s.conn.WriteMessage(websocket.TextMessage, message) + if err != nil { + fblog.Error(component.Socket, "error when sending message", "msg", message, "err", err) + } + case <-ticker.C: + err := s.conn.WriteMessage(websocket.PingMessage, nil) + if err != nil { + fblog.Error(component.Socket, "couldn't ping client", "ip", s.conn.RemoteAddr()) + } + case <-s.ctx.Done(): + return + } + } +} diff --git a/internal/infra/ws/types.go b/internal/infra/ws/types.go deleted file mode 100644 index 58467c1..0000000 --- a/internal/infra/ws/types.go +++ /dev/null @@ -1,15 +0,0 @@ -package ws - -type Hub interface { - RegisterClient(HubClient) - UnregisterClient(HubClient) - Broadcast(ClientMessage) -} - -type HubClient interface { - Send([]byte) - // Closes with notifing client - Close() - // Closes immediately without sending any closing message - CloseNow() -} diff --git a/internal/infra/ws/wshandler.go b/internal/infra/ws/wshandler.go new file mode 100644 index 0000000..a6e0916 --- /dev/null +++ b/internal/infra/ws/wshandler.go @@ -0,0 +1,13 @@ +package ws + +import "github.com/feelbeatapp/feelbeatserver/internal/lib/roomrepository" + +type WSHandler struct { + roomRepo roomrepository.RoomRepository +} + +func NewWSHandler(basePath string, roomRepo roomrepository.RoomRepository) WSHandler { + return WSHandler{ + roomRepo: roomRepo, + } +} diff --git a/internal/infra/ws/wshub.go b/internal/infra/ws/wshub.go index 5af5f62..9c8a256 100644 --- a/internal/infra/ws/wshub.go +++ b/internal/infra/ws/wshub.go @@ -1,72 +1,141 @@ package ws import ( - "github.com/feelbeatapp/feelbeatserver/internal/lib/component" + "encoding/json" + "fmt" + "github.com/feelbeatapp/feelbeatserver/internal/infra/fblog" + "github.com/feelbeatapp/feelbeatserver/internal/lib/component" + "github.com/feelbeatapp/feelbeatserver/internal/lib/feelbeaterror" + "github.com/feelbeatapp/feelbeatserver/internal/lib/messages" ) type WSHub struct { - clients map[HubClient]bool - broadcast chan ClientMessage - register chan HubClient - unregister chan HubClient - exit chan bool + clients map[string]WSHubUser + snd <-chan messages.ServerMessage + rcv chan messages.ClientMessage + register chan messages.UserClient + unregister chan string + closed chan bool +} + +type WSHubUser struct { + userClient messages.UserClient + snd chan []byte } -func NewHub() *WSHub { +func NewWSHub() messages.Hub { return &WSHub{ - clients: make(map[HubClient]bool), - broadcast: make(chan ClientMessage), - register: make(chan HubClient), - unregister: make(chan HubClient), - exit: make(chan bool), + clients: make(map[string]WSHubUser), + rcv: make(chan messages.ClientMessage), + register: make(chan messages.UserClient), + unregister: make(chan string), + closed: make(chan bool), + } +} + +func (h *WSHub) Run(snd <-chan messages.ServerMessage) <-chan messages.ClientMessage { + h.snd = snd + go h.run() + return h.rcv +} + +func (h *WSHub) Register(user messages.UserClient) error { + select { + case h.register <- user: + return nil + case <-h.closed: + return fmt.Errorf("Hub is already closed") } } -func (h *WSHub) Run() { +func (h *WSHub) run() { defer func() { - for c := range h.clients { - c.Close() - } - close(h.broadcast) close(h.register) close(h.unregister) - close(h.exit) + + for msg := range h.snd { + h.sendMessage(msg) + } + + close(h.rcv) + + for _, client := range h.clients { + close(client.snd) + delete(h.clients, client.userClient.User.Profile.Id) + } }() for { select { - case client := <-h.register: - h.clients[client] = true - fblog.Info(component.Hub, "new client registered") - case client := <-h.unregister: - delete(h.clients, client) - client.CloseNow() - fblog.Info(component.Client, "client unregistered") - case message := <-h.broadcast: - for client := range h.clients { - if client != message.From { - client.Send(message.Payload) - } + case usersocket := <-h.register: + hubUser := WSHubUser{ + userClient: usersocket, + snd: make(chan []byte), } - case <-h.exit: - break + h.clients[usersocket.User.Profile.Id] = hubUser + go usersocket.Client.Run(hubUser.snd) + go h.passMessages(hubUser.userClient.User.Profile.Id, hubUser.userClient.Client.ReceiveChannel()) + + h.rcv <- messages.ClientMessage{ + Type: messages.JoiningPlayer, + From: usersocket.User.Profile.Id, + Payload: messages.JoiningPlayerPayload{ + User: usersocket.User.Profile, + }, + } + case userId := <-h.unregister: + h.rcv <- messages.ClientMessage{ + Type: messages.LeavingPlayer, + From: userId, + } + close(h.clients[userId].snd) + delete(h.clients, userId) + + case message, ok := <-h.snd: + if !ok { + close(h.closed) + return + } + h.sendMessage(message) } } } -func (h *WSHub) RegisterClient(client HubClient) { - h.register <- client -} +func (h *WSHub) passMessages(from string, rcv <-chan []byte) { + for bytes := range rcv { + var message messages.ClientMessage + err := json.Unmarshal(bytes, &message) + if err != nil { + fblog.Error(component.Hub, "Failed to parse client message", "err", err) + } + message.From = from + fmt.Println("Check") + fmt.Println(message) + fmt.Println(from) -func (h *WSHub) Broadcast(message ClientMessage) { - h.broadcast <- message -} + h.rcv <- message + } -func (h *WSHub) UnregisterClient(client HubClient) { - h.unregister <- client + h.unregister <- from } -func (h *WSHub) Stop() { - h.exit <- true +func (h *WSHub) sendMessage(message messages.ServerMessage) { + if msg, ok := message.Payload.(string); ok { + if msg == messages.KickUser { + for _, id := range message.To { + close(h.clients[id].snd) + } + return + } + } + + bytes, err := json.Marshal(message.Payload) + if err != nil { + bytes = []byte(feelbeaterror.EncodingMessageFailed) + } + + for _, id := range message.To { + h.clients[id].snd <- bytes + } } diff --git a/internal/lib/component/main.go b/internal/lib/component/main.go index bb515e3..553b125 100644 --- a/internal/lib/component/main.go +++ b/internal/lib/component/main.go @@ -4,10 +4,11 @@ const ( FeelBeatServer = "FeelBeatServer" Config = "config" WebSocket = "websocket" - Client = "client" + Socket = "socket" Hub = "hub" Api = "api" Auth = "auth" + Room = "room" AudioDownloadTask = "audiodownloadtask" ) diff --git a/internal/lib/feelbeaterror/errorcode.go b/internal/lib/feelbeaterror/errorcode.go index 4e4ac22..9a2edf6 100644 --- a/internal/lib/feelbeaterror/errorcode.go +++ b/internal/lib/feelbeaterror/errorcode.go @@ -8,10 +8,16 @@ const ( Default = "Unexpected error occurred" AuthFailed = "Authorization failed" LoadingPlaylistFailed = "Playlist loading failed" + RoomNotFound = "Room not found" + EncodingMessageFailed = "Encoding message failed" ) func StatusCode(code ErrorCode) int { switch code { + case RoomNotFound: + return http.StatusNotFound + case AuthFailed: + return http.StatusForbidden default: return http.StatusInternalServerError } diff --git a/internal/lib/messages/client.go b/internal/lib/messages/client.go new file mode 100644 index 0000000..2646dc7 --- /dev/null +++ b/internal/lib/messages/client.go @@ -0,0 +1,20 @@ +package messages + +import "github.com/feelbeatapp/feelbeatserver/internal/lib" + +type ClientMessageType string + +const ( + JoiningPlayer = "JOIN" + LeavingPlayer = "LEAVE" +) + +type ClientMessage struct { + Type ClientMessageType `json:"type"` + From string + Payload interface{} `json:"payload"` +} + +type JoiningPlayerPayload struct { + User lib.UserProfile +} diff --git a/internal/lib/messages/hub.go b/internal/lib/messages/hub.go new file mode 100644 index 0000000..c72e108 --- /dev/null +++ b/internal/lib/messages/hub.go @@ -0,0 +1,11 @@ +package messages + +type Hub interface { + Run(<-chan ServerMessage) <-chan ClientMessage + Register(UserClient) error +} + +type HubClient interface { + Run(<-chan []byte) + ReceiveChannel() <-chan []byte +} diff --git a/internal/lib/messages/server.go b/internal/lib/messages/server.go new file mode 100644 index 0000000..004500a --- /dev/null +++ b/internal/lib/messages/server.go @@ -0,0 +1,10 @@ +package messages + +type ServerMessageType string + +type ServerMessage struct { + To []string + Payload interface{} +} + +const KickUser = "KICK" diff --git a/internal/lib/messages/usersocket.go b/internal/lib/messages/usersocket.go new file mode 100644 index 0000000..2340b5b --- /dev/null +++ b/internal/lib/messages/usersocket.go @@ -0,0 +1,17 @@ +package messages + +import ( + "github.com/feelbeatapp/feelbeatserver/internal/infra/auth" +) + +type UserClient struct { + Client HubClient + User auth.User +} + +func NewUserSocket(client HubClient, user auth.User) UserClient { + return UserClient{ + Client: client, + User: user, + } +} diff --git a/internal/lib/room/room.go b/internal/lib/room/room.go index a441d60..078c51d 100644 --- a/internal/lib/room/room.go +++ b/internal/lib/room/room.go @@ -1,7 +1,10 @@ package room import ( + "github.com/feelbeatapp/feelbeatserver/internal/infra/fblog" "github.com/feelbeatapp/feelbeatserver/internal/lib" + "github.com/feelbeatapp/feelbeatserver/internal/lib/component" + "github.com/feelbeatapp/feelbeatserver/internal/lib/messages" ) type Room struct { @@ -9,37 +12,68 @@ type Room struct { playlist lib.PlaylistData owner lib.UserProfile settings RoomSettings - players []Player + players map[string]Player + hub messages.Hub + snd chan messages.ServerMessage + rcv <-chan messages.ClientMessage } type Player struct { + profile lib.UserProfile } -func NewRoom(id string, playlist lib.PlaylistData, owner lib.UserProfile, settings RoomSettings) Room { - return Room{ +func NewRoom(id string, playlist lib.PlaylistData, owner lib.UserProfile, settings RoomSettings, hub messages.Hub) *Room { + return &Room{ id: id, playlist: playlist, owner: owner, settings: settings, - players: make([]Player, 0), + players: make(map[string]Player), + hub: hub, + snd: make(chan messages.ServerMessage), } } -func (r Room) Id() string { +func (r *Room) Id() string { return r.id } -func (r Room) Name() string { +func (r *Room) Name() string { return r.playlist.Name } -func (r Room) Players() []Player { - return r.players +func (r *Room) PlayerProfiles() []lib.UserProfile { + profiles := make([]lib.UserProfile, 0) + for _, p := range r.players { + profiles = append(profiles, p.profile) + } + + return profiles } -func (r Room) ImageUrl() string { +func (r *Room) ImageUrl() string { return r.playlist.ImageUrl } -func (r Room) Settings() RoomSettings { + +func (r *Room) Settings() RoomSettings { return r.settings } + +func (r *Room) Start() { + r.rcv = r.hub.Run(r.snd) + go r.processMessages() +} + +func (r *Room) Stop() { + close(r.snd) +} + +func (r *Room) Hub() messages.Hub { + return r.hub +} + +func (r *Room) processMessages() { + for message := range r.rcv { + fblog.Info(component.Room, "Received message", "room", r.id, "from", message.From, "type", message.Type, "payload", message.Payload) + } +} diff --git a/internal/lib/roomrepository/inmemoryroomrepository.go b/internal/lib/roomrepository/inmemoryroomrepository.go index f81f1a9..e5a4d23 100644 --- a/internal/lib/roomrepository/inmemoryroomrepository.go +++ b/internal/lib/roomrepository/inmemoryroomrepository.go @@ -5,6 +5,7 @@ import ( "github.com/feelbeatapp/feelbeatserver/internal/infra/auth" "github.com/feelbeatapp/feelbeatserver/internal/lib" + "github.com/feelbeatapp/feelbeatserver/internal/lib/messages" "github.com/feelbeatapp/feelbeatserver/internal/lib/room" "github.com/google/uuid" ) @@ -14,14 +15,16 @@ type SpotifyApi interface { } type InMemoryRoomRepository struct { - spotify SpotifyApi - rooms map[string]room.Room + createHub func() messages.Hub + spotify SpotifyApi + rooms map[string]*room.Room } -func NewInMemoryRoomRepository(spotify SpotifyApi) InMemoryRoomRepository { +func NewInMemoryRoomRepository(spotify SpotifyApi, createHub func() messages.Hub) InMemoryRoomRepository { return InMemoryRoomRepository{ - spotify: spotify, - rooms: make(map[string]room.Room), + createHub: createHub, + spotify: spotify, + rooms: make(map[string]*room.Room), } } @@ -31,19 +34,24 @@ func (r InMemoryRoomRepository) CreateRoom(user auth.User, settings room.RoomSet return "", err } - newRoom := room.NewRoom(uuid.NewString(), playlistData, user.Profile, settings) + newRoom := room.NewRoom(uuid.NewString(), playlistData, user.Profile, settings, r.createHub()) r.rooms[newRoom.Id()] = newRoom - fmt.Println(newRoom) + fmt.Println(r.rooms) + newRoom.Start() return newRoom.Id(), nil } -func (r InMemoryRoomRepository) GetAllRooms() []room.Room { - result := make([]room.Room, 0) +func (r InMemoryRoomRepository) GetAllRooms() []*room.Room { + result := make([]*room.Room, 0) for _, room := range r.rooms { result = append(result, room) } return result } + +func (r InMemoryRoomRepository) GetRoom(id string) *room.Room { + return r.rooms[id] +} diff --git a/internal/lib/roomrepository/roomrepository.go b/internal/lib/roomrepository/roomrepository.go index a687603..490edce 100644 --- a/internal/lib/roomrepository/roomrepository.go +++ b/internal/lib/roomrepository/roomrepository.go @@ -7,5 +7,6 @@ import ( type RoomRepository interface { CreateRoom(user auth.User, settings room.RoomSettings) (string, error) - GetAllRooms() []room.Room + GetAllRooms() []*room.Room + GetRoom(id string) *room.Room } From cc7efed0fd2a547e4aa8a908424bfa1099f9ad0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20St=C4=99pie=C5=84?= Date: Thu, 9 Jan 2025 22:39:59 +0100 Subject: [PATCH 2/2] Implement message flow and prototype room joining --- go.mod | 5 +- go.sum | 7 -- internal/infra/api/roomapi/creategame.go | 4 +- internal/infra/ws/servewebsockets.go | 2 + internal/infra/ws/socketclient.go | 5 - internal/infra/ws/wshub.go | 91 ++++++++++--------- internal/infra/ws_test/hub_test.go | 64 ------------- internal/lib/component/main.go | 1 + internal/lib/messages/server.go | 46 +++++++++- internal/lib/room/processMessages.go | 29 ++++++ internal/lib/room/room.go | 83 +++++++++++++++-- internal/lib/room/roomsettings.go | 10 -- .../roomrepository/inmemoryroomrepository.go | 9 +- internal/lib/roomrepository/roomrepository.go | 3 +- internal/lib/types.go | 32 ++++--- .../spotify/playlistdataresponse.go | 5 + internal/thirdparty/spotify/spotifyapi.go | 10 +- 17 files changed, 247 insertions(+), 159 deletions(-) delete mode 100644 internal/infra/ws_test/hub_test.go create mode 100644 internal/lib/room/processMessages.go delete mode 100644 internal/lib/room/roomsettings.go diff --git a/go.mod b/go.mod index 281a448..1bd07fa 100644 --- a/go.mod +++ b/go.mod @@ -13,21 +13,18 @@ require ( github.com/knadh/koanf/v2 v2.1.2 github.com/lmittmann/tint v1.0.5 github.com/lrstanley/go-ytdlp v0.0.0-20241221063727-6717edbb36dd - github.com/stretchr/testify v1.9.0 ) require ( github.com/ProtonMail/go-crypto v1.0.0 // indirect github.com/cloudflare/circl v1.3.7 // indirect - github.com/davecgh/go-spew v1.1.1 // indirect github.com/fsnotify/fsnotify v1.8.0 // indirect github.com/go-viper/mapstructure/v2 v2.2.1 // indirect github.com/knadh/koanf/maps v0.1.1 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/pelletier/go-toml v1.9.5 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/stretchr/testify v1.9.0 // indirect golang.org/x/crypto v0.21.0 // indirect golang.org/x/sys v0.28.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 99004fd..6cf86d1 100644 --- a/go.sum +++ b/go.sum @@ -26,10 +26,6 @@ github.com/knadh/koanf/providers/file v1.1.2 h1:aCC36YGOgV5lTtAFz2qkgtWdeQsgfxUk github.com/knadh/koanf/providers/file v1.1.2/go.mod h1:/faSBcv2mxPVjFrXck95qeoyoZ5myJ6uxN8OOVNJJCI= github.com/knadh/koanf/v2 v2.1.2 h1:I2rtLRqXRy1p01m/utEtpZSSA6dcJbgGVuE27kW2PzQ= github.com/knadh/koanf/v2 v2.1.2/go.mod h1:Gphfaen0q1Fc1HTgJgSTC4oRX9R2R5ErYMZJy8fLJBo= -github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= -github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lmittmann/tint v1.0.5 h1:NQclAutOfYsqs2F1Lenue6OoWCajs5wJcP3DfWVpePw= github.com/lmittmann/tint v1.0.5/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE= github.com/lrstanley/go-ytdlp v0.0.0-20241221063727-6717edbb36dd h1:lLajTMgNTs/W4H05uQYnJDRIbIvHk6XXy7DQNFRbvzU= @@ -89,8 +85,5 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/infra/api/roomapi/creategame.go b/internal/infra/api/roomapi/creategame.go index cdf0c42..713a07f 100644 --- a/internal/infra/api/roomapi/creategame.go +++ b/internal/infra/api/roomapi/creategame.go @@ -9,9 +9,9 @@ import ( "github.com/feelbeatapp/feelbeatserver/internal/infra/api" "github.com/feelbeatapp/feelbeatserver/internal/infra/auth" "github.com/feelbeatapp/feelbeatserver/internal/infra/fblog" + "github.com/feelbeatapp/feelbeatserver/internal/lib" "github.com/feelbeatapp/feelbeatserver/internal/lib/component" "github.com/feelbeatapp/feelbeatserver/internal/lib/feelbeaterror" - "github.com/feelbeatapp/feelbeatserver/internal/lib/room" ) type createGameResponse struct { @@ -19,7 +19,7 @@ type createGameResponse struct { } func (r RoomApi) createGameHandler(user auth.User, res http.ResponseWriter, req *http.Request) { - var payload room.RoomSettings + var payload lib.RoomSettings err := api.ParseBody(req.Body, &payload) if err != nil { http.Error(res, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) diff --git a/internal/infra/ws/servewebsockets.go b/internal/infra/ws/servewebsockets.go index ce81fa3..bb407c5 100644 --- a/internal/infra/ws/servewebsockets.go +++ b/internal/infra/ws/servewebsockets.go @@ -39,6 +39,8 @@ func (w WSHandler) websocketHandler(user auth.User, res http.ResponseWriter, req api.LogApiError("Registering socket failed, hub is closed", err, user.Profile.Id, req) return } + + api.LogApiCall(user.Profile.Id, req) } func (w WSHandler) ServeWebsockets(basePath string, authWrapper auth.AuthWrapper) { diff --git a/internal/infra/ws/socketclient.go b/internal/infra/ws/socketclient.go index e0b017d..14c807b 100644 --- a/internal/infra/ws/socketclient.go +++ b/internal/infra/ws/socketclient.go @@ -2,7 +2,6 @@ package ws import ( "context" - "fmt" "time" "github.com/feelbeatapp/feelbeatserver/internal/infra/fblog" @@ -11,8 +10,6 @@ import ( ) const ( - defaultOutBufferSize = 256 - // Time allowed to read the next pong mesage from the peer pongWait = 60 * time.Second @@ -119,8 +116,6 @@ func (s *SocketClient) sendingLoop() { return } - fmt.Println("Sending") - fmt.Println(string(message)) err := s.conn.WriteMessage(websocket.TextMessage, message) if err != nil { fblog.Error(component.Socket, "error when sending message", "msg", message, "err", err) diff --git a/internal/infra/ws/wshub.go b/internal/infra/ws/wshub.go index 9c8a256..8ae88a5 100644 --- a/internal/infra/ws/wshub.go +++ b/internal/infra/ws/wshub.go @@ -1,8 +1,10 @@ package ws import ( + "context" "encoding/json" "fmt" + "sync" "github.com/feelbeatapp/feelbeatserver/internal/infra/fblog" "github.com/feelbeatapp/feelbeatserver/internal/lib/component" @@ -16,7 +18,7 @@ type WSHub struct { rcv chan messages.ClientMessage register chan messages.UserClient unregister chan string - closed chan bool + isOpen bool } type WSHubUser struct { @@ -30,7 +32,7 @@ func NewWSHub() messages.Hub { rcv: make(chan messages.ClientMessage), register: make(chan messages.UserClient), unregister: make(chan string), - closed: make(chan bool), + isOpen: true, } } @@ -41,29 +43,30 @@ func (h *WSHub) Run(snd <-chan messages.ServerMessage) <-chan messages.ClientMes } func (h *WSHub) Register(user messages.UserClient) error { - select { - case h.register <- user: - return nil - case <-h.closed: - return fmt.Errorf("Hub is already closed") + if !h.isOpen { + return fmt.Errorf("Hub is not open") } + + h.register <- user + return nil } func (h *WSHub) run() { + ctx, cancel := context.WithCancel(context.Background()) + var wg sync.WaitGroup + defer func() { - close(h.register) - close(h.unregister) + h.isOpen = false + cancel() + wg.Wait() - for msg := range h.snd { - h.sendMessage(msg) + for _, c := range h.clients { + close(c.snd) } + close(h.register) + close(h.unregister) close(h.rcv) - - for _, client := range h.clients { - close(client.snd) - delete(h.clients, client.userClient.User.Profile.Id) - } }() for { @@ -74,8 +77,9 @@ func (h *WSHub) run() { snd: make(chan []byte), } h.clients[usersocket.User.Profile.Id] = hubUser + wg.Add(1) go usersocket.Client.Run(hubUser.snd) - go h.passMessages(hubUser.userClient.User.Profile.Id, hubUser.userClient.Client.ReceiveChannel()) + go h.passMessages(ctx, &wg, hubUser.userClient.User.Profile.Id, hubUser.userClient.Client.ReceiveChannel()) h.rcv <- messages.ClientMessage{ Type: messages.JoiningPlayer, @@ -89,12 +93,14 @@ func (h *WSHub) run() { Type: messages.LeavingPlayer, From: userId, } - close(h.clients[userId].snd) + + if c, ok := h.clients[userId]; ok { + close(c.snd) + } delete(h.clients, userId) case message, ok := <-h.snd: if !ok { - close(h.closed) return } h.sendMessage(message) @@ -102,39 +108,40 @@ func (h *WSHub) run() { } } -func (h *WSHub) passMessages(from string, rcv <-chan []byte) { - for bytes := range rcv { - var message messages.ClientMessage - err := json.Unmarshal(bytes, &message) - if err != nil { - fblog.Error(component.Hub, "Failed to parse client message", "err", err) - } - message.From = from - fmt.Println("Check") - fmt.Println(message) - fmt.Println(from) - - h.rcv <- message - } - - h.unregister <- from -} +func (h *WSHub) passMessages(ctx context.Context, wg *sync.WaitGroup, from string, rcv <-chan []byte) { + defer func() { + wg.Done() + }() -func (h *WSHub) sendMessage(message messages.ServerMessage) { - if msg, ok := message.Payload.(string); ok { - if msg == messages.KickUser { - for _, id := range message.To { - close(h.clients[id].snd) + for { + select { + case bytes, ok := <-rcv: + if !ok { + h.unregister <- from + return + } + var message messages.ClientMessage + err := json.Unmarshal(bytes, &message) + if err != nil { + fblog.Error(component.Hub, "Failed to parse client message", "err", err) } + message.From = from + + h.rcv <- message + case <-ctx.Done(): return } } +} - bytes, err := json.Marshal(message.Payload) +func (h *WSHub) sendMessage(message messages.ServerMessage) { + bytes, err := json.Marshal(message.ToUnit()) if err != nil { bytes = []byte(feelbeaterror.EncodingMessageFailed) } + fblog.Info(component.Hub, "sending message", "type", message.Type, "to", message.To) + for _, id := range message.To { h.clients[id].snd <- bytes } diff --git a/internal/infra/ws_test/hub_test.go b/internal/infra/ws_test/hub_test.go deleted file mode 100644 index 8a93a28..0000000 --- a/internal/infra/ws_test/hub_test.go +++ /dev/null @@ -1,64 +0,0 @@ -package ws_test - -import ( - "testing" - "time" - - "github.com/feelbeatapp/feelbeatserver/internal/infra/ws" - "github.com/stretchr/testify/assert" -) - -type FakeClient struct { - payloads [][]byte - closed bool -} - -func newFakeClient() *FakeClient { - return &FakeClient{ - payloads: make([][]byte, 0), - closed: false, - } -} - -func (c *FakeClient) Send(payload []byte) { - c.payloads = append(c.payloads, payload) -} - -func (c *FakeClient) CloseNow() { - c.closed = true -} - -func (c *FakeClient) Close() { - c.closed = true -} - -const testMessage = "hi there" - -func TestHubBroadcastsMessages(t *testing.T) { - assert := assert.New(t) - hub := ws.NewHub() - - go hub.Run() - - clients := make([]*FakeClient, 5) - for i := range clients { - clients[i] = newFakeClient() - hub.RegisterClient(clients[i]) - } - - hub.Broadcast(ws.ClientMessage{ - From: clients[0], - Payload: []byte(testMessage), - }) - - assert.Equal(0, len(clients[0].payloads)) - - time.Sleep(time.Millisecond * 1) - - for i := 1; i < len(clients); i++ { - assert.NotEmpty(clients[i].payloads) - assert.Contains(clients[i].payloads, []byte(testMessage)) - } - - hub.Stop() -} diff --git a/internal/lib/component/main.go b/internal/lib/component/main.go index 553b125..3f078cd 100644 --- a/internal/lib/component/main.go +++ b/internal/lib/component/main.go @@ -9,6 +9,7 @@ const ( Api = "api" Auth = "auth" Room = "room" + RoomRepository = "room repository" AudioDownloadTask = "audiodownloadtask" ) diff --git a/internal/lib/messages/server.go b/internal/lib/messages/server.go index 004500a..a660654 100644 --- a/internal/lib/messages/server.go +++ b/internal/lib/messages/server.go @@ -1,10 +1,54 @@ package messages +import ( + "github.com/feelbeatapp/feelbeatserver/internal/lib" +) + type ServerMessageType string type ServerMessage struct { To []string + Type ServerMessageType Payload interface{} } -const KickUser = "KICK" +func (m ServerMessage) ToUnit() ServerMessageUnit { + return ServerMessageUnit{ + Type: m.Type, + Payload: m.Payload, + } +} + +type ServerMessageUnit struct { + Type ServerMessageType `json:"type"` + Payload interface{} `json:"payload"` +} + +const ( + InitialMessage = "INITIAL" + NewPlayer = "NEW_PLAYER" + PlayerLeft = "PLAYER_LEFT" +) + +type InitialGameState struct { + Id string `json:"id"` + Me string `json:"me"` + Admin string `json:"admin"` + Playlist PlaylistState `json:"playlist"` + Players []lib.UserProfile `json:"players"` + Settings lib.RoomSettings `json:"settings"` +} + +type PlaylistState struct { + Name string `json:"name"` + ImageUrl string `json:"imageUrl"` + Songs []SongState `json:"songs"` +} + +type SongState struct { + Id string `json:"id"` + Title string `json:"title"` + Artist string `json:"artist"` + ImageUrl string `json:"imageUrl"` + DurationSec int `json:"durationSec"` +} diff --git a/internal/lib/room/processMessages.go b/internal/lib/room/processMessages.go new file mode 100644 index 0000000..6c6f548 --- /dev/null +++ b/internal/lib/room/processMessages.go @@ -0,0 +1,29 @@ +package room + +import ( + "github.com/feelbeatapp/feelbeatserver/internal/infra/fblog" + "github.com/feelbeatapp/feelbeatserver/internal/lib/component" + "github.com/feelbeatapp/feelbeatserver/internal/lib/messages" +) + +func (r *Room) processMessages() { + for message := range r.rcv { + switch message.Type { + case messages.JoiningPlayer: + payload, ok := message.Payload.(messages.JoiningPlayerPayload) + if !ok { + logIncorrectPayload("Incorrect payload in player joining", message.Payload, message.From) + } else { + r.addPlayer(payload.User) + } + case messages.LeavingPlayer: + r.removePlayer(message.From) + default: + fblog.Warn(component.Room, "Received unexpected message", "room", r.id, "from", message.From, "type", message.Type, "payload", message.Payload) + } + } +} + +func logIncorrectPayload(text string, payload interface{}, from string) { + fblog.Error(component.Room, text, "payload", payload, "from", from) +} diff --git a/internal/lib/room/room.go b/internal/lib/room/room.go index 078c51d..1cfafc9 100644 --- a/internal/lib/room/room.go +++ b/internal/lib/room/room.go @@ -11,7 +11,7 @@ type Room struct { id string playlist lib.PlaylistData owner lib.UserProfile - settings RoomSettings + settings lib.RoomSettings players map[string]Player hub messages.Hub snd chan messages.ServerMessage @@ -22,7 +22,7 @@ type Player struct { profile lib.UserProfile } -func NewRoom(id string, playlist lib.PlaylistData, owner lib.UserProfile, settings RoomSettings, hub messages.Hub) *Room { +func NewRoom(id string, playlist lib.PlaylistData, owner lib.UserProfile, settings lib.RoomSettings, hub messages.Hub) *Room { return &Room{ id: id, playlist: playlist, @@ -55,7 +55,7 @@ func (r *Room) ImageUrl() string { return r.playlist.ImageUrl } -func (r *Room) Settings() RoomSettings { +func (r *Room) Settings() lib.RoomSettings { return r.settings } @@ -72,8 +72,79 @@ func (r *Room) Hub() messages.Hub { return r.hub } -func (r *Room) processMessages() { - for message := range r.rcv { - fblog.Info(component.Room, "Received message", "room", r.id, "from", message.From, "type", message.Type, "payload", message.Payload) +func (r *Room) addPlayer(profile lib.UserProfile) { + r.players[profile.Id] = Player{ + profile: profile, + } + + fblog.Info(component.Room, "new player", "roomId", r.id, "userId", profile.Id) + + playerProfiles := make([]lib.UserProfile, 0) + for _, p := range r.players { + playerProfiles = append(playerProfiles, p.profile) + } + + packedSongs := make([]messages.SongState, 0) + for _, s := range r.playlist.Songs { + packedSongs = append(packedSongs, messages.SongState{ + Id: s.Id, + Title: s.Details.Title, + Artist: s.Details.Artist, + ImageUrl: s.Details.ImageUrl, + DurationSec: int(s.Details.Duration.Seconds()), + }) + } + + r.snd <- messages.ServerMessage{ + To: []string{profile.Id}, + Type: messages.InitialMessage, + Payload: messages.InitialGameState{ + Id: r.id, + Me: profile.Id, + Admin: r.owner.Id, + Playlist: messages.PlaylistState{ + Name: r.Name(), + ImageUrl: r.ImageUrl(), + Songs: packedSongs, + }, + Players: playerProfiles, + Settings: r.settings, + }, + } + r.sendToAllExcept(profile.Id, messages.NewPlayer, profile) +} + +func (r *Room) removePlayer(id string) { + if _, ok := r.players[id]; !ok { + return + } + + delete(r.players, id) + recipents := make([]string, 0) + for _, p := range r.players { + recipents = append(recipents, p.profile.Id) + } + + fblog.Info(component.Room, "player leaves", "roomId", r.id, "playerId", id) + + r.snd <- messages.ServerMessage{ + To: recipents, + Type: messages.PlayerLeft, + Payload: id, + } +} + +func (r *Room) sendToAllExcept(id string, messageType messages.ServerMessageType, payload interface{}) { + recipents := make([]string, 0) + for _, p := range r.players { + if p.profile.Id != id { + recipents = append(recipents, p.profile.Id) + } + } + + r.snd <- messages.ServerMessage{ + To: recipents, + Type: messageType, + Payload: payload, } } diff --git a/internal/lib/room/roomsettings.go b/internal/lib/room/roomsettings.go deleted file mode 100644 index 217e12c..0000000 --- a/internal/lib/room/roomsettings.go +++ /dev/null @@ -1,10 +0,0 @@ -package room - -type RoomSettings struct { - MaxPlayers int `json:"maxPlayers"` - TurnCount int `json:"turnCount"` - TimePenaltyPerSecond int `json:"timePenaltyPerSecond"` - BasePoints int `json:"basePoints"` - IncorrectGuessPenalty int `json:"incorrectGuessPenalty"` - PlaylistId string `json:"playlistId"` -} diff --git a/internal/lib/roomrepository/inmemoryroomrepository.go b/internal/lib/roomrepository/inmemoryroomrepository.go index e5a4d23..6bba980 100644 --- a/internal/lib/roomrepository/inmemoryroomrepository.go +++ b/internal/lib/roomrepository/inmemoryroomrepository.go @@ -1,10 +1,10 @@ package roomrepository import ( - "fmt" - "github.com/feelbeatapp/feelbeatserver/internal/infra/auth" + "github.com/feelbeatapp/feelbeatserver/internal/infra/fblog" "github.com/feelbeatapp/feelbeatserver/internal/lib" + "github.com/feelbeatapp/feelbeatserver/internal/lib/component" "github.com/feelbeatapp/feelbeatserver/internal/lib/messages" "github.com/feelbeatapp/feelbeatserver/internal/lib/room" "github.com/google/uuid" @@ -28,7 +28,7 @@ func NewInMemoryRoomRepository(spotify SpotifyApi, createHub func() messages.Hub } } -func (r InMemoryRoomRepository) CreateRoom(user auth.User, settings room.RoomSettings) (string, error) { +func (r InMemoryRoomRepository) CreateRoom(user auth.User, settings lib.RoomSettings) (string, error) { playlistData, err := r.spotify.FetchPlaylistData(settings.PlaylistId, user.Token) if err != nil { return "", err @@ -37,9 +37,10 @@ func (r InMemoryRoomRepository) CreateRoom(user auth.User, settings room.RoomSet newRoom := room.NewRoom(uuid.NewString(), playlistData, user.Profile, settings, r.createHub()) r.rooms[newRoom.Id()] = newRoom - fmt.Println(r.rooms) newRoom.Start() + fblog.Info(component.RoomRepository, "room created and started", "id", newRoom.Id(), "room count", len(r.rooms)) + return newRoom.Id(), nil } diff --git a/internal/lib/roomrepository/roomrepository.go b/internal/lib/roomrepository/roomrepository.go index 490edce..f656e0c 100644 --- a/internal/lib/roomrepository/roomrepository.go +++ b/internal/lib/roomrepository/roomrepository.go @@ -2,11 +2,12 @@ package roomrepository import ( "github.com/feelbeatapp/feelbeatserver/internal/infra/auth" + "github.com/feelbeatapp/feelbeatserver/internal/lib" "github.com/feelbeatapp/feelbeatserver/internal/lib/room" ) type RoomRepository interface { - CreateRoom(user auth.User, settings room.RoomSettings) (string, error) + CreateRoom(user auth.User, settings lib.RoomSettings) (string, error) GetAllRooms() []*room.Room GetRoom(id string) *room.Room } diff --git a/internal/lib/types.go b/internal/lib/types.go index 48f5220..212b76e 100644 --- a/internal/lib/types.go +++ b/internal/lib/types.go @@ -3,24 +3,34 @@ package lib import "time" type SongDetails struct { - Title string - Artist string - Duration time.Duration + Title string `json:"title"` + Artist string `json:"artist"` + ImageUrl string `json:"imageUrl"` + Duration time.Duration `json:"duration"` } type Song struct { - Id string - Details SongDetails + Id string `json:"id"` + Details SongDetails `json:"details"` } type PlaylistData struct { - Name string - ImageUrl string - Songs []Song + Name string `json:"name"` + ImageUrl string `json:"imageUrl"` + Songs []Song `json:"songs"` } type UserProfile struct { - Id string - Name string - ImageUrl string + Id string `json:"id"` + Name string `json:"name"` + ImageUrl string `json:"imageUrl"` +} + +type RoomSettings struct { + MaxPlayers int `json:"maxPlayers"` + TurnCount int `json:"turnCount"` + TimePenaltyPerSecond int `json:"timePenaltyPerSecond"` + BasePoints int `json:"basePoints"` + IncorrectGuessPenalty int `json:"incorrectGuessPenalty"` + PlaylistId string `json:"playlistId"` } diff --git a/internal/thirdparty/spotify/playlistdataresponse.go b/internal/thirdparty/spotify/playlistdataresponse.go index 5ee84d7..87360dc 100644 --- a/internal/thirdparty/spotify/playlistdataresponse.go +++ b/internal/thirdparty/spotify/playlistdataresponse.go @@ -14,6 +14,11 @@ type playlistDataResponse struct { Name string `json:"name"` ID string `json:"id"` DurationMs int `json:"duration_ms"` + Album struct { + Images []struct { + Url string `json:"url"` + } `json:"images"` + } `json:"album"` } `json:"track"` } `json:"items"` } `json:"tracks"` diff --git a/internal/thirdparty/spotify/spotifyapi.go b/internal/thirdparty/spotify/spotifyapi.go index e5a2e9d..7cc9f5e 100644 --- a/internal/thirdparty/spotify/spotifyapi.go +++ b/internal/thirdparty/spotify/spotifyapi.go @@ -16,7 +16,7 @@ type SpotifyApi struct { } func (s SpotifyApi) FetchPlaylistData(plalistId string, token string) (lib.PlaylistData, error) { - url := fmt.Sprintf("/playlists/%s?additional_types=track&fields=name,images(url),tracks(items(track(id,images,name,artists(name),duration_ms)))", plalistId) + url := fmt.Sprintf("/playlists/%s?additional_types=track&fields=name,images(url),tracks(items(track(id,name,artists(name),duration_ms,album(images(url)))))", plalistId) req, err := newGetApiCall(url, token) if err != nil { return lib.PlaylistData{}, &feelbeaterror.FeelBeatError{ @@ -63,12 +63,18 @@ func (s SpotifyApi) FetchPlaylistData(plalistId string, token string) (lib.Playl artistNames = append(artistNames, a.Name) } + var imageUrl string + if imageUrl = ""; len(item.Track.Album.Images) > 0 { + imageUrl = item.Track.Album.Images[0].Url + } + songs = append(songs, lib.Song{ Id: item.Track.ID, Details: lib.SongDetails{ Title: item.Track.Name, - Artist: strings.Join(artistNames, " "), + Artist: strings.Join(artistNames, ", "), Duration: time.Duration(item.Track.DurationMs) * time.Millisecond, + ImageUrl: imageUrl, }, }) }