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

Add new Follow/Unfollow runtime APIs #1285

Open
wants to merge 2 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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ 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
- 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: 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
2 changes: 0 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -130,8 +130,6 @@ 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/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 @@ -1820,6 +1820,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 @@ -1954,6 +1979,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 @@ -228,12 +228,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 @@ -4017,6 +4020,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 @@ -4322,6 +4393,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
Loading