package main import ( "crypto/tls" "encoding/json" "fmt" "io/ioutil" "net/http" "net/url" "os" "path/filepath" "strconv" "strings" "text/tabwriter" "github.com/spf13/cobra" ) type Config struct { AccessToken string `json:"access_token"` APIUrl string `json:"api_url"` InsecureSkipVerify bool `json:"insecure_skip_verify"` } var configPath = filepath.Join(os.Getenv("HOME"), ".klog", "config.json") func saveConfig(cfg Config) error { data, err := json.MarshalIndent(cfg, "", " ") if err != nil { return err } os.MkdirAll(filepath.Dir(configPath), 0700) return ioutil.WriteFile(configPath, data, 0600) } func loadConfig() (Config, error) { var cfg Config data, err := ioutil.ReadFile(configPath) if err != nil { return cfg, err } err = json.Unmarshal(data, &cfg) return cfg, err } func buildAPIEndpoint(baseURL, method string) string { // ensure /api.php?method=... is appended correctly if strings.HasSuffix(baseURL, "/") { return fmt.Sprintf("%sapi.php?method=%s", baseURL, method) } return fmt.Sprintf("%s/api.php?method=%s", baseURL, method) } func postFormRequest(method string, formValues url.Values) error { cfg, err := loadConfig() if err != nil { return fmt.Errorf("failed to load config: %w", err) } endpoint := buildAPIEndpoint(cfg.APIUrl, method) req, err := http.NewRequest("POST", endpoint, strings.NewReader(formValues.Encode())) if err != nil { return err } req.Header.Set("Authorization", "Bearer "+cfg.AccessToken) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") client := &http.Client{ Transport: &http.Transport{ TLSClientConfig: &tls.Config{InsecureSkipVerify: cfg.InsecureSkipVerify}, }, } resp, err := client.Do(req) if err != nil { return err } defer resp.Body.Close() respBody, _ := ioutil.ReadAll(resp.Body) // Try to parse as JSON array first var arr []map[string]interface{} if err := json.Unmarshal(respBody, &arr); err == nil { if len(arr) == 0 { fmt.Println("(no data)") return nil } printTable(arr) return nil } // Try to parse as single object var obj map[string]interface{} if err := json.Unmarshal(respBody, &obj); err == nil { printTable([]map[string]interface{}{obj}) return nil } // fallback: print raw fmt.Println(string(respBody)) return nil } func printTable(data []map[string]interface{}) { w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) // print headers for k := range data[0] { fmt.Fprintf(w, "%s\t", k) } fmt.Fprintln(w) // print separator for range data[0] { fmt.Fprintf(w, "--------\t") } fmt.Fprintln(w) // print rows for _, row := range data { for _, v := range row { fmt.Fprintf(w, "%v\t", v) } fmt.Fprintln(w) } w.Flush() } func main() { var rootCmd = &cobra.Command{Use: "klog"} // config command var configCmd = &cobra.Command{Use: "config", Short: "Configure klog settings"} var configTokenCmd = &cobra.Command{ Use: "token [token]", Short: "Save access token", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { cfg, _ := loadConfig() cfg.AccessToken = args[0] return saveConfig(cfg) }, } var configURLCmd = &cobra.Command{ Use: "url [url]", Short: "Save API base URL", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { cfg, _ := loadConfig() cfg.APIUrl = args[0] return saveConfig(cfg) }, } var configInsecureCmd = &cobra.Command{ Use: "insecure [true|false]", Short: "Ignore self-signed certificates", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { cfg, _ := loadConfig() val := strings.ToLower(args[0]) cfg.InsecureSkipVerify = val == "true" || val == "1" || val == "yes" return saveConfig(cfg) }, } var configShowCmd = &cobra.Command{ Use: "show", Short: "Show current configuration", RunE: func(cmd *cobra.Command, args []string) error { cfg, err := loadConfig() if err != nil { return err } fmt.Printf("Access Token: %s\nAPI URL: %s\nIgnore Self-Signed Certs: %v\n", cfg.AccessToken, cfg.APIUrl, cfg.InsecureSkipVerify) return nil }, } configCmd.AddCommand(configTokenCmd, configURLCmd, configInsecureCmd, configShowCmd) // coffee command var cCmd = &cobra.Command{ Use: "c", Aliases: []string{"k", "coffee", "kaffee"}, Short: "Log a coffee", RunE: func(cmd *cobra.Command, args []string) error { return postFormRequest("coffee", nil) }, } var balanceCmd = &cobra.Command{ Use: "balance", Short: "Show my balance", RunE: func(cmd *cobra.Command, args []string) error { return postFormRequest("balance", nil) }, } // brew command var planTime int var notify bool var plan bool var brewCmd = &cobra.Command{ Use: "brew [waterIn]", Short: "Log a brew with parameters", Args: cobra.MaximumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { if len(args) == 0 { cmd.Help() return nil } form := url.Values{} form.Set("waterIn", args[0]) form.Set("notify", strconv.FormatBool(notify)) form.Set("plan", strconv.FormatBool(plan)) if plan { form.Set("planTime", strconv.Itoa(planTime)) } return postFormRequest("brew", form) }, } brewCmd.Flags().IntVar(&planTime, "planTime", 8, "Planning time in minutes (e.g. 4, 5, 6, 7, 8) (relevant only if plan=true)") brewCmd.Flags().BoolVar(¬ify, "notify", true, "Enable/disable notifications") brewCmd.Flags().BoolVar(&plan, "plan", true, "Enable/disable planning") rootCmd.AddCommand(configCmd, balanceCmd, cCmd, brewCmd) rootCmd.Execute() }