diff --git a/README.md b/README.md index cfa7119f..ab7776e5 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,13 @@ - Guided walkthrough - Audio annotations and TTS (Text-to-Speech) +## External Links +This project is hosted on a VPS, deployed and running via Docker containers. Source code is hosted on GitHub, and static code analysis is run by SonarCloud to ensure code maintainability. +- [Fableous](https://fableous.daystram.com) +- [GitHub](https://github.com/deco-finter/fableous) +- [DockerHub](https://hub.docker.com/r/daystram/fableous) +- [SonarCloud](https://sonarcloud.io/project/overview?id=deco-finter_fableous) + ## Service The application is divided into two parts: diff --git a/fableous-be/config/config.go b/fableous-be/config/config.go index 5c481d08..c9c977f0 100644 --- a/fableous-be/config/config.go +++ b/fableous-be/config/config.go @@ -6,16 +6,20 @@ import ( "github.com/spf13/viper" ) +// AppConfig is a global reference to the application config. var AppConfig Config +// Config is the base application configuration struct. type Config struct { Version string Port int Environment string Debug bool + // Directory to store static gallery files StaticDir string + // PostgreSQL database configuration DBHost string DBPort int DBDatabase string @@ -25,6 +29,7 @@ type Config struct { JWTSecret string } +// InitializeAppConfig loads AppConfig from environment variables. func InitializeAppConfig() { viper.SetConfigName(".env") // allow directly reading from .env file viper.SetConfigType("env") diff --git a/fableous-be/constants/auth.go b/fableous-be/constants/auth.go index 0863f2e3..44a8bcd4 100644 --- a/fableous-be/constants/auth.go +++ b/fableous-be/constants/auth.go @@ -5,5 +5,6 @@ import ( ) const ( + // AuthenticationTimeout is the expiry duration for issued JWT tokens. AuthenticationTimeout = time.Hour * 24 * 2 ) diff --git a/fableous-be/constants/classroom.go b/fableous-be/constants/classroom.go index 64dc9b1b..8e2b4599 100644 --- a/fableous-be/constants/classroom.go +++ b/fableous-be/constants/classroom.go @@ -1,5 +1,6 @@ package constants const ( + // ClassroomTokenLength is the length of the session token. ClassroomTokenLength = 4 ) diff --git a/fableous-be/constants/router.go b/fableous-be/constants/router.go index 03170a89..aad2bb6c 100644 --- a/fableous-be/constants/router.go +++ b/fableous-be/constants/router.go @@ -1,6 +1,10 @@ package constants const ( + // RouterKeyIsAuthenticated is the key for the request authentication status injected by the auth middleware. RouterKeyIsAuthenticated = "is_authenticated" - RouterKeyUserID = "user_id" + + // RouterKeyUserID is the key for the requesting userID injected by the auth middleware. + // Empty if the request is not authenticated. + RouterKeyUserID = "user_id" ) diff --git a/fableous-be/controllers/auth.go b/fableous-be/controllers/auth.go index a0d1b8ca..d6a0dd2f 100644 --- a/fableous-be/controllers/auth.go +++ b/fableous-be/controllers/auth.go @@ -10,6 +10,7 @@ import ( "github.com/deco-finter/fableous/fableous-be/handlers" ) +// POSTLogin handles login request. func POSTLogin(c *gin.Context) { var err error var user datatransfers.UserLogin @@ -26,6 +27,7 @@ func POSTLogin(c *gin.Context) { c.JSON(http.StatusOK, datatransfers.Response{}) } +// POSTRegister handles register request. func POSTRegister(c *gin.Context) { var err error var user datatransfers.UserSignup diff --git a/fableous-be/controllers/classroom.go b/fableous-be/controllers/classroom.go index 83f180c6..25104762 100644 --- a/fableous-be/controllers/classroom.go +++ b/fableous-be/controllers/classroom.go @@ -10,28 +10,31 @@ import ( "github.com/deco-finter/fableous/fableous-be/handlers" ) -func GETClassroom(c *gin.Context) { +// GETClassroomList handles classroom list request. +func GETClassroomList(c *gin.Context) { var err error - var classroomInfo datatransfers.ClassroomInfo - classroomInfo.ID = c.Param("classroom_id") - if classroomInfo, err = handlers.Handler.ClassroomGetOneByID(classroomInfo.ID); err != nil { + var classroomInfos []datatransfers.ClassroomInfo + userID := c.GetString(constants.RouterKeyUserID) + if classroomInfos, err = handlers.Handler.ClassroomGetAllByUserID(userID); err != nil { c.JSON(http.StatusNotFound, datatransfers.Response{Error: "cannot find classroom"}) return } - c.JSON(http.StatusOK, datatransfers.Response{Data: classroomInfo}) + c.JSON(http.StatusOK, datatransfers.Response{Data: classroomInfos}) } -func GETClassroomList(c *gin.Context) { +// GETClassroom handles classroom detail request. +func GETClassroom(c *gin.Context) { var err error - var classroomInfos []datatransfers.ClassroomInfo - userID := c.GetString(constants.RouterKeyUserID) - if classroomInfos, err = handlers.Handler.ClassroomGetAllByUserID(userID); err != nil { + var classroomInfo datatransfers.ClassroomInfo + classroomInfo.ID = c.Param("classroom_id") + if classroomInfo, err = handlers.Handler.ClassroomGetOneByID(classroomInfo.ID); err != nil { c.JSON(http.StatusNotFound, datatransfers.Response{Error: "cannot find classroom"}) return } - c.JSON(http.StatusOK, datatransfers.Response{Data: classroomInfos}) + c.JSON(http.StatusOK, datatransfers.Response{Data: classroomInfo}) } +// POSTClassroom handles classroom creation request. func POSTClassroom(c *gin.Context) { var err error var classroomInfo datatransfers.ClassroomInfo @@ -47,6 +50,7 @@ func POSTClassroom(c *gin.Context) { c.JSON(http.StatusOK, datatransfers.Response{Data: classroomInfo.ID}) } +// PUTClassroom handles classroom update request. func PUTClassroom(c *gin.Context) { var err error var classroomInfo datatransfers.ClassroomInfo @@ -69,6 +73,7 @@ func PUTClassroom(c *gin.Context) { c.JSON(http.StatusOK, datatransfers.Response{}) } +// DELETEClassroom handles classroom deletion request. func DELETEClassroom(c *gin.Context) { var err error var classroomInfo datatransfers.ClassroomInfo diff --git a/fableous-be/controllers/init.go b/fableous-be/controllers/init.go index 235ee3f8..28ecc256 100644 --- a/fableous-be/controllers/init.go +++ b/fableous-be/controllers/init.go @@ -10,6 +10,7 @@ import ( "github.com/deco-finter/fableous/fableous-be/utils" ) +// InitializeRouter initialize the gin router. func InitializeRouter() (router *gin.Engine) { router = gin.Default() router.Use( diff --git a/fableous-be/controllers/middleware/auth.go b/fableous-be/controllers/middleware/auth.go index d48fefe3..c81d64a6 100644 --- a/fableous-be/controllers/middleware/auth.go +++ b/fableous-be/controllers/middleware/auth.go @@ -14,6 +14,7 @@ import ( "github.com/deco-finter/fableous/fableous-be/handlers" ) +// AuthMiddleware is a middleware that checks for a valid JWT token. func AuthMiddleware(c *gin.Context) { var token string if token = strings.TrimPrefix(c.GetHeader("Authorization"), "Bearer "); token == "" { @@ -38,6 +39,7 @@ func AuthMiddleware(c *gin.Context) { c.Next() } +// parseToken parses a JWT token and returns the claims. func parseToken(tokenString, secret string) (claims datatransfers.JWTClaims, err error) { if token, err := jwt.ParseWithClaims(tokenString, &claims, func(token *jwt.Token) (interface{}, error) { return []byte(secret), nil diff --git a/fableous-be/controllers/middleware/cors.go b/fableous-be/controllers/middleware/cors.go index ca047221..b9c15663 100644 --- a/fableous-be/controllers/middleware/cors.go +++ b/fableous-be/controllers/middleware/cors.go @@ -7,6 +7,7 @@ import ( "github.com/gin-gonic/gin" ) +// CORSMiddleware allows cross-origin resource sharing. func CORSMiddleware(c *gin.Context) { cors.New(cors.Config{ AllowOrigins: []string{"*"}, diff --git a/fableous-be/controllers/ping.go b/fableous-be/controllers/ping.go index 93240389..16fa50ea 100644 --- a/fableous-be/controllers/ping.go +++ b/fableous-be/controllers/ping.go @@ -8,6 +8,8 @@ import ( "github.com/deco-finter/fableous/fableous-be/config" ) +// GETPing is a healthcheck endpoint. +// It reponds with the application version. func GETPing(c *gin.Context) { c.JSON(http.StatusOK, map[string]string{"version": config.AppConfig.Version}) } diff --git a/fableous-be/controllers/session.go b/fableous-be/controllers/session.go index d926ed9d..74ce694c 100644 --- a/fableous-be/controllers/session.go +++ b/fableous-be/controllers/session.go @@ -11,6 +11,7 @@ import ( "github.com/deco-finter/fableous/fableous-be/handlers" ) +// GETSessionList handles session list for a classroom request. func GETSessionList(c *gin.Context) { var err error var sessionInfos []datatransfers.SessionInfo @@ -24,6 +25,7 @@ func GETSessionList(c *gin.Context) { c.JSON(http.StatusOK, datatransfers.Response{Data: sessionInfos}) } +// GETSession handles session detail request. func GETSession(c *gin.Context) { var err error var sessionInfo datatransfers.SessionInfo @@ -37,6 +39,8 @@ func GETSession(c *gin.Context) { c.JSON(http.StatusOK, datatransfers.Response{Data: sessionInfo}) } +// GETOngoinSession handles ongoing session request. +// 404 if no ongoing session is found. func GETOngoingSession(c *gin.Context) { var err error var sessionInfo datatransfers.SessionInfo @@ -50,6 +54,7 @@ func GETOngoingSession(c *gin.Context) { c.JSON(http.StatusOK, datatransfers.Response{Data: sessionInfo}) } +// POSTSession handles session creation request. func POSTSession(c *gin.Context) { var err error var classroomInfo datatransfers.ClassroomInfo @@ -84,6 +89,7 @@ func POSTSession(c *gin.Context) { c.JSON(http.StatusOK, datatransfers.Response{Data: sessionInfo.ID}) } +// PUTSession handles session update request. func PUTSession(c *gin.Context) { var err error var classroomInfo datatransfers.ClassroomInfo @@ -112,6 +118,7 @@ func PUTSession(c *gin.Context) { c.JSON(http.StatusOK, datatransfers.Response{}) } +// DELETESession handles session deletion request. func DELETESession(c *gin.Context) { var err error var classroomInfo datatransfers.ClassroomInfo diff --git a/fableous-be/controllers/user.go b/fableous-be/controllers/user.go index 28316fbe..8f6043a1 100644 --- a/fableous-be/controllers/user.go +++ b/fableous-be/controllers/user.go @@ -10,6 +10,7 @@ import ( "github.com/deco-finter/fableous/fableous-be/handlers" ) +// GETUser handles user detail request. func GETUser(c *gin.Context) { var err error var userInfo datatransfers.UserInfo @@ -20,6 +21,7 @@ func GETUser(c *gin.Context) { c.JSON(http.StatusOK, datatransfers.Response{Data: userInfo}) } +// PUTUser handles user update request. func PUTUser(c *gin.Context) { var err error var user datatransfers.UserUpdate diff --git a/fableous-be/controllers/websocket.go b/fableous-be/controllers/websocket.go index 616ca277..180f6866 100644 --- a/fableous-be/controllers/websocket.go +++ b/fableous-be/controllers/websocket.go @@ -12,6 +12,7 @@ import ( pb "github.com/deco-finter/fableous/fableous-be/protos" ) +// GETConnectHubWS handles WebSocket connection from the hub. func GETConnectHubWS(c *gin.Context) { var err error var classroomInfo datatransfers.ClassroomInfo @@ -30,6 +31,7 @@ func GETConnectHubWS(c *gin.Context) { _ = handlers.Handler.ConnectHubWS(c, classroomInfo.ID) } +// GETConnectControllerWS handles WebSocket connection from the controller. func GETConnectControllerWS(c *gin.Context) { var err error var classroomToken string diff --git a/fableous-be/datatransfers/auth.go b/fableous-be/datatransfers/auth.go index 2ce10fc3..58598185 100644 --- a/fableous-be/datatransfers/auth.go +++ b/fableous-be/datatransfers/auth.go @@ -5,6 +5,7 @@ import ( "time" ) +// JWTClaims is the JWT claims struct. type JWTClaims struct { ID string `json:"sub,omitempty"` ExpiresAt int64 `json:"exp,omitempty"` diff --git a/fableous-be/datatransfers/classroom.go b/fableous-be/datatransfers/classroom.go index fc7a7ea2..e6edca0c 100644 --- a/fableous-be/datatransfers/classroom.go +++ b/fableous-be/datatransfers/classroom.go @@ -4,6 +4,7 @@ import ( "time" ) +// ClassroomInfo is the data transfer object for the Classroom entity. type ClassroomInfo struct { ID string `json:"id" binding:"-"` UserID string `json:"-" binding:"-"` diff --git a/fableous-be/datatransfers/response.go b/fableous-be/datatransfers/response.go index 71f6275e..9e252c10 100644 --- a/fableous-be/datatransfers/response.go +++ b/fableous-be/datatransfers/response.go @@ -1,5 +1,6 @@ package datatransfers +// Response is the API response wrapper. type Response struct { Code int `json:"code,omitempty"` Data interface{} `json:"data,omitempty"` diff --git a/fableous-be/datatransfers/session.go b/fableous-be/datatransfers/session.go index 6ec66188..423c4027 100644 --- a/fableous-be/datatransfers/session.go +++ b/fableous-be/datatransfers/session.go @@ -2,6 +2,7 @@ package datatransfers import "time" +// SessionUpdate is the data transfer object used for updating the Session entity. type SessionUpdate struct { ID string `json:"id" binding:"-"` ClassroomID string `json:"-" binding:"-"` @@ -9,6 +10,7 @@ type SessionUpdate struct { Description string `json:"description" binding:"required"` } +// SessionInfo is the data transfer object for the Session entity. type SessionInfo struct { ID string `json:"id" binding:"-"` ClassroomID string `json:"-" binding:"-"` diff --git a/fableous-be/datatransfers/user.go b/fableous-be/datatransfers/user.go index 91bc0e75..3e25ee4c 100644 --- a/fableous-be/datatransfers/user.go +++ b/fableous-be/datatransfers/user.go @@ -4,22 +4,26 @@ import ( "time" ) +// UserLogin is the data transfer object used for logging in a user. type UserLogin struct { Email string `json:"email" binding:"required"` Password string `json:"password" binding:"required"` } +// UserSignup is the data transfer object used for signing up a new user. type UserSignup struct { Name string `json:"name" binding:"required"` Email string `json:"email" binding:"required"` Password string `json:"password" binding:"required"` } +// UserUpdate is the data transfer object used for updating the User entity. type UserUpdate struct { Name string `json:"name" binding:"-"` Email string `json:"email" binding:"-"` } +// UserInfo is the data transfer object for the User entity. type UserInfo struct { ID string `json:"id" uri:"id"` Name string `json:"name"` diff --git a/fableous-be/handlers/auth.go b/fableous-be/handlers/auth.go index 014a32b3..24a06d3a 100644 --- a/fableous-be/handlers/auth.go +++ b/fableous-be/handlers/auth.go @@ -13,6 +13,7 @@ import ( "github.com/deco-finter/fableous/fableous-be/models" ) +// Authenticate issues a token for the given user credentials func (m *module) Authenticate(credentials datatransfers.UserLogin) (token string, err error) { var user models.User if user, err = m.db.userOrmer.GetOneByEmail(credentials.Email); err != nil { @@ -24,6 +25,7 @@ func (m *module) Authenticate(credentials datatransfers.UserLogin) (token string return generateToken(user) } +// generateToken generates a JWT token for the given user func generateToken(user models.User) (string, error) { now := time.Now() expiry := time.Now().Add(constants.AuthenticationTimeout) diff --git a/fableous-be/handlers/classroom.go b/fableous-be/handlers/classroom.go index 047e8ad5..8a2357e9 100644 --- a/fableous-be/handlers/classroom.go +++ b/fableous-be/handlers/classroom.go @@ -11,6 +11,7 @@ import ( "github.com/deco-finter/fableous/fableous-be/models" ) +// ClassroomGetByID returns a classroom by its ID. func (m *module) ClassroomGetOneByID(id string) (classroomInfo datatransfers.ClassroomInfo, err error) { var classroom models.Classroom if classroom, err = m.db.classroomOrmer.GetOneByID(id); err != nil { @@ -25,6 +26,7 @@ func (m *module) ClassroomGetOneByID(id string) (classroomInfo datatransfers.Cla return } +// ClassroomGetAllByUserID returns all classrooms owned by a user. func (m *module) ClassroomGetAllByUserID(userID string) (classroomInfos []datatransfers.ClassroomInfo, err error) { var classrooms []models.Classroom classroomInfos = make([]datatransfers.ClassroomInfo, 0) @@ -44,6 +46,7 @@ func (m *module) ClassroomGetAllByUserID(userID string) (classroomInfos []datatr return } +// ClassroomInsert inserts a new classroom. func (m *module) ClassroomInsert(classroomInfo datatransfers.ClassroomInfo) (classroomID string, err error) { if classroomID, err = m.db.classroomOrmer.Insert(models.Classroom{ UserID: classroomInfo.UserID, @@ -54,6 +57,7 @@ func (m *module) ClassroomInsert(classroomInfo datatransfers.ClassroomInfo) (cla return } +// ClassroomUpdate updates a classroom. func (m *module) ClassroomUpdate(classroomInfo datatransfers.ClassroomInfo) (err error) { if err = m.db.classroomOrmer.Update(models.Classroom{ ID: classroomInfo.ID, @@ -64,6 +68,8 @@ func (m *module) ClassroomUpdate(classroomInfo datatransfers.ClassroomInfo) (err return } +// ClassroomDeleteByID deletes a classroom by its ID. +// It also stops any ongoing session and removes all its sessions' static files. func (m *module) ClassroomDeleteByID(classroomID string) (err error) { m.sessions.mutex.Lock() for classroomToken, sess := range m.sessions.keys { diff --git a/fableous-be/handlers/init.go b/fableous-be/handlers/init.go index 420c71b2..156616c9 100644 --- a/fableous-be/handlers/init.go +++ b/fableous-be/handlers/init.go @@ -17,25 +17,27 @@ import ( pb "github.com/deco-finter/fableous/fableous-be/protos" ) +// Handler is a global reference to the application handlers. var Handler HandlerFunc +// HandlerFunc is the handler interface. type HandlerFunc interface { - // Auth + // Auth handlers Authenticate(userInfo datatransfers.UserLogin) (token string, err error) - // User + // User handlers UserRegister(userInfo datatransfers.UserSignup) (err error) UserGetOneByID(id string) (userInfo datatransfers.UserInfo, err error) UserUpdate(userInfo datatransfers.UserInfo) (err error) - // Classroom + // Classroom handlers ClassroomGetOneByID(id string) (classroomInfo datatransfers.ClassroomInfo, err error) ClassroomGetAllByUserID(userID string) (classroomInfos []datatransfers.ClassroomInfo, err error) ClassroomInsert(classroomInfo datatransfers.ClassroomInfo) (classroomID string, err error) ClassroomUpdate(classroomInfo datatransfers.ClassroomInfo) (err error) ClassroomDeleteByID(classroomID string) (err error) - // Session + // Session handlers SessionGetAllByClassroomID(classroomID string) (sessionInfos []datatransfers.SessionInfo, err error) SessionGetOneByIDByClassroomID(id, classroomID string) (sessionInfo datatransfers.SessionInfo, err error) SessionGetOneOngoingByClassroomID(classroomID string) (sessionInfo datatransfers.SessionInfo, err error) @@ -44,19 +46,21 @@ type HandlerFunc interface { SessionDeleteByIDByClassroomID(id, classroomID string) (err error) SessionCleanUp(sess *activeSession) - // WebSocket + // WebSocket handlers ConnectHubWS(ctx *gin.Context, classroomID string) (err error) ConnectControllerWS(ctx *gin.Context, classroomToken string, role pb.ControllerRole, name string) (err error) HubCommandWorker(conn *websocket.Conn, sess *activeSession) (err error) ControllerCommandWorker(conn *websocket.Conn, sess *activeSession, role pb.ControllerRole, name string) (err error) } +// module holds all shared references for the handlers. type module struct { db *dbEntity upgrader websocket.Upgrader sessions activeSessionsEntity } +// dbEntity contains all databse ormers. type dbEntity struct { conn *gorm.DB userOrmer models.UserOrmer @@ -64,13 +68,17 @@ type dbEntity struct { sessionOrmer models.SessionOrmer } +// activeSessionsEntity is a collection of all active sessions. type activeSessionsEntity struct { - keys map[string]*activeSession // key: classroomToken, value: activeSession + keys map[string]*activeSession // key: classroomToken, value: activeSession + + // Mutex for keys mutex sync.RWMutex } +// InitalizeHandler initializes the handler module. func InitializeHandler() (err error) { - // Initialize DB + // initialize database var db *gorm.DB db, err = gorm.Open(postgres.Open( fmt.Sprintf("host=%s port=%d dbname=%s user=%s password=%s sslmode=disable TimeZone=Etc/UTC", @@ -82,7 +90,7 @@ func InitializeHandler() (err error) { } log.Println("[INIT] successfully connected to PostgreSQL") - // Compose handler modules + // compose handler modules Handler = &module{ db: &dbEntity{ conn: db, diff --git a/fableous-be/handlers/session.go b/fableous-be/handlers/session.go index 35c40f33..39ba4feb 100644 --- a/fableous-be/handlers/session.go +++ b/fableous-be/handlers/session.go @@ -12,6 +12,7 @@ import ( "github.com/deco-finter/fableous/fableous-be/utils" ) +// SessionGetAllByClassroomID returns all sessions by its classroomID. func (m *module) SessionGetAllByClassroomID(classroomID string) (sessionInfos []datatransfers.SessionInfo, err error) { var sessions []models.Session sessionInfos = make([]datatransfers.SessionInfo, 0) @@ -36,6 +37,7 @@ func (m *module) SessionGetAllByClassroomID(classroomID string) (sessionInfos [] return } +// SessionGetOneByIDByClassroomID returns one session by its ID and ClassroomID. func (m *module) SessionGetOneByIDByClassroomID(id, classroomID string) (sessionInfo datatransfers.SessionInfo, err error) { var session models.Session if session, err = m.db.sessionOrmer.GetOneByIDByClassroomID(id, classroomID); err != nil { @@ -55,6 +57,7 @@ func (m *module) SessionGetOneByIDByClassroomID(id, classroomID string) (session return } +// SessionGetOneOngoingByClassroomID returns a classroom's ongoing session by its classroomID. func (m *module) SessionGetOneOngoingByClassroomID(classroomID string) (sessionInfo datatransfers.SessionInfo, err error) { var session models.Session if session, err = m.db.sessionOrmer.GetOneOngoingByClassroomID(classroomID); err != nil { @@ -74,6 +77,7 @@ func (m *module) SessionGetOneOngoingByClassroomID(classroomID string) (sessionI return } +// SessionInsert inserts a new session. func (m *module) SessionInsert(sessionInfo datatransfers.SessionInfo) (id string, err error) { if id, err = m.db.sessionOrmer.Insert(models.Session{ ClassroomID: sessionInfo.ClassroomID, @@ -90,6 +94,7 @@ func (m *module) SessionInsert(sessionInfo datatransfers.SessionInfo) (id string return } +// SessionUpdate updates a session. func (m *module) SessionUpdate(sessionUpdate datatransfers.SessionUpdate) (err error) { if err = m.db.sessionOrmer.Update(models.Session{ ID: sessionUpdate.ID, @@ -102,6 +107,8 @@ func (m *module) SessionUpdate(sessionUpdate datatransfers.SessionUpdate) (err e return } +// SessionDeleteByIDByClassroomID deletes a session by its ID and ClassroomID. +// It stops the session if it is still ongoing and deletes its static files. func (m *module) SessionDeleteByIDByClassroomID(id, classroomID string) (err error) { if err = m.db.sessionOrmer.DeleteByIDByClassroomID(id, classroomID); err != nil { return err @@ -124,6 +131,7 @@ func (m *module) SessionDeleteByIDByClassroomID(id, classroomID string) (err err return } +// SessionCleanUp kicks all controllers and hub from the session and deletes its static files. func (m *module) SessionCleanUp(sess *activeSession) { _ = sess.BroadcastMessage(&pb.WSMessage{ Type: pb.WSMessageType_JOIN, @@ -149,6 +157,7 @@ func (m *module) SessionCleanUp(sess *activeSession) { go m.sessionClearStaticByIDByClassroomID(sess.sessionID, sess.classroomID) } +// sessionClearStaticByIDByClassroomID deletes a session's static files. func (m *module) sessionClearStaticByIDByClassroomID(id, classroomID string) { _ = os.RemoveAll(utils.GetSessionStaticDir(id, classroomID)) } diff --git a/fableous-be/handlers/user.go b/fableous-be/handlers/user.go index 270be84a..88e75e77 100644 --- a/fableous-be/handlers/user.go +++ b/fableous-be/handlers/user.go @@ -10,6 +10,7 @@ import ( "github.com/deco-finter/fableous/fableous-be/models" ) +// UserRegister creates a new user. func (m *module) UserRegister(credentials datatransfers.UserSignup) (err error) { var hashedPassword []byte if hashedPassword, err = bcrypt.GenerateFromPassword([]byte(credentials.Password), bcrypt.DefaultCost); err != nil { @@ -25,6 +26,7 @@ func (m *module) UserRegister(credentials datatransfers.UserSignup) (err error) return } +// UserGetOneByID returns a user by its ID. func (m *module) UserGetOneByID(id string) (userInfo datatransfers.UserInfo, err error) { var user models.User if user, err = m.db.userOrmer.GetOneByID(id); err != nil { @@ -39,6 +41,7 @@ func (m *module) UserGetOneByID(id string) (userInfo datatransfers.UserInfo, err return } +// UserUpdate updates a user. func (m *module) UserUpdate(userInfo datatransfers.UserInfo) (err error) { if err = m.db.userOrmer.Update(models.User{ ID: userInfo.ID, diff --git a/fableous-be/handlers/websocket.go b/fableous-be/handlers/websocket.go index 2f971224..f5873c0c 100644 --- a/fableous-be/handlers/websocket.go +++ b/fableous-be/handlers/websocket.go @@ -17,17 +17,24 @@ import ( "github.com/deco-finter/fableous/fableous-be/utils" ) +// activeSession contains all the active session data and reference to all the websocket connections. type activeSession struct { + // Session metadata classroomToken string classroomID string sessionID string currentPage int + + // WebSocket connections hubConn *websocket.Conn controllerConn map[pb.ControllerRole]*websocket.Conn // key: role, value: ws.Conn controllerName map[pb.ControllerRole]string // key: role, value: user name - mutex sync.RWMutex + + // Mutex for controllerConn and controllerName + mutex sync.RWMutex } +// BroadcastMessage sends a message to all the connected controllers. func (sess *activeSession) BroadcastMessage(message *pb.WSMessage) (err error) { sess.mutex.RLock() defer sess.mutex.RUnlock() @@ -39,6 +46,7 @@ func (sess *activeSession) BroadcastMessage(message *pb.WSMessage) (err error) { return } +// KickController disconnects a controller from the session. func (sess *activeSession) KickController(role pb.ControllerRole, announceHub bool) (err error) { sess.mutex.Lock() if targetConn, ok := sess.controllerConn[role]; ok { @@ -70,6 +78,7 @@ func (sess *activeSession) KickController(role pb.ControllerRole, announceHub bo return } +// ConnectHubWS handles the hub's initial WebSocket connnection. func (m *module) ConnectHubWS(ctx *gin.Context, classroomID string) (err error) { ctx.Request.Header.Del("Sec-Websocket-Extensions") var conn *websocket.Conn @@ -107,6 +116,7 @@ func (m *module) ConnectHubWS(ctx *gin.Context, classroomID string) (err error) return m.HubCommandWorker(conn, sess) } +// ConnectHubWS handles the controllers's initial WebSocket connnection. func (m *module) ConnectControllerWS(ctx *gin.Context, classroomToken string, role pb.ControllerRole, name string) (err error) { ctx.Request.Header.Del("Sec-Websocket-Extensions") var conn *websocket.Conn @@ -147,12 +157,16 @@ func (m *module) ConnectControllerWS(ctx *gin.Context, classroomToken string, ro return m.ControllerCommandWorker(conn, sess, role, name) } +// HubCommandWorker handles all messages from the hub. +// It keeps the WebSocket connection alive by blocking the goroutine. func (m *module) HubCommandWorker(conn *websocket.Conn, sess *activeSession) (err error) { + // cleanup session when hub disconnects defer func() { m.sessions.mutex.Lock() delete(m.sessions.keys, sess.classroomToken) m.sessions.mutex.Unlock() }() + // send session metadata _ = utils.SendMessage(conn, &pb.WSMessage{ Type: pb.WSMessageType_CONTROL, Data: &pb.WSMessage_Control{ @@ -165,6 +179,7 @@ func (m *module) HubCommandWorker(conn *websocket.Conn, sess *activeSession) (er }, }, }) + // listen for messages for { var message *pb.WSMessage if message, err = utils.RecieveMessage(conn); err != nil { @@ -180,8 +195,10 @@ func (m *module) HubCommandWorker(conn *websocket.Conn, sess *activeSession) (er } switch message.Type { case pb.WSMessageType_ACHIEVEMENT: + // relay ACHIEVEMENT message to all controllers _ = sess.BroadcastMessage(message) case pb.WSMessageType_CONTROL: + // check if CONTROL message pushes next page if message.Data.(*pb.WSMessage_Control).Control.NextPage { sess.currentPage++ _ = sess.BroadcastMessage(&pb.WSMessage{ @@ -192,6 +209,7 @@ func (m *module) HubCommandWorker(conn *websocket.Conn, sess *activeSession) (er }, }, }) + // check if story has finished var session models.Session if session, err = m.db.sessionOrmer.GetOneByIDByClassroomID(sess.sessionID, sess.classroomID); err == nil && sess.currentPage > session.Pages { session.Completed = true @@ -209,10 +227,12 @@ func (m *module) HubCommandWorker(conn *websocket.Conn, sess *activeSession) (er } } } + // check if CONTROL message clears a controller's canvas if clearedController := message.Data.(*pb.WSMessage_Control).Control.Clear; clearedController != pb.ControllerRole_NONE { _ = utils.SendMessage(sess.hubConn, message) _ = utils.SendMessage(sess.controllerConn[clearedController], message) } + // check if CONTROL message kicks a controller if kickedController := message.Data.(*pb.WSMessage_Control).Control.Kick; kickedController != pb.ControllerRole_NONE { _ = sess.KickController(kickedController, false) } @@ -238,7 +258,10 @@ func (m *module) HubCommandWorker(conn *websocket.Conn, sess *activeSession) (er return } +// ControllerCommandWorker handles all messages from the controller. +// It keeps the WebSocket connection alive by blocking the goroutine. func (m *module) ControllerCommandWorker(conn *websocket.Conn, sess *activeSession, role pb.ControllerRole, name string) (err error) { + // send session metadata _ = utils.SendMessage(conn, &pb.WSMessage{ Type: pb.WSMessageType_CONTROL, Data: &pb.WSMessage_Control{ @@ -251,6 +274,7 @@ func (m *module) ControllerCommandWorker(conn *websocket.Conn, sess *activeSessi }, }, }) + // notify hub of controller connection _ = utils.SendMessage(sess.hubConn, &pb.WSMessage{ Type: pb.WSMessageType_JOIN, Data: &pb.WSMessage_Join{ @@ -261,6 +285,7 @@ func (m *module) ControllerCommandWorker(conn *websocket.Conn, sess *activeSessi }, }, }) + // listen for messages for { var message *pb.WSMessage if message, err = utils.RecieveMessage(conn); err != nil { @@ -273,8 +298,10 @@ func (m *module) ControllerCommandWorker(conn *websocket.Conn, sess *activeSessi switch message.Type { case pb.WSMessageType_PAINT, pb.WSMessageType_FILL, pb.WSMessageType_TEXT, pb.WSMessageType_CHECKPOINT, pb.WSMessageType_UNDO, pb.WSMessageType_CURSOR, pb.WSMessageType_CONTROL: + // relay message to hub _ = utils.SendMessage(sess.hubConn, message) case pb.WSMessageType_AUDIO: + // concurrently save payload then send filename to hub go func() { message.Data.(*pb.WSMessage_Paint).Paint.Id = int32(sess.currentPage) // override page numbber if filename, page := m.SavePayload(sess, message, true); filename != "" { @@ -307,6 +334,7 @@ func (m *module) ControllerCommandWorker(conn *websocket.Conn, sess *activeSessi return } +// SavePayload saves the payload to disk and returns the filename. func (m *module) SavePayload(sess *activeSession, message *pb.WSMessage, isBase64 bool) (filename string, page int) { var err error var data []byte @@ -345,6 +373,7 @@ func (m *module) SavePayload(sess *activeSession, message *pb.WSMessage, isBase6 return } +// GetClassroomActiveSession returns the active session bound to a classroomToken. func (m *module) GetClassroomActiveSession(classroomToken string) (sess *activeSession) { m.sessions.mutex.RLock() defer m.sessions.mutex.RUnlock() @@ -354,6 +383,7 @@ func (m *module) GetClassroomActiveSession(classroomToken string) (sess *activeS return } +// GetActiveSessionController returns the WebSocket connetion of the controller with the given role of a session. func (m *module) GetActiveSessionController(sess *activeSession, role pb.ControllerRole) (conn *websocket.Conn) { sess.mutex.RLock() defer sess.mutex.RUnlock() diff --git a/fableous-be/main.go b/fableous-be/main.go index 82ab5362..19fae3c7 100644 --- a/fableous-be/main.go +++ b/fableous-be/main.go @@ -14,6 +14,7 @@ import ( ) var ( + // Injected at build time version string ) diff --git a/fableous-be/models/classroom.go b/fableous-be/models/classroom.go index 6d8c9ff6..5083a6b2 100644 --- a/fableous-be/models/classroom.go +++ b/fableous-be/models/classroom.go @@ -10,6 +10,7 @@ type classroomOrm struct { db *gorm.DB } +// Classroom represents a classroom entity. type Classroom struct { ID string `gorm:"column:id;primaryKey;type:uuid;default:uuid_generate_v4()" json:"-"` User User `gorm:"foreignKey:UserID;references:id;constraint:OnUpdate:CASCADE,OnDelete:CASCADE" json:"-"` @@ -19,6 +20,7 @@ type Classroom struct { UpdatedAt time.Time `gorm:"autoUpdateTime" json:"-"` } +// ClassroomOrmer is the interface for all database operations for Classroom. type ClassroomOrmer interface { GetOneByID(id string) (classroom Classroom, err error) GetAllByUserID(userID string) (classrooms []Classroom, err error) @@ -27,6 +29,7 @@ type ClassroomOrmer interface { DeleteByID(classroomID string) (err error) } +// NewClassroomOrmer initializes a new ClassroomOrmer. func NewClassroomOrmer(db *gorm.DB) ClassroomOrmer { _ = db.AutoMigrate(&Classroom{}) // builds table when enabled return &classroomOrm{db} @@ -48,7 +51,8 @@ func (o *classroomOrm) Insert(classroom Classroom) (id string, err error) { } func (o *classroomOrm) Update(classroom Classroom) (err error) { - // By default, only non-empty fields are updated. See https://gorm.io/docs/update.html#Updates-multiple-columns + // By default, only non-empty fields are updated + // See https://gorm.io/docs/update.html#Updates-multiple-columns result := o.db.Model(&Classroom{}).Model(&classroom).Updates(&classroom) return result.Error } diff --git a/fableous-be/models/session.go b/fableous-be/models/session.go index 234ac522..44074fca 100644 --- a/fableous-be/models/session.go +++ b/fableous-be/models/session.go @@ -10,6 +10,7 @@ type sessionOrm struct { db *gorm.DB } +// Session represents a drawing session entity. type Session struct { ID string `gorm:"column:id;primaryKey;type:uuid;default:uuid_generate_v4()" json:"-"` Classroom Classroom `gorm:"foreignKey:ClassroomID;references:id;constraint:OnUpdate:CASCADE,OnDelete:CASCADE" json:"-"` @@ -25,6 +26,7 @@ type Session struct { UpdatedAt time.Time `gorm:"autoUpdateTime" json:"-"` } +// SessionOrmer is the interface for all database operations for Session. type SessionOrmer interface { GetOneByIDByClassroomID(id, classroomID string) (session Session, err error) GetOneOngoingByClassroomID(classroomID string) (session Session, err error) @@ -35,6 +37,7 @@ type SessionOrmer interface { DeleteByIDByClassroomID(id, classroomID string) (err error) } +// NewSessionOrmer initializes a new SessionOrmer. func NewSessionOrmer(db *gorm.DB) SessionOrmer { _ = db.AutoMigrate(&Session{}) return &sessionOrm{db} diff --git a/fableous-be/models/user.go b/fableous-be/models/user.go index f28a7cc3..e465110c 100644 --- a/fableous-be/models/user.go +++ b/fableous-be/models/user.go @@ -10,6 +10,7 @@ type userOrm struct { db *gorm.DB } +// User represents a user entity. type User struct { ID string `gorm:"column:id;primaryKey;type:uuid;default:uuid_generate_v4()" json:"-"` Name string `gorm:"column:name;type:varchar(32);not null" json:"-"` @@ -19,6 +20,7 @@ type User struct { UpdatedAt time.Time `gorm:"autoUpdateTime" json:"-"` } +// UserOrmer is the interface for all database operations for User. type UserOrmer interface { GetOneByID(id string) (user User, err error) GetOneByEmail(email string) (user User, err error) @@ -26,6 +28,7 @@ type UserOrmer interface { Update(user User) (err error) } +// NewUserOrmer initializes a new UserOrmer. func NewUserOrmer(db *gorm.DB) UserOrmer { _ = db.AutoMigrate(&User{}) // builds table when enabled return &userOrm{db} @@ -47,7 +50,8 @@ func (o *userOrm) Insert(user User) (id string, err error) { } func (o *userOrm) Update(user User) (err error) { - // By default, only non-empty fields are updated. See https://gorm.io/docs/update.html#Updates-multiple-columns + // By default, only non-empty fields are updated + // See https://gorm.io/docs/update.html#Updates-multiple-columns result := o.db.Model(&User{}).Model(&user).Updates(&user) return result.Error } diff --git a/fableous-be/utils/router.go b/fableous-be/utils/router.go index 3edbf7f6..caf0fd2f 100644 --- a/fableous-be/utils/router.go +++ b/fableous-be/utils/router.go @@ -9,6 +9,7 @@ import ( "github.com/deco-finter/fableous/fableous-be/datatransfers" ) +// AuthOnly is a middleware that checks if the user is authenticated. func AuthOnly(c *gin.Context) { if !c.GetBool(constants.RouterKeyIsAuthenticated) { c.AbortWithStatusJSON(http.StatusUnauthorized, datatransfers.Response{Error: "user not authenticated"}) diff --git a/fableous-be/utils/standard.go b/fableous-be/utils/standard.go deleted file mode 100644 index f26e37b1..00000000 --- a/fableous-be/utils/standard.go +++ /dev/null @@ -1,5 +0,0 @@ -package utils - -func BoolAddr(b bool) *bool { - return &b -} diff --git a/fableous-be/utils/static.go b/fableous-be/utils/static.go index 6f05db03..4f06c419 100644 --- a/fableous-be/utils/static.go +++ b/fableous-be/utils/static.go @@ -9,11 +9,13 @@ import ( "github.com/deco-finter/fableous/fableous-be/config" ) -// FS to disable directory listing +// FilteredFileSystem is a custom filesystem which disables directory listing. type FilteredFileSystem struct { FileSystem http.FileSystem } +// Open overrides the default Open method of http.FileSystem. +// It disables directory listing. func (f FilteredFileSystem) Open(path string) (file http.File, err error) { if file, err = f.FileSystem.Open(path); err != nil { return @@ -28,6 +30,7 @@ func (f FilteredFileSystem) Open(path string) (file http.File, err error) { return file, nil } +// GetSessionStaticDir returns the static directory for the session. func GetSessionStaticDir(sessionID, classroomID string) string { return fmt.Sprintf("%s/%s/%s", config.AppConfig.StaticDir, classroomID, sessionID) } diff --git a/fableous-be/utils/token.go b/fableous-be/utils/token.go index 70b021ec..4c544f5c 100644 --- a/fableous-be/utils/token.go +++ b/fableous-be/utils/token.go @@ -4,8 +4,10 @@ import ( "crypto/rand" ) +// letterBytes is the alphabet used to generate the random strings. const letterBytes = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" +// GenerateRandomString generates a random string of the given length. func GenerateRandomString(length int) string { byteString := make([]byte, length) _, _ = rand.Read(byteString) diff --git a/fableous-be/utils/websocket.go b/fableous-be/utils/websocket.go index 23f62b26..ce7f2481 100644 --- a/fableous-be/utils/websocket.go +++ b/fableous-be/utils/websocket.go @@ -10,6 +10,7 @@ import ( pb "github.com/deco-finter/fableous/fableous-be/protos" ) +// SendMessage sends a message to the WebSocket client. func SendMessage(conn *websocket.Conn, message *pb.WSMessage) (err error) { var bytes []byte if bytes, err = proto.Marshal(message); err != nil { @@ -19,6 +20,8 @@ func SendMessage(conn *websocket.Conn, message *pb.WSMessage) (err error) { return } +// RecieveMessage recieves a message from the WebSocket client. +// This is a blocking call. func RecieveMessage(conn *websocket.Conn) (message *pb.WSMessage, err error) { var bytes []byte if _, bytes, err = conn.ReadMessage(); err != nil { @@ -29,6 +32,8 @@ func RecieveMessage(conn *websocket.Conn) (message *pb.WSMessage, err error) { return } +// ExtractPayload extracts the payload from a WebSocket message. +// The payload could be in plaintext or encoded in base64. func ExtractPayload(message *pb.WSMessage, isBase64 bool) (payload []byte, page int, err error) { data := message.Data.(*pb.WSMessage_Paint).Paint stringPayload := data.Text diff --git a/fableous-fe/src/components/AuthProvider.tsx b/fableous-fe/src/components/AuthProvider.tsx index 73211f8e..d0dbc155 100644 --- a/fableous-fe/src/components/AuthProvider.tsx +++ b/fableous-fe/src/components/AuthProvider.tsx @@ -17,6 +17,10 @@ export const AuthContext = createContext([ () => {}, ]); +/** + * Provides centralised auth info and helper functions to manage it. + * Centralisation allows different components to stay up-to-date with auth status. + */ export default function AuthProvider(props: { children: ReactNode }) { const [token, setToken] = useState(getLocalStorage(TOKEN_KEY) || ""); const { children } = props; diff --git a/fableous-fe/src/components/CustomNavProvider.tsx b/fableous-fe/src/components/CustomNavProvider.tsx index 84881d08..3437bf7a 100644 --- a/fableous-fe/src/components/CustomNavProvider.tsx +++ b/fableous-fe/src/components/CustomNavProvider.tsx @@ -23,6 +23,19 @@ export const CustomNavContext = createContext([ export const useCustomNav = () => useContext(CustomNavContext); +/** + * Allows pages to have customized buttons in navbar. + * + * @example + * + * useEffect(() => { + * setAdditionalNavs(navItemsUniqueToThisPage) + * + * return () => { + * setAdditionalNavs([]) + * } + * }, []) + */ export default function CustomNavProvider(props: { children: ReactNode }) { const [additionalNavs, setAdditionalNavs] = useState([]); const [isLogoClickable, setIsLogoClickable] = useState(true); diff --git a/fableous-fe/src/components/FormikTagField.tsx b/fableous-fe/src/components/FormikTagField.tsx index 3dbf8617..9ba07112 100644 --- a/fableous-fe/src/components/FormikTagField.tsx +++ b/fableous-fe/src/components/FormikTagField.tsx @@ -5,6 +5,9 @@ import { Autocomplete } from "@material-ui/lab"; import { FormikProps } from "formik"; import { useState } from "react"; +/** + * Convenience formik wrapper for custom tag input + */ export default function FormikTagField(props: { formik: FormikProps; name: string; diff --git a/fableous-fe/src/components/FormikTextField.tsx b/fableous-fe/src/components/FormikTextField.tsx index 9347253b..235711bd 100644 --- a/fableous-fe/src/components/FormikTextField.tsx +++ b/fableous-fe/src/components/FormikTextField.tsx @@ -2,6 +2,9 @@ import { TextField, TextFieldProps } from "@material-ui/core"; import { FormikProps } from "formik"; +/** + * Convenience formik wrapper around Mui's TextField + */ export default function FormikTextField(props: { formik: FormikProps; name: string; diff --git a/fableous-fe/src/components/InjectAxiosRespInterceptor.tsx b/fableous-fe/src/components/InjectAxiosRespInterceptor.tsx index ffbe4573..1b55ec9d 100644 --- a/fableous-fe/src/components/InjectAxiosRespInterceptor.tsx +++ b/fableous-fe/src/components/InjectAxiosRespInterceptor.tsx @@ -3,6 +3,12 @@ import { useHistory } from "react-router-dom"; import { setupResponseInterceptor } from "../api"; import { AuthContext } from "./AuthProvider"; +/** + * Component to place in top-level App to clear auth token when it expires. + * + * It is done by attaching a callback within React context to run + * when axios' response interceptor receives 401 Unauthorized with token expiry error message. + */ export default function InjectAxiosRespInterceptor() { const history = useHistory(); const [, , , clearToken] = useContext(AuthContext); diff --git a/fableous-fe/src/components/achievement/AchievementButton.tsx b/fableous-fe/src/components/achievement/AchievementButton.tsx index a190d966..d8aacbed 100644 --- a/fableous-fe/src/components/achievement/AchievementButton.tsx +++ b/fableous-fe/src/components/achievement/AchievementButton.tsx @@ -51,6 +51,10 @@ const useStyles = makeStyles(() => ({ }, })); +/** + * Button that shows a popup modal containing progress of achievements. + * Fires a confetti effect on the button when completing an achievement. + */ export default function AchievementButton(props: { achievements: Achievement; confetti?: boolean; diff --git a/fableous-fe/src/components/canvas/Canvas.tsx b/fableous-fe/src/components/canvas/Canvas.tsx index 656b122e..31a3ea20 100644 --- a/fableous-fe/src/components/canvas/Canvas.tsx +++ b/fableous-fe/src/components/canvas/Canvas.tsx @@ -84,6 +84,9 @@ interface SimplePointerEventData { onLeave: boolean; } +/** + * Shows text and drawing from students and handles processing of incoming messages. + */ const Canvas = forwardRef( (props: CanvasProps, ref) => { let FRAME_COUNTER = 0; diff --git a/fableous-fe/src/components/canvas/CursorScreen.tsx b/fableous-fe/src/components/canvas/CursorScreen.tsx index 055e4ada..2db99dc5 100644 --- a/fableous-fe/src/components/canvas/CursorScreen.tsx +++ b/fableous-fe/src/components/canvas/CursorScreen.tsx @@ -26,6 +26,9 @@ const CURSOR_COLOR = "gray"; const CURSOR_WIDTH = 3; const CURSOR_ROLE_TEXT = "24px Arial"; +/** + * Shows cursor position of students + */ const CursorScreen = (props: CursorScreenProps) => { const { cursor, name, isShown, offsetWidth, offsetHeight } = props; const canvasRef = useRef(document.createElement("canvas")); diff --git a/fableous-fe/src/containers/ControllerCanvasPage.tsx b/fableous-fe/src/containers/ControllerCanvasPage.tsx index c843c1f1..bcafc945 100644 --- a/fableous-fe/src/containers/ControllerCanvasPage.tsx +++ b/fableous-fe/src/containers/ControllerCanvasPage.tsx @@ -23,7 +23,7 @@ import Joyride, { Step, StoreHelpers } from "react-joyride"; import Canvas from "../components/canvas/Canvas"; import { restAPI, wsAPI } from "../api"; import { APIResponse, ControllerJoin, Session } from "../data"; -import useWsConn from "../hooks/useWsConn"; +import { useWsConn, useContainRatio, useTutorial } from "../hooks"; import CursorScreen, { Cursor } from "../components/canvas/CursorScreen"; import FormikTextField from "../components/FormikTextField"; import { @@ -40,11 +40,9 @@ import { BRUSH_COLORS, BRUSH_WIDTHS, } from "../components/canvas/constants"; -import useContainRatio from "../hooks/useContainRatio"; import ChipRow from "../components/ChipRow"; import { colors } from "../colors"; import { TutorialTargetId } from "../tutorialTargetIds"; -import useTutorial from "../hooks/useTutorial"; import { useCustomNav } from "../components/CustomNavProvider"; import StoryCompletionPrompt from "../components/StoryCompletionPrompt"; import { proto as pb } from "../proto/message_pb"; diff --git a/fableous-fe/src/containers/HubCanvasPage.tsx b/fableous-fe/src/containers/HubCanvasPage.tsx index 9b4d1b57..f516187c 100644 --- a/fableous-fe/src/containers/HubCanvasPage.tsx +++ b/fableous-fe/src/containers/HubCanvasPage.tsx @@ -21,11 +21,10 @@ import AchievementButton from "../components/achievement/AchievementButton"; import Canvas from "../components/canvas/Canvas"; import CursorScreen, { Cursor } from "../components/canvas/CursorScreen"; import FormikTextField from "../components/FormikTextField"; -import { useAchievement, useWsConn } from "../hooks"; +import { useAchievement, useWsConn, useContainRatio } from "../hooks"; import { ROLE_ICON, StudentRole } from "../constant"; import BackButton from "../components/BackButton"; import { ImperativeCanvasRef, TextShapeMap } from "../components/canvas/data"; -import useContainRatio from "../hooks/useContainRatio"; import { ASPECT_RATIO } from "../components/canvas/constants"; import ChipRow from "../components/ChipRow"; import FormikTagField from "../components/FormikTagField"; diff --git a/fableous-fe/src/containers/StoryDetailPage.tsx b/fableous-fe/src/containers/StoryDetailPage.tsx index 70862f19..d2a4fb0e 100644 --- a/fableous-fe/src/containers/StoryDetailPage.tsx +++ b/fableous-fe/src/containers/StoryDetailPage.tsx @@ -31,8 +31,7 @@ import { ASPECT_RATIO } from "../components/canvas/constants"; import ChipRow from "../components/ChipRow"; import BackButton from "../components/BackButton"; import { APIResponse, Manifest, Session } from "../data"; -import useContainRatio from "../hooks/useContainRatio"; -import useTutorial from "../hooks/useTutorial"; +import { useContainRatio, useTutorial } from "../hooks"; import { proto as pb } from "../proto/message_pb"; import { TutorialTargetId } from "../tutorialTargetIds"; diff --git a/fableous-fe/src/hooks/index.ts b/fableous-fe/src/hooks/index.ts index a199fac9..04f86a22 100644 --- a/fableous-fe/src/hooks/index.ts +++ b/fableous-fe/src/hooks/index.ts @@ -1,4 +1,6 @@ import useAchievement from "./useAchievement"; +import useContainRatio from "./useContainRatio"; +import useTutorial from "./useTutorial"; import useWsConn from "./useWsConn"; -export { useAchievement, useWsConn }; +export { useAchievement, useContainRatio, useTutorial, useWsConn }; diff --git a/fableous-fe/src/hooks/useAchievement.ts b/fableous-fe/src/hooks/useAchievement.ts index b4bcf492..df619aa1 100644 --- a/fableous-fe/src/hooks/useAchievement.ts +++ b/fableous-fe/src/hooks/useAchievement.ts @@ -8,6 +8,17 @@ import { import { BRUSH_COLORS } from "../components/canvas/constants"; import { proto as pb } from "../proto/message_pb"; +/** + * Manages state of each achievements and exposes handlers to update them. + * + * @param {boolean} debug true to log achievement state to console + * + * @return {[Achievement, (ev: MessageEvent) => void, () => void, () => void]} + * achievement state, + * handler to attach as websocket message event listener, + * handler to run on story page change and + * function to reset state + */ export default function useAchievement(config?: { debug?: boolean; }): [Achievement, (ev: MessageEvent) => void, () => void, () => void] { diff --git a/fableous-fe/src/hooks/useContainRatio.ts b/fableous-fe/src/hooks/useContainRatio.ts index da8389dd..64ded349 100644 --- a/fableous-fe/src/hooks/useContainRatio.ts +++ b/fableous-fe/src/hooks/useContainRatio.ts @@ -1,11 +1,13 @@ import { useResizeDetector } from "react-resize-detector"; /** - * returns dimension of child dom that maintains aspect ratio + * Returns dimension of child DOM that maintains aspect ratio * and is contained within container * * @param {React.MutableRefObject} containerRef Ref to container * @param {number} ratio Width to height ratio + * + * @return {[number, number]} width and height */ export default function useContainRatio(config: { containerRef: React.MutableRefObject; diff --git a/fableous-fe/src/hooks/useTutorial.ts b/fableous-fe/src/hooks/useTutorial.ts index 4743695c..e2d71c85 100644 --- a/fableous-fe/src/hooks/useTutorial.ts +++ b/fableous-fe/src/hooks/useTutorial.ts @@ -20,7 +20,9 @@ enum TutorialState { * @param {() => {}} onManualStartCallback custom logic to run on navbar tutorial button click * @param {number} duration duration in milliseconds to not automatically start tutorial from most recent use * - * @return {[boolean, (data: CallBackProps) => void]} tutorial running state and callback function to pass to Joyride component + * @return {[boolean, (data: CallBackProps) => void]} + * tutorial running state and + * callback function to pass to Joyride component */ export default function useTutorial(config: { showTutorialButton: boolean; diff --git a/fableous-fe/src/hooks/useWsConn.ts b/fableous-fe/src/hooks/useWsConn.ts index a8cb450b..fffe90b7 100644 --- a/fableous-fe/src/hooks/useWsConn.ts +++ b/fableous-fe/src/hooks/useWsConn.ts @@ -1,6 +1,14 @@ import { useCallback, useEffect, useState } from "react"; import { proto as pb } from "../proto/message_pb"; +/** + * Manages websocket state by handling pings to keep connection alive. + * + * @return {[WebSocket | undefined, Dispatch>, () => void]} + * websocket state, + * setstate and + * function to clear websocket + */ export default function useWsConn(): [ WebSocket | undefined, React.Dispatch>, diff --git a/proto/message.proto b/proto/message.proto index d235dc50..2779083a 100644 --- a/proto/message.proto +++ b/proto/message.proto @@ -1,6 +1,7 @@ syntax = "proto3"; package proto; +// WSMessageType defines the type of the message. enum WSMessageType { UNKNOWN = 0; ERROR = 1; @@ -19,6 +20,7 @@ enum WSMessageType { MANIFEST = 14; } +// ControllerRole defines the type of controller roles. enum ControllerRole { NONE = 0; HUB = 1; @@ -27,9 +29,10 @@ enum ControllerRole { BACKGROUND = 4; } +// WSMessage wraps all WebSocket messages sent between the client and server. message WSMessage { - WSMessageType type = 1; - ControllerRole role = 2; + WSMessageType type = 1; // type of message + ControllerRole role = 2; // origin of message oneof data { WSPaintMessageData paint = 3; WSControlMessageData control = 4; @@ -37,32 +40,37 @@ message WSMessage { WSAchievementMessageData achievement = 6; WSErrorMessageData error = 7; } - int64 timestamp = 8; + int64 timestamp = 8; // used during benchmarking } +// WSPaintMessageData contains the data for a paint message. +// All coordinates (x1, y1, x2, y2) and width are normalized to the range [0, 1]. message WSPaintMessageData { float x1 = 1; float y1 = 2; float x2 = 3; float y2 = 4; int32 id = 5; - string text = 6; + string text = 6; // can contain plaintext or base64 payload depending on WSMessageType string color = 7; float width = 8; } +// WSControlMessageData contains the data for a control message. message WSControlMessageData { string classroom_token = 1; string classroom_id = 2; string session_id = 3; int32 current_page = 4; - bool next_page = 5; - bool help = 6; - bool done = 7; - ControllerRole clear = 8; - ControllerRole kick = 9; + bool next_page = 5; // true if hub pushes next page + bool help = 6; // true if controller requests help + bool done = 7; // true if controller marks done + ControllerRole clear = 8; // role to clear + ControllerRole kick = 9; // role to kick } +// WSAchievementMessageData contains the data for an achievement message. +// Must be in sync with Achievements in fableous-fe. message WSAchievementMessageData { float all_color = 1; float five_text = 2; @@ -72,12 +80,14 @@ message WSAchievementMessageData { float five_page = 6; } +// WSJoinMessageData contains the data for a join message. message WSJoinMessageData { ControllerRole role = 1; string name = 2; - bool joining = 3; + bool joining = 3; // true if joining, false if leaving } +// WSErrorMessageData contains the data for an error message. message WSErrorMessageData { string error = 1; }