Skip to content

Commit

Permalink
Merge pull request #16 from argonautdev/feat/subscriber-notification-…
Browse files Browse the repository at this point in the history
…feed

feat: Subscriber Notification Feed.
  • Loading branch information
samsoft00 authored Apr 10, 2023
2 parents ddf40e9 + 99e0e0d commit 794b4dd
Show file tree
Hide file tree
Showing 8 changed files with 473 additions and 4 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@ Class | Method
*SubscriberApi* | [**Identify**](https://docs.novu.co/platform/subscribers#creating-a-subscriber) | **Post** /subscribers | Create a subscriber
*SubscriberApi* | [**Update**](https://docs.novu.co/platform/subscribers#updating-subscriber-data) | **Put** /subscribers/:subscriberID | Update subscriber data
*SubscriberApi* | [**Delete**](https://docs.novu.co/platform/subscribers#removing-a-subscriber) | **Delete** /subscribers/:subscriberID | Removing a subscriber
*SubscriberApi* | [**Get**](https://docs.novu.co/api/get-a-notification-feed-for-a-particular-subscriber) | **Get** /subscribers/:subscriberId/notifications/feed | Get a notification feed for a particular subscriber
*SubscriberApi* | [**Get**](https://docs.novu.co/api/get-the-unseen-notification-count-for-subscribers-feed) | **Get** /subscribers/:subscriberId/notifications/feed | Get the unseen notification count for subscribers feed
*SubscriberApi* | [**Post**](https://docs.novu.co/api/mark-a-subscriber-feed-message-as-seen) | **Post** /v1/subscribers/:subscriberId/messages/markAs | Mark a subscriber feed message as seen
*SubscriberApi* | [**Get**](https://docs.novu.co/api/get-subscriber-preferences/) | **Get** /subscribers/:subscriberId/preferences | Get subscriber preferences
*SubscriberApi* | [**Patch**](https://docs.novu.co/api/update-subscriber-preference/) | **Patch** /subscribers/:subscriberId/preferences/:templateId | Update subscriber preference
*IntegrationsApi* | [**Create**](https://docs.novu.co/platform/integrations) | **Post** /integrations | Create an integration
Expand Down
87 changes: 86 additions & 1 deletion lib/model.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package lib

import "io"
import (
"io"
"time"
)

type ChannelType string
type GeneralError error
Expand Down Expand Up @@ -161,6 +164,88 @@ type SubscribersTopicRequest struct {
Subscribers []string `json:"subscribers"`
}

type SubscriberNotificationFeedOptions struct {
Page int `queryKey:"page"`
FeedIdentifier string `queryKey:"feedIdentifier"`
Seen bool `queryKey:"seen"`
}

type SubscriberUnseenCountOptions struct {
Seen *bool `json:"seen"`
}

type SubscriberMarkMessageSeenOptions struct {
MessageID string `json:"messageId"`
Seen bool `json:"seen"`
Read bool `json:"read"`
}

type NotificationFeedData struct {
CTA CTA `json:"cta"`
Channel string `json:"channel"`
Content string `json:"content"`
CreatedAt time.Time `json:"createdAt"`
Deleted bool `json:"deleted"`
DeviceTokens []string `json:"deviceTokens"`
DirectWebhookURL string `json:"directWebhookUrl"`
EnvironmentID string `json:"_environmentId"`
ErrorID string `json:"errorId"`
ErrorText string `json:"errorText"`
FeedID string `json:"_feedId"`
ID string `json:"_id"`
JobID string `json:"_jobId"`
LastReadDate time.Time `json:"lastReadDate"`
LastSeenDate time.Time `json:"lastSeenDate"`
MessageTemplate string `json:"_messageTemplateId"`
NotificationID string `json:"_notificationId"`
OrganizationID string `json:"_organizationId"`
Payload struct {
UpdateMessage string `json:"updateMessage"`
} `json:"payload"`
ProviderID string `json:"providerId"`
Read bool `json:"read"`
ResponseID string `json:"id"`
Seen bool `json:"seen"`
Status string `json:"status"`
Subscriber struct {
ID string `json:"_id"`
SubscriberID string `json:"subscriberId"`
} `json:"subscriber"`
SubscriberID string `json:"_subscriberId"`
TemplateID string `json:"_templateId"`
TemplateIdentifier string `json:"templateIdentifier"`
TransactionID string `json:"transactionId"`
UpdatedAt time.Time `json:"updatedAt"`
}

type SubscriberNotificationFeedResponse struct {
TotalCount int `json:"totalCount"`
Data []NotificationFeedData `json:"data"`
PageSize int `json:"pageSize"`
Page int `json:"page"`
}

type SubscriberUnseenCountResponse struct {
Data struct {
Count int `json:"count"`
} `json:"data"`
}

type CTA struct {
Type string `json:"type"`
Action struct {
Status string `json:"status"`
Buttons struct {
Type string `json:"type"`
Content string `json:"content"`
ResultContent string `json:"resultContent"`
} `json:"buttons"`
Result struct {
Payload map[string]interface{} `json:"payload"`
Type string `json:"type"`
} `json:"result"`
}
}
type IntegrationCredentials struct {
ApiKey string `json:"apiKey,omitempty"`
User string `json:"user,omitempty"`
Expand Down
86 changes: 84 additions & 2 deletions lib/subscribers.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"encoding/json"
"io"
"net/http"
"strconv"

"github.com/pkg/errors"
)
Expand All @@ -15,6 +16,9 @@ type ISubscribers interface {
Get(ctx context.Context, subscriberID string) (SubscriberResponse, error)
Update(ctx context.Context, subscriberID string, data interface{}) (SubscriberResponse, error)
Delete(ctx context.Context, subscriberID string) (SubscriberResponse, error)
GetNotificationFeed(ctx context.Context, subscriberID string, opts *SubscriberNotificationFeedOptions) (*SubscriberNotificationFeedResponse, error)
GetUnseenCount(ctx context.Context, subscriberID string, opts *SubscriberUnseenCountOptions) (*SubscriberUnseenCountResponse, error)
MarkMessageSeen(ctx context.Context, subscriberID string, opts SubscriberMarkMessageSeenOptions) (*SubscriberNotificationFeedResponse, error)
GetPreferences(ctx context.Context, subscriberID string) (*SubscriberPreferencesResponse, error)
UpdatePreferences(ctx context.Context, subscriberID string, templateId string, opts *UpdateSubscriberPreferencesOptions) (*SubscriberPreferencesResponse, error)
}
Expand Down Expand Up @@ -98,6 +102,37 @@ func (s *SubscriberService) Delete(ctx context.Context, subscriberID string) (Su
return resp, nil
}

func (s *SubscriberService) GetNotificationFeed(ctx context.Context, subscriberID string, opts *SubscriberNotificationFeedOptions) (*SubscriberNotificationFeedResponse, error) {
var resp SubscriberNotificationFeedResponse
URL := s.client.config.BackendURL.JoinPath("subscribers", subscriberID, "notifications", "feed")

if opts != nil {
queryValues := URL.Query()

params, err := GenerateQueryParamsFromStruct(*opts)
if err != nil {
return nil, err
}

for _, param := range params {
queryValues.Add(param.Key, param.Value)
}

URL.RawQuery = queryValues.Encode()
}

req, err := http.NewRequestWithContext(ctx, http.MethodGet, URL.String(), http.NoBody)
if err != nil {
return nil, err
}
_, err = s.client.sendRequest(req, &resp)
if err != nil {
return nil, err
}

return &resp, nil
}

func (s *SubscriberService) GetPreferences(ctx context.Context, subscriberID string) (*SubscriberPreferencesResponse, error) {
var resp SubscriberPreferencesResponse
URL := s.client.config.BackendURL.JoinPath("subscribers", subscriberID, "preferences")
Expand All @@ -109,7 +144,32 @@ func (s *SubscriberService) GetPreferences(ctx context.Context, subscriberID str

_, err = s.client.sendRequest(req, &resp)
if err != nil {
return &resp, err
return nil, err
}

return &resp, nil
}

func (s *SubscriberService) GetUnseenCount(ctx context.Context, subscriberID string, opts *SubscriberUnseenCountOptions) (*SubscriberUnseenCountResponse, error) {
var resp SubscriberUnseenCountResponse
URL := s.client.config.BackendURL.JoinPath("subscribers", subscriberID, "notifications", "unseen")

if opts != nil {
if opts.Seen != nil {
queryValues := URL.Query()
queryValues.Add("seen", strconv.FormatBool(*opts.Seen))
URL.RawQuery = queryValues.Encode()
}
}

req, err := http.NewRequestWithContext(ctx, http.MethodGet, URL.String(), http.NoBody)
if err != nil {
return nil, err
}

_, err = s.client.sendRequest(req, &resp)
if err != nil {
return nil, err
}

return &resp, nil
Expand Down Expand Up @@ -137,7 +197,29 @@ func (s *SubscriberService) UpdatePreferences(ctx context.Context, subscriberID

_, err = s.client.sendRequest(req, &resp)
if err != nil {
return &resp, err
return nil, err
}

return &resp, nil
}

func (s *SubscriberService) MarkMessageSeen(ctx context.Context, subscriberID string, opts SubscriberMarkMessageSeenOptions) (*SubscriberNotificationFeedResponse, error) {
var resp SubscriberNotificationFeedResponse
URL := s.client.config.BackendURL.JoinPath("subscribers", subscriberID, "messages", "markAs")

jsonBody, err := json.Marshal(opts)
if err != nil {
return nil, err
}

req, err := http.NewRequestWithContext(ctx, http.MethodPost, URL.String(), bytes.NewBuffer(jsonBody))
if err != nil {
return nil, err
}

_, err = s.client.sendRequest(req, &resp)
if err != nil {
return nil, err
}

return &resp, nil
Expand Down
82 changes: 82 additions & 0 deletions lib/subscribers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"net/http"
"net/http/httptest"
"path/filepath"
"strconv"
"strings"
"testing"

Expand Down Expand Up @@ -169,6 +170,36 @@ func TestSubscriberService_Delete_Success(t *testing.T) {
})
}

func TestSubscriberService_GetNotificationFeed_Success(t *testing.T) {
var expectedResponse *lib.SubscriberNotificationFeedResponse
fileToStruct(filepath.Join("../testdata", "subscriber_notification_feed_response.json"), &expectedResponse)

page := 1
seen := true
feedIdentifier := "feed_identifier"

opts := lib.SubscriberNotificationFeedOptions{
Page: page,
Seen: seen,
FeedIdentifier: feedIdentifier,
}

httpServer := createTestServer(t, TestServerOptions[io.Reader, *lib.SubscriberNotificationFeedResponse]{
expectedURLPath: fmt.Sprintf("/v1/subscribers/%s/notifications/feed?feedIdentifier=%s&page=%s&seen=%s", subscriberID, feedIdentifier, strconv.Itoa(page), strconv.FormatBool(seen)),
expectedSentMethod: http.MethodGet,
expectedSentBody: http.NoBody,
responseStatusCode: http.StatusOK,
responseBody: expectedResponse,
})

ctx := context.Background()
c := lib.NewAPIClient(novuApiKey, &lib.Config{BackendURL: lib.MustParseURL(httpServer.URL)})
resp, err := c.SubscriberApi.GetNotificationFeed(ctx, subscriberID, &opts)

require.NoError(t, err)
require.Equal(t, resp, expectedResponse)
}

func TestSubscriberService_GetSubscriber_Success(t *testing.T) {
var expectedResponse lib.SubscriberResponse
fileToStruct(filepath.Join("../testdata", "subscriber_response.json"), &expectedResponse)
Expand Down Expand Up @@ -209,6 +240,57 @@ func TestSubscriberService_GetPreferences_Success(t *testing.T) {
require.Equal(t, resp, expectedResponse)
}

func TestSubscriberService_GetUnseenCount_Success(t *testing.T) {
var expectedResponse *lib.SubscriberUnseenCountResponse
fileToStruct(filepath.Join("../testdata", "subscriber_notification_feed_unseen.json"), &expectedResponse)

seen := false

opts := lib.SubscriberUnseenCountOptions{
Seen: &seen,
}

httpServer := createTestServer(t, TestServerOptions[io.Reader, *lib.SubscriberUnseenCountResponse]{
expectedURLPath: fmt.Sprintf("/v1/subscribers/%s/notifications/unseen?seen=false", subscriberID),
expectedSentMethod: http.MethodGet,
expectedSentBody: http.NoBody,
responseStatusCode: http.StatusOK,
responseBody: expectedResponse,
})

ctx := context.Background()
c := lib.NewAPIClient(novuApiKey, &lib.Config{BackendURL: lib.MustParseURL(httpServer.URL)})
resp, err := c.SubscriberApi.GetUnseenCount(ctx, subscriberID, &opts)

require.NoError(t, err)
require.Equal(t, resp, expectedResponse)
}

func TestSubscriberService_MarkMessageSeen(t *testing.T) {
var expectedResponse *lib.SubscriberNotificationFeedResponse
fileToStruct(filepath.Join("../testdata", "subscriber_notification_feed_response.json"), &expectedResponse)

opts := lib.SubscriberMarkMessageSeenOptions{
MessageID: "message_id",
Seen: true,
Read: true,
}

httpServer := createTestServer(t, TestServerOptions[lib.SubscriberMarkMessageSeenOptions, *lib.SubscriberNotificationFeedResponse]{
expectedURLPath: fmt.Sprintf("/v1/subscribers/%s/messages/markAs", subscriberID),
expectedSentMethod: http.MethodPost,
expectedSentBody: opts,
responseStatusCode: http.StatusOK,
responseBody: expectedResponse,
})

ctx := context.Background()
c := lib.NewAPIClient(novuApiKey, &lib.Config{BackendURL: lib.MustParseURL(httpServer.URL)})
resp, err := c.SubscriberApi.MarkMessageSeen(ctx, subscriberID, opts)
require.NoError(t, err)
require.Equal(t, resp, expectedResponse)
}

func TestSubscriberService_UpdatePreferences_Success(t *testing.T) {
var topicID = "topicId"

Expand Down
56 changes: 55 additions & 1 deletion lib/utils.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
package lib

import "net/url"
import (
"errors"
"net/url"
"reflect"
"strconv"
)

func MustParseURL(rawURL string) *url.URL {
u, err := url.Parse(rawURL)
Expand All @@ -10,3 +15,52 @@ func MustParseURL(rawURL string) *url.URL {

return u
}

type QueryParam struct {
Key string
Value string
}

func GenerateQueryParamsFromStruct[T interface{}](queryParamsStruct T) ([]QueryParam, error) {
var queryParamList []QueryParam = make([]QueryParam, 0)

values := reflect.ValueOf(queryParamsStruct)
if values.Kind() != reflect.Struct {
return nil, errors.New("queryParamsStruct must be a struct. Make sure you are not passing a pointer to struct")
}

types := values.Type()
for i := 0; i < values.NumField(); i++ {
field := values.Field(i)
if !field.IsZero() {
// Check if the field has a queryKey tag and use that as the key for the query param
key := types.Field(i).Tag.Get("queryKey")
if key == "" {
key = types.Field(i).Name
}

// Check if the field is a string, bool or int and convert it to string
var value string
switch field.Kind() {
case reflect.String:
value = field.Interface().(string)

case reflect.Bool:
value = strconv.FormatBool(field.Interface().(bool))

case reflect.Int:
value = strconv.FormatInt(int64(field.Interface().(int)), 10)

default:
return nil, errors.New("unsupported type in struct field. Supported types are: string, bool and int")
}

queryParamList = append(queryParamList, QueryParam{
Key: key,
Value: value,
})
}
}

return queryParamList, nil
}
Loading

0 comments on commit 794b4dd

Please sign in to comment.