Skip to content

Commit

Permalink
feat(service): Adds Mattermost service (#516)
Browse files Browse the repository at this point in the history
* feat(service): Adds Mattermost service

* refactor: Removes log msgs
fix: Adds PreSend and PostSend capabilities to user
docs: Updates doc.go and README.md

* refactor: Adds test cases
Adds presend and postsend tests for mattermost service

* refactor: Updates mattermost test cases

* refactor: update mattermost test cases

* refactor: update mattermost hook(s) test cases

---------

Co-authored-by: EthanEFung <[email protected]>
Co-authored-by: Niko Köser <[email protected]>
  • Loading branch information
3 people authored Jun 21, 2023
1 parent 67c5b79 commit 6e1878c
Show file tree
Hide file tree
Showing 7 changed files with 514 additions and 0 deletions.
1 change: 1 addition & 0 deletions service/http/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ func main() {
// from the given subject and message.
httpService.AddReceivers(&http.Webhook{
URL: "http://localhost:8080",
Header: stdhttp.Header{},
ContentType: "text/plain",
Method: stdhttp.MethodPost,
BuildPayload: func(subject, message string) (payload any) {
Expand Down
1 change: 1 addition & 0 deletions service/http/doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ Usage:
// from the given subject and message.
httpService.AddReceivers(&http.Webhook{
URL: "http://localhost:8080",
Header: stdhttp.Header{},
ContentType: "text/plain",
Method: stdhttp.MethodPost,
BuildPayload: func(subject, message string) (payload any) {
Expand Down
80 changes: 80 additions & 0 deletions service/mattermost/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# Mattermost Usage

Ensure that you have already navigated to your GOPATH and installed the following packages:

* `go get -u github.com/nikoksr/notify`

## Steps for Mattermost Server

These are general and very high level instructions

1. Create a new Mattermost server / Join existing Mattermost server
2. Make sure your Username/loginID have the OAuth permission scope(s): `create_post`
3. Copy the *Channel ID* of the channel you want to post a message to. You can grab the *Channel ID* in channel info. example: *yfgstwuisnshydhd*
4. Now you should be good to use the code below

## Sample Code

```go
package main

import (
"os"

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

func main() {

// Init notifier
notifier := notify.New()
ctx := context.Background()

// Provide your Mattermost server url
mattermostService := mattermost.New("https://myserver.cloud.mattermost.com")

// Provide username as loginID and password to login into above server.
// NOTE: This generates auth token which will get expired, invoking this method again
// after expiry will generate new token and uses for further requests.
err := mattermostService.LoginWithCredentials(ctx, "[email protected]", "somepassword")
if err != nil {
fmt.Println(err)
os.Exit(1)
}

// Passing a Mattermost channel/chat id as receiver for our messages.
// Where to send our messages.
mattermostService.AddReceivers("CHANNEL_ID")

// Tell our notifier to use the Mattermost service. You can repeat the above process
// for as many services as you like and just tell the notifier to use them.
notifier.UseServices(mattermostService)

// Add presend and postsend hooks that you need to execute before every requests and after
// every response respectively. Multiple presend and postsend are executed in the order defined here.
// refer service/http for the more info.
// PreSend hook
mattermostService.PreSend(func(req *stdhttp.Request) error {
log.Printf("Sending message to %s server", req.URL)
return nil
})
// PostSend hook
mattermostService.PostSend(func(req *stdhttp.Request, resp *stdhttp.Response) error {
log.Printf("Message sent to %s server with status %d", req.URL, resp.StatusCode)
return nil
})

// Send a message
err = notifier.Send(
ctx,
"Hello from notify :wave:\n",
"Message written in Go!",
)
if err != nil {
fmt.Println(err)
os.Exit(1)
}

}
```
67 changes: 67 additions & 0 deletions service/mattermost/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*
Package mattermost provides message notification integration for mattermost.com.
Usage:
package main
import (
"os"
"github.com/nikoksr/notify"
"github.com/nikoksr/notify/service/mattermost"
)
func main() {
// Init notifier
notifier := notify.New()
ctx := context.Background()
// Provide your Mattermost server url
mattermostService := mattermost.New("https://myserver.cloud.mattermost.com")
// Provide username as loginID and password to login into above server.
// NOTE: This generates auth token which will get expired, invoking this method again
// after expiry will generate new token and uses for further requests.
err := mattermostService.LoginWithCredentials(ctx, "[email protected]", "somepassword")
if err != nil {
fmt.Println(err)
os.Exit(1)
}
// Passing a Mattermost channel/chat id as receiver for our messages.
// Where to send our messages.
mattermostService.AddReceivers("CHANNEL_ID")
// Tell our notifier to use the Mattermost service. You can repeat the above process
// for as many services as you like and just tell the notifier to use them.
notifier.UseServices(mattermostService)
// Add presend and postsend hooks that you need to execute before every requests and after
// every response respectively. Multiple presend and postsend are executed in the order defined here.
// refer service/http for the more info.
// PreSend hook
mattermostService.PreSend(func(req *stdhttp.Request) error {
log.Printf("Sending message to %s server", req.URL)
return nil
})
// PostSend hook
mattermostService.PostSend(func(req *stdhttp.Request, resp *stdhttp.Response) error {
log.Printf("Message sent to %s server with status %d", req.URL, resp.StatusCode)
return nil
})
// Send a message
err = notifier.Send(
ctx,
"Hello from notify :wave:",
"Message written in Go!",
)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
}
*/
package mattermost
155 changes: 155 additions & 0 deletions service/mattermost/mattermost.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
// Package mattermost provides message notification integration for mattermost.com.
package mattermost

import (
"context"
"io"
stdhttp "net/http"

"github.com/pkg/errors"

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

//go:generate mockery --name=httpClient --output=. --case=underscore --inpackage
type httpClient interface {
AddReceivers(wh ...*http.Webhook)
PreSend(prefn http.PreSendHookFn)
Send(ctx context.Context, subject, message string) error
PostSend(postfn http.PostSendHookFn)
}

// Service encapsulates the notify httpService client and contains mattermost channel ids.
type Service struct {
loginClient httpClient
messageClient httpClient
channelIDs map[string]bool
}

// New returns a new instance of a Mattermost notification service.
func New(url string) *Service {
httpService := setupMsgService(url)
return &Service{
setupLoginService(url, httpService),
httpService,
make(map[string]bool),
}
}

// LoginWithCredentials provides helper for authentication using Mattermost user/admin credentials.
func (s *Service) LoginWithCredentials(ctx context.Context, loginID, password string) error {
// request login
if err := s.loginClient.Send(ctx, loginID, password); err != nil {
return errors.Wrapf(err, "failed login to Mattermost server")
}
return nil
}

// AddReceivers takes Mattermost channel IDs or Chat IDs and adds them to the internal channel ID list.
// The Send method will send a given message to all these channels.
func (s *Service) AddReceivers(channelIDs ...string) {
for i := range channelIDs {
s.channelIDs[channelIDs[i]] = true
}
}

// Send takes a message subject and a message body and send them to added channel ids.
// you will need a 'create_post' permission for your username.
// refer https://api.mattermost.com/ for more info
func (s *Service) Send(ctx context.Context, subject, message string) error {
for id := range s.channelIDs {
select {
case <-ctx.Done():
return ctx.Err()
default:
// create post
if err := s.messageClient.Send(ctx, id, subject+"\n"+message); err != nil {
return errors.Wrapf(err, "failed to send message")
}
}
}
return nil
}

// PreSend adds a pre-send hook to the service. The hook will be executed before sending a request to a receiver.
func (s *Service) PreSend(hook http.PreSendHookFn) {
s.messageClient.PreSend(hook)
}

// PostSend adds a post-send hook to the service. The hook will be executed after sending a request to a receiver.
func (s *Service) PostSend(hook http.PostSendHookFn) {
s.messageClient.PostSend(hook)
}

// setups main message service for creating posts
func setupMsgService(url string) *http.Service {
// create new http client for sending messages/notifications
httpService := http.New()

// add custom payload builder
httpService.AddReceivers(&http.Webhook{
URL: url + "/api/v4/posts",
Header: stdhttp.Header{},
ContentType: "application/json",
Method: stdhttp.MethodPost,
BuildPayload: func(channelID, subjectAndMessage string) (payload any) {
return map[string]string{
"channel_id": channelID,
"message": subjectAndMessage,
}
},
})

// add post-send hook for error checks
httpService.PostSend(func(req *stdhttp.Request, resp *stdhttp.Response) error {
if resp.StatusCode != stdhttp.StatusCreated {
b, _ := io.ReadAll(resp.Body)
return errors.New("failed to create post with status: " + resp.Status + " body: " + string(b))
}
return nil
})
return httpService
}

// setups login service to get token
func setupLoginService(url string, msgService *http.Service) *http.Service {
// create another new http client for login request call.
httpService := http.New()

// append login path for the given mattermost server with custom payload builder.
httpService.AddReceivers(&http.Webhook{
URL: url + "/api/v4/users/login",
Header: stdhttp.Header{},
ContentType: "application/json",
Method: stdhttp.MethodPost,
BuildPayload: func(loginID, password string) (payload any) {
return map[string]string{
"login_id": loginID,
"password": password,
}
},
})

// Add post-send hook to do error checks and log the response after it is received.
// Also extract token from response header and set it as part of pre-send hook of main http client for further requests.
httpService.PostSend(func(req *stdhttp.Request, resp *stdhttp.Response) error {
if resp.StatusCode != stdhttp.StatusOK {
b, _ := io.ReadAll(resp.Body)
return errors.New("login failed with status: " + resp.Status + " body: " + string(b))
}

// get token from header
token := resp.Header.Get("Token")
if token == "" {
return errors.New("received empty token")
}

// set token as pre-send hook
msgService.PreSend(func(req *stdhttp.Request) error {
req.Header.Set("Authorization", "Bearer "+token)
return nil
})
return nil
})
return httpService
}
Loading

0 comments on commit 6e1878c

Please sign in to comment.