Skip to content

Commit

Permalink
[NK-603] Add new Follow/Unfollow runtime APIs (heroiclabs#1285)
Browse files Browse the repository at this point in the history
Add NotificationsUpodate runtime function.
  • Loading branch information
sesposito authored and abenzone committed Dec 19, 2024
1 parent 195bdc6 commit e9c0437
Show file tree
Hide file tree
Showing 9 changed files with 479 additions and 7 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ The format is based on [keep a changelog](http://keepachangelog.com) and this pr
## [Unreleased]
### Added
- Add new runtime function to get a list of user's friend status.
- Add new Follow/Unfollow runtime APIs.
- Add new NotificationsUpdate runtime API.

### Changed
- Increase limit of runtime friend listing operations to 1,000.
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ require (
github.com/gorilla/mux v1.8.1
github.com/gorilla/websocket v1.5.1
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0
github.com/heroiclabs/nakama-common v1.34.1-0.20241121103154-13c252805056
github.com/heroiclabs/nakama-common v1.34.1-0.20241121105602-e24311ad3419
github.com/heroiclabs/sql-migrate v0.0.0-20240528102547-233afc8cf05a
github.com/jackc/pgerrcode v0.0.0-20240316143900-6e2875d9b438
github.com/jackc/pgx/v5 v5.6.0
Expand Down
6 changes: 2 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -130,10 +130,8 @@ github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZH
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/heroiclabs/nakama-common v1.34.0 h1:7/F5v5yoCFBMTn5Aih/cqR/GW7hbEbup8blq5OmhzjM=
github.com/heroiclabs/nakama-common v1.34.0/go.mod h1:lPG64MVCs0/tEkh311Cd6oHX9NLx2vAPx7WW7QCJHQ0=
github.com/heroiclabs/nakama-common v1.34.1-0.20241121103154-13c252805056 h1:a3N9IG9w1K9m1EEZu9bFVcfb3PDdb7vGTUInkkkIsfA=
github.com/heroiclabs/nakama-common v1.34.1-0.20241121103154-13c252805056/go.mod h1:lPG64MVCs0/tEkh311Cd6oHX9NLx2vAPx7WW7QCJHQ0=
github.com/heroiclabs/nakama-common v1.34.1-0.20241121105602-e24311ad3419 h1:sC+dslMcIE3E8lc1fXOIvYb7cPjtZUVvHiegHdLah4k=
github.com/heroiclabs/nakama-common v1.34.1-0.20241121105602-e24311ad3419/go.mod h1:lPG64MVCs0/tEkh311Cd6oHX9NLx2vAPx7WW7QCJHQ0=
github.com/heroiclabs/sql-migrate v0.0.0-20240528102547-233afc8cf05a h1:tuL2ZPaeCbNw8rXmV9ywd00nXRv95V4/FmbIGKLQJAE=
github.com/heroiclabs/sql-migrate v0.0.0-20240528102547-233afc8cf05a/go.mod h1:hzCTPoEi/oml2BllVydJcNP63S7b56e5DzeQeLGvw1U=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
Expand Down
36 changes: 36 additions & 0 deletions server/core_notification.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"errors"
"fmt"
"github.com/heroiclabs/nakama-common/runtime"
"github.com/jackc/pgx/v5"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"time"
Expand Down Expand Up @@ -436,3 +437,38 @@ func NotificationsDeleteId(ctx context.Context, logger *zap.Logger, db *sql.DB,

return nil
}

type notificationUpdate struct {
Id uuid.UUID
Content map[string]any
Subject *string
Sender *string
}

func NotificationsUpdate(ctx context.Context, logger *zap.Logger, db *sql.DB, updates ...notificationUpdate) error {
if len(updates) == 0 {
// NOOP
return nil
}

b := &pgx.Batch{}
for _, update := range updates {
b.Queue("UPDATE notification SET content = coalesce($1, content), subject = coalesce($2, subject), sender_id = coalesce($3, sender_id) WHERE id = $4", update.Content, update.Subject, update.Sender, update.Id)
}

if err := ExecuteInTxPgx(ctx, db, func(tx pgx.Tx) error {
r := tx.SendBatch(ctx, b)
_, err := r.Exec()
defer r.Close()
if err != nil {
return err
}

return nil
}); err != nil {
logger.Error("failed to update notifications", zap.Error(err))
return fmt.Errorf("failed to update notifications: %s", err.Error())
}

return nil
}
83 changes: 83 additions & 0 deletions server/runtime_go_nakama.go
Original file line number Diff line number Diff line change
Expand Up @@ -1853,6 +1853,31 @@ func (n *RuntimeGoNakamaModule) NotificationsDeleteId(ctx context.Context, userI
return NotificationsDeleteId(ctx, n.logger, n.db, userID, ids...)
}

// @group notifications
// @summary Update notifications by their id.
// @param ctx(type=context.Context) The context object represents information about the server and requester.
// @param userID(type=[]runtime.NotificationUpdate)
// @return error(error) An optional error value if an error occurred.
func (n *RuntimeGoNakamaModule) NotificationsUpdate(ctx context.Context, updates ...runtime.NotificationUpdate) error {
nUpdates := make([]notificationUpdate, 0, len(updates))

for _, update := range updates {
uid, err := uuid.FromString(update.Id)
if err != nil {
return errors.New("expects id to be a valid UUID")
}

nUpdates = append(nUpdates, notificationUpdate{
Id: uid,
Content: update.Content,
Subject: update.Subject,
Sender: update.Sender,
})
}

return NotificationsUpdate(ctx, n.logger, n.db, nUpdates...)
}

// @group wallets
// @summary Update a user's wallet with the given changeset.
// @param ctx(type=context.Context) The context object represents information about the server and requester.
Expand Down Expand Up @@ -1987,6 +2012,64 @@ func (n *RuntimeGoNakamaModule) WalletLedgerList(ctx context.Context, userID str
return runtimeItems, newCursor, nil
}

// @group status
// @summary Follow a player's status changes on a given session.
// @param sessionID(type=string) A valid session identifier.
// @param userIDs(type=[]string) A list of userIDs to follow.
// @return error(error) An optional error value if an error occurred.
func (n *RuntimeGoNakamaModule) StatusFollow(sessionID string, userIDs []string) error {
suid, err := uuid.FromString(sessionID)
if err != nil {
return errors.New("expects a valid session id")
}

if len(userIDs) == 0 {
return nil
}

uids := make(map[uuid.UUID]struct{}, len(userIDs))
for _, id := range userIDs {
uid, err := uuid.FromString(id)
if err != nil {
return errors.New("expects a valid user id")
}
uids[uid] = struct{}{}
}

n.statusRegistry.Follow(suid, uids)

return nil
}

// @group status
// @summary Unfollow a player's status changes on a given session.
// @param sessionID(type=string) A valid session identifier.
// @param userIDs(type=[]string) A list of userIDs to unfollow.
// @return error(error) An optional error value if an error occurred.
func (n *RuntimeGoNakamaModule) StatusUnfollow(sessionID string, userIDs []string) error {
suid, err := uuid.FromString(sessionID)
if err != nil {
return errors.New("expects a valid session id")
}

if len(userIDs) == 0 {
return nil
}

uids := make([]uuid.UUID, 0, len(userIDs))
for _, id := range userIDs {
uid, err := uuid.FromString(id)
if err != nil {
return errors.New("expects a valid user id")
}
uids = append(uids, uid)
}

n.statusRegistry.Unfollow(suid, uids)

return nil
}

// @group storage
// @summary List records in a collection and page through results. The records returned can be filtered to those owned by the user or "" for public records.
// @param ctx(type=context.Context) The context object represents information about the server and requester.
Expand Down
151 changes: 151 additions & 0 deletions server/runtime_javascript_nakama.go
Original file line number Diff line number Diff line change
Expand Up @@ -237,12 +237,15 @@ func (n *runtimeJavascriptNakamaModule) mappings(r *goja.Runtime) map[string]fun
"notificationsList": n.notificationsList(r),
"notificationsSend": n.notificationsSend(r),
"notificationsDelete": n.notificationsDelete(r),
"notificationsUpdate": n.notificationsUpdate(r),
"notificationsGetId": n.notificationsGetId(r),
"notificationsDeleteId": n.notificationsDeleteId(r),
"walletUpdate": n.walletUpdate(r),
"walletsUpdate": n.walletsUpdate(r),
"walletLedgerUpdate": n.walletLedgerUpdate(r),
"walletLedgerList": n.walletLedgerList(r),
"statusFollow": n.statusFollow(r),
"statusUnfollow": n.statusUnfollow(r),
"storageList": n.storageList(r),
"storageRead": n.storageRead(r),
"storageWrite": n.storageWrite(r),
Expand Down Expand Up @@ -4082,6 +4085,74 @@ func (n *runtimeJavascriptNakamaModule) notificationsDelete(r *goja.Runtime) fun
}
}

// @group notifications
// @summary Update notifications by their id.
// @param updates(type=nkruntime.NotificationUpdate[])
// @return error(error) An optional error value if an error occurred.
func (n *runtimeJavascriptNakamaModule) notificationsUpdate(r *goja.Runtime) func(goja.FunctionCall) goja.Value {
return func(f goja.FunctionCall) goja.Value {
updatesIn := f.Argument(0)

dataSlice, err := exportToSlice[[]map[string]any](updatesIn)
if err != nil {
panic(r.NewTypeError("expects an array of notification updates objects"))
}

nUpdates := make([]notificationUpdate, 0, len(dataSlice))
for _, u := range dataSlice {
update := notificationUpdate{}
id, ok := u["id"]
if !ok || id == "" {
panic(r.NewTypeError("expects 'id' value to be set"))
}
idstr, ok := id.(string)
if !ok || idstr == "" {
panic(r.NewTypeError("expects 'id' value to be a non-empty string"))
}
uid, err := uuid.FromString(idstr)
if err != nil {
panic(r.NewGoError(fmt.Errorf("expects 'id' value to be a valid id")))
}
update.Id = uid

content, ok := u["content"]
if ok {
cmap, ok := content.(map[string]any)
if !ok {
panic(r.NewTypeError("expects 'content' value to be a non-empty map"))
}
update.Content = cmap
}

subject, ok := u["subject"]
if ok {
substr, ok := subject.(string)
if !ok || substr == "" {
panic(r.NewTypeError("expects 'subject' value to be a non-empty string"))
}
update.Subject = &substr
}

sender, ok := u["sender"]
if ok {
substr, ok := sender.(string)
if !ok || substr == "" {
panic(r.NewTypeError("expects 'sender' value to be a non-empty string"))
}
update.Sender = &substr
}

nUpdates = append(nUpdates, update)
}

if err := NotificationsUpdate(n.ctx, n.logger, n.db, nUpdates...); err != nil {
panic(r.NewGoError(fmt.Errorf("failed to update notifications: %s", err.Error())))
}

return goja.Undefined()
}
}

// @group notifications
// @summary Get notifications by their id.
// @param ids(type=string[]) A list of notification ids.
Expand Down Expand Up @@ -4387,6 +4458,86 @@ func (n *runtimeJavascriptNakamaModule) walletLedgerUpdate(r *goja.Runtime) func
}
}

// @group status
// @summary Follow a player's status changes on a given session.
// @param sessionID(type=string) A valid session identifier.
// @param userIDs(type=string[]) A list of userIDs to follow.
// @return error(error) An optional error value if an error occurred.
func (n *runtimeJavascriptNakamaModule) statusFollow(r *goja.Runtime) func(goja.FunctionCall) goja.Value {
return func(f goja.FunctionCall) goja.Value {
sid := getJsString(r, f.Argument(0))

suid, err := uuid.FromString(sid)
if err != nil {
panic(r.NewTypeError("expects a valid session id"))
}

uidsIn := f.Argument(1)

uidsSlice, err := exportToSlice[[]string](uidsIn)
if err != nil {
panic(r.NewTypeError("expects an array of user ids"))
}

if len(uidsSlice) == 0 {
return goja.Undefined()
}

uids := make(map[uuid.UUID]struct{}, len(uidsSlice))
for _, id := range uidsSlice {
uid, err := uuid.FromString(id)
if err != nil {
panic(r.NewTypeError("expects a valid user id"))
}
uids[uid] = struct{}{}
}

n.statusRegistry.Follow(suid, uids)

return nil
}
}

// @group status
// @summary Unfollow a player's status changes on a given session.
// @param sessionID(type=string) A valid session identifier.
// @param userIDs(type=string[]) A list of userIDs to unfollow.
// @return error(error) An optional error value if an error occurred.
func (n *runtimeJavascriptNakamaModule) statusUnfollow(r *goja.Runtime) func(goja.FunctionCall) goja.Value {
return func(f goja.FunctionCall) goja.Value {
sid := getJsString(r, f.Argument(0))

suid, err := uuid.FromString(sid)
if err != nil {
panic(r.NewTypeError("expects a valid session id"))
}

uidsIn := f.Argument(1)

uidsSlice, err := exportToSlice[[]string](uidsIn)
if err != nil {
panic(r.NewTypeError("expects an array of user ids"))
}

if len(uidsSlice) == 0 {
return goja.Undefined()
}

uids := make([]uuid.UUID, 0, len(uidsSlice))
for _, id := range uidsSlice {
uid, err := uuid.FromString(id)
if err != nil {
panic(r.NewTypeError("expects a valid user id"))
}
uids = append(uids, uid)
}

n.statusRegistry.Unfollow(suid, uids)

return nil
}
}

// @group wallets
// @summary List all wallet updates for a particular user from oldest to newest.
// @param userId(type=string) The ID of the user to list wallet updates for.
Expand Down
Loading

0 comments on commit e9c0437

Please sign in to comment.