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): Adds Mattermost service #516

Merged
merged 12 commits into from
Jun 21, 2023
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, "someone@gmail.com", "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, "someone@gmail.com", "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