diff --git a/constants/event.go b/constants/event.go index 58ca6ac..4ba7304 100644 --- a/constants/event.go +++ b/constants/event.go @@ -2,6 +2,7 @@ package constants const ( PE2Name = "Pre-event 2" + PE3Name = "Pre-event 3" MainEventEarlyBirdWithMerch = "Early Bird with merchandise bundle" MainEventPreSaleWithMerch = "Pre Sale with merchandise bundle" MainEventNormalWithMerch = "Normal with merchandise bundle" @@ -12,7 +13,7 @@ const ( const ( MainEventEarlyBirdWithMerchCapacity = 7 - MainEventPreSaleWithMerchCapacity = 13 + MainEventPreSaleWithMerchCapacity = 14 MainEventNormalWithMerchCapacity = 20 MainEventEarlyBirdNoMerchCapacity = 13 MainEventPreSaleNoMerchCapacity = 57 @@ -27,4 +28,5 @@ const ( MainEventPreSaleWithMerchID = "edef91cb-f43f-4a30-a5f0-43acd0d6853f" MainEventNormalWithMerchID = "0594226b-c314-42e7-9743-c76fdd6b7099" PreEvent2ID = "7de24efe-0aec-469a-bf0c-8fa8cae3ff3f" + PreEvent3ID = "d436ff9d-5956-48a5-acb1-1e96d94fc3c4" ) diff --git a/controller/pre-event-3.go b/controller/pre-event-3.go new file mode 100644 index 0000000..2969933 --- /dev/null +++ b/controller/pre-event-3.go @@ -0,0 +1,59 @@ +package controller + +import ( + "net/http" + + "github.com/TEDxITS/website-backend-2024/constants" + "github.com/TEDxITS/website-backend-2024/dto" + "github.com/TEDxITS/website-backend-2024/service" + "github.com/TEDxITS/website-backend-2024/utils" + "github.com/gin-gonic/gin" +) + +type ( + PreEvent3Controller interface { + RegisterPreEvent3(ctx *gin.Context) + GetPreEvent3Status(ctx *gin.Context) + } + + preEvent3Controller struct { + preEvent3Service service.PreEvent3Service + } +) + +func NewPreEvent3Controller(service service.PreEvent3Service) PreEvent3Controller { + return &preEvent3Controller{ + preEvent3Service: service, + } +} + +func (c *preEvent3Controller) RegisterPreEvent3(ctx *gin.Context) { + var req dto.PE3RSVPRegister + if err := ctx.ShouldBind(&req); err != nil { + res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_GET_DATA_FROM_BODY, err.Error(), nil) + ctx.AbortWithStatusJSON(http.StatusBadRequest, res) + return + } + + err := c.preEvent3Service.RegisterPE3(req, ctx.GetString(constants.CTX_KEY_USER_ID)) + if err != nil { + res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_CREATE_TICKET, err.Error(), nil) + ctx.JSON(http.StatusBadRequest, res) + return + } + + res := utils.BuildResponseSuccess(dto.MESSAGE_SUCCESS_CREATE_TICKET, nil) + ctx.JSON(http.StatusOK, res) +} + +func (c *preEvent3Controller) GetPreEvent3Status(ctx *gin.Context) { + status, err := c.preEvent3Service.GetStatus() + if err != nil { + res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_GET_PE3_STATUS, err.Error(), nil) + ctx.JSON(http.StatusBadRequest, res) + return + } + + res := utils.BuildResponseSuccess(dto.MESSAGE_SUCCESS_GET_PE3_STATUS, status) + ctx.JSON(http.StatusOK, res) +} diff --git a/dto/pre-event-3.go b/dto/pre-event-3.go new file mode 100644 index 0000000..418984f --- /dev/null +++ b/dto/pre-event-3.go @@ -0,0 +1,29 @@ +package dto + +import ( + "errors" + "mime/multipart" + "time" +) + +const ( + MESSAGE_FAILED_GET_PE3_STATUS = "Failed to get pre-Event 3 status" + MESSAGE_SUCCESS_GET_PE3_STATUS = "Successfully get pre-Event 3 status" +) + +var ( + ErrPreEvent3NotYetOpen = errors.New("pre-Event 3 registration is not yet open") + ErrPreEvent3Closed = errors.New("pre-Event 3 registration is closed") +) + +type ( + PE3RSVPRegister struct { + Handphone string `json:"handphone" form:"handphone" binding:"required"` + Birthdate time.Time `json:"birthdate" form:"birthdate" binding:"required"` + PaymentFile *multipart.FileHeader `json:"payment_file" form:"payment_file" binding:"required"` + } + + PE3RSVPStatus struct { + Status *bool `json:"status"` + } +) diff --git a/dto/ticket-queue.go b/dto/ticket-queue.go deleted file mode 100644 index 91894fd..0000000 --- a/dto/ticket-queue.go +++ /dev/null @@ -1,36 +0,0 @@ -package dto - -import "errors" - -const ( - WSOCKET_AUTH_REQUEST = "TOKEN %v" - WSOCKET_ENUM_NO_MERCH_REQUEST = "MERCH 0" - WSOCKET_ENUM_WITH_MERCH_REQUEST = "MERCH 1" - - WSOCKET_PAYMENT_CODE = "PAYMENT CODE %v" - - WSOCKET_AUTH_SUCCESS = "authentication successful" - WSOCKET_TRANSACTION_START = "proceed transaction" - WSOCKET_TRANSACTION_SUCCESS = "transaction successful" -) - -var ( - ErrWSAlreadyInQueue = errors.New("already in queue") - ErrWSBadRequest = errors.New("bad request") - ErrWSInvalidToken = errors.New("invalid token") - ErrWSInvalidCommand = errors.New("invalid command") - ErrWSMainEventFull = errors.New("main event is full") - ErrWSCommunicateWithDB = errors.New("error fetching data") -) - -type ( - // S2C = Server to Client - S2CQueueLineInfo struct { - QueueNumber int `json:"queue_number"` - } - - S2CMerchStockInfo struct { - WithMerch int `json:"with_merch"` - NoMerch int `json:"no_merch"` - } -) diff --git a/main.go b/main.go index 8d15093..5fe5ffe 100644 --- a/main.go +++ b/main.go @@ -39,11 +39,6 @@ func main() { ticketRepository repository.TicketRepository = repository.NewTicketRepository(db) bucketRepository repository.BucketRepository = repository.NewSupabaseBucketRepository(bucket) - // websocket hub - // earlyBirdHub websocket.QueueHub = websocket.RunConnHub(eventRepository, 4, constants.MainEventEarlyBirdNoMerchID, constants.MainEventEarlyBirdWithMerchID) - // preSaleHub websocket.QueueHub = websocket.RunConnHub(eventRepository, 4, constants.MainEventPreSaleNoMerchID, constants.MainEventPreSaleWithMerchID) - // normalHub websocket.QueueHub = websocket.RunConnHub(eventRepository, 4, constants.MainEventNormalNoMerchID, constants.MainEventNormalWithMerchID) - // services userService service.UserService = service.NewUserService(userRepository, roleRepo) linkShortenerService service.LinkShortenerService = service.NewLinkShortenerService(linkShortenerRepository) @@ -51,6 +46,7 @@ func main() { eventService service.EventService = service.NewEventService(eventRepository) mainEventService service.MainEventService = service.NewMainEventService(userRepository, ticketRepository, eventRepository, bucketRepository) storageService service.StorageService = service.NewStorageService(bucketRepository) + preEvent3Service service.PreEvent3Service = service.NewPreEvent3Service(userRepository, ticketRepository, eventRepository, bucketRepository) // controllers userController controller.UserController = controller.NewUserController(userService, jwtService) @@ -59,11 +55,7 @@ func main() { preEvent2Controller controller.PreEvent2Controller = controller.NewPreEvent2Controller(preEvent2Service) mainEventController controller.MainEventController = controller.NewMainEventController(mainEventService) storageController controller.StorageController = controller.NewStorageController(storageService) - - // websocket handler - // earlyBirdQueue websocket.TicketQueue = websocket.NewTicketQueue(earlyBirdHub, jwtService) - // preSaleQueue websocket.TicketQueue = websocket.NewTicketQueue(preSaleHub, jwtService) - // normalQueue websocket.TicketQueue = websocket.NewTicketQueue(normalHub, jwtService) + preEvent3Controller controller.PreEvent3Controller = controller.NewPreEvent3Controller(preEvent3Service) ) server := gin.Default() @@ -76,8 +68,8 @@ func main() { routes.Event(server, eventController, jwtService) routes.PreEvent2(server, preEvent2Controller, jwtService) routes.MainEvent(server, mainEventController, jwtService) - // routes.TicketQueue(server, earlyBirdQueue, preSaleQueue, normalQueue) routes.Storage(server, storageController, jwtService) + routes.PreEvent3(server, preEvent3Controller, jwtService) // https://github.com/gin-contrib/cors // https://stackoverflow.com/questions/76196547/websocket-returning-403-every-time diff --git a/migrations/seeder/seeders/event.go b/migrations/seeder/seeders/event.go index 3009d18..ef349fc 100644 --- a/migrations/seeder/seeders/event.go +++ b/migrations/seeder/seeders/event.go @@ -87,7 +87,17 @@ func EventSeeder(db *gorm.DB) error { Registers: 0, StartDate: time.Date(2024, time.May, 16, 15, 0, 0, 0, time.Now().UTC().Location()), EndDate: time.Date(2024, time.May, 24, 12, 0, 0, 0, time.Now().UTC().Location()), - }) + }, entity.Event{ + ID: uuid.MustParse(constants.PreEvent3ID), + Name: constants.PE3Name, + Price: 0, + WithKit: &False, + Capacity: 999, + Registers: 0, + StartDate: time.Date(2024, time.May, 19, 15, 0, 0, 0, time.Now().UTC().Location()), + EndDate: time.Date(2024, time.May, 24, 12, 0, 0, 0, time.Now().UTC().Location()), + }, + ) for _, data := range eventList { event := entity.Event{} diff --git a/routes/pre-event-3.go b/routes/pre-event-3.go new file mode 100644 index 0000000..0365c44 --- /dev/null +++ b/routes/pre-event-3.go @@ -0,0 +1,16 @@ +package routes + +import ( + "github.com/TEDxITS/website-backend-2024/config" + "github.com/TEDxITS/website-backend-2024/controller" + "github.com/TEDxITS/website-backend-2024/middleware" + "github.com/gin-gonic/gin" +) + +func PreEvent3(r *gin.Engine, c controller.PreEvent3Controller, jwt config.JWTService) { + preEvent3 := r.Group("/api/ticket/pre-event-3") + { + preEvent3.POST("", middleware.Authenticate(jwt), c.RegisterPreEvent3) + preEvent3.GET("/status", c.GetPreEvent3Status) + } +} diff --git a/routes/ticket-queue.go b/routes/ticket-queue.go deleted file mode 100644 index b802a0a..0000000 --- a/routes/ticket-queue.go +++ /dev/null @@ -1,15 +0,0 @@ -package routes - -import ( - "github.com/TEDxITS/website-backend-2024/websocket" - "github.com/gin-gonic/gin" -) - -func TicketQueue(route *gin.Engine, earlyBirdHandler, preSaleHandler, normalHandler websocket.TicketQueue) { - routes := route.Group("/ws") - { - routes.GET("/early-bird", earlyBirdHandler.Serve) - routes.GET("/pre-sale", preSaleHandler.Serve) - routes.GET("/normal", normalHandler.Serve) - } -} diff --git a/service/pre-event-3.go b/service/pre-event-3.go new file mode 100644 index 0000000..7d084fb --- /dev/null +++ b/service/pre-event-3.go @@ -0,0 +1,190 @@ +package service + +import ( + "bytes" + "net/http" + "os" + "strconv" + "text/template" + "time" + + "github.com/TEDxITS/website-backend-2024/constants" + "github.com/TEDxITS/website-backend-2024/dto" + "github.com/TEDxITS/website-backend-2024/entity" + "github.com/TEDxITS/website-backend-2024/repository" + "github.com/TEDxITS/website-backend-2024/utils" + "gorm.io/gorm" +) + +type ( + PreEvent3Service interface { + RegisterPE3(dto.PE3RSVPRegister, string) error + GetStatus() (dto.PE3RSVPStatus, error) + } + + preEvent3Service struct { + eventRepo repository.EventRepository + userRepo repository.UserRepository + ticketRepo repository.TicketRepository + bucketRepo repository.BucketRepository + } +) + +func NewPreEvent3Service( + uRepo repository.UserRepository, + tRepo repository.TicketRepository, + eRepo repository.EventRepository, + bRepo repository.BucketRepository, +) PreEvent3Service { + return &preEvent3Service{ + eventRepo: eRepo, + userRepo: uRepo, + ticketRepo: tRepo, + bucketRepo: bRepo, + } +} + +func (s *preEvent3Service) RegisterPE3(req dto.PE3RSVPRegister, userID string) error { + event, err := s.eventRepo.GetByID(constants.PreEvent3ID) + if err != nil { + return err + } + + if time.Now().Before(event.StartDate.Add(-7 * time.Hour)) { + return dto.ErrPreEvent3NotYetOpen + } + + if time.Now().After(event.EndDate.Add(-7 * time.Hour)) { + return dto.ErrPreEvent3Closed + } + + // validating uploaded file + if req.PaymentFile.Size > dto.MB*5 { + return dto.ErrMaxFileSize5MB + } + + file, err := req.PaymentFile.Open() + if err != nil { + return err + } + defer file.Close() + + fileBuffer := make([]byte, 512) + if _, err := file.Read(fileBuffer); err != nil { + return err + } + + if _, err := file.Seek(0, 0); err != nil { + return err + } + + // only allow for jpeg/jpg/png + fileType := http.DetectContentType(fileBuffer) + if fileType != dto.ENUM_FILE_TYPE_JPEG && fileType != dto.ENUM_FILE_TYPE_PNG { + return dto.ErrFileMustBeImage + } + ext := "." + utils.GetExtensions(req.PaymentFile.Filename) + + // generating unique code + var code string + for { + code = utils.GenUniqueCode() + if _, err := s.ticketRepo.GetTicketById(code); err != nil && err != gorm.ErrRecordNotFound { + return err + } else { + break + } + } + + req.PaymentFile.Filename = code + ext + err = s.bucketRepo.UploadFile(dto.ENUM_STORAGE_FOLDER_MAIN_EVENT, req.PaymentFile) + if err != nil { + return dto.ErrFailedToStorePaymentFile + } + + False := false + getFileEndpoint := dto.STORAGE_ENDPOINT_MAIN_EVENT + code + ext + ticket := entity.Ticket{ + TicketID: code, + UserID: userID, + EventID: constants.PreEvent3ID, + Handphone: req.Handphone, + Birthdate: req.Birthdate, + Payment: getFileEndpoint, + PaymentConfirmed: &False, + CheckedIn: &False, + } + + if _, err := s.ticketRepo.CreateTicket(ticket); err != nil { + return err + } + + // send email + user, err := s.userRepo.GetUserById(userID) + if err != nil { + return err + } + + readHtml, err := os.ReadFile("./utils/template/mail_payment_received.html") + if err != nil { + return err + } + + tmpl, err := template.New("custom").Parse(string(readHtml)) + if err != nil { + return err + } + + var price string + if event.Price >= 1000 { + price = strconv.Itoa(event.Price) + price = price[:len(price)-3] + "." + price[len(price)-3:] + } + + var strMail bytes.Buffer + if err := tmpl.Execute(&strMail, struct { + Name string + TicketType string + TotalPrice string + }{ + Name: user.Name, + TicketType: event.Name, + TotalPrice: price, + }); err != nil { + return err + } + + emailData := utils.Email{ + Email: user.Email, + Subject: "Payment Received", + Body: strMail.String(), + } + + err = utils.SendMail(emailData) + if err != nil { + return dto.ErrSendEmail + } + + return nil +} + +func (s *preEvent3Service) GetStatus() (dto.PE3RSVPStatus, error) { + status := true + + event, err := s.eventRepo.GetByID(constants.PreEvent3ID) + if err != nil { + return dto.PE3RSVPStatus{}, err + } + + if time.Now().Before(event.StartDate.Add(-7 * time.Hour)) { + status = false + } + + if time.Now().After(event.EndDate.Add(-7 * time.Hour)) { + status = false + } + + return dto.PE3RSVPStatus{ + Status: &status, + }, nil +} diff --git a/websocket/client.go b/websocket/client.go deleted file mode 100644 index ecad158..0000000 --- a/websocket/client.go +++ /dev/null @@ -1,120 +0,0 @@ -package websocket - -import ( - "sync" - "time" - - "github.com/gorilla/websocket" -) - -type ( - Client struct { - Conn *websocket.Conn - - UserID string - Authenticated bool - Waiting bool - WithMerch bool - - Next chan *Client - Notification chan string - Quit chan error - - *sync.Mutex - } -) - -func NewClient(conn *websocket.Conn) *Client { - return &Client{ - Conn: conn, - - Authenticated: false, - Waiting: false, - WithMerch: false, - - Next: make(chan *Client, 1), - Notification: make(chan string), - Quit: make(chan error), - - Mutex: new(sync.Mutex), - } -} - -func (c *Client) SetDeadline(deadline time.Time) { - c.Conn.SetReadDeadline(deadline) - c.Conn.SetWriteDeadline(deadline) -} - -func (c *Client) SendTextMessage(msg string) error { - c.Lock() - defer c.Unlock() - - if err := c.Conn.WriteMessage(websocket.TextMessage, []byte(msg)); nil != err { - return err - } - return nil -} - -func (c *Client) ReadMessage() ([]byte, error) { - c.Lock() - defer c.Unlock() - - _, message, err := c.Conn.ReadMessage() - if nil != err { - return nil, err - } - - return message, nil -} - -func (c *Client) Done(err error) { - c.Quit <- err -} - -func (c *Client) Notify(notif string) { - c.Notification <- notif -} - -func (c *Client) InformNext(next *Client) { - c.Next <- next -} - -func (c *Client) SetUserID(userID string) { - c.Lock() - defer c.Unlock() - - c.UserID = userID -} - -func (c *Client) SetAuthenticated() { - c.Lock() - defer c.Unlock() - - c.Authenticated = true -} - -func (c *Client) IsAuthenticated() bool { - return c.Authenticated -} - -func (c *Client) SetWaiting() { - c.Lock() - defer c.Unlock() - - c.Waiting = true -} - -func (c *Client) IsWaiting() bool { - return c.Waiting -} - -func (c *Client) SetWithMerch(b bool) { - c.Lock() - defer c.Unlock() - - c.WithMerch = b -} - -func (c *Client) IsWithMerch() bool { - return c.WithMerch -} diff --git a/websocket/handler.go b/websocket/handler.go deleted file mode 100644 index dd8348c..0000000 --- a/websocket/handler.go +++ /dev/null @@ -1,269 +0,0 @@ -package websocket - -import ( - "encoding/json" - "fmt" - "net/http" - "strings" - "time" - - "github.com/TEDxITS/website-backend-2024/config" - "github.com/TEDxITS/website-backend-2024/constants" - "github.com/TEDxITS/website-backend-2024/dto" - "github.com/gin-gonic/gin" - "github.com/gorilla/websocket" -) - -var upgrader = websocket.Upgrader{ - ReadBufferSize: 1024, - WriteBufferSize: 1024, - CheckOrigin: func(r *http.Request) bool { - return true - }, -} - -type ( - TicketQueue interface { - Serve(*gin.Context) - WaitQueueTurn(*Client) error - - Register(*Client) - Unregister(*Client) - SendOperation(operation) - } - - ticketQueue struct { - Hub QueueHub - jwtService config.JWTService - } -) - -func NewTicketQueue(hub QueueHub, jwt config.JWTService) TicketQueue { - return &ticketQueue{ - Hub: hub, - jwtService: jwt, - } -} - -func (Handler *ticketQueue) Serve(ctx *gin.Context) { - conn, err := upgrader.Upgrade(ctx.Writer, ctx.Request, nil) - if nil != err { - return - } - defer conn.Close() - - client := NewClient(conn) - - // 10s to authenticate - client.SetDeadline(time.Now().Add(constants.WSOCKET_AUTH_TIME_LIMIT)) - for !client.IsAuthenticated() { - var token string - - message, err := client.ReadMessage() - if nil != err { - return - } - - if !strings.HasPrefix(string(message), constants.CTX_KEY_TOKEN) { - if err := client.SendTextMessage(dto.ErrWSInvalidCommand.Error()); nil != err { - return - } - continue - } - - if _, err := fmt.Sscanf(string(message), dto.WSOCKET_AUTH_REQUEST, &token); nil != err { - if err := client.SendTextMessage(err.Error()); nil != err { - return - } - continue - } - - userId, _, err := Handler.jwtService.GetPayloadInsideToken(token) - if nil != err { - if err := client.SendTextMessage(dto.ErrWSInvalidToken.Error()); nil != err { - return - } - continue - } - - client.SetUserID(userId) - client.SetAuthenticated() - - if err := client.SendTextMessage(dto.WSOCKET_AUTH_SUCCESS); nil != err { - return - } - } - - // already in queue, (detect using one account to queue multiple times) - if Handler.Hub.IsInQueueByUserID(client.UserID) { - _ = client.SendTextMessage(dto.ErrWSAlreadyInQueue.Error()) - return - } - - // register client to hub for tracking - Handler.Register(client) - - // set no timeout when waiting in queue - client.SetDeadline(time.Time{}) - - // the only error comes from this queue turn - // is only if the main event is full - time.Sleep(200 * time.Millisecond) - if err := Handler.WaitQueueTurn(client); nil != err { - if err != dto.ErrWSMainEventFull { - Handler.Unregister(client) - } - client.Conn.Close() - return - } - - // we don't wanna unregister if the the event is full - // because it would trigger Hub.BroadcastNext() - // which in turn will trigger client.Notify() - // which is a waste of resource - - // the two mentioned method above is already executed - // in the event of the main event is full - - // so we only unregister in case of: - // 1. client's transaction is done - // 2. client's transaction timeout - defer Handler.Unregister(client) - - // 3m to finish transaction - client.SetDeadline(time.Now().Add(constants.WSOCKET_TRANSACTION_TIME_LIMIT)) - if err := client.SendTextMessage(dto.WSOCKET_TRANSACTION_START); nil != err { - return - } - - withMerchPrice, noMerchPrice := Handler.Hub.GetBasePrice() - - message, _ := json.Marshal(struct { - NoMerchBasePrice int `json:"no_merch_price"` - WithMerchBasePrice int `json:"with_merch_price"` - }{ - noMerchPrice, - withMerchPrice, - }) - if err := client.SendTextMessage(string(message)); nil != err { - return - } - - messageFromClient := make(chan []byte) - go func() { - for { - // otherwise, mutex block, thread hang - _, message, err := client.Conn.ReadMessage() - if nil != err { - client.Done(err) - return - } - messageFromClient <- message - } - }() - - for { - select { - // case for the client's action: - // 1. change the transaction type (with/without merch) - // 2. seat selection - case message := <-messageFromClient: - if len(message) == 0 { - if err := client.SendTextMessage(dto.ErrWSInvalidCommand.Error()); nil != err { - return - } - continue - } - - switch string(message) { - case dto.WSOCKET_ENUM_WITH_MERCH_REQUEST: - Handler.SendOperation(operation{ - client: client, - command: dto.WSOCKET_ENUM_WITH_MERCH_REQUEST, - }) - case dto.WSOCKET_ENUM_NO_MERCH_REQUEST: - Handler.SendOperation(operation{ - client: client, - command: dto.WSOCKET_ENUM_NO_MERCH_REQUEST, - }) - default: - if err := client.SendTextMessage(dto.ErrWSInvalidCommand.Error()); nil != err { - return - } - } - - // notify client of changing in information such as the stock of merch or the seat - case messageFromHub := <-client.Notification: - if err := client.SendTextMessage(messageFromHub); err != nil { - return - } - - // notify the handler to exit/return in case of the transaction is done - case err := <-client.Quit: - if err == nil { - client.SendTextMessage(dto.WSOCKET_TRANSACTION_SUCCESS) - } - - return - } - } - -} - -func (Handle *ticketQueue) WaitQueueTurn(client *Client) error { - if !client.IsWaiting() { - - // if not buffered at construct, we need to consume or else - // the thread will hang/block - // <-client.Notification - - return nil - } - - // get and send the initial queue line number - queueNumber := Handle.Hub.GetWaitingLength() - message, _ := json.Marshal(dto.S2CQueueLineInfo{ - QueueNumber: queueNumber, - }) - if err := client.SendTextMessage(string(message)); nil != err { - return err - } - - for { - select { - // notification of the main event is full - case notif := <-client.Notification: - if notif == dto.ErrWSMainEventFull.Error() { - client.SendTextMessage(notif) - return dto.ErrWSMainEventFull - } - // receiving the next client to forward to transaction - // if not the current client, then it will simply decrement its queue number - case next := <-client.Next: - if next == client { - return nil - } - - queueNumber-- - message, _ := json.Marshal(dto.S2CQueueLineInfo{ - QueueNumber: queueNumber, - }) - - if err := client.SendTextMessage(string(message)); nil != err { - return err - } - } - } -} - -func (Handle *ticketQueue) Register(client *Client) { - Handle.Hub.GetRegisterChannel() <- client -} - -func (Handle *ticketQueue) Unregister(client *Client) { - Handle.Hub.GetUnregisterChannel() <- client -} - -func (Handle *ticketQueue) SendOperation(ops operation) { - Handle.Hub.GetOperationChannel() <- ops -} diff --git a/websocket/hub.go b/websocket/hub.go deleted file mode 100644 index e2d5e60..0000000 --- a/websocket/hub.go +++ /dev/null @@ -1,362 +0,0 @@ -package websocket - -import ( - "encoding/json" - "math/rand" - "sync" - - "github.com/TEDxITS/website-backend-2024/dto" - "github.com/TEDxITS/website-backend-2024/entity" - "github.com/TEDxITS/website-backend-2024/repository" -) - -type ( - QueueHub interface { - PushWaiting(*Client) - WalkRemove(*Client) - UpdateMaxTransaction() (entity.Event, entity.Event) - - BroadcastNext() - BroadcastStock() - - IsEventHandler(string) bool - IsInQueueByUserID(userID string) bool - GetClientInTransactionByUserID(string) *Client - - GetWaitingLength() int - GetRegisterChannel() chan *Client - GetUnregisterChannel() chan *Client - GetOperationChannel() chan operation - GetBasePrice() (int, int) - } - - queueHub struct { - Transaction []*Client - Waiting []*Client - MaxTransaction int - - Register chan *Client - Unregister chan *Client - Operation chan operation - Done chan string - - NoMerchID string - WithMerchID string - - NoMerchPrice int - WithMerchPrice int - - repository repository.EventRepository - - *sync.Mutex - } - - operation struct { - client *Client - command string - } -) - -func RunConnHub(repo repository.EventRepository, max int, noMerchID, withMerchID string) QueueHub { - hub := &queueHub{ - Transaction: make([]*Client, 0), - Waiting: make([]*Client, 0), - MaxTransaction: max, - - Register: make(chan *Client), - Unregister: make(chan *Client), - Operation: make(chan operation), - Done: make(chan string), - - NoMerchID: noMerchID, - WithMerchID: withMerchID, - - repository: repo, - - Mutex: new(sync.Mutex), - } - - go func() { - withMerch, noMerch := hub.UpdateMaxTransaction() - hub.WithMerchPrice = withMerch.Price - hub.NoMerchPrice = noMerch.Price - - for { - select { - // register takes the client and records it for tracking - case client := <-hub.Register: - // when the main event is full - if hub.MaxTransaction <= 0 { - client.SetWaiting() - client.Notify(dto.ErrWSMainEventFull.Error()) - continue - } - - _, noMerch := hub.UpdateMaxTransaction() - if (noMerch.Capacity - noMerch.Registers) <= 0 { - client.SetWithMerch(true) - } - - // if the main event is full, push the client to waiting list - if len(hub.Transaction) >= hub.MaxTransaction { - hub.PushWaiting(client) - continue - } - - // otherwise, push the client to transaction - hub.PushTransaction(client) - client.InformNext(client) - hub.BroadcastStock() - - // remove the client from the queue - case client := <-hub.Unregister: - hub.WalkRemove(client) // remove the client from the queue - if hub.MaxTransaction > 0 { - hub.UpdateMaxTransaction() // update the max transaction that could happening at a time - } - hub.BroadcastNext() // broadcast the next client in the waiting list (or notify the main event is full) - hub.BroadcastStock() // broadcast the remaining stock - - // operation is a command to change the client's transaction type - // 1. with/without merch selection - // 2. seat selection - case ops := <-hub.Operation: - withMerch, noMerch := hub.UpdateMaxTransaction() - withMerchRemainder := (withMerch.Capacity - withMerch.Registers) - noMerchRemainder := (noMerch.Capacity - noMerch.Registers) - - switch ops.command { - case dto.WSOCKET_ENUM_WITH_MERCH_REQUEST: - if withMerchRemainder <= 0 { - ops.client.Notify(dto.ErrWSInvalidCommand.Error()) - continue - } - ops.client.SetWithMerch(true) - case dto.WSOCKET_ENUM_NO_MERCH_REQUEST: - if noMerchRemainder <= 0 { - ops.client.Notify(dto.ErrWSInvalidCommand.Error()) - continue - } - ops.client.SetWithMerch(false) - } - - hub.BroadcastStock() - - message, _ := json.Marshal(struct { - Price int `json:"payment_price"` - }{ - Price: func() int { - if ops.client.IsWithMerch() { - return withMerch.Price + rand.Intn(999) - } - return noMerch.Price + rand.Intn(999) - }(), - }) - ops.client.Notify(string(message)) - } - } - }() - - return hub -} - -func (Hub *queueHub) UpdateMaxTransaction() (entity.Event, entity.Event) { - Hub.Lock() - defer Hub.Unlock() - - withMerch, _ := Hub.repository.GetByID(Hub.WithMerchID) - noMerch, _ := Hub.repository.GetByID(Hub.NoMerchID) - remainingCapacity := (withMerch.Capacity - withMerch.Registers) + (noMerch.Capacity - noMerch.Registers) + len(Hub.Transaction) - - // updates the max transaction that could happening at a time - // in case of the initial set max transaction is larger - // than the remaining capacity - if remainingCapacity <= Hub.MaxTransaction { - Hub.MaxTransaction = remainingCapacity - } - - return withMerch, noMerch -} - -func (Hub *queueHub) PushWaiting(client *Client) { - Hub.Lock() - defer Hub.Unlock() - - client.SetWaiting() - Hub.Waiting = append(Hub.Waiting, client) -} - -func (Hub *queueHub) PushTransaction(client *Client) { - Hub.Lock() - defer Hub.Unlock() - - Hub.Transaction = append(Hub.Transaction, client) -} - -func (Hub *queueHub) WalkRemove(client *Client) { - Hub.Lock() - defer Hub.Unlock() - - for idx, c := range Hub.Transaction { - if c == client { - Hub.Transaction = append(Hub.Transaction[:idx], Hub.Transaction[idx+1:]...) - return - } - } - - for idx, c := range Hub.Waiting { - if c == client { - Hub.Waiting = append(Hub.Waiting[:idx], Hub.Waiting[idx+1:]...) - return - } - } -} - -func (Hub *queueHub) BroadcastNext() { - Hub.Lock() - defer Hub.Unlock() - - // if no one is waiting, nothing to broadcast - if len(Hub.Waiting) == 0 { - return - } - - // if max transaction is 0, i.e. the main event is full - // broadcast the error message to all waiting clients - // which in turn also signal the handlers to exit - // and clear the waiting list - if Hub.MaxTransaction == 0 { - for _, client := range Hub.Waiting { - client.Notify(dto.ErrWSMainEventFull.Error()) - } - - // clear the waiting list, - // since the client upon exit will also unregister it self from the hub - // this will prevent this loop block to broadcast to be executed again - // just to save some resource - Hub.Waiting = nil - return - } - - // pop next in queue to forward to transaction - next := Hub.Waiting[0] - if len(Hub.Waiting) == 1 { - Hub.Waiting = nil - } else { - Hub.Waiting = Hub.Waiting[1:] - } - - // forward the client to transaction - Hub.Transaction = append(Hub.Transaction, next) - - // broadcast the next client, - // if the client isn't the next to continue to transaction - // it'll simply updates and inform the user of current queue number - // they're at - next.InformNext(next) - for _, client := range Hub.Waiting { - client.InformNext(next) - } -} - -func (Hub *queueHub) BroadcastStock() { - Hub.Lock() - defer Hub.Unlock() - - // no more allowed transaction, i.e. main event is full - // return early - if Hub.MaxTransaction <= 0 { - return - } - - // get the current recorded transaction - noMerch, err1 := Hub.repository.GetByID(Hub.NoMerchID) - withMerch, err2 := Hub.repository.GetByID(Hub.WithMerchID) - if err1 != nil || err2 != nil { - for _, client := range Hub.Transaction { - client.Notify(dto.ErrWSCommunicateWithDB.Error()) - } - return - } - - for _, client := range Hub.Transaction { - if client.IsWithMerch() { - withMerch.Registers++ - } else { - noMerch.Registers++ - } - } - - message, _ := json.Marshal(dto.S2CMerchStockInfo{ - WithMerch: withMerch.Capacity - withMerch.Registers, - NoMerch: noMerch.Capacity - noMerch.Registers, - }) - - for _, client := range Hub.Transaction { - client.Notify(string(message)) - } - -} - -func (Hub *queueHub) IsInQueueByUserID(userID string) bool { - Hub.Lock() - defer Hub.Unlock() - - for _, c := range Hub.Transaction { - if c.UserID == userID { - return true - } - } - - for _, c := range Hub.Waiting { - if c.UserID == userID { - return true - } - } - - return false -} - -func (Hub *queueHub) GetClientInTransactionByUserID(userID string) *Client { - Hub.Lock() - defer Hub.Unlock() - - for _, c := range Hub.Transaction { - if c.UserID == userID { - return c - } - } - - return nil -} - -func (Hub *queueHub) GetRegisterChannel() chan *Client { - return Hub.Register -} - -func (Hub *queueHub) GetUnregisterChannel() chan *Client { - return Hub.Unregister -} - -func (Hub *queueHub) GetOperationChannel() chan operation { - return Hub.Operation -} - -func (Hub *queueHub) GetWaitingLength() int { - Hub.Lock() - defer Hub.Unlock() - - return len(Hub.Waiting) -} - -func (Hub *queueHub) IsEventHandler(eventID string) bool { - if Hub.NoMerchID == eventID || Hub.WithMerchID == eventID { - return true - } - - return false -} - -func (Hub *queueHub) GetBasePrice() (int, int) { - return Hub.WithMerchPrice, Hub.NoMerchPrice -}