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/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
66 changes: 66 additions & 0 deletions service/mattermost/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# 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 requets.
shivuslr41 marked this conversation as resolved.
Show resolved Hide resolved
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)

// 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)
}

}
```
53 changes: 53 additions & 0 deletions service/mattermost/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
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 requets.
shivuslr41 marked this conversation as resolved.
Show resolved Hide resolved
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)

// 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
151 changes: 151 additions & 0 deletions service/mattermost/mattermost.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
// Package mattermost provides message notification integration for mattermost.com.
package mattermost

import (
"context"
"io"
"log"
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 []string
}

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

// 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) {
s.channelIDs = append(s.channelIDs, channelIDs...)
}

// 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
}

// 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 pre-send hook to log the request before it is sent.
shivuslr41 marked this conversation as resolved.
Show resolved Hide resolved
httpService.PreSend(func(req *stdhttp.Request) error {
log.Printf("Sending login request to %s", req.URL)
Fixed Show fixed Hide fixed
return nil
})

// 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))
}
log.Printf("Login successful for %s", resp.Request.URL)
Fixed Show fixed Hide fixed
EthanEFung marked this conversation as resolved.
Show resolved Hide resolved

// 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
}
94 changes: 94 additions & 0 deletions service/mattermost/mattermost_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package mattermost

import (
"context"
"errors"
"testing"

"github.com/stretchr/testify/require"
)

const url = "https://host.mattermost.com"

func TestService_New(t *testing.T) {
t.Parallel()
assert := require.New(t)
service := New(url)
assert.NotNil(service)
}

func TestService_LoginWithCredentials(t *testing.T) {
t.Parallel()
assert := require.New(t)
service := New(url)
// Test responses
mockClient := newMockHttpClient(t)
mockClient.
On("Send", context.TODO(), "fake-loginID", "fake-password").Return(nil)
service.loginClient = mockClient
// test call
err := service.LoginWithCredentials(context.TODO(), "fake-loginID", "fake-password")
assert.Nil(err)
mockClient.AssertExpectations(t)

// Test errors
// Test responses
mockClient = newMockHttpClient(t)
mockClient.
On("Send", context.TODO(), "fake-loginID", "").Return(errors.New("empty password"))
service.loginClient = mockClient
// test call
err = service.LoginWithCredentials(context.TODO(), "fake-loginID", "")
assert.NotNil(err)
mockClient.AssertExpectations(t)
}

func TestService_AddReceivers(t *testing.T) {
t.Parallel()

assert := require.New(t)

service := New(url)
assert.NotNil(service)

service.AddReceivers("yfgstwuisnshydhd")
assert.Equal(1, len(service.channelIDs))

service.AddReceivers("yfgstwuisnshydhd", "nwudneyfrwqjs")
assert.Equal(3, len(service.channelIDs))
shivuslr41 marked this conversation as resolved.
Show resolved Hide resolved

hooks := []string{"yfgstwuisnshydhd", "nwudneyfrwqjs"}
service.channelIDs = []string{}
service.AddReceivers(hooks...)
assert.Equal(service.channelIDs, hooks)
}

func TestService_Send(t *testing.T) {
t.Parallel()
assert := require.New(t)

service := New(url)
channelID := "yfgstwuisnshydhd"
service.channelIDs = append(service.channelIDs, channelID)

// Test responses
mockClient := newMockHttpClient(t)
mockClient.
On("Send", context.TODO(), channelID, "fake-sub\nfake-msg").Return(nil)
service.messageClient = mockClient
// test call
err := service.Send(context.TODO(), "fake-sub", "fake-msg")
assert.Nil(err)
mockClient.AssertExpectations(t)

// Test error on Send
// Test responses
mockClient = newMockHttpClient(t)
mockClient.
On("Send", context.TODO(), channelID, "fake-sub\nfake-msg").Return(errors.New("internal error"))
service.messageClient = mockClient
// test call
err = service.Send(context.TODO(), "fake-sub", "fake-msg")
assert.NotNil(err)
mockClient.AssertExpectations(t)
}
Loading