kaffeecli/main.go

238 lines
5.5 KiB
Go

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(&notify, "notify", true, "Enable/disable notifications")
brewCmd.Flags().BoolVar(&plan, "plan", true, "Enable/disable planning")
rootCmd.AddCommand(configCmd, balanceCmd, cCmd, brewCmd)
rootCmd.Execute()
}