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

NK-600 Add runtime notifications list functions #1275

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion .github/workflows/golangci-lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,6 @@ jobs:
- name: golangci-lint
uses: golangci/golangci-lint-action@v3
with:
version: v1.54
version: v1.61
only-new-issues: true
args: --timeout=10m
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ All notable changes to this project are documented below.
The format is based on [keep a changelog](http://keepachangelog.com) and this project uses [semantic versioning](http://semver.org).

## [Unreleased]
### Added
- New runtime function to list user notifications.

### Changed
- Increased limit on runtimes group users list functions.

Expand Down
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -74,3 +74,5 @@ require (
golang.org/x/text v0.16.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240604185151-ef581f913117 // indirect
)

replace github.com/heroiclabs/nakama-common => ../nakama-common
21 changes: 1 addition & 20 deletions server/api_notification.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,7 @@
package server

import (
"bytes"
"context"
"encoding/base64"
"encoding/gob"

"github.com/gofrs/uuid/v5"
"github.com/heroiclabs/nakama-common/api"
"go.uber.org/zap"
Expand Down Expand Up @@ -62,22 +58,7 @@ func (s *ApiServer) ListNotifications(ctx context.Context, in *api.ListNotificat
limit = int(in.GetLimit().Value)
}

cursor := in.GetCacheableCursor()
var nc *notificationCacheableCursor
if cursor != "" {
nc = &notificationCacheableCursor{}
cb, err := base64.RawURLEncoding.DecodeString(cursor)
if err != nil {
s.logger.Warn("Could not base64 decode notification cursor.", zap.String("cursor", cursor))
return nil, status.Error(codes.InvalidArgument, "Malformed cursor was used.")
}
if err := gob.NewDecoder(bytes.NewReader(cb)).Decode(nc); err != nil {
s.logger.Warn("Could not decode notification cursor.", zap.String("cursor", cursor))
return nil, status.Error(codes.InvalidArgument, "Malformed cursor was used.")
}
}

notificationList, err := NotificationList(ctx, s.logger, s.db, userID, limit, cursor, nc)
notificationList, err := NotificationList(ctx, s.logger, s.db, userID, limit, in.CacheableCursor)
if err != nil {
return nil, status.Error(codes.Internal, "Error retrieving notifications.")
}
Expand Down
2 changes: 1 addition & 1 deletion server/core_account.go
Original file line number Diff line number Diff line change
Expand Up @@ -408,7 +408,7 @@ func ExportAccount(ctx context.Context, logger *zap.Logger, db *sql.DB, userID u
}

// Notifications.
notifications, err := NotificationList(ctx, logger, db, userID, 0, "", nil)
notifications, err := NotificationList(ctx, logger, db, userID, 0, "")
if err != nil {
logger.Error("Could not fetch notifications", zap.Error(err), zap.String("user_id", userID.String()))
return nil, status.Error(codes.Internal, "An error occurred while trying to export user data.")
Expand Down
18 changes: 17 additions & 1 deletion server/core_notification.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ import (
"errors"
"fmt"
"github.com/heroiclabs/nakama-common/runtime"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"time"

"github.com/gofrs/uuid/v5"
Expand Down Expand Up @@ -199,7 +201,21 @@ func NotificationSendAll(ctx context.Context, logger *zap.Logger, db *sql.DB, go
return nil
}

func NotificationList(ctx context.Context, logger *zap.Logger, db *sql.DB, userID uuid.UUID, limit int, cursor string, nc *notificationCacheableCursor) (*api.NotificationList, error) {
func NotificationList(ctx context.Context, logger *zap.Logger, db *sql.DB, userID uuid.UUID, limit int, cursor string) (*api.NotificationList, error) {
var nc *notificationCacheableCursor
if cursor != "" {
nc = &notificationCacheableCursor{}
cb, err := base64.RawURLEncoding.DecodeString(cursor)
if err != nil {
logger.Warn("Could not base64 decode notification cursor.", zap.String("cursor", cursor))
return nil, status.Error(codes.InvalidArgument, "Malformed cursor was used.")
}
if err = gob.NewDecoder(bytes.NewReader(cb)).Decode(nc); err != nil {
logger.Warn("Could not decode notification cursor.", zap.String("cursor", cursor))
return nil, status.Error(codes.InvalidArgument, "Malformed cursor was used.")
}
}

params := []interface{}{userID}

limitQuery := " "
Expand Down
37 changes: 36 additions & 1 deletion server/runtime_go_nakama.go
Original file line number Diff line number Diff line change
Expand Up @@ -1780,6 +1780,41 @@ func (n *RuntimeGoNakamaModule) NotificationsGetId(ctx context.Context, userID s
return NotificationsGetId(ctx, n.logger, n.db, userID, ids...)
}

// @group notifications
// @summary List notifications by user id.
// @param ctx(type=context.Context) The context object represents information about the server and requester.
// @param userID(type=string) Optional userID to scope results to that user only.
// @param limit(type=int) Limit number of results. Must be a value between 1 and 1000.
// @param cursor(type=string) Pagination cursor from previous result. Don't set to start fetching from the beginning.
// @return notifications(*api.NotificationList) A list of notifications.
// @return error(error) An optional error value if an error occurred.
func (n *RuntimeGoNakamaModule) NotificationsList(ctx context.Context, userID string, limit int, cursor string) ([]*api.Notification, string, error) {
if userID == "" {
return nil, "", errors.New("expects a valid user id")
}

if limit < 0 || limit > 1000 {
return nil, "", errors.New("expects limit to be 0-100")
}

uid, err := uuid.FromString(userID)
if err != nil {
return nil, "", errors.New("expects a valid user id")
}

list, err := NotificationList(ctx, n.logger, n.db, uid, limit, cursor)
if err != nil {
return nil, "", err
}

if len(list.Notifications) == 0 {
// Cursor is returned even if there are no more entries so return no cursor to indicate we've gone through all.
return []*api.Notification{}, "", nil
}

return list.Notifications, list.CacheableCursor, nil
}

// @group notifications
// @summary Delete notifications by their id.
// @param ctx(type=context.Context) The context object represents information about the server and requester.
Expand Down Expand Up @@ -1899,7 +1934,7 @@ func (n *RuntimeGoNakamaModule) WalletLedgerUpdate(ctx context.Context, itemID s
// @param ctx(type=context.Context) The context object represents information about the server and requester.
// @param userId(type=string) The ID of the user to list wallet updates for.
// @param limit(type=int, optional=true, default=100) Limit number of results.
// @param cursor(type=string, optional=true, default="") Pagination cursor from previous result. Don't set to start fetching from the beginning.
// @param cursor(type=string, default="") Pagination cursor from previous result. Don't set to start fetching from the beginning.
// @return runtimeItems([]runtime.WalletLedgerItem) A Go slice containing wallet entries with Id, UserId, CreateTime, UpdateTime, Changeset, Metadata parameters.
// @return error(error) An optional error value if an error occurred.
func (n *RuntimeGoNakamaModule) WalletLedgerList(ctx context.Context, userID string, limit int, cursor string) ([]runtime.WalletLedgerItem, string, error) {
Expand Down
73 changes: 70 additions & 3 deletions server/runtime_javascript_nakama.go
Original file line number Diff line number Diff line change
Expand Up @@ -224,8 +224,9 @@ func (n *runtimeJavascriptNakamaModule) mappings(r *goja.Runtime) map[string]fun
"matchList": n.matchList(r),
"matchSignal": n.matchSignal(r),
"notificationSend": n.notificationSend(r),
"notificationsSend": n.notificationsSend(r),
"notificationSendAll": n.notificationSendAll(r),
"notificationsList": n.notificationsList(r),
"notificationsSend": n.notificationsSend(r),
"notificationsDelete": n.notificationsDelete(r),
"notificationsGetId": n.notificationsGetId(r),
"notificationsDeleteId": n.notificationsDeleteId(r),
Expand Down Expand Up @@ -3703,6 +3704,72 @@ func (n *runtimeJavascriptNakamaModule) notificationSend(r *goja.Runtime) func(g
}
}

// @group notifications
// @summary List notifications by user id.
// @param userID(type=string) Optional userID to scope results to that user only.
// @param limit(type=int, optiona=true, default=100) Limit number of results. Must be a value between 1 and 1000.
// @param cursor(type=string, optional=true, default="") Pagination cursor from previous result. Don't set to start fetching from the beginning.
// @return notifications(nkruntime.NotificationList) A list of notifications.
// @return error(error) An optional error value if an error occurred.
func (n *runtimeJavascriptNakamaModule) notificationsList(r *goja.Runtime) func(goja.FunctionCall) goja.Value {
return func(f goja.FunctionCall) goja.Value {
userIDString := getJsString(r, f.Argument(0))
if userIDString == "" {
panic(r.ToValue(r.NewTypeError("expects user id")))
}
userID, err := uuid.FromString(userIDString)
if err != nil {
panic(r.NewTypeError("invalid user id"))
}

limit := 100
if f.Argument(1) != goja.Undefined() && f.Argument(1) != goja.Null() {
limit = int(getJsInt(r, f.Argument(1)))
if limit < 1 || limit > 1000 {
panic(r.ToValue(r.NewTypeError("expects limit between 1 and 1000")))
}
}

cursor := ""
if f.Argument(2) != goja.Undefined() && f.Argument(2) != goja.Null() {
cursor = getJsString(r, f.Argument(2))
}

list, err := NotificationList(n.ctx, n.logger, n.db, userID, limit, cursor)
if err != nil {
panic(r.ToValue(r.NewGoError(fmt.Errorf("failed to list notifications: %s", err.Error()))))
}

if len(list.Notifications) == 0 {
list.CacheableCursor = ""
}

notObjs := make([]any, 0, len(list.Notifications))
for _, n := range list.Notifications {
no := r.NewObject()
_ = no.Set("id", n.Id)
_ = no.Set("subject", n.Subject)
_ = no.Set("content", n.Content)
_ = no.Set("code", n.Code)
_ = no.Set("senderId", n.SenderId)
_ = no.Set("persistent", n.Persistent)
_ = no.Set("createTime", n.CreateTime.Seconds)

notObjs = append(notObjs, no)
}

outObj := r.NewObject()
_ = outObj.Set("notifications", r.NewArray(notObjs...))
if list.CacheableCursor != "" {
_ = outObj.Set("cursor", list.CacheableCursor)
} else {
_ = outObj.Set("cursor", goja.Null())
}

return outObj
}
}

// @group notifications
// @summary Send one or more in-app notifications to a user.
// @param notifications(type=any[]) A list of notifications to be sent together.
Expand Down Expand Up @@ -3981,13 +4048,13 @@ func (n *runtimeJavascriptNakamaModule) notificationsGetId(r *goja.Runtime) func
notifObj := r.NewObject()

_ = notifObj.Set("id", no.Id)
_ = notifObj.Set("user_id", no.UserID)
_ = notifObj.Set("userId", no.UserID)
_ = notifObj.Set("subject", no.Subject)
_ = notifObj.Set("persistent", no.Persistent)
_ = notifObj.Set("content", no.Content)
_ = notifObj.Set("code", no.Code)
_ = notifObj.Set("sender", no.Sender)
_ = notifObj.Set("create_time", no.CreateTime.Seconds)
_ = notifObj.Set("createTime", no.CreateTime.Seconds)
_ = notifObj.Set("persistent", no.Persistent)

notifications = append(notifications, notifObj)
Expand Down
61 changes: 61 additions & 0 deletions server/runtime_lua_nakama.go
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,7 @@ func (n *RuntimeLuaNakamaModule) Loader(l *lua.LState) int {
"notification_send": n.notificationSend,
"notifications_send": n.notificationsSend,
"notification_send_all": n.notificationSendAll,
"notifications_list": n.notificationsList,
"notifications_delete": n.notificationsDelete,
"notifications_get_id": n.notificationsGetId,
"notifications_delete_id": n.notificationsDeleteId,
Expand Down Expand Up @@ -5198,6 +5199,66 @@ func (n *RuntimeLuaNakamaModule) notificationSendAll(l *lua.LState) int {
return 0
}

// @group notifications
// @summary List notifications by user id.
// @param userID(type=string) Optional userID to scope results to that user only.
// @param limit(type=int, optiona=true, default=100) Limit number of results. Must be a value between 1 and 1000.
// @param cursor(type=string, optional=true, default="") Pagination cursor from previous result. Don't set to start fetching from the beginning.
// @return notifications(table) A list of notifications.
// @return cursor(string) A cursor to fetch the next page of results.
// @return error(error) An optional error value if an error occurred.
func (n *RuntimeLuaNakamaModule) notificationsList(l *lua.LState) int {
u := l.CheckString(1)
userID, err := uuid.FromString(u)
if err != nil {
l.ArgError(1, "expects user_id to be a valid uuid")
return 0
}

limit := l.OptInt(2, 100)
if limit < 1 || limit > 1000 {
l.ArgError(2, "expects limit to be value between 1 and 1000")
return 0
}

cursor := l.OptString(3, "")

list, err := NotificationList(l.Context(), n.logger, n.db, userID, limit, cursor)
if err != nil {
l.RaiseError("failed to list notifications: %s", err.Error())
return 0
}

if len(list.Notifications) == 0 {
list.CacheableCursor = ""
}

notifsTable := l.CreateTable(len(list.Notifications), 0)
for i, no := range list.Notifications {
noTable := l.CreateTable(0, len(list.Notifications))

noTable.RawSetString("id", lua.LString(no.Id))
noTable.RawSetString("subject", lua.LString(no.Subject))
noTable.RawSetString("content", lua.LString(no.Content))
noTable.RawSetString("code", lua.LNumber(no.Code))
noTable.RawSetString("senderId", lua.LString(no.SenderId))
noTable.RawSetString("persistent", lua.LBool(no.Persistent))
noTable.RawSetString("createTime", lua.LNumber(no.CreateTime.Seconds))

notifsTable.RawSetInt(i+1, noTable)
}

l.Push(notifsTable)

if list.CacheableCursor != "" {
l.Push(lua.LString(list.CacheableCursor))
} else {
l.Push(lua.LNil)
}

return 2
}

// @group notifications
// @summary Delete one or more in-app notifications.
// @param notifications(type=table) A list of notifications to be deleted.
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion vendor/modules.txt
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2/internal/genopena
github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2/options
github.com/grpc-ecosystem/grpc-gateway/v2/runtime
github.com/grpc-ecosystem/grpc-gateway/v2/utilities
# github.com/heroiclabs/nakama-common v1.33.1-0.20240920140332-3cdf52bdf781
# github.com/heroiclabs/nakama-common v1.33.1-0.20240920140332-3cdf52bdf781 => ../nakama-common
## explicit; go 1.19
github.com/heroiclabs/nakama-common/api
github.com/heroiclabs/nakama-common/rtapi
Expand Down Expand Up @@ -417,3 +417,4 @@ gopkg.in/natefinch/lumberjack.v2
# gopkg.in/yaml.v3 v3.0.1
## explicit
gopkg.in/yaml.v3
# github.com/heroiclabs/nakama-common => ../nakama-common
Loading