From 5a68cbd0ca03df22526807535678b94282d870f7 Mon Sep 17 00:00:00 2001 From: Prem Kumar Date: Fri, 1 May 2020 03:01:06 +0530 Subject: [PATCH] cli: add basic cli --- Makefile | 6 ++ cmd/gncli/client/client.go | 163 +++++++++++++++++++++++++++++++++++++ cmd/gncli/cmd/login.go | 47 +++++++++++ cmd/gncli/cmd/root.go | 57 +++++++++++++ cmd/gncli/cmd/send.go | 39 +++++++++ cmd/gncli/config/config.go | 57 +++++++++++++ cmd/gncli/main.go | 7 ++ go.mod | 1 + go.sum | 2 + pkg/api/whatsapp.go | 2 +- 10 files changed, 380 insertions(+), 1 deletion(-) create mode 100644 cmd/gncli/client/client.go create mode 100644 cmd/gncli/cmd/login.go create mode 100644 cmd/gncli/cmd/root.go create mode 100644 cmd/gncli/cmd/send.go create mode 100644 cmd/gncli/config/config.go create mode 100644 cmd/gncli/main.go diff --git a/Makefile b/Makefile index 57479f8..fd355ab 100644 --- a/Makefile +++ b/Makefile @@ -70,6 +70,12 @@ build-static: assets @echo ">> building statically linked $(PROJECTNAME) binary" @CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o $(BUILD_DIR)/gonotify ./cmd/gonotify +.PHONY: build-cli +build-cli: ## Builds gncli - the GoNotify CLI client. +build-cli: + @echo ">> building gncli binary" + @go build -o $(BUILD_DIR)/gncli ./cmd/gncli + .PHONY: assets assets: ## Repacks all static assets into go file for easier deploy. assets: $(GOBINDATA) $(REACT_APP_OUTPUT_DIR) diff --git a/cmd/gncli/client/client.go b/cmd/gncli/client/client.go new file mode 100644 index 0000000..72c72a0 --- /dev/null +++ b/cmd/gncli/client/client.go @@ -0,0 +1,163 @@ +package client + +import ( + "bytes" + "encoding/json" + "errors" + "net/http" + "net/url" +) + +// Client represents the GoNotify API client +type Client struct { + base *url.URL + token string + hc *http.Client + ep struct { + login *url.URL + register *url.URL + send *url.URL + } +} + +// NewClient return an instance of Client +func NewClient(baseURL string, token string) (*Client, error) { + base, err := url.Parse(baseURL + "/api/v1/") + if err != nil { + return nil, err + } + + c := &Client{ + base: base, + token: token, + hc: &http.Client{}, + } + err = c.register() + if err != nil { + return nil, err + } + + return c, nil +} + +func (c *Client) register() error { + u, err := url.Parse("login") + if err != nil { + return err + } + c.ep.login = c.base.ResolveReference(u) + + u, err = url.Parse("register") + if err != nil { + return err + } + c.ep.register = c.base.ResolveReference(u) + + u, err = url.Parse("send") + if err != nil { + return err + } + c.ep.send = c.base.ResolveReference(u) + + return nil +} + +// Login returns a token given valid credentials +func (c *Client) Login(phone, password string) (string, error) { + var token string + + if phone == "" || password == "" { + return token, errors.New("Number and password cannot be empty") + } + + values := map[string]string{ + "phone": phone, + "password": password, + } + + v, err := json.Marshal(values) + if err != nil { + return token, err + } + + req, err := http.NewRequest("POST", c.ep.login.String(), bytes.NewBuffer(v)) + if err != nil { + return token, err + } + + req.Header.Set("Content-Type", "application/json") + + resp, err := c.hc.Do(req) + if err != nil { + return token, err + } + defer resp.Body.Close() + + res := map[string]string{} + + err = json.NewDecoder(resp.Body).Decode(&res) + if err != nil { + return token, err + } + + msg, ok := res["error"] + if ok { + return token, errors.New(msg) + } + + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + return token, errors.New("Request failed. Status: " + resp.Status) + } + + token = res["token"] + return token, nil +} + +// Send sends a message to given group +func (c *Client) Send(body, group string) error { + if c.token == "" { + return errors.New("You are not logged in") + } + + if body == "" { + return errors.New("Cannot send empty message") + } + + values := map[string]string{ + "body": body, + "group": group, + } + + v, err := json.Marshal(values) + if err != nil { + return err + } + + req, err := http.NewRequest("POST", c.ep.send.String(), bytes.NewBuffer(v)) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+c.token) + + resp, err := c.hc.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + res := map[string]string{} + + err = json.NewDecoder(resp.Body).Decode(&res) + if err != nil { + return err + } + + errMsg, ok := res["error"] + if ok { + return errors.New(errMsg) + } + + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + return errors.New("Request failed. Status: " + resp.Status) + } + + return nil +} diff --git a/cmd/gncli/cmd/login.go b/cmd/gncli/cmd/login.go new file mode 100644 index 0000000..f8b45b9 --- /dev/null +++ b/cmd/gncli/cmd/login.go @@ -0,0 +1,47 @@ +package cmd + +import ( + "fmt" + "os" + + "github.com/spf13/cobra" +) + +var loginCmd = &cobra.Command{ + Use: "login", + Short: "Login to GoNotify", + Long: "This subcommand lets you sign in into GoNotify", + Run: login, +} + +var number string +var password string + +func login(cmd *cobra.Command, args []string) { + conf := getConfig() + + c := getClient(conf.BaseURL, conf.Token) + + token, err := c.Login(number, password) + if err != nil { + fmt.Println(err) + os.Exit(2) + } + + conf.Token = token + conf.Phone = number + conf.Password = password + err = conf.Save() + if err != nil { + fmt.Println(err) + os.Exit(2) + } + + fmt.Println("Login successful") +} + +func init() { + loginCmd.Flags().StringVarP(&number, "number", "n", "", "Primary phone number of your account") + loginCmd.Flags().StringVarP(&password, "password", "p", "", "Password of your account") + rootCmd.AddCommand(loginCmd) +} diff --git a/cmd/gncli/cmd/root.go b/cmd/gncli/cmd/root.go new file mode 100644 index 0000000..6250ee5 --- /dev/null +++ b/cmd/gncli/cmd/root.go @@ -0,0 +1,57 @@ +package cmd + +import ( + "fmt" + "os" + "path" + + "github.com/prmsrswt/gonotify/cmd/gncli/client" + "github.com/prmsrswt/gonotify/cmd/gncli/config" + "github.com/spf13/cobra" +) + +var rootCmd = &cobra.Command{ + Use: "gncli", + Short: "gncli - a commanf-line client for GoNotify", + Version: "v0.1.0", + // Run: list, +} + +func getClient(baseURL, token string) *client.Client { + c, err := client.NewClient(baseURL, token) + if err != nil { + fmt.Println("Error initialising API client") + os.Exit(2) + } + + return c +} + +func getConfig() *config.Config { + homeDir, err := os.UserHomeDir() + if err != nil { + panic(err) + } + baseDir := path.Join(homeDir, ".gonotify") + + err = os.MkdirAll(baseDir, 0755) + if err != nil { + panic(err) + } + + c := &config.Config{Path: path.Join(baseDir, "config.json")} + + err = c.Load() + if err != nil { + panic(err) + } + + return c +} + +// Execute executes the root command +func Execute() { + if err := rootCmd.Execute(); err != nil { + os.Exit(1) + } +} diff --git a/cmd/gncli/cmd/send.go b/cmd/gncli/cmd/send.go new file mode 100644 index 0000000..33848d5 --- /dev/null +++ b/cmd/gncli/cmd/send.go @@ -0,0 +1,39 @@ +package cmd + +import ( + "fmt" + "os" + "strings" + + "github.com/spf13/cobra" +) + +var sendCmd = &cobra.Command{ + Use: "send", + Short: "Send message to a group", + Long: "This subcommand sends a message to the given group", + // Aliases: []string{"ls"}, + Run: send, +} + +var group string + +func send(cmd *cobra.Command, args []string) { + data := strings.Join(args, " ") + + conf := getConfig() + client := getClient(conf.BaseURL, conf.Token) + + err := client.Send(data, group) + if err != nil { + fmt.Println(err) + os.Exit(2) + } + + fmt.Println("Message sent successfully") +} + +func init() { + sendCmd.Flags().StringVarP(&group, "group", "g", "", "Name of group to send message") + rootCmd.AddCommand(sendCmd) +} diff --git a/cmd/gncli/config/config.go b/cmd/gncli/config/config.go new file mode 100644 index 0000000..e57df5e --- /dev/null +++ b/cmd/gncli/config/config.go @@ -0,0 +1,57 @@ +package config + +import ( + "encoding/json" + "io/ioutil" + "os" +) + +// Config represnts cli config +type Config struct { + Phone string `json:"phone"` + Password string `json:"password"` + Token string `json:"token"` + BaseURL string `json:"baseURL"` + Path string `json:"-"` +} + +// LoadDefault loads the default config +func (c *Config) LoadDefault() { + c.BaseURL = "https://gonotify.xyz" +} + +// Save saves the config at given path +func (c *Config) Save() error { + data, err := json.Marshal(c) + if err != nil { + return err + } + + return ioutil.WriteFile(c.Path, data, 0644) +} + +// Load loads the config from given path +func (c *Config) Load() error { + file, err := os.Open(c.Path) + if err != nil { + c.LoadDefault() + + err := c.Save() + if err != nil { + return err + } + + return nil + } + + data, err := ioutil.ReadAll(file) + if err != nil { + return err + } + + err = json.Unmarshal(data, c) + if err != nil { + return err + } + return nil +} diff --git a/cmd/gncli/main.go b/cmd/gncli/main.go new file mode 100644 index 0000000..9bdced2 --- /dev/null +++ b/cmd/gncli/main.go @@ -0,0 +1,7 @@ +package main + +import "github.com/prmsrswt/gonotify/cmd/gncli/cmd" + +func main() { + cmd.Execute() +} diff --git a/go.mod b/go.mod index 5207acb..93d0038 100644 --- a/go.mod +++ b/go.mod @@ -9,5 +9,6 @@ require ( github.com/ilyakaznacheev/cleanenv v1.2.2 github.com/mattn/go-sqlite3 v2.0.3+incompatible github.com/nyaruka/phonenumbers v1.0.55 + github.com/spf13/cobra v0.0.3 golang.org/x/crypto v0.0.0-20200423195118-18b771bd64f1 ) diff --git a/go.sum b/go.sum index 593e1e6..e840e94 100644 --- a/go.sum +++ b/go.sum @@ -249,7 +249,9 @@ github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1 github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= +github.com/spf13/cobra v0.0.3 h1:ZlrZ4XsMRm04Fr5pSFxBgfND2EBVa1nLpiy1stUsX/8= github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= +github.com/spf13/pflag v1.0.1 h1:aCvUg6QPl3ibpQUxyLkrEkCHtPqYJL4x9AuhqVqFis4= github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= diff --git a/pkg/api/whatsapp.go b/pkg/api/whatsapp.go index 708ec4b..efe75f6 100644 --- a/pkg/api/whatsapp.go +++ b/pkg/api/whatsapp.go @@ -29,7 +29,7 @@ func (api *API) handleWhatsApp(c *gin.Context) { logger := log.With(api.logger, "route", "whatsapp") type message struct { - Group string `json:"group" binding:"alpha"` + Group string `json:"group" binding:"-"` Body string `json:"body" binding:"required"` }