diff --git a/internal/cli/client/client.go b/internal/cli/client/client.go index 8fe5fe5a..bee55699 100644 --- a/internal/cli/client/client.go +++ b/internal/cli/client/client.go @@ -37,3 +37,14 @@ func SendRequestAndReadResponse(url *url.URL, enableAuth bool, method string, bo } return data, nil } + +func CheckConnection(url string) error { + _, err := http.Get(url) + if err != nil { + // fang/lipgloss lowk nukes this custom formatting + return fmt.Errorf("\x1b[1;37;41mUNABLE TO CONNECT\x1b[0m | %s\n\t↳ %v", + "Did you forget to start the server?", + err) + } + return nil +} diff --git a/internal/cli/events/delete.go b/internal/cli/events/delete.go index fbe6de2e..a859986d 100644 --- a/internal/cli/events/delete.go +++ b/internal/cli/events/delete.go @@ -2,16 +2,13 @@ package events import ( "fmt" - "io" "net/http" - "net/url" "os" - "github.com/charmbracelet/huh" "github.com/spf13/cobra" + "github.com/acmcsufoss/api.acmcsuf.com/internal/cli/client" "github.com/acmcsufoss/api.acmcsuf.com/internal/cli/config" - "github.com/acmcsufoss/api.acmcsuf.com/internal/cli/oauth" "github.com/acmcsufoss/api.acmcsuf.com/utils" ) @@ -20,32 +17,8 @@ var DeleteEvent = &cobra.Command{ Short: "Delete an event with its id", Run: func(cmd *cobra.Command, args []string) { - var uuidVal string - cmd.Flags().Set("id", uuidVal) - err := huh.NewForm().Run() - if err != nil { - if err == huh.ErrUserAborted { - fmt.Println("User canceled the form — exiting.") - } - fmt.Println("Uh oh:", err) - os.Exit(1) - } - err = huh.NewInput(). - Title("ACMCSUF-CLI Event Delete:"). - Description("Please enter the event's uuid:"). - Prompt("> "). - Value(&uuidVal). - Run() - if err != nil { - if err == huh.ErrUserAborted { - fmt.Println("User canceled the form — exiting.") - } - fmt.Println("Uh oh:", err) - os.Exit(1) - } - cmd.Flags().Set("id", uuidVal) - id, _ := cmd.Flags().GetString("id") - deleteEvent(id, config.Cfg) + uuid, _ := cmd.Flags().GetString("id") + deleteEvent(uuid, config.Cfg) }, } @@ -55,42 +28,14 @@ func init() { } func deleteEvent(id string, cfg *config.Config) { - baseURL := &url.URL{ - Scheme: "http", - Host: fmt.Sprintf("%s:%s", cfg.Host, cfg.Port), - } - if err := utils.CheckConnection(baseURL.JoinPath("/health").String()); err != nil { - fmt.Println(err) - return - } - - deleteURL := baseURL.JoinPath(fmt.Sprint("/v1/events/", id)) - - // ----- Delete Request ----- - request, err := oauth.NewRequestWithAuth(http.MethodDelete, deleteURL.String(), nil) - if err != nil { - fmt.Println("Error making delete request:", err) - return - } - - client := &http.Client{} - response, err := client.Do(request) - if err != nil { - fmt.Println("Error with delete response:", err) - return - } - defer response.Body.Close() + deleteURL := config.GetBaseURL(cfg).JoinPath("v1", "events", id) - // ----- Read Response Info ----- - if response.StatusCode != http.StatusOK { - fmt.Println("Response status:", response.Status) - return - } - - body, err := io.ReadAll(response.Body) - if err != nil { - fmt.Println("Error reading delete response body:", err) - return + if body, err := client.SendRequestAndReadResponse(deleteURL, true, http.MethodDelete, nil); err != nil { + fmt.Fprintln(os.Stderr, "Error:", err) + if body != nil { + utils.PrettyPrintJSONErr(body) + } + } else { + utils.PrettyPrintJSON(body) } - utils.PrettyPrintJSON(body) } diff --git a/internal/cli/events/events.go b/internal/cli/events/events.go deleted file mode 100644 index eafaf94b..00000000 --- a/internal/cli/events/events.go +++ /dev/null @@ -1,70 +0,0 @@ -package events - -import ( - "fmt" - "os" - - "github.com/charmbracelet/huh" - "github.com/spf13/cobra" -) - -type eventFlags struct { - uuid bool - location bool - startat bool - duration bool - isallday bool - host bool -} - -var CLIEvents = &cobra.Command{ - Use: "events HEADER", - Short: "A command to manage events.", -} - -func init() { - CLIEvents.AddCommand(PostEvent) - CLIEvents.AddCommand(GetEvent) - CLIEvents.AddCommand(PutEvents) - CLIEvents.AddCommand(DeleteEvent) -} - -func ShowMenu(backCallback func()) { - var eventState string - eventMenu := huh.NewForm( - huh.NewGroup( - huh.NewSelect[string](). - Title("ACMCSUF-CLI Event"). - Description("Choose an option to your heart's content."). - Options( - huh.NewOption("Delete", "delete"), - huh.NewOption("Get", "get"), - huh.NewOption("Post", "post"), - huh.NewOption("Put", "put"), - huh.NewOption("Back", "back"), - ). - Value(&eventState), - ), - ) - err := eventMenu.Run() - if err != nil { - fmt.Println("Uh oh:", err) - os.Exit(1) - } - - if eventState == "delete" { - DeleteEvent.Run(DeleteEvent, []string{}) - backCallback() - } else if eventState == "get" { - GetEvent.Run(GetEvent, []string{}) - backCallback() - } else if eventState == "post" { - PostEvent.Run(PostEvent, []string{}) - backCallback() - } else if eventState == "put" { - PutEvents.Run(PutEvents, []string{}) - backCallback() - } else if eventState == "back" { - backCallback() - } -} diff --git a/internal/cli/events/get.go b/internal/cli/events/get.go index 4583bd3c..c6a8af8a 100644 --- a/internal/cli/events/get.go +++ b/internal/cli/events/get.go @@ -1,67 +1,21 @@ package events import ( - "encoding/json" "fmt" "net/http" - "net/url" "os" - "github.com/acmcsufoss/api.acmcsuf.com/internal/api/dbmodels" + "github.com/acmcsufoss/api.acmcsuf.com/internal/cli/client" "github.com/acmcsufoss/api.acmcsuf.com/internal/cli/config" "github.com/acmcsufoss/api.acmcsuf.com/utils" - "github.com/charmbracelet/huh" "github.com/spf13/cobra" ) var GetEvent = &cobra.Command{ Use: "get", - Short: "Get events", + Short: "Get one or all events", Run: func(cmd *cobra.Command, args []string) { - blankUUID := "" - cmd.Flags().Set("id", blankUUID) - var flagsChosen []string - err := huh.NewForm( - huh.NewGroup( - huh.NewMultiSelect[string](). - //Ask the user what commands they want to use. - Title("ACMCSUF-CLI Event Get"). - Description("Choose a command(s). Note: Use spacebar to select and if done click enter.\nTo get all events, simply click enter."). - Options( - huh.NewOption("Get Specific ID", "id"), - ). - Value(&flagsChosen), - ), - ).Run() - if err != nil { - if err == huh.ErrUserAborted { - fmt.Println("User canceled the form — exiting.") - } - fmt.Println("Uh oh:", err) - os.Exit(1) - } - for _, flag := range flagsChosen { - var uuidVal string - switch flag { - case "id": - err = huh.NewInput(). - Title("ACMCSUF-CLI Event Get:"). - Description("Please enter the event's ID:"). - Prompt("> "). - Value(&uuidVal). - Run() - cmd.Flags().Set("id", uuidVal) - } - if err != nil { - if err == huh.ErrUserAborted { - fmt.Println("User canceled the form — exiting.") - } - fmt.Println("Uh oh:", err) - os.Exit(1) - } - } - // If these where global, unexpected behavior would be expected :( id, _ := cmd.Flags().GetString("id") getEvents(id, config.Cfg) }, @@ -72,56 +26,14 @@ func init() { } func getEvents(id string, cfg *config.Config) { - baseURL := &url.URL{ - Scheme: "http", - Host: fmt.Sprintf("%s:%s", cfg.Host, cfg.Port), - } - if err := utils.CheckConnection(baseURL.JoinPath("/health").String()); err != nil { - fmt.Println(err) - return - } + getUrl := config.GetBaseURL(cfg).JoinPath("v1", "events", id) - getURL := baseURL.JoinPath(fmt.Sprint("v1/events/", id)) - - // ----- Get ----- - req, err := http.NewRequest(http.MethodGet, getURL.String(), nil) - if err != nil { - fmt.Println("Error getting the request:", err) - return - } - - client := &http.Client{} - resp, err := client.Do(req) - if err != nil { - fmt.Fprintf(os.Stderr, "Error: couldn't make GET request: %v", err) - } - defer resp.Body.Close() - - // ----- Read Response Information ----- - if resp.StatusCode != http.StatusOK { - fmt.Println("Response status:", resp.Status) - return - } - - if id == "" { - var getPayload []dbmodels.CreateEventParams - err = json.NewDecoder(resp.Body).Decode(&getPayload) - if err != nil { - fmt.Println("Failed to read response body without id:", err) - return - } - - for i := range getPayload { - fmt.Println(utils.PrintStruct(getPayload[i])) + if body, err := client.SendRequestAndReadResponse(getUrl, false, http.MethodGet, nil); err != nil { + fmt.Fprintln(os.Stderr, "Error:", err) + if body != nil { + utils.PrettyPrintJSONErr(body) } } else { - var getPayload dbmodels.CreateEventParams - err = json.NewDecoder(resp.Body).Decode(&getPayload) - if err != nil { - fmt.Println("Failed to read response body with id:", err) - return - } - - fmt.Println(utils.PrintStruct(getPayload)) + utils.PrettyPrintJSON(body) } } diff --git a/internal/cli/events/post.go b/internal/cli/events/post.go index 31fd6d5f..c22030fb 100644 --- a/internal/cli/events/post.go +++ b/internal/cli/events/post.go @@ -1,389 +1,107 @@ package events import ( - "bufio" + "bytes" "encoding/json" "fmt" - "io" "net/http" - "net/url" "os" - "strings" "github.com/charmbracelet/huh" "github.com/spf13/cobra" - // TODO: db params shouldn't be exposed here "github.com/acmcsufoss/api.acmcsuf.com/internal/api/dbmodels" + "github.com/acmcsufoss/api.acmcsuf.com/internal/cli/client" "github.com/acmcsufoss/api.acmcsuf.com/internal/cli/config" - "github.com/acmcsufoss/api.acmcsuf.com/internal/cli/oauth" + "github.com/acmcsufoss/api.acmcsuf.com/internal/cli/forms" "github.com/acmcsufoss/api.acmcsuf.com/utils" ) var PostEvent = &cobra.Command{ Use: "post", - Short: "Post a new event.", + Short: "Post a new event", Run: func(cmd *cobra.Command, args []string) { - payload := dbmodels.CreateEventParams{} - err := huh.NewForm().Run() - if err != nil { - if err == huh.ErrUserAborted { - fmt.Println("User canceled the form — exiting.") - } - fmt.Println("Uh oh:", err) - os.Exit(1) - } - - payload.Uuid, _ = cmd.Flags().GetString("uuid") - payload.Location, _ = cmd.Flags().GetString("location") - startAtString, _ := cmd.Flags().GetString("startat") - duration, _ := cmd.Flags().GetString("duration") - payload.IsAllDay, _ = cmd.Flags().GetBool("isallday") - payload.Host, _ = cmd.Flags().GetString("host") - - if startAtString != "" { - var err error - payload.StartAt, err = utils.ByteSlicetoUnix([]byte(startAtString)) - if err != nil { - fmt.Println(err) - return - } - if duration != "" { - var err error - payload.EndAt, err = utils.TimeAfterDuration(payload.StartAt, duration) - if err != nil { - fmt.Println(err) - return - } - } - } - - if duration != "" && startAtString == "" { - fmt.Printf("--startat is required in order to use --duration") - } - - changedFlags := eventFlags{ - uuid: cmd.Flags().Lookup("uuid").Changed, - location: cmd.Flags().Lookup("location").Changed, - startat: cmd.Flags().Lookup("startat").Changed, - duration: cmd.Flags().Lookup("duration").Changed, - isallday: cmd.Flags().Lookup("isallday").Changed, - host: cmd.Flags().Lookup("host").Changed, - } - - postEvent(&payload, changedFlags, config.Cfg) + postEvent(config.Cfg) }, } -func init() { - PostEvent.Flags().StringP("uuid", "u", "", "Set uuid of new event") - PostEvent.Flags().StringP("location", "l", "", "Set location of new event") - PostEvent.Flags().StringP("startat", "s", "", "Set the start time of new event (Format: 03:04:05PM 01/02/06)") - PostEvent.Flags().StringP("duration", "d", "", "Set the duration of new event (Format: 03:04:05)") - PostEvent.Flags().StringP("host", "H", "", "Set host of new event") - PostEvent.Flags().BoolP("isallday", "a", false, "Set if new event is all day") -} +func postEvent(cfg *config.Config) { + postUrl := config.GetBaseURL(cfg).JoinPath("v1", "events") -func postEvent(payload *dbmodels.CreateEventParams, changedFlag eventFlags, cfg *config.Config) { - baseURL := &url.URL{ - Scheme: "http", - Host: fmt.Sprintf("%s:%s", cfg.Host, cfg.Port), - } - if err := utils.CheckConnection(baseURL.JoinPath("/health").String()); err != nil { - fmt.Println(err) + payload, err := postForm() + if err != nil { + fmt.Fprintln(os.Stderr, "Error:", err) return } - - // ----- Uuid ----- - for { - if changedFlag.uuid { - break - } - - var uuid string - err := huh.NewInput(). - Title("ACMCSUF-CLI Event Post:"). - Description("Please enter event's uuid:"). - Prompt("> "). - Value(&uuid). - Run() - if err != nil { - if err == huh.ErrUserAborted { - fmt.Println("User canceled the form — exiting.") - } - fmt.Println("Uh oh:", err) - os.Exit(1) - } - scanner := bufio.NewScanner(strings.NewReader(uuid)) - scanner.Scan() - if err := scanner.Err(); err != nil { - fmt.Println(err) - continue - } - - uuidBuffer := scanner.Bytes() - payload.Uuid = string(uuidBuffer) - break - } - - // ----- Location ----- - for { - if changedFlag.location { - break - } - - var location string - err := huh.NewInput(). - Title("ACMCSUF-CLI Event Post:"). - Description("Please enter the event's location:"). - Prompt("> "). - Value(&location). - Run() - if err != nil { - if err == huh.ErrUserAborted { - fmt.Println("User canceled the form — exiting.") - } - fmt.Println("Uh oh:", err) - os.Exit(1) - } - scanner := bufio.NewScanner(strings.NewReader(location)) - scanner.Scan() - if err := scanner.Err(); err != nil { - fmt.Println(err) - continue - } - - locationBuffer := scanner.Bytes() - payload.Location = string(locationBuffer) - break - } - - // ----- Start Time ----- - for { - - if changedFlag.startat { - break - } - - var timeStart string - err := huh.NewInput(). - Title("ACMCSUF-CLI Event Post:"). - Description("Please enter the start time of the event in the following format:\n [Month]/[Day]/[Year] [Hour]:[Minute][PM | AM]\nFor example: \x1b[93m01/02/06 03:04PM\x1b[0m"). - Prompt("> "). - Value(&timeStart). - Run() - if err != nil { - if err == huh.ErrUserAborted { - fmt.Println("User canceled the form — exiting.") - } - fmt.Println("Uh oh:", err) - os.Exit(1) - } - scanner := bufio.NewScanner(strings.NewReader(timeStart)) - scanner.Scan() - if err := scanner.Err(); err != nil { - fmt.Println("error reading start time:", err) - continue - } - startTimeBuffer := scanner.Bytes() - startTime, err := utils.ByteSlicetoUnix(startTimeBuffer) - if err != nil { - fmt.Println(err) - continue - } - - payload.StartAt = startTime - break - } - - // ----- End Time (Duration) ----- - for { - - if changedFlag.duration { - break - } - - var duration string - err := huh.NewInput(). - Title("ACMCSUF-CLI Event Post:"). - Description("Please enter the duration of the event in the following format:\n [Hour]:[Minute]\nFor example: \x1b[93m03:04\x1b[0m"). - Prompt("> "). - Value(&duration). - Run() - if err != nil { - if err == huh.ErrUserAborted { - fmt.Println("User canceled the form — exiting.") - } - fmt.Println("Uh oh:", err) - os.Exit(1) - } - scanner := bufio.NewScanner(strings.NewReader(duration)) - scanner.Scan() - if err := scanner.Err(); err != nil { - fmt.Println("error reading end time:", err) - continue - } - - endTimeBuffer := scanner.Bytes() - endTime, err := utils.TimeAfterDuration(payload.StartAt, string(endTimeBuffer)) - if err != nil { - fmt.Println(err) - continue - } - - payload.EndAt = endTime - break - } - - // ----- Is all day ----- - - for { - if changedFlag.isallday { - break - } - - var allDayYes string - err := huh.NewSelect[string](). - Title("ACMCSUF-CLI Event Post:"). - Description("Is your event all day?"). - Options( - huh.NewOption("Yes", "yes"), - huh.NewOption("No", "n"), - ). - Value(&allDayYes). - Run() - if err != nil { - if err == huh.ErrUserAborted { - fmt.Println("User canceled the form — exiting.") - } - fmt.Println("Uh oh:", err) - os.Exit(1) - } - scanner := bufio.NewScanner(strings.NewReader(allDayYes)) - scanner.Scan() - if err := scanner.Err(); err != nil { - fmt.Println(err) - continue - } - - isAllDayBuffer := scanner.Bytes() - - isAllDay, err := utils.YesOrNo(isAllDayBuffer, scanner) - if err != nil { - fmt.Println(err) - } - payload.IsAllDay = isAllDay - break - } - - // ----- Host ----- - for { - if changedFlag.host { - break - } - - var host string - err := huh.NewInput(). - Title("ACMCSUF-CLI Event Post:"). - Description("Please enter the event host"). - Prompt("> "). - Value(&host). - Run() - if err != nil { - if err == huh.ErrUserAborted { - fmt.Println("User canceled the form — exiting.") - } - fmt.Println("Uh oh:", err) - os.Exit(1) - } - scanner := bufio.NewScanner(strings.NewReader(host)) - scanner.Scan() - if err := scanner.Err(); err != nil { - fmt.Println(err) - continue - } - - hostBuffer := scanner.Bytes() - payload.Host = string(hostBuffer) - break + b, err := json.Marshal(payload) + if err != nil { + fmt.Fprintln(os.Stderr, "Error: failed to marshal data:", err) + return } - // ----- Confirmation ----- - for { - var option string - description := "Is your event data correct?\n" + utils.PrintStruct(payload) - err := huh.NewSelect[string](). - Title("ACMCSUF-CLI Event Post:"). - Description(description). - Options( - huh.NewOption("Yes", "yes"), - huh.NewOption("No", "n"), - ). - Value(&option). - Run() - if err != nil { - if err == huh.ErrUserAborted { - fmt.Println("User canceled the form — exiting.") - } - fmt.Println("Uh oh:", err) - os.Exit(1) - } - scanner := bufio.NewScanner(strings.NewReader(option)) - scanner.Scan() - if err := scanner.Err(); err != nil { - fmt.Println(err) - return - } - - confirmationBuffer := scanner.Bytes() - confirmationBool, err := utils.YesOrNo(confirmationBuffer, scanner) - if err != nil { - fmt.Println("error with reading confirmation:", err) - } - if !confirmationBool { - // Sorry :( - return - } else { - break + if body, err := client.SendRequestAndReadResponse(postUrl, true, http.MethodPost, + bytes.NewBuffer(b)); err != nil { + fmt.Fprintln(os.Stderr, "Error:", err) + if body != nil { + utils.PrettyPrintJSONErr(body) } - + } else { + utils.PrettyPrintJSON(body) } +} - // ----- Convert to Json ----- - jsonEvent, err := json.Marshal(*payload) - if err != nil { - fmt.Println(err) - return +func postForm() (*dbmodels.CreateEventParams, error) { + var payload dbmodels.CreateEventParams + var err error + var ( + startAtStr string + endAtStr string + ) + + form := huh.NewForm( + huh.NewGroup( + huh.NewInput(). + Title("Event ID"). + Value(&payload.Uuid). + Validate(forms.ValidateNonEmpty()), + huh.NewInput(). + Title("Location"). + Value(&payload.Location). + Validate(forms.ValidateNonEmpty()), + huh.NewInput(). + Title("Starts At\n"+ + "Format: \x1b[93mMM/DD/YY HH:MM[PM | AM]\x1b[0m\n"+ + "Example: \x1b[93m01/02/06 03:04PM\x1b[0m"). + Value(&startAtStr). + Validate(forms.ValidateNonEmpty()), + huh.NewInput(). + Title("Ends At\n"+ + "Format: \x1b[93mMM/DD/YY HH:MM[PM | AM]\x1b[0m\n"+ + "Example: \x1b[93m01/02/06 04:04PM\x1b[0m"). + Value(&endAtStr). + Validate(forms.ValidateNonEmpty()), + huh.NewConfirm(). + Title("All day event?"). + Value(&payload.IsAllDay), + huh.NewInput(). + Title("Host"). + Value(&payload.Host). + Validate(forms.ValidateNonEmpty()), + ), + ) + if err = form.Run(); err != nil { + return nil, err } - postURL := baseURL.JoinPath("v1/events") - - // ----- Post ----- - request, err := oauth.NewRequestWithAuth(http.MethodPost, postURL.String(), - strings.NewReader(string(jsonEvent))) + payload.StartAt, err = utils.ByteSlicetoUnix([]byte(startAtStr)) if err != nil { - fmt.Fprintf(os.Stderr, "Error creating post request: %v", err) + return nil, err } - - client := &http.Client{} - response, err := client.Do(request) + payload.EndAt, err = utils.ByteSlicetoUnix([]byte(endAtStr)) if err != nil { - fmt.Println("Failed to post event:", err) - return - } - defer response.Body.Close() - - // ----- Read Response Info ----- - if response.StatusCode != http.StatusOK { - fmt.Println("Response status", response.Status) - return + return nil, err } - body, err := io.ReadAll(response.Body) - if err != nil { - fmt.Println("Failed to read response body:", err) - return - } - utils.PrettyPrintJSON(body) + return &payload, nil } diff --git a/internal/cli/events/put.go b/internal/cli/events/put.go index 52dded39..50841d83 100644 --- a/internal/cli/events/put.go +++ b/internal/cli/events/put.go @@ -1,381 +1,126 @@ package events import ( - "bufio" "bytes" "encoding/json" "fmt" - "io" "net/http" - "net/url" "os" - "strconv" - "strings" "github.com/charmbracelet/huh" "github.com/spf13/cobra" "github.com/acmcsufoss/api.acmcsuf.com/internal/api/dbmodels" + "github.com/acmcsufoss/api.acmcsuf.com/internal/cli/client" "github.com/acmcsufoss/api.acmcsuf.com/internal/cli/config" - "github.com/acmcsufoss/api.acmcsuf.com/internal/cli/oauth" + "github.com/acmcsufoss/api.acmcsuf.com/internal/cli/forms" "github.com/acmcsufoss/api.acmcsuf.com/utils" ) var PutEvents = &cobra.Command{ - Use: "put", - Short: "Used to update an event", + Use: "put --id ", + Short: "Update an existing event by its id", Run: func(cmd *cobra.Command, args []string) { - var uuidVal string - cmd.Flags().Set("id", uuidVal) - err := huh.NewForm().Run() - if err != nil { - if err == huh.ErrUserAborted { - fmt.Println("User canceled the form — exiting.") - } - fmt.Println("Uh oh:", err) - os.Exit(1) - } - err = huh.NewInput(). - Title("ACMCSUF-CLI Event Put:"). - Description("Please enter the event's ID:"). - Prompt("> "). - Value(&uuidVal). - Run() - if err != nil { - if err == huh.ErrUserAborted { - fmt.Println("User canceled the form — exiting.") - } - fmt.Println("Uh oh:", err) - os.Exit(1) - } - cmd.Flags().Set("id", uuidVal) - - // CLI for url id, _ := cmd.Flags().GetString("id") - - payload := dbmodels.CreateEventParams{} - payload.Uuid, _ = cmd.Flags().GetString("uuid") - payload.Location, _ = cmd.Flags().GetString("location") - startAtString, _ := cmd.Flags().GetString("startat") - durationString, _ := cmd.Flags().GetString("duration") - payload.IsAllDay, _ = cmd.Flags().GetBool("allday") - payload.Host, _ = cmd.Flags().GetString("host") - - if startAtString != "" { - var err error - payload.StartAt, err = utils.ByteSlicetoUnix([]byte(startAtString)) - if err != nil { - fmt.Println(err) - return - } - if durationString != "" { - var err error - payload.EndAt, err = utils.TimeAfterDuration(payload.StartAt, durationString) - if err != nil { - fmt.Println(err) - return - } - } - } - - changedFlags := eventFlags{ - uuid: cmd.Flags().Lookup("uuid").Changed, - location: cmd.Flags().Lookup("location").Changed, - startat: cmd.Flags().Lookup("startat").Changed, - duration: cmd.Flags().Lookup("duration").Changed, - isallday: cmd.Flags().Lookup("isallday").Changed, - host: cmd.Flags().Lookup("host").Changed, - } - - updateEvent(id, &payload, changedFlags, config.Cfg) + putEvents(id, config.Cfg) }, } func init() { - // URL Flags - PutEvents.Flags().String("id", "", "Event to update") - PutEvents.Flags().String("urlhost", "127.0.0.1", "Custom host") - PutEvents.Flags().String("port", "8080", "Custom port") - - // Payload flags - PutEvents.Flags().StringP("uuid", "u", "", "Set uuid of new event") - PutEvents.Flags().StringP("location", "l", "", "Set location of new event") - PutEvents.Flags().StringP("startat", "s", "", "Set the start time of new event (Format: 03:04:05PM 01/02/06)") - PutEvents.Flags().StringP("duration", "d", "", "Set the end time of new event (Format: 03:04:05)") - PutEvents.Flags().StringP("host", "H", "", "Set host of new event") - PutEvents.Flags().BoolP("isallday", "a", false, "Set if new event is all day") - - // This flag is neccessary + PutEvents.Flags().String("id", "", "ID of the event to update") PutEvents.MarkFlagRequired("id") } -func updateEvent(id string, payload *dbmodels.CreateEventParams, changedFlags eventFlags, cfg *config.Config) { - baseURL := &url.URL{ - Scheme: "http", - Host: fmt.Sprintf("%s:%s", cfg.Host, cfg.Port), - } - - if err := utils.CheckConnection(baseURL.JoinPath("/health").String()); err != nil { - fmt.Println(err) - return - } - - if id == "" { - fmt.Println("Event id required for put!") - return - } - - // ----- Retrieve payload ----- - client := &http.Client{} - - retrievalURL := baseURL.JoinPath(fmt.Sprint("v1/events/", id)) - getReq, err := oauth.NewRequestWithAuth(http.MethodGet, retrievalURL.String(), nil) - if err != nil { - fmt.Printf("Error retrieving %s: %s", id, err) - return - } - - getRes, err := client.Do(getReq) - if err != nil { - fmt.Println("Error getting request:", err) - return - } - defer getRes.Body.Close() - - if getRes.StatusCode != http.StatusOK { - fmt.Println("get response status:", getRes.Status) - return - } - - body, err := io.ReadAll(getRes.Body) - if err != nil { - fmt.Println("Error reading response body:", err) - return - } - - var oldpayload dbmodels.CreateEventParams - err = json.Unmarshal(body, &oldpayload) - if err != nil { - fmt.Println("error unmarshalling previous event data:", err) - return - } - - scanner := bufio.NewScanner(os.Stdin) - - // ----- Change the event ----- - // Note: We want to PUT the payload, not old payload - // payload values are empty if user did not input a value in the command line - - // ----- uuid ----- - for { - if payload.Uuid == "" { - changeTheEventUuid, err := utils.ChangePrompt("uuid", oldpayload.Uuid, scanner, "event") - if err != nil { - fmt.Println(err) // Custom errors in changePrompt() - continue - } - - if changeTheEventUuid != nil { - payload.Uuid = string(changeTheEventUuid) - } else { - payload.Uuid = oldpayload.Uuid - } - } - break - } - - // ----- Location ----- - for { - if changedFlags.location { - break - } - changeTheEventLocation, err := utils.ChangePrompt("location", oldpayload.Location, scanner, "event") - if err != nil { - fmt.Println(err) - continue - } - - if changeTheEventLocation != nil { - payload.Location = string(changeTheEventLocation) - } else { - payload.Location = oldpayload.Location - } - break - } - - // ----- Start time ----- - for { - if changedFlags.startat { - break - } - changeTheEventStartAt, err := utils.ChangePrompt("start time (format: 01/02/06 03:04PM)", utils.FormatUnix(oldpayload.StartAt), scanner, "event") - if err != nil { - fmt.Println(err) - continue - } +func putEvents(id string, cfg *config.Config) { + resourceURL := config.GetBaseURL(cfg).JoinPath("v1", "events", id) - if changeTheEventStartAt != nil { - payload.StartAt, err = utils.ByteSlicetoUnix(changeTheEventStartAt) - if err != nil { - fmt.Println("Error with reading start integer:", err) - continue - } - } else { - payload.StartAt = oldpayload.StartAt + var oldPayload dbmodels.CreateEventParams + if body, err := client.SendRequestAndReadResponse(resourceURL, false, http.MethodGet, nil); err != nil { + fmt.Fprintln(os.Stderr, "Error:", err) + if body != nil { + utils.PrettyPrintJSONErr(body) } - break + return + } else { + err = json.Unmarshal(body, &oldPayload) + cobra.CheckErr(err) } - // ----- End time (Duration) ----- - for { - if changedFlags.duration { - break - } - changeTheEventEndAt, err := utils.ChangePrompt("end time (format: 01/02/06 03:04 )", utils.FormatUnix(oldpayload.EndAt), scanner, "event") - if err != nil { - fmt.Println(err) - continue - } + newPayload, err := putForm(&oldPayload) + cobra.CheckErr(err) + b, err := json.Marshal(newPayload) + cobra.CheckErr(err) - if changeTheEventEndAt != nil { - payload.EndAt, err = utils.ByteSlicetoUnix(changeTheEventEndAt) - if err != nil { - fmt.Println("Error with reading end integer:", err) - continue - } - } else { - payload.EndAt = oldpayload.EndAt + if body, err := client.SendRequestAndReadResponse(resourceURL, true, http.MethodPut, + bytes.NewBuffer(b)); err != nil { + fmt.Fprintln(os.Stderr, "Error:", err) + if body != nil { + utils.PrettyPrintJSONErr(body) } - break + } else { + utils.PrettyPrintJSON(body) } +} - // ----- All day ----- - // This is kind of awkward but I don't know have a workaround at the moment - for { - if changedFlags.isallday { - break - } - - changeTheEventAllDay, err := utils.ChangePrompt("all day status", strconv.FormatBool(oldpayload.IsAllDay), scanner, "event") +func putForm(oldPayload *dbmodels.CreateEventParams) (*dbmodels.UpdateEventParams, error) { + var payload dbmodels.UpdateEventParams + var err error + var ( + locationStr string = oldPayload.Location + startAtStr string + endAtStr string + isAllDay bool = oldPayload.IsAllDay + hostStr string = oldPayload.Host + ) + + form := huh.NewForm( + huh.NewGroup( + huh.NewInput(). + Title("Location"). + Value(&locationStr). + Validate(forms.ValidateNonEmpty()), + huh.NewInput(). + Title("Starts At\n"+ + "Format: \x1b[93mMM/DD/YY HH:MM[PM | AM]\x1b[0m\n"+ + "Leave empty to keep existing value"). + Value(&startAtStr), + huh.NewInput(). + Title("Ends At\n"+ + "Format: \x1b[93mMM/DD/YY HH:MM[PM | AM]\x1b[0m\n"+ + "Leave empty to keep existing value"). + Value(&endAtStr), + huh.NewConfirm(). + Title("All day event?"). + Value(&isAllDay), + huh.NewInput(). + Title("Host"). + Value(&hostStr). + Validate(forms.ValidateNonEmpty()), + ), + ) + if err = form.Run(); err != nil { + return nil, err + } + + payload.Uuid = oldPayload.Uuid + payload.Location = utils.StringtoNullString(locationStr) + payload.IsAllDay = utils.BooltoNullBool(isAllDay) + payload.Host = utils.StringtoNullString(hostStr) + if startAtStr != "" { + timestamp, err := utils.ByteSlicetoUnix([]byte(startAtStr)) if err != nil { - fmt.Println(err) - continue - } - - if changeTheEventAllDay != nil { - newAllDayBuffer := scanner.Bytes() - payload.IsAllDay, err = utils.YesOrNo(newAllDayBuffer, scanner) - if err != nil { - fmt.Println(err) - continue - } - } else { - payload.IsAllDay = oldpayload.IsAllDay + return nil, err } - break + payload.StartAt = utils.Int64toNullInt64(timestamp) } - - // ----- Host ----- - for { - if changedFlags.host { - break - } - changeTheEventHost, err := utils.ChangePrompt("host", oldpayload.Host, scanner, "event") + if endAtStr != "" { + timestamp, err := utils.ByteSlicetoUnix([]byte(endAtStr)) if err != nil { - fmt.Println(err) - continue - } - - if changeTheEventHost != nil { - payload.Host = string(changeTheEventHost) - } else { - payload.Host = oldpayload.Host + return nil, err } - break - } - - // ----- PUT the payload ----- - - updatePayload := dbmodels.UpdateEventParams{ - Uuid: payload.Uuid, - Location: utils.StringtoNullString(payload.Location), - StartAt: utils.Int64toNullInt64(payload.StartAt), - EndAt: utils.Int64toNullInt64(payload.EndAt), - IsAllDay: utils.BooltoNullBool(payload.IsAllDay), - Host: utils.StringtoNullString(payload.Host), + payload.EndAt = utils.Int64toNullInt64(timestamp) } - // Confirmation - for { - var option string - description := "Is your event data correct?\n" + utils.PrintStruct(payload) - err := huh.NewSelect[string](). - Title("ACMCSUF-CLI Event Put:"). - Description(description). - Options( - huh.NewOption("Yes", "yes"), - huh.NewOption("No", "n"), - ). - Value(&option). - Run() - if err != nil { - if err == huh.ErrUserAborted { - fmt.Println("User canceled the form — exiting.") - } - fmt.Println("Uh oh:", err) - os.Exit(1) - } - scanner := bufio.NewScanner(strings.NewReader(option)) - scanner.Scan() - if err := scanner.Err(); err != nil { - fmt.Println("error scanning confirmation:", err) - continue - } - - confirmationBuffer := scanner.Bytes() - confirmation, err := utils.YesOrNo(confirmationBuffer, scanner) - if err != nil { - fmt.Println(err) - continue - } - - if !confirmation { - return - } - - break - } - - // ----- Put the Payload ----- - newPayload, err := json.Marshal(updatePayload) - if err != nil { - fmt.Println("Error marshaling data:", err) - return - } - - request, err := oauth.NewRequestWithAuth(http.MethodPut, retrievalURL.String(), bytes.NewBuffer(newPayload)) - if err != nil { - fmt.Println("Problem with PUT:", err) - return - } - - putResponse, err := client.Do(request) - if err != nil { - fmt.Println("Error with response:", err) - return - } - defer putResponse.Body.Close() - - if putResponse.StatusCode != http.StatusOK { - fmt.Println("put response status:", putResponse.Status) - return - } - - body, err = io.ReadAll(putResponse.Body) - if err != nil { - fmt.Println("Error with body:", err) - return - } - utils.PrettyPrintJSON(body) + return &payload, nil } diff --git a/internal/cli/events/root.go b/internal/cli/events/root.go new file mode 100644 index 00000000..3ed4e088 --- /dev/null +++ b/internal/cli/events/root.go @@ -0,0 +1,17 @@ +package events + +import ( + "github.com/spf13/cobra" +) + +var CLIEvents = &cobra.Command{ + Use: "events", + Short: "Manage ACM CSUF's events", +} + +func init() { + CLIEvents.AddCommand(PostEvent) + CLIEvents.AddCommand(GetEvent) + CLIEvents.AddCommand(PutEvents) + CLIEvents.AddCommand(DeleteEvent) +} diff --git a/internal/cli/root.go b/internal/cli/root.go index bdc80e0a..91f72fc8 100644 --- a/internal/cli/root.go +++ b/internal/cli/root.go @@ -10,6 +10,7 @@ import ( "github.com/spf13/cobra" "github.com/acmcsufoss/api.acmcsuf.com/internal/cli/announcements" + "github.com/acmcsufoss/api.acmcsuf.com/internal/cli/client" "github.com/acmcsufoss/api.acmcsuf.com/internal/cli/config" "github.com/acmcsufoss/api.acmcsuf.com/internal/cli/events" "github.com/acmcsufoss/api.acmcsuf.com/internal/cli/officers" @@ -66,7 +67,9 @@ func init() { if err != nil { return fmt.Errorf("failed to load config: %w", err) } - return nil + + url := config.GetBaseURL(config.Cfg).JoinPath("health") + return client.CheckConnection(url.String()) } } @@ -76,7 +79,6 @@ func Execute() exitCode { log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile) if err := fang.Execute(context.Background(), rootCmd, fang.WithVersion(Version)); err != nil { - log.Println("Error:", err) return exitError } diff --git a/utils/cli_helper.go b/utils/cli_helper.go deleted file mode 100644 index b3a91dea..00000000 --- a/utils/cli_helper.go +++ /dev/null @@ -1,222 +0,0 @@ -package utils - -import ( - "bufio" - "database/sql" - "fmt" - "math" - "net/http" - "os" - "reflect" - "regexp" - "strconv" - "strings" - "time" - - "github.com/charmbracelet/huh" -) - -// Reoccuring functions for CLI files - -// --------------------------- Printing to terminal --------------------------- - -// Returns a byte slice, if nil, no changes shall be made. Else, if a byte slice were to return, change the payload value -func ChangePrompt(dataToBeChanged string, currentData string, scanner *bufio.Scanner, entity string) ([]byte, error) { - var option string - question := fmt.Sprintf("Would you like to change this %s's \x1b[1m%s\x1b[0m?\nCurrent %s's %s: \x1b[93m%s\x1b[0m\n", entity, dataToBeChanged, entity, dataToBeChanged, currentData) - err := huh.NewSelect[string](). - Title("ACMCSUF-CLI Put:"). - Description(question). - Options( - huh.NewOption("Yes", "yes"), - huh.NewOption("No", "n"), - ). - Value(&option). - Run() - if err != nil { - if err == huh.ErrUserAborted { - fmt.Println("User canceled the form — exiting.") - } - fmt.Println("Uh oh:", err) - os.Exit(1) - } - if option == "yes" { - var input string - newInputText := fmt.Sprintf("Please enter a new \x1b[1m%s\x1b[0m for the %s:\n", dataToBeChanged, entity) - err := huh.NewInput(). - Title("ACMCSUF-CLI Put:"). - Description(newInputText). - Prompt("> "). - Value(&input). - Run() - if err != nil { - if err == huh.ErrUserAborted { - fmt.Println("User canceled the form — exiting.") - } - fmt.Println("Uh oh:", err) - os.Exit(1) - } - scanner := bufio.NewScanner(strings.NewReader(input)) - scanner.Scan() - if err := scanner.Err(); err != nil { - return nil, fmt.Errorf("error reading new %s: %s", dataToBeChanged, err) - } - return scanner.Bytes(), nil - } else { - return nil, nil - } -} - -// For unix times of int64 to readable format of 03:04:05PM 01/02/06 -func FormatUnix(unixTime int64) string { - t := time.Unix(unixTime, 0) - return t.Format("01/02/06 03:04PM") -} - -// Print the struct passed into it in a nice display in the terminal -func PrintStruct(s any) string { - params := "" - val := reflect.ValueOf(s) - typ := reflect.TypeOf(s) - - // Incase a pointer to a struct was passed - if val.Kind() == reflect.Ptr { - val = val.Elem() - typ = typ.Elem() - } - - if val.Kind() != reflect.Struct { - fmt.Println("error, not a struct") - return "" - } - - for i := 0; i < val.NumField(); i++ { - v := val.Field(i) - t := typ.Field(i) - display := "" - - // If a value is still in interface, such as CreateEventParams.StartAt (as of writing this) unwrap it. - if v.Kind() == reflect.Interface && !v.IsNil() { - v = v.Elem() - } - if v.Kind() == reflect.Ptr && !v.IsNil() { - v = v.Elem() - } - - switch v.Type() { - case reflect.TypeOf(sql.NullInt64{}): - n := v.Interface().(sql.NullInt64) - if n.Valid { - display = FormatUnix(n.Int64) - } else { - display = "NULL" - } - - case reflect.TypeOf(sql.NullString{}): - n := v.Interface().(sql.NullString) - if n.Valid { - display = n.String - } else { - display = "NULL" - } - - case reflect.TypeOf(sql.NullBool{}): - n := v.Interface().(sql.NullBool) - if n.Valid { - display = strconv.FormatBool(n.Bool) - } else { - display = "NULL" - } - - default: - switch v.Kind() { - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - display = FormatUnix(v.Int()) - - case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: - u := v.Uint() - if u <= math.MaxInt64 { - display = FormatUnix(int64(u)) - } else { - display = fmt.Sprintf("%v", u) - } - - default: - if v.CanInterface() { - display = fmt.Sprintf("%v", v.Interface()) - } else { - display = "" - } - } - } - - params += fmt.Sprintf("\033[94m\t%-20s\033[0m| \033[92m%s\033[0m\n", t.Name, display) - } - params += "------------------------------------------------------------------" - - return params -} - -func CheckConnection(url string) error { - _, err := http.Get(url) - if err != nil { - return fmt.Errorf("\x1b[1;37;41mUNABLE TO CONNECT\x1b[0m | %s\n\t↳ %v", - "Did you forget to start the server?", - err) - } - return nil -} - -// --------------------------- Getting values from input --------------------------- - -// YesOrNo checks the user input for a yes or no response. -func YesOrNo(userInput []byte, scanner *bufio.Scanner) (bool, error) { - userInputString := strings.ToUpper(string(userInput)) - - switch userInputString { - case "YES", "Y", "TRUE": - return true, nil - case "NO", "N", "FALSE": - return false, nil - default: - fmt.Println("Invalid input, please try again.") - scanner.Scan() - if err := scanner.Err(); err != nil { - return false, fmt.Errorf("error scanning new input: %s", err) - } - return YesOrNo(scanner.Bytes(), scanner) - } -} - -func TimeAfterDuration(startTime int64, duration string) (int64, error) { - startUnix := time.Unix(startTime, 0) - - // Since I cant go from 03:04:05 -> time.Time directly, I am left doing some... parsing - re := regexp.MustCompile(`(\d{2}):(\d{2})`) - parsedDuration := re.FindStringSubmatch(duration) - - if parsedDuration == nil { - return -1, fmt.Errorf("error, duration time must be in the format: 03:04") - } - - durHour := parsedDuration[1] - durMin := parsedDuration[2] - - // fmt.Println("Parsed times:", durHour, durMin, durSec) - intDurHour, err := strconv.Atoi(durHour) - if err != nil { - return -1, fmt.Errorf("error converting hour to int: %s", err) - } - - intDurMin, err := strconv.Atoi(durMin) - if err != nil { - return -1, fmt.Errorf("error converting minute to int: %s", err) - } - - totalDuration := startUnix.Add( - time.Duration(intDurHour)*time.Hour + - time.Duration(intDurMin)*time.Minute, - ) - - return totalDuration.Unix(), nil -} diff --git a/utils/time.go b/utils/time.go new file mode 100644 index 00000000..d41b2c3a --- /dev/null +++ b/utils/time.go @@ -0,0 +1,47 @@ +package utils + +import ( + "fmt" + "regexp" + "strconv" + "time" +) + +// For unix times of int64 to readable format of 03:04:05PM 01/02/06 +func FormatUnix(unixTime int64) string { + t := time.Unix(unixTime, 0) + return t.Format("01/02/06 03:04PM") +} + +func TimeAfterDuration(startTime int64, duration string) (int64, error) { + startUnix := time.Unix(startTime, 0) + + // Since I cant go from 03:04:05 -> time.Time directly, I am left doing some... parsing + re := regexp.MustCompile(`(\d{2}):(\d{2})`) + parsedDuration := re.FindStringSubmatch(duration) + + if parsedDuration == nil { + return -1, fmt.Errorf("error, duration time must be in the format: 03:04") + } + + durHour := parsedDuration[1] + durMin := parsedDuration[2] + + // fmt.Println("Parsed times:", durHour, durMin, durSec) + intDurHour, err := strconv.Atoi(durHour) + if err != nil { + return -1, fmt.Errorf("error converting hour to int: %s", err) + } + + intDurMin, err := strconv.Atoi(durMin) + if err != nil { + return -1, fmt.Errorf("error converting minute to int: %s", err) + } + + totalDuration := startUnix.Add( + time.Duration(intDurHour)*time.Hour + + time.Duration(intDurMin)*time.Minute, + ) + + return totalDuration.Unix(), nil +}