Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(service): Zulip Service integration #711

Open
wants to merge 20 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
d345625
feat: zulip client to send messages via bot email and bot api key
saisumith770 Oct 3, 2023
1139977
fix: validations for topic and content
saisumith770 Oct 3, 2023
47c8d53
refactor: extracted all errors into errors.go
saisumith770 Oct 3, 2023
df62055
test(message): added tests for message validation
saisumith770 Oct 3, 2023
5abae06
doc(validation): added comments for the validations
saisumith770 Oct 3, 2023
6f44263
feat(hooks): hooks for configuring the client
saisumith770 Oct 3, 2023
3527f9c
test(hooks): added tests for all hooks
saisumith770 Oct 3, 2023
2f2b9dd
Merge branch 'nikoksr:main' into zulip-integration
saisumith770 Oct 3, 2023
4620583
refactor(test): cleaned up the assert messages
saisumith770 Oct 3, 2023
3fbaa9c
ci: nits
saisumith770 Oct 3, 2023
4797ca3
test(client): added tests for all client methods
saisumith770 Oct 3, 2023
df8b1bb
Merge branch 'zulip-integration' of https://github.com/saisumith770/n…
saisumith770 Oct 3, 2023
9fcff13
ci: fix linting issue
saisumith770 Oct 3, 2023
34003d1
test: changed assert to require as per repo testing rules
saisumith770 Oct 3, 2023
a0c958e
feat(service): added wrapper for zulip client
saisumith770 Oct 5, 2023
3e3e738
test(service): testing zulip wrapper
saisumith770 Oct 5, 2023
45d3cf5
ci: linting nits
saisumith770 Oct 5, 2023
68c236a
doc: added docs as per repo style
saisumith770 Oct 5, 2023
fba8d3b
doc: updated README
saisumith770 Oct 5, 2023
35292f0
Merge branch 'main' into zulip-integration
saisumith770 Oct 5, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ Yes, please! Contributions of all kinds are very welcome! Feel free to check our
| [WeChat](https://www.wechat.com) | [service/wechat](service/wechat) | [silenceper/wechat](https://github.com/silenceper/wechat) | :heavy_check_mark: |
| [Webpush Notification](https://developer.mozilla.org/en-US/docs/Web/API/Push_API) | [service/webpush](service/webpush) | [SherClockHolmes/webpush-go](https://github.com/SherClockHolmes/webpush-go/) | :heavy_check_mark: |
| [WhatsApp](https://www.whatsapp.com) | [service/whatsapp](service/whatsapp) | [Rhymen/go-whatsapp](https://github.com/Rhymen/go-whatsapp) | :x: |
| [Zulip](https://zulip.com/) | [service/zulip](service/zulip) | - | :heavy_check_mark: |

## Special Thanks <a id="special_thanks"></a>

Expand Down
43 changes: 43 additions & 0 deletions service/zulip/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Zulip

## Steps for creating a Zulip Bot

Follow the below instructions to create a bot email and api key required for the service:

1. Create a Zulip Organization
2. Go to settings and create a new bot. Copy the bot email and api key.
3. Copy your Oranization URL. Copy the entire url `https://your-domain.zulipchat.com`
4. Copy the stream name of the stream and its topic if you want to post a message to stream or just copy an email address of the receiver.

## Sample Code

```go
package main

import (
"context"
"log"

"github.com/nikoksr/notify"
"github.com/nikoksr/notify/service/zulip"
)

func main() {
zulipSvc, err := zulip.New("server-base-url", "bot-email", "api-key")
if err != nil {
log.Fatalf("zulip.New() failed: %s", err.Error())
}

zulipSvc.AddReceivers(Direct("[email protected]"), Stream("stream", "topic"))

notifier := notify.New()
notifier.UseServices(zulipSvc)

err = notifier.Send(context.Background(), "subject", "message")
if err != nil {
log.Fatalf("notifier.Send() failed: %s", err.Error())
}

log.Println("notification sent")
}
```
135 changes: 135 additions & 0 deletions service/zulip/client/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
package client

import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/url"
"strings"
"time"
)

const (
// DefaultBaseURL contains example base URL of zulip service.
DefaultBaseURL = "https://yourZulipDomain.zulipchat.com"

// DefaultTimeout duration in second
DefaultTimeout time.Duration = 30 * time.Second
)

// Client abstracts the interaction between the application server and the
// Zulip server via HTTP protocol. The developer must obtain an API key from the
// Zulip's personal settings page and pass it to the `Client` so that it can
// perform authorized requests on the application server's behalf.
// To send a message to one or more devices use the Client's Send.
//
// If the `HTTP` field is nil, a zeroed http.Client will be allocated and used
// to send messages.
type Client struct {
email string
apiKey string
baseURL string
client *http.Client
timeout time.Duration
}

type Option func(*Client) error

// NewClient creates new Zulip Client based on opts passed and
// with default endpoint and http client.
func NewClient(opts ...Option) (*Client, error) {
c := &Client{
baseURL: DefaultBaseURL,
client: &http.Client{},
timeout: DefaultTimeout,
}

for _, opt := range opts {
if err := opt(c); err != nil {
return nil, err
}
}

if c.apiKey == "" || c.email == "" {
return nil, ErrInvalidCreds
}

return c, nil
}

type Response struct {
ID int `json:"id"`
Msg string `json:"msg"`
Result string `json:"result"`
Code string `json:"code"`
}

// SendWithContext sends a message to the Zulip server without retrying in case of service
// unavailability. A non-nil error is returned if a non-recoverable error
// occurs (i.e. if the response status is not "200 OK").
// Behaves just like regular send, but uses external context.
func (c *Client) SendWithContext(ctx context.Context, msg *Message) (*Response, error) {
// validate
if err := msg.Validate(); err != nil {
return nil, err
}

return c.send(ctx, msg)
}

// Send sends a message to the Zulip server without retrying in case of service
// unavailability. A non-nil error is returned if a non-recoverable error
// occurs (i.e. if the response status is not "200 OK").
func (c *Client) Send(msg *Message) (*Response, error) {
ctx, cancel := context.WithTimeout(context.Background(), c.timeout)
defer cancel()

return c.SendWithContext(ctx, msg)
}

// send sends a request.
func (c *Client) send(ctx context.Context, msg *Message) (*Response, error) {
// set the message data
data := url.Values{}
data.Set("type", msg.Type)
data.Set("to", fmt.Sprintf("%v", msg.To))
data.Set("topic", msg.Topic)
data.Set("content", msg.Content)

// create request
url, _ := url.JoinPath(c.baseURL, "/api/v1/messages")
req, err := http.NewRequest("POST", url, strings.NewReader(data.Encode()))
if err != nil {
return nil, err
}

req = req.WithContext(ctx)

// add headers
req.SetBasicAuth(c.email, c.apiKey)
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

// execute request
resp, err := c.client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()

// check response status
if resp.StatusCode != http.StatusOK {
if resp.StatusCode >= http.StatusInternalServerError {
return nil, fmt.Errorf("%d error: %s", resp.StatusCode, resp.Status)
}
return nil, fmt.Errorf("%d error: %s", resp.StatusCode, resp.Status)
}

// build return
response := new(Response)
if err := json.NewDecoder(resp.Body).Decode(response); err != nil {
return nil, err
}

return response, nil
}
Loading