From 1baaebb91591f3cdb66c01872c4bb25a115885fb Mon Sep 17 00:00:00 2001 From: puneeth8994 Date: Tue, 11 May 2021 21:43:02 +0530 Subject: [PATCH] Adds support to zulip --- go.mod | 3 +- main.go | 3 + service/zulip/zulip.go | 178 +++++++++++++++++++++++++++++++++++ service/zulip/zulip_test.go | 181 ++++++++++++++++++++++++++++++++++++ 4 files changed, 364 insertions(+), 1 deletion(-) create mode 100644 service/zulip/zulip.go create mode 100644 service/zulip/zulip_test.go diff --git a/go.mod b/go.mod index 5bb0762..c2608c6 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/gregdel/pushover v0.0.0-20210216095829-2131362cb888 github.com/nikoksr/notify v0.15.0 github.com/russross/blackfriday/v2 v2.1.0 // indirect - github.com/sfreiberg/gotwilio v0.0.0-20201211181435-c426a3710ab5 // indirect + github.com/sfreiberg/gotwilio v0.0.0-20201211181435-c426a3710ab5 + github.com/stretchr/testify v1.7.0 github.com/urfave/cli/v2 v2.3.0 ) diff --git a/main.go b/main.go index 3a79a81..3054f4c 100644 --- a/main.go +++ b/main.go @@ -4,6 +4,8 @@ import ( "log" "os" + "github.com/kha7iq/pingme/service/zulip" + "github.com/kha7iq/pingme/service/twillio" "github.com/kha7iq/pingme/service/discord" @@ -44,6 +46,7 @@ RocketChat, Discord, Pushover, Mattermost, Pushbullet, Microsoft Teams and email mattermost.Send(), pushbullet.Send(), twillio.Send(), + zulip.Send(), } err := app.Run(os.Args) diff --git a/service/zulip/zulip.go b/service/zulip/zulip.go new file mode 100644 index 0000000..cb909a0 --- /dev/null +++ b/service/zulip/zulip.go @@ -0,0 +1,178 @@ +package zulip + +import ( + "encoding/json" + "errors" + "log" + "net/http" + "net/url" + "strings" + "time" + + "github.com/urfave/cli/v2" +) + +// Zulip holds all the necessary options to use zulip +type Zulip struct { + ZBot + Type string + To string + Topic string + Content string + Domain string +} + +type ZBot struct { + EmailID string + APIKey string +} + +type ZResponse struct { + ID int `json:"id"` + Message string `json:"msg"` + Result string `json:"result"` + Code string `json:"code"` +} + +// HTTPClient interface +type HTTPClient interface { + Do(req *http.Request) (*http.Response, error) +} + +var ( + Client HTTPClient +) + +func initialize() { + Client = &http.Client{ + Timeout: 10 * time.Second, + } +} + +func Send() *cli.Command { + var zulipOpts Zulip + return &cli.Command{ + Name: "zulip", + Usage: "Send message to zulip", + UsageText: "pingme zulip --email 'john.doe@email.com' --api-key '12345567' --to 'london' --type 'stream' " + + "--topic 'some topic' --content 'content of the message'", + Description: `Zulip uses token and email to authenticate and ids for users or streams. +You can specify multiple userIds by separating the value with ','.`, + Flags: []cli.Flag{ + &cli.StringFlag{ + Destination: &zulipOpts.Domain, + Name: "domain", + Aliases: []string{}, + Required: true, + Usage: "Your zulip domain", + EnvVars: []string{"ZULIP_DOMAIN"}, + }, + &cli.StringFlag{ + Destination: &zulipOpts.EmailID, + Name: "email", + Aliases: []string{}, + Required: true, + Usage: "Email ID of the bot", + EnvVars: []string{"ZULIP_BOT_EMAIL_ADDRESS"}, + }, + &cli.StringFlag{ + Destination: &zulipOpts.APIKey, + Name: "api-key", + Aliases: []string{}, + Required: true, + Usage: "API Key of the bot", + EnvVars: []string{"ZULIP_BOT_API_KEY"}, + }, + &cli.StringFlag{ + Destination: &zulipOpts.Type, + Name: "type", + Aliases: []string{}, + Required: true, + Usage: "The type of message to be sent. private for a private message and stream for a stream message.", + EnvVars: []string{}, + }, + &cli.StringFlag{ + Destination: &zulipOpts.To, + Name: "to", + Aliases: []string{}, + Required: true, + Usage: "For stream messages, the name of the stream. For private messages, csv of email addresses", + EnvVars: []string{}, + }, + &cli.StringFlag{ + Destination: &zulipOpts.Topic, + Name: "topic", + Aliases: []string{}, + Required: true, + Usage: "The topic of the message. Only required for stream messages (type=\"stream\"), ignored otherwise.", + EnvVars: []string{}, + }, + &cli.StringFlag{ + Destination: &zulipOpts.Content, + Name: "content", + Aliases: []string{}, + Required: true, + Usage: "The content of the message.", + EnvVars: []string{}, + }, + }, + Action: func(ctx *cli.Context) error { + initialize() + resp, err := SendZulipMessage(zulipOpts.Domain, zulipOpts) + if err != nil { + return err + } + if resp.Result == "success" { + log.Printf("Server Reply ID: %v\nResult: %v\n", resp.ID, resp.Result) + } + return errors.New(resp.Message) + }, + } +} + +func getTo(messageType string, to string) string { + if messageType == "stream" { + return to + } + privateTo, _ := json.Marshal(strings.Split(to, ",")) + return string(privateTo) +} + +// SendZulipMessage function takes the zulip domain and zulip bot +// type, to, topic and content in the form of json byte array and sends +// message to zulip. +func SendZulipMessage(zulipDomain string, zulipOpts Zulip) (*ZResponse, error) { + data := url.Values{} + data.Set("type", zulipOpts.Type) + data.Set("to", getTo(zulipOpts.Type, zulipOpts.To)) + data.Set("topic", zulipOpts.Topic) + data.Set("content", zulipOpts.Content) + + var response ZResponse + + endPointURL := "https://" + zulipDomain + "/api/v1/messages" + // Create a new request using http + req, err := http.NewRequest("POST", endPointURL, strings.NewReader(data.Encode())) + if err != nil { + return nil, err + } + + zulipBot := zulipOpts.ZBot + + req.SetBasicAuth(zulipBot.EmailID, zulipBot.APIKey) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + + resp, err := Client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + // decode response sent from server + err = json.NewDecoder(resp.Body).Decode(&response) + if err != nil { + return nil, err + } + + return &response, nil +} diff --git a/service/zulip/zulip_test.go b/service/zulip/zulip_test.go new file mode 100644 index 0000000..2f3ccf6 --- /dev/null +++ b/service/zulip/zulip_test.go @@ -0,0 +1,181 @@ +package zulip_test + +import ( + "bytes" + "encoding/json" + "io/ioutil" + "net/http" + "testing" + + "github.com/kha7iq/pingme/service/zulip" + "github.com/stretchr/testify/assert" +) + +// MockDoType +type MockDoType func(req *http.Request) (*http.Response, error) + +// MockClient is the mock client +type MockClient struct { + MockDo MockDoType +} + +// Overriding what the Do function should "do" in our MockClient +func (m *MockClient) Do(req *http.Request) (*http.Response, error) { + return m.MockDo(req) +} + +func TestSendMessage_Success(t *testing.T) { + // build our response JSON + + successResponse, _ := json.Marshal(zulip.ZResponse{ + ID: 1, + Message: "", + Result: "success", + Code: "", + }) + // create a new reader with that JSON + r := ioutil.NopCloser(bytes.NewReader(successResponse)) + + zulip.Client = &MockClient{ + MockDo: func(*http.Request) (*http.Response, error) { + return &http.Response{ + StatusCode: 200, + Body: r, + }, nil + }, + } + + z := zulip.Zulip{ + ZBot: zulip.ZBot{ + EmailID: "test@test.com", + APIKey: "api-key", + }, + Type: "stream", + To: "general", + Topic: "test", + Content: "test content", + Domain: "user.zulipchat.com", + } + + resp, err := zulip.SendZulipMessage(z.Domain, z) + + assert.Nil(t, err) + + assert.Equal(t, "success", resp.Result) +} + +func TestSendMessageStream_Fail(t *testing.T) { + // build our response JSON + + failureResponse, _ := json.Marshal(zulip.ZResponse{ + Message: "Stream 'nonexistent_stream' does not exist", + Result: "error", + Code: "STREAM_DOES_NOT_EXIST", + }) + // create a new reader with that JSON + r := ioutil.NopCloser(bytes.NewReader(failureResponse)) + + zulip.Client = &MockClient{ + MockDo: func(*http.Request) (*http.Response, error) { + return &http.Response{ + StatusCode: 404, + Body: r, + }, nil + }, + } + + z := zulip.Zulip{ + ZBot: zulip.ZBot{ + EmailID: "test@test.com", + APIKey: "api-key", + }, + Type: "stream", + To: "general", + Topic: "test", + Content: "test content", + Domain: "user.zulipchat.com", + } + + resp, err := zulip.SendZulipMessage(z.Domain, z) + + assert.Nil(t, err) + + assert.Equal(t, "error", resp.Result) +} + +func TestSendMessagePrivate_Fail(t *testing.T) { + // build our response JSON + + failureResponse, _ := json.Marshal(zulip.ZResponse{ + Message: "some error", + Result: "error", + Code: "BAD_REQUEST", + }) + // create a new reader with that JSON + r := ioutil.NopCloser(bytes.NewReader(failureResponse)) + + zulip.Client = &MockClient{ + MockDo: func(*http.Request) (*http.Response, error) { + return &http.Response{ + StatusCode: 404, + Body: r, + }, nil + }, + } + + z := zulip.Zulip{ + ZBot: zulip.ZBot{ + EmailID: "test@test.com", + APIKey: "api-key", + }, + Type: "private", + To: "1,2", + Topic: "test", + Content: "test content", + Domain: "user.zulipchat.com", + } + + resp, err := zulip.SendZulipMessage(z.Domain, z) + + assert.Nil(t, err) + + assert.Equal(t, "error", resp.Result) +} + +func TestSendMessagePrivate_Success(t *testing.T) { + // build our response JSON + successResponse, _ := json.Marshal(zulip.ZResponse{ + Message: "", + Result: "success", + ID: 1, + }) + // create a new reader with that JSON + r := ioutil.NopCloser(bytes.NewReader(successResponse)) + + zulip.Client = &MockClient{ + MockDo: func(*http.Request) (*http.Response, error) { + return &http.Response{ + StatusCode: 200, + Body: r, + }, nil + }, + } + + z := zulip.Zulip{ + ZBot: zulip.ZBot{ + EmailID: "test@test.com", + APIKey: "api-key", + }, + Type: "private", + To: "1,2", + Topic: "test", + Content: "test content", + Domain: "user.zulipchat.com", + } + + resp, err := zulip.SendZulipMessage(z.Domain, z) + + assert.Nil(t, err) + + assert.Equal(t, "success", resp.Result) +}