Feat(ktm-booking): Initial commit
Some checks failed
ktm-booking-bot/ktm-booking-bot/pipeline/head Something is wrong with the build of this commit
Some checks failed
ktm-booking-bot/ktm-booking-bot/pipeline/head Something is wrong with the build of this commit
This commit is contained in:
477
backend/internal/ktmtrainbot/backgroundbookingjob.go
Normal file
477
backend/internal/ktmtrainbot/backgroundbookingjob.go
Normal file
@@ -0,0 +1,477 @@
|
||||
package ktmtrainbot
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.samuelpua.com/telboon/ktm-train-bot/backend/internal/user"
|
||||
"github.com/go-rod/rod"
|
||||
"github.com/go-rod/rod/lib/devices"
|
||||
"github.com/go-rod/rod/lib/launcher"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
)
|
||||
|
||||
const IMAGE_DIR = "/tmp/screenshots"
|
||||
const TIMEOUT_MINUTE = 60
|
||||
|
||||
func (env *Env) BackgroundJobRunner() {
|
||||
log.Println("Initialising background job...")
|
||||
initialiseRodBrowser()
|
||||
log.Println("Browser initialised...")
|
||||
|
||||
// Initialise silent logger
|
||||
newLogger := logger.New(
|
||||
log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer
|
||||
logger.Config{
|
||||
SlowThreshold: time.Second, // Slow SQL threshold
|
||||
LogLevel: logger.Silent, // Log level
|
||||
IgnoreRecordNotFoundError: true, // Ignore ErrRecordNotFound error for logger
|
||||
Colorful: true, // Disable color
|
||||
},
|
||||
)
|
||||
|
||||
tx := env.DB.Session(&gorm.Session{Logger: newLogger})
|
||||
for {
|
||||
var jobToDo Booking
|
||||
err := tx.Model(&jobToDo).
|
||||
Where("status = ?", "pending").
|
||||
Preload("User").
|
||||
First(&jobToDo).Error
|
||||
|
||||
// if no jobs pending found
|
||||
if err != nil {
|
||||
time.Sleep(time.Second)
|
||||
continue
|
||||
} else { // if there's job to do
|
||||
// Create next run where it's not the past (either from old NextRun or now())
|
||||
timeNow := time.Now()
|
||||
|
||||
if timeNow.Hour() == 0 && timeNow.Minute() == 10 {
|
||||
err := env.DB.Where(&user.Profile{UserID: jobToDo.UserID}).First(&jobToDo.User.Profile).Error
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
|
||||
log.Printf("Start doing job: %v", jobToDo.ID)
|
||||
username := jobToDo.User.Profile.KtmTrainUsername
|
||||
password := jobToDo.User.Profile.KtmTrainPassword
|
||||
creditCardType := jobToDo.User.Profile.KtmTrainCreditCardType
|
||||
creditCard := jobToDo.User.Profile.KtmTrainCreditCard
|
||||
creditCardCVV := jobToDo.User.Profile.KtmTrainCreditCardCVV
|
||||
creditCardExpiry := jobToDo.User.Profile.KtmTrainCreditCardExpiry
|
||||
|
||||
func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
log.Println("Recovering from job panic...")
|
||||
jobToDo.Status = "pending"
|
||||
env.DB.Save(&jobToDo)
|
||||
}
|
||||
}()
|
||||
success := env.startBooking(&jobToDo, username, password, creditCardType, creditCard, creditCardCVV, creditCardExpiry)
|
||||
if success {
|
||||
fmt.Println("Successfully made a booking.")
|
||||
jobToDo.Status = "success"
|
||||
env.DB.Save(jobToDo)
|
||||
} else {
|
||||
jobToDo.Status = "pending"
|
||||
env.DB.Save(&jobToDo)
|
||||
fmt.Println("Failed to make a booking.")
|
||||
}
|
||||
}()
|
||||
jobToDo.Status = "running"
|
||||
env.DB.Save(&jobToDo)
|
||||
log.Printf("Job Started: %v", jobToDo.ID)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func initialiseRodBrowser() {
|
||||
u := launcher.New().
|
||||
Set("headless").
|
||||
MustLaunch()
|
||||
|
||||
defaultDevice := devices.LaptopWithMDPIScreen
|
||||
browser := rod.New().ControlURL(u).MustConnect().DefaultDevice(defaultDevice)
|
||||
page := browser.MustPage("https://www.google.com").MustWindowFullscreen()
|
||||
page.MustWaitLoad()
|
||||
browser.MustClose()
|
||||
|
||||
// Initialise screenshot directory
|
||||
if _, err := os.Stat(IMAGE_DIR); errors.Is(err, os.ErrNotExist) {
|
||||
err := os.Mkdir(IMAGE_DIR, os.ModePerm)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (env *Env) startBooking(job *Booking, username string, password string, creditCardType string, creditCard string, creditCardCVV string, creditCardExpiry string) bool {
|
||||
timerCtx, cancelTimer := context.WithTimeout(context.Background(), TIMEOUT_MINUTE*time.Minute)
|
||||
defer cancelTimer()
|
||||
|
||||
headless := os.Getenv("HEADLESS")
|
||||
var u string
|
||||
if strings.ToUpper(headless) == "FALSE" {
|
||||
u = launcher.New().
|
||||
Set("headless").
|
||||
Delete("--headless").
|
||||
MustLaunch()
|
||||
} else {
|
||||
u = launcher.New().
|
||||
Set("headless").
|
||||
MustLaunch()
|
||||
}
|
||||
|
||||
defaultDevice := devices.LaptopWithMDPIScreen
|
||||
// defaultDevice.Screen.Vertical.Height = defaultDevice.Screen.Horizontal.Height
|
||||
// defaultDevice.Screen.Vertical.Width = defaultDevice.Screen.Horizontal.Width
|
||||
defaultDevice.UserAgent = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.0.0 Safari/537.36"
|
||||
browser := rod.New().ControlURL(u).MustConnect().DefaultDevice(defaultDevice)
|
||||
|
||||
// Defer closing browser
|
||||
defer browser.MustClose()
|
||||
|
||||
postLoginPage := ktmTrainLogin(browser, username, password)
|
||||
nowTimeStr := time.Now().Format("2006-01-02-15_04_05")
|
||||
postLoginPage.MustWaitLoad().MustScreenshot(fmt.Sprintf("%s/%s-01-login.png", IMAGE_DIR, nowTimeStr))
|
||||
|
||||
// Exits if context cancelled
|
||||
select {
|
||||
case <-timerCtx.Done():
|
||||
browser.MustClose()
|
||||
return false
|
||||
default:
|
||||
}
|
||||
|
||||
var page *rod.Page
|
||||
|
||||
getBookingSlotCtx, cancelGetBookingSlot := context.WithTimeout(context.Background(), TIMEOUT_MINUTE*time.Minute)
|
||||
defer cancelGetBookingSlot()
|
||||
|
||||
pageChan := make(chan *rod.Page)
|
||||
|
||||
onwardDate := job.TravelDate.Format("2 Jan 2006")
|
||||
timeCode := job.TimeCode
|
||||
name := job.Name
|
||||
gender := job.Gender
|
||||
passport := job.Passport
|
||||
passportExpiry := job.PassportExpiry.Format("2 Jan 2006")
|
||||
contact := job.Contact
|
||||
|
||||
threadCount := 10
|
||||
for i := 0; i < threadCount; i++ {
|
||||
time.Sleep(time.Millisecond * 100)
|
||||
go func() {
|
||||
currPage := getBookingSlots(browser, onwardDate)
|
||||
log.Println("Booking page loaded.")
|
||||
|
||||
currPage = selectBookingSlot(getBookingSlotCtx, currPage, timeCode)
|
||||
log.Println("Booking slot selected.")
|
||||
|
||||
select {
|
||||
case <-getBookingSlotCtx.Done():
|
||||
return
|
||||
default:
|
||||
log.Println("First page loaded.")
|
||||
}
|
||||
|
||||
// Cancelling context
|
||||
cancelGetBookingSlot()
|
||||
pageChan <- currPage
|
||||
}()
|
||||
}
|
||||
|
||||
page = <-pageChan
|
||||
page.MustActivate()
|
||||
|
||||
// Exits if context cancelled
|
||||
select {
|
||||
case <-timerCtx.Done():
|
||||
browser.MustClose()
|
||||
return false
|
||||
default:
|
||||
}
|
||||
|
||||
page = fillPassengerDetails(page, name, gender, passport, passportExpiry, contact)
|
||||
log.Println("Passenger details filled.")
|
||||
|
||||
// Exits if context cancelled
|
||||
select {
|
||||
case <-timerCtx.Done():
|
||||
browser.MustClose()
|
||||
return false
|
||||
default:
|
||||
}
|
||||
|
||||
page = choosePayment(page)
|
||||
log.Println("Payment method chosen.")
|
||||
|
||||
// Wait 5 seconds for payment gateway to load
|
||||
time.Sleep(time.Second * 5)
|
||||
|
||||
for _, currPage := range browser.MustPages() {
|
||||
currPage.MustWaitLoad()
|
||||
|
||||
var currTitle string
|
||||
err := rod.Try(func() {
|
||||
currTitle = currPage.Timeout(100 * time.Millisecond).MustElement("title").MustText()
|
||||
})
|
||||
if err != nil {
|
||||
currTitle = ""
|
||||
}
|
||||
|
||||
if strings.Contains(currTitle, "Payment Acceptance") {
|
||||
page = currPage
|
||||
}
|
||||
}
|
||||
|
||||
// Exits if context cancelled
|
||||
select {
|
||||
case <-timerCtx.Done():
|
||||
browser.MustClose()
|
||||
return false
|
||||
default:
|
||||
}
|
||||
|
||||
expiryMonth := strings.Split(creditCardExpiry, "/")[0]
|
||||
expiryYear := strings.Split(creditCardExpiry, "/")[1]
|
||||
|
||||
page = makePayment(page, creditCardType, creditCard, expiryMonth, expiryYear, creditCardCVV)
|
||||
log.Println("Payment made.")
|
||||
|
||||
// // Start debug screenshots
|
||||
// debugScreenshotCtx, cancelDebugScreenshot := context.WithCancel(context.Background())
|
||||
// go takeDebugScreenshots(debugScreenshotCtx, courtPage)
|
||||
|
||||
// // Defer done with debug screenshot
|
||||
// defer cancelDebugScreenshot()
|
||||
|
||||
time.Sleep(600 * time.Second)
|
||||
_ = page
|
||||
browser.MustClose()
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func ktmTrainLogin(browser *rod.Browser, username string, password string) *rod.Page {
|
||||
page := browser.MustPage("https://online.ktmb.com.my/Account/Login")
|
||||
page.MustElement("#Email").MustInput(username)
|
||||
page.MustElement("#Password").MustInput(password)
|
||||
page.MustElement("#LoginButton").MustClick()
|
||||
|
||||
return page
|
||||
}
|
||||
|
||||
func getBookingSlots(browser *rod.Browser, onwardDate string) *rod.Page {
|
||||
page := browser.MustPage("https://shuttleonline.ktmb.com.my/Home/Shuttle")
|
||||
page.MustWaitLoad()
|
||||
|
||||
// Dismiss system maintenance warning
|
||||
bodyText := page.MustElement("body").MustText()
|
||||
containsCheckStr := "System maintenance scheduled at 23:00 to 00:15"
|
||||
if strings.Contains(bodyText, containsCheckStr) {
|
||||
page.MustEval(`() => document.querySelector("#validationSummaryModal > div > div > div.modal-body > div > div.text-center > button").click()`)
|
||||
}
|
||||
|
||||
passengerCount := 1
|
||||
passengerCountStr := strconv.Itoa(passengerCount)
|
||||
requestVerificationToken := page.MustElement("#theForm > input[name=__RequestVerificationToken]").MustAttribute("value")
|
||||
|
||||
// Get JB Sentral Station Info
|
||||
jBSentralData := page.MustElement("#FromStationData").MustAttribute("value")
|
||||
jBSentralID := page.MustElement("#FromStationId").MustAttribute("value")
|
||||
|
||||
// Get Woodlands Station Info
|
||||
woodlandsData := page.MustElement("#ToStationData").MustAttribute("value")
|
||||
woodlandsID := page.MustElement("#ToStationId").MustAttribute("value")
|
||||
|
||||
sensitiveCustomForm(page, *woodlandsData, *jBSentralData, *woodlandsID, *jBSentralID, onwardDate, passengerCountStr, *requestVerificationToken)
|
||||
page.MustWaitLoad()
|
||||
|
||||
return page
|
||||
}
|
||||
|
||||
func selectBookingSlot(ctx context.Context, page *rod.Page, timeCode string) *rod.Page {
|
||||
time.Sleep(1 * time.Second)
|
||||
// Initial closing of maintenance modal
|
||||
bodyText := page.MustElement("body").MustText()
|
||||
if strings.Contains(bodyText, "System maintenance scheduled at 23:00 to 00:15 (UTC+8)") {
|
||||
closeModalButton := page.MustElement("#popupModalCloseButton")
|
||||
closeModalButton.Eval(`this.click()`)
|
||||
time.Sleep(1000 * time.Millisecond)
|
||||
}
|
||||
|
||||
// Start probing
|
||||
completed := false
|
||||
for !completed {
|
||||
page.MustWaitLoad()
|
||||
departTripsTable := page.MustElement(".depart-trips")
|
||||
departRows := departTripsTable.MustElements("tr")
|
||||
var rowElement *rod.Element
|
||||
for _, row := range departRows {
|
||||
timeCodeElement := row.MustAttribute("data-hourminute")
|
||||
if *timeCodeElement == timeCode {
|
||||
rowElement = row
|
||||
}
|
||||
}
|
||||
|
||||
// Checks for context before clicking
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return page
|
||||
default:
|
||||
}
|
||||
|
||||
selectButtonElement := rowElement.MustElement("a")
|
||||
selectButtonElement.Eval(`this.click()`)
|
||||
|
||||
time.Sleep(1000 * time.Millisecond)
|
||||
page.MustWaitLoad()
|
||||
|
||||
// Check before exiting
|
||||
bodyText := page.MustElement("body").MustText()
|
||||
if strings.Contains(bodyText, "System maintenance scheduled at 23:00 to 00:15 (UTC+8).") {
|
||||
completed = false
|
||||
closeModalButton := page.MustElement("#popupModalCloseButton")
|
||||
closeModalButton.Eval(`this.click()`)
|
||||
time.Sleep(1000 * time.Millisecond)
|
||||
} else {
|
||||
log.Println("Completed probing")
|
||||
completed = true
|
||||
}
|
||||
}
|
||||
|
||||
// Checks for context before clicking
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return page
|
||||
default:
|
||||
}
|
||||
|
||||
proceedButton := page.MustElement(".proceed-btn")
|
||||
proceedButton.Eval(`this.click()`)
|
||||
|
||||
return page
|
||||
}
|
||||
|
||||
func fillPassengerDetails(page *rod.Page, name string, gender string, passport string, passportExpiry string, contact string) *rod.Page {
|
||||
ticketType := "DEWASA/ADULT"
|
||||
|
||||
nameElement := page.MustElement(".FullName")
|
||||
nameElement.MustInput(name)
|
||||
|
||||
passportElement := page.MustElement(".PassportNo")
|
||||
passportElement.MustInput(passport)
|
||||
|
||||
passportExpiryElement := page.MustElement("#Passengers_0__PassportExpiryDate")
|
||||
passportExpiryElement.MustInput(passportExpiry)
|
||||
|
||||
contactElement := page.MustElement(".ContactNo")
|
||||
contactElement.MustInput(contact)
|
||||
|
||||
if gender == "M" {
|
||||
maleElement := page.MustElement("#Passengers_0__GenderMale")
|
||||
maleElement.Eval(`this.click()`)
|
||||
} else {
|
||||
|
||||
femaleElement := page.MustElement("#Passengers_0__GenderFemale")
|
||||
femaleElement.Eval(`this.click()`)
|
||||
}
|
||||
ticketTypeElement := page.MustElement("#Passengers_0__TicketTypeId")
|
||||
ticketTypeElement.MustSelect(ticketType)
|
||||
|
||||
paymentButton := page.MustElement("#btnConfirmPayment")
|
||||
paymentButton.Eval(`this.click()`)
|
||||
|
||||
page.MustWaitLoad()
|
||||
return page
|
||||
}
|
||||
|
||||
func choosePayment(page *rod.Page) *rod.Page {
|
||||
creditCardButton := page.MustElement(".btn-public-bank")
|
||||
creditCardButton.Eval(`this.click()`)
|
||||
page.MustWaitLoad()
|
||||
|
||||
paymentGatewayButton := page.MustElement("#PaymentGateway")
|
||||
paymentGatewayButton.Eval(`this.click()`)
|
||||
page.MustWaitLoad()
|
||||
|
||||
return page
|
||||
}
|
||||
|
||||
func makePayment(page *rod.Page, cardType string, creditCard string, expiryMonth string, expiryYear string, creditCardCVV string) *rod.Page {
|
||||
if cardType == "Visa" {
|
||||
visaRadio := page.MustElement("#card_type_001")
|
||||
visaRadio.Eval(`this.click()`)
|
||||
} else if cardType == "Mastercard" {
|
||||
masterRadio := page.MustElement("#card_type_002")
|
||||
masterRadio.Eval(`this.click()`)
|
||||
}
|
||||
|
||||
creditCardElement := page.MustElement("#card_number")
|
||||
creditCardElement.MustInput(creditCard)
|
||||
|
||||
expiryMonthElement := page.MustElement("#card_expiry_month")
|
||||
expiryMonthElement.MustSelect(expiryMonth)
|
||||
|
||||
expiryYearElement := page.MustElement("#card_expiry_year")
|
||||
expiryYearElement.MustSelect(expiryYear)
|
||||
|
||||
creditCardCVVElement := page.MustElement("#card_cvn")
|
||||
creditCardCVVElement.MustInput(creditCardCVV)
|
||||
|
||||
log.Println("Before payment")
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
payButton := page.MustElement(".pay_button")
|
||||
payButton.Eval(`this.click()`)
|
||||
|
||||
return page
|
||||
}
|
||||
|
||||
func sensitiveCustomForm(
|
||||
page *rod.Page,
|
||||
fromStationData string,
|
||||
toStationData string,
|
||||
fromStationID string,
|
||||
toStationID string,
|
||||
onwardDate string,
|
||||
passengerCount string,
|
||||
csrf string,
|
||||
) *rod.Page {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
log.Println("Recovered in sensitiveCustomForm", r)
|
||||
}
|
||||
}()
|
||||
|
||||
formHTML := fmt.Sprintf(`
|
||||
<form action="https://shuttleonline.ktmb.com.my/ShuttleTrip" method="POST">
|
||||
<input type="hidden" name="FromStationData" value="%s" />
|
||||
<input type="hidden" name="ToStationData" value="%s" />
|
||||
<input type="hidden" name="FromStationId" value="%s" />
|
||||
<input type="hidden" name="ToStationId" value="%s" />
|
||||
<input type="hidden" name="OnwardDate" value="%s" />
|
||||
<input type="hidden" name="ReturnDate" value="" />
|
||||
<input type="hidden" name="PassengerCount" value="%s" />
|
||||
<input type="hidden" name="__RequestVerificationToken" value="%s" />
|
||||
<input type="submit" id="presshere" value="Submit request" />
|
||||
</form>
|
||||
UniqueStringHere
|
||||
`, fromStationData, toStationData, fromStationID, toStationID, onwardDate, passengerCount, csrf)
|
||||
|
||||
page.MustElement("body").MustEval(fmt.Sprintf("() => this.innerHTML = `%s`", formHTML))
|
||||
// page.MustElement("#presshere").MustClick()
|
||||
page.MustEval(`() => document.querySelector("#presshere").click()`)
|
||||
|
||||
return page
|
||||
}
|
||||
82
backend/internal/ktmtrainbot/bookingcontroller.go
Normal file
82
backend/internal/ktmtrainbot/bookingcontroller.go
Normal file
@@ -0,0 +1,82 @@
|
||||
package ktmtrainbot
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"git.samuelpua.com/telboon/ktm-train-bot/backend/internal/user"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
func (env *Env) createBooking(
|
||||
user *user.User,
|
||||
travelDate time.Time,
|
||||
timeCode string,
|
||||
name string,
|
||||
gender string,
|
||||
passport string,
|
||||
passportExpiry time.Time,
|
||||
contact string,
|
||||
) (*Booking, error) {
|
||||
var newBooking Booking
|
||||
|
||||
newBooking.User = *user
|
||||
newBooking.TravelDate = travelDate
|
||||
newBooking.TimeCode = timeCode
|
||||
newBooking.Name = name
|
||||
newBooking.Gender = gender
|
||||
newBooking.Passport = passport
|
||||
newBooking.PassportExpiry = passportExpiry
|
||||
|
||||
newBooking.Status = "pending"
|
||||
|
||||
err := env.DB.Create(&newBooking).Error
|
||||
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return nil, errors.New("failed create new booking")
|
||||
}
|
||||
|
||||
return &newBooking, nil
|
||||
}
|
||||
|
||||
func (env *Env) getAllBooking(user *user.User) ([]Booking, error) {
|
||||
var booking []Booking
|
||||
err := env.DB.Where("user_id = ?", user.ID).Order("court_weekday asc").Find(&booking).Error
|
||||
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return nil, errors.New("failed get booking")
|
||||
}
|
||||
|
||||
return booking, nil
|
||||
}
|
||||
|
||||
func (env *Env) deleteBooking(
|
||||
user *user.User,
|
||||
bookingIDStr string,
|
||||
) (*Booking, error) {
|
||||
var newBooking Booking
|
||||
|
||||
bookingID, err := uuid.Parse(bookingIDStr)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return nil, errors.New("invalid uuid")
|
||||
}
|
||||
|
||||
err = env.DB.Where(&Booking{ID: bookingID}).Where("user_id = ?", user.ID).First(&newBooking).Error
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return nil, errors.New("failed retrieve booking")
|
||||
}
|
||||
|
||||
err = env.DB.Delete(&newBooking).Error
|
||||
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return nil, errors.New("failed to delete booking")
|
||||
}
|
||||
|
||||
return &newBooking, nil
|
||||
}
|
||||
92
backend/internal/ktmtrainbot/bookingmodel.go
Normal file
92
backend/internal/ktmtrainbot/bookingmodel.go
Normal file
@@ -0,0 +1,92 @@
|
||||
package ktmtrainbot
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"git.samuelpua.com/telboon/ktm-train-bot/backend/internal/user"
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type Booking struct {
|
||||
ID uuid.UUID `gorm:"primaryKey;type:uuid;default:uuid_generate_v4()"`
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
DeletedAt gorm.DeletedAt `gorm:"index"`
|
||||
User user.User
|
||||
UserID uuid.UUID
|
||||
TravelDate time.Time // Only date matters
|
||||
TimeCode string // e.g. 1400
|
||||
Name string
|
||||
Gender string // M/F
|
||||
Passport string
|
||||
PassportExpiry time.Time // Only date matters
|
||||
Contact string // +6512345678
|
||||
Status string // "success", "error", "pending", "running"
|
||||
}
|
||||
|
||||
type BookingCreateRequest struct {
|
||||
TravelDate time.Time `json:"travelDate" validate:"required"`
|
||||
TimeCode string `json:"timeCode" validate:"required len=4"`
|
||||
Name string `json:"name" validate:"required"`
|
||||
Gender string `json:"gender" validate:"required len=1 containsany=MF"`
|
||||
Passport string `json:"passport" validate:"required"`
|
||||
PassportExpiry time.Time `json:"passportExpiry" validate:"required"`
|
||||
Contact string `json:"contact" validate:"required e164"`
|
||||
}
|
||||
|
||||
type BookingResponse struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
TravelDate time.Time `json:"travelDate"`
|
||||
TimeCode string `json:"timeCode"`
|
||||
Name string `json:"name"`
|
||||
Gender string `json:"gender"`
|
||||
Passport string `json:"passport"`
|
||||
PassportExpiry time.Time `json:"passportExpiry"`
|
||||
Contact string `json:"contact"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
type BookingListResponse []BookingResponse
|
||||
|
||||
func (res *BookingResponse) Render(w http.ResponseWriter, r *http.Request) error {
|
||||
// Pre-processing before a response is marshalled and sent across the wire
|
||||
return nil
|
||||
}
|
||||
|
||||
func (res BookingListResponse) Render(w http.ResponseWriter, r *http.Request) error {
|
||||
// Pre-processing before a response is marshalled and sent across the wire
|
||||
// if res == nil {
|
||||
// var empty []BookingResponse
|
||||
// res = empty
|
||||
// }
|
||||
return nil
|
||||
}
|
||||
|
||||
func (env *Env) NewBookingResponse(model *Booking) *BookingResponse {
|
||||
res := &BookingResponse{
|
||||
ID: model.ID,
|
||||
TravelDate: model.TravelDate,
|
||||
TimeCode: model.TimeCode,
|
||||
Name: model.Name,
|
||||
Gender: model.Gender,
|
||||
Passport: model.Passport,
|
||||
PassportExpiry: model.PassportExpiry,
|
||||
Contact: model.Contact,
|
||||
Status: model.Status,
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
func (env *Env) NewBookingListResponse(model []Booking) BookingListResponse {
|
||||
var res []BookingResponse
|
||||
|
||||
for _, item := range model {
|
||||
curr := env.NewBookingResponse(&item)
|
||||
res = append(res, *curr)
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
124
backend/internal/ktmtrainbot/bookingroute.go
Normal file
124
backend/internal/ktmtrainbot/bookingroute.go
Normal file
@@ -0,0 +1,124 @@
|
||||
package ktmtrainbot
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"git.samuelpua.com/telboon/ktm-train-bot/backend/internal/common"
|
||||
"git.samuelpua.com/telboon/ktm-train-bot/backend/internal/user"
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/go-chi/render"
|
||||
"github.com/go-playground/validator/v10"
|
||||
)
|
||||
|
||||
// Get All Bookings
|
||||
// @Summary Get All Booking
|
||||
// @Description Description
|
||||
// @Tags ktmtrainbot Booking
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {object} []BookingResponse
|
||||
// @Failure 400 {object} common.ErrResponse
|
||||
// @Router /api/v1/ktmtrainbot/booking [get]
|
||||
func (env *Env) getBookingRoute(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
currUser, ok := ctx.Value(user.UserContextKey).(*user.User)
|
||||
if !ok {
|
||||
err := errors.New("user not logged in")
|
||||
render.Render(w, r, common.ErrInternalError(err))
|
||||
return
|
||||
}
|
||||
_ = currUser
|
||||
|
||||
booking, err := env.getAllBooking(currUser)
|
||||
|
||||
if err != nil {
|
||||
render.Render(w, r, common.ErrInvalidRequest(err))
|
||||
return
|
||||
}
|
||||
|
||||
render.Render(w, r, env.NewBookingListResponse(booking))
|
||||
}
|
||||
|
||||
// Create New Booking
|
||||
// @Summary Create New Booking
|
||||
// @Description Description
|
||||
// @Tags ktmtrainbot Booking
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param user body BookingCreateRequest true "Booking Create Request"
|
||||
// @Success 200 {object} BookingResponse
|
||||
// @Failure 400 {object} common.ErrResponse
|
||||
// @Router /api/v1/ktmtrainbot/booking [post]
|
||||
func (env *Env) createBookingRoute(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
currUser, ok := ctx.Value(user.UserContextKey).(*user.User)
|
||||
if !ok {
|
||||
err := errors.New("user not logged in")
|
||||
render.Render(w, r, common.ErrInternalError(err))
|
||||
return
|
||||
}
|
||||
_ = currUser
|
||||
|
||||
data := &BookingCreateRequest{}
|
||||
err := render.DecodeJSON(r.Body, data)
|
||||
if err != nil {
|
||||
render.Render(w, r, common.ErrInvalidRequest(err))
|
||||
return
|
||||
}
|
||||
err = validator.New().Struct(data)
|
||||
if err != nil {
|
||||
render.Render(w, r, common.ErrValidationError(err))
|
||||
return
|
||||
}
|
||||
|
||||
booking, err := env.createBooking(
|
||||
currUser,
|
||||
data.TravelDate,
|
||||
data.TimeCode,
|
||||
data.Name,
|
||||
data.Gender,
|
||||
data.Passport,
|
||||
data.PassportExpiry,
|
||||
data.Contact,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
render.Render(w, r, common.ErrInvalidRequest(err))
|
||||
return
|
||||
}
|
||||
|
||||
render.Render(w, r, env.NewBookingResponse(booking))
|
||||
}
|
||||
|
||||
// Delete booking
|
||||
// @Summary Delete booking
|
||||
// @Description Description
|
||||
// @Tags ktmtrainbot Booking
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param bookingID path string true "Booking ID"
|
||||
// @Success 200 {object} BookingResponse
|
||||
// @Failure 400 {object} common.ErrResponse
|
||||
// @Router /api/v1/ktmtrainbot/booking/{bookingID} [delete]
|
||||
func (env *Env) deleteBookingRoute(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
currUser, ok := ctx.Value(user.UserContextKey).(*user.User)
|
||||
if !ok {
|
||||
err := errors.New("user not logged in")
|
||||
render.Render(w, r, common.ErrInternalError(err))
|
||||
return
|
||||
}
|
||||
_ = currUser
|
||||
|
||||
bookingID := chi.URLParam(r, "bookingID")
|
||||
|
||||
booking, err := env.deleteBooking(currUser, bookingID)
|
||||
|
||||
if err != nil {
|
||||
render.Render(w, r, common.ErrInvalidRequest(err))
|
||||
return
|
||||
}
|
||||
|
||||
render.Render(w, r, env.NewBookingResponse(booking))
|
||||
}
|
||||
25
backend/internal/ktmtrainbot/getcurrenttime.go
Normal file
25
backend/internal/ktmtrainbot/getcurrenttime.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package ktmtrainbot
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/render"
|
||||
)
|
||||
|
||||
// Get Current Server Time
|
||||
// @Summary Get current server time
|
||||
// @Description Description
|
||||
// @Tags Info
|
||||
// @Produce json
|
||||
// @Success 200 {object} ServerTimeResponse
|
||||
// @Failure 400 {object} common.ErrResponse
|
||||
// @Router /api/v1/ktmtrainbot/current-time [get]
|
||||
func (env *Env) getCurrentTime(w http.ResponseWriter, r *http.Request) {
|
||||
timeNow := time.Now()
|
||||
|
||||
var res ServerTimeResponse
|
||||
res.ServerLocalTime = timeNow.In(time.Local).Format(time.RFC1123Z)
|
||||
|
||||
render.Render(w, r, &res)
|
||||
}
|
||||
31
backend/internal/ktmtrainbot/main.go
Normal file
31
backend/internal/ktmtrainbot/main.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package ktmtrainbot
|
||||
|
||||
import (
|
||||
"git.samuelpua.com/telboon/ktm-train-bot/backend/internal/user"
|
||||
"github.com/go-chi/chi"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type Env struct {
|
||||
DB *gorm.DB
|
||||
}
|
||||
|
||||
func KTMTrainBotRoutes(db *gorm.DB) chi.Router {
|
||||
var env Env
|
||||
env.DB = db
|
||||
|
||||
// Start running job
|
||||
go env.BackgroundJobRunner()
|
||||
|
||||
userEnv := user.NewUserEnv(db)
|
||||
|
||||
r := chi.NewRouter()
|
||||
checkLoggedInUserGroup := r.Group(nil)
|
||||
r.Get("/current-time", env.getCurrentTime)
|
||||
checkLoggedInUserGroup.Use(userEnv.CheckUserMiddleware)
|
||||
checkLoggedInUserGroup.Get("/booking", env.getBookingRoute)
|
||||
checkLoggedInUserGroup.Post("/booking", env.createBookingRoute)
|
||||
checkLoggedInUserGroup.Delete("/booking/{bookingID}", env.deleteBookingRoute)
|
||||
|
||||
return r
|
||||
}
|
||||
12
backend/internal/ktmtrainbot/servertimeresponsemodel.go
Normal file
12
backend/internal/ktmtrainbot/servertimeresponsemodel.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package ktmtrainbot
|
||||
|
||||
import "net/http"
|
||||
|
||||
type ServerTimeResponse struct {
|
||||
ServerLocalTime string `json:"serverLocalTime"`
|
||||
}
|
||||
|
||||
func (serverTimeResponse *ServerTimeResponse) Render(w http.ResponseWriter, r *http.Request) error {
|
||||
// Pre-processing before a response is marshalled and sent across the wire
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user